| id | 212 | |
|---|---|---|
| title | `mdsmith extract` projects paragraph inline spans as data | |
| status | ✅ | |
| summary | Add a content-entry projection mode that emits a paragraph's inline spans (text, emphasis, strong, code, link) as a structured list. Consumers stop walking AST or matching Markdown markup by hand; they read the typed segments from the extract JSON. | |
| model | opus | |
| depends-on |
|
A paragraph's plain text projection works for prose where
rendering is up to the consumer. It fails when the consumer
needs the structure inside the paragraph. Which span is
emphasised. Which token is a link. Which fragment is code.
Today paragraph → text drops every inline mark. Downstream
consumers walk the AST themselves. internal/release/messaging.go
parses the messaging headline's *…* span by re-running the
goldmark parser inside the release tool. Others reach for
regex on raw markup.
After this plan, a content entry can declare an inline
projection that emits a typed list of spans:
sections:
- heading: { regex: '^Headline$' }
content:
- { kind: paragraph, projection: inline, required: true }…and extract emits the recursive shape. Container spans
carry children. Leaf spans carry value. For the headline
Mark*down*, smithed.:
"headline": {
"inline": [
{ "span": "text", "value": "Mark" },
{ "span": "emphasis", "level": 1, "children": [
{ "span": "text", "value": "down" }
]},
{ "span": "text", "value": ", smithed." }
]
}Nesting is supported uniformly: a **mdsmith fix** strong
containing a code span emits
{ "span": "strong", "level": 2, "children": [
{ "span": "code", "value": "mdsmith fix" }
] }Consumers walk one shape — no flat-vs-recursive mode switch, no fail mode for content that happens to nest.
Then internal/release/messaging.go reads headline.inline
and looks for the one emphasis span. No Markdown parsing on
the release side. Just a typed walk over data.
-
Content-projection field on schema. Add a
projection:key to schema content entries. Allowed values:text(the current default),code(for code blocks, already implicit), andinline(new). Validate at schema-load time; rejectprojection: inlineon non-paragraph kinds. -
AST → typed-span walker. In
internal/extract, implement the inline-span walker. Container spans (emphasis, strong, link) carrychildren; leaf spans (text, code, autolink) carryvalue. The mapping from goldmark AST to span object:AST node Emitted span ast.Text{span: text, value}ast.CodeSpan{span: code, value}ast.AutoLink{span: autolink, value, url}ast.EmphasisLevel 1{span: emphasis, level: 1, children: [...]}ast.EmphasisLevel 2{span: strong, level: 2, children: [...]}ast.Link{span: link, url, title?, children: [...]}Container spans recurse through the same walker, so nesting composes naturally. Anything not in this table (raw HTML, images, custom inline) is a hard error from extract.
-
YAML / msgpack passthrough. The same projection mode works for
--format yamland--format msgpack. The in-memory tree is one shape; only the serializer changes. -
Default-key collision. A scope declaring both
{kind: paragraph, projection: text}and{kind: paragraph, projection: inline}would emit two sibling keys; declare and document the default keys (textandinline) so a schema author can resolve a collision viabind:. -
Adopt in messaging. Switch
docs/brand/messaging.md's## Headlinefrom a code block to a paragraph withprojection: inline. Drop theparseHeadlineEmphasishelper. Drop the import ofpkg/markdownfrominternal/release/. The release tool replaces the AST walker with a typed walk: find the firstemphasisspan at the top level, flatten itschildrento text (rejecting non-text children), pre / em / post fall out from the sibling positions.mdsmith-release sync-messaging --checkstays clean.Note:
messaging.gono longer importspkg/markdown, and the headline helper is deleted.syncdocs.gokeeps its ownpkg/markdownuse. That use is Hugo doc reconciliation, not headline parsing, and is out of scope here. -
Documentation. Add an "Inline-span projection" subsection to the extract reference showing the mapping table and a nesting example, and a worked example to the extract-markdown-as-data guide.
- A paragraph content entry with
projection: inlineemits a{inline: [...]}object where each element is a typed span (text / emphasis / strong / code / link / autolink). Container spans carrychildren; leaf spans carryvalue. - Nested inline (
**`code`**,[**bold**](url), etc.) round-trips through the projection without error; the consumer walks one uniform shape. -
internal/release/messaging.gono longer importspkg/markdownor parses Markdown itself. The headline parsing helper is deleted; the release tool readsheadline.inlinedirectly. (syncdocs.go's unrelatedpkg/markdownuse for Hugo doc reconciliation is out of scope.) -
mdsmith extractrejects an unsupported inline node (raw HTML, image, custom) when the schema asks forinline. - The mapping table is documented in the extract reference; the worked example in the guide shows both schema and JSON output (including one nested case).
- Every file this plan touches passes
mdsmith check;mdsmith-release sync-messaging --checkreports no drift on the messaging surfaces. (A repo-widemdsmith check .from this isolated worktree reports a pre-existing baseline of index / catalog drift in untouched files, because the.claude/worktrees/**ignore pattern shadows the worktree path; that baseline is unchanged by this plan.) - All tests pass:
go test ./...— except the pre-existingpkg/mdsmith.TestInvalidateRewritesDependentFile(plan 215's acceptance test, failing on the branch base, in a package this plan does not touch). -
go tool golangci-lint runreports no issues.