markdown-codec 1.3.6 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -71
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> Hand-written CommonMark+GFM ⇄ `ContentDocument` codec, built on [document-schema.js](https://github.com/ExaDev/document-schema.js).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The same "hand-write the format instead of wrapping a third-party library" bet as [`pdf-codec`](https://github.com/ExaDev/pdf-codec), aimed at CommonMark and GFM. No `micromark`/`remark`/`marked`/`markdown-it`/`commonmark`/`mdast`/`unified`/`turndown`/`showdown` dependency (enforced by eslint `no-restricted-imports`). Runtime dependencies: `document-schema.js` (the shared pivot) and `zod`. `readMarkdown`/`writeMarkdown` read and write that pivot's `ContentDocument` directly — the same model [`documents.js`](https://github.com/ExaDev/documents.js) builds docx/pptx/odt/odp conversions around.
|
|
8
8
|
|
|
9
9
|
```mermaid
|
|
10
10
|
graph TD
|
|
@@ -51,9 +51,7 @@ graph TD
|
|
|
51
51
|
|
|
52
52
|
## Status
|
|
53
53
|
|
|
54
|
-
The scanner, block parser
|
|
55
|
-
|
|
56
|
-
The conformance suites (`src/conformance.test.ts`, `src/gfm-conformance.test.ts`) measure the real public surface end to end — `readMarkdown` → `writeMarkdown` → reparse → render to HTML — against the vendored CommonMark and GFM spec corpora, and are a materially stricter bar than measuring the bare parser alone: a round trip through `ContentDocument` has to survive `src/lower`'s own semantic mapping *and* `src/emit`'s own inverse rendering with no loss the reparse can detect. See [Fidelity](#fidelity) for what that measures and why the number is lower than 100%: it is dominated by what `ContentDocument` itself can represent, not by parsing gaps.
|
|
54
|
+
The scanner, block parser, and inline parser are complete hand-written implementations of CommonMark 0.31.2's two-phase algorithm plus GFM's table/strikethrough/autolink/task-list-item extensions. `readMarkdown`/`writeMarkdown`/`markdownCodec` are wired and real. Conformance suites measure the full public surface (`readMarkdown` → `writeMarkdown` → reparse → render to HTML) against the vendored CommonMark/GFM corpora — see [Fidelity](#fidelity) for why the rate is below 100% (dominated by what `ContentDocument` can represent, not parsing gaps).
|
|
57
55
|
|
|
58
56
|
## Getting started
|
|
59
57
|
|
|
@@ -71,7 +69,7 @@ pnpm add markdown-codec
|
|
|
71
69
|
npm install markdown-codec
|
|
72
70
|
```
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
Published to [npmjs.org](https://www.npmjs.com/package/markdown-codec) via OIDC trusted publishing. `dist/` is committed rather than gitignored — a holdover from a pre-publish period when a git-tarball install needed a working build; now a known cleanup.
|
|
75
73
|
|
|
76
74
|
## Usage
|
|
77
75
|
|
|
@@ -92,9 +90,9 @@ const markdown = writeMarkdown(document, {
|
|
|
92
90
|
});
|
|
93
91
|
```
|
|
94
92
|
|
|
95
|
-
Both accept an optional `signal` (`AbortSignal`) and `sink` (
|
|
93
|
+
Both accept an optional `signal` (`AbortSignal`) and `sink` (`MarkdownDiagnosticSink`, called once per recoverable issue or construct-mapping gap — see [Gotchas](#gotchas-and-quirks)). `writeMarkdown` throws `MarkdownUnsupportedDocumentKindError` for a non-`'wordprocessing'` `ContentDocument`.
|
|
96
94
|
|
|
97
|
-
The same round trip
|
|
95
|
+
The same round trip as a schema-validated [`z.codec()`](https://zod.dev) pair, mirroring `pdf-codec`'s `pdfCodec`:
|
|
98
96
|
|
|
99
97
|
```ts
|
|
100
98
|
import { z } from 'zod';
|
|
@@ -104,85 +102,83 @@ const document = z.decode(markdownCodec, bytes); // throws if bytes are not well
|
|
|
104
102
|
const bytes2 = z.encode(markdownCodec, document);
|
|
105
103
|
```
|
|
106
104
|
|
|
107
|
-
`MarkdownBytesSchema` checks for well-formed UTF-8
|
|
108
|
-
|
|
109
|
-
Every construct-mapping gap either side cannot represent losslessly reports through the sink as a stable, namespaced code (e.g. `md/nested-emphasis-flattened`) — see `MarkdownDiagnosticCodes` (`src/diagnostics/diagnostics.ts`) and [Gotchas](#gotchas-and-quirks) below for the full, named list.
|
|
105
|
+
`MarkdownBytesSchema` checks for well-formed UTF-8. The no-options form only; `readMarkdown`/`writeMarkdown` remain the entry points for an `AbortSignal` or diagnostic sink. Every construct-mapping gap reports through the sink as a stable code (e.g. `md/nested-emphasis-flattened`) — see `MarkdownDiagnosticCodes` and [Gotchas](#gotchas-and-quirks).
|
|
110
106
|
|
|
111
107
|
## Architecture
|
|
112
108
|
|
|
113
|
-
Modelled on `pdf-codec`'s own layering
|
|
114
|
-
|
|
115
|
-
- **`src/diagnostics/`** —
|
|
116
|
-
- **`src/ast/`** —
|
|
117
|
-
- **`src/options/`** / **`src/defaults/`** —
|
|
118
|
-
- **`src/scan/`** —
|
|
119
|
-
- **`src/block/`** — CommonMark
|
|
120
|
-
- **`src/inline/`** —
|
|
121
|
-
- **`src/html/`** — raw
|
|
122
|
-
- **`src/image/`** —
|
|
123
|
-
- **`src/shared/`** — string-shape conventions `src/lower
|
|
124
|
-
- **`src/lower/`** —
|
|
125
|
-
- **`src/emit/`** —
|
|
126
|
-
- **`src/read.ts`** / **`src/write.ts`** / **`src/codec.ts`** —
|
|
109
|
+
Modelled on `pdf-codec`'s own layering, aimed at CommonMark+GFM instead of PDF:
|
|
110
|
+
|
|
111
|
+
- **`src/diagnostics/`** — three-tier diagnostic policy (throw/recover/degrade); `MarkdownDiagnosticCodes` names every code.
|
|
112
|
+
- **`src/ast/`** — markdown AST node types (document/block/inline union), Zod-first.
|
|
113
|
+
- **`src/options/`** / **`src/defaults/`** — read/write options (GFM toggles, sink, `AbortSignal`, write-side style) and defaults.
|
|
114
|
+
- **`src/scan/`** — CommonMark line/character scanner, plus `entity-table.ts` (generated from `assets/html-entities/entities.json`).
|
|
115
|
+
- **`src/block/`** — CommonMark block-structure algorithm (open-block stack, continuation matching): paragraphs, headings, code blocks, block quotes, lists (incl. GFM task-list-item), thematic breaks, link references, GFM tables.
|
|
116
|
+
- **`src/inline/`** — emphasis, code spans, links, autolinks, raw HTML, GFM strikethrough, line breaks.
|
|
117
|
+
- **`src/html/`** — raw HTML recognition (bounded rules, not a general parser) plus `render.ts` (conformance oracle; internal only).
|
|
118
|
+
- **`src/image/`** — PNG/JPEG dimension reader and base64 codec, shared by `src/lower/` and `src/emit/`.
|
|
119
|
+
- **`src/shared/`** — string-shape conventions `src/lower`/`src/emit` agree on (`style-constants.ts`, `list-id.ts`'s opaque `numId`). Re-exported so `documents.js`'s `MarkdownEditor` reuses the identical grammar.
|
|
120
|
+
- **`src/lower/`** — AST → `ContentDocument` lowering (thin adapter, not a second parser); top-of-file table maps each construct to its diagnostic gap.
|
|
121
|
+
- **`src/emit/`** — `ContentDocument` → markdown text emission, the structural inverse of `src/lower`.
|
|
122
|
+
- **`src/read.ts`** / **`src/write.ts`** / **`src/codec.ts`** — public `readMarkdown`/`writeMarkdown` entry points and `markdownCodec` (`z.codec()` pair).
|
|
127
123
|
|
|
128
124
|
## Vendored assets
|
|
129
125
|
|
|
130
|
-
`assets/` holds real, unmodified conformance corpora
|
|
126
|
+
`assets/` holds real, unmodified conformance corpora (each with a `NOTICE.md` recording source, version, licence). None is read at runtime: `assets/html-entities/entities.json` is compiled into `src/scan/entity-table.ts`, and the spec corpora are test-only. So `package.json`'s `"files": ["dist"]` is correct.
|
|
131
127
|
|
|
132
|
-
- **`assets/commonmark/`** —
|
|
133
|
-
- **`assets/gfm/`** —
|
|
134
|
-
- **`assets/html-entities/`** —
|
|
128
|
+
- **`assets/commonmark/`** — CommonMark spec + corpus (652 examples), tag `0.31.2` (CC-BY-SA 4.0).
|
|
129
|
+
- **`assets/gfm/`** — GitHub Flavored Markdown Spec (CC-BY-SA 4.0).
|
|
130
|
+
- **`assets/html-entities/`** — WHATWG HTML5 named character reference table (BSD 3-Clause).
|
|
135
131
|
|
|
136
132
|
## Build, test, and lint
|
|
137
133
|
|
|
138
134
|
```sh
|
|
139
135
|
pnpm build # turbo run _build (tsdown -> dist/, ESM + CJS + .d.ts)
|
|
140
|
-
pnpm typecheck # turbo run _typecheck _typecheck:node (
|
|
136
|
+
pnpm typecheck # turbo run _typecheck _typecheck:node (dual tsconfig)
|
|
141
137
|
pnpm lint # turbo run _lint (eslint . --fix --cache --max-warnings 0)
|
|
142
|
-
pnpm test # turbo run _test (vitest run --project unit,
|
|
143
|
-
pnpm test:workers # turbo run _test:workers (
|
|
138
|
+
pnpm test # turbo run _test (vitest run --project unit, incl. CommonMark/GFM conformance)
|
|
139
|
+
pnpm test:workers # turbo run _test:workers (unit suite under the real Cloudflare Workers/workerd runtime)
|
|
144
140
|
pnpm test:watch # vitest --project unit
|
|
145
141
|
pnpm test:coverage # turbo run _test:coverage (vitest run --project unit --coverage)
|
|
146
|
-
pnpm test:smoke # turbo run _test:smoke (rebuilds dist/,
|
|
147
|
-
pnpm test:corpus # turbo run _test:corpus (optional, gitignored real-world
|
|
142
|
+
pnpm test:smoke # turbo run _test:smoke (rebuilds dist/, verifies ESM/CJS parity + a real round trip per bundle)
|
|
143
|
+
pnpm test:corpus # turbo run _test:corpus (optional, gitignored real-world sanity check -- see Fidelity)
|
|
148
144
|
```
|
|
149
145
|
|
|
150
146
|
To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
|
|
151
147
|
|
|
152
148
|
## Conventions
|
|
153
149
|
|
|
154
|
-
- **Zod-first schema/type/guard**, matching `pdf-codec`/`documents.js`: every model type
|
|
155
|
-
- **No type assertions
|
|
156
|
-
- **No markdown-parsing library dependency**, enforced by
|
|
157
|
-
- **`z.codec()` for the
|
|
158
|
-
- **
|
|
159
|
-
- **Conventional commits**, enforced via commitlint + husky
|
|
150
|
+
- **Zod-first schema/type/guard**, matching `pdf-codec`/`documents.js`: every model type inferred from its Zod schema.
|
|
151
|
+
- **No type assertions.** Every loosely-typed value narrowed through a type guard or Zod parse at the boundary.
|
|
152
|
+
- **No markdown-parsing library dependency**, enforced by eslint `no-restricted-imports`.
|
|
153
|
+
- **`z.codec()` for the round trip** (`markdownCodec`), matching `pdf-codec`'s `pdfCodec`: wraps the independently-tested `readMarkdown`/`writeMarkdown` with automatic two-way schema validation (no-options form only).
|
|
154
|
+
- **Shrink-only conformance exclusion list.** Every spec example the read → write → reparse → render pipeline does not reproduce byte for byte is named in `src/test-support/conformance-exclusions.ts`, with a test asserting it genuinely still fails — the list shrinks as gaps close, never quietly grows.
|
|
155
|
+
- **Conventional commits**, enforced via commitlint + husky.
|
|
160
156
|
|
|
161
157
|
## Gotchas and quirks
|
|
162
158
|
|
|
163
|
-
Every construct
|
|
164
|
-
|
|
165
|
-
- **`md/invented-page-geometry`** —
|
|
166
|
-
- **`md/nested-emphasis-flattened`** —
|
|
167
|
-
- **`md/link-title-dropped`** —
|
|
168
|
-
- **`md/code-block-info-string-dropped`** —
|
|
169
|
-
- **`md/blockquote-nested-depth`** —
|
|
170
|
-
- **`md/list-item-block-unlisted`** — a table
|
|
171
|
-
- **`md/list-item-multi-block-flattened`** —
|
|
172
|
-
- **`md/image-unresolved`** —
|
|
173
|
-
- **`md/raw-html-preserved-as-text` / `md/raw-html-dropped`** — raw HTML
|
|
174
|
-
- **`md/front-matter-key-unmapped`** —
|
|
175
|
-
- **`md/heading-level-clamped`** —
|
|
176
|
-
- **`md/adjacent-links-merged`**
|
|
177
|
-
- **`md/paragraph-indent-dropped`** —
|
|
178
|
-
- **`md/list-numid-fallback`** — a `numId`
|
|
179
|
-
- **`md/table-cell-formatting-dropped`**
|
|
159
|
+
Every construct `src/lower`/`src/emit` cannot represent losslessly is a documented `MarkdownDiagnosticCodes` entry:
|
|
160
|
+
|
|
161
|
+
- **`md/invented-page-geometry`** — no page concept in markdown; one `ContentSection` with A4 + 1in defaults (overridable). Fires once.
|
|
162
|
+
- **`md/nested-emphasis-flattened`** — same-kind nested emphasis flattens to one run.
|
|
163
|
+
- **`md/link-title-dropped`** — link/image title has no `ContentRun`/`ContentImageBlock` field.
|
|
164
|
+
- **`md/code-block-info-string-dropped`** — fenced code info string has no `ContentParagraph` field.
|
|
165
|
+
- **`md/blockquote-nested-depth`** — nesting beyond one level is indent depth only; same-depth blockquotes are indistinguishable.
|
|
166
|
+
- **`md/list-item-block-unlisted`** — a table/image in a list item cannot carry `ContentListMembership` (paragraphs only).
|
|
167
|
+
- **`md/list-item-multi-block-flattened`** — multi-block list items lose item-boundary identity.
|
|
168
|
+
- **`md/image-unresolved`** — no resolver, `undefined` return, or non-PNG/JPEG bytes degrades to alt-text run.
|
|
169
|
+
- **`md/raw-html-preserved-as-text` / `md/raw-html-dropped`** — raw HTML kept as literal text (default) or dropped; never interpreted.
|
|
170
|
+
- **`md/front-matter-key-unmapped`** — no YAML/TOML engine; only five known `LayoutMetadata` keys recognised.
|
|
171
|
+
- **`md/heading-level-clamped`** — styleId beyond `Heading6` (from another format) clamps to level 6.
|
|
172
|
+
- **`md/adjacent-links-merged`** / **`md/code-span-as-monospace-run`** — same-destination adjacent links merge; monospace runs emit as code spans.
|
|
173
|
+
- **`md/paragraph-indent-dropped`** — `indentLeftPt` without a recognised styleId; indent dropped, paragraph renders.
|
|
174
|
+
- **`md/list-numid-fallback`** — a foreign `numId` falls back to a plain bullet list.
|
|
175
|
+
- **`md/table-cell-formatting-dropped`** / **`md/table-cell-multi-paragraph-joined`** — GFM cells have no rich-formatting or multi-paragraph representation.
|
|
180
176
|
|
|
181
177
|
## Fidelity
|
|
182
178
|
|
|
183
|
-
**Markdown → `ContentDocument` is dominated by target-schema limits, not parsing gaps
|
|
179
|
+
**Markdown → `ContentDocument` is dominated by target-schema limits, not parsing gaps.** The parser recognises every construct CommonMark and GFM define; the limiting factor is what `ContentDocument` can hold — a cross-format pivot shaped around docx/pptx/odt/odp/ods/odg, not markdown's richer model. Each gap is a permanent structural mismatch.
|
|
184
180
|
|
|
185
|
-
**
|
|
181
|
+
**Round-trip conformance rate** (read → write → reparse → render to HTML, compared byte for byte against expected HTML):
|
|
186
182
|
|
|
187
183
|
| Corpus | Examples | Passing round trip | Rate |
|
|
188
184
|
| --- | --- | --- | --- |
|
|
@@ -190,34 +186,34 @@ Every construct either `src/lower` (read) or `src/emit` (write) cannot represent
|
|
|
190
186
|
| GFM tagged extensions (table/strikethrough/autolink/task-list, `assets/gfm/spec.txt`) | 23 | 22 | 95.7% |
|
|
191
187
|
| Combined | 675 | 483 | 71.6% |
|
|
192
188
|
|
|
193
|
-
Every
|
|
189
|
+
Every non-passing example is named individually in `src/test-support/conformance-exclusions.ts`, attributed to a closed set of causes (shrink-only — see [Conventions](#conventions)): most commonly a soft line break collapsing to a space, a dropped title/info string, a flattened list item/blockquote, or touching emphasis spans.
|
|
190
|
+
|
|
191
|
+
**Optional real-world corpus.** `test/corpus/` (gitignored) holds a `pnpm test:corpus` project for a manual sanity check against sibling READMEs on disk — asserts no throw and real content on reparse, not byte fidelity. Not part of `pnpm test`; run locally before significant parser/lower/emit changes.
|
|
194
192
|
|
|
195
|
-
This is also why `pdf-codec`'s own permanent "no round-trip-losslessness claim" framing applies here for the identical underlying reason but the opposite direction of blame: `pdf-codec` cannot promise fidelity because arbitrary real-world PDF vastly exceeds what any parser can safely assume about it; `markdown-codec`'s parser is complete, but `ContentDocument` itself is the narrower vessel a full CommonMark+GFM document is being poured into.
|
|
196
193
|
|
|
197
|
-
**Optional real-world corpus.** `test/corpus/` (gitignored, never committed) holds a `pnpm test:corpus` vitest project for a manual sanity check against real, large, table-heavy, fence-heavy markdown a hand-built fixture can't fully stand in for — this family's own sibling repository READMEs (`documents.js`, `pdf-codec`, `odf.js`, `ooxml.js`, `document-schema.js`), read straight from their checkout locations on disk. It asserts only that `readMarkdown`/`writeMarkdown` don't throw and that a reparse still produces real content — not byte-for-byte fidelity, which real-world markdown was never going to hold to anyway. It is not part of `pnpm test` and never gates CI; run it locally before a significant change to `src/lower/`, `src/emit/`, or the scanner/block/inline layers.
|
|
198
194
|
|
|
199
195
|
## Release and publishing
|
|
200
196
|
|
|
201
|
-
`.github/workflows/ci.yml` runs commitlint, lint, typecheck,
|
|
197
|
+
`.github/workflows/ci.yml` runs commitlint, lint, typecheck, unit suite (incl. conformance), and smoke test on every push/PR. On a push to `main` where those pass, `release.config.ts` drives [semantic-release](https://semantic-release.gitbook.io/semantic-release): commit history decides the version bump, `CHANGELOG.md` and `package.json` are committed back to `main`, a GitHub Release is cut, and the package publishes to [npmjs.org](https://www.npmjs.com/package/markdown-codec) via OIDC trusted publishing (no `NPM_TOKEN`).
|
|
202
198
|
|
|
203
|
-
|
|
199
|
+
Release detection diffs `package.json`'s version before/after the release step. Four further jobs gate on that: a `sibling-released` `repository_dispatch` to `documents.js`; a republish under `@exadev/markdown-codec` to GitHub Packages (`GITHUB_TOKEN`); a republish under `mrkdwn.js` to npmjs.org (same OIDC exchange); and an SPDX SBOM + build-provenance attestation signed against the packed tarball.
|
|
204
200
|
|
|
205
201
|
## Contributing
|
|
206
202
|
|
|
207
|
-
|
|
203
|
+
Conventional Commits enforced by commitlint (`commitlint.config.ts`) via a husky `commit-msg` hook and CI job — semantic-release's version bump depends on well-formed messages. `pre-commit` runs `lint-staged` (`eslint --fix` on staged `*.ts`); `pre-push` runs the test suite. Single `main` branch, no open PR workflow.
|
|
208
204
|
|
|
209
205
|
## References
|
|
210
206
|
|
|
211
|
-
- [document-schema.js](https://github.com/ExaDev/document-schema.js) —
|
|
212
|
-
- [pdf-codec](https://github.com/ExaDev/pdf-codec) — the sibling
|
|
213
|
-
- [documents.js](https://github.com/ExaDev/documents.js) —
|
|
214
|
-
- [CommonMark Spec](https://spec.commonmark.org/) — the base specification
|
|
215
|
-
- [GitHub Flavored Markdown Spec](https://github.github.com/gfm/) —
|
|
216
|
-
- [WHATWG HTML
|
|
207
|
+
- [document-schema.js](https://github.com/ExaDev/document-schema.js) — owns the shared `ContentDocument` pivot.
|
|
208
|
+
- [pdf-codec](https://github.com/ExaDev/pdf-codec) — the sibling whose scaffold, tooling, and "hand-write the format" philosophy this project mirrors.
|
|
209
|
+
- [documents.js](https://github.com/ExaDev/documents.js) — bridges markdown to docx/odt/PDF via this package's `ContentDocument`. Markdown has no presentation/spreadsheet/drawing variant, so pptx/odp/ods/odg are structurally out of reach.
|
|
210
|
+
- [CommonMark Spec](https://spec.commonmark.org/) — the base specification targeted.
|
|
211
|
+
- [GitHub Flavored Markdown Spec](https://github.github.com/gfm/) — GFM extensions layered on top.
|
|
212
|
+
- [WHATWG HTML § named character references](https://html.spec.whatwg.org/multipage/named-characters.html) — the entity table `assets/html-entities/` vendors.
|
|
217
213
|
|
|
218
214
|
## npm aliases
|
|
219
215
|
|
|
220
|
-
This package also publishes under the
|
|
216
|
+
This package also publishes under the alternate name — identical build, same version, republished by CI:
|
|
221
217
|
|
|
222
218
|
- [mrkdwn.js](https://www.npmjs.com/package/mrkdwn.js)
|
|
223
219
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markdown-codec",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"description": "Hand-written CommonMark+GFM <-> ContentDocument codec, built on document-schema.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"license": "MIT",
|
|
80
80
|
"packageManager": "pnpm@11.6.0",
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"document-schema.js": "^2.7.
|
|
82
|
+
"document-schema.js": "^2.7.5",
|
|
83
83
|
"zod": "^4.4.3"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|