Skip to content

Instantly share code, notes, and snippets.

@kolja
Last active August 15, 2024 08:20
Show Gist options
  • Save kolja/a25141a79bb54b9d483653916052ad4b to your computer and use it in GitHub Desktop.
Save kolja/a25141a79bb54b9d483653916052ad4b to your computer and use it in GitHub Desktop.
transform markdown to typst and generate pdf
#!/usr/bin/env bb
; // this is minimal typst template, you could use with this script:
; #let conf(
; // the variables below *must* be defined in the yaml frontmatter of your markdown file
; (font, foo, bar),
; doc,
; ) = {
; // set the font in the yaml frontmatter of your markdown file
; set text(
; font: font,
; )
; doc
; }
(require '[clojure.string :as str]
'[babashka.fs :as fs]
'[babashka.process :refer [shell]]
'[clojure.tools.cli :refer [parse-opts]])
(def cli-opts [["-o" "--open APP" "open with pdf viewer after creation"]
["-d" "--dry-run" "Print typst to stdout and quit"]
["-h" "--help" "Show help"]])
(defn bold [s] (str "\033[1m" s "\033[0m"))
(defn exit [message code]
(if (= code 0)
(println message)
(binding [*out* *err*] (println message)))
(System/exit code))
(defn usage [options-summary]
(->> [""
"Create a pdf from a markdown file with pandoc and typst."
""
(bold (str " Usage:\t" (fs/file-name *file*) " <markdown-file>"))
""
options-summary
""
"Typst templates will be looked for in TYPST_ROOT which is either taken from an env var with that name, or assumed to be the same directory as the markdown-file."
""
"A default template will be used if it is found in TYPST_ROOT/templates/default.typ"
"The markdown file can have a YAML frontmatter with the following fields:"
""
(str " " (bold "template:") " <template>\ttypst template relative to TYPST_ROOT")
(str " " (bold "pdf:") " <pdf_path>\toptional, path to save the pdf file (absolute or relative to the file that the markdown resides in.)")
""
]
(str/join \newline)))
(defn validate-md-file [md-file]
(when (or (nil? md-file)
(not (fs/exists? md-file))
(not= (fs/extension md-file) "md"))
(exit (str (bold "Error:") " You must provide a valid markdown file.") 1)))
(defn parse [markdown]
(let [[a b] (str/split (str/replace-first markdown #"(?m)^---$" "")
#"(?m)^---$" 2)
hdr (if b a "")
bdy (or b a)
header (yaml/parse-string hdr)
body (shell {:in bdy :out :string} "pandoc --to typst")]
{:header header
:body (:out body)
}))
(defn indent [lvl] (apply str (repeat lvl " ")))
(defn data-to-typst [data & [lvl]] ;; lvl is the nesting level
(let [lvl (or lvl 1)]
(cond
(string? data)
(str "\"" data "\"")
(number? data)
data
(seq? data)
(str "(" (str/join ", "
(map #(data-to-typst % lvl) (doall data))) ")")
(map? data)
(str "(" (str/join ", "
(map (fn [[k v]] (str "\n" (indent lvl) (name k) ": "
(data-to-typst v (inc lvl)))) (doall data))) "\n" (indent (dec lvl)) ")")
)))
;; to-typst expects the markdown headers as a clojure data structure
;; And the body as typst (converted from markdown with pandoc)
(defn to-typst [{:keys [:header :body]} root]
(let [header-template (str root "/" (:template header))
default-template (str root "/templates/default.typ")
template (if (:template header)
(if (fs/exists? header-template)
header-template
(exit (str (bold "Error: ")
"The template " (bold (:template header))
" wasn't found in " (bold root)) 1))
(when (fs/exists? default-template) default-template))]
(if template
(str "#import \"" template "\": *\n"
"#show: doc => conf(\n"
(if header (str (data-to-typst header 1) ",\n ") "(),\n ")
"doc\n)\n"
body)
body)))
(defn env2path [env dir]
(let [path (System/getenv env)]
(if (nil? path)
dir
(if (fs/exists? path)
(fs/real-path path)
(exit (str (bold "Error:") "The path " env " = " path "does not exist.") 1)))))
(defn get-pdf-file [pdf-file dir]
(let [file (if (fs/relative? pdf-file)
(str dir "/" pdf-file)
pdf-file)
directory (fs/parent file)]
(if (fs/directory? directory)
file
(exit (str (bold "Error:") " Can't create pdf in " directory) 1))))
(defn -main [& args]
(let [{:keys [options arguments summary errors]} (parse-opts args cli-opts)
md-file (first arguments)]
(when (:help options) (exit (usage summary) 0))
(when (not-empty errors)
(exit (str (bold "Errors: ") (str/join ", " errors)) 1))
(validate-md-file md-file)
(when (nil? (fs/which "pandoc"))
(exit (str (bold "Error:") "pandoc not found in PATH.") 1))
(when (and (not (options :dry-run))
(nil? (fs/which "typst")))
(exit (str (bold "Error:") "typst not found in PATH.") 1))
(let [
file-name (first (fs/split-ext md-file))
pdf-opener (options :open)
dir (fs/parent (fs/real-path md-file))
root (env2path "TYPST_ROOT" dir)
parsed (parse (slurp md-file))
pdf-file (get-pdf-file (or (get-in parsed [:header :pdf])
(str file-name ".pdf"))
dir)
typst (to-typst parsed root)]
(if (options :dry-run)
(println typst)
(do
(shell {:in typst} "typst" "compile" "--root" "/" "/dev/stdin" pdf-file)
(println "Created PDF:\t" pdf-file)
(when pdf-opener
(if (fs/exists? pdf-opener)
(shell "open" pdf-opener pdf-file)
(println "PDF opener not found: " pdf-opener)))
)))))
(apply -main *command-line-args*)
;# vim: ft=clojure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment