commit f02082e674c3fa47cca6bd199631e561e0415375 Spenser Truex <truex@equwal.com> 2025-09-01 15:36:33 -0400 Move generator components out for load order And fix load order for user configured values. Now they can be used as needed by different components. Means of abstraction.
data/articles.lisp | 12 +++- generator-aux-pre.lisp | 74 +++++++++++++++++++++ generator-aux.lisp | 176 +++++++++++++++++++++++++++++++++++++++++++++++++ generators-util.lisp | 11 ++++ 4 files changed, 272 insertions(+), 1 deletion(-)
diff --git a/data/articles.lisp b/data/articles.lisp index 9c6a4a7..597a7d8 100644 --- a/data/articles.lisp +++ b/data/articles.lisp @@ -1,6 +1,9 @@ ;; MIND: The tilde character "~" must be escaped like this '~~' to use it as a literal. -(defvar *config* +(in-package #:cl-yag) + + +(setf *config* (list :webmaster "Your autor name here" :title "Your website's title." @@ -23,6 +26,13 @@ ;; :gopher-index "gophermap" ;; menu file (gophernicus and others) )) +(register-generator :key :html + :create-site-fn #'create-html-site) +(register-generator :key :gopher + :create-site-fn #'create-gopher-hole) +(register-generator :key :gemini + :create-site-fn #'create-gemini-capsule) + ;; Define Your Webpage (converter :name :markdown :extension ".md" :command "peg-markdown -t html -o %OUT data/%IN") diff --git a/generator-aux-pre.lisp b/generator-aux-pre.lisp new file mode 100644 index 0000000..e2390d2 --- /dev/null +++ b/generator-aux-pre.lisp @@ -0,0 +1,74 @@ +(in-package #:cl-yag) + +;;;; GLOBAL VARIABLES + +(defvar *config* '()) +(defvar *generators* '()) + +(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")) + +;; 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) + +;;;; FUNCTIONS + +(require 'asdf) + +(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*))) + +(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)) + +;; we add a generator to the list of available ones +(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 diff --git a/generator-aux.lisp b/generator-aux.lisp new file mode 100644 index 0000000..12d51a2 --- /dev/null +++ b/generator-aux.lisp @@ -0,0 +1,176 @@ +(in-package :cl-yag) + +(setf *articles* (reverse *articles*) + *generators* (reverse *generators*)) + +(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) + (handler-case (with-open-file (stream path :if-exists :supersede :if-does-not-exist :create) + (let ((contents (make-string (file-length stream)))) + (read-sequence contents stream) + contents)) + (file-error (condition) + (cerror "ERROR : file ~a not found. Aborting~%" condition))) + )) + +(defun save-file(path data) + "Save a string to a file." + (with-open-file (stream path :direction :output :if-exists :supersede :if-does-not-exist :create) + (write-sequence data stream))) + +(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 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)) + +(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 gen in *generators* + 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 diff --git a/generators-util.lisp b/generators-util.lisp new file mode 100644 index 0000000..76b7e53 --- /dev/null +++ b/generators-util.lisp @@ -0,0 +1,11 @@ +(in-package #:cl-yag) + +(defmacro template(before &body after) + "Simplify string replacement work in templates." + `(progn + (setf output (replace-all output ,before ,@after)))) + +(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