#!/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 replace-dir [f new-dir] (str new-dir (fs/file-separator) (rest (fs/components f)))) (defn replace-extension [f new-extension] (str (fs/strip-ext f) "." new-extension)) (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 build-css [src-file dst-file] ;; TODO cless or copy then minify ) (defn build-img [src-file dst-file] ;; TODO jpg => webp ) (defn find-filter-cmds [{:keys [filters] :as ctx} ext] (get filters ext) ) (defn build "Table with specific commands per type of file" [ctx src-path dst-path ext] (if-let [filter-cmds (find-filter-cmds ctx ext)] (doseq [filter-cmd filter-cmds] (log-cmd ctx (concat filter-cmd [src-path dst-path]))) (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 discover-filters "Generate a data structure with: {:map {ext [:dst-dir [{:dst-ext ... :cmd-path ...}]] } :reduces {dst-ext executable-path}} " [ctx] (doseq [f (file-seq "filters/*/map")] ;; ... ) ) (defn all-dst-cmd-and-dst [{:keys [filters] :as ctx} ext src-path] (let [dst-dirs (get-in filters [:map ext])] ;; ... ) ) (defn generate-static-files [ctx] (let [ctx (assoc ctx :filters (discover-filters ctx))] (doseq [src-path (map str (file-seq (io/file (:source-directory config))))] (prn src-path) (let [ext (fs/extension src-path)] (doseq [{:keys [cmd dst-path]} (all-dst-cmd-and-dst ctx ext src-path)] (when (is-newer? src-path dst-path) (prn cmd) (log-cmd ctx (concat cmd [src-path dst-path])))))))) (defn show-help [] (println (string/join "\n" ["Usage: statyk [] []" "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 {:options options}) "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)/*