diff options
| author | Jeanne-Kamikaze <jeannekamikaze@gmail.com> | 2020-05-30 13:20:29 -0700 |
|---|---|---|
| committer | Jeanne-Kamikaze <jeannekamikaze@gmail.com> | 2020-05-30 13:20:29 -0700 |
| commit | 2608753d9cc08d133c2ba50e5f53104220dd3229 (patch) | |
| tree | e1549b270f7e56524602880be5d087401ac72bab | |
| parent | 9a325e40b48e10d61f880e953dfd5504345f18f5 (diff) | |
Initial commit.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | project.janet | 9 | ||||
| -rw-r--r-- | webgen.janet | 119 |
3 files changed, 129 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| build/ | |||
diff --git a/project.janet b/project.janet new file mode 100644 index 0000000..82a5880 --- /dev/null +++ b/project.janet | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | (declare-project | ||
| 2 | :name "webgen" | ||
| 3 | :description "Static web generator" | ||
| 4 | :dependencies ["https://github.com/janet-lang/path.git" | ||
| 5 | "https://github.com/jeannekamikaze/janet-filesystem.git"]) | ||
| 6 | |||
| 7 | (declare-executable | ||
| 8 | :name "webgen" | ||
| 9 | :entry "webgen.janet") | ||
diff --git a/webgen.janet b/webgen.janet new file mode 100644 index 0000000..1ba3d5a --- /dev/null +++ b/webgen.janet | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | (import filesystem) | ||
| 2 | (import path) | ||
| 3 | |||
| 4 | (defn pairs->table | ||
| 5 | "Convert an array of pairs into a table." | ||
| 6 | [pairs] | ||
| 7 | (reduce (fn [acc [key val]] (put acc key val)) @{} pairs)) | ||
| 8 | |||
| 9 | (defn split-first | ||
| 10 | "Split the string on the first occurrence of the delimeter. If the delimeter | ||
| 11 | is not found, return the original string." | ||
| 12 | [delim str] | ||
| 13 | (def idx (string/find delim str)) | ||
| 14 | (if idx | ||
| 15 | @[(string/slice str 0 (- idx 1)) (string/slice str (+ idx 1))] | ||
| 16 | str)) | ||
| 17 | |||
| 18 | (defn nesting-levels | ||
| 19 | "Return the number of nesting levels in the given file path." | ||
| 20 | [path] | ||
| 21 | (length (string/find-all path/sep path))) | ||
| 22 | |||
| 23 | (defn markdown-file-to-html | ||
| 24 | "Read and convert a markdown file to html. Return an html buffer." | ||
| 25 | [path] | ||
| 26 | (def quoted-path (string "\"" path "\"")) | ||
| 27 | (with [stdout (file/popen (string "pandoc -f markdown --mathjax " quoted-path) :r)] | ||
| 28 | (file/read stdout :all))) | ||
| 29 | |||
| 30 | (defn substitute-variables | ||
| 31 | "Perform variable substitution on the given string." | ||
| 32 | [variables path str] | ||
| 33 | (var str str) | ||
| 34 | (def max-recursion-depth 5) | ||
| 35 | (loop [i :in (range max-recursion-depth)] | ||
| 36 | (loop [[variable body] :pairs variables] | ||
| 37 | (def variable-str (string "${" variable "}")) | ||
| 38 | (set str (string/replace variable-str body str)))) | ||
| 39 | (string/replace-all "${ROOT}/" (string/repeat "../" (- (nesting-levels path) 1)) str)) | ||
| 40 | |||
| 41 | (defn process-html | ||
| 42 | "Process the given HTML contents, applying the website's template and | ||
| 43 | applying variable substitution." | ||
| 44 | [template variables path contents] | ||
| 45 | (substitute-variables variables path | ||
| 46 | (string/replace "${CONTENTS}" contents | ||
| 47 | template))) | ||
| 48 | |||
| 49 | (defn process-markdown-file | ||
| 50 | "Process a markdown file. Return the resulting HTML." | ||
| 51 | [process-html path] | ||
| 52 | (process-html path (markdown-file-to-html path))) | ||
| 53 | |||
| 54 | (defn process-file | ||
| 55 | "Process a file with the given contents processor. The process takes a file | ||
| 56 | path and the file's contents." | ||
| 57 | [process path] | ||
| 58 | (process path (filesystem/read-file path))) | ||
| 59 | |||
| 60 | (defn process-files | ||
| 61 | "Process the files in the source directory and write the results to the | ||
| 62 | destination directory." | ||
| 63 | [process source-dir dest-dir] | ||
| 64 | (each file (filesystem/list-all-files source-dir) | ||
| 65 | (process file))) | ||
| 66 | |||
| 67 | (defn make-processor | ||
| 68 | "Create a function that processes files based on their extension." | ||
| 69 | [src-dir build-dir template variables] | ||
| 70 | (fn [src-filepath] | ||
| 71 | (print "Processing file " src-filepath) | ||
| 72 | (def ext (path/ext src-filepath)) | ||
| 73 | (def processed-contents | ||
| 74 | (match ext | ||
| 75 | ".css" (process-file (partial substitute-variables variables) src-filepath) | ||
| 76 | ".html" (process-file (partial process-html template variables) src-filepath) | ||
| 77 | ".md" (process-markdown-file (partial process-html template variables) src-filepath) | ||
| 78 | ".markdown" (process-markdown-file (partial process-html template variables) src-filepath) | ||
| 79 | _ nil)) # nil means we copy the file as is. | ||
| 80 | (var dst-filepath (string/replace src-dir build-dir src-filepath)) | ||
| 81 | (set dst-filepath | ||
| 82 | (match ext | ||
| 83 | ".md" (string/replace ext ".html" dst-filepath) | ||
| 84 | ".markdown" (string/replace ext ".html" dst-filepath) | ||
| 85 | _ dst-filepath)) | ||
| 86 | (if processed-contents | ||
| 87 | (filesystem/write-file dst-filepath processed-contents) | ||
| 88 | (filesystem/copy-file src-filepath dst-filepath)))) | ||
| 89 | |||
| 90 | (defn read-map-file | ||
| 91 | "Read key-value pairs from a file. The file should be a text file with lines | ||
| 92 | of the form |key = value|. Return a table." | ||
| 93 | [path] | ||
| 94 | (def contents (filesystem/read-file path)) | ||
| 95 | (def lines (filter (fn [x] (not (empty? x))) (string/split "\n" contents))) | ||
| 96 | (def name-body-pairs (map (fn [line] | ||
| 97 | (def @[name body] (split-first "=" line)) | ||
| 98 | @[(string/trim name) (string/trim body)]) | ||
| 99 | lines)) | ||
| 100 | (pairs->table name-body-pairs)) | ||
| 101 | |||
| 102 | (defn usage [argv0] | ||
| 103 | (print "Usage: " argv0 " <source dir>") | ||
| 104 | (os/exit 0)) | ||
| 105 | |||
| 106 | (defn main [argv0 &opt src-dir] | ||
| 107 | (when (nil? src-dir) (usage argv0)) | ||
| 108 | (def src-dir (path/normalize src-dir)) | ||
| 109 | (def config (read-map-file (path/join src-dir "config.txt"))) | ||
| 110 | (def build-dir (path/normalize (string (get config "BUILD-DIR") path/sep))) | ||
| 111 | (print "Generating website: " src-dir " -> " build-dir) | ||
| 112 | (def template-file (get config "TEMPLATE-FILE")) | ||
| 113 | (def variables-file (get config "VARIABLES-FILE")) | ||
| 114 | (def template (filesystem/read-file (path/join src-dir template-file))) | ||
| 115 | (def variables (read-map-file (path/join src-dir variables-file))) | ||
| 116 | (def processor (make-processor src-dir build-dir template variables)) | ||
| 117 | # For clean builds, re-create the build directory. | ||
| 118 | (filesystem/recreate-directory build-dir) | ||
| 119 | (process-files processor src-dir build-dir)) | ||
