js.documents 6.0.1 → 6.1.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 +283 -115
- package/dist/bin-dispatch.d.cts +2 -2
- package/dist/bin-dispatch.d.ts +2 -2
- package/dist/{blr-types-DVgO1DQ9.d.cts → blr-types-CMN0QKUE.d.cts} +1 -1
- package/dist/{blr-types-DVgO1DQ9.d.ts → blr-types-CMN0QKUE.d.ts} +1 -1
- package/dist/{capability-an5gSNsu.d.cts → capability-BZ3iNsWh.d.cts} +1 -1
- package/dist/{capability-DX0lCSiI.d.ts → capability-D-a4okp0.d.ts} +1 -1
- package/dist/{cell-typing-CEWlywIc.d.cts → cell-typing-BQ-xQ5X5.d.cts} +4 -4
- package/dist/{cell-typing-CEWlywIc.d.ts → cell-typing-BQ-xQ5X5.d.ts} +4 -4
- package/dist/codecs/read.d.cts +1 -1
- package/dist/codecs/read.d.ts +1 -1
- package/dist/convert/capability.d.cts +1 -1
- package/dist/convert/capability.d.ts +1 -1
- package/dist/convert/composition.d.cts +8 -8
- package/dist/convert/composition.d.ts +8 -8
- package/dist/convert/convert.d.cts +3 -3
- package/dist/convert/convert.d.ts +3 -3
- package/dist/convert/port.d.cts +1 -1
- package/dist/convert/port.d.ts +1 -1
- package/dist/convert/variant-bridges.d.cts +3 -3
- package/dist/convert/variant-bridges.d.ts +3 -3
- package/dist/csv/read.d.cts +1 -1
- package/dist/csv/read.d.ts +1 -1
- package/dist/csv/records.cjs +1 -1
- package/dist/csv/records.js +1 -1
- package/dist/csv/write.d.cts +2 -2
- package/dist/csv/write.d.ts +2 -2
- package/dist/{diagnostics-C5-bG09J.d.cts → diagnostics-DdO0xPGy.d.cts} +1 -1
- package/dist/{diagnostics-C5-bG09J.d.ts → diagnostics-DdO0xPGy.d.ts} +1 -1
- package/dist/edit/docx/content.d.cts +1 -1
- package/dist/edit/docx/content.d.ts +1 -1
- package/dist/edit/docx/image.d.cts +1 -1
- package/dist/edit/docx/image.d.ts +1 -1
- package/dist/edit/docx/paragraph.cjs +1 -1
- package/dist/edit/docx/paragraph.d.cts +6 -6
- package/dist/edit/docx/paragraph.d.ts +6 -6
- package/dist/edit/docx/paragraph.js +1 -1
- package/dist/edit/docx/props.d.cts +4 -4
- package/dist/edit/docx/props.d.ts +4 -4
- package/dist/edit/docx/table.d.cts +1 -1
- package/dist/edit/docx/table.d.ts +1 -1
- package/dist/edit/markdown/list.d.cts +1 -1
- package/dist/edit/markdown/list.d.ts +1 -1
- package/dist/edit/odg/page.d.cts +3 -3
- package/dist/edit/odg/page.d.ts +3 -3
- package/dist/edit/odg/vector.d.cts +1 -1
- package/dist/edit/odg/vector.d.ts +1 -1
- package/dist/edit/odp/image.d.cts +1 -1
- package/dist/edit/odp/image.d.ts +1 -1
- package/dist/edit/odp/shape.d.cts +1 -1
- package/dist/edit/odp/shape.d.ts +1 -1
- package/dist/edit/odp/slide.d.cts +4 -4
- package/dist/edit/odp/slide.d.ts +4 -4
- package/dist/edit/odt/content.d.cts +3 -3
- package/dist/edit/odt/content.d.ts +3 -3
- package/dist/edit/odt/editor.d.cts +3 -3
- package/dist/edit/odt/editor.d.ts +3 -3
- package/dist/edit/odt/image.d.cts +1 -1
- package/dist/edit/odt/image.d.ts +1 -1
- package/dist/edit/odt/list.d.cts +1 -1
- package/dist/edit/odt/list.d.ts +1 -1
- package/dist/edit/odt/paragraph.cjs +1 -1
- package/dist/edit/odt/paragraph.d.cts +1 -1
- package/dist/edit/odt/paragraph.d.ts +1 -1
- package/dist/edit/odt/paragraph.js +1 -1
- package/dist/edit/odt/table.d.cts +1 -1
- package/dist/edit/odt/table.d.ts +1 -1
- package/dist/edit/pdf/item.cjs +1 -1
- package/dist/edit/pdf/item.d.cts +1 -1
- package/dist/edit/pdf/item.d.ts +1 -1
- package/dist/edit/pdf/item.js +1 -1
- package/dist/edit/pdf/page.d.cts +1 -1
- package/dist/edit/pdf/page.d.ts +1 -1
- package/dist/edit/pdf/util.d.cts +1 -1
- package/dist/edit/pdf/util.d.ts +1 -1
- package/dist/edit/pptx/content.cjs +1 -1
- package/dist/edit/pptx/content.d.cts +1 -1
- package/dist/edit/pptx/content.d.ts +1 -1
- package/dist/edit/pptx/content.js +1 -1
- package/dist/edit/pptx/editor.cjs +1 -1
- package/dist/edit/pptx/editor.js +1 -1
- package/dist/edit/pptx/image.d.cts +1 -1
- package/dist/edit/pptx/image.d.ts +1 -1
- package/dist/edit/pptx/shape.d.cts +1 -1
- package/dist/edit/pptx/shape.d.ts +1 -1
- package/dist/edit/pptx/slide.d.cts +3 -3
- package/dist/edit/pptx/slide.d.ts +3 -3
- package/dist/edit/pptx/table.d.cts +1 -1
- package/dist/edit/pptx/table.d.ts +1 -1
- package/dist/{evaluate-BU5N1Jk1.d.ts → evaluate-Cu7iEnxX.d.ts} +1 -1
- package/dist/{evaluate-BhMXY6DM.d.cts → evaluate-Rl9P68se.d.cts} +1 -1
- package/dist/firebird/blr-types.d.cts +1 -1
- package/dist/firebird/blr-types.d.ts +1 -1
- package/dist/firebird/schema.d.cts +1 -1
- package/dist/firebird/schema.d.ts +1 -1
- package/dist/fonts/ooxml.d.cts +1 -1
- package/dist/fonts/ooxml.d.ts +1 -1
- package/dist/fonts/registry.d.cts +1 -1
- package/dist/fonts/registry.d.ts +1 -1
- package/dist/{formula-Cs0IexVi.d.cts → formula-BH1FBblm.d.cts} +1 -1
- package/dist/{formula-_8feXQH7.d.cts → formula-BpuJ3ae0.d.cts} +1 -1
- package/dist/{formula-DDSYieJg.d.ts → formula-Cb-LRoIE.d.ts} +1 -1
- package/dist/{formula-BKgIMfhi.d.ts → formula-DF4R1QEJ.d.ts} +1 -1
- package/dist/{image-DRqXQmEg.d.cts → image-BQ7F9xxz.d.cts} +1 -1
- package/dist/{image-DRqXQmEg.d.ts → image-BQ7F9xxz.d.ts} +1 -1
- package/dist/{image-CRgc_j2Q.d.cts → image-CwbCQ49u.d.ts} +2 -2
- package/dist/{image-Biwtr667.d.ts → image-DQxA-zGN.d.cts} +1 -1
- package/dist/{image-Biwtr667.d.cts → image-DQxA-zGN.d.ts} +1 -1
- package/dist/{image-3ql9_z1y.d.ts → image-DzqEWMDw.d.cts} +1 -1
- package/dist/{image-3ql9_z1y.d.cts → image-DzqEWMDw.d.ts} +1 -1
- package/dist/{image-CyYr22TE.d.ts → image-fah3VkZg.d.cts} +2 -2
- package/dist/index.d.cts +19 -19
- package/dist/index.d.ts +19 -19
- package/dist/{item-DhXpJNAQ.d.cts → item-SUC4uKDO.d.cts} +5 -5
- package/dist/{item-DhXpJNAQ.d.ts → item-SUC4uKDO.d.ts} +5 -5
- package/dist/latex/diagnostics.d.cts +1 -1
- package/dist/latex/diagnostics.d.ts +1 -1
- package/dist/latex/lint.cjs +4 -6
- package/dist/latex/lint.d.cts +1 -1
- package/dist/latex/lint.d.ts +1 -1
- package/dist/latex/lint.js +4 -6
- package/dist/latex/lower.cjs +1 -1
- package/dist/latex/lower.d.cts +1 -1
- package/dist/latex/lower.d.ts +1 -1
- package/dist/latex/lower.js +1 -1
- package/dist/latex/symbols.d.cts +1 -1
- package/dist/latex/symbols.d.ts +1 -1
- package/dist/latex/temml.d.cts +2 -2
- package/dist/latex/temml.d.ts +2 -2
- package/dist/layout/cell-typing.d.cts +1 -1
- package/dist/layout/cell-typing.d.ts +1 -1
- package/dist/layout/drawing.d.cts +1 -1
- package/dist/layout/drawing.d.ts +1 -1
- package/dist/layout/engine.d.cts +1 -1
- package/dist/layout/engine.d.ts +1 -1
- package/dist/layout/reconstruct.cjs +1 -1
- package/dist/layout/reconstruct.d.cts +1 -1
- package/dist/layout/reconstruct.d.ts +1 -1
- package/dist/layout/reconstruct.js +1 -1
- package/dist/layout/sheets.d.cts +1 -1
- package/dist/layout/sheets.d.ts +1 -1
- package/dist/layout/slides.d.cts +1 -1
- package/dist/layout/slides.d.ts +1 -1
- package/dist/layout/text-layout.d.cts +1 -1
- package/dist/layout/text-layout.d.ts +1 -1
- package/dist/{lexer-DGPWx_f2.d.cts → lexer-DgsW7jMA.d.cts} +9 -9
- package/dist/{lexer-DGPWx_f2.d.ts → lexer-DgsW7jMA.d.ts} +9 -9
- package/dist/{list-C55W_YCp.d.ts → list-B_Sy668f.d.ts} +1 -1
- package/dist/{list-lTB3eDLC.d.cts → list-BiTN6oop.d.cts} +1 -1
- package/dist/markdown/math.d.cts +1 -1
- package/dist/markdown/math.d.ts +1 -1
- package/dist/markdown/render.d.cts +1 -1
- package/dist/markdown/render.d.ts +1 -1
- package/dist/mathml/layout-types.d.cts +1 -1
- package/dist/mathml/layout-types.d.ts +1 -1
- package/dist/mathml/layout.d.cts +2 -2
- package/dist/mathml/layout.d.ts +2 -2
- package/dist/mathml/nodes.d.cts +1 -1
- package/dist/mathml/nodes.d.ts +1 -1
- package/dist/mathml/variant.d.cts +1 -1
- package/dist/mathml/variant.d.ts +1 -1
- package/dist/model/embedded-drawing.d.cts +1 -1
- package/dist/model/embedded-drawing.d.ts +1 -1
- package/dist/model/paint-order.d.cts +2 -2
- package/dist/model/paint-order.d.ts +2 -2
- package/dist/{nodes-pArN9ilm.d.cts → nodes-jrlUmrNs.d.cts} +3 -3
- package/dist/{nodes-pArN9ilm.d.ts → nodes-jrlUmrNs.d.ts} +3 -3
- package/dist/odb/formula/evaluate.d.cts +4 -4
- package/dist/odb/formula/evaluate.d.ts +4 -4
- package/dist/odb/formula/parser.d.cts +6 -6
- package/dist/odb/formula/parser.d.ts +6 -6
- package/dist/odb/read.d.cts +1 -1
- package/dist/odb/read.d.ts +1 -1
- package/dist/odb/report/render.d.cts +1 -1
- package/dist/odb/report/render.d.ts +1 -1
- package/dist/odb/report/source.d.cts +1 -1
- package/dist/odb/report/source.d.ts +1 -1
- package/dist/odb/sql/evaluate.d.cts +1 -1
- package/dist/odb/sql/evaluate.d.ts +1 -1
- package/dist/odb/sql/lexer.d.cts +1 -1
- package/dist/odb/sql/lexer.d.ts +1 -1
- package/dist/odb/sql/parser.d.cts +1 -1
- package/dist/odb/sql/parser.d.ts +1 -1
- package/dist/odb/values.d.cts +1 -1
- package/dist/odb/values.d.ts +1 -1
- package/dist/odf/vector/detect.cjs +1 -1
- package/dist/odf/vector/detect.js +1 -1
- package/dist/odf-package/media.d.cts +1 -1
- package/dist/odf-package/media.d.ts +1 -1
- package/dist/omml/read.d.cts +1 -1
- package/dist/omml/read.d.ts +1 -1
- package/dist/omml/shared.d.cts +1 -1
- package/dist/omml/shared.d.ts +1 -1
- package/dist/omml/write.d.cts +1 -1
- package/dist/omml/write.d.ts +1 -1
- package/dist/ooxml/docx/embedded-objects.cjs +10 -24
- package/dist/ooxml/docx/embedded-objects.d.cts +1 -1
- package/dist/ooxml/docx/embedded-objects.d.ts +1 -1
- package/dist/ooxml/docx/embedded-objects.js +10 -24
- package/dist/ooxml/docx/formula.d.cts +1 -1
- package/dist/ooxml/docx/formula.d.ts +1 -1
- package/dist/ooxml/docx/read.d.cts +1 -1
- package/dist/ooxml/docx/read.d.ts +1 -1
- package/dist/ooxml/pptx/formula.d.cts +1 -1
- package/dist/ooxml/pptx/formula.d.ts +1 -1
- package/dist/ooxml/pptx/read.d.cts +1 -1
- package/dist/ooxml/pptx/read.d.ts +1 -1
- package/dist/ooxml/pptx/vector.cjs +1 -1
- package/dist/ooxml/pptx/vector.js +1 -1
- package/dist/opc/media.d.cts +1 -1
- package/dist/opc/media.d.ts +1 -1
- package/dist/opc/rels.d.cts +1 -1
- package/dist/opc/rels.d.ts +1 -1
- package/dist/{paragraph-CoPeTuHR.d.cts → paragraph-BBmkECjw.d.cts} +1 -1
- package/dist/{paragraph--69uh-Ys.d.ts → paragraph-D_fDin5K.d.ts} +1 -1
- package/dist/{parser-DQSgYJzQ.d.ts → parser-D0IJI7LA.d.ts} +22 -22
- package/dist/{parser-B9vC1T6T.d.cts → parser-DYfiAasj.d.cts} +22 -22
- package/dist/{registry-BcBT-bnX.d.cts → registry-913jEe6-.d.ts} +3 -3
- package/dist/{registry-D1Pu5XxB.d.ts → registry-Bs4IHqVQ.d.cts} +3 -3
- package/dist/{shape-DyIjuKE9.d.ts → shape-BY5P8kDn.d.ts} +2 -2
- package/dist/{shape-CaG-RPiz.d.cts → shape-Ca-xsrde.d.cts} +2 -2
- package/dist/{shape-CLHm7U8Y.d.cts → shape-DqkYq1OL.d.cts} +2 -2
- package/dist/{shape-Dnz6-wmG.d.ts → shape-xpgICOEx.d.ts} +2 -2
- package/dist/{shared-DUOzjwcL.d.ts → shared-DHu5jDJf.d.ts} +2 -2
- package/dist/{shared-DLZ3IQUl.d.cts → shared-c0Lp3_iY.d.cts} +2 -2
- package/dist/svg/paint.d.cts +5 -5
- package/dist/svg/paint.d.ts +5 -5
- package/dist/svg/path.d.cts +2 -2
- package/dist/svg/path.d.ts +2 -2
- package/dist/svg/write.d.cts +2 -2
- package/dist/svg/write.d.ts +2 -2
- package/dist/{table-CTeOe_vm.d.ts → table-C3VWWdjr.d.ts} +1 -1
- package/dist/{table-DH0lJRVv.d.cts → table-C_KDsX5R.d.cts} +1 -1
- package/dist/{table-BxMiEZXd.d.ts → table-Ck0fyr8d.d.ts} +1 -1
- package/dist/{table-C_tSXnbt.d.cts → table-CpEmblWG.d.cts} +1 -1
- package/dist/{values-BetetbaH.d.cts → values-BBN9E8dV.d.cts} +5 -5
- package/dist/{values-BetetbaH.d.ts → values-BBN9E8dV.d.ts} +5 -5
- package/dist/{variant-BMrebjMw.d.cts → variant-uwYyRetr.d.cts} +1 -1
- package/dist/{variant-BMrebjMw.d.ts → variant-uwYyRetr.d.ts} +1 -1
- package/dist/{vector-D03KX8Ux.d.cts → vector-DDP7hgvm.d.cts} +2 -2
- package/dist/{vector-D03KX8Ux.d.ts → vector-DDP7hgvm.d.ts} +2 -2
- package/dist/{write-BQ6SK8r8.d.cts → write-DWupt1QI.d.cts} +2 -2
- package/dist/{write-THs7ipBq.d.ts → write-qKlz1R0J.d.ts} +2 -2
- package/dist/xml/odf-text.cjs +1 -1
- package/dist/xml/odf-text.js +1 -1
- package/package.json +7 -16
package/README.md
CHANGED
|
@@ -75,14 +75,16 @@ npm install documents.js document-schema.js
|
|
|
75
75
|
A single function, `convertDocument`, sits behind every named conversion and reaches every pair the composition engine can route — all 111 supported (source, target) combinations. The named functions below are thin one-line forwarders to it; they remain the ergonomic layer for a caller who wants a fixed pair and autocomplete discovery, while `convertDocument` is the first-class entry point for a caller working from a runtime format pair (CLI, MCP tool, matrix enumeration).
|
|
76
76
|
|
|
77
77
|
```ts
|
|
78
|
-
import { convertDocument } from
|
|
78
|
+
import { convertDocument } from "documents.js";
|
|
79
79
|
|
|
80
80
|
// markdown -> pptx has no named function of its own: the composition engine routes it
|
|
81
81
|
// as one cross-variant transform hop (read wordprocessing, wordprocessingToPresentation, build pptx).
|
|
82
|
-
const pptxBytes = convertDocument(
|
|
82
|
+
const pptxBytes = convertDocument("markdown", "pptx", markdownBytes);
|
|
83
83
|
|
|
84
84
|
// Every option a named function accepts is accepted here too, threaded to whichever hop consumes it.
|
|
85
|
-
const odtBytes = convertDocument(
|
|
85
|
+
const odtBytes = convertDocument("docx", "odt", docxBytes, {
|
|
86
|
+
onMathDiagnostic: (d) => console.warn(d),
|
|
87
|
+
});
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
`convertDocument` throws `UnsupportedConversionError` (a named class, so a caller can branch on it) for any pair the composition engine cannot route — there is no silent fallback. `resolveCompositionPlan(source, target)` is exported too, for surfacing the resolved hop plan without running it.
|
|
@@ -92,7 +94,28 @@ const odtBytes = convertDocument('docx', 'odt', docxBytes, { onMathDiagnostic: (
|
|
|
92
94
|
The sixteen round-trip ergonomic conversions between the formats with their own layout engine and PDF (docx/pptx/odt/odp/ods/odg/markdown/svg ⇄ PDF, all round-tripping both ways), plus `xlsxToPdf`/`pdfToXlsx` and `csvToPdf`/`pdfToCsv` (each composing its ods bridge with the ods⇄pdf layout pair internally — neither xlsx nor csv has a layout engine of its own):
|
|
93
95
|
|
|
94
96
|
```ts
|
|
95
|
-
import {
|
|
97
|
+
import {
|
|
98
|
+
csvToPdf,
|
|
99
|
+
docxToPdf,
|
|
100
|
+
markdownToPdf,
|
|
101
|
+
odgToPdf,
|
|
102
|
+
odpToPdf,
|
|
103
|
+
odsToPdf,
|
|
104
|
+
odtToPdf,
|
|
105
|
+
pdfToCsv,
|
|
106
|
+
pdfToDocx,
|
|
107
|
+
pdfToMarkdown,
|
|
108
|
+
pdfToOdg,
|
|
109
|
+
pdfToOdp,
|
|
110
|
+
pdfToOds,
|
|
111
|
+
pdfToOdt,
|
|
112
|
+
pdfToPptx,
|
|
113
|
+
pdfToSvg,
|
|
114
|
+
pdfToXlsx,
|
|
115
|
+
pptxToPdf,
|
|
116
|
+
svgToPdf,
|
|
117
|
+
xlsxToPdf,
|
|
118
|
+
} from "documents.js";
|
|
96
119
|
|
|
97
120
|
const pdfBytes = docxToPdf(docxBytes);
|
|
98
121
|
const docxBytes2 = pdfToDocx(pdfBytes);
|
|
@@ -134,7 +157,12 @@ Each accepts an optional `signal` (`AbortSignal`) and either `onSubstitution` (X
|
|
|
134
157
|
Twenty-four bridge functions across twelve pairs bypass the PDF pivot where a direct path exists. Eight same-variant direct-copy pairs (`odtToDocx`/`docxToOdt`, `odpToPptx`/`pptxToOdp`, `odsToXlsx`/`xlsxToOds`, `csvToOds`/`odsToCsv`, `csvToXlsx`/`xlsxToCsv`, `svgToOdg`/`odgToSvg`, `markdownToDocx`/`docxToMarkdown`, `markdownToOdt`/`odtToMarkdown`) compose a direct `readXContent` → `buildYPackage` pivot copy — the csv pairs are one hop to its spreadsheet siblings, so csv never needs PDF to reach ods or xlsx, and `svgToOdg`/`odgToSvg` bridge svg to its drawing sibling odg the same way. Two cross-variant semantic-transform pairs (`docxToPptx`/`pptxToDocx`, `odtToOdp`/`odpToOdt`) go through `src/convert/variant-bridges.ts`. Two PDF-composed pairs (`xlsxToMarkdown`/`markdownToXlsx`, `csvToMarkdown`/`markdownToCsv`) route through PDF internally — the lossiest conversions in the package.
|
|
135
158
|
|
|
136
159
|
```ts
|
|
137
|
-
import {
|
|
160
|
+
import {
|
|
161
|
+
odtToDocx,
|
|
162
|
+
docxToOdt,
|
|
163
|
+
markdownToDocx,
|
|
164
|
+
docxToMarkdown,
|
|
165
|
+
} from "documents.js";
|
|
138
166
|
|
|
139
167
|
const docxBytes = odtToDocx(odtBytes);
|
|
140
168
|
const odtBytes2 = docxToOdt(docxBytes);
|
|
@@ -150,11 +178,11 @@ Each takes an optional `{ signal }` — no `onSubstitution`/`sink`, since there
|
|
|
150
178
|
The same conversions behind a swappable port, for a caller that wants to inject a different implementation without changing call sites:
|
|
151
179
|
|
|
152
180
|
```ts
|
|
153
|
-
import { createLocalDocumentConverter } from
|
|
181
|
+
import { createLocalDocumentConverter } from "documents.js";
|
|
154
182
|
|
|
155
183
|
const converter = createLocalDocumentConverter();
|
|
156
184
|
const { document, diagnostics } = await converter.convert(
|
|
157
|
-
{ source: { format:
|
|
185
|
+
{ source: { format: "docx", bytes: docxBytes }, targetFormat: "pdf" },
|
|
158
186
|
{ signal: new AbortController().signal },
|
|
159
187
|
);
|
|
160
188
|
```
|
|
@@ -164,7 +192,7 @@ const { document, diagnostics } = await converter.convert(
|
|
|
164
192
|
The port also exposes `contractVersion: number`, bumped only when `DocumentConverter`'s own contract shape changes — a new field on `ConversionResult` a caller might need to branch on, or a new `ConversionOptions` field an implementation is now expected to honour — never when the `conversions` table simply grows with more supported source/target pairs (that's discoverable at runtime via `conversions` itself). It is currently `7`: the bump from `6` reflects `ConversionResult.package` changing type to the tree-form `DocumentTree` described below, which a caller reading that field must now flatten rather than read directly.
|
|
165
193
|
|
|
166
194
|
```ts
|
|
167
|
-
import { DOCUMENT_FORMATS, DocumentFormatSchema } from
|
|
195
|
+
import { DOCUMENT_FORMATS, DocumentFormatSchema } from "documents.js";
|
|
168
196
|
|
|
169
197
|
console.log(DOCUMENT_FORMATS); // ['docx', 'pptx', 'xlsx', 'odt', 'odp', 'ods', 'odg', 'svg', 'odf', 'csv', 'markdown', 'pdf']
|
|
170
198
|
DocumentFormatSchema.parse(userSuppliedFormat); // throws a ZodError for anything outside that list
|
|
@@ -175,16 +203,21 @@ DocumentFormatSchema.parse(userSuppliedFormat); // throws a ZodError for anythin
|
|
|
175
203
|
Every conversion function accepts an `onDocument` callback receiving the intermediate `DocumentTree` — since document-schema.js 4, the single hierarchical tree: `children` carry the decomposed group tree (one group per container — a section, slide, sheet, or draw page — with heading and list paragraphs anchoring nested groups inside their container's flow), and the content nodes embedded in that tree carry `frames`, the rendered page positions the layout pass stamped onto them, in PDF user-space. `pages` (each rendered page's size, indexed to match every `frames[].pageIndex`) and the minted `styles` table ride the root. The port surfaces the same value as `package` on `ConversionResult`. For PDF-bypassing bridges, `pkg.pages` is always `undefined` and no node carries frames — no layout pass ran.
|
|
176
204
|
|
|
177
205
|
```ts
|
|
178
|
-
import { flattenTree } from
|
|
179
|
-
import { docxToPdf } from
|
|
206
|
+
import { flattenTree } from "document-schema.js";
|
|
207
|
+
import { docxToPdf } from "documents.js";
|
|
180
208
|
|
|
181
209
|
const pdfBytes = docxToPdf(docxBytes, {
|
|
182
210
|
onDocument: (pkg) => {
|
|
183
211
|
console.log(pkg.kind); // 'wordprocessing' -- the document kind rides the tree's root
|
|
184
212
|
console.log(pkg.pages?.length); // populated for every X-to-PDF/PDF-to-X conversion
|
|
185
213
|
const content = flattenTree(pkg); // the flat ContentDocument, fully materialised
|
|
186
|
-
const block =
|
|
187
|
-
|
|
214
|
+
const block =
|
|
215
|
+
content.kind === "wordprocessing"
|
|
216
|
+
? content.sections[0]?.blocks[0]
|
|
217
|
+
: undefined;
|
|
218
|
+
console.log(
|
|
219
|
+
block?.kind === "paragraph" ? block.runs[0]?.frames : "no paragraph",
|
|
220
|
+
); // that run's rendered placements
|
|
188
221
|
},
|
|
189
222
|
});
|
|
190
223
|
```
|
|
@@ -196,7 +229,12 @@ Three flat-form signals drive the grouping, and all three are reproduced exactly
|
|
|
196
229
|
`assembleTree` is the one constructor behind every construction site — decompose, then `factorStyles`, the minting pass that hoists property tuples occurring two or more times onto a group-wrapper ref plus a `styles` table entry (deterministic order; `frames`/`sourcePath`/`styleId` are per-node facts and never factor). The transform belongs to `document-schema.js`, which owns both encodings and publishes `assembleTree`, `decompose`, `flattenTree`, `factorStyles`, `ConstructMarkerImbalanceError`, and the `TreeChildren` type for any caller composing its own boundary — import them from there, not from this package. documents.js consumes that transform at its own boundary and re-exports none of it; the readers, builders, layout engines, and editors here keep producing and consuming the flat form, so the tree exists only where a `DocumentTree` is constructed or consumed:
|
|
197
230
|
|
|
198
231
|
```ts
|
|
199
|
-
import {
|
|
232
|
+
import {
|
|
233
|
+
assembleTree,
|
|
234
|
+
decompose,
|
|
235
|
+
factorStyles,
|
|
236
|
+
flattenTree,
|
|
237
|
+
} from "document-schema.js";
|
|
200
238
|
|
|
201
239
|
const tree = assembleTree(content, pages); // decompose + mint: the tree a conversion reports
|
|
202
240
|
const flat = flattenTree(tree); // the exact flat ContentDocument back, refs materialised
|
|
@@ -206,24 +244,30 @@ const again = factorStyles(tree); // re-mint: identical table and tree (law iii)
|
|
|
206
244
|
`documentTreeWithSchema`/`documentFromJson` turn a `DocumentTree` into self-describing JSON and back (re-exported from `document-schema.js`); the version-pinned `$schema` URI the dumper stamps is the package's version — the hand-kept `formatVersion` integer is gone:
|
|
207
245
|
|
|
208
246
|
```ts
|
|
209
|
-
import { documentFromJson, documentTreeWithSchema } from
|
|
247
|
+
import { documentFromJson, documentTreeWithSchema } from "documents.js";
|
|
210
248
|
|
|
211
249
|
const tagged = documentTreeWithSchema(pkg);
|
|
212
|
-
writeFileSync(
|
|
250
|
+
writeFileSync("converted.doc.json", JSON.stringify(tagged, null, 2));
|
|
213
251
|
|
|
214
|
-
const { kind, value } = documentFromJson(
|
|
252
|
+
const { kind, value } = documentFromJson(
|
|
253
|
+
JSON.parse(readFileSync("converted.doc.json", "utf8")),
|
|
254
|
+
);
|
|
215
255
|
// kind: 'DocumentTree' (here) | 'ContentDocument'
|
|
216
256
|
```
|
|
217
257
|
|
|
218
258
|
`buildDocumentBytes` rebuilds any `DocumentFormat`'s bytes from a tree-form `DocumentTree` — it flattens once at the boundary and hands the flat form to the builders, whose signatures never changed. `'pdf'` rebuilds the pdf-codec view from the package's own frames+pages (`layoutDocumentFromPackage`, a mechanical inverse walking the flattened content and emitting `LayoutItem`s from each node's recorded placements; throwing if the package carries no `pages`), `'odf'` has no builder and throws, everything else rebuilds from the flattened `ContentDocument`. `layoutDocumentFromPackage` is exported too, for a caller wanting the rebuilt `LayoutDocument` without writing bytes. Two honest limits on the pdf rebuild, both structural properties of what a package records: a run's frames carry positions, not the wrap decisions that distributed its text across them, so a wrapped run re-renders once, whole, at its first recorded placement; and no font registry or positioned formula survives a bare package (a formula block's frame records where it sat while its glyphs render as nothing):
|
|
219
259
|
|
|
220
260
|
```ts
|
|
221
|
-
import { buildDocumentBytes, docxToPdf } from
|
|
261
|
+
import { buildDocumentBytes, docxToPdf } from "documents.js";
|
|
222
262
|
|
|
223
263
|
let captured;
|
|
224
|
-
docxToPdf(docxBytes, {
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
docxToPdf(docxBytes, {
|
|
265
|
+
onDocument: (pkg) => {
|
|
266
|
+
captured = pkg;
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const pdfBytesAgain = buildDocumentBytes(captured, "pdf");
|
|
270
|
+
const docxBytesAgain = buildDocumentBytes(captured, "docx");
|
|
227
271
|
```
|
|
228
272
|
|
|
229
273
|
### Package decode/encode, metadata, and deep imports
|
|
@@ -231,33 +275,40 @@ const docxBytesAgain = buildDocumentBytes(captured, 'docx');
|
|
|
231
275
|
`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`/`csv`/`svg`/`pdf` (none of the four is a package — the first three are plain text, pdf is bytes). `decodeOdbPackage` is the `.odb`-specific sibling (`.odb` is not a `DocumentFormat` member):
|
|
232
276
|
|
|
233
277
|
```ts
|
|
234
|
-
import {
|
|
278
|
+
import {
|
|
279
|
+
decodeDocumentPackage,
|
|
280
|
+
decodeOdbPackage,
|
|
281
|
+
encodeDocumentPackage,
|
|
282
|
+
} from "documents.js";
|
|
235
283
|
|
|
236
|
-
const pkg = decodeDocumentPackage(
|
|
237
|
-
const docxBytesAgain = encodeDocumentPackage(
|
|
284
|
+
const pkg = decodeDocumentPackage("docx", docxBytes);
|
|
285
|
+
const docxBytesAgain = encodeDocumentPackage("docx", pkg);
|
|
238
286
|
const odbPkg = decodeOdbPackage(odbBytes);
|
|
239
287
|
```
|
|
240
288
|
|
|
241
289
|
`readDocumentMetadata`/`setDocumentMetadata` read or patch metadata across any `DocumentFormat`. `setDocumentMetadata` patches in place (source/target formats must match); `odf` is rejected in both directions, and `csv` is rejected in both directions too (RFC 4180 text has no metadata container) — `readDocumentMetadata('csv', ...)` answers an empty `LayoutMetadata` for the same reason. `svg` reads its root `<title>` as `metadata.title` and is rejected as a `setDocumentMetadata` source/target for the mirror-image reason: `<title>` is svg's whole metadata surface, so any other override would be silently dropped by the rebuild. `readDocumentMetadata('xlsx', ...)` reads the workbook's own `docProps` like every other content format — `createdIso`/`modifiedIso` from `docProps/core.xml` when the file declares them, `docProps/app.xml`'s Application as `creator`, and `producer` unset (a PDF-only concept no semantic reader sets). It previously rendered a `xlsxToPdf` preview and read that PDF's metadata instead, which for a file carrying no timestamps of its own reported facts about the render rather than the workbook: timestamps stamped at the render moment and a producer naming the preview PDF's writer.
|
|
242
290
|
|
|
243
291
|
```ts
|
|
244
|
-
import { readDocumentMetadata, setDocumentMetadata } from
|
|
292
|
+
import { readDocumentMetadata, setDocumentMetadata } from "documents.js";
|
|
245
293
|
|
|
246
|
-
const metadata = readDocumentMetadata(
|
|
247
|
-
const patchedBytes = setDocumentMetadata(
|
|
294
|
+
const metadata = readDocumentMetadata("docx", docxBytes);
|
|
295
|
+
const patchedBytes = setDocumentMetadata("docx", "docx", docxBytes, {
|
|
296
|
+
title: "New title",
|
|
297
|
+
keywords: ["a", "b"],
|
|
298
|
+
});
|
|
248
299
|
```
|
|
249
300
|
|
|
250
301
|
Every module under `src/` is deep-importable by package-relative path:
|
|
251
302
|
|
|
252
303
|
```ts
|
|
253
|
-
import { emuToPt } from
|
|
254
|
-
import { buildOdtPackage } from
|
|
304
|
+
import { emuToPt } from "documents.js/model/units";
|
|
305
|
+
import { buildOdtPackage } from "documents.js/edit/odt/content";
|
|
255
306
|
```
|
|
256
307
|
|
|
257
308
|
One subpath is a declared entry point in its own right: **`documents.js/read`** (an explicit `exports` entry onto `src/convert/from-pdf.ts`, where the `pdfTo*` family lives). A consumer that only ever converts FROM pdf and imports the root barrel statically reaches every X-to-PDF renderer — and through pdf-codec's root barrel, ~2.9 MB of vendored font binaries it can never execute, which on Cloudflare Workers' free plan (3 MB gzipped for an entire Worker) is most of the budget. The read entry's module graph provably excludes them, all the way across the workspace boundary into pdf-codec's own source:
|
|
258
309
|
|
|
259
310
|
```ts
|
|
260
|
-
import { pdfToMarkdown } from
|
|
311
|
+
import { pdfToMarkdown } from "documents.js/read";
|
|
261
312
|
```
|
|
262
313
|
|
|
263
314
|
It carries the ten `pdfTo*` conversions, `PdfToDocumentOptions`, and `readDocumentMetadata` (with `ReadDocumentMetadataOptions`) — identical functions to the root barrel's (the same forwarders, run through the composition engine's read half, and the same metadata reader dispatched through the read-only codec half `src/codecs/read.ts`), never a forked behaviour; `convertDocument` and every X-to-PDF direction stay on the root barrel. `src/read-graph.test.ts` walks the entry's static import graph, follows `pdf-codec` specifiers through that package's real `exports` map into its source, and fails the build if the write path or any font asset becomes reachable.
|
|
@@ -267,10 +318,15 @@ It carries the ten `pdfTo*` conversions, `PdfToDocumentOptions`, and `readDocume
|
|
|
267
318
|
Every other content format has its own standalone `readXContent`-shaped entry point (`readDocxContent`, `readPptxContent`, `readOdtContent`, `readOdpContent`, `readOdsContent`, `readOdgContent`) — xlsx is no longer the exception. `readXlsxContent`/`buildXlsxPackage` are this package's names for `ooxml.js`'s own spreadsheet `ContentDocument` read/build pair — the same one the `ods⇄xlsx` bridge and every xlsx metadata-rebuild path already use internally — re-exported here directly rather than wrapped, since `readXlsxContent` already produces the right shape on its own. (Since `ooxml.js` 4.0.0 the upstream flat builder is named `buildXlsxPackageFromContent` — the bare `buildXlsxPackage` name moved to that package's tree-form `DocumentTree` builder — so this package re-exports the flat builder under its own long-standing `buildXlsxPackage` name and the `ContentDocument`-in/`Package`-out contract is unchanged.) csv's `readCsvContent`/`buildCsvText` are the same kind of directly-exported stage pair, one level further in: they operate on RFC 4180 text rather than a decoded package (see `src/csv/` under Architecture). svg's `readSvgContent`/`buildSvgText` are the drawing-variant counterpart of csv's pair, operating on SVG text rather than a decoded package (see `src/svg/` under Architecture).
|
|
268
319
|
|
|
269
320
|
```ts
|
|
270
|
-
import {
|
|
321
|
+
import {
|
|
322
|
+
buildXlsxPackage,
|
|
323
|
+
decodeDocumentPackage,
|
|
324
|
+
encodeDocumentPackage,
|
|
325
|
+
readXlsxContent,
|
|
326
|
+
} from "documents.js";
|
|
271
327
|
|
|
272
|
-
const content = readXlsxContent(decodeDocumentPackage(
|
|
273
|
-
const rebuiltBytes = encodeDocumentPackage(
|
|
328
|
+
const content = readXlsxContent(decodeDocumentPackage("xlsx", xlsxBytes)); // ContentDocument, kind: 'spreadsheet'
|
|
329
|
+
const rebuiltBytes = encodeDocumentPackage("xlsx", buildXlsxPackage(content));
|
|
274
330
|
```
|
|
275
331
|
|
|
276
332
|
This pair is comparatively newer than the ODF/DrawingML readers above, and inherits their maturity level: percentage, currency, and date cell kinds round-trip with their semantic kind intact, but two narrower gaps are worth knowing before relying on it for more than read-only extraction — an ODS-style time-only value has no xlsx serial to write into and degrades to a plain string cell, and a written column width survives a read back only within about a point of its original value (an algebraic-inverse rounding artifact in the character-width unit conversion, not a dropped value). See `src/convert/bridges.test.ts`'s own `ods⇄xlsx` section for the exact, currently-tested numbers.
|
|
@@ -280,91 +336,144 @@ This pair is comparatively newer than the ODF/DrawingML readers above, and inher
|
|
|
280
336
|
Read-and-write editors for docx/pptx/odt/odp/ods/odg content, holding a direct reference into the real `Package`/`XmlElement` objects. Saving is `encodePackage(pkg)` — everything you didn't touch stays byte-faithful.
|
|
281
337
|
|
|
282
338
|
```ts
|
|
283
|
-
import { openDocx, createDocx } from
|
|
339
|
+
import { openDocx, createDocx } from "documents.js";
|
|
284
340
|
|
|
285
341
|
const editor = openDocx(existingDocxBytes);
|
|
286
|
-
const paragraph = editor.body.appendParagraph({ alignment:
|
|
287
|
-
const run = paragraph.appendRun({ text:
|
|
342
|
+
const paragraph = editor.body.appendParagraph({ alignment: "center" });
|
|
343
|
+
const run = paragraph.appendRun({ text: "Hello" });
|
|
288
344
|
run.bold = true;
|
|
289
345
|
run.color = { r: 1, g: 0, b: 0 };
|
|
290
346
|
const bytes = editor.toBytes();
|
|
291
347
|
|
|
292
348
|
const fresh = createDocx();
|
|
293
|
-
fresh.body.appendParagraph().appendRun({ text:
|
|
349
|
+
fresh.body.appendParagraph().appendRun({ text: "New document" });
|
|
294
350
|
```
|
|
295
351
|
|
|
296
352
|
A docx's comments, footnotes, header/footer parts, section header/footer references, and numbering definitions never fit `ContentDocument`'s section/block shape — `readDocxExtras` is a second, independent read returning exactly that data:
|
|
297
353
|
|
|
298
354
|
```ts
|
|
299
|
-
import { readDocxExtras } from
|
|
300
|
-
import { decodePackage } from
|
|
355
|
+
import { readDocxExtras } from "documents.js";
|
|
356
|
+
import { decodePackage } from "ooxml.js";
|
|
301
357
|
|
|
302
|
-
const {
|
|
303
|
-
|
|
358
|
+
const {
|
|
359
|
+
comments,
|
|
360
|
+
footnotes,
|
|
361
|
+
headerFooterParts,
|
|
362
|
+
sectionHeaderFooters,
|
|
363
|
+
numbering,
|
|
364
|
+
} = readDocxExtras(decodePackage(docxBytes));
|
|
365
|
+
console.log(Object.values(numbering)[0]?.levels["0"]?.format); // numbering is keyed by numId, each level by its own level index
|
|
304
366
|
```
|
|
305
367
|
|
|
306
368
|
`openPptx`/`createPptx` and `PptxSlide`/`PptxShape` are the pptx equivalent. `embeddedPresentationSerialiser` is ooxml.js's embedded-presentation port wired from this package's own pptx builder. ooxml.js has no PresentationML writer and cannot depend on the one pptx writer in the ecosystem (`buildPptxPackage`, living here one layer above it), so its docx writer instead accepts an injected serialiser; pass this value as `BuildDocxContentOptions.serialiseEmbeddedPresentation` and a docx carrying an OLE-embedded presentation — which `readDocxContent` genuinely recovers as an `embeddedObject` block — round-trips through that writer, the nested deck re-serialised into a real `word/embeddings/oleObject<N>.pptx` payload rather than refused:
|
|
307
369
|
|
|
308
370
|
```ts
|
|
309
|
-
import { embeddedPresentationSerialiser } from
|
|
310
|
-
import {
|
|
371
|
+
import { embeddedPresentationSerialiser } from "documents.js";
|
|
372
|
+
import {
|
|
373
|
+
buildDocxPackageFromContent,
|
|
374
|
+
decodePackage,
|
|
375
|
+
encodePackage,
|
|
376
|
+
readDocxContent,
|
|
377
|
+
} from "ooxml.js";
|
|
311
378
|
|
|
312
379
|
const content = readDocxContent(decodePackage(docxBytes)); // carries a presentation embed
|
|
313
|
-
const rebuilt = encodePackage(
|
|
380
|
+
const rebuilt = encodePackage(
|
|
381
|
+
buildDocxPackageFromContent(content, {
|
|
382
|
+
serialiseEmbeddedPresentation: embeddedPresentationSerialiser,
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
314
385
|
```
|
|
315
386
|
|
|
316
387
|
`openOdt`/`createOdt` and `OdtParagraph`/`OdtRun`/`OdtTable`/`OdtList` are the odt equivalent, built on ODF's style-name-referencing model. `openOdp`/`createOdp` and `OdpSlide`/`OdpShape` reuse `OdtParagraph`/`OdtRun`/`OdtList` directly (a `draw:frame`'s `draw:text-box` holds the identical `text:p`/`text:span` model):
|
|
317
388
|
|
|
318
389
|
```ts
|
|
319
|
-
import { createOdp } from
|
|
390
|
+
import { createOdp } from "documents.js";
|
|
320
391
|
|
|
321
392
|
const editor = createOdp();
|
|
322
393
|
const slide = editor.addSlide();
|
|
323
|
-
const title = slide.addTextBox({
|
|
394
|
+
const title = slide.addTextBox({
|
|
395
|
+
frame: { xPt: 40, yPt: 30, widthPt: 640, heightPt: 80 },
|
|
396
|
+
text: "Title",
|
|
397
|
+
});
|
|
324
398
|
title.rotationDeg = 15; // OdpShape has a genuine draw:transform rotation setter
|
|
325
|
-
const bullets = slide.addTextBox({
|
|
399
|
+
const bullets = slide.addTextBox({
|
|
400
|
+
frame: { xPt: 40, yPt: 130, widthPt: 300, heightPt: 200 },
|
|
401
|
+
text: "",
|
|
402
|
+
});
|
|
326
403
|
bullets.paragraphs()[0].remove();
|
|
327
|
-
bullets
|
|
328
|
-
|
|
404
|
+
bullets
|
|
405
|
+
.addList()
|
|
406
|
+
.addItem()
|
|
407
|
+
.appendParagraph({ text: "A real bulleted text:list" });
|
|
408
|
+
slide.notes = "Speaker notes for this slide";
|
|
329
409
|
const bytes = editor.toBytes();
|
|
330
410
|
```
|
|
331
411
|
|
|
332
412
|
`createOds`/`openOds` and `OdsEditor`/`OdsSheet`/`OdsCell` are the spreadsheet equivalent — the one editor family built from scratch (cell addressing has no docx/pptx analogue). Setting a cell far from the origin splits `table:number-*-repeated` runs in place rather than materialising every cell in between:
|
|
333
413
|
|
|
334
414
|
```ts
|
|
335
|
-
import { createOds } from
|
|
415
|
+
import { createOds } from "documents.js";
|
|
336
416
|
|
|
337
417
|
const editor = createOds();
|
|
338
|
-
const sheet = editor.addSheet(
|
|
339
|
-
sheet.printSettings = {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
418
|
+
const sheet = editor.addSheet("Sheet1");
|
|
419
|
+
sheet.printSettings = {
|
|
420
|
+
pageSize: { widthPt: 595, heightPt: 842 },
|
|
421
|
+
margins: { topPt: 20, rightPt: 20, bottomPt: 20, leftPt: 20 },
|
|
422
|
+
gridlines: true,
|
|
423
|
+
headers: true,
|
|
424
|
+
pageOrder: "downThenOver",
|
|
425
|
+
};
|
|
426
|
+
sheet.cell(0, 0).value = { kind: "string", value: "Total" }; // 0-based (row, column)
|
|
427
|
+
sheet.cell(0, 1).value = { kind: "currency", value: 42.5, currency: "USD" };
|
|
428
|
+
sheet.cell(500, 50).value = { kind: "boolean", value: true }; // does not materialise 500x50 empty cells
|
|
343
429
|
const bytes = editor.toBytes();
|
|
344
430
|
```
|
|
345
431
|
|
|
346
432
|
`createOdg`/`openOdg` and `OdgEditor`/`OdgPage` are the drawing equivalent. `OdgPage.addTextBox`/`.addImage` return `OdpShape` instances; `addRect`/`addEllipse`/`addLine`/`addPath` return vector classes writing real `draw:rect`/`draw:ellipse`/`draw:line`/`draw:path` elements:
|
|
347
433
|
|
|
348
434
|
```ts
|
|
349
|
-
import { createOdg } from
|
|
435
|
+
import { createOdg } from "documents.js";
|
|
350
436
|
|
|
351
437
|
const editor = createOdg();
|
|
352
438
|
const page = editor.addPage();
|
|
353
|
-
page.addRect({
|
|
354
|
-
|
|
439
|
+
page.addRect({
|
|
440
|
+
frame: { xPt: 20, yPt: 20, widthPt: 100, heightPt: 60 },
|
|
441
|
+
fill: { r: 1, g: 0.5, b: 0 },
|
|
442
|
+
});
|
|
443
|
+
page.addEllipse({
|
|
444
|
+
frame: { xPt: 140, yPt: 20, widthPt: 100, heightPt: 60 },
|
|
445
|
+
stroke: { color: { r: 0, g: 0, b: 0 }, widthPt: 1 },
|
|
446
|
+
});
|
|
355
447
|
page.addPath({
|
|
356
448
|
frame: { xPt: 20, yPt: 100, widthPt: 80, heightPt: 80 },
|
|
357
|
-
subpaths: [
|
|
449
|
+
subpaths: [
|
|
450
|
+
{
|
|
451
|
+
start: { xPt: 0, yPt: 80 },
|
|
452
|
+
closed: true,
|
|
453
|
+
segments: [
|
|
454
|
+
{ kind: "line", to: { xPt: 60, yPt: 80 } },
|
|
455
|
+
{
|
|
456
|
+
kind: "cubic",
|
|
457
|
+
control1: { xPt: 80, yPt: 80 },
|
|
458
|
+
control2: { xPt: 80, yPt: 0 },
|
|
459
|
+
to: { xPt: 40, yPt: 0 },
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
},
|
|
463
|
+
],
|
|
358
464
|
fill: { r: 1, g: 1, b: 0 },
|
|
359
465
|
}); // a genuine Bezier curve -- writes a real svg:d/svg:viewBox pair, not a polygon approximation
|
|
360
|
-
page.addTextBox({
|
|
466
|
+
page.addTextBox({
|
|
467
|
+
frame: { xPt: 20, yPt: 200, widthPt: 300, heightPt: 30 },
|
|
468
|
+
text: "A label on top",
|
|
469
|
+
});
|
|
361
470
|
const bytes = editor.toBytes();
|
|
362
471
|
```
|
|
363
472
|
|
|
364
473
|
### PDF bytes and `z.codec()` pairs
|
|
365
474
|
|
|
366
475
|
```ts
|
|
367
|
-
import { readPdf, writePdf } from
|
|
476
|
+
import { readPdf, writePdf } from "documents.js";
|
|
368
477
|
|
|
369
478
|
const layout = readPdf(pdfBytes); // -> LayoutDocument: pages of positioned text/image/rect/link items
|
|
370
479
|
const bytes = writePdf(layout);
|
|
@@ -373,8 +482,8 @@ const bytes = writePdf(layout);
|
|
|
373
482
|
The eleven PDF round trips and sixteen PDF-bypassing bridge directions are also available as schema-validated [`z.codec()`](https://zod.dev) pairs (`pdfCodec`, `docxPdfCodec`, `pptxPdfCodec`, `odtPdfCodec`, `odpPdfCodec`, `odsPdfCodec`, `odgPdfCodec`, `svgPdfCodec`, `xlsxPdfCodec`, `csvPdfCodec`, `markdownPdfCodec`, `odtDocxCodec`, `odpPptxCodec`, `odsXlsxCodec`, `odsCsvCodec`, `xlsxCsvCodec`, `odgSvgCodec`, `markdownDocxCodec`, `markdownOdtCodec`) — the no-options form, adding automatic two-way schema validation. The two PDF-composed pairs have codec forms too (`xlsxMarkdownCodec`, `csvMarkdownCodec`):
|
|
374
483
|
|
|
375
484
|
```ts
|
|
376
|
-
import { z } from
|
|
377
|
-
import { docxPdfCodec, pdfCodec } from
|
|
485
|
+
import { z } from "zod";
|
|
486
|
+
import { docxPdfCodec, pdfCodec } from "documents.js";
|
|
378
487
|
|
|
379
488
|
const layout = z.decode(pdfCodec, pdfBytes); // throws a ZodError if pdfBytes has no %PDF- header
|
|
380
489
|
const pdfBytes2 = z.encode(pdfCodec, layout);
|
|
@@ -387,19 +496,21 @@ const docxBack = z.encode(docxPdfCodec, pdfFromDocx);
|
|
|
387
496
|
**`odmToPdf`** — ODF master document → PDF. A `.odm` never carries its chapters' content (each `text:section` is an external `.odt` reference), so it requires a caller-supplied `resolveSubDocument` callback. Not wired into the `DocumentConverter` port (its contract is bytes-in/bytes-out):
|
|
388
497
|
|
|
389
498
|
```ts
|
|
390
|
-
import { readFileSync } from
|
|
391
|
-
import { odmToPdf, OdmUnresolvedSectionError } from
|
|
499
|
+
import { readFileSync } from "node:fs";
|
|
500
|
+
import { odmToPdf, OdmUnresolvedSectionError } from "documents.js";
|
|
392
501
|
|
|
393
502
|
const chapterBytes = new Map([
|
|
394
|
-
[
|
|
395
|
-
[
|
|
503
|
+
["../chapter1.odt", new Uint8Array(readFileSync("chapter1.odt"))],
|
|
504
|
+
["../chapter2.odt", new Uint8Array(readFileSync("chapter2.odt"))],
|
|
396
505
|
]);
|
|
397
506
|
|
|
398
507
|
try {
|
|
399
|
-
const pdfBytes = odmToPdf(odmBytes, {
|
|
508
|
+
const pdfBytes = odmToPdf(odmBytes, {
|
|
509
|
+
resolveSubDocument: (href) => chapterBytes.get(href),
|
|
510
|
+
});
|
|
400
511
|
} catch (error) {
|
|
401
512
|
if (error instanceof OdmUnresolvedSectionError) {
|
|
402
|
-
console.error(
|
|
513
|
+
console.error("missing chapters:", error.hrefs); // every unresolved href, not just the first
|
|
403
514
|
}
|
|
404
515
|
}
|
|
405
516
|
```
|
|
@@ -407,19 +518,19 @@ try {
|
|
|
407
518
|
**`.odb` database front-end** — `readOdbTables` extracts every table; `odbToXlsx`/`odbToCsv` produce xlsx or CSV. All four storage tiers are supported (HSQLDB TEXT-script Tier 1, HSQLDB CACHED binary Tier 2, Firebird gbak Tier 3, HSQLDB BINARY/COMPRESSED Tier 4), dispatched automatically:
|
|
408
519
|
|
|
409
520
|
```ts
|
|
410
|
-
import { decodePackage } from
|
|
411
|
-
import { odbToCsv, odbToXlsx, readOdbTables } from
|
|
521
|
+
import { decodePackage } from "odf.js";
|
|
522
|
+
import { odbToCsv, odbToXlsx, readOdbTables } from "documents.js";
|
|
412
523
|
|
|
413
524
|
const xlsxBytes = odbToXlsx(odbBytes); // one xlsx sheet per table
|
|
414
|
-
const csvBytes = odbToCsv(odbBytes, { table:
|
|
525
|
+
const csvBytes = odbToCsv(odbBytes, { table: "CUSTOMERS" }); // required when the .odb has more than one table
|
|
415
526
|
const tables = readOdbTables(decodePackage(odbBytes)); // Package -> HsqldbTable[]
|
|
416
527
|
```
|
|
417
528
|
|
|
418
|
-
Form/Report
|
|
529
|
+
Form/Report _structure_: `readOdbForms`/`readOdbReports` read every declared component's static structure (bound controls, bands/groups/functions):
|
|
419
530
|
|
|
420
531
|
```ts
|
|
421
|
-
import { decodePackage } from
|
|
422
|
-
import { readOdbForms, readOdbReports } from
|
|
532
|
+
import { decodePackage } from "odf.js";
|
|
533
|
+
import { readOdbForms, readOdbReports } from "documents.js";
|
|
423
534
|
|
|
424
535
|
const forms = readOdbForms(decodePackage(odbBytes));
|
|
425
536
|
const reports = readOdbReports(decodePackage(odbBytes));
|
|
@@ -428,30 +539,42 @@ const reports = readOdbReports(decodePackage(odbBytes));
|
|
|
428
539
|
`readFirebirdBackup` decodes a Firebird `.fbk` directly:
|
|
429
540
|
|
|
430
541
|
```ts
|
|
431
|
-
import { readFirebirdBackup } from
|
|
542
|
+
import { readFirebirdBackup } from "documents.js";
|
|
432
543
|
const { summary, tables } = readFirebirdBackup(firebirdBackupBytes);
|
|
433
544
|
```
|
|
434
545
|
|
|
435
546
|
**SQL `SELECT` engine** — `parseSelect`/`evaluateSelect` run a bounded single-table `SELECT` over `readOdbTables`' output. Closed allowlist grammar: column list or `*` or aggregates (`COUNT`/`SUM`/`AVG`/`MIN`/`MAX`), `FROM` one table, optional `WHERE`/`GROUP BY`/`ORDER BY`. Everything else throws `HsqldbSqlUnsupportedError`:
|
|
436
547
|
|
|
437
548
|
```ts
|
|
438
|
-
import { decodePackage, readOdbInventory } from
|
|
439
|
-
import { evaluateSelect, parseSelect, readOdbTables } from
|
|
549
|
+
import { decodePackage, readOdbInventory } from "odf.js";
|
|
550
|
+
import { evaluateSelect, parseSelect, readOdbTables } from "documents.js";
|
|
440
551
|
|
|
441
552
|
const pkg = decodePackage(odbBytes);
|
|
442
553
|
const [query] = readOdbInventory(pkg).queries;
|
|
443
|
-
const { columns, rows } = evaluateSelect(
|
|
554
|
+
const { columns, rows } = evaluateSelect(
|
|
555
|
+
parseSelect(query.command),
|
|
556
|
+
readOdbTables(pkg),
|
|
557
|
+
);
|
|
444
558
|
```
|
|
445
559
|
|
|
446
560
|
**rpt formula engine** — `runRptReport` evaluates a report's group breaks and per-group totals. Closed allowlist: `rpt:HASCHANGED(X)`, `rpt:LEFT(X;n)` (semicolon separator), `rpt:SUM`/`COUNT`/`AVG`/`MIN`/`MAX`, and `field:[COLUMN]`. Everything else throws `RptFormulaUnsupportedError`:
|
|
447
561
|
|
|
448
562
|
```ts
|
|
449
|
-
import { decodePackage, readOdbInventory } from
|
|
450
|
-
import {
|
|
563
|
+
import { decodePackage, readOdbInventory } from "odf.js";
|
|
564
|
+
import {
|
|
565
|
+
evaluateSelect,
|
|
566
|
+
parseSelect,
|
|
567
|
+
readOdbReports,
|
|
568
|
+
readOdbTables,
|
|
569
|
+
rptDefinitionFromReport,
|
|
570
|
+
runRptReport,
|
|
571
|
+
} from "documents.js";
|
|
451
572
|
|
|
452
573
|
const pkg = decodePackage(odbBytes);
|
|
453
574
|
const [report] = readOdbReports(pkg);
|
|
454
|
-
const query = readOdbInventory(pkg).queries.find(
|
|
575
|
+
const query = readOdbInventory(pkg).queries.find(
|
|
576
|
+
(candidate) => candidate.name === report.command,
|
|
577
|
+
);
|
|
455
578
|
const rows = evaluateSelect(parseSelect(query.command), readOdbTables(pkg));
|
|
456
579
|
const { bands } = runRptReport(rptDefinitionFromReport(report), rows);
|
|
457
580
|
```
|
|
@@ -459,10 +582,17 @@ const { bands } = runRptReport(rptDefinitionFromReport(report), rows);
|
|
|
459
582
|
**Report rendering** — `readOdbReportContent` resolves data binding, runs the query, evaluates formulas, and renders bands as a real `ContentDocument`. `odbReportToDocx`/`odbReportToOdt`/`odbReportToPdf` dispatch it to bytes:
|
|
460
583
|
|
|
461
584
|
```ts
|
|
462
|
-
import { decodePackage } from
|
|
463
|
-
import {
|
|
585
|
+
import { decodePackage } from "odf.js";
|
|
586
|
+
import {
|
|
587
|
+
odbReportToDocx,
|
|
588
|
+
odbReportToOdt,
|
|
589
|
+
odbReportToPdf,
|
|
590
|
+
readOdbReportContent,
|
|
591
|
+
} from "documents.js";
|
|
464
592
|
|
|
465
|
-
const report = readOdbReportContent(decodePackage(odbBytes), {
|
|
593
|
+
const report = readOdbReportContent(decodePackage(odbBytes), {
|
|
594
|
+
report: "SalesByRegion",
|
|
595
|
+
});
|
|
466
596
|
const docxBytes = odbReportToDocx(report);
|
|
467
597
|
const pdfBytes = odbReportToPdf(report);
|
|
468
598
|
```
|
|
@@ -470,7 +600,7 @@ const pdfBytes = odbReportToPdf(report);
|
|
|
470
600
|
**`odfToPdf`** — standalone `.odf` formula document → PDF via the MathML typesetting engine. No reverse `pdfToOdf` (recovering structured MathML from rendered glyphs is OCR-adjacent). Formulas embedded inside odt/odp/ods render automatically through `odtToPdf`/`odpToPdf`/`odsToPdf`:
|
|
471
601
|
|
|
472
602
|
```ts
|
|
473
|
-
import { odtToPdf, odfToPdf } from
|
|
603
|
+
import { odtToPdf, odfToPdf } from "documents.js";
|
|
474
604
|
|
|
475
605
|
const pdfBytes = odfToPdf(odfBytes); // a single formula, faithfully typeset
|
|
476
606
|
const pdfFromOdtWithFormula = odtToPdf(odtBytes); // embedded formulas render as real typeset MathML
|
|
@@ -479,26 +609,44 @@ const pdfFromOdtWithFormula = odtToPdf(odtBytes); // embedded formulas render as
|
|
|
479
609
|
A formula's MathML travels inside the `ContentDocument` as a `ContentEmbeddedObjectBlock` whose `document` is a `'formula'`-kind `ContentDocument`:
|
|
480
610
|
|
|
481
611
|
```ts
|
|
482
|
-
import {
|
|
612
|
+
import {
|
|
613
|
+
convertWordprocessingToLayout,
|
|
614
|
+
formulaOfBlock,
|
|
615
|
+
readOdtContent,
|
|
616
|
+
} from "documents.js";
|
|
483
617
|
|
|
484
618
|
const document = readOdtContent(pkg);
|
|
485
|
-
const block = document.sections[0].blocks.find(
|
|
619
|
+
const block = document.sections[0].blocks.find(
|
|
620
|
+
(b) => b.kind === "embeddedObject",
|
|
621
|
+
);
|
|
486
622
|
formulaOfBlock(block); // -> { mathml, starMath? }, or undefined for a non-formula embedded object
|
|
487
623
|
|
|
488
|
-
const { document: layout, formulas: positioned } =
|
|
624
|
+
const { document: layout, formulas: positioned } =
|
|
625
|
+
convertWordprocessingToLayout(document, { measurer });
|
|
489
626
|
const pdfBytes = writePdf(layout, { formulas: positioned });
|
|
490
627
|
```
|
|
491
628
|
|
|
492
629
|
`layoutFormula`/`loadMathFont` are exported for direct formula layout. `buildOfficeMath`/`buildOfficeMathParagraph` translate MathML into OMML for docx. `readOfficeMath`/`collectOfficeMathElements` are the read-side inverse:
|
|
493
630
|
|
|
494
631
|
```ts
|
|
495
|
-
import {
|
|
632
|
+
import {
|
|
633
|
+
buildOfficeMathParagraph,
|
|
634
|
+
layoutFormula,
|
|
635
|
+
loadMathFont,
|
|
636
|
+
openDocx,
|
|
637
|
+
} from "documents.js";
|
|
496
638
|
|
|
497
639
|
const { metricsAt } = loadMathFont();
|
|
498
|
-
const { box, diagnostics } = layoutFormula(mathml, {
|
|
640
|
+
const { box, diagnostics } = layoutFormula(mathml, {
|
|
641
|
+
metrics: metricsAt(12),
|
|
642
|
+
sizePt: 12,
|
|
643
|
+
color: { r: 0, g: 0, b: 0 },
|
|
644
|
+
});
|
|
499
645
|
|
|
500
646
|
const editor = openDocx(existingDocxBytes);
|
|
501
|
-
const { diagnostics: ommlDiagnostics } = editor.body
|
|
647
|
+
const { diagnostics: ommlDiagnostics } = editor.body
|
|
648
|
+
.appendParagraph()
|
|
649
|
+
.appendOfficeMath(mathml);
|
|
502
650
|
```
|
|
503
651
|
|
|
504
652
|
### LaTeX lowering into the semantic core
|
|
@@ -513,13 +661,18 @@ A formula in the 3.2.0 schema carries two co-equal layers: `presentation` (a ver
|
|
|
513
661
|
- **The coherence lint** (`lintMathCoherence`) re-parses and re-lowers every stored presentation string against the document's own symbol table and compares with the stored content layer — divergence means somebody edited one layer deliberately, so it reports a **warning carrying provenance** and re-derives nothing.
|
|
514
662
|
|
|
515
663
|
```ts
|
|
516
|
-
import { latexToFormula, lintMathCoherence, lowerLatex } from
|
|
664
|
+
import { latexToFormula, lintMathCoherence, lowerLatex } from "documents.js";
|
|
517
665
|
|
|
518
|
-
const { expression, diagnostics, mintedSymbols } = lowerLatex(
|
|
666
|
+
const { expression, diagnostics, mintedSymbols } = lowerLatex(
|
|
667
|
+
"\\sum_{i=1}^{n} \\frac{1}{i^2}",
|
|
668
|
+
);
|
|
519
669
|
// expression: { kind: 'sum', binder: 'i', lower: {kind:'num',numerator:'1',denominator:'1'}, ... }
|
|
520
670
|
// diagnostics: [] — fully mechanical; '2x' would degrade to unparsed + 'latex/juxtaposition-unparsed'
|
|
521
671
|
|
|
522
|
-
const { formula } = latexToFormula(
|
|
672
|
+
const { formula } = latexToFormula("x^2", {
|
|
673
|
+
symbolEntries: table.symbols,
|
|
674
|
+
source: "my:pipeline",
|
|
675
|
+
});
|
|
523
676
|
// formula: { mathml, presentation: { latex: 'x^2' }, content, provenance } — ready to embed
|
|
524
677
|
|
|
525
678
|
const warnings = lintMathCoherence(pkg); // [{ code: 'math/coherence-divergence', severity: 'warning', provenance, detail }]
|
|
@@ -537,23 +690,38 @@ Every X → PDF conversion resolves each typeface through a real `FontRegistry`,
|
|
|
537
690
|
The same registry drives both the `TextMeasurer` (line breaking) and the writer (glyph emission) — measuring against one font's metrics and drawing through another would wrap text at wrong positions.
|
|
538
691
|
|
|
539
692
|
```ts
|
|
540
|
-
import { docxToPdf } from
|
|
693
|
+
import { docxToPdf } from "documents.js";
|
|
541
694
|
|
|
542
695
|
const pdfBytes = docxToPdf(docxBytes); // nothing to configure for embedded fonts
|
|
543
696
|
|
|
544
697
|
const withFallbackFace = docxToPdf(docxBytes, {
|
|
545
|
-
fonts: [
|
|
546
|
-
|
|
698
|
+
fonts: [
|
|
699
|
+
{
|
|
700
|
+
family: "Brand Sans",
|
|
701
|
+
bold: false,
|
|
702
|
+
italic: false,
|
|
703
|
+
bytes: brandSansTtfBytes,
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
onFontSubstitution: (substitution) =>
|
|
707
|
+
console.warn(
|
|
708
|
+
substitution.requestedFamily,
|
|
709
|
+
"->",
|
|
710
|
+
substitution.resolvedFamily,
|
|
711
|
+
),
|
|
547
712
|
});
|
|
548
713
|
```
|
|
549
714
|
|
|
550
715
|
A document that embeds nothing and asks for no vendored-substitute family writes byte-identical output to the old standard-14-only pipeline. Two structural limits: an embedded face is normally subsetted, so it can legitimately lack a synthesised character (list bullet, `###` overflow marker) — resolved per character via `onMissingGlyph`. And `odfToPdf` accepts font options but consults neither — a standalone formula emits only the embedded STIX Two Math font's glyphs. `extractSourceFonts`/`extractSourceFontsForFormat`/`createDocumentFontRegistry` are exported for callers composing the pipeline manually. `describeFontFace` inspects a standalone `.ttf`/`.otf` file.
|
|
551
716
|
|
|
552
717
|
```ts
|
|
553
|
-
import { describeFontFace, extractSourceFontsForFormat } from
|
|
718
|
+
import { describeFontFace, extractSourceFontsForFormat } from "documents.js";
|
|
554
719
|
|
|
555
|
-
const faces = extractSourceFontsForFormat(
|
|
556
|
-
const { family, bold, italic } = describeFontFace(
|
|
720
|
+
const faces = extractSourceFontsForFormat("docx", docxBytes); // -> readonly ProvidedFont[]
|
|
721
|
+
const { family, bold, italic } = describeFontFace(
|
|
722
|
+
fontBytes,
|
|
723
|
+
"BrandSans-Regular.ttf",
|
|
724
|
+
);
|
|
557
725
|
```
|
|
558
726
|
|
|
559
727
|
## Architecture
|
|
@@ -617,7 +785,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
|
|
|
617
785
|
- **ODF text content is not a plain string.** ODF represents runs of spaces as `<text:s>`, tabs as `<text:tab/>`, line breaks as `<text:line-break/>` — all elements, not text nodes. Every ODF text getter MUST call `decodeOdfText`, never `textContent()` — which silently drops them (no error, just shorter text).
|
|
618
786
|
- **docx⇄PDF and pptx⇄PDF are explicitly not round-trip-lossless** — see [Fidelity](#fidelity). The cross-format bridge pairs are a genuinely different case.
|
|
619
787
|
- **A `DocumentTree` from `onDocument`/`ConversionResult.package` is a snapshot, not a live view** — mutating the tree's content nodes after the layout pass leaves their `frames` stale; nothing detects or rejects that, and the schema keeps the tree's populated `frames` and `pages` in sync with nothing.
|
|
620
|
-
- **A construct group is the one tree node that does not embed the block it came from.** Everywhere else `decompose` wraps rather than copies, so the tree and the flat form share node objects. `TreeBlockLeaf` excludes both marker kinds by construction, so a construct group can only hold the `constructStart`'s `ConstructDescriptor` — that descriptor object
|
|
788
|
+
- **A construct group is the one tree node that does not embed the block it came from.** Everywhere else `decompose` wraps rather than copies, so the tree and the flat form share node objects. `TreeBlockLeaf` excludes both marker kinds by construction, so a construct group can only hold the `constructStart`'s `ConstructDescriptor` — that descriptor object _is_ shared, by identity — while the marker wrapper around it has no tree spelling and is rebuilt fresh by `flattenTree`. Two further boundary facts follow from promotion being a property of one container's own block flow: which group type a marker pair promotes to depends on where it sits (a `SectionConstructGroupNode`, whose children are a full section flow, at a section/heading scope; a `ShapeConstructGroupNode`, whose children are a list/shape flow where a heading paragraph is ordinary content, inside a list item or a shape) — and markers inside a table cell's blocks or inside an embedded document ride through on their leaf, neither promoted nor balance-checked, exactly as a heading level in the same position is not a grouping signal.
|
|
621
789
|
- **`frames` are stamped in place onto the caller's own content tree** — `convertXToLayout` mutates its `ContentDocument` argument (each node's placements are appended to its own `frames` array, one frame per rendered placement: per wrapped fragment on a run, the cell box on a cell, the emitted item's box on an image/vector/shape) and returns `pages` alongside the internal `LayoutDocument`. A run wrapped across three lines carries three frames; a repeat-row spreadsheet cell carries one per page it re-renders on. Reconstructors attach frames from the exact items each reconstructed node was clustered from, so every PDF-to-X conversion's content carries genuine positions too. The tree an `onDocument` callback receives embeds those same framed node objects (decompose wraps, it never copies — only a styles-minted paragraph or run is a copy), so the positions are identical in both encodings by construction.
|
|
622
790
|
- **ODF text getters must call `decodeOdfText`.** See the dedicated gotcha above.
|
|
623
791
|
- **`readPdf` recovers rect/ellipse/line as their own `LayoutRect`/`LayoutEllipse`/`LayoutLine` kinds** via pdf-codec's shape-pattern detection — an axis-aligned closed four-corner subpath is a rect, four kappa-ratio cubics at cardinal points is an ellipse, an open single straight stroke is a line. A false positive changes kind, never geometry. Off-axis rotations, freeform curves, and multi-subpath figures narrow to `LayoutPath`.
|
|
@@ -655,7 +823,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
|
|
|
655
823
|
- **A docx inline image reads as a real `ContentImageBlock`** — `buildDocxPackage` recognises the flat docx reader's two-block pattern (empty-text paragraph + image) and writes it back as one paragraph, avoiding spurious blank paragraphs on round trip.
|
|
656
824
|
- **pptx speaker notes survive via a hidden `/Subtype /Text` annotation** — specific to this package's writer/reader pair; other PDF producers/consumers won't see it.
|
|
657
825
|
- **`odmToPdf` is the one non-bytes-in/bytes-out conversion** — chapters are external `.odt` references requiring `resolveSubDocument`. All unresolved sections are collected before throwing `OdmUnresolvedSectionError`.
|
|
658
|
-
- **`.odb` has no `odbToPdf`** — a database front-end's tables/queries/reports are three unrelated output shapes. Rendered
|
|
826
|
+
- **`.odb` has no `odbToPdf`** — a database front-end's tables/queries/reports are three unrelated output shapes. Rendered _reports_ are the exception: `odbReportToDocx`/`odbReportToOdt`/`odbReportToPdf` take an already-rendered `ContentDocument`.
|
|
659
827
|
- **The rpt formula engine's group scoping cascades enclosing breaks inward.** A group at level L starts a new instance when its own expression breaks OR when any enclosing group breaks — otherwise a "Q2" subtotal would span two regions. `HASCHANGED` itself knows nothing about groups; the cascade lives in the report structure. Aggregates are computed over complete ranges (not running totals); group expressions may not transitively depend on aggregates (circular).
|
|
660
828
|
- **The rpt function set is a closed allowlist; separator is semicolon.** `rpt:HASCHANGED`/`rpt:LEFT`/`rpt:SUM`/`COUNT`/`AVG`/`MIN`/`MAX`/`field:[COLUMN]` — everything else throws. `[NAME]` and `"NAME"` are one concept. Three refusals where guessing would produce wrong values: non-boolean group expressions, `rpt:LEFT` over non-text, per-row formulas in report header/footer.
|
|
661
829
|
- **The rpt engine emits no page headers/footers** — the renderer places them under a single-logical-page model, at report scope.
|
|
@@ -700,19 +868,19 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
|
|
|
700
868
|
Read as **row → column**. `✓` lossless, `~` bounded, `✗` lossy, `✗✗` severe, `→` one-way, `–` no conversion. `.odm`/`.odb` sit outside this table.
|
|
701
869
|
|
|
702
870
|
| ↓ from \ to → | docx | pptx | xlsx | odt | odp | ods | odg | svg | odf | markdown | csv | pdf |
|
|
703
|
-
|
|
|
704
|
-
| **docx**
|
|
705
|
-
| **pptx**
|
|
706
|
-
| **xlsx**
|
|
707
|
-
| **odt**
|
|
708
|
-
| **odp**
|
|
709
|
-
| **ods**
|
|
710
|
-
| **odg**
|
|
711
|
-
| **svg**
|
|
712
|
-
| **odf**
|
|
713
|
-
| **markdown**
|
|
714
|
-
| **csv**
|
|
715
|
-
| **pdf**
|
|
871
|
+
| ------------- | ---- | ---- | ---- | --- | --- | --- | --- | --- | --- | -------- | --- | --- |
|
|
872
|
+
| **docx** | — | ~ | – | ✓ | – | – | – | ✗ | – | ✗ | ✗ | ~ |
|
|
873
|
+
| **pptx** | ~ | — | – | – | ✓ | – | – | ✗ | – | – | ✗ | ~ |
|
|
874
|
+
| **xlsx** | – | – | — | – | – | ~ | – | ✗ | – | ✗✗ | ~ | ~ |
|
|
875
|
+
| **odt** | ✓ | – | – | — | ~ | – | – | ✗ | – | ✗ | ✗ | ~ |
|
|
876
|
+
| **odp** | – | ✓ | – | ~ | — | – | – | ✗ | – | – | ✗ | ~ |
|
|
877
|
+
| **ods** | – | – | ~ | – | – | — | – | ✗ | – | – | ~ | ~ |
|
|
878
|
+
| **odg** | – | – | – | – | – | – | — | ✓ | – | – | ✗ | ~ |
|
|
879
|
+
| **svg** | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | — | – | ✗✗ | ✗✗ | ~ |
|
|
880
|
+
| **odf** | – | – | – | – | – | – | – | – | — | – | – | → |
|
|
881
|
+
| **markdown** | ~ | – | ✗✗ | ~ | – | – | – | ✗✗ | – | — | ✗✗ | ~ |
|
|
882
|
+
| **csv** | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ | – | ✗✗ | — | ~ |
|
|
883
|
+
| **pdf** | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | – | ✗✗ | ✗ | — |
|
|
716
884
|
|
|
717
885
|
111 of 132 directional pairs are routable. The shared `ContentDocument` model is the hub, not PDF — twenty bridges bypass PDF entirely.
|
|
718
886
|
|