diff --git a/README.md b/README.md index 557ee5b..76b4171 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Every construct `src/lower`/`src/emit` cannot represent losslessly is a document - **`md/image-unresolved`** — no resolver, `undefined` return, or non-PNG/JPEG bytes degrades to alt-text run. - **`md/raw-html-preserved-as-text` / `md/raw-html-dropped`** — raw HTML kept as literal text (default) or dropped; never interpreted. - **`md/front-matter-key-unmapped`** — no YAML/TOML engine; only five known `LayoutMetadata` keys recognised. -- **`md/heading-level-clamped`** — styleId beyond `Heading6` (from another format) clamps to level 6. +- **`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. diff --git a/dist/codec.d.cts b/dist/codec.d.cts index 0cf40c9..99d9abe 100644 --- a/dist/codec.d.cts +++ b/dist/codec.d.cts @@ -3,7 +3,7 @@ import { z } from "zod"; declare const MarkdownBytesSchema: z.ZodCustom, Uint8Array>; declare const markdownCodec: z.ZodCodec, Uint8Array>, z.ZodDiscriminatedUnion<[z.ZodObject<{ kind: z.ZodLiteral<"wordprocessing">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -29,7 +29,7 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"presentation">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -62,13 +62,20 @@ declare const markdownCodec: z.ZodCodec, Uin lineSpacingReduction: z.ZodOptional; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; blocks: z.ZodArray>; }, z.core.$strip>>; notes: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"spreadsheet">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -135,6 +142,13 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; hyperlink: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>>>; colSpan: z.ZodOptional; rowSpan: z.ZodOptional; @@ -213,6 +227,13 @@ declare const markdownCodec: z.ZodCodec, Uin middle: "middle"; }>>; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>>; columns: z.ZodArray, Uin heightPt: z.ZodNumber; altText: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; anchorRow: z.ZodNumber; anchorColumn: z.ZodNumber; offsetXPt: z.ZodNumber; @@ -285,7 +313,7 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"drawing">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -318,6 +346,13 @@ declare const markdownCodec: z.ZodCodec, Uin lineSpacingReduction: z.ZodOptional; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; blocks: z.ZodArray>; }, z.core.$strip>>; vectors: z.ZodArray, Uin }, z.core.$strip>>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"ellipse">; frame: z.ZodObject<{ @@ -380,6 +422,13 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"line">; from: z.ZodObject<{ @@ -406,6 +455,13 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"path">; frame: z.ZodObject<{ @@ -468,11 +524,18 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>], "kind">>; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"formula">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; diff --git a/dist/codec.d.ts b/dist/codec.d.ts index 0cf40c9..99d9abe 100644 --- a/dist/codec.d.ts +++ b/dist/codec.d.ts @@ -3,7 +3,7 @@ import { z } from "zod"; declare const MarkdownBytesSchema: z.ZodCustom, Uint8Array>; declare const markdownCodec: z.ZodCodec, Uint8Array>, z.ZodDiscriminatedUnion<[z.ZodObject<{ kind: z.ZodLiteral<"wordprocessing">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -29,7 +29,7 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"presentation">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -62,13 +62,20 @@ declare const markdownCodec: z.ZodCodec, Uin lineSpacingReduction: z.ZodOptional; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; blocks: z.ZodArray>; }, z.core.$strip>>; notes: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"spreadsheet">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -135,6 +142,13 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; hyperlink: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>>>; colSpan: z.ZodOptional; rowSpan: z.ZodOptional; @@ -213,6 +227,13 @@ declare const markdownCodec: z.ZodCodec, Uin middle: "middle"; }>>; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>>; columns: z.ZodArray, Uin heightPt: z.ZodNumber; altText: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; anchorRow: z.ZodNumber; anchorColumn: z.ZodNumber; offsetXPt: z.ZodNumber; @@ -285,7 +313,7 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"drawing">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; @@ -318,6 +346,13 @@ declare const markdownCodec: z.ZodCodec, Uin lineSpacingReduction: z.ZodOptional; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; blocks: z.ZodArray>; }, z.core.$strip>>; vectors: z.ZodArray, Uin }, z.core.$strip>>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"ellipse">; frame: z.ZodObject<{ @@ -380,6 +422,13 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"line">; from: z.ZodObject<{ @@ -406,6 +455,13 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"path">; frame: z.ZodObject<{ @@ -468,11 +524,18 @@ declare const markdownCodec: z.ZodCodec, Uin }, z.core.$strip>>; paintOrder: z.ZodOptional; sourcePath: z.ZodOptional; + frames: z.ZodOptional>>; }, z.core.$strip>], "kind">>; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"formula">; - formatVersion: z.ZodLiteral<2>; + formatVersion: z.ZodLiteral<3>; metadata: z.ZodObject<{ title: z.ZodOptional; author: z.ZodOptional; diff --git a/dist/emit/emit.cjs b/dist/emit/emit.cjs index 765f94e..af99f84 100644 --- a/dist/emit/emit.cjs +++ b/dist/emit/emit.cjs @@ -7,6 +7,7 @@ const require_emit_front_matter = require("./front-matter.cjs"); const require_emit_inline = require("./inline.cjs"); const require_emit_image = require("./image.cjs"); const require_emit_table = require("./table.cjs"); +let document_schema_js = require("document-schema.js"); //#region src/emit/emit.ts const MAX_SETEXT_LEVEL = 2; const SETEXT_LEVEL_1_CHAR = "="; @@ -56,15 +57,12 @@ function renderParagraphBody(paragraph, context) { if (paragraph.styleId === "MathBlock") return `$$\n${paragraph.runs.map((run) => run.text).join("")}\n$$`; const headingLevel = paragraph.styleId === void 0 ? void 0 : require_shared_style_constants.parseHeadingStyleId(paragraph.styleId); if (headingLevel !== void 0) { - let level = headingLevel; - if (level > 6) { - context.sink({ - code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED, - severity: "info", - message: `heading level ${String(level)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(6)}` - }); - level = 6; - } + const level = (0, document_schema_js.clampHeadingLevel)(headingLevel); + if (level !== headingLevel) context.sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED, + severity: "info", + message: `heading level ${String(headingLevel)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(level)}` + }); const text = require_emit_inline.emitRuns(paragraph.runs, context); if (context.headingStyle === "setext" && level <= MAX_SETEXT_LEVEL) return renderSetextHeading(level, text); return `${"#".repeat(level)} ${text}`; diff --git a/dist/emit/emit.js b/dist/emit/emit.js index a572407..77bb2f0 100644 --- a/dist/emit/emit.js +++ b/dist/emit/emit.js @@ -6,6 +6,7 @@ import { emitFrontMatter } from "./front-matter.js"; import { emitRuns } from "./inline.js"; import { emitImage } from "./image.js"; import { emitTable } from "./table.js"; +import { clampHeadingLevel } from "document-schema.js"; //#region src/emit/emit.ts const MAX_SETEXT_LEVEL = 2; const SETEXT_LEVEL_1_CHAR = "="; @@ -55,15 +56,12 @@ function renderParagraphBody(paragraph, context) { if (paragraph.styleId === "MathBlock") return `$$\n${paragraph.runs.map((run) => run.text).join("")}\n$$`; const headingLevel = paragraph.styleId === void 0 ? void 0 : parseHeadingStyleId(paragraph.styleId); if (headingLevel !== void 0) { - let level = headingLevel; - if (level > 6) { - context.sink({ - code: MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED, - severity: "info", - message: `heading level ${String(level)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(6)}` - }); - level = 6; - } + const level = clampHeadingLevel(headingLevel); + if (level !== headingLevel) context.sink({ + code: MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED, + severity: "info", + message: `heading level ${String(headingLevel)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(level)}` + }); const text = emitRuns(paragraph.runs, context); if (context.headingStyle === "setext" && level <= MAX_SETEXT_LEVEL) return renderSetextHeading(level, text); return `${"#".repeat(level)} ${text}`; diff --git a/dist/lower/lower.cjs b/dist/lower/lower.cjs index 4f89dcd..d538173 100644 --- a/dist/lower/lower.cjs +++ b/dist/lower/lower.cjs @@ -36,7 +36,8 @@ function lowerHeading(node, context) { return [decorateParagraph({ kind: "paragraph", runs: require_lower_inline.lowerInlineNodes(node.children, inlineContext(context)), - styleId: require_shared_style_constants.headingStyleId(node.level) + styleId: require_shared_style_constants.headingStyleId(node.level), + headingLevel: node.level }, context)]; } function lowerParagraph(node, context) { diff --git a/dist/lower/lower.js b/dist/lower/lower.js index f04577b..645440b 100644 --- a/dist/lower/lower.js +++ b/dist/lower/lower.js @@ -35,7 +35,8 @@ function lowerHeading(node, context) { return [decorateParagraph({ kind: "paragraph", runs: lowerInlineNodes(node.children, inlineContext(context)), - styleId: headingStyleId(node.level) + styleId: headingStyleId(node.level), + headingLevel: node.level }, context)]; } function lowerParagraph(node, context) { diff --git a/package.json b/package.json index e370b7c..83daff8 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "license": "MIT", "packageManager": "pnpm@11.6.0", "dependencies": { - "document-schema.js": "^2.7.17", + "document-schema.js": "^3.0.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ab7691..9fc0842 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: document-schema.js: - specifier: ^2.7.17 - version: 2.7.17 + specifier: ^3.0.0 + version: 3.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@2.7.17: - resolution: {integrity: sha512-z+1XNFToTrERlxIgoeyFb4Rw5b2nP+EgFOMH/CQfIjo0V9+To0LG6jJe2JUbTUfaggvbBRAEfDAHDMPVcBPROQ==} + document-schema.js@3.0.0: + resolution: {integrity: sha512-19ARydD8C5GjTlEq3UjYgi4imX77Wg56N8cjgEGhRsS1PF1PLqblY9sLjG/b+eQTXzN1SKvDLQSOKvUwWFUa4g==} engines: {node: '>=20'} dot-prop@5.3.0: @@ -4522,7 +4522,7 @@ snapshots: dependencies: path-type: 4.0.0 - document-schema.js@2.7.17: + document-schema.js@3.0.0: dependencies: zod: 4.4.3 diff --git a/src/emit/emit.ts b/src/emit/emit.ts index 214da9e..a87d110 100644 --- a/src/emit/emit.ts +++ b/src/emit/emit.ts @@ -1,6 +1,6 @@ // ContentDocument -> markdown text: writeMarkdown's own build-side half, the structural inverse of src/lower/lower.ts. Every mapping mirrors that module's own top-of-file table in reverse: // -// - "Heading{1..6}" styleId -> ATX heading, "#" repeated to the level -- 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). +// - "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). @@ -11,6 +11,7 @@ // ContentPageBreak and ContentEmbeddedObjectBlock have no markdown representation of any kind (this package's own src/lower never produces either, but ContentDocument is a shared pivot a caller can construct directly) -- both are silently dropped, contributing no output at all; this is not one of this package's own named mapping gaps (there was never a markdown construct to lose fidelity from), so it carries no diagnostic code. import type { ContentBlock, ContentDocument, ContentParagraph } from 'document-schema.js'; +import { clampHeadingLevel } from 'document-schema.js'; import { MarkdownUnsupportedDocumentKindError } from '../diagnostics/diagnostics'; import type { MarkdownDiagnosticSink } from '../diagnostics/diagnostics'; import { MarkdownDiagnosticCodes, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from '../diagnostics/diagnostics'; @@ -18,7 +19,7 @@ import { DEFAULT_BULLET_LIST_MARKER, DEFAULT_CODE_FENCE_CHAR, DEFAULT_EMPHASIS_M import type { MarkdownHeadingStyle, WriteMarkdownOptions } from '../options/options'; import type { ListNumIdInfo } from '../shared/list-id'; import { parseListNumId } from '../shared/list-id'; -import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, MAX_HEADING_STYLE_LEVEL, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, parseHeadingStyleId } from '../shared/style-constants'; +import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, parseHeadingStyleId } from '../shared/style-constants'; import { emitFrontMatter } from './front-matter'; import { emitImage } from './image'; import type { InlineEmitContext } from './inline'; @@ -109,10 +110,9 @@ function renderParagraphBody(paragraph: ContentParagraph, context: EmitContext): } const headingLevel = paragraph.styleId === undefined ? undefined : parseHeadingStyleId(paragraph.styleId); if (headingLevel !== undefined) { - let level = headingLevel; - if (level > MAX_HEADING_STYLE_LEVEL) { - context.sink({ code: MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED, severity: 'info', message: `heading level ${String(level)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(MAX_HEADING_STYLE_LEVEL)}` }); - level = MAX_HEADING_STYLE_LEVEL; + const level = clampHeadingLevel(headingLevel); + if (level !== headingLevel) { + context.sink({ code: MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED, severity: 'info', message: `heading level ${String(headingLevel)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(level)}` }); } const text = emitRuns(paragraph.runs, context); if (context.headingStyle === 'setext' && level <= MAX_SETEXT_LEVEL) { diff --git a/src/lower/lower.test.ts b/src/lower/lower.test.ts index 029e015..0f8e269 100644 --- a/src/lower/lower.test.ts +++ b/src/lower/lower.test.ts @@ -43,12 +43,16 @@ describe('document envelope', () => { }); describe('headings', () => { - it('maps an ATX heading level to a "Heading{N}" styleId', () => { - expect(paragraph(blocks('### foo')[0]).styleId).toBe('Heading3'); + it('maps an ATX heading level to a "Heading{N}" styleId plus the canonical headingLevel', () => { + const heading = paragraph(blocks('### foo')[0]); + expect(heading.styleId).toBe('Heading3'); + expect(heading.headingLevel).toBe(3); }); it('maps a setext heading the same way', () => { - expect(paragraph(blocks('foo\n===')[0]).styleId).toBe('Heading1'); + const heading = paragraph(blocks('foo\n===')[0]); + expect(heading.styleId).toBe('Heading1'); + expect(heading.headingLevel).toBe(1); }); }); @@ -117,6 +121,7 @@ describe('blockquotes', () => { it('keeps a heading inside a quote styled as Heading{N}, not Quote', () => { const block = paragraph(blocks('> # foo')[0]); expect(block.styleId).toBe('Heading1'); + expect(block.headingLevel).toBe(1); expect(block.indentLeftPt).toBe(36); }); }); diff --git a/src/lower/lower.ts b/src/lower/lower.ts index 5a73b3a..6e35434 100644 --- a/src/lower/lower.ts +++ b/src/lower/lower.ts @@ -1,7 +1,7 @@ // The AST -> ContentDocument lowering stage: this package's own counterpart to ooxml.js's readDocx/readPptx and odf.js's readOdt/readOdp -- a thin adapter from parseMarkdown's own AST onto document-schema.js's shared ContentDocument pivot, not a second parser. Every mapping below, and the stable diagnostic code its own gap is recorded under (MarkdownDiagnosticCodes, src/diagnostics/diagnostics.ts), mirrors this package's own construct-by-construct design table: // // - document envelope -> one ContentSection, A4 + 1in default page geometry, overridable via ReadMarkdownOptions.pageSize/margins -- MarkdownDiagnosticCodes.INVENTED_PAGE_GEOMETRY (markdown has no page concept of its own; this ALWAYS fires, once per lowered document). -// - ATX/setext heading -> styleId "Heading1".."Heading6", mirroring odf.js's readOdt convention exactly (src/shared/style-constants.ts's headingStyleId). +// - ATX/setext heading -> styleId "Heading1".."Heading6", mirroring odf.js's readOdt convention exactly (src/shared/style-constants.ts's headingStyleId), plus the canonical ContentParagraph.headingLevel document-schema.js defines -- the level number itself (always 1-6 here: ATX/setext cap at six), so a consumer that never learned this package's own styleId spelling still knows the heading's depth. // - emphasis/strong/strikethrough -> italic/bold/strike ContentRun fields; links/autolinks -> ContentRun.hyperlink; code spans -> a Courier New run; hard/soft breaks -> literal '\n'/' ' -- all in src/lower/inline.ts, alongside MarkdownDiagnosticCodes.NESTED_EMPHASIS_FLATTENED and LINK_TITLE_DROPPED. // - fenced/indented code block -> one paragraph, styleId 'CodeBlock', '\n'-joined literal, monospace -- MarkdownDiagnosticCodes.CODE_BLOCK_INFO_STRING_DROPPED when a fence's own info string is non-empty. // - blockquote -> styleId 'Quote' (a real Word built-in style name) plus indentLeftPt per nesting level; a heading inside a quote keeps its own Heading{N} styleId (decorateParagraph below only applies 'Quote' when nothing more specific already set a styleId) -- MarkdownDiagnosticCodes.BLOCKQUOTE_NESTED_DEPTH beyond level 1. @@ -65,7 +65,7 @@ function decorateParagraph(paragraph: ContentParagraph, context: BlockLowerConte } function lowerHeading(node: MarkdownHeadingNode, context: BlockLowerContext): ContentBlock[] { - const paragraph: ContentParagraph = { kind: 'paragraph', runs: lowerInlineNodes(node.children, inlineContext(context)), styleId: headingStyleId(node.level) }; + const paragraph: ContentParagraph = { kind: 'paragraph', runs: lowerInlineNodes(node.children, inlineContext(context)), styleId: headingStyleId(node.level), headingLevel: node.level }; return [decorateParagraph(paragraph, context)]; } diff --git a/src/shared/style-constants.ts b/src/shared/style-constants.ts index 58fa98e..4b3c838 100644 --- a/src/shared/style-constants.ts +++ b/src/shared/style-constants.ts @@ -7,7 +7,7 @@ export function headingStyleId(level: number): string { return `${HEADING_STYLE_PREFIX}${String(level)}`; } -// ATX/setext both cap at six `#`/two underline characters -- the ceiling src/emit/emit.ts clamps a "Heading{N}" styleId back down to (MarkdownDiagnosticCodes.HEADING_LEVEL_CLAMPED) when N exceeds it. +// ATX/setext both cap at six `#`/two underline characters -- the ceiling of this vocabulary's own "Heading{N}" spelling. Kept exported so a sibling package building or validating heading styleIds against this vocabulary has the same bound; src/emit/emit.ts's own write-side clamping goes through document-schema.js's clampHeadingLevel (the same 1-6 range, defined once at the schema) rather than through this constant. export const MAX_HEADING_STYLE_LEVEL = 6; const HEADING_STYLE_ID_PATTERN = /^Heading([0-9]+)$/;