WYSIWYG-first markdown engine for Noto.
The goal is a parser built around real editing habits, not around dumping a full AST once and hoping the editor can live with it: block-oriented spans with byte offsets, gaps preserved for untouched regions, dialect coverage Noto already needs (CommonMark + GFM tables/strikethrough/task lists + math + YAML frontmatter + CJK-friendly emphasis), and open times that beat the micromark baseline Noto measures today (~570 ms medium / ~2.3 s large on the Linux corpus).
Noto itself is AGPL-3.0. This package is MIT, same as
@roobli/canvas, so hosts that are not Noto
can depend on it without inheriting that copyleft.
Phase 19 shipped — same-indent sibling lists with a different bullet
(-/+/*) or ordered delimiter (./)) open a new span (CommonMark /
micromark parity); Phase 16 indented mixed nests unchanged. Phase 18 —
md serve <dir> thin local read-only folder browser (localhost HTTP shell:
tree + markdown/text/image preview via parseBlocks → HTML; root pinned;
path resolved with realpath so symlink escapes outside the served root are
rejected). Phase 17 — GFM table header/delimiter column-count parity vs
micromark (looksLikeTable only when counts match; mismatched → paragraph;
ragged body with matching header/delim still table).
Phase 16 — mixed-marker nested lists stay one span when indented to the
parent item content column (Noto intentional golden gap #2). Phase 15 —
CommonMark setext level-2 (text + continuous ---) vs thematic-break parity
(Noto intentional golden gap #1). Phase 14 — nest /
interrupt parity vs micromark: definition lazy continuations, GFM tables
interrupt paragraphs, and lists keep indented nested blocks after a blank.
Phase 13 — CommonMark lazy continuation in the native
split: unprefixed paragraph lines stay inside quotes and list items. Phase 12 — CJK emphasis / Typora interop lock-in:
renderMarkdown keeps Typora-shaped **注意:**… without numeric-escaping
Chinese flanking. Phase 11 — sourceEditBetween + reparseFromText so
hosts with a prior split and a full next buffer can incremental-reparse without
inventing ordinals. Phase 10 — line-prefix offset alignment (0–3 leading ASCII spaces
before a block marker → leading/gaps, micromark parity). Phase 9 — table
delimiter widening to vault three-dash style
(| --- | :--- | :---: | ---: |) while keeping tablePipeAlign: false
(content cells unpadded). Phase 8 — serialize dialect verbatim runs
(wiki links, alerts, footnotes, [TOC], snake_case / metrics) and bare
http(s) autolinks, matching Noto’s host handlers. Phase 7 — hard-break
(two trailing spaces) and list marker / ordered delimiter from node.data.
Phase 6 — micromark quarantined from the hot path; legacy entry
@roobli/md/legacy-micromark. Phase 5 serializeDocument / hardened
joinSplit implement Noto-aligned byte-exact saves. Phase 3 native scanner
covers heading / paragraph / list / fenced code / indented code / quote /
thematic / GFM tables / task lists / display math / YAML frontmatter /
HTML blocks / link + footnote definitions.
Synthetic medium/large A/B vs micromark (via legacy entry): native ~4.5 ms /
~14.5 ms vs micromark ~304 ms / ~1.5 s (see
docs/design/bench.md).
v0.1.16 — Phase 19 same-indent list marker/delimiter split. v0.1.15 — Phase 18 md serve + symlink-escape harden. v0.1.14 — GFM table header/delimiter column-count parity. v0.1.13 — mixed-marker nested lists. v0.1.12 — setext---- vs hr. v0.1.11 — adjacent defs as block starts.
v0.1.10 — nest / interrupt parity. v0.1.9 — lazy continuation.
v0.1.8 — CJK / Typora-shaped strong lock-in. v0.1.7 —
sourceEditBetween / reparseFromText. v0.1.6 line-prefix offset
alignment. v0.1.5 table delimiter widening. v0.1.4 verbatim runs + bare
autolink. v0.1.3 hard-break + list-marker; v0.1.2 native indented-code;
v0.1.1 quote/callout split.
Noto may pin github:roobli/md#v0.1.16 when ready.
See:
docs/design/vision.md— product goaldocs/design/roadmap.md— phased plandocs/design/bench.md— native vs micromark numbersdocs/design/typora-notes.md— Typora study (interop research)docs/design/noto-bridge.md— how this plugs into Noto v3docs/design/contract-v0.md— engine contract sketch
pnpm add github:roobli/md
import {
parseBlocks,
parseDocument,
reparseBlocks,
reparseFromText,
serializeDocument,
identityUnits,
replaceBlock,
} from "@roobli/md";
const split = parseBlocks("# Hello\n\nWorld\n");
// split.spans[0].kind === "heading"
// split.spans[0].start / .end index into the source string
const next = reparseFromText(split, "# Hello\n\nMoon\n");
// or reparseBlocks({ prior, edit, replacedBlocks, neighborSlack })
// next.spans[0] === split.spans[0] when the heading is outside the dirty window
const doc = parseDocument(new TextEncoder().encode("# Hello\n\nWorld\n"));
if (doc.status === "parsed") {
const saved = replaceBlock(doc.document, 0, "# Renamed");
// untouched "World" block sliced from source — not re-serialized
}Thin read-only localhost HTTP UI for a directory. Markdown preview uses
parseBlocks → IR → HTML (same dogfood path as Noto.docs). Not Noto Electron,
not holt, not a collab surface.
pnpm build
node dist/serve/cli.js serve ./folder
# or, after link/install: md serve ./folder
Defaults to 127.0.0.1:4321. Optional --port / explicit --host 0.0.0.0.
Root is pinned by the CLI argument; the UI cannot change it.
- Default entry no longer falls back to micromark. Documents outside the Phase 1–5 native dialect are still scanned natively (best-effort kinds), not bounced to micromark.
- Direct dependencies on
micromark-extension-*andmdast-util-from-markdownmoved tooptionalDependencies(needed only for the legacy entry). The default path still depends onmdast-util-to-markdown(+ GFM/math/frontmatter / CJK to-markdown helpers) forrenderMarkdownon edited blocks — those pull some micromark-util packages transitively, but the micromark parser is not called on open / reparse / serialize of pristine spans. - Compatibility import:
import { splitWithMicromark } from "@roobli/md/legacy-micromark";pnpm install
pnpm verify # typecheck + test + build
pnpm bench:ab # synthetic medium/large native vs @roobli/md/legacy-micromark
MIT © roobli