harness
What the checks in ../ and the Quality page read and judge with. No tests
live here, so one change to how an example is filed or judged reaches both at once.
grammar.ts: the dialect's grammar, read fromdocs/grammar.mdwith markz: one entry per construct with its id, part, origin, EBNF productions and side rules.ebnf.tsreads the notation and recognizes a string by it.syntax.ts: whatsyntax.mdsays about each construct, by id, and the Not supported rows, by code.fences.ts: the one format every example file is in, read and written.examples.ts: every example markz is held to, upstream and its own, filed under a construct id or a Not supported row's warning code, andcheck, which gives each its status.oracle.ts: micromark with GFM and frontmatter, the normalization,yamlfor metadata, github-slugger for heading ids and the math extension's spans.cases.ts: every construct at its edges. Cases written from its productions, and their one-character neighbours, must be read as the grammar reads them, or be settled by a named side rule or a Not supported row. It also lists the side rules the oracle or the judge holds, so every side rule is held by something.generate.ts: fast-check arbitraries. Documents written from the grammar's productions, optionally only those of chosen origins; noise from Markdown's characters and the ones that trouble offsets; known examples with a few random edits; and how far a search goes.sound.tsandtree.ts: what every document must satisfy, whatever the input, and the tree invariants among it.corpus.ts: the real documents in../documents/by tier, and the variants built from them: common, formatted and repeated to a size.pnpm speedand the Quality page time them.node.ts: lets a plain Node script loadsrc/and the harness, which Vite otherwise resolves.speed.ts: warm-up, timed passes, one version against another and retained memory, forpnpm speed,pnpm sizeand the Quality page's Size and Speed.profile.ts: a warm CPU profile and its table of self time by area and function, forpnpm hotspots.parsers.ts: the other parserspnpm comparetimes markz beside, for our own insight.adversarial.ts: patterns that would make a careless parser quadratic, each growing in proportion to a count.
A failure fast-check finds is shrunk to its smallest form. Once fixed, it goes into its
construct's file in ../examples/markz/ as an example, so it stays fixed
without the fuzzer finding it again.
Code
- adversarial.ts
Patterns that make a careless parser quadratic, one per way it can go wrong: an opener that never closes and so scans to the end each time (
${,<!--,:span[,`), a structure nested deeper on every line (>, list items,:::), and a repeat whose bookkeeping grows with the count (heading ids, attribute lines). Each takes a count and returns input that grows in proportion to it, so the test can hold the time to the same proportion. Every pattern here once failed, or guards a scan that would. - cases.ts
Every construct held to its edges, with the grammar as the judge. A construct's own productions write valid cases, and one-character edits of them write their neighbours: an edit the grammar still accepts is a boundary case, and one it rejects is a near miss. For each, markz must read the construct exactly when the grammar accepts the text. Where they part, a side rule decides, and the case is settled by that rule's name here, so every gap between the productions and the parser is either closed in the grammar or named.
- corpus.ts
Real documents, and the variants built from them, which
documents.test.tsholds markz to andpnpm speedtimes. The variants are built on each run and never committed. Each tier of documents answers its own question: - ebnf.ts
The notation
docs/grammar.mdis written in, read into a tree so the grammar can be checked (every name defined, every production reachable), generate documents (step 16) and judge them (step 18:recognizer). It is the W3C notation of the XML spec, kept small:name ::= expression,|for alternatives, juxtaposition for sequence,?,*and+, parentheses,'literal'or"literal",#xAfor a character by code point, and[a-z]or[^…]for a character class, which may hold#x…too. - examples.ts
Every example markz is held to, in one shape and filed by the dialect: under a construct, by its id in
grammar.ts(Metadata, Block, Inline), or under the Not supported row it exercises, by its warning code. Where an example comes from is a label, not a category. The upstream suites are checked against an oracle (micromark for the Markdown,yamlfor metadata); markz's own examples, inexamples/markz/, carry their expected output. - fences.ts
The one format every example is kept in, upstream or markz's own: the CommonMark spec's, in the fence oxfmt writes. A file may start with a metadata block, and each
##heading names the section the examples under it belong to. An example is a backtick fence with the info stringexample, long enough for what it holds, then the Markdown, a.line, the expected output, and optionally a second.line and the text each warning covers, one per line. Without a., the expected output is empty. - generate.ts
Documents built from the dialect's own grammar, so the fuzzer writes what an author could, not only noise. Each production in
grammar.tsbecomes a fast-check arbitrary: a literal is itself, a sequence joins its parts, an alternative picks one, and a repeat takes up to three (orrepeats). fast-check shrinks a failing document along the same structure, so a failure comes back as the smallest document the grammar can write that still fails. - grammar.ts
The dialect as data, read from
docs/grammar.md: every construct ofsyntax.md, by its id, with its part, its origin, its productions and the side rules EBNF can't state. The page is the only copy, so there is nothing to drift: it is read with markz itself, a###heading's explicit id naming the construct, the##above it the part, anebnfcode block its productions and the list after it its side rules,`name`: texteach. What comes before the first part, under Document, is the document's own. The line under a construct's heading,Origin: X., is its origin. - node.ts
Vite resolves what the harness and
src/import; plain Node doesn't. Importing this first lets a Node script (test/speed.tsand the processes it starts) load them as they are: an extensionless relative import finds its.tsfile, and a?rawimport is the file's text. Node strips the types itself. Import it before anything it serves, and load those dynamically, since static imports are resolved before any module runs. - oracle.ts
The reference markz's
html()is held to: micromark with GFM and YAML frontmatter, which are well-tested and dev-only (quality: How markz is tested). Frontmatter writes nothing, as metadata doesn't inhtml(), so every example checks that markz finds the same block. Two settings make it render what markz should, not what micromark's own policy would: - parsers.ts
What
pnpm comparetimes markz beside, for our own insight and never published: markdown-exit, the fastest JavaScript parser; marked, a regex lexer; and micromark with GFM, the spec-exact state machine that is also the tests' oracle. Each gives its parse to HTML, set up with GFM where it has it, and each is imported only when it's loaded, so a process that times one parser never pays for another's startup. They read the common variant of each document (corpus.ts), the blocks every parser reads alike, so none is slower only because it does more. - profile.ts
Where markz's time goes, for
pnpm hotspots. Apnpm speedcell says that something is slow, and this says where: self time by area (block pass, inline pass,html()) and by function, the same table from one run to the next. Whether a change helped is stillpnpm speed's job, againstorigin/main. - sound.ts
What every document must satisfy, whatever the input: the properties the fuzzer holds each generated document to. markz never throws, and it builds a valid tree whose every warning lies inside the source under a known code.
html()gives the same string for the same input, and the same page whichever line endings the source uses.walkreaches every node,textContentandpositionanswer for any node and offset, and the HTML is safe: no element that runs script, no event-handler attribute, and nojavascript:URL, unless the author wrote a```=htmlraw block, which is theirs to write. - speed.ts
How markz's speed and memory are measured, shared by
pnpm speed(test/speed.ts),pnpm sizeand the Quality page, which times this commit when the page is generated. It is given the functions to time rather than importing markz, so each caller says what it times. - syntax.ts
syntax.mdread as data: each construct by the id on the{#id}line above its heading, and the Not supported rows by warning code. The grammar says which constructs exist and where they belong; this reads what the page says about them, so the tests can hold the two together. Nothing here depends on wording: headings and cells can be reworded as long as the ids and the codes stay. - tree.ts
What every
Documentmust satisfy, whatever produced it: one tree rooted at 0 that reaches every node exactly once, parent links that agree with child links, and ranges that nest inside their parent and follow each other in sibling order. The AST tests check hand-built trees with it, and the parser tests check every parsed document with it from step 4 on.