js.documents 6.0.1 → 6.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.
Files changed (248) hide show
  1. package/README.md +283 -115
  2. package/dist/bin-dispatch.d.cts +2 -2
  3. package/dist/bin-dispatch.d.ts +2 -2
  4. package/dist/{blr-types-DVgO1DQ9.d.cts → blr-types-CMN0QKUE.d.cts} +1 -1
  5. package/dist/{blr-types-DVgO1DQ9.d.ts → blr-types-CMN0QKUE.d.ts} +1 -1
  6. package/dist/{capability-an5gSNsu.d.cts → capability-BZ3iNsWh.d.cts} +1 -1
  7. package/dist/{capability-DX0lCSiI.d.ts → capability-D-a4okp0.d.ts} +1 -1
  8. package/dist/{cell-typing-CEWlywIc.d.cts → cell-typing-BQ-xQ5X5.d.cts} +4 -4
  9. package/dist/{cell-typing-CEWlywIc.d.ts → cell-typing-BQ-xQ5X5.d.ts} +4 -4
  10. package/dist/codecs/read.d.cts +1 -1
  11. package/dist/codecs/read.d.ts +1 -1
  12. package/dist/convert/capability.d.cts +1 -1
  13. package/dist/convert/capability.d.ts +1 -1
  14. package/dist/convert/composition.d.cts +8 -8
  15. package/dist/convert/composition.d.ts +8 -8
  16. package/dist/convert/convert.d.cts +3 -3
  17. package/dist/convert/convert.d.ts +3 -3
  18. package/dist/convert/port.d.cts +1 -1
  19. package/dist/convert/port.d.ts +1 -1
  20. package/dist/convert/variant-bridges.d.cts +3 -3
  21. package/dist/convert/variant-bridges.d.ts +3 -3
  22. package/dist/csv/read.d.cts +1 -1
  23. package/dist/csv/read.d.ts +1 -1
  24. package/dist/csv/records.cjs +1 -1
  25. package/dist/csv/records.js +1 -1
  26. package/dist/csv/write.d.cts +2 -2
  27. package/dist/csv/write.d.ts +2 -2
  28. package/dist/{diagnostics-C5-bG09J.d.cts → diagnostics-DdO0xPGy.d.cts} +1 -1
  29. package/dist/{diagnostics-C5-bG09J.d.ts → diagnostics-DdO0xPGy.d.ts} +1 -1
  30. package/dist/edit/docx/content.d.cts +1 -1
  31. package/dist/edit/docx/content.d.ts +1 -1
  32. package/dist/edit/docx/image.d.cts +1 -1
  33. package/dist/edit/docx/image.d.ts +1 -1
  34. package/dist/edit/docx/paragraph.cjs +1 -1
  35. package/dist/edit/docx/paragraph.d.cts +6 -6
  36. package/dist/edit/docx/paragraph.d.ts +6 -6
  37. package/dist/edit/docx/paragraph.js +1 -1
  38. package/dist/edit/docx/props.d.cts +4 -4
  39. package/dist/edit/docx/props.d.ts +4 -4
  40. package/dist/edit/docx/table.d.cts +1 -1
  41. package/dist/edit/docx/table.d.ts +1 -1
  42. package/dist/edit/markdown/list.d.cts +1 -1
  43. package/dist/edit/markdown/list.d.ts +1 -1
  44. package/dist/edit/odg/page.d.cts +3 -3
  45. package/dist/edit/odg/page.d.ts +3 -3
  46. package/dist/edit/odg/vector.d.cts +1 -1
  47. package/dist/edit/odg/vector.d.ts +1 -1
  48. package/dist/edit/odp/image.d.cts +1 -1
  49. package/dist/edit/odp/image.d.ts +1 -1
  50. package/dist/edit/odp/shape.d.cts +1 -1
  51. package/dist/edit/odp/shape.d.ts +1 -1
  52. package/dist/edit/odp/slide.d.cts +4 -4
  53. package/dist/edit/odp/slide.d.ts +4 -4
  54. package/dist/edit/odt/content.d.cts +3 -3
  55. package/dist/edit/odt/content.d.ts +3 -3
  56. package/dist/edit/odt/editor.d.cts +3 -3
  57. package/dist/edit/odt/editor.d.ts +3 -3
  58. package/dist/edit/odt/image.d.cts +1 -1
  59. package/dist/edit/odt/image.d.ts +1 -1
  60. package/dist/edit/odt/list.d.cts +1 -1
  61. package/dist/edit/odt/list.d.ts +1 -1
  62. package/dist/edit/odt/paragraph.cjs +1 -1
  63. package/dist/edit/odt/paragraph.d.cts +1 -1
  64. package/dist/edit/odt/paragraph.d.ts +1 -1
  65. package/dist/edit/odt/paragraph.js +1 -1
  66. package/dist/edit/odt/table.d.cts +1 -1
  67. package/dist/edit/odt/table.d.ts +1 -1
  68. package/dist/edit/pdf/item.cjs +1 -1
  69. package/dist/edit/pdf/item.d.cts +1 -1
  70. package/dist/edit/pdf/item.d.ts +1 -1
  71. package/dist/edit/pdf/item.js +1 -1
  72. package/dist/edit/pdf/page.d.cts +1 -1
  73. package/dist/edit/pdf/page.d.ts +1 -1
  74. package/dist/edit/pdf/util.d.cts +1 -1
  75. package/dist/edit/pdf/util.d.ts +1 -1
  76. package/dist/edit/pptx/content.cjs +1 -1
  77. package/dist/edit/pptx/content.d.cts +1 -1
  78. package/dist/edit/pptx/content.d.ts +1 -1
  79. package/dist/edit/pptx/content.js +1 -1
  80. package/dist/edit/pptx/editor.cjs +1 -1
  81. package/dist/edit/pptx/editor.js +1 -1
  82. package/dist/edit/pptx/image.d.cts +1 -1
  83. package/dist/edit/pptx/image.d.ts +1 -1
  84. package/dist/edit/pptx/shape.d.cts +1 -1
  85. package/dist/edit/pptx/shape.d.ts +1 -1
  86. package/dist/edit/pptx/slide.d.cts +3 -3
  87. package/dist/edit/pptx/slide.d.ts +3 -3
  88. package/dist/edit/pptx/table.d.cts +1 -1
  89. package/dist/edit/pptx/table.d.ts +1 -1
  90. package/dist/{evaluate-BU5N1Jk1.d.ts → evaluate-Cu7iEnxX.d.ts} +1 -1
  91. package/dist/{evaluate-BhMXY6DM.d.cts → evaluate-Rl9P68se.d.cts} +1 -1
  92. package/dist/firebird/blr-types.d.cts +1 -1
  93. package/dist/firebird/blr-types.d.ts +1 -1
  94. package/dist/firebird/schema.d.cts +1 -1
  95. package/dist/firebird/schema.d.ts +1 -1
  96. package/dist/fonts/ooxml.d.cts +1 -1
  97. package/dist/fonts/ooxml.d.ts +1 -1
  98. package/dist/fonts/registry.d.cts +1 -1
  99. package/dist/fonts/registry.d.ts +1 -1
  100. package/dist/{formula-Cs0IexVi.d.cts → formula-BH1FBblm.d.cts} +1 -1
  101. package/dist/{formula-_8feXQH7.d.cts → formula-BpuJ3ae0.d.cts} +1 -1
  102. package/dist/{formula-DDSYieJg.d.ts → formula-Cb-LRoIE.d.ts} +1 -1
  103. package/dist/{formula-BKgIMfhi.d.ts → formula-DF4R1QEJ.d.ts} +1 -1
  104. package/dist/{image-DRqXQmEg.d.cts → image-BQ7F9xxz.d.cts} +1 -1
  105. package/dist/{image-DRqXQmEg.d.ts → image-BQ7F9xxz.d.ts} +1 -1
  106. package/dist/{image-CRgc_j2Q.d.cts → image-CwbCQ49u.d.ts} +2 -2
  107. package/dist/{image-Biwtr667.d.ts → image-DQxA-zGN.d.cts} +1 -1
  108. package/dist/{image-Biwtr667.d.cts → image-DQxA-zGN.d.ts} +1 -1
  109. package/dist/{image-3ql9_z1y.d.ts → image-DzqEWMDw.d.cts} +1 -1
  110. package/dist/{image-3ql9_z1y.d.cts → image-DzqEWMDw.d.ts} +1 -1
  111. package/dist/{image-CyYr22TE.d.ts → image-fah3VkZg.d.cts} +2 -2
  112. package/dist/index.d.cts +19 -19
  113. package/dist/index.d.ts +19 -19
  114. package/dist/{item-DhXpJNAQ.d.cts → item-SUC4uKDO.d.cts} +5 -5
  115. package/dist/{item-DhXpJNAQ.d.ts → item-SUC4uKDO.d.ts} +5 -5
  116. package/dist/latex/diagnostics.d.cts +1 -1
  117. package/dist/latex/diagnostics.d.ts +1 -1
  118. package/dist/latex/lint.cjs +4 -6
  119. package/dist/latex/lint.d.cts +1 -1
  120. package/dist/latex/lint.d.ts +1 -1
  121. package/dist/latex/lint.js +4 -6
  122. package/dist/latex/lower.cjs +1 -1
  123. package/dist/latex/lower.d.cts +1 -1
  124. package/dist/latex/lower.d.ts +1 -1
  125. package/dist/latex/lower.js +1 -1
  126. package/dist/latex/symbols.d.cts +1 -1
  127. package/dist/latex/symbols.d.ts +1 -1
  128. package/dist/latex/temml.d.cts +2 -2
  129. package/dist/latex/temml.d.ts +2 -2
  130. package/dist/layout/cell-typing.d.cts +1 -1
  131. package/dist/layout/cell-typing.d.ts +1 -1
  132. package/dist/layout/drawing.d.cts +1 -1
  133. package/dist/layout/drawing.d.ts +1 -1
  134. package/dist/layout/engine.d.cts +1 -1
  135. package/dist/layout/engine.d.ts +1 -1
  136. package/dist/layout/lattice.cjs +24 -0
  137. package/dist/layout/lattice.js +24 -0
  138. package/dist/layout/reconstruct.cjs +1 -1
  139. package/dist/layout/reconstruct.d.cts +1 -1
  140. package/dist/layout/reconstruct.d.ts +1 -1
  141. package/dist/layout/reconstruct.js +1 -1
  142. package/dist/layout/sheets.d.cts +1 -1
  143. package/dist/layout/sheets.d.ts +1 -1
  144. package/dist/layout/slides.d.cts +1 -1
  145. package/dist/layout/slides.d.ts +1 -1
  146. package/dist/layout/text-layout.d.cts +1 -1
  147. package/dist/layout/text-layout.d.ts +1 -1
  148. package/dist/{lexer-DGPWx_f2.d.cts → lexer-DgsW7jMA.d.cts} +9 -9
  149. package/dist/{lexer-DGPWx_f2.d.ts → lexer-DgsW7jMA.d.ts} +9 -9
  150. package/dist/{list-C55W_YCp.d.ts → list-B_Sy668f.d.ts} +1 -1
  151. package/dist/{list-lTB3eDLC.d.cts → list-BiTN6oop.d.cts} +1 -1
  152. package/dist/markdown/math.d.cts +1 -1
  153. package/dist/markdown/math.d.ts +1 -1
  154. package/dist/markdown/render.d.cts +1 -1
  155. package/dist/markdown/render.d.ts +1 -1
  156. package/dist/mathml/layout-types.d.cts +1 -1
  157. package/dist/mathml/layout-types.d.ts +1 -1
  158. package/dist/mathml/layout.d.cts +2 -2
  159. package/dist/mathml/layout.d.ts +2 -2
  160. package/dist/mathml/nodes.d.cts +1 -1
  161. package/dist/mathml/nodes.d.ts +1 -1
  162. package/dist/mathml/variant.d.cts +1 -1
  163. package/dist/mathml/variant.d.ts +1 -1
  164. package/dist/model/embedded-drawing.d.cts +1 -1
  165. package/dist/model/embedded-drawing.d.ts +1 -1
  166. package/dist/model/paint-order.d.cts +2 -2
  167. package/dist/model/paint-order.d.ts +2 -2
  168. package/dist/{nodes-pArN9ilm.d.cts → nodes-jrlUmrNs.d.cts} +3 -3
  169. package/dist/{nodes-pArN9ilm.d.ts → nodes-jrlUmrNs.d.ts} +3 -3
  170. package/dist/odb/formula/evaluate.d.cts +4 -4
  171. package/dist/odb/formula/evaluate.d.ts +4 -4
  172. package/dist/odb/formula/parser.d.cts +6 -6
  173. package/dist/odb/formula/parser.d.ts +6 -6
  174. package/dist/odb/read.d.cts +1 -1
  175. package/dist/odb/read.d.ts +1 -1
  176. package/dist/odb/report/render.d.cts +1 -1
  177. package/dist/odb/report/render.d.ts +1 -1
  178. package/dist/odb/report/source.d.cts +1 -1
  179. package/dist/odb/report/source.d.ts +1 -1
  180. package/dist/odb/sql/evaluate.d.cts +1 -1
  181. package/dist/odb/sql/evaluate.d.ts +1 -1
  182. package/dist/odb/sql/lexer.d.cts +1 -1
  183. package/dist/odb/sql/lexer.d.ts +1 -1
  184. package/dist/odb/sql/parser.d.cts +1 -1
  185. package/dist/odb/sql/parser.d.ts +1 -1
  186. package/dist/odb/values.d.cts +1 -1
  187. package/dist/odb/values.d.ts +1 -1
  188. package/dist/odf/vector/detect.cjs +1 -1
  189. package/dist/odf/vector/detect.js +1 -1
  190. package/dist/odf-package/media.d.cts +1 -1
  191. package/dist/odf-package/media.d.ts +1 -1
  192. package/dist/omml/read.d.cts +1 -1
  193. package/dist/omml/read.d.ts +1 -1
  194. package/dist/omml/shared.d.cts +1 -1
  195. package/dist/omml/shared.d.ts +1 -1
  196. package/dist/omml/write.d.cts +1 -1
  197. package/dist/omml/write.d.ts +1 -1
  198. package/dist/ooxml/docx/embedded-objects.cjs +10 -24
  199. package/dist/ooxml/docx/embedded-objects.d.cts +1 -1
  200. package/dist/ooxml/docx/embedded-objects.d.ts +1 -1
  201. package/dist/ooxml/docx/embedded-objects.js +10 -24
  202. package/dist/ooxml/docx/formula.d.cts +1 -1
  203. package/dist/ooxml/docx/formula.d.ts +1 -1
  204. package/dist/ooxml/docx/read.d.cts +1 -1
  205. package/dist/ooxml/docx/read.d.ts +1 -1
  206. package/dist/ooxml/pptx/formula.d.cts +1 -1
  207. package/dist/ooxml/pptx/formula.d.ts +1 -1
  208. package/dist/ooxml/pptx/read.d.cts +1 -1
  209. package/dist/ooxml/pptx/read.d.ts +1 -1
  210. package/dist/ooxml/pptx/vector.cjs +1 -1
  211. package/dist/ooxml/pptx/vector.js +1 -1
  212. package/dist/opc/media.d.cts +1 -1
  213. package/dist/opc/media.d.ts +1 -1
  214. package/dist/opc/rels.d.cts +1 -1
  215. package/dist/opc/rels.d.ts +1 -1
  216. package/dist/{paragraph-CoPeTuHR.d.cts → paragraph-BBmkECjw.d.cts} +1 -1
  217. package/dist/{paragraph--69uh-Ys.d.ts → paragraph-D_fDin5K.d.ts} +1 -1
  218. package/dist/{parser-DQSgYJzQ.d.ts → parser-D0IJI7LA.d.ts} +22 -22
  219. package/dist/{parser-B9vC1T6T.d.cts → parser-DYfiAasj.d.cts} +22 -22
  220. package/dist/{registry-BcBT-bnX.d.cts → registry-913jEe6-.d.ts} +3 -3
  221. package/dist/{registry-D1Pu5XxB.d.ts → registry-Bs4IHqVQ.d.cts} +3 -3
  222. package/dist/{shape-DyIjuKE9.d.ts → shape-BY5P8kDn.d.ts} +2 -2
  223. package/dist/{shape-CaG-RPiz.d.cts → shape-Ca-xsrde.d.cts} +2 -2
  224. package/dist/{shape-CLHm7U8Y.d.cts → shape-DqkYq1OL.d.cts} +2 -2
  225. package/dist/{shape-Dnz6-wmG.d.ts → shape-xpgICOEx.d.ts} +2 -2
  226. package/dist/{shared-DUOzjwcL.d.ts → shared-DHu5jDJf.d.ts} +2 -2
  227. package/dist/{shared-DLZ3IQUl.d.cts → shared-c0Lp3_iY.d.cts} +2 -2
  228. package/dist/svg/paint.d.cts +5 -5
  229. package/dist/svg/paint.d.ts +5 -5
  230. package/dist/svg/path.d.cts +2 -2
  231. package/dist/svg/path.d.ts +2 -2
  232. package/dist/svg/write.d.cts +2 -2
  233. package/dist/svg/write.d.ts +2 -2
  234. package/dist/{table-CTeOe_vm.d.ts → table-C3VWWdjr.d.ts} +1 -1
  235. package/dist/{table-DH0lJRVv.d.cts → table-C_KDsX5R.d.cts} +1 -1
  236. package/dist/{table-BxMiEZXd.d.ts → table-Ck0fyr8d.d.ts} +1 -1
  237. package/dist/{table-C_tSXnbt.d.cts → table-CpEmblWG.d.cts} +1 -1
  238. package/dist/{values-BetetbaH.d.cts → values-BBN9E8dV.d.cts} +5 -5
  239. package/dist/{values-BetetbaH.d.ts → values-BBN9E8dV.d.ts} +5 -5
  240. package/dist/{variant-BMrebjMw.d.cts → variant-uwYyRetr.d.cts} +1 -1
  241. package/dist/{variant-BMrebjMw.d.ts → variant-uwYyRetr.d.ts} +1 -1
  242. package/dist/{vector-D03KX8Ux.d.cts → vector-DDP7hgvm.d.cts} +2 -2
  243. package/dist/{vector-D03KX8Ux.d.ts → vector-DDP7hgvm.d.ts} +2 -2
  244. package/dist/{write-BQ6SK8r8.d.cts → write-DWupt1QI.d.cts} +2 -2
  245. package/dist/{write-THs7ipBq.d.ts → write-qKlz1R0J.d.ts} +2 -2
  246. package/dist/xml/odf-text.cjs +1 -1
  247. package/dist/xml/odf-text.js +1 -1
  248. 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 'documents.js';
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('markdown', 'pptx', markdownBytes);
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('docx', 'odt', docxBytes, { onMathDiagnostic: (d) => console.warn(d) });
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 { csvToPdf, docxToPdf, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, pdfToCsv, pdfToDocx, pdfToMarkdown, pdfToOdg, pdfToOdp, pdfToOds, pdfToOdt, pdfToPptx, pdfToSvg, pdfToXlsx, pptxToPdf, svgToPdf, xlsxToPdf } from 'documents.js';
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 { odtToDocx, docxToOdt, markdownToDocx, docxToMarkdown } from 'documents.js';
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 'documents.js';
181
+ import { createLocalDocumentConverter } from "documents.js";
154
182
 
155
183
  const converter = createLocalDocumentConverter();
156
184
  const { document, diagnostics } = await converter.convert(
157
- { source: { format: 'docx', bytes: docxBytes }, targetFormat: 'pdf' },
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 'documents.js';
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 'document-schema.js';
179
- import { docxToPdf } from 'documents.js';
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 = content.kind === 'wordprocessing' ? content.sections[0]?.blocks[0] : undefined;
187
- console.log(block?.kind === 'paragraph' ? block.runs[0]?.frames : 'no paragraph'); // that run's rendered placements
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 { assembleTree, decompose, factorStyles, flattenTree } from 'document-schema.js';
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 'documents.js';
247
+ import { documentFromJson, documentTreeWithSchema } from "documents.js";
210
248
 
211
249
  const tagged = documentTreeWithSchema(pkg);
212
- writeFileSync('converted.doc.json', JSON.stringify(tagged, null, 2));
250
+ writeFileSync("converted.doc.json", JSON.stringify(tagged, null, 2));
213
251
 
214
- const { kind, value } = documentFromJson(JSON.parse(readFileSync('converted.doc.json', 'utf8')));
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 'documents.js';
261
+ import { buildDocumentBytes, docxToPdf } from "documents.js";
222
262
 
223
263
  let captured;
224
- docxToPdf(docxBytes, { onDocument: (pkg) => { captured = pkg; } });
225
- const pdfBytesAgain = buildDocumentBytes(captured, 'pdf');
226
- const docxBytesAgain = buildDocumentBytes(captured, 'docx');
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 { decodeDocumentPackage, decodeOdbPackage, encodeDocumentPackage } from 'documents.js';
278
+ import {
279
+ decodeDocumentPackage,
280
+ decodeOdbPackage,
281
+ encodeDocumentPackage,
282
+ } from "documents.js";
235
283
 
236
- const pkg = decodeDocumentPackage('docx', docxBytes);
237
- const docxBytesAgain = encodeDocumentPackage('docx', pkg);
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 'documents.js';
292
+ import { readDocumentMetadata, setDocumentMetadata } from "documents.js";
245
293
 
246
- const metadata = readDocumentMetadata('docx', docxBytes);
247
- const patchedBytes = setDocumentMetadata('docx', 'docx', docxBytes, { title: 'New title', keywords: ['a', 'b'] });
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 'documents.js/model/units';
254
- import { buildOdtPackage } from 'documents.js/edit/odt/content';
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 'documents.js/read';
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 { buildXlsxPackage, decodeDocumentPackage, encodeDocumentPackage, readXlsxContent } from 'documents.js';
321
+ import {
322
+ buildXlsxPackage,
323
+ decodeDocumentPackage,
324
+ encodeDocumentPackage,
325
+ readXlsxContent,
326
+ } from "documents.js";
271
327
 
272
- const content = readXlsxContent(decodeDocumentPackage('xlsx', xlsxBytes)); // ContentDocument, kind: 'spreadsheet'
273
- const rebuiltBytes = encodeDocumentPackage('xlsx', buildXlsxPackage(content));
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 'documents.js';
339
+ import { openDocx, createDocx } from "documents.js";
284
340
 
285
341
  const editor = openDocx(existingDocxBytes);
286
- const paragraph = editor.body.appendParagraph({ alignment: 'center' });
287
- const run = paragraph.appendRun({ text: 'Hello' });
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: 'New document' });
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 'documents.js';
300
- import { decodePackage } from 'ooxml.js';
355
+ import { readDocxExtras } from "documents.js";
356
+ import { decodePackage } from "ooxml.js";
301
357
 
302
- const { comments, footnotes, headerFooterParts, sectionHeaderFooters, numbering } = readDocxExtras(decodePackage(docxBytes));
303
- console.log(Object.values(numbering)[0]?.levels['0']?.format); // numbering is keyed by numId, each level by its own level index
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 'documents.js';
310
- import { buildDocxPackageFromContent, decodePackage, encodePackage, readDocxContent } from 'ooxml.js';
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(buildDocxPackageFromContent(content, { serialiseEmbeddedPresentation: embeddedPresentationSerialiser }));
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 'documents.js';
390
+ import { createOdp } from "documents.js";
320
391
 
321
392
  const editor = createOdp();
322
393
  const slide = editor.addSlide();
323
- const title = slide.addTextBox({ frame: { xPt: 40, yPt: 30, widthPt: 640, heightPt: 80 }, text: 'Title' });
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({ frame: { xPt: 40, yPt: 130, widthPt: 300, heightPt: 200 }, text: '' });
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.addList().addItem().appendParagraph({ text: 'A real bulleted text:list' });
328
- slide.notes = 'Speaker notes for this slide';
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 'documents.js';
415
+ import { createOds } from "documents.js";
336
416
 
337
417
  const editor = createOds();
338
- const sheet = editor.addSheet('Sheet1');
339
- sheet.printSettings = { pageSize: { widthPt: 595, heightPt: 842 }, margins: { topPt: 20, rightPt: 20, bottomPt: 20, leftPt: 20 }, gridlines: true, headers: true, pageOrder: 'downThenOver' };
340
- sheet.cell(0, 0).value = { kind: 'string', value: 'Total' }; // 0-based (row, column)
341
- sheet.cell(0, 1).value = { kind: 'currency', value: 42.5, currency: 'USD' };
342
- sheet.cell(500, 50).value = { kind: 'boolean', value: true }; // does not materialise 500x50 empty cells
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 'documents.js';
435
+ import { createOdg } from "documents.js";
350
436
 
351
437
  const editor = createOdg();
352
438
  const page = editor.addPage();
353
- page.addRect({ frame: { xPt: 20, yPt: 20, widthPt: 100, heightPt: 60 }, fill: { r: 1, g: 0.5, b: 0 } });
354
- page.addEllipse({ frame: { xPt: 140, yPt: 20, widthPt: 100, heightPt: 60 }, stroke: { color: { r: 0, g: 0, b: 0 }, widthPt: 1 } });
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: [{ start: { xPt: 0, yPt: 80 }, closed: true, segments: [{ kind: 'line', to: { xPt: 60, yPt: 80 } }, { kind: 'cubic', control1: { xPt: 80, yPt: 80 }, control2: { xPt: 80, yPt: 0 }, to: { xPt: 40, yPt: 0 } }] }],
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({ frame: { xPt: 20, yPt: 200, widthPt: 300, heightPt: 30 }, text: 'A label on top' });
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 'documents.js';
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 'zod';
377
- import { docxPdfCodec, pdfCodec } from 'documents.js';
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 'node:fs';
391
- import { odmToPdf, OdmUnresolvedSectionError } from 'documents.js';
499
+ import { readFileSync } from "node:fs";
500
+ import { odmToPdf, OdmUnresolvedSectionError } from "documents.js";
392
501
 
393
502
  const chapterBytes = new Map([
394
- ['../chapter1.odt', new Uint8Array(readFileSync('chapter1.odt'))],
395
- ['../chapter2.odt', new Uint8Array(readFileSync('chapter2.odt'))],
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, { resolveSubDocument: (href) => chapterBytes.get(href) });
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('missing chapters:', error.hrefs); // every unresolved href, not just the first
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 'odf.js';
411
- import { odbToCsv, odbToXlsx, readOdbTables } from 'documents.js';
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: 'CUSTOMERS' }); // required when the .odb has more than one 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 *structure*: `readOdbForms`/`readOdbReports` read every declared component's static structure (bound controls, bands/groups/functions):
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 'odf.js';
422
- import { readOdbForms, readOdbReports } from 'documents.js';
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 'documents.js';
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 'odf.js';
439
- import { evaluateSelect, parseSelect, readOdbTables } from 'documents.js';
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(parseSelect(query.command), readOdbTables(pkg));
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 'odf.js';
450
- import { evaluateSelect, parseSelect, readOdbReports, readOdbTables, rptDefinitionFromReport, runRptReport } from 'documents.js';
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((candidate) => candidate.name === report.command);
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 'odf.js';
463
- import { odbReportToDocx, odbReportToOdt, odbReportToPdf, readOdbReportContent } from 'documents.js';
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), { report: 'SalesByRegion' });
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 'documents.js';
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 { convertWordprocessingToLayout, formulaOfBlock, readOdtContent } from 'documents.js';
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((b) => b.kind === 'embeddedObject');
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 } = convertWordprocessingToLayout(document, { measurer });
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 { buildOfficeMathParagraph, layoutFormula, loadMathFont, openDocx } from 'documents.js';
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, { metrics: metricsAt(12), sizePt: 12, color: { r: 0, g: 0, b: 0 } });
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.appendParagraph().appendOfficeMath(mathml);
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 'documents.js';
664
+ import { latexToFormula, lintMathCoherence, lowerLatex } from "documents.js";
517
665
 
518
- const { expression, diagnostics, mintedSymbols } = lowerLatex('\\sum_{i=1}^{n} \\frac{1}{i^2}');
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('x^2', { symbolEntries: table.symbols, source: 'my:pipeline' });
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 'documents.js';
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: [{ family: 'Brand Sans', bold: false, italic: false, bytes: brandSansTtfBytes }],
546
- onFontSubstitution: (substitution) => console.warn(substitution.requestedFamily, '->', substitution.resolvedFamily),
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 'documents.js';
718
+ import { describeFontFace, extractSourceFontsForFormat } from "documents.js";
554
719
 
555
- const faces = extractSourceFontsForFormat('docx', docxBytes); // -> readonly ProvidedFont[]
556
- const { family, bold, italic } = describeFontFace(fontBytes, 'BrandSans-Regular.ttf');
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 *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.
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 *reports* are the exception: `odbReportToDocx`/`odbReportToOdt`/`odbReportToPdf` take an already-rendered `ContentDocument`.
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