js.documents 1.81.1 → 1.81.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -6
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -123,7 +123,14 @@ const { document, diagnostics } = await converter.convert(
|
|
|
123
123
|
);
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
`DocumentFormat` includes `xlsx` and `markdown` alongside `docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`pdf` — xlsx because `createLocalDocumentConverter`'s `{ source, targetFormat }` contract already generalises past "targetFormat always means pdf" (xlsx has no PDF conversion of its own; markdown genuinely does, see `markdownToPdf`/`pdfToMarkdown` above). `odt`→`docx`, `docx`→`odt`, `odp`→`pptx`, `pptx`→`odp`, `ods`→`xlsx`, `xlsx`→`ods`, `markdown`→`docx`, `docx`→`markdown`, `markdown`→`odt`, and `odt`→`markdown` are ten further entries in the same `conversions` list, routed to the ten bridge functions above with an empty `diagnostics` array.
|
|
126
|
+
`DocumentFormat` includes `xlsx` and `markdown` alongside `docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`odf`/`pdf` — ten members in total — xlsx because `createLocalDocumentConverter`'s `{ source, targetFormat }` contract already generalises past "targetFormat always means pdf" (xlsx has no PDF conversion of its own; markdown genuinely does, see `markdownToPdf`/`pdfToMarkdown` above). `odt`→`docx`, `docx`→`odt`, `odp`→`pptx`, `pptx`→`odp`, `ods`→`xlsx`, `xlsx`→`ods`, `markdown`→`docx`, `docx`→`markdown`, `markdown`→`odt`, and `odt`→`markdown` are ten further entries in the same `conversions` list, routed to the ten bridge functions above with an empty `diagnostics` array. `DocumentFormat` itself is inferred from a real Zod schema, `DocumentFormatSchema`, rather than hand-written — both it and `DOCUMENT_FORMATS` (every member as a plain `readonly DocumentFormat[]`, derived from that same schema so it cannot drift out of sync) are exported, for a caller that wants to enumerate or validate against the full format set without constructing its own schema — a CLI's own usage-error text, or an MCP tool's JSON-schema `enum` input:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { DOCUMENT_FORMATS, DocumentFormatSchema } from 'documents.js';
|
|
130
|
+
|
|
131
|
+
console.log(DOCUMENT_FORMATS); // ['docx', 'pptx', 'xlsx', 'odt', 'odp', 'ods', 'odg', 'odf', 'markdown', 'pdf']
|
|
132
|
+
DocumentFormatSchema.parse(userSuppliedFormat); // throws a ZodError for anything outside that list
|
|
133
|
+
```
|
|
127
134
|
|
|
128
135
|
Getting back the intermediate `DocumentPackage` (content + layout, from `document-schema.js`) a conversion built internally, instead of only the target bytes — every ergonomic conversion function above accepts an `onDocument` callback for this, and the port surfaces the same value as `package` on its `ConversionResult`:
|
|
129
136
|
|
|
@@ -160,6 +167,43 @@ const { kind, value } = documentFromJson(JSON.parse(readFileSync('converted.doc.
|
|
|
160
167
|
|
|
161
168
|
`contentDocumentWithSchema`/`layoutDocumentWithSchema` are the `ContentDocument`/`LayoutDocument` equivalents — these operate on the identical `ContentDocument`/`ContentDocumentSchema` this package imports and re-exports from `document-schema.js` above (a discriminated union of `wordprocessing`/`presentation`/`spreadsheet`/`drawing` variants wrapping `ContentSection`/`ContentSlide`/`ContentSheet`/`ContentDrawPage`), so no separate import or conversion step is needed to construct one for `contentDocumentWithSchema`.
|
|
162
169
|
|
|
170
|
+
Building any `DocumentFormat`'s own bytes back out of an already-assembled `DocumentPackage`, instead of only ever getting one out of a conversion's own `onDocument` callback — `buildDocumentBytes` is the reverse of that callback: `'pdf'` writes the package's own `LayoutDocument` half directly (throwing if the package carries none — only a `<format>-to-pdf`/`pdf-to-<format>` conversion's own dump has one; a bridge conversion's own dump, e.g. `odtToDocx`, never does), `'odf'` (a standalone formula document) has no builder at all and throws outright, and every other target rebuilds a fresh package from the `ContentDocument` half through the identical `buildXPackage` function the matching `pdfToX`/bridge conversion already uses — xlsx included, via `ooxml.js`'s own `buildXlsxPackage`:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { buildDocumentBytes, docxToPdf } from 'documents.js';
|
|
174
|
+
|
|
175
|
+
let captured;
|
|
176
|
+
docxToPdf(docxBytes, { onDocument: (pkg) => { captured = pkg; } });
|
|
177
|
+
const pdfBytesAgain = buildDocumentBytes(captured, 'pdf');
|
|
178
|
+
const docxBytesAgain = buildDocumentBytes(captured, 'docx'); // rebuilds via buildDocxPackage, same as pdfToDocx's own package-building half
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Decoding/encoding a `DocumentFormat`'s own raw package container directly, without going through `ContentDocument` at all — the format-aware counterpart to `ooxml.js`'s/`odf.js`'s own `decodePackage`/`encodePackage`, for a caller holding a format + bytes rather than already knowing which of the two underlying container codecs applies. `decodeDocumentPackage`/`encodeDocumentPackage` dispatch docx/pptx/xlsx through `ooxml.js`'s OPC codec and odt/odp/ods/odg/odf through `odf.js`'s ODF codec, throwing `UnsupportedPackageFormatError` for `'markdown'`/`'pdf'` (neither has a raw-package concept at all — markdown is plain text, not a zip container, and PDF is its own binary format, not OPC/ODF). `decodeOdbPackage` is the `.odb`-specific sibling: `'odb'` is deliberately not a `DocumentFormat` member (see the `.odb` entries below), but its bytes are an ordinary ODF package, decoded through the identical `odf.js` `decodePackage` every `readOdb*`/`odbTo*` function below already starts from — there is no `encodeOdbPackage`, since nothing in this package's `.odb` support ever writes a new `.odb` file:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { decodeDocumentPackage, decodeOdbPackage, encodeDocumentPackage } from 'documents.js';
|
|
185
|
+
|
|
186
|
+
const pkg = decodeDocumentPackage('docx', docxBytes); // -> ooxml.js's own Package
|
|
187
|
+
const docxBytesAgain = encodeDocumentPackage('docx', pkg);
|
|
188
|
+
|
|
189
|
+
const odbPkg = decodeOdbPackage(odbBytes); // -> odf.js's own Package -- feed straight into readOdbTables/readOdbInventory/etc. below
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Reading a document's own `title`/`author`/`subject`/`keywords`/`creator`/`producer`/`created`/`modified`, or patching its `title`/`author`/`subject`/`keywords` (the four fields `MetadataOverrides` covers), across any of the ten `DocumentFormat`s, without caring which underlying reader/writer a given format uses:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { readDocumentMetadata, setDocumentMetadata } from 'documents.js';
|
|
196
|
+
|
|
197
|
+
const metadata = readDocumentMetadata('docx', docxBytes); // -> LayoutMetadata
|
|
198
|
+
console.log(metadata.title, metadata.author);
|
|
199
|
+
|
|
200
|
+
const patchedBytes = setDocumentMetadata('docx', 'docx', docxBytes, { title: 'New title', keywords: ['a', 'b'] });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`setDocumentMetadata` patches metadata in place; it does not convert format — `sourceFormat` and `targetFormat` must match (or both be `'pdf'`), and it throws naming which one to fix otherwise. A `'pdf'` source/target patches the parsed `LayoutDocument` directly (no `ContentDocument`, no layout engine — genuinely lossless for everything else on the page); every other supported format (`docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`markdown`/`xlsx`) rebuilds a fresh package from that format's own `ContentDocument`, which costs whatever that format's own `buildXPackage` already costs (docx, for instance, still drops comments/footnotes/headers-footers/numbering on a rebuild — see `readDocxExtras` above). `xlsx` now rebuilds through this same path too — it is no longer rejected. `'odf'` (a standalone formula document) is rejected outright in both directions, since it has no write path back out at all. An override omitted from the call (rather than passed as an empty string/array) leaves that field exactly as the source document already had it, matching every other partial-update convention in this package.
|
|
204
|
+
|
|
205
|
+
`readDocumentMetadata('xlsx', ...)` is the one deliberate, named exception to the "dispatch by format" rule above: rather than reading a fresh `ContentDocument.metadata` directly (which leaves `createdIso`/`modifiedIso`/`producer` unset), it renders the workbook to PDF via `xlsxToPdf` and reads `.metadata` off that PDF instead — kept because a direct `readXlsxContent(...).metadata` and that PDF-preview path genuinely disagree on those three fields, not merely incidentally (confirmed directly, `src/metadata/read.test.ts`'s own xlsx case). `setDocumentMetadata`/`buildDocumentBytes` do **not** carry this exception: both now treat xlsx uniformly with every other rebuildable format, via `ooxml.js`'s own `readXlsxContent`/`buildXlsxPackage`.
|
|
206
|
+
|
|
163
207
|
Reading and editing docx/pptx content directly, without going through PDF at all:
|
|
164
208
|
|
|
165
209
|
```ts
|
|
@@ -316,7 +360,7 @@ const reports = readOdbReports(decodePackage(odbBytes)); // OdbReport[] -- each
|
|
|
316
360
|
|
|
317
361
|
This is *structure*, not *rendering* — but rendering is now a real thing this package does with it, and `readOdbReportContent` below is the whole chain in one call: it resolves the report's own query against the data, evaluates its bands' formulas over the result, and lays the printed bands out as a real `ContentDocument`. What is *not* offered is a pixel-faithful reproduction of Report Builder's own page output; see [Fidelity](#fidelity) for exactly where that line falls.
|
|
318
362
|
|
|
319
|
-
`readOdbTables` takes a decoded `Package` (matching `readOdtContent`/`readOdsContent`/etc.'s own convention), while `odbToXlsx`/`odbToCsv` take raw bytes and decode them internally, matching every other ergonomic conversion in this package. `.odb` has no `odbToPdf` ergonomic conversion and no reverse (xlsx/CSV → `.odb`) direction, and — like `odmToPdf` — is not wired into the `DocumentConverter` port below: the write direction would need a real embedded SQL engine this package deliberately does not implement, and `.odb` has no single natural target format, since a database front-end's tables, its saved queries, and its reports are three unrelated output shapes rather than one. A rendered report is
|
|
363
|
+
`readOdbTables` takes a decoded `Package` (matching `readOdtContent`/`readOdsContent`/etc.'s own convention), while `odbToXlsx`/`odbToCsv` take raw bytes and decode them internally, matching every other ergonomic conversion in this package. `.odb` has no `odbToPdf` ergonomic conversion over the whole database and no reverse (xlsx/CSV → `.odb`) direction, and — like `odmToPdf` — is not wired into the `DocumentConverter` port below: the write direction would need a real embedded SQL engine this package deliberately does not implement, and `.odb` as a whole has no single natural target format, since a database front-end's tables, its saved queries, and its reports are three unrelated output shapes rather than one. A rendered *report* is a narrower, real exception to that: it is an ordinary wordprocessing `ContentDocument`, so `odbReportToDocx`/`odbReportToOdt`/`odbReportToPdf` (see below) dispatch it to real bytes the same one-call way every other ergonomic conversion in this package does.
|
|
320
364
|
|
|
321
365
|
`readFirebirdBackup` (`src/firebird/backup.ts`) is also exported individually, for a caller that has already extracted a Firebird-backed `.odb`'s own `database/firebird.fbk` bytes and wants to decode them directly without going through a `Package` at all:
|
|
322
366
|
|
|
@@ -371,6 +415,18 @@ const document = readOdbReportContent(decodePackage(odbBytes)); // a 'wordproces
|
|
|
371
415
|
const another = readOdbReportContent(decodePackage(odbBytes), { report: 'SalesByRegion' }); // required whenever the .odb declares more than one
|
|
372
416
|
```
|
|
373
417
|
|
|
418
|
+
`odbReportToDocx`/`odbReportToOdt`/`odbReportToPdf` are the last step, dispatching a rendered report's own `ContentDocument` to real bytes the same "read/render → encode" shape every other ergonomic conversion in this package has — they take the `ContentDocument` `readOdbReportContent` already produced, not a `Package`, since a rendered report has no source package of its own left to round-trip through:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { decodePackage } from 'odf.js';
|
|
422
|
+
import { odbReportToDocx, odbReportToOdt, odbReportToPdf, readOdbReportContent } from 'documents.js';
|
|
423
|
+
|
|
424
|
+
const report = readOdbReportContent(decodePackage(odbBytes), { report: 'SalesByRegion' });
|
|
425
|
+
const docxBytes = odbReportToDocx(report); // via buildDocxPackage -- takes the same onMathDiagnostic every other ContentDocument-to-docx entry point does, though a report control's own text is plain and never triggers it
|
|
426
|
+
const odtBytes = odbReportToOdt(report); // via buildOdtPackage
|
|
427
|
+
const pdfBytes = odbReportToPdf(report); // via convertWordprocessingToLayout + writePdf -- options are DocumentToPdfOptions verbatim, the same type docxToPdf/odtToPdf/markdownToPdf already use; throws if content is not the wordprocessing variant readOdbReportContent always produces
|
|
428
|
+
```
|
|
429
|
+
|
|
374
430
|
Resolving the report's own `rpt:command`/`rpt:command-type` binding is the one part the formula engine never saw: `"table"` means the command names a table and the report reads all of it (turned into a real `SELECT * FROM "<table>"` and run through the same engine, rather than a second resolution rule that could disagree with it), `"query"` means it names a saved query in the `.odb`'s own `db:queries` whose `db:command` holds the SQL, and `"command"` means the command *is* the SQL. Rows arrive in that command's own `ORDER BY` order, and the report's `rpt:sort-expression` is deliberately *not* applied on top — a group's sort expression is a bare column name, so re-sorting by it would discard whatever finer ordering the command already asked for (the real fixture's saved query orders `REGION`, `QUARTER`, then `AMOUNT` **descending**, and the two group sort expressions name only the first two).
|
|
375
431
|
|
|
376
432
|
Each printed band becomes one single-row `ContentTable`, one cell per control, in document order — the same shape the band has in the report file itself, where every band *is* a `table:table` whose cells hold its controls. Every cell's paragraph carries the band's own name as its `styleId` (`Report Header`, `Page Header`, `Group Header 1`, `Detail`, `Group Footer 1`, `Report Footer`, …), so which band a block printed from survives into the document rather than having to be inferred from its position. Its three stages stay independently usable like every other `.odb` stage: `odbReportCommandSql` (a report → the SQL it issues), `resolveOdbReportRows` (a package + a report → those rows), and `renderOdbReportContent` (a report + any equivalently-shaped rows → the document — useful for rendering the same report over an unfiltered table, say). See [Fidelity](#fidelity) for what "structural, not pixel-faithful" means here in detail.
|
|
@@ -484,6 +540,14 @@ Two honest limits, both structural rather than provisional. An embedded face is
|
|
|
484
540
|
|
|
485
541
|
`extractOoxmlEmbeddedFonts`/`extractOdfEmbeddedFonts`, `extractSourceFonts`, and `createDocumentFontRegistry` are exported for a caller composing `readXContent` → `convertXToLayout` → `writePdf` themselves rather than going through an ergonomic conversion.
|
|
486
542
|
|
|
543
|
+
`extractSourceFontsForFormat` is the `DocumentFormat`-aware counterpart to `extractSourceFonts` above, for a caller holding a format + bytes rather than an already-decoded `Package`: docx/pptx decode via `ooxml.js`'s own `decodePackage`, odt/odp/ods/odg via `odf.js`'s. `xlsx`, `pdf`, `markdown`, and `odf` (a standalone formula document, which embeds only the STIX Two Math font pdf-codec itself carries, never a caller-resolvable face) throw `UnsupportedFontSourceFormatError` — none of the four has a source-embedded-font concept of its own to extract:
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import { extractSourceFontsForFormat } from 'documents.js';
|
|
547
|
+
|
|
548
|
+
const faces = extractSourceFontsForFormat('docx', docxBytes); // -> readonly ProvidedFont[], the same shape createDocumentFontRegistry consumes
|
|
549
|
+
```
|
|
550
|
+
|
|
487
551
|
## Architecture
|
|
488
552
|
|
|
489
553
|
The package is layered from generic primitives outward to the two conversion directions:
|
|
@@ -498,7 +562,7 @@ The package is layered from generic primitives outward to the two conversion dir
|
|
|
498
562
|
- **`src/mathml/`** — a MathML presentation-layer typesetting engine, comparable in scope to pdf-codec's own standard-14 text-layout half — genuinely self-contained: no import from `model`, `pdf-codec`, or `odf.js` at all (not even `document-schema.js`), matching `src/layout/`'s own "pure conversion algorithm" isolation one tier further down. `nodes.ts` defines `MathMlNode`/`MathMlElement` as a local, structurally-compatible mirror of `odf.js`'s own `XmlNode` (the same "mirror the shape, don't import the package" trick `src/interop.test.ts` already proves holds between `ooxml.js` and `odf.js`), so `odf.js`'s `readOdfFormula`'s real return value type-checks against it with zero cast. `variant.ts` maps `mathvariant` to the Unicode Mathematical Alphanumeric Symbols block (Latin/Greek/digits, including the block's own well-known Letterlike-Symbols hole-fillers — italic small h, eleven Script/Fraktur/Double-struck capitals — generated directly from Unicode's own `UnicodeData.txt`, not transcribed by hand). `operators.ts` is a deliberately bounded operator dictionary (lspace/rspace/stretchy/largeop/movablelimits per operator), not the MathML3 spec's own multi-thousand-entry table. `layout.ts` is the recursive box-model engine itself (`mrow`/`mi`/`mn`/`mo`/`mtext`/`mspace`/`msub`/`msup`/`msubsup`/`munder`/`mover`/`munderover`/`mfrac`/`msqrt`/`mroot`/`mtable`/`mtr`/`mtd`/`mstyle`/`semantics`, plus a text-content fallback with a diagnostic for anything else), driven entirely by the injected `MathFontMetrics` port (`metrics.ts`) rather than any font-parsing code of its own — pdf-codec's own `math-font.ts` is the real implementation, consumed only through this structural port, never imported directly. `compose.ts`/`radical.ts`/`length.ts` are its own small geometry helpers (baseline-offset box placement, a hand-drawn hooked radical sign built from line segments rather than a bare glyph substitute, MathML length-unit parsing). `layout.ts` additionally stretches a row's own vertical fences through the same `MathFontMetrics` port (its `stretch` method resolves the font's OpenType MATH `MathVariants` data into positioned glyph IDs), emitting them as `MathAssembledGlyphs` items — the one item kind addressed by glyph ID rather than by Unicode text, because most of the glyphs such a construction names have no code point at all. Output is a flat `MathBox` (positioned glyph runs, rules, strokes, and assembled glyph placements, box-local top-left/y-down coordinates), passed with zero cast into pdf-codec's `writePdf({ formulas })` — see pdf-codec's own README for the structural-typing mechanism that makes this work across a package boundary with no shared class or branded type.
|
|
499
563
|
- **`src/omml/`** — the MathML ⇄ OMML (Office Math Markup Language, ECMA-376 Part 1 §22.1's own `m:` vocabulary) structural translator, both directions. `write.ts`'s `buildOfficeMath`/`buildOfficeMathParagraph` are the write side, the counterpart to `src/mathml/`'s own typesetting engine, covering the identical construct set deliberately, so a formula rendered to PDF and the same formula written into a docx degrade in exactly the same places rather than one being silently better than the other: each MathML construct maps onto its real OMML element (`mfrac` → `m:f`, `msqrt`/`mroot` → `m:rad` with `m:radPr/m:degHide` and the degree/radicand order reversed, `msub`/`msup`/`msubsup` → `m:sSub`/`m:sSup`/`m:sSubSup`, `munder`/`mover` → `m:limLow`/`m:limUpp` and `munderover` → the two nested, `mtable`/`mtr`/`mtd` → `m:m`/`m:mr`/`m:e` with per-column `m:mcs`/`m:mc` justification, and every token element → an `m:r`/`m:t` run whose `mathvariant` becomes OMML's own `m:scr` script + `m:sty` style pair). `read.ts`'s `readOfficeMath`/`collectOfficeMathElements` are the read side, the structural inverse of every one of those mappings, and read STRICTLY MORE than the writer writes — deliberately, since the writer only ever has to express what MathML can say while the reader has to cope with whatever Word itself authored: `m:d` (Word's representation of every parenthesised sub-expression), `m:nary` (a sum/product/integral with limits AND its own operand), `m:acc`, `m:bar`, `m:func`, and `m:sPre` each have one exact MathML inverse and no writer counterpart at all. Both directions emit no geometry, measure nothing, and load no font — this is a vocabulary translation, not a rendering. The directory lives outside `src/mathml/` for that directory's own isolation rule: `write.ts`'s whole output type (and `read.ts`'s whole input type) is `ooxml.js`'s `XmlElement`, and `src/mathml/` imports no package at all. `shared.ts` holds what neither direction owns: the `OmmlDiagnostic` shape both report through, the one `mathvariant` ⇄ `m:scr`/`m:sty` table each reads in its own direction, and `mi`'s own intrinsic-variant default. `buildDocxPackage` and `readDocxContent` are their real callers; a construct with no counterpart in the target vocabulary degrades on its own, with a diagnostic, exactly as `src/mathml/layout.ts`'s own `unsupported` fallback does for the PDF path.
|
|
500
564
|
- **`src/ooxml/`** — resolves a `Package` into a `ContentDocument`: `docx/read.ts` and `pptx/read.ts` are now thin adapters over `ooxml.js`'s own `readDocx`/`readPptx`, wrapping their `{ metadata, sections }`/`{ metadata, slides }` result into `ContentDocument`'s `wordprocessing`/`presentation` shape. The docx style cascade (`docDefaults` → named-style `basedOn` chains → paragraph-mark run properties → character styles → direct formatting), the pptx placeholder → layout → master → theme inheritance cascade, and DrawingML geometry/colour resolution all now live upstream in `ooxml.js` itself, not in this package. `docx/formula.ts` is the one piece of genuinely local reading work left: a second, independent pass over the same `word/document.xml`, splicing every OOXML math equation `src/omml/read.ts` recovers into the sections `readDocx` produced — needed because `readDocx` has no `m:oMath` handling at all, exactly the way `src/odf/odt/read.ts` needs its own pass for a formula `odf.js`'s `readOdt` likewise does not read. Positioning is derived rather than approximated, and by a shorter route than the ODF side's own block-counting mirror needs: every `w:p` produces exactly one top-level `ContentParagraph` block and nothing else produces one, so the Nth `w:p` in the body IS the Nth paragraph-kind block. A `w:p` carrying nothing but its equation is CONSUMED by the formula block rather than emitted alongside it, which is what keeps a `docx → odt → docx` round trip from accumulating one blank paragraph per formula per hop. `docx/extras.ts`'s `readDocxExtras` is a second, independent re-projection of that same `readDocx` call, for the data `readDocxContent` genuinely cannot carry through `ContentDocument`'s section/block shape at all: comments, footnotes, headers/footers, and numbering (`abstractNum`/`num`) definitions. It calls `readDocx` a second time rather than being fused onto `readDocxContent`'s own return value — an accepted cost matching every other "each pipeline stage independently exported" pair in this codebase — and reuses `ooxml.js`'s own `Comment`/`Footnote`/`NumberingDefinitions` types directly rather than mirroring them locally.
|
|
501
|
-
- **`src/odf/`** — the ODF-side counterpart to `src/ooxml/`, resolving an `odf.js` `Package` into a `ContentDocument`: `odt/read.ts`'s `readOdtContent` is a thin adapter over `odf.js`'s own `readOdt`, wrapping its `{ metadata, sections }` result into the identical `wordprocessing` shape `readDocxContent` produces — the concrete proof that odt and docx genuinely share one pivot and one layout engine. `odp/read.ts`'s `readOdpContent` is the same adapter over `odf.js`'s own `readOdp`, wrapping `{ metadata, slides }` into the identical `presentation` shape `readPptxContent` produces. `ods/read.ts`'s `readOdsContent` wraps `odf.js`'s `readOds`'s `{ metadata, sheets }` into the `spreadsheet` `ContentDocument` variant, and `odg/read.ts`'s `readOdgContent` wraps `odf.js`'s `readOdg`'s `{ metadata, pages }` into the `drawing` variant — `odg` still has no OOXML-side sibling adapter at all (no drawing-equivalent OOXML format this package reads); `ods` now does, `ooxml.js`'s own `readXlsxContent`/`buildXlsxPackage`, consumed directly by `src/convert/convert.ts`'s `odsToXlsx`/`xlsxToOds` bridge (see below) but deliberately not re-exported from this package's own public surface, mirroring the `readDocx`/`readPptx` non-re-export choice above. `buildOdtPackage`/`buildOdpPackage`/`buildOdsPackage`/`buildOdgPackage` (`src/edit/{odt,odp,ods,odg}/content.ts`) each bridge a `ContentDocument` back to a fresh package built on that format's own live-view editor, closing the PDF → odt/odp/ods/odg direction (`pdfToOdt`/`pdfToOdp`/`pdfToOds`/`pdfToOdg` each call the matching one) — see the `pdfToOds` gotcha below for `buildOdsPackage`'s own printSettings-writing addition. `formula/read.ts`'s `readOdfFormulaContent`/`readOdfEmbeddedFormula` are the same thin-adapter pattern over `odf.js`'s own `readOdfFormulaDocument`, for a standalone `.odf` (the whole `'formula'`-kind `ContentDocument`) and an embedded sub-object (its bare `ContentFormula`) respectively — the latter reading the sub-object's own `content.xml` directly out of the outer package's flat `Package.parts` record, no separate unzip step needed; `formula/detect.ts`'s `collectFormulaFrames`/`collectSlideFormulaFrames` are genuinely new work with no `odf.js`-side equivalent at all — `odf.js`'s own `readDrawFrameContent` doesn't recognise a `draw:object`-bearing `draw:frame` yet, so `odt/read.ts` and `odp/read.ts` each run one of these as a second pass over the same package's raw `content.xml` to find and inject a formula's own embedded-object block. `collectFormulaFrames` is a deep walk (a frame directly in the container, one nested inside a `draw:g` group with that group's own `draw:transform` composed exactly as `walkDrawShapes` composes it, and one anchored inline inside a paragraph's own run content); `collectSlideFormulaFrames` replicates `odf.js`'s own `walkDrawShapes` traversal precisely so each formula's `ContentShape` index is derived rather than guessed. See the Gotchas entry below for where each detected formula's block actually lands.
|
|
565
|
+
- **`src/odf/`** — the ODF-side counterpart to `src/ooxml/`, resolving an `odf.js` `Package` into a `ContentDocument`: `odt/read.ts`'s `readOdtContent` is a thin adapter over `odf.js`'s own `readOdt`, wrapping its `{ metadata, sections }` result into the identical `wordprocessing` shape `readDocxContent` produces — the concrete proof that odt and docx genuinely share one pivot and one layout engine. `odp/read.ts`'s `readOdpContent` is the same adapter over `odf.js`'s own `readOdp`, wrapping `{ metadata, slides }` into the identical `presentation` shape `readPptxContent` produces. `ods/read.ts`'s `readOdsContent` wraps `odf.js`'s `readOds`'s `{ metadata, sheets }` into the `spreadsheet` `ContentDocument` variant, and `odg/read.ts`'s `readOdgContent` wraps `odf.js`'s `readOdg`'s `{ metadata, pages }` into the `drawing` variant — `odg` still has no OOXML-side sibling adapter at all (no drawing-equivalent OOXML format this package reads); `ods` now does, `ooxml.js`'s own `readXlsxContent`/`buildXlsxPackage`, consumed directly by `src/convert/convert.ts`'s `odsToXlsx`/`xlsxToOds` bridge and by `src/codecs/registry.ts`'s own xlsx `content` codec (see the `src/codecs/` entry below — the latter is what lets `readDocumentMetadata`/`setDocumentMetadata`/`buildDocumentBytes` treat xlsx uniformly with the rest of `DocumentFormat`) but deliberately not re-exported from this package's own public surface, mirroring the `readDocx`/`readPptx` non-re-export choice above. `buildOdtPackage`/`buildOdpPackage`/`buildOdsPackage`/`buildOdgPackage` (`src/edit/{odt,odp,ods,odg}/content.ts`) each bridge a `ContentDocument` back to a fresh package built on that format's own live-view editor, closing the PDF → odt/odp/ods/odg direction (`pdfToOdt`/`pdfToOdp`/`pdfToOds`/`pdfToOdg` each call the matching one) — see the `pdfToOds` gotcha below for `buildOdsPackage`'s own printSettings-writing addition. `formula/read.ts`'s `readOdfFormulaContent`/`readOdfEmbeddedFormula` are the same thin-adapter pattern over `odf.js`'s own `readOdfFormulaDocument`, for a standalone `.odf` (the whole `'formula'`-kind `ContentDocument`) and an embedded sub-object (its bare `ContentFormula`) respectively — the latter reading the sub-object's own `content.xml` directly out of the outer package's flat `Package.parts` record, no separate unzip step needed; `formula/detect.ts`'s `collectFormulaFrames`/`collectSlideFormulaFrames` are genuinely new work with no `odf.js`-side equivalent at all — `odf.js`'s own `readDrawFrameContent` doesn't recognise a `draw:object`-bearing `draw:frame` yet, so `odt/read.ts` and `odp/read.ts` each run one of these as a second pass over the same package's raw `content.xml` to find and inject a formula's own embedded-object block. `collectFormulaFrames` is a deep walk (a frame directly in the container, one nested inside a `draw:g` group with that group's own `draw:transform` composed exactly as `walkDrawShapes` composes it, and one anchored inline inside a paragraph's own run content); `collectSlideFormulaFrames` replicates `odf.js`'s own `walkDrawShapes` traversal precisely so each formula's `ContentShape` index is derived rather than guessed. See the Gotchas entry below for where each detected formula's block actually lands.
|
|
502
566
|
- **`src/markdown/`** — a third, independent counterpart to `src/ooxml/`/`src/odf/`, resolving markdown text into a `ContentDocument` via the external [`markdown-codec`](https://github.com/ExaDev/markdown-codec) dependency rather than a package format: `read.ts`'s `readMarkdownContent` is a thin adapter over `markdown-codec`'s own `readMarkdown`, re-stamping `documents.js`'s own `CONTENT_FORMAT_VERSION` onto a fresh envelope (`markdown-codec`'s `readMarkdown` already produces a full `document-schema.js` `ContentDocument`, structurally identical to but nominally separate from this package's local one) — mirroring `readOdtContent`/`readDocxContent` exactly, and the concrete third proof (after odt/docx) that this pivot and layout engine are genuinely format-agnostic. `write.ts`'s `buildMarkdownText` is the reverse, a thin wrapper over `markdown-codec`'s own `writeMarkdown` — deliberately living beside `read.ts` rather than under `src/edit/markdown/`, since `MarkdownEditor` (`src/edit/markdown/editor.ts`) calls it directly as its own `toMarkdownText` rather than this module reaching back into `src/edit/`. `MarkdownEditor` does now exist, alongside `DocxEditor`/`OdtEditor`/etc., but it holds a mutable in-memory `ContentDocument` rather than a real `XmlElement` tree inside a decoded `Package` — markdown has no such tree at all — so every `MarkdownParagraph`/`MarkdownRun`/`MarkdownTable`/`MarkdownTableCell` it produces holds a direct reference into that plain object instead, and saving is nothing more than calling `buildMarkdownText` again. `text.ts`'s `decodeMarkdownText`/`encodeMarkdownText` are the byte↔text boundary neither `readMarkdown`/`writeMarkdown` nor `markdownCodec`'s own `MarkdownBytesSchema` sit on (both operate on strings, not bytes) — the step every bytes-in/bytes-out ergonomic conversion in `convert.ts` needs, using a fatal-mode `TextDecoder` so a non-UTF-8 input throws immediately rather than silently producing replacement characters.
|
|
503
567
|
- **`src/layout/`** — the pure conversion algorithms, importing `model`, (for formula placement) `mathml`, and — for line-wrapping/pagination itself — several primitives sourced from the external `pdf-codec` dependency: the injected `TextMeasurer` port and `wrapRunsToWidth` (pdf-codec's own `measure.ts`/`text-layout.ts`, since deciding where a line breaks needs to know how wide text renders in a PDF standard-14 font, regardless of which format the content came from), `loadMathFont` (pdf-codec's own `math-font.ts`, for formula placement), pdf-codec's `matrix.ts`'s `rotatePointAboutCenter` (`slides.ts`'s own shape-rotation placement), and pdf-codec's `afm-widths.ts`/`fonts.ts`'s `STANDARD_METRICS`/`resolveStandardFont` (`reconstruct.ts`'s own font-matching when reconstructing from a `LayoutDocument`) — this package's one dependency on external font/text-measurement primitives, since text layout is inherently coupled to the one font model (pdf-codec's own standard-14 resolution) every conversion direction ultimately renders through: `engine.ts` (`ContentDocument` wordprocessing → `LayoutDocument`: flow, line-breaking, pagination — fed identically by docx-, odt-, and markdown-sourced content; also returns `WordprocessingLayoutResult.formulas`, every embedded formula block it laid out via `src/mathml`'s `layoutFormula`, positioned in PDF page space — see the Gotchas entry below on why a formula can't become an ordinary `LayoutItem`), `slides.ts` (`ContentDocument` presentation → `LayoutDocument`: direct EMU-to-point placement, no pagination needed — fed identically by pptx- and odp-sourced content; also exports `convertShape`, the single-`ContentShape`-to-`LayoutItem[]` conversion `drawing.ts` below reuses verbatim, now optionally formula-aware via its own trailing `formulaContext` parameter so `drawing.ts`'s existing call site keeps compiling unchanged), `sheets.ts` (`ContentDocument` spreadsheet → `LayoutDocument`: resolve the print range, build cumulative column/row offsets skipping hidden ones, reserve header/repeat-row-column space, resolve an explicit or non-iterative fit-to-page scale, partition into column/row bands honouring manual breaks with the same "an oversized item gets its own band and overflows rather than looping" guarantee `engine.ts`'s `ensureRoom` documents, emit pages in `downThenOver`/`overThenDown` order, then per page paint cell backgrounds/gridlines/cell borders/headers/cell text, honouring a cell's own `alignment`/`verticalAlignment` where it declares one and falling back to the value-kind default and bottom where it doesn't, with `###`/spill-then-truncate overflow handling — the first layout algorithm in this package that accepts an `AbortSignal`, since a 50k-cell sheet needs cancellation where a docx/pptx page count never did; also returns `SpreadsheetLayoutResult.formulas`, every cell-anchored embedded formula it laid out via `src/mathml`'s `layoutFormula`, resolved against the anchor cell's own already-positioned axis geometry plus the frame's cell-relative offset and positioned in PDF page space — the sheets-side counterpart to `engine.ts`'s and `slides.ts`'s own formula output), `drawing.ts` (`ContentDocument` drawing → `LayoutDocument`: one `ContentDrawPage` per PDF page, direct placement like `slides.ts`, with one new emission path — an unrotated `ContentVector` `rect`/`ellipse`/`line` maps onto the pre-existing `LayoutRect`/`LayoutEllipse`/`LayoutLine` kinds, a `path` vector's local, viewBox-relative subpath points are resolved through the vector's own frame offset then a single page-space flip into a `LayoutPath` value, and a *rotated* rect/ellipse/path becomes a `LayoutPath` of rotated points since neither `LayoutRect` nor `LayoutEllipse` models rotation; the page's `shapes` and `vectors` are merged into one true-paint-order walk through their shared `paintOrder` field rather than painted as two sequential arrays), `reconstruct.ts` (`LayoutDocument` → `ContentDocument`: `reconstructWordprocessing`/`reconstructPresentation` do baseline-proximity line clustering, then paragraph/text-block clustering from geometry — PDF has no semantic paragraph or shape structure to recover, only positioned glyphs; `reconstructDrawing` does no clustering at all, since a drawing has no such structure to infer in the first place — every `LayoutItem` maps close to 1:1 back onto a `ContentVector` `rect`/`ellipse`/`line`/`path` or a `ContentShape`, in the exact z-order it was painted, bucketed into `ContentDrawPageSchema`'s own `shapes`/`vectors` arrays with each item's walk position stamped as its `paintOrder`, so the relative order between the two arrays survives; `reconstructSpreadsheet` tries a real gridline lattice first — scanning the page's `LayoutLine`/stroked-single-segment-`LayoutPath` items for enough parallel horizontal and vertical lines at consistent positions to call it a printed grid, using those line positions directly as cell boundaries when found — and falls back to text-position clustering otherwise, reusing this same module's `clusterIntoLines` for rows and a parallel recurring-x-position generalisation of `clusterIntoParagraphs`'s own `dominantLeftX` for columns; every recovered cell is a bare string, column widths/row heights are genuinely measured from whichever geometry was used, and no print range/scale/repeat-rows/repeat-columns/manual-breaks are ever inferred).
|
|
504
568
|
- **`src/hsqldb/`** — the `.odb` decoders, in two tiers over two genuinely different on-disk storage shapes a HSQLDB table can use. `script.ts` (Tier 1): a small, bounded HSQLDB TEXT-script-format (`hsqldb.script_format=0`) DDL/DML text parser, not a database engine — `parseHsqldbScript(bytes)` extracts `CREATE TABLE`'s own column names/types and `INSERT INTO`'s own row values into `HsqldbTable[]`, tolerating (skipping) every other statement kind real HSQLDB output emits that this package has no use for (users, grants, sequences, indexes, views), and throwing `HsqldbScriptParseError` for anything matching neither list. `rowformat.ts`/`cache.ts` (Tier 2): a CACHED table's own binary row-store format — LibreOffice's embedded-HSQLDB default (`database.isStoredFileAccess()` switches `hsqldb.default_table_type` to `cached` specifically for storage-backed access, confirmed against the decompiled engine source) — a CACHED table's DDL still lives in `database/script` as ordinary TEXT (Tier 1 parses it unmodified) but its row *data* lives in a separate binary page-cache file, `database/data`. `rowformat.ts` decodes one column's own binary field at a time (`HsqldbDataCursor`, a big-endian `DataView` cursor; `readHsqldbColumnValue`, one branch per SQL type code); `cache.ts` walks a table's own AVL row-position tree (`readHsqldbCachedTableRows`, following each row's persisted `iLeft`/`iRight` child positions recursively, needing no key-comparison or free-list logic at all — a deleted row is already unlinked from the tree before its space can be reused, so a traversal rooted at the tree's current root only ever reaches live rows), rooted at the position `parseHsqldbIndexRoots` recovers from each table's own `SET TABLE ... INDEX'...'` script line, using `parseHsqldbProperties`'s reading of `database/properties` (cache-file scale, engine version) to resolve byte offsets; `decodeHsqldbCachedTables` is the orchestration `src/odb/read.ts` calls, splicing real rows into every table with an index-root line and leaving every other table (MEMORY/TEXT, or a genuinely empty CACHED table — HSQLDB never writes an index-root line for one) exactly as Tier 1 already produced it. `binary-script.ts` (Tier 4): HSQLDB's own whole-script BINARY (`hsqldb.script_format=1`) and COMPRESSED (`=3`) serialisations of `database/script` itself — `parseHsqldbBinaryScript` reads the leading `org.hsqldb.Result` record carrying the database's DDL, rejoins its statements into exactly the TEXT-format script text the same database would have written at `script_format=0`, feeds that to Tier 1, and then decodes the per-table row sections that follow through `rowformat.ts`'s existing per-column decoder; `inflateHsqldbCompressedScript` is the zlib unwrap `=3` needs first, `fflate`'s `unzlibSync`, the one place in `src/hsqldb/` with a dependency beyond `document-schema.js`. All tiers mirror pdf-codec's own isolation discipline: `script.ts` imports only `document-schema.js`'s `ContentCellValue` type; `rowformat.ts` imports the same plus nothing else; `cache.ts` imports only those two and `script.ts`'s own types — no odf.js `Package`/`XmlElement` knowledge anywhere in `src/hsqldb/` — the caller is responsible for handing every function its raw bytes/text already extracted from a real `.odb` package. `HsqldbTable`/`HsqldbColumn` are also the shared pivot shape `src/firebird/`'s own Tier 3 decoder below produces. See Gotchas for Tier 2's own version scope and verification account.
|
|
@@ -509,7 +573,10 @@ The package is layered from generic primitives outward to the two conversion dir
|
|
|
509
573
|
- **`src/odb/values.ts`** — the `ContentCellValue` comparison and aggregation semantics `src/odb/sql/` and `src/odb/formula/` share: `cellComparisonKey`/`compareCellKeys`/`compareCellValues` (values compare within three classes — numeric, boolean, text — and never across them), `cellValuesEqual` (the *total* counterpart, since a cross-class pair is unambiguously unequal where an ordering comparison has to throw; this is what `rpt:HASCHANGED` needs), and `aggregateCellValues` (the five aggregates over SQL's own NULL-skipping rules). Both engines implement the identical five aggregates over identical inputs, so the semantics live here once rather than in each — a fix to one would otherwise silently leave the other wrong. What it deliberately does *not* own is which error a violation raises: every function takes a `fail` factory and throws what the caller builds, so the same comparison failure surfaces as an `HsqldbSqlEvaluationError` quoting the statement or an `RptFormulaEvaluationError` quoting the formula.
|
|
510
574
|
- **`src/odb/formula/`** — a LibreOffice Report Builder rpt formula engine over the result set `src/odb/sql/` produces, in four modules: `errors.ts` (the same three-class policy as the SQL engine — `RptFormulaUnsupportedError` naming a genuine Report Builder function outside the implemented set, `RptFormulaParseError` for text that is not a well-formed formula, `RptFormulaEvaluationError` for one that parsed but cannot run against the report's own data — plus `RptReportStructureError` for a failure about the report rather than any one formula), `parser.ts` (`parseRptFormula`: a self-contained recursive-descent scanner with no separate lexer, since this language has no keyword vocabulary or operator precedence to keep out of the grammar — `field:[X]` and `rpt:NAME(arg{;arg})`, with `[NAME]` and `"NAME"` as one reference concept and a **semicolon** argument separator), `evaluate.ts` (`runRptReport`: the group-break cascade, group instance ranges, and per-band formula evaluation, the substance of the engine — see the Gotchas entry below), and `definition.ts` (`rptDefinitionFromReport`: the only file here that knows odf.js's own `OdbReport` shape, flattening its nested `rpt:group` tree into the outermost-first chain the evaluator's level-indexed scoping assumes). `evaluate.ts`/`parser.ts` import `document-schema.js`'s `ContentCellValue`, `src/odb/sql/`'s `SqlResultSet` type, and `src/odb/values.ts` only — the same isolation discipline `src/odb/sql/` follows, with odf.js knowledge quarantined in `definition.ts` exactly as `src/odb/read.ts` quarantines it for the decoders. There is no write direction here either: this engine reads formulas, it never generates them.
|
|
511
575
|
- **`src/odb/report/`** — the renderer that turns everything above into a document, in three modules matching the three questions rendering a report actually poses: `source.ts` (`odbReportCommandSql`/`resolveOdbReportRows`: what data does this report bind to? — the `rpt:command`/`rpt:command-type` triple of table name, saved-query name, and inline SQL, all three resolved to one statement run through `src/odb/sql/`, so an unknown table fails with that engine's own message naming every table the `.odb` really has rather than through a second resolution rule that could disagree with it), `render.ts` (`renderOdbReportContent`: what does a printed band look like as content? — one single-row `ContentTable` per band instance, one cell per control, the same shape the band has in the file itself, plus the two page bands the formula engine deliberately never emits, evaluated here through `evaluateRptBandOutsideData` under this renderer's own single-logical-page model), and `content.ts` (`readOdbReportContent`: the composition, plus `OdbReportNotSpecifiedError` for a package declaring no report or more than one with none named — mirroring `csv.ts`'s own table-selection convention). `render.ts` is the only module here that knows what a `ContentDocument` is, and `source.ts` the only one that reads a `Package`; both flattening the report's `rpt:group` tree and evaluating a band's formulas are `src/odb/formula/`'s (via that module's exported `odbReportGroupChain`, so a band instance's own group level and the `OdbReportGroup` its controls come from can never index different chains). There is no reverse direction: a `ContentDocument` holds a report's *output*, not the band/group/formula design that produced it.
|
|
512
|
-
- **`src/convert/`** — `convert.ts` (the fourteen PDF-pivot round-trip ergonomic wrappers — docx/pptx/odt/odp/ods/odg each with a genuine layout-engine edge, `xlsxToPdf`/`pdfToXlsx` composing the ods⇄xlsx bridge with the ods⇄pdf layout pair internally, and `markdownToPdf`/`pdfToMarkdown` reusing the wordprocessing layout engine directly — plus a dedicated "cross-format bridges" section, ten functions across five pairs: `odtToDocx`/`docxToOdt`, `odpToPptx`/`pptxToOdp`, `odsToXlsx`/`xlsxToOds`, and `markdownToDocx`/`docxToMarkdown`, `markdownToOdt`/`odtToMarkdown`, each a direct `readXContent` → `buildYPackage` composition bypassing PDF entirely — see [Fidelity](#fidelity) — `odmToPdf`, the one further conversion shaped around a caller-supplied `resolveSubDocument` callback rather than being purely bytes-in/bytes-out, since a `.odm` master document's own chapters are external references odf.js's `readOdm` never inlines — see Gotchas — `odbToXlsx`/`odbToCsv`, thin compositions over `readOdbTables` and `src/odb/`'s own pivot/CSV mapping, and `odfToPdf`, a standalone `.odf` formula document → PDF via `readOdfFormulaContent` → `src/mathml`'s `layoutFormula` → `writePdf`'s own formula-aware option, with no reverse `pdfToOdf` at all), `codec.ts` (`docxPdfCodec`/`pptxPdfCodec`/`odtPdfCodec`/`odpPdfCodec`/`odsPdfCodec`/`odgPdfCodec`/`xlsxPdfCodec`/`markdownPdfCodec` plus `odtDocxCodec`/`odpPptxCodec`/`odsXlsxCodec`/`markdownDocxCodec`/`markdownOdtCodec`, a `z.codec()` pair over each — `odmToPdf`/`odbToXlsx`/`odbToCsv`/`odfToPdf` have no codec of their own, for the same fixed-signature/one-directional reasons each has no port entry, or a one-way port entry, below), `port.ts`/`local.ts` (the swappable `DocumentConverter` contract and its synchronous local implementation, covering `docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`odf`/`xlsx`/`markdown` → `pdf`, `pdf` → `docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`xlsx`/`markdown`, and the ten bridge functions — `DocumentFormat` includes `xlsx` even though xlsx has no PDF conversion of its own (the port composes one, see `xlsxToPdf`); `odm` and `odb` are deliberately not `DocumentFormat` members, since neither `odmToPdf` nor `odbToXlsx`/`odbToCsv` is wired into this port at all; `odf` IS a member, but with only the one `odf → pdf` entry — no `pdf → odf`). Every conversion function that builds a `ContentDocument`/`LayoutDocument` internally (the fourteen PDF-pivot conversions and the ten bridges; `odfToPdf` accepts but never invokes it) also accepts an `onDocument` callback, and `ConversionResult` carries the same value through the port as an optional `package` field — the full `DocumentPackage` (content + layout, from `document-schema.js`) that conversion built, not just its target bytes. `ConversionOptions` carries `fonts`/`onFontSubstitution` alongside `signal` for the same reason `DocumentToPdfOptions` does (see [Fonts](#fonts)), reaching only the `toPdf` edges — a PDF-to-X reconstruction reads a page's already-positioned glyphs and a bridge runs no layout engine, so neither resolves a face at all — and the local implementation reports every substitution as a `font/substituted` diagnostic as well as through the caller's own callback.
|
|
576
|
+
- **`src/convert/`** — `convert.ts` (the fourteen PDF-pivot round-trip ergonomic wrappers — docx/pptx/odt/odp/ods/odg each with a genuine layout-engine edge, `xlsxToPdf`/`pdfToXlsx` composing the ods⇄xlsx bridge with the ods⇄pdf layout pair internally, and `markdownToPdf`/`pdfToMarkdown` reusing the wordprocessing layout engine directly — plus a dedicated "cross-format bridges" section, ten functions across five pairs: `odtToDocx`/`docxToOdt`, `odpToPptx`/`pptxToOdp`, `odsToXlsx`/`xlsxToOds`, and `markdownToDocx`/`docxToMarkdown`, `markdownToOdt`/`odtToMarkdown`, each a direct `readXContent` → `buildYPackage` composition bypassing PDF entirely — see [Fidelity](#fidelity) — `odmToPdf`, the one further conversion shaped around a caller-supplied `resolveSubDocument` callback rather than being purely bytes-in/bytes-out, since a `.odm` master document's own chapters are external references odf.js's `readOdm` never inlines — see Gotchas — `odbToXlsx`/`odbToCsv`, thin compositions over `readOdbTables` and `src/odb/`'s own pivot/CSV mapping, `odbReportToDocx`/`odbReportToOdt`/`odbReportToPdf`, the last step dispatching `readOdbReportContent`'s own rendered `ContentDocument` to real bytes via `buildDocxPackage`/`buildOdtPackage`/`convertWordprocessingToLayout`+`writePdf` respectively — taking a `ContentDocument` rather than a `Package`, since a rendered report has no source package left to round-trip through — and `odfToPdf`, a standalone `.odf` formula document → PDF via `readOdfFormulaContent` → `src/mathml`'s `layoutFormula` → `writePdf`'s own formula-aware option, with no reverse `pdfToOdf` at all), `codec.ts` (`docxPdfCodec`/`pptxPdfCodec`/`odtPdfCodec`/`odpPdfCodec`/`odsPdfCodec`/`odgPdfCodec`/`xlsxPdfCodec`/`markdownPdfCodec` plus `odtDocxCodec`/`odpPptxCodec`/`odsXlsxCodec`/`markdownDocxCodec`/`markdownOdtCodec`, a `z.codec()` pair over each — `odmToPdf`/`odbToXlsx`/`odbToCsv`/`odfToPdf` have no codec of their own, for the same fixed-signature/one-directional reasons each has no port entry, or a one-way port entry, below), `port.ts`/`local.ts` (the swappable `DocumentConverter` contract and its synchronous local implementation, covering `docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`odf`/`xlsx`/`markdown` → `pdf`, `pdf` → `docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`xlsx`/`markdown`, and the ten bridge functions — `DocumentFormat` includes `xlsx` even though xlsx has no PDF conversion of its own (the port composes one, see `xlsxToPdf`); `odm` and `odb` are deliberately not `DocumentFormat` members, since neither `odmToPdf` nor `odbToXlsx`/`odbToCsv` is wired into this port at all; `odf` IS a member, but with only the one `odf → pdf` entry — no `pdf → odf`). Every conversion function that builds a `ContentDocument`/`LayoutDocument` internally (the fourteen PDF-pivot conversions and the ten bridges; `odfToPdf` accepts but never invokes it) also accepts an `onDocument` callback, and `ConversionResult` carries the same value through the port as an optional `package` field — the full `DocumentPackage` (content + layout, from `document-schema.js`) that conversion built, not just its target bytes. `ConversionOptions` carries `fonts`/`onFontSubstitution` alongside `signal` for the same reason `DocumentToPdfOptions` does (see [Fonts](#fonts)), reaching only the `toPdf` edges — a PDF-to-X reconstruction reads a page's already-positioned glyphs and a bridge runs no layout engine, so neither resolves a face at all — and the local implementation reports every substitution as a `font/substituted` diagnostic as well as through the caller's own callback. `from-package.ts`'s `buildDocumentBytes` is the reverse of every conversion's own `onDocument`/`package` output above: a `DocumentPackage` → any `DocumentFormat`'s own bytes, dispatched through `src/codecs/registry.ts`'s `DOCUMENT_FORMAT_CODECS` (see the `src/codecs/` entry below) for every target except `'pdf'` (writes the package's own `LayoutDocument` half directly) and `'odf'` (rejected outright — no `ContentDocument`-to-odf builder exists).
|
|
577
|
+
- **`src/codecs/`** — `registry.ts`'s `DOCUMENT_FORMAT_CODECS`, every `DocumentFormat`'s own read/build capability expressed as data (a `ContentCodec`/`LayoutCodec` pair per format, both types imported from `document-schema.js`) rather than three independent per-function switch statements re-deriving the same "given a format, which reader/builder do I call" dispatch. A format's `content` entry wraps the identical `readXContent`/`buildXPackage` pair every ergonomic conversion in this package already uses for it (via `decodeDocumentPackage`/`encodeDocumentPackage`, below, for the raw-package half); `pdf`'s `layout` entry wraps `readPdf`/`writePdf` directly. xlsx has a real `content` entry too, wrapping `ooxml.js`'s own `readXlsxContent`/`buildXlsxPackage` — this does not contradict this README's own "not re-exported from this package's public surface" statement elsewhere (that is about `src/index.ts`'s own export list, still true: neither name is exported from there), only that this internal registry may call them directly. `odf` (a standalone formula document) has `content.read` but no `content.write`, since `odf.js` has no write path for a formula document at all. `readDocumentMetadata`/`setDocumentMetadata` (`src/metadata/`, below) and `buildDocumentBytes` (`src/convert/from-package.ts`, above) all dispatch through this one registry rather than each maintaining its own per-format switch — this is what let `setDocumentMetadata`/`buildDocumentBytes` stop rejecting xlsx once the registry gained a real xlsx codec, with no change needed at either call site beyond removing the special case.
|
|
578
|
+
- **`src/metadata/`** — cross-format metadata read/write, both dispatched through `DOCUMENT_FORMAT_CODECS` (above) rather than a hand-written per-format switch. `read.ts`'s `readDocumentMetadata` resolves a `LayoutMetadata` for any of the ten `DocumentFormat`s, with one deliberately-kept named exception: xlsx does **not** dispatch through the registry's own `content` codec at all, instead rendering through `xlsxToPdf` and reading the resulting PDF's own metadata, because a direct `readXlsxContent(...).metadata` and that PDF-preview path disagree on real fields (`createdIso`/`modifiedIso`/`producer`) — confirmed directly rather than assumed (`read.test.ts`'s own xlsx case), so switching xlsx onto the uniform path here would silently change what this function reports. `write.ts`'s `setDocumentMetadata` patches `title`/`author`/`subject`/`keywords` in place without converting format: a `pdf` source/target patches the parsed `LayoutDocument` directly, and every other `REBUILD_FORMATS` member (`docx`/`pptx`/`odt`/`odp`/`ods`/`odg`/`markdown`/`xlsx`) rebuilds a fresh package from that format's own `ContentDocument` via the registry's `content` codec — xlsx joined this set once the registry gained a real xlsx codec (`src/codecs/registry.ts`), so it is no longer rejected the way it once was; `odf` is still rejected outright in both directions (no write path back out at all).
|
|
579
|
+
- **`src/package-codec.ts`** — `decodeDocumentPackage`/`encodeDocumentPackage`/`decodeOdbPackage` (Usage above), the format-aware counterpart to `ooxml.js`'s/`odf.js`'s own `decodePackage`/`encodePackage`. Dispatches docx/pptx/xlsx through `ooxml.js`'s OPC codec and odt/odp/ods/odg/odf through `odf.js`'s ODF codec by a plain format-membership lookup, throwing `UnsupportedPackageFormatError` (a named class, matching this package's own "recognised but unsupported" convention — `OdbUnsupportedFormatError`, `UnsupportedFontSourceFormatError`) for `markdown`/`pdf`, neither of which has a raw-package concept at all. `decodeOdbPackage` decodes `.odb` bytes through the identical `odf.js` `decodePackage` regardless — `.odb` is at the raw-zip-container level an ordinary ODF package — but is deliberately kept out of `decodeDocumentPackage`'s own `DocumentFormat`-keyed dispatch, since `'odb'` is not, and cannot be, a `DocumentFormat` member (see the `.odb` Architecture/Gotchas entries below); there is no `encodeOdbPackage`, since nothing in this package's `.odb` support ever writes a new `.odb` file.
|
|
513
580
|
|
|
514
581
|
Dependency direction among this package's own local modules is downward and checkable, with one deliberate exception (`layout`, noted below): `mathml`/`ports` import nothing local (`mathml` is fully self-contained — no dependency on `model`, `document-schema.js`, or any ODF package, since it consumes only its own locally-mirrored `MathMlNode` input and its own injected `MathFontMetrics` port); `model` imports nothing local at all any more — `formula.ts`'s former type-only `MathMlNode` import from `mathml` is gone with the local `EmbeddedFormula` type it served, since document-schema.js now owns a fully-specified `MathMlNode` of its own; `ooxml/*` imports no local module at all (now a thin adapter over `ooxml.js`'s own `readDocx`/`readPptx` — see the `src/ooxml/` entry above — with no `model`/`xml/*` dependency of its own left, since `ContentDocument`/`CONTENT_FORMAT_VERSION` now come straight from `document-schema.js`; no PDF knowledge either); `odf/*` imports `model` only, and only for `formula.ts`'s block/document builders and `geometry.ts`'s `Box`/`PAGE_SIZE_A4` (its own `ContentDocument`/`CONTENT_FORMAT_VERSION` usage is `document-schema.js`-direct too now — no PDF knowledge, no `xml/*` — `odf.js` already owns its own XML query helpers); `markdown` imports `model` only, and only for `formula.ts`'s stand-in text on the write side (`write.ts` flattens a formula block markdown cannot represent), plus the external `markdown-codec` dependency directly (no PDF knowledge, no odf.js/ooxml.js knowledge at all — the one adapter package in this family whose source format is not a zip archive); `omml` imports `mathml` (its node helpers, operator dictionary, `mathvariant` type, and length parser) and `xml/*` (`fragment.ts`'s `el`/`txt`, `entities.ts`'s `encodeXmlText`) only, plus `ooxml.js` for its own `XmlElement` output type — never `model`, `layout`, or any ODF package, and never in the other direction: `mathml` still imports nothing local at all, which is exactly why this translator is a sibling of it rather than a file inside it; `hsqldb` imports `document-schema.js` only (no odf.js knowledge); `firebird` imports `document-schema.js` (its own row/schema decoding, `ContentCellValue` only) and `hsqldb` (`HsqldbTable`/`HsqldbColumn`, a type-only import for its own output shape — the deliberate pivot-sharing point between Tier 1 and Tier 3) but no odf.js knowledge at all; `layout` imports `model`+`mathml`+`ports`, plus, genuinely upward and outward, several text-measurement/font-metric/matrix primitives from the external `pdf-codec` dependency (`measure.ts`/`text-layout.ts`/`math-font.ts`/`matrix.ts`/`afm-widths.ts`/`fonts.ts` — see the `src/layout/` entry above for exactly which); `odf-package` imports odf.js only (no local dependency, mirroring `opc`'s relationship to `ooxml.js`); `fonts` imports no local module at all either — only `ooxml.js`/`odf.js` for the two package shapes it reads and `pdf-codec` for the `ProvidedFont`/`FontRegistry` shapes it produces, so it sits beside `layout` rather than under it despite both feeding the same conversion; `odb` imports `hsqldb`+`firebird`+`model`+`odf-package`+odf.js only, and its own `odb/sql` and `odb/formula` subtrees import strictly less than that — `odb/values.ts` plus `document-schema.js`'s `ContentCellValue` plus `hsqldb`'s `HsqldbTable` type for the former, and `odb/values.ts` plus `ContentCellValue` plus `odb/sql`'s `SqlResultSet` type for the latter, with odf.js reaching `odb/formula` only through its one `definition.ts` adapter; `odb/report` is the one subtree that imports *more* than `odb` itself rather than less, since rendering is where the two halves finally meet — `odb/sql`, `odb/formula`, `odb/read.ts`, `hsqldb`'s `displayTextFor`, `model`'s `PAGE_SIZE_A4`, `document-schema.js`'s content vocabulary, and odf.js's `OdbReport` shape — and it still keeps each of those to one module: `Package` reaches only `source.ts`/`content.ts`, and `ContentDocument` only `render.ts`; `convert` composes everything else, including `fonts` and `pdf-codec` directly for `readPdf`/`writePdf`/`loadMathFont`/`createFontMeasurer`/`createFontRegistry` and `markdown-codec` indirectly via `markdown/read.ts`/`markdown/write.ts`/`markdown/text.ts`. Beyond this package's own local modules, five external dependencies each own a distinct concern with no overlap: `ooxml.js` (docx/pptx/xlsx ⇄ JSON), `odf.js` (odt/ods/odp/odg ⇄ JSON), `document-schema.js` (the shared `ContentDocument`/`LayoutDocument` schemas), `pdf-codec` (the PDF codec itself, plus the text-layout/font-resolution/byte/image primitives built on it), and `markdown-codec` (CommonMark+GFM ⇄ `ContentDocument`). No `PdfObject`/`PdfDict`/`PdfStream` type appears anywhere in this package at all — that type is pdf-codec's own internal concern now, never exposed across the package boundary.
|
|
515
582
|
|
|
@@ -577,7 +644,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
|
|
|
577
644
|
- **A docx inline image now reads as a real `ContentImageBlock`, and — since `buildDocxPackage` was taught to recognise the exact shape `readDocx` produces for one — round-trips back to docx without the extra blank paragraph a naive per-block write would otherwise insert.** `readDocx` (`ooxml.js` 2.6.1+) always represents an inline image as TWO adjacent `ContentBlock`s sourced from the one physical `<w:p>`: a paragraph block carrying that paragraph's own (often all-empty) text runs, immediately followed by an image block for the `w:drawing` found inside it — there is no field anywhere in `ContentDocument` distinguishing that pairing from a genuinely separate, intentionally-blank paragraph that happens to sit immediately before an unrelated image; both produce the identical two-block shape. `buildDocxPackage`'s `appendBlocks` (`src/edit/docx/content.ts`) special-cases the pattern `readDocx` actually produces — a paragraph whose runs are all empty text, directly followed by an image block — and writes it back as the single physical paragraph it came from (paragraph properties applied, then `insertImageAfter` called on that SAME paragraph) rather than as two separate paragraphs. This is what makes a full `readDocxContent`/`buildDocxPackage` read → build → read cycle equal byte-for-byte again once an image is involved, rather than accumulating one spurious empty paragraph before every image on every round trip. The one honestly-scoped residual: a paragraph that genuinely is separate and blank, immediately followed by an unrelated image in its own paragraph, is indistinguishable from the common inline-image case and gets merged the same way — an edge case, not the common one this fix targets.
|
|
578
645
|
- **pptx speaker notes survive `pptxToPdf`/`pdfToPptx`, but not through any real PDF feature.** PDF has no native concept of hidden presenter notes, so `convertPresentationToLayout` carries `ContentSlide.notes` as a hidden `/Subtype /Text` annotation on the page (the same construct Acrobat's own sticky-note tool uses, marked with the `Hidden` annotation flag so it never renders or prints), and `reconstructPresentation` reads it back via a `/T` marker that distinguishes this package's own notes annotation from a genuine third-party sticky note. This is a round-trip mechanism specific to this package's own writer/reader pair — a PDF produced by anything else will never carry it, and a PDF consumer other than this package's own `readPdf` will never see it as anything but an invisible, empty sticky note.
|
|
579
646
|
- **`odmToPdf` is the one conversion in this package that is not purely bytes-in/bytes-out.** A `.odm` (ODF master document) never carries its own chapters' content — each `text:section` is a bare external reference (`text:section-source`'s `xlink:href` + `text:filter-name`) to a standalone `.odt` file, confirmed against real, unmodified LibreOffice 26.2 output while building `odf.js`'s own `readOdm`: a self-closing `text:section-source` with no `xlink:show`/`xlink:type`, no manifest entry for the linked part, and no chapter text anywhere in the master document's own `content.xml`. There is consequently no way for `odmToPdf` to read a chapter's content from the `.odm` bytes alone — it takes an `options.resolveSubDocument` callback, called once per section with that section's own `href`, to hand back the chapter's own `.odt` bytes. Every section left unresolved (no callback given, or the callback returns `undefined` for that `href`) is collected across the *whole* document before anything throws, and reported together in one `OdmUnresolvedSectionError` naming every unresolved `href` — not just whichever section the read loop happened to reach first. `odmToPdf` is consequently not one of the fourteen round-trip conversions or ten bridges above, and is deliberately not wired into the `DocumentConverter` port either: that port's `convert(request, options)` contract is a fixed single-bytes-in/bytes-out shape, and widening it with a resolver parameter for this one format would leak an odm-specific concern into every other conversion's own request shape — a caller wanting `odmToPdf` behind the port can wrap it in their own adapter. `OdmSection.inlineContent` (declared by `odf.js`'s own `readOdm` for schema-completeness, covering a producer that caches a chapter's content inline rather than only linking it) is handled too, via the same `readOdfParagraph`/`readOdfTable` primitives `odf.js`'s own `readOdt` calls internally — but the installed `odf.js` 1.10.0 never actually populates it for any real document `readOdm` was tested against, so this branch is exercised only by a directly-constructed `OdmSection` in this package's own test suite, not by any `.odm` fixture.
|
|
580
|
-
- **`.odb` has no `odbToPdf` of its own, and does not need one.** All three parts of rendering a Report are
|
|
647
|
+
- **`.odb` as a whole has no `odbToPdf` of its own, and does not need one — but a rendered *Report* specifically now has real one-call wrappers to PDF, docx, and odt.** All three parts of rendering a Report are real — `src/odb/sql/`'s `parseSelect`/`evaluateSelect` run the report's own query over `readOdbTables`' output, `src/odb/formula/`'s `runRptReport` evaluates its rpt formulas and group breaks over the result, and `src/odb/report/`'s `readOdbReportContent` renders the printed bands into a `ContentDocument` — and because that document is an ordinary `wordprocessing` one, every consumer of that variant already accepts it: `convertWordprocessingToLayout` lays it out, `writePdf` writes it, `buildDocxPackage`/`buildOdtPackage` build a docx or odt from it. `odbReportToPdf`/`odbReportToDocx`/`odbReportToOdt` (`src/convert/convert.ts`, Usage above) are exactly that composition, wrapped as the same one-call ergonomic shape every other conversion in this package has. What has genuinely NOT changed: a wrapper over `.odb` **as a whole** — `odbToPdf` in the "give me a PDF of this entire database front-end" sense — would still pick one of tables/queries/reports arbitrarily and imply `.odb` had a single natural output format, which it does not; that is why `.odb` stays out of `DocumentFormat` and the `DocumentConverter` port entirely, and why `odbReportToPdf` takes an already-rendered report `ContentDocument`, not raw `.odb` bytes, as its input. What no part of this chain does is reproduce Report Builder's own *page* output — see [Fidelity](#fidelity) for exactly what "structural, not pixel-faithful" excludes.
|
|
581
648
|
- **The rpt formula engine's group scoping cascades an enclosing break inward, and that is the one part of it most easily got subtly wrong.** An aggregate is scoped to the band it appears in — a `rpt:SUM([AMOUNT])` in an inner group's footer totals only that group instance's rows, one in the outer group's footer totals that whole instance, one in the report footer totals every row. The catch is when an instance *ends*: a group at level L starts a new instance when its own group-expression breaks **or when any enclosing group breaks**, unconditionally. The real fixture demonstrates exactly why. Its inner group breaks on `rpt:HASCHANGED("LEFT_QUARTER")` and its outer on `rpt:HASCHANGED("REGION")`; between the rows `(North, Q2)` and `(South, Q2)` the quarter does *not* change, so the inner expression is false there — yet the region does, and a "Q2" subtotal spanning North's Q2 rows and South's Q2 rows would be a number no reader asked for. The cascade lives in the report structure, **not** in `HASCHANGED`: that function is implemented exactly as its name says (the referenced value differs from its value on the immediately preceding row, and true on the first row), with no knowledge of groups at all, and `src/odb/formula/report.test.ts` proves both halves separately against the same real rows — the two-group report splits South's and West's Q2 rows, and the identical expression as the *only* group merges them. Two further consequences worth stating: aggregates are computed over a group instance's complete row range rather than accumulated row by row (the result set is already fully in memory, so a `SUM` in a group *header* is the true total for the group about to print, not a running total of its first row), and a group expression may not transitively depend on an aggregate — that is genuinely circular, since group expressions decide the very boundaries an aggregate's range is defined by, so it throws `RptFormulaEvaluationError` from a static walk of the named-function graph before a single row is read.
|
|
582
649
|
- **The rpt formula engine's function set is a closed allowlist, and its argument separator is a semicolon.** `rpt:HASCHANGED(X)`, `rpt:LEFT(X;n)`, and `rpt:SUM`/`COUNT`/`AVG`/`MIN`/`MAX`, plus the separate `field:[COLUMN]` bound-field form — every other rpt function throws `RptFormulaUnsupportedError` carrying the function name and the offending formula, the same policy `src/odb/sql/` and `src/hsqldb/script.ts` follow. The separator is `;`, not `,` (LibreOffice's formula languages use the Basic/Calc convention throughout, and the real fixture's `rpt:LEFT([QUARTER];2)` is the confirmation); a comma-separated argument list is rejected outright rather than accepted as a second convention. The two reference spellings, `[NAME]` and `"NAME"`, are treated as one concept and resolve by one rule, since the real fixture writes `rpt:HASCHANGED("REGION")` with quotes and `rpt:SUM([AMOUNT])` with brackets to no observable difference; a name matching *both* a declared `rpt:function` and a data column is ambiguous and throws rather than letting one shadow the other. Three further bounded refusals, each a place where guessing would produce a plausible wrong value rather than a visible failure: a group expression that does not evaluate to a boolean break test (real Report Builder writes `rpt:HASCHANGED(...)` and nothing else there, so a "group by this value's changes" reinterpretation has no real output to verify against); `rpt:LEFT` over a non-text value (a report's own number format lives in its band styles, which this engine does not read, so formatting a number to text here would mean inventing one); and a per-row formula in the report header or footer, which print outside the data and so belong to no row.
|
|
583
650
|
- **The rpt formula engine emits no page headers or footers, deliberately — the renderer places them, under a single-logical-page model it states rather than hides.** Which rows land on which page is a layout decision the formula engine has no basis for making, so `RptReportDefinition` carries no page bands and `rptDefinitionFromReport` drops odf.js's own `pageHeader`/`pageFooter` explicitly rather than silently. `src/odb/report/render.ts` is the renderer that decides: having no pagination engine, it declares the whole report one logical page, prints each page band once (the page header below the report header and above the body, matching the banded-report convention where a report's title sits above the column labels that then repeat on every page; the page footer above the report footer), and evaluates their formulas through `evaluateRptBandOutsideData` at **report** scope — which for a single page is not an approximation but exactly the right scope, since that page's rows are every row. Two failure modes need no special-casing because both already fail correctly: a per-row formula in a page band (`field:[X]`, `rpt:HASCHANGED`) throws for belonging to no row, exactly as it does in the report header, and `rpt:PAGENUMBER` or any other genuinely page-dependent function throws from the parser as an unsupported function rather than being rendered as a plausible-looking wrong value. In the real fixture the page header carries only `rpt:fixed-content` labels and the page footer declares no controls at all — a band with no controls prints no block, which is why nothing sits between the last region total and the grand total in the rendered output.
|
|
@@ -685,7 +752,7 @@ Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore:`, …), e
|
|
|
685
752
|
|
|
686
753
|
## References
|
|
687
754
|
|
|
688
|
-
- [ooxml.js](https://github.com/ExaDev/ooxml.js) — the sibling package this depends on for all docx/pptx/xlsx ⇄ JSON handling and cascade-resolved typed reading, including its own `readXlsxContent`/`buildXlsxPackage` (a `ContentDocument`-shaped xlsx reader/writer pair), consumed directly by `src/convert/convert.ts`'s `odsToXlsx`/`xlsxToOds` bridge but not re-exported from this package's own public surface.
|
|
755
|
+
- [ooxml.js](https://github.com/ExaDev/ooxml.js) — the sibling package this depends on for all docx/pptx/xlsx ⇄ JSON handling and cascade-resolved typed reading, including its own `readXlsxContent`/`buildXlsxPackage` (a `ContentDocument`-shaped xlsx reader/writer pair), consumed directly by `src/convert/convert.ts`'s `odsToXlsx`/`xlsxToOds` bridge and by `src/codecs/registry.ts`'s own xlsx content codec (which in turn drives `readDocumentMetadata`/`setDocumentMetadata`/`buildDocumentBytes`) but not re-exported from this package's own public surface.
|
|
689
756
|
- [document-schema.js](https://github.com/ExaDev/document-schema.js) — the sibling package that owns `ContentDocument`/`LayoutDocument` themselves; `ooxml.js`, `odf.js`, `pdf-codec`, `markdown-codec`, and `documents.js` all import from it rather than each maintaining an independent copy.
|
|
690
757
|
- [markdown-codec](https://github.com/ExaDev/markdown-codec) — the sibling package this depends on for CommonMark+GFM ⇄ `ContentDocument` handling (`readMarkdown`/`writeMarkdown`), also built on `document-schema.js`. A dependency of `documents.js` for: this package's `MarkdownBytesSchema` (`src/model/bytes.ts`), which checks well-formed UTF-8 the same way that package's own `MarkdownBytesSchema` does; `src/markdown/read.ts`'s `readMarkdownContent`, a thin adapter over `markdown-codec`'s own `readMarkdown`, feeding `markdownToPdf`/`pdfToMarkdown` and the `markdownToDocx`/`markdownToOdt` bridges (`src/convert/convert.ts`); `src/markdown/write.ts`'s `buildMarkdownText`, the same adapter over `markdown-codec`'s own `writeMarkdown`, feeding `pdfToMarkdown` and the `docxToMarkdown`/`odtToMarkdown` bridges. markdown is the third format (after docx and odt) proven to share the `wordprocessing` `ContentDocument` variant and its layout engine.
|
|
691
758
|
- [pdf-codec](https://github.com/ExaDev/pdf-codec) — the sibling package this depends on for the hand-written PDF codec itself (`readPdf`/`writePdf`/`pdfCodec`), extracted from this repository: parsing arbitrary real-world PDFs and generating new ones, the embedded STIX Two Math font, and the text-measurement/font-resolution/byte/image primitives `src/layout/` builds on. See [Architecture](#architecture) above for exactly where the boundary between the two packages sits, and pdf-codec's own README for its internals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "js.documents",
|
|
3
|
-
"version": "1.81.
|
|
3
|
+
"version": "1.81.2",
|
|
4
4
|
"description": "Bidirectional docx/pptx <-> PDF conversion and a read+write editable OOXML document model, built on ooxml.js and Zod 4 codecs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
],
|
|
59
59
|
"license": "MIT",
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"document-schema.js": "^2.3.
|
|
61
|
+
"document-schema.js": "^2.3.2",
|
|
62
62
|
"fflate": "^0.8.3",
|
|
63
|
-
"markdown-codec": "^1.1.
|
|
64
|
-
"odf.js": "^2.3.
|
|
65
|
-
"ooxml.js": "^2.6.
|
|
66
|
-
"pdf-codec": "^1.11.
|
|
63
|
+
"markdown-codec": "^1.1.4",
|
|
64
|
+
"odf.js": "^2.3.5",
|
|
65
|
+
"ooxml.js": "^2.6.16",
|
|
66
|
+
"pdf-codec": "^1.11.5",
|
|
67
67
|
"zod": "^4.4.3"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|