statyk/statyk
2024-01-22 18:13:33 +01:00

447 lines
12 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 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 [<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 {: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)/*