diff options
Diffstat (limited to 'PLAN.md')
| -rw-r--r-- | PLAN.md | 320 |
1 files changed, 320 insertions, 0 deletions
@@ -0,0 +1,320 @@ +# Recursive Markdown rendering templates + +## Goal + +Let site templates override the HTML rendering of every element-producing +CommonMark or GFM construct supported by Weft. Overrides must compose +recursively: a node template receives its already-rendered children as +`.Content`, and nested overrides continue to apply inside it. + +The feature is complete when a site can put `class="weft"` on every HTML +element produced from Markdown without replacing page templates or disabling +Goldmark parsing. + +## Constraints + +- Keep Goldmark as the Markdown parser and default HTML renderer. +- Preserve current output when no Markdown node templates are defined. +- Preserve `.Sections` as `[]template.HTML`, with one fragment per top-level + Markdown node in source order. +- Keep raw Markdown HTML disabled. Do not provide an override that can turn an + omitted raw HTML node back on. +- Continue using `html/template`; do not mark destinations or other + user-controlled strings as `template.URL`. +- Add no dependencies and no configuration or plugin layer. +- Do not expose Goldmark AST nodes to site templates. +- Implement production behavior before changing tests that currently assume + sections are rendered during discovery. + +## Template naming and fallback + +Markdown rendering templates share the existing site template set and use the +`markdown/` prefix: + +```gotemplate +{{define "markdown/paragraph"}}<p class="weft">{{.Content}}</p>{{end}} +{{define "markdown/strong"}}<strong class="weft">{{.Content}}</strong>{{end}} +``` + +For each node: + +1. Render its children recursively. +2. Execute its `markdown/...` template when that template is defined. +3. Otherwise emit Goldmark's current default HTML for that node. + +An override may intentionally omit `.Content` to discard the node's children. +One subtype override must not disable defaults for sibling subtypes. For +example, defining only `markdown/strong` must leave ordinary emphasis rendered +as `<em>` while still allowing the nested strong override to run. + +Reject defined template names beginning with `markdown/` when they are not in +the supported-name table below. This makes misspelled overrides build errors +instead of silently unused templates. Other template names remain unaffected. + +## Data conventions + +- `.Content` is always `template.HTML` and always means the node's primary + rendered content. For container nodes it is recursively rendered children; + for code spans and code blocks it is escaped code text. +- `.Content` is omitted only for nodes with no child content, such as images, + task checkboxes, thematic breaks, and hard breaks. +- `.Destination`, `.Title`, `.Alt`, `.Language`, and `.ID` are plain strings so + `html/template` applies the correct contextual escaping. +- Optional string values are the empty string when absent. +- No generic `.Node`, `.Attributes`, `.Text`, or metadata map is exposed. + +## Supported template objects + +### Core block constructs + +| Template | Fields | Meaning | +| --- | --- | --- | +| `markdown/heading1` through `markdown/heading6` | `Content template.HTML`, `ID string` | Heading contents and Goldmark's generated heading ID. | +| `markdown/paragraph` | `Content template.HTML` | Rendered inline paragraph contents. | +| `markdown/blockquote` | `Content template.HTML` | Rendered block contents. | +| `markdown/code_block` | `Content template.HTML` | Escaped contents of an indented code block. The template owns both `<pre>` and `<code>` wrappers. | +| `markdown/fenced_code_block` | `Content template.HTML`, `Language string` | Escaped code and the first word of the fence info string. The template owns both wrappers. | +| `markdown/unordered_list` | `Content template.HTML`, `Tight bool` | Rendered list items. | +| `markdown/ordered_list` | `Content template.HTML`, `Start int`, `Tight bool` | Rendered list items and the first source marker's number. `Start` is populated even when it is `1`. | +| `markdown/list_item` | `Content template.HTML` | Rendered block contents, including nested lists. | +| `markdown/thematic_break` | no fields | A thematic break such as `---`; the template owns the `<hr>`. | + +`Tight` follows CommonMark. Tight list items contain unwrapped inline content; +loose list item paragraphs pass through `markdown/paragraph` and normally +produce `<p>` elements. + +Do not expose list marker, indentation offset, item index, or item number. +Goldmark keeps `Start` on the list, while CommonMark disregards numeric markers +after the first item. The ordered-list wrapper is responsible for `<ol +start="...">`; `<li>` elements need only `.Content`. + +### Core inline constructs + +| Template | Fields | Meaning | +| --- | --- | --- | +| `markdown/emphasis` | `Content template.HTML` | Contents of `*emphasis*`. | +| `markdown/strong` | `Content template.HTML` | Contents of `**strong emphasis**`. | +| `markdown/code_span` | `Content template.HTML` | Escaped, Goldmark-normalized inline code contents. | +| `markdown/link` | `Content template.HTML`, `Destination string`, `Title string` | Explicit link label, rewritten destination, and optional Markdown title. | +| `markdown/autolink` | `Content template.HTML`, `Destination string` | URL autolink label and destination. | +| `markdown/email_autolink` | `Content template.HTML`, `Destination string` | Email label and a destination including the `mailto:` prefix. | +| `markdown/image` | `Alt string`, `Destination string`, `Title string` | Flattened plain-text image description, destination, and optional title. Images have no rendered child content. | +| `markdown/hard_break` | no fields | The `<br>` produced by a Markdown hard line break. | + +Image formatting is flattened for `.Alt`, matching Goldmark: + +```markdown + +``` + +provides `Alt: "A very black cat"`, not HTML containing `<strong>`. + +Code-span `.Content` must preserve Goldmark's code-span whitespace +normalization before being HTML-escaped. Plain text and soft line breaks do not +produce elements and therefore have no override. + +### GFM constructs + +| Template | Fields | Meaning | +| --- | --- | --- | +| `markdown/strikethrough` | `Content template.HTML` | Contents of `~~strikethrough~~`. | +| `markdown/task_checkbox` | `Checked bool` | Disabled task-list checkbox; the template owns the `<input>`. | +| `markdown/table` | `Content template.HTML` | Rendered table header followed by an optional table body. | +| `markdown/table_header` | `Content template.HTML` | Rendered header cells. The template owns both `<thead>` and its `<tr>`. | +| `markdown/table_body` | `Content template.HTML` | Rendered body rows. This is a synthetic Weft node for Goldmark's generated `<tbody>`. It is omitted when there are no body rows. | +| `markdown/table_row` | `Content template.HTML` | Rendered body cells; the template owns `<tr>`. | +| `markdown/table_header_cell` | `Content template.HTML`, `Alignment string` | Header-cell contents and `left`, `right`, `center`, or `none`. | +| `markdown/table_cell` | `Content template.HTML`, `Alignment string` | Body-cell contents and `left`, `right`, `center`, or `none`. | + +The synthetic `table_body` is required because Goldmark's AST has table, +header, row, and cell nodes but its HTML renderer creates `<tbody>` as a side +effect between those nodes. Modeling the body explicitly makes partial table +overrides composable and lets a site class the generated `<tbody>`. + +Raw inline HTML and raw HTML blocks remain rendered as Goldmark's omission +comments and have no template names. Document, text-block, text, and string +nodes do not emit their own HTML elements and also have no templates. + +## Representative usage + +```gotemplate +{{define "markdown/ordered_list"}} +<ol class="weft"{{if ne .Start 1}} start="{{.Start}}"{{end}}>{{.Content}}</ol> +{{end}} + +{{define "markdown/list_item"}} +<li class="weft">{{.Content}}</li> +{{end}} + +{{define "markdown/fenced_code_block"}} +<pre class="weft"><code class="weft{{with .Language}} language-{{.}}{{end}}">{{.Content}}</code></pre> +{{end}} + +{{define "markdown/table_header"}} +<thead class="weft"><tr class="weft">{{.Content}}</tr></thead> +{{end}} + +{{define "markdown/table_body"}} +<tbody class="weft">{{.Content}}</tbody> +{{end}} +``` + +## Build lifecycle refactor + +Current discovery renders `.Sections` before site templates are loaded. Split +parsing from rendering: + +1. Create one site-level Goldmark instance with GFM and automatic heading IDs. +2. During discovery, read frontmatter, parse each Markdown body, rewrite links, + derive page metadata, and retain the source bytes and AST on unexported page + fields. Leave `.Sections` empty. +3. Discover and load the complete site template set as today. +4. Validate `markdown/` template names and configure the Markdown template + renderer before Goldmark's renderer is used for the first time. +5. Render every page's retained top-level AST nodes into `.Sections`. +6. Only after every page has sections, render Markdown pages and explicit + `.html.tmpl` or `.xml.tmpl` outputs. This preserves feeds and other templates + that consume another page's `.Sections`. +7. Keep validation and atomic output writing unchanged. + +Retaining parse state should use unexported fields on `Page` rather than a new +public model. Site templates must continue to see only the documented exported +page fields. + +## Renderer design + +Add a small Goldmark `NodeRenderer` backed by the loaded `html/template` set. +It should register handlers only for node kinds affected by at least one +defined override, except for the table adapter described below. + +For an overridden container node: + +1. On the entering callback, recursively render each child through the same + fully configured Goldmark renderer into a buffer. +2. Execute the selected site template with typed data containing that buffer as + `.Content`. +3. Return `ast.WalkSkipChildren` so the outer traversal does not render the + subtree twice. +4. Do nothing on the matching exiting callback. + +Re-entering the configured renderer preserves custom templates at every nested +level while allowing an unhandled parent to continue through Goldmark's stock +renderer. Do not remove or reparent AST nodes while rendering. + +Goldmark represents headings, emphasis/strong, ordered/unordered lists, and +URL/email autolinks with shared node kinds. If any subtype is overridden, the +handler must dispatch by subtype and emit Goldmark-equivalent wrapper HTML for +an unoverridden sibling subtype while still recursively rendering its children. + +Leaf handlers extract their values directly: + +- Escape and normalize code before assigning trusted `.Content`. +- Flatten image descendants to plain `.Alt` independently of inline override + templates. +- Resolve email autolink destinations to include `mailto:`. +- Leave explicit link destinations as plain strings after existing `.md` to + `.html` rewriting. + +Hard breaks need a narrowly scoped text-node handler only when +`markdown/hard_break` is defined. It must preserve Goldmark's existing text +escaping and newline behavior and replace only the generated `<br>` fragment. + +### Table adapter + +If no table-related override exists, leave the complete table subtree to +Goldmark for exact current output. If any table-related override exists, +intercept the table root and render its semantic pieces recursively: + +```text +table +├── table_header +│ └── table_header_cell... +└── table_body (only with body rows) + └── table_row... + └── table_cell... +``` + +For each undefined table template, emit the equivalent current Goldmark HTML +wrapper. This localized fallback is necessary because Goldmark opens and closes +`<tbody>` across separate AST callbacks, which makes independent partial +overrides invalid without the adapter. Preserve Goldmark's current cell +alignment output when a cell template is absent. + +## Error handling + +- Wrap Markdown template execution errors with the page source path and + template name, for example `render notes/example.md markdown/link: ...`. +- Treat unknown `markdown/` definitions as template-loading errors and list the + unsupported name. +- Propagate recursive child-render errors without continuing the build. +- Preserve the existing all-or-nothing output behavior: no files change after + any Markdown rendering failure. + +## Production changes + +Keep the implementation to the existing files unless the renderer makes +`main.go` materially harder to follow: + +1. Change page parsing in `main.go` to retain Markdown source and AST without + rendering sections. +2. Add the post-template-load section-rendering phase. +3. Add the node-template lookup, typed template-data values, recursive + renderer, hard-break handler, and table adapter. +4. Keep `renderSections` responsible for the top-level `.Sections` boundary, + but have it use the newly configured renderer. +5. Update `README.md` with the lifecycle-neutral public contract, full template + table, list-numbering rule, escaping behavior, and representative examples. + +No CLI flag, manifest change, dependency, default-template file, or public AST +wrapper is needed. + +## Checks after production behavior exists + +Use the existing `main_test.go`; add the smallest set of table-driven or +end-to-end checks that covers the contract: + +1. **No-override compatibility:** Existing Markdown/GFM output and `.Sections` + behavior remain unchanged when no `markdown/` definitions exist. +2. **Recursive composition:** A paragraph containing emphasis, strong text, + code, and a link receives every nested override once and in source order. +3. **Subtype fallback:** Defining only `markdown/strong`, one heading level, or + one list type leaves sibling subtypes at Goldmark defaults while nested + overrides still apply. +4. **Block fields:** Heading IDs, fenced-code language, escaped code content, + and blockquote content reach templates correctly. +5. **List contract:** Tight and loose lists report `.Tight`; ordered lists + beginning with `1`, repeated `1`, repeated `3`, and arbitrary later numbers + expose only the correct first-item `.Start`; nested lists remain recursive. +6. **Link and image fields:** Explicit link title and rewritten destination, + URL/email autolinks, image title, and formatting-flattened `.Alt` are exact. +7. **GFM fields:** Strikethrough, checked and unchecked task boxes, all table + pieces, empty table bodies, and each alignment value render through their + templates. +8. **Generated elements:** A representative document defining every supported + override can place `class="weft"` on every emitted Markdown HTML element, + including `<br>`, `<input>`, and `<tbody>`. +9. **Partial table overrides:** Overriding only a cell or only `table_body` + still yields valid, balanced table HTML with default wrappers elsewhere. +10. **Safety:** Code and alt text stay escaped, dangerous custom link + destinations remain subject to `html/template` URL filtering, and raw HTML + cannot be re-enabled. +11. **Errors and atomicity:** Unknown names and execution failures identify the + page/template and preserve previously generated files. +12. **Cross-page consumers:** Feed and explicit output templates see populated + `.Sections` for every page regardless of source sort order. + +Update discovery-focused tests that currently inspect `.Sections` immediately +after `discover()` to invoke the new preparation phase or assert only discovery +state. Do not retain eager production rendering solely for those tests. + +## Acceptance criteria + +- A site can override every template listed above independently. +- Recursive `.Content` works through arbitrarily nested supported nodes. +- A site can mark every Markdown-generated HTML element with a class. +- Missing overrides preserve Goldmark behavior, including inside overridden + ancestors and around overridden descendants. +- Existing sites with no `markdown/` templates produce the same pages. +- `.Sections`, page inventory, feed generation, link rewriting, validation, + output ownership, and atomic writes retain their current public behavior. +- Documentation defines every template name and every available field. |
