ooxml.js 6.1.0 → 6.2.0

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
@@ -292,7 +292,7 @@ The package layers a lossless core outward to lossy convenience views:
292
292
  - **`src/typed/`** — lossy projections. `readDocxContent` resolves the full style cascade (`docDefaults` → `basedOn` → paragraph-mark → character styles → direct formatting) into ordered `sections` plus comments/footnotes/header-footer parts/numbering; `readPptxContent` resolves placeholder → layout → master → theme inheritance into `slides` (presentation order via `p:sldIdLst`); `readXlsxWorkbook` covers cell values/formulas, merged ranges, defined names. `typed/shared/` holds shared OOXML primitives (`drawingml.ts` geometry/theme/colour, `color.ts` `ColorTransform` cascade, `units.ts`, `metadata.ts`, `source-path.ts`). Types come from `document-schema.js`. Only the docx and xlsx `ContentDocument`-shaped pairs encode back to a `Package`; everything else is one-way, and faithful round-tripping goes through `decodePackage`/`encodePackage`.
293
293
  - **`src/typed/document-tree.ts`** — the `DocumentTree`-native surface, and nothing else: `readDocx`/`readPptx`/`readXlsx` are their content-level reader composed with `document-schema.js`'s `assembleTree`, `buildDocxPackage`/`buildXlsxPackage` are `flattenTree` composed with their content-level writer. One module rather than one per format, because the adapters are format-uniform and the reasoning behind them (why a reader mints styles rather than calling bare `decompose`, why a writer's kind guard lives at this boundary, what a docx's non-content parts do instead of riding the tree) is one argument stated once.
294
294
  - **`src/typed/docx/`** — `read.ts` and `write.ts` are a read/write pair over `ContentSection[]`. `constructs.ts` owns the fidelity construct vocabulary both halves share: the descriptor shapes (`contentControl` from `w:sdt`, `field` from `w:fldChar`/`w:fldSimple`, `anchor` from `w:bookmarkStart`/`End`, `provenance` from `w:ins`/`w:del`/`w:moveFrom`/`w:moveTo`) and the rule deciding which occurrences are block-scoped enough to bracket. `buildDocxPackageFromContent` builds a complete docx `Package` from scratch; it writes no `styles.xml`, `numbering.xml`, comments, footnotes, or headers/footers, so the `DocxDocument` fields outside `sections` do not survive the pair.
295
- - **`src/typed/xlsx/`** — a `ContentDocument`-shaped read/write pair alongside the lossy `readXlsxWorkbook` (both exported; different callers). `readXlsxContent` reads column widths, row heights, hidden rows/columns, merged ranges, every cell value kind, print settings, and cell comments (`comments.ts`: legacy `xl/comments{N}.xml` notes plus `[MS-XLSX]` threaded comments, both resolved through the worksheet part's own relationships, never by part name); `buildXlsxPackageFromContent` builds a complete xlsx `Package` from scratch (never editing the decoded package). `number-format.ts`/`styles.ts`/`serial.ts` run both ways: reading classifies style index → format code → kind (`percentage`/`currency`/`date`/`time`/`dateTime`); writing emits interned `numFmt` codes, fed back through the classifier in tests. The classifier is not a formatter (`displayText` is the typed-value spelling). Scope limits: `currency` with no ISO code writes as plain `number`; non-canonical temporal values degrade to text; cell comments read but do not write (`buildXlsxPackageFromContent` emits no comment part, so they do not survive this pair); column widths re-approximate through xlsx's own character-width unit on every write, so a width drifts slightly on each round trip rather than reaching a fixed point.
295
+ - **`src/typed/xlsx/`** — a `ContentDocument`-shaped read/write pair alongside the lossy `readXlsxWorkbook` (both exported; different callers). `readXlsxContent` reads column widths, row heights, hidden rows/columns, merged ranges, every cell value kind, print settings, and cell comments (`comments.ts`: legacy `xl/comments{N}.xml` notes plus `[MS-XLSX]` threaded comments, both resolved through the worksheet part's own relationships, never by part name); `buildXlsxPackageFromContent` builds a complete xlsx `Package` from scratch (never editing the decoded package). `number-format.ts`/`styles.ts`/`serial.ts` run both ways: reading classifies style index → format code → kind (`percentage`/`currency`/`date`/`time`/`dateTime`); writing emits interned `numFmt` codes, fed back through the classifier in tests. The classifier is not a formatter (`displayText` is the typed-value spelling). Scope limits: `currency` with no ISO code writes as plain `number`; non-canonical temporal values degrade to text; cell comments read but do not write (`buildXlsxPackageFromContent` emits no comment part, so they do not survive this pair); column widths re-approximate through xlsx's own character-width unit on every write, so a width drifts slightly on each round trip rather than reaching a fixed point. `data-validation.ts`/`conditional-format.ts` are the same real-vocabulary read/write pair for `ContentSheet.dataValidations`/`conditionalFormats` (see the gotchas section below for the shape), sharing `sqref.ts`'s A1 range-list parsing/formatting and `rule-residue.ts`'s attribute-level residue helper; `styles.ts` additionally resolves a `cellIs`-family rule's own `dxfId` against `xl/styles.xml`'s `<dxfs>` table (`readDxfElements`), the differential-format counterpart to its existing `<cellXfs>` decoration resolution.
296
296
 
297
297
  ## Conventions
298
298
 
@@ -308,7 +308,7 @@ The package layers a lossless core outward to lossy convenience views:
308
308
  - **A construct marker brackets whole blocks, never a sub-sequence of runs.** A construct covering a sub-sequence of one paragraph's runs lands on that paragraph's `constructs` field as a run range (see the fidelity-constructs section above) — bookmarks, fields, internal links, comment extents, note references, and `w:ffData` form fields alike. An inline SDT or a few inserted words inside an otherwise untouched paragraph are still read exactly as before — the text survives, the construct does not — as is a bookmark whose two halves sit in different paragraphs. So does a block extent that crosses another, or that straddles a section break or table-cell boundary: neither is expressible as balanced brackets (nor as a tree group, which is what the marker pair promotes to), and both are dropped — a drop `document-schema.js`'s extent-scope note ratifies — rather than emitted at a position that would decode to a different nesting.
309
309
  - **The docx pair round-trips `sections`, not a whole `DocxDocument`.** Numbering definitions, cell border styling/shading, section break kinds (`w:sectPr/w:type` onto `ContentSection.breakType`, both directions), and `w:themeColor` (without `themeShade`/`themeTint`) are read; images read into `ContentImageBlock` (floating `wp:anchor` position not recorded); `PAGE`/`NUMPAGES` fields resolve to Word's cached text. Headers and footers are read structurally — every `word/header*.xml` / `word/footer*.xml` part as block flow (`headerFooterParts`, referenced or not) plus per-section default/first/even references (`sectionHeaderFooters`). An inline `w:object`/`o:OLEObject` whose payload part is itself a ZIP archive — a modern producer's embedded xlsx/docx/pptx, detected by magic bytes through `archive-codec` — or a classic OLE compound-file `.bin` whose root storage carries the embedded file as an OLE-packaged `Package` stream, read through `archive-codec`'s bounded CFB reader, is decoded into a nested content document and emitted as a `ContentEmbeddedObjectBlock` beside its containing paragraph's own block, sized from `w:dxaOrig`/`w:dyaOrig`; the object's VML preview picture (`v:imagedata`) and a `w:object` inside a footnote (notes ride as text, with no block flow to lift an object into — a header/footer's own objects DO recover, those parts being walked as block flow) are not read, a compound file holding native legacy streams (BIFF and friends — no `Package` stream, or a packaged file that is not a ZIP) stays opaque, and a payload that does not decode as one of the three OOXML flavours (a plain archive, corrupt zip or compound-file data, a bodyless docx) degrades to no embedded block rather than failing the host read. The writer emits such a block back out as the inverse `w:object`/`o:OLEObject`: the nested document is re-serialised through its own format's builder into a fresh `word/embeddings/oleObject<N>.<docx|xlsx|pptx>` ZIP part carrying its relationship and content-type override, and `w:dxaOrig`/`w:dyaOrig` are derived from the block's frame (twips). An embedded presentation document serialises through an injected port — `BuildDocxContentOptions.serialiseEmbeddedPresentation`, a presentation `ContentDocument` → pptx-bytes function this package accepts because it has no PresentationML writer of its own, and the ecosystem's one pptx writer lives one layer up in documents.js (which exports `embeddedPresentationSerialiser`, wired from its own pptx builder, for exactly this call); with no serialiser injected, a `ContentDocument` carrying one is refused with a thrown error rather than a silent drop, and no VML preview picture is regenerated (the reader never read one into the model, and real previews are WMF/EMF this ecosystem has no writer for; Word shows a blank until activated). The writer emits no `styles.xml`, `numbering.xml`, comments, footnotes, endnotes, or headers/footers, so the `DocxDocument` fields outside `sections` do not come back — a paragraph's `styleId` is still written as a `w:pStyle` reference, resolving to nothing, since every property the style would have contributed is already spelled as direct formatting by then.
310
310
  - **pptx is read-only.** Connector shapes (`p:cxnSp`) are skipped; shape rotation composes through groups; a dynamic field (`a:fld`, e.g. slidenum/datetime) reads as an ordinary run plus a `field` run construct carrying `@type` as the instruction and the cached `a:t` as the result; an internal slide-jump link (`a:hlinkClick` resolving to a slide relationship, no `TargetMode`) records a `link` run construct with an internal target naming the destination slide's package part path (e.g. `ppt/slides/slide2.xml` — PresentationML addresses slides by part relationship, never by name; the flat run's `hyperlink` field stays reserved for external URIs, and an action-only `ppaction://hlinkshowjump` names no target part and records nothing); a chart graphic frame reads its chart part's cached series/category model into a table block (header row = series names, one row per category index); a SmartArt graphic frame reads its diagram data model's node text as paragraphs in diagram order (depth-first over `parOf` connections, siblings by `srcOrd`); an OLE graphic frame reads the fallback picture its `mc:Fallback` carries (or, with none reachable, a paragraph naming the `p:oleObj`'s `progId`), and when the payload part is itself a ZIP archive — a modern producer's embedded xlsx/docx/pptx, detected by magic bytes through `archive-codec` — or a classic OLE compound-file `.bin` whose root storage carries the embedded file as an OLE-packaged `Package` stream, read through `archive-codec`'s bounded CFB reader, also decodes it into a nested content document and emits it as an `embeddedObject` block beside the picture, sized to the frame's own geometry; a compound file holding native legacy streams (no `Package` stream, or a packaged file that is not a ZIP) stays opaque external-application data, and a payload that does not decode as one of the three OOXML flavours degrades to no embedded block rather than failing the slide read.
311
- - **xlsx has no native percentage/currency/date/time cell type.** Both directions are closed via the number-format engine: reading classifies style → format code → kind; writing emits interned `numFmt` codes, fed back through the classifier in tests. `displayText` is the typed-value spelling, not the producer's rendered string. A chart graphic frame reads as an embedded `chart` object (`embeddedObjects`, one row per series over the shared category axis, values verbatim `c:v` text, frame resolved through the sheet's own column widths and row heights), and a drawing picture (`xdr:pic`) as a `ContentSheetImage` (`images`, media bytes sniffed through the drawing part's relationships, frame resolved the same way) — both rows under any of the drawing's three anchor spellings (`twoCellAnchor` marker pairs, `oneCellAnchor` sized by its own `xdr:ext`, and `absoluteAnchor`, whose page-absolute `xdr:pos` is re-based into the cell anchor vocabulary through that same grid geometry) — one-way rows, since the writer emits no drawing part. A workbook's general defined names and table/List objects ride the tree reader's root `definitions` table (`readXlsx`), the landing the schema's own verdict gives a named range; the flat reader and the write pair carry neither. `dataValidation` and `conditionalFormatting` rules quarantine verbatim on their range's anchor cell through the residue channel, pending the corpus-gated vocabulary the construct inventory defers.
311
+ - **xlsx has no native percentage/currency/date/time cell type.** Both directions are closed via the number-format engine: reading classifies style → format code → kind; writing emits interned `numFmt` codes, fed back through the classifier in tests. `displayText` is the typed-value spelling, not the producer's rendered string. A chart graphic frame reads as an embedded `chart` object (`embeddedObjects`, one row per series over the shared category axis, values verbatim `c:v` text, frame resolved through the sheet's own column widths and row heights), and a drawing picture (`xdr:pic`) as a `ContentSheetImage` (`images`, media bytes sniffed through the drawing part's relationships, frame resolved the same way) — both rows under any of the drawing's three anchor spellings (`twoCellAnchor` marker pairs, `oneCellAnchor` sized by its own `xdr:ext`, and `absoluteAnchor`, whose page-absolute `xdr:pos` is re-based into the cell anchor vocabulary through that same grid geometry) — one-way rows, since the writer emits no drawing part. A workbook's general defined names and table/List objects ride the tree reader's root `definitions` table (`readXlsx`), the landing the schema's own verdict gives a named range; the flat reader and the write pair carry neither. `dataValidation` (`typed/xlsx/data-validation.ts`) and `conditionalFormatting` (`typed/xlsx/conditional-format.ts`) rules are real vocabulary both ways (`ContentSheet.dataValidations`/`conditionalFormats`, ExaDev/documents.js#758), verified against two real LibreOffice-produced fixtures: every `ContentSheetDataValidationTypeSchema` member (the full ECMA-376 vocabulary bar `none`) and every closed-form `ContentSheetConditionalFormatSchema` union member (`cellIs`, the text-predicate and operand-free families, `top10`, `aboveAverage`, `timePeriod`, and the inline-colour `colorScale`/`dataBar`/`iconSet` trio) promote structurally, resolving a `cellIs`-family rule's own `dxfId` against `xl/styles.xml`'s `<dxfs>` table into a `textColor`/`background` style (whatever of a referenced `<dxf>` a real producer's differential format carries beyond those two — alignment, border, a font/fill's own other properties — rides the style's own `source` residue verbatim, reconstructed on write around the two structured colours). A rule's own `sqref` is parsed as the list of one or more ranges it really is (a real fixture carries a space-separated multi-range sqref, e.g. `"A1 C1"`), and a small set of producer attributes neither schema names (a data validation's `showDropDown`/`imeMode`, a cfRule's `pivot`/`id`) rides each rule's own `source` as attribute-level residue, restored on write underneath whatever the structured fields recompute. The one deliberate hold-out is `expression` (an arbitrary boolean formula with no closed-form structure to promote without a general formula engine) and any type/rule this reader does not recognise (`none` foremost) — both continue through the narrowed anchor-cell residue mechanism unchanged, quarantining the rule verbatim on its first range's top-left cell exactly as every promoted rule's own family used to.
312
312
  - **`test:smoke` depends on a fresh build.** It runs `tsdown && vitest run --project smoke`, always rebuilding `dist/` first. A bare `vitest` runs both projects; `smoke` fails loudly (`Cannot find module '../dist/index.js'`) if `dist/` is unbuilt.
313
313
  - **Binary-vs-XML part classification is a byte sniff, not an extension check.** `looksLikeXml` looks for a leading `<` after skipping a UTF-8 BOM and whitespace; any future binary format starting with `<` would misclassify.
314
314
  - **`Array.isArray` narrows `unknown` to `any[]`, not `unknown[]`.** Indexing the result reintroduces `any` and trips `no-unsafe-assignment`. `compact.ts` and `xml/parse.ts` each define a local `isUnknownArray` guard (`value is unknown[]`) — use it wherever the narrowed element is read.
@@ -10,6 +10,8 @@ const require_typed_xlsx_units = require("./units.cjs");
10
10
  const require_typed_xlsx_serial = require("./serial.cjs");
11
11
  const require_typed_xlsx_shared_strings = require("./shared-strings.cjs");
12
12
  const require_typed_xlsx_styles = require("./styles.cjs");
13
+ const require_typed_xlsx_conditional_format = require("./conditional-format.cjs");
14
+ const require_typed_xlsx_data_validation = require("./data-validation.cjs");
13
15
  let document_schema_js = require("document-schema.js");
14
16
  //#region src/typed/xlsx/build.ts
15
17
  const SML_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
@@ -184,7 +186,7 @@ function buildSharedStringsPart(sharedStrings) {
184
186
  uniqueCount: String(entries.length)
185
187
  }, siElements));
186
188
  }
187
- function buildStylesPart(cellFormats) {
189
+ function buildStylesPart(cellFormats, dxfTable) {
188
190
  const children = [];
189
191
  const declarations = cellFormats.declarations();
190
192
  if (declarations.length > 0) {
@@ -250,6 +252,8 @@ function buildStylesPart(cellFormats) {
250
252
  xfId: "0",
251
253
  builtinId: "0"
252
254
  })]));
255
+ const dxfElements = dxfTable.dxfElements();
256
+ if (dxfElements.length > 0) children.push(require_xml_fragment.el("dxfs", { count: String(dxfElements.length) }, [...dxfElements]));
253
257
  return xmlPart(require_xml_fragment.el("styleSheet", { xmlns: SML_NS }, children));
254
258
  }
255
259
  function buildCorePropertiesPart(metadata) {
@@ -488,13 +492,16 @@ function buildBreaksElements(settings) {
488
492
  }
489
493
  return result;
490
494
  }
491
- function buildWorksheetPart(sheet, sharedStrings, cellFormats) {
495
+ function buildWorksheetPart(sheet, sharedStrings, cellFormats, dxfTable) {
492
496
  const children = [buildSheetPrElement(sheet.printSettings), require_xml_fragment.el("dimension", { ref: computeDimension(sheet) })];
493
497
  const colsElement = buildColsElement(sheet.columns);
494
498
  if (colsElement !== void 0) children.push(colsElement);
495
499
  children.push(buildSheetDataElement(sheet, sharedStrings, cellFormats));
496
500
  const mergeCellsElement = buildMergeCellsElement(sheet.cells);
497
501
  if (mergeCellsElement !== void 0) children.push(mergeCellsElement);
502
+ children.push(...require_typed_xlsx_conditional_format.buildConditionalFormattingElements(sheet.conditionalFormats ?? [], dxfTable));
503
+ const dataValidationsElement = require_typed_xlsx_data_validation.buildDataValidationsElement(sheet.dataValidations ?? []);
504
+ if (dataValidationsElement !== void 0) children.push(dataValidationsElement);
498
505
  children.push(buildPrintOptionsElement(sheet.printSettings), buildPageMarginsElement(sheet.printSettings), buildPageSetupElement(sheet.printSettings));
499
506
  const { rowBreaks, colBreaks } = buildBreaksElements(sheet.printSettings);
500
507
  if (rowBreaks !== void 0) children.push(rowBreaks);
@@ -509,13 +516,14 @@ function buildXlsxPackageFromContent(document) {
509
516
  const sheets = document.sheets;
510
517
  const sharedStrings = new require_typed_xlsx_shared_strings.SharedStringTable();
511
518
  const cellFormats = new require_typed_xlsx_styles.CellFormatTable();
512
- const worksheetParts = sheets.map((sheet) => buildWorksheetPart(sheet, sharedStrings, cellFormats));
519
+ const dxfTable = new require_typed_xlsx_conditional_format.DxfTable();
520
+ const worksheetParts = sheets.map((sheet) => buildWorksheetPart(sheet, sharedStrings, cellFormats, dxfTable));
513
521
  const parts = {
514
522
  "[Content_Types].xml": buildContentTypesPart(sheets.length),
515
523
  "_rels/.rels": buildPackageRelsPart(),
516
524
  "xl/workbook.xml": buildWorkbookPart(sheets),
517
525
  "xl/_rels/workbook.xml.rels": buildWorkbookRelsPart(sheets.length),
518
- "xl/styles.xml": buildStylesPart(cellFormats),
526
+ "xl/styles.xml": buildStylesPart(cellFormats, dxfTable),
519
527
  "xl/sharedStrings.xml": buildSharedStringsPart(sharedStrings),
520
528
  "docProps/core.xml": buildCorePropertiesPart(document.metadata),
521
529
  "docProps/app.xml": buildAppPropertiesPart(document.metadata)
@@ -9,6 +9,8 @@ import { ptToColumnWidthChars } from "./units.js";
9
9
  import { isoDateTimeToSerial, isoDateToSerial, isoTimeToSerial } from "./serial.js";
10
10
  import { SharedStringTable } from "./shared-strings.js";
11
11
  import { CellFormatTable, RESERVED_BORDER_INDICES, RESERVED_FILL_INDICES } from "./styles.js";
12
+ import { DxfTable, buildConditionalFormattingElements } from "./conditional-format.js";
13
+ import { buildDataValidationsElement } from "./data-validation.js";
12
14
  import { cellReference, rangeReference } from "document-schema.js";
13
15
  //#region src/typed/xlsx/build.ts
14
16
  const SML_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
@@ -183,7 +185,7 @@ function buildSharedStringsPart(sharedStrings) {
183
185
  uniqueCount: String(entries.length)
184
186
  }, siElements));
185
187
  }
186
- function buildStylesPart(cellFormats) {
188
+ function buildStylesPart(cellFormats, dxfTable) {
187
189
  const children = [];
188
190
  const declarations = cellFormats.declarations();
189
191
  if (declarations.length > 0) {
@@ -249,6 +251,8 @@ function buildStylesPart(cellFormats) {
249
251
  xfId: "0",
250
252
  builtinId: "0"
251
253
  })]));
254
+ const dxfElements = dxfTable.dxfElements();
255
+ if (dxfElements.length > 0) children.push(el("dxfs", { count: String(dxfElements.length) }, [...dxfElements]));
252
256
  return xmlPart(el("styleSheet", { xmlns: SML_NS }, children));
253
257
  }
254
258
  function buildCorePropertiesPart(metadata) {
@@ -487,13 +491,16 @@ function buildBreaksElements(settings) {
487
491
  }
488
492
  return result;
489
493
  }
490
- function buildWorksheetPart(sheet, sharedStrings, cellFormats) {
494
+ function buildWorksheetPart(sheet, sharedStrings, cellFormats, dxfTable) {
491
495
  const children = [buildSheetPrElement(sheet.printSettings), el("dimension", { ref: computeDimension(sheet) })];
492
496
  const colsElement = buildColsElement(sheet.columns);
493
497
  if (colsElement !== void 0) children.push(colsElement);
494
498
  children.push(buildSheetDataElement(sheet, sharedStrings, cellFormats));
495
499
  const mergeCellsElement = buildMergeCellsElement(sheet.cells);
496
500
  if (mergeCellsElement !== void 0) children.push(mergeCellsElement);
501
+ children.push(...buildConditionalFormattingElements(sheet.conditionalFormats ?? [], dxfTable));
502
+ const dataValidationsElement = buildDataValidationsElement(sheet.dataValidations ?? []);
503
+ if (dataValidationsElement !== void 0) children.push(dataValidationsElement);
497
504
  children.push(buildPrintOptionsElement(sheet.printSettings), buildPageMarginsElement(sheet.printSettings), buildPageSetupElement(sheet.printSettings));
498
505
  const { rowBreaks, colBreaks } = buildBreaksElements(sheet.printSettings);
499
506
  if (rowBreaks !== void 0) children.push(rowBreaks);
@@ -508,13 +515,14 @@ function buildXlsxPackageFromContent(document) {
508
515
  const sheets = document.sheets;
509
516
  const sharedStrings = new SharedStringTable();
510
517
  const cellFormats = new CellFormatTable();
511
- const worksheetParts = sheets.map((sheet) => buildWorksheetPart(sheet, sharedStrings, cellFormats));
518
+ const dxfTable = new DxfTable();
519
+ const worksheetParts = sheets.map((sheet) => buildWorksheetPart(sheet, sharedStrings, cellFormats, dxfTable));
512
520
  const parts = {
513
521
  "[Content_Types].xml": buildContentTypesPart(sheets.length),
514
522
  "_rels/.rels": buildPackageRelsPart(),
515
523
  "xl/workbook.xml": buildWorkbookPart(sheets),
516
524
  "xl/_rels/workbook.xml.rels": buildWorkbookRelsPart(sheets.length),
517
- "xl/styles.xml": buildStylesPart(cellFormats),
525
+ "xl/styles.xml": buildStylesPart(cellFormats, dxfTable),
518
526
  "xl/sharedStrings.xml": buildSharedStringsPart(sharedStrings),
519
527
  "docProps/core.xml": buildCorePropertiesPart(document.metadata),
520
528
  "docProps/app.xml": buildAppPropertiesPart(document.metadata)