Recently Written · git

cl-yag

Mirror and rewrite of Solène Rapenne's cl-yag SSG of renown

git clone https://github.com/equwal/cl-yag

Log | Files | Refs


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