Here I’ll post notes about Quicklisp projects. Also I published them
on Twitter account svetlyak40wt. Project on twitter is closed, and moved to YouTube https://www.youtube.com/@40Ants and PeerTube: https://diode.zone/c/40ants!
- 03 December - codex (twitter)
- 12 December - cl-gendoc (twitter)
- 20 December - eazy-documentation (twitter)
- 26 December - cl-api (twitter)
- 31 December - atdoc (twitter)
- 01 November - sphinxcontrib-cldomain (twitter)
- 05 November - coo (twitter)
- 09 November - staple (twitter)
- 29 November - mgl-pax (twitter)
- 02 October - dynamic-mixins (twitter)
- 05 October - trivial-thumbnail (twitter)
- 08 October - lmdb (twitter)
- 10 October - cl-cont (twitter)
- 12 October - tesseract-capi (twitter)
- 14 October - font-discovery (twitter)
- 16 October - quickfork (twitter)
- 19 October - pzmq (twitter)
- 23 October - parseq (twitter)
- 26 October - cl-async-await (twitter)
- 28 October - cl-pdf (twitter)
- 01 September - geneva (twitter)
- 03 September - scriba (twitter)
- 04 September - conduit-packages (twitter)
- 05 September - global-vars (twitter)
- 06 September - trivial-left-pad (twitter)
- 07 September - cl-vcr (twitter)
- 08 September - fare-memoization (twitter)
- 09 September - function-cache (twitter)
- 10 September - vcr (twitter)
- 11 September - secret-values (twitter)
- 12 September - cl-info (twitter)
- 13 September - zenekindarl (twitter)
- 14 September - spinneret (twitter)
- 15 September - cl-mustache (twitter)
- 16 September - djula (twitter)
- 17 September - cl-emb (twitter)
- 18 September - eco (twitter)
- 21 September - persistent-variables (twitter)
- 23 September - trivial-timeout (twitter)
- 25 September - which (twitter)
- 27 September - common-lisp-jupyter (twitter)
- 29 September - place-modifiers (twitter)
- 01 August - modf (twitter)
- 02 August - cl-stream (twitter)
- 03 August - cl-speedy-queue (twitter)
- 04 August - deeds (twitter)
- 05 August - cl-qprint (twitter)
- 06 August - packet (twitter)
- 07 August - macrodynamics (twitter)
- 08 August - dufy (twitter)
- 09 August - trivial-features (twitter)
- 10 August - simple-rgb (twitter)
- 11 August - cl-json-pointer (twitter)
- 12 August - glacier (twitter)
- 13 August - moira (twitter)
- 14 August - cl-svg (twitter)
- 15 August - shuffletron (twitter)
- 16 August - trivial-string-template (twitter)
- 17 August - simple-tasks (twitter)
- 18 August - lyrics (twitter)
- 19 August - breeze (twitter)
- 20 August - cl-flat-tree (twitter)
- 21 August - texp (twitter)
- 22 August - snakes (twitter)
- 23 August - portable-threads (twitter)
- 26 August - dynamic-classes (twitter)
- 27 August - lazy (twitter)
- 28 August - temporal-functions (twitter)
- 29 August - list-named-class (twitter)
- 30 August - smug (twitter)
- 31 August - cl-ltsv (twitter)
- 01 July - clack-pretend (twitter)
- 02 July - cl-tui (twitter)
- 03 July - bourbaki (twitter)
- 04 July - make-hash (twitter)
- 05 July - cl-skip-list (twitter)
- 06 July - path-parse (twitter)
- 07 July - cl-coveralls (twitter)
- 08 July - cl-spark (twitter)
- 09 July - lisp-critic (twitter)
- 10 July - with-output-to-stream (twitter)
- 11 July - chameleon (twitter)
- 12 July - trivial-benchmark (twitter)
- 13 July - trivial-with-current-source-form (twitter)
- 14 July - cl-events (twitter)
- 15 July - piping (twitter)
- 16 July - cl-irc (twitter)
- 17 July - open-location-code (twitter)
- 18 July - taglib (twitter)
- 19 July - log4cl (twitter)
- 20 July - log4cl-extras (twitter)
- 21 July - thread.comm.rendezvous (twitter)
- 22 July - freebsd-sysctl (twitter)
- 23 July - papyrus (twitter)
- 24 July - cl-mechanize (twitter)
- 25 July - cl-flow (twitter)
- 26 July - cl-difflib (twitter)
- 27 July - cl-html-diff (twitter)
- 28 July - scriptl (twitter)
- 29 July - utilities.print-items (twitter)
- 30 July - crypto-shortcuts (twitter)
- 31 July - graylex (twitter)
- 01 June - pythonic-string-reader (twitter)
- 02 June - cl-cron (twitter)
- 03 June - group-by (twitter)
- 04 June - lambda-fiddle (twitter)
- 05 June - defmain (twitter)
- 06 June - defclass-std (twitter)
- 07 June - lime (twitter)
- 08 June - lparallel (twitter)
- 09 June - lfarm (twitter)
- 10 June - cl-gearman (twitter)
- 11 June - cl-reddit (twitter)
- 12 June - sblint (twitter)
- 13 June - cl-store (twitter)
- 14 June - teddy (twitter)
- 15 June - the-cost-of-nothing (twitter)
- 16 June - prbs (twitter)
- 17 June - lass-flexbox (twitter)
- 18 June - trivial-tco (twitter)
- 19 June - pcall (twitter)
- 20 June - clack (twitter)
- 21 June - lack-middleware-accesslog (twitter)
- 22 June - lack (twitter)
- 23 June - lack-middleware-backtrace (twitter)
- 24 June - lack-middleware-mount (twitter)
- 25 June - lack-middleware-static (twitter)
- 26 June - lack-middleware-auth-basic (twitter)
- 27 June - lack-middleware-session (twitter)
- 28 June - lack-middleware-csrf (twitter)
- 29 June - clack-errors (twitter)
- 30 June - clack-static-asset-middleware (twitter)
- 01 May - cl-heredoc (twitter)
- 02 May - trivial-indent (twitter)
- 03 May - pretty-function (twitter)
- 04 May - atomics (twitter)
- 05 May - cl-ncurses (twitter)
- 06 May - clawk (twitter)
- 07 May - cl-charms (twitter)
- 08 May - magic-ed (twitter)
- 09 May - asdf-viz (twitter)
- 10 May - xml-emitter (twitter)
- 11 May - rutils (twitter)
- 12 May - str (twitter)
- 13 May - parse-declarations (twitter)
- 14 May - cl-utilities (twitter)
- 15 May - osicat (twitter)
- 16 May - split-sequence (twitter)
- 17 May - parenscript (twitter)
- 18 May - plump (twitter)
- 19 May - clss (twitter)
- 20 May - lquery (twitter)
- 21 May - cl-who (twitter)
- 22 May - cl-change-case (twitter)
- 23 May - named-readtables (twitter)
- 24 May - trivial-ssh (twitter)
- 25 May - cl-collider (twitter)
- 26 May - jose (twitter)
- 27 May - cl-ppcre-unicode (twitter)
- 28 May - data-table (twitter)
- 29 May - assoc-utils (twitter)
- 30 May - cl-ascii-table (twitter)
- 31 May - simplified-types (twitter)
- 01 April - april (twitter)
- 02 April - periods (twitter)
- 03 April - golden-utils (twitter)
- 04 April - cl-conspack (twitter)
- 05 April - skippy (twitter)
- 06 April - skippy-renderer & zpng (twitter)
- 07 April - lispqr (twitter)
- 08 April - buildnode (twitter)
- 09 April - trivialib.type-unify (twitter)
- 10 April - cl-progress-bar (twitter)
- 11 April - trivial-mmap (twitter)
- 12 April - mtlisp (twitter)
- 13 April - deploy (twitter)
- 14 April - asdf-linguist (twitter)
- 15 April - com.google.base (twitter)
- 16 April - asdf-finalizers (twitter)
- 17 April - bodge-blobs-support (twitter)
- 18 April - trivial-package-manager (twitter)
- 19 April - hu.dwim.asdf (twitter)
- 20 April - hu.dwim.walker (twitter)
- 21 April - cl-lexer (twitter)
- 22 April - arrows (twitter)
- 23 April - constantfold (twitter)
- 24 April - find-port (twitter)
- 25 April - print-html (twitter)
- 26 April - literate-lisp (twitter)
- 27 April - should-test (twitter)
- 28 April - beast (twitter)
- 29 April - cl-locale (twitter)
- 30 April - hu.dwim.def (twitter)
- 07 March – cl-mpg123 (twitter)
- 08 March – rate-monotonic (twitter)
- 09 March – cl-custom-hash-table (twitter)
- 10 March - secure-random (twitter)
- 11 March - physical-quantities (twitter)
- 12 March - simple-inferiors (twitter)
- 13 March - unix-opts (twitter)
- 14 March - access (twitter)
- 15 March - re (twitter)
- 16 March - random-sample (twitter)
- 17 March - ppath (twitter)
- 18 March - pango-markup (twitter)
- 19 March - cl-pack (twitter)
- 20 March - doplus (twitter)
- 21 March - cserial-port (twitter)
- 22 March - cl-bert (twitter)
- 23 March - kmrcl (twitter)
- 24 March - cl-org-mode (twitter)
- 25 March - cl-bootstrap (twitter)
- 26 March - archive (twitter)
- 27 March - cl-hash-table-destructuring (twitter)
- 28 March - lass (twitter)
- 29 March - bubble-operator-upwards (twitter)
- 30 March - softdrink (twitter)
- 31 March - chronicity (twitter)
First of all, we need to define a package for our code:
(defpackage #:poftheday
(:use #:cl)
(:import-from #:rutils
#:iter
#:with
#:fmt)
(:export
#:choose))
(in-package poftheday)
Then a function to select random project among all projects, provided by Quicklisp. Quicklisp client call them “releases”.
(defun choose ()
(let ((published (find-published-systems)))
(flet ((is-published (release)
(loop for system-file in (ql::system-files release)
for system-name = (str:replace-all ".asd" "" system-file)
when (member system-name published :test #'string-equal)
do (return-from is-published t))))
(let* ((releases (ql::provided-releases t))
(non-published (remove-if #'is-published releases))
(idx (random (length non-published)))
(release (nth idx non-published)))
(values
(ql::project-name release)
(ql::system-files release))))))
By the way, this function will choose all projects from all installed Quicklisp distributions. You can have many of them:
CL-USER> (ql-dist:install-dist "http://dist.ultralisp.org/"
:prompt nil)
CL-USER> (ql-dist:all-dists)
(#<QL-DIST:DIST quicklisp 2019-08-13> #<QL-DIST:DIST ultralisp 20200307123509>)
To make randomizer choose different packages after Lisp restart, we need to initialize it:
(setf *random-state*
(make-random-state t))
First, we need to read walk all org-mode files in folder “content”. We will keep a relative path pointing to the file and parse this file with cl-org-mode:
(defclass file ()
((filename :initarg :filename
:type string
:documentation "A relative path to the source org-mode file."
:reader get-filename)
(root :initarg :root
:documentation "Parsed org-mode document, root node."
:reader get-root)))
(defmethod print-object ((file file) stream)
(print-unreadable-object (file stream :type t)
(format stream "~A" (get-filename file))))
(defun read-files ()
(uiop:while-collecting (collect)
(flet ((org-mode-p (name)
(string-equal (pathname-type name)
"org"))
(make-file (filename)
(collect
(let ((relative-filename
(ppath:relpath (pathname-to-string filename)
"content/")))
(make-instance 'file
:filename relative-filename
:root (cl-org-mode::read-org-file filename))))))
(cl-fad:walk-directory "content/"
#'make-file
:test #'org-mode-p))))
For each page we need a skeleton with header, footer and necessary Bootstrap styles.
With “cl-who” easiest way to create template is to use lisp macro like that:
(eval-when (:compile-toplevel :load-toplevel :execute)
(defparameter *google-code* "
<!-- Google tag (gtag.js) -->
<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-FL71WXK73K\"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-FL71WXK73K');
</script>
")
(defparameter *yandex-metrika-code* "
<!-- Yandex.Metrika counter -->
<script type=\"text/javascript\" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, \"script\", \"https://mc.yandex.ru/metrika/tag.js\", \"ym\");
ym(42462884, \"init\", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
</script>
<noscript><div><img src=\"https://mc.yandex.ru/watch/42462884\" style=\"position:absolute ; left:-9999px;\" alt=\"\" /></div></noscript>
<!-- /Yandex.Metrika counter -->
"))
(defvar *index-uri* nil
"This is a path to the site's top level. When it is nil, consider we are on the front page.")
(defun construct-uri (uri &rest args)
(if *index-uri*
(concatenate 'string
*index-uri*
(apply #'rutils:fmt uri args))
(apply #'rutils:fmt uri args)))
(defmacro app-page ((stream &key title index-uri (site-title "Lisp Project of the Day")) &body body)
`(let ((*index-uri* ,index-uri))
(cl-who:with-html-output (*standard-output* ,stream :prologue t :indent t)
(:html :lang "en"
(:head
(:meta :charset "utf-8")
,@(when title
`((:title (cl-who:esc ,title))))
(:link :rel "alternate"
:href "https://40ants.com/lisp-project-of-the-day/rss.xml"
:type "application/rss+xml")
(:meta :name "viewport"
:content "width=device-width, initial-scale=1")
*google-code*
*yandex-metrika-code*
(:link
:type "text/css"
:rel "stylesheet"
:href ,cl-bootstrap:*bootstrap-css-url*)
(:script :src ,cl-bootstrap:*jquery-url*)
(:script :src ,cl-bootstrap:*bootstrap-js-url*)
(:link :rel "stylesheet"
:href "../../highlight/styles/tomorrow-night.css")
(:script :src "../../highlight/highlight.pack.js")
(:script "hljs.initHighlightingOnLoad() ;")
(:style "
.tags .label {
margin-right: 1em;
}
.posts tr {
line-height: 1.7em;
}
.posts tr td.number {
font-weight: bold;
padding-right: 0.7em;
}
.posts tr td.tags {
padding-left: 0.7em;
}
h1 .tags {
font-size: 1.2rem;
position: relative;
left: 1.5rem;
top: -1.5rem;
}
.tags a {
text-decoration: none;
}
"))
(:body
(cl-bootstrap:bs-container ()
(cl-bootstrap:bs-row
(:a :href "https://40ants.com/lisp-project-of-the-day/rss.xml"
:style "display: block; float: right;"
(:img :alt "RSS Feed"
:src "https://40ants.com/lisp-project-of-the-day/media/images/rss.png"))
(:header
(:h1 :style "text-align: center"
(if ,index-uri
(cl-who:htm
(:a :href (rutils:fmt "~Aindex.html" ,index-uri)
(cl-who:esc ,site-title)))
(cl-who:esc ,site-title)))
,@(when title
`((:h2 :style "text-align: center"
(cl-who:esc ,title)))))
(cl-bootstrap:bs-col-md ()
(:center
(:h3 "You can support this project by donating at:")
(:a :href "https://www.patreon.com/bePatron?u=33868637"
(:img :alt "Donate using Patreon"
:src "https://40ants.com/lisp-project-of-the-day/media/images/patreon-btn.png"
:width "160"))
(:a :href "https://liberapay.com/poftheday/donate"
(:img :alt "Donate using Liberapay"
:src "https://liberapay.com/assets/widgets/donate.svg"))
(:p "Or see "
(:a :href "https://40ants.com/lisp-project-of-the-day/patrons/index.html"
"the list of project sponsors")
"."))
,@body))
(:div
(:hr)
(:center
(:p (cl-who:str "Brought to you by 40Ants under ")
(:a :rel "license"
:href "http://creativecommons.org/licenses/by-sa/4.0/"
(:img :alt "Creative Commons License"
:style "border-width:0"
:src "https://i.creativecommons.org/l/by-sa/4.0/88x31.png")))))))))))
When source files are collected, we need to render them to HTML inside the “docs” folder. Github will use content of this folder, to serve the site at http://40ants.com/lisp-project-of-the-day/
To render the page, we need to extract a title from the first outline node of org-mode file:
(defun remove-tags (title)
(cl-ppcre:regex-replace-all " *:.*:$" title ""))
(defun extract-tags (title)
(declare (type simple-string title))
(when (find #\: title :test #'char=)
(mapcar (alexandria:curry #'str:replace-all "_" "-")
(str:split #\:
(cl-ppcre:regex-replace-all ".*?:(.*):$" title "\\1")))))
(defun get-title (file)
;; Title can ends with tags, we need to extract them
;; and return as a second value.
(let ((full-title (cl-org-mode::node.heading
(cl-org-mode::node.next-node
(get-root file)))))
(values (remove-tags full-title)
(extract-tags full-title))))
I’ll need to render HTML in two modes. First one - for the web page, and second - for RSS feed. For RSS feed I need to omit the first H1 header and a table of properties.
(defvar *rss-mode* nil)
Org mode file can contain nodes of different types, we will render them using this generic function:
(defgeneric render-node (node stream)
(:documentation "Renders org-mode node into the HTML stream"))
Outline node contains a header of a section and should be rendered as H1, H2, etc:
(defmethod render-node ((node cl-org-mode::outline-node) stream)
(cl-who:with-html-output (stream)
;; First node is a title
(with ((level (1- (length (cl-org-mode::node.heading-level-indicator node))))
(full-title (cl-org-mode::node.heading node))
(title (remove-tags full-title)))
(ecase level
(1 (unless *rss-mode*
(cl-who:htm
(:h1 (cl-who:esc title)
(:span :class "tags"
(loop for tag in (extract-tags full-title)
do (cl-who:htm
(:a :href (construct-uri "tags/~A.html" tag)
(cl-bootstrap:bs-label ()
(cl-who:esc tag))))))))))
(2 (cl-who:htm
(:h2 (cl-who:esc title))))
(3 (cl-who:htm
(:h3 (cl-who:esc title)))))))
(call-render-for-all-children node stream))
First outline of the article can have properties. These properties describe the state of the project, if it has documentation, how active it is, etc. These properties have grades:
- :) everything is good
- :| means, for example, that documentation exists as a short readme and dont cover all functionality
- :( the project lack of this category at all.
Also, we’ll transform links into proper HTML nodes.
(defun autolink (text)
(cond
((str:starts-with-p "http" text)
(format nil "<a href=\"~A\">~A</a>" text text))
(t text)))
(defun smile->unicode (text)
(arrows:->>
text
(str:replace-all ":)" "😀")
(str:replace-all ":|" "🤨")
(str:replace-all ":(" "🥺")))
;; This method was removed from cl-org-mode at some moment :(
(defmethod cl-org-mode::node.children ((node CL-ORG-MODE::TEXT-NODE))
nil)
(defmethod render-node ((node cl-org-mode::properties-node) stream)
(unless *rss-mode*
(cl-who:with-html-output (stream)
(:table :style "position: relative; float: right; background-color: #F1F1F1; padding: 1em; margin-left: 1em; margin-bottom: 1em; border: 1px solid #D1D1D1;"
(mapcar
(lambda (item)
(render-node item stream))
(cl-org-mode::node.children node))))))
(defmethod render-node ((node cl-org-mode::property-node) stream)
(cl-who:with-html-output (stream)
(:tr
(:td :style "padding-left: 0.5rem; padding-right: 0.5rem"
(cl-who:esc
(cl-org-mode::property-node.property node)))
(:td :style "padding-left: 0.5rem; padding-right: 0.5rem; border-left: 1px solid #DDD"
(cl-who:str
(autolink
(smile->unicode
(cl-org-mode::property-node.value node))))))))
Text node contains code snippets, we need to wrap them into <code> tags and add a syntax highlighting:
(defmethod render-node ((node cl-org-mode::src-node) stream)
(let ((mode (str:trim (cl-org-mode::node.emacs-mode node)))
(text (str:trim (cl-org-mode::node.text node))))
(cond
((and (str:starts-with-p "html " mode)
(str:containsp ":render-without-code" mode))
(cl-who:with-html-output (stream)
(cl-who:str text)))
((and (str:starts-with-p "html " mode)
(str:containsp ":render" mode))
(cl-who:with-html-output (stream)
(:h4 "Code")
(:pre
(:code :class mode
(cl-who:esc text))))
(cl-who:with-html-output (stream)
(:h4 "Result")
(cl-who:str text)))
(t
(cl-who:with-html-output (stream)
(:pre
(:code :class mode
(cl-who:esc text))))))))
(defmethod render-node ((node cl-org-mode::closing-delimiter-node) stream)
;; Closing delimiters for source code blocks should be ignored.
)
In text node we need to process paragraphs, links, images and quotes. We will use a separate function to process text like this:
Today’s Common Lisp project of the Day is: rate-monotonic.
It is a periodic thread scheduler inspired by RTEMS:
into HTML:
<p>Today’s Common Lisp project of the Day is: rate-monotonic.</p>
<p>It is a periodic thread scheduler inspired by RTEMS:</p>
<a href=”http://quickdocs.org/rate-monotonic/”>http://quickdocs.org/rate-monotonic/</a>
To do this, we’ll write a simple state machine, which will read text line by line and wrap it’s pieces in appropriate HTML tags:
(defun replace-images (text)
(cl-ppcre:regex-replace-all
"\\[\\[(.*?\\.(png|jpg|gif))\\]\\]"
text
"<img style=\"max-width: 100%\" src=\"\\1\"/>"))
(defun replace-links (text)
(cl-ppcre:regex-replace-all
"\\[\\[(.*?)\\]\\[(.*?)\\]\\]"
text
"<a href=\"\\1\">\\2</a>"))
(defun replace-raw-urls (text)
(cl-ppcre:regex-replace-all
"(^| )(https?://.*?)[,.!]?( |$)"
text
"\\1<a href=\"\\2\">\\2</a>\\3"))
(defun replace-inline-code (text)
(cl-ppcre:regex-replace-all
"~(.*?)~"
text
"<code>\\1</code>"))
(defun replace-org-mode-markup-with-html (text)
(replace-inline-code
(replace-raw-urls
(replace-links
(replace-images
text)))))
(defun render-text (text stream)
(let ((buffer nil)
(reading-quote nil)
(reading-list nil))
(labels
((write-paragraph ()
(cl-who:with-html-output (stream)
(:p (cl-who:str
;; Here we don't escape the text, because
;; it is from trusted source and will contain
;; links to the images
(replace-org-mode-markup-with-html
(str:join " " (nreverse buffer))))))
(write-char #\Newline stream)
(setf buffer nil))
(write-quote ()
(cl-who:with-html-output (stream)
(:blockquote
(:pre
(cl-who:esc
(str:join #\Newline (nreverse buffer))))))
(write-char #\Newline stream)
(setf buffer nil))
(write-list ()
(cl-who:with-html-output (stream)
(:ul
(loop for item in (reverse buffer)
do (cl-who:htm
(:li (cl-who:str (replace-org-mode-markup-with-html item)))))))
(write-char #\Newline stream)
(setf buffer nil))
(process (line)
(cond
((and (str:starts-with-p "- " line)
(not reading-quote))
(push (subseq line 2)
buffer)
(setf reading-list t))
((and reading-list
(string= line ""))
(write-list)
(setf reading-list nil))
(reading-list
(setf buffer
(list*
(format nil "~A ~A"
(car buffer)
line)
(cdr buffer))))
((string-equal line
"#+BEGIN_QUOTE")
(setf reading-quote t))
((string-equal line
"#+END_QUOTE")
(setf reading-quote nil)
(write-quote))
((not (string= line ""))
(push line buffer))
((and (not reading-quote)
(and (string= line "")
buffer))
(write-paragraph)))))
(mapc #'process
(str:split #\Newline text)))))
Now, we will use this text processing function to render all text nodes in our org-mode files:
(defmethod render-node ((node cl-org-mode::text-node) stream)
(render-text (cl-org-mode::node.text node)
stream))
Now it is time to write a code which will render all org mode files into HTML:
(defun make-output-filename (file)
(check-type file file)
(ppath:join "docs"
(format nil "~A.html" (car (ppath:splitext (get-filename file))))))
(defmethod render-node ((file file) stream)
(render-node (get-root file)
stream))
(defun call-render-for-all-children (node stream)
(loop for child in (cl-org-mode::node.children node)
do (render-node child
stream)))
(defmethod render-node ((file cl-org-mode::org-file) stream)
(call-render-for-all-children file stream))
(defun render-file (file)
(with ((filename (make-output-filename file))
(title (get-title file)))
(ensure-directories-exist filename)
(alexandria:with-output-to-file (stream filename :if-exists :supersede)
(app-page (stream :index-uri "../../"
:title title)
(cl-who:with-html-output (stream)
(render-node file stream)
(write-string "
<script src=\"https://utteranc.es/client.js\"
repo=\"40ants/lisp-project-of-the-day\"
issue-term=\"title\"
label=\"comments\"
theme=\"github-light\"
crossorigin=\"anonymous\"
async>
</script>
" stream))))))
We want to show in RSS only posts, published at Twitter. This information can be extracted from the README.org, because there I’m adding a link to the tweet. If there is a link, the post is published.
So, we have to find all list items inside “2020” heading and choose only those, having a link to the twitter.
(defun find-published-systems ()
(let* ((file (cl-org-mode::read-org-file "README.org"))
(years (loop for node = file then (cl-org-mode::node.next-node node)
while node
when (and (typep node 'cl-org-mode::outline-node)
(str:starts-with-p "20"
(cl-org-mode::node.heading node)))
collect node))
(months (loop for year in years
appending (cl-org-mode::node.children year)))
(text-nodes (loop for month in months
appending (cl-org-mode::node.children month)))
(texts (loop for node in text-nodes
collect (cl-org-mode::node.text node)))
(lines (loop for text in texts
appending (str:split #\Newline text))))
(loop for line in lines
when (and (str:starts-with-p "-" line)
;; If there are two links, then the second link is to the twitter post.
;; In this case this post is published.
(= (str:count-substring "[[" line)
2))
appending (str:split " & "
(cl-ppcre:regex-replace
".*?\\]\\[(.*?)\\].*"
line
"\\1")))))
Also, for each file we need to know when it was created. Without a date, many RSS clients will display feed in a wrong ways.
Next function get’s the timestamp of the commit with “publish” keyword in a text. Or the timestamp of the first commit where the file was added to the repository.
As the second value, it returns a commit message a timestamp was take from. This was useful for debugging:
(defun get-file-timestamp (file)
(let* ((all-commits (with-output-to-string (*standard-output*)
(legit:git-log :paths (fmt "content/~A"
(get-filename file))
:reverse t
:format "%at %s")))
(lines (str:split #\Newline all-commits))
(first-timestamp
(parse-integer (first (str:split #\Space
(first lines))))))
(local-time:unix-to-timestamp first-timestamp)))
(defun render-rss (files)
(alexandria:with-output-to-file (stream "docs/rss.xml"
:if-exists :supersede)
(let ((base-url "http://40ants.com/lisp-project-of-the-day/")
(published (find-published-systems)))
(flet ((is-not-published (file)
(let ((title (get-title file))
(filename (get-filename file)))
(or (not
(member title
published
:test #'string-equal))
(str:containsp "draft"
filename)))))
(xml-emitter:with-rss2 (stream)
(xml-emitter:rss-channel-header "Common Lisp Project of the Day"
base-url)
(loop for file in (rutils:take 20 (reverse
(remove-if #'is-not-published
files)))
for title = (get-title file)
for uri = (get-uri file)
for full-url = (format nil "~A~A" base-url uri)
for description = (make-description file)
for timestamp = (get-file-timestamp file)
do (xml-emitter:rss-item title
:description description
:link full-url
:pubdate (local-time:format-rfc1123-timestring
nil timestamp))))))))
On index page we want to output a list of all articles. Probably later, we’ll want to print only the latest and to create a tags based catalogue, but now a simple list is enough.
We’ll use few helpers to create urls and titles for the index page:
(defun strip-doc-folder (filename)
"Removes doc/ from beginning of the filename"
(cond
((str:starts-with-p "docs/" filename)
(subseq filename 5))
(t filename)))
(defun get-uri (file)
"Returns a link like 2020/03/001-some.html"
(strip-doc-folder (make-output-filename file)))
(defun get-title-for-index (file)
(rutils:with ((title tags (get-title file))
(filename (get-filename file))
(splitted (ppath:split filename))
(only-file (cdr splitted))
(number (first (str:split #\- only-file))))
(values title number tags)))
We’ll reuse this function for the front page and for tag pages:
(defun title-to-systems (title)
"Title may contain several systems, separated by &.
Like \"skippy-renderer & zpng\".
This function returns a list of separate systems."
(mapcar #'str:trim
(str:split "&" title)))
(defun render-index-page (files filename &key
(index-uri nil)
(path "docs")
(title "Latest posts"))
(let ((filename (ppath:join path
(rutils:fmt "~A.html"
filename)))
(published (find-published-systems)))
(ensure-directories-exist filename)
(flet ((is-not-published (file)
(let* ((title (get-title file))
(systems (title-to-systems title)))
(and (not (string= title "Day Zero"))
(loop for system in systems
never (member system
published
:test #'string-equal))))))
(alexandria:with-output-to-file (stream filename :if-exists :supersede)
(app-page (stream :index-uri index-uri)
(:section :style "margin-left: auto; margin-right: auto; margin-top: 2em; width: 50%"
(:h3 :style "margin-left: 1.6em"
title)
(:table :class "posts"
(loop for file in (reverse files)
for uri = (get-uri file)
do (cl-who:htm
(:tr
(multiple-value-bind (title number tags)
(get-title-for-index file)
(unless (string-equal number
"draft")
(cl-who:with-html-output (stream)
(:td :class "number"
(cl-who:esc (format nil "#~A" number)))
(:td (:a :href (construct-uri uri)
(cl-who:esc title)))
(:td :class "tags"
(loop for tag in tags
do (cl-who:htm
(:a :href (construct-uri "tags/~A.html" tag)
(cl-bootstrap:bs-label ()
(cl-who:esc tag)))))
(when (is-not-published file)
(cl-bootstrap:bs-label-danger
(cl-who:esc "draft")))))))))))))))
(values)))
(defun render-index (files)
(render-index-page files "index"))
For each tag we want to generate a separate page where will be listed only posts having a tag.
First, we need a function to collect a set of tags, used by all posts:
(defun get-all-tags (files)
(let (results)
(iter outer
(:for file :in files)
(with ((_ tags (get-title file)))
(declare (ignorable _))
(iter (:for tag :in tags)
(pushnew tag results :test #'string-equal))))
results))
Also we need a function to filter files having specific tag:
(defun get-files-with-tag (files tag)
(iter (:for file :in files)
(with ((_ tags (get-title file)))
(declare (ignorable _))
(when (member tag tags :test #'string-equal)
(:collect file)))))
Now we can write a function which will render a one page:
(defun render-tag (all-files tag)
(render-index-page (get-files-with-tag all-files tag)
tag
:path "docs/tags/"
:index-uri "../"
:title (rutils:fmt "Posts with tag \"~A\""
tag)))
(defun render-all-tag-pages (all-files)
(mapcar (alexandria:curry #'render-tag all-files)
(get-all-tags all-files)))
(defun render-patrons ()
(let ((filename (ppath:join "docs"
"patrons"
"index.html"))
(patrons '(("Jean-Philippe Paradis (Hexstream)" "https://www.hexstreamsoft.com/"))))
(alexandria:with-output-to-file (stream filename :if-exists :supersede)
(app-page (stream :index-uri "../")
(:section :style " margin-left: auto; margin-right: auto; margin-top: 2em; width: 50%"
(:h3 :style "margin-left: 1.6em"
"Project Patrons")
(:p "Special thanks to these people and companies supporting the project!")
(:ul
(loop for (name url) in patrons
do (cl-who:htm
(:li (:a :href url
(cl-who:esc name)))))))))
(values)))
Also, we need a entry-point function which will do all the job - read files and write html:
(defun render-site (&key (no-tags nil))
(let ((files (read-files)))
(mapc #'render-file files)
(render-index files)
(unless no-tags
(render-all-tag-pages files))
(render-patrons)
(render-rss files)
(values)))
(defun make-description (file)
(let ((*rss-mode* t))
(with-output-to-string (s)
(render-node file s))))
(defclass lowercased-src-node (cl-org-mode::src-node)
()
(:default-initargs
:opening-delimiter "#+begin_src"
:closing-delimiter (format nil "~%#+end_src")
:text nil
:include-end-node nil))
(defmethod cl-org-mode::node-dispatchers ((node cl-org-mode::org-node))
(or cl-org-mode::*dispatchers*
(mapcar #'make-instance '(lowercased-src-node
cl-org-mode::src-node
cl-org-mode::properties-node
cl-org-mode::outline-node))))
Cl-org-mode from the Quicklisp is a 10 years old library which seems unmaintained. Probably it is better to move to a library I’ve found on the GitHub or to this library.
To work with files we will use ppath. This library is able to make relative path. However, it operates with strings, not pathnames.
(defun pathname-to-string (p)
(format nil "~A" p))
This morning I decided to do a week of ASDF extensions review. There is incomplete listing of ASDF extensions in it’s documentation, but how to find all available ASDF extensions? Obviously, by parsing all “*.asd” files, and extracting their “:defsystem-depends-on”.
(defun install-all-quicklisp ()
(loop with dist = (ql-dist:find-dist "quicklisp")
with releases = (ql-dist:provided-releases dist)
for release in releases
do (ql-dist:install release)))
(defun get-software-dir ()
(let ((dist (ql-dist:find-dist "quicklisp")))
(ql-dist:relative-to dist
(make-pathname :directory
(list :relative "software")))))
(defun grep-defsystem-depends ()
"Returns lines produced by grep"
(str:split #\Newline
(with-output-to-string (s)
(uiop:run-program (format nil "find ~A -name '*.asd' -print0 | xargs -0 grep -i defsystem-depends-on"
(get-software-dir))
:output s))))
(defun extract-systems (line)
(when (str:contains? "defsystem-depends-on"
line)
(loop with names = (str:words
(cl-ppcre:regex-replace
".*:defsystem-depends-on.*\\((.*?)\\).*"
line
"\\1"))
for name in names
collect (string-trim "\":#"
name))))
(defun get-asdf-extensions (&key show-paths)
(loop with result = (make-hash-table :test #'equal)
for line in (grep-defsystem-depends)
for systems = (extract-systems line)
do (loop for system in systems
do (push line (gethash system result nil)))
finally (return
(loop with sorted = (sort (alexandria:hash-table-alist result)
#'>
:key (lambda (item)
(length (cdr item))))
for (system . lines) in sorted
collect (cons system (if show-paths
lines
(length lines)))))))
- a bunch of hu.dwim.* systems seems can be very interesting. We can
make “A Week of DWIM.HU”!
- defclass-star - a more clever defclass
- serializer - flexible and fast object serialization/deserialization
- stefil - a test framework that just doesn’t get in the way
- computed-class - functional reactive programming for classes, local variables, etc.
- quasi-quote - efficient templating
- rdbms - database access (mostly for Postgres)
- perec - persistent CLOS
- web-server
-logger
- there are many interesting
darts.lib.*
libraries on the GitHub. - https://github.com/ruricolist/vernacular - interesting system to extend Lisp with other syntax
- wuwei - stateful ajax framework based on continuations.
- bobbin - text wrapping utility
- http://quickdocs.org/parse-float/
- http://quickdocs.org/legit
- https://github.com/Shinmera/modularize
- http://quickdocs.org/modularize-hooks/
- https://github.com/markcox80/lisp-executable
- http://clast.sourceforge.net/
- https://github.com/s-expressionists/Trucler
- https://github.com/bufferswap/ViralityEngine
- http://quickdocs.org/cl-hash-util/
- https://github.com/g000001/mbe - Scheme Macros for Common Lisp
- http://verisimilitudes.net/2017-12-30 shut-it-down
- http://quickdocs.org/blackbird/
- https://github.com/selwynsimsek/lisp-o-motive obtain token at: https://datafeeds.networkrail.co.uk/ntrod/
- http://quickdocs.org/cl-stomp/
- http://clsql.kpe.io/manual/
- oook
- file-local-variable
- everblocking-stream
- https://github.com/scymtym/more-conditions
- http://quickdocs.org/cl-olefs/ - reading Excel
- http://quickdocs.org/mockingbird/
- http://quickdocs.org/trivial-main-thread/
- https://github.com/sirherrbatka/vellum - dataframes
- http://massung.github.io/quickdoc/example.html - interesting markup language
- https://github.com/naryl/cl-cooperative
- http://quickdocs.org/screamer/
- https://github.com/mmontone/ten - fork of the http://quickdocs.org/eco/
- https://gist.github.com/markasoftware/ab357f1b967b3f656d026e33fec3bc0e html->string
- https://github.com/Shinmera/flow
- https://github.com/Shinmera/messagebox
- some zeromq lib examples are here http://zguide.zeromq.org/page:chapter1 http://zguide.zeromq.org/lisp:hwclient http://zguide.zeromq.org/lisp:hwserver and common-lisp-jupter uses it somehow. but there are 3 libs, which one to review???
- tesseract-capi - OCR lowlevel binding
- https://github.com/antimer/antimer - wiki on CL
- http://git.kpe.io/?p=lml.git;a=summary - Yet another HTML generator
- https://github.com/GordianNaught/cl-durian Another HTML generator
- esrap - parser used by 3bmd (suggested by Vsevolod)
- https://github.com/oyvinht/cl-earley-parser natural language parser (suggested by Mark David)
- lsx - embeddable HTML templating by Fukamachi
- trace-db by GrammaTech
- https://github.com/cbeo/gtwiwtg - a new iterators library
- https://github.com/windchime/cl-spasm html generator built, a Common Lisp port of Hiccup, Clojure’s HTML-building, vector- and map-based library.
- 3bmd - Markdown parser. Interesting feature is integration with clhs-lookup library, when rendering lisp code blocks. If clhs-lookup exists 3bmd will try to make a links. Found it when working on cl-gendoc post. Actually, it uses gh:redline6561/colorize which in turn
- https://github.com/JunSuzukiJapan/cl-reex - Reactive extensions for common lisp
- qbook (active fork https://github.com/mmontone/qbook) documentation system
- https://github.com/Shinmera/trivial-arguments
- projects from https://tfeb.github.io/
- stmx - transactional memory for CLOS.
- https://github.com/digikar99/trivial-coerce
- https://github.com/fisxoj/sanity-clause - form/structure validation
- https://gitlab.com/ralt/pvars - persistent variables
- https://github.com/digikar99/reader - interesting syntax extensions for hash tables, sets, arrays, etc.
- https://github.com/tdrhq/slite можно записать демку и показать, как добавить в него поддержку Rove.
Simple
- cl-sentiment - sentiment text analyze
The site is hosted at GitHub pages right from the docs
folder. Thus you need
to build the site on your machine and to push results to the master branch.
To build site do this in the REPL:
(ql:quickload :poftheday)
(poftheday::render-site)
In this project I’ve used RSS Icon by Alex Prunici.