@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,9 @@
1
+ import { type Table, type TableOptions } from '../../core/table.ts';
2
+ export declare function tableXml(table: Table, id: number): string;
3
+ /**
4
+ * Parse a `<table>` part into the options that reconstruct it, or `undefined` when the XML is not a
5
+ * usable table (no name, no ref, or no columns — Excel treats such a part as corrupt, so we drop it
6
+ * rather than fabricate a degenerate table). Duplicate column names are not resolved here — the
7
+ * {@link Table} constructor disambiguates them, so authoring and loading share one implementation.
8
+ */
9
+ export declare function parseTable(xml: string): TableOptions | undefined;
@@ -0,0 +1,208 @@
1
+ // Worksheet tables (OOXML `<table>` parts, `xl/tables/table{n}.xml`) — both directions. The writer
2
+ // (`tableXml`) turns a `Table` into its part; the reader (`parseTable`) is its inverse, turning a
3
+ // stored part back into the `TableOptions` a worksheet re-registers.
4
+ //
5
+ // The part stores the table's *full* occupied range (`ref="A1:B3"`), whereas the model anchors at a
6
+ // single top-left cell plus a data-row count. The two are equivalent: the data-row count is the
7
+ // range height minus the header row (present unless `headerRowCount="0"`) and the totals row (present
8
+ // only when `totalsRowCount` is positive), so reconstructing one from the other is lossless.
9
+ import { decodeRange, encodeAddress } from "../../core/address.js";
10
+ import { isTotalsRowFunction, } from "../../core/table.js";
11
+ import { boolAttr, escapeAttr, escapeText, XML_DECLARATION } from "../../xml/xml.js";
12
+ import { localName, parseXml } from "../../xml/xml-read.js";
13
+ import { NS } from "./relationships.js";
14
+ export function tableXml(table, id) {
15
+ const name = escapeAttr(table.name);
16
+ const displayName = escapeAttr(table.displayName);
17
+ // headerRowCount defaults to 1 in OOXML, so only a headerless table needs it stated.
18
+ const headerRowCount = table.headerRow ? '' : ' headerRowCount="0"';
19
+ // A present totals row implies it is shown, so it only needs the count. Without a totals row the
20
+ // model's tri-state totalsRowShown decides: emit the flag Excel recorded, or nothing when the
21
+ // source omitted it — injecting `totalsRowShown="0"` onto a table that lacked the attribute is
22
+ // exactly the spurious change that makes Excel treat an otherwise-valid table as corrupt.
23
+ let totals;
24
+ if (table.totalsRow) {
25
+ totals = ' totalsRowCount="1"';
26
+ }
27
+ else if (table.totalsRowShown !== undefined) {
28
+ totals = ` totalsRowShown="${table.totalsRowShown ? '1' : '0'}"`;
29
+ }
30
+ else {
31
+ totals = '';
32
+ }
33
+ const autoFilter = table.autoFilterRef !== undefined ? `<autoFilter ref="${table.autoFilterRef}"/>` : '';
34
+ const columns = table.columns.map((column, i) => tableColumnXml(column, i + 1)).join('');
35
+ return (XML_DECLARATION +
36
+ `<table xmlns="${NS.main}" id="${id}" name="${name}" displayName="${displayName}" ` +
37
+ `ref="${table.range}"${headerRowCount}${totals}>` +
38
+ autoFilter +
39
+ `<tableColumns count="${table.columns.length}">${columns}</tableColumns>` +
40
+ tableStyleInfoXml(table.style) +
41
+ '</table>');
42
+ }
43
+ // Excel's default table appearance, written for a table that carries no style of its own.
44
+ const DEFAULT_TABLE_STYLE = '<tableStyleInfo name="TableStyleMedium2" showFirstColumn="0" showLastColumn="0" ' +
45
+ 'showRowStripes="1" showColumnStripes="0"/>';
46
+ // Emit `<tableStyleInfo>` from the model's style, or the default when none was captured. Each
47
+ // attribute is written only when the model holds it, so a style read without (say) a `name` — or a
48
+ // part that omitted a banding flag — re-emits exactly as it arrived rather than gaining an attribute.
49
+ function tableStyleInfoXml(style) {
50
+ if (style === undefined)
51
+ return DEFAULT_TABLE_STYLE;
52
+ let attrs = '';
53
+ if (style.name !== undefined)
54
+ attrs += ` name="${escapeAttr(style.name)}"`;
55
+ attrs +=
56
+ boolAttr('showFirstColumn', style.showFirstColumn) +
57
+ boolAttr('showLastColumn', style.showLastColumn) +
58
+ boolAttr('showRowStripes', style.showRowStripes) +
59
+ boolAttr('showColumnStripes', style.showColumnStripes);
60
+ return `<tableStyleInfo${attrs}/>`;
61
+ }
62
+ function tableColumnXml(column, id) {
63
+ let attrs = `id="${id}" name="${escapeAttr(column.name)}"`;
64
+ if (column.totalsRowLabel !== undefined) {
65
+ attrs += ` totalsRowLabel="${escapeAttr(column.totalsRowLabel)}"`;
66
+ }
67
+ if (column.totalsRowFunction !== undefined) {
68
+ attrs += ` totalsRowFunction="${escapeAttr(column.totalsRowFunction)}"`;
69
+ }
70
+ // A `custom` total is carried by a `<totalsRowFormula>` child rather than a built-in function, so
71
+ // the element is non-self-closing when one is present. The formula is stored without a leading `=`,
72
+ // matching how Excel writes it.
73
+ if (column.totalsRowFormula !== undefined) {
74
+ return `<tableColumn ${attrs}><totalsRowFormula>${escapeText(column.totalsRowFormula)}</totalsRowFormula></tableColumn>`;
75
+ }
76
+ return `<tableColumn ${attrs}/>`;
77
+ }
78
+ // OOXML booleans spell false as "0" or "false"; every other spelling (including "1"/"true") is true.
79
+ function parseOoxmlBool(value) {
80
+ return value !== '0' && value !== 'false';
81
+ }
82
+ /**
83
+ * Parse a `<table>` part into the options that reconstruct it, or `undefined` when the XML is not a
84
+ * usable table (no name, no ref, or no columns — Excel treats such a part as corrupt, so we drop it
85
+ * rather than fabricate a degenerate table). Duplicate column names are not resolved here — the
86
+ * {@link Table} constructor disambiguates them, so authoring and loading share one implementation.
87
+ */
88
+ export function parseTable(xml) {
89
+ let name;
90
+ let displayName;
91
+ let ref;
92
+ let headerRowCount = 1; // OOXML default: a table carries a header row unless it says otherwise.
93
+ let totalsRowCount = 0; // OOXML default: no totals row.
94
+ let totalsRowShown; // Absent unless the part states the attribute.
95
+ let style; // Absent unless the part carries a `<tableStyleInfo>`.
96
+ let hasAutoFilter = false; // Only present when the part carries an `<autoFilter>` element.
97
+ const columns = [];
98
+ // A `<totalsRowFormula>` is a text child of the current `<tableColumn>`, so it is captured across
99
+ // open/text/close rather than from an attribute. `calculatedColumnFormula` is a sibling child of
100
+ // the same type (CT_TableFormula), so guard on the exact element to avoid capturing its text.
101
+ let inTotalsFormula = false;
102
+ let totalsFormula = '';
103
+ parseXml(xml, {
104
+ onOpen(elementName, attrs) {
105
+ switch (localName(elementName)) {
106
+ case 'table':
107
+ // OOXML makes `displayName` the required identifier and `name` an optional alias; the
108
+ // model inverts the roles (`name` is the formula identifier, `displayName` the label),
109
+ // so read each from its own attribute and fall back across the pair when one is absent.
110
+ name = attrs.name ?? attrs.displayName;
111
+ displayName = attrs.displayName ?? attrs.name;
112
+ ref = attrs.ref;
113
+ if (attrs.headerRowCount !== undefined)
114
+ headerRowCount = Number(attrs.headerRowCount);
115
+ if (attrs.totalsRowCount !== undefined)
116
+ totalsRowCount = Number(attrs.totalsRowCount);
117
+ // Capture the flag verbatim so it re-emits exactly (or, absent, stays absent) rather
118
+ // than being normalised.
119
+ if (attrs.totalsRowShown !== undefined)
120
+ totalsRowShown = parseOoxmlBool(attrs.totalsRowShown);
121
+ break;
122
+ case 'autoFilter':
123
+ hasAutoFilter = true;
124
+ break;
125
+ case 'tableStyleInfo': {
126
+ // Keep each attribute off the literal so an absent one stays absent (not `key: undefined`),
127
+ // preserving the round-trip — the writer re-emits only the attributes we actually saw.
128
+ const captured = {};
129
+ if (attrs.name !== undefined)
130
+ captured.name = attrs.name;
131
+ if (attrs.showFirstColumn !== undefined)
132
+ captured.showFirstColumn = parseOoxmlBool(attrs.showFirstColumn);
133
+ if (attrs.showLastColumn !== undefined)
134
+ captured.showLastColumn = parseOoxmlBool(attrs.showLastColumn);
135
+ if (attrs.showRowStripes !== undefined)
136
+ captured.showRowStripes = parseOoxmlBool(attrs.showRowStripes);
137
+ if (attrs.showColumnStripes !== undefined) {
138
+ captured.showColumnStripes = parseOoxmlBool(attrs.showColumnStripes);
139
+ }
140
+ style = captured;
141
+ break;
142
+ }
143
+ case 'tableColumn': {
144
+ if (attrs.name === undefined)
145
+ break;
146
+ const column = { name: attrs.name };
147
+ if (attrs.totalsRowLabel !== undefined)
148
+ column.totalsRowLabel = attrs.totalsRowLabel;
149
+ // An unrecognised totalsRowFunction is dropped rather than trusted in verbatim — the token
150
+ // is a closed OOXML enumeration, so a foreign value is malformed input, not a future Excel
151
+ // addition to accommodate.
152
+ if (attrs.totalsRowFunction !== undefined &&
153
+ isTotalsRowFunction(attrs.totalsRowFunction)) {
154
+ column.totalsRowFunction = attrs.totalsRowFunction;
155
+ }
156
+ columns.push(column);
157
+ break;
158
+ }
159
+ case 'totalsRowFormula':
160
+ inTotalsFormula = true;
161
+ totalsFormula = '';
162
+ break;
163
+ }
164
+ },
165
+ onText(text) {
166
+ if (inTotalsFormula)
167
+ totalsFormula += text;
168
+ },
169
+ onClose(elementName) {
170
+ if (localName(elementName) !== 'totalsRowFormula')
171
+ return;
172
+ inTotalsFormula = false;
173
+ // Attach to the column currently being parsed — the last one pushed. Excel writes the child
174
+ // only for `totalsRowFunction="custom"`, so a formula on any other column is meaningless, but
175
+ // preserving whatever the part carried keeps the round-trip faithful rather than second-guessing.
176
+ const column = columns[columns.length - 1];
177
+ if (column !== undefined)
178
+ column.totalsRowFormula = totalsFormula;
179
+ },
180
+ });
181
+ if (name === undefined || ref === undefined || columns.length === 0)
182
+ return undefined;
183
+ const { top, left, bottom } = decodeRange(ref);
184
+ if (top === undefined || left === undefined || bottom === undefined)
185
+ return undefined;
186
+ const headerRow = headerRowCount !== 0;
187
+ const totalsRow = totalsRowCount > 0;
188
+ const dataRows = bottom - top + 1 - (headerRow ? 1 : 0) - (totalsRow ? 1 : 0);
189
+ const options = {
190
+ name,
191
+ displayName: displayName ?? name,
192
+ ref: encodeAddress(left, top),
193
+ columns,
194
+ rowCount: Math.max(0, dataRows),
195
+ headerRow,
196
+ totalsRow,
197
+ // Reconstruct the autoFilter state explicitly from the part: a header table read without an
198
+ // `<autoFilter>` must not have one fabricated on the next write.
199
+ autoFilter: hasAutoFilter,
200
+ };
201
+ // Kept off the literal so an absent attribute stays absent (not `totalsRowShown: undefined`),
202
+ // preserving the round-trip: a table that never stated the flag must not gain one.
203
+ if (totalsRowShown !== undefined)
204
+ options.totalsRowShown = totalsRowShown;
205
+ if (style !== undefined)
206
+ options.style = style;
207
+ return options;
208
+ }
@@ -0,0 +1,102 @@
1
+ import { type CommentThread, type Person } from '../../core/comment-thread.ts';
2
+ /** A registered author of threaded comments — one `<person>` of `xl/persons/person.xml`. */
3
+ export interface ParsedPerson {
4
+ /** Brace-wrapped GUID a message's `personId` points at. */
5
+ readonly id: string;
6
+ readonly displayName: string;
7
+ /** Identity-provider handle, `S::<email>::<tenant-guid>` for an AzureAD account. */
8
+ readonly userId?: string;
9
+ /** Identity provider, e.g. `AD`. */
10
+ readonly providerId?: string;
11
+ }
12
+ /**
13
+ * One `<mention>` of a message's `<mentions>` block: who was named, and the run of the message text
14
+ * that renders as the mention chip.
15
+ *
16
+ * All four wire attributes are required (Excel rejects a file missing any), and note the lowercase `p`
17
+ * in `mentionpersonId` — the capitalised spelling is not a declared attribute.
18
+ */
19
+ export interface ParsedMention {
20
+ /** The mentioned {@link ParsedPerson.id}, from `mentionpersonId`. */
21
+ readonly personId: string;
22
+ /** Excel's own id for the mention itself. Absent only in a file that omitted it. */
23
+ readonly mentionId?: string;
24
+ /**
25
+ * 0-based character offset into the message text. Verified against desktop Excel by rendering: the
26
+ * chip covers exactly `[startIndex, startIndex + length)` of the text.
27
+ */
28
+ readonly startIndex: number;
29
+ /** The mention's length in characters, **including the leading `@`** (`@Grace Hopper` is 13). */
30
+ readonly length: number;
31
+ }
32
+ /** One message of a threaded conversation — a `<threadedComment>` of a `threadedComment{n}.xml`. */
33
+ export interface ParsedThreadedComment {
34
+ /** A1 reference of the cell the whole thread anchors to; every message of a thread repeats it. */
35
+ readonly ref: string;
36
+ /** Brace-wrapped GUID identifying this message, and the `parentId` its replies carry. */
37
+ readonly id: string;
38
+ /** The author's {@link ParsedPerson.id}. Absent in a file that recorded no author. */
39
+ readonly personId?: string;
40
+ /**
41
+ * The `dT` timestamp verbatim. Excel writes local wall-clock with fractional seconds and no
42
+ * timezone (`2026-07-24T10:56:41.72`), which is not a round-trippable instant — keeping the
43
+ * string spares the reader from inventing a zone the file never stated.
44
+ */
45
+ readonly date?: string;
46
+ readonly text: string;
47
+ /** The thread head's {@link ParsedThreadedComment.id}; absent iff this message *is* the head. */
48
+ readonly parentId?: string;
49
+ /**
50
+ * The `done` flag exactly as written. Excel puts it on the head alone and omits it entirely on an
51
+ * open thread (never `done="0"`), so read a thread's resolved state off its head — a reply's
52
+ * `false` here means "did not say", not "not resolved".
53
+ */
54
+ readonly done: boolean;
55
+ /** The `<mentions>` this message carries, in document order; empty when it names no one. */
56
+ readonly mentions: readonly ParsedMention[];
57
+ }
58
+ /**
59
+ * Parse `xl/persons/person.xml` into its registered authors, in document order. Order carries no
60
+ * meaning — Excel re-sorts the list by person id when it saves — so nothing may depend on it. An
61
+ * entry without an `id` is skipped: no message could reference it.
62
+ */
63
+ export declare function parsePersons(xml: string): ParsedPerson[];
64
+ /**
65
+ * Parse a `threadedComment{n}.xml` part into its messages, in document order — thread order, with
66
+ * each thread's replies following its head. Grouping into threads is {@link buildCommentThreads}'s
67
+ * job; this stays faithful to the part. A message without a `ref` or `id` cannot be anchored or
68
+ * replied to and is skipped.
69
+ */
70
+ export declare function parseThreadedComments(xml: string): ParsedThreadedComment[];
71
+ /**
72
+ * Group a part's messages into the model's threads, resolving every author and mention against the
73
+ * workbook's person registry (`personById`, typically `Workbook.getPerson`).
74
+ *
75
+ * Document order is thread order with each thread's replies following its head, so one pass suffices: a
76
+ * message with no `parentId` opens a thread, and a reply joins the thread its `parentId` names. A reply
77
+ * whose parent is unknown — a dangling `parentId` no Excel file produces — opens a thread of its own
78
+ * rather than being dropped, so a foreign generator's damage costs structure, never content.
79
+ */
80
+ export declare function buildCommentThreads(messages: readonly ParsedThreadedComment[], personById: (id: string) => Person | undefined): CommentThread[];
81
+ /**
82
+ * Serialise one sheet's conversations into its `xl/threadedComments/threadedComment{n}.xml` part.
83
+ *
84
+ * Messages are written flat, in thread order with each thread's replies after its head — the shape
85
+ * {@link parseThreadedComments} reads back. The head/reply distinction the model holds as array position
86
+ * becomes `parentId` on every reply but the head, and `done="1"` goes on the head alone: only the head
87
+ * carries the flag on the wire, so a reply can never contradict the thread it belongs to. An open thread
88
+ * omits `done` entirely rather than writing `done="0"`, exactly as Excel does.
89
+ *
90
+ * A thread with no messages writes nothing: it has neither text to say nor a head id for its replies and
91
+ * its legacy fallback to hang off.
92
+ */
93
+ export declare function threadedCommentsXml(threads: readonly CommentThread[]): string;
94
+ /**
95
+ * Serialise the workbook's identity registry into `xl/persons/person.xml` — singular and unnumbered,
96
+ * unlike the per-sheet thread parts.
97
+ *
98
+ * Entries are written in registry order, which carries no meaning: Excel re-sorts the list by person id
99
+ * whenever it saves, so this only has to be deterministic, not canonical. `userId`/`providerId` are
100
+ * written when the model holds them; a registry read from a file holds whatever that file stated.
101
+ */
102
+ export declare function personsXml(persons: readonly Person[]): string;
@@ -0,0 +1,308 @@
1
+ // Modern threaded comments — the review-style conversations Excel has written since 2018, read from and
2
+ // written to their two parts: `xl/threadedComments/threadedComment{n}.xml` (per sheet) and
3
+ // `xl/persons/person.xml` (per workbook, the author registry).
4
+ //
5
+ // These are a Microsoft extension, not base ECMA-376, and they are a *separate* feature from legacy
6
+ // notes (`comments{n}.xml`, see `comments.ts`) rather than a newer spelling of them. A cell carries
7
+ // one or the other, never both — Excel refuses to add a note to a threaded cell and vice versa.
8
+ //
9
+ // A message identifies its author by `personId` into the registry and its thread by `id`/`parentId`:
10
+ // the first message of a thread has no `parentId`, every reply carries the head's `id`. Only the head
11
+ // carries `done`, so a thread's resolved state is its head's — a reply never says.
12
+ //
13
+ // These parsers describe a file that already exists, so they read leniently: an unrecognised or
14
+ // missing attribute yields a sensible default rather than a throw, and a message too incomplete to
15
+ // anchor is skipped instead of crashing the read. Optional wire attributes stay optional in the
16
+ // parsed shape rather than collapsing to `''`, so "the file did not say" never masquerades as a value
17
+ // the file contained.
18
+ //
19
+ // {@link buildCommentThreads} turns the flat message list into the model's threads, resolving each
20
+ // author and mention against the workbook's person registry. That grouping is deliberately not the
21
+ // parsers' job: they stay faithful to the part, one function per wire form.
22
+ //
23
+ // The writers are the exact inverse and carry no clock and no id generator: every guid and every
24
+ // timestamp is written from the model, verbatim, so the same workbook always serialises to the same
25
+ // bytes. A conversation Excel wrote round-trips as itself; one authored in the model carries whatever
26
+ // ids and dates the caller supplied.
27
+ import { decodeAddress } from "../../core/address.js";
28
+ import { MENTION_OFFSET_MAX, } from "../../core/comment-thread.js";
29
+ import { escapeAttr, escapeText, XML_DECLARATION } from "../../xml/xml.js";
30
+ import { boolStrict, localName, parseXml } from "../../xml/xml-read.js";
31
+ import { THREADED_COMMENTS_NS } from "./namespaces.js";
32
+ /**
33
+ * Parse `xl/persons/person.xml` into its registered authors, in document order. Order carries no
34
+ * meaning — Excel re-sorts the list by person id when it saves — so nothing may depend on it. An
35
+ * entry without an `id` is skipped: no message could reference it.
36
+ */
37
+ export function parsePersons(xml) {
38
+ const persons = [];
39
+ parseXml(xml, {
40
+ onOpen(name, attrs) {
41
+ // `personList` shares the prefix, so match the exact local name rather than a `startsWith`.
42
+ if (localName(name) !== 'person')
43
+ return;
44
+ const { id, displayName, userId, providerId } = attrs;
45
+ if (id === undefined)
46
+ return;
47
+ persons.push({
48
+ id,
49
+ displayName: displayName ?? '',
50
+ ...(userId !== undefined ? { userId } : {}),
51
+ ...(providerId !== undefined ? { providerId } : {}),
52
+ });
53
+ },
54
+ });
55
+ return persons;
56
+ }
57
+ // Committing a message from its close handling alone would silently drop `<threadedComment …/>`,
58
+ // which fires no close of its own; presenting the self-closing form as an empty element gives it one.
59
+ const THREADED_COMMENT_EMPTY_CLOSES = new Set(['threadedComment']);
60
+ /**
61
+ * Parse a `threadedComment{n}.xml` part into its messages, in document order — thread order, with
62
+ * each thread's replies following its head. Grouping into threads is {@link buildCommentThreads}'s
63
+ * job; this stays faithful to the part. A message without a `ref` or `id` cannot be anchored or
64
+ * replied to and is skipped.
65
+ */
66
+ export function parseThreadedComments(xml) {
67
+ const messages = [];
68
+ let open;
69
+ let mentions = [];
70
+ let inText = false;
71
+ let text = '';
72
+ parseXml(xml, {
73
+ onOpen(name, attrs) {
74
+ const local = localName(name);
75
+ if (local === 'threadedComment') {
76
+ open = attrs;
77
+ text = '';
78
+ mentions = [];
79
+ }
80
+ else if (local === 'text' && open !== undefined) {
81
+ inText = true;
82
+ }
83
+ else if (local === 'mention' && open !== undefined) {
84
+ const mention = mentionFrom(attrs);
85
+ if (mention !== undefined)
86
+ mentions.push(mention);
87
+ }
88
+ },
89
+ onText(chunk) {
90
+ if (inText)
91
+ text += chunk;
92
+ },
93
+ onClose(name) {
94
+ const local = localName(name);
95
+ if (local === 'text') {
96
+ inText = false;
97
+ }
98
+ else if (local === 'threadedComment') {
99
+ if (open !== undefined) {
100
+ const message = threadedCommentFrom(open, text, mentions);
101
+ if (message !== undefined)
102
+ messages.push(message);
103
+ }
104
+ open = undefined;
105
+ text = '';
106
+ mentions = [];
107
+ }
108
+ },
109
+ }, { closeEmptyElements: THREADED_COMMENT_EMPTY_CLOSES });
110
+ return messages;
111
+ }
112
+ function threadedCommentFrom(attrs, text, mentions) {
113
+ const { ref, id, personId, dT, parentId } = attrs;
114
+ if (ref === undefined || id === undefined)
115
+ return undefined;
116
+ return {
117
+ ref,
118
+ id,
119
+ text,
120
+ done: boolStrict(attrs.done),
121
+ mentions: [...mentions],
122
+ ...(personId !== undefined ? { personId } : {}),
123
+ ...(dT !== undefined ? { date: dT } : {}),
124
+ ...(parentId !== undefined ? { parentId } : {}),
125
+ };
126
+ }
127
+ // A mention without a target person or a usable span cannot be resolved or rendered, so it is dropped
128
+ // rather than carried as a mention over nothing — a `length` of 0 would be an invisible chip, and a
129
+ // negative or non-numeric offset would place it outside the text it is supposed to cover.
130
+ //
131
+ // The upper bound is what makes this a hostile-input guard rather than a tidiness check: an offset the
132
+ // wire cannot express would be re-emitted by the writer as an invalid attribute — and JavaScript spells a
133
+ // large enough number in exponent form (`1e+21`), which no schema accepts at all — costing the reader's
134
+ // leniency the entire conversation when Excel repairs the part. Dropping the chip costs a highlight.
135
+ function mentionFrom(attrs) {
136
+ const personId = attrs.mentionpersonId;
137
+ const startIndex = integerAttribute(attrs.startIndex);
138
+ const length = integerAttribute(attrs.length);
139
+ if (personId === undefined || startIndex === undefined || length === undefined)
140
+ return undefined;
141
+ // A negative offset points outside the text and a zero length spans nothing, so neither could render.
142
+ if (startIndex < 0 || length <= 0)
143
+ return undefined;
144
+ if (startIndex > MENTION_OFFSET_MAX || length > MENTION_OFFSET_MAX)
145
+ return undefined;
146
+ return {
147
+ personId,
148
+ startIndex,
149
+ length,
150
+ ...(attrs.mentionId !== undefined ? { mentionId: attrs.mentionId } : {}),
151
+ };
152
+ }
153
+ // An integer attribute, or undefined when the file did not write a usable one. Blank is rejected before
154
+ // `Number` sees it, since `Number('')` is 0 — an empty attribute must not read as offset zero.
155
+ function integerAttribute(raw) {
156
+ if (raw === undefined || raw.trim() === '')
157
+ return undefined;
158
+ const value = Number(raw);
159
+ return Number.isInteger(value) ? value : undefined;
160
+ }
161
+ /**
162
+ * Group a part's messages into the model's threads, resolving every author and mention against the
163
+ * workbook's person registry (`personById`, typically `Workbook.getPerson`).
164
+ *
165
+ * Document order is thread order with each thread's replies following its head, so one pass suffices: a
166
+ * message with no `parentId` opens a thread, and a reply joins the thread its `parentId` names. A reply
167
+ * whose parent is unknown — a dangling `parentId` no Excel file produces — opens a thread of its own
168
+ * rather than being dropped, so a foreign generator's damage costs structure, never content.
169
+ */
170
+ export function buildCommentThreads(messages, personById) {
171
+ const threads = [];
172
+ // The head's own message list, so a reply appends to the thread already published in `threads`.
173
+ const commentsByHeadId = new Map();
174
+ for (const message of messages) {
175
+ const comment = commentFrom(message, personById);
176
+ const siblings = message.parentId === undefined ? undefined : commentsByHeadId.get(message.parentId);
177
+ if (siblings !== undefined) {
178
+ siblings.push(comment);
179
+ continue;
180
+ }
181
+ const ref = anchorRef(message.ref);
182
+ if (ref === undefined)
183
+ continue;
184
+ const comments = [comment];
185
+ commentsByHeadId.set(message.id, comments);
186
+ // Resolved is the head's flag: a reply never carries `done`, so it never contradicts its thread.
187
+ threads.push({ ref, resolved: message.done, comments });
188
+ }
189
+ return threads;
190
+ }
191
+ // The canonical A1 form of a thread's anchor, or undefined when the file wrote something that cannot
192
+ // anchor one — a range, a bare row or column, or outright garbage. Canonicalising here is what lets
193
+ // every later consumer compare anchors as plain strings (`$B$2` and `B2` are one cell) and keeps a
194
+ // foreign file's malformed reference out of the writer, which anchors the thread's legacy fallback by it.
195
+ function anchorRef(reference) {
196
+ let decoded;
197
+ try {
198
+ decoded = decodeAddress(reference);
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ return decoded.col === undefined || decoded.row === undefined ? undefined : decoded.address;
204
+ }
205
+ function commentFrom(message, personById) {
206
+ const author = message.personId === undefined ? undefined : personById(message.personId);
207
+ return {
208
+ id: message.id,
209
+ text: message.text,
210
+ mentions: message.mentions.map((mention) => mentionOf(mention, personById)),
211
+ ...(author !== undefined ? { author } : {}),
212
+ ...(message.personId !== undefined ? { personId: message.personId } : {}),
213
+ ...(message.date !== undefined ? { date: message.date } : {}),
214
+ };
215
+ }
216
+ function mentionOf(mention, personById) {
217
+ const person = personById(mention.personId);
218
+ return {
219
+ personId: mention.personId,
220
+ startIndex: mention.startIndex,
221
+ length: mention.length,
222
+ ...(person !== undefined ? { person } : {}),
223
+ ...(mention.mentionId !== undefined ? { mentionId: mention.mentionId } : {}),
224
+ };
225
+ }
226
+ /**
227
+ * Serialise one sheet's conversations into its `xl/threadedComments/threadedComment{n}.xml` part.
228
+ *
229
+ * Messages are written flat, in thread order with each thread's replies after its head — the shape
230
+ * {@link parseThreadedComments} reads back. The head/reply distinction the model holds as array position
231
+ * becomes `parentId` on every reply but the head, and `done="1"` goes on the head alone: only the head
232
+ * carries the flag on the wire, so a reply can never contradict the thread it belongs to. An open thread
233
+ * omits `done` entirely rather than writing `done="0"`, exactly as Excel does.
234
+ *
235
+ * A thread with no messages writes nothing: it has neither text to say nor a head id for its replies and
236
+ * its legacy fallback to hang off.
237
+ */
238
+ export function threadedCommentsXml(threads) {
239
+ const messages = threads.flatMap((thread) => {
240
+ const [head, ...replies] = thread.comments;
241
+ if (head === undefined)
242
+ return [];
243
+ return [
244
+ threadedCommentXml(thread.ref, head, thread.resolved ? ' done="1"' : ''),
245
+ ...replies.map((reply) => threadedCommentXml(thread.ref, reply, ` parentId="${escapeAttr(head.id)}"`)),
246
+ ];
247
+ });
248
+ return (XML_DECLARATION +
249
+ `<ThreadedComments xmlns="${THREADED_COMMENTS_NS}">${messages.join('')}</ThreadedComments>`);
250
+ }
251
+ // One `<threadedComment>`. `tail` is the attribute that distinguishes the message's role — `done` for a
252
+ // resolved head, `parentId` for a reply, nothing for an open head. A `dT` or `personId` the model never
253
+ // held is omitted rather than written empty, so "the file did not say" stays distinguishable from "the
254
+ // file said nothing". Every value is escaped: an authored message's text and a foreign file's ids alike
255
+ // are untrusted, and an unescaped `"` would end the attribute and reshape the part.
256
+ function threadedCommentXml(ref, comment, tail) {
257
+ const date = comment.date === undefined ? '' : ` dT="${escapeAttr(comment.date)}"`;
258
+ const person = comment.personId === undefined ? '' : ` personId="${escapeAttr(comment.personId)}"`;
259
+ return (`<threadedComment ref="${escapeAttr(ref)}"${date}${person} id="${escapeAttr(comment.id)}"${tail}>` +
260
+ `<text>${escapeText(comment.text)}</text>` +
261
+ mentionsXml(comment.mentions) +
262
+ '</threadedComment>');
263
+ }
264
+ // The `<mentions>` block, which follows `<text>` in the message. All four `<mention>` attributes are
265
+ // required — verified by dropping each in turn and getting `Sch_MissRequiredAttribute` — so a mention the
266
+ // model holds without a `mentionId`, or with a span the wire cannot express, cannot be written at all. It
267
+ // is dropped rather than given an invented id or a clamped span: the `@name` stays in the text and only
268
+ // the chip is lost, whereas an invalid part risks Excel repairing the whole conversation away.
269
+ //
270
+ // The bounds check is deliberately here as well as in the reader. Both the authoring verb and the parser
271
+ // already refuse an out-of-range offset, so nothing should reach this — but `restoreCommentThreads` takes
272
+ // a model wholesale, and a serialiser that *cannot* emit `length="1e+21"` beats one that merely is not
273
+ // currently handed one.
274
+ function mentionsXml(mentions) {
275
+ const entries = mentions.flatMap(({ personId, mentionId, startIndex, length }) => {
276
+ if (mentionId === undefined)
277
+ return [];
278
+ // A zero-length span is in range but renders nothing, so it is no more writable than one out of range.
279
+ if (!writableOffset(startIndex) || !writableOffset(length) || length === 0)
280
+ return [];
281
+ return [
282
+ `<mention mentionpersonId="${escapeAttr(personId)}" mentionId="${escapeAttr(mentionId)}"` +
283
+ ` startIndex="${startIndex}" length="${length}"/>`,
284
+ ];
285
+ });
286
+ return entries.length === 0 ? '' : `<mentions>${entries.join('')}</mentions>`;
287
+ }
288
+ // An offset the wire can express: a whole number within the schema's UInt32 ceiling.
289
+ function writableOffset(value) {
290
+ return Number.isInteger(value) && value >= 0 && value <= MENTION_OFFSET_MAX;
291
+ }
292
+ /**
293
+ * Serialise the workbook's identity registry into `xl/persons/person.xml` — singular and unnumbered,
294
+ * unlike the per-sheet thread parts.
295
+ *
296
+ * Entries are written in registry order, which carries no meaning: Excel re-sorts the list by person id
297
+ * whenever it saves, so this only has to be deterministic, not canonical. `userId`/`providerId` are
298
+ * written when the model holds them; a registry read from a file holds whatever that file stated.
299
+ */
300
+ export function personsXml(persons) {
301
+ const entries = persons.map((person) => {
302
+ const userId = person.userId === undefined ? '' : ` userId="${escapeAttr(person.userId)}"`;
303
+ const providerId = person.providerId === undefined ? '' : ` providerId="${escapeAttr(person.providerId)}"`;
304
+ return (`<person displayName="${escapeAttr(person.displayName)}"` +
305
+ ` id="${escapeAttr(person.id)}"${userId}${providerId}/>`);
306
+ });
307
+ return `${XML_DECLARATION}<personList xmlns="${THREADED_COMMENTS_NS}">${entries.join('')}</personList>`;
308
+ }
@@ -0,0 +1,12 @@
1
+ import type { Workbook, WorkbookProperties } from '../../core/workbook.ts';
2
+ import type { PivotPlan, PreservedPartPlan, PreservedRootReferencePlan, PreservedWorkbookReferencePlan, TablePlan } from './package-plan.ts';
3
+ export type PreservedWorkbookRel = PreservedWorkbookReferencePlan & {
4
+ readonly relId: string;
5
+ };
6
+ export declare function contentTypesXml(sheetCount: number, tables: readonly TablePlan[], commentNumbers: readonly number[], drawingNumbers: readonly number[], printerSettingsNumbers: readonly number[], mediaExtensions: readonly string[], hasSharedStrings: boolean, preservedParts: readonly PreservedPartPlan[], pivots: readonly PivotPlan[], preservedWorkbookRefs: readonly PreservedWorkbookReferencePlan[], threadedCommentNumbers: readonly number[], hasPersons: boolean): string;
7
+ export declare function rootRelsXml(rootRefs: readonly PreservedRootReferencePlan[]): string;
8
+ export declare function workbookXml(workbook: Workbook, preservedRels: readonly PreservedWorkbookRel[], pivots: readonly PivotPlan[]): string;
9
+ export declare const FIXED_WORKBOOK_REL_COUNT = 2;
10
+ export declare function workbookRelsXml(sheetCount: number, hasSharedStrings: boolean, personsRelId: string | null, preservedRels: readonly PreservedWorkbookRel[], pivots: readonly PivotPlan[]): string;
11
+ export declare function corePropsXml(properties: WorkbookProperties): string;
12
+ export declare function appPropsXml(): string;