From 2608753d9cc08d133c2ba50e5f53104220dd3229 Mon Sep 17 00:00:00 2001 From: Jeanne-Kamikaze Date: Sat, 30 May 2020 13:20:29 -0700 Subject: Initial commit. --- .gitignore | 1 + project.janet | 9 +++++ webgen.janet | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 .gitignore create mode 100644 project.janet create mode 100644 webgen.janet 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 @@ +(declare-project + :name "webgen" + :description "Static web generator" + :dependencies ["https://github.com/janet-lang/path.git" + "https://github.com/jeannekamikaze/janet-filesystem.git"]) + +(declare-executable + :name "webgen" + :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 @@ +(import filesystem) +(import path) + +(defn pairs->table + "Convert an array of pairs into a table." + [pairs] + (reduce (fn [acc [key val]] (put acc key val)) @{} pairs)) + +(defn split-first + "Split the string on the first occurrence of the delimeter. If the delimeter + is not found, return the original string." + [delim str] + (def idx (string/find delim str)) + (if idx + @[(string/slice str 0 (- idx 1)) (string/slice str (+ idx 1))] + str)) + +(defn nesting-levels + "Return the number of nesting levels in the given file path." + [path] + (length (string/find-all path/sep path))) + +(defn markdown-file-to-html + "Read and convert a markdown file to html. Return an html buffer." + [path] + (def quoted-path (string "\"" path "\"")) + (with [stdout (file/popen (string "pandoc -f markdown --mathjax " quoted-path) :r)] + (file/read stdout :all))) + +(defn substitute-variables + "Perform variable substitution on the given string." + [variables path str] + (var str str) + (def max-recursion-depth 5) + (loop [i :in (range max-recursion-depth)] + (loop [[variable body] :pairs variables] + (def variable-str (string "${" variable "}")) + (set str (string/replace variable-str body str)))) + (string/replace-all "${ROOT}/" (string/repeat "../" (- (nesting-levels path) 1)) str)) + +(defn process-html + "Process the given HTML contents, applying the website's template and + applying variable substitution." + [template variables path contents] + (substitute-variables variables path + (string/replace "${CONTENTS}" contents + template))) + +(defn process-markdown-file + "Process a markdown file. Return the resulting HTML." + [process-html path] + (process-html path (markdown-file-to-html path))) + +(defn process-file + "Process a file with the given contents processor. The process takes a file + path and the file's contents." + [process path] + (process path (filesystem/read-file path))) + +(defn process-files + "Process the files in the source directory and write the results to the + destination directory." + [process source-dir dest-dir] + (each file (filesystem/list-all-files source-dir) + (process file))) + +(defn make-processor + "Create a function that processes files based on their extension." + [src-dir build-dir template variables] + (fn [src-filepath] + (print "Processing file " src-filepath) + (def ext (path/ext src-filepath)) + (def processed-contents + (match ext + ".css" (process-file (partial substitute-variables variables) src-filepath) + ".html" (process-file (partial process-html template variables) src-filepath) + ".md" (process-markdown-file (partial process-html template variables) src-filepath) + ".markdown" (process-markdown-file (partial process-html template variables) src-filepath) + _ nil)) # nil means we copy the file as is. + (var dst-filepath (string/replace src-dir build-dir src-filepath)) + (set dst-filepath + (match ext + ".md" (string/replace ext ".html" dst-filepath) + ".markdown" (string/replace ext ".html" dst-filepath) + _ dst-filepath)) + (if processed-contents + (filesystem/write-file dst-filepath processed-contents) + (filesystem/copy-file src-filepath dst-filepath)))) + +(defn read-map-file + "Read key-value pairs from a file. The file should be a text file with lines + of the form |key = value|. Return a table." + [path] + (def contents (filesystem/read-file path)) + (def lines (filter (fn [x] (not (empty? x))) (string/split "\n" contents))) + (def name-body-pairs (map (fn [line] + (def @[name body] (split-first "=" line)) + @[(string/trim name) (string/trim body)]) + lines)) + (pairs->table name-body-pairs)) + +(defn usage [argv0] + (print "Usage: " argv0 " ") + (os/exit 0)) + +(defn main [argv0 &opt src-dir] + (when (nil? src-dir) (usage argv0)) + (def src-dir (path/normalize src-dir)) + (def config (read-map-file (path/join src-dir "config.txt"))) + (def build-dir (path/normalize (string (get config "BUILD-DIR") path/sep))) + (print "Generating website: " src-dir " -> " build-dir) + (def template-file (get config "TEMPLATE-FILE")) + (def variables-file (get config "VARIABLES-FILE")) + (def template (filesystem/read-file (path/join src-dir template-file))) + (def variables (read-map-file (path/join src-dir variables-file))) + (def processor (make-processor src-dir build-dir template variables)) + # For clean builds, re-create the build directory. + (filesystem/recreate-directory build-dir) + (process-files processor src-dir build-dir)) -- cgit v1.2.3