diff options
| -rw-r--r-- | PLAN.md | 381 |
1 files changed, 0 insertions, 381 deletions
diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 5b4a5cd..0000000 --- a/PLAN.md +++ /dev/null @@ -1,381 +0,0 @@ -# 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 exactly one production dependency, `github.com/alecthomas/chroma/v3`, for - fenced-code syntax highlighting. Add 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. -- `markdown/fenced_code_block` also receives `.HighlightedContent`. It is - `template.HTML` containing only Chroma-generated token markup, with no code - block wrappers, and is empty when the fence has no known language or cannot - be highlighted. Its `.Content` remains the escaped source-code fallback. -- 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`, `HighlightedContent template.HTML`, `Language string` | Escaped code, optional Chroma token markup, 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 chroma"><code class="weft language-{{.Language}}">{{if .HighlightedContent}}{{.HighlightedContent}}{{else}}{{.Content}}{{end}}</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-time syntax highlighting - -Defining `markdown/fenced_code_block` activates build-time highlighting through -`github.com/alecthomas/chroma/v3`. It emits static, class-based HTML; the site -supplies matching CSS for the fixed Chroma token-class prefix in its normal -stylesheet, including any light/dark theme rules. No JavaScript, client-side -highlighter, external executable, generated stylesheet, or new CLI option is -involved. - -The Weft fenced-code handler owns the node when this template is defined: - -1. Extract the raw fence contents and the existing first-word `.Language`. -2. Look up that exact language in Chroma. Do not guess a lexer for blank or - unknown info strings. -3. For a known lexer, format the code with Chroma's class-based HTML formatter, - configured not to emit its own code-block wrappers, and assign that trusted - fragment to `.HighlightedContent`. -4. On an absent/unknown language or any Chroma tokenization/formatting failure, - leave `.HighlightedContent` empty. `.Content` remains the escaped source in - every case, so templates have a deterministic safe fallback. -5. Execute the fenced-code template. Indented `markdown/code_block` nodes are - not highlighted by this feature. - -Only Chroma's output may become `template.HTML`; the Markdown code source must -never be trusted directly. A fence containing text such as -`</span><script>…</script>` must remain escaped even when highlighting succeeds. - -### Rejected integrations - -Goldmark remains the renderer that walks every rendered AST subtree. Weft -renders one direct child of the document at a time solely to preserve the -existing `.Sections` contract—one `template.HTML` fragment for each top-level -Markdown node. Rendering the whole document once would lose those source-node -boundaries; moving the same per-child buffering into a document renderer would -only obscure the current `renderSections` responsibility. - -Do not register `goldmark-highlighting` as a Goldmark extension. Like Weft's -planned handler, it registers a renderer for `ast.KindFencedCodeBlock`, but it -writes the complete highlighted block, including its own wrappers, directly to -Goldmark's output writer. It therefore cannot compose with a template that -owns the wrappers and needs a `.HighlightedContent` value. Its useful -integration is already just Chroma tokenization and HTML formatting, and its -fenced-block render method is not a public fragment-returning API; invoking it -and stripping wrappers would be brittle. It also depends on Chroma v2 rather -than the chosen v3. Call Chroma directly from Weft's fenced-code handler -instead. - -## 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, including the Chroma-backed fenced-code handler when that template - is defined, 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`. The fenced-code - handler additionally follows the build-time syntax-highlighting contract. -- 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, table adapter, and direct Chroma v3 - fenced-code highlighter. -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/highlighting behavior, and - representative examples. - -No CLI flag, manifest change, default-template file, or public AST wrapper is -needed. Add only the Chroma v3 production dependency. - -## 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 and highlighting:** Heading IDs, fenced-code language, - escaped code content, Chroma token markup for a known language, an empty - highlighted value for blank/unknown languages, and blockquote content reach - templates correctly. Highlighted hostile source remains escaped. -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. -- Fenced-code highlighting produces static Chroma markup only when its node - template is defined; blank/unknown languages fall back to escaped source and - no client-side JavaScript is required. -- Documentation defines every template name and every available field. |
