ooxml.js 2.0.4 → 2.1.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 +8 -2
- package/dist/index.cjs +24 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +24 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -167,12 +167,12 @@ The package is layered from a lossless core outward to lossy convenience views:
|
|
|
167
167
|
- **`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
168
|
- **`src/codec.ts`** — the public round-trip surface: `packageCodec`/`xmlCodec` are `z.codec()` pairs, and `decodePackage`/`encodePackage` are the ergonomic wrappers around them.
|
|
169
169
|
- **`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
|
|
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.
|
|
171
171
|
|
|
172
172
|
## Conventions
|
|
173
173
|
|
|
174
174
|
- **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
|
-
- **`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`)
|
|
175
|
+
- **`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
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`.
|
|
177
177
|
- **XML entities stay raw in the lossless layer.** `parseXml` runs with `processEntities: false` so encoded entities (e.g. `&`) 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
178
|
- **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.
|
|
@@ -203,6 +203,12 @@ Whether that release actually published a new version is detected by diffing `pa
|
|
|
203
203
|
|
|
204
204
|
Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore:`, …), enforced by commitlint (`commitlint.config.ts`) via a husky `commit-msg` hook and a CI `commitlint` job — semantic-release's version bump depends on these being well-formed, not just style. A husky `pre-commit` hook runs `lint-staged` (`eslint --fix` on staged `*.ts` files) and `pre-push` runs the test suite. There is a single `main` branch and no open pull request workflow established so far.
|
|
205
205
|
|
|
206
|
+
## References
|
|
207
|
+
|
|
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.
|
|
209
|
+
- [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
|
+
- [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
|
+
|
|
206
212
|
## License
|
|
207
213
|
|
|
208
214
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -874,6 +874,22 @@ function applyGroupTransform(group, childFrame) {
|
|
|
874
874
|
};
|
|
875
875
|
}
|
|
876
876
|
//#endregion
|
|
877
|
+
//#region src/typed/shared/source-path.ts
|
|
878
|
+
function assignSourcePaths(blocks, prefix) {
|
|
879
|
+
blocks.forEach((block, blockIndex) => {
|
|
880
|
+
const blockPath = `${prefix}.blocks[${blockIndex}]`;
|
|
881
|
+
block.sourcePath = blockPath;
|
|
882
|
+
if (block.kind === "paragraph") block.runs.forEach((run, runIndex) => {
|
|
883
|
+
run.sourcePath = `${blockPath}.runs[${runIndex}]`;
|
|
884
|
+
});
|
|
885
|
+
else if (block.kind === "table") block.rows.forEach((row, rowIndex) => {
|
|
886
|
+
row.cells.forEach((cell, cellIndex) => {
|
|
887
|
+
assignSourcePaths(cell.blocks, `${blockPath}.rows[${rowIndex}].cells[${cellIndex}]`);
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
877
893
|
//#region src/typed/docx/styles.ts
|
|
878
894
|
function mergeParagraphLayer(base, layer) {
|
|
879
895
|
return {
|
|
@@ -1267,6 +1283,7 @@ function readSections(body, context, rels) {
|
|
|
1267
1283
|
margins: DEFAULT_MARGINS,
|
|
1268
1284
|
blocks: currentBlocks
|
|
1269
1285
|
});
|
|
1286
|
+
sections.forEach((section, sectionIndex) => assignSourcePaths(section.blocks, `sections[${sectionIndex}]`));
|
|
1270
1287
|
return sections;
|
|
1271
1288
|
}
|
|
1272
1289
|
function readDocumentTheme(pkg, docRels) {
|
|
@@ -1758,6 +1775,13 @@ function readPptx(pkg) {
|
|
|
1758
1775
|
const presentationRoot = rootElement(pkg.parts[PRESENTATION_PATH]);
|
|
1759
1776
|
const size = readSlideSize(presentationRoot);
|
|
1760
1777
|
const slides = readSlidePathsInOrder(pkg, presentationRoot).map((slidePath) => readSlide(pkg, slidePath, size));
|
|
1778
|
+
slides.forEach((slide, slideIndex) => {
|
|
1779
|
+
slide.shapes.forEach((shape, shapeIndex) => {
|
|
1780
|
+
const shapePath = `slides[${slideIndex}].shapes[${shapeIndex}]`;
|
|
1781
|
+
shape.sourcePath = shapePath;
|
|
1782
|
+
assignSourcePaths(shape.blocks, shapePath);
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1761
1785
|
return {
|
|
1762
1786
|
metadata: readCoreProperties(pkg),
|
|
1763
1787
|
slides
|
package/dist/index.d.cts
CHANGED
|
@@ -463,6 +463,7 @@ declare const PptxDocumentSchema: z.ZodObject<{
|
|
|
463
463
|
insetBottomPt: z.ZodNumber;
|
|
464
464
|
fontScale: z.ZodOptional<z.ZodNumber>;
|
|
465
465
|
lineSpacingReduction: z.ZodOptional<z.ZodNumber>;
|
|
466
|
+
sourcePath: z.ZodOptional<z.ZodString>;
|
|
466
467
|
blocks: z.ZodArray<z.ZodCustom<ContentBlock$1, ContentBlock$1>>;
|
|
467
468
|
}, z.core.$strip>>;
|
|
468
469
|
notes: z.ZodString;
|
package/dist/index.d.ts
CHANGED
|
@@ -463,6 +463,7 @@ declare const PptxDocumentSchema: z.ZodObject<{
|
|
|
463
463
|
insetBottomPt: z.ZodNumber;
|
|
464
464
|
fontScale: z.ZodOptional<z.ZodNumber>;
|
|
465
465
|
lineSpacingReduction: z.ZodOptional<z.ZodNumber>;
|
|
466
|
+
sourcePath: z.ZodOptional<z.ZodString>;
|
|
466
467
|
blocks: z.ZodArray<z.ZodCustom<ContentBlock$1, ContentBlock$1>>;
|
|
467
468
|
}, z.core.$strip>>;
|
|
468
469
|
notes: z.ZodString;
|
package/dist/index.js
CHANGED
|
@@ -873,6 +873,22 @@ function applyGroupTransform(group, childFrame) {
|
|
|
873
873
|
};
|
|
874
874
|
}
|
|
875
875
|
//#endregion
|
|
876
|
+
//#region src/typed/shared/source-path.ts
|
|
877
|
+
function assignSourcePaths(blocks, prefix) {
|
|
878
|
+
blocks.forEach((block, blockIndex) => {
|
|
879
|
+
const blockPath = `${prefix}.blocks[${blockIndex}]`;
|
|
880
|
+
block.sourcePath = blockPath;
|
|
881
|
+
if (block.kind === "paragraph") block.runs.forEach((run, runIndex) => {
|
|
882
|
+
run.sourcePath = `${blockPath}.runs[${runIndex}]`;
|
|
883
|
+
});
|
|
884
|
+
else if (block.kind === "table") block.rows.forEach((row, rowIndex) => {
|
|
885
|
+
row.cells.forEach((cell, cellIndex) => {
|
|
886
|
+
assignSourcePaths(cell.blocks, `${blockPath}.rows[${rowIndex}].cells[${cellIndex}]`);
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
//#endregion
|
|
876
892
|
//#region src/typed/docx/styles.ts
|
|
877
893
|
function mergeParagraphLayer(base, layer) {
|
|
878
894
|
return {
|
|
@@ -1266,6 +1282,7 @@ function readSections(body, context, rels) {
|
|
|
1266
1282
|
margins: DEFAULT_MARGINS,
|
|
1267
1283
|
blocks: currentBlocks
|
|
1268
1284
|
});
|
|
1285
|
+
sections.forEach((section, sectionIndex) => assignSourcePaths(section.blocks, `sections[${sectionIndex}]`));
|
|
1269
1286
|
return sections;
|
|
1270
1287
|
}
|
|
1271
1288
|
function readDocumentTheme(pkg, docRels) {
|
|
@@ -1757,6 +1774,13 @@ function readPptx(pkg) {
|
|
|
1757
1774
|
const presentationRoot = rootElement(pkg.parts[PRESENTATION_PATH]);
|
|
1758
1775
|
const size = readSlideSize(presentationRoot);
|
|
1759
1776
|
const slides = readSlidePathsInOrder(pkg, presentationRoot).map((slidePath) => readSlide(pkg, slidePath, size));
|
|
1777
|
+
slides.forEach((slide, slideIndex) => {
|
|
1778
|
+
slide.shapes.forEach((shape, shapeIndex) => {
|
|
1779
|
+
const shapePath = `slides[${slideIndex}].shapes[${shapeIndex}]`;
|
|
1780
|
+
shape.sourcePath = shapePath;
|
|
1781
|
+
assignSourcePaths(shape.blocks, shapePath);
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1760
1784
|
return {
|
|
1761
1785
|
metadata: readCoreProperties(pkg),
|
|
1762
1786
|
slides
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ooxml.js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Type-safe, lossless round-trip conversion between OOXML packages (docx, pptx, xlsx) and JSON, built on Zod 4 codecs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"packageManager": "pnpm@11.6.0",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"document-content-model": "^1.
|
|
64
|
+
"document-content-model": "^1.1.0",
|
|
65
65
|
"fast-xml-parser": "^5.10.1",
|
|
66
66
|
"fflate": "^0.8.3",
|
|
67
67
|
"zod": "^4.4.3"
|