Files
blog/org/source.org

1144 lines
34 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+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 "\"" "&quot;"
(replace-regexp-in-string "\n" "&#10;" 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
#+BEGIN_SRC css :tangle ../html/static/style.css
.footnote-tooltip {
position: 'absolute';
z-index: 1000;
background-color: var(--background-tooltip);
border: 1px solid var(--accent1);
border-radius: = 4px;
padding: 8px 12px;
max-width: 300px;
font-size: calc(var(--font-size) * 0.9);
box-shadow: 1px 1px 1px var(--grey2);
}
#+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", "Noto Sans Symbols 2";
--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
*** No unstyled content :autocollapse:
#+BEGIN_SRC css :tangle ../html/static/style.css
html {
visibility: visible;
opacity: 1;
}
#+END_SRC