split the theme's fragments into a directory, keeping the file form
One file held every fragment, and it gained one per feature all session: figure, gallery, icon, three admonitions, details, aside, contents. A theme author overriding one had to copy the file or redefine into it, and a diff of the theme became a diff of everything. Both forms are supported, because a small theme is happier with one file and the contract should not force a directory on it. Parse order is embedded file, embedded directory, site file, site directory, and the last definition wins — so the directory overrides the file within one source and a site overrides the binary either way. Verified with a site root using both at once: its shortcodes.html supplied `icon`, its shortcodes/note.html supplied `note`, the embedded directory supplied the rest, and with the same name in both the directory won. The embedded theme ships the directory only, seven files, so nothing is defined twice. parseSet takes globs now and lost a branch doing it. Its old guard — a literal embedded name must exist — had to go, since shortcodes.html is deliberately absent; the replacement is stronger, failing at startup when a set matches nothing anywhere, which also catches a renamed base.html.
This commit is contained in:
@@ -14,7 +14,8 @@ whatever builds it. What exists is `state.md`'s job. Never build a `[spec]` sect
|
||||
|
||||
The engine is pointed at a site root outside this repository (`-site <dir>`, or `KHOSRA_SITE`), with
|
||||
its own git history (ADR-0011). Nothing in the engine repo is content. `templates/` in the site root
|
||||
overrides the defaults the binary embeds, so a bare root still renders.
|
||||
overrides the defaults the binary embeds, so a bare root still renders. Shortcode fragments may be one
|
||||
`templates/shortcodes.html` or a `templates/shortcodes/` directory, or both (ADR-0071).
|
||||
|
||||
```
|
||||
<site>/
|
||||
|
||||
@@ -1166,3 +1166,23 @@ person meets the counter rather than in an ADR they would have to find. Expensiv
|
||||
sentence that may be genuinely hard to write, which is the point and will still feel like friction.
|
||||
Revisit if: a counter is added whose exclusion is honestly "nothing" — then the column is wrong, or the
|
||||
counter is.
|
||||
|
||||
## ADR-0071 — Shortcode fragments may be a file or a directory; the directory wins
|
||||
Date: 2026-08-01 · Status: accepted (extends ADR-0019's parse-order mechanism to a glob)
|
||||
Decision: fragments are parsed from `templates/shortcodes.html` and then `templates/shortcodes/*.html`,
|
||||
embedded first and the site's after, so within one source the directory overrides the file and a site
|
||||
overrides the binary. The embedded reference theme now ships the directory only — seven files, no
|
||||
`shortcodes.html` — so nothing is defined twice. `parseSet` takes globs, and a set that matches nothing
|
||||
anywhere is a startup failure.
|
||||
Why: one file held every fragment, and it grew a fragment per feature all session — figure, gallery, icon,
|
||||
three admonitions, details, aside, contents. A theme author overriding one of them had to copy the file or
|
||||
redefine into it, and a diff of the theme became a diff of everything. Both forms stay supported because a
|
||||
small theme is happier with one file and the contract should not force a directory on it; the directory wins
|
||||
because it is the more specific statement, the same way a site override beats the embedded set.
|
||||
Consequence: cheap — the embedded theme is now seven readable files, `parseSet` grew a glob and lost a
|
||||
branch, and a site may use either form or both at once, which is tested. Expensive — fragments now live in
|
||||
two possible places, so "where is `figure` defined" has two answers; and the old guard, that a literal
|
||||
embedded name must exist, had to go, since `shortcodes.html` is deliberately absent. Its replacement is
|
||||
stronger: a set that matches nothing at all fails at startup, which also catches a renamed `base.html`.
|
||||
Revisit if: the page and listing sets want the same treatment, which they do not yet — each is one file with
|
||||
one block.
|
||||
|
||||
+1
-1
@@ -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, declared or derived (ADR-0058, ADR-0066), and this is the one renderer that enables raw HTML (ADR-0060). `Compose` is the seam a merging bundle's splice arrives through |
|
||||
| `internal/render/view.go` | the theme contract in Go, and now actually all of it: `Page`, `List`, `Sequence`, `Extras`, `Item`, `Partial`, `Fragment` (with `Body`, `Headings` and `Lang` — ADR-0064, ADR-0065, ADR-0067), `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), including the words a shortcode fragment supplies when the author gives none (ADR-0067) |
|
||||
| `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 — with the `sizes` its own layout implies, ADR-0068), `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/` — seven fragment files rather than one, and a site may use either form (ADR-0071) — with the `sizes` its own layout implies (ADR-0068), `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, to a depth the call may set (ADR-0065, ADR-0066). `Merge` splices includes before the parse for a bundle that asks for it (ADR-0066) |
|
||||
| `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 |
|
||||
|
||||
+13
-13
@@ -266,9 +266,9 @@ doc.go 8 · watch.go 125
|
||||
- watch.go:90 func record(sum hash.Hash, p string, d fs.DirEntry, err error) error
|
||||
- watch.go:113 func dropping(name string) bool
|
||||
|
||||
## internal/render — 749 lines + 489 test
|
||||
## internal/render — 756 lines + 489 test
|
||||
|
||||
chrome.go 115 · render.go 449 · view.go 185
|
||||
chrome.go 115 · render.go 456 · view.go 185
|
||||
|
||||
- chrome.go:19 var chrome = map[string]map[string]string{
|
||||
- chrome.go:38 var months = map[string][]string{
|
||||
@@ -293,16 +293,16 @@ chrome.go 115 · render.go 449 · view.go 185
|
||||
- render.go:190 func (r *Renderer) Refresh() error
|
||||
- render.go:201 func (r *Renderer) Partial(name string, data Fragment) ([]byte, error)
|
||||
- render.go:220 func parseSet(siteFS fs.FS, names ...string) (*template.Template, error)
|
||||
- render.go:242 func readStyle(siteFS fs.FS) (template.CSS, error)
|
||||
- render.go:260 func (r *Renderer) Extras(b content.Bundle, served string, entries []content.Entry, selected *Selected) ([]byte, error)
|
||||
- render.go:281 func (r *Renderer) RenderText(kind string, data []byte) (template.HTML, error)
|
||||
- render.go:299 func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, seq *content.Sequence) ([]byte, error)
|
||||
- render.go:343 func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int) ([]byte, error)
|
||||
- render.go:362 func (r *Renderer) Tag(section, slug, lang string, all []content.Bundle, page int) ([]byte, error)
|
||||
- render.go:389 func (r *Renderer) sequence(seq *content.Sequence, lang string) *Sequence
|
||||
- render.go:416 func (r *Renderer) item(b content.Bundle, lang string) Item
|
||||
- render.go:421 func (r *Renderer) paginate(title, lang, canonical string, all []content.Bundle, page int, url func(int) string) (List, []content.Bundle)
|
||||
- render.go:443 func (r *Renderer) execute(set *template.Template, data any, what string) ([]byte, error)
|
||||
- render.go:249 func readStyle(siteFS fs.FS) (template.CSS, error)
|
||||
- render.go:267 func (r *Renderer) Extras(b content.Bundle, served string, entries []content.Entry, selected *Selected) ([]byte, error)
|
||||
- render.go:288 func (r *Renderer) RenderText(kind string, data []byte) (template.HTML, error)
|
||||
- render.go:306 func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, seq *content.Sequence) ([]byte, error)
|
||||
- render.go:350 func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int) ([]byte, error)
|
||||
- render.go:369 func (r *Renderer) Tag(section, slug, lang string, all []content.Bundle, page int) ([]byte, error)
|
||||
- render.go:396 func (r *Renderer) sequence(seq *content.Sequence, lang string) *Sequence
|
||||
- render.go:423 func (r *Renderer) item(b content.Bundle, lang string) Item
|
||||
- render.go:428 func (r *Renderer) paginate(title, lang, canonical string, all []content.Bundle, page int, url func(int) string) (List, []content.Bundle)
|
||||
- render.go:450 func (r *Renderer) execute(set *template.Template, data any, what string) ([]byte, error)
|
||||
- view.go:17 type head struct
|
||||
- view.go:37 type Page struct
|
||||
- view.go:56 type Sequence struct
|
||||
@@ -318,7 +318,7 @@ chrome.go 115 · render.go 449 · view.go 185
|
||||
- view.go:160 type Picture struct
|
||||
- view.go:177 type Origin struct
|
||||
|
||||
## internal/web — 734 lines + 1631 test
|
||||
## internal/web — 734 lines + 1633 test
|
||||
|
||||
asset.go 58 · discover.go 71 · extras.go 93 · feed.go 125 · resolve.go 170 · web.go 217
|
||||
|
||||
|
||||
+18
-7
@@ -81,8 +81,21 @@ block; site-supplied strings wait for the settings cascade (`ideas/deferred-deci
|
||||
|
||||
## Shortcode fragments
|
||||
|
||||
`templates/shortcodes.html` holds one named template per shortcode, and that is where a shortcode's markup
|
||||
lives — the engine parses the call and supplies its data, never any HTML (ADR-0036).
|
||||
Fragments live in `templates/shortcodes.html`, or in `templates/shortcodes/*.html`, or both — one named
|
||||
template per shortcode, and that is where a shortcode's markup lives. The engine parses the call and supplies
|
||||
its data, never any HTML (ADR-0036).
|
||||
|
||||
Parse order is embedded file, embedded directory, site file, site directory, and the last definition of a name
|
||||
wins (ADR-0071). So **the directory overrides the file** within one source, and a site overrides the binary
|
||||
either way. The reference theme ships the directory: `figure`, `gallery`, `icon`, `admonitions` (note, warn,
|
||||
tip), `details`, `aside`, `toc`. Override one by defining that name anywhere the engine looks; everything you
|
||||
leave alone is inherited.
|
||||
|
||||
Templates in the same set may call each other — `{{template "picture" .}}` from `figure.html` reaches a block
|
||||
defined in `gallery.html`, because both are parsed into the partials set. The pipeline is all that passes, and
|
||||
there is no way to build a value in a template, so inclusion is useful where the callee needs exactly what the
|
||||
caller has. Across sets it does not work: `base`, `page`, `list`, `extras` and the fragments are five separate
|
||||
sets, which is what stops a listing's `main` leaking into a bundle page (ADR-0019).
|
||||
|
||||
Every fragment receives the same two fields (ADR-0037):
|
||||
|
||||
@@ -146,9 +159,7 @@ missing renders nothing and logs; it never fails the page.
|
||||
A gallery entry has no alt text: nothing in a filename supplies one, and inventing it would be worse than an
|
||||
empty `alt` on a picture the page has already introduced. A figure's alt is the author's, from `.Args.alt`.
|
||||
|
||||
The set is overlaid the same way as the page kinds: a site's `templates/shortcodes.html` is parsed after
|
||||
the embedded one, so redefining `figure` replaces it and any fragment left alone is inherited. Argument
|
||||
names are contract, added but never renamed.
|
||||
Argument names are contract, added but never renamed.
|
||||
|
||||
## Feed discovery
|
||||
|
||||
@@ -259,8 +270,8 @@ two drift together.
|
||||
|
||||
A site root's `templates/` is parsed **after** the embedded set, and the last definition of a name wins, so
|
||||
a theme redefines one named block and inherits the document (ADR-0019). Each set is built from named files
|
||||
and only those are overlaid — `base.html` plus that kind's block file (`page.html` or `list.html`), and
|
||||
`shortcodes.html` on its own. Overlaying every site template into every set would let a listing's `main`
|
||||
and only those are overlaid — `base.html` plus that kind's block file (`page.html` or `list.html`), and the
|
||||
fragments on their own (`shortcodes.html` and `shortcodes/*.html`, ADR-0071). Overlaying every site template into every set would let a listing's `main`
|
||||
leak into bundle pages, which is the collision per-kind sets exist to prevent.
|
||||
|
||||
`templates/theme.css` in the site root replaces the reference stylesheet entirely; there is no merging.
|
||||
|
||||
+20
-13
@@ -125,7 +125,7 @@ func parseTheme(siteFS fs.FS) (*parsedTheme, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing templates: %w", err)
|
||||
}
|
||||
partials, err := parseSet(siteFS, "templates/shortcodes.html")
|
||||
partials, err := parseSet(siteFS, "templates/shortcodes.html", "templates/shortcodes/*.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("partial templates: %w", err)
|
||||
}
|
||||
@@ -219,22 +219,29 @@ func (r *Renderer) Partial(name string, data Fragment) ([]byte, error) {
|
||||
// which is the collision per-kind sets exist to prevent.
|
||||
func parseSet(siteFS fs.FS, names ...string) (*template.Template, error) {
|
||||
// Funcs are attached before anything is parsed, so the chrome helpers are available to a site
|
||||
// override's blocks as well as the embedded ones (ADR-0034).
|
||||
set, err := template.New("theme").Funcs(funcs).ParseFS(themeFS, names...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse embedded: %w", err)
|
||||
}
|
||||
if siteFS == nil {
|
||||
return set, nil
|
||||
}
|
||||
for _, name := range names {
|
||||
if _, err := fs.Stat(siteFS, name); err != nil {
|
||||
// override's blocks as well as the embedded ones (ADR-0034). A name may be a glob, which is how a
|
||||
// directory of fragments is parsed after the single file it may replace (ADR-0071).
|
||||
set, parsed := template.New("theme").Funcs(funcs), false
|
||||
for _, from := range []fs.FS{themeFS, siteFS} {
|
||||
if from == nil {
|
||||
continue
|
||||
}
|
||||
if set, err = set.ParseFS(siteFS, name); err != nil {
|
||||
return nil, fmt.Errorf("parse site override %s: %w", name, err)
|
||||
for _, name := range names {
|
||||
if matches, _ := fs.Glob(from, name); len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
if set, err = set.ParseFS(from, name); err != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", name, err)
|
||||
}
|
||||
parsed = true
|
||||
}
|
||||
}
|
||||
// Nothing matched anywhere, which means a name the binary embeds has been renamed. A startup failure,
|
||||
// because the alternative is an empty set and a template error on the first request.
|
||||
if !parsed {
|
||||
return nil, fmt.Errorf("no template matched %v", names)
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
{{/* `sizes` is layout knowledge, so it belongs to whoever wrote the layout. Without it a browser assumes
|
||||
100vw and fetches the widest variant for a thumbnail, which makes the derivative pass cost bandwidth
|
||||
rather than save it. These values describe *this* stylesheet — a 32rem measure, two gallery columns
|
||||
above 36rem — and a theme that changes the layout must change them with it (ADR-0068). */}}
|
||||
{{define "figure" -}}
|
||||
<figure>
|
||||
{{- range .Pictures}}
|
||||
<img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}" sizes="(min-width: 36rem) 32rem, 100vw"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} alt="{{$.Args.alt}}">
|
||||
{{- end}}
|
||||
{{- if not .Pictures}}
|
||||
<img src="{{.Args.src}}" alt="{{.Args.alt}}">
|
||||
{{- end}}
|
||||
{{- if .Args.caption}}
|
||||
<figcaption>{{.Args.caption}}</figcaption>
|
||||
{{- end}}
|
||||
</figure>
|
||||
{{- end}}
|
||||
|
||||
{{define "gallery" -}}
|
||||
{{if .Pictures -}}
|
||||
<div class="gallery">
|
||||
{{- range .Pictures}}
|
||||
<figure><img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}" sizes="(min-width: 36rem) 16rem, 100vw"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} loading="lazy" decoding="async" alt=""></figure>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{/* A kind with no title of its own is labelled by the engine's own words, which are localised: the reader
|
||||
of a Bengali page should not be told "Warning" in English (ADR-0067). */}}
|
||||
{{/* One template for the whole set: which names exist is the theme's business, never the engine's
|
||||
(ADR-0063). Unicode here, so the reference theme ships no sprite, no font and no asset — a theme
|
||||
wanting drawn icons redefines this block and emits <use> against its own sprite. An unknown name
|
||||
renders nothing, which is how a theme says so, and the engine puts the author's text back. */}}
|
||||
{{define "icon"}}
|
||||
{{- if eq .Args.name "warn"}}⚠️
|
||||
{{- else if eq .Args.name "note"}}ℹ️
|
||||
{{- else if eq .Args.name "tip"}}💡
|
||||
{{- else if eq .Args.name "star"}}⭐
|
||||
{{- else if eq .Args.name "check"}}✅
|
||||
{{- else if eq .Args.name "cross"}}❌
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{/* Admonitions. One template per kind, because a call renders through a template of its own name — the
|
||||
markup is the theme's and the engine supplies only the name, the arguments and the rendered body
|
||||
(ADR-0064). `.Body` is already HTML. A theme with no template for a kind renders nothing, and the
|
||||
engine writes the body out unwrapped rather than losing it. */}}
|
||||
{{define "note"}}<aside class="admonition note"><p class="admonition-title">{{with .Args.title}}{{.}}{{else}}{{t $.Lang "note"}}{{end}}</p>{{.Body}}</aside>{{end}}
|
||||
{{define "warn"}}<aside class="admonition warn"><p class="admonition-title">{{with .Args.title}}{{.}}{{else}}{{t $.Lang "warn"}}{{end}}</p>{{.Body}}</aside>{{end}}
|
||||
{{define "tip"}}<aside class="admonition tip"><p class="admonition-title">{{with .Args.title}}{{.}}{{else}}{{t $.Lang "tip"}}{{end}}</p>{{.Body}}</aside>{{end}}
|
||||
|
||||
{{/* A flat list with a level class per entry, so nesting is a CSS decision rather than a markup one: the
|
||||
engine supplies depth and the theme decides whether to indent (ADR-0065). An entry with no id is
|
||||
skipped rather than linked nowhere. */}}
|
||||
{{define "toc"}}
|
||||
{{- if .Headings}}<nav class="toc" aria-label="{{t .Lang "contents"}}">
|
||||
<p class="toc-title">{{t .Lang "contents"}}</p>
|
||||
<ol>
|
||||
{{- range .Headings}}{{if .ID}}
|
||||
<li class="toc-{{.Level}}"><a href="#{{.ID}}">{{.Text}}</a></li>
|
||||
{{- end}}{{end}}
|
||||
</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}}{{with $.Args.title}}{{.}}{{else}}{{t $.Lang "details"}}{{end}}{{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}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{/* Admonitions. One template per kind, because a call renders through a template of its own name — the
|
||||
markup is the theme's and the engine supplies only the name, the arguments and the rendered body
|
||||
(ADR-0064). `.Body` is already HTML. A theme with no template for a kind renders nothing, and the
|
||||
engine writes the body out unwrapped rather than losing it. */}}
|
||||
{{define "note"}}<aside class="admonition note"><p class="admonition-title">{{with .Args.title}}{{.}}{{else}}{{t $.Lang "note"}}{{end}}</p>{{.Body}}</aside>{{end}}
|
||||
{{define "warn"}}<aside class="admonition warn"><p class="admonition-title">{{with .Args.title}}{{.}}{{else}}{{t $.Lang "warn"}}{{end}}</p>{{.Body}}</aside>{{end}}
|
||||
{{define "tip"}}<aside class="admonition tip"><p class="admonition-title">{{with .Args.title}}{{.}}{{else}}{{t $.Lang "tip"}}{{end}}</p>{{.Body}}</aside>{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{/* 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}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{{/* 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}}{{with $.Args.title}}{{.}}{{else}}{{t $.Lang "details"}}{{end}}{{end}}</summary>
|
||||
{{.Body}}</details>{{end}}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{/* `sizes` is layout knowledge, so it belongs to whoever wrote the layout. Without it a browser assumes
|
||||
100vw and fetches the widest variant for a thumbnail, which makes the derivative pass cost bandwidth
|
||||
rather than save it. These values describe *this* stylesheet — a 32rem measure, two gallery columns
|
||||
above 36rem — and a theme that changes the layout must change them with it (ADR-0068). */}}
|
||||
{{define "figure" -}}
|
||||
<figure>
|
||||
{{- range .Pictures}}
|
||||
<img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}" sizes="(min-width: 36rem) 32rem, 100vw"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} alt="{{$.Args.alt}}">
|
||||
{{- end}}
|
||||
{{- if not .Pictures}}
|
||||
<img src="{{.Args.src}}" alt="{{.Args.alt}}">
|
||||
{{- end}}
|
||||
{{- if .Args.caption}}
|
||||
<figcaption>{{.Args.caption}}</figcaption>
|
||||
{{- end}}
|
||||
</figure>
|
||||
{{- end}}
|
||||
@@ -0,0 +1,9 @@
|
||||
{{define "gallery" -}}
|
||||
{{if .Pictures -}}
|
||||
<div class="gallery">
|
||||
{{- range .Pictures}}
|
||||
<figure><img src="{{.Src}}"{{if .Srcset}} srcset="{{.Srcset}}" sizes="(min-width: 36rem) 16rem, 100vw"{{end}}{{if .Width}} width="{{.Width}}" height="{{.Height}}"{{end}} loading="lazy" decoding="async" alt=""></figure>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{/* One template for the whole set: which names exist is the theme's business, never the engine's
|
||||
(ADR-0063). Unicode here, so the reference theme ships no sprite, no font and no asset — a theme
|
||||
wanting drawn icons redefines this block and emits <use> against its own sprite. An unknown name
|
||||
renders nothing, which is how a theme says so, and the engine puts the author's text back. */}}
|
||||
{{define "icon"}}
|
||||
{{- if eq .Args.name "warn"}}⚠️
|
||||
{{- else if eq .Args.name "note"}}ℹ️
|
||||
{{- else if eq .Args.name "tip"}}💡
|
||||
{{- else if eq .Args.name "star"}}⭐
|
||||
{{- else if eq .Args.name "check"}}✅
|
||||
{{- else if eq .Args.name "cross"}}❌
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{/* A flat list with a level class per entry, so nesting is a CSS decision rather than a markup one: the
|
||||
engine supplies depth and the theme decides whether to indent (ADR-0065). An entry with no id is
|
||||
skipped rather than linked nowhere. */}}
|
||||
{{define "toc"}}
|
||||
{{- if .Headings}}<nav class="toc" aria-label="{{t .Lang "contents"}}">
|
||||
<p class="toc-title">{{t .Lang "contents"}}</p>
|
||||
<ol>
|
||||
{{- range .Headings}}{{if .ID}}
|
||||
<li class="toc-{{.Level}}"><a href="#{{.ID}}">{{.Text}}</a></li>
|
||||
{{- end}}{{end}}
|
||||
</ol></nav>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -135,6 +135,8 @@ var exampleFeatures = []featureCase{
|
||||
expect: []string{"<kbd>Shift</kbd>"}, absent: []string{"raw HTML omitted"}},
|
||||
{what: "a margin note is a container the theme places", path: "/writing/typography/", code: 200,
|
||||
expect: []string{`<aside class="side">`, `<p class="side-title">On line breaking</p>`}},
|
||||
{what: "the reference theme's fragments come from a directory, not one file", path: "/pages/colophon/", code: 200,
|
||||
expect: []string{`<details name="run" open>`, "💡"}},
|
||||
{what: "an icon renders through the theme, and an unknown one keeps its text", path: "/pages/colophon/", code: 200,
|
||||
expect: []string{"💡", ":nosuchicon:"}},
|
||||
{what: "grouped panels are tabs, and need no script", path: "/pages/colophon/", code: 200,
|
||||
|
||||
Reference in New Issue
Block a user