Last active
August 15, 2024 08:20
-
-
Save kolja/a25141a79bb54b9d483653916052ad4b to your computer and use it in GitHub Desktop.
transform markdown to typst and generate pdf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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