Method of Generation
It's traditional for personal websites and similar publications to list their methods of generation,
the automatic processes by which they're forced into existence. Also traditionally, this is usually
one of few such articles before the entire thing becomes abandoned by its author. I prefer to avoid
such traditions, but one reader recently asked about my method, and so I finally document it herein.
I'd like to improve my process sometime, but such improvements will be mentioned in a later article.
I originally used a manual process, and still do for the articles themselves, but have added a minor
touch of automation where easy. Each article's Gopher version is written first, as I write in a way
where almost all lines measure at exactly one hundred columns, sans title lines and whatnot, which I
recently learned to be known as ``bricktext'' to some; this occurred to me when I manually justified
the articles written before I provided a Gopher service, as I realized I could avoid the need for it
through careful word choice, and I now write to force entire paragraphs to be perfect rectangles, as
it became too easy otherwise. To prepare the WWW versions of each article, I cannibalize formatting
from a previous article and use simple recorded input macros to place the text in paragraph tags all
on one line. This makes unique formatting for a page nearly as convenient as otherwise, although it
typically leads me to a dearth of what pass for hyperlinks on the WWW, which I may eventually amend.
It's easy to prove the correctness of a process, for some values of correctness, when one has a pool
of known questions and answers. I'd like to automate that WWW page generation, and it would suffice
to write a program I believe correct to watch it generate pages identical to what I've made by hand,
leading either to tweaking the program or the pages upon inequality. This is only reasonable for my
most common page type, a simple article with no special formatting, with extra formatting or changes
to the styling feasible to spread to all such pages if and when some such program exists. I believe
I may write special programs for the unusual pages, but certain unusual pages deserve no such thing.
Certain unusual pages, however, are changed so often as to become bothersome to change manually over
time, and I've automated their generation. These are the prime indices, the RSS feed, and the blank
pages for comments. As HTML is a shitty textual format, a shitty textual program suits it. I could
use one of the Common Lisp HTML generation libraries, but that would be overkill; escaping and other
nonsense becomes unnecessary when controlling the domain, and I manually escape the few titles which
can otherwise cause issues alongside running text replacement over the text of articles when needed.
I now include bits and pieces of my Common Lisp program used for generation here, ignoring licenses.
Note the program looks better on my side, because I bother not to keep lines at a reasonable length:
(defparameter www-index-header "
An Index of Verisimilitudes")
(defparameter www-index-footer "
")
(defun generate-prime-www-index (list)
(with-output-to-string (*standard-output*)
(format t www-index-header) ;WRITE-STRING is here and there in my version for header and footer.
(loop :for (date title) :in list
:doing (write-string "
"))
(format t www-index-footer)))
(defparameter rss-template "http:/~
/verisimilitudes.net/crest.pngCresthttp://verisimilitudes.netA Syndication of Verisimilitudeshttp://verisimilitudes.netThese are articles I've written concerning topics that interest me, largely related to my progr~
amming works.en-uscomments@verisimilitudes.net~
managingEditor>720~{~1{~1@*~Ahttp://verisimilitudes.net/~0@*~
~Ahttp://verisimilitudes.net/~0@*~A-comments~0@*~A ~0@*~A~}~}")
(defun cl-user::get-date-by-format (*standard-output* &rest ignore)
(declare (ignorable ignore)) ;This function could write different answers, at the end of the day.
(format t "~1{~4D-~2,'0D-~2,'0D~}" (cdddr (nreverse (multiple-value-list (get-decoded-time))))))
(defparameter rss-update-template "~{~{Updated: ~*~Ahttp://verisimilitud~
es.net/~0@*~A~:*http://verisimilitudes.net/~A~:*-comments~A ~/CL-USER::GET-DATE-BY-FORMAT/~}~}")
(defparameter comments-template "Comments for ~A
")
(defun generate-prime-gopher-menu (list)
(with-output-to-string (*standard-output*)
(destructuring-bind (type selector ignore title) (car list)
(declare (ignorable ignore))
(write-char (ecase type (:menu #\1) (:text #\0)))
(write-string title)
(write-char #\Tab)
(write-string selector)
(write-char #\Tab)
(write-string "verisimilitudes.net")
(write-char #\Tab)
(write-string "70")
(write-char #\Return)
(write-char #\Linefeed))
(loop :for (type date ignore title) :in (cdr list)
:doing (write-char (ecase type (:menu #\1) (:text #\0)))
(write-string date)
(write-char #\Space)
(write-string title)
(write-char #\Tab)
(write-string date)
(write-char #\Tab)
(write-string "verisimilitudes.net")
(write-char #\Tab)
(write-string "70")
(write-char #\Return)
(write-char #\Linefeed)
:finally (write-char #\.)
(write-char #\Return)
(write-char #\Linefeed))))
(defun generate-gopher-comments-menu (list)
(with-output-to-string (*standard-output*)
(loop :for (type date comment title) :in list
:doing (write-char #\0)
(write-string date)
(write-char (cdr (assoc comment '((* . #\*) (- . #\ )))))
(write-string title)
(write-char #\Tab)
(write-string date)
(write-string "-comments")
(write-char #\Tab)
(write-string "verisimilitudes.net")
(write-char #\Tab)
(write-string "70")
(write-char #\Return)
(write-char #\Linefeed)
:finally (write-char #\.)
(write-char #\Return)
(write-char #\Linefeed))))
It's naturally necessary to have a data structure hold the information for such functions, and I got
tired of looking at my code to read in lists from files; I reminded myself that Common Lisp provides
a standard mechanism for having something look like a variable but that actually runs arbitrary code
upon access, symbol macros. I never made SETF methods for them, but these have the code much nicer:
(define-symbol-macro www-prime
(with-open-file (*standard-input* (make-pathname :name "www-prime" :directory '(:absolute ...)))
(let (*read-eval*) (read))))
(define-symbol-macro gopher-prime
(with-open-file (*standard-input* (make-pathname :name "gopher-prime"
:directory '(:absolute ...)))
(let (*read-eval*) (read))))
There are two symbol macros instead of one for a simple reason: My Gopher map generation needs more,
and different, information than the WWW page generation. This is easy enough to correct by defining
WWW-PRIME in terms of GOPHER-PRIME; however, there's a pair of articles with different names on both
sides, 2018-02-22 and 2020-05-15, so I never bothered with this and instead updated both, until now.
Here are the first ten entries of WWW-PRIME and the first eleven of GOPHER-PRIME, for demonstration:
(("2017-02-02" "The Masturbation Language")
("2017-05-07" "Insidious Optimizations I: Machine Architecture")
("2017-05-27" "Code of Conduct Keeping")
("2017-06-06" "Experimentations with 33554432 and Meta-CHIP-8")
("2017-07-07" "The Meta-Machine Code (MMC) Tool")
("2017-12-30" "The SHUT-IT-DOWN Common Lisp Library")
("2017-12-31" "The CL-ECMA-48 Common Lisp Library")
("2018-01-01" "The Meta-Machine Code Interface Concept")
("2018-02-02" "The Meta-Machine Code Targeted at MIPS: The Operating System Infrastructure")
("2018-02-22" "Website Affiliations"))
((:MENU "prime" - "A Gopher Hole of Verisimilitudes")
(:MENU "2017-02-02" * "The Masturbation Language")
(:TEXT "2017-05-07" * "Insidious Optimizations I: Machine Architecture")
(:TEXT "2017-05-27" - "Code of Conduct Keeping")
(:TEXT "2017-06-06" - "Experimentations with 33554432 and Meta-CHIP-8")
(:MENU "2017-07-07" - "The Meta-Machine Code (MMC) Tool")
(:MENU "2017-12-30" - "The SHUT-IT-DOWN Common Lisp Library")
(:MENU "2017-12-31" - "The CL-ECMA-48 Common Lisp Library")
(:TEXT "2018-01-01" - "The Meta-Machine Code Interface Concept")
(:MENU "2018-02-02" -
"The Meta-Machine Code Targeted at MIPS: The Operating System Infrastructure")
(:MENU "2018-02-22" - "Gopher Affiliations"))
I now maintain but one file, PRIME, with the following form, and following symbol macro definitions:
((:MENU "prime" - "A Gopher Hole of Verisimilitudes")
(:MENU "2017-02-02" * "The Masturbation Language")
(:TEXT "2017-05-07" * "Insidious Optimizations I: Machine Architecture")
(:TEXT "2017-05-27" - "Code of Conduct Keeping")
(:TEXT "2017-06-06" - "Experimentations with 33554432 and Meta-CHIP-8")
(:MENU "2017-07-07" - "The Meta-Machine Code (MMC) Tool")
(:MENU "2017-12-30" - "The SHUT-IT-DOWN Common Lisp Library")
(:MENU "2017-12-31" - "The CL-ECMA-48 Common Lisp Library")
(:TEXT "2018-01-01" - "The Meta-Machine Code Interface Concept")
(:MENU "2018-02-02" -
"The Meta-Machine Code Targeted at MIPS: The Operating System Infrastructure")
(:MENU "2018-02-22" - (:WWW "Website Affiliations" :GOPHER "Gopher Affiliations")))
(define-symbol-macro www-prime
(with-open-file (*standard-input* (make-pathname :name "prime" :directory '(:absolute ...)))
(let* (*read-eval* (list (read)))
(loop :for (type date comment title) :in (cdr list)
:for elt := (etypecase title
(string title)
(list (getf title :www)))
:collect (list date elt)))))
(define-symbol-macro gopher-prime
(with-open-file (*standard-input* (make-pathname :name "prime" :directory '(:absolute ...)))
(let* (*read-eval* (list (read)))
(loop :for (type date comment title) :in list
:for elt := (etypecase title
(string title)
(list (getf title :gopher)))
:collect (list type date comment elt)))))
There's no error checking in these symbol macros because it would be overkill; it's easier for me to
ensure the file has its correct form than to cover every possible mistake with interactive restarts.
I wrote a GENERATE-EVERYTHING function but continue to use this expression for regeneration instead:
(progn (with-open-file (*standard-output* (make-pathname :name "index" :type "html"
:directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(write-string (generate-prime-www-index www-prime)))
(with-open-file (*standard-output* (make-pathname :name "map" :directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(write-string (generate-prime-gopher-menu gopher-prime)))
(with-open-file (*standard-output* (make-pathname :name "rss" :type "xml"
:directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(format t rss-template (last www-prime 8)) ;The following empty string is changed to taste.
(format t rss-update-template (list (find "" www-prime :key 'car :test 'string=)))
(write-string ""))
(with-open-file (*standard-output* (make-pathname :name "comments"
:directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(write-string (generate-gopher-comments-menu (cdr gopher-prime))))
(with-open-file (*standard-output* (make-pathname :name (concatenate 'string
(caar (last www-prime))
"-comments")
:directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(format t comments-template (caar (last www-prime))))
(with-open-file (*standard-output* (make-pathname :name (concatenate 'string
(cadar (last
gopher-prime))
"-comments")
:directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(format t "Comments for ~A:~C~C.~C~C" (cadar (last gopher-prime))
#\Return #\Linefeed #\Return #\Linefeed)))
I used to use GNU Emacs' Tramp mode to upload articles directly to the server, but haven't been able
to do so in years, for stupid reasons. Instead, I transfer them to a medium and upload them through
SCP, and I update infrequently enough that this bothers me little enough. I've recently developed a
need to execute code on that server as well, to generate the symbolic links used for the alternative
WWW mapping; I used the following code to generate the initial mapping, to be executed in GNU Emacs:
(with-open-file (*standard-output* (make-pathname :name "symlink-elisp" :directory '(:absolute ...))
:direction :output :if-exists :supersede
:if-does-not-exist :create)
(let (*print-circle* (*print-case* :downcase))
(print `(progn ,@(loop :for count :from 1 :for (date title) :in www-prime
:collecting `(make-symbolic-link ,date ,(format nil "~D" count) nil))))))
More recently, I just use SSH to login and manually create this symbolic link, which is less effort.
.