add shortcodes as the first internal/ext feature

A call is `{{< name key="value" >}}` alone on a line, parsed by a goldmark block
parser into an AST node and rendered by executing a theme template of that name
(ADR-0036). `figure` ships; `include` and `gallery` need the including bundle's
directory, which the parser does not carry yet, so they wait.

The layering did the design work here. internal/render may not import
internal/ext, so render.New takes a callback that receives a Partial and returns
Markdown extensions, and cmd/khosra/wire.go holds the only list of enabled
features. Empty that list and the engine still builds and serves — which is the
property extensions.md says the contract should have.

Raw HTML stays disabled. An author's text reaches a page only as arguments that
html/template escapes in context, which the real binary shows: a hostile alt
becomes &lt;script&gt; and src="javascript:…" becomes #ZgotmplZ. Getting
contextual escaping from the standard library rather than writing it is the whole
reason a fragment renders this instead of the feature.

parseSet became variadic so the fragment set reuses it rather than growing a
second copy of the overlay logic; `Partial` takes map[string]string after the
advisory correctly flagged `any` as generality nothing had asked for.
This commit is contained in:
2026-07-30 10:29:00 +06:00
parent 0b3f88a166
commit 820720de08
14 changed files with 459 additions and 55 deletions
+21 -4
View File
@@ -73,6 +73,23 @@ these functions. An address is not chrome (`content-model.md`).
A site root cannot add or override a phrase yet. A theme needing its own words writes them in its own
block; site-supplied strings wait for the settings cascade (`ideas/deferred-decisions.md`).
## 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 arguments, never any HTML (ADR-0036).
| Shortcode | Template | Receives |
|---|---|---|
| `{{< figure src="…" alt="…" caption="…" >}}` | `figure` | `.src`, `.alt`, `.caption` — every argument as written, escaped on output |
Arguments arrive as strings and are escaped by `html/template` like any other data, which is what keeps an
author's text out of the markup. A call whose template is missing renders nothing and logs; it never fails
the page.
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.
## The stability rule
Fields and names are **added, never renamed or removed**. Absence is always legal: a template reading a
@@ -137,10 +154,10 @@ two drift together.
## Overriding it
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). Per kind, exactly two files are
overlaid — `base.html` and that kind's block file (`page.html` or `list.html`). 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.
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`
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.