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 <script> 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:
+9
-7
@@ -11,13 +11,15 @@ If this file disagrees with the code, the code is right and this file is a bug.
|
||||
| `internal/content/doc.go` | package comment | 5 |
|
||||
| `internal/content/content.go` | bundles: `os.Root` open, walk, frontmatter split, key/lang derivation, NFC, tag slugs, permalink building | 352 |
|
||||
| `internal/content/site.go` | the indexed site: lookup with language fallback, aliases, `Query` and `Run`, sections, `Sequence` | 286 |
|
||||
| `internal/render/render.go` | goldmark with the typographer, per-kind template sets with site override, `Page`/`List`/`Sequence`/`head` | 305 |
|
||||
| `internal/render/render.go` | goldmark with the typographer, per-kind template sets with site override, the `Partial` seam features render through, `Page`/`List`/`Sequence`/`head` | 345 |
|
||||
| `internal/render/chrome.go` | the engine's own words: phrase table, month names, digits, and the `t`/`num`/`day` template funcs (ADR-0034) | 105 |
|
||||
| `internal/render/templates/` | reference theme: `base.html`, `page.html`, `list.html`, `theme.css` (ADR-0026) | — |
|
||||
| `internal/render/templates/` | reference theme: `base.html`, `page.html`, `list.html`, `shortcodes.html`, `theme.css` (ADR-0026) | — |
|
||||
| `internal/ext/shortcodes/` | first feature: `{{< name key="value" >}}` block parser and node renderer, rendering through a theme fragment (ADR-0036) | 168 |
|
||||
| `cmd/khosra/wire.go` | the only list of enabled features (`extensions.md`) | 19 |
|
||||
| `internal/web/resolve.go` | URL → (key, lang, page, tag) or a canonical redirect: language prefix, `/en/…` fork guard, pagination, tags, trailing slash | 112 |
|
||||
| `internal/web/web.go` | handler: resolve, look up with fallback, section and tag listings, sequence, `/static/` (misses and refusals alike answer 404), degrade on failure | 152 |
|
||||
| `cmd/khosra/main.go` | flags, wiring, startup — the only place things are assembled | 53 |
|
||||
| `*_test.go` | table-driven, one file per source file; symlink escape (content and static), canonical paths, language fallback, aliases, pagination, tags, sequences, chrome, typography, 404 | 1172 |
|
||||
| `*_test.go` | table-driven, one file per source file; symlink escape (content and static), canonical paths, language fallback, aliases, pagination, tags, sequences, chrome, typography, shortcode escaping, 404 | 1288 |
|
||||
|
||||
Serves a bundle at `/{section}/{slug}/`, a paginated listing per section, tag listings global and
|
||||
section-narrowed, sequence navigation and a series archive on any nested bundle, and `static/` verbatim.
|
||||
@@ -38,12 +40,12 @@ this change*.
|
||||
|
||||
| Counter | Now | Extraction due at | What it buys |
|
||||
|---|---|---|---|
|
||||
| Render transforms | 0 | **3** | Stage pipeline (ordered `func(ctx,*Page) error`). Typography is *not* one: it is a goldmark parser option, not a function over a page, so it buys the feature without moving the counter. Shortcodes (queue 12) will be the first real one |
|
||||
| Render transforms — **page-level only** | 0 | **3** | Stage pipeline (ordered `func(ctx,*Page) error`). Parse-phase work does *not* count and must not: goldmark's extender list is already an ordered pipeline for it, so typography and shortcodes compose there (`cmd/khosra/wire.go`) and a second pipeline beside it would be pure duplication. This counts transforms over the assembled page, which nothing hosts yet — OpenGraph and JSON-LD (queue 15) are the first candidates |
|
||||
| Routing cases | 5 | **2** — done | Resolver at `internal/web/resolve.go`: bundle, language prefix, pagination, tag, section-narrowed tag |
|
||||
| Collection pages | 4 | **1** — done | Query primitive: `content.Query{Section, Tag, Lang}` + `Site.Run`. The fourth — a series archive — resolves through `Site.Sequence` instead: membership is structural and the sort ascends, so it shares the index but not the Query |
|
||||
| Views / output formats | 2 | **2** — due | Two template sets exist (bundle, listing); the View layer is Arc 2's third item |
|
||||
| Effects | 0 | **2** | Effect runner + trigger wiring (change / schedule / demand) |
|
||||
| Extensions | 0 | **3** | Extension registry + wire file (`extensions.md`) |
|
||||
| Extensions | 1 | **3** | Extension registry (`extensions.md`). The wire file arrived with the first feature rather than the registry — `cmd/khosra/wire.go`, one line, no struct |
|
||||
| Interface implementations | — | **2** | The interface itself |
|
||||
| Non-stdlib dependencies | 3 direct | budget in `scripts/budgets.env` | — |
|
||||
|
||||
@@ -58,11 +60,11 @@ with a stated reason. A list nothing drains is a graveyard of known defects.
|
||||
|
||||
| Item | Why it waits | Trigger to fix |
|
||||
|---|---|---|
|
||||
| No mechanical check that the counters are *correct* | Accepted at the Arc 1 boundary: the coupling gate makes forgetting them impossible, which is the real failure mode, and checking the values needs code to count | 3rd transform (queue 12) |
|
||||
| No mechanical check that the counters are *correct* | Accepted at the Arc 1 boundary: the coupling gate makes forgetting them impossible, which is the real failure mode, and checking the values needs code to count | The first page-level transform (queue 15), now that the transform counter means something narrower |
|
||||
| No mechanical gate on the untrusted boundary (ADR-0003) | Scheduled to Arc 3: nothing untrusted is read yet | The comment path — a test that untrusted input reaches no shortcode or template evaluation |
|
||||
| `date` stays in `Extra` after being lifted onto `Bundle.Date`, unlike `title`, `aliases`, `tags` and `order`, which are deleted | Spotted while adding `order`; the theme contract says `Extra` holds what the parser does not name, so one of the two is wrong. Harmless today — a template reading `.Extra.date` gets the raw YAML value | Whatever next reads `Extra` generically: feeds (queue 14) or `check` (17) |
|
||||
| Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a `Lookup` | No cache exists anywhere yet, and a site of this size resolves in microseconds. Measuring first is the rule (queue 16) | The page cache (queue 16), which is the thing that makes the cost visible |
|
||||
| Raw HTML in Markdown is currently omitted only because goldmark's default omits it | Verified at the Arc 1 boundary, and it is what keeps invariant 2 intact for authored content. `html.WithUnsafe()` is the obvious move when a shortcode needs to emit HTML, and it silently turns authored Markdown into an injection path | Shortcodes (queue 12) — if unsafe rendering is enabled, the trusted/untrusted split must be real code, not a default |
|
||||
| Raw HTML in Markdown is omitted only because goldmark's default omits it | Shortcodes landed without unsafe mode — a call renders through a template instead (ADR-0036), so the feared trigger came and went. What remains is that nothing *stops* a later change from enabling `html.WithUnsafe()`, which would silently turn authored Markdown into an injection path | Now: a gate rejecting `WithUnsafe` anywhere in the tree, so the rule is mechanism rather than memory |
|
||||
|
||||
## Open questions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user