The wiki as a static site
What this page is #BNH91U
The wiki is a set of Org files, and Org is pleasant to write in Emacs but
awkward to read anywhere else. This page records how the wiki becomes a
browsable static site, why the build is expressed in ASDF, and what the
Org-subset reader deliberately does and does not understand. It is a
design page for luv-wiki, the Lisp system in wiki/org.lisp,
wiki/html.lisp, and wiki/asdf.lisp, and for the corpus system
luv-wiki-site in luv-wiki-site.asd whose components are the pages themselves.
The rendered site lives at mbrock.github.io/luv once the Pages
workflow in wiki.yml has run; locally, make wiki writes it to
build/wiki/. Besides the pages it carries a figure index and, per
#58SVCM, the whole source of the repository's four primary system families as
browsable dexp pages.
Why ASDF, and how the wiki slots into it #RYHRUL
ASDF describes itself as a tool that "plays a role like make or ant" for Lisp
systems, and it warns that it is not a general installer. Underneath, though,
its model is not Lisp-specific: a component has input files, an operation
applied to a component has output files, perform does the work, and the plan
is a dependency graph over (operation, component) actions with timestamps.
The manual invites extension by subclassing operation and component and
defining perform, input-files, output-files, and component-depends-on
methods. cffi-toolchain uses exactly this to compile .c files: a c-file
component whose compile-op outputs an object file.
The wiki fits the same shape, and more naturally than a fresh script would:
- An
org-fileis a component of theluv-wiki-sitesystem. Itsload-opreads the page into adocumentobject and remembers it on the component, so "loading the wiki" means having the corpus in the image as inspectable Lisp objects, which is where the figure vocabulary of #F2N8VX wants to go anyway.org-fileinherits fromstatic-file, so the ordinarycompile-opis a no-op and dependency propagation works unchanged. render-opis a downward, selfward operation: rendering the system renders each page; rendering a page first loads it and, because#ABC123mentions and backlinks cross pages, also depends on the load of the whole system. Every page'sinput-filesare all the pages, so any edit re-renders the site; ASDF's timestamps make an unchanged corpus a no-op.- The site is a build product, not a fasl.
output-filesreturns its second value true, which ASDF documents as "do not translate," so pages land inbuild/wiki/rather than the compiled-file cache. Astatic-filesuch as an image is copied by the same operation; the stylesheet is compiled from Lisp definitions (#UODI74) by the system's own render. :build-operation "luv.wiki:render-op"makes(asdf:make :luv-wiki-site)build the site, matching howluvcraftis built.
Two ASDF details are worth remembering. defsystem-depends-on cannot name a
system defined in the same .asd file (ASDF reports a circular define-op),
so the tooling is the primary system luv-wiki in its own file and the corpus
is the primary luv-wiki-site system in another. Class names in a defsystem can be given as strings
such as "luv.wiki:org-file", which ASDF reads in its own package; that avoids
interning anything into ASDF's package, which the manual asks extensions not to
do.
Capture recipes are source; captured media is a cache #IVRWI8
Pictures that carry design evidence should be reproducible without making Git
the video store. capture-specification is the semantic boundary: a stable
name, wiki figure ID, media kind, extension, description, and an ordinary named
renderer function. define-capture is only a thin definition-site macro over
that object and function. The body may assemble a gazetteer world, borrow an
atelier camera flight, or set up a focused agent interaction; it still ends in
the renderer which already owns those things.
The deterministic filename joins figure and recipe, for example
Y7X7WK-construction-proposal-still.png. A bare
[[capture:Y7X7WK-construction-proposal-still.png]] paragraph transcludes it
from media/; MP4 and WebM links become native, muted, looping video figures.
The HTML build does not require the bytes to exist. This is important: prose
and recipes remain cheap and portable, while a local build without the artifact
has an honest missing image rather than silently substituting stale evidence.
capture-op makes (asdf:make :luv/showcase) render the registered set into
build/wiki/media/, which is already under the ignored build/ tree. Each
run also writes a small manifest naming the source revision and every figure,
recipe, kind, and filename. scripts/captures list discovers the set and
scripts/captures render can select a few recipes during iteration. Captures
announce progress because shader compilation, readback, and video encoding are
real GPU work, not a reason for a silent build.
Execution placement is deliberately outside this graph. A GPU worker runs the same command through SSH, but its hostname and checkout path are deployment configuration, never recipe data. Git annex is likewise a publication cache, not part of capture identity: the web host explicitly obtains each annexed file before Caddy serves it. A pointer alone is never mistaken for a browser artifact.
Mentioned in: The showcase is an atlas with three lanes, Publish generated media out of band
Referenced from code: A named recipe for one generated wiki image or video. The semantic metadata is inspectable and retained in source; Create the small public-index derivative for captured media at Image originals get 480w and 768w WebPs whenever those are true downscales.
Films get a card-sized WebP poster from their first frame. Originals remain
untouched and retain their stable capture identity. #IVRWI8defclass capture-specification capture-specification.lisp:9 ↗
renderer is the
ordinary named function installed by define-capture. Rendered bytes live
under the capture output directory and do not belong in Git. #IVRWI8defun prepare-capture-web-media capture-specification.lisp:177 ↗
pathname.defclass showcase-page showcase.lisp:9 ↗
DONE Make wiki media executable without committing it #S6I9C2
Intent. Establish the smallest end-to-end path from a figure-linked Lisp recipe to ignored image or video bytes and a wiki transclusion, while leaving machine selection and artifact publication open.
Evidence. Two #Y7X7WK recipes construct the same inert six-block wall and gnome on a hidden luvcraft canvas. One captures the final focused camera and one records its four-second orbit. The registry has live-redefinition and stable-path tests; the wiki renderer tests image and video transclusion; and the generated manifest binds both artifacts to their source revision.
Done when. A recipe is inspectable and selectable, ASDF can render the set, the wiki can show both media kinds without tracking their bytes, and no source file names a GPU host.
DONE Publish generated media out of band #0BGP3P
Intent. Put durable capture bytes somewhere the static site can fetch without growing the ordinary source clone, and run expensive recipes on a configured GPU executor.
Evidence. The local contract in #IVRWI8 supplies deterministic paths and a
source-revision manifest. scripts/showcase now gives that contract an
ordinary two-machine deployment:
origin/main -> chapel:~/luv-showcase -> capture recipes
| |
`-> ~/luv-showcase-media -> annex content
| |
`----------> swa:/var/www/luv/showcase
origin/main --------------------------------> swa:/srv/luvThe separate showcase-media branch contains tiny annex pointers and the
plain-text manifest, so normal source history stays light. Chapel is the
authoritative annex content store. SWA fast-forwards both branches, obtains
the bytes directly from Chapel, verifies that no referenced artifact is
missing, and only then restarts the web service. Caddy's existing /video/
static route exposes those files while the Lisp web application reads the
manifest and renders /showcase/. This split keeps byte serving out of the
character-oriented HTTP implementation.
The common tailnet layout is deliberately boring and recorded as overrideable
.envrc defaults: hosts chapel and swa, source checkouts
~/luv-showcase and /srv/luv, and media checkouts
~/luv-showcase-media and /var/www/luv/showcase. A differently named host
or checkout only needs the corresponding LUV_SHOWCASE_* environment value.
The whole trip is:
make showcase-publishFor diagnosis or iteration, showcase-bootstrap establishes clones and annex
remotes, showcase-render only captures and publishes media,
showcase-deploy only obtains it and restarts the service, and
showcase-status reports both machines and the public page. Each remote
checkout must be clean and can advance only by fast-forward; a human edit on a
deployment machine stops publication instead of being overwritten.
Operational evidence. On 21 August 2026 Chapel rendered source
35ef9bafd893 through its Radeon Vulkan device, committed capture manifest
aaba121 on showcase-media, and served both annex keys to SWA over its
explicit pinned git-annex-shell. https://luv.swa.sh/showcase/ then returned
the manifest-backed page; Caddy returned image/png for the 960x640 still and
video/mp4 for the 960x640, 24 fps, four-second H.264 orbit. A cold service
restart became healthy on its sixth one-second connection attempt.
Done when. The public showcase has been exercised end to end, including real image and video responses.
IDEA Give historical captures an annex retention policy #9H2KVT
Intent. Decide which historical annex keys remain reachable when recipes or figures change, without turning the ordinary source clone into a media archive.
Evidence. The initial publication keeps current keys in Chapel and SWA and
keeps their metadata on the git-annex branch. That is sufficient for current
showcase publication but deliberately says nothing yet about pruning superseded
captures or retaining named milestones.
Done when. Publication has an explicit, inspectable rule for current, milestone, and unreferenced capture keys, plus a recovery story if Chapel is rebuilt.
The Org subset the reader understands #NS35X9
The reader is not a general Org parser. It reads the subset the wiki actually
uses, chosen by surveying the pages: #+title, headings with property drawers,
paragraphs, bulleted and numbered lists with wrapped and blank-separated
items, example and src blocks, simple tables with an optional header rule,
TeX math as $x^2$ or \(...\) inline and \[...\] or $$...$$ on
display (drawn by KaTeX, loaded only when a page has math), Mermaid
diagrams in #+begin_src mermaid blocks (likewise), bracket links with
file:, https:, id:, lisp:, capture: or no scheme, a bare
file: link to an image in the wiki directory as an inline image (a
paragraph of only that link becomes a figure, and a paragraph of only a
lisp: link becomes the definition it names), a bare capture: image or video
as ignored generated media under build/wiki/media/, the light #ABC123 mention,
and the six emphasis markers with Org's pre- and post-character rules. Anything else stays paragraph text, so an unfamiliar construct can
never fail the build; it can only render plainly.
Emphasis rules follow Org: a marker opens only after whitespace or an opening
bracket and closes only before whitespace or punctuation, so 2 * 3,
foo_bar, and ../src/ are text. Verbatim and code keep their contents raw.
Emphasis may cross one line break but not a blank line. Headings whose title
starts with one of the work-mark words of #W3K9MK carry that word as a
keyword and render with a status badge.
The element model is CLOS: document, heading, paragraph, plain-list,
list-item, example-block, src-block, and table at the block level;
emphasis, link, and mention inline, with plain strings for text.
render-html is a generic function over those classes, so a new element is
one class and one method. reference-id is the small protocol that lets both
mentions and id: links count as references when the site indexes backlinks.
The reader was written rather than borrowed. deepfire's cl-org-mode (2018, built on parser-combinators) parses headline, drawer, and block structure but leaves section bodies as text and has no inline layer, links, lists, or HTML output, so it would have supplied perhaps a hundred lines and cost two dependencies and a second parser for the inside of sections. Its layered raw-AST/dressed-object design is a good precedent for the shape used here.
Spinneret as the HTML layer #VRKEYI
Spinneret's with-html compiles keyword forms to tags and passes the value of
every other body form to its exported generic spinneret:html. Three
consequences shaped the renderer:
ecaseinsidewith-htmlis misread, because its clause heads look like tags; a small kind-to-tag table with:tag :nameis used instead.- The pretty printer fills text and buffers a space after each write, so a
dynamically written
"figure"came out as"figure "inside its<b>; output is written with*print-pretty*off and*suppress-inserted-spaces*on, and rendering methods return no values so Spinneret has nothing to space. - A bare string at the top of
with-htmlis returned, not printed, so text is written explicitly withspinneret:html.
Mentions carry their target with them: each page embeds a small card per
figure it mentions (title, page, status, opening prose) and per definition
it links (kind, name, lambda list, file and line, docstring), and a few
lines of script in site.js show the card as a top-layer popover beside
the link on hover, focus, or a first tap, placed below the link or above it
when the viewport is short. The same script loads KaTeX and Mermaid from
a CDN only for pages that carry math or diagrams. The site stays static
and the links stay plain links.
Each page is one HTML file: a header linking to the index and the page's Org
source on GitHub, an <h1> from #+title, and a <section> per heading whose
id is the figure ID, so page.html#ABC123 is the figure's stable address.
Mentions become links with the target's title as a tooltip; dangling ones are
struck through. Each figure that is mentioned elsewhere ends with a
"Mentioned in" line, the disposable backlink index that #F2N8VX anticipates.
A figures.html page lists every figure by page with its work-mark status.
DONE Render the wiki as a static site from ASDF #QH1ONS
Intent: make the wiki browsable outside Emacs without a second source of truth, and express the build as an ASDF operation so it composes with the rest of the project.
Evidence: (asdf:make :luv-wiki-site) renders all pages, figures.html, and the
stylesheet into build/wiki/; a second make wiki rewrites nothing and
touching a page re-renders. The corpus loads with no dangling mentions.
luv-wiki/test covers the reader, the site index, the rendered HTML, and the
render-op output placement.
Done when: the six tests pass and the site builds from a fresh SBCL. Publishing is the next mark.
The wiki from a shell: scripts/wiki #GB4FT0
Agents and people who want the table of contents, the work marks, one
figure's text, or the places a figure is mentioned should not need Emacs
or a browser. scripts/wiki is a small executable built with ASDF's
program-op (system luv-wiki/cli, entry point luv.wiki.cli:main,
build/wiki-cli), rebuilt by its wrapper when the tooling changed and run
inside the Nix shell. It reads the same objects the site renders — pages
through the Org reader, code through the Eclector index — so what it prints
agrees with the site. Commands: toc, marks, figure, page,
mentions, defs, dangling, ids, build; help lists them.
Three details of the executable are worth recording. The Nix sbcl wrapper
supplies CL_SOURCE_REGISTRY and ASDF_OUTPUT_TRANSLATIONS to SBCL
processes only, so a dumped image would look for compiled files in the
wrong place; wiki/build.lisp captures that environment before the dump
and main re-establishes it. The build command registers luv.asd,
luvcraft.asd, luv-wiki.asd, and luv-wiki-site.asd before
asking ASDF to render, so the source browser sees every component tree without
loading the GPU, game, or native systems. make wiki uses that command rather
than a partial fresh image that knows only the two wiki ASD files.
DONE Serve the site from GitHub Pages #SIWPDS
Intent: publish build/wiki/ at mbrock.github.io/luv on every push
that touches the wiki or its tooling.
Evidence: wiki.yml installs Nix, restores the closure with hestia
(chunked GitHub Actions cache; about a minute warm, replacing the
sunsetting magic-nix-cache), runs make wiki, and deploys with the Pages
actions. Lisp and ASD changes trigger the workflow because they can change
the derived source browser even when no Org page changed. Pages had to be enabled once by hand with
gh api -X POST repos/mbrock/luv/pages -f build_type=workflow; the
workflow token cannot create the site. The index and source pages are
reachable at the public URL.
Done when: the workflow has succeeded once and the index page is reachable at the public URL. Both hold.
A fluid dual system: code contributes to the wiki #F6B3NI
The wiki does not want to become Lisp forms; Org in Emacs is the better writing surface, and the reader already gives the corpus to Lisp as objects. The interesting direction is the reverse flow: the code contributing to the discourse where that is more natural than prose, with less text and more references and examples. Two facts make that cheap.
Figure IDs are already valid, readable symbol names, and index.org anticipated
a WIKI package of them (#F2N8VX). And code already mentions figures: the
interpret-quantity-specification docstring in arithmetic/semantics.lisp cites
#PLRP3A and the tests in
arithmetic/tests.lisp cite #LNRY72 as the table they check. Nothing yet
sees those mentions. Three rungs, cheapest first:
- The site sees code.
render-opasks ASDF for thecl-source-filecomponents of the luv systems (pathnames only, no loading), scans them for#IDmentions, and each figure gains a "Referenced from" line beside "Mentioned in", linking to the source line on GitHub. Backlinks then span discourse and code, which is the point of string figures. - A WIKI package.
load-opinterns and exports each figure ID intoWIKIand hangs the heading off the symbol as documentation or a property. The figure index becomes the package: apropos, completion, and CLIMacceptwork on figures. A no-op macro(wiki:ref LNRY72)is macroexpanded, sowho-macroexpandsfinds every code reference, and it can note(sb-c:source-location)at compile time into the same table the scanner fills, with plain text as the fallback so the site never depends on the whole system compiling. A symbol should mean only an identity; the heading is resolved late through the index, never stored in code. - The reverse direction. Wiki prose naming a Lisp symbol could resolve to a definition location when the system is loaded, which is what a McCLIM browser does natively with a symbol presentation and an Edit Definition translator. Do it there first.
The 2021 "chronicle" experiment, dummy macros over dated todo, hmm,
done entries so xref finds notes embedded in source, is the same shape as
work marks with the status words renamed; what it adds is the reminder that
symbols in a package are the identity mechanism and that macroexpansion is
free cross-referencing.
Mentioned in: The site shows code references to figures, A WIKI package of figure symbols
The model a McCLIM browser wants #AGTIO4
The loaded documents on the org-file components are already the browser's
data source. What the element model still lacks is small and each piece is
motivated by a browser affordance:
figureandwork-markas classes decided at read time, so presentation types map onto classes one to one and each needs onepresentmethod.- Parent links and source line ranges on elements, for "which figure am I in", an Edit Figure command that jumps Emacs to the Org line, and HTML source links that point at the line.
- Mentions stay IDs, not object pointers; the site or index resolves them.
- A second renderer, not a second traversal: when a CLIM presenter exists,
render-htmlshould become(render backend element)dispatching on the relationship, withhtmlandclimbackends over one model. Two consumers justify that; one does not.
The live loop is then: edit Org in Emacs, (asdf:load-system :luv-wiki-site)
re-reads only the changed page, the browser redisplays. Mentions become
presentations that jump on click and offer backlinks on right-click; the
shader lab's presentation types could accept a figure to attach it to a
definition.
Mentioned in: Dexp on McCLIM, A wiki browser frame in mcluv
Code views: a dexp renderer for Common Lisp #N06GKK
Source links that leave the page for GitHub are a poor reading experience,
especially on a phone. The wiki should show code in place, and the way it
should show it comes from wisp's structure editor, whose "dexp" renderer draws
Lisp as nested boxes rather than text with indentation:
The mechanism is small and, since flexbox does the work, responsive for
free. Every cons is a div.list with display: flex; flex-wrap: wrap and a
small column gap; atoms are inline spans. Forms flow horizontally while they
fit and wrap when they do not, so there is no line-breaking logic anywhere.
The parentheses are the box's own left and right borders with rounded
corners, so a wrapped list becomes a bracket around a paragraph of code, and
depth is visible as nesting rather than indentation. Structure is carried in
attributes, data-callee on the list and data-package and
data-symbol-name on symbols, and CSS does the rest: body forms of defun or
let take full width so they stack; quote, backquote, and unquote hide their
symbol and paint ', `, and , with ::before; keywords get their colon
the same way; the current package's prefix is hidden; the head of a list is
coloured by :first-child with no separate classification pass. Ordinary
prose text and code then share one flowing layout, which is what a wiki
wants.
Two things change in the Common Lisp version. Dexp renders sexp data, so
comments and original formatting are gone; the wiki shows source
definitions and needs a reader that keeps them, see #SKNK2N. And dexp keeps
per-operator layout in CSS selectors that cannot see method qualifiers or
lambda-list keywords; the wiki renderer should assign roles in Lisp, using
the same table the parinfer checker already keeps in parinfer/implementation.lisp
(*binding-form-heads*, *clause-form-heads*, child-role), and emit
class"head"= or class"body"= so the stylesheet needs only
.list > .body { width: 100% }. Everything else transfers verbatim: side
borders as parentheses, quote glyphs, keyword colons, home-package hiding,
head colouring, and ; comments as full-width children.
The same renderer serves three places: the "Referenced from" backlinks of
#PF39II, where a figure's code references expand in place; a Give a compatible anonymous This is a semantic interpretation, never a numerical unit conversion. An
already named quantity may only retain its name; anonymous derived results may
acquire one when their dimension, exact unit, and tensor order agree with
lisp: link
protocol, [[lisp:luv.arithmetic:interpret-quantity-specification]], that
resolves against a definition index and shows the definition; and the wiki's
own #+begin_src lisp blocks, which are read and drawn structurally with
the <pre> as fallback whenever reading fails. A paragraph consisting only
of a lisp: link becomes the definition itself, drawn from luv's source at
build time:defun interpret-quantity-specification semantics.lisp:1010 ↗
derived specification an explicit meaning.interpretation. Character must agree too, with one deliberate exception: a
signed difference may be interpreted as an absolute. That is the explicit
promotion the affine algebra otherwise never performs — the author asserts
the amount is non-negative, and no lowering checks it (#PLRP3A). Points
never cross to or from the other characters here.
Inside a definition, #ID mentions in strings and comments are links to
their figures, so the loop closes: the docstring above points back at
#PLRP3A, and that figure lists this definition under "Referenced from
code".
Mentioned in: The site shows code references to figures, The dexp HTML renderer
Layouts are objects chosen by operator and role #BGIM3I
The first renderer assigned layout with a table of head counts and a
growing set of role strings and special cases; let bindings drawn as a
two-column grid, loop keywords starting rows, and lambda lists whose
sublists are (var default) clauses made that untenable. The design that
replaced it follows clos-design: a layout is an object, and a list is
drawn by the layout that its operator and the role its parent gave it
select together (dispatch on the relationship, not on one participant).
list-layout (list role)chooses: a child the parent calledbindingsgets agrid-layoutwhatever its first element;lambda-listgets alambda-list-layout;clauseorstacked-clausetheir clause layouts; otherwise the operator's own layout from a small table (define-layout), withwith-=/=define-=/=defnaming conventions as the default andflow-layoutlast.child-role (layout list index child)answers the role of each argument (comments are drawn where they occur and never counted):body-layoutstacks after a head count,bindings-layoutmarks the binding list,variables-layout(multiple-value-bind) marks a variable list,spec-layout(dolist,with-open-file) marks one clause,lambda-layoutandmethod-layoutfind the lambda list,clauses-layout(cond,case,handler-case,defstruct) makes body forms stacked clauses,grid-layoutmakes children clauses,loop-layoutgives loop keywordsrow-start.render-layout (layout list)draws: the general routine with roles, or a clause's name and.restcell so a grid can align columns.
Hand-written layouts are only hints for the standard forms whose lambda
lists say too little (let, cond, defmethod, loop). For everything
else, luv's own macros above all, the layout is derived from the real
lambda list, the way SLY's slynk-indentation derives indentation from
macro arglists: scripts/wiki introspect loads the systems, resolves every
operator symbol the corpus uses against its file's in-package, records
its kind and sb-introspect:function-lambda-list, and writes
arglists.sexp, one readable form per operator, keyed by home package
(2151 operators today). From such a lambda list a derived-layout knows
that what precedes &body is the head, that a destructuring pattern in a
position makes that argument a clause (with-block-content-storage
((domain palette indices) chunk &body body)), that &key marks where
pairs begin, and, from SBCL's own parameter names, that BINDINGS,
DEFINITIONS, or SLOT-SPECS mean a binding grid and LAMBDA-LIST or
VARS a lambda list. The file is committed, so the site renders without
loading luv, and it is among render-op's inputs; run the pass again after
adding or changing macros.
Two rules apply across layouts. A trailing run of :keyword value
arguments is drawn pair by pair in a .pair container that never breaks
between key and value; where the pairs are body forms, as in the options
of a definer, each pair is a row with the keys aligned, so define-unit
and define-quantity read as small tables. And an operator known only by
its def prefix is inspected rather than assumed: a list in second
position makes it a lambda form, anything else a name followed by options.
The stylesheet knows only roles: a list with body forms is .stacked, a
grid of rows with the head in a .head span (#993QQQ); .bindings is a
CSS grid, its .clause children subgrids spanning two columns; a
.clauses form is a two-column table of .stacked-clause rows
(#4175NC); .rest a cell, .pair an inline unit, .break a zero-height
row break before a row-start. The rules live beside the layouts, as
style definitions (#UODI74). Adding a form's layout is one
define-layout line or one class with two methods, and nothing else
changes.
Mentioned in: Clauses as tables: cond, case, typecase, handler-case, The dexp HTML renderer
DONE Boxes hug their widest row #993QQQ
Intent. In the rendered source pages a nested body form stretches to
its parent's full width (.body is flex-basis: 100%), and worse, a
wrapping flex box's intrinsic width is the single-line sum of its
children, so even an argument box like a multi-line (lambda …) comes
out far wider than its tallest row. Closing parentheses pile up at the
right margin, far from what they close. This costs more readability than
any operator-specific layout, and it needs no operator knowledge to fix.
Evidence. Injecting CSS into the built hal/shader/language.lisp page:
when a list with body forms becomes a grid of rows (a .head flex row
holding the operator and head arguments, then one row per body form)
with width: fit-content; max-width: 100%, every box hugs its widest row
and the page reads like a pretty-printer's output — (unless operands
(error …)) is exactly as wide as its error row. Row gaps and baseline
alignment survive unchanged.
Done. The drawing routine now works in two passes: layout-items
groups a list's children into items (a comment, a child with its role,
or a keyword pair), and render-children-with-roles draws them; when
any item is a body form the list is stacked, its head items in a
.head span and every body item a row (a comment just before the first
body form is a row too). The stacked style group draws it as a
one-column grid with width: fit-content. On the shader-expression
and streaming pages every box hugs its widest row at every depth;
(unless operands (error …)) is as wide as its error row, a
(lambda …) argument as wide as its longest body row, and top-level
definitions still start at the left margin. What remains of the
flex-wrap intrinsic-width problem is inside flow boxes: a function call
holding a stacked argument, like (every (lambda …) types) as a cond
test, is still sized as if its children stood on one line, so the
argument after the lambda floats right of a gap; that wants the call
layout of #ACX8LK.
Mentioned in: Layouts are objects chosen by operator and role
Referenced from code: One drawn unit of a list: KIND is :comment, :child, or :pair. ROLE is
the role the layout gave it (a pair takes its value's role); A list with body forms is a grid of rows: the .head row, then one row
per body form, so the box hugs its widest row instead of stretching to
its parent (#993QQQ).defstruct (item (:constructor make-item (kind role node &key value comments index previous))) dexp.lisp:447 ↗
node the
child or the key; VALUE and COMMENTS the pair's value and the comments
between key and value; INDEX and PREVIOUS the argument index and previous
argument, for docstring position.define-style stacked style.lisp:634 ↗
DONE Clauses as tables: cond, case, typecase, handler-case #4175NC
Intent. A cond clause today stacks its test and its result as two
identical rows, so (null vectors) and (find-shader-type :float) look
like siblings and the reader cannot tell test from consequent. What the
let grid did for bindings (#BGIM3I) should be done for clauses: a
two-column table, key or test in the first column, result in the second,
aligned across all clauses of the form.
Evidence. The same page with a two-column subgrid on clauses-layout
bodies: (null vectors) → (find-shader-type :float), (every …) → (first
vectors), t → (error …) line up, and a clause with several body forms
stacks them in the second column. A tall test keeps the result on its
first baseline. Nothing operator-specific is needed beyond what
clauses-layout already marks, and every luv macro whose lambda list
says &body clauses or cases gets it through the derived layout.
Done. clauses-layout carries a clause-head-count, 1 for cond,
case, typecase and their variants, 2 for handler-case and
restart-case; each clause is drawn by a stacked-clause-layout with
that head count (and, for the two-element key, the lambda list marked as
one) as a .head cell and a .rest cell of stacked body forms. The
clauses style group makes the form a two-column grid, fit-content(50%)
for the keys and the remainder for the bodies, each clause a subgrid
row. On the source pages cond reads as test → result with the results
aligned, handler-case as error () → "<unprintable>", and defstruct,
which already used the clauses layout, became a slot table with names in
one column and defaults and options in the other for free. Tests cover
a cond with a multi-form clause and a handler-case. Alongside, the
data-callee attribute is now decided by layout-callee-p, a generic
with methods on the clause, grid, and lambda-list layouts, which also
stopped lambda lists from claiming their first parameter as a callee.
Mentioned in: Layouts are objects chosen by operator and role
Referenced from code: A COND-style clause, one row of its parent's clause table (#4175NC): the
key or test in a .head cell, then the body forms stacked in a .rest cell. COND, CASE, HANDLER-CASE and other clause forms are tables (#4175NC):
each clause a row with its key or test in the first column and its
body forms stacked in the second, aligned across the clauses.defmethod render-layout dexp.lisp:570 ↗
define-style clauses style.lisp:649 ↗
DONE The stylesheet as Lisp definitions #UODI74
Intent. wiki/style.css is 850 lines of hand-written CSS that the
renderer's roles must agree with by convention. luv already compiles
several small languages from S-expressions (#C6E3LA the shader language, #SQC5JN
the arithmetic language, MSL); the stylesheet should be one more: rules
written as Lisp definitions in the wiki system, compiled to CSS at build
time, so a layout class and its rules can live side by side, be
re-evaluated in the running image, and be read by the same tools as the
rest of the site.
Evidence. The CSS in use needs only a small vocabulary: selectors with
nesting and &, properties with keyword, string, number, and list
values, var() and a handful of functions (clamp, minmax, repeat,
color-mix), one @media query for dark mode and one for narrow
screens. A compiler for that is a page of Lisp.
Done. wiki/css.lisp is the compiler, the package luv.css, in three
parts. A reader syntax, luv.css:syntax (a named readtable that makes
the digits and the hyphen read a token), reads CSS quantities and
references as objects: 0.85rem and 94% are dimension instances,
0.72rem/1 a slash, --ink a variable-reference; no number-and-unit
symbols are interned. A syntax tree of CLOS instances -- style,
rule, at-rule, declaration, selector-list of complex-selector
strings, and the value classes -- is built by the rule and
define-style macros, whose bodies alternate :property keywords with
values: bare symbols are CSS words (grid, solid), strings are written
verbatim, and lists are Lisp, so (clamp 1rem 4vw 3rem) calls the CSS
function and (gutter) or (palette :dark) call the sheet's own
helpers, whose values, declarations, or rules join the enclosing rule
(add-item dispatches on what a form returned). Nested rules combine
with the parent, & standing for it, groups as lists of strings crossed
with groups -- nothing is ever split on a comma -- and (:media "query"
…) is an at-rule, hoisted around what it wraps. write-css is the text
backend, a generic over the tree, so another backend can walk the same
objects. wiki/style.lisp, package luv.wiki.style, holds the sheet:
the palette is a table of light and dark values that (palette scheme)
turns into declarations, the mark and definition-kind colours are loops,
and the layout rows the dexp boxes share (flowing-row, stacked-rows,
subgrid-row) are functions returning declarations. The generated
style.css was diffed against the previous one: identical apart from
the --fg=/--bg= aliases and one repeated font stack, both removed, and
groups with equal declarations merged.
Mentioned in: Why ASDF, and how the wiki slots into it, Layouts are objects chosen by operator and role
TODO Pairs, loop tables, and layouts from arglists alone #ACX8LK
Intent. Smaller layout wins in the order they pay: keyword pairs on
rows aligned by a subgrid instead of a guessed min-width for the key;
loop as a keyword table, each clause a row of keyword and rest as
Emacs indents it; and function calls whose arguments wrap continuing
under the first argument, which would also size a call holding a
stacked argument by its rows rather than by one imagined line. Done
already: setf, setq, psetf, psetq draw as a place/value table
when they have more than one pair (pairs-layout; a single pair stays
inline), and symbol names no longer break at hyphens.
Underneath, fold derived-layout into constructors of the existing
layout classes so that a table entry and an introspected lambda list
produce the same objects — (bindings &body body) a bindings-layout,
(keyform &body cases) a clauses-layout — and every refinement above
applies to both; extend the parameter-name heuristics with PLACE,
KEYFORM, TEST-FORM, SPECIALIZED-LAMBDA-LIST, OPTIONS; then trim
the hand table to true overrides. Alongside, the renderer's remaining
typecase and typep on layout classes (lambda-list-of,
docstring-position-p, layout-pairs-index, the data-callee test)
become generics with methods on the layouts, and child-role answers a
placement and a child layout rather than a space-separated role string
that role-p parses back, per clos-design.
Done when. Each item lands as its own small commit with the source
pages checked in the browser; the layout table is shorter than today;
luv-wiki/test passes.
Mentioned in: Boxes hug their widest row
Reading source without loading it: Eclector #SKNK2N
The site is built by luv-wiki, which does not and should not load luv
itself; the Vulkan and SDL systems have no business in a documentation
build. So definitions must be read from files without interning their
symbols or evaluating anything. Eclector is the ally here: a portable Common
Lisp reader whose client protocol lets interpret-symbol return a token
(package prefix, name, and whether the package is the file's current one)
instead of interning, whose parse-result protocol yields source ranges for
every object, and which reports skipped input, comments and #+ branches
included, as results too. It reads #', #p, #(, ,@, and reader
conditionals correctly, which a hand-rolled tokenizer would not; parinfer's
tokenizer is deliberately a repair heuristic and stays that.
Reading with Eclector gives a definition index beside the figure index:
for every cl-source-file in the registered luv systems' component trees,
each top-level form with its kind, name, qualifiers or specializers, file,
line range, comment text, and the #ID mentions inside it. Figures ask it
who references them; lisp: links ask it where a name is defined; the
McCLIM browser can present it directly. Reader conditionals and #.
are rendered as visible tokens, never evaluated.
Platform availability belongs to ASDF's :if-feature metadata, not to a
#+darwin conditional around the defsystem form. ASDF therefore registers
the Objective-C and Metal systems and constructs their component trees on
Linux, while their compile and load actions remain invalid there. The wiki
deliberately walks those complete trees rather than ASDF's active
sub-components, so a Linux documentation build renders the Darwin sources
without loading them. Individual files such as the Cocoa canvas helper use
the same component-level :if-feature convention.
Mentioned in: Code views: a dexp renderer for Common Lisp, Dexp on McCLIM, The site shows code references to figures, Read luv sources with Eclector into a definition index
Dexp on McCLIM #CEON0R
The token tree from #SKNK2N is a model, like the Org elements, and can be
drawn by more than one backend. In McCLIM the natural rendering is the
same idea in a different medium: a list is an output record with a rounded
border, its children laid out by formatting-item-list or a small flow
layout, symbols are presentations of a lisp-symbol type with translators
to Edit Definition and to the wiki figures that mention them. Since McCLIM
is meant to become a significant part of the 3D world's own interface, a
dexp view of live definitions inside luvcraft is not far-fetched. As with
the wiki browser, the HTML backend should come first and the CLIM one should
drive the (render backend element) refactor of #AGTIO4.
Browsing the whole source from the wiki #58SVCM
Once every source file is read into nodes, showing all of it is one more
render. Each Common Lisp file owned by the five root ASD files -- luv,
luvcraft, mcluv, luv-wiki, and luv-wiki-site -- becomes a page
under source/, drawn entirely as dexp boxes: a table of its definitions on
top, every top-level form anchored by its starting line (#L956), the
file's in-package hiding that package's prefix, comment blocks kept as one
box each. source.html lists the files by system; on a wide screen a source
page keeps a sidebar of the systems and their files on the left, the
current file's system open, and the status bar reads as a breadcrumb trail
(Source › system › file; on wiki pages Pages › title). The reference graph runs
both ways through these pages: any symbol that names a definition anywhere
in the corpus links to that definition (a defining form is preferred over a
method), "Referenced from" summaries and lisp: links point at the local
page with GitHub as a secondary ↗, and #ID mentions in docstrings and
comments point back at the wiki. Source pages use a wider column than
prose, and since dexp needs no monospace alignment the boxes are set in the
system sans-serif.
Docstrings, :documentation strings, any multi-line string, and comment
blocks are not code and are not drawn as code: their content goes through
the same reader as wiki prose and comes out as paragraphs and lists set in
the prose face inside the box, with #ID mentions and verbatim markup
live. One rule differs from Org: in code prose *name* without spaces is a
special variable and reads as code, not bold.
This is the reading surface for keeping up with the codebase: the pages
are regenerated by render-op whenever a source file changes, since the
files are among its inputs.
Mentioned in: What this page is, Browse the source as dexp pages
DONE Browse the source as dexp pages #2KK966
Intent: make the whole codebase readable from the rendered wiki, per #58SVCM, so that following a figure into code and code back into figures never leaves the site.
Evidence: wiki/source.lisp adds source-file objects (path, system, text,
nodes, definitions), render-source-page, and render-source-index;
render-op on the system lists source.html and one page per file among
its output-files. find-named-definition backs symbol links; the mesher
page alone carries a few hundred. Adjacent comment lines are merged into
one node, and clause lists (let bindings, defclass slots, lambda lists,
cond clauses) do not colour their first element as an operator. The canonical
make wiki path registers all five root ASD files before rendering, and the
Pages workflow watches Lisp and ASD changes, so a clean build cannot silently
collapse the index to the wiki tooling alone.
Done when: every source file has a page reachable from the site header, definitions are anchored and linked from symbols, and code references open locally rather than on GitHub. All hold.
DONE The site shows code references to figures #PF39II
Intent: the first rung of #F6B3NI. Build the definition index of #SKNK2N
during render-op on the system and render a "Referenced from" line per
figure naming the definitions that mention it, each expandable in place to
the definition drawn by the dexp renderer of #N06GKK, with the GitHub line
link as the secondary action, so #PLRP3A and #LNRY72 pay off.
Evidence: render-op on the system reads the cl-source-file components
of every registered luv and luvcraft system (67 files, about 2500
definitions, under a second) and every page's input-files include them, so
a source edit re-renders the site. #PLRP3A now shows
interpret-quantity-specification and #LNRY72 shows the
define-quantity :light-level form that follows its section comment, each
as a <details> whose summary is kind, name, and file:line source link and
whose body is the definition as dexp boxes; the #ID in the docstring links
back. [[lisp:name]] links resolve against the same index. Dangling code
mentions are warned about like dangling wiki mentions and are covered by a
test; luv-wiki's own sources are excluded because their docstrings use
placeholder IDs.
Done when: those two figures show their referencing definitions inline on the site, the index and renderer are covered by tests, and a dangling code mention is reported like a dangling wiki mention. All three hold.
Mentioned in: Code views: a dexp renderer for Common Lisp
DONE Read luv sources with Eclector into a definition index #YFDCSB
Intent: the reader of #SKNK2N as part of luv-wiki: an Eclector client that
produces a token tree with source ranges and comments without interning, and
a definition index over the cl-source-file components ASDF lists.
Evidence: wiki/lisp.lisp. source-reader subclasses Eclector's
parse-result-client; interpret-symbol returns a token with the package
prefix as written, quote-like macros and #. return marker lists that
make-expression-result turns into lisp-prefix nodes, feature expressions
are evaluated by name against *features* with the skipped branch kept as a
lisp-conditional from its source text, and comments arrive through
make-skipped-input-result as lisp-comment nodes, inside forms as well as
between them. Every luv source file reads without error; the index names
interpret-quantity-specification at arithmetic/semantics.lisp:956 with its
#PLRP3A mention, and no package or symbol is created (the tests check).
Done when: every luv source file reads without error, the index names
interpret-quantity-specification with its line range and its #PLRP3A
mention, and a reader conditional appears as a token rather than a choice.
All hold; the last is a lisp-conditional node with sign, feature, and form.
Mentioned in: The dexp HTML renderer
DONE The dexp HTML renderer #Z50KYF
Intent: render a token tree from #YFDCSB as dexp boxes per #N06GKK, with
roles assigned in Lisp and painted by the wiki stylesheet, and use it for
#+begin_src lisp blocks first, since the wiki already has those.
Evidence: wiki/dexp.lisp and the .lisp rules in style.css. A list is
a flex-wrapping div.list with side borders; roles come from the layout
objects of #BGIM3I and are emitted as operator, body, bindings,
lambda-list, clause, and row-start classes, comments not counting as
arguments. The
stylesheet does .body { flex-basis: 100% } and paints operators, keywords,
strings, numbers, comments, and prefixes. All lisp source blocks in the
wiki now render structurally, readable at phone width; input that reads to
nothing falls back to <pre>. The screenshot above is not yet reproduced
from luv's code, but the same shapes are visible in the definitions the
figures now show; a luv-flavoured version can replace it later.
Done when: the source blocks of the wiki render structurally and readably on
a phone-width viewport, fall back to <pre> on read failure, and the
screenshot above can be reproduced from luv's own code. The first two hold;
the third is left as a nice-to-have.
TODO A wiki browser frame in mcluv #NPZWLR
Intent: a McCLIM application frame over the loaded luv-wiki-site documents, with
figure and mention presentations, page navigation, and backlinks; the
exercise that tells us what the model in #AGTIO4 is missing.
Evidence: none yet; the shader lab in shader-lab.lisp shows the presentation-type pattern to follow.
Done when: every wiki page can be read in the frame, clicking a mention jumps
to its figure, and re-loading luv-wiki-site after an Org edit redisplays.
Mentioned in: A WIKI package of figure symbols
WAIT A WIKI package of figure symbols #MAE5RT
Intent: the second rung of #F6B3NI: intern figure IDs as external symbols of
a WIKI package at load-op, attach the heading, and offer wiki:ref for
xref-visible references from code.
Evidence: waits on #NPZWLR, which should show what accepting or invoking a figure symbol ought to do before the notation is fixed.
Done when: 'wiki:LNRY72 names a figure at the REPL, (wiki:ref LNRY72) is
found by xref, and the site's "Referenced from" line uses the compile-time
table when available.
IDEA Structural associations through the MOP #D0NQ54
Beyond textual mentions, a metaclass mixin or class option could make the
association between a class or generic function and its wiki figures
structural: (defclass ... (:metaclass documented-class) (:figures LNRY72))
or a wiki:figures slot on a metaobject, so the browser and the site can ask
an object which figures explain it and a figure which definitions realize
it. clos-design's rule applies: use the MOP only when the definition itself
carries the meaning. Vague on purpose until the package and browser exist.
IDEA Source-anchored file links #MJQDRA
Org file: links with a ::(defun name search string presently point at the
file on GitHub without the search. A later render step could resolve the
search to a line number at build time, since the source is in the same tree.
A named recipe for one generated wiki image or video. The semantic metadata is inspectable and retained in source; RENDERER is the ordinary named function installed by DEFINE-CAPTURE. Rendered bytes live under the capture output directory and do not belong in Git. #IVRWI8
How the children of a list are arranged.
(name (&key figure kind extension (description "") (layout :landscape))
(pathname) &body body)Define one inspectable wiki capture recipe. NAME is its command-line identity. FIGURE is the stable six-character wiki figure ID; KIND is :IMAGE or :VIDEO; EXTENSION is the generated file suffix. LAYOUT is :LANDSCAPE by default or :PORTRAIT for uncropped 9:16 presentation. BODY is an ordinary named renderer function…
(specification pathname)Create the small public-index derivative for captured media at PATHNAME. Image originals get 480w and 768w WebPs whenever those are true downscales. Films get a card-sized WebP poster from their first frame. Originals remain untouched and retain their stable capture identity. #IVRWI8
(pathname)The deterministic responsive widths generated for each larger still.
Test whether one compatible scalar is greater than another.
(source destination width)(pathname &optional (width +capture-web-image-width+))(pathname)The minimum of compatible quantities.
The maximum intrinsic width of a showcase video poster.
The generated capture catalog mounted beside externally served media. DIRECTORY contains the capture manifest. MEDIA-PATH is the public static URL subtree whose files are served by the deployment edge. #IVRWI8
A semantic page mounted at one URL subtree.
(derived interpretation)Give a compatible anonymous DERIVED specification an explicit meaning. This is a semantic interpretation, never a numerical unit conversion. An already named quantity may only retain its name; anonymous derived results may acquire one when their dimension, exact unit, and tensor order agree with INTERPRETATION. …
Logical conjunction of tests and raw truth values.
Logical disjunction of tests and raw truth values.
(left right)(left right)Test whether two compatible scalars are equal.
Name a compatible derived quantity.
A list with body forms is a grid of rows: the .head row, then one row per body form, so the box hugs its widest row instead of stretching to its parent (#993QQQ).
Anything in a stylesheet: a declaration, rule, at-rule, or style group.
(name &body body)Define the style group NAME: an optional docstring, then rules. A rule is (SELECTOR . BODY), SELECTOR a string or a list of strings. BODY alternates :property keywords with values -- bare symbols are CSS words, strings are written verbatim, numbers and 0.85rem quantities and --name references are themselves -- and…
(layout list)Emit the children of LIST arranged by LAYOUT, inside the list's own box, which the caller has opened.
A COND-style clause: a key or test, then body forms stacked.
(layout list)The children of LIST grouped for drawing under LAYOUT. Comments are kept where they occur and are not counted. A trailing run of :keyword value pairs is grouped pair by pair, keeping the key with its value.
(items)ITEMS before the first body item as the head, and the rest as rows; comments just before the first body item are rows, not head.
(layout list item)COND, CASE, HANDLER-CASE and other clause forms are tables (#4175NC): each clause a row with its key or test in the first column and its body forms stacked in the second, aligned across the clauses.
Once every source file is read into nodes, showing all of it is one more render. Each Common Lisp file owned by the five root ASD files -- luv, luvcraft, mcluv, luv-wiki, and luv-wiki-site -- becomes a page under source/, drawn entirely as dexp boxes: a table of its definitions on top, every top-level form anchored…
An addressable piece of discourse is a figure. For now a figure is an Org heading with a stable six-character ID property. Files gather figures by subject; they do not provide their identities. A figure can move, acquire a better title, or be substantially revised while its identifier remains the same. A plain…
Intent. wiki/style.css is 850 lines of hand-written CSS that the renderer's roles must agree with by convention. luv already compiles several small languages from S-expressions (#C6E3LA the shader language, #SQC5JN the arithmetic language, MSL); the stylesheet should be one more: rules written as Lisp definitions in…
A tool approval should show its consequence, not merely ask whether a verb may run. Construction makes that unusually literal. tool-approval retains an inert semantic payload, its :proposed state, and a private decision mailbox; the provider's turn may wait in settle-command-result while the canvas keeps rendering. …
Some figures are also work marks: tiny roadmap entries that live in the same wiki page as the design they move forward. A work mark is an Org heading with: – a status keyword in the title: NEXT, TODO, WAIT, DONE, or IDEA; – a stable six-character ID property, so code, commits, and other pages can mention it as…
Three positions on the source's runtime machinery, taken so that #LNRY72 does not silently import mp-units' cost model into a zero-cost lowering: – Non-negativity is a compile-time fact by default. A non-negative specifier participates in the character algebra — it decides what an absolute may become, and it is what…
The two-way split above is mp-units V2, and it is what affine-p in arithmetic/semantics.lisp implements today: a specification is either a point or a difference. mp-units V3's central discovery (#QFCPRA) is that this misses the commonest case. Most physical equations are written over absolutes: non-negative amounts…
The site is built by luv-wiki, which does not and should not load luv itself; the Vulkan and SDL systems have no business in a documentation build. So definitions must be read from files without interning their symbols or evaluating anything. Eclector is the ally here: a portable Common Lisp reader whose client…
Intent: the first rung of #F6B3NI. Build the definition index of #SKNK2N during render-op on the system and render a "Referenced from" line per figure naming the definitions that mention it, each expandable in place to the definition drawn by the dexp renderer of #N06GKK, with the GitHub line link as the secondary…
Intent. Smaller layout wins in the order they pay: keyword pairs on rows aligned by a subgrid instead of a guessed min-width for the key; loop as a keyword table, each clause a row of keyword and rest as Emacs indents it; and function calls whose arguments wrap continuing under the first argument, which would also…
The first renderer assigned layout with a table of head counts and a growing set of role strings and special cases; let bindings drawn as a two-column grid, loop keywords starting rows, and lambda lists whose sublists are (var default) clauses made that untenable. The design that replaced it follows clos-design: a…
A shader-specification owns interface declarations, resources, ordered shader-binding objects, and output assignments. Every literal, reference, and call is a shader-expression with: – an inferred shader-type; – its original source form; – expression-specific operands or target; – and, for a let* value, its lexical…
The quantity algebra in arithmetic/semantics.lisp is already independent of any execution backend, but the compiled language which presently exercises it is still named and owned as a shader language. Its literal, reference, call, binding, quantity-boundary, and conversion objects live beside shader…
The loaded documents on the org-file components are already the browser's data source. What the element model still lacks is small and each piece is motivated by a browser affordance: – figure and work-mark as classes decided at read time, so presentation types map onto classes one to one and each needs one present…
The wiki does not want to become Lisp forms; Org in Emacs is the better writing surface, and the reader already gives the corpus to Lisp as objects. The interesting direction is the reverse flow: the code contributing to the discourse where that is more natural than prose, with less text and more references and…
Source links that leave the page for GitHub are a poor reading experience, especially on a phone. The wiki should show code in place, and the way it should show it comes from wisp's structure editor, whose "dexp" renderer draws Lisp as nested boxes rather than text with indentation: images/dexp.png The mechanism is…
Intent: the reader of #SKNK2N as part of luv-wiki: an Eclector client that produces a token tree with source ranges and comments without interning, and a definition index over the cl-source-file components ASDF lists. Evidence: wiki/lisp.lisp. source-reader subclasses Eclector's parse-result-client; interpret-symbol…
Intent: a McCLIM application frame over the loaded luv-wiki-site documents, with figure and mention presentations, page navigation, and backlinks; the exercise that tells us what the model in #AGTIO4 is missing. Evidence: none yet; the shader lab in shader-lab.lisp shows the presentation-type pattern to follow. Done…
Pictures that carry design evidence should be reproducible without making Git the video store. capture-specification is the semantic boundary: a stable name, wiki figure ID, media kind, extension, description, and an ordinary named renderer function. define-capture is only a thin definition-site macro over that…
Intent. In the rendered source pages a nested body form stretches to its parent's full width (.body is flex-basis: 100%), and worse, a wrapping flex box's intrinsic width is the single-line sum of its children, so even an argument box like a multi-line (lambda …) comes out far wider than its tallest row. Closing…
Intent. A cond clause today stacks its test and its result as two identical rows, so (null vectors) and (find-shader-type :float) look like siblings and the reader cannot tell test from consequent. What the let grid did for bindings (#BGIM3I) should be done for clauses: a two-column table, key or test in the first…
Drawing
The children of a list are first grouped into ITEMs -- a comment, one child with its role, or a :keyword value pair -- and then drawn. A list with body items is
stacked: its head items go in a .head row and each body item is a row of its own, so the box is a grid of rows and hugs its widest row instead of stretching to the parent (#993QQQ).