{ "description": "Markdown rendering contract (issue #531): message text -> platform-neutral semantic tree. Both parsers — web (vendored markdown-it walked by web/static/markdown.js) and native (apple/swift-markdown walked by MarkdownTree.swift) — must normalize every input to exactly this tree. Blocks: p{children} quote{children} ul{items} ol{start,items} codeblock{text,lang}. Inlines: text{text} br code{text} link{href,children} strong/em/strike{children}. Adjacent text runs always merge. These vectors are the spec: when the twins disagree, fix the implementations, never the vectors.", "vectors": [ { "name": "plain_text", "input": "hello world", "plain": "hello world", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "hello world" } ] } ] }, { "name": "empty_message", "input": "", "tree": [], "note": "no blocks at all — renderers must tolerate an empty tree" }, { "name": "whitespace_only", "input": " ", "tree": [] }, { "name": "bold", "input": "**b**", "plain": "b", "tree": [ { "t": "p", "children": [ { "t": "strong", "children": [ { "t": "text", "text": "b" } ] } ] } ] }, { "name": "italic_star", "input": "*i*", "tree": [ { "t": "p", "children": [ { "t": "em", "children": [ { "t": "text", "text": "i" } ] } ] } ] }, { "name": "italic_underscore", "input": "_i_", "tree": [ { "t": "p", "children": [ { "t": "em", "children": [ { "t": "text", "text": "i" } ] } ] } ] }, { "name": "bold_italic", "input": "***bi***", "tree": [ { "t": "p", "children": [ { "t": "em", "children": [ { "t": "strong", "children": [ { "t": "text", "text": "bi" } ] } ] } ] } ], "note": "CommonMark nests em > strong (the old grammar rendered strong > em — visually identical)" }, { "name": "intraword_underscore_literal", "input": "snake_case_name", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "snake_case_name" } ] } ], "note": "CommonMark forbids intraword _ emphasis, matching the old grammar's word-boundary gate" }, { "name": "intraword_star_formats", "input": "in*ner*", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "in" }, { "t": "em", "children": [ { "t": "text", "text": "ner" } ] } ] } ] }, { "name": "spaced_stars_literal", "input": "a * b * c", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "a * b * c" } ] } ], "note": "flanking rules: the old grammar wrongly italicised ' b ' here — deliberate fix" }, { "name": "nested_marks", "input": "**b _i_**", "tree": [ { "t": "p", "children": [ { "t": "strong", "children": [ { "t": "text", "text": "b " }, { "t": "em", "children": [ { "t": "text", "text": "i" } ] } ] } ] } ] }, { "name": "inline_code", "input": "`co*de*`", "plain": "co*de*", "tree": [ { "t": "p", "children": [ { "t": "code", "text": "co*de*" } ] } ], "note": "markers never format inside code" }, { "name": "inline_code_backtick", "input": "``a`b``", "tree": [ { "t": "p", "children": [ { "t": "code", "text": "a`b" } ] } ] }, { "name": "multiple_code_spans_with_link_like_content", "input": "hello `example.com` and `other.com` OK?", "plain": "hello example.com and other.com OK?", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "hello " }, { "t": "code", "text": "example.com" }, { "t": "text", "text": " and " }, { "t": "code", "text": "other.com" }, { "t": "text", "text": " OK?" } ] } ], "note": "issue #818: two SEPARATE code spans, each ending at its own nearest closing backtick — not one span greedily spanning first-backtick to last-backtick. Neither domain-looking span autolinks; code content never re-parses." }, { "name": "strike_single_tilde", "input": "~s~", "tree": [ { "t": "p", "children": [ { "t": "strike", "children": [ { "t": "text", "text": "s" } ] } ] } ], "note": "history: old messages use the original grammar's single ~" }, { "name": "strike_double_tilde", "input": "~~s~~", "tree": [ { "t": "p", "children": [ { "t": "strike", "children": [ { "t": "text", "text": "s" } ] } ] } ] }, { "name": "strike_mismatched_literal", "input": "~~s~", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "~~s~" } ] } ], "note": "an opener only pairs with a closer of the same length" }, { "name": "strike_triple_tilde_inline_literal", "input": "x ~~~s~~~ y", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "x ~~~s~~~ y" } ] } ], "note": "runs of 3+ tildes never strike (cmark-gfm)" }, { "name": "tilde_fence_info_string", "input": "~~~s~~~", "tree": [ { "t": "codeblock", "text": "", "lang": "s~~~" } ], "note": "at line start this is a spec-valid unterminated tilde fence with info string 's~~~' — both parsers agree" }, { "name": "strike_inside_em", "input": "*a ~b~ c*", "tree": [ { "t": "p", "children": [ { "t": "em", "children": [ { "t": "text", "text": "a " }, { "t": "strike", "children": [ { "t": "text", "text": "b" } ] }, { "t": "text", "text": " c" } ] } ] } ] }, { "name": "bold_link_interplay", "input": "**github.com/apps/x**", "tree": [ { "t": "p", "children": [ { "t": "strong", "children": [ { "t": "link", "href": "https://github.com/apps/x", "children": [ { "t": "text", "text": "github.com/apps/x" } ] } ] } ] } ], "note": "the motivating #531 bug: emphasis spanning a bare link" }, { "name": "bare_https_url", "input": "see https://example.com/a?b=1 now", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "see " }, { "t": "link", "href": "https://example.com/a?b=1", "children": [ { "t": "text", "text": "https://example.com/a?b=1" } ] }, { "t": "text", "text": " now" } ] } ] }, { "name": "www_url", "input": "www.example.com", "tree": [ { "t": "p", "children": [ { "t": "link", "href": "https://www.example.com", "children": [ { "t": "text", "text": "www.example.com" } ] } ] } ] }, { "name": "bare_tld_allowlisted", "input": "try example.com", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "try " }, { "t": "link", "href": "https://example.com", "children": [ { "t": "text", "text": "example.com" } ] } ] } ] }, { "name": "filenames_stay_plain", "input": "main.js and node.go", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "main.js and node.go" } ] } ], "note": "BARE_TLDS excludes file extensions" }, { "name": "email_not_linked", "input": "mail a@b.com", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "mail a@b.com" } ] } ] }, { "name": "explicit_link", "input": "[docs](https://example.com/a)", "plain": "docs", "tree": [ { "t": "p", "children": [ { "t": "link", "href": "https://example.com/a", "children": [ { "t": "text", "text": "docs" } ] } ] } ] }, { "name": "explicit_link_formatted_label", "input": "[**b**](https://example.com)", "tree": [ { "t": "p", "children": [ { "t": "link", "href": "https://example.com", "children": [ { "t": "strong", "children": [ { "t": "text", "text": "b" } ] } ] } ] } ] }, { "name": "no_autolink_inside_link_label", "input": "[see example.com](https://y.com)", "tree": [ { "t": "p", "children": [ { "t": "link", "href": "https://y.com", "children": [ { "t": "text", "text": "see example.com" } ] } ] } ], "note": "links never nest — bare URLs in a label stay text" }, { "name": "angle_autolink", "input": "", "tree": [ { "t": "p", "children": [ { "t": "link", "href": "https://example.com", "children": [ { "t": "text", "text": "https://example.com" } ] } ] } ] }, { "name": "javascript_link_literal", "input": "[x](javascript:alert(1))", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "[x](javascript:alert(1))" } ] } ], "note": "validateLink: http(s) only — rejected links never tokenize" }, { "name": "relative_link_literal", "input": "[x](/rel)", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "[x](/rel)" } ] } ] }, { "name": "mailto_link_literal", "input": "[x](mailto:a@b.c)", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "[x](mailto:a@b.c)" } ] } ] }, { "name": "heading_stays_literal", "input": "# hi **b**", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "# hi " }, { "t": "strong", "children": [ { "t": "text", "text": "b" } ] } ] } ], "note": "heading rule never enabled; inline still formats" }, { "name": "hr_stays_literal", "input": "---", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "---" } ] } ] }, { "name": "setext_stays_literal", "input": "title\n===", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "title" }, { "t": "br" }, { "t": "text", "text": "===" } ] } ] }, { "name": "table_stays_literal", "input": "| a | b |\n| - | - |", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "| a | b |" }, { "t": "br" }, { "t": "text", "text": "| - | - |" } ] } ] }, { "name": "image_stays_literal", "input": "![alt](https://x.com/i.png)", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "![alt](https://x.com/i.png)" } ] } ], "note": "images reconstruct as literal source text on both twins" }, { "name": "image_bad_scheme_literal", "input": "![a](/rel)", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "![a](/rel)" } ] } ] }, { "name": "tasklist_stays_literal", "input": "- [x] task", "tree": [ { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "[x] task" } ] } ] ] } ], "note": "native re-synthesizes the checkbox prefix cmark-gfm force-parses" }, { "name": "inline_html_literal", "input": "hi **b**", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "hi " }, { "t": "strong", "children": [ { "t": "text", "text": "b" } ] } ] } ], "note": "native merges adjacent text around InlineHTML to match" }, { "name": "indented_code_stays_plain", "input": " indented", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "indented" } ] } ], "note": "the 4-space rule is never enabled — it mangles pasted text" }, { "name": "smart_typography_stays_literal", "input": "\"quotes\" -- dashes...", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "\"quotes\" -- dashes..." } ] } ], "note": "pins native's .disableSmartOpts (cmark curls quotes by default)" }, { "name": "bullet_list", "input": "- a\n- b", "plain": "a b", "tree": [ { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] } ], [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ] }, { "name": "nested_list", "input": "- a\n - b", "tree": [ { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] }, { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ] ] } ] }, { "name": "ordered_list_start", "input": "3. a\n4. b", "tree": [ { "t": "ol", "start": 3, "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] } ], [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ] }, { "name": "loose_list_normalizes_tight", "input": "- a\n\n- b", "tree": [ { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] } ], [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ], "note": "loose/tight is erased — the tree (and rendering) is always tight" }, { "name": "empty_list_item", "input": "- \n- b", "tree": [ { "t": "ul", "items": [ [], [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ] }, { "name": "paren_ordered_marker", "input": "1) paren marker", "tree": [ { "t": "ol", "start": 1, "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "paren marker" } ] } ] ] } ] }, { "name": "list_interrupts_paragraph", "input": "text\n- a\n- b", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "text" } ] }, { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] } ], [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ] }, { "name": "blockquote", "input": "> q **b**", "tree": [ { "t": "quote", "children": [ { "t": "p", "children": [ { "t": "text", "text": "q " }, { "t": "strong", "children": [ { "t": "text", "text": "b" } ] } ] } ] } ] }, { "name": "blockquote_lazy_continuation", "input": "> a\nb", "tree": [ { "t": "quote", "children": [ { "t": "p", "children": [ { "t": "text", "text": "a" }, { "t": "br" }, { "t": "text", "text": "b" } ] } ] } ] }, { "name": "blockquote_multi_paragraph", "input": "> a\n>\n> b", "tree": [ { "t": "quote", "children": [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] }, { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] } ] }, { "name": "nested_blockquote", "input": "> > deep", "tree": [ { "t": "quote", "children": [ { "t": "quote", "children": [ { "t": "p", "children": [ { "t": "text", "text": "deep" } ] } ] } ] } ] }, { "name": "list_in_blockquote", "input": "> - a\n> - b", "tree": [ { "t": "quote", "children": [ { "t": "ul", "items": [ [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] } ], [ { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] ] } ] } ] }, { "name": "fence_in_blockquote", "input": "> ```\n> code\n> ```", "tree": [ { "t": "quote", "children": [ { "t": "codeblock", "text": "code", "lang": "" } ] } ] }, { "name": "fenced_code", "input": "```js\ncode **here**\n```", "plain": "code **here**", "tree": [ { "t": "codeblock", "text": "code **here**", "lang": "js" } ], "note": "trailing newline stripped from the tree's text" }, { "name": "unterminated_fence", "input": "```\nunterminated", "tree": [ { "t": "codeblock", "text": "unterminated", "lang": "" } ] }, { "name": "tilde_fence", "input": "~~~\ntilde fence\n~~~", "tree": [ { "t": "codeblock", "text": "tilde fence", "lang": "" } ] }, { "name": "soft_break", "input": "a\nb", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "a" }, { "t": "br" }, { "t": "text", "text": "b" } ] } ], "note": "every newline is a break in chat — soft and hard breaks both normalize to br" }, { "name": "hard_break_two_spaces", "input": "line \nhard", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "line" }, { "t": "br" }, { "t": "text", "text": "hard" } ] } ] }, { "name": "hard_break_backslash", "input": "back\\\nslash", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "back" }, { "t": "br" }, { "t": "text", "text": "slash" } ] } ] }, { "name": "paragraph_split", "input": "a\n\nb", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "a" } ] }, { "t": "p", "children": [ { "t": "text", "text": "b" } ] } ] }, { "name": "backslash_escape", "input": "\\*lit\\*", "plain": "*lit*", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "*lit*" } ] } ], "note": "old clients show the backslashes — the composer serializer escapes minimally for this reason" }, { "name": "entity_decodes", "input": "& <", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "& <" } ] } ], "note": "cmark decodes entities unconditionally; web enables the rule to match" }, { "name": "mention_is_render_layer", "input": "@Ada hello", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "@Ada hello" } ] } ], "note": "mention chips are scanned at render time against the roster, never in the tree" }, { "name": "emoji_is_render_layer", "input": "🎉", "tree": [ { "t": "p", "children": [ { "t": "text", "text": "🎉" } ] } ], "note": "jumbo-emoji detection stays a raw-text helper" } ] }