Skip to content

marmot-markdown: emit a structured block for <details>/<summary> disclosures #891

Description

@mubarakcoded

Summary

marmot-markdown passes raw HTML tags through as literal text, so <details> / <summary> disclosure markup reaches consumers as plain Inline::Text instead of a structured block. Downstream clients therefore can't render a native collapsible disclosure without reimplementing HTML parsing themselves. Requesting that the parser recognize <details> / <summary> and emit a structured block through the AST and the marmot-uniffi FFI.

Current behavior

The parser intentionally does not interpret raw HTML:

  • crates/marmot-markdown/src/inline.rs:405-406"tag-like sequences pass through unchanged", buf.push('<').
  • No HTML-block leaf exists in crates/marmot-markdown/src/block.rs — the verbatim leaves are only FencedCode / MathBlock (line 127). A line starting with <details> just accumulates into Leaf::Paragraph.

Dumping marmot_markdown::parse for a disclosure shows two shapes depending on blank lines.

No blank lines — one paragraph, tags as literal Text, body inline-parsed:

Input:

<details>
<summary>Tap to expand</summary>
Hidden body **bold**
</details>

Output:

Document { blocks: [ Paragraph { inlines: [
  Text("<details>"), SoftBreak,
  Text("<summary>Tap to expand</summary>"), SoftBreak,
  Text("Hidden body "), Strong([Text("bold")]), SoftBreak,
  Text("</details>") ] } ] }

With blank lines — split into separate paragraphs:

Document { blocks: [
  Paragraph { inlines: [Text("<details>"), SoftBreak, Text("<summary>Sum</summary>")] },
  Paragraph { inlines: [Text("Body para")] },
  Paragraph { inlines: [Text("</details>")] } ] }

Impact

Because the structure is never emitted, any consumer that wants to render a disclosure has to reimplement HTML parsing on top of the AST — mapping tag boundaries that fall inside Text inlines, or re-parsing a body substring — which is fragile and must be duplicated in every client that consumes marmot-markdown. Concretely, a downstream client change that attempted app-side detection is a silent no-op (it renders the literal tags) while its unit tests stay green, because those tests hand-build an assumed AST the real parser never produces.

Requested change

Recognize <details ...> / <summary ...> and emit a structured block, e.g.:

Block::Details {
    summary: Vec<Inline>,
    open: bool,
    body: Vec<Block>,
}

surfaced through marmot-uniffi as MarkdownBlockFfi::Details { summary, open, body }, so every consumer renders it directly and consistently.

Suggested semantics:

  • Honor the open attribute (initially expanded).
  • Summary carries inline formatting; body is block-level markdown.
  • Keep the existing DoS bounds (source-scan / tag-length caps).
  • Keep escaped (&lt;details&gt;) and inline-code (`<details>`) forms literal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions