1122 lines
34 KiB
Org Mode
1122 lines
34 KiB
Org Mode
#+title: Webbieweb.org Sources
|
||
* Webbieweb.org Literate Sources :h4left:h5left:h3underline:
|
||
** Introduction
|
||
These are the [[https://en.wikipedia.org/wiki/Literate_programming][literate]] sources of /webbieweb.org/. The code blocks here constitute the actual sources; therefore, this document is guaranteed to be up to date.
|
||
|
||
This page is self-contained; the only dependency is =ox-publish=. The following directory structure is expected:
|
||
#+BEGIN_EXAMPLE
|
||
.
|
||
|-- html ;; Export destination; you can copy this dir to any static file host and host the website that way.
|
||
|-- org ;; Org-mode sources; these will get compiled and the output copied to html/.
|
||
`-- source.org ;; This file.
|
||
|-- static ;; Images, videos, CSS files, fonts etc will be copied unchanged to html/static.
|
||
`-- util
|
||
|-- head.html ;; Contains HTML <head>; will be prepended to every output file.
|
||
|-- postamble.html ;; Navbar; will be prepended to <body> of every output file.
|
||
`-- preamble.html ;; Footer; as above.
|
||
#+END_EXAMPLE
|
||
#+BEGIN_CENTER
|
||
(Apologies for the misalignment, I don't know why that happens.)
|
||
#+END_CENTER
|
||
|
||
** Export Commands
|
||
Load this file up in Emacs and hit C-c C-c on the code block below to export changes to HTML.
|
||
#+NAME: blog-publish
|
||
#+BEGIN_SRC emacs-lisp :noweb no-export :results silent
|
||
<<blog-scaffolding>>
|
||
(blog/publish)
|
||
(org-babel-tangle)
|
||
#+END_SRC
|
||
|
||
The one below will ignore caching; necessary in some situations, for instance: changes under =util/=.
|
||
Currently the habit tracker table also gets erronously cached.
|
||
#+name: force-publish
|
||
#+BEGIN_SRC emacs-lisp :noweb no-export :results silent
|
||
<<blog-scaffolding>>
|
||
(blog/force-publish)
|
||
(org-babel-tangle)
|
||
#+END_SRC
|
||
** Elisp
|
||
*** Basic Settings/Scaffolding :autocollapse:
|
||
#+NAME: blog-scaffolding
|
||
#+BEGIN_SRC emacs-lisp :results output :noweb no-export
|
||
(require 'ox-publish)
|
||
|
||
<<no-sitemap>>
|
||
|
||
(defun blog/get-util (x)
|
||
(with-temp-buffer
|
||
(insert-file-contents (concat "../util/" x))
|
||
(buffer-string)))
|
||
|
||
(setq org-html-metadata-timestamp-format "%Y-%m-%d")
|
||
|
||
(defun blog/spec ()
|
||
"Return project settings for use with `org-publish-project-alist'."
|
||
(let* ((html-head (blog/get-util "head.html"))
|
||
(html-preamble (blog/get-util "preamble.html"))
|
||
(html-postamble (blog/get-util "postamble.html")))
|
||
|
||
|
||
`(("pages"
|
||
:base-directory "."
|
||
:base-extension "org"
|
||
:recursive t
|
||
:publishing-directory "../html"
|
||
:publishing-function org-html-publish-to-html
|
||
|
||
:html-doctype "html5"
|
||
:html-html5-fancy t
|
||
|
||
:html-viewport ((width "100%")
|
||
(initial-scale "0.7")
|
||
(minimum-scale "")
|
||
(maximum-scale "")
|
||
(user-scalable ""))
|
||
|
||
:language "en"
|
||
:section-numbers nil
|
||
|
||
:with-toc nil
|
||
:with-date t
|
||
:with-title nil
|
||
:with-author nil
|
||
|
||
:auto-sitemap t
|
||
|
||
:sitemap-sort-files anti-chronologically
|
||
:sitemap-format-entry blog/sitemap-format-entry
|
||
|
||
:sitemap-function
|
||
blog/sitemap-function
|
||
|
||
|
||
:headline-levels 4
|
||
:html-head ,html-head
|
||
|
||
:html-preamble ,html-preamble
|
||
:html-postamble
|
||
(lambda (p)
|
||
(let* ((timestamp-format (plist-get p :html-metadata-timestamp-format))
|
||
(date (org-export-data (org-export-get-date p timestamp-format)
|
||
p))
|
||
(file (plist-get p :input-file))
|
||
(modified (format-time-string
|
||
timestamp-format
|
||
(and file (file-attribute-modification-time
|
||
(file-attributes file))))))
|
||
(concat
|
||
"<div id='footer'>"
|
||
(when (not (string-blank-p (format "%s" date)))
|
||
(format "<div id='publish-date'>Published: %s</div>" date))
|
||
(format "<div id='modified-date'>Last modified: %s</div>" modified)
|
||
,html-postamble
|
||
"</div>"))))
|
||
|
||
("static"
|
||
:base-directory "../static"
|
||
:base-extension "css\\|txt\\|jpg\\|gif\\|png\\|ttf\\|js\\|mp4\\|webm"
|
||
:recursive t
|
||
:publishing-directory "../html/static"
|
||
:publishing-function org-publish-attachment)
|
||
|
||
("RSS"
|
||
:base-directory "../RSS"
|
||
:base-extension "xml"
|
||
:publishing-directory "../html"
|
||
:publishing-function org-publish-attachment)
|
||
|
||
("blog" :components ("pages" "static" "RSS")))))
|
||
|
||
(defun blog/publish ()
|
||
(setq org-publish-project-alist (blog/spec))
|
||
(org-publish-all))
|
||
|
||
(defun blog/force-publish ()
|
||
(setq org-publish-project-alist (blog/spec))
|
||
(org-publish-remove-all-timestamps)
|
||
(org-publish-all))
|
||
#+END_SRC
|
||
|
||
** Features
|
||
*** Sitemap :autocollapse:
|
||
Files with =#+FILETAGS: :no-sitemap:= at the top will be excluded from [[./sitemap.org][the sitemap]]. Every directory =dir/= will link to =dir/index.org=, with =index.org= being removed as a child entry.
|
||
#+NAME: no-sitemap
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defun blog/sitemap-format-entry
|
||
(entry style project)
|
||
;; Blank out entries with :no_sitemap:
|
||
(let* ((tags (org-publish-find-property entry :filetags project))
|
||
(skip (member "no_sitemap" tags)))
|
||
|
||
(cond
|
||
(skip "")
|
||
|
||
((not (directory-name-p entry))
|
||
(format
|
||
"[[file:%s][%s]]\n"
|
||
entry
|
||
(org-publish-find-title entry project)))
|
||
|
||
;; Link index to top level
|
||
((eq style 'tree)
|
||
(format
|
||
"[[file:%sindex.org][%s]]\n"
|
||
entry
|
||
(capitalize (file-name-nondirectory (directory-file-name entry)))))
|
||
|
||
(t entry))))
|
||
|
||
|
||
;; Remove blank entries from sitemap
|
||
(defun blog/sitemap-function
|
||
(title lst)
|
||
(let* ((sitemap (org-publish-sitemap-default title lst)))
|
||
(replace-regexp-in-string "^[ \t]*-[ \t]*\n" "" sitemap)))
|
||
|
||
#+END_SRC
|
||
|
||
*** Tag-based Section Control :autocollapse:
|
||
Org-mode headers support tags:
|
||
#+BEGIN_SRC org
|
||
,* Header :foo:
|
||
Sample text
|
||
#+END_SRC
|
||
|
||
Which will compile into something like:
|
||
#+BEGIN_SRC html
|
||
<div id="outline-container-orgf43334f" class="outline-5">
|
||
<h5 id="orgf43334f" style="cursor: pointer;">Header
|
||
<span class="tag">
|
||
<span class="foo">foo</span>
|
||
</span>
|
||
</h5>
|
||
<div class="outline-text-5" id="text-orgf43334f">
|
||
<p> Sample text </p>
|
||
</div>
|
||
</div>
|
||
#+END_SRC
|
||
|
||
We can use these tags to granularly control styling and JS features per header.
|
||
We will hide the tags themselves:
|
||
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.tag { display: none; }
|
||
#+END_SRC
|
||
|
||
*** Header Styling Tags :autocollapse:
|
||
Tags for granular styling. This page has =:h4left:h5left:h3underline:= at the top level.
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
div:has(* .tag .h4left) h4 {
|
||
text-align: left;
|
||
}
|
||
|
||
div:has(* .tag .h5left) h5 {
|
||
text-align: left;
|
||
}
|
||
|
||
div:has(* .tag .h3underline) h3 {
|
||
text-decoration: underline;
|
||
}
|
||
#+END_SRC
|
||
|
||
*** Journal :autocollapse:
|
||
Here's a template for the journal files, as reference for what follows:
|
||
**** Template :autocollapse:
|
||
#+INCLUDE: "./journal/template.org" src org
|
||
|
||
**** Styling
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
div:has(h3 .tag .journal) {
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
div:has(> h3 .tag .journal)::after {
|
||
content: '~ ❦ ~';
|
||
display: block;
|
||
text-align: center;
|
||
padding-top: 0.2rem;
|
||
padding-bottom: 0.2rem;
|
||
color: var(--grey4);
|
||
font-size: 2rem;
|
||
}
|
||
|
||
h3:has(.tag .journal) {
|
||
text-align: left;
|
||
}
|
||
|
||
h4:has(.tag .jentry) {
|
||
display: none;
|
||
}
|
||
|
||
h4:has(.tag .habits) {
|
||
text-align: left;
|
||
}
|
||
#+END_SRC
|
||
*** Habit Tracker :autocollapse:
|
||
Implements the habit tracking table on the [[./index.org][front page]]. Habits are extracted from journal entries and rendered into a table, including a "streak" heatmap and the ability to click cells to see the corresponding entry.
|
||
|
||
**** Elisp :autocollapse:
|
||
#+NAME: habit-tracker
|
||
#+BEGIN_SRC emacs-lisp :results silent
|
||
(defun akk0/org-to-html (org-string)
|
||
"Convert ORG-STRING to HTML."
|
||
(with-temp-buffer
|
||
(insert org-string)
|
||
(org-mode)
|
||
(org-export-as 'html nil nil t nil)))
|
||
|
||
(defun akk0/sort-habits (habit-alist)
|
||
"Sort habit-alist by a predefined order of custom-ids."
|
||
(let ((order '("dailies-blogging" "dailies-meditation" "dailies-french" "dailies-engineering"
|
||
"dailies-exercise" "dailies-drawing" "dailies-reading" "dailies-social")))
|
||
(sort (copy-sequence habit-alist)
|
||
(lambda (a b)
|
||
(let ((pos-a (or (cl-position (car a) order :test #'equal) 999))
|
||
(pos-b (or (cl-position (car b) order :test #'equal) 999)))
|
||
(< pos-a pos-b))))))
|
||
|
||
|
||
(defun akk0/extract-habits (file)
|
||
"Extract habits with date context from FILE.
|
||
Returns a list of plists with :custom-id, :todo-state, :date, :day-of-year."
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(org-mode)
|
||
(let (results)
|
||
(org-element-map (org-element-parse-buffer) 'headline
|
||
(lambda (hl)
|
||
(let ((custom-id (org-element-property :CUSTOM_ID hl))
|
||
(todo-state (org-element-property :todo-keyword hl)))
|
||
(when (and custom-id todo-state)
|
||
;; Get parent properties for context
|
||
(let* ((parent (org-element-property :parent hl))
|
||
(day-of-year (or (org-element-property :DAILIES-DAY parent)
|
||
(let ((grandparent (org-element-property :parent parent)))
|
||
(when grandparent
|
||
(org-element-property :DAILIES-DAY grandparent)))))
|
||
(body (org-element-interpret-data
|
||
(org-element-contents hl))))
|
||
(push (list :custom-id custom-id
|
||
:todo-state todo-state
|
||
:day-of-year day-of-year
|
||
:file file
|
||
:body body)
|
||
results))))))
|
||
(nreverse results))))
|
||
|
||
(defun akk0/extract-all-habits (files)
|
||
(mapcan #'akk0/extract-habits files))
|
||
|
||
(setq akk0/journal-files
|
||
(directory-files "./journal/" t "^w.*\\.org$"))
|
||
|
||
(defun akk0/habits-alist (habits)
|
||
"Transform HABITS list into nested alists: custom-id → day-of-year → habit-data."
|
||
(let (result)
|
||
(dolist (habit habits)
|
||
(let* ((custom-id (plist-get habit :custom-id))
|
||
(day-of-year (plist-get habit :day-of-year))
|
||
(todo-state (plist-get habit :todo-state))
|
||
(body (plist-get habit :body))
|
||
;; Get the alist for this custom-id
|
||
(inner-alist (alist-get custom-id result nil nil #'equal))
|
||
;; Store full data instead of just todo-state
|
||
(habit-data (list :todo-state todo-state :body body :day-of-year day-of-year))
|
||
;; Update the inner alist
|
||
(updated-inner (cons (cons day-of-year habit-data) inner-alist)))
|
||
;; Update result
|
||
(setf (alist-get custom-id result nil nil #'equal) updated-inner)))
|
||
result))
|
||
|
||
|
||
(setq habit-alist (akk0/habits-alist (akk0/extract-all-habits akk0/journal-files)))
|
||
|
||
(defun akk0/get-habit-history (habit-alist custom-id day-number days-back window-size)
|
||
(let* ((inner-alist (alist-get custom-id habit-alist nil nil #'equal))
|
||
(result nil)
|
||
(all-states nil)
|
||
(score-for-state (lambda (state)
|
||
(cond ((equal state "NO") -1)
|
||
((equal state "YES") 1)
|
||
((equal state "EXCELLENT") 2)
|
||
(t 0)))))
|
||
(dotimes (i days-back)
|
||
(let* ((current-day (+ (- day-number days-back) i 1))
|
||
(current-day-str (number-to-string current-day))
|
||
(habit-data (alist-get current-day-str inner-alist nil nil #'equal))
|
||
(todo-state (if habit-data
|
||
(plist-get habit-data :todo-state)
|
||
"NODATA"))
|
||
(body (if habit-data
|
||
(plist-get habit-data :body)
|
||
""))
|
||
|
||
(doy (if habit-data
|
||
(plist-get habit-data :day-of-year)
|
||
""))
|
||
)
|
||
(push todo-state all-states)
|
||
(let* ((window-states (seq-take all-states window-size))
|
||
(rolling-score (apply #'+ (mapcar score-for-state window-states))))
|
||
(push (list :todo-state todo-state
|
||
:score (max 1 (min 5 (/ (+ rolling-score 5) 2)))
|
||
:body body
|
||
:doy doy)
|
||
result))))
|
||
(nreverse result)))
|
||
|
||
(defun akk0/habits-to-html-table (habit-alist day-number days-back window-size)
|
||
"Generate HTML table of habits with rolling scores.
|
||
Rows are custom-ids, columns are days."
|
||
(let ((color-map '(("NODATA" . "grey")
|
||
("YES" . "green")
|
||
("NO" . "red")
|
||
("FREED" . "purple")
|
||
("EXCELLENT" . "blue")))
|
||
(symbol-map '(("NODATA" . "")
|
||
("YES" . "●")
|
||
("NO" . "×")
|
||
("FREED" . "♣")
|
||
("EXCELLENT" . "♦")))
|
||
|
||
(sorted-habits (akk0/sort-habits habit-alist))
|
||
(html ""))
|
||
;; Start table
|
||
(setq html (concat html "<table class='habit-table' style='margin-left: auto; margin-right:auto; margin-bottom: 0.8rem;'>\n"))
|
||
|
||
;; Header row with day numbers
|
||
(setq html (concat html " <tr>\n <th></th>\n"))
|
||
(dotimes (i days-back)
|
||
(let ((day (+ (- day-number days-back) i 1)))
|
||
(setq html (concat
|
||
html
|
||
(cond ((= day day-number) "<th class='habit-click-me'>click me ↓</th>")
|
||
((= 0 (% (- day day-number) 7)) "<th>○</th>")
|
||
(t "<th />"))))))
|
||
(setq html (concat html " </tr>\n"))
|
||
|
||
|
||
;; Data rows - one per habit
|
||
(dolist (entry sorted-habits)
|
||
(let* ((custom-id (car entry))
|
||
(history (akk0/get-habit-history habit-alist custom-id day-number days-back window-size)))
|
||
(setq html (concat html (format " <tr>\n <td class='habit-name' style='padding-right: 20px; padding-top: 5px; padding-bottom: 5px;'><i>%s</i></td>\n" (capitalize (string-remove-prefix "dailies-" custom-id)))))
|
||
|
||
;; Cell for each day
|
||
(dolist (day-data history)
|
||
(let* ((todo-state (plist-get day-data :todo-state))
|
||
(score (plist-get day-data :score))
|
||
(body (plist-get day-data :body))
|
||
(doy (plist-get day-data :doy))
|
||
(body-html (if (and body (not (string-empty-p body)))
|
||
(akk0/org-to-html body)
|
||
""))
|
||
(color (alist-get todo-state color-map nil nil #'equal))
|
||
(symbol (alist-get todo-state symbol-map nil nil #'equal))
|
||
(class (format "habit-brightness-%d" score))
|
||
(style-var (format "--%s%d" color score))
|
||
(escaped-body (replace-regexp-in-string "\"" """
|
||
(replace-regexp-in-string "\n" " " body-html)))
|
||
)
|
||
(setq html (concat html (format " <td class=\"%s habit-cell\" style=\"background-color:var(%s)\" data-body=\"%s\" onclick=\"showHabitPopup(this)\" data-doy=\"%s\" data-activity=\"%s\"
|
||
data-status=\"%s\">%s</td>\n"
|
||
class style-var
|
||
escaped-body
|
||
doy
|
||
custom-id
|
||
todo-state
|
||
symbol)))))
|
||
|
||
(setq html (concat html " </tr>\n"))))
|
||
|
||
(setq html (concat html "</table>\n"))
|
||
|
||
;; Legend and expansion section
|
||
(setq html (concat html "<span class='center'><b>Key:</b>
|
||
<span style='color: var(--grey3);'>Unknown</span> |
|
||
<span style='color: var(--red3);'>× No</span> |
|
||
<span style='color: var(--green3);'>● Yes</span> |
|
||
<span style='color: var(--blue3);'>♦ Excellent</span> |
|
||
<span style='color: var(--purple3);'>♣ Freed Up</span>
|
||
</span>
|
||
<hr />
|
||
<div class='habit-popup' id='habitPopup'>
|
||
<div class='habit-popup-content' id='habitPopupContent'>
|
||
<span class='center'><i>This section intentionally left blank.</i></span>
|
||
</div>
|
||
</div>
|
||
"))
|
||
|
||
html))
|
||
|
||
(akk0/habits-to-html-table habit-alist (string-to-number (format-time-string "%j")) 21 5)
|
||
#+END_SRC
|
||
|
||
**** Expansion Section :autocollapse:
|
||
A bit of JS for enabling the expanding of entries.
|
||
#+BEGIN_SRC javascript :tangle ../html/static/footnote.js
|
||
function formatDayOfYear(dayOfYear, year) {
|
||
const date = new Date(year, 0, dayOfYear);
|
||
|
||
return date.toLocaleDateString('en-GB', {
|
||
day: 'numeric',
|
||
month: 'short',
|
||
year: 'numeric'
|
||
});
|
||
}
|
||
|
||
let selectedCell = null;
|
||
|
||
function showHabitPopup(cell) {
|
||
var bodyHtml = cell.getAttribute('data-body');
|
||
var doy = cell.getAttribute('data-doy');
|
||
var activity = cell.getAttribute('data-activity');
|
||
var status = cell.getAttribute('data-status');
|
||
activity = activity.replace(/^dailies-/, '').replace(/^./, c => c.toUpperCase())
|
||
|
||
if (!bodyHtml) bodyHtml = "<span class='center'><i>This section intentionally left blank.</i></span>"
|
||
|
||
bodyHtml = `<h3><span class="grid ${status}">${activity}</span> — ${formatDayOfYear(parseInt(doy), 2025)}</h3> ${bodyHtml}`
|
||
|
||
document.getElementById('habitPopupContent').innerHTML = bodyHtml;
|
||
|
||
if (selectedCell) {
|
||
selectedCell.classList.remove('habitgrid-selected');
|
||
}
|
||
|
||
cell.classList.add('habitgrid-selected');
|
||
selectedCell = cell;
|
||
enableFootnotes();
|
||
}
|
||
#+END_SRC
|
||
**** CSS :autocollapse:
|
||
Color habit entries based on their completion status:
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.done.YES, .todo.NO, .done.EXCELLENT, .done.FREED {
|
||
display: none;
|
||
}
|
||
|
||
h2:has(.done.YES), h3:has(.done.YES), h4:has(.done.YES), h5:has(.done.YES), .grid.YES {
|
||
color: var(--green3);
|
||
}
|
||
|
||
h2:has(.todo.NO), h3:has(.todo.NO), h4:has(.todo.NO), h5:has(.todo.NO), .grid.NO {
|
||
color: var(--red3);
|
||
}
|
||
|
||
h2:has(.done.EXCELLENT), h3:has(.done.EXCELLENT), h4:has(.done.EXCELLENT), h5:has(.done.EXCELLENT), .grid.EXCELLENT {
|
||
color: var(--blue3);
|
||
}
|
||
|
||
h2:has(.done.FREED), h3:has(.done.FREED), h4:has(.done.FREED), h5:has(.done.FREED), .grid.FREED {
|
||
color: var(--purple3);
|
||
}
|
||
#+END_SRC
|
||
|
||
Style the cells, set legible text color, outline clickable:
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.habit-table th, .habit-table td {
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
padding: 8px;
|
||
max-width: 22px;
|
||
}
|
||
|
||
.habit-table th {
|
||
padding: 0;
|
||
}
|
||
|
||
.habit-click-me {
|
||
font-weight: normal;
|
||
font-size: calc(var(--font-size) * 0.7);
|
||
line-height: calc(var(--line-height) * 0.7);
|
||
text-align: center;
|
||
}
|
||
|
||
.habit-name {
|
||
max-width: 15rem !important;
|
||
}
|
||
|
||
.habit-cell.habit-brightness-1 { color: var(--grey5); outline-color: var(--grey5) !important; }
|
||
.habit-cell.habit-brightness-2 { color: var(--grey5); outline-color: var(--grey5) !important; }
|
||
.habit-cell.habit-brightness-3 { color: var(--grey5); outline-color: var(--grey5) !important; }
|
||
.habit-cell.habit-brightness-4 { color: var(--grey2); outline-color: var(--grey5) !important; }
|
||
.habit-cell.habit-brightness-5 { color: var(--grey3); outline-color: var(--grey5) !important; }
|
||
|
||
.habit-cell:not([data-body=""]) {
|
||
outline: dotted 2px;
|
||
outline-offset: -2px;
|
||
}
|
||
.habitgrid-selected {
|
||
outline: solid 2px var(--purple5) !important;
|
||
outline-offset: -2px;
|
||
}
|
||
#+END_SRC
|
||
|
||
|
||
|
||
*** Header Collapsing :autocollapse:
|
||
Sections can be expanded and collapsed by clicking on their headers; this will assign =.orgjq-expanded= and =.orgjq-contracted= CSS classes as appropriate. Headers with the =:autocollapse:= tag will be collapsed by default (like this section).
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.orgjq-expanded p {
|
||
margin-top: 0;
|
||
padding-bottom: 1.5rem;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.orgjq-contracted > div {
|
||
display: none;
|
||
}
|
||
|
||
.orgjq-contracted h2, .orgjq-contracted h3, .orgjq-contracted h4, orgjq-contracted h5 {
|
||
padding-top: 0.3rem !important;
|
||
padding-bottom: 0.3rem !important;
|
||
}
|
||
|
||
.orgjq-contracted > :first-child::before {
|
||
content: '⮞ ';
|
||
}
|
||
|
||
.orgjq-expanded > h3:has(.tag .autocollapse)::before,
|
||
.orgjq-expanded > h4:has(.tag .autocollapse)::before,
|
||
.orgjq-expanded > h5:has(.tag .autocollapse)::before {
|
||
content: '⮟ ';
|
||
}
|
||
#+END_SRC
|
||
|
||
#+BEGIN_SRC javascript :tangle ../html/static/hide.js
|
||
function isHideable(div_obj) {
|
||
// Ignore for TOC since it is handled differently
|
||
if (div_obj.id === "text-table-of-contents") return false;
|
||
if (div_obj.id === "table-of-contents") return false;
|
||
// No point in hiding top level
|
||
if (div_obj.classList.contains("outline-2")) return false;
|
||
if (div_obj.classList.contains("outline-text-2")) return false;
|
||
return true;
|
||
}
|
||
|
||
function orgjqHide(div_obj) {
|
||
if (!isHideable(div_obj)) return;
|
||
const parent = div_obj.parentElement;
|
||
parent.classList.remove("orgjq-expanded");
|
||
parent.classList.add("orgjq-contracted");
|
||
}
|
||
|
||
function orgjqShow(div_obj) {
|
||
const parent = div_obj.parentElement;
|
||
parent.classList.remove("orgjq-contracted");
|
||
parent.classList.add("orgjq-expanded");
|
||
}
|
||
|
||
function orgjqToggle(div_obj) {
|
||
const parent = div_obj.parentElement;
|
||
if (parent.classList.contains("orgjq-expanded")) {
|
||
orgjqHide(div_obj);
|
||
} else {
|
||
orgjqShow(div_obj);
|
||
}
|
||
}
|
||
|
||
function orgjqEnable() {
|
||
// Called once e.g. the first time the page is loaded
|
||
// handle the click event for each header
|
||
for (let i = 2; i <= 7; ++i) {
|
||
const headers = document.querySelectorAll(`h${i}`);
|
||
headers.forEach(header => {
|
||
header.style.cursor = "pointer";
|
||
header.addEventListener('click', function() {
|
||
// Get the first div sibling after the header
|
||
const parent = this.parentElement;
|
||
const divs = parent.querySelectorAll(':scope > div');
|
||
if (divs.length > 0) {
|
||
orgjqToggle(divs[0]);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Mark everything as open...
|
||
for (let i = 2; i <= 7; ++i) {
|
||
const headers = document.querySelectorAll(`h${i}`);
|
||
headers.forEach(header => {
|
||
const parent = header.parentElement;
|
||
parent.classList.remove("orgjq-contracted");
|
||
parent.classList.add("orgjq-expanded");
|
||
});
|
||
}
|
||
|
||
// ... except TOC ...
|
||
const toc = document.querySelector("div#table-of-contents");
|
||
if (toc) {
|
||
toc.classList.remove("orgjq-expanded");
|
||
toc.classList.add("orgjq-contracted");
|
||
}
|
||
// ... and autocollapse.
|
||
const autocollapse = document.querySelectorAll(".autocollapse");
|
||
autocollapse.forEach(element => {
|
||
const grandparent = element.parentElement?.parentElement;
|
||
if (grandparent) {
|
||
orgjqHide(grandparent);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Run when DOM is loaded
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', orgjqEnable);
|
||
} else {
|
||
orgjqEnable();
|
||
}
|
||
#+END_SRC
|
||
*** Hover Notes :autocollapse:
|
||
Turns org-mode footnotes (=[fn::Like this]=) into hover-notes[fn::Like this].
|
||
#+BEGIN_SRC javascript :tangle ../html/static/footnote.js
|
||
function enableFootnotes() {
|
||
const footnoteRefs = document.querySelectorAll('.footref')
|
||
|
||
footnoteRefs.forEach(ref => {
|
||
const footnoteId = ref.href.split('#')[1]
|
||
ref.setAttribute('data-footnote-id', footnoteId)
|
||
ref.href = 'javascript:void(0)';
|
||
|
||
ref.addEventListener('mouseenter', function() {
|
||
// Find footnote element
|
||
const footnoteId = this.getAttribute('data-footnote-id')
|
||
|
||
const footnoteElement = document.getElementById(footnoteId).parentElement.nextElementSibling.children[0]
|
||
|
||
if (footnoteElement) {
|
||
// Create tooltip container
|
||
const tooltip = document.createElement('div');
|
||
tooltip.className = 'footnote-tooltip'
|
||
tooltip.innerHTML = footnoteElement.innerHTML
|
||
|
||
// Position tooltip
|
||
const rect = this.getBoundingClientRect();
|
||
tooltip.style.position = 'absolute'
|
||
tooltip.style.top = (rect.bottom + window.scrollY + 5) + 'px'
|
||
tooltip.style.left = rect.left + 'px'
|
||
|
||
// Add to page
|
||
document.body.appendChild(tooltip)
|
||
|
||
// Store reference for cleanup
|
||
this._tooltip = tooltip
|
||
}
|
||
});
|
||
|
||
ref.addEventListener('mouseleave', function() {
|
||
// Remove tooltip
|
||
if (this._tooltip) {
|
||
this._tooltip.remove()
|
||
this._tooltip = null
|
||
}
|
||
});
|
||
});
|
||
|
||
// Hide footnotes section
|
||
document.querySelector("#footnotes").style.display='none'
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', enableFootnotes);
|
||
#+END_SRC
|
||
|
||
|
||
** Appearance
|
||
*** Global :autocollapse:
|
||
**** Variables
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
:root {
|
||
--site-width: 1000px;
|
||
--vert-content-margin: 0.3rem;
|
||
|
||
--font-size: 1.2rem;
|
||
--line-height: 1.6rem;
|
||
--font-family: "Source Serif Pro";
|
||
|
||
--scale: 2;
|
||
}
|
||
#+END_SRC
|
||
**** Color Variables
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
:root {
|
||
--green1: #123218;
|
||
--green2: #254f1b;
|
||
--green3: #356c22;
|
||
--green4: #5e8e40;
|
||
--green5: #85aa5f;
|
||
|
||
--blue1: #1b2459;
|
||
--blue2: #153a79;
|
||
--blue3: #2f5394;
|
||
--blue4: #417eaf;
|
||
--blue5: #69a8c6;
|
||
|
||
--purple1: #2e1e58;
|
||
--purple2: #493281;
|
||
--purple3: #6a45a6;
|
||
--purple4: #855aa3;
|
||
--purple5: #9b77b5;
|
||
|
||
--red1: #4b1313;
|
||
--red2: #682017;
|
||
--red3: #84301c;
|
||
--red4: #9c4830;
|
||
--red5: #c46849;
|
||
|
||
--grey1: #202124;
|
||
--grey2: #33333b;
|
||
--grey3: #605b66;
|
||
--grey4: #a79fa7;
|
||
--grey5: #c5bcbc;
|
||
|
||
--accent1: var(--purple4);
|
||
--link: var(--blue3);
|
||
--black: var(--grey1);
|
||
--grey: var(--grey3);
|
||
--background: var(--grey5);
|
||
--background-tooltip: var(--grey5);
|
||
}
|
||
#+END_SRC
|
||
**** Background image
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
body::before,
|
||
body::after {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0;
|
||
height: 100vh;
|
||
width: 50%;
|
||
background-image: url('/static/images/background.png');
|
||
background-repeat: no-repeat;
|
||
z-index: -1;
|
||
|
||
image-rendering: pixelated; /* For Chrome/Safari */
|
||
image-rendering: -moz-crisp-edges; /* For Firefox */
|
||
image-rendering: crisp-edges; /* Fallback */
|
||
|
||
background-size: calc(960px * var(--scale)) calc(1080px * var(--scale));
|
||
}
|
||
|
||
body::before {
|
||
left: 0;
|
||
background-position: top right;
|
||
}
|
||
|
||
body::after {
|
||
right: 0;
|
||
background-position: top right;
|
||
/* Flip the right side horizontally */
|
||
transform: scaleX(-1);
|
||
}
|
||
#+END_SRC
|
||
|
||
***** TODO This is a weird approach and has some issues
|
||
- White line in center in some setups
|
||
- Crunchy lines depending on DPI
|
||
|
||
**** Fonts
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
body {
|
||
font-family: var(--font-family);
|
||
font-size: var(--font-size);
|
||
line-height: var(--line-height);
|
||
word-spacing: 0.25ch;
|
||
font-weight: 400;
|
||
}
|
||
#+END_SRC
|
||
|
||
**** Header/Content/Footer blocks
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
#content, #header, #footer {
|
||
min-width: 500px;
|
||
max-width: min(var(--site-width), 90vw);
|
||
margin: 0 auto;
|
||
padding: 5px 25px;
|
||
|
||
border: 2px double var(--grey1);
|
||
margin-bottom: 1.2rem;
|
||
background-color: var(--background);
|
||
}
|
||
|
||
#header {
|
||
display: flex;
|
||
}
|
||
|
||
#content, #footer {
|
||
box-shadow: 7px 7px 7px var(--grey1);
|
||
}
|
||
|
||
#footer {
|
||
padding-top: 1rem;
|
||
padding-bottom: 1rem;
|
||
}
|
||
|
||
#header a, #header a:visited{
|
||
color: var(--purple2);
|
||
}
|
||
|
||
#header hr {
|
||
margin-bottom: var(--vert-content-margin);
|
||
}
|
||
|
||
#footer hr {
|
||
margin-top: var(--vert-content-margin);
|
||
}
|
||
|
||
#content h2, #content h3, #content h4, #content h5 {
|
||
margin-bottom: 1rem;
|
||
margin-top: 0.5rem;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
#content h2 {
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
#content h3, #content h4 {
|
||
padding-top: 0.5rem;
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
|
||
#content h5 {
|
||
padding-top: 0.3rem;
|
||
padding-bottom: 0.3rem;
|
||
}
|
||
|
||
#+END_SRC
|
||
*** Colors :autocollapse:
|
||
**** Color Assignments
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
body {
|
||
color: var(--black);
|
||
background-color: var(--background);
|
||
}
|
||
|
||
#+END_SRC
|
||
**** Syntax Highlighting
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
pre {background-color:var(--grey1); color:var(--grey5);}
|
||
pre span.org-builtin {color:var(--blue4);font-weight:bold;}
|
||
pre span.org-string {color:var(--green4);}
|
||
pre span.org-keyword {color:var(--purple5);font-weight:bold;}
|
||
pre span.org-variable-name {color:var(--green5);font-style:italic;}
|
||
pre span.org-function-name {color:var(--blue5);}
|
||
pre span.org-type {color:var(--purple4);}
|
||
pre span.org-preprocessor {color:var(--grey5);font-weight:bold;}
|
||
pre span.org-constant {color:var(--red5);}
|
||
pre span.org-comment-delimiter {color:var(--grey3);}
|
||
pre span.org-comment {color:var(--grey3);font-style:italic}
|
||
pre span.org-outshine-level-1 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-2 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-3 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-4 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-5 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-6 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-7 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-8 {color:var(--grey5);font-style:italic}
|
||
pre span.org-outshine-level-9 {color:var(--grey5);font-style:italic}
|
||
pre span.org-rainbow-delimiters-depth-1 {color:var(--grey4);}
|
||
pre span.org-rainbow-delimiters-depth-2 {color:var(--blue4);}
|
||
pre span.org-rainbow-delimiters-depth-3 {color:var(--green4);}
|
||
pre span.org-rainbow-delimiters-depth-4 {color:var(--red4);}
|
||
pre span.org-rainbow-delimiters-depth-5 {color:var(--purple4);}
|
||
pre span.org-rainbow-delimiters-depth-6 {color:var(--blue4);}
|
||
pre span.org-rainbow-delimiters-depth-7 {color:var(--green4);}
|
||
pre span.org-rainbow-delimiters-depth-8 {color:var(--red4);}
|
||
pre span.org-rainbow-delimiters-depth-9 {color:var(--purple4);}
|
||
pre span.org-sh-quoted-exec {color:var(--purple3);}
|
||
pre span.org-doc {color:var(--green5);font-style:italic;}
|
||
pre span.org-css-selector {color:var(--blue5);font-weight:bold;}
|
||
pre span.org-css-property {color:var(--purple4); font-weight: bold;}
|
||
#+END_SRC
|
||
|
||
*** General CSS :autocollapse:
|
||
**** Et Cetera
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
tbody { border-bottom: 1px dotted var(--grey1); }
|
||
thead { border-bottom: 1px solid var(--grey1); }
|
||
th, td { padding-right: 4rem; }
|
||
th.org-left { text-align: left; }
|
||
|
||
hr {
|
||
border: 0;
|
||
border-top: 2px dotted var(--grey1);
|
||
}
|
||
|
||
a, a:visited {
|
||
color: var(--link);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.figure-number {
|
||
display: none;
|
||
}
|
||
|
||
.caption {
|
||
padding-top: 0.5rem;
|
||
font-style: italic;
|
||
}
|
||
|
||
.outline-3, .outline-4, .outline-5 {
|
||
padding-left: 0.6rem;
|
||
border-radius: 0.2em;
|
||
margin: 0.7rem;
|
||
}
|
||
|
||
.outline-text-3, .outline-text-4, .outline-text-5 {
|
||
margin: 0.3rem 1rem 0.5rem 0.5rem;
|
||
}
|
||
|
||
h2 {
|
||
line-height: 1.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
h3, h4, h5 {
|
||
text-align: center;
|
||
}
|
||
|
||
.section-number-1, .section-number-2, .section-number-3, .section-number-4 { display: none; }
|
||
|
||
ul { margin-top: 0; }
|
||
|
||
#+END_SRC
|
||
|
||
**** Utility Classes
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.center {
|
||
display: block;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
text-align: center;
|
||
}
|
||
|
||
.navbar-link {
|
||
margin-right: 5px;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
.center, div.center {
|
||
text-align: center;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.center figure,
|
||
.center figcaption,
|
||
div.center figure,
|
||
div.center figcaption {
|
||
text-align: center;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.multi-img {
|
||
display: flex;
|
||
justify-content: center;
|
||
text-align: center;
|
||
margin: 20px;
|
||
gap: 20px;
|
||
}
|
||
|
||
.multi-img figure { margin: 0px; }
|
||
|
||
.poetry {
|
||
padding-top: 4rem;
|
||
padding-bottom: 4rem;
|
||
text-align: center;
|
||
}
|
||
#+END_SRC
|
||
|
||
*** Elements :autocollapse:
|
||
**** Looking for Work
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
#looking-for-work {
|
||
background-color: var(--red2);
|
||
color: var(--grey5);
|
||
padding: 5px 25px;
|
||
margin-left: auto;
|
||
margin-top: -0.32rem;
|
||
margin-bottom: -0.32rem;
|
||
margin-right: -1.55rem;
|
||
border: 2px double var(--purple5);
|
||
}
|
||
|
||
#looking-for-work a, #looking-for-work a:visited {
|
||
color: var(--blue5) !important;
|
||
}
|
||
#+END_SRC
|
||
**** Code Blocks
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.src, .example {
|
||
font-family: monospace;
|
||
font-size: 1rem;
|
||
}
|
||
#+END_SRC
|
||
**** Publish/modified Date
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
#publish-date, #modified-date {
|
||
font-style: italic;
|
||
}
|
||
#+END_SRC
|
||
**** Table of Contents
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
#table-of-contents {
|
||
z-index: 1;
|
||
margin-top: 105px;
|
||
margin-right: 5%;
|
||
font-size: calc(var(--font-size) * 0.8);
|
||
position: fixed;
|
||
right: 0em;
|
||
top: 0em;
|
||
background: var(--background-toc);
|
||
text-align: right;
|
||
min-height: 3rem;
|
||
|
||
box-shadow: 0 0 0.5em var(--shadow-toc);
|
||
-webkit-box-shadow: 0 0 0.5em var(--shadow-toc);
|
||
-moz-box-shadow: 0 0 0.5em var(--shadow-toc);
|
||
-webkit-border-bottom-left-radius: 5px;
|
||
-moz-border-radius-bottomleft: 5px;
|
||
/* ensure doesn't flow off the screen when expanded */
|
||
max-height: 80%;
|
||
overflow: auto;
|
||
}
|
||
#table-of-contents h2 {
|
||
font-size: 13pt;
|
||
max-width: 9em;
|
||
border: 0;
|
||
font-weight: normal;
|
||
margin-top: 0.75em;
|
||
margin-bottom: 0.75em;
|
||
padding-left: 0.5em;
|
||
padding-right: 0.5em;
|
||
padding-top: 0.05em;
|
||
padding-bottom: 0.05em; }
|
||
#table-of-contents #text-table-of-contents {
|
||
display: none;
|
||
text-align: left; }
|
||
#table-of-contents:hover #text-table-of-contents {
|
||
display: block;
|
||
padding: 0.5em;
|
||
margin-top: -1.5em;
|
||
padding-right: 20px;
|
||
}
|
||
|
||
#table-of-contents {
|
||
display: none;
|
||
}
|
||
#+END_SRC
|
||
**** To-Dos
|
||
#+BEGIN_SRC css :tangle ../html/static/style.css
|
||
.TODO { color: var(--red3); }
|
||
.DONE { color: var(--green3); }
|
||
#+END_SRC
|