URI: 
       generator.lisp - cl-yag - Common Lisp Yet Another website Generator
  HTML git clone git://bitreich.org/cl-yag/ git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/cl-yag/
   DIR Log
   DIR Files
   DIR Refs
   DIR Tags
   DIR README
   DIR LICENSE
       ---
       generator.lisp (20812B)
       ---
            1 ;;;; GLOBAL VARIABLES
            2 
            3 (defparameter *articles* '())
            4 (defparameter *converters* '())
            5 (defparameter *days* '("Monday" "Tuesday" "Wednesday" "Thursday"
            6                        "Friday" "Saturday" "Sunday"))
            7 (defparameter *months* '("January" "February" "March" "April"
            8                          "May" "June" "July" "August" "September"
            9                          "October" "November" "December"))
           10 
           11 ;; structure to store links
           12 (defstruct article title tag date id tiny author rawdate converter)
           13 (defstruct converter name command extension)
           14 
           15 ;;;; FUNCTIONS
           16 
           17 (require 'asdf)
           18 
           19 ;; return the day of the week
           20 (defun get-day-of-week(day month year)
           21   (multiple-value-bind
           22    (second minute hour date month year day-of-week dst-p tz)
           23    (decode-universal-time (encode-universal-time 0 0 0 day month year))
           24    (declare (ignore second minute hour date month year dst-p tz))
           25    day-of-week))
           26 
           27 ;; parse the date to
           28 (defun date-parse(date)
           29   (if (= 8 (length date))
           30       (let* ((year     (parse-integer date :start 0 :end 4))
           31              (monthnum (parse-integer date :start 4 :end 6))
           32              (daynum   (parse-integer date :start 6 :end 8))
           33              (day      (nth (get-day-of-week daynum monthnum year) *days*))
           34              (month    (nth (- monthnum 1) *months*)))
           35         (list
           36          :dayname day
           37          :daynumber daynum
           38          :monthname month
           39          :monthnumber monthnum
           40          :year year))
           41     nil))
           42 
           43 (defun post(&optional &key title tag date id (tiny nil) (author (getf *config* :webmaster)) (converter nil))
           44   (push (make-article :title title
           45                       :tag tag
           46                       :date (date-parse date)
           47                       :rawdate date
           48                       :tiny tiny
           49                       :author author
           50                       :id id
           51                       :converter converter)
           52         *articles*))
           53 
           54 ;; we add a converter to the list of the one availables
           55 (defun converter(&optional &key name command extension)
           56   (setf *converters*
           57         (append
           58          (list name
           59                (make-converter :name name
           60                                :command command
           61                                :extension extension))
           62          *converters*)))
           63 
           64 ;; load data from metadata and load config
           65 (load "data/articles.lisp")
           66 (setf *articles* (reverse *articles*))
           67 
           68 
           69 ;; common-lisp don't have a replace string function natively
           70 (defun replace-all (string part replacement &key (test #'char=))
           71   (with-output-to-string (out)
           72     (loop with part-length = (length part)
           73        for old-pos = 0 then (+ pos part-length)
           74        for pos = (search part string
           75                          :start2 old-pos
           76                          :test test)
           77        do (write-string string out
           78                         :start old-pos
           79                         :end (or pos (length string)))
           80        when pos do (write-string replacement out)
           81        while pos)))
           82 
           83 ;; common-lisp don't have a split string function natively
           84 (defun split-str(text &optional (separator #\Space))
           85   "this function split a string with separator and return a list"
           86   (let ((text (concatenate 'string text (string separator))))
           87     (loop for char across text
           88        counting char into count
           89        when (char= char separator)
           90        collect
           91        ;; we look at the position of the left separator from right to left
           92          (let ((left-separator-position (position separator text :from-end t :end (- count 1))))
           93            (subseq text
           94                    ;; if we can't find a separator at the left of the current, then it's the start of
           95                    ;; the string
           96                    (if left-separator-position (+ 1 left-separator-position) 0)
           97                    (- count 1))))))
           98 
           99 ;; load a file as a string
          100 ;; we escape ~ to avoid failures with format
          101 (defun load-file(path)
          102   (if (probe-file path)
          103       (with-open-file (stream path)
          104         (let ((contents (make-string (file-length stream))))
          105           (read-sequence contents stream)
          106           contents))
          107     (progn
          108       (format t "ERROR : file ~a not found. Aborting~%" path)
          109       (quit))))
          110 
          111 ;; save a string in a file
          112 (defun save-file(path data)
          113   (with-open-file (stream path :direction :output :if-exists :supersede)
          114                   (write-sequence data stream)))
          115 
          116 ;; simplify the str replace work
          117 (defmacro template(before &body after)
          118   `(progn
          119      (setf output (replace-all output ,before ,@after))))
          120 
          121 ;; get the converter object of "article"
          122 (defmacro with-converter(&body code)
          123   `(progn
          124      (let ((converter-name (if (article-converter article)
          125                                (article-converter article)
          126                              (getf *config* :default-converter))))
          127        (let ((converter-object (getf *converters* converter-name)))
          128          ,@code))))
          129 
          130 ;; generate the html file from the source file
          131 ;; using the converter associated with the post
          132 (defun use-converter-to-html(filename &optional (converter-name nil))
          133   (let* ((converter-object (getf *converters*
          134                                  (or converter-name
          135                                      converter-name
          136                                      (getf *config* :default-converter))))
          137          (output           (converter-command converter-object))
          138          (src-file (format nil "~a~a" filename (converter-extension converter-object)))
          139          (dst-file (format nil "temp/data/~a.html" filename ))
          140          (full-src-file (format nil "data/~a" src-file)))
          141       ;; skip generating if the destination exists
          142       ;; and is more recent than source
          143       (unless (and
          144                (probe-file dst-file)
          145                (>=
          146                 (file-write-date dst-file)
          147                 (file-write-date full-src-file)))
          148         (ensure-directories-exist "temp/data/")
          149         (template "%IN" src-file)
          150         (template "%OUT" dst-file)
          151         (format t "~a~%" output)
          152         (uiop:run-program output))))
          153 
          154 ;; format the date
          155 (defun date-format(format date)
          156   (let ((output format))
          157     (template "%DayName"     (getf date :dayname))
          158     (template "%DayNumber"   (format nil "~2,'0d" (getf date :daynumber)))
          159     (template "%MonthName"   (getf date :monthname))
          160     (template "%MonthNumber" (format nil "~2,'0d" (getf date :monthnumber)))
          161     (template "%Year"        (write-to-string (getf date :year )))
          162     output))
          163 
          164 ;; simplify the declaration of a new page type
          165 (defmacro prepare(template &body code)
          166   `(progn
          167      (let ((output (load-file ,template)))
          168        ,@code
          169        output)))
          170 
          171 ;; simplify the file saving by using the layout
          172 (defmacro generate(name &body data)
          173   `(progn
          174      (save-file ,name (generate-layout ,@data))))
          175 
          176 ;; generate a gemini index file
          177 (defun generate-gemini-index(articles)
          178   (let ((output (load-file "templates/gemini_head.tpl")))
          179     (dolist (article articles)
          180       (setf output
          181             (string
          182              (concatenate 'string output
          183                           (format nil "=> ~a/articles/~a.gmi ~a-~2,'0d-~2,'0d ~a~%"
          184                                   (getf *config* :gemini-path)
          185                                   (article-id article)
          186                                   (getf (article-date article) :year)
          187                                   (getf (article-date article) :monthnumber)
          188                                   (getf (article-date article) :daynumber)
          189                                   (article-title article))))))
          190     output))
          191 
          192 ;; generate a gopher index file
          193 (defun generate-gopher-index(articles)
          194   (let ((output (load-file "templates/gopher_head.tpl")))
          195     (dolist (article articles)
          196       (setf output
          197             (string
          198              (concatenate 'string output
          199                           (format nil (getf *config* :gopher-format)
          200                                   0 ;;;; gopher type, 0 for text files
          201                                   ;; here we create a 80 width char string with title on the left
          202                                   ;; and date on the right
          203                                   ;; we truncate the article title if it's too large
          204                                   (let ((title (format nil "~80a"
          205                                                        (if (< 80 (length (article-title article)))
          206                                                            (subseq (article-title article) 0 80)
          207                                                            (article-title article)))))
          208                                     (replace title (article-rawdate article) :start1 (- (length title) (length (article-rawdate article)))))
          209                                   (concatenate 'string
          210                                                (getf *config* :gopher-path) "/article-" (article-id article) ".txt")
          211                                   (getf *config* :gopher-server)
          212                                   (getf *config* :gopher-port)
          213                                   )))))
          214     output))
          215 
          216 ;; generate the list of tags
          217 (defun articles-by-tag()
          218   (let ((tag-list))
          219     (loop for article in *articles* do
          220           (when (article-tag article) ;; we don't want an error if no tag
          221             (loop for tag in (split-str (article-tag article)) do ;; for each word in tag keyword
          222                   (setf (getf tag-list (intern tag "KEYWORD")) ;; we create the keyword is inexistent and add ID to :value
          223                         (list
          224                          :name tag
          225                          :value (push (article-id article) (getf (getf tag-list (intern tag "KEYWORD")) :value)))))))
          226     (loop for i from 1 to (length tag-list) by 2 collect ;; removing the keywords
          227           (nth i tag-list))))
          228 
          229 ;; generates the html of the list of tags for an article
          230 (defun get-tag-list-article(&optional article)
          231   (apply #'concatenate 'string
          232          (mapcar #'(lambda (item)
          233                      (prepare "templates/one-tag.tpl" (template "%%Name%%" item)))
          234                  (split-str (article-tag article)))))
          235 
          236 ;; generates the html of the whole list of tags
          237 (defun get-tag-list()
          238   (apply #'concatenate 'string
          239          (mapcar #'(lambda (item)
          240                      (prepare "templates/one-tag.tpl"
          241                               (template "%%Name%%" (getf item :name))))
          242                  (articles-by-tag))))
          243 
          244 
          245 ;; generates the html of only one article
          246 ;; this is called in a loop to produce the homepage
          247 (defun create-article(article &optional &key (tiny t) (no-text nil))
          248   (prepare "templates/article.tpl"
          249            (template "%%Author%%" (let ((author (article-author article)))
          250                                     (or author (getf *config* :webmaster))))
          251            (template "%%Date%%"   (date-format (getf *config* :date-format)
          252                                                (article-date article)))
          253            (template "%%Raw-Date%%" (article-rawdate article))
          254            (template "%%Title%%"  (article-title article))
          255            (template "%%Id%%"     (article-id article))
          256            (template "%%Tags%%"   (get-tag-list-article article))
          257            (template "%%Date-Url%%"  (date-format "%Year-%MonthNumber-%DayNumber"
          258                                                   (article-date article)))
          259            (template "%%Text%%"   (if no-text
          260                                       ""
          261                                       (if (and tiny (article-tiny article))
          262                                           (format nil "<p>~a</p>" (article-tiny article))
          263                                           (load-file (format nil "temp/data/~d.html" (article-id article))))))))
          264 
          265 ;; return a html string
          266 ;; produce the code of a whole page with title+layout with the parameter as the content
          267 (defun generate-layout(body &optional &key (title nil))
          268   (prepare "templates/layout.tpl"
          269            (template "%%Title%%" (if title title (getf *config* :title)))
          270            (template "%%Tags%%" (get-tag-list))
          271            (template "%%Body%%" body)
          272            output))
          273 
          274 
          275 ;; html generation of index homepage
          276 (defun generate-semi-mainpage(&key (tiny t) (no-text nil))
          277   (apply #'concatenate 'string
          278          (loop for article in *articles* collect
          279               (create-article article :tiny tiny :no-text no-text))))
          280 
          281 ;; html generation of a tag homepage
          282 (defun generate-tag-mainpage(articles-in-tag)
          283   (apply #'concatenate 'string
          284          (loop for article in *articles*
          285             when (member (article-id article) articles-in-tag :test #'equal)
          286             collect (create-article article :tiny t))))
          287 
          288 ;; xml generation of the items for the rss
          289 (defun generate-rss-item(&key (gopher nil))
          290   (apply #'concatenate 'string
          291          (loop for article in *articles*
          292             for i from 1 to (min (length *articles*) (getf *config* :rss-item-number))
          293             collect
          294               (prepare "templates/rss-item.tpl"
          295                        (template "%%Title%%" (article-title article))
          296                        (template "%%Description%%" (load-file (format nil "temp/data/~d.html" (article-id article))))
          297                        (template "%%Date%%" (format nil
          298                                                     (date-format "~a, %DayNumber ~a %Year 00:00:00 GMT"
          299                                                                  (article-date article))
          300                                                     (subseq (getf (article-date article) :dayname) 0 3)
          301                                                     (subseq (getf (article-date article) :monthname) 0 3)))
          302                        (template "%%Url%%"
          303                                  (if gopher
          304                                      (format nil "gopher://~a:~d/0~a/article-~a.txt"
          305                                              (getf *config* :gopher-server)
          306                                              (getf *config* :gopher-port)
          307                                              (getf *config* :gopher-path)
          308                                              (article-id article))
          309                                      (format nil "~d~d-~d.html"
          310                                              (getf *config* :url)
          311                                              (date-format "%Year-%MonthNumber-%DayNumber"
          312                                                           (article-date article))
          313                                              (article-id article))))))))
          314 
          315 
          316 ;; Generate the rss xml data
          317 (defun generate-rss(&key (gopher nil))
          318   (prepare "templates/rss.tpl"
          319            (template "%%Description%%" (getf *config* :description))
          320            (template "%%Title%%" (getf *config* :title))
          321            (template "%%Url%%" (getf *config* :url))
          322            (template "%%Items%%" (generate-rss-item :gopher gopher))))
          323 
          324 ;; We do all the website
          325 (defun create-html-site()
          326 
          327   ;; produce each article file
          328   (loop for article in *articles*
          329      do
          330      ;; use the article's converter to get html code of it
          331        (use-converter-to-html (article-id article) (article-converter article))
          332 
          333         (generate  (format nil "output/html/~d-~d.html"
          334                            (date-format "%Year-%MonthNumber-%DayNumber"
          335                                         (article-date article))
          336                            (article-id article))
          337                    (create-article article :tiny nil)
          338                    :title (concatenate 'string (getf *config* :title) " : " (article-title article))))
          339 
          340   ;; produce index.html
          341   (generate "output/html/index.html" (generate-semi-mainpage))
          342 
          343   ;; produce index-titles.html where there are only articles titles
          344   (generate "output/html/index-titles.html" (generate-semi-mainpage :no-text t))
          345 
          346   ;; produce index file for each tag
          347   (loop for tag in (articles-by-tag) do
          348        (generate (format nil "output/html/tag-~d.html" (getf tag :NAME))
          349                   (generate-tag-mainpage (getf tag :VALUE))))
          350 
          351   ;; generate rss gopher in html folder if gopher is t
          352   (when (getf *config* :gopher)
          353     (save-file "output/html/rss-gopher.xml" (generate-rss :gopher t)))
          354 
          355   ;;(generate-file-rss)
          356   (save-file "output/html/rss.xml" (generate-rss)))
          357 
          358 ;; we do all the gemini capsule
          359 (defun create-gemini-capsule()
          360 
          361   ;; produce the index.gmi file
          362   (save-file (concatenate 'string "output/gemini/" (getf *config* :gemini-index))
          363              (generate-gemini-index *articles*))
          364 
          365   ;; produce a tag list menu
          366   (let* ((directory-path "output/gemini/_tags_/")
          367          (index-path (concatenate 'string directory-path (getf *config* :gemini-index))))
          368     (ensure-directories-exist directory-path)
          369     (save-file index-path
          370                (let ((output (load-file "templates/gemini_head.tpl")))
          371                  (loop for tag in
          372                       ;; sort tags per articles in it
          373                       (sort (articles-by-tag) #'>
          374                             :key #'(lambda (x) (length (getf x :value))))
          375                     do
          376                       (setf output
          377                             (string
          378                              (concatenate
          379                               'string output
          380                               (format nil "=> ~a/~a/index.gmi ~a ~d~%"
          381                                       (getf *config* :gemini-path)
          382                                       (getf tag :name)
          383                                       (getf tag :name)
          384                                       (length (getf tag :value)))))))
          385                  output)))
          386 
          387   ;; produce each tag gemini index
          388   (loop for tag in (articles-by-tag) do
          389        (let* ((directory-path (concatenate 'string "output/gemini/" (getf tag :NAME) "/"))
          390               (index-path (concatenate 'string directory-path (getf *config* :gemini-index)))
          391               (articles-with-tag (loop for article in *articles*
          392                                     when (member (article-id article) (getf tag :VALUE) :test #'equal)
          393                                     collect article)))
          394          (ensure-directories-exist directory-path)
          395          (save-file index-path (generate-gemini-index articles-with-tag))))
          396 
          397   ;; produce each article file (adding some headers)
          398   (loop for article in *articles*
          399      do
          400        (with-converter
          401            (let ((id (article-id article)))
          402              (save-file (format nil "output/gemini/articles/~a.gmi" id)
          403                         (format nil "~{~a~}"
          404                                 (list
          405                                  "Title : " (article-title article) #\Newline
          406                                  "Author: " (article-author article) #\Newline
          407                                  "Date  : " (date-format (getf *config* :date-format) (article-date article)) #\Newline
          408                                  "Tags  : " (article-tag article) #\Newline #\Newline
          409                                  (load-file (format nil "data/~d~d" id (converter-extension converter-object))))))))))
          410 
          411 ;; we do all the gopher hole
          412 (defun create-gopher-hole()
          413 
          414   ;;(generate-file-rss)
          415   (save-file "output/gopher/rss.xml" (generate-rss :gopher t))
          416 
          417   ;; produce the gophermap file
          418   (save-file (concatenate 'string "output/gopher/" (getf *config* :gopher-index))
          419              (generate-gopher-index *articles*))
          420 
          421   ;; produce a tag list menu
          422   (let* ((directory-path "output/gopher/_tags_/")
          423          (index-path (concatenate 'string directory-path (getf *config* :gopher-index))))
          424     (ensure-directories-exist directory-path)
          425     (save-file index-path
          426                (let ((output (load-file "templates/gopher_head.tpl")))
          427                  (loop for tag in
          428                       ;; sort tags per articles in it
          429                       (sort (articles-by-tag) #'>
          430                             :key #'(lambda (x) (length (getf x :value))))
          431                     do
          432                       (setf output
          433                             (string
          434                              (concatenate
          435                               'string output
          436                               (format nil (getf *config* :gopher-format)
          437                                       1 ;; gopher type, 1 for menus
          438                                       ;; here we create a 72 width char string with title on the left
          439                                       ;; and number of articles on the right
          440                                       ;; we truncate the article title if it's too large
          441                                       (let ((title (format nil "~72a"
          442                                                            (if (< 72 (length (getf tag :NAME)))
          443                                                                (subseq (getf tag :NAME) 0 80)
          444                                                                (getf tag :NAME))))
          445                                             (article-number (format nil "~d article~p" (length (getf tag :value)) (length (getf tag :value)))))
          446                                         (replace title article-number :start1 (- (length title) (length article-number))))
          447                                       (concatenate 'string
          448                                                    (getf *config* :gopher-path) "/" (getf tag :NAME) "/")
          449                                       (getf *config* :gopher-server)
          450                                       (getf *config* :gopher-port)
          451                                       )))))
          452                  output)))
          453 
          454   ;; produce each tag gophermap index
          455   (loop for tag in (articles-by-tag) do
          456        (let* ((directory-path (concatenate 'string "output/gopher/" (getf tag :NAME) "/"))
          457               (index-path (concatenate 'string directory-path (getf *config* :gopher-index)))
          458               (articles-with-tag (loop for article in *articles*
          459                                     when (member (article-id article) (getf tag :VALUE) :test #'equal)
          460                                     collect article)))
          461          (ensure-directories-exist directory-path)
          462          (save-file index-path (generate-gopher-index articles-with-tag))))
          463 
          464   ;; produce each article file (adding some headers)
          465   (loop for article in *articles*
          466      do
          467        (with-converter
          468            (let ((id (article-id article)))
          469              (save-file (format nil "output/gopher/article-~d.txt" id)
          470                         (format nil "Title: ~a~%Author: ~a~%Date: ~a~%Tags: ~a~%============~%~%~a"
          471                                  (article-title article)
          472                                  (article-author article)
          473                                  (date-format (getf *config* :date-format) (article-date article))
          474                                  (article-tag article)
          475                                          (load-file (format nil "data/~d~d" id (converter-extension converter-object)))))))))
          476 
          477 
          478 ;; This is function called when running the tool
          479 (defun generate-site()
          480   (if (getf *config* :html)
          481       (create-html-site))
          482   (if (getf *config* :gemini)
          483       (create-gemini-capsule))
          484   (if (getf *config* :gopher)
          485       (create-gopher-hole)))
          486 
          487 ;;;; EXECUTION
          488 
          489 (generate-site)
          490 
          491 (quit)