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 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