src
The whole package. index.ts is the only public entry point; everything else is
internal to the parser.
ast.ts: the flat document and the builder the passes write into.parse.ts: the pipeline, from source toDocument.block.ts: the block pass, withmetadata.tsfor the---block.inline.ts: the inline pass, run on each leaf as it closes.attributes.ts,expression.tsandchars.ts: scanners both passes share.elements.ts: the names an element may have, which both passes andhtml()check.html.ts: the HTML fold.walk.tsandposition.ts: the utilities consumers fold and report with,walk,textContent,headingsandposition.
Code
- ast.test.ts
Hand-built trees, since the parser doesn't exist yet: the builder's links and ranges, the document's accessors, and the type check on
data. - ast.ts
The flat, read-only document markz parses into. A node is an index; its type, range and tree links live in parallel typed arrays, and the few node types that carry data keep it in a side table. The parser fills a
Builder, which hands over aDocumentonce and is done: there is no mutation API, so offsets can never drift from the source they point into (design: AST). - attributes.ts
The
{…}block, markz's one extension syntax, wherever syntax.md allows one:#id,.class,key=value, withkey="a quoted value"for spaces, and a barekeyfor a boolean attribute, on one line. An element's block starts with@name, and a block element's may end in/. Items are kept verbatim and in source order, each with its range; merging (classes accumulate, a later value wins) is the renderer's job. Anything that doesn't parse returnsnull, and the caller keeps the braces as text (syntax.md: Attributes). - block.test.ts
The block pass on what an example's HTML can't show: exact source ranges, node data (heading ids, code bodies, element nesting, list tightness, table cells) and the order of warnings. What the block constructs write is in
test/examples/markz/. Every parsed document is also held to the tree invariants. - block.ts
Source lines to containers and leaves, in one pass over the lines (design: Parser foundation). It keeps a stack of open containers (blockquotes, lists, list items, container elements) and at most one open leaf. Each line first walks the stack, letting each container consume its prefix; whatever is left either continues the open leaf or starts new blocks. Every block construct in
syntax.mdis a case here, and so is every rejected one: a setext underline, indented code, a~~~fence or a lazy line is recognised where it is met, stays text, and adds a warning. - chars.ts
The character classes both passes share, and backslash-escape decoding for the plain-text values the block pass stores itself (a fence's info string, an attribute value). Text inside paragraphs is the inline pass's job.
- elements.ts
An element's
@nameis the element it writes (Design): an HTML element on the allowlist for its kind, or a custom element. The lists leave out what Markdown already writes (em,a,pre, …) andspan, which[text]{.x}writes, so each element has one way in, and anything active (script,iframe, form controls, media), so a name can never run code. Inline and block are separate, so a block element never lands inside a paragraph. - expression.ts
Where a
${…}ends. markz never evaluates or validates the JavaScript inside; it only finds the}that matches the opening brace, skipping strings, template literals (with their own nested${}) and comments, so a brace inside any of them doesn't count (grammar:brace-depth). Regex literals aren't recognised, which is the documented limit. The same scanner serves inline expressions, link destinations and attribute values. - html.ts
html()is a fold over the document into a string, the output prose and base consume (design: HTML output). It never touches the DOM, so it runs the same in Node, Workers and the browser. The markup is micromark's for everything markz shares with GFM, so the oracle can compare them, and syntax.md's shapes for the rest. - index.test.ts
The public entry point: what
import … from '@amitkaps/markz'exposes, and the BOM rule for the root. - index.ts
Small, opinionated Markdown: one fixed dialect (GFM's everyday syntax without the parts that need backtracking, plus
{…}attributes and elements, math,${…}expressions and YAML metadata), one compact source-mapped AST, andhtml()output, with no options. This is the package entry point: the public API lives here and nothing else is importable. - inline.test.ts
The inline pass on what an example's HTML can't show: node data (math and expression values, link expressions, text values), exact ranges, and heading ids. What the inline constructs write is in
test/examples/markz/. Every parsed document is also held to the tree invariants. - inline.ts
Turns a leaf's content lines into inline nodes under the builder's current node, in one pass (design: Parser foundation). The block pass hands it the lines as source ranges with container prefixes and outer whitespace already cut, so it never sees a
>or an item's indentation. - metadata.test.ts
The metadata rule, held to the
yamlpackage: every block markz accepts without a warning gives the object YAML 1.2 gives. The checks catch what writers get wrong (no,1.10,Issue #42), and every number form YAML reads (1e3,0x1F) is one of them (syntax.md: Metadata). - metadata.ts
The
---block at the top of a document, read by syntax.md's rule: onekey: valueper line, where a value is null, a boolean, a number, a quoted string, a[…]list, or otherwise a string as written. A list may wrap onto the indented lines after its key, the way a formatter writes a long one. Keys are flat, and a.in one is an ordinary character, as YAML reads it. The rule warns on the mistakes writers make, not on every corner of YAML. A value YAML would read as another type (no,1.10,1e3) or as structure (a leading*or{) is a warning, and its key is skipped, since any guess could be wrong. A value holding:or#is a warning too, but markz keeps it as written. Skipping it would lose a title likeIssue #42, which YAML cuts short. A line that isn'tkey: valueskips its key, and the rest of the block is still read. - parse.ts
Source text to
Document, in the block pass, which runs the inline pass on each leaf and settles each heading's id as it closes (design: Parser foundation). Nothing runs after it. A leading BOM is part of the source and falls before the root's start. - position.ts
Offsets are what the AST stores; editors and error messages want lines and columns.
positionbuilds a table of line starts once, then converts each offset by binary search. Lines are 1-based and columns 0-based in UTF-16 code units, visdown's convention and source-map v3's. CRLF, LF and a lone CR each end a line, as they do for the parser. An offset past the end is clamped to the end. - walk.test.ts
walk,textContent,headingsandposition: the orderwalkvisits in and what skipping does, that it survives nesting too deep to recurse, what text a node reads as, which headings the outline lists and with what ids, and how offsets become lines and columns across every line ending. - walk.ts
The one traversal consumers need, since every transformation of a markz document is a fold (design: Read-only, and transformations are folds).
walkvisits a subtree depth-first, callingenterbefore a node's children andexitafter them. It follows the parent and sibling columns rather than recursing, so a deeply nested document can't overflow the stack, and it allocates nothing.enterreturningfalseskips that node's children; itsexitstill runs. - warnings.ts
Every warning markz can raise, by a stable code, with its default message and the supported form to write instead. The code is what editors, tests and
syntax.mdrefer to, so the wording can change without breaking anything that keys on it. Each formsyntax.mdcuts has one code, named in the Code column of its Not supported table; the rest belong to a construct (a reused heading id, a malformed metadata line) and are named in that construct's section.