# HG changeset patch # User Steve Losh # Date 1578549932 28800 # Node ID c499267711c3ceebf491e4755ca6077a8539ef38 # Parent 2b4f7c840c5147e9c3cf014d8ed70aa6daf95f35 Tables of contents and RSS diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2008/08/on-leading.markdown --- a/content/blog/2008/08/on-leading.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2008/08/on-leading.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,6 +3,7 @@ :snip "Some of my thoughts on leading after five years of doing it." :date "2008-08-01T15:28:33Z" :draft nil +:hidden t ) diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2009/04/why-people-dont-like-metal.markdown --- a/content/blog/2009/04/why-people-dont-like-metal.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2009/04/why-people-dont-like-metal.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,6 +3,7 @@ :snip "It’s probably not what you think." :date "2009-04-02T22:32:27Z" :draft nil +:hidden t ) diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2009/11/my-sitesprint-project-lindyhub.markdown --- a/content/blog/2009/11/my-sitesprint-project-lindyhub.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2009/11/my-sitesprint-project-lindyhub.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,6 +3,7 @@ :snip "I want to make something awesome for dancers." :date "2009-11-16T19:15:07Z" :draft nil +:hidden t ) diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2010/02/my-extravagant-zsh-prompt.markdown --- a/content/blog/2010/02/my-extravagant-zsh-prompt.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2010/02/my-extravagant-zsh-prompt.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -222,10 +222,10 @@ out = (filled + empty).encode('utf-8') import sys -color_green = '%{%}' -color_yellow = '%{%}' -color_red = '%{%}' -color_reset = '%{%}' +color_green = '%{%}' +color_yellow = '%{%}' +color_red = '%{%}' +color_reset = '%{%}' color_out = ( color_green if len(filled) > 6 else color_yellow if len(filled) > 4 diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2010/09/making-my-site-sing.markdown --- a/content/blog/2010/09/making-my-site-sing.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2010/09/making-my-site-sing.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,6 +3,7 @@ :snip "Designing with music." :date "2010-09-08T20:10:00Z" :draft nil +:hidden t ) diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2010/11/keep-calm-and-carry-on.markdown --- a/content/blog/2010/11/keep-calm-and-carry-on.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2010/11/keep-calm-and-carry-on.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,6 +3,7 @@ :snip "You don't always need to be sexy." :date "2010-11-05T16:30:00Z" :draft nil +:hidden t ) diff -r 2b4f7c840c51 -r c499267711c3 content/blog/2011/05/on-learning-and-teaching.markdown --- a/content/blog/2011/05/on-learning-and-teaching.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/blog/2011/05/on-learning-and-teaching.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,6 +3,7 @@ :snip "Learning one thing isn't enough." :date "2011-05-22T17:00:00Z" :draft nil +:hidden t ) diff -r 2b4f7c840c51 -r c499267711c3 content/links.markdown --- a/content/links.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/links.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -8,12 +8,7 @@ every now and then when I'm bored. I figured other people might find it interesting too. -* [Blogs](#blogs) -* [YouTube Channels](#youtube-channels) -* [Subreddits](#subreddits) -* [Tools](#tools) -* [Game Development Beginner Resources](#game-development-beginner-resources) -* [Common Lisp Utility Libraries](#common-lisp-utility-libraries) +
Blogs ----- diff -r 2b4f7c840c51 -r c499267711c3 content/projects.markdown --- a/content/projects.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/projects.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -3,14 +3,9 @@ :draft nil) The following is a list of projects I've created. They're grouped by -maintenance status: +maintenance status. -* [Actively Maintained](#actively-maintained) -* [Under Development](#under-development) -* [Looking for Maintainers](#looking-for-maintainers) -* [Transferred Maintainership](#transferred-maintainership) -* [Finished](#finished) -* [Deprecated/Abandoned](#deprecated-abandoned) +
## Actively Maintained diff -r 2b4f7c840c51 -r c499267711c3 content/resume.markdown --- a/content/resume.markdown Wed Jan 08 22:05:24 2020 -0800 +++ b/content/resume.markdown Wed Jan 08 22:05:32 2020 -0800 @@ -20,6 +20,8 @@ [github]: https://github.com/sjl/ [RU]: https://www.ru.is/ +
+ ## Languages My current programming language of choice is for most of my projects is [Common diff -r 2b4f7c840c51 -r c499267711c3 generate.lisp --- a/generate.lisp Wed Jan 08 22:05:24 2020 -0800 +++ b/generate.lisp Wed Jan 08 22:05:32 2020 -0800 @@ -1,19 +1,19 @@ (eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload (list :alexandria :iterate :losh :local-time - :cl-who :3bmd :3bmd-ext-code-blocks) + :cl-who :3bmd :3bmd-ext-code-blocks + :plump :cl-slug) :silent t)) (defpackage :stevelosh.com (:use :cl :iterate :losh) (:import-from :cl-who - :with-html-output :htm :str) + :with-html-output :htm :str :fmt) (:export :toplevel)) (in-package :stevelosh.com) ;;;; State and Configuration -------------------------------------------------- -(setf (cl-who:html-mode) :html5 - 3bmd-code-blocks:*code-blocks* t) +(setf 3bmd-code-blocks:*code-blocks* t) ;;;; Utilities ---------------------------------------------------------------- @@ -23,9 +23,22 @@ nil timestamp :format `(:long-month " " :ordinal-day ", " :year))) +(defun rfc-822-date (timestamp) + (check-type timestamp local-time:timestamp) + (local-time:format-timestring + nil timestamp + :format `(:short-weekday ", " (:day 2) " " :short-month " " :year + " " (:hour 2) ":" (:min 2) ":" (:sec 2) " " :timezone) + :timezone local-time:+gmt-zone+)) + + (defmacro who (&body body) `(with-html-output (*standard-output*) ,@body)) +(defmacro whos (&body body) + `(with-output-to-string (s) + (with-html-output (s) ,@body))) + (defmacro delay (&body body) `(let (result done) (lambda () @@ -36,23 +49,154 @@ (defun force (delay) (funcall delay)) + (defun cat (&rest strings) (apply #'concatenate 'string strings)) +(defun esc (string) + (cl-who:escape-string string)) + + +;;;; Tables of Contents ------------------------------------------------------- +(defparameter *header-number* 0) + +(defun subheaderp (node) + (and (plump:element-p node) + (member (plump:tag-name node) '("h2" "h3" "h4" "h5" "h6") :test #'string=))) + +(defun replace-node (node html) + (plump:replace-child node (elt (plump:children (plump:parse html)) 0))) + +(defun replace-content (node html) + (plump:clear node) + (loop :for child :across (plump:children (plump:parse html)) + :do (plump:append-child node child))) + +(defun linkify-subheader (node) + (let* ((text (plump:text node)) + (id (format nil "s~D-~A" (incf *header-number*) (slug:slugify text))) + (href (concatenate 'string "#" id))) + (plump:set-attribute node "id" id) + (replace-content node (whos (:a :href href (str text)))))) + +(defun linkify-subheaders (root) + (let ((*header-number* 0)) + (plump:traverse root #'linkify-subheader :test #'subheaderp))) + +(defun header-level (node) + (digit-char-p (char (plump:tag-name node) 1))) + +(defun subheaders (root) + (-<> root + (gathering + (plump:traverse <> #'gather :test #'subheaderp)) + (mapcar (lambda (node) (cons (header-level node) node)) <>))) + +(defun split-if (pred list) + "Split list into two pieces, at the point where pred first becomes true. + + The first element of the second list will be the point where pred becomes true. + + " + (loop :for tail :on list + :for (next . more) = tail + :until (funcall pred next) + :collect next :into head + :finally (return (values head tail)))) + +(defun extract-toc (root) + "Extract a table of contents from `root` as a tree. + + The result will be a tree of `(node &rest children)`, e.g.: + + h2 ((h2 (h3) + h3 (h3)) + h3 (h2 (h3 (h4) + h2 (h4)) + h3 → (h3 (h4 + h4 (h5) + h4 (h5)))) + h3 (h2 (nil (h4) + h4 (h4)) + h5 (h3)) + h5 + h2 + h4 + h4 + h3 + + Note the addition of dummy headers when the level jumps unexpectedly, to keep + the proper TOC structure even when the source is borked. + + " + (labels + ((split (headers) + "Split `headers` into the first header, its children, and whatever else remains." + (destructuring-bind (first-header . remaining) headers + (multiple-value-bind (head tail) + (split-if (lambda (header) + (<= (car header) (car first-header))) + remaining) + (values first-header head tail)))) + (section (level header children) + "Handle a single section (i.e. one header and its children)." + (list* (cdr header) (sections (1+ level) children))) + (sections (level headers) + "Split `headers` into sibling sections, expecting to be at `level`." + (if (null headers) + nil + (let ((l (car (first headers)))) + (if (< level l) + (sections level (cons (cons (1- l) nil) headers)) + (multiple-value-bind (header children remaining) (split headers) + (list* + (section level header children) + (sections level remaining)))))))) + (sections 2 (subheaders root)))) + +(defun render-toc (toc) + "Render a TOC tree from `extract-toc` to HTML." + (whos + (:ol :class "table-of-contents" + (recursively ((sections toc)) + (unless (null sections) + (destructuring-bind ((header &rest children) . remaining) sections + (htm (:li + (when header + (htm (:a :href (format nil "#~A" (plump:attribute header "id")) + (str (plump:text header))))) + (when children + (htm (:ol (recur children)))))) + (recur remaining))))))) + +(defun insert-toc (html) + (let* ((root (plump:parse html)) + (div (plump:get-element-by-id root "toc"))) + (when div + (linkify-subheaders root) + (replace-node div (render-toc (extract-toc root)))) + (plump:serialize root nil))) + + + ;;;; Content ------------------------------------------------------------------ (defclass* content () (input-path output-path date modified-timestamp body - url title draft gallery snip + url title gallery snip + (draft :initform nil) + (hidden :initform nil) (mathjax :initform nil))) + (defun parse-markdown (path) - (with-open-file (stream path :direction :input) - (read stream) ; discard metadata this time - (with-output-to-string (string) - (3bmd:parse-string-and-print-to-stream - (alexandria:read-stream-content-into-string stream) - string)))) + (insert-toc + (with-open-file (stream path :direction :input) + (read stream) ; discard metadata this time + (with-output-to-string (string) + (3bmd:parse-string-and-print-to-stream + (alexandria:read-stream-content-into-string stream) + string))))) (defun read-content (path) (with-open-file (s path :direction :input) @@ -87,7 +231,7 @@ ;;;; Base Templates ----------------------------------------------------------- (defmacro css (href &rest more) - `(who (:link :href ,href :rel "stylesheet" :type "text/css" :charset "utf-8" ,@more))) + `(who (:link :href ,href :rel "stylesheet" :type "text/css" ,@more))) (defun t/header () (who @@ -117,9 +261,9 @@ (:meta :charset "utf-8") (css "/static/css/style.css") (css "/static/css/print.css" :media "print") - (:title (str (if title - (format nil "~A / Steve Losh" title) - "Steve Losh")))) + (:title (str (esc (if title + (format nil "~A / Steve Losh" title) + "Steve Losh"))))) (:body (t/header) (:hr :class "main-separator") @@ -134,16 +278,16 @@ ;;;; Page Templates ----------------------------------------------------------- (defun t/mathjax () - (who (str ""))) + (who (str ""))) (defun t/index-item (item) (who (:li :class "item" - (:a :href (url item) (str (title item))) + (:a :href (url item) (str (esc (title item)))) (:p :class "date" (str (human-date (date item)))) - (:p :class "snippet" (str (snip item)))))) + (:p :class "snippet" (str (esc (snip item))))))) (defun t/index (items) (who (:ol :class "index" (map nil #'t/index-item items)))) @@ -158,7 +302,7 @@ (who (:article (when (mathjax entry) (t/mathjax)) - (:h1 (:a :href (url entry) (str (title entry)))) + (:h1 (:a :href (url entry) (str (esc (title entry))))) (:p :class "date" "Posted on " (str (human-date (date entry))) ".") (str (force (body entry))))))) @@ -177,7 +321,7 @@ (with-base ("photography-gallery" (title gallery)) (who (:article - (:h1 (:a :href (url gallery) (str (title gallery)))) + (:h1 (:a :href (url gallery) (str (esc (title gallery))))) (:p :class "date" "Last updated on " (str (human-date (date gallery))) ".") (str (force (body gallery))) @@ -189,7 +333,7 @@ (with-base ("simple" (title content)) (who (:article - (:h1 (:a :href (url content) (str (title content)))) + (:h1 (:a :href (url content) (str (esc (title content))))) (:p :class "date" "Last updated on " (str (human-date (date content))) ".") (str (force (body content))))))) @@ -212,6 +356,29 @@ (:h1 "404 Not Found") (:p "If you followed a link to get here, it's bad.")))) +(defun t/rss (entries) + (who + (:rss :version "2.0" + (:channel + (:title "Steve Losh") + (:link "https://stevelosh.com/") + (:description "Steve Losh's blog.") + (:language "en-us") + (:copyright "Copyright 2020, Steve Losh") + (:|webMaster| "steve@stevelosh.com (Steve Losh)") + (:|lastBuildDate| (str (rfc-822-date (local-time:now)))) + (:generator "Common Lisp") + (:docs "https://validator.w3.org/feed/docs/rss2.html") + (dolist (entry entries) + (let ((link (format nil "https://stevelosh.com~A" (url entry)))) + (htm (:item + (:title (str (esc (title entry)))) + (:link (str link)) + (:guid :|isPermaLink| "true" (str link)) + (:description (str (esc (snip entry)))) + (:|pubDate| (str (rfc-822-date (date entry))))))))))) + (values)) + ;;;; Generation --------------------------------------------------------------- (defparameter *generate-timestamp* (file-write-date "generate.lisp")) @@ -223,7 +390,7 @@ (some (lambda (time) (> time prev)) (alexandria:ensure-list times)))))) -(defun render% (path times thunk) +(defun render% (path mode times thunk) (if (needs-render-p path times) (progn (format t "Rendering ~S~%" path) @@ -231,43 +398,54 @@ (with-open-file (*standard-output* path :direction :output :if-exists :supersede) - (with-html-output (*standard-output* nil :prologue t) - (funcall thunk)))) + (setf (cl-who:html-mode) mode) + (ecase mode + (:html5 (write-line "")) + (:xml (write-line ""))) + (who (funcall thunk)))) #+no (format t "Skipping ~S, file is up to date.~%" path))) -(defmacro render (path &optional times &body body) - `(render% ,path ,times (lambda () ,@body))) +(defmacro render (path (&key times (mode :html5)) &body body) + `(render% ,path ,mode ,times (lambda () ,@body))) (defun page/home () - (render "build/index.html" nil + (render "build/index.html" () (t/home))) (defun page/404 () - (render "build/404.html" nil + (render "build/404.html" () (t/404))) (defun page/blog/index (entries) - (render "build/blog/index.html" (mapcar #'modified-timestamp entries) - (t/blog/index entries))) + (render "build/blog/index.html" + (:times (mapcar #'modified-timestamp entries)) + (t/blog/index (remove-if #'hidden entries)))) (defun page/photography/index (galleries) - (render "build/photography/index.html" (mapcar #'modified-timestamp galleries) + (render "build/photography/index.html" + (:times (mapcar #'modified-timestamp galleries)) (t/photography/index galleries))) (defun page/blog/entries (entries) (dolist (entry entries) - (render (output-path entry) (modified-timestamp entry) + (render (output-path entry) (:times (modified-timestamp entry)) (t/blog/entry entry)))) (defun page/photography/galleries (galleries) (dolist (gallery galleries) - (render (output-path gallery) (modified-timestamp gallery) + (render (output-path gallery) (:times (modified-timestamp gallery)) (t/photography/gallery gallery)))) (defun page/simple (content) - (render (output-path content) (modified-timestamp content) + (render (output-path content) (:times (modified-timestamp content)) (t/simple content))) +(defun page/rss (entries) + (render "build/rss.xml" + (:times (mapcar #'modified-timestamp entries) + :mode :xml) + (t/rss entries))) + ;;;; Toplevel ----------------------------------------------------------------- (defun toplevel () @@ -284,9 +462,13 @@ (page/photography/galleries photo-galleries) (page/simple links) (page/simple projects) - (page/simple resume))) + (page/simple resume) + (page/rss blog-entries))) #; Scratch -------------------------------------------------------------------- + (defparameter *blog-entries* (walk "content/blog")) + +(plump:parse *x*) diff -r 2b4f7c840c51 -r c499267711c3 static/css/style.css --- a/static/css/style.css Wed Jan 08 22:05:24 2020 -0800 +++ b/static/css/style.css Wed Jan 08 22:05:32 2020 -0800 @@ -5,8 +5,8 @@ text-rendering: optimizeLegibility; margin: 10px auto 200px; width: 700px; - font-family: Palatino, "Palatino Linotype", serif; - font-size: 17px; + font-family: serif; + font-size: 18px; line-height: 24px; background: #fcfcfc; color: black; @@ -29,7 +29,7 @@ } header > nav { float: right; - font-size: 17px; + font-size: 18px; line-height: 30px; } @@ -61,7 +61,7 @@ /* Headings ---------------------------------------------------------------- */ h1, h2, h3, h4, h5, h6 { - font-family: HoeflerText-Regular, 'Hoefler Text', 'Goudy Old Style','Palatino', 'Palatino Linotype', serif; + /* font-family: HoeflerText-Regular, 'Hoefler Text', 'Goudy Old Style','Palatino', 'Palatino Linotype', serif; */ font-weight: normal; } h1 a, h2 a, h3 a, h4 a { @@ -136,6 +136,17 @@ a#logo { color: #000000; } a#logo:hover { color: #e50053; } +ol.table-of-contents { + list-style-type: none; + margin: 0px 0px 24px 0px; + padding: 0px; +} +ol.table-of-contents ol { + list-style-type: none; + margin: 0px 0px 0px 36px; + padding: 0px; +} + /* Index Pages ------------------------------------------------------------- */ ol.index { list-style-type: none; @@ -145,6 +156,7 @@ margin-bottom: 24px; } ol.index p.snippet { + margin-top: 2px; font-style: italic; } ol.index p.date {