sprig and prose docs classic docs
Reference› ADRs› ADR016 UsingMarkdown

ADR016 UsingMarkdown

Title: Using markdown

  • Status: Accepted
  • Decided: D2026-08-30
  • Followed by: ADR010 TitleBlock
  • Revises: ADR015 MarkdownInDescribe
  • Related: ADR031 BracesInProse
  • Affects: Describe, Parser, Ui

Context

015 refused markdown as a describe feature. The language would not parse it, the manifest would not carry it, and a consumer would not have to speak it. What 015 left open was markdown-lite: emphasis, strong emphasis, and lists, as editorial syntax inside describe.

The problem with taking that opening into the language is that markdown is itself a language. A small dialect in the parser is still a dialect. The compiler would have to accept it, reject the rest, and explain the line. Every later mark — a heading, a link, a fence — would arrive as a request to widen a feature that already shipped. That is the erosion 014 named for values, applied here to markup.

So the standing 015 already implied, and this record keeps, is that markdown remains outside the scope of prose. This record is a slight revision to that ruling, not a reversal of it.

Describe still has to store something. The handwritten parser captures the block as the author's text. The question this record answers is whether that text is a markdown document the language has opinions about, or the words as written, with rendering left to whoever reads the manifest. Two UIs already disagreed: the original explorer turns obvious lists into lists, and the beta field guide reflows paragraphs and stops there. A language-level markdown-lite would have forced them to agree. The revision is that they do not have to.

010 later stated the test this split implies. A second renderer can legitimately disagree with the author about emphasis. It cannot legitimately disagree about Carpenter's Workshop. Formatting is the consumer's business. Authored knowledge is not.

Decision

Markdown remains outside the scope of prose. Describe accepts whatever the author wrote, verbatim. The language makes no guarantee about how that prose is rendered.

Downstream consumers may apply whatever rendering they choose to the prose in the manifest. The sprig UI may apply light editorial formatting according to rules it chooses.

Consequences

Authoring. An author may write *this*, **that**, or a hyphen at the start of a line. Prose will not reject those characters, interpret them, or promise that a reader will see italics, bold, or bullets. What the author wrote is what the manifest holds.

Cost. Two consumers of the same universe can show the same describe differently, and the author cannot control that from the language. A field guide that reflows paragraphs and an explorer that turns hyphens into list items are both within the ruling. The disagreement is the price of not making markdown a language feature. Authors who care how a particular UI looks are writing for that UI, not for prose.

What this does not do. It does not add markdown-lite to the grammar. It does not require any consumer to parse markdown. It does not take back 015's refusal of full markdown as a describe feature. A consumer that renders nothing but paragraphs is as correct as one that renders lists.

The title test. 010 follows this record because title had looked like the same kind of display concern. The useful question is whether a second renderer could disagree. For editorial marks it can, which is why they stay out of the language. For a humanized name it can, which is why humanization stays out too. For the real title it cannot, which is why title stays in.

What to watch. A consumer that does parse describe as markdown will treat the text as markdown, including the parts that were never meant that way. Four spaces of continuation, which source files use as indent, is a markdown indented code block. Verbatim storage plus a markdown-reading consumer is how 015's unstructured-space problem returns without anyone adding a language feature. The capture and dedent work in prose-parser 0.9.0 exists because that misreading was real.

Alternatives considered

Markdown-lite in the parser. Emphasis, strong, lists, as grammar. Rejected. Markdown is a language. A small one is still one, and the compiler would own every later widening. 015 left this open; this record closes it as a language change and leaves it available as a consumer choice.

Strip or forbid markdown-looking characters in describe. Rejected. Verbatim means the author's words. Forbidding * or a leading hyphen would be the language having a markdown opinion, which is the thing being refused. It would also mangle ordinary prose: asterisks, dashes, and numbered lines are writing, not markup, until a renderer decides otherwise.

A rendering contract: every consumer formats lists and emphasis the same way. Rejected. That is markdown-lite enforced at the ecosystem boundary instead of in the grammar. It drafts every consumer into one dialect, which is 015's original burden, and it still does not say what to do with a heading or a fence when someone writes one.

Keep 015 as a hard line that consumers must not interpret formatting. This is the part that was revised. 015's reason that consumers should not have to parse markdown still stands. This record says they are not forbidden to format what they are given. The burden becomes optional, which is the slightness of the revision: the language still does not speak markdown, and a consumer that wants editorial marks may apply them.

Canonical HTML (or another rendered form) in the manifest, plus the source. Rejected. The compiler would then pick a markdown dialect and a renderer, and every other consumer would either trust that HTML or ignore it and do the work again. Prose would be in the business of producing pages.

Evidence

The parser already stores the block as authored text. parseDescribe in packages/prose-parser/src/parser.js slices the source between the braces. After trim and dedent, that string is the describe value. There is no markdown step.

The two UIs are the live demonstration that rendering is a consumer choice. packages/sprig-ui/src/lib/components/Prose.svelte parses lists when the server sends X-Describe-Render-Mode: lists, which packages/sprig-ui/src/server.js currently always does. The field guide does not: packages/sprig-ui-beta/src/lib/components/ProseBlock.svelte splits on blank lines via paragraphs() in packages/sprig-ui-beta/src/lib/model/text.js and prints the result. Same manifest field, two renderings, no grammar in between.

Prose-parser 0.9.0 recorded a related fact in its changelog: continuation lines used to keep their source indentation, "whose second and later paragraphs any markdown reader would treat as an indented code block." That is what verbatim storage costs a consumer that applies markdown rules. The fix was to dedent the captured text, not to make prose a markdown language.