commit 0428f39713dfcadd7d1ba413e6ebd47430d92cf2 Spenser Truex <truex@equwal.com> 2025-08-31 21:53:02 -0400 Style: Convert comments to docstrings Better style, now the project can be documented with Quickref, and inspection tools work more nicely.
1 | 218 ++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 2 +- Makefile | 20 ++++- README.md | 40 ++++----- cl-yag.ad | 16 ++++ config.lisp | 4 + data/README.md | 22 +++-- data/articles.lisp | 3 + generate.lisp | 7 ++ generator.lisp | 136 ++++++++++++++++++++++++----- generators/gemini.lisp | 76 ++++++++++++++++ generators/gopher.lisp | 55 ++++++------ packages.lisp | 24 +++++ templates/gemini_head.tpl | 6 ++ templates/gopher_head.tpl | 2 +- 15 files changed, 546 insertions(+), 85 deletions(-)
diff --cc 1 index 0000000,0000000..86dd0aa new file mode 100644 --- /dev/null +++ b/1 @@@ -1,0 -1,0 +1,218 @@@ ++ ++(in-package :cl-yag) ++ ++;; load data from metadata and load config ++;; TODO: own file? ++ ++(defparameter *converters* '()) ++(defstruct converter name command extension) ++;; we add a converter to the list of the one availables ++(defun converter(&optional &key name command extension) ++ (setf *converters* ++ (append ++ (list name ++ (make-converter :name name ++ :command command ++ :extension extension)) ++ *converters*))) ++(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*)) ++ ++ ++(defparameter *articles* '()) ++(load "data/articles.lisp") ++(setf *articles* (reverse *articles*)) ++ ++;;;; GLOBAL VARIABLES ++ ++(defparameter *days* '("Monday" "Tuesday" "Wednesday" "Thursday" ++ "Friday" "Saturday" "Sunday")) ++(defparameter *months* '("January" "February" "March" "April" ++ "May" "June" "July" "August" "September" ++ "October" "November" "December")) ++ ++(defvar *generators* '()) ++ ++;; structure to store links ++(defstruct article title tag date id tiny author rawdate converter) ++(defstruct generator key create-site-fn) ++ ++(make-generator :key :html ++ :create-site-fn #'create-html-site) ++ ++(make-generator :key :gopher ++ :create-site-fn #'create-gopher-hole) ++ ++ ++;; put these somewhere else and automagically get them pushed to *generators* ++ ++;;;; FUNCTIONS ++ ++(require 'asdf) ++ ++;; return the day of the week ++(defun get-day-of-week(day month year) ++ (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)) ++ ++;; parse the date to ++(defun date-parse(date) ++ (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)) ++ ++;; common-lisp don't have a replace string function natively ++(defun replace-all (string part replacement &key (test #'char=)) ++ (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))) ++ ++;; common-lisp don't have a split string function natively ++(defun split-str(text &optional (separator #\Space)) ++ "this function split a string with separator and return 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)))))) ++ ++;; load a file as a string ++;; we escape ~ to avoid failures with format ++(defun load-file(path) ++ (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)))) ++ ++;; save a string in a file ++(defun save-file(path data) ++ (with-open-file (stream path :direction :output :if-exists :supersede) ++ (write-sequence data stream))) ++ ++;; simplify the str replace work ++(defmacro template(before &body after) ++ `(progn ++ (setf output (replace-all output ,before ,@after)))) ++ ++;; simplify the declaration of a new page type ++(defmacro prepare(template &body code) ++ `(progn ++ (let ((output (load-file ,template))) ++ ,@code ++ output))) ++ ++ ++;; get the converter object of "article" ++(defmacro with-converter(&body code) ++ `(progn ++ (let ((converter-name (if (article-converter article) ++ (article-converter article) ++ (getf *config* :default-converter)))) ++ (let ((converter-object (getf *converters* converter-name))) ++ ,@code)))) ++ ++;; generate the html file from the source file ++;; using the converter associated with the post ++(defun use-converter-to-html(filename &optional (converter-name nil)) ++ (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)))) ++ ++;; format the date ++(defun date-format(format date) ++ (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)) ++ ++;; xml generation of the items for the rss ++(defun generate-rss-item (fn) ++ (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)))))) ++ ++ ++;; Generate the rss xml data ++(defun generate-rss(fn) ++ (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)))) ++ ++; This is function called when running the tool ++(defun generate-site() ++ (loop for i in *generators* ++ do (if (getf *config* (getf i :key)) ++ (funcall (getf i :create-site-fn))))) ++ ++ diff --cc Makefile index 22ab1d5,bf8741e..f2e6d33 --- a/Makefile +++ b/Makefile @@@ -1,23 -1,19 +1,35 @@@ - LISP=sbcl #ecl -LISP= ecl ++# Find the first available Lisp implementation ++LISP := $(shell for lisp in ecl sbcl clisp abcl gcl cmu corman lucid lispworks allegro; do \ ++ if command -v $$lisp >/dev/null 2>&1; then \ ++ echo $$lisp; \ ++ break; \ ++ fi; \ ++done) - all: html gopher -all: dirs html ++all: html gopher gemini html: $(HTML) css + mkdir -p "output/html/static" $(LISP) --load generator.lisp -dirs: - mkdir -p "output/html/static" - mkdir -p "output/gopher" ++gemini: + mkdir -p "output/gemini/articles/" + - -clean: - rm -fr output/html/* output/gopher/* "temp" - css: mkdir -p "output/html/static" cp -fr static/* "output/html/static/" + +gopher: + mkdir -p "output/gopher" + - clean: cleanhtml cleangopher ++clean: cleanhtml cleangopher cleangemini + rm -fr "temp" + +cleanhtml: + rm -fr output/html/* + +cleangopher: - output/gopher/* ++ rm -fr output/gopher/* ++ ++cleangemini: ++ rm -fr output/gemini/* diff --cc cl-yag.ad index 0000000,0000000..1676d44 new file mode 100644 --- /dev/null +++ b/cl-yag.ad @@@ -1,0 -1,0 +1,16 @@@ ++(defpackage :cl-yag-system (:use :cl :asdf)) ++(in-package :cl-yag-system) ++ ++(defsystem cl-yag ++ :name "cl-yag" ++ :author "Solene Rapenne" ++ :version "0.1" ++ :maintainer "Spenser Truex <truex@equwal.com>" ++ :license "GNU GPLv3.0" ++ :description "Simple Static Site Generator for arbitrary files and targets." ++ :serial t ++ :components ((:file "packages") ++ (:file "generators/html") ++ (:file "generators/gopher") ++ (:file "generator") ++ (:file "data/articles"))) diff --cc config.lisp index 0000000,0000000..40e2b94 new file mode 100644 --- /dev/null +++ b/config.lisp @@@ -1,0 -1,0 +1,4 @@@ ++;; This file loads the configuration from data/articles.lisp ++;; It's separated to avoid circular dependencies when loading generators ++ ++(load "data/articles.lisp") diff --cc generate.lisp index 0000000,0000000..1e65c85 new file mode 100644 --- /dev/null +++ b/generate.lisp @@@ -1,0 -1,0 +1,7 @@@ ++(asdf:load-system :cl-yag) ++ ++;;;; EXECUTION ++ ++(generate-site) ++ ++(quit) diff --cc generator.lisp index e58a43c,c7cedfe..bd846d1 --- a/generator.lisp +++ b/generator.lisp @@@ -1,5 -1,5 +1,22 @@@ ;;;; 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" @@@ -20,18 -16,16 +37,18 @@@ (require 'asdf) -;; return the day of the week +(load "config.lisp") + - ;; return the day of the week (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)) --;; parse the date to (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)) @@@ -57,8 -51,8 +74,8 @@@ :converter converter) *articles*)) --;; we add a converter to the list of the one availables (defun converter(&optional &key name command extension) ++ "Add a converter to the list of those available." (setf *converters* (append (list name @@@ -67,12 -61,13 +84,18 @@@ :extension extension)) *converters*))) --;; load data from metadata and load config ++;; 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*)) - --;; common-lisp don't have a replace string function natively (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) @@@ -85,9 -80,9 +108,8 @@@ when pos do (write-string replacement out) while pos))) --;; common-lisp don't have a split string function natively (defun split-str(text &optional (separator #\Space)) -- "this function split a string with separator and return a list" ++ "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 @@@ -101,9 -96,9 +123,8 @@@ (if left-separator-position (+ 1 left-separator-position) 0) (- count 1)))))) --;; load a file as a string --;; we escape ~ to avoid failures with format (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)))) @@@ -113,36 -108,27 +134,34 @@@ (format t "ERROR : file ~a not found. Aborting~%" path) (quit)))) --;; save a string in a file (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))) --;; simplify the str replace work (defmacro template(before &body after) ++ "Simplify string replacement work in templates." `(progn (setf output (replace-all output ,before ,@after)))) - ;; simplify the declaration of a new page type -;; get the converter object of "article" +(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))) + + - ;; get the converter object of "article" (defmacro with-converter(&body code) ++ "Get the converter object for an article and execute code in that context." `(progn - (let ((converter-name (if (article-converter article) - (article-converter article) + (let ((converter-name (or (article-converter article) (getf *config* :default-converter)))) (let ((converter-object (getf *converters* converter-name))) ,@code)))) --;; generate the html file from the source file --;; using the converter associated with the post (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)))) @@@ -163,8 -149,8 +182,8 @@@ (format t "~a~%" output) (uiop:run-program output)))) --;; format the date (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))) @@@ -173,8 -159,132 +192,80 @@@ (template "%Year" (write-to-string (getf date :year ))) output)) - ;; xml generation of the items for the rss -;; simplify the declaration of a new page type -(defmacro prepare(template &body code) - `(progn - (let ((output (load-file ,template))) - ,@code - output))) - -;; simplify the file saving by using the layout + (defmacro generate(name &body data) ++ "Simplify file saving by using the layout system." + `(progn + (save-file ,name (generate-layout ,@data)))) + -;; generate a gemini index file -(defun generate-gemini-index(articles) - (let ((output (load-file "templates/gemini_head.tpl"))) - (dolist (article articles) - (setf output - (string - (concatenate 'string output - (format nil "=> ~a/articles/~a.gmi ~a-~2,'0d-~2,'0d ~a~%" - (getf *config* :gemini-path) - (article-id article) - (getf (article-date article) :year) - (getf (article-date article) :monthnumber) - (getf (article-date article) :daynumber) - (article-title article)))))) - output)) - -;; generate a gopher index file -(defun generate-gopher-index(articles) - (let ((output (load-file "templates/gopher_head.tpl"))) - (dolist (article articles) - (setf output - (string - (concatenate 'string output - (format nil (getf *config* :gopher-format) - 0 ;;;; gopher type, 0 for text files - ;; here we create a 80 width char string with title on the left - ;; and date on the right - ;; we truncate the article title if it's too large - (let ((title (format nil "~80a" - (if (< 80 (length (article-title article))) - (subseq (article-title article) 0 80) - (article-title article))))) - (replace title (article-rawdate article) :start1 (- (length title) (length (article-rawdate article))))) - (concatenate 'string - (getf *config* :gopher-path) "/article-" (article-id article) ".txt") - (getf *config* :gopher-server) - (getf *config* :gopher-port) - ))))) - output)) - -;; generate the list of tags + (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)))) + -;; generates the html of the list of tags for an article + (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))))) + -;; generates the html of the whole list of tags + (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)))) + - -;; generates the html of only one article -;; this is called in a loop to produce the homepage + (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)))))))) + -;; return a html string -;; produce the code of a whole page with title+layout with the parameter as the content + (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)) + - -;; html generation of index homepage + (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)))) + -;; html generation of a tag homepage + (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)))) - -;; xml generation of the items for the rss -(defun generate-rss-item(&key (gopher nil)) +(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)) @@@ -187,28 -297,190 +278,27 @@@ (article-date article)) (subseq (getf (article-date article) :dayname) 0 3) (subseq (getf (article-date article) :monthname) 0 3))) - (template "%%Url%%" - (if gopher - (format nil "gopher://~a:~d/0~a/article-~a.txt" - (getf *config* :gopher-server) - (getf *config* :gopher-port) - (getf *config* :gopher-path) - (article-id article)) - (format nil "~d~d-~d.html" - (getf *config* :url) - (date-format "%Year-%MonthNumber-%DayNumber" - (article-date article)) - (article-id article)))))))) - - -;; Generate the rss xml data -(defun generate-rss(&key (gopher nil)) + (template "%%Url%%" (funcall fn article)))))) + + - ;; Generate the rss xml data +(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 :gopher gopher)))) - -;; We do all the website -(defun create-html-site() - - ;; produce each article file - (loop for article in *articles* - do - ;; use the article's converter to get html code of it - (use-converter-to-html (article-id article) (article-converter article)) - - (generate (format nil "output/html/~d-~d.html" - (date-format "%Year-%MonthNumber-%DayNumber" - (article-date article)) - (article-id article)) - (create-article article :tiny nil) - :title (concatenate 'string (getf *config* :title) " : " (article-title article)))) - - ;; produce index.html - (generate "output/html/index.html" (generate-semi-mainpage)) - - ;; produce index-titles.html where there are only articles titles - (generate "output/html/index-titles.html" (generate-semi-mainpage :no-text t)) - - ;; produce index file for each tag - (loop for tag in (articles-by-tag) do - (generate (format nil "output/html/tag-~d.html" (getf tag :NAME)) - (generate-tag-mainpage (getf tag :VALUE)))) - - ;; generate rss gopher in html folder if gopher is t - (when (getf *config* :gopher) - (save-file "output/html/rss-gopher.xml" (generate-rss :gopher t))) - - ;;(generate-file-rss) - (save-file "output/html/rss.xml" (generate-rss))) - -;; we do all the gemini capsule -(defun create-gemini-capsule() - - ;; produce the index.gmi file - (save-file (concatenate 'string "output/gemini/" (getf *config* :gemini-index)) - (generate-gemini-index *articles*)) - - ;; produce a tag list menu - (let* ((directory-path "output/gemini/_tags_/") - (index-path (concatenate 'string directory-path (getf *config* :gemini-index)))) - (ensure-directories-exist directory-path) - (save-file index-path - (let ((output (load-file "templates/gemini_head.tpl"))) - (loop for tag in - ;; sort tags per articles in it - (sort (articles-by-tag) #'> - :key #'(lambda (x) (length (getf x :value)))) - do - (setf output - (string - (concatenate - 'string output - (format nil "=> ~a/~a/index.gmi ~a ~d~%" - (getf *config* :gemini-path) - (getf tag :name) - (getf tag :name) - (length (getf tag :value))))))) - output))) - - ;; produce each tag gemini index - (loop for tag in (articles-by-tag) do - (let* ((directory-path (concatenate 'string "output/gemini/" (getf tag :NAME) "/")) - (index-path (concatenate 'string directory-path (getf *config* :gemini-index))) - (articles-with-tag (loop for article in *articles* - when (member (article-id article) (getf tag :VALUE) :test #'equal) - collect article))) - (ensure-directories-exist directory-path) - (save-file index-path (generate-gemini-index articles-with-tag)))) - - ;; produce each article file (adding some headers) - (loop for article in *articles* - do - (with-converter - (let ((id (article-id article))) - (save-file (format nil "output/gemini/articles/~a.gmi" id) - (format nil "~{~a~}" - (list - "Title : " (article-title article) #\Newline - "Author: " (article-author article) #\Newline - "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)))))))))) - -;; we do all the gopher hole -(defun create-gopher-hole() - - ;;(generate-file-rss) - (save-file "output/gopher/rss.xml" (generate-rss :gopher t)) - - ;; produce the gophermap file - (save-file (concatenate 'string "output/gopher/" (getf *config* :gopher-index)) - (generate-gopher-index *articles*)) - - ;; produce a tag list menu - (let* ((directory-path "output/gopher/_tags_/") - (index-path (concatenate 'string directory-path (getf *config* :gopher-index)))) - (ensure-directories-exist directory-path) - (save-file index-path - (let ((output (load-file "templates/gopher_head.tpl"))) - (loop for tag in - ;; sort tags per articles in it - (sort (articles-by-tag) #'> - :key #'(lambda (x) (length (getf x :value)))) - do - (setf output - (string - (concatenate - 'string output - (format nil (getf *config* :gopher-format) - 1 ;; gopher type, 1 for menus - ;; here we create a 72 width char string with title on the left - ;; and number of articles on the right - ;; we truncate the article title if it's too large - (let ((title (format nil "~72a" - (if (< 72 (length (getf tag :NAME))) - (subseq (getf tag :NAME) 0 80) - (getf tag :NAME)))) - (article-number (format nil "~d article~p" (length (getf tag :value)) (length (getf tag :value))))) - (replace title article-number :start1 (- (length title) (length article-number)))) - (concatenate 'string - (getf *config* :gopher-path) "/" (getf tag :NAME) "/") - (getf *config* :gopher-server) - (getf *config* :gopher-port) - ))))) - output))) - - ;; produce each tag gophermap index - (loop for tag in (articles-by-tag) do - (let* ((directory-path (concatenate 'string "output/gopher/" (getf tag :NAME) "/")) - (index-path (concatenate 'string directory-path (getf *config* :gopher-index))) - (articles-with-tag (loop for article in *articles* - when (member (article-id article) (getf tag :VALUE) :test #'equal) - collect article))) - (ensure-directories-exist directory-path) - (save-file index-path (generate-gopher-index articles-with-tag)))) - - ;; produce each article file (adding some headers) - (loop for article in *articles* - do - (with-converter - (let ((id (article-id article))) - (save-file (format nil "output/gopher/article-~d.txt" id) - (format nil "Title: ~a~%Author: ~a~%Date: ~a~%Tags: ~a~%============~%~%~a" - (article-title article) - (article-author article) - (date-format (getf *config* :date-format) (article-date article)) - (article-tag article) - (load-file (format nil "data/~d~d" id (converter-extension converter-object))))))))) - - -;; This is function called when running the tool + (template "%%Items%%" (generate-rss-item fn)))) + - ; This is function called when running the tool (defun generate-site() - (if (getf *config* :html) - (create-html-site)) - (if (getf *config* :gemini) - (create-gemini-capsule)) - (if (getf *config* :gopher) - (create-gopher-hole))) ++ "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 diff --cc generators/gemini.lisp index 0000000,0000000..d9f7728 new file mode 100644 --- /dev/null +++ b/generators/gemini.lisp @@@ -1,0 -1,0 +1,76 @@@ ++(defun generate-rss-gemini(article) ++ (format nil "gemini://~a/articles/~a.gmi" ++ (getf *config* :gemini-path) ++ (article-id article))) ++ ++;; generate a gemini index file ++(defun generate-gemini-index(articles) ++ (let ((output (load-file "templates/gemini_head.tpl"))) ++ (dolist (article articles) ++ (setf output ++ (string ++ (concatenate 'string output ++ (format nil "=> ~a/articles/~a.gmi ~a-~2,'0d-~2,'0d ~a~%" ++ (getf *config* :gemini-path) ++ (article-id article) ++ (getf (article-date article) :year) ++ (getf (article-date article) :monthnumber) ++ (getf (article-date article) :daynumber) ++ (article-title article)))))) ++ output)) ++ ++;; we do all the gemini capsule ++(defun create-gemini-capsule() ++ ++ ;; produce the index.gmi file ++ (save-file (concatenate 'string "output/gemini/" (getf *config* :gemini-index)) ++ (generate-gemini-index *articles*)) ++ ++ ;; produce a tag list menu ++ (let* ((directory-path "output/gemini/_tags_/") ++ (index-path (concatenate 'string directory-path (getf *config* :gemini-index)))) ++ (ensure-directories-exist directory-path) ++ (save-file index-path ++ (let ((output (load-file "templates/gemini_head.tpl"))) ++ (loop for tag in ++ ;; sort tags per articles in it ++ (sort (articles-by-tag) #'> ++ :key #'(lambda (x) (length (getf x :value)))) ++ do ++ (setf output ++ (string ++ (concatenate ++ 'string output ++ (format nil "=> ~a/~a/index.gmi ~a ~d~%" ++ (getf *config* :gemini-path) ++ (getf tag :name) ++ (getf tag :name) ++ (length (getf tag :value))))))) ++ output))) ++ ++ ;; produce each tag gemini index ++ (loop for tag in (articles-by-tag) do ++ (let* ((directory-path (concatenate 'string "output/gemini/" (getf tag :NAME) "/")) ++ (index-path (concatenate 'string directory-path (getf *config* :gemini-index))) ++ (articles-with-tag (loop for article in *articles* ++ when (member (article-id article) (getf tag :VALUE) :test #'equal) ++ collect article))) ++ (ensure-directories-exist directory-path) ++ (save-file index-path (generate-gemini-index articles-with-tag)))) ++ ++ ;; produce each article file (adding some headers) ++ (loop for article in *articles* ++ do ++ (with-converter ++ (let ((id (article-id article))) ++ (save-file (format nil "output/gemini/articles/~a.gmi" id) ++ (format nil "~{~a~}" ++ (list ++ "Title : " (article-title article) #\Newline ++ "Author: " (article-author article) #\Newline ++ "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) diff --cc generators/gopher.lisp index c0c629c,0000000..a0c9443 mode 100644,000000..100644 --- a/generators/gopher.lisp +++ b/generators/gopher.lisp @@@ -1,102 -1,0 +1,99 @@@ +(defun generate-rss-gopher(article) + (format nil "gopher://~a:~d/0~a/article-~a.txt" + (getf *config* :gopher-server) + (getf *config* :gopher-port) + (getf *config* :gopher-path) + (article-id article))) + ++;; generate a gopher index file ++(defun generate-gopher-index(articles) ++ (let ((output (load-file "templates/gopher_head.tpl"))) ++ (dolist (article articles) ++ (setf output ++ (string ++ (concatenate 'string output ++ (format nil (getf *config* :gopher-format) ++ 0 ;;;; gopher type, 0 for text files ++ ;; here we create a 80 width char string with title on the left ++ ;; and date on the right ++ ;; we truncate the article title if it's too large ++ (let ((title (format nil "~80a" ++ (if (< 80 (length (article-title article))) ++ (subseq (article-title article) 0 80) ++ (article-title article))))) ++ (replace title (article-rawdate article) :start1 (- (length title) (length (article-rawdate article))))) ++ (concatenate 'string ++ (getf *config* :gopher-path) "/article-" (article-id article) ".txt") ++ (getf *config* :gopher-server) ++ (getf *config* :gopher-port) ++ ))))) ++ output)) ++ +;; we do all the gopher hole +(defun create-gopher-hole() + + ;;(generate-file-rss) + (save-file "output/gopher/rss.xml" (generate-rss #'generate-rss-gopher)) + + ;; produce the gophermap file + (save-file (concatenate 'string "output/gopher/" (getf *config* :gopher-index)) + (generate-gopher-index *articles*)) + + ;; produce a tag list menu + (let* ((directory-path "output/gopher/_tags_/") + (index-path (concatenate 'string directory-path (getf *config* :gopher-index)))) + (ensure-directories-exist directory-path) + (save-file index-path + (let ((output (load-file "templates/gopher_head.tpl"))) + (loop for tag in + ;; sort tags per articles in it + (sort (articles-by-tag) #'> + :key #'(lambda (x) (length (getf x :value)))) + do + (setf output + (string + (concatenate + 'string output + (format nil (getf *config* :gopher-format) + 1 ;; gopher type, 1 for menus + ;; here we create a 72 width char string with title on the left + ;; and number of articles on the right + ;; we truncate the article title if it's too large + (let ((title (format nil "~72a" + (if (< 72 (length (getf tag :NAME))) + (subseq (getf tag :NAME) 0 80) + (getf tag :NAME)))) + (article-number (format nil "~d article~p" (length (getf tag :value)) (length (getf tag :value))))) + (replace title article-number :start1 (- (length title) (length article-number)))) + (concatenate 'string + (getf *config* :gopher-path) "/" (getf tag :NAME) "/") + (getf *config* :gopher-server) + (getf *config* :gopher-port) + ))))) + output))) + - (make-generator :key :gopher - :create-site-fn #'create-gopher-hole) - + ;; produce each tag gophermap index + (loop for tag in (articles-by-tag) do + (let* ((directory-path (concatenate 'string "output/gopher/" (getf tag :NAME) "/")) + (index-path (concatenate 'string directory-path (getf *config* :gopher-index))) + (articles-with-tag (loop for article in *articles* + when (member (article-id article) (getf tag :VALUE) :test #'equal) + collect article))) + (ensure-directories-exist directory-path) + (save-file index-path (generate-gopher-index articles-with-tag)))) + + ;; produce each article file (adding some headers) + (loop for article in *articles* + do + (with-converter + (let ((id (article-id article))) + (save-file (format nil "output/gopher/article-~d.txt" id) + (format nil "Title: ~a~%Author: ~a~%Date: ~a~%Tags: ~a~%============~%~%~a" + (article-title article) + (article-author article) + (date-format (getf *config* :date-format) (article-date article)) + (article-tag article) + (load-file (format nil "data/~d~d" id (converter-extension converter-object))))))))) + - ;; generate a gopher index file - (defun generate-gopher-index(articles) - (let ((output (load-file "templates/gopher_head.tpl"))) - (dolist (article articles) - (setf output - (string - (concatenate 'string output - (format nil (getf *config* :gopher-format) - 0 ;;;; gopher type, 0 for text files - ;; here we create a 80 width char string with title on the left - ;; and date on the right - ;; we truncate the article title if it's too large - (let ((title (format nil "~80a" - (if (< 80 (length (article-title article))) - (subseq (article-title article) 0 80) - (article-title article))))) - (replace title (article-rawdate article) :start1 (- (length title) (length (article-rawdate article))))) - (concatenate 'string - (getf *config* :gopher-path) "/article-" (article-id article) ".txt") - (getf *config* :gopher-server) - (getf *config* :gopher-port) - ))))) - output)) - - - ++(make-generator :key :gopher ++ :create-site-fn #'create-gopher-hole) diff --cc packages.lisp index 0000000,0000000..a4b1fef new file mode 100644 --- /dev/null +++ b/packages.lisp @@@ -1,0 -1,0 +1,24 @@@ ++ ++(in-package :cl-yag-system) ++ ++(defpackage #:html-generator ++ (:use #:cl) ++ (:export #:create-html-site ++ #:generate-rss-html)) ++ ++(defpackage #:gopher-generator ++ (:use #:cl) ++ (:export #:create-gopher-hole ++ #:generate-rss-gopher)) ++ ++(defpackage #:cl-yag ++ (:use #:cl) ++ (:import-from #:html-generator ++ ++ #:create-html-site ++ #:generate-rss-html) ++ (:import-from #:gopher-generator ++ ++ #:create-gopher-hole ++ #:generate-rss-gopher) ++ (:export #:generate-site)) diff --cc templates/gemini_head.tpl index 0000000,8845c90..ba5afb4 mode 000000,100644..100644 --- a/templates/gemini_head.tpl +++ b/templates/gemini_head.tpl @@@ -1,0 -1,6 +1,6 @@@ -Hello, this is the head of your gophermap page, you can ++Hello, this is the head of your gemini capsule, you can + customize it how you want ! + -=> /index.md Home ++=> /index.gmi Home + ------------------------------------------------------------------- ++------------------------------------------------------------------