add panels and margin notes, with no engine code at all

Expandable sections, tabs and asides — three of the things parked earlier — turn
out to be two theme fragments and nine lines of CSS. That is what the container
mechanism bought: they needed no Go.

Tabs without script were the only real problem, and containers do not nest, so
:::tabs wrapping :::tab was never available. Sibling <details> elements sharing a
name attribute are natively mutually exclusive, which is what tabs are, so
grouping is one argument on the same fragment an expando already uses. A browser
too old for grouping opens them independently — the content is never hidden,
which is the failure mode worth caring about.

The aside is beside the text where the viewport has room and in the flow where
it does not, in one media query. No JS anywhere, and the demo case asserts the
page contains no <script> at all.

No ADR: nothing here is expensive to reverse, and the contract grew additively
as its stability rule allows.
This commit is contained in:
Claude Opus 5
2026-08-01 22:14:58 +06:00
committed by bdeshi
parent 8bfacc7e98
commit fdc76ba1b9
8 changed files with 43 additions and 5 deletions
+3 -2
View File
@@ -372,8 +372,9 @@ half-working.
Three colons open a **container** directive — `:::name{…}`, body, `:::` on its own line — for calls that wrap
content (ADR-0064). The body is Markdown and renders as such. One level: a `:::` inside a container closes it
rather than opening another, the same limit an include carries. `note`, `warn` and `tip` exist; a kind the
theme does not define renders its body unwrapped rather than losing it.
rather than opening another, the same limit an include carries. `note`, `warn` and `tip` exist, plus `details` — an
expandable panel, and tabs when siblings share a `group` — and `aside`, a margin note. A kind the theme does
not define renders its body unwrapped rather than losing it.
**Migrating from the retired form.** `{{< name key="value" >}}` is no longer a call and renders as literal
text. `khosra check` reports every one as fatal and names the replacement, so a site root is migrated by
+1 -1
View File
@@ -24,7 +24,7 @@ table owns.
| `internal/render/render.go` | goldmark with the typographer, per-kind template sets with site override, the render methods. The parsed sets plus the stylesheet are one snapshot behind an `atomic.Pointer`; `Refresh` is the only thing that replaces it, so every page serves one theme (ADR-0055, ADR-0056). Heading ids are a parser option set here (ADR-0058), and this is the one renderer that enables raw HTML (ADR-0060) |
| `internal/render/view.go` | the theme contract in Go: `Page`, `List`, `Sequence`, `Extras`, `Item`, `Fragment` (with `Body` and `Headings`, ADR-0064, ADR-0065), `Heading`, `Picture`, `Origin` |
| `internal/render/chrome.go` | the engine's own words: phrase table, month names, digits, and the `t`/`num`/`day` template funcs (ADR-0034) |
| `internal/render/templates/` | reference theme, complete (six icon names map to Unicode, no assets — ADR-0063): `base.html` (shell, navigation, language links, feed and OpenGraph), `page.html` (bundle, sequence, tags, extras), `list.html`, `extras.html`, `shortcodes.html`, `theme.css` (ADR-0026, ADR-0049) |
| `internal/render/templates/` | reference theme, complete (six icon names map to Unicode, no assets — ADR-0063): `base.html` (shell, navigation, language links, feed and OpenGraph), `page.html` (bundle, sequence, tags, extras), `list.html`, `extras.html`, `shortcodes.html` (figure, gallery, icon, note/warn/tip, details, aside, toc), `theme.css` (ADR-0026, ADR-0049) |
| `internal/ext/shortcodes/` | first feature: `::name{key=value}` block parser and node renderer, rendering through a theme fragment (ADR-0036). `figure`, `gallery`, `include`, plus the derivative pass and remembered picture inspection (ADR-0042, ADR-0044). `FootnotePrefix` namespaces an included file's footnote ids (ADR-0058). Directive syntax since ADR-0059, plus `icons.go`: `:name:` inline, rendered by the theme's one `icon` fragment (ADR-0063), `containers.go`: `:::name{…}``:::` wrapping a rendered body (ADR-0064), and `toc.go`: the document's headings for a `::toc` call (ADR-0065) |
| `internal/ext/notation/` | the inline marks CommonMark lacks: `~sub~`, `^sup^`, `==mark==`, and `~~strike~~`, which it owns so a single tilde can mean subscript (ADR-0061). `abbr.go` adds `*[TERM]:` definitions and the pass that expands them (ADR-0062) |
| `internal/ext/scaffold/` | writes one draft directory bundle into a site root through `os.Root`: never an overwrite |
+1 -1
View File
@@ -314,7 +314,7 @@ chrome.go 110 · render.go 483 · view.go 130
- view.go:111 type Item struct
- view.go:122 type Alternate struct
## internal/web — 734 lines + 1615 test
## internal/web — 734 lines + 1619 test
asset.go 58 · discover.go 71 · extras.go 93 · feed.go 125 · resolve.go 170 · web.go 217
+2
View File
@@ -108,6 +108,8 @@ Each picture carries:
| `:name:` | `icon` | `.Args.name` — the name as written, nothing else |
| `:::note{title=…}``:::` | `note` | `.Args.title`, `.Body` — the rendered content |
| `::toc` | `toc` | `.Headings` — every heading in the document, in order |
| `:::details{summary=… group=… open=…}``:::` | `details` | `.Args`, `.Body`. Siblings sharing a `group` open one at a time, through `<details name>` and no script |
| `:::aside{title=…}``:::` | `aside` | `.Args.title`, `.Body`. Beside the text where there is room, in the flow where there is not |
A **container** call wraps content: `:::name{…}`, a body, then `:::`. It renders through the fragment of that
name with `.Body` already HTML, and one level only — a `:::` inside closes the one it is in. A theme with no
@@ -38,6 +38,22 @@ is Markdown: *emphasis*, links and H~2~O all work inside it.
| North | 41 mm | steady |
| South | 12 mm | falling |
:::aside{title="On the gauges"}
Both were replaced in 2024, which is why the older readings are noisier.
:::
:::details{summary="Show the raw transcription"}
Twelve pages of it, kept out of the way until asked for.
:::
:::details{group=readings summary="North gauge" open=yes}
41 mm, steady through the week.
:::
:::details{group=readings summary="South gauge"}
12 mm, falling. Grouped panels open one at a time, with no script.
:::
Monsoon
: The season these readings belong to.
+10
View File
@@ -55,3 +55,13 @@
</ol></nav>
{{- end}}
{{- end}}
{{/* An expandable section, and tabs, from one template. `<details>` needs no script; siblings sharing a
`group` become mutually exclusive through the native `name` attribute, which is what tabs are. A
browser too old for grouping simply opens them independently — the content is never hidden. */}}
{{define "details"}}<details{{with .Args.group}} name="{{.}}"{{end}}{{if .Args.open}} open{{end}}>
<summary>{{with .Args.summary}}{{.}}{{else}}{{.Args.title}}{{end}}</summary>
{{.Body}}</details>{{end}}
{{/* A margin note: beside the text where there is room, in the flow where there is not. CSS only. */}}
{{define "aside"}}<aside class="side">{{with .Args.title}}<p class="side-title">{{.}}</p>{{end}}{{.Body}}</aside>{{end}}
+6 -1
View File
@@ -24,6 +24,11 @@ dd { margin-left: 1.25rem; }
.footnotes { font-size: 0.9em; }
.admonition { border-left: 3px solid #d8d5cd; border-radius: 0; padding: 0.25rem 0 0.25rem 1rem; margin: 1.5rem 0; }
.admonition-title { font-weight: 600; margin: 0 0 0.5rem; }
details { margin: 1rem 0; }
summary { cursor: pointer; font-weight: 600; }
aside.side { border-left: 3px solid #d8d5cd; border-radius: 0; padding: 0 0 0 1rem; margin: 1.5rem 0; font-size: 0.95em; }
.side-title { font-weight: 600; margin: 0 0 0.5rem; }
@media (min-width: 55rem) { aside.side { float: right; width: 12rem; margin: 0.25rem 0 1rem 1.5rem; } }
nav.toc ol { list-style: none; padding-left: 0; }
nav.toc .toc-3 { padding-left: 1rem; }
nav.toc .toc-4 { padding-left: 2rem; }
@@ -33,5 +38,5 @@ nav.toc .toc-4 { padding-left: 2rem; }
pre { background: #22222a; }
.kind { color: #9a9a9a; }
th, td { border-bottom-color: #33333c; }
.admonition { border-left-color: #33333c; }
.admonition, aside.side { border-left-color: #33333c; }
}
+4
View File
@@ -127,6 +127,10 @@ var exampleFeatures = []featureCase{
{what: "an abbreviation expands and its definition line renders nothing", path: "/writing/notes-on-water/", code: 200,
expect: []string{`<abbr title="National Institute of Water and Atmospheric Research">NIWA</abbr>`},
absent: []string{"*[NIWA]"}},
{what: "panels and margin notes need no script", path: "/writing/notes-on-water/", code: 200,
expect: []string{"<summary>Show the raw transcription</summary>", `<details name="readings"`,
`<aside class="side">`, `<p class="side-title">On the gauges</p>`},
absent: []string{"<script"}},
{what: "a table of contents links the page's own headings", path: "/writing/notes-on-water/", code: 200,
expect: []string{`<nav class="toc">`, `<a href="#the-dialect">The dialect</a>`, `class="toc-2"`}},
{what: "a container renders its body through the theme fragment", path: "/writing/notes-on-water/", code: 200,