commit a3ed2d0347fa40a4598ddee005faeb1fd0f7438f Spenser Truex <truex@equwal.com> 2025-09-01 16:09:16 -0400 Refactor generator.lisp to separate out different parts as in README (big refactor)
Makefile | 7 +- config.lisp | 4 - generator-aux-pre.lisp | 2 +- generator-aux.lisp | 19 ++- generator.lisp | 310 +------------------------------------------------ generators-util.lisp | 23 +++- generators/gemini.lisp | 6 +- generators/gopher.lisp | 6 +- generators/html.lisp | 25 +++- 9 files changed, 76 insertions(+), 326 deletions(-)
diff --git a/Makefile b/Makefile index f2e6d33..d98ba6d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ # Find the first available Lisp implementation -LISP := $(shell for lisp in ecl sbcl clisp abcl gcl cmu corman lucid lispworks allegro; do \ +LISPS?=sbcl ecl clisp abcl gcl cmu corman lucid lispworks allegro +LISP := $(shell for lisp in $(LISPS); do \ if command -v $$lisp >/dev/null 2>&1; then \ echo $$lisp; \ break; \ @@ -22,7 +23,9 @@ css: gopher: mkdir -p "output/gopher" -clean: cleanhtml cleangopher cleangemini +clean: temp cleanhtml cleangopher cleangemini + +temp: rm -fr "temp" cleanhtml: diff --git a/config.lisp b/config.lisp deleted file mode 100644 index 40e2b94..0000000 --- a/config.lisp +++ /dev/null @@ -1,4 +0,0 @@ -;; This file loads the configuration from data/articles.lisp -;; It's separated to avoid circular dependencies when loading generators - -(load "data/articles.lisp") \ No newline at end of file diff --git a/generator-aux-pre.lisp b/generator-aux-pre.lisp index e2390d2..de4158d 100644 --- a/generator-aux-pre.lisp +++ b/generator-aux-pre.lisp @@ -71,4 +71,4 @@ (defun register-generator(&key key create-site-fn) (push (make-generator :key key :create-site-fn create-site-fn) - *generators*)) \ No newline at end of file + *generators*)) diff --git a/generator-aux.lisp b/generator-aux.lisp index 12d51a2..977f0cc 100644 --- a/generator-aux.lisp +++ b/generator-aux.lisp @@ -3,6 +3,22 @@ (setf *articles* (reverse *articles*) *generators* (reverse *generators*)) +(defun quit () + "quit portably (ripped from Sharp-CLOCC)" + #+abcl (ext:quit) + #+allegro (excl:exit) + #+clisp (ext:quit) + #+cmu (ext:quit) + #+cormanlisp (win32:exitprocess) + #+ecl (ext:quit) + #+gcl (lisp:bye) + #+lispworks (lw:quit) + #+lucid (lcl:quit) + #+sbcl (sb-ext:quit) + #-(or allegro clisp cmu cormanlisp gcl lispworks lucid sbcl) + (error 'not-implemented :proc (list 'quit)) + ) + (defun replace-all (string part replacement &key (test #'char=)) "Replace all occurrences of PART with REPLACEMENT in STRING." (with-output-to-string (out) @@ -55,7 +71,6 @@ ,@code output))) - (defmacro with-converter(&body code) "Get the converter object for an article and execute code in that context." `(progn @@ -173,4 +188,4 @@ do (if (getf *config* (generator-key gen)) (funcall (generator-create-site-fn gen))))) -;; Note: generate-site is called manually or via `make` \ No newline at end of file +;; Note: generate-site is called manually or via `make` diff --git a/generator.lisp b/generator.lisp index bd846d1..e6b303c 100644 --- a/generator.lisp +++ b/generator.lisp @@ -1,307 +1,3 @@ -;;;; GLOBAL VARIABLES - -(defun quit () - "quit portably (ripped from Sharp-CLOCC)" - #+abcl (ext:quit) - #+allegro (excl:exit) - #+clisp (ext:quit) - #+cmu (ext:quit) - #+cormanlisp (win32:exitprocess) - #+ecl (ext:quit) - #+gcl (lisp:bye) - #+lispworks (lw:quit) - #+lucid (lcl:quit) - #+sbcl (sb-ext:quit) - #-(or allegro clisp cmu cormanlisp gcl lispworks lucid sbcl) - (error 'not-implemented :proc (list 'quit)) - ) - - -(defparameter *articles* '()) -(defparameter *converters* '()) -(defparameter *days* '("Monday" "Tuesday" "Wednesday" "Thursday" - "Friday" "Saturday" "Sunday")) -(defparameter *months* '("January" "February" "March" "April" - "May" "June" "July" "August" "September" - "October" "November" "December")) -(defparameter *generators* '()) - -;; structure to store links -(defstruct article title tag date id tiny author rawdate converter) -(defstruct converter name command extension) -(defstruct generator key create-site-fn) - -;; put these somewhere else and automagically get them pushed to *generators* - -;;;; FUNCTIONS - -(require 'asdf) - -(load "config.lisp") - -(defun get-day-of-week(day month year) - "Return the day of the week." - (multiple-value-bind - (second minute hour date month year day-of-week dst-p tz) - (decode-universal-time (encode-universal-time 0 0 0 day month year)) - (declare (ignore second minute hour date month year dst-p tz)) - day-of-week)) - -(defun date-parse(date) - "Parse a date into components." - (if (= 8 (length date)) - (let* ((year (parse-integer date :start 0 :end 4)) - (monthnum (parse-integer date :start 4 :end 6)) - (daynum (parse-integer date :start 6 :end 8)) - (day (nth (get-day-of-week daynum monthnum year) *days*)) - (month (nth (- monthnum 1) *months*))) - (list - :dayname day - :daynumber daynum - :monthname month - :monthnumber monthnum - :year year)) - nil)) - -(defun post(&optional &key title tag date id (tiny nil) (author (getf *config* :webmaster)) (converter nil)) - (push (make-article :title title - :tag tag - :date (date-parse date) - :rawdate date - :tiny tiny - :author author - :id id - :converter converter) - *articles*)) - -(defun converter(&optional &key name command extension) - "Add a converter to the list of those available." - (setf *converters* - (append - (list name - (make-converter :name name - :command command - :extension extension)) - *converters*))) - -;; we add a generator to the list of available ones -(defun make-generator(&optional &key key create-site-fn) - (push (make-generator :key key - :create-site-fn create-site-fn) - *generators*)) - -;; Load data from metadata and config -(load "data/articles.lisp") -(setf *articles* (reverse *articles*)) - -(defun replace-all (string part replacement &key (test #'char=)) - "Replace all occurrences of PART with REPLACEMENT in STRING." - (with-output-to-string (out) - (loop with part-length = (length part) - for old-pos = 0 then (+ pos part-length) - for pos = (search part string - :start2 old-pos - :test test) - do (write-string string out - :start old-pos - :end (or pos (length string))) - when pos do (write-string replacement out) - while pos))) - -(defun split-str(text &optional (separator #\Space)) - "This function splits a string with separator and returns a list." - (let ((text (concatenate 'string text (string separator)))) - (loop for char across text - counting char into count - when (char= char separator) - collect - ;; we look at the position of the left separator from right to left - (let ((left-separator-position (position separator text :from-end t :end (- count 1)))) - (subseq text - ;; if we can't find a separator at the left of the current, then it's the start of - ;; the string - (if left-separator-position (+ 1 left-separator-position) 0) - (- count 1)))))) - -(defun load-file(path) - "Load a file as a string. We escape ~ to avoid failures with format." - (if (probe-file path) - (with-open-file (stream path) - (let ((contents (make-string (file-length stream)))) - (read-sequence contents stream) - contents)) - (progn - (format t "ERROR : file ~a not found. Aborting~%" path) - (quit)))) - -(defun save-file(path data) - "Save a string to a file." - (with-open-file (stream path :direction :output :if-exists :supersede) - (write-sequence data stream))) - -(defmacro template(before &body after) - "Simplify string replacement work in templates." - `(progn - (setf output (replace-all output ,before ,@after)))) - -(defmacro prepare(template &body code) - "Simplify the declaration of a new page type by loading a template and executing code." - `(progn - (let ((output (load-file ,template))) - ,@code - output))) - - -(defmacro with-converter(&body code) - "Get the converter object for an article and execute code in that context." - `(progn - (let ((converter-name (or (article-converter article) - (getf *config* :default-converter)))) - (let ((converter-object (getf *converters* converter-name))) - ,@code)))) - -(defun use-converter-to-html(filename &optional (converter-name nil)) - "Generate HTML file from source file using the converter associated with the post." - (let* ((converter-object (getf *converters* - (or converter-name - (getf *config* :default-converter)))) - (output (converter-command converter-object)) - (src-file (format nil "~a~a" filename (converter-extension converter-object))) - (dst-file (format nil "temp/data/~a.html" filename )) - (full-src-file (format nil "data/~a" src-file))) - ;; skip generating if the destination exists - ;; and is more recent than source - (unless (and - (probe-file dst-file) - (>= - (file-write-date dst-file) - (file-write-date full-src-file))) - (ensure-directories-exist "temp/data/") - (template "%IN" src-file) - (template "%OUT" dst-file) - (format t "~a~%" output) - (uiop:run-program output)))) - -(defun date-format(format date) - "Format a date using the given format string with template substitutions." - (let ((output format)) - (template "%DayName" (getf date :dayname)) - (template "%DayNumber" (format nil "~2,'0d" (getf date :daynumber))) - (template "%MonthName" (getf date :monthname)) - (template "%MonthNumber" (format nil "~2,'0d" (getf date :monthnumber))) - (template "%Year" (write-to-string (getf date :year ))) - output)) - -(defmacro generate(name &body data) - "Simplify file saving by using the layout system." - `(progn - (save-file ,name (generate-layout ,@data)))) - -(defun articles-by-tag() - "Generate a list of tags with associated article IDs." - (let ((tag-list)) - (loop for article in *articles* do - (when (article-tag article) ;; we don't want an error if no tag - (loop for tag in (split-str (article-tag article)) do ;; for each word in tag keyword - (setf (getf tag-list (intern tag "KEYWORD")) ;; we create the keyword is inexistent and add ID to :value - (list - :name tag - :value (push (article-id article) (getf (getf tag-list (intern tag "KEYWORD")) :value))))))) - (loop for i from 1 to (length tag-list) by 2 collect ;; removing the keywords - (nth i tag-list)))) - -(defun get-tag-list-article(&optional article) - "Generate HTML for the list of tags for a specific article." - (apply #'concatenate 'string - (mapcar #'(lambda (item) - (prepare "templates/one-tag.tpl" (template "%%Name%%" item))) - (split-str (article-tag article))))) - -(defun get-tag-list() - "Generate HTML for the complete list of all tags." - (apply #'concatenate 'string - (mapcar #'(lambda (item) - (prepare "templates/one-tag.tpl" - (template "%%Name%%" (getf item :name)))) - (articles-by-tag)))) - -(defun create-article(article &optional &key (tiny t) (no-text nil)) - "Generate HTML for a single article. Called in a loop to produce the homepage." - (prepare "templates/article.tpl" - (template "%%Author%%" (let ((author (article-author article))) - (or author (getf *config* :webmaster)))) - (template "%%Date%%" (date-format (getf *config* :date-format) - (article-date article))) - (template "%%Raw-Date%%" (article-rawdate article)) - (template "%%Title%%" (article-title article)) - (template "%%Id%%" (article-id article)) - (template "%%Tags%%" (get-tag-list-article article)) - (template "%%Date-Url%%" (date-format "%Year-%MonthNumber-%DayNumber" - (article-date article))) - (template "%%Text%%" (if no-text - "" - (if (and tiny (article-tiny article)) - (format nil "<p>~a</p>" (article-tiny article)) - (load-file (format nil "temp/data/~d.html" (article-id article)))))))) - -(defun generate-layout(body &optional &key (title nil)) - "Return HTML string for a complete page with title and layout, using the parameter as content." - (prepare "templates/layout.tpl" - (template "%%Title%%" (or title (getf *config* :title))) - (template "%%Tags%%" (get-tag-list)) - (template "%%Body%%" body) - output)) - -(defun generate-semi-mainpage(&key (tiny t) (no-text nil)) - "Generate HTML for the index homepage." - (apply #'concatenate 'string - (loop for article in *articles* collect - (create-article article :tiny tiny :no-text no-text)))) - -(defun generate-tag-mainpage(articles-in-tag) - "Generate HTML for a tag-specific homepage." - (apply #'concatenate 'string - (loop for article in *articles* - when (member (article-id article) articles-in-tag :test #'equal) - collect (create-article article :tiny t)))) -(defun generate-rss-item (fn) - "Generate XML for RSS feed items." - (apply #'concatenate 'string - (loop for article in *articles* - for i from 1 to (min (length *articles*) (getf *config* :rss-item-number)) - collect - (prepare "templates/rss-item.tpl" - (template "%%Title%%" (article-title article)) - (template "%%Description%%" (load-file (format nil "temp/data/~d.html" (article-id article)))) - (template "%%Date%%" (format nil - (date-format "~a, %DayNumber ~a %Year 00:00:00 GMT" - (article-date article)) - (subseq (getf (article-date article) :dayname) 0 3) - (subseq (getf (article-date article) :monthname) 0 3))) - (template "%%Url%%" (funcall fn article)))))) - - -(defun generate-rss(fn) - "Generate complete RSS XML data." - (prepare "templates/rss.tpl" - (template "%%Description%%" (getf *config* :description)) - (template "%%Title%%" (getf *config* :title)) - (template "%%Url%%" (getf *config* :url)) - (template "%%Items%%" (generate-rss-item fn)))) - -(defun generate-site() - "Main function called when running the site generation tool." - (loop for i in *generators* - do (if (getf *config* (getf i :key)) - (funcall (function (getf i :create-site-fn)))))) - -;; all the generators -(load "generators/html.lisp") -(load "generators/gopher.lisp") -(load "generators/gemini.lisp") - -;;;; EXECUTION - -(generate-site) - -(quit) +(asdf:load-system :cl-yag) +(cl-yag:generate-site) +(cl-yag::quit) diff --git a/generators-util.lisp b/generators-util.lisp index 76b7e53..e5a5685 100644 --- a/generators-util.lisp +++ b/generators-util.lisp @@ -1,11 +1,32 @@ +<<<<<<< HEAD (in-package #:cl-yag) +======= + +(in-package :cl-yag) + +(defmacro generate(name &body data) + "Simplify file saving by using the layout system." + `(progn + (save-file ,name (generate-layout ,@data)))) +>>>>>>> abstract-protocols (defmacro template(before &body after) "Simplify string replacement work in templates." `(progn (setf output (replace-all output ,before ,@after)))) +<<<<<<< HEAD (defmacro generate(name &body data) "Simplify file saving by using the layout system." `(progn - (save-file ,name (generate-layout ,@data)))) \ No newline at end of file + (save-file ,name (generate-layout ,@data)))) +======= +(defmacro with-converter(&body code) + "Get the converter object for an article and execute code in that context." + `(progn + (let ((converter-name (or (article-converter article) + (getf *config* :default-converter)))) + (let ((converter-object (getf *converters* converter-name))) + ,@code)))) + +>>>>>>> abstract-protocols diff --git a/generators/gemini.lisp b/generators/gemini.lisp index d9f7728..f34bdd7 100644 --- a/generators/gemini.lisp +++ b/generators/gemini.lisp @@ -1,3 +1,6 @@ + +(in-package #:cl-yag) + (defun generate-rss-gemini(article) (format nil "gemini://~a/articles/~a.gmi" (getf *config* :gemini-path) @@ -71,6 +74,3 @@ "Date : " (date-format (getf *config* :date-format) (article-date article)) #\Newline "Tags : " (article-tag article) #\Newline #\Newline (load-file (format nil "data/~d~d" id (converter-extension converter-object)))))))))) - -(make-generator :key :gemini - :create-site-fn #'create-gemini-capsule) \ No newline at end of file diff --git a/generators/gopher.lisp b/generators/gopher.lisp index a0c9443..b7d29a2 100644 --- a/generators/gopher.lisp +++ b/generators/gopher.lisp @@ -1,3 +1,6 @@ + +(in-package #:cl-yag) + (defun generate-rss-gopher(article) (format nil "gopher://~a:~d/0~a/article-~a.txt" (getf *config* :gopher-server) @@ -94,6 +97,3 @@ (date-format (getf *config* :date-format) (article-date article)) (article-tag article) (load-file (format nil "data/~d~d" id (converter-extension converter-object))))))))) - -(make-generator :key :gopher - :create-site-fn #'create-gopher-hole) \ No newline at end of file diff --git a/generators/html.lisp b/generators/html.lisp index 79495c1..1c5f06c 100644 --- a/generators/html.lisp +++ b/generators/html.lisp @@ -1,4 +1,5 @@ +(in-package #:cl-yag) (defun generate-rss-html(article) (format nil "~d~d-~d.html" @@ -7,6 +8,27 @@ (article-date article)) (article-id article))) +(defun use-converter-to-html(filename &optional (converter-name nil)) + "Generate HTML file from source file using the converter associated with the post." + (let* ((converter-object (getf *converters* + (or converter-name + (getf *config* :default-converter)))) + (output (converter-command converter-object)) + (src-file (format nil "~a~a" filename (converter-extension converter-object))) + (dst-file (format nil "temp/data/~a.html" filename )) + (full-src-file (format nil "data/~a" src-file))) + ;; skip generating if the destination exists + ;; and is more recent than source + (unless (and + (probe-file dst-file) + (>= + (file-write-date dst-file) + (file-write-date full-src-file))) + (ensure-directories-exist "temp/data/") + (template "%IN" src-file) + (template "%OUT" dst-file) + (format t "~a~%" output) + (uiop:run-program output)))) ;; We do all the website (defun create-html-site() @@ -42,9 +64,6 @@ ;;(generate-file-rss) (save-file "output/html/rss.xml" (generate-rss #'generate-rss-html))) -(make-generator :key :html - :create-site-fn #'create-html-site) - ;; html generation of index homepage (defun generate-semi-mainpage(&key (tiny t) (no-text nil)) (apply #'concatenate 'string