From da2797e6c7b7d45751a8dc19384eda108c87c2df Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 17 Aug 2026 17:29:55 +0100 Subject: [PATCH 1/3] build(deps): adopt the schema-3 dependency set with documents.js 2.0.0 documents.js 2.0.0 replaces the DocumentPackage layout half with per-node frames and pages, and carries the whole family on document-schema.js 3.x. The lockfile previously held schema 3.1.0 (via markdown-codec 2.0.0 / odf.js 3.0.1) and 3.2.0 (via documents.js 2.0.0) side by side; deduped to a single 3.2.0. The contentCounts test fixtures now assert CONTENT_FORMAT_VERSION instead of the hardcoded literal 2 so they track the schema major. --- package.json | 2 +- pnpm-lock.yaml | 99 ++++++-------------------------- src/shared/contentCounts.test.ts | 10 ++-- 3 files changed, 22 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index d60b82b..91b2013 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@vanilla-extract/recipes": "^0.5.7", "dexie": "^4.4.4", "dexie-react-hooks": "^4.4.0", - "documents.js": "^1.102.2", + "documents.js": "^2.0.0", "markdown-codec": "^2.0.0", "odf.js": "^3.0.1", "react": "^19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 119e7be..7d4876e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ^4.4.0 version: 4.4.0(dexie@4.4.4)(react@19.2.8) documents.js: - specifier: ^1.102.2 - version: 1.102.2 + specifier: ^2.0.0 + version: 2.0.0 markdown-codec: specifier: ^2.0.0 version: 2.0.0 @@ -2106,11 +2106,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.11.12: - resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} - engines: {node: '>=6.0.0'} - hasBin: true - baseline-browser-mapping@2.11.13: resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} engines: {node: '>=6.0.0'} @@ -2139,11 +2134,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.8: resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2440,20 +2430,12 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - document-schema.js@2.7.17: - resolution: {integrity: sha512-z+1XNFToTrERlxIgoeyFb4Rw5b2nP+EgFOMH/CQfIjo0V9+To0LG6jJe2JUbTUfaggvbBRAEfDAHDMPVcBPROQ==} - engines: {node: '>=20'} - - document-schema.js@3.1.0: - resolution: {integrity: sha512-Qj+0yx3lkorssXCAOIhLtrPKdJEMaB3E1QyloXWJt2H3NM8+ed7w6TyjWHeiaCnl57eGoAuUDWFGgwYDm/CGqA==} - engines: {node: '>=20'} - document-schema.js@3.2.0: resolution: {integrity: sha512-XDu/+fo56WrXrcR3f16xH5lYEQX+3b4W6kJELRNFwrrWxOqhQBQepXMkCi+niSrgCEIcfaC1IeaGPlZ8oj5gSw==} engines: {node: '>=20'} - documents.js@1.102.2: - resolution: {integrity: sha512-Dpcbl15Z5O2x5Ps0bWi4PYvesZ27kzfl5hA99Zh4jdqdhhKgJFJETZp9mSQqI9HB9d9K+oJkn8C9AA27fM/87g==} + documents.js@2.0.0: + resolution: {integrity: sha512-ryI83OYP6JimwjrWsU7DEV7fGZuLy2QDyEoO6WRZW+ZFPHtJh+EQ1Q3wtaHxnW9gHizjumh00rjtlxc2rx9HXQ==} engines: {node: '>=20'} hasBin: true @@ -2476,9 +2458,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.402: - resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} - electron-to-chromium@1.5.403: resolution: {integrity: sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==} @@ -3453,10 +3432,6 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - markdown-codec@1.4.2: - resolution: {integrity: sha512-SfY0hXE5GdgS+GuiUVOf3v4SeD9XxIfT6ZBcJfLVY3Qa9Q3TfBzvNAVstE30g9CrW8vykvcz1xEACEYOos11Pw==} - engines: {node: '>=20'} - markdown-codec@2.0.0: resolution: {integrity: sha512-vbNj6KPo4hJxA7EAzqIiTAx9x3hF1xmFHFEJm/VVptlzuy8KfYYp3wmq7ZkWPbQ47/pIvANXfiA3Zc5NUvVitQ==} engines: {node: '>=20'} @@ -3673,10 +3648,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - odf.js@2.7.23: - resolution: {integrity: sha512-qmee21eZYO0WRBjmwUL72ZaggZ/NvkQ1LNkgK+DAn/9sV4PKtjOAuIb4uScL2jSJJ1a8S3afMP32fV+1eGDngw==} - engines: {node: '>=20'} - odf.js@3.0.1: resolution: {integrity: sha512-gVTXbQc6oHy1y1m7VxXXCQFU0HkTYoTSg7j/CH2Q+hSPulzOV4SCyPucS4CilSpmruckRONILpIMCi8DX4bFrA==} engines: {node: '>=20'} @@ -3685,8 +3656,8 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} - ooxml.js@2.11.32: - resolution: {integrity: sha512-SwSn/CkRANEeaxSMyvD3s1EMrpXFvBTWF9c/uQo+A5sFCNGVYYPpBjn1VT+rsw9EE5/WGXzRvOelkjDklT9Itw==} + ooxml.js@2.16.0: + resolution: {integrity: sha512-NLoTLONWlZ09ZdUURCfF+oC+yZ2+A7VO+qDYy6vGbhcOVCRka8W95TuESNKEO+737JRDRSI527IBv82YS1EHTg==} engines: {node: '>=20'} openapi-types@12.1.3: @@ -4976,7 +4947,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -7049,8 +7020,6 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.11.12: {} - baseline-browser-mapping@2.11.13: {} before-after-hook@4.0.0: {} @@ -7078,14 +7047,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.7: - dependencies: - baseline-browser-mapping: 2.11.12 - caniuse-lite: 1.0.30001809 - electron-to-chromium: 1.5.402 - node-releases: 2.0.53 - update-browserslist-db: 1.3.0(browserslist@4.28.7) - browserslist@4.28.8: dependencies: baseline-browser-mapping: 2.11.13 @@ -7364,26 +7325,18 @@ snapshots: dependencies: path-type: 4.0.0 - document-schema.js@2.7.17: - dependencies: - zod: 4.4.3 - - document-schema.js@3.1.0: - dependencies: - zod: 4.4.3 - document-schema.js@3.2.0: dependencies: zod: 4.4.3 - documents.js@1.102.2: + documents.js@2.0.0: dependencies: byte-codec: 1.1.9 - document-schema.js: 2.7.17 + document-schema.js: 3.2.0 fflate: 0.8.3 - markdown-codec: 1.4.2 - odf.js: 2.7.23 - ooxml.js: 2.11.32 + markdown-codec: 2.0.0 + odf.js: 3.0.1 + ooxml.js: 2.16.0 pdf-codec: 2.2.35 zod: 4.4.3 @@ -7410,8 +7363,6 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.402: {} - electron-to-chromium@1.5.403: {} emoji-regex@10.6.0: {} @@ -8503,14 +8454,9 @@ snapshots: dependencies: semver: 7.8.5 - markdown-codec@1.4.2: - dependencies: - document-schema.js: 2.7.17 - zod: 4.4.3 - markdown-codec@2.0.0: dependencies: - document-schema.js: 3.1.0 + document-schema.js: 3.2.0 zod: 4.4.3 marked-terminal@7.3.0(marked@15.0.12): @@ -8653,16 +8599,9 @@ snapshots: obug@2.1.4: {} - odf.js@2.7.23: - dependencies: - document-schema.js: 2.7.17 - fast-xml-parser: 5.10.1 - fflate: 0.8.3 - zod: 4.4.3 - odf.js@3.0.1: dependencies: - document-schema.js: 3.1.0 + document-schema.js: 3.2.0 fast-xml-parser: 5.10.1 fflate: 0.8.3 zod: 4.4.3 @@ -8671,9 +8610,9 @@ snapshots: dependencies: mimic-fn: 4.0.0 - ooxml.js@2.11.32: + ooxml.js@2.16.0: dependencies: - document-schema.js: 2.7.17 + document-schema.js: 3.2.0 fast-xml-parser: 5.10.1 fflate: 0.8.3 zod: 4.4.3 @@ -9603,12 +9542,6 @@ snapshots: upath@1.2.0: {} - update-browserslist-db@1.3.0(browserslist@4.28.7): - dependencies: - browserslist: 4.28.7 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.3.0(browserslist@4.28.8): dependencies: browserslist: 4.28.8 diff --git a/src/shared/contentCounts.test.ts b/src/shared/contentCounts.test.ts index 0a3dce0..5c8ce48 100644 --- a/src/shared/contentCounts.test.ts +++ b/src/shared/contentCounts.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { ContentDocument } from 'documents.js'; +import { CONTENT_FORMAT_VERSION, type ContentDocument } from 'documents.js'; import { contentSummary } from './contentCounts'; @@ -10,7 +10,7 @@ const PAGE_SIZE = { widthPt: 595, heightPt: 842 }; function wordprocessing(sections: number, blocksPerSection: number): ContentDocument { return { kind: 'wordprocessing', - formatVersion: 2, + formatVersion: CONTENT_FORMAT_VERSION, metadata: {}, sections: Array.from({ length: sections }, () => ({ pageSize: PAGE_SIZE, @@ -23,7 +23,7 @@ function wordprocessing(sections: number, blocksPerSection: number): ContentDocu function spreadsheet(sheets: number, cellsPerSheet: number): ContentDocument { return { kind: 'spreadsheet', - formatVersion: 2, + formatVersion: CONTENT_FORMAT_VERSION, metadata: {}, sheets: Array.from({ length: sheets }, (_, i) => ({ name: `Sheet${i}`, @@ -53,7 +53,7 @@ describe('contentSummary', () => { it('counts blocks inside table cells, not just the table itself', () => { const doc: ContentDocument = { kind: 'wordprocessing', - formatVersion: 2, + formatVersion: CONTENT_FORMAT_VERSION, metadata: {}, sections: [ { @@ -86,7 +86,7 @@ describe('contentSummary', () => { }); it('summarises a formula document', () => { - const doc = { kind: 'formula', formatVersion: 2, metadata: {}, formula: { mathml: [] } } as ContentDocument; + const doc = { kind: 'formula', formatVersion: CONTENT_FORMAT_VERSION, metadata: {}, formula: { mathml: [] } } as ContentDocument; expect(contentSummary(doc)).toEqual(['formula']); }); }); From 6bd22ea5cbea71b16fcad41bb65d6f0e188dbb9a Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 17 Aug 2026 17:30:17 +0100 Subject: [PATCH 2/3] feat(rpc): detect headings via ContentParagraph.headingLevel, read xlsx content directly markdown-codec 2.0.0 lowerHeading and the docx/odt readers under schema 3 all populate ContentParagraph.headingLevel, so the app's heading-{N} convention is now minted from that field; the docx/odt path keeps the Heading{N} styleId pattern as a fallback because the two signals have different coverage (a style named Heading3 without an outline level is caught only by the pattern; a custom style inheriting an outline level is caught only by the field). The markdown path drops parseHeadingStyleId entirely -- headingLevel is authoritative there. documents.js 2.0.0 re-exports readXlsxContent, so xlsx content reads no longer detour through the xlsx->ods bridge conversion and read .content off its result -- the direct package reader is used like every other format, which also makes readContentForFormat synchronous and drops its unused signal parameter. --- src/rpc/router.ts | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/rpc/router.ts b/src/rpc/router.ts index c7cf13f..2d7c39f 100644 --- a/src/rpc/router.ts +++ b/src/rpc/router.ts @@ -19,10 +19,11 @@ import { readMarkdownContent, readPptxContent, readPdf, + readXlsxContent, setDocumentMetadata, } from 'documents.js'; import type { ContentBlock, ContentDocument, ContentParagraph, DocumentFormat, LayoutImageAsset } from 'documents.js'; -import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, parseHeadingStyleId, parseListNumId, QUOTE_STYLE_ID } from 'markdown-codec'; +import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, parseListNumId, QUOTE_STYLE_ID } from 'markdown-codec'; import { z } from 'zod'; // This module runs only inside src/workers/documents.worker.ts. It is the one place in the app allowed to call documents.js's real conversion/metadata functions -- everything on the main thread reaches it only through the oRPC client in src/rpc/client.ts. @@ -48,13 +49,13 @@ const ConversionResultSchema = z.object({ content: ContentDocumentSchema.optional(), }); -// markdown-codec marks a heading paragraph with its own private styleId convention ("Heading1".."Heading6") and a list paragraph's ordered-vs-bullet distinction is encoded inside its own numId string ("md{n}:bullet|ordered@start", via parseListNumId) -- neither is part of document-schema.js's own schema, both are markdown-codec's internal vocabulary. Rewritten here, worker-side (the only place allowed to import markdown-codec), into a small convention this app documents and owns itself, so MarkdownPreview.tsx never needs to depend on markdown-codec's internal string formats -- only on what this router promises to hand it. Only ever applied to a markdown-sourced ContentDocument (see the convert handler's own call site below). +// markdown-codec 2.0 carries a heading paragraph's level in the schema's own ContentParagraph.headingLevel field, so headings need no vocabulary rewrite -- only the residual private conventions are translated here: quote/code-block/horizontal-rule styleIds and a list paragraph's ordered-vs-bullet distinction encoded inside its numId string ("md{n}:bullet|ordered@start", via parseListNumId). Rewritten worker-side (the only place allowed to import markdown-codec) into a small convention this app documents and owns itself, so MarkdownPreview.tsx never needs to depend on markdown-codec's internal string formats -- only on what this router promises to hand it. Only ever applied to a markdown-sourced ContentDocument (see the convert handler's own call site below). function normalizeMarkdownStyling(document: ContentDocument): ContentDocument { if (document.kind !== 'wordprocessing') return document; return { ...document, sections: document.sections.map((section) => ({ ...section, blocks: section.blocks.map(normalizeMarkdownBlock) })) }; } -// docx and odt both carry heading paragraphs with styleId "Heading1".."Heading6" (docx: the raw w:pStyle/@w:val; odt: synthesized to the same convention by odf.js/src/typed/odt/read.ts). Rewritten here into the same "heading-{N}" convention normalizeMarkdownStyling produces. Blockquote and code-block styleIds are detected by heuristic name matching (docx: "Quote"/"IntenseQuote"; odt: "Quotations"; both: any styleId containing "Code"/"Source"/"Preformatted"), rewritten into the same "quote"/"code-block" convention markdown-codec uses. +// docx and odt heading paragraphs are identified primarily by the schema's ContentParagraph.headingLevel (docx: ooxml.js resolves w:outlineLvl through the style chain; odt: odf.js reads text:outline-level), with the "Heading1".."Heading6" styleId pattern as a fallback because the two signals have different coverage: a style NAMED "Heading3" can carry no outline level (caught only by the pattern), and a custom style can inherit an outline level while having a non-Heading name (caught only by the field). Rewritten here into the same "heading-{N}" convention normalizeMarkdownStyling produces. Blockquote and code-block styleIds are detected by heuristic name matching (docx: "Quote"/"IntenseQuote"; odt: "Quotations"; both: any styleId containing "Code"/"Source"/"Preformatted"), rewritten into the same "quote"/"code-block" convention markdown-codec uses. const WORDPROCESSING_HEADING_PATTERN = /^Heading([1-6])$/; function normalizeWordprocessingSemantics(document: ContentDocument): ContentDocument { @@ -63,12 +64,15 @@ function normalizeWordprocessingSemantics(document: ContentDocument): ContentDoc } function normalizeWordprocessingBlock(block: ContentBlock): ContentBlock { - if (block.kind === 'paragraph' && block.styleId !== undefined) { - const headingMatch = WORDPROCESSING_HEADING_PATTERN.exec(block.styleId); - if (headingMatch !== null) return { ...block, styleId: `heading-${headingMatch[1]}` }; - if (block.styleId.includes('Quote')) return { ...block, styleId: 'quote' }; - if (block.styleId.includes('Code') || block.styleId.includes('Source') || block.styleId.includes('Preformatted')) { - return { ...block, styleId: 'code-block' }; + if (block.kind === 'paragraph') { + if (block.headingLevel !== undefined) return { ...block, styleId: `heading-${block.headingLevel}` }; + if (block.styleId !== undefined) { + const headingMatch = WORDPROCESSING_HEADING_PATTERN.exec(block.styleId); + if (headingMatch !== null) return { ...block, styleId: `heading-${headingMatch[1]}` }; + if (block.styleId.includes('Quote')) return { ...block, styleId: 'quote' }; + if (block.styleId.includes('Code') || block.styleId.includes('Source') || block.styleId.includes('Preformatted')) { + return { ...block, styleId: 'code-block' }; + } } return block; } @@ -130,10 +134,9 @@ function normalizeMarkdownBlock(block: ContentBlock): ContentBlock { } function normalizeMarkdownParagraph(paragraph: ContentParagraph): ContentParagraph { - const headingLevel = paragraph.styleId === undefined ? undefined : parseHeadingStyleId(paragraph.styleId); const styleId = - headingLevel !== undefined - ? `heading-${headingLevel}` + paragraph.headingLevel !== undefined + ? `heading-${paragraph.headingLevel}` : paragraph.styleId === QUOTE_STYLE_ID ? 'quote' : paragraph.styleId === CODE_BLOCK_STYLE_ID @@ -181,22 +184,15 @@ const SanitizedLayoutDocumentSchema = LayoutDocumentSchema.extend({ images: z.record(z.string(), SanitizedLayoutImageAssetSchema), }); -// Reads a ContentDocument directly from bytes, bypassing the conversion engine entirely -- no target build/encode, no PDF layout pass. Every format's standalone content reader is exported from documents.js except xlsx (deliberately not re-exported, see documents.js/src/index.ts:3), so xlsx falls back to the cheapest same-variant bridge (xlsx->ods) and reads .content from the result. -async function readContentForFormat(format: DocumentFormat, bytes: Uint8Array, signal?: AbortSignal): Promise { +// Reads a ContentDocument directly from bytes, bypassing the conversion engine entirely -- no target build/encode, no PDF layout pass. Every format's standalone content reader is exported from documents.js (xlsx included since documents.js 2.0 -- before that, xlsx had to detour through the xlsx->ods bridge and read .content off the conversion result). +function readContentForFormat(format: DocumentFormat, bytes: Uint8Array): ContentDocument { if (format === 'markdown') return readMarkdownContent(new TextDecoder().decode(bytes)); - if (format === 'xlsx') { - const result = await createLocalDocumentConverter().convert( - { source: { format: 'xlsx', bytes }, targetFormat: 'ods' }, - { signal: signal ?? new AbortController().signal }, - ); - if (result.package?.content === undefined) throw new Error('xlsx bridge conversion produced no content'); - return result.package.content; - } if (format === 'pdf') throw new Error('PDF has no standalone content reader'); const pkg = decodeDocumentPackage(format, bytes); switch (format) { case 'docx': return readDocxContent(pkg); case 'pptx': return readPptxContent(pkg); + case 'xlsx': return readXlsxContent(pkg); case 'odt': return readOdtContent(pkg); case 'odp': return readOdpContent(pkg); case 'ods': return readOdsContent(pkg); @@ -249,8 +245,8 @@ export const router = { read: os .input(z.object({ format: DocumentFormatSchema, bytes: BytesSchema })) .output(ContentDocumentSchema) - .handler(async ({ input, signal }) => { - const content = await readContentForFormat(input.format, input.bytes, signal); + .handler(({ input }) => { + const content = readContentForFormat(input.format, input.bytes); return normalizeContentForSource(content, input.format, input.bytes); }), }, From 5bcd421f16063ce1d7f3855857c67a5c602dc8eb Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 17 Aug 2026 17:30:30 +0100 Subject: [PATCH 3/3] docs(ui): name the real heading rewrite path in WordProcessingPreview The comment cited a normalizeWordprocessingHeadings function that does not exist, and the rewrite now sources headingLevel rather than only raw Heading{N} styleIds. --- src/ui/WordProcessingPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/WordProcessingPreview.tsx b/src/ui/WordProcessingPreview.tsx index f1c2407..600b9a9 100644 --- a/src/ui/WordProcessingPreview.tsx +++ b/src/ui/WordProcessingPreview.tsx @@ -13,7 +13,7 @@ export interface WordProcessingPreviewProps { error?: unknown; } -// Renders a docx/odt-sourced ContentDocument natively as HTML instead of round-tripping it through a PDF rendition. Headings are detected via the same heading-{N} convention MarkdownPreview uses (router.ts's normalizeWordprocessingHeadings rewrites the raw Heading{N} styleIds into it). Known, bounded gaps vs a full wordprocessor: (1) blockquotes, code blocks, and horizontal rules render as plain paragraphs -- no mapping from a real docx/odt style name to those semantic roles exists today; (2) lists render with a neutral marker (not bullet, not numbered) because ordered-vs-bullet cannot be determined from ContentDocument alone (numbering definitions aren't folded in); (3) embedded objects (formulas, nested documents) and page breaks render nothing -- they have no HTML equivalent in this flowing-text preview (a PDF export still renders them correctly via the layout engine). Block rendering itself is shared with SlidesPreview via renderBlocksNeutral in contentBlocks.tsx. +// Renders a docx/odt-sourced ContentDocument natively as HTML instead of round-tripping it through a PDF rendition. Headings are detected via the same heading-{N} convention MarkdownPreview uses (router.ts's normalizeWordprocessingSemantics rewrites headingLevel / raw Heading{N} styleIds into it). Known, bounded gaps vs a full wordprocessor: (1) blockquotes, code blocks, and horizontal rules render as plain paragraphs -- no mapping from a real docx/odt style name to those semantic roles exists today; (2) lists render with a neutral marker (not bullet, not numbered) because ordered-vs-bullet cannot be determined from ContentDocument alone (numbering definitions aren't folded in); (3) embedded objects (formulas, nested documents) and page breaks render nothing -- they have no HTML equivalent in this flowing-text preview (a PDF export still renders them correctly via the layout engine). Block rendering itself is shared with SlidesPreview via renderBlocksNeutral in contentBlocks.tsx. export function WordProcessingPreview({ label, format, content, loading, error }: WordProcessingPreviewProps) { return (