diff --git a/README.md b/README.md index 25a8bc8..493c5c6 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/markdown-codec) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/markdown-codec) [![Release](https://img.shields.io/github/v/release/ExaDev/markdown-codec)](https://github.com/ExaDev/markdown-codec/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/markdown-codec/ci.yml?branch=main)](https://github.com/ExaDev/markdown-codec/actions) -> Hand-written CommonMark+GFM ⇄ `ContentDocument` codec, built on [document-schema.js](https://github.com/ExaDev/document-schema.js). +> Hand-written CommonMark+GFM ⇄ `DocumentPackage` codec, built on [document-schema.js](https://github.com/ExaDev/document-schema.js). -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. +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 tree-form `DocumentPackage`; `readMarkdownContent`/`writeMarkdownContent` read and write the flat `ContentDocument` underneath it — the same model [`documents.js`](https://github.com/ExaDev/documents.js) builds docx/pptx/odt/odp conversions around. See [Two encodings](#two-encodings-documentpackage-and-contentdocument). ```mermaid graph TD @@ -51,7 +51,7 @@ graph TD ## Status -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 and GitHub's footnotes (see [Footnotes](#footnotes)). `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). +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 and GitHub's footnotes (see [Footnotes](#footnotes)). Both encodings' read/write pairs and both `z.codec()` pairs are wired and real. Conformance suites measure the full public surface (`readMarkdownContent` → `writeMarkdownContent` → 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). ## Getting started @@ -78,20 +78,22 @@ Reading and writing markdown text: ```ts import { readMarkdown, writeMarkdown } from 'markdown-codec'; -const { document, diagnostics } = readMarkdown('# Title\n\nSome **bold** text with a [link](https://example.com).', { - frontMatter: true, // parse a leading YAML front matter block into ContentDocument.metadata +const { documentPackage, diagnostics } = readMarkdown('# Title\n\nSome **bold** text with a [link](https://example.com).', { + frontMatter: true, // parse a leading YAML front matter block into the package's metadata footnotes: true, // recognise [^label] markers and [^label]: definitions (default; see Footnotes) images: (destination) => undefined, // a synchronous MarkdownImageResolver port for non-data: URI images }); -const markdown = writeMarkdown(document, { +const markdown = writeMarkdown(documentPackage, { bulletListMarker: '-', emphasisMarker: '_', - frontMatter: true, // emit ContentDocument.metadata back out as a leading front matter block + frontMatter: true, // emit the package's metadata back out as a leading front matter block }); ``` -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`. +`documentPackage` is a `DocumentPackage` — document-schema.js's tree form, with a minted styles table (see [Two encodings](#two-encodings-documentpackage-and-contentdocument)). The field is named `documentPackage` rather than `package` because `package` is a reserved word in strict mode, so `const { package } = readMarkdown(src)` would not parse. + +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 package whose `kind` is not `'wordprocessing'`, checked before flattening so every non-`'wordprocessing'` package reaches it the same way regardless of what else about that package would have failed document-schema.js's own `flattenPackage`. A `'wordprocessing'` package can still fail to flatten — a group carrying a style reference the package's own `styles` table has no entry for — and that failure surfaces as `MarkdownPackageFlattenError`, not a bare `Error` from the dependency. A `DocumentPackage`'s own `definitions`/`layers`/`attachments`/`destinations`/`pages` tables have no flat-`ContentDocument` home to land in; `writeMarkdown` reports one `PACKAGE_TABLE_DROPPED` diagnostic per non-empty table it finds rather than dropping them without a trace. The same round trip as a schema-validated [`z.codec()`](https://zod.dev) pair, mirroring `pdf-codec`'s `pdfCodec`: @@ -99,12 +101,34 @@ The same round trip as a schema-validated [`z.codec()`](https://zod.dev) pair, m import { z } from 'zod'; import { markdownCodec, MarkdownBytesSchema } from 'markdown-codec'; -const document = z.decode(markdownCodec, bytes); // throws if bytes are not well-formed UTF-8 -const bytes2 = z.encode(markdownCodec, document); +const documentPackage = z.decode(markdownCodec, bytes); // throws if bytes are not well-formed UTF-8 +const bytes2 = z.encode(markdownCodec, documentPackage); ``` `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). +## Two encodings: `DocumentPackage` and `ContentDocument` + +document-schema.js states one document in two shapes, and owns the transform between them: the flat `ContentDocument` every codec's lowering pipeline actually builds, and the tree-form `DocumentPackage` a serialised artefact carries — sections, headings, lists, and construct boundaries as real nested groups, plus a styles table minted over repeated property tuples. `assemblePackage` goes flat → tree (`decompose` then `factorStyles`), `flattenPackage` goes tree → flat. Only one direction is a genuine round trip: `flattenPackage(assemblePackage(document))` reproduces `document` exactly, for any `ContentDocument` this package's own read side produces (checked against the full CommonMark and GFM conformance corpora, not just a hand-picked fixture — see `src/conformance.test.ts`/`src/gfm-conformance.test.ts`'s own "tree pair matches the flat pair" suite). `assemblePackage(flattenPackage(documentPackage))` does not, in general, reproduce `documentPackage` — a package carrying `definitions`/`layers`/`attachments`/`destinations`/`pages` loses all of them on the way through `flattenPackage`, which carries forward only `metadata` and `symbolTable` (see [Gotchas](#gotchas-and-quirks)). + +This package exposes a read/write pair and a codec at each level. The unsuffixed names are the tree-form ones and are what to reach for by default — a codec is a construction site, so the tree is what a caller gets unless they ask for otherwise. The `Content`-suffixed names are the flat pair one level down, mirroring the `readXlsx`/`readXlsxContent` naming already in [`ooxml.js`](https://github.com/ExaDev/ooxml.js): + +| Level | Read | Write | Codec | Value type | +| --- | --- | --- | --- | --- | +| Tree (default) | `readMarkdown` | `writeMarkdown` | `markdownCodec` | `DocumentPackage` | +| Flat | `readMarkdownContent` | `writeMarkdownContent` | `markdownContentCodec` | `ContentDocument` | + +The tree pair is exactly the flat pair with the transform composed on — `readMarkdown` is `assemblePackage` over `readMarkdownContent`, `writeMarkdown` is `flattenPackage` before `writeMarkdownContent` — so both render identical markdown from the same source, pinned in `src/package.test.ts`. Options, diagnostics, and error behaviour are identical at both levels. + +Reach for the flat pair when composing a package boundary by hand (`decompose`/`flattenPackage` directly, or `factorStyles` with your own minting policy), when feeding a `ContentDocument`-consuming builder such as `documents.js`'s conversion pipeline, or when a layout stage needs to stamp frames onto content before it is decomposed. Everything else wants the tree. + +```ts +import { readMarkdownContent, writeMarkdownContent } from 'markdown-codec'; + +const { document } = readMarkdownContent(source); // a ContentDocument: kind, metadata, sections +const markdown = writeMarkdownContent(document); +``` + ## Architecture Modelled on `pdf-codec`'s own layering, aimed at CommonMark+GFM instead of PDF: @@ -120,7 +144,7 @@ Modelled on `pdf-codec`'s own layering, aimed at CommonMark+GFM instead of PDF: - **`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. - **`src/lower/`** — AST → `ContentDocument` lowering (thin adapter, not a second parser); top-of-file table maps each construct to its diagnostic gap. - **`src/emit/`** — `ContentDocument` → markdown text emission, the structural inverse of `src/lower`. -- **`src/read.ts`** / **`src/write.ts`** / **`src/codec.ts`** — public `readMarkdown`/`writeMarkdown` entry points and `markdownCodec` (`z.codec()` pair). +- **`src/read.ts`** / **`src/write.ts`** / **`src/codec.ts`** — the public entry points at both levels: `readMarkdown`/`writeMarkdown`/`markdownCodec` over `DocumentPackage`, and `readMarkdownContent`/`writeMarkdownContent`/`markdownContentCodec` over `ContentDocument`. The tree-form functions are thin compositions of `document-schema.js`'s `assemblePackage`/`flattenPackage` onto the flat ones; no conversion logic of their own lives here. ## Vendored assets @@ -151,7 +175,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`. - **Zod-first schema/type/guard**, matching `pdf-codec`/`documents.js`: every model type inferred from its Zod schema. - **No type assertions.** Every loosely-typed value narrowed through a type guard or Zod parse at the boundary. - **No markdown-parsing library dependency**, enforced by eslint `no-restricted-imports`. -- **`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). +- **`z.codec()` for the round trip** (`markdownCodec`, `markdownContentCodec`), matching `pdf-codec`'s `pdfCodec`: each wraps the independently-tested read/write pair at its own level with automatic two-way schema validation (no-options form only). - **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. - **Conventional commits**, enforced via commitlint + husky. @@ -178,6 +202,7 @@ Every construct `src/lower`/`src/emit` cannot represent losslessly is a document - **`md/footnote-reference-preserved-as-text`** — a reference site is a marked run, not an `anchor` construct; see [Footnotes](#footnotes). - **`md/footnote-body-heading-flattened`** — a heading inside a definition body is carried as literal ATX text, since a construct extent may not open or close a heading scope. - **`md/construct-unrepresented`** — a construct kind markdown has no syntax for renders transparently: its extent still appears, the construct itself does not. +- **`md/package-table-dropped`** — `writeMarkdown` only, ahead of flattening: a `DocumentPackage`'s own `definitions`/`layers`/`attachments`/`destinations`/`pages` table has no flat-`ContentDocument` home (`flattenPackage`'s own envelope carries forward only `metadata` and `symbolTable`); fires once per non-empty table present. ## Footnotes @@ -185,12 +210,12 @@ GitHub's footnote extension (`[^label]` markers, `[^label]: body` definitions) i The two halves of a footnote map onto **two different mechanisms**, and that split is structural rather than a choice: -- **A definition becomes an `anchor` construct.** `readMarkdown` emits document-schema.js 4.2.0's construct boundary markers — a `constructStart` carrying `{ kind: 'anchor', anchorType: 'footnote', name }`, the definition's own lowered body blocks, and a `constructEnd`. The body rides the construct's extent rather than `AnchorDescriptor.definition`, which names a key in a package-level definitions table a flat `ContentDocument` has no root to carry; a body is genuinely block content (several paragraphs, a code block, a list) that a string field could not have held either way. A bodyless `[^1]:` lowers to the point anchor the same descriptor describes: a pair with nothing between it. +- **A definition becomes an `anchor` construct.** Lowering emits document-schema.js's construct boundary markers — a `constructStart` carrying `{ kind: 'anchor', anchorType: 'footnote', name }`, the definition's own lowered body blocks, and a `constructEnd` — which is what `readMarkdownContent` returns in its block flow, and what `decompose` promotes to a construct group of its own in the `DocumentPackage` `readMarkdown` returns (the descriptor rides the group's `node`, the body blocks its `children`). The body rides the construct's extent rather than `AnchorDescriptor.definition`, which names a key in a package-level definitions table: `DocumentPackage` does carry that table as a root (unlike the flat `ContentDocument`), but a table entry there is a flat descriptor record, not a container for block content, so a body that is genuinely several paragraphs, a code block, or a list still has nowhere to live as a table value either way — the construct's own bracketed extent is the one shape in this schema built to hold real block content. A bodyless `[^1]:` lowers to the point anchor the same descriptor describes: a pair with nothing between it. - **A reference site stays a marked run.** A construct's extent is block-scoped by document-schema.js's own definition, and a reference sits between two runs inside a paragraph, so no block-level boundary marker can bracket it without splitting the paragraph in two. The schema names this gap itself and parks the inline-anchor case on a run-level extent mechanism it has not shipped. Until it does, the reference is a `ContentRun` keeping its own `[^label]` spelling and carrying `FOOTNOTE_REFERENCE_FONT_MARKER`, reported through `md/footnote-reference-preserved-as-text`. Definitions are recognised only at the document's own top level. Inside a block quote or a list item, the pair's extent would sit inside a scope the enclosing container had already opened, which the marker contract forbids a producer from emitting — so the text stays an ordinary paragraph there. A heading inside a definition body is flattened to literal ATX text for the same reason. -`writeMarkdown` is the inverse and validates first: a section's markers must pair as balanced brackets (checked through document-schema.js's own `findConstructMarkerImbalance`, the shared definition every codec and `decompose` agree on) or it throws `MarkdownUnbalancedConstructMarkersError`. A construct kind with no markdown syntax — a bookmark, a division, a tracked change — renders transparently: its extent still appears in place, only the construct's own identity is lost. +Emission is the inverse and validates first: a section's markers must pair as balanced brackets (checked through document-schema.js's own `findConstructMarkerImbalance`, the shared definition every codec and `decompose` agree on) or `writeMarkdownContent` throws `MarkdownUnbalancedConstructMarkersError`. A tree already satisfies that balance by construction — `decompose` refuses to build one from an unbalanced stream — so `writeMarkdown` reaches this check only on a hand-built package flattened back to an unbalanced flow. A construct kind with no markdown syntax — a bookmark, a division, a tracked change — renders transparently: its extent still appears in place, only the construct's own identity is lost. ## Fidelity @@ -222,9 +247,9 @@ Conventional Commits enforced by commitlint (`commitlint.config.ts`) via a husky ## References -- [document-schema.js](https://github.com/ExaDev/document-schema.js) — owns the shared `ContentDocument` pivot. +- [document-schema.js](https://github.com/ExaDev/document-schema.js) — owns both shared encodings (`ContentDocument`, `DocumentPackage`) and the `assemblePackage`/`flattenPackage` transform between them. - [pdf-codec](https://github.com/ExaDev/pdf-codec) — the sibling whose scaffold, tooling, and "hand-write the format" philosophy this project mirrors. -- [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. +- [documents.js](https://github.com/ExaDev/documents.js) — bridges markdown to docx/odt/PDF via this package's `ContentDocument` (the flat pair; its own conversion pipeline assembles the package itself). Markdown has no presentation/spreadsheet/drawing variant, so pptx/odp/ods/odg are structurally out of reach. - [CommonMark Spec](https://spec.commonmark.org/) — the base specification targeted. - [GitHub Flavored Markdown Spec](https://github.github.com/gfm/) — GFM extensions layered on top. - [WHATWG HTML § named character references](https://html.spec.whatwg.org/multipage/named-characters.html) — the entity table `assets/html-entities/` vendors. diff --git a/dist/block/block.d.cts b/dist/block/block.d.cts index 8332b7e..1310acf 100644 --- a/dist/block/block.d.cts +++ b/dist/block/block.d.cts @@ -1,6 +1,6 @@ import { s as MarkdownDocumentNode } from "../ast-8XCbjRQT.cjs"; import { r as FootnoteLabelSet } from "../footnote-CKk4JbLk.cjs"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { n as LinkReferenceMap } from "../link-Dv4kxVjk.cjs"; import { t as InlineParseOptions } from "../inline-CXVQWQnW.cjs"; //#region src/block/block.d.ts diff --git a/dist/block/block.d.ts b/dist/block/block.d.ts index fd6ddbe..6c62afe 100644 --- a/dist/block/block.d.ts +++ b/dist/block/block.d.ts @@ -1,6 +1,6 @@ import { s as MarkdownDocumentNode } from "../ast-8XCbjRQT.js"; import { r as FootnoteLabelSet } from "../footnote-CKk4JbLk.js"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { n as LinkReferenceMap } from "../link-Dv4kxVjk.js"; import { t as InlineParseOptions } from "../inline-B_V7bs5j.js"; //#region src/block/block.d.ts diff --git a/dist/block/definitions.d.cts b/dist/block/definitions.d.cts index 9ecab99..d0823c3 100644 --- a/dist/block/definitions.d.cts +++ b/dist/block/definitions.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { t as LinkReferenceDefinition } from "../link-Dv4kxVjk.cjs"; //#region src/block/definitions.d.ts declare function extractDefinitions(content: string, references: Map, sink?: MarkdownDiagnosticSink, startLine?: number): string; diff --git a/dist/block/definitions.d.ts b/dist/block/definitions.d.ts index 248c928..8c7aa87 100644 --- a/dist/block/definitions.d.ts +++ b/dist/block/definitions.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { t as LinkReferenceDefinition } from "../link-Dv4kxVjk.js"; //#region src/block/definitions.d.ts declare function extractDefinitions(content: string, references: Map, sink?: MarkdownDiagnosticSink, startLine?: number): string; diff --git a/dist/codec.cjs b/dist/codec.cjs index 6e9ed81..fe6d132 100644 --- a/dist/codec.cjs +++ b/dist/codec.cjs @@ -13,10 +13,15 @@ function isWellFormedUtf8Text(bytes) { } } const MarkdownBytesSchema = zod.z.instanceof(Uint8Array).refine(isWellFormedUtf8Text, { message: "not well-formed UTF-8 text" }); -const markdownCodec = zod.z.codec(MarkdownBytesSchema, document_schema_js.ContentDocumentSchema, { - decode: (bytes) => require_read.readMarkdown(new TextDecoder().decode(bytes)).document, - encode: (document) => new TextEncoder().encode(require_write.writeMarkdown(document)) +const markdownCodec = zod.z.codec(MarkdownBytesSchema, document_schema_js.DocumentPackageSchema, { + decode: (bytes) => require_read.readMarkdown(new TextDecoder().decode(bytes)).documentPackage, + encode: (documentPackage) => new TextEncoder().encode(require_write.writeMarkdown(documentPackage)) +}); +const markdownContentCodec = zod.z.codec(MarkdownBytesSchema, document_schema_js.ContentDocumentSchema, { + decode: (bytes) => require_read.readMarkdownContent(new TextDecoder().decode(bytes)).document, + encode: (document) => new TextEncoder().encode(require_write.writeMarkdownContent(document)) }); //#endregion exports.MarkdownBytesSchema = MarkdownBytesSchema; exports.markdownCodec = markdownCodec; +exports.markdownContentCodec = markdownContentCodec; diff --git a/dist/codec.d.cts b/dist/codec.d.cts index 2934cdf..09d820c 100644 --- a/dist/codec.d.cts +++ b/dist/codec.d.cts @@ -2,6 +2,539 @@ import { z } from "zod"; //#region src/codec.d.ts declare const MarkdownBytesSchema: z.ZodCustom, Uint8Array>; declare const markdownCodec: z.ZodCodec, Uint8Array>, z.ZodDiscriminatedUnion<[z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"wordprocessing">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"presentation">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"spreadsheet">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"drawing">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + starMath: z.ZodOptional; + presentation: z.ZodOptional>; + content: z.ZodOptional>; + provenance: z.ZodOptional; + editTrail: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"formula">; +}, z.core.$strip>], "kind">>; +declare const markdownContentCodec: z.ZodCodec, Uint8Array>, z.ZodDiscriminatedUnion<[z.ZodObject<{ sections: z.ZodArray, Uin }, z.core.$strip>; }, z.core.$strip>], "kind">>; //#endregion -export { MarkdownBytesSchema, markdownCodec }; \ No newline at end of file +export { MarkdownBytesSchema, markdownCodec, markdownContentCodec }; \ No newline at end of file diff --git a/dist/codec.d.ts b/dist/codec.d.ts index 2934cdf..09d820c 100644 --- a/dist/codec.d.ts +++ b/dist/codec.d.ts @@ -2,6 +2,539 @@ import { z } from "zod"; //#region src/codec.d.ts declare const MarkdownBytesSchema: z.ZodCustom, Uint8Array>; declare const markdownCodec: z.ZodCodec, Uint8Array>, z.ZodDiscriminatedUnion<[z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"wordprocessing">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"presentation">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"spreadsheet">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"drawing">; +}, z.core.$strip>, z.ZodObject<{ + children: z.ZodArray>; + starMath: z.ZodOptional; + presentation: z.ZodOptional>; + content: z.ZodOptional>; + provenance: z.ZodOptional; + editTrail: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>>; + pages: z.ZodOptional>>; + styles: z.ZodOptional>; + list: z.ZodOptional; + level: z.ZodNumber; + }, z.core.$strip>>; + spacingBeforePt: z.ZodOptional; + spacingAfterPt: z.ZodOptional; + lineSpacing: z.ZodOptional; + indentLeftPt: z.ZodOptional; + indentFirstLinePt: z.ZodOptional; + }, z.core.$strict>>; + run: z.ZodOptional; + italic: z.ZodOptional; + underline: z.ZodOptional; + strike: z.ZodOptional; + fontFamily: z.ZodOptional; + sizePt: z.ZodOptional; + color: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>>>; + definitions: z.ZodOptional>>; + layers: z.ZodOptional>>; + attachments: z.ZodOptional>>; + destinations: z.ZodOptional>>; + symbolTable: z.ZodOptional; + preferredUnit: z.ZodOptional; + definitionSource: z.ZodOptional; + }, z.core.$strip>>; + units: z.ZodArray; + dimension: z.ZodRecord & z.core.$partial, z.ZodNumber>; + factorToSi: z.ZodObject<{ + numerator: z.ZodString; + denominator: z.ZodString; + }, z.core.$strip>; + offsetToSi: z.ZodOptional>; + context: z.ZodOptional; + }, z.core.$strip>>; + contexts: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>>; + metadata: z.ZodObject<{ + title: z.ZodOptional; + author: z.ZodOptional; + subject: z.ZodOptional; + keywords: z.ZodOptional>; + creator: z.ZodOptional; + producer: z.ZodOptional; + createdIso: z.ZodOptional; + modifiedIso: z.ZodOptional; + }, z.core.$strip>; + kind: z.ZodLiteral<"formula">; +}, z.core.$strip>], "kind">>; +declare const markdownContentCodec: z.ZodCodec, Uint8Array>, z.ZodDiscriminatedUnion<[z.ZodObject<{ sections: z.ZodArray, Uin }, z.core.$strip>; }, z.core.$strip>], "kind">>; //#endregion -export { MarkdownBytesSchema, markdownCodec }; \ No newline at end of file +export { MarkdownBytesSchema, markdownCodec, markdownContentCodec }; \ No newline at end of file diff --git a/dist/codec.js b/dist/codec.js index aa329a6..1028802 100644 --- a/dist/codec.js +++ b/dist/codec.js @@ -1,7 +1,7 @@ -import { readMarkdown } from "./read.js"; -import { writeMarkdown } from "./write.js"; +import { readMarkdown, readMarkdownContent } from "./read.js"; +import { writeMarkdown, writeMarkdownContent } from "./write.js"; import { z } from "zod"; -import { ContentDocumentSchema } from "document-schema.js"; +import { ContentDocumentSchema, DocumentPackageSchema } from "document-schema.js"; //#region src/codec.ts function isWellFormedUtf8Text(bytes) { try { @@ -12,9 +12,13 @@ function isWellFormedUtf8Text(bytes) { } } const MarkdownBytesSchema = z.instanceof(Uint8Array).refine(isWellFormedUtf8Text, { message: "not well-formed UTF-8 text" }); -const markdownCodec = z.codec(MarkdownBytesSchema, ContentDocumentSchema, { - decode: (bytes) => readMarkdown(new TextDecoder().decode(bytes)).document, - encode: (document) => new TextEncoder().encode(writeMarkdown(document)) +const markdownCodec = z.codec(MarkdownBytesSchema, DocumentPackageSchema, { + decode: (bytes) => readMarkdown(new TextDecoder().decode(bytes)).documentPackage, + encode: (documentPackage) => new TextEncoder().encode(writeMarkdown(documentPackage)) +}); +const markdownContentCodec = z.codec(MarkdownBytesSchema, ContentDocumentSchema, { + decode: (bytes) => readMarkdownContent(new TextDecoder().decode(bytes)).document, + encode: (document) => new TextEncoder().encode(writeMarkdownContent(document)) }); //#endregion -export { MarkdownBytesSchema, markdownCodec }; +export { MarkdownBytesSchema, markdownCodec, markdownContentCodec }; diff --git a/dist/diagnostics-BuO5-SW1.d.cts b/dist/diagnostics-BWK1iGy7.d.cts similarity index 85% rename from dist/diagnostics-BuO5-SW1.d.cts rename to dist/diagnostics-BWK1iGy7.d.cts index 69e6c26..0f219bd 100644 --- a/dist/diagnostics-BuO5-SW1.d.cts +++ b/dist/diagnostics-BWK1iGy7.d.cts @@ -32,6 +32,7 @@ declare const MarkdownDiagnosticCodes: { readonly FOOTNOTE_REFERENCE_PRESERVED_AS_TEXT: "md/footnote-reference-preserved-as-text"; readonly FOOTNOTE_BODY_HEADING_FLATTENED: "md/footnote-body-heading-flattened"; readonly CONSTRUCT_UNREPRESENTED: "md/construct-unrepresented"; + readonly PACKAGE_TABLE_DROPPED: "md/package-table-dropped"; readonly HEADING_LEVEL_CLAMPED: "md/heading-level-clamped"; readonly ADJACENT_LINKS_MERGED: "md/adjacent-links-merged"; readonly CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run"; @@ -69,5 +70,8 @@ declare class MarkdownUnsupportedDocumentKindError extends MarkdownWriteError { readonly kind: string; constructor(kind: string); } +declare class MarkdownPackageFlattenError extends MarkdownWriteError { + constructor(cause: unknown); +} //#endregion -export { MarkdownInputTooLargeError as a, MarkdownParseError as c, MarkdownWriteError as d, NOOP_MARKDOWN_DIAGNOSTIC_SINK as f, MarkdownDiagnosticSink as i, MarkdownUnbalancedConstructMarkersError as l, MarkdownDiagnosticCodes as n, MarkdownInvalidUtf8Error as o, MarkdownDiagnosticSeverity as r, MarkdownNestingLimitExceededError as s, MarkdownDiagnostic as t, MarkdownUnsupportedDocumentKindError as u }; \ No newline at end of file +export { MarkdownInputTooLargeError as a, MarkdownPackageFlattenError as c, MarkdownUnsupportedDocumentKindError as d, MarkdownWriteError as f, MarkdownDiagnosticSink as i, MarkdownParseError as l, MarkdownDiagnosticCodes as n, MarkdownInvalidUtf8Error as o, NOOP_MARKDOWN_DIAGNOSTIC_SINK as p, MarkdownDiagnosticSeverity as r, MarkdownNestingLimitExceededError as s, MarkdownDiagnostic as t, MarkdownUnbalancedConstructMarkersError as u }; \ No newline at end of file diff --git a/dist/diagnostics-BuO5-SW1.d.ts b/dist/diagnostics-BWK1iGy7.d.ts similarity index 85% rename from dist/diagnostics-BuO5-SW1.d.ts rename to dist/diagnostics-BWK1iGy7.d.ts index 69e6c26..0f219bd 100644 --- a/dist/diagnostics-BuO5-SW1.d.ts +++ b/dist/diagnostics-BWK1iGy7.d.ts @@ -32,6 +32,7 @@ declare const MarkdownDiagnosticCodes: { readonly FOOTNOTE_REFERENCE_PRESERVED_AS_TEXT: "md/footnote-reference-preserved-as-text"; readonly FOOTNOTE_BODY_HEADING_FLATTENED: "md/footnote-body-heading-flattened"; readonly CONSTRUCT_UNREPRESENTED: "md/construct-unrepresented"; + readonly PACKAGE_TABLE_DROPPED: "md/package-table-dropped"; readonly HEADING_LEVEL_CLAMPED: "md/heading-level-clamped"; readonly ADJACENT_LINKS_MERGED: "md/adjacent-links-merged"; readonly CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run"; @@ -69,5 +70,8 @@ declare class MarkdownUnsupportedDocumentKindError extends MarkdownWriteError { readonly kind: string; constructor(kind: string); } +declare class MarkdownPackageFlattenError extends MarkdownWriteError { + constructor(cause: unknown); +} //#endregion -export { MarkdownInputTooLargeError as a, MarkdownParseError as c, MarkdownWriteError as d, NOOP_MARKDOWN_DIAGNOSTIC_SINK as f, MarkdownDiagnosticSink as i, MarkdownUnbalancedConstructMarkersError as l, MarkdownDiagnosticCodes as n, MarkdownInvalidUtf8Error as o, MarkdownDiagnosticSeverity as r, MarkdownNestingLimitExceededError as s, MarkdownDiagnostic as t, MarkdownUnsupportedDocumentKindError as u }; \ No newline at end of file +export { MarkdownInputTooLargeError as a, MarkdownPackageFlattenError as c, MarkdownUnsupportedDocumentKindError as d, MarkdownWriteError as f, MarkdownDiagnosticSink as i, MarkdownParseError as l, MarkdownDiagnosticCodes as n, MarkdownInvalidUtf8Error as o, NOOP_MARKDOWN_DIAGNOSTIC_SINK as p, MarkdownDiagnosticSeverity as r, MarkdownNestingLimitExceededError as s, MarkdownDiagnostic as t, MarkdownUnbalancedConstructMarkersError as u }; \ No newline at end of file diff --git a/dist/diagnostics/diagnostics.cjs b/dist/diagnostics/diagnostics.cjs index 9c8f841..1a0e944 100644 --- a/dist/diagnostics/diagnostics.cjs +++ b/dist/diagnostics/diagnostics.cjs @@ -25,6 +25,7 @@ const MarkdownDiagnosticCodes = { FOOTNOTE_REFERENCE_PRESERVED_AS_TEXT: "md/footnote-reference-preserved-as-text", FOOTNOTE_BODY_HEADING_FLATTENED: "md/footnote-body-heading-flattened", CONSTRUCT_UNREPRESENTED: "md/construct-unrepresented", + PACKAGE_TABLE_DROPPED: "md/package-table-dropped", HEADING_LEVEL_CLAMPED: "md/heading-level-clamped", ADJACENT_LINKS_MERGED: "md/adjacent-links-merged", CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run", @@ -91,11 +92,19 @@ var MarkdownUnsupportedDocumentKindError = class extends MarkdownWriteError { this.kind = kind; } }; +var MarkdownPackageFlattenError = class extends MarkdownWriteError { + constructor(cause) { + const detail = cause instanceof Error ? cause.message : String(cause); + super("md/package-flatten-failed", `flattening the package for write failed: ${detail}`); + this.name = "MarkdownPackageFlattenError"; + } +}; //#endregion exports.MarkdownDiagnosticCodes = MarkdownDiagnosticCodes; exports.MarkdownInputTooLargeError = MarkdownInputTooLargeError; exports.MarkdownInvalidUtf8Error = MarkdownInvalidUtf8Error; exports.MarkdownNestingLimitExceededError = MarkdownNestingLimitExceededError; +exports.MarkdownPackageFlattenError = MarkdownPackageFlattenError; exports.MarkdownParseError = MarkdownParseError; exports.MarkdownUnbalancedConstructMarkersError = MarkdownUnbalancedConstructMarkersError; exports.MarkdownUnsupportedDocumentKindError = MarkdownUnsupportedDocumentKindError; diff --git a/dist/diagnostics/diagnostics.d.cts b/dist/diagnostics/diagnostics.d.cts index 798854c..b717e46 100644 --- a/dist/diagnostics/diagnostics.d.cts +++ b/dist/diagnostics/diagnostics.d.cts @@ -1,2 +1,2 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as MarkdownWriteError, f as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnbalancedConstructMarkersError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnsupportedDocumentKindError } from "../diagnostics-BuO5-SW1.cjs"; -export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; \ No newline at end of file +import { a as MarkdownInputTooLargeError, c as MarkdownPackageFlattenError, d as MarkdownUnsupportedDocumentKindError, f as MarkdownWriteError, i as MarkdownDiagnosticSink, l as MarkdownParseError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, p as NOOP_MARKDOWN_DIAGNOSTIC_SINK, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnbalancedConstructMarkersError } from "../diagnostics-BWK1iGy7.cjs"; +export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownPackageFlattenError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; \ No newline at end of file diff --git a/dist/diagnostics/diagnostics.d.ts b/dist/diagnostics/diagnostics.d.ts index 395245d..a923bc5 100644 --- a/dist/diagnostics/diagnostics.d.ts +++ b/dist/diagnostics/diagnostics.d.ts @@ -1,2 +1,2 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as MarkdownWriteError, f as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnbalancedConstructMarkersError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnsupportedDocumentKindError } from "../diagnostics-BuO5-SW1.js"; -export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; \ No newline at end of file +import { a as MarkdownInputTooLargeError, c as MarkdownPackageFlattenError, d as MarkdownUnsupportedDocumentKindError, f as MarkdownWriteError, i as MarkdownDiagnosticSink, l as MarkdownParseError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, p as NOOP_MARKDOWN_DIAGNOSTIC_SINK, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnbalancedConstructMarkersError } from "../diagnostics-BWK1iGy7.js"; +export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownPackageFlattenError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; \ No newline at end of file diff --git a/dist/diagnostics/diagnostics.js b/dist/diagnostics/diagnostics.js index 5a8cb19..3b2250d 100644 --- a/dist/diagnostics/diagnostics.js +++ b/dist/diagnostics/diagnostics.js @@ -24,6 +24,7 @@ const MarkdownDiagnosticCodes = { FOOTNOTE_REFERENCE_PRESERVED_AS_TEXT: "md/footnote-reference-preserved-as-text", FOOTNOTE_BODY_HEADING_FLATTENED: "md/footnote-body-heading-flattened", CONSTRUCT_UNREPRESENTED: "md/construct-unrepresented", + PACKAGE_TABLE_DROPPED: "md/package-table-dropped", HEADING_LEVEL_CLAMPED: "md/heading-level-clamped", ADJACENT_LINKS_MERGED: "md/adjacent-links-merged", CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run", @@ -90,5 +91,12 @@ var MarkdownUnsupportedDocumentKindError = class extends MarkdownWriteError { this.kind = kind; } }; +var MarkdownPackageFlattenError = class extends MarkdownWriteError { + constructor(cause) { + const detail = cause instanceof Error ? cause.message : String(cause); + super("md/package-flatten-failed", `flattening the package for write failed: ${detail}`); + this.name = "MarkdownPackageFlattenError"; + } +}; //#endregion -export { MarkdownDiagnosticCodes, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; +export { MarkdownDiagnosticCodes, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownPackageFlattenError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; diff --git a/dist/emit/inline.d.cts b/dist/emit/inline.d.cts index 79a4519..037a9f1 100644 --- a/dist/emit/inline.d.cts +++ b/dist/emit/inline.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { ContentRun } from "document-schema.js"; //#region src/emit/inline.d.ts interface InlineEmitContext { diff --git a/dist/emit/inline.d.ts b/dist/emit/inline.d.ts index 246da09..c2b9be4 100644 --- a/dist/emit/inline.d.ts +++ b/dist/emit/inline.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { ContentRun } from "document-schema.js"; //#region src/emit/inline.d.ts interface InlineEmitContext { diff --git a/dist/emit/table.d.cts b/dist/emit/table.d.cts index 870d31f..b7e060e 100644 --- a/dist/emit/table.d.cts +++ b/dist/emit/table.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { InlineEmitContext } from "./inline.cjs"; import { ContentTable } from "document-schema.js"; //#region src/emit/table.d.ts diff --git a/dist/emit/table.d.ts b/dist/emit/table.d.ts index 783c592..fe30dcd 100644 --- a/dist/emit/table.d.ts +++ b/dist/emit/table.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { InlineEmitContext } from "./inline.js"; import { ContentTable } from "document-schema.js"; //#region src/emit/table.d.ts diff --git a/dist/index.cjs b/dist/index.cjs index 18396e4..5673907 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -26,9 +26,12 @@ exports.QUOTE_STYLE_ID = require_shared_style_constants.QUOTE_STYLE_ID; exports.createNumIdMintState = require_shared_list_id.createNumIdMintState; exports.headingStyleId = require_shared_style_constants.headingStyleId; exports.markdownCodec = require_codec.markdownCodec; +exports.markdownContentCodec = require_codec.markdownContentCodec; exports.mintListNumId = require_shared_list_id.mintListNumId; exports.mintedListType = require_shared_list_id.mintedListType; exports.parseHeadingStyleId = require_shared_style_constants.parseHeadingStyleId; exports.parseListNumId = require_shared_list_id.parseListNumId; exports.readMarkdown = require_read.readMarkdown; +exports.readMarkdownContent = require_read.readMarkdownContent; exports.writeMarkdown = require_write.writeMarkdown; +exports.writeMarkdownContent = require_write.writeMarkdownContent; diff --git a/dist/index.d.cts b/dist/index.d.cts index fa3dc42..979b755 100644 --- a/dist/index.d.cts +++ b/dist/index.d.cts @@ -1,9 +1,9 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as MarkdownWriteError, f as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnbalancedConstructMarkersError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnsupportedDocumentKindError } from "./diagnostics-BuO5-SW1.cjs"; -import { MarkdownBytesSchema, markdownCodec } from "./codec.cjs"; +import { a as MarkdownInputTooLargeError, d as MarkdownUnsupportedDocumentKindError, f as MarkdownWriteError, i as MarkdownDiagnosticSink, l as MarkdownParseError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, p as NOOP_MARKDOWN_DIAGNOSTIC_SINK, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnbalancedConstructMarkersError } from "./diagnostics-BWK1iGy7.cjs"; +import { MarkdownBytesSchema, markdownCodec, markdownContentCodec } from "./codec.cjs"; import { n as MarkdownImageResolver, r as MarkdownResolvedImageBytes, t as MarkdownImageResolveContext } from "./image-C4KYmz_L.cjs"; import { MarkdownBulletListMarker, MarkdownCodeFenceChar, MarkdownEmphasisMarker, MarkdownHeadingStyle, MarkdownLineEnding, MarkdownOrderedListDelimiter, MarkdownThematicBreakChar, ReadMarkdownOptions, WriteMarkdownOptions, WriteMarkdownStyleOptions } from "./options/options.cjs"; -import { ReadMarkdownResult, readMarkdown } from "./read.cjs"; -import { writeMarkdown } from "./write.cjs"; +import { ReadMarkdownContentResult, ReadMarkdownResult, readMarkdown, readMarkdownContent } from "./read.cjs"; +import { writeMarkdown, writeMarkdownContent } from "./write.cjs"; import { ListNumIdInfo, ListNumIdMintOptions, NumIdMintState, createNumIdMintState, mintListNumId, mintedListType, parseListNumId } from "./shared/list-id.cjs"; import { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, headingStyleId, parseHeadingStyleId } from "./shared/style-constants.cjs"; -export { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, type ListNumIdInfo, type ListNumIdMintOptions, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, type MarkdownBulletListMarker, MarkdownBytesSchema, type MarkdownCodeFenceChar, type MarkdownDiagnostic, MarkdownDiagnosticCodes, type MarkdownDiagnosticSeverity, type MarkdownDiagnosticSink, type MarkdownEmphasisMarker, type MarkdownHeadingStyle, type MarkdownImageResolveContext, type MarkdownImageResolver, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, type MarkdownLineEnding, MarkdownNestingLimitExceededError, type MarkdownOrderedListDelimiter, MarkdownParseError, type MarkdownResolvedImageBytes, type MarkdownThematicBreakChar, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK, type NumIdMintState, QUOTE_INDENT_PT, QUOTE_STYLE_ID, type ReadMarkdownOptions, type ReadMarkdownResult, type WriteMarkdownOptions, type WriteMarkdownStyleOptions, createNumIdMintState, headingStyleId, markdownCodec, mintListNumId, mintedListType, parseHeadingStyleId, parseListNumId, readMarkdown, writeMarkdown }; \ No newline at end of file +export { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, type ListNumIdInfo, type ListNumIdMintOptions, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, type MarkdownBulletListMarker, MarkdownBytesSchema, type MarkdownCodeFenceChar, type MarkdownDiagnostic, MarkdownDiagnosticCodes, type MarkdownDiagnosticSeverity, type MarkdownDiagnosticSink, type MarkdownEmphasisMarker, type MarkdownHeadingStyle, type MarkdownImageResolveContext, type MarkdownImageResolver, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, type MarkdownLineEnding, MarkdownNestingLimitExceededError, type MarkdownOrderedListDelimiter, MarkdownParseError, type MarkdownResolvedImageBytes, type MarkdownThematicBreakChar, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK, type NumIdMintState, QUOTE_INDENT_PT, QUOTE_STYLE_ID, type ReadMarkdownContentResult, type ReadMarkdownOptions, type ReadMarkdownResult, type WriteMarkdownOptions, type WriteMarkdownStyleOptions, createNumIdMintState, headingStyleId, markdownCodec, markdownContentCodec, mintListNumId, mintedListType, parseHeadingStyleId, parseListNumId, readMarkdown, readMarkdownContent, writeMarkdown, writeMarkdownContent }; \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 59617d1..0048f4b 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,9 +1,9 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as MarkdownWriteError, f as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnbalancedConstructMarkersError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnsupportedDocumentKindError } from "./diagnostics-BuO5-SW1.js"; -import { MarkdownBytesSchema, markdownCodec } from "./codec.js"; +import { a as MarkdownInputTooLargeError, d as MarkdownUnsupportedDocumentKindError, f as MarkdownWriteError, i as MarkdownDiagnosticSink, l as MarkdownParseError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, p as NOOP_MARKDOWN_DIAGNOSTIC_SINK, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownUnbalancedConstructMarkersError } from "./diagnostics-BWK1iGy7.js"; +import { MarkdownBytesSchema, markdownCodec, markdownContentCodec } from "./codec.js"; import { n as MarkdownImageResolver, r as MarkdownResolvedImageBytes, t as MarkdownImageResolveContext } from "./image-Cm3hT5PS.js"; import { MarkdownBulletListMarker, MarkdownCodeFenceChar, MarkdownEmphasisMarker, MarkdownHeadingStyle, MarkdownLineEnding, MarkdownOrderedListDelimiter, MarkdownThematicBreakChar, ReadMarkdownOptions, WriteMarkdownOptions, WriteMarkdownStyleOptions } from "./options/options.js"; -import { ReadMarkdownResult, readMarkdown } from "./read.js"; -import { writeMarkdown } from "./write.js"; +import { ReadMarkdownContentResult, ReadMarkdownResult, readMarkdown, readMarkdownContent } from "./read.js"; +import { writeMarkdown, writeMarkdownContent } from "./write.js"; import { ListNumIdInfo, ListNumIdMintOptions, NumIdMintState, createNumIdMintState, mintListNumId, mintedListType, parseListNumId } from "./shared/list-id.js"; import { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, headingStyleId, parseHeadingStyleId } from "./shared/style-constants.js"; -export { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, type ListNumIdInfo, type ListNumIdMintOptions, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, type MarkdownBulletListMarker, MarkdownBytesSchema, type MarkdownCodeFenceChar, type MarkdownDiagnostic, MarkdownDiagnosticCodes, type MarkdownDiagnosticSeverity, type MarkdownDiagnosticSink, type MarkdownEmphasisMarker, type MarkdownHeadingStyle, type MarkdownImageResolveContext, type MarkdownImageResolver, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, type MarkdownLineEnding, MarkdownNestingLimitExceededError, type MarkdownOrderedListDelimiter, MarkdownParseError, type MarkdownResolvedImageBytes, type MarkdownThematicBreakChar, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK, type NumIdMintState, QUOTE_INDENT_PT, QUOTE_STYLE_ID, type ReadMarkdownOptions, type ReadMarkdownResult, type WriteMarkdownOptions, type WriteMarkdownStyleOptions, createNumIdMintState, headingStyleId, markdownCodec, mintListNumId, mintedListType, parseHeadingStyleId, parseListNumId, readMarkdown, writeMarkdown }; \ No newline at end of file +export { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, type ListNumIdInfo, type ListNumIdMintOptions, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, type MarkdownBulletListMarker, MarkdownBytesSchema, type MarkdownCodeFenceChar, type MarkdownDiagnostic, MarkdownDiagnosticCodes, type MarkdownDiagnosticSeverity, type MarkdownDiagnosticSink, type MarkdownEmphasisMarker, type MarkdownHeadingStyle, type MarkdownImageResolveContext, type MarkdownImageResolver, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, type MarkdownLineEnding, MarkdownNestingLimitExceededError, type MarkdownOrderedListDelimiter, MarkdownParseError, type MarkdownResolvedImageBytes, type MarkdownThematicBreakChar, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK, type NumIdMintState, QUOTE_INDENT_PT, QUOTE_STYLE_ID, type ReadMarkdownContentResult, type ReadMarkdownOptions, type ReadMarkdownResult, type WriteMarkdownOptions, type WriteMarkdownStyleOptions, createNumIdMintState, headingStyleId, markdownCodec, markdownContentCodec, mintListNumId, mintedListType, parseHeadingStyleId, parseListNumId, readMarkdown, readMarkdownContent, writeMarkdown, writeMarkdownContent }; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index fd3290a..9006e8a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,7 +1,7 @@ import { MarkdownDiagnosticCodes, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from "./diagnostics/diagnostics.js"; import { createNumIdMintState, mintListNumId, mintedListType, parseListNumId } from "./shared/list-id.js"; import { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, headingStyleId, parseHeadingStyleId } from "./shared/style-constants.js"; -import { readMarkdown } from "./read.js"; -import { writeMarkdown } from "./write.js"; -import { MarkdownBytesSchema, markdownCodec } from "./codec.js"; -export { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, MarkdownBytesSchema, MarkdownDiagnosticCodes, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK, QUOTE_INDENT_PT, QUOTE_STYLE_ID, createNumIdMintState, headingStyleId, markdownCodec, mintListNumId, mintedListType, parseHeadingStyleId, parseListNumId, readMarkdown, writeMarkdown }; +import { readMarkdown, readMarkdownContent } from "./read.js"; +import { writeMarkdown, writeMarkdownContent } from "./write.js"; +import { MarkdownBytesSchema, markdownCodec, markdownContentCodec } from "./codec.js"; +export { CODE_BLOCK_STYLE_ID, FOOTNOTE_REFERENCE_FONT_MARKER, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, MarkdownBytesSchema, MarkdownDiagnosticCodes, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK, QUOTE_INDENT_PT, QUOTE_STYLE_ID, createNumIdMintState, headingStyleId, markdownCodec, markdownContentCodec, mintListNumId, mintedListType, parseHeadingStyleId, parseListNumId, readMarkdown, readMarkdownContent, writeMarkdown, writeMarkdownContent }; diff --git a/dist/lower/front-matter.d.cts b/dist/lower/front-matter.d.cts index 40e20d0..207251b 100644 --- a/dist/lower/front-matter.d.cts +++ b/dist/lower/front-matter.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { LayoutMetadata } from "document-schema.js"; //#region src/lower/front-matter.d.ts interface FrontMatterResult { diff --git a/dist/lower/front-matter.d.ts b/dist/lower/front-matter.d.ts index e344283..b5ac516 100644 --- a/dist/lower/front-matter.d.ts +++ b/dist/lower/front-matter.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { LayoutMetadata } from "document-schema.js"; //#region src/lower/front-matter.d.ts interface FrontMatterResult { diff --git a/dist/lower/inline.d.cts b/dist/lower/inline.d.cts index 1218a47..48efa94 100644 --- a/dist/lower/inline.d.cts +++ b/dist/lower/inline.d.cts @@ -1,5 +1,5 @@ import { v as MarkdownInlineNode } from "../ast-8XCbjRQT.cjs"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { ContentRun } from "document-schema.js"; //#region src/lower/inline.d.ts interface InlineLowerContext { diff --git a/dist/lower/inline.d.ts b/dist/lower/inline.d.ts index f52c917..5f8ef66 100644 --- a/dist/lower/inline.d.ts +++ b/dist/lower/inline.d.ts @@ -1,5 +1,5 @@ import { v as MarkdownInlineNode } from "../ast-8XCbjRQT.js"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { ContentRun } from "document-schema.js"; //#region src/lower/inline.d.ts interface InlineLowerContext { diff --git a/dist/options/options.d.cts b/dist/options/options.d.cts index eb4c11d..5dda5c7 100644 --- a/dist/options/options.d.cts +++ b/dist/options/options.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.cjs"; import { n as MarkdownImageResolver } from "../image-C4KYmz_L.cjs"; import { Margins, PageSize } from "document-schema.js"; //#region src/options/options.d.ts diff --git a/dist/options/options.d.ts b/dist/options/options.d.ts index 24979d1..04060e1 100644 --- a/dist/options/options.d.ts +++ b/dist/options/options.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-BuO5-SW1.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-BWK1iGy7.js"; import { n as MarkdownImageResolver } from "../image-Cm3hT5PS.js"; import { Margins, PageSize } from "document-schema.js"; //#region src/options/options.d.ts diff --git a/dist/read.cjs b/dist/read.cjs index 2923be5..8687d28 100644 --- a/dist/read.cjs +++ b/dist/read.cjs @@ -1,7 +1,15 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const require_lower_lower = require("./lower/lower.cjs"); +let document_schema_js = require("document-schema.js"); //#region src/read.ts function readMarkdown(text, options = {}) { + const { document, diagnostics } = readMarkdownContent(text, options); + return { + documentPackage: (0, document_schema_js.assemblePackage)(document), + diagnostics + }; +} +function readMarkdownContent(text, options = {}) { options.signal?.throwIfAborted(); const diagnostics = []; const callerSink = options.sink; @@ -18,3 +26,4 @@ function readMarkdown(text, options = {}) { } //#endregion exports.readMarkdown = readMarkdown; +exports.readMarkdownContent = readMarkdownContent; diff --git a/dist/read.d.cts b/dist/read.d.cts index b26cc6d..c50111f 100644 --- a/dist/read.d.cts +++ b/dist/read.d.cts @@ -1,11 +1,16 @@ -import { t as MarkdownDiagnostic } from "./diagnostics-BuO5-SW1.cjs"; +import { t as MarkdownDiagnostic } from "./diagnostics-BWK1iGy7.cjs"; import { ReadMarkdownOptions } from "./options/options.cjs"; -import { ContentDocument } from "document-schema.js"; +import { ContentDocument, DocumentPackage } from "document-schema.js"; //#region src/read.d.ts interface ReadMarkdownResult { + readonly documentPackage: DocumentPackage; + readonly diagnostics: readonly MarkdownDiagnostic[]; +} +interface ReadMarkdownContentResult { readonly document: ContentDocument; readonly diagnostics: readonly MarkdownDiagnostic[]; } declare function readMarkdown(text: string, options?: ReadMarkdownOptions): ReadMarkdownResult; +declare function readMarkdownContent(text: string, options?: ReadMarkdownOptions): ReadMarkdownContentResult; //#endregion -export { ReadMarkdownResult, readMarkdown }; \ No newline at end of file +export { ReadMarkdownContentResult, ReadMarkdownResult, readMarkdown, readMarkdownContent }; \ No newline at end of file diff --git a/dist/read.d.ts b/dist/read.d.ts index 85e1c2b..b80498c 100644 --- a/dist/read.d.ts +++ b/dist/read.d.ts @@ -1,11 +1,16 @@ -import { t as MarkdownDiagnostic } from "./diagnostics-BuO5-SW1.js"; +import { t as MarkdownDiagnostic } from "./diagnostics-BWK1iGy7.js"; import { ReadMarkdownOptions } from "./options/options.js"; -import { ContentDocument } from "document-schema.js"; +import { ContentDocument, DocumentPackage } from "document-schema.js"; //#region src/read.d.ts interface ReadMarkdownResult { + readonly documentPackage: DocumentPackage; + readonly diagnostics: readonly MarkdownDiagnostic[]; +} +interface ReadMarkdownContentResult { readonly document: ContentDocument; readonly diagnostics: readonly MarkdownDiagnostic[]; } declare function readMarkdown(text: string, options?: ReadMarkdownOptions): ReadMarkdownResult; +declare function readMarkdownContent(text: string, options?: ReadMarkdownOptions): ReadMarkdownContentResult; //#endregion -export { ReadMarkdownResult, readMarkdown }; \ No newline at end of file +export { ReadMarkdownContentResult, ReadMarkdownResult, readMarkdown, readMarkdownContent }; \ No newline at end of file diff --git a/dist/read.js b/dist/read.js index 352cd4a..4afab2b 100644 --- a/dist/read.js +++ b/dist/read.js @@ -1,6 +1,14 @@ import { lowerMarkdown } from "./lower/lower.js"; +import { assemblePackage } from "document-schema.js"; //#region src/read.ts function readMarkdown(text, options = {}) { + const { document, diagnostics } = readMarkdownContent(text, options); + return { + documentPackage: assemblePackage(document), + diagnostics + }; +} +function readMarkdownContent(text, options = {}) { options.signal?.throwIfAborted(); const diagnostics = []; const callerSink = options.sink; @@ -16,4 +24,4 @@ function readMarkdown(text, options = {}) { }; } //#endregion -export { readMarkdown }; +export { readMarkdown, readMarkdownContent }; diff --git a/dist/write.cjs b/dist/write.cjs index fe87ba7..9ce56b0 100644 --- a/dist/write.cjs +++ b/dist/write.cjs @@ -1,9 +1,41 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); +const require_diagnostics_diagnostics = require("./diagnostics/diagnostics.cjs"); const require_emit_emit = require("./emit/emit.cjs"); +let document_schema_js = require("document-schema.js"); //#region src/write.ts -function writeMarkdown(document, options = {}) { +function reportDroppedPackageTables(documentPackage, sink) { + const tables = [ + ["definitions", documentPackage.definitions !== void 0 && Object.keys(documentPackage.definitions).length > 0], + ["layers", documentPackage.layers !== void 0 && Object.keys(documentPackage.layers).length > 0], + ["attachments", documentPackage.attachments !== void 0 && Object.keys(documentPackage.attachments).length > 0], + ["destinations", documentPackage.destinations !== void 0 && Object.keys(documentPackage.destinations).length > 0], + ["pages", documentPackage.pages !== void 0 && documentPackage.pages.length > 0] + ]; + for (const [name, present] of tables) { + if (!present) continue; + sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED, + severity: "info", + message: `the package's own "${name}" table has no markdown representation; flattenPackage's envelope carries forward only metadata and symbolTable, so "${name}" is dropped rather than rendered` + }); + } +} +function writeMarkdown(documentPackage, options = {}) { + options.signal?.throwIfAborted(); + if (documentPackage.kind !== "wordprocessing") throw new require_diagnostics_diagnostics.MarkdownUnsupportedDocumentKindError(documentPackage.kind); + reportDroppedPackageTables(documentPackage, options.sink ?? require_diagnostics_diagnostics.NOOP_MARKDOWN_DIAGNOSTIC_SINK); + let flattened; + try { + flattened = (0, document_schema_js.flattenPackage)(documentPackage); + } catch (error) { + throw new require_diagnostics_diagnostics.MarkdownPackageFlattenError(error); + } + return writeMarkdownContent(flattened, options); +} +function writeMarkdownContent(document, options = {}) { options.signal?.throwIfAborted(); return require_emit_emit.emitMarkdown(document, options); } //#endregion exports.writeMarkdown = writeMarkdown; +exports.writeMarkdownContent = writeMarkdownContent; diff --git a/dist/write.d.cts b/dist/write.d.cts index b9ef22a..5bdd611 100644 --- a/dist/write.d.cts +++ b/dist/write.d.cts @@ -1,6 +1,7 @@ import { WriteMarkdownOptions } from "./options/options.cjs"; -import { ContentDocument } from "document-schema.js"; +import { ContentDocument, DocumentPackage } from "document-schema.js"; //#region src/write.d.ts -declare function writeMarkdown(document: ContentDocument, options?: WriteMarkdownOptions): string; +declare function writeMarkdown(documentPackage: DocumentPackage, options?: WriteMarkdownOptions): string; +declare function writeMarkdownContent(document: ContentDocument, options?: WriteMarkdownOptions): string; //#endregion -export { writeMarkdown }; \ No newline at end of file +export { writeMarkdown, writeMarkdownContent }; \ No newline at end of file diff --git a/dist/write.d.ts b/dist/write.d.ts index ab0eaf6..ef5b070 100644 --- a/dist/write.d.ts +++ b/dist/write.d.ts @@ -1,6 +1,7 @@ import { WriteMarkdownOptions } from "./options/options.js"; -import { ContentDocument } from "document-schema.js"; +import { ContentDocument, DocumentPackage } from "document-schema.js"; //#region src/write.d.ts -declare function writeMarkdown(document: ContentDocument, options?: WriteMarkdownOptions): string; +declare function writeMarkdown(documentPackage: DocumentPackage, options?: WriteMarkdownOptions): string; +declare function writeMarkdownContent(document: ContentDocument, options?: WriteMarkdownOptions): string; //#endregion -export { writeMarkdown }; \ No newline at end of file +export { writeMarkdown, writeMarkdownContent }; \ No newline at end of file diff --git a/dist/write.js b/dist/write.js index 929a056..f6d6f3e 100644 --- a/dist/write.js +++ b/dist/write.js @@ -1,8 +1,39 @@ +import { MarkdownDiagnosticCodes, MarkdownPackageFlattenError, MarkdownUnsupportedDocumentKindError, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from "./diagnostics/diagnostics.js"; import { emitMarkdown } from "./emit/emit.js"; +import { flattenPackage } from "document-schema.js"; //#region src/write.ts -function writeMarkdown(document, options = {}) { +function reportDroppedPackageTables(documentPackage, sink) { + const tables = [ + ["definitions", documentPackage.definitions !== void 0 && Object.keys(documentPackage.definitions).length > 0], + ["layers", documentPackage.layers !== void 0 && Object.keys(documentPackage.layers).length > 0], + ["attachments", documentPackage.attachments !== void 0 && Object.keys(documentPackage.attachments).length > 0], + ["destinations", documentPackage.destinations !== void 0 && Object.keys(documentPackage.destinations).length > 0], + ["pages", documentPackage.pages !== void 0 && documentPackage.pages.length > 0] + ]; + for (const [name, present] of tables) { + if (!present) continue; + sink({ + code: MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED, + severity: "info", + message: `the package's own "${name}" table has no markdown representation; flattenPackage's envelope carries forward only metadata and symbolTable, so "${name}" is dropped rather than rendered` + }); + } +} +function writeMarkdown(documentPackage, options = {}) { + options.signal?.throwIfAborted(); + if (documentPackage.kind !== "wordprocessing") throw new MarkdownUnsupportedDocumentKindError(documentPackage.kind); + reportDroppedPackageTables(documentPackage, options.sink ?? NOOP_MARKDOWN_DIAGNOSTIC_SINK); + let flattened; + try { + flattened = flattenPackage(documentPackage); + } catch (error) { + throw new MarkdownPackageFlattenError(error); + } + return writeMarkdownContent(flattened, options); +} +function writeMarkdownContent(document, options = {}) { options.signal?.throwIfAborted(); return emitMarkdown(document, options); } //#endregion -export { writeMarkdown }; +export { writeMarkdown, writeMarkdownContent }; diff --git a/package.json b/package.json index b8594de..4af6091 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "markdown-codec", "version": "3.1.1", - "description": "Hand-written CommonMark+GFM <-> ContentDocument codec, built on document-schema.js", + "description": "Hand-written CommonMark+GFM <-> DocumentPackage codec, built on document-schema.js", "type": "module", "repository": { "type": "git", diff --git a/src/codec.ts b/src/codec.ts index 1229b0a..1074a9b 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -1,10 +1,10 @@ -// markdownCodec: a z.codec() pair over readMarkdown/writeMarkdown, matching this family's own convention (pdf-codec's pdfCodec, documents.js's docxPdfCodec/odtDocxCodec/etc.) of wrapping an already-independently-tested function pair with automatic two-way schema validation. Deliberately the no-options form -- readMarkdown/writeMarkdown remain the entry points wherever a caller needs an AbortSignal or a diagnostic sink, since z.codec()'s fixed decode(input)/encode(output) signature has no room for side-channel options. +// markdownCodec / markdownContentCodec: a z.codec() pair per encoding document-schema.js states for one document, each wrapping the matching read/write pair from src/read.ts and src/write.ts with automatic two-way schema validation -- this family's own convention (pdf-codec's pdfCodec, documents.js's docxPdfCodec/odtDocxCodec/etc.) of wrapping an already-independently-tested function pair. markdownCodec decodes to the tree-form DocumentPackage and markdownContentCodec to the flat ContentDocument, matching which of readMarkdown/readMarkdownContent each is built over, so the codec surface and the function surface name the same thing the same way. Both are deliberately the no-options form -- readMarkdown/writeMarkdown remain the entry points wherever a caller needs an AbortSignal or a diagnostic sink, since z.codec()'s fixed decode(input)/encode(output) signature has no room for side-channel options. // // MarkdownBytesSchema is the one genuinely checkable thing about arbitrary markdown bytes -- unlike pdf-codec's PdfBytesSchema (a real "%PDF-" magic-byte header) or documents.js's docx/pptx magic-byte schemas, markdown has no header, no magic bytes, and no reserved byte sequence of its own: any well-formed UTF-8 text is, structurally, valid markdown (CommonMark's own grammar has no "this is not markdown" rejection path -- worst case, an unparseable line becomes an ordinary paragraph). So the one thing actually worth validating at the bytes boundary is well-formed UTF-8 -- decoding is done with a `fatal: true` TextDecoder specifically so a malformed byte sequence is caught here, at the schema, rather than surfacing later as silently-mangled replacement characters inside readMarkdown's own output. import { z } from 'zod'; -import { ContentDocumentSchema } from 'document-schema.js'; -import { readMarkdown } from './read'; -import { writeMarkdown } from './write'; +import { ContentDocumentSchema, DocumentPackageSchema } from 'document-schema.js'; +import { readMarkdown, readMarkdownContent } from './read'; +import { writeMarkdown, writeMarkdownContent } from './write'; function isWellFormedUtf8Text(bytes: Uint8Array): boolean { try { @@ -17,8 +17,13 @@ function isWellFormedUtf8Text(bytes: Uint8Array): boolean { export const MarkdownBytesSchema = z.instanceof(Uint8Array).refine(isWellFormedUtf8Text, { message: 'not well-formed UTF-8 text' }); -export const markdownCodec = z.codec(MarkdownBytesSchema, ContentDocumentSchema, { +export const markdownCodec = z.codec(MarkdownBytesSchema, DocumentPackageSchema, { // isWellFormedUtf8Text already validated `bytes` above, so a plain (non-fatal) decode here cannot itself fail on malformed input -- MarkdownInvalidUtf8Error (src/diagnostics/diagnostics.ts) exists for a caller that decodes bytes to text itself, outside this schema-guarded path, not for this one. - decode: (bytes) => readMarkdown(new TextDecoder().decode(bytes)).document, - encode: (document) => new TextEncoder().encode(writeMarkdown(document)), + decode: (bytes) => readMarkdown(new TextDecoder().decode(bytes)).documentPackage, + encode: (documentPackage) => new TextEncoder().encode(writeMarkdown(documentPackage)), +}); + +export const markdownContentCodec = z.codec(MarkdownBytesSchema, ContentDocumentSchema, { + decode: (bytes) => readMarkdownContent(new TextDecoder().decode(bytes)).document, + encode: (document) => new TextEncoder().encode(writeMarkdownContent(document)), }); diff --git a/src/conformance.test.ts b/src/conformance.test.ts index a4c0440..2e9f3c9 100644 --- a/src/conformance.test.ts +++ b/src/conformance.test.ts @@ -1,27 +1,30 @@ // The whole pipeline -- read, write, and reparse together -- measured against the REAL CommonMark 0.31.2 conformance corpus (assets/commonmark/spec.json), every example in every section, not a subset. // -// Each example is run end to end through the real PUBLIC readMarkdown/writeMarkdown surface, not just the bare parser: readMarkdown (src/read.ts, itself src/block/block.ts's parseMarkdown plus src/lower/lower.ts's lowering to a ContentDocument) produces the document-schema.js pivot; writeMarkdown (src/write.ts, src/emit/emit.ts) renders that pivot back to markdown text; parseMarkdown reads that rewritten text a second time, under the identical CommonMark-only options, back to this package's own internal AST; and src/html/render.ts (the real CommonMark-HTML conformance oracle) renders that AST to HTML, compared byte for byte against the corpus's own `html` field. This is deliberately a stricter bar than measuring the bare parser alone: a round trip through the ContentDocument pivot has to survive src/lower's own semantic mapping AND src/emit's own inverse rendering with no loss the reparse can detect, which is exactly the wiring this test exists to prove now that read/write/codec are assembled -- see src/lower/ and src/emit/'s own top-of-file comments for what each stage is documented to gain or lose. +// Each example is run end to end through the real PUBLIC readMarkdownContent/writeMarkdownContent surface, not just the bare parser: readMarkdownContent (src/read.ts, itself src/block/block.ts's parseMarkdown plus src/lower/lower.ts's lowering to a ContentDocument) produces the document-schema.js pivot; writeMarkdownContent (src/write.ts, src/emit/emit.ts) renders that pivot back to markdown text; parseMarkdown reads that rewritten text a second time, under the identical CommonMark-only options, back to this package's own internal AST; and src/html/render.ts (the real CommonMark-HTML conformance oracle) renders that AST to HTML, compared byte for byte against the corpus's own `html` field. This is deliberately a stricter bar than measuring the bare parser alone: a round trip through the ContentDocument pivot has to survive src/lower's own semantic mapping AND src/emit's own inverse rendering with no loss the reparse can detect, which is exactly the wiring this test exists to prove now that read/write/codec are assembled -- see src/lower/ and src/emit/'s own top-of-file comments for what each stage is documented to gain or lose. // -// GFM's own extensions, and GitHub's footnote extension alongside them, are switched OFF for both the read and the reparse: a bare `http://example.com` in paragraph text is plain text under CommonMark and a link under GFM, a `~~x~~` is literal tildes, a delimiter row is ordinary paragraph text, and a leading `[ ]`/`[x]` is ordinary paragraph text rather than a task-list marker -- this suite measures CommonMark, and src/gfm-conformance.test.ts measures the extensions (through the identical read -> write -> reparse -> render path) against their own corpus. writeMarkdown itself has no GFM toggle of its own to match: it emits whatever markdown syntax a given ContentDocument construct needs (a ContentTable always becomes a GFM table, a strike run always becomes `~~x~~`), and with the extensions off on the read side no such construct is ever produced from a CommonMark-only example in the first place. +// The flat ContentDocument pair is what this suite measures rather than the tree-native readMarkdown/writeMarkdown above it: lowering and emission are where every conformance-relevant decision is made, and the package boundary between them is a pure structural transform document-schema.js proves bijective in its own suite. src/package.test.ts pins that claim over two hand-picked fixtures; the "tree pair matches the flat pair" describe block below re-checks both of its halves (flattenPackage(assemblePackage(document)) reproducing document, and writeMarkdown rendering identically to writeMarkdownContent) over every example in this suite's own corpus, so measuring the flat pair for the conformance rate above is measuring the tree-native pair too, on real content rather than two fixtures alone. +// +// GFM's own extensions, and GitHub's footnote extension alongside them, are switched OFF for both the read and the reparse: a bare `http://example.com` in paragraph text is plain text under CommonMark and a link under GFM, a `~~x~~` is literal tildes, a delimiter row is ordinary paragraph text, and a leading `[ ]`/`[x]` is ordinary paragraph text rather than a task-list marker -- this suite measures CommonMark, and src/gfm-conformance.test.ts measures the extensions (through the identical read -> write -> reparse -> render path) against their own corpus. writeMarkdownContent itself has no GFM toggle of its own to match: it emits whatever markdown syntax a given ContentDocument construct needs (a ContentTable always becomes a GFM table, a strike run always becomes `~~x~~`), and with the extensions off on the read side no such construct is ever produced from a CommonMark-only example in the first place. // // Anything not yet passing is named individually in src/test-support/conformance-exclusions.ts, with a test below asserting that every excluded example genuinely still fails -- see that file for why the list can only shrink. +import { assemblePackage, flattenPackage } from 'document-schema.js'; import { describe, expect, it } from 'vitest'; import { parseMarkdown } from './block/block'; import { renderDocumentToHtml } from './html/render'; -import { readMarkdown } from './read'; +import { readMarkdownContent } from './read'; import { COMMONMARK_EXCLUSIONS } from './test-support/conformance-exclusions'; import type { SpecExample } from './test-support/spec-corpus'; import { loadSpecExamples } from './test-support/spec-corpus'; -import { writeMarkdown } from './write'; +import { writeMarkdown, writeMarkdownContent } from './write'; // CommonMark, not CommonMark+GFM -- see this file's own top-of-file note. const COMMONMARK_ONLY = { gfmAutolinks: false, gfmStrikethrough: false, gfmTables: false, gfmTaskLists: false, footnotes: false }; // read -> write -> reparse -> render, all through this package's real public surface -- see this file's own top-of-file note for why this is the bar now, not a direct parseMarkdown -> render measurement. function render(example: SpecExample): string { - const { document } = readMarkdown(example.markdown, COMMONMARK_ONLY); - const rewritten = writeMarkdown(document); + const { document } = readMarkdownContent(example.markdown, COMMONMARK_ONLY); + const rewritten = writeMarkdownContent(document); return renderDocumentToHtml(parseMarkdown(rewritten, COMMONMARK_ONLY).document); } @@ -49,3 +52,13 @@ describe('CommonMark 0.31.2 conformance', () => { expect([...COMMONMARK_EXCLUSIONS.keys()].filter((number) => !numbers.has(number))).toEqual([]); }); }); + +// The tree pair's own two properties (src/package.test.ts's (i) and (ii)), re-checked over every example in this suite's own corpus rather than the two fixtures that file hand-picks -- run over ALL 652 examples, not just the ones `covered` renders correct HTML for: both properties are about the assemblePackage/flattenPackage transform and the writeMarkdown/writeMarkdownContent pair agreeing with each other, neither of which depends on whether the CommonMark HTML round trip above happens to succeed for a given example. +describe('tree pair matches the flat pair this suite measures', () => { + it.each(examples.map((example) => [`example ${String(example.example)} (${example.section})`, example] as const))('%s: flattenPackage(assemblePackage(document)) reproduces document, and writeMarkdown renders what writeMarkdownContent renders', (_name, example) => { + const { document } = readMarkdownContent(example.markdown, COMMONMARK_ONLY); + + expect(flattenPackage(assemblePackage(document))).toEqual(document); + expect(writeMarkdown(assemblePackage(document))).toBe(writeMarkdownContent(document)); + }); +}); diff --git a/src/diagnostics/diagnostics.test.ts b/src/diagnostics/diagnostics.test.ts index 6671f01..c4886cb 100644 --- a/src/diagnostics/diagnostics.test.ts +++ b/src/diagnostics/diagnostics.test.ts @@ -1,12 +1,13 @@ -// Coverage sweep: every entry in MarkdownDiagnosticCodes must be reachable from some real input to this package's own read/write surface (parseMarkdown, lowerMarkdown, emitMarkdown) -- a code that exists in the table but that nothing ever fires is dead documentation, worse than no documentation at all. Each case below is deliberately minimal and independent of src/block/block.test.ts, src/lower/lower.test.ts, and src/emit/emit.test.ts's own (more thoroughly asserted) per-gap tests -- this file only cares whether the code fires at all, not what else the surrounding output looks like. The final test asserts the codes proven reachable here cover the whole MarkdownDiagnosticCodes table, so the list can never grow a new, silently-unreachable entry. +// Coverage sweep: every entry in MarkdownDiagnosticCodes must be reachable from some real input to this package's own read/write surface (parseMarkdown, lowerMarkdown, emitMarkdown, writeMarkdown) -- a code that exists in the table but that nothing ever fires is dead documentation, worse than no documentation at all. Each case below is deliberately minimal and independent of src/block/block.test.ts, src/lower/lower.test.ts, src/emit/emit.test.ts, and src/package.test.ts's own (more thoroughly asserted) per-gap tests -- this file only cares whether the code fires at all, not what else the surrounding output looks like. The final test asserts the codes proven reachable here cover the whole MarkdownDiagnosticCodes table, so the list can never grow a new, silently-unreachable entry. -import type { ContentBlock, ContentDocument, ContentTable } from 'document-schema.js'; +import type { ContentBlock, ContentDocument, ContentTable, DocumentPackage } from 'document-schema.js'; import { PAGE_SIZE_A4 } from 'document-schema.js'; import { describe, expect, it } from 'vitest'; import { parseMarkdown } from '../block/block'; import { emitMarkdown } from '../emit/emit'; import { lowerMarkdown } from '../lower/lower'; import { createDiagnosticCollector } from '../test-support/diagnostics'; +import { writeMarkdown } from '../write'; import { MarkdownDiagnosticCodes } from './diagnostics'; function minimalDocument(blocks: readonly ContentBlock[]): ContentDocument { @@ -236,6 +237,14 @@ describe('every MarkdownDiagnosticCodes entry is reachable from real input', () reached.add(MarkdownDiagnosticCodes.CONSTRUCT_UNREPRESENTED); }); + it('PACKAGE_TABLE_DROPPED: a DocumentPackage carrying a non-empty definitions table', () => { + const collector = createDiagnosticCollector(); + const pkg: DocumentPackage = { kind: 'wordprocessing', metadata: {}, children: [], definitions: { d1: { kind: 'bookmark' } } }; + writeMarkdown(pkg, { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED)).toBe(true); + reached.add(MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED); + }); + it('has no dead code: every value in MarkdownDiagnosticCodes was proven reachable above', () => { expect(reached).toEqual(new Set(Object.values(MarkdownDiagnosticCodes))); }); diff --git a/src/diagnostics/diagnostics.ts b/src/diagnostics/diagnostics.ts index f109564..7a0b10b 100644 --- a/src/diagnostics/diagnostics.ts +++ b/src/diagnostics/diagnostics.ts @@ -50,6 +50,8 @@ export const MarkdownDiagnosticCodes = { FOOTNOTE_BODY_HEADING_FLATTENED: 'md/footnote-body-heading-flattened', // src/emit (write side: ContentDocument -> markdown) CONSTRUCT_UNREPRESENTED: 'md/construct-unrepresented', + // src/write.ts (tree write side: DocumentPackage -> markdown, ahead of flattening) + PACKAGE_TABLE_DROPPED: 'md/package-table-dropped', HEADING_LEVEL_CLAMPED: 'md/heading-level-clamped', ADJACENT_LINKS_MERGED: 'md/adjacent-links-merged', CODE_SPAN_AS_MONOSPACE_RUN: 'md/code-span-as-monospace-run', @@ -138,3 +140,12 @@ export class MarkdownUnsupportedDocumentKindError extends MarkdownWriteError { this.kind = kind; } } + +// Thrown by writeMarkdown when document-schema.js's own flattenPackage rejects the package it was handed -- a group carrying a style ref with no top-level styles table to resolve it against is the one case reachable for a 'wordprocessing' package (writeMarkdown's own kind check above rules out the formula/spreadsheet-specific cases flattenPackage also guards). flattenPackage itself throws a bare Error for this, which is not part of this package's own documented error hierarchy -- wrapped here so a caller catching MarkdownWriteError catches it too, rather than needing to know about a dependency's own internal exception type. +export class MarkdownPackageFlattenError extends MarkdownWriteError { + constructor(cause: unknown) { + const detail = cause instanceof Error ? cause.message : String(cause); + super('md/package-flatten-failed', `flattening the package for write failed: ${detail}`); + this.name = 'MarkdownPackageFlattenError'; + } +} diff --git a/src/footnote.test.ts b/src/footnote.test.ts index 6167d32..2bd719a 100644 --- a/src/footnote.test.ts +++ b/src/footnote.test.ts @@ -1,6 +1,6 @@ // GitHub footnotes end to end (ExaDev/markdown-codec#66): the block/inline phases that recognise `[^label]` and `[^label]: body`, the lowering that turns a definition into an `anchor` construct's boundary-marker pair (document-schema.js 4.2.0) and a reference into a marked run, and the writer that renders both back. Deliberately one file across all four stages rather than four scattered additions: the whole point of the feature is that the two halves of a footnote are carried by two DIFFERENT mechanisms and still have to reproduce each other, which no single-stage test can show. // -// The round-trip assertion below is "read -> write -> read -> write reproduces the same text", not "write reproduces the source byte for byte". That is not a weaker bar chosen for convenience: this package normalises freely on the way out (it escapes ASCII punctuation, regenerates code fences, and picks its own bullet glyph), so byte equality with arbitrary source text is not a property `writeMarkdown` has for ANY construct. What must hold, and what is asserted, is that nothing about a footnote is lost on the way through -- the second pass produces the identical document and the identical text. +// The round-trip assertion below is "read -> write -> read -> write reproduces the same text", not "write reproduces the source byte for byte". That is not a weaker bar chosen for convenience: this package normalises freely on the way out (it escapes ASCII punctuation, regenerates code fences, and picks its own bullet glyph), so byte equality with arbitrary source text is not a property `writeMarkdownContent` has for ANY construct. What must hold, and what is asserted, is that nothing about a footnote is lost on the way through -- the second pass produces the identical document and the identical text. import type { ContentBlock, ContentDocument } from 'document-schema.js'; import { PAGE_SIZE_A4 } from 'document-schema.js'; @@ -8,10 +8,10 @@ import { describe, expect, it } from 'vitest'; import { parseMarkdown } from './block/block'; import { MarkdownDiagnosticCodes, MarkdownUnbalancedConstructMarkersError } from './diagnostics/diagnostics'; import { emitMarkdown } from './emit/emit'; -import { readMarkdown } from './read'; +import { readMarkdownContent } from './read'; import { FOOTNOTE_REFERENCE_FONT_MARKER } from './shared/style-constants'; import { createDiagnosticCollector } from './test-support/diagnostics'; -import { writeMarkdown } from './write'; +import { writeMarkdownContent } from './write'; function blocksOf(document: ContentDocument): ContentBlock[] { if (document.kind !== 'wordprocessing') { @@ -21,7 +21,7 @@ function blocksOf(document: ContentDocument): ContentBlock[] { } function lowered(source: string): ContentBlock[] { - return blocksOf(readMarkdown(source).document); + return blocksOf(readMarkdownContent(source).document); } function minimalDocument(blocks: readonly ContentBlock[]): ContentDocument { @@ -30,10 +30,10 @@ function minimalDocument(blocks: readonly ContentBlock[]): ContentDocument { // One full pass through the public surface and back, twice -- see this file's own top-of-file note on why the fixed point, rather than the source text, is what a round trip is measured against here. function roundTrip(source: string): { readonly written: string; readonly rewritten: string; readonly document: ContentDocument; readonly reread: ContentDocument } { - const document = readMarkdown(source).document; - const written = writeMarkdown(document); - const reread = readMarkdown(written).document; - return { written, rewritten: writeMarkdown(reread), document, reread }; + const document = readMarkdownContent(source).document; + const written = writeMarkdownContent(document); + const reread = readMarkdownContent(written).document; + return { written, rewritten: writeMarkdownContent(reread), document, reread }; } describe('reading footnote definitions', () => { @@ -203,7 +203,7 @@ describe('lowering a footnote onto the schema', () => { it('lowers a reference to a marked run keeping its own source spelling', () => { const collector = createDiagnosticCollector(); - const document = readMarkdown('see[^1]\n\n[^1]: note', { sink: collector.sink }).document; + const document = readMarkdownContent('see[^1]\n\n[^1]: note', { sink: collector.sink }).document; expect(blocksOf(document)[0]).toEqual({ kind: 'paragraph', runs: [{ text: 'see' }, { text: '[^1]', fontFamily: FOOTNOTE_REFERENCE_FONT_MARKER }], @@ -212,7 +212,7 @@ describe('lowering a footnote onto the schema', () => { }); it('carries a reference inside a link as one run of that link', () => { - expect(blocksOf(readMarkdown('[text[^1]](/u)\n\n[^1]: note').document)[0]).toEqual({ + expect(blocksOf(readMarkdownContent('[text[^1]](/u)\n\n[^1]: note').document)[0]).toEqual({ kind: 'paragraph', runs: [{ text: 'text', hyperlink: '/u' }, { text: '[^1]', hyperlink: '/u', fontFamily: FOOTNOTE_REFERENCE_FONT_MARKER }], }); @@ -220,7 +220,7 @@ describe('lowering a footnote onto the schema', () => { it('flattens a heading inside a definition body to literal ATX text, and says so', () => { const collector = createDiagnosticCollector(); - const document = readMarkdown('[^1]: intro\n\n ## inner', { sink: collector.sink }).document; + const document = readMarkdownContent('[^1]: intro\n\n ## inner', { sink: collector.sink }).document; expect(blocksOf(document)).toEqual([ { kind: 'constructStart', descriptor: { kind: 'anchor', anchorType: 'footnote', name: '1' } }, { kind: 'paragraph', runs: [{ text: 'intro' }] }, @@ -341,6 +341,6 @@ describe('round trip', () => { it('keeps a definition body that a plain reparse would otherwise flatten into the surrounding flow', () => { const { written } = roundTrip('intro[^1]\n\n[^1]: first\n\n second\n\nafter the note'); expect(written).toBe('intro[^1]\n\n[^1]: first\n\n second\n\nafter the note'); - expect(blocksOf(readMarkdown(written).document).map((block) => block.kind)).toEqual(['paragraph', 'constructStart', 'paragraph', 'paragraph', 'constructEnd', 'paragraph']); + expect(blocksOf(readMarkdownContent(written).document).map((block) => block.kind)).toEqual(['paragraph', 'constructStart', 'paragraph', 'paragraph', 'constructEnd', 'paragraph']); }); }); diff --git a/src/gfm-conformance.test.ts b/src/gfm-conformance.test.ts index 72fb3ea..a1482b9 100644 --- a/src/gfm-conformance.test.ts +++ b/src/gfm-conformance.test.ts @@ -6,21 +6,22 @@ // // The one remaining tagged extension, `tagfilter`, is genuinely out of scope: it is an output-sanitisation pass over already-parsed raw HTML, not a parsing rule at all, and this package's read side has no HTML output to sanitise. +import { assemblePackage, flattenPackage } from 'document-schema.js'; import { describe, expect, it } from 'vitest'; import { parseMarkdown } from './block/block'; import { renderDocumentToHtml } from './html/render'; -import { readMarkdown } from './read'; +import { readMarkdownContent } from './read'; import { GFM_EXCLUSIONS } from './test-support/conformance-exclusions'; import type { SpecExample } from './test-support/spec-corpus'; import { loadGfmExtensionExamples } from './test-support/spec-corpus'; -import { writeMarkdown } from './write'; +import { writeMarkdown, writeMarkdownContent } from './write'; const GFM_EXTENSIONS = ['table', 'strikethrough', 'autolink', 'disabled']; // read -> write -> reparse -> render, all through this package's real public surface -- the identical bar src/conformance.test.ts holds the CommonMark corpus to, applied here to the GFM extensions (all four toggles default on, matching this package's own CommonMark+GFM target). See that file's own top-of-file note for the full rationale. function render(example: SpecExample): string { - const { document } = readMarkdown(example.markdown); - const rewritten = writeMarkdown(document); + const { document } = readMarkdownContent(example.markdown); + const rewritten = writeMarkdownContent(document); return renderDocumentToHtml(parseMarkdown(rewritten).document); } @@ -41,4 +42,12 @@ describe.each(GFM_EXTENSIONS)('GFM %s extension conformance', (extension) => { it.each(excluded.map((example) => [`example ${String(example.example)} (${example.section})`, example] as const))('excluded %s still fails', (_name, example) => { expect(render(example)).not.toBe(example.html); }); + + // The tree pair's own two properties (src/package.test.ts's (i) and (ii)), re-checked over this extension's own tagged examples -- mirroring src/conformance.test.ts's own "tree pair matches the flat pair" block, extended here to the GFM extensions (all four toggles on, matching this suite's own default read options). + it.each(examples.map((example) => [`example ${String(example.example)} (${example.section})`, example] as const))('%s: flattenPackage(assemblePackage(document)) reproduces document, and writeMarkdown renders what writeMarkdownContent renders', (_name, example) => { + const { document } = readMarkdownContent(example.markdown); + + expect(flattenPackage(assemblePackage(document))).toEqual(document); + expect(writeMarkdown(assemblePackage(document))).toBe(writeMarkdownContent(document)); + }); }); diff --git a/src/index.ts b/src/index.ts index a340a87..666bbbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,17 @@ // Public barrel. May contain only re-export statements (enforced by local/no-side-effects-in-index, eslint.config.ts) -- nothing here can have a side effect at import time. // -// src/html/render.ts (the CommonMark-HTML conformance oracle) and src/ast/* (this package's own internal AST) are deliberately NOT re-exported here -- both are pipeline-internal: readMarkdown/writeMarkdown convert between markdown text and document-schema.js's ContentDocument, never HTML or a raw AST, and exposing either would invite a caller to reach for the wrong thing rather than genuinely offering two competing outputs. +// src/html/render.ts (the CommonMark-HTML conformance oracle) and src/ast/* (this package's own internal AST) are deliberately NOT re-exported here -- both are pipeline-internal: readMarkdown/writeMarkdown convert between markdown text and document-schema.js's own document encodings, never HTML or a raw AST, and exposing either would invite a caller to reach for the wrong thing rather than genuinely offering two competing outputs. +// The primary read/write pair, over document-schema.js's tree-form DocumentPackage -- what a caller reaching for "read a markdown file" or "write one" should use. The *Content pair below is the same conversion one level down, over the flat ContentDocument the lower/emit pipeline itself builds; see src/read.ts's own top-of-file comment for why both exist and which to reach for. export type { ReadMarkdownResult } from './read'; export { readMarkdown } from './read'; export { writeMarkdown } from './write'; -export { markdownCodec, MarkdownBytesSchema } from './codec'; +export { markdownCodec } from './codec'; + +export type { ReadMarkdownContentResult } from './read'; +export { readMarkdownContent } from './read'; +export { writeMarkdownContent } from './write'; +export { markdownContentCodec, MarkdownBytesSchema } from './codec'; export type { MarkdownDiagnostic, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink } from './diagnostics/diagnostics'; export { diff --git a/src/package.test.ts b/src/package.test.ts new file mode 100644 index 0000000..348fc35 --- /dev/null +++ b/src/package.test.ts @@ -0,0 +1,269 @@ +// The tree-form half of the public surface: readMarkdown/writeMarkdown/markdownCodec over document-schema.js's DocumentPackage, and the three properties that make them trustworthy as the primary entry points. +// +// (i) They are exactly assemblePackage/flattenPackage composed onto the flat pair -- pinned by constructing the same value both ways, so a future edit that swapped assemblePackage for bare decompose (dropping the styles-minting pass) or forgot to flatten before emitting would fail here rather than silently changing what callers get. (ii) The transform is transparent to the markdown itself: the tree pair renders byte-identical text to the flat pair over real multi-construct content, which is what lets src/conformance.test.ts keep measuring the flat pair alone and still speak for both. (iii) Bytes survive a full round trip through the tree: decode -> encode -> decode reproduces the identical package, and the re-encoded bytes still carry the document's real content rather than an empty-but-valid shell. +// +// The blockquote fixture below is not decorative: two blockquote paragraphs share an indentLeftPt tuple, which is the one construct this package's lowering produces that assemblePackage's minting actually hoists onto a styles-table entry. It is the case where "assemblePackage" and "decompose plus an envelope" produce genuinely different values, so it is the case that proves which one readMarkdown calls. + +import { assemblePackage, DocumentPackageSchema, flattenPackage, isPackageGroup, isSectionConstructGroupNode, type DocumentPackage, type SectionConstructGroupNode } from 'document-schema.js'; +import { z } from 'zod'; +import { describe, expect, it } from 'vitest'; +import { markdownCodec, markdownContentCodec } from './codec'; +import { MarkdownDiagnosticCodes, MarkdownPackageFlattenError, MarkdownUnsupportedDocumentKindError } from './diagnostics/diagnostics'; +import { readMarkdown, readMarkdownContent } from './read'; +import { writeMarkdown, writeMarkdownContent } from './write'; + +// Every construct the lower/emit pair maps differently -- headings, a nested-paragraph blockquote, both list kinds, a GFM table, inline emphasis and a link, and a footnote whose definition rides a constructStart/constructEnd pair (the one block shape decompose promotes to a group of its own). +const SAMPLE = [ + '# Title', + '', + 'Some **bold** and *italic* text with [a link](https://example.com) and `code`.', + '', + '## Section two', + '', + '- alpha', + '- beta', + '', + '1. one', + '2. two', + '', + '| a | b |', + '| - | - |', + '| 1 | 2 |', + '', + 'Body with a footnote[^1].', + '', + '[^1]: The note body.', + '', +].join('\n'); + +// Two blockquote paragraphs sharing one indentLeftPt tuple -- the minting case, see this file's own top-of-file note. +const BLOCKQUOTED = '> Quoted one.\n>\n> Quoted two.\n\n> Quoted three.\n>\n> Quoted four.\n'; + +const SAMPLE_BYTES = new TextEncoder().encode(SAMPLE); + +// SAMPLE above carries exactly one footnote, in the simplest possible shape (a single-paragraph body straight after the reference). The construct-group path is the one genuinely new structural shape decompose promotes over a bare {metadata, sections} envelope, so it gets its own fixture set here, one shape per case that the parser and lowerer treat differently (src/footnote.test.ts pins each at the flat ContentDocument level; these same shapes are exercised here at the tree level instead). +const FOOTNOTE_SHAPES = { + bodyless: 'Body[^1].\n\n[^1]:\n', + duplicateLabel: 'Body[^1] and[^1] again.\n\n[^1]: first\n\n[^1]: second\n', + multiParagraphBody: 'Body[^1].\n\n[^1]: One.\n\n Two.\n\n Three.\n', + afterList: 'Body[^1].\n\n- a\n- b\n\n[^1]: note\n', + afterBlockquote: 'Body[^1].\n\n> quoted\n\n[^1]: note\n', + afterHeading: '# Heading\n\nBody[^1].\n\n[^1]: note\n', +} as const; + +// Construct groups sit wherever their marker pair sat in the block flow, which for a footnote definition following a heading is inside that heading's own group rather than at the section's top level -- so this walks the whole subtree rather than filtering one children array. +function collectConstructGroups(node: unknown): SectionConstructGroupNode[] { + if (!isPackageGroup(node)) return []; + const here = isSectionConstructGroupNode(node) ? [node] : []; + return [...here, ...node.children.flatMap(collectConstructGroups)]; +} + +describe('readMarkdown: markdown text -> DocumentPackage', () => { + it('produces a schema-valid wordprocessing package with one section group per lowered section', () => { + const { documentPackage } = readMarkdown(SAMPLE); + const { document } = readMarkdownContent(SAMPLE); + if (document.kind !== 'wordprocessing') throw new Error('markdown lowers to wordprocessing content'); + + expect(DocumentPackageSchema.safeParse(documentPackage).success).toBe(true); + expect(documentPackage.kind).toBe('wordprocessing'); + expect(documentPackage.children).toHaveLength(document.sections.length); + }); + + it('promotes the footnote definition to a construct group carrying its own anchor descriptor', () => { + const { documentPackage } = readMarkdown(SAMPLE); + const constructGroups = documentPackage.children.flatMap(collectConstructGroups); + + expect(constructGroups).toHaveLength(1); + expect(constructGroups[0]?.node).toMatchObject({ kind: 'anchor', anchorType: 'footnote', name: '1' }); + }); + + it('is assemblePackage composed onto readMarkdownContent, minting included', () => { + for (const source of [SAMPLE, BLOCKQUOTED]) { + expect(readMarkdown(source).documentPackage).toEqual(assemblePackage(readMarkdownContent(source).document)); + } + }); + + it('mints a styles table for repeated paragraph properties rather than leaving the tree unfactored', () => { + const { documentPackage } = readMarkdown(BLOCKQUOTED); + + expect(documentPackage.styles).toBeDefined(); + expect(Object.values(documentPackage.styles ?? {})).toContainEqual({ paragraph: { indentLeftPt: 36 } }); + }); + + it('flattens back to exactly the document readMarkdownContent produces', () => { + for (const source of [SAMPLE, BLOCKQUOTED]) { + expect(flattenPackage(readMarkdown(source).documentPackage)).toEqual(readMarkdownContent(source).document); + } + }); + + it('reports the same diagnostics as readMarkdownContent, through the return value and the caller sink alike', () => { + const seen: string[] = []; + const { diagnostics } = readMarkdown(SAMPLE, { sink: (diagnostic) => seen.push(diagnostic.code) }); + + expect(diagnostics.map((diagnostic) => diagnostic.code)).toEqual(readMarkdownContent(SAMPLE).diagnostics.map((diagnostic) => diagnostic.code)); + expect(seen).toEqual(diagnostics.map((diagnostic) => diagnostic.code)); + expect(seen).toContain(MarkdownDiagnosticCodes.INVENTED_PAGE_GEOMETRY); + }); + + it('throws an already-aborted signal before parsing', () => { + expect(() => readMarkdown(SAMPLE, { signal: AbortSignal.abort() })).toThrow(); + }); +}); + +describe('writeMarkdown: DocumentPackage -> markdown text', () => { + it('renders byte-identical text to writeMarkdownContent over the flat document', () => { + for (const source of [SAMPLE, BLOCKQUOTED]) { + expect(writeMarkdown(readMarkdown(source).documentPackage)).toBe(writeMarkdownContent(readMarkdownContent(source).document)); + } + }); + + it('round-trips text -> package -> text -> package to the identical package and text', () => { + const first = readMarkdown(SAMPLE).documentPackage; + const written = writeMarkdown(first); + const second = readMarkdown(written).documentPackage; + + expect(second).toEqual(first); + expect(writeMarkdown(second)).toBe(written); + }); + + it('carries every source construct through the round trip rather than emitting a valid-but-empty document', () => { + const written = writeMarkdown(readMarkdown(SAMPLE).documentPackage); + + expect(written).toContain('# Title'); + expect(written).toContain('## Section two'); + expect(written).toContain('[a link](https://example.com)'); + expect(written).toContain('| a | b |'); + // The trailing full stop comes back escaped (`body\.`) -- this package escapes ASCII punctuation on emit -- so the assertion stops at the last unescaped character rather than pinning an escape this test has no opinion about. + expect(written).toContain('[^1]: The note body'); + }); + + it('honours the same write-side style options the flat writer takes', () => { + const written = writeMarkdown(readMarkdown('- alpha\n- beta\n').documentPackage, { bulletListMarker: '*' }); + + expect(written).toContain('* alpha'); + }); + + it('throws MarkdownUnsupportedDocumentKindError for a package whose kind markdown cannot represent', () => { + const spreadsheet = assemblePackage({ kind: 'spreadsheet', metadata: {}, sheets: [] }); + + expect(() => writeMarkdown(spreadsheet)).toThrow(MarkdownUnsupportedDocumentKindError); + }); + + it('throws MarkdownUnsupportedDocumentKindError, not a bare Error, for a formula package with no formula node -- flattenPackage has its own single-ContentFormula-node constraint for this kind that this check pre-empts entirely', () => { + // Hand-built rather than routed through assemblePackage: assemblePackage(ContentDocument) always produces exactly one formula node for a 'formula' document, so this empty-children shape (the one flattenPackage itself rejects) can only arise from a caller constructing a DocumentPackage directly. + const formula: DocumentPackage = { kind: 'formula', metadata: {}, children: [] }; + + expect(() => writeMarkdown(formula)).toThrow(MarkdownUnsupportedDocumentKindError); + }); + + it('throws an already-aborted signal before flattening', () => { + const documentPackage = readMarkdown(SAMPLE).documentPackage; + + expect(() => writeMarkdown(documentPackage, { signal: AbortSignal.abort() })).toThrow(); + }); + + it('wraps flattenPackage\'s own bare Error as MarkdownPackageFlattenError when a group carries a style ref the package has no styles table to resolve', () => { + // A minimal, hand-built reproduction of the one flattenPackage failure reachable for a 'wordprocessing' package: the section group below still references its minted style, but the package's own top-level styles table has been stripped out from under it. + const { styles, ...packageWithoutStyles } = readMarkdown(BLOCKQUOTED).documentPackage; + expect(styles).toBeDefined(); + + expect(() => writeMarkdown(packageWithoutStyles)).toThrow(MarkdownPackageFlattenError); + expect(() => writeMarkdown(packageWithoutStyles)).toThrow(/style ref/); + }); + + it('reports a PACKAGE_TABLE_DROPPED diagnostic per non-empty package-level table flattenPackage cannot carry into markdown', () => { + const base = readMarkdown(SAMPLE).documentPackage; + const withExtraTables = { + ...base, + definitions: { d1: { kind: 'bookmark' } }, + layers: { l1: { kind: 'layer' } }, + attachments: { a1: { kind: 'file' } }, + destinations: { dest1: { kind: 'anchor' } }, + pages: [{ widthPt: 100, heightPt: 100 }], + }; + + const seen: string[] = []; + writeMarkdown(withExtraTables, { sink: (diagnostic) => seen.push(diagnostic.code) }); + + expect(seen.filter((code) => code === MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED)).toHaveLength(5); + }); + + it('reports nothing extra, and renders identically, for a package that carries none of those tables', () => { + const base = readMarkdown(SAMPLE).documentPackage; + const seen: string[] = []; + + const written = writeMarkdown(base, { sink: (diagnostic) => seen.push(diagnostic.code) }); + + expect(seen.filter((code) => code === MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED)).toHaveLength(0); + expect(written).toBe(writeMarkdown(base)); + }); +}); + +describe('the construct-group path over footnote shapes beyond SAMPLE\'s single case', () => { + for (const [name, source] of Object.entries(FOOTNOTE_SHAPES)) { + it(`${name}: readMarkdown is assemblePackage(readMarkdownContent(...).document), and flattens back to it exactly`, () => { + const { documentPackage } = readMarkdown(source); + const { document } = readMarkdownContent(source); + + expect(documentPackage).toEqual(assemblePackage(document)); + expect(flattenPackage(documentPackage)).toEqual(document); + }); + + it(`${name}: writeMarkdown renders byte-identical text to writeMarkdownContent`, () => { + const { documentPackage } = readMarkdown(source); + const { document } = readMarkdownContent(source); + + expect(writeMarkdown(documentPackage)).toBe(writeMarkdownContent(document)); + }); + } + + it('promotes exactly one construct group per definition, including both definitions sharing a duplicated label', () => { + expect(readMarkdown(FOOTNOTE_SHAPES.bodyless).documentPackage.children.flatMap(collectConstructGroups)).toHaveLength(1); + expect(readMarkdown(FOOTNOTE_SHAPES.multiParagraphBody).documentPackage.children.flatMap(collectConstructGroups)).toHaveLength(1); + expect(readMarkdown(FOOTNOTE_SHAPES.afterList).documentPackage.children.flatMap(collectConstructGroups)).toHaveLength(1); + expect(readMarkdown(FOOTNOTE_SHAPES.afterBlockquote).documentPackage.children.flatMap(collectConstructGroups)).toHaveLength(1); + expect(readMarkdown(FOOTNOTE_SHAPES.afterHeading).documentPackage.children.flatMap(collectConstructGroups)).toHaveLength(1); + expect(readMarkdown(FOOTNOTE_SHAPES.duplicateLabel).documentPackage.children.flatMap(collectConstructGroups)).toHaveLength(2); + }); + + it('lowers a bodyless definition to a construct group with no body blocks between its start and end', () => { + const [group] = readMarkdown(FOOTNOTE_SHAPES.bodyless).documentPackage.children.flatMap(collectConstructGroups); + + expect(group?.children).toEqual([]); + }); +}); + +describe('markdownCodec: bytes <-> DocumentPackage', () => { + it('decodes real bytes to a package and encodes it back to bytes carrying the same content', () => { + const documentPackage = z.decode(markdownCodec, SAMPLE_BYTES); + expect(documentPackage.kind).toBe('wordprocessing'); + + const encoded = z.encode(markdownCodec, documentPackage); + expect(encoded).toBeInstanceOf(Uint8Array); + + const text = new TextDecoder().decode(encoded); + expect(text).toContain('# Title'); + expect(text).toContain('| a | b |'); + }); + + it('round-trips bytes -> package -> bytes -> package to the identical package and bytes', () => { + const first = z.decode(markdownCodec, SAMPLE_BYTES); + const encoded = z.encode(markdownCodec, first); + const second = z.decode(markdownCodec, encoded); + + expect(second).toEqual(first); + expect(z.encode(markdownCodec, second)).toEqual(encoded); + }); + + it('rejects bytes that are not well-formed UTF-8', () => { + expect(() => z.decode(markdownCodec, new Uint8Array([0xff, 0xfe, 0xfd]))).toThrow(); + }); + + it('decodes to the tree form where markdownContentCodec decodes to the flat form, over the same bytes', () => { + const documentPackage = z.decode(markdownCodec, SAMPLE_BYTES); + const document = z.decode(markdownContentCodec, SAMPLE_BYTES); + + expect(flattenPackage(documentPackage)).toEqual(document); + expect(z.encode(markdownCodec, documentPackage)).toEqual(z.encode(markdownContentCodec, document)); + }); +}); diff --git a/src/read.ts b/src/read.ts index 6720cc9..fb17206 100644 --- a/src/read.ts +++ b/src/read.ts @@ -1,27 +1,44 @@ -// readMarkdown: markdown source text -> ContentDocument. +// The read side of this package's public surface, in both of the two encodings document-schema.js states for one document: readMarkdown produces the tree-form DocumentPackage (the primary entry point), readMarkdownContent produces the flat ContentDocument the whole lower pipeline actually builds. // -// RECONCILIATION DECISION (recorded here per the scaffolding task that created this file): readMarkdown/writeMarkdown operate on document-schema.js's full ContentDocument directly (kind/metadata/sections), not a bare {metadata, sections} shape wrapped by a documents.js-side adapter. The envelope this decision was recorded against carried a formatVersion field per arm; document-schema.js 4.0.0 retired it, and the full-envelope-vs-bare-shape fork the decision documents is unchanged by that. +// Why two: document-schema.js owns both encodings and the structural transform between them (decompose/assemblePackage in that direction, flattenPackage back), and its own barrel names assemblePackage "the one helper a construction site calls". A codec IS a construction site -- it is where a document first comes into existence from bytes -- so the tree is what this package hands a caller by default, and the flat form is what the pipeline internally produces on the way there. Naming follows ooxml.js's readXlsx/readXlsxContent pair: the unsuffixed name is the one a caller should reach for, the `Content` suffix names the flat constituent underneath it. +// +// Which composition the tree side uses: assemblePackage, not bare decompose. decompose alone returns only the children array (PackageChildren), leaving the envelope splice and the styles-minting pass to the caller; assemblePackage is decompose + envelope + factorStyles in one, and is what documents.js's own conversion sites call for every onDocument payload. There is no `pages` argument here because markdown has no layout stage at all -- no page geometry is ever rendered, matching the layoutless bridge conversions in documents.js that likewise call assemblePackage with content only. +// +// RECONCILIATION DECISION (recorded here per the scaffolding task that created this file): readMarkdownContent/writeMarkdownContent operate on document-schema.js's full ContentDocument directly (kind/metadata/sections), not a bare {metadata, sections} shape wrapped by a documents.js-side adapter. The envelope this decision was recorded against carried a formatVersion field per arm; document-schema.js 4.0.0 retired it, and the full-envelope-vs-bare-shape fork the decision documents is unchanged by that. // // Reasoning, from the two precedents this family's own sibling packages already established for exactly this fork: // // - odf.js's readOdt/readOdp/readOds/readOdg (consumed by documents.js's src/odf/*/read.ts thin adapters) each return a bare {metadata, sections|slides|sheets|pages} shape. documents.js's own src/odf/odt/read.ts wraps that bare shape into the ContentDocument envelope itself (adding kind/formatVersion) before handing it to the shared layout engine. // - ooxml.js's readXlsxContent (src/typed/xlsx/content.ts) is the newer, and more recently reasoned-about, sibling precedent -- and it deliberately does NOT follow readOds's bare-shape convention. Its own top-of-file comment states the reasoning explicitly: "Unlike readOds, this returns a full ContentDocument envelope directly (kind/formatVersion/metadata/sheets) rather than a bare {metadata, sheets} shape -- readXlsxContent and typed/xlsx/build.ts's buildXlsxPackage are designed as a matched read/write pair around ContentDocument specifically, so a caller can round-trip readXlsxContent(buildXlsxPackage(x)) without an extra wrapping/unwrapping step, and documents.js's own future ods<->xlsx bridge ... can treat this reader's own output as an already-correctly-shaped pivot value." buildXlsxPackage's own entry point (src/typed/xlsx/build.ts) mirrors this: it accepts a full ContentDocument and throws outright if `document.kind !== 'spreadsheet'`, rather than accepting a bare {metadata, sheets} value a caller would need to wrap first. // -// readXlsxContent/buildXlsxPackage is the more recent design decision in this ecosystem and the one built specifically to solve the "does a bridge-style reader need its own wrapping step" problem markdown-codec faces here -- markdown-codec has no PDF-pivot layout stage of its own (there is no "markdown page layout" concept the way docx/pptx/odt/odp have one), so it is structurally closer to the xlsx<->ods PDF-bypassing bridge case than to the odt/odp/ods/odg PDF-pivot case odf.js's bare-shape convention was built for. Following readXlsxContent's own shape means a future documents.js-side markdownToDocx/docxToMarkdown-style bridge (or any other caller composing readMarkdown directly with a ContentDocument-consuming builder) never needs an extra wrap/unwrap step either -- readMarkdown(writeMarkdown(x)) round-trips through the identical envelope shape with nothing lost or added in between. +// readXlsxContent/buildXlsxPackage is the more recent design decision in this ecosystem and the one built specifically to solve the "does a bridge-style reader need its own wrapping step" problem markdown-codec faces here -- markdown-codec has no PDF-pivot layout stage of its own (there is no "markdown page layout" concept the way docx/pptx/odt/odp have one), so it is structurally closer to the xlsx<->ods PDF-bypassing bridge case than to the odt/odp/ods/odg PDF-pivot case odf.js's bare-shape convention was built for. Following readXlsxContent's own shape means a caller composing readMarkdownContent directly with a ContentDocument-consuming builder never needs an extra wrap/unwrap step either -- readMarkdownContent(writeMarkdownContent(x)) round-trips through the identical envelope shape with nothing lost or added in between. // -// Wiring: readMarkdown is a thin wrapper over src/lower/lower.ts's lowerMarkdown (front matter extraction, block parsing, and lowering, already composed there over raw text) -- the pipeline src/lower/lower.ts's own top-of-file table documents in full: src/scan (tokenize) -> src/block (block structure) -> src/inline (inline content, deferred per block) -> src/lower (AST -> ContentDocument), with src/html and src/image consulted by src/block/src/inline respectively. What this module itself adds: collecting every diagnostic lowerMarkdown reports into the returned `diagnostics` array (in addition to forwarding each one to a caller-supplied sink, exactly like it would see them calling lowerMarkdown directly), and a single AbortSignal check up front -- the parser is synchronous and single-pass, so there is no natural mid-parse checkpoint to check again later, matching this package's own "identity, clock, and observability are first-class ports" convention without overclaiming incremental cancellation it cannot actually provide. +// Wiring: readMarkdownContent is a thin wrapper over src/lower/lower.ts's lowerMarkdown (front matter extraction, block parsing, and lowering, already composed there over raw text) -- the pipeline src/lower/lower.ts's own top-of-file table documents in full: src/scan (tokenize) -> src/block (block structure) -> src/inline (inline content, deferred per block) -> src/lower (AST -> ContentDocument), with src/html and src/image consulted by src/block/src/inline respectively. What this module itself adds: collecting every diagnostic lowerMarkdown reports into the returned `diagnostics` array (in addition to forwarding each one to a caller-supplied sink, exactly like it would see them calling lowerMarkdown directly), and a single AbortSignal check up front -- the parser is synchronous and single-pass, so there is no natural mid-parse checkpoint to check again later, matching this package's own "identity, clock, and observability are first-class ports" convention without overclaiming incremental cancellation it cannot actually provide. -import type { ContentDocument } from 'document-schema.js'; +import { assemblePackage, type ContentDocument, type DocumentPackage } from 'document-schema.js'; import type { MarkdownDiagnostic } from './diagnostics/diagnostics'; import { lowerMarkdown } from './lower/lower'; import type { ReadMarkdownOptions } from './options/options'; +// The field is `documentPackage` rather than the bare noun `package`: `package` is a reserved word in strict mode, so `const { package } = readMarkdown(src)` -- the idiom every caller reaches for first -- is a syntax error, and the only spellings that work are an alias or a property access. A name whose obvious use is illegal is the wrong name. export interface ReadMarkdownResult { + readonly documentPackage: DocumentPackage; + readonly diagnostics: readonly MarkdownDiagnostic[]; +} + +export interface ReadMarkdownContentResult { readonly document: ContentDocument; readonly diagnostics: readonly MarkdownDiagnostic[]; } +// Markdown source text -> the tree-form DocumentPackage. The diagnostics are the identical set readMarkdownContent collects -- the tree transform reports none of its own, since decomposition and minting are pure structure over content the lower pipeline has already finished producing. export function readMarkdown(text: string, options: ReadMarkdownOptions = {}): ReadMarkdownResult { + const { document, diagnostics } = readMarkdownContent(text, options); + return { documentPackage: assemblePackage(document), diagnostics }; +} + +// Markdown source text -> the flat ContentDocument, without the tree transform. The form documents.js's own conversion pipeline consumes, and the level to work at when composing a package boundary by hand (decompose/flattenPackage rather than assemblePackage) or feeding a ContentDocument-consuming builder directly. +export function readMarkdownContent(text: string, options: ReadMarkdownOptions = {}): ReadMarkdownContentResult { options.signal?.throwIfAborted(); const diagnostics: MarkdownDiagnostic[] = []; diff --git a/src/write.ts b/src/write.ts index 038ec9c..222deed 100644 --- a/src/write.ts +++ b/src/write.ts @@ -1,12 +1,58 @@ -// writeMarkdown: ContentDocument -> markdown source text. The build-side counterpart to src/read.ts's readMarkdown -- see that file's own top-of-file comment for the full-ContentDocument-vs-bare-shape reconciliation decision both sides of this pair follow. +// The write side of this package's public surface, mirroring src/read.ts's two encodings: writeMarkdown accepts the tree-form DocumentPackage (the primary entry point), writeMarkdownContent accepts the flat ContentDocument src/emit actually renders. See src/read.ts's own top-of-file comment for the naming convention (ooxml.js's readXlsx/readXlsxContent pair) and the full-ContentDocument-vs-bare-shape reconciliation decision both sides of this pair follow. // -// A thin wrapper over src/emit/emit.ts's emitMarkdown, mirroring ooxml.js's buildXlsxPackage(document: ContentDocument) signature rather than odf.js's bare-shape build*Package equivalents -- emitMarkdown already accepts WriteMarkdownOptions directly (src/options/options.ts), so there is no options-shape reconciliation needed on this side the way src/read.ts needed for src/lower/lower.ts's own options. Throws MarkdownUnsupportedDocumentKindError (src/diagnostics/diagnostics.ts) for a non-'wordprocessing' ContentDocument -- emitMarkdown's own responsibility, not reimplemented here. +// The tree side is flattenPackage then the flat writer: flattening materialises every style ref away into direct properties, which is exactly what src/emit needs -- it reads a paragraph's own properties and this package's own styleId vocabulary (src/shared/style-constants.ts), and has no notion of a package-level styles table to resolve against. flattenPackage is document-schema.js's own stated inverse of the assemblePackage src/read.ts's readMarkdown calls, so writeMarkdown(readMarkdown(text).documentPackage) renders exactly what writeMarkdownContent(readMarkdownContent(text).document) does. +// +// writeMarkdownContent is a thin wrapper over src/emit/emit.ts's emitMarkdown, mirroring ooxml.js's buildXlsxPackage(document: ContentDocument) signature rather than odf.js's bare-shape build*Package equivalents -- emitMarkdown already accepts WriteMarkdownOptions directly (src/options/options.ts), so there is no options-shape reconciliation needed on this side the way src/read.ts needed for src/lower/lower.ts's own options. Throws MarkdownUnsupportedDocumentKindError (src/diagnostics/diagnostics.ts) for a non-'wordprocessing' ContentDocument -- emitMarkdown's own responsibility, not reimplemented here. +// +// writeMarkdown checks the package's own `kind` itself, ahead of flattening, rather than letting a non-'wordprocessing' package reach emitMarkdown's own check by way of flattenPackage preserving `kind`: flattenPackage has kind-specific validation of its own (a 'formula' package's single-ContentFormula-node constraint, a 'spreadsheet' sheet group's own "no style ref" constraint) that can throw a bare Error before ever reaching that check, for a package this function was never going to accept anyway. Checking first means every non-'wordprocessing' package reaches the identical, correctly-typed MarkdownUnsupportedDocumentKindError, regardless of what else is wrong with it. +// +// flattenPackage's own envelope (document-schema.js's dist/flatten.js) carries forward only `metadata` and, when present, `symbolTable` -- a DocumentPackage's `definitions`/`layers`/`attachments`/`destinations`/`pages` tables have no flat-ContentDocument home to land in and are silently absent from its return value. This matters here specifically because the tree form exists for interop with packages ooxml.js/odf.js/documents.js produce, which DO populate those tables (a docx footnote/comment table, page geometry) -- reportDroppedPackageTables below turns that structural gap into a MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED diagnostic per non-empty table, matching this package's own "every mapping gap reports through the sink as a stable code" contract, rather than leaving it silent the way a bare pass-through to flattenPackage would. +// +// The remaining way flattenPackage can throw for a 'wordprocessing' package -- a heading/list group carrying a style ref with no top-level styles table to resolve it against -- is not something writeMarkdown can rule out with a cheap up-front check the way the kind mismatch above is, so it is caught and rewrapped as MarkdownPackageFlattenError instead: a bare Error from a dependency is not part of this package's own MarkdownWriteError hierarchy, and a caller catching that hierarchy around this entry point should not need to know flattenPackage's own exception type to catch it. -import type { ContentDocument } from 'document-schema.js'; +import { flattenPackage, type ContentDocument, type DocumentPackage } from 'document-schema.js'; +import { MarkdownDiagnosticCodes, MarkdownPackageFlattenError, MarkdownUnsupportedDocumentKindError, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from './diagnostics/diagnostics'; +import type { MarkdownDiagnosticSink } from './diagnostics/diagnostics'; import { emitMarkdown } from './emit/emit'; import type { WriteMarkdownOptions } from './options/options'; -export function writeMarkdown(document: ContentDocument, options: WriteMarkdownOptions = {}): string { +// Reports one PACKAGE_TABLE_DROPPED diagnostic per non-empty package-level table flattenPackage's own envelope does not carry into the ContentDocument it returns. Named individually (rather than one diagnostic for "some table was dropped") so a caller's sink can tell which table's data it needs to have captured before calling writeMarkdown, the same granularity every other degrade-tier code in this package already gives. +function reportDroppedPackageTables(documentPackage: DocumentPackage, sink: MarkdownDiagnosticSink): void { + const tables: readonly (readonly [name: string, present: boolean])[] = [ + ['definitions', documentPackage.definitions !== undefined && Object.keys(documentPackage.definitions).length > 0], + ['layers', documentPackage.layers !== undefined && Object.keys(documentPackage.layers).length > 0], + ['attachments', documentPackage.attachments !== undefined && Object.keys(documentPackage.attachments).length > 0], + ['destinations', documentPackage.destinations !== undefined && Object.keys(documentPackage.destinations).length > 0], + ['pages', documentPackage.pages !== undefined && documentPackage.pages.length > 0], + ]; + for (const [name, present] of tables) { + if (!present) continue; + sink({ + code: MarkdownDiagnosticCodes.PACKAGE_TABLE_DROPPED, + severity: 'info', + message: `the package's own "${name}" table has no markdown representation; flattenPackage's envelope carries forward only metadata and symbolTable, so "${name}" is dropped rather than rendered`, + }); + } +} + +// The tree-form DocumentPackage -> markdown source text. The abort check is here as well as in writeMarkdownContent because flattening is real work done before that delegated check would run, and an already-aborted call should not perform it. +export function writeMarkdown(documentPackage: DocumentPackage, options: WriteMarkdownOptions = {}): string { + options.signal?.throwIfAborted(); + if (documentPackage.kind !== 'wordprocessing') throw new MarkdownUnsupportedDocumentKindError(documentPackage.kind); + + reportDroppedPackageTables(documentPackage, options.sink ?? NOOP_MARKDOWN_DIAGNOSTIC_SINK); + + let flattened: ContentDocument; + try { + flattened = flattenPackage(documentPackage); + } catch (error) { + throw new MarkdownPackageFlattenError(error); + } + return writeMarkdownContent(flattened, options); +} + +// The flat ContentDocument -> markdown source text, without the tree transform. The level documents.js's own conversion pipeline writes at, and the one to use when a caller already holds flat content rather than a package. +export function writeMarkdownContent(document: ContentDocument, options: WriteMarkdownOptions = {}): string { options.signal?.throwIfAborted(); return emitMarkdown(document, options); } diff --git a/test/smoke.test.mjs b/test/smoke.test.mjs index 5176e24..8268ffd 100644 --- a/test/smoke.test.mjs +++ b/test/smoke.test.mjs @@ -1,6 +1,6 @@ // Smoke test: the built dist/ artifact loads and works under both ESM and CJS. Run only via `pnpm test:smoke` (tsdown, then vitest scoped to this file by vitest.config.ts's "smoke" project) -- never part of the default `pnpm test` file set, since it requires a fresh build to mean anything. // -// Now that readMarkdown/writeMarkdown/markdownCodec are real (src/read.ts/src/write.ts/src/codec.ts), this follows pdf-codec's own smoke.test.mjs shape: a representative slice of the public surface checked for presence in both builds, then real read -> write -> reparse assertions run against each build independently, proving the built artifact itself (not just the source under vitest's own transform) round-trips real markdown through readMarkdown/writeMarkdown and through the z.codec() pair. +// This follows pdf-codec's own smoke.test.mjs shape: a representative slice of the public surface checked for presence in both builds, then real read -> write -> reparse assertions run against each build independently, proving the built artifact itself (not just the source under vitest's own transform) round-trips real markdown. Both encodings are exercised -- the tree-form readMarkdown/writeMarkdown/markdownCodec trio over document-schema.js's DocumentPackage, and the flat readMarkdownContent/writeMarkdownContent/markdownContentCodec trio over its ContentDocument -- because the tree pair pulls document-schema.js's own decompose/factorStyles/flattenPackage into the bundle, and a dual-build failure confined to that dependency would be invisible to a flat-only check. import { createRequire } from 'node:module'; import { z } from 'zod'; import { describe, expect, it } from 'vitest'; @@ -10,8 +10,8 @@ const require = createRequire(import.meta.url); const cjs = require('../dist/index.cjs'); // A representative slice of the public surface, not an exhaustive list -- enough to catch a genuinely broken dual build without duplicating src/index.ts's own export list here. Error classes are real invocable functions at runtime (typeof === 'function'), so they're checked here alongside ordinary functions rather than in OBJECTS below. -const FUNCTIONS = ['readMarkdown', 'writeMarkdown', 'NOOP_MARKDOWN_DIAGNOSTIC_SINK', 'MarkdownParseError', 'MarkdownWriteError', 'MarkdownUnsupportedDocumentKindError', 'MarkdownInvalidUtf8Error', 'MarkdownInputTooLargeError', 'MarkdownNestingLimitExceededError']; -const OBJECTS = ['markdownCodec', 'MarkdownBytesSchema', 'MarkdownDiagnosticCodes']; +const FUNCTIONS = ['readMarkdown', 'writeMarkdown', 'readMarkdownContent', 'writeMarkdownContent', 'NOOP_MARKDOWN_DIAGNOSTIC_SINK', 'MarkdownParseError', 'MarkdownWriteError', 'MarkdownUnsupportedDocumentKindError', 'MarkdownInvalidUtf8Error', 'MarkdownInputTooLargeError', 'MarkdownNestingLimitExceededError']; +const OBJECTS = ['markdownCodec', 'markdownContentCodec', 'MarkdownBytesSchema', 'MarkdownDiagnosticCodes']; describe('dist/ exports are present in both builds', () => { for (const name of FUNCTIONS) { @@ -36,40 +36,73 @@ describe.each([ ['CJS', cjs], ])('%s artifact behaviour', (_label, api) => { describe('readMarkdown then writeMarkdown', () => { + it('lowers real markdown to a wordprocessing DocumentPackage and renders it back to markdown', () => { + const { documentPackage, diagnostics } = api.readMarkdown(SAMPLE_MARKDOWN); + expect(documentPackage.kind).toBe('wordprocessing'); + expect(documentPackage.children.length).toBeGreaterThan(0); + expect(Array.isArray(diagnostics)).toBe(true); + + const rewritten = api.writeMarkdown(documentPackage); + expect(rewritten).toContain('# Title'); + expect(rewritten).toContain('[a link](http://example.com)'); + + // Reparsing the rewritten text should reproduce the identical package, proving the round trip isn't merely returning the input unchanged. + expect(api.readMarkdown(rewritten).documentPackage).toEqual(documentPackage); + }); + + it('throws MarkdownUnsupportedDocumentKindError for a non-wordprocessing DocumentPackage', () => { + const spreadsheet = { kind: 'spreadsheet', metadata: {}, children: [] }; + expect(() => api.writeMarkdown(spreadsheet)).toThrow(api.MarkdownUnsupportedDocumentKindError); + }); + }); + + describe('readMarkdownContent then writeMarkdownContent', () => { it('lowers real markdown to a wordprocessing ContentDocument and renders it back to markdown', () => { - const { document, diagnostics } = api.readMarkdown(SAMPLE_MARKDOWN); + const { document, diagnostics } = api.readMarkdownContent(SAMPLE_MARKDOWN); expect(document.kind).toBe('wordprocessing'); expect(document.sections[0].blocks.length).toBeGreaterThan(0); expect(Array.isArray(diagnostics)).toBe(true); - const rewritten = api.writeMarkdown(document); + const rewritten = api.writeMarkdownContent(document); expect(rewritten).toContain('# Title'); expect(rewritten).toContain('[a link](http://example.com)'); // Reparsing the rewritten text should still contain the same real content, proving the round trip isn't merely returning the input unchanged. - const { document: reparsed } = api.readMarkdown(rewritten); + const { document: reparsed } = api.readMarkdownContent(rewritten); expect(reparsed.sections[0].blocks.some((block) => block.kind === 'table')).toBe(true); }); it('throws MarkdownUnsupportedDocumentKindError for a non-wordprocessing ContentDocument', () => { const spreadsheet = { kind: 'spreadsheet', metadata: {}, sheets: [] }; - expect(() => api.writeMarkdown(spreadsheet)).toThrow(api.MarkdownUnsupportedDocumentKindError); + expect(() => api.writeMarkdownContent(spreadsheet)).toThrow(api.MarkdownUnsupportedDocumentKindError); }); }); describe('markdownCodec', () => { - it('decodes real UTF-8 bytes to a ContentDocument and encodes back to bytes', () => { + it('decodes real UTF-8 bytes to a DocumentPackage and encodes back to bytes', () => { const bytes = new TextEncoder().encode(SAMPLE_MARKDOWN); expect(api.MarkdownBytesSchema.safeParse(bytes).success).toBe(true); expect(api.MarkdownBytesSchema.safeParse(new Uint8Array([0xff, 0xfe, 0xfd])).success).toBe(false); - const document = z.decode(api.markdownCodec, bytes); - expect(document.kind).toBe('wordprocessing'); + const documentPackage = z.decode(api.markdownCodec, bytes); + expect(documentPackage.kind).toBe('wordprocessing'); - const encoded = z.encode(api.markdownCodec, document); + const encoded = z.encode(api.markdownCodec, documentPackage); expect(encoded).toBeInstanceOf(Uint8Array); expect(encoded.length).toBeGreaterThan(0); expect(new TextDecoder().decode(encoded)).toContain('Title'); }); }); + + describe('markdownContentCodec', () => { + it('decodes the same bytes to a ContentDocument and encodes back to the identical bytes', () => { + const bytes = new TextEncoder().encode(SAMPLE_MARKDOWN); + + const document = z.decode(api.markdownContentCodec, bytes); + expect(document.kind).toBe('wordprocessing'); + + const encoded = z.encode(api.markdownContentCodec, document); + expect(encoded).toEqual(z.encode(api.markdownCodec, z.decode(api.markdownCodec, bytes))); + }); + }); }); diff --git a/test/workers/markdown-codec.test.ts b/test/workers/markdown-codec.test.ts index b6fbe88..afd7ffb 100644 --- a/test/workers/markdown-codec.test.ts +++ b/test/workers/markdown-codec.test.ts @@ -1,23 +1,38 @@ import { describe, expect, it } from 'vitest'; -import { readMarkdown, writeMarkdown } from '../../src'; +import { flattenPackage } from 'document-schema.js'; +import { readMarkdown, readMarkdownContent, writeMarkdown, writeMarkdownContent } from '../../src'; -// Proves markdown-codec's readMarkdown/writeMarkdown execute inside a Cloudflare Workers isolate (workerd, via @cloudflare/vitest-pool-workers) with no Node-only APIs. The codec is isomorphic by design -- CommonMark+GFM scan/parse/lower/emit hand-written with only zod as a runtime sibling, no node:fs/Buffer/path anywhere -- so if either direction touched a Node-only API the workerd isolate would throw instead of these passing. This is the runtime complement to the existing node `vitest run --project unit` suite, not a replacement for it. +// Proves markdown-codec's public read/write surface executes inside a Cloudflare Workers isolate (workerd, via @cloudflare/vitest-pool-workers) with no Node-only APIs -- both encodings: the tree-form readMarkdown/writeMarkdown pair over document-schema.js's DocumentPackage, and the flat readMarkdownContent/writeMarkdownContent pair over its ContentDocument. The codec is isomorphic by design -- CommonMark+GFM scan/parse/lower/emit hand-written with only zod as a runtime sibling, no node:fs/Buffer/path anywhere -- so if either direction touched a Node-only API the workerd isolate would throw instead of these passing. The tree pair matters here in its own right rather than being covered by the flat one: it runs document-schema.js's own decompose/factorStyles/flattenPackage inside the isolate too, so this is equally a check that the schema package's package-boundary transform is Worker-isomorphic on the path this package puts it on. This is the runtime complement to the existing node `vitest run --project unit` suite, not a replacement for it. describe('markdown-codec under the Cloudflare Workers runtime', () => { const source = '# Heading text\n\nA paragraph with **bold** and `code`.\n'; - it('readMarkdown lowers a heading + paragraph to a wordprocessing ContentDocument', () => { - const { document } = readMarkdown(source); + it('readMarkdown lowers a heading + paragraph to a wordprocessing DocumentPackage', () => { + const { documentPackage } = readMarkdown(source); + expect(documentPackage.kind).toBe('wordprocessing'); + expect(documentPackage.children.length).toBeGreaterThan(0); + }); + + it('writeMarkdown round-trips that package back to a string containing the heading text', () => { + const { documentPackage } = readMarkdown(source); + expect(writeMarkdown(documentPackage)).toContain('Heading text'); + }); + + it('flattens a package to exactly the document readMarkdownContent produces', () => { + expect(flattenPackage(readMarkdown(source).documentPackage)).toEqual(readMarkdownContent(source).document); + }); + + it('readMarkdownContent lowers a heading + paragraph to a wordprocessing ContentDocument', () => { + const { document } = readMarkdownContent(source); expect(document.kind).toBe('wordprocessing'); }); - it('writeMarkdown round-trips that content back to a string containing the heading text', () => { - const { document } = readMarkdown(source); - const roundTripped = writeMarkdown(document); - expect(roundTripped).toContain('Heading text'); + it('writeMarkdownContent round-trips that content back to a string containing the heading text', () => { + const { document } = readMarkdownContent(source); + expect(writeMarkdownContent(document)).toContain('Heading text'); }); - it('round-trips a footnote, whose definition rides a construct boundary-marker pair', () => { - const { document } = readMarkdown('Body[^1].\n\n[^1]: The note.'); - expect(writeMarkdown(document)).toContain('[^1]: The note'); + it('round-trips a footnote through the package form, whose definition rides a construct boundary-marker pair', () => { + const { documentPackage } = readMarkdown('Body[^1].\n\n[^1]: The note.'); + expect(writeMarkdown(documentPackage)).toContain('[^1]: The note'); }); });