ooxml.js 2.1.1 → 2.2.1

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 CHANGED
@@ -60,6 +60,15 @@ const doc = readDocx(decodePackage(bytes));
60
60
  // inside tables); each run already carries its cascade-resolved bold/italic/colour/font.
61
61
  ```
62
62
 
63
+ `readXlsx` is the equivalent lossy, cell-values-only view for xlsx (see below). Alongside it, `readXlsxContent`/`buildXlsxPackage` are a separate, `ContentDocument`-shaped pair — a richer reader (column widths, row heights, hidden rows/columns, merged ranges, every cell value kind xlsx distinguishes, and print settings) matched with this package's first writer, so a caller can round-trip a spreadsheet through the same `ContentDocument` shape `documents.js` and `odf.js` already use:
64
+
65
+ ```ts
66
+ import { buildXlsxPackage, decodePackage, readXlsxContent } from 'ooxml.js';
67
+
68
+ const content = readXlsxContent(decodePackage(bytes)); // ContentDocument, kind: 'spreadsheet'
69
+ const pkg = buildXlsxPackage(content); // a fresh Package built from scratch, not a write-back into `pkg`
70
+ ```
71
+
63
72
  ## The ooxml.js format
64
73
 
65
74
  The verbose `Package` JSON is faithful but repetitive: every node repeats its `type`/`tag`/`attributes`/`children` keys, and tag and namespace strings recur thousands of times across a real document. **The ooxml.js format** is a compact, still-plain-JSON alternative — tuple-encoded nodes plus a single interned string table — that composes on top of `packageCodec` without changing what it guarantees:
@@ -167,13 +176,14 @@ The package is layered from a lossless core outward to lossy convenience views:
167
176
  - **`src/package-io/`** — `read.ts` and `write.ts` sit between the zip and XML layers: unzip a package into path -> bytes, classify each entry as XML or binary (`looksLikeXml` sniffs the leading non-whitespace byte for `<`), and parse/serialize accordingly.
168
177
  - **`src/codec.ts`** — the public round-trip surface: `packageCodec`/`xmlCodec` are `z.codec()` pairs, and `decodePackage`/`encodePackage` are the ergonomic wrappers around them.
169
178
  - **`src/compact.ts`** — the ooxml.js format: `compactCodec` (`z.codec(PackageSchema, CompactPackageSchema, …)`) maps `Package ⇄ CompactPackage`, with `toCompact`/`fromCompact` as the ergonomic wrappers. `compactPackageCodec` composes `packageCodec` and `compactCodec` into a direct bytes ⇄ `CompactPackage` codec (`decodeCompactPackage`/`encodeCompactPackage`), so all three format pairs — bytes/`Package`, `Package`/`CompactPackage`, bytes/`CompactPackage` — have a named codec rather than requiring callers to chain two.
170
- - **`src/typed/`** — one-way, lossy projections that read the generic `Package` into ergonomic document/presentation/workbook models. `docx/` and `pptx/` share one block content model — `ContentParagraph`/`ContentTable`/`ContentImageBlock`/`ContentPageBreak`, discriminated as `ContentBlock`, imported from the sibling [`document-content-model`](https://github.com/ExaDev/document-content-model) package rather than defined here (see below) — instead of each keeping its own, disjoint shape: `readDocx` resolves the full WordprocessingML style cascade (`docx/styles.ts`: `docDefaults` → named-style `basedOn` chains → paragraph-mark run properties → character styles → direct formatting) into ordered `sections` of paragraphs/tables/page-breaks (document order preserved, including inside tables), plus `comments`, `footnotes`, and `headers`/`footers`; `readPptx` resolves the placeholder → layout → master → theme inheritance cascade (`pptx/inherit.ts`) into `slides` of positioned, styled `shapes` (geometry, run/paragraph formatting, embedded images, tables, speaker notes) in presentation order (`p:sldIdLst`, never slide filename order); `readXlsx` covers cell values and formulas, merged ranges and defined names. `typed/shared/` holds the OOXML-specific primitives both `docx/` and `pptx/` build on: `drawingml.ts` (DrawingML `a:xfrm` geometry, theme/colour resolution, group-transform composition — including `ColorTransform`/`applyColorTransforms`, the shade/tint/lumMod/lumOff cascade maths, which stays here rather than in `document-content-model` since it's OOXML-cascade-resolution logic, not a content-model shape), `units.ts` (OOXML unit conversions — EMU/twip/half-point), `metadata.ts` (`docProps/core.xml` + `docProps/app.xml` → `DocumentMetadata`, shared verbatim across docx/pptx/xlsx), and `source-path.ts` (stamps a deterministic, document-order path like `sections[0].blocks[2].runs[1]` onto every `ContentRun`/`ContentBlock`/`ContentShape`, so a downstream consumer can trace a rendered item back to where it came from — see `document-content-model`'s own `sourcePath` field). Geometry (`Box`/`PageSize`/`Margins`), colour (`Color`/`ColorSchema`), and alignment (`Alignment`) types are imported from `document-content-model`, not defined locally. `src/image/sniff.ts` (magic-byte PNG/JPEG detection) supports `readPptx`'s picture-shape reading. None of this can be encoded back to a `Package` — round-tripping always goes through `decodePackage`/`encodePackage`, never through a typed view. `typed/util.ts` holds the shared XML-walking helpers (`walk`, `elementsWithTag`, `childrenWithTag`, `attr`, `rootElement`, `textContent`, entity decoding, `resolveRelationships`) every typed reader builds on.
179
+ - **`src/typed/`** — one-way, lossy projections that read the generic `Package` into ergonomic document/presentation/workbook models. `docx/` and `pptx/` share one block content model — `ContentParagraph`/`ContentTable`/`ContentImageBlock`/`ContentPageBreak`, discriminated as `ContentBlock`, imported from the sibling [`document-content-model`](https://github.com/ExaDev/document-content-model) package rather than defined here (see below) — instead of each keeping its own, disjoint shape: `readDocx` resolves the full WordprocessingML style cascade (`docx/styles.ts`: `docDefaults` → named-style `basedOn` chains → paragraph-mark run properties → character styles → direct formatting) into ordered `sections` of paragraphs/tables/page-breaks (document order preserved, including inside tables), plus `comments`, `footnotes`, and `headers`/`footers`; `readPptx` resolves the placeholder → layout → master → theme inheritance cascade (`pptx/inherit.ts`) into `slides` of positioned, styled `shapes` (geometry, run/paragraph formatting, embedded images, tables, speaker notes) in presentation order (`p:sldIdLst`, never slide filename order); `readXlsx` covers cell values and formulas, merged ranges and defined names. `typed/shared/` holds the OOXML-specific primitives both `docx/` and `pptx/` build on: `drawingml.ts` (DrawingML `a:xfrm` geometry, theme/colour resolution, group-transform composition — including `ColorTransform`/`applyColorTransforms`, the shade/tint/lumMod/lumOff cascade maths, which stays here rather than in `document-content-model` since it's OOXML-cascade-resolution logic, not a content-model shape), `units.ts` (OOXML unit conversions — EMU/twip/half-point), `metadata.ts` (`docProps/core.xml` + `docProps/app.xml` → `DocumentMetadata`, shared verbatim across docx/pptx/xlsx), and `source-path.ts` (stamps a deterministic, document-order path like `sections[0].blocks[2].runs[1]` onto every `ContentRun`/`ContentBlock`/`ContentShape`, so a downstream consumer can trace a rendered item back to where it came from — see `document-content-model`'s own `sourcePath` field). Geometry (`Box`/`PageSize`/`Margins`), colour (`Color`/`ColorSchema`), and alignment (`Alignment`) types are imported from `document-content-model`, not defined locally. `src/image/sniff.ts` (magic-byte PNG/JPEG detection) supports `readPptx`'s picture-shape reading. None of `readDocx`/`readPptx`/`readXlsx` can be encoded back to a `Package` — round-tripping those always goes through `decodePackage`/`encodePackage`, never through a typed view; see `src/typed/xlsx/` below for this package's one write-back exception. `typed/util.ts` holds the shared XML-walking helpers (`walk`, `elementsWithTag`, `childrenWithTag`, `attr`, `rootElement`, `textContent`, entity decoding, `resolveRelationships`) every typed reader builds on.
180
+ - **`src/typed/xlsx/`** — a second, `ContentDocument`-shaped xlsx pair alongside the lossy `readXlsx` above, not a replacement for it (both stay exported; they serve different callers). `content.ts`'s `readXlsxContent` reads a `Package` straight into `ContentDocument` (`kind: 'spreadsheet'`): real column widths, row heights, hidden rows/columns, merged ranges (resolved onto the anchor cell's `colSpan`/`rowSpan`), every cell value kind xlsx itself distinguishes, and a genuinely populated `ContentSheetPrintSettings` — matching the bar the sibling `odf.js` package's own `readOds` already sets, rather than `readXlsx`'s flattened `XlsxWorkbook`/`XlsxCell` shape. `build.ts`'s `buildXlsxPackage` is `readXlsxContent`'s write-side inverse and this package's first writer of genuinely new content: given a `ContentDocument`, it constructs a complete xlsx `Package` from scratch — workbook, worksheets, a minimal-but-real `xl/styles.xml`, shared strings, core/app properties — via `xml/fragment.ts`'s `el`/`txt`, rather than editing whatever package `readXlsxContent` itself decoded. Both stay scoped to what `ContentDocument`'s `spreadsheet` variant models: no number-format engine (a numeric cell's percentage/currency/date semantics live in `xl/styles.xml`'s own `numFmt` codes, which neither side interprets) and no per-cell rich-text runs.
171
181
 
172
182
  ## Conventions
173
183
 
174
184
  - **Zod-first schema/type/guard.** Every model type is inferred from its Zod schema (`z.infer<typeof XSchema>`), not hand-written — schema, type, and validator stay in lockstep.
175
185
  - **`XmlNode` uses a recursive structural guard, not `z.lazy`.** `z.lazy` collapses to `unknown` for the element-children case in the Zod version this project pins, so `XmlElementSchema` validates `children` via `z.custom<XmlNode>(isXmlNode)`, a hand-written recursive type guard in `model/node.ts`. Any change to `XmlNode`'s shape must update `isXmlNode` in step. `src/compact.ts`'s `CompactXmlNode` (`isCompactXmlNode` + `z.custom`) reuses the same pattern for the same reason; `document-content-model`'s own `ContentBlock` (`isContentBlock` + `z.custom`, since a table cell's blocks can themselves contain a table) does too, one level up the dependency graph.
176
- - **Lossless core vs. lossy views is a hard boundary.** `decodePackage`/`encodePackage` (and the underlying codecs) must stay byte/part faithful — every part round-trips unchanged. `src/typed/*` readers are explicitly one-way and are allowed to drop information (documented per-reader, e.g. `readDocx` resolves cached field-result text rather than re-evaluating live `PAGE`/`NUMPAGES` fields, docx's own `w:themeColor` references aren't resolved, and `readXlsx` drops cell styles, formats and charts). Don't blur this line by adding write-back support to a typed reader; a full round-trip always goes through the generic `Package`.
186
+ - **Lossless core vs. lossy views is a hard boundary.** `decodePackage`/`encodePackage` (and the underlying codecs) must stay byte/part faithful — every part round-trips unchanged. `src/typed/*`'s lossy readers (`readDocx`, `readPptx`, `readXlsx`) are explicitly one-way and are allowed to drop information (documented per-reader, e.g. `readDocx` resolves cached field-result text rather than re-evaluating live `PAGE`/`NUMPAGES` fields, docx's own `w:themeColor` references aren't resolved, and `readXlsx` drops cell styles, formats and charts). Don't blur this line by adding write-back support to one of those readers; a full round-trip of a package one of them decoded always goes through the generic `Package`. `readXlsxContent`/`buildXlsxPackage` (`src/typed/xlsx/`) are a deliberate, separate exception, not a violation of this rule: they were designed together as a genuine read/write pair around the shared `ContentDocument` model — matching the sibling `odf.js`/`documents.js` packages' own established convention of building fresh output from a `ContentDocument`, rather than editing a decoded package in place — and `buildXlsxPackage` never touches whatever package `readXlsxContent` itself decoded.
177
187
  - **XML entities stay raw in the lossless layer.** `parseXml` runs with `processEntities: false` so encoded entities (e.g. `&amp;`) are preserved verbatim for round-trip fidelity; typed readers decode the five standard entities (`decodeEntities` in `typed/util.ts`) only in their own lossy projection, never in the core model.
178
188
  - **No type assertions.** `eslint.config.ts` runs `@typescript-eslint/consistent-type-assertions` with `assertionStyle: "never"`, banning `as` and angle-bracket casts outright, with `linterOptions.noInlineConfig: true` so there is no `eslint-disable` escape hatch either — narrow with a guard or parse with Zod. An exception would have to be scoped structurally, as a `files`-matched override block in `eslint.config.ts`, not an inline comment.
179
189
 
@@ -205,7 +215,7 @@ Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore:`, …), e
205
215
 
206
216
  ## References
207
217
 
208
- - [document-content-model](https://github.com/ExaDev/document-content-model) — the canonical `ContentBlock`/`ContentSection`/`ContentSlide`/geometry/colour/alignment schemas `readDocx`/`readPptx` return, imported here rather than defined locally.
218
+ - [document-content-model](https://github.com/ExaDev/document-content-model) — the canonical `ContentBlock`/`ContentSection`/`ContentSlide`/geometry/colour/alignment schemas `readDocx`/`readPptx` return, plus the `ContentDocument`/`LayoutMetadata` types `readXlsxContent`/`buildXlsxPackage` read and write directly, all imported here rather than defined locally — the single source of truth this package shares with `odf.js` and `documents.js` so none of the three maintains an independent, drift-prone copy.
209
219
  - [odf.js](https://github.com/ExaDev/odf.js) — a sibling package doing the equivalent job for the OpenDocument Format (odt/ods/odp/odg/…), also built on `document-content-model`.
210
220
  - [documents.js](https://github.com/ExaDev/documents.js) — depends on this package for lossless OOXML handling and its cascade-resolved typed readers, adding PDF conversion and a read-and-write docx/pptx editor on top.
211
221