@shbernal/ts-xlsx 1.0.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.
Files changed (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. package/package.json +117 -0
@@ -0,0 +1,33 @@
1
+ export declare const NS: {
2
+ readonly contentTypes: "http://schemas.openxmlformats.org/package/2006/content-types";
3
+ readonly packageRels: "http://schemas.openxmlformats.org/package/2006/relationships";
4
+ readonly main: "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
5
+ readonly docRels: "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
6
+ readonly coreProps: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
7
+ readonly extProps: "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
8
+ readonly dc: "http://purl.org/dc/elements/1.1/";
9
+ readonly dcterms: "http://purl.org/dc/terms/";
10
+ readonly dcmitype: "http://purl.org/dc/dcmitype/";
11
+ readonly xsi: "http://www.w3.org/2001/XMLSchema-instance";
12
+ };
13
+ export declare const REL: {
14
+ readonly worksheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
15
+ readonly styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
16
+ readonly theme: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
17
+ readonly officeDocument: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
18
+ readonly coreProps: "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
19
+ readonly extProps: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
20
+ readonly table: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
21
+ readonly comments: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
22
+ readonly vmlDrawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing";
23
+ readonly drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
24
+ readonly printerSettings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings";
25
+ readonly image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
26
+ readonly hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
27
+ readonly sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
28
+ readonly pivotTable: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable";
29
+ readonly pivotCacheDefinition: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition";
30
+ readonly pivotCacheRecords: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords";
31
+ readonly threadedComment: "http://schemas.microsoft.com/office/2017/10/relationships/threadedComment";
32
+ readonly person: "http://schemas.microsoft.com/office/2017/10/relationships/person";
33
+ };
@@ -0,0 +1,47 @@
1
+ // The relationship-type URIs and namespace groupings the writer's serialisers reference. The
2
+ // canonical namespace set lives in `namespaces.ts`; the writer-local groupings here (the props
3
+ // vocabularies, the content-types namespace, and the rel-type URIs derived from them) are plumbing
4
+ // every part of the writer references.
5
+ //
6
+ // The `.rels` part envelope these types are written into is container-level, not SpreadsheetML, and
7
+ // lives in `../opc/rels.ts`.
8
+ import { CONTENT_TYPES_NS, PKG_RELS_NS, RELATIONSHIPS_NS } from "../opc/namespaces.js";
9
+ import { SPREADSHEETML_NS } from "./namespaces.js";
10
+ export const NS = {
11
+ contentTypes: CONTENT_TYPES_NS,
12
+ packageRels: PKG_RELS_NS,
13
+ main: SPREADSHEETML_NS,
14
+ docRels: RELATIONSHIPS_NS,
15
+ coreProps: 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
16
+ extProps: 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
17
+ dc: 'http://purl.org/dc/elements/1.1/',
18
+ dcterms: 'http://purl.org/dc/terms/',
19
+ dcmitype: 'http://purl.org/dc/dcmitype/',
20
+ xsi: 'http://www.w3.org/2001/XMLSchema-instance',
21
+ };
22
+ // The Office extension relationships namespace the 2018 threaded-comment feature wires both of its parts
23
+ // through. Local to this table: nothing outside it needs the base URI.
24
+ const MS_OFFICE_2017_RELS_NS = 'http://schemas.microsoft.com/office/2017/10/relationships';
25
+ export const REL = {
26
+ worksheet: `${NS.docRels}/worksheet`,
27
+ styles: `${NS.docRels}/styles`,
28
+ theme: `${NS.docRels}/theme`,
29
+ officeDocument: `${NS.docRels}/officeDocument`,
30
+ coreProps: `${NS.packageRels}/metadata/core-properties`,
31
+ extProps: `${NS.docRels}/extended-properties`,
32
+ table: `${NS.docRels}/table`,
33
+ comments: `${NS.docRels}/comments`,
34
+ vmlDrawing: `${NS.docRels}/vmlDrawing`,
35
+ drawing: `${NS.docRels}/drawing`,
36
+ printerSettings: `${NS.docRels}/printerSettings`,
37
+ image: `${NS.docRels}/image`,
38
+ hyperlink: `${NS.docRels}/hyperlink`,
39
+ sharedStrings: `${NS.docRels}/sharedStrings`,
40
+ pivotTable: `${NS.docRels}/pivotTable`,
41
+ pivotCacheDefinition: `${NS.docRels}/pivotCacheDefinition`,
42
+ pivotCacheRecords: `${NS.docRels}/pivotCacheRecords`,
43
+ // Threaded comments are a Microsoft extension, so both types live under the 2017/10 Office
44
+ // relationships namespace rather than the standard officeDocument one.
45
+ threadedComment: `${MS_OFFICE_2017_RELS_NS}/threadedComment`,
46
+ person: `${MS_OFFICE_2017_RELS_NS}/person`,
47
+ };
@@ -0,0 +1,13 @@
1
+ import type { RichTextRun } from '../../core/value.ts';
2
+ import type { XmlAttributes } from '../../xml/xml-read.ts';
3
+ export declare class RunAccumulator {
4
+ #private;
5
+ reset(): void;
6
+ /** The runs gathered so far. */
7
+ get runs(): RichTextRun[];
8
+ beginRun(): void;
9
+ beginProperties(): void;
10
+ applyProperty(local: string, attrs: XmlAttributes): void;
11
+ appendText(text: string): boolean;
12
+ endRun(): void;
13
+ }
@@ -0,0 +1,59 @@
1
+ // Rich-text run accumulation, shared by the two readers that parse `<r>` runs identically: an inline
2
+ // string's `<is>` in a worksheet body and a pooled `<si>` in sharedStrings.xml. One `<r>` opens a run
3
+ // (resetting its font and text so an unformatted run inherits nothing from the last), an `<rPr>` opens
4
+ // the run's font bundle whose self-closing children each set one facet, and a `<t>` appends the run's
5
+ // text; the run commits on `</r>`. The surrounding parser owns only whether it is inside an `<is>`/`<si>`
6
+ // and where a bare (non-run) `<t>` goes.
7
+ import { applyFontChild } from "./read-styles.js";
8
+ export class RunAccumulator {
9
+ #runs = [];
10
+ #font = null;
11
+ #text = '';
12
+ #inRun = false;
13
+ // Discard accumulated runs, readying the accumulator for a fresh `<is>`/`<si>`. A new array is
14
+ // installed, so a value already built from a previous reset's runs keeps its own array.
15
+ reset() {
16
+ this.#runs = [];
17
+ this.#font = null;
18
+ this.#inRun = false;
19
+ }
20
+ /** The runs gathered so far. */
21
+ get runs() {
22
+ return this.#runs;
23
+ }
24
+ // Open a run: reset the per-run font and text so an unformatted run inherits nothing from the last.
25
+ beginRun() {
26
+ this.#inRun = true;
27
+ this.#font = null;
28
+ this.#text = '';
29
+ }
30
+ // Open the run's `<rPr>` font bundle; its children stream in via {@link applyProperty}.
31
+ beginProperties() {
32
+ if (this.#inRun)
33
+ this.#font = {};
34
+ }
35
+ // Apply one `<rPr>` child (`<b/>`, `<sz>`, `<color>`, `<rFont>`, …) to the open run's font; a no-op
36
+ // when no `<rPr>` is open.
37
+ applyProperty(local, attrs) {
38
+ if (this.#font !== null)
39
+ applyFontChild(this.#font, local, attrs);
40
+ }
41
+ // Append a `<t>`'s text to the open run. Returns false when no run is open, so the caller routes the
42
+ // text to its surrounding plain/inline string instead.
43
+ appendText(text) {
44
+ if (!this.#inRun)
45
+ return false;
46
+ this.#text += text;
47
+ return true;
48
+ }
49
+ // Commit the open run — with its font, only if that font set at least one facet — to the runs list.
50
+ endRun() {
51
+ if (!this.#inRun)
52
+ return;
53
+ const run = { text: this.#text };
54
+ if (this.#font !== null && Object.keys(this.#font).length > 0)
55
+ run.font = this.#font;
56
+ this.#runs.push(run);
57
+ this.#inRun = false;
58
+ }
59
+ }
@@ -0,0 +1,7 @@
1
+ import type { RichTextRun } from '../../core/value.ts';
2
+ /**
3
+ * Serialise a rich-text value's runs as the inner content of an `<is>` element. A zero-length run
4
+ * is dropped: an empty `<t/>` is schema-invalid — Excel flags the file as corrupt — and an empty
5
+ * run contributes nothing to the rendered text, so omitting it is loss-free.
6
+ */
7
+ export declare function richTextRunsXml(runs: readonly RichTextRun[]): string;
@@ -0,0 +1,23 @@
1
+ // Rich-text runs — a cell value composed of independently-formatted text runs.
2
+ //
3
+ // OOXML stores rich text as a sequence of `<r>` runs, each an optional `<rPr>` (the run's font, in
4
+ // the CT_RPrElt shape — identical to a styles `<font>` except the face element is `<rFont>`, not
5
+ // `<name>`) followed by a `<t>` text element. The writer serialises a rich-text value inline
6
+ // (`t="inlineStr"`), never into the shared-strings table, matching how it writes every other string
7
+ // value; the reader reconstructs the runs while scanning the inline string.
8
+ import { textElement } from "../../xml/xml.js";
9
+ import { fontXml } from "./styles.js";
10
+ /**
11
+ * Serialise a rich-text value's runs as the inner content of an `<is>` element. A zero-length run
12
+ * is dropped: an empty `<t/>` is schema-invalid — Excel flags the file as corrupt — and an empty
13
+ * run contributes nothing to the rendered text, so omitting it is loss-free.
14
+ */
15
+ export function richTextRunsXml(runs) {
16
+ return runs
17
+ .filter((run) => run.text !== '')
18
+ .map((run) => {
19
+ const rPr = run.font !== undefined ? `<rPr>${fontXml(run.font, 'rFont')}</rPr>` : '';
20
+ return `<r>${rPr}${textElement(run.text)}</r>`;
21
+ })
22
+ .join('');
23
+ }
@@ -0,0 +1,6 @@
1
+ import type { Worksheet } from '../../core/worksheet.ts';
2
+ export interface SharedFormulaRole {
3
+ readonly si: number;
4
+ readonly ref?: string;
5
+ }
6
+ export declare function planSharedFormulas(sheet: Worksheet): Map<string, SharedFormulaRole>;
@@ -0,0 +1,52 @@
1
+ // Shared-formula group planning: assigning each master/clone group a sheet-unique index and the
2
+ // master's `ref` range before the row loop serialises a single cell.
3
+ import { encodeAddress } from "../../core/address.js";
4
+ import { isFormulaValue, isSharedFormulaValue } from "../../core/value.js";
5
+ import { AuthoringError } from "../../errors.js";
6
+ // Plan a sheet's shared-formula groups: every clone cell (a {@link SharedFormulaValue}) names its
7
+ // master by address, so group the clones by master, assign each group a sheet-unique `si`, and record
8
+ // the `ref` range (master through the furthest clone) on the master. Excel requires the master to sit
9
+ // at the top-left of that range, so a clone above or left of its master — or a master with no formula
10
+ // (an orphan) — is rejected here, named, rather than emitted as a package Excel repairs on open.
11
+ export function planSharedFormulas(sheet) {
12
+ const groups = new Map();
13
+ for (const { cells } of sheet.rows()) {
14
+ for (const cell of cells) {
15
+ if (isSharedFormulaValue(cell.value)) {
16
+ const clones = groups.get(cell.value.sharedFormula);
17
+ if (clones !== undefined)
18
+ clones.push(cell);
19
+ else
20
+ groups.set(cell.value.sharedFormula, [cell]);
21
+ }
22
+ }
23
+ }
24
+ const roles = new Map();
25
+ let si = 0;
26
+ for (const [masterAddress, clones] of groups) {
27
+ const master = sheet.getCell(masterAddress);
28
+ if (!isFormulaValue(master.value)) {
29
+ const offender = clones[0];
30
+ throw new AuthoringError(`shared-formula clone ${offender.address} names master ${masterAddress}, which holds no formula`);
31
+ }
32
+ let maxCol = master.col;
33
+ let maxRow = master.row;
34
+ for (const clone of clones) {
35
+ if (clone.col < master.col || clone.row < master.row) {
36
+ throw new AuthoringError(`shared-formula master ${masterAddress} must sit above and/or left of clone ${clone.address}`);
37
+ }
38
+ if (clone.col > maxCol)
39
+ maxCol = clone.col;
40
+ if (clone.row > maxRow)
41
+ maxRow = clone.row;
42
+ }
43
+ roles.set(masterAddress, {
44
+ si,
45
+ ref: `${encodeAddress(master.col, master.row)}:${encodeAddress(maxCol, maxRow)}`,
46
+ });
47
+ for (const clone of clones)
48
+ roles.set(clone.address, { si });
49
+ si += 1;
50
+ }
51
+ return roles;
52
+ }
@@ -0,0 +1,2 @@
1
+ import type { SharedString } from './cell-value.ts';
2
+ export declare function parseSharedStrings(xml: string): SharedString[];
@@ -0,0 +1,70 @@
1
+ // Parser for the shared-string table (`xl/sharedStrings.xml`), the pool that `t="s"` cells index into.
2
+ // Split out of read.ts beside its sibling parsers (read-styles.ts, rich-runs.ts) so read.ts stays
3
+ // orchestration; the run structure it decodes is owned by RunAccumulator, shared with inline strings.
4
+ import { localName, parseXml } from "../../xml/xml-read.js";
5
+ import { RunAccumulator } from "./rich-runs.js";
6
+ // Shared strings resolve `t="s"` cells. Each `<si>` is one entry: a plain `<si><t>…</t>` decodes to a
7
+ // string, while a rich `<si><r><rPr>…</rPr><t>…</t></r>…` decodes to a {@link RichTextValue} whose runs
8
+ // carry their per-run fonts — so rich text Excel pooled reads back formatted, not flattened to text.
9
+ // The run structure inside an `<si>` is identical to an inline string's `<is>`, so it is parsed the
10
+ // same way (see the inline-run accumulation in `parseWorksheet`).
11
+ export function parseSharedStrings(xml) {
12
+ if (xml === '')
13
+ return [];
14
+ const strings = [];
15
+ // Per-`<si>` accumulation: `plain` gathers a bare `<t>`; `runs` gathers `<r>` runs. An `<si>` is
16
+ // rich the moment it holds one `<r>`, at which point its runs — not `plain` — become the entry.
17
+ let plain = '';
18
+ const runs = new RunAccumulator();
19
+ let isRich = false;
20
+ let capture = false;
21
+ let text = '';
22
+ parseXml(xml, {
23
+ onOpen(name, attrs) {
24
+ const local = localName(name);
25
+ switch (local) {
26
+ case 'si':
27
+ plain = '';
28
+ runs.reset();
29
+ isRich = false;
30
+ break;
31
+ case 'r':
32
+ isRich = true;
33
+ runs.beginRun();
34
+ break;
35
+ case 'rPr':
36
+ runs.beginProperties();
37
+ break;
38
+ case 't':
39
+ capture = true;
40
+ text = '';
41
+ break;
42
+ default:
43
+ runs.applyProperty(local, attrs);
44
+ break;
45
+ }
46
+ },
47
+ onText(chunk) {
48
+ if (capture)
49
+ text += chunk;
50
+ },
51
+ onClose(name) {
52
+ const local = localName(name);
53
+ switch (local) {
54
+ case 't':
55
+ // A `<t>` inside a run is that run's text; a bare `<t>` directly in the `<si>` is plain.
56
+ if (!runs.appendText(text))
57
+ plain += text;
58
+ capture = false;
59
+ break;
60
+ case 'r':
61
+ runs.endRun();
62
+ break;
63
+ case 'si':
64
+ strings.push(isRich ? { richText: runs.runs } : plain);
65
+ break;
66
+ }
67
+ },
68
+ });
69
+ return strings;
70
+ }
@@ -0,0 +1,17 @@
1
+ import type { RichTextValue } from '../../core/value.ts';
2
+ /**
3
+ * Interns cell string values into the shared-strings pool. {@link intern} returns the index a
4
+ * `t="s"` cell writes as its `<v>`; identical entries collapse to one, so `count` (total references)
5
+ * and `uniqueCount` (distinct entries) diverge exactly as Excel records them. Each entry is stored as
6
+ * its rendered `<si>` inner XML, which is also its dedup key — a plain string (`<t>…`) and rich runs
7
+ * (`<r>…`) render to distinct markup, so the two kinds never collide in the pool.
8
+ */
9
+ export declare class SharedStringTable {
10
+ #private;
11
+ /** Intern a plain or rich string and return its `<si>` index, reusing the entry when it repeats. */
12
+ intern(value: string | RichTextValue): number;
13
+ /** Whether no string has been interned — the writer omits the part entirely when so. */
14
+ get isEmpty(): boolean;
15
+ /** Serialise the pool as the `xl/sharedStrings.xml` part. */
16
+ toXml(): string;
17
+ }
@@ -0,0 +1,50 @@
1
+ // The shared-strings table: `xl/sharedStrings.xml`, the workbook-wide pool a `t="s"` cell indexes.
2
+ //
3
+ // When the writer runs with `useSharedStrings`, a plain string cell value is interned here — an
4
+ // identical string is stored once and every cell holding it references the single `<si>` entry by
5
+ // index — rather than repeating the text inline in each cell. This trades a second part and an
6
+ // indirection for a smaller package when strings repeat, the storage Excel itself prefers.
7
+ //
8
+ // Both plain strings and rich text are pooled: a plain value becomes a `<si><t>…</t></si>` entry, a
9
+ // rich value a `<si><r>…</r>…</si>` entry carrying its per-run formatting — the rich `<si>` runs Excel
10
+ // itself writes. A `t="s"` cell then indexes either, and the reader reconstructs the runs, so pooled
11
+ // rich text round-trips its formatting rather than flattening to text.
12
+ import { textElement, XML_DECLARATION } from "../../xml/xml.js";
13
+ import { SPREADSHEETML_NS } from "./namespaces.js";
14
+ import { richTextRunsXml } from "./rich-text.js";
15
+ /**
16
+ * Interns cell string values into the shared-strings pool. {@link intern} returns the index a
17
+ * `t="s"` cell writes as its `<v>`; identical entries collapse to one, so `count` (total references)
18
+ * and `uniqueCount` (distinct entries) diverge exactly as Excel records them. Each entry is stored as
19
+ * its rendered `<si>` inner XML, which is also its dedup key — a plain string (`<t>…`) and rich runs
20
+ * (`<r>…`) render to distinct markup, so the two kinds never collide in the pool.
21
+ */
22
+ export class SharedStringTable {
23
+ #indexByEntry = new Map();
24
+ #entries = [];
25
+ #references = 0;
26
+ /** Intern a plain or rich string and return its `<si>` index, reusing the entry when it repeats. */
27
+ intern(value) {
28
+ this.#references += 1;
29
+ const inner = typeof value === 'string' ? textElement(value) : richTextRunsXml(value.richText);
30
+ const existing = this.#indexByEntry.get(inner);
31
+ if (existing !== undefined)
32
+ return existing;
33
+ const index = this.#entries.length;
34
+ this.#indexByEntry.set(inner, index);
35
+ this.#entries.push(inner);
36
+ return index;
37
+ }
38
+ /** Whether no string has been interned — the writer omits the part entirely when so. */
39
+ get isEmpty() {
40
+ return this.#entries.length === 0;
41
+ }
42
+ /** Serialise the pool as the `xl/sharedStrings.xml` part. */
43
+ toXml() {
44
+ const items = this.#entries.map((inner) => `<si>${inner}</si>`).join('');
45
+ return (XML_DECLARATION +
46
+ `<sst xmlns="${SPREADSHEETML_NS}" count="${this.#references}" uniqueCount="${this.#entries.length}">` +
47
+ items +
48
+ '</sst>');
49
+ }
50
+ }
@@ -0,0 +1,13 @@
1
+ import type { AutoFilter } from '../../core/autofilter.ts';
2
+ import type { HeaderFooter, PageBreak, PageMargins, PageSetup, PrintOptions } from '../../core/page-setup.ts';
3
+ import { type SheetProtection } from '../../core/protection.ts';
4
+ import type { SheetView, Worksheet } from '../../core/worksheet.ts';
5
+ export declare function sheetViewsXml(view: SheetView, active: boolean): string;
6
+ export declare function sheetPrXml(sheet: Worksheet): string;
7
+ export declare function sheetProtectionXml(protection: SheetProtection | undefined): string;
8
+ export declare function autoFilterXml(filter: AutoFilter | undefined): string;
9
+ export declare function headerFooterXml(hf: HeaderFooter): string;
10
+ export declare function printOptionsXml(printOptions: PrintOptions): string;
11
+ export declare function pageMarginsXml(margins: PageMargins): string;
12
+ export declare function pageSetupXml(pageSetup: PageSetup, printerSettingsRelId: string | null): string;
13
+ export declare function pageBreaksXml(breaks: readonly PageBreak[], element: 'rowBreaks' | 'colBreaks'): string;
@@ -0,0 +1,211 @@
1
+ // The worksheet's non-cell property blocks: `<sheetPr>`, `<sheetViews>`, `<sheetProtection>`,
2
+ // `<autoFilter>`, and the print settings (`<printOptions>`, `<pageMargins>`, `<pageSetup>`,
3
+ // `<headerFooter>`, `<rowBreaks>`/`<colBreaks>`). Each renders one CT_Worksheet child (or child
4
+ // group) independently of the row/cell body `worksheet-xml.ts` orchestrates them alongside.
5
+ import { encodeAddress } from "../../core/address.js";
6
+ import { SHEET_PROTECTION_FLAGS } from "../../core/protection.js";
7
+ import { attr, boolAttr, escapeAttr, escapeText, numberText } from "../../xml/xml.js";
8
+ import { colorAttrs } from "./styles.js";
9
+ // `<sheetViews>` holds the sheet's single view. A frozen view adds a `<pane>` recording the split
10
+ // and a `<selection>` naming the pane the split activates, exactly as Excel writes it — a normal
11
+ // view carries neither, so unfreezing leaves no leftover `<pane>` that would trip a repair prompt.
12
+ // The active pane is whichever scrolling region the freeze creates: bottom-right when both axes are
13
+ // frozen, else top-right (columns only) or bottom-left (rows only).
14
+ //
15
+ // `active` marks this sheet as the one selected on open (`tabSelected`). Exactly one sheet in a
16
+ // workbook carries it — with none, the consumer opens with no sheet view initialised; with several,
17
+ // the sheets form a *group selection*, where an edit to one is applied to all of them. The caller
18
+ // (`worksheetXml`, fed from `Workbook.activeTabIndex`) is what guarantees the "exactly one".
19
+ export function sheetViewsXml(view, active) {
20
+ const selected = active ? ' tabSelected="1"' : '';
21
+ const xSplit = view.xSplit ?? 0;
22
+ const ySplit = view.ySplit ?? 0;
23
+ if (view.state !== 'frozen' || (xSplit === 0 && ySplit === 0)) {
24
+ return `<sheetViews><sheetView${selected} workbookViewId="0"/></sheetViews>`;
25
+ }
26
+ const topLeftCell = view.topLeftCell ?? encodeAddress(xSplit + 1, ySplit + 1);
27
+ const activePane = xSplit > 0 && ySplit > 0 ? 'bottomRight' : xSplit > 0 ? 'topRight' : 'bottomLeft';
28
+ const pane = '<pane' +
29
+ (xSplit > 0 ? ` xSplit="${xSplit}"` : '') +
30
+ (ySplit > 0 ? ` ySplit="${ySplit}"` : '') +
31
+ ` topLeftCell="${escapeAttr(topLeftCell)}" activePane="${activePane}" state="frozen"/>`;
32
+ const selection = `<selection pane="${activePane}" activeCell="${escapeAttr(topLeftCell)}" sqref="${escapeAttr(topLeftCell)}"/>`;
33
+ return `<sheetViews><sheetView${selected} workbookViewId="0">${pane}${selection}</sheetView></sheetViews>`;
34
+ }
35
+ // `<sheetPr>` carries the sheet's appearance properties: the tab colour, the outline
36
+ // summary-position flags, and the fit-to-page flag. It is the first child of `<worksheet>` in
37
+ // CT_Worksheet order; its own children follow CT_SheetPr order — `<tabColor>`, `<outlinePr>`, then
38
+ // `<pageSetUpPr>`. Omitted entirely when the sheet carries none, so an unadorned sheet stays
39
+ // byte-clean.
40
+ export function sheetPrXml(sheet) {
41
+ const children = (sheet.tabColor !== undefined ? `<tabColor ${colorAttrs(sheet.tabColor)}/>` : '') +
42
+ outlinePrXml(sheet.outline) +
43
+ pageSetUpPrXml(sheet.pageSetup);
44
+ return children === '' ? '' : `<sheetPr>${children}</sheetPr>`;
45
+ }
46
+ // `<pageSetUpPr>` holds the fit-to-page toggle, which lives on the sheet properties rather than on
47
+ // `<pageSetup>` — Excel reads it from here to decide whether the `fitToWidth`/`fitToHeight` counts
48
+ // or the fixed `scale` govern printing. Emitted only when the author set the flag.
49
+ function pageSetUpPrXml(pageSetup) {
50
+ return pageSetup.fitToPage ? '<pageSetUpPr fitToPage="1"/>' : '';
51
+ }
52
+ // `<outlinePr>` carries only the summary-position flags today. Each is emitted solely when the
53
+ // caller set it, so an inverted placement (`summaryBelow="0"`) is honoured while an untouched sheet
54
+ // keeps the element out of the file entirely.
55
+ function outlinePrXml(outline) {
56
+ const attrs = boolAttr('summaryBelow', outline.summaryBelow) + boolAttr('summaryRight', outline.summaryRight);
57
+ return attrs === '' ? '' : `<outlinePr${attrs}/>`;
58
+ }
59
+ // Each sheet-protection flag maps to a `<sheetProtection>` attribute whose value is INVERTED
60
+ // from the author-facing allow-flag: the attribute records that an operation is *forbidden*
61
+ // ("1"), so `allow: true` serialises as "0". Only a value that differs from OOXML's per-
62
+ // attribute default (see SHEET_PROTECTION_FLAGS) is written — most editing operations default
63
+ // to forbidden under protection, while selecting cells defaults to permitted.
64
+ //
65
+ // <sheetProtection> is what makes the per-cell locked/hidden flags bite. `sheet="1"` marks the
66
+ // sheet protected; the password credential (when present) guards lifting it; the flag attributes
67
+ // carve out the operations that stay available. base64 salt/hash use only XML-safe characters.
68
+ export function sheetProtectionXml(protection) {
69
+ if (protection === undefined)
70
+ return '';
71
+ const { flags, credential } = protection;
72
+ let attrs = '';
73
+ if (credential !== undefined) {
74
+ attrs +=
75
+ ` algorithmName="${credential.algorithmName}"` +
76
+ ` hashValue="${credential.hashValue}"` +
77
+ ` saltValue="${credential.saltValue}"` +
78
+ ` spinCount="${credential.spinCount}"`;
79
+ }
80
+ attrs += ' sheet="1"';
81
+ for (const { key, defaultForbidden } of SHEET_PROTECTION_FLAGS) {
82
+ const allow = flags[key];
83
+ if (allow === undefined)
84
+ continue;
85
+ const forbidden = !allow;
86
+ if (forbidden === defaultForbidden)
87
+ continue;
88
+ attrs += boolAttr(key, forbidden);
89
+ }
90
+ return `<sheetProtection${attrs}/>`;
91
+ }
92
+ // The sheet's autofilter: `<autoFilter ref="A1:C10"/>` when it only draws dropdowns, or with nested
93
+ // `<filterColumn>` children when columns carry criteria. Its companion `_FilterDatabase` defined name
94
+ // (the range Excel derives filtering from) is written in the workbook part, so a sheet with no filter
95
+ // emits nothing here and nothing there.
96
+ export function autoFilterXml(filter) {
97
+ if (filter === undefined)
98
+ return '';
99
+ const ref = escapeAttr(filter.ref);
100
+ if (filter.columns.length === 0)
101
+ return `<autoFilter ref="${ref}"/>`;
102
+ return `<autoFilter ref="${ref}">${filter.columns.map(filterColumnXml).join('')}</autoFilter>`;
103
+ }
104
+ function filterColumnXml(column) {
105
+ return `<filterColumn colId="${column.colId}">${filterCriteriaXml(column.criteria)}</filterColumn>`;
106
+ }
107
+ // A values filter is `<filters>` with a `<filter val>` per allowed value (and `blank="1"` to admit
108
+ // empty cells); a custom filter is `<customFilters>` with one or two `<customFilter operator val>`
109
+ // predicates, `and="1"` when they are AND-combined rather than OR.
110
+ function filterCriteriaXml(criteria) {
111
+ if (criteria.kind === 'values') {
112
+ const blankAttr = criteria.blank ? ' blank="1"' : '';
113
+ const filters = criteria.values.map((value) => `<filter val="${escapeAttr(value)}"/>`).join('');
114
+ return `<filters${blankAttr}>${filters}</filters>`;
115
+ }
116
+ const andAttr = criteria.and ? ' and="1"' : '';
117
+ const predicates = criteria.predicates
118
+ .map((p) => `<customFilter operator="${p.operator}" val="${escapeAttr(p.val)}"/>`)
119
+ .join('');
120
+ return `<customFilters${andAttr}>${predicates}</customFilters>`;
121
+ }
122
+ // CT_HeaderFooter child order, paired with the flag their presence gates: the even- and
123
+ // first-page variants are silently ignored by Excel unless differentOddEven / differentFirst
124
+ // are set, so the writer derives each flag from whether any variant in its class was provided.
125
+ const HF_CHILDREN = [
126
+ { tag: 'oddHeader', key: 'oddHeader' },
127
+ { tag: 'oddFooter', key: 'oddFooter' },
128
+ { tag: 'evenHeader', key: 'evenHeader' },
129
+ { tag: 'evenFooter', key: 'evenFooter' },
130
+ { tag: 'firstHeader', key: 'firstHeader' },
131
+ { tag: 'firstFooter', key: 'firstFooter' },
132
+ ];
133
+ export function headerFooterXml(hf) {
134
+ const children = HF_CHILDREN.filter(({ key }) => hf[key] !== undefined);
135
+ if (children.length === 0)
136
+ return '';
137
+ const differentOddEven = hf.evenHeader !== undefined || hf.evenFooter !== undefined;
138
+ const differentFirst = hf.firstHeader !== undefined || hf.firstFooter !== undefined;
139
+ let attrs = '';
140
+ if (differentOddEven)
141
+ attrs += ' differentOddEven="1"';
142
+ if (differentFirst)
143
+ attrs += ' differentFirst="1"';
144
+ const body = children
145
+ .map(({ tag, key }) => `<${tag}>${escapeText(hf[key])}</${tag}>`)
146
+ .join('');
147
+ return `<headerFooter${attrs}>${body}</headerFooter>`;
148
+ }
149
+ // Excel's "Normal" margins, in inches — the defaults Excel writes for an untouched sheet.
150
+ const DEFAULT_MARGINS = {
151
+ left: 0.7,
152
+ right: 0.7,
153
+ top: 0.75,
154
+ bottom: 0.75,
155
+ header: 0.3,
156
+ footer: 0.3,
157
+ };
158
+ const MARGIN_SIDES = ['left', 'right', 'top', 'bottom', 'header', 'footer'];
159
+ // `<printOptions>` carries the print-toggle flags and sits just before `<pageMargins>` in
160
+ // CT_Worksheet order. Each attribute is emitted only when the model carries it — as an explicit
161
+ // `="1"`/`="0"` so a caller can force a flag off against Excel's default — and an untouched sheet
162
+ // keeps the element out of the file entirely.
163
+ export function printOptionsXml(printOptions) {
164
+ const attrs = boolAttr('horizontalCentered', printOptions.horizontalCentered) +
165
+ boolAttr('verticalCentered', printOptions.verticalCentered) +
166
+ boolAttr('headings', printOptions.headings) +
167
+ boolAttr('gridLines', printOptions.gridLines) +
168
+ boolAttr('gridLinesSet', printOptions.gridLinesSet);
169
+ return attrs === '' ? '' : `<printOptions${attrs}/>`;
170
+ }
171
+ // OOXML's <pageMargins> is all-or-nothing: setting any one margin requires all six, or Excel
172
+ // repairs the file. So the element is emitted only when the caller set at least one, and the
173
+ // untouched sides fall back to the Normal-preset defaults.
174
+ export function pageMarginsXml(margins) {
175
+ if (MARGIN_SIDES.every((side) => margins[side] === undefined))
176
+ return '';
177
+ const attrs = MARGIN_SIDES.map((side) => `${side}="${numberText(margins[side] ?? DEFAULT_MARGINS[side])}"`).join(' ');
178
+ return `<pageMargins ${attrs}/>`;
179
+ }
180
+ // `<pageSetup>` carries the print-scaling attributes (all but `fitToPage`, which is a `<sheetPr>`
181
+ // flag). It sits between `<pageMargins>` and `<headerFooter>` in CT_Worksheet order. Each attribute
182
+ // is emitted only when the author set it, so an untouched sheet keeps the element out of the file
183
+ // and a partially-set one never fabricates the counts Excel would otherwise default. A non-null
184
+ // `printerSettingsRelId` links the sheet's opaque printer-settings blob and forces the element out
185
+ // even when no scaling attribute is set — the reference is the only thing the model has to carry.
186
+ export function pageSetupXml(pageSetup, printerSettingsRelId) {
187
+ const attrs = attr('paperSize', pageSetup.paperSize) +
188
+ attr('scale', pageSetup.scale) +
189
+ attr('fitToWidth', pageSetup.fitToWidth) +
190
+ attr('fitToHeight', pageSetup.fitToHeight) +
191
+ (pageSetup.pageOrder !== undefined ? ` pageOrder="${pageSetup.pageOrder}"` : '') +
192
+ (pageSetup.orientation !== undefined ? ` orientation="${pageSetup.orientation}"` : '') +
193
+ (printerSettingsRelId !== null ? ` r:id="${printerSettingsRelId}"` : '');
194
+ return attrs === '' ? '' : `<pageSetup${attrs}/>`;
195
+ }
196
+ // Manual page breaks (`<rowBreaks>`/`<colBreaks>`): one `<brk>` per row/column the layout splits
197
+ // before. Excel records both the running total (`count`) and the manual subset (`manualBreakCount`);
198
+ // every break the model carries is a manual, author-set one, so the two counts coincide. `max` bounds
199
+ // the break across the other axis (Excel writes the last row/column index); a break without one is
200
+ // emitted bare. Row and column breaks share this shape, differing only in the wrapping element.
201
+ export function pageBreaksXml(breaks, element) {
202
+ if (breaks.length === 0)
203
+ return '';
204
+ const brks = breaks
205
+ .map((brk) => {
206
+ const maxAttr = brk.max !== undefined ? ` max="${brk.max}"` : '';
207
+ return `<brk id="${brk.id}"${maxAttr} man="1"/>`;
208
+ })
209
+ .join('');
210
+ return `<${element} count="${breaks.length}" manualBreakCount="${breaks.length}">${brks}</${element}>`;
211
+ }