CommonMark, plus what GitHub renders, for sysl. The module is sh.sysl.markdown.
Status: 0.4.0 — CommonMark and GitHub's Markdown complete, plus smart punctuation and definition lists. Every one of the CommonMark spec's 652 examples renders exactly, with the GitHub extensions off and on; every extension example of GitHub's spec renders exactly; hostile inputs are read in linear time; and differential checks against cmark and cmark-gfm agree on their whole corpora but for the cases listed below.
dependencies {
markdown { git = "github.com/sysl-lang/markdown", version = "0.4.0" }
}import sh.sysl.markdown.{parse, to_html}
print(to_html(parse("# Hello\n"))) // <h1>Hello</h1>
parse(src: string) -> Doc // CommonMark: parse_with(src, commonmark())
parse_with(src: string, opts: ParseOptions) -> Doc
parse_with_regions(src: string, opts: ParseOptions, regions: Buf[Region]) -> Result[Doc, RegionError]
to_html(d: Doc) -> string // as the spec renders: to_html_with(d, commonmark_html())
to_html_with(d: Doc, opts: HtmlOptions) -> string
render_html(d: Doc, opts: HtmlOptions, out: *Writer)
plain_text(d: Doc, n: NodeId) -> string // the text under a node, for slugs, alt text, search
struct ParseOptions // commonmark(): no extensions
exts: Registry // gfm(): tables, strikethrough, autolinks, task lists, math, mermaid
struct HtmlOptions // commonmark_html(): true, "\n", None, false, Standard, Standard
raw_html: bool // gfm_html(): true, "\n", None, true, Standard, Standard
softbreak: string
highlight: Option[&Fn(string, string) -> Option[string]] // (info, code) -> the block's markup
tagfilter: bool // GitHub's filter on raw HTML
math: ClientMarkup // Standard (KaTeX/MathJax auto-render) or GitHub
mermaid: ClientMarkup // Standard (mermaid.js) or GitHub
struct Region // a run of program lines, for weave
span: Span
info: string
literal: string
- A parse and a rendering are told separately, so one parse can be rendered several ways; the
link reference definitions live on the
Doc. raw_html:truepasses raw HTML through as the spec renders it (the CommonMark preset);falsewrites<!-- raw HTML omitted -->in place of each HTML block and each piece of inline HTML, as cmark's "safe" mode does. It governs raw HTML only -- ajavascript:link is written either way.highlightis asked about every code block with its whole info string and its text; what it answers replaces the whole<pre><code>…</code></pre>, andNonefalls back to that.parse_with_regionsreads a woven file: each region -- whole lines, in source order, not overlapping -- closes every open block and becomes a code block of the document whose span points into the real file. The rest is Markdown, and it is one document with one set of link references.plain_textis the text a reader sees: entities decoded, code spans' content, link and image text but not their destinations, no raw HTML, a soft break as a space and a hard break as a newline, one newline between blocks. A node an extension made reads as itsExtension.plain_textanswers (Unseen,Words(text)orBlock(text)), followed by what it holds: a table is a row a line with its cells separated by a tab, an alert its title and then its body, a formula its TeX, an emoji its character, a footnote reference its number, a mermaid diagram nothing.
This is checked line for line by tests_api.sysl's readme_example:
import sh.sysl.markdown.*
val d = parse("# Hello, *world*\n\nSee <b>this</b>.\n")
print(to_html(d)) // <h1>Hello, <em>world</em></h1> <p>See <b>this</b>.</p>
print(to_html_with(d, HtmlOptions(false, "\n", None, false, Standard, Standard))) // ... <p>See <!-- raw HTML omitted -->this<!-- raw HTML omitted -->.</p>
print(plain_text(d, d.root())) // Hello, world / See this.
val code: &Fn(string, string) -> Option[string] = (info, text) ->
if info == "sysl" then Some("<pre class=\"sysl\">" + escape_html(text) + "</pre>") else None
val opts = HtmlOptions(true, "\n", Some(code), false, Standard, Standard)
print(to_html_with(parse("```sysl\na < b\n```\n"), opts)) // <pre class="sysl">a < b</pre>
val d = parse_with("| a | b |\n|---|--:|\n| ~~c~~ | www.d.com |\n", gfm())
print(to_html_with(d, gfm_html()))
gfm() turns on the five extensions cmark-gfm has, then GitHub's emoji shortcodes, math, mermaid
and alerts, and gfm_html() its tag filter. Each extension is an ordinary Extension, so a parse
wanting some of them builds its own Registry: footnotes() (first, as cmark-gfm settles footnotes
before the others look at the tree), tables(), strikethrough(), autolinks(), task_lists(),
emoji(), math(), mermaid(), alerts(). Every rule of the first five is cmark-gfm's
(0.29.0.gfm.13, which GitHub runs), down to the corners its spec does not write down:
-
Tables -- a delimiter row under a paragraph whose last line has as many cells;
\|is a pipe in a cell, code spans included; a short row is filled out and a long one cut; any line that is not blank continues the table unless a block starts on it; a paragraph that met a delimiter row of the wrong width never becomes a table; and a table stops taking rows once it has filled out 524,288 cells, so a wide header over many one-cell rows stays linear. Cells carryalign="…". -
Strikethrough --
~a~or~~a~~, both runs the same length; three or more~are text. -
Task list items --
[ ],[x]or[X]after a list marker that begins the line, written as a disabled checkbox (Item(Some(checked))in the tree). -
Extended autolinks --
http://,https://,ftp://,www.and email addresses (withmailto:andxmpp:forms), without trailing punctuation or an unmatched). -
The tag filter --
HtmlOptions.tagfilterwrites the<oftitle,textarea,style,xmp,iframe,noembed,noframes,scriptandplaintextas<. -
Footnotes --
[^label]refers to a definition[^label]: text, which holds blocks: the rest of its line, then lines indented four columns, empty lines and lazy lines. Labels match as link labels do, the first of two definitions wins, footnotes are numbered by first reference (those inside definitions counting where they stand), an unreferenced definition is dropped and a reference to nothing is the text[^label]. The definitions are written at the end in<section class="footnotes" data-footnotes><ol>, each<li id="fn-label">ending in↩back-references, the second and later references to a footnote taking idsfnref-label-2… The one place this departs from cmark-gfm is a reference with a line break inside it, whose label cmark-gfm measures by column and loses (tests_gfm_deviations.sysl). -
Emoji --
:smile:is 😄, for every alias of GitHub's gemoji table; anything else between colons, GitHub's own pictures such as:octocat:included, stays text. Read wherever text is and never in code or a destination;\:before one keeps it text.emoji_for("smile")looks one up. -
Alerts -- a block quote directly in the document whose first line is
[!NOTE],[!TIP],[!IMPORTANT],[!WARNING]or[!CAUTION](any case, nothing else on the line) and which holds something more. cmark-gfm does not read these -- GitHub adds them afterwards -- so the rules are what GitHub's renderer was observed to do (its/markdownAPI ingfmmode, 2026-10-08): a marker inside a list item or a nested quote is text,\[!NOTE]is still a marker and*[!NOTE]*is not, and a quote holding only the marker stays a quote. The HTML is GitHub's structure without the octicon<svg>it puts before the title's word, and with a newline between blocks as everywhere else:<div class="markdown-alert markdown-alert-note"> <p class="markdown-alert-title">Note</p> <p>body</p> </div>
Heading ids are an extension of their own, heading_ids(), and not in gfm(): GitHub's spec
shows headings without ids, and the preset renders what the spec shows.
val r = registry()
r.add(heading_ids())
print(to_html(parse_with("# Hello, World!\n# Hello, World!\n", ParseOptions(r))))
// <h1 id="hello-world">Hello, World!</h1>
// <h1 id="hello-world-1">Hello, World!</h1>
The slug is GitHub's (github_slug, github-slugger's: lowercased, punctuation and symbols dropped,
spaces to -, Unicode letters kept), taken from the heading's text, a shortcode counted as written
(## :rocket: Go is rocket-go, as on GitHub); a slug already used is numbered -1, -2 in
document order. Each heading is then Heading(level, Some(id)) in the tree, which is what a table of
contents reads. heading_ids_with(HeadingIdOptions(slug, prefix, anchors)) plugs in a slug of one's
own, writes a prefix before every id (GitHub's pages use user-content-), and with anchors gives
each heading GitHub's <a id="…" class="anchor" aria-hidden="true" href="#…"> link to itself, which
then carries the id in the heading's place. A heading whose slug comes out empty (# alone, # !)
has no id and no anchor, and takes no number from the headings after it.
Smart punctuation is an extension of its own too, smart_punctuation(), and not in gfm():
GitHub writes quotes as they are typed. Its rules are cmark's --smart, and tools/differential.sh --smart compares the two over the whole corpus.
val r = registry()
r.add(smart_punctuation())
print(to_html(parse_with("\"Don't,\" she said -- 'wait'...\n", ParseOptions(r))))
// <p>“Don’t,” she said – ‘wait’…</p>
A " or ' pairs with another as emphasis does, by the same flanking rules (an opener may not
follow ] or )), and a pair makes no node, so a quotation may overlap emphasis as text does; a
' that pairs with nothing is an apostrophe, ’. A run of hyphens is em dashes where its length
is a multiple of three, en dashes where it is even, and otherwise em dashes then one en dash or
two; a single hyphen stays. ... is …. Code spans, code blocks, autolinks, raw HTML, entities
and backslash escapes are never changed.
Definition lists are definition_lists(), in no preset either: PHP Markdown Extra's and
Pandoc's syntax, a paragraph of terms (one to a line) followed by definitions, each opened by :
and a space or tab.
val r = registry()
r.add(definition_lists())
print(to_html(parse_with("Apple\n: A round fruit.\n: A company.\n", ParseOptions(r))))
// <dl>
// <dt>Apple</dt>
// <dd>A round fruit.</dd>
// <dd>A company.</dd>
// </dl>
A definition is a container, as a list item is: a line indented to its text continues it, so it may
hold several paragraphs, a list or code, and its paragraph takes lazy continuation lines. A : opens
one at most three spaces in, and may interrupt a paragraph, whose lines become the terms; ::, a
: with nothing after it, and a : not followed by a space or tab open nothing. A definition after
a definition joins its list; one that follows no paragraph -- at the start of a container, after a
heading -- is a paragraph again, its : kept as text. A blank line between any two parts of a
list makes it loose, each definition's paragraphs in <p> (what the Scala juicer writes for its
blank-line-separated items); otherwise a definition's paragraph is bare in the <dd>, as a tight
list item's is. The tree is Ext nodes named definition_list, tagged DEFINITION_LIST_NODE,
DEFINITION_TERM_NODE and DEFINITION_NODE.
Math and mermaid have no spec and no cmark-gfm extension; their rules are github.com's, read off
its own renderer (gh api markdown, gfm mode) and written out at the top of gfm_math.sysl:
- Math --
$…$and$`…`$inline,$$…$$for display, and a fence whose info string beginsmath. An opening$is not followed by a space and not preceded by a letter or digit; its closing$is the next unescaped one, not preceded by a space and not followed by a letter or digit -- so$5 and $10is text. A paragraph of nothing but$$…$$is a display block and may run over several lines. The TeX is literal: no emphasis or escape is read inside it (\$stays\$, TeX's dollar), and a code span, raw HTML or autolink is never cut in half by a$. - Mermaid -- a fence whose info string begins
mermaidis a diagram. - Both take precedence over
highlight: the fences becomeExtnodes when the parse finishes, so the renderer never shows them to the hook. With the extensions off they are ordinary code blocks, and the hook is asked. - The markup is the rendering's --
HtmlOptions.mathand.mermaid, each aClientMarkup.Standard(both presets) writes<span class="math math-inline">\(x\)</span>,<div class="math math-display">\[x\]</div>(<span>for$$…$$inside a line) and<pre class="mermaid">…</pre>: what KaTeX's or MathJax's auto-render and mermaid.js find with their defaults.GitHubwrites github.com's<math-renderer class="js-inline-math">$x$</math-renderer>andjs-display-mathelements, and<pre lang="mermaid"><code>…</code></pre>.
An extension is an impl Extension; every method but name has a default that declines, so it
writes only the hooks it uses. The built-in extensions are written against exactly this.
trait Extension
name(self) -> string
block_start(self, line: Line) -> BlockStart = Declined
interrupts_paragraph(self) -> bool = false
block_continue(self, line: Line, kind: Kind, state: *u64) -> BlockContinue = Ends
block_close(self, c: Closing, state: u64) -> Kind = c.kind
triggers(self) -> string = ""
inline_match(self, s: Subject) -> InlineMatch = NoMatch
delimiter_node(self, ch: u8, opener: usize, closer: usize) -> Option[Kind] = None
bracket(self, s: Subject, from: usize) -> Option[Kind] = None
post_pass(self, d: Doc) = ()
post_pass_phase(self) -> PostPhase = Structure // Structure passes, then Annotate ones
render(self, w: *HtmlWriter, d: Doc, n: NodeId, ev: Event) = ()
plain_text(self, d: Doc, n: NodeId) -> Seen = Unseen // what a reader sees, for plain_text
enum Seen // Unseen: nothing of its own; children still read
Unseen
Words(text: string) // in the run of text
Block(text: string) // a block of its own; "" is a row's separation
enum BlockStart
Declined
Mark(word: u64) // decline, and keep a word with the paragraph
Leaf(kind: Kind, consumed: usize, state: u64) // lines taken verbatim
Container(kind: Kind, consumed: usize, state: u64) // holds blocks
Lines(kind: Kind, consumed: usize, state: u64, from_para: usize) // lines taken as a paragraph's
Retag(container: Kind, consumed: usize) // re-mark the block the line reached
enum InlineMatch
NoMatch
Inline(kind: Kind, consumed: usize) // a node; Text(lit) puts lit in the bytes' place
Delimiters(count: usize, can_open: bool, can_close: bool) // a run, paired into a node as emphasis is
Paired(can_open: bool, can_close: bool, alone: string, opening: string, closing: string)
// one byte pairing as a quotation mark: no node
struct ExtNode // Kind.Ext(node: ExtNode)
ext: string // the extension that made it, and renders it
tag: u32
data: string
holds: bool // has children
- A node of the extension's own is
Ext(ExtNode(...)), so an extension outside this package adds kinds of node without touchingKind. The document keeps the registry it was read with, and the HTML renderer hands eachExtnode to therenderof the extensionextnames -- on entering it, and on exiting it where it holds children -- with theHtmlWriterthe page goes to:lit,tag(nothing inside an image's alt text),esc,href,crandoptions(). - A block hook sees a
Line-- the text, where the containers' prefixes stop, the block the line has reached (within), the open paragraph's text (para) and the word the extension kept with it (para_word).Linesis how a block takes a paragraph's last lines as its own, as a table's header does;Retagis how a task list marks its list item. - A closing block is shown a
Closing: its text, andchild(parent, kind, from, to)andinlines(node, pieces), which give it children read from that text -- each mapped back to the source for its span, andpiecesleaving out bytes the block's own syntax owns, as a cell's\|. - An inline hook sees a
Subject-- the leaf's text, the trigger's offset, and whether a link's brackets are open -- and answers a node (a container node is given the bytes it read as itsText) or a run of delimiters, paired by the rule of three as emphasis is, then bydelimiter_node. bracketis asked about a]that closed an active[or![without making a link -- aSubjectat the]and where the bracketed text begins -- andSome(kind)replaces the brackets and everything between them with one node, as a footnote reference does.block_continuemay answerIndented(columns)where the block takes indentation by column, a tab counted to its stop, as a footnote definition takes its four.post_passsees the finishedDocand rewrites it withset_kind,append_child,insert_after,append_nodeandunlink, as the email autolinks, the footnotes and the alerts do.- Every
Structurepass runs before everyAnnotatepass, each phase in registry order, so a pass that reads what another made does not depend on the order a program added them in.Structureis the default;heading_idsisAnnotate, so## Notes[^n]isnotes1whetherheading_ids()was added beforefootnotes()or after. A pass that only reads the tree another pass has finished answersAnnotatefrompost_pass_phase.
- The syntax is CommonMark 0.31.2, checked against the spec's 652 examples, comparing HTML exactly with no normalisation: the spec's HTML is the reference renderer's own output.
- Plus what GitHub renders, as extensions: tables, strikethrough, task lists, extended autolinks,
footnotes, alerts (
> [!NOTE]), heading ids slugged as GitHub slugs them, and emoji shortcodes. - Plus math and mermaid, as GitHub does them:
$…$,$$…$$and```mathfences are written out as TeX for a client-side renderer;```mermaidfences are handed on the way GitHub hands them to mermaid.js. - Raw HTML is passed through as the spec renders it. GitHub's tag filter is an option, off in the CommonMark preset.
- Out unless asked for: Pandoc attributes, definition lists, smart punctuation.
- One arena of nodes, linked by index. Blocks and inlines alike live in one
Buf[Node]on theDoc, each with its parent, first and last child, and siblings as indices, and a byteSpaninto the source. A rewrite over every node is a flat loop withset_kind; moving a node is a relink. - Walking and rendering do not recurse, so input nested a hundred thousand deep renders.
- The HTML renderer writes what the reference renderers write, byte for byte:
&,<,>and"escaped, a link destination percent-encoded asmdurl.encodedoes it (an existing%XXkept,äas%C3%A4), and a code block's class from the info string's first word as cmark cuts it, at ASCII whitespace, with no secondlanguage-before one that has it. - Linear on hostile input. cmark's pathological inputs are read in time proportional to their size, and references expand to at most the document's size (or 100,000 bytes, whichever is more), as cmark caps them, so a long definition used many times cannot make the output explode.
- Extensions are an open trait,
Extension, asked at three points -- a block starter (with whether it may interrupt a paragraph, a continuation check and a close), an inline trigger byte, and a pass over the finished tree -- and asked to render the nodes it made. The built-in extensions are written against the same trait; see Writing an extension. - Link reference definitions live on the
Doc, so one parse can be rendered several ways.
spec/spec.json is CommonMark 0.31.2's, unmodified, as published at
https://spec.commonmark.org/0.31.2/spec.json:
sha256 d431b29d97b6f73e69d547109cf5081578fac931e72afe95639ebe766c1b2a20
tools/gen_spec_tests.py writes it out as sh/sysl/markdown/tests_spec_*.sysl, one file per spec
section and one @test per example. A section the parser does not read yet is generated with its
examples ignored; listing it in the script's ENABLED and rerunning makes them run. The generated
files are committed.
spec/gfm-spec.txt is GitHub Flavored Markdown's spec, unmodified, as test/spec.txt of
github/cmark-gfm at the tag 0.29.0.gfm.13 (commit
587a12bb54d95ac37241377e6ddc93ea0e45439b):
sha256 7d8e5814befec287ac116786d81ff14e0adc9b13295b4494649e995408fd871c
The same script writes its 24 extension examples -- 8 tables, 2 strikethrough, 2 task lists, 11
autolinks, 1 tag filter -- as tests_gfm_<extension>.sysl, read with gfm() and gfm_html() and
compared exactly. The two task-list examples' HTML is written normalised in that spec (its runner
normalises before comparing), so their tests assert the HTML cmark-gfm writes. Its CommonMark
examples are 0.29's and are not taken; instead tests_gfm_commonmark.sysl reads all 652 of
CommonMark 0.31.2's with every extension on. Eleven render differently, as cmark-gfm's own runner
lists such examples -- six HTML blocks the tag filter rewrites and five autolink examples whose text
an extended autolink now links -- and each asserts what cmark-gfm renders with the same extensions.
Math and mermaid change none of the 652: no example's $ meets the delimiter rules, and none has a
math or mermaid fence. Footnotes and alerts change none either.
spec/cmark-gfm-extensions.txt is cmark-gfm's own extension tests, unmodified, as
test/extensions.txt at the same tag:
sha256 a2a45e98be9fca95f564f927265a0f63beea6cae5369d1cf4bde44caa51b2a3a
GitHub's spec has no footnote examples; this file's three are written out as
tests_gfm_footnotes.sysl, compared exactly.
spec/entities.json is WHATWG's list of HTML5 named character references, unmodified, as published
at https://html.spec.whatwg.org/entities.json:
sha256 d741d877ac77c4194c4ad526b5b4a19aef8dfe411ab840a466891cdbb9f362e6
tools/gen_entities.py writes the 2,125 names that end in ; -- the only ones CommonMark
recognises -- into sh/sysl/markdown/entity_table.sysl as one sorted string, which a lookup
bisects. One string literal adds about 0.01 s to a cold build, where a table of pairs would add
seconds. The generated file is committed.
spec/gemoji.json is db/emoji.json of github/gemoji, unmodified,
at commit fadaeaf1f1a9be82b321316a6c5502e43138b2f6; spec/github-slugger-regex.js and
spec/github-slugger-fixtures.json are regex.js and test/fixtures.json of
Flet/github-slugger 2.0.0, unmodified, at commit
3461c4350868329c8530904d170358bca1d31448:
sha256 b174ae2aeb321b52f64adb9ff412f966a7f338839d780784dd15dcad702c2dd6 gemoji.json
sha256 3bcbb3836a4e76f0f343c34d223ddd9eac83cc73a4e2c7a1118ef0829bc9129b github-slugger-regex.js
sha256 26e59aec9fb3adee7a56445049ef2d537126b28581802903b529d2556778b248 github-slugger-fixtures.json
tools/gen_github_tables.py writes the 1,913 aliases into emoji_table.sysl in the entity table's
layout (34,775 bytes packed), the 977,500 code points a slug drops into slug_table.sysl as 735
ranges -- reading the regex as JavaScript does, surrogate pairs and all, and checking the set against
node where node is installed -- and the 78 fixtures into tests_slug_fixtures.sysl, run once through
one slugger and once as headings of one document. The two tables add about 0.04 s to a cold build of a
program using gfm(). The generated files are committed.
sysl test .
sh/sysl/markdown/tests_pathological.sysl carries every input of cmark's
test/pathological_tests.py at the tag 0.31.2
(https://github.com/commonmark/cmark/blob/0.31.2/test/pathological_tests.py), each at cmark's size
and a tenth of it, with the HTML checked exactly at both and the time checked against the input's
growth: a ratio under 20 where linear reading gives about 10 and quadratic about 100.
tests_gfm_scaling.sysl does the same for the extensions' own hostile inputs: tables of 50,000
columns and of 50,000 rows, a cell of escaped pipes, a paragraph of mismatched delimiter rows, runs of
~, and text an autolink almost starts in at every byte. tests_gfm_math_scaling.sysl adds math's:
runs of 50,000 $, and openers that never close or whose closers each fail a different rule.
tests_footnote_scaling.sysl does it for
footnotes and alerts: 20,000 references to one footnote, 20,000 footnotes, 20,000 unreferenced
definitions, 20,000 definitions nested one inside the next, undefined references and unclosed [^,
20,000 alerts and a 20,000-byte almost-marker.
tests_footnotes.sysl holds 36 footnote cases, each asserting the HTML cmark-gfm writes for it, and
tests_alerts.sysl 34 alert cases asserting the HTML GitHub writes (less the icon).
tests_smart_punctuation.sysl holds the examples of the CommonMark spec's smart_punct.txt and the
edges around them -- quotes at a paragraph's ends, beside punctuation and brackets, nested, inside
emphasis and links, hyphen runs of two to thirteen -- each asserting the HTML cmark --smart writes.
tests_definition_lists.sysl holds definition lists as exact HTML: tight and loose, several terms
and definitions, lazy and indented continuation, blocks inside a definition, lists inside a list
item and a block quote, inline markup, the colons that open nothing, definitions that follow no
term, and the extension off. The loose case is the page juicer's documentation renders with the
Scala juicer.
tests_gfm_math.sysl holds math's and mermaid's rules as exact HTML. Neither has a spec or a
cmark-gfm extension, so the oracle is github.com itself: the delimiter cases its header names were
rendered by gh api markdown -f mode=gfm, and the GitHub markup is asserted against what it wrote.
tests_gfm_edges.sysl holds 187 corner cases for the extensions, each asserting the HTML cmark-gfm
writes for it -- the inputs tools/differential.sh --gfm was run over while they were written.
tools/differential.sh [--gfm | --smart] [file or directory ...]
renders a corpus through this package and through cmark --unsafe and names every file whose HTML
differs, keeping a diff of each. With no arguments the corpus is the spec's examples, cmark's
regression examples, the README of every repository under ~/dev/sysl-lang and the pages of
~/dev/sysl-lang/sysl-census-34/docs/content; it needs cmark on the PATH (brew install cmark) and python3. SYSL and CMARK name other binaries.
--gfm compares GitHub Flavored Markdown: gfm() less emoji, math, mermaid and alerts
(which cmark-gfm does not read) and gfm_html() here against cmark-gfm --unsafe -e footnotes -e table -e strikethrough -e autolink -e tasklist -e tagfilter (brew install cmark-gfm; CMARK_GFM names another binary),
with GitHub's spec examples and cmark-gfm's extension tests added to the corpus. cmark-gfm reads
CommonMark 0.29, so a file on which it and cmark 0.31.2 disagree with every extension off is marked
as such: there the difference is the CommonMark version's, and this package renders as cmark 0.31.2
does. Over the whole corpus -- 1,591 files -- every difference is one of those.
--smart parses with smart_punctuation() alone against cmark --unsafe --smart. Over the
default corpus -- 789 files -- none differs.
spec/cmark-regression.txt is cmark 0.31.2's test/regression.txt, unmodified:
sha256 aaa16d0e50464dfd03628acb4eaa4db462efb3ae523d7419ba15787add74d3ed
Where cmark and this package disagree and the spec decides for this package, each is pinned in
tests_cmark_deviations.sysl:
| input | cmark 0.31.2 | the spec |
|---|---|---|
a comment whose text ends in -: <!--<!----> |
not a comment | §6.6, an HTML comment is <!--, text not including -->, and --> (example 625) |
a CDATA section whose text ends in ]: <![CDATA[x]]]> |
not raw HTML | §6.6, a CDATA section (example 629) |
<![CDATA[…]]> or <!X…> after an unclosed <!-- |
text | §6.6: each kind of raw HTML is matched on its own |
| leading whitespace of a lazy line, or of one indented four or more | kept, showing in code spans, after a hard break, and stopping a definition | §4.8, examples 222 and 223: leading spaces or tabs are skipped |
[a]: /u then "t"x on the next line |
[a] keeps the title t |
§4.7, example 210: the title is not part of the definition |
[x [a](b) [c] ](d) |
a link inside a link | §6.3, examples 518 and 519: links never contain links |
<script/> alone on a line |
an HTML block | §4.6, start condition 7 excludes open tags named pre, script, style and textarea |
| a tab before a line ending | removed | §6.8: spaces at the end of a line are removed |
Where cmark-gfm and this package disagree with the extensions on, each is pinned in
tests_gfm_deviations.sysl:
| input | cmark-gfm 0.29.0.gfm.13 | this package |
|---|---|---|
| a link reference definition in the lines above a table's header | left as paragraph text, and never defined | a definition (CommonMark §4.7) |
| in the lines above a table's header |
its backslash removed, inside a code span too | CommonMark's escape, which a code span does not read |
a ' in a link's destination |
written ' (cmark does the same) |
written ', as commonmark.js writes it |
ISC