diff --git a/package.json b/package.json index 792a54a..c48ed5b 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "license": "MIT", "packageManager": "pnpm@11.6.0", "dependencies": { - "document-schema.js": "^3.3.0", + "document-schema.js": "^4.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 cd9665f..b0c029d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: .: dependencies: document-schema.js: - specifier: ^3.3.0 - version: 3.3.0 + specifier: ^4.0.0 + version: 4.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@3.3.0: - resolution: {integrity: sha512-Jcg+sLEKF6T2w6WdCD1S0DcKFt1Kpk/rO1AZlE7fi0pqN4Ys8tN3qeY97DPc78QRy+EkbCW5hKLwM0EyV+Ojqw==} + document-schema.js@4.0.0: + resolution: {integrity: sha512-aCBeWHDubmchJBXPx7ZesLmGgOCAPOEI3n/PzunPLBEUq/w+hGpl/XLczGyyPkURu7nKR+dP6M1p7u6k4YxDsw==} engines: {node: '>=20'} dot-prop@5.3.0: @@ -4540,7 +4540,7 @@ snapshots: dependencies: path-type: 4.0.0 - document-schema.js@3.3.0: + document-schema.js@4.0.0: dependencies: zod: 4.4.3 diff --git a/src/typed/formula/read.test.ts b/src/typed/formula/read.test.ts index f3811d1..9d257f1 100644 --- a/src/typed/formula/read.test.ts +++ b/src/typed/formula/read.test.ts @@ -1,7 +1,6 @@ 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'; @@ -140,7 +139,6 @@ describe('readOdfFormulaDocument', () => { if (document.kind !== 'formula') { throw new Error('expected a formula-kind ContentDocument'); } - 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/formula/read.ts b/src/typed/formula/read.ts index c81a174..c774627 100644 --- a/src/typed/formula/read.ts +++ b/src/typed/formula/read.ts @@ -1,5 +1,4 @@ import type { ContentDocument, LayoutMetadata } from 'document-schema.js'; -import { CONTENT_FORMAT_VERSION } from 'document-schema.js'; import type { XmlElement, XmlNode } from '../../model/node'; import type { Package } from '../../model/package'; import { attrValue, elementsWithTag, rootElement } from '../../xml/query'; @@ -100,7 +99,6 @@ export function readOdfFormulaDocument(pkg: Package): ContentDocument { return { kind: 'formula', - formatVersion: CONTENT_FORMAT_VERSION, metadata, formula: starMath === undefined ? { mathml } : { mathml, starMath }, }; diff --git a/src/typed/odb/form.ts b/src/typed/odb/form.ts index 0b371fd..d65c4d2 100644 --- a/src/typed/odb/form.ts +++ b/src/typed/odb/form.ts @@ -11,7 +11,7 @@ import { subDocumentPackage } from './subdocument'; // EMPIRICALLY CONFIRMED against real, unmodified LibreOffice 26.2 output (src/typed/odb/fixtures/form-and-report.odb -- see typed/odb/read.ts's own top-of-file note for how that fixture was generated and cross-verified), not assumed: // // 1. A form sub-document is a COMPLETE, ordinary ODF TEXT document. Its own directory holds content.xml/styles.xml/settings.xml (plus a manifest.rdf), its manifest:media-type is "application/vnd.oasis.opendocument.text", and its content.xml root is the usual office:document-content/office:body/office:text. readOdt therefore reads it unmodified through a synthetic sub-Package (see subdocument.ts) -- no form-specific text reader needed, and the paragraphs/tables a form's designer laid out around its controls come back exactly as they would from a standalone .odt. -// 2. The control tree hangs off office:text/office:forms, NOT off the drawing layer. office:forms holds one form:form per top-level form; a control is a form: ELEMENT (form:text, form:formatted-text, form:listbox, form:fixed-text, form:checkbox, ...) whose own form:data-field names the bound column. The drawing layer separately carries a draw:control element per control, referencing the control by its form:id -- that is the control's own GEOMETRY (position/size/anchor), which readOdt already reads as ordinary document content and which this reader deliberately does not re-derive. +// 2. The control tree hangs off office:text/office:forms, NOT off the drawing layer. office:forms holds one form:form per top-level form; a control is a form: ELEMENT (form:text, form:formatted-text, form:listbox, form:fixed-text, form:checkbox, ...) whose own form:data-field names the bound column. The drawing layer separately carries a draw:control element per control, referencing the control by its form:id -- that is the control's own GEOMETRY (position/size/anchor), which no reader here resolves today: readBlocks (typed/odt/read.ts) has no draw:control branch, and the ods shape walker skips the element explicitly (see typed/ods/read.ts's collectAnchoredFrames note), so control geometry is dropped entirely rather than re-derived here. // 3. A form:form can NEST another form:form (a real Base sub-form, bound to its own command -- the fixture's own "HighValueSubForm" is a genuine nested form:form bound to a QUERY while its parent is bound to a TABLE). Sub-forms are consequently modelled as their own recursive OdbFormDefinition list rather than flattened into the parent's controls. // 4. form:properties (an untyped bag of form:property elements carrying UNO property values LibreOffice round-trips for its own benefit -- PropertyChangeNotificationEnabled, DefaultControl, ObjIDinMSO, ...) appears on the form and on most controls. It is deliberately never read: none of it is form STRUCTURE, and surfacing a producer-specific property bag would invite callers to depend on LibreOffice internals. // diff --git a/src/typed/ods/read.ts b/src/typed/ods/read.ts index bbb2a1a..726194f 100644 --- a/src/typed/ods/read.ts +++ b/src/typed/ods/read.ts @@ -14,7 +14,7 @@ import type { LayoutMetadata, Margins, } 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 { XmlElement, XmlNode } from '../../model/node'; import type { Package } from '../../model/package'; import { attrValue, childrenWithTag, findChildElement, rootElement } from '../../xml/query'; @@ -225,19 +225,19 @@ function readEmbeddedObjectDocument(reference: EmbeddedDrawObject): ContentDocum switch (reference.objectKind) { case 'wordprocessing': { const { metadata, sections } = readOdt(reference.package); - return { kind: 'wordprocessing', formatVersion: CONTENT_FORMAT_VERSION, metadata, sections }; + return { kind: 'wordprocessing', metadata, sections }; } case 'presentation': { const { metadata, slides } = readOdp(reference.package); - return { kind: 'presentation', formatVersion: CONTENT_FORMAT_VERSION, metadata, slides }; + return { kind: 'presentation', metadata, slides }; } case 'drawing': { const { metadata, pages } = readOdg(reference.package); - return { kind: 'drawing', formatVersion: CONTENT_FORMAT_VERSION, metadata, pages }; + return { kind: 'drawing', metadata, pages }; } case 'spreadsheet': { const { metadata, sheets } = readOds(reference.package); - return { kind: 'spreadsheet', formatVersion: CONTENT_FORMAT_VERSION, metadata, sheets }; + return { kind: 'spreadsheet', metadata, sheets }; } case 'formula': // The one embedded kind whose own reader already returns a finished ContentDocument (readOdfFormulaDocument), because a formula document has no per-format {metadata, sections/slides/pages/sheets} shape to re-wrap -- its whole content IS the MathML. diff --git a/src/typed/odt/read.ts b/src/typed/odt/read.ts index 9ad2cb5..4e26e05 100644 --- a/src/typed/odt/read.ts +++ b/src/typed/odt/read.ts @@ -14,7 +14,7 @@ import { parseOdfLength } from '../shared/units'; // // 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), 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 typed/shared/list.ts's resolveOdfListKind), since that is what downstream consumers need to render
    vs
      . +// 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 in their entirety (no field branch exists in run collection -- readOdfParagraph drops a field child without even its cached/last-computed text value), change tracking (text:change-*), 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 typed/shared/list.ts's resolveOdfListKind), since that is what downstream consumers need to render
        vs
          . // // LIST HANDLING: the numId minting convention (a monotonically increasing per-encounter counter, never text:style-name), the ordered:/bullet: kind prefix, and the text:list/text:list-item structural nesting walk itself all live in typed/shared/list.ts -- read that module's own top-of-file notes in full for the derivation -- because the odp reader meets the IDENTICAL text:list construct inside slide text frames and shares every line of it. This reader's own remaining list responsibility is the one genuinely odt-specific part: threading a single document-wide OdfListIdState through its office:text walk, so list identities are unique across the whole body exactly as they are across a whole presentation's slides.