502 lines
14 KiB
Clojure
Executable file
502 lines
14 KiB
Clojure
Executable file
#!/bin/bash # -*- mode: Clojure; -*-
|
|
#_(
|
|
|
|
export COMMANDNAME="$0"
|
|
|
|
#_DEPS is same format as deps.edn. Multiline is okay.
|
|
DEPS='
|
|
{:paths ["." "resources"]
|
|
:deps {
|
|
org.clojure/tools.cli {:mvn/version "1.0.214"}
|
|
ring/ring-core {:mvn/version "1.11.0"}
|
|
ring/ring-jetty-adapter {:mvn/version "1.11.0"}
|
|
babashka/fs {:mvn/version "0.5.20"}
|
|
;; logs for jetty
|
|
org.slf4j/slf4j-simple {:mvn/version "2.0.11"}
|
|
}}
|
|
'
|
|
|
|
#_You can put other options here
|
|
OPTS='
|
|
-J-Xms4m -J-Xmx256m
|
|
'
|
|
#_Install Clojure if not present on the system (java is needed though)
|
|
if [[ ! -x .local/bin/clojure ]]; then
|
|
[[ ! -d .local ]] && mkdir .local
|
|
pushd .local
|
|
curl -O https://download.clojure.org/install/posix-install-1.11.1.1273.sh
|
|
chmod +x posix-install-1.11.1.1273.sh
|
|
./posix-install-1.11.1.1273.sh -p $PWD
|
|
popd
|
|
fi
|
|
|
|
exec clojure $OPTS -Sdeps "$DEPS" -M "$0" "$@"
|
|
)
|
|
|
|
(ns statyk
|
|
(:require
|
|
[clojure.java.shell :refer [sh]]
|
|
[clojure.tools.cli :refer [parse-opts]]
|
|
[clojure.java.io :as io]
|
|
[clojure.pprint :as pp]
|
|
[clojure.string :as string]
|
|
[ring.util.mime-type :as mime]
|
|
[ring.util.response :as resp]
|
|
[babashka.fs :as fs]
|
|
[ring.adapter.jetty :refer [run-jetty]]))
|
|
|
|
(def config
|
|
{:source-directory "_src"
|
|
:dest-directory "_site"
|
|
:templates-directory "_templates"
|
|
:port 13375
|
|
:host "127.0.0.1"})
|
|
|
|
(def cli-options
|
|
[])
|
|
|
|
(defn p [& args]
|
|
(println (apply format args)))
|
|
|
|
(defn pr [& args]
|
|
(print (apply format args))
|
|
(flush))
|
|
|
|
(defn pr-err!
|
|
"Print on std error"
|
|
[s & args]
|
|
(binding [*out* *err*]
|
|
(apply p s args)))
|
|
|
|
(defn err!
|
|
[s & args]
|
|
(apply pr-err! s args)
|
|
(System/exit 1))
|
|
|
|
(defn read-file
|
|
[path]
|
|
(try
|
|
(slurp path)
|
|
(catch Exception e
|
|
(pr-err! "Could not read %s" path)
|
|
nil)))
|
|
|
|
(defn is-newer?
|
|
"returns true if file-1 is newer than file-2 or if file-2 does not exists"
|
|
[f1 f2]
|
|
(when-not (fs/exists? f1)
|
|
(err! "File '%s' does not exists..." f1))
|
|
(if (fs/exists? f2)
|
|
(let [[t1 t2] (mapv #(fs/file-time->millis (fs/get-attribute % "basic:lastModifiedTime"))
|
|
[f1 f2])]
|
|
(< t2 t1))
|
|
true))
|
|
|
|
(defn serve-dest-dir-handler
|
|
[request]
|
|
(let [path (str (:dest-directory config) (:uri request))]
|
|
(if-let [content (read-file path)]
|
|
(let [mime-type (mime/ext-mime-type path)]
|
|
(cond-> (resp/response content)
|
|
mime-type (resp/content-type mime-type)))
|
|
{:status 404
|
|
:header {"Content-Type" "text/plain"}
|
|
:body (format "File '%s' not found." path)})))
|
|
|
|
(defn html-src-type? [extension]
|
|
(contains? #{"org" "md"} extension))
|
|
|
|
(defn css-src-type? [extension]
|
|
(contains? #{"css" "scss" "sass"} extension))
|
|
|
|
(defn img-src-type? [extension]
|
|
(contains? #{"jpg" "jpeg" "png" "wepb"} extension))
|
|
|
|
(defn replace-extension
|
|
[f new-extension]
|
|
(str (fs/strip-ext f) "." new-extension))
|
|
|
|
(defn dst-file-name-and-ext
|
|
[src-file]
|
|
(let [ext (fs/extension src-file)
|
|
tmp-path (string/replace src-file
|
|
(:source-directory config)
|
|
(:dest-directory config))
|
|
dst-path (cond (html-src-type? ext) (replace-extension tmp-path "html")
|
|
(css-src-type? ext) (replace-extension tmp-path "css")
|
|
(img-src-type? ext) (replace-extension tmp-path "webp")
|
|
:else tmp-path)]
|
|
{:ext ext
|
|
:dst-path dst-path}))
|
|
|
|
(defn find-org-mode-options
|
|
[org]
|
|
;; TODO search for options in the org mode file
|
|
;; (seek org #"^#+options:")
|
|
nil)
|
|
|
|
(defn find-md-mode-options
|
|
[md]
|
|
;; TODO search for options in the metadata of the md file
|
|
;; (seek org #"^#+options:")
|
|
nil)
|
|
|
|
(defn log-cmd
|
|
([cmd]
|
|
(when-let [error-msg (seq (:err (apply sh cmd)))]
|
|
(pr-err! "\n\n%s\n" error-msg)))
|
|
([msg cmd]
|
|
(when msg
|
|
(pr msg))
|
|
(if-let [error-msg (seq (:err (apply sh cmd)))]
|
|
(do
|
|
(p " [failed!]")
|
|
(pr-err! "\n\n%s\n" error-msg))
|
|
(p " [done]"))))
|
|
|
|
(defn org->html
|
|
[src-file dst-file]
|
|
(let [options (find-org-mode-options src-file)
|
|
cmd (filter some?
|
|
["pandoc"
|
|
(when (:toc options)
|
|
"--toc")
|
|
(if-let [tpl (:template options)]
|
|
(format "--template=%s" tpl)
|
|
(format "--template=_templates/post.html"))
|
|
"--lua-filter=.statyk/org-links-to-html.lua"
|
|
"--lua-filter=.statyk/img-to-webp.lua"
|
|
"--lua-filter=.statyk/metas.lua"
|
|
"--mathml"
|
|
"--from" "org"
|
|
"--to" "html5"
|
|
"--standalone"
|
|
src-file
|
|
"--output"
|
|
dst-file])]
|
|
(log-cmd (format "Generating %s" dst-file) cmd)))
|
|
|
|
(defn md->html
|
|
[src-file dst-file]
|
|
(let [options (find-md-mode-options src-file)
|
|
cmd (filter some?
|
|
["pandoc"
|
|
(when (:toc options) "--toc")
|
|
(if-let [tpl (:template options)]
|
|
(format "--template=%s" tpl)
|
|
(format "--template=_templates/post.html"))
|
|
"--lua-filter=.statyk/img-to-webp.lua"
|
|
"--lua-filter=.statyk/metas.lua"
|
|
"--mathml"
|
|
"--from" "markdown"
|
|
"--to" "html5"
|
|
"--standalone"
|
|
src-file
|
|
"--output"
|
|
dst-file])]
|
|
(log-cmd (format "Generating %s" dst-file) cmd)))
|
|
|
|
(defn build-html
|
|
[ext src-file dst-file]
|
|
;; TODO PANDOC then minify
|
|
(case ext
|
|
"org" (org->html src-file dst-file)
|
|
"md" (md->html src-file dst-file)
|
|
(p "no support for %s" ext)))
|
|
|
|
(defn build-css
|
|
[src-file dst-file]
|
|
;; TODO cless or copy then minify
|
|
)
|
|
|
|
(defn build-img
|
|
[src-file dst-file]
|
|
;; TODO jpg => webp
|
|
)
|
|
|
|
(defn build
|
|
"Table with specific commands per type of file"
|
|
[src-path dst-path ext]
|
|
(cond
|
|
(html-src-type? ext) (build-html ext src-path dst-path)
|
|
(css-src-type? ext) (build-css src-path dst-path)
|
|
(img-src-type? ext) (build-img src-path dst-path)
|
|
:else
|
|
(if (fs/directory? src-path)
|
|
(do
|
|
(p "Creating directory %s" dst-path)
|
|
(fs/create-dirs dst-path))
|
|
(do
|
|
(p "Copying to %s" dst-path)
|
|
(fs/copy src-path dst-path)))))
|
|
|
|
(defn generate-static-files
|
|
[]
|
|
(doseq [src-path (map str (file-seq (io/file (:source-directory config))))]
|
|
(prn src-path)
|
|
(let [{:keys [ext dst-path] :as r} (dst-file-name-and-ext src-path)]
|
|
(when (is-newer? src-path dst-path)
|
|
(prn r)
|
|
(build src-path dst-path ext)))))
|
|
|
|
(defn show-help
|
|
[]
|
|
(println (string/join "\n"
|
|
["Usage: statyk [<options>] <command> [<args>]"
|
|
"Where commands:"
|
|
"- serve serve the directory _site"
|
|
"- build build the site from the sources"
|
|
"- clean delete generated files in _site"]))
|
|
(System/exit 0))
|
|
|
|
(defn cp-resource [dst-dir resource-name]
|
|
(let [dst (str dst-dir "/" resource-name)]
|
|
(println (format "Generating: %s" dst))
|
|
(if-let [resource (io/resource (str "statyk/" resource-name))]
|
|
(spit dst (slurp resource))
|
|
(err! "Cannot read %s" (io/resource (str "statyk/" resource-name))))))
|
|
|
|
(defn init-site
|
|
[_options]
|
|
(doseq [d [(:source-directory config)
|
|
(:dest-directory config)
|
|
(:templates-directory config)
|
|
".statyk"]]
|
|
(println (format "Create dir: %s" d))
|
|
(fs/create-dirs d))
|
|
(doseq [f ["org-links-to-html.lua"
|
|
"img-to-webp.lua"
|
|
"metas.lua"]]
|
|
(cp-resource ".statyk" f))
|
|
(doseq [f ["index.html"
|
|
"post.html"]]
|
|
(cp-resource (:templates-directory config) f)))
|
|
|
|
(defn do-command
|
|
[command options]
|
|
(case command
|
|
"serve" (do
|
|
(println "Serve: http://127.0.0.1:13375")
|
|
(run-jetty serve-dest-dir-handler
|
|
{:port 13375
|
|
:host "127.0.0.1"}))
|
|
"build" (generate-static-files)
|
|
"init" (init-site options)
|
|
(show-help)
|
|
(err! (format "unknown command %s" command))))
|
|
|
|
(defn initialized?
|
|
"Returns false if the current directory does not conform to an initalized statyk dir."
|
|
[]
|
|
(and
|
|
(fs/exists? (:source-directory config))
|
|
(fs/exists? (:dest-directory config))))
|
|
|
|
(defn -main [& args]
|
|
(let [{:keys [options arguments summary errors]
|
|
:as parsed}
|
|
(parse-opts args cli-options)]
|
|
(when errors
|
|
(doseq [err errors]
|
|
(pr-err! err)
|
|
(System/exit 1)))
|
|
(if-let [command (first arguments)]
|
|
(do-command command options)
|
|
(do
|
|
(when-not (initialized?)
|
|
(do-command "init" options))
|
|
|
|
(future (do-command "serve" options))
|
|
(do-command "watch" options))))
|
|
(shutdown-agents))
|
|
|
|
(apply -main *command-line-args*)
|
|
|
|
|
|
|
|
;; Generate a website out of
|
|
;; # any kind of document files
|
|
;; # txt, markdown, org-mode, gemini etc...
|
|
;;
|
|
;; all: site
|
|
;; SRC_DIR ?= src
|
|
;; DST_DIR ?= _site
|
|
;; CACHE_DIR ?= .cache
|
|
;;
|
|
;; # we don't copy source files
|
|
;; NO_SRC_FILE := ! -name '*.org' ! -name '*.css' ! -name '*.md'
|
|
;;
|
|
;; define adv_rule
|
|
;; SRC_$(1) := $$(shell find $$(SRC_DIR) -type f $(2))
|
|
;; DST_$(1) := $$(patsubst $$(SRC_DIR)/%,$$(DST_DIR)/%,$$(SRC_$(1)))
|
|
;; $$(DST_DIR)/%$(4): $$(SRC_DIR)/%$(4)
|
|
;; @mkdir -p "$$(dir $$@)"
|
|
;; $(3)
|
|
;; .PHONY: $(1)
|
|
;; $(1): $$(DST_$(1))
|
|
;; ALL += $(1)
|
|
;; endef
|
|
;;
|
|
;; define rule
|
|
;; SRC_$(1) := $$(shell find $$(SRC_DIR) -type f $(2))
|
|
;; DST_$(1) := $$(patsubst $$(SRC_DIR)/%,$$(DST_DIR)/%,$$(SRC_$(1)))
|
|
;; $$(DST_DIR)/%.$(1): $$(SRC_DIR)/%.$(1)
|
|
;; @mkdir -p "$$(dir $$@)"
|
|
;; $(3)
|
|
;; .PHONY: $(1)
|
|
;; $(1): $$(DST_$(1))
|
|
;; ALL += $(1)
|
|
;; endef
|
|
;;
|
|
;; # copy assets
|
|
;; $(eval $(call adv_rule,assets, $$(NO_SRC_FILE),cp "$$<" "$$@",))
|
|
;;
|
|
;; # CSS
|
|
;; $(eval $(call rule,css,-name '*.css',minify "$$<" > "$$@"))
|
|
;;
|
|
;;
|
|
;; # ORG, MD -> HTML
|
|
;; EXT ?= .org
|
|
;; SRC_PANDOC_FILES ?= $(shell find $(SRC_DIR) -type f \( -name "*$(EXT)" -o \) $(NO_DRAFT))
|
|
;; DST_PANDOC_FILES ?= $(patsubst %$(EXT),%.html, \
|
|
;; $(patsubst $(SRC_DIR)/%,$(DST_DIR)/%, \
|
|
;; $(SRC_PANDOC_FILES)))
|
|
;; PANDOC_TEMPLATE ?= templates/post.html
|
|
;; PANDOC_LUA_FILTER ?= engine/links-to-html.lua
|
|
;; PANDOC_LUA_FILTER_IMG ?= engine/img-to-webp.lua
|
|
;; PANDOC_LUA_METAS ?= engine/metas.lua
|
|
;; MK_HTML := engine/mk-html.sh
|
|
;; PANDOC := $(MK_HTML) $(PANDOC_TEMPLATE) $(PANDOC_LUA_FILTER) $(PANDOC_LUA_FILTER_IMG) $(PANDOC_LUA_METAS)
|
|
;; $(DST_DIR)/%.html: $(SRC_DIR)/%.org $(PANDOC_TEMPLATE) $(PANDOC_LUA_FILTER) $(PANDOC_LUA_FILTER_IMG) $(PANDOC_LUA_METAS) $(MK_HTML) $(ENV_VARS)
|
|
;; @mkdir -p "$(dir $@)"
|
|
;; $(PANDOC) "$<" "$@.tmp"
|
|
;; minify --mime text/html "$@.tmp" > "$@"
|
|
;; @rm "$@.tmp"
|
|
;; .PHONY: html
|
|
;; html: $(DST_PANDOC_FILES)
|
|
;; ALL += html
|
|
;;
|
|
;; # INDEXES
|
|
;; SRC_POSTS_DIR ?= $(SRC_DIR)/posts
|
|
;; DST_POSTS_DIR ?= $(DST_DIR)/posts
|
|
;; SRC_POSTS_FILES ?= $(shell find $(SRC_POSTS_DIR) -type f -name "*$(EXT)")
|
|
;; RSS_CACHE_DIR ?= $(CACHE_DIR)/rss
|
|
;; DST_XML_FILES ?= $(patsubst %.org,%.xml, \
|
|
;; $(patsubst $(SRC_POSTS_DIR)/%,$(RSS_CACHE_DIR)/%, \
|
|
;; $(SRC_POSTS_FILES)))
|
|
;; $(RSS_CACHE_DIR)/%.xml: $(DST_POSTS_DIR)/%.html $(ENV_VARS)
|
|
;; @mkdir -p "$(dir $@)"
|
|
;; hxclean "$<" > "$@"
|
|
;; .PHONY: indexcache
|
|
;; indexcache: $(DST_XML_FILES)
|
|
;; ALL += indexcache
|
|
;;
|
|
;; # HTML INDEX
|
|
;; DST_INDEX_FILES ?= $(patsubst %.xml,%.index, $(DST_XML_FILES))
|
|
;; MK_INDEX_ENTRY := ./engine/mk-index-entry.sh
|
|
;; INDEX_CACHE_DIR ?= $(CACHE_DIR)/rss
|
|
;; $(INDEX_CACHE_DIR)/%.index: $(INDEX_CACHE_DIR)/%.xml $(MK_INDEX_ENTRY) $(ENV_VARS)
|
|
;; @mkdir -p $(INDEX_CACHE_DIR)
|
|
;; $(MK_INDEX_ENTRY) "$<" "$@"
|
|
;;
|
|
;; HTML_INDEX := $(DST_DIR)/index.html
|
|
;; MKINDEX := engine/mk-index.sh
|
|
;; INDEX_TEMPLATE ?= templates/index.html
|
|
;; $(HTML_INDEX): $(DST_INDEX_FILES) $(MKINDEX) $(INDEX_TEMPLATE) $(ENV_VARS)
|
|
;; @mkdir -p $(DST_DIR)
|
|
;; $(MKINDEX)
|
|
;; .PHONY: index
|
|
;; index: $(HTML_INDEX)
|
|
;; ALL += index
|
|
;;
|
|
;; # RSS
|
|
;; DST_RSS_FILES ?= $(patsubst %.xml,%.rss, $(DST_XML_FILES)) $(ENV_VARS)
|
|
;; MK_RSS_ENTRY := ./engine/mk-rss-entry.sh
|
|
;; $(RSS_CACHE_DIR)/%.rss: $(RSS_CACHE_DIR)/%.xml $(MK_RSS_ENTRY)
|
|
;; @mkdir -p $(RSS_CACHE_DIR)
|
|
;; $(MK_RSS_ENTRY) "$<" "$@"
|
|
;;
|
|
;; RSS := $(DST_DIR)/rss.xml
|
|
;; MKRSS := engine/mkrss.sh
|
|
;; $(RSS): $(DST_RSS_FILES) $(MKRSS) $(ENV_VARS)
|
|
;; $(MKRSS)
|
|
;;
|
|
;; .PHONY: rss
|
|
;; rss: $(RSS)
|
|
;; ALL += rss
|
|
;;
|
|
;;
|
|
;; # ORG -> GEMINI
|
|
;; EXT := .org
|
|
;; SRC_GMI_FILES ?= $(shell find $(SRC_DIR) -type f -name "*$(EXT)" $(NO_DRAFT))
|
|
;; DST_GMI_FILES ?= $(subst $(EXT),.gmi, \
|
|
;; $(patsubst $(SRC_DIR)/%,$(DST_DIR)/%, \
|
|
;; $(SRC_GMI_FILES)))
|
|
;; GMI := engine/org2gemini.sh
|
|
;; $(DST_DIR)/%.gmi: $(SRC_DIR)/%.org $(GMI) engine/org2gemini_step1.sh
|
|
;; @mkdir -p $(dir $@)
|
|
;; $(GMI) "$<" "$@"
|
|
;; ALL += $(DST_GMI_FILES)
|
|
;; .PHONY: gmi
|
|
;; gmi: $(DST_GMI_FILES)
|
|
;;
|
|
;; # GEMINI INDEX
|
|
;; GMI_INDEX := $(DST_DIR)/index.gmi
|
|
;; MK_GMI_INDEX := engine/mk-gemini-index.sh
|
|
;; $(GMI_INDEX): $(DST_GMI_FILES) $(MK_GMI_INDEX) $(ENV_VARS)
|
|
;; @mkdir -p $(DST_DIR)
|
|
;; $(MK_GMI_INDEX)
|
|
;; ALL += $(GMI_INDEX)
|
|
;; .PHONY: gmi-index
|
|
;; gmi-index: $(GMI_INDEX)
|
|
;;
|
|
;; # RSS
|
|
;; GMI_ATOM := $(DST_DIR)/gem-atom.xml
|
|
;; MK_GEMINI_ATOM := engine/mk-gemini-atom.sh
|
|
;; $(GMI_ATOM): $(DST_GMI_FILES) $(MK_GEMINI_ATOM)
|
|
;; $(MK_GEMINI_ATOM)
|
|
;; ALL += $(GMI_ATOM)
|
|
;; .PHONY: gmi-atom
|
|
;; gmi-atom: $(GMI_ATOM)
|
|
;;
|
|
;; .PHONY: gemini
|
|
;; gemini: $(DST_GMI_FILES) $(GMI_INDEX) $(GMI_ATOM)
|
|
;;
|
|
;; # Images
|
|
;; OPTIM_IMG := engine/optim-img.sh
|
|
;;
|
|
;; define img
|
|
;; SRC_IMG_$(1) ?= $$(shell find $$(SRC_DIR) -type f -name "*.$(1)")
|
|
;; DST_IMG_$(1) ?= $$(patsubst $$(SRC_DIR)/%,$$(DST_DIR)/%,$$(SRC_IMG_$(1)))
|
|
;; $$(DST_DIR)/%.$(1): $$(SRC_DIR)/%.$(1) $$(OPTIM_IMG)
|
|
;; @mkdir -p $$(dir $$@)
|
|
;; $$(OPTIM_IMG) "$$<" "$$@"
|
|
;; .PHONY: $(1)
|
|
;; $(1): $$(DST_IMG_$(1))
|
|
;; ALL += $(1)
|
|
;; endef
|
|
;;
|
|
;; $(info $(call img,jpg))
|
|
;; $(eval $(call img,jpg))
|
|
;; $(eval $(call img,jpeg))
|
|
;; $(eval $(call img,gif))
|
|
;; $(eval $(call img,png))
|
|
;;
|
|
;; .PHONY: img
|
|
;; img: jpg jpeg gif png
|
|
;;
|
|
;; # DEPLOY
|
|
;; .PHONY: site
|
|
;; site: $(ALL)
|
|
;;
|
|
;; .PHONY: deploy
|
|
;; deploy: $(ALL)
|
|
;; engine/sync.sh # deploy to her.esy.fun
|
|
;; engine/ye-com-fastpublish.hs # deploy to yannesposito.com (via github pages)
|
|
;;
|
|
;; .PHONY: clean
|
|
;; clean:
|
|
;; -[ -f $(ENV_VARS) ] && rm $(ENV_VARS)
|
|
;; -[ ! -z "$(DST_DIR)" ] && rm -rf $(DST_DIR)/*
|
|
;; -[ ! -z "$(CACHE_DIR)" ] && rm -rf $(CACHE_DIR)/*
|