diff --git a/README.md b/README.md index 76b4171..a173f54 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Every construct `src/lower`/`src/emit` cannot represent losslessly is a document - **`md/heading-level-clamped`** — styleId beyond `Heading6` (from another format) clamps to level 6 via document-schema.js's shared `clampHeadingLevel()`. - **`md/adjacent-links-merged`** / **`md/code-span-as-monospace-run`** — same-destination adjacent links merge; monospace runs emit as code spans. - **`md/paragraph-indent-dropped`** — `indentLeftPt` without a recognised styleId; indent dropped, paragraph renders. -- **`md/list-numid-fallback`** — a foreign `numId` falls back to a plain bullet list. +- **`md/list-numid-fallback`** — a foreign or absent `numId` (depth-only `ContentListMembership`) falls back to a plain bullet list. - **`md/table-cell-formatting-dropped`** / **`md/table-cell-multi-paragraph-joined`** — GFM cells have no rich-formatting or multi-paragraph representation. ## Fidelity diff --git a/dist/codec.d.cts b/dist/codec.d.cts index 99d9abe..2934cdf 100644 --- a/dist/codec.d.cts +++ b/dist/codec.d.cts @@ -2,18 +2,6 @@ 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<{ - kind: z.ZodLiteral<"wordprocessing">; - formatVersion: z.ZodLiteral<3>; - 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>; sections: z.ZodArray, Uin }, z.core.$strip>; blocks: z.ZodArray>; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"presentation">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"wordprocessing">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -40,6 +69,7 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ slides: z.ZodArray, Uin }, z.core.$strip>>; notes: z.ZodString; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"spreadsheet">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"presentation">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -86,6 +157,7 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ sheets: z.ZodArray, Uin bottom: "bottom"; middle: "middle"; }>>; + comment: z.ZodOptional; + createdAt: z.ZodOptional; + replies: z.ZodOptional; + }, z.core.$strip>>>; + }, z.core.$strip>>; sourcePath: z.ZodOptional; frames: z.ZodOptional, Uin }, z.core.$strip>; embeddedObjects: z.ZodOptional>>; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"drawing">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"spreadsheet">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -324,6 +446,7 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ pages: z.ZodArray, Uin }, z.core.$strip>>>; }, z.core.$strip>], "kind">>; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"formula">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"drawing">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -546,9 +710,73 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ formula: z.ZodObject<{ mathml: z.ZodArray>; starMath: z.ZodOptional; + presentation: z.ZodOptional>; + content: z.ZodOptional>; + provenance: z.ZodOptional; + editTrail: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>; + 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>>; + kind: z.ZodLiteral<"formula">; + 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>; }, z.core.$strip>], "kind">>; //#endregion diff --git a/dist/codec.d.ts b/dist/codec.d.ts index 99d9abe..2934cdf 100644 --- a/dist/codec.d.ts +++ b/dist/codec.d.ts @@ -2,18 +2,6 @@ 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<{ - kind: z.ZodLiteral<"wordprocessing">; - formatVersion: z.ZodLiteral<3>; - 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>; sections: z.ZodArray, Uin }, z.core.$strip>; blocks: z.ZodArray>; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"presentation">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"wordprocessing">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -40,6 +69,7 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ slides: z.ZodArray, Uin }, z.core.$strip>>; notes: z.ZodString; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"spreadsheet">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"presentation">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -86,6 +157,7 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ sheets: z.ZodArray, Uin bottom: "bottom"; middle: "middle"; }>>; + comment: z.ZodOptional; + createdAt: z.ZodOptional; + replies: z.ZodOptional; + }, z.core.$strip>>>; + }, z.core.$strip>>; sourcePath: z.ZodOptional; frames: z.ZodOptional, Uin }, z.core.$strip>; embeddedObjects: z.ZodOptional>>; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"drawing">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"spreadsheet">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -324,6 +446,7 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ pages: z.ZodArray, Uin }, z.core.$strip>>>; }, z.core.$strip>], "kind">>; }, z.core.$strip>>; -}, z.core.$strip>, z.ZodObject<{ - kind: z.ZodLiteral<"formula">; - formatVersion: z.ZodLiteral<3>; + 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>>; + kind: z.ZodLiteral<"drawing">; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -546,9 +710,73 @@ declare const markdownCodec: z.ZodCodec, Uin createdIso: z.ZodOptional; modifiedIso: z.ZodOptional; }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ formula: z.ZodObject<{ mathml: z.ZodArray>; starMath: z.ZodOptional; + presentation: z.ZodOptional>; + content: z.ZodOptional>; + provenance: z.ZodOptional; + editTrail: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>; + 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>>; + kind: z.ZodLiteral<"formula">; + 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>; }, z.core.$strip>], "kind">>; //#endregion diff --git a/dist/emit/emit.cjs b/dist/emit/emit.cjs index af99f84..c445fac 100644 --- a/dist/emit/emit.cjs +++ b/dist/emit/emit.cjs @@ -94,6 +94,17 @@ function renderTopLevelBlock(block, context) { } } function listInfoFor(numId, context) { + if (numId === void 0) { + if (!context.reportedAbsentNumIdFallback) { + context.reportedAbsentNumIdFallback = true; + context.sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK, + severity: "info", + message: "a list membership with no numId of its own (a depth-only ContentListMembership) has no marker type, task-ness, or loose-ness to recover and falls back to an ordinary, tight, non-task bullet list" + }); + } + return; + } const info = require_shared_list_id.parseListNumId(numId); if (info === void 0 && !context.reportedFallbackNumIds.has(numId)) { context.reportedFallbackNumIds.add(numId); @@ -128,7 +139,7 @@ function stripCheckboxRun(item, checkboxPrefix) { } function renderListItemMarker(numId, info, item, context) { const checkboxText = (info?.task === true ? checkboxPrefixFor(item) : void 0) ?? ""; - if (info?.type === "ordered") { + if (info?.type === "ordered" && numId !== void 0) { const next = context.orderedCounters.get(numId) ?? info.start ?? 1; context.orderedCounters.set(numId, next + 1); const bare = `${String(next)}${context.orderedDelimiter} `; @@ -175,7 +186,7 @@ function renderListRegion(items, context) { if (partIndex > 0) { const previous = parts[partIndex - 1]; const sameList = previous.numId === part.numId; - const loose = sameList && (require_shared_list_id.parseListNumId(previous.numId)?.loose ?? false); + const loose = sameList && previous.numId !== void 0 && (require_shared_list_id.parseListNumId(previous.numId)?.loose ?? false); out += sameList && !loose ? "\n" : "\n\n"; } out += part.text; @@ -217,7 +228,8 @@ function emitMarkdown(document, options = {}) { headingStyle: options.headingStyle ?? "atx", embedImages: options.images ?? true, orderedCounters: /* @__PURE__ */ new Map(), - reportedFallbackNumIds: /* @__PURE__ */ new Set() + reportedFallbackNumIds: /* @__PURE__ */ new Set(), + reportedAbsentNumIdFallback: false }; const body = document.sections.map((section) => emitBlocks(section.blocks, context)).join("\n\n"); const frontMatter = options.frontMatter === true ? require_emit_front_matter.emitFrontMatter(document.metadata) : void 0; diff --git a/dist/emit/emit.js b/dist/emit/emit.js index 77bb2f0..ab23cf5 100644 --- a/dist/emit/emit.js +++ b/dist/emit/emit.js @@ -93,6 +93,17 @@ function renderTopLevelBlock(block, context) { } } function listInfoFor(numId, context) { + if (numId === void 0) { + if (!context.reportedAbsentNumIdFallback) { + context.reportedAbsentNumIdFallback = true; + context.sink({ + code: MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK, + severity: "info", + message: "a list membership with no numId of its own (a depth-only ContentListMembership) has no marker type, task-ness, or loose-ness to recover and falls back to an ordinary, tight, non-task bullet list" + }); + } + return; + } const info = parseListNumId(numId); if (info === void 0 && !context.reportedFallbackNumIds.has(numId)) { context.reportedFallbackNumIds.add(numId); @@ -127,7 +138,7 @@ function stripCheckboxRun(item, checkboxPrefix) { } function renderListItemMarker(numId, info, item, context) { const checkboxText = (info?.task === true ? checkboxPrefixFor(item) : void 0) ?? ""; - if (info?.type === "ordered") { + if (info?.type === "ordered" && numId !== void 0) { const next = context.orderedCounters.get(numId) ?? info.start ?? 1; context.orderedCounters.set(numId, next + 1); const bare = `${String(next)}${context.orderedDelimiter} `; @@ -174,7 +185,7 @@ function renderListRegion(items, context) { if (partIndex > 0) { const previous = parts[partIndex - 1]; const sameList = previous.numId === part.numId; - const loose = sameList && (parseListNumId(previous.numId)?.loose ?? false); + const loose = sameList && previous.numId !== void 0 && (parseListNumId(previous.numId)?.loose ?? false); out += sameList && !loose ? "\n" : "\n\n"; } out += part.text; @@ -216,7 +227,8 @@ function emitMarkdown(document, options = {}) { headingStyle: options.headingStyle ?? "atx", embedImages: options.images ?? true, orderedCounters: /* @__PURE__ */ new Map(), - reportedFallbackNumIds: /* @__PURE__ */ new Set() + reportedFallbackNumIds: /* @__PURE__ */ new Set(), + reportedAbsentNumIdFallback: false }; const body = document.sections.map((section) => emitBlocks(section.blocks, context)).join("\n\n"); const frontMatter = options.frontMatter === true ? emitFrontMatter(document.metadata) : void 0; diff --git a/dist/lower/lower.cjs b/dist/lower/lower.cjs index d538173..64b4608 100644 --- a/dist/lower/lower.cjs +++ b/dist/lower/lower.cjs @@ -271,15 +271,13 @@ function lowerParsedMarkdown(parsed, options = {}, metadata = {}) { quoteDepth: 0, list: void 0 }; - const blocks = parsed.document.children.flatMap((child) => lowerBlock(child, context, contentWidthPt)); return { kind: "wordprocessing", - formatVersion: document_schema_js.CONTENT_FORMAT_VERSION, metadata, sections: [{ pageSize, margins, - blocks + blocks: parsed.document.children.flatMap((child) => lowerBlock(child, context, contentWidthPt)) }] }; } diff --git a/dist/lower/lower.js b/dist/lower/lower.js index 645440b..ce8379a 100644 --- a/dist/lower/lower.js +++ b/dist/lower/lower.js @@ -7,7 +7,7 @@ import { extractFrontMatter } from "./front-matter.js"; import { resolveMarkdownImage } from "./image.js"; import { lowerCodeBlockRun, lowerInlineNodes } from "./inline.js"; import { lowerTable } from "./table.js"; -import { CONTENT_FORMAT_VERSION, PAGE_SIZE_A4 } from "document-schema.js"; +import { PAGE_SIZE_A4 } from "document-schema.js"; //#region src/lower/lower.ts function inlineContext(context) { return { @@ -270,15 +270,13 @@ function lowerParsedMarkdown(parsed, options = {}, metadata = {}) { quoteDepth: 0, list: void 0 }; - const blocks = parsed.document.children.flatMap((child) => lowerBlock(child, context, contentWidthPt)); return { kind: "wordprocessing", - formatVersion: CONTENT_FORMAT_VERSION, metadata, sections: [{ pageSize, margins, - blocks + blocks: parsed.document.children.flatMap((child) => lowerBlock(child, context, contentWidthPt)) }] }; } diff --git a/package.json b/package.json index 1317d15..8324a69 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "license": "MIT", "packageManager": "pnpm@11.6.0", "dependencies": { - "document-schema.js": "^3.2.0", + "document-schema.js": "^4.0.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79656c1..045f400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: document-schema.js: - specifier: ^3.2.0 - version: 3.2.0 + specifier: ^4.0.0 + version: 4.0.0 zod: specifier: ^4.4.3 version: 4.4.3 @@ -1603,8 +1603,8 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - document-schema.js@3.2.0: - resolution: {integrity: sha512-XDu/+fo56WrXrcR3f16xH5lYEQX+3b4W6kJELRNFwrrWxOqhQBQepXMkCi+niSrgCEIcfaC1IeaGPlZ8oj5gSw==} + document-schema.js@4.0.0: + resolution: {integrity: sha512-aCBeWHDubmchJBXPx7ZesLmGgOCAPOEI3n/PzunPLBEUq/w+hGpl/XLczGyyPkURu7nKR+dP6M1p7u6k4YxDsw==} engines: {node: '>=20'} dot-prop@5.3.0: @@ -4522,7 +4522,7 @@ snapshots: dependencies: path-type: 4.0.0 - document-schema.js@3.2.0: + document-schema.js@4.0.0: dependencies: zod: 4.4.3 diff --git a/src/diagnostics/diagnostics.test.ts b/src/diagnostics/diagnostics.test.ts index 0761ff6..cad663c 100644 --- a/src/diagnostics/diagnostics.test.ts +++ b/src/diagnostics/diagnostics.test.ts @@ -1,7 +1,7 @@ // 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. import type { ContentBlock, ContentDocument, ContentTable } from 'document-schema.js'; -import { CONTENT_FORMAT_VERSION, PAGE_SIZE_A4 } 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'; @@ -10,7 +10,7 @@ import { createDiagnosticCollector } from '../test-support/diagnostics'; import { MarkdownDiagnosticCodes } from './diagnostics'; function minimalDocument(blocks: readonly ContentBlock[]): ContentDocument { - return { kind: 'wordprocessing', formatVersion: CONTENT_FORMAT_VERSION, metadata: {}, sections: [{ pageSize: PAGE_SIZE_A4, margins: { topPt: 72, rightPt: 72, bottomPt: 72, leftPt: 72 }, blocks: [...blocks] }] }; + return { kind: 'wordprocessing', metadata: {}, sections: [{ pageSize: PAGE_SIZE_A4, margins: { topPt: 72, rightPt: 72, bottomPt: 72, leftPt: 72 }, blocks: [...blocks] }] }; } const reached = new Set(); diff --git a/src/emit/emit.test.ts b/src/emit/emit.test.ts index e1beefb..545e1d4 100644 --- a/src/emit/emit.test.ts +++ b/src/emit/emit.test.ts @@ -1,7 +1,7 @@ // Construct-by-construct tests for the ContentDocument -> markdown emission stage (src/emit/emit.ts), the structural inverse of src/lower/lower.test.ts. Most tests here build a ContentDocument directly (bypassing src/lower entirely) so each construct -- including a cross-format shape src/lower itself never produces, like a paragraph with indentLeftPt but no quotable styleId -- can be exercised in isolation; a handful round-trip through src/lower/lower.ts first where that is the more natural way to obtain a real value (a code span run, a task-list item). import type { ContentDocument, ContentImageBlock, ContentParagraph, ContentTable } from 'document-schema.js'; -import { CONTENT_FORMAT_VERSION, PAGE_SIZE_A4 } from 'document-schema.js'; +import { PAGE_SIZE_A4 } from 'document-schema.js'; import { describe, expect, it } from 'vitest'; import { DEFAULT_MARGINS } from '../defaults/defaults'; import { MarkdownDiagnosticCodes } from '../diagnostics/diagnostics'; @@ -12,7 +12,6 @@ import { emitMarkdown } from './emit'; function doc(blocks: readonly (ContentParagraph | ContentTable | ContentImageBlock)[]): ContentDocument { return { kind: 'wordprocessing', - formatVersion: CONTENT_FORMAT_VERSION, metadata: {}, sections: [{ pageSize: PAGE_SIZE_A4, margins: DEFAULT_MARGINS, blocks: [...blocks] }], }; @@ -216,6 +215,16 @@ describe('gaps (MarkdownDiagnosticCodes)', () => { expect(collector.has(MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK)).toBe(true); }); + it('LIST_NUMID_FALLBACK fires once for depth-only memberships with no numId, falling back to one tight plain-bullet list', () => { + const collector = createDiagnosticCollector(); + const markdown = emitMarkdown(doc([ + { kind: 'paragraph', runs: [{ text: 'x' }], list: { level: 0 } }, + { kind: 'paragraph', runs: [{ text: 'y' }], list: { level: 1 } }, + ]), { sink: collector.sink }); + expect(markdown).toBe('- x\n - y'); + expect(collector.diagnostics.filter((diagnostic) => diagnostic.code === MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK)).toHaveLength(1); + }); + it('TABLE_CELL_FORMATTING_DROPPED fires for colSpan/rowSpan/background and for a non-paragraph cell block', () => { const collector = createDiagnosticCollector(); const table: ContentTable = { diff --git a/src/emit/emit.ts b/src/emit/emit.ts index a87d110..e378ced 100644 --- a/src/emit/emit.ts +++ b/src/emit/emit.ts @@ -3,7 +3,7 @@ // - "Heading{1..6}" styleId -> ATX heading, "#" repeated to the level, clamped through document-schema.js's own shared clampHeadingLevel (one heading-range clamp across the ecosystem instead of a private copy here) -- MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED when the level exceeds 6 (a markdown-produced document never carries one, but ContentDocument is a shared cross-format pivot; a paragraph from, say, odt's own unbounded readOutlineLevel can). // - 'CodeBlock'/'HorizontalRule'/'HTMLPreformatted' styleId -> a fenced code block / a thematic break / literal, unescaped text. // - 'Quote' styleId, or ANY of the four styleIds above while indentLeftPt is also set (a heading/code-block/rule/preformatted-HTML block that sat inside a blockquote when this package's own src/lower produced it) -> '> ' repeated per recovered nesting level (Math.round(indentLeftPt / QUOTE_INDENT_PT)) prefixed to every line of the block's own rendering. A paragraph with indentLeftPt set but none of these five styleIds is a genuine cross-format ambiguity this package cannot resolve (is it a quote, or just some other format's own paragraph indentation?) -- MarkdownDiagnosticCodes.PARAGRAPH_INDENT_DROPPED; the indent is dropped, the paragraph still renders. -// - ContentListMembership -> a bullet/ordered/task-list item, decoded from its own numId string (src/shared/list-id.ts) -- MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK for a numId this package never minted itself (falls back to a plain, tight, non-task bullet, per that module's own documented cross-format contract). +// - ContentListMembership -> a bullet/ordered/task-list item, decoded from its own numId string (src/shared/list-id.ts) -- MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK for a numId this package never minted itself, or a depth-only membership carrying no numId at all (both fall back to a plain, tight, non-task bullet, per that module's own documented cross-format contract). // - ContentTable -> a GFM table, src/emit/table.ts. // - ContentImageBlock -> a markdown image, src/emit/image.ts. // - ContentRun[] -> inline text, src/emit/inline.ts. @@ -36,6 +36,8 @@ interface EmitContext extends TableEmitContext { readonly embedImages: boolean; readonly orderedCounters: Map; readonly reportedFallbackNumIds: Set; + // One-shot latch for the no-numId-at-all fallback diagnostic -- reportedFallbackNumIds cannot key an absent numId without inventing a sentinel string, so this is a mutable flag where its sibling is a mutable-by-reference collection. + reportedAbsentNumIdFallback: boolean; } // setext's own grammar (spec 0.31.2, "Setext headings") only distinguishes two levels (a run of '=' for level 1, of '-' for level 2) -- there is no setext spelling for level 3 and deeper, so headingStyle: 'setext' still falls back to ATX there. @@ -157,7 +159,15 @@ function renderTopLevelBlock(block: ContentBlock, context: EmitContext): string // --- List rendering: every ContentParagraph carrying .list is its own list item (see src/lower/lower.ts's own top-of-file note on why ContentListMembership cannot distinguish a continuation paragraph from a fresh sibling item -- this package resolves that ambiguity the same way on both sides, consistently). --- -function listInfoFor(numId: string, context: EmitContext): ListNumIdInfo | undefined { +// numId undefined is a depth-only ContentListMembership -- document-schema.js 3.3.0+ makes numId optional for sources that carry a level but no numbering identity of their own (OOXML drawing paragraphs' a:pPr/@lvl being the motivating case) -- and it lands in the same documented cross-format fallback as a foreign numId string: with no marker type, task-ness, or loose-ness to recover, the item renders as an ordinary, tight, non-task bullet at its own level. +function listInfoFor(numId: string | undefined, context: EmitContext): ListNumIdInfo | undefined { + if (numId === undefined) { + if (!context.reportedAbsentNumIdFallback) { + context.reportedAbsentNumIdFallback = true; + context.sink({ code: MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK, severity: 'info', message: 'a list membership with no numId of its own (a depth-only ContentListMembership) has no marker type, task-ness, or loose-ness to recover and falls back to an ordinary, tight, non-task bullet list' }); + } + return undefined; + } const info = parseListNumId(numId); if (info === undefined && !context.reportedFallbackNumIds.has(numId)) { context.reportedFallbackNumIds.add(numId); @@ -202,10 +212,11 @@ interface RenderedListMarker { readonly bareLength: number; } -function renderListItemMarker(numId: string, info: ListNumIdInfo | undefined, item: ContentParagraph, context: EmitContext): RenderedListMarker { +function renderListItemMarker(numId: string | undefined, info: ListNumIdInfo | undefined, item: ContentParagraph, context: EmitContext): RenderedListMarker { const checkboxPrefix = info?.task === true ? checkboxPrefixFor(item) : undefined; const checkboxText = checkboxPrefix ?? ''; - if (info?.type === 'ordered') { + // Only a parsed numId string can carry type 'ordered', so the ordered-counter key is present exactly when this branch is live. + if (info?.type === 'ordered' && numId !== undefined) { const next = context.orderedCounters.get(numId) ?? (info.start ?? 1); context.orderedCounters.set(numId, next + 1); const bare = `${String(next)}${context.orderedDelimiter} `; @@ -216,7 +227,8 @@ function renderListItemMarker(numId: string, info: ListNumIdInfo | undefined, it } interface ListItemPart { - readonly numId: string; + // undefined = a depth-only membership with no numId of its own; consecutive such parts share that absence as their list identity, rendering as one tight bullet list. + readonly numId: string | undefined; readonly text: string; } @@ -261,7 +273,7 @@ function renderListRegion(items: readonly ContentParagraph[], context: EmitConte if (partIndex > 0) { const previous = parts[partIndex - 1]!; const sameList = previous.numId === part.numId; - const loose = sameList && (parseListNumId(previous.numId)?.loose ?? false); + const loose = sameList && previous.numId !== undefined && (parseListNumId(previous.numId)?.loose ?? false); out += sameList && !loose ? '\n' : '\n\n'; } out += part.text; @@ -315,6 +327,7 @@ export function emitMarkdown(document: ContentDocument, options: WriteMarkdownOp embedImages: options.images ?? true, orderedCounters: new Map(), reportedFallbackNumIds: new Set(), + reportedAbsentNumIdFallback: false, }; const sections = document.sections.map((section) => emitBlocks(section.blocks, context)); diff --git a/src/lower/lower.ts b/src/lower/lower.ts index 6e35434..6409f25 100644 --- a/src/lower/lower.ts +++ b/src/lower/lower.ts @@ -14,7 +14,7 @@ // - front matter (src/lower/front-matter.ts) -> a flat-scalar-only LayoutMetadata subset -- MarkdownDiagnosticCodes.FRONT_MATTER_KEY_UNMAPPED. import type { ContentBlock, ContentDocument, ContentParagraph, ContentRun, LayoutMetadata } from 'document-schema.js'; -import { CONTENT_FORMAT_VERSION, PAGE_SIZE_A4 } from 'document-schema.js'; +import { PAGE_SIZE_A4 } from 'document-schema.js'; import type { MarkdownBlockNode, MarkdownHeadingNode, MarkdownListItemNode, MarkdownListNode, MarkdownParagraphNode } from '../ast/ast'; import type { MarkdownParseOptions, ParsedMarkdown } from '../block/block'; import { parseMarkdown } from '../block/block'; @@ -276,7 +276,6 @@ export function lowerParsedMarkdown(parsed: ParsedMarkdown, options: ReadMarkdow return { kind: 'wordprocessing', - formatVersion: CONTENT_FORMAT_VERSION, metadata, sections: [{ pageSize, margins, blocks }], }; diff --git a/src/read.ts b/src/read.ts index cfb8880..6720cc9 100644 --- a/src/read.ts +++ b/src/read.ts @@ -1,6 +1,6 @@ // readMarkdown: markdown source text -> ContentDocument. // -// RECONCILIATION DECISION (recorded here per the scaffolding task that created this file): readMarkdown/writeMarkdown operate on document-schema.js's full ContentDocument directly (kind/formatVersion/metadata/sections), not a bare {metadata, sections} shape wrapped by a documents.js-side adapter. +// 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. // // Reasoning, from the two precedents this family's own sibling packages already established for exactly this fork: // diff --git a/src/shared/list-id.ts b/src/shared/list-id.ts index e3af1ce..a6dd647 100644 --- a/src/shared/list-id.ts +++ b/src/shared/list-id.ts @@ -2,7 +2,7 @@ // // Nesting mints NO new numId at all -- a nested list reuses its ENCLOSING list's own numId, incrementing only `level`, exactly mirroring odf.js's own nesting rule (a nested text:list keeps its enclosing list's numId, level+1). This is a deliberate, accepted limitation, not an oversight: if a nested list's own real marker type disagrees with the type baked into the numId at mint time, the numId's own type tag wins (first-wins) and the loser is reported via MarkdownDiagnosticCodes.LIST_MARKER_TYPE_CONFLICT (src/lower/lower.ts). // -// A numId that does not match this grammar at all (e.g. "list1", "3" -- odf.js's own convention, or any other format's own numId scheme entirely) is a cross-format value this package never minted itself: src/emit falls back to rendering it as an ordinary bullet list -- tight, start 1, never a task list -- per MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK, the documented cross-format contract. +// A numId that does not match this grammar at all (e.g. "list1", "3" -- odf.js's own convention, or any other format's own numId scheme entirely) is a cross-format value this package never minted itself: src/emit falls back to rendering it as an ordinary bullet list -- tight, start 1, never a task list -- per MarkdownDiagnosticCodes.LIST_NUMID_FALLBACK, the documented cross-format contract. A ContentListMembership with no numId at all (optional since document-schema.js 3.3.0, for a source that carries only a depth) gets the same src/emit fallback under the same code. const NUMID_PATTERN = /^md(\d+):(bullet|ordered)(?:@(\d+))?(\+task)?(\+loose)?$/; const DEFAULT_ORDERED_START = 1; diff --git a/test/smoke.test.mjs b/test/smoke.test.mjs index c0c92fb..5176e24 100644 --- a/test/smoke.test.mjs +++ b/test/smoke.test.mjs @@ -52,7 +52,7 @@ describe.each([ }); it('throws MarkdownUnsupportedDocumentKindError for a non-wordprocessing ContentDocument', () => { - const spreadsheet = { kind: 'spreadsheet', formatVersion: 1, metadata: {}, sheets: [] }; + const spreadsheet = { kind: 'spreadsheet', metadata: {}, sheets: [] }; expect(() => api.writeMarkdown(spreadsheet)).toThrow(api.MarkdownUnsupportedDocumentKindError); }); });