feat: shortcode round-trip engine — .md ⇄ element tree with canonical serialization #19
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!19
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #2
What this does
Adds
internal/content, a standalone package (no deps on server/supervisor) that parses Hugo content files into a typed element tree and serializes the tree back to one canonical, human-readable form — plus a debug endpoint exposing the parsed tree as JSON.Design decisions
parser/pageparser(v0.161.1, matching the CI/hugo binary), no hand-rolled tokenization.go mod tidy'd to the minimal require set..Innerusage; this engine has no templates, so a close tag pops to the nearest matching open (which becomes a block), unmatched opens are inline, and canonical inline form is the explicit self-close{{< button label="x" />}}— unambiguous to both this parser and Hugo regardless of template.string|bool|int64|float64, HugoValTypedsemantics), sorted by name, unique; positional params are a parse error. One body per element.{{<in text, unrepresentable backslash/backtick combos, front-matter fence hazards, shortcodes nested in.inlinebodies. Comment-escaped shortcodes ({{</* … */>}}) round-trip verbatim.Round-trip guarantees (all tested)
serialize(parse(x))byte-identical for canonicalx— 13-file corpus incl. every planned v1 shape (grid>column nesting, accordion items, hero with inline buttons mid-text)parse(serialize(tree))structurally identical — 720-tree seeded property test + Go native fuzz target (FuzzParseRoundTrip, 185s/13.7M execs clean; the generator caught one real bug — the.inlinenesting guard — before it shipped)Debug surface
GET /api/debug/tree?page=posts/first.md→{"page":…,"tree":…}withkind-discriminated nodes; 400/404/405/422 (parse errors include the line) with JSON error bodies; path-traversal guarded viafilepath.IsLocal.Verification
go build ./...,go vet ./...,go test ./...(content, server, supervisor) and the frontend vitest suite all green locally; CI runs the same steps.🤖 Generated with Claude Code
https://claude.ai/code/session_012RD9ScCJbiaeV1uvbkcFfs
Parse Hugo content files (front matter + body) into a typed element tree using Hugo's vendored parser/pageparser as the lexer, and serialize the tree back to a single canonical, human-readable form. - Typed nodes: front matter (opaque, format-preserving), block shortcode, inline (self-closing) shortcode, text run; JSON marshaling with kind discriminators for the debug surface. - Pairing resolved syntactically (Hugo consults templates, we cannot): a close tag pops to the nearest matching open; unmatched opens are inline; explicit '/>' self-close is canonical for inline elements. - Flat named scalar params (string|bool|int64|float64, Hugo ValTyped semantics), sorted by name; positional params rejected at parse. - Canonical layout: tags on their own lines at column 0 (indentation would trip markdown's 4-space code-block rule), single-space tags, double-quoted strings with backtick-raw fallback, bare scalars. - serialize(parse(x)) is byte-identical for canonical x (corpus tests); parse(serialize(tree)) is structurally identical for valid trees (720-tree seeded property test + native fuzz target, 185s+ clean). - Serialize errors instead of silently mangling: newlines in param values, unescaped '{{<' in text, unrepresentable backslash/backtick combos, front-matter fence hazards, nested shortcodes in '.inline' bodies. - Corpus covers the planned v1 element shapes: grid>column nesting, accordion items, hero with inline buttons mid-text, plus front matter formats, markup shortcodes, comment-escaped shortcodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RD9ScCJbiaeV1uvbkcFfsGET /api/debug/tree?page=<content-relative-path> parses the page with internal/content and returns {page, tree} as JSON. Guards: GET only (405), required page param (400), filepath.IsLocal traversal check (400), missing file (404), parse failure (422 with line info). The handler now takes the site's content directory at construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RD9ScCJbiaeV1uvbkcFfs