diff --git a/package.json b/package.json index 20a4964..a00e1c1 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "license": "MIT", "packageManager": "pnpm@11.6.0", "dependencies": { - "document-schema.js": "^2.7.17", + "document-schema.js": "^3.0.0", "fast-xml-parser": "^5.10.1", "fflate": "^0.8.3", "zod": "^4.4.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20b4985..9c97bb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: .: dependencies: document-schema.js: - specifier: ^2.7.17 - version: 2.7.17 + specifier: ^3.0.0 + version: 3.0.0 fast-xml-parser: specifier: ^5.10.1 version: 5.10.1 @@ -1608,8 +1608,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: @@ -4540,7 +4540,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/typed/formula/read.test.ts b/src/typed/formula/read.test.ts index a395b78..f3811d1 100644 --- a/src/typed/formula/read.test.ts +++ b/src/typed/formula/read.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { Package } from '../../model/package'; import type { XmlElement } from '../../model/node'; +import { CONTENT_FORMAT_VERSION } from 'document-schema.js'; import { el, txt } from '../../xml/fragment'; import { readOdfFormula, readOdfFormulaDocument } from './read'; @@ -139,7 +140,7 @@ describe('readOdfFormulaDocument', () => { if (document.kind !== 'formula') { throw new Error('expected a formula-kind ContentDocument'); } - expect(document.formatVersion).toBe(2); + expect(document.formatVersion).toBe(CONTENT_FORMAT_VERSION); expect(document.metadata.title).toBe('Pythagoras'); expect(document.formula.starMath).toBe('f(x) = {x^2} over {2} + sqrt {x}'); expect(document.formula.mathml).toEqual(readOdfFormula(realFormulaPackage()).mathml); diff --git a/src/typed/odt/read.test.ts b/src/typed/odt/read.test.ts index efd1a06..ba337b2 100644 --- a/src/typed/odt/read.test.ts +++ b/src/typed/odt/read.test.ts @@ -78,15 +78,17 @@ describe('readOdt: kitchen-sink.odt (real LibreOffice output)', () => { expect(section.margins.rightPt).toBeCloseTo(knownLength('1.499cm'), 5); }); - it('maps a level-1 heading (text:h, text:outline-level="1") onto styleId "Heading1"', () => { + it('maps a level-1 heading (text:h, text:outline-level="1") onto styleId "Heading1" and headingLevel 1', () => { const chapterOne = asParagraph(blocks[0]); expect(chapterOne.styleId).toBe('Heading1'); + expect(chapterOne.headingLevel).toBe(1); expect(chapterOne.runs.map((r) => r.text).join('')).toBe('Chapter One'); }); - it('maps a level-2 heading onto styleId "Heading2"', () => { + it('maps a level-2 heading onto styleId "Heading2" and headingLevel 2', () => { const sectionHeading = blocks.find((b) => b.kind === 'paragraph' && b.runs[0]?.text === 'Section One Point One'); expect(asParagraph(sectionHeading).styleId).toBe('Heading2'); + expect(asParagraph(sectionHeading).headingLevel).toBe(2); }); it('reads plain multi-paragraph body text in document order', () => { @@ -163,6 +165,7 @@ describe('readOdt: kitchen-sink.odt (real LibreOffice output)', () => { const tableSection = asParagraph(blocks.find((b) => b.kind === 'paragraph' && b.runs[0]?.text === 'Table Section')); expect(tableSection.list).toBeUndefined(); expect(tableSection.styleId).toBe('Heading1'); + expect(tableSection.headingLevel).toBe(1); }); it('reads a table with a genuinely merged cell: colSpan on the anchor cell, an empty placeholder cell for the covered cell (mirroring ooxml.js\'s own vMerge-continuation convention), and the third cell unaffected', () => { @@ -192,6 +195,7 @@ describe('readOdt: kitchen-sink.odt (real LibreOffice output)', () => { expect(chapterTwoIndex).toBeGreaterThan(-1); const chapterTwo = asParagraph(blocks[chapterTwoIndex]); expect(chapterTwo.styleId).toBe('Heading1'); + expect(chapterTwo.headingLevel).toBe(1); const closing = asParagraph(blocks[chapterTwoIndex + 1]); expect(closing.runs.map((r) => r.text).join('')).toContain("second chapter's opening paragraph"); }); @@ -221,6 +225,7 @@ describe('readOdt: minimal.odt (real LibreOffice output, default/unmodified page expect(section.blocks).toHaveLength(2); const heading = asParagraph(section.blocks[0]); expect(heading.styleId).toBe('Heading1'); + expect(heading.headingLevel).toBe(1); expect(heading.runs.map((r) => r.text).join('')).toBe('Minimal Document'); const body = asParagraph(section.blocks[1]); expect(body.list).toBeUndefined(); diff --git a/src/typed/odt/read.ts b/src/typed/odt/read.ts index a44b246..676b67a 100644 --- a/src/typed/odt/read.ts +++ b/src/typed/odt/read.ts @@ -11,7 +11,7 @@ import { parseOdfLength } from '../shared/units'; // Package -> OdtDocument: the first end-to-end ODF content reader, producing GENUINE ContentSection[] values (document-schema.js's own pivot type, the one documents.js's docx flow/pagination engine already consumes) from a real .odt package. This is the concrete proof of the whole odf.js architectural bet -- that odt and docx can share one pivot and one layout algorithm despite being completely unrelated XML formats -- so every mapping below is deliberately expressed in terms document-schema.js already defines, never a lookalike shape of its own. // -// This reader is deliberately thin: paragraph/run reading (readOdfParagraph) and table reading (readOdfTable) already live in typed/shared/ -- built for reuse across odt/ods/odp/odg, not odt-specific -- so this module's own job is the odt-SPECIFIC structure those shared readers have no opinion on: walking office:text's actual block sequence (paragraphs interleaved with lists and tables, in document order), turning a text:list's purely structural nesting into ContentParagraph.list (numId/level), mapping text:h's own text:outline-level onto a docx-equivalent styleId, and resolving the document's own page geometry from its first master page. readOdfParagraph is tag-agnostic (it never inspects which tag its own caller found it at) and reads text:h exactly as it reads text:p, so this reader calls straight through to it for both, then overrides ONLY the resulting styleId for a heading -- see readParagraphOrHeading below. +// This reader is deliberately thin: paragraph/run reading (readOdfParagraph) and table reading (readOdfTable) already live in typed/shared/ -- built for reuse across odt/ods/odp/odg, not odt-specific -- so this module's own job is the odt-SPECIFIC structure those shared readers have no opinion on: walking office:text's actual block sequence (paragraphs interleaved with lists and tables, in document order), turning a text:list's purely structural nesting into ContentParagraph.list (numId/level), mapping text:h's own text:outline-level onto a docx-equivalent styleId alongside document-schema.js's own headingLevel field, and resolving the document's own page geometry from its first master page. readOdfParagraph is tag-agnostic (it never inspects which tag its own caller found it at) and reads text:h exactly as it reads text:p, so this reader calls straight through to it for both, then overrides ONLY the resulting heading identity (styleId plus headingLevel) for a heading -- see readParagraphOrHeading below. // // SCOPE, matching ooxml.js's own readDocx's already-established, deliberately narrower gaps (see that module's own top-of-file note for the identical reasoning applied to OOXML): footnotes/endnotes, annotations/comments, header/footer content, inline frames/images (draw:frame inside text flow -- odp/odg's job, not odt's), fields beyond their cached/last-computed text value, change tracking (text:change-*), cell borders, explicit page breaks (fo:break-before/fo:break-after -- not modelled by styles/properties.ts's StyleProperties, so the cascade this reader relies on can't surface it; a genuinely separate, bounded follow-on), and documents with more than one master page (only the first is read, in document order -- see readFirstMasterPageGeometry below). A text:h or a nested text:list/text:table inside a table cell is also out of scope here, inherited directly from readOdfTable's own cell reading (table:table-cell content there is read as text:p only) -- not a gap introduced by this module. src/typed/formula/read.ts does not exist yet at the time this reader was written, so there is no formula-embedding recursion to account for either. List marker GLYPHS (the exact bullet character or number format string) remain unread -- only the ordered-vs-bullet KIND is resolved (see resolveListKind below), since that is what downstream consumers need to render
    vs