Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Changelog

All notable changes to `@roobli/md`. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
[Semantic Versioning](https://semver.org/). Before 1.0 a minor release may
change the API; each entry says so when it does.

Phases refer to [`docs/design/roadmap.md`](docs/design/roadmap.md).

## [0.1.19] - 2026-09-29

### Fixed
- Lazy continuation needs an open paragraph: empty quotes and marker-only empty
list items no longer absorb unindented lines. A complete link definition
absorbs at most one indented title line. Ordered lists that start at a
number other than 1 no longer interrupt a paragraph. (Phase 22, CommonMark
and micromark parity.)

## [0.1.18] - 2026-09-27

### Fixed
- Pipe-less delimiter rows such as `- | -` or `1. | ---` are list items, not
tables. (Phase 21.)

## [0.1.17] - 2026-09-27

### Fixed
- Marker-only empty list items are lists; an empty item followed by a blank
line and an indented block leaves that block outside the list. (Phase 20.)

## [0.1.16] - 2026-09-23

### Fixed
- Sibling lists at the same indent with a different bullet (`-` `+` `*`) or
ordered delimiter (`.` `)`) start a new span. (Phase 19.)

## [0.1.15] - 2026-09-22

### Added
- `md serve <dir>`: a read-only local folder browser over HTTP, previewing
Markdown through `parseBlocks`. The root is pinned and resolved with
`realpath`, so symlinks cannot escape it. (Phase 18.)

## [0.1.14] - 2026-09-21

### Fixed
- A GFM table needs as many delimiter cells as header cells; otherwise it
stays a paragraph. (Phase 17.)

## [0.1.13] - 2026-09-19

### Fixed
- Nested lists that mix markers stay one span when indented to the parent
item's content column. (Phase 16.)

## [0.1.12] - 2026-09-18

### Fixed
- `text` followed by a continuous `---` is a setext level-2 heading, not a
thematic break. (Phase 15.)

## [0.1.11] - 2026-09-17

### Fixed
- Adjacent definitions each start a block.

## [0.1.10] - 2026-09-17

### Fixed
- Nesting and interruption parity: definition lazy continuations, GFM tables
interrupting paragraphs, and indented blocks after a blank line staying
inside a list. (Phase 14.)

## [0.1.9] - 2026-09-17

### Fixed
- CommonMark lazy continuation: unprefixed paragraph lines stay inside quotes
and list items. (Phase 13.)

## [0.1.8] - 2026-09-15

### Fixed
- CJK emphasis: `renderMarkdown` keeps Typora-shaped `**ζ³¨ζ„οΌš**…` without
escaping the Chinese flanking characters. (Phase 12.)

## [0.1.7] - 2026-09-15

### Added
- `sourceEditBetween` and `reparseFromText`, so a host holding a previous
split and a full new buffer can reparse incrementally. (Phase 11.)

## [0.1.6] - 2026-09-15

### Fixed
- Up to three leading spaces before a block marker are counted as leading
whitespace or gap, as micromark does. (Phase 10.)

## [0.1.5] - 2026-09-15

### Changed
- Table delimiters are widened to the three-dash style
(`| --- | :--- | :---: |`); cell content stays unpadded. (Phase 9.)

## [0.1.4] - 2026-09-14

### Changed
- Serialize keeps verbatim runs (wiki links, alerts, footnotes, `[TOC]`,
snake_case identifiers) and bare http(s) autolinks as written. (Phase 8.)

## [0.1.3] - 2026-09-14

### Changed
- Serialize writes a hard break as two trailing spaces and keeps each list's
marker and ordered delimiter. (Phase 7.)

## [0.1.2] - 2026-09-11

### Added
- Native indented code blocks.

## [0.1.1] - 2026-09-11

### Fixed
- Tightly adjacent quotes and callouts split the way micromark splits them.

## [0.1.0] - 2026-09-11

### Added
- A native block scanner with exact byte offsets and preserved gaps:
headings, paragraphs, lists, task lists, fenced code, quotes, thematic
breaks, GFM tables, display math, YAML frontmatter, HTML blocks, and link
and footnote definitions. (Phases 1–3.)
- Incremental reparse of a dirty window with `reparseBlocks`. (Phase 4.)
- Byte-exact saves: `serializeDocument`, `identityUnits` and `joinSplit`
slice untouched blocks from the source rather than re-emitting them.
(Phase 5.)

### Changed
- micromark is off the hot path and available only from
`@roobli/md/legacy-micromark`. (Phase 6; breaking for callers that relied
on the fallback.)

[0.1.19]: https://github.com/roobli/md/compare/v0.1.18...v0.1.19
[0.1.18]: https://github.com/roobli/md/compare/v0.1.17...v0.1.18
[0.1.17]: https://github.com/roobli/md/compare/v0.1.16...v0.1.17
[0.1.16]: https://github.com/roobli/md/compare/v0.1.15...v0.1.16
[0.1.15]: https://github.com/roobli/md/compare/v0.1.14...v0.1.15
[0.1.14]: https://github.com/roobli/md/compare/v0.1.13...v0.1.14
[0.1.13]: https://github.com/roobli/md/compare/v0.1.12...v0.1.13
[0.1.12]: https://github.com/roobli/md/compare/v0.1.11...v0.1.12
[0.1.11]: https://github.com/roobli/md/compare/v0.1.10...v0.1.11
[0.1.10]: https://github.com/roobli/md/compare/v0.1.9...v0.1.10
[0.1.9]: https://github.com/roobli/md/compare/v0.1.8...v0.1.9
[0.1.8]: https://github.com/roobli/md/compare/v0.1.7...v0.1.8
[0.1.7]: https://github.com/roobli/md/compare/v0.1.6...v0.1.7
[0.1.6]: https://github.com/roobli/md/compare/v0.1.5...v0.1.6
[0.1.5]: https://github.com/roobli/md/compare/v0.1.4...v0.1.5
[0.1.4]: https://github.com/roobli/md/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/roobli/md/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/roobli/md/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/roobli/md/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/roobli/md/releases/tag/v0.1.0
129 changes: 41 additions & 88 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,56 @@
# @roobli/md

WYSIWYG-first markdown engine for [Noto](https://github.com/roobli/Noto).
A Markdown engine for editors that must not rewrite the file.

`@roobli/md` splits a document into top-level blocks with exact byte offsets
and keeps the gaps between them. A host can then re-parse only the part that
changed, and save by slicing every untouched block straight from the original
source instead of re-serializing it. It is the engine under
[Noto](https://github.com/roobli/Noto), and it has no dependency on any editor
or UI.

MIT licensed, so a host that is not Noto can use it without taking on Noto's
AGPL.

## What it guarantees

- **Byte-exact identity.** Parse a document, serialize it with every block
untouched, and you get the same bytes back: line endings, BOM, trailing
whitespace and odd gaps included. Noto's docs site checks this on every page
it publishes.
- **Stable spans.** Every block carries its `start` and `end` in the source,
its kind, and the literal gap before it.
- **Local reparse.** `reparseBlocks` and `reparseFromText` rebuild only the
window around an edit; blocks outside it keep their identity.
- **One dialect.** CommonMark plus GFM tables, task lists and strikethrough,
`$`/`$$` math, YAML frontmatter, footnotes, callouts, wiki links and
CJK-friendly emphasis. Behaviour is checked against micromark for parity.

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).
## Status

Noto itself is AGPL-3.0. This package is **MIT**, same as
[`@roobli/canvas`](https://github.com/roobli/canvas), so hosts that are not Noto
can depend on it without inheriting that copyleft.
`0.1.x`: in production as Noto's default engine since Noto
`v0.0.2-alpha.112`. The API may still change before 1.0; every change is in
the [changelog](CHANGELOG.md). Published to npm once the contract reaches v1;
until then, install from a tag.

## Status
On a synthetic corpus, the native scanner splits a medium document in about
4.5 ms and a large one in about 14.5 ms, against about 304 ms and 1.5 s for
micromark ([bench](docs/design/bench.md)).

**Phase 22 shipped** β€” lazy continuation only with an open paragraph: empty
quotes / marker-only empty list items do not absorb unindented lazy lines;
complete link-definitions absorb at most one indented title line; ordered lists
with start β‰  1 do not interrupt paragraphs (CommonMark / micromark). Footnote
lazy (Phase 14) unchanged. **Phase 21 shipped** β€” micromark list-steal pipe-less delimiters (`- | -`) are not tables. **Phase 20 shipped** β€” marker-only empty list items + blank put following structural outside (micromark). **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`](docs/design/bench.md)).
**v0.1.19** — Phase 22 lazy empty-container / link-def title / ordered start≠1. **v0.1.18** — Phase 21 list-steal pipe-less `- | -` delimiters. **v0.1.17** — Phase 20 empty list item + blank + structural outside. **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.19` when ready.

See:

- [`docs/design/vision.md`](docs/design/vision.md) β€” product goal
- [`docs/design/roadmap.md`](docs/design/roadmap.md) β€” phased plan
- [`docs/design/bench.md`](docs/design/bench.md) β€” native vs micromark numbers
- [`docs/design/typora-notes.md`](docs/design/typora-notes.md) β€” Typora study (interop research)
- [`docs/design/noto-bridge.md`](docs/design/noto-bridge.md) β€” how this plugs into Noto v3
- [`docs/design/contract-v0.md`](docs/design/contract-v0.md) β€” engine contract sketch
Design notes: [vision](docs/design/vision.md) Β·
[roadmap](docs/design/roadmap.md) Β· [contract](docs/design/contract-v0.md) Β·
[Noto bridge](docs/design/noto-bridge.md) Β·
[Typora interop notes](docs/design/typora-notes.md)

## Install

```
pnpm add github:roobli/md
pnpm add github:roobli/md#v0.1.19
```

Pin a tag: `main` moves. A host that runs install scripts may need to allow
this package's `prepare` step, which builds `dist/` with `tsc`.

## Usage

```ts
Expand Down Expand Up @@ -125,23 +95,6 @@ node dist/serve/cli.js 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.

## Breaking change (Phase 6)

- 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-*` and `mdast-util-from-markdown`
moved to **`optionalDependencies`** (needed only for the legacy entry). The
default path still depends on `mdast-util-to-markdown` (+ GFM/math/frontmatter
/ CJK to-markdown helpers) for `renderMarkdown` on 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:

```ts
import { splitWithMicromark } from "@roobli/md/legacy-micromark";
```

## Scripts

```
Expand Down
Loading