@shbernal/ts-xlsx 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +5 -1
  16. package/dist/core/column.js +34 -35
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +9 -3
  27. package/dist/core/data-validation.js +21 -18
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +14 -10
  39. package/dist/core/image.js +9 -9
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
@@ -0,0 +1,78 @@
1
+ export interface XmlAttributes {
2
+ readonly [name: string]: string;
3
+ }
4
+ /**
5
+ * One parse event from {@link xmlEvents}. The payloads are what the push adapter in `xml-read.ts`
6
+ * hands a handler triple, unchanged: `text` is already entity-decoded (or verbatim CDATA), and a
7
+ * `<x/>` yields one `open` with `selfClosing: true` and no matching `close`. The discriminated
8
+ * `kind` lets a *pull* consumer drive the parse: the shape the streaming reader needs, where a push
9
+ * callback cannot `yield`.
10
+ */
11
+ export type XmlEvent = {
12
+ readonly kind: 'open';
13
+ readonly name: string;
14
+ readonly attrs: XmlAttributes;
15
+ readonly selfClosing: boolean;
16
+ } | {
17
+ readonly kind: 'text';
18
+ readonly text: string;
19
+ } | {
20
+ readonly kind: 'close';
21
+ readonly name: string;
22
+ };
23
+ /**
24
+ * Decode XML character references and the five predefined entities. An unrecognised
25
+ * `&name;` is left verbatim rather than expanded: there is no DTD, so there is nothing
26
+ * to expand it to, and refusing to invent one is what makes entity-expansion attacks
27
+ * impossible.
28
+ *
29
+ * A reference naming a code point XML 1.0 has no representation for is left verbatim too, on the
30
+ * same grounds as one naming no code point at all. `&#1;` is not an escape for U+0001, it is another
31
+ * way of spelling an ill-formed document, and decoding it puts in the model a character the writer
32
+ * is *guaranteed* to refuse: a hostile file would then read cleanly and fail on the next save with
33
+ * an `AuthoringError` blaming the caller, several layers from the input that caused it. The bound
34
+ * comes from `./xml-chars.ts` rather than from the writer's own guard so that this half of the
35
+ * codec keeps importing none of the serialisation vocabulary.
36
+ */
37
+ export declare function decodeEntities(value: string): string;
38
+ export declare function parseAttributes(source: string): XmlAttributes;
39
+ export type Markup = {
40
+ readonly kind: 'comment' | 'pi' | 'declaration';
41
+ readonly next: number;
42
+ } | {
43
+ readonly kind: 'cdata';
44
+ readonly contentStart: number;
45
+ readonly contentEnd: number;
46
+ readonly next: number;
47
+ };
48
+ export declare function markupAt(source: string, lt: number): Markup | undefined;
49
+ export interface Tag {
50
+ readonly close: boolean;
51
+ readonly name: string;
52
+ readonly attrSource: string;
53
+ readonly selfClosing: boolean;
54
+ /** One past the tag's `>`, so a caller capturing verbatim source can slice up to it. */
55
+ readonly next: number;
56
+ }
57
+ export declare function tagAt(source: string, lt: number): Tag;
58
+ /**
59
+ * Scan an XML document as a *pull* stream of {@link XmlEvent}s in a single O(n) pass with no
60
+ * recursion. This is the parser's core; {@link parseXml} is a thin push adapter over it. A
61
+ * consumer that must produce output incrementally (the streaming row reader) pulls events and
62
+ * yields as it goes, holding only its own running state; a push callback cannot.
63
+ *
64
+ * Throws {@link XmlParseError} on malformed markup.
65
+ */
66
+ export declare function xmlEvents(source: string): Generator<XmlEvent>;
67
+ /** Strip a namespace prefix from a qualified name (`r:id` → `id`, `sheet` → `sheet`). */
68
+ export declare function localName(qualified: string): string;
69
+ /** An OOXML boolean that is on when present with no value (`<b/>` is bold) and off only on an
70
+ * explicit `"0"`/`"false"`; absence reads as on. */
71
+ export declare function boolPresent(val: string | undefined): boolean;
72
+ /** An OOXML boolean that is on only when explicitly `"1"`/`"true"`; anything else, including
73
+ * absence and a truthy-looking `"0"`, is off. */
74
+ export declare function boolStrict(val: string | undefined): boolean;
75
+ /** An optional OOXML boolean: `undefined` when the attribute is absent or carries an unrecognised
76
+ * token, otherwise its `"1"`/`"true"` vs `"0"`/`"false"` value. Lets a caller store only the
77
+ * attributes the source actually carried, so a re-write stays byte-clean. */
78
+ export declare function boolTristate(val: string | undefined): boolean | undefined;
@@ -0,0 +1,180 @@
1
+ import { XmlParseError } from './errors.js';
2
+ import { isRepresentableCodePoint, stripUnrepresentable } from './xml-chars.js';
3
+ const PREDEFINED_ENTITIES = new Map([
4
+ ['amp', '&'],
5
+ ['lt', '<'],
6
+ ['gt', '>'],
7
+ ['quot', '"'],
8
+ ['apos', "'"],
9
+ ]);
10
+ const ENTITY = /&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g;
11
+ export function decodeEntities(value) {
12
+ if (!value.includes('&'))
13
+ return value;
14
+ return value.replace(ENTITY, (match, body) => {
15
+ if (body.charCodeAt(0) === 0x23) {
16
+ const codePoint = body.charCodeAt(1) === 0x78
17
+ ? Number.parseInt(body.slice(2), 16)
18
+ : Number.parseInt(body.slice(1), 10);
19
+ if (!isRepresentableCodePoint(codePoint))
20
+ return match;
21
+ return String.fromCodePoint(codePoint);
22
+ }
23
+ return PREDEFINED_ENTITIES.get(body) ?? match;
24
+ });
25
+ }
26
+ function admitText(value) {
27
+ return stripUnrepresentable(decodeEntities(value));
28
+ }
29
+ const ATTRIBUTE = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
30
+ export function parseAttributes(source) {
31
+ const attrs = Object.create(null);
32
+ ATTRIBUTE.lastIndex = 0;
33
+ let match = ATTRIBUTE.exec(source);
34
+ while (match !== null) {
35
+ const value = match[2] ?? match[3] ?? '';
36
+ attrs[match[1]] = admitText(value);
37
+ match = ATTRIBUTE.exec(source);
38
+ }
39
+ return attrs;
40
+ }
41
+ function findTagEnd(source, start) {
42
+ let quote = '';
43
+ for (let i = start + 1; i < source.length; i++) {
44
+ const ch = source[i];
45
+ if (quote !== '') {
46
+ if (ch === quote)
47
+ quote = '';
48
+ }
49
+ else if (ch === '"' || ch === "'") {
50
+ quote = ch;
51
+ }
52
+ else if (ch === '>') {
53
+ return i;
54
+ }
55
+ }
56
+ throw new XmlParseError('unterminated tag: missing ">"');
57
+ }
58
+ function skipDeclaration(source, start) {
59
+ let depth = 0;
60
+ for (let i = start; i < source.length; i++) {
61
+ const ch = source[i];
62
+ if (ch === '[')
63
+ depth++;
64
+ else if (ch === ']')
65
+ depth--;
66
+ else if (ch === '>' && depth <= 0)
67
+ return i + 1;
68
+ }
69
+ throw new XmlParseError('unterminated markup declaration: missing ">"');
70
+ }
71
+ export function markupAt(source, lt) {
72
+ if (source.startsWith('<!--', lt)) {
73
+ const end = source.indexOf('-->', lt + 4);
74
+ if (end === -1)
75
+ throw new XmlParseError('unterminated comment');
76
+ return { kind: 'comment', next: end + 3 };
77
+ }
78
+ if (source.startsWith('<![CDATA[', lt)) {
79
+ const end = source.indexOf(']]>', lt + 9);
80
+ if (end === -1)
81
+ throw new XmlParseError('unterminated CDATA section');
82
+ return { kind: 'cdata', contentStart: lt + 9, contentEnd: end, next: end + 3 };
83
+ }
84
+ if (source.startsWith('<?', lt)) {
85
+ const end = source.indexOf('?>', lt + 2);
86
+ if (end === -1)
87
+ throw new XmlParseError('unterminated processing instruction');
88
+ return { kind: 'pi', next: end + 2 };
89
+ }
90
+ if (source.startsWith('<!', lt))
91
+ return { kind: 'declaration', next: skipDeclaration(source, lt) };
92
+ return undefined;
93
+ }
94
+ export function tagAt(source, lt) {
95
+ const gt = findTagEnd(source, lt);
96
+ const raw = source.slice(lt + 1, gt);
97
+ const next = gt + 1;
98
+ if (raw.charCodeAt(0) === 0x2f) {
99
+ return { close: true, name: raw.slice(1).trim(), attrSource: '', selfClosing: false, next };
100
+ }
101
+ const selfClosing = raw.charCodeAt(raw.length - 1) === 0x2f;
102
+ const body = selfClosing ? raw.slice(0, -1) : raw;
103
+ const nameEnd = firstWhitespace(body);
104
+ return {
105
+ close: false,
106
+ name: nameEnd === -1 ? body : body.slice(0, nameEnd),
107
+ attrSource: nameEnd === -1 ? '' : body.slice(nameEnd),
108
+ selfClosing,
109
+ next,
110
+ };
111
+ }
112
+ export function* xmlEvents(source) {
113
+ const length = source.length;
114
+ let i = 0;
115
+ while (i < length) {
116
+ const lt = source.indexOf('<', i);
117
+ if (lt === -1) {
118
+ const chunk = source.slice(i);
119
+ if (chunk.length > 0)
120
+ yield { kind: 'text', text: admitText(normalizeLineEndings(chunk)) };
121
+ return;
122
+ }
123
+ if (lt > i) {
124
+ const chunk = source.slice(i, lt);
125
+ if (chunk.length > 0)
126
+ yield { kind: 'text', text: admitText(normalizeLineEndings(chunk)) };
127
+ }
128
+ const markup = markupAt(source, lt);
129
+ if (markup !== undefined) {
130
+ if (markup.kind === 'cdata') {
131
+ yield {
132
+ kind: 'text',
133
+ text: stripUnrepresentable(source.slice(markup.contentStart, markup.contentEnd)),
134
+ };
135
+ }
136
+ i = markup.next;
137
+ continue;
138
+ }
139
+ const tag = tagAt(source, lt);
140
+ yield tag.close
141
+ ? { kind: 'close', name: tag.name }
142
+ : {
143
+ kind: 'open',
144
+ name: tag.name,
145
+ attrs: parseAttributes(tag.attrSource),
146
+ selfClosing: tag.selfClosing,
147
+ };
148
+ i = tag.next;
149
+ }
150
+ }
151
+ function normalizeLineEndings(chunk) {
152
+ if (!chunk.includes('\r'))
153
+ return chunk;
154
+ return chunk.replace(/\r\n?/g, '\n');
155
+ }
156
+ function firstWhitespace(source) {
157
+ for (let i = 0; i < source.length; i++) {
158
+ const code = source.charCodeAt(i);
159
+ if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d)
160
+ return i;
161
+ }
162
+ return -1;
163
+ }
164
+ export function localName(qualified) {
165
+ const colon = qualified.indexOf(':');
166
+ return colon === -1 ? qualified : qualified.slice(colon + 1);
167
+ }
168
+ export function boolPresent(val) {
169
+ return val === undefined || (val !== '0' && val !== 'false');
170
+ }
171
+ export function boolStrict(val) {
172
+ return val === '1' || val === 'true';
173
+ }
174
+ export function boolTristate(val) {
175
+ if (val === '1' || val === 'true')
176
+ return true;
177
+ if (val === '0' || val === 'false')
178
+ return false;
179
+ return undefined;
180
+ }
package/dist/xml/xml.d.ts CHANGED
@@ -1,18 +1,24 @@
1
- /**
2
- * Refuse a string that XML cannot carry, naming the character and where it is so the author
3
- * can find it in a value they never inspected. These arrive from a database column or a CSV
4
- * field, not from a literal in the calling code.
5
- *
6
- * Exported for the one escape that cannot be {@link escapeAttr}: a number format code escapes
7
- * everything but the apostrophe, and needs this guard just the same.
8
- *
9
- * @throws {AuthoringError} naming the code point and its offset.
10
- */
11
- export declare function assertRepresentable(value: string): void;
12
1
  /** Escape a string for use as XML element text. */
13
2
  export declare function escapeText(value: string): string;
14
3
  /** Escape a string for use inside a double-quoted XML attribute value. */
15
4
  export declare function escapeAttr(value: string): string;
5
+ /**
6
+ * {@link escapeAttr} for a number format code: the same escapes minus the apostrophe.
7
+ *
8
+ * A format code can legitimately contain `"` (a quoted literal like `"$"`), `<` and `&`, and Excel
9
+ * writes one with bare apostrophes. `'` is not markup-significant inside a double-quoted attribute,
10
+ * so leaving it keeps a round-tripped code byte-identical to the source; that divergence is the whole
11
+ * reason this exists.
12
+ *
13
+ * It is built by *omitting* one entry from {@link ATTR_ESCAPES} rather than by listing the escapes it
14
+ * does want, which is how it used to be written, in `io/xlsx/style-elements.ts`. An independently
15
+ * maintained list does not stay a one-character divergence: that one silently dropped the tab, line
16
+ * feed and carriage return escapes as well, and XML 1.0 3.3.3 requires a parser to normalise all
17
+ * three in an attribute value to a space, so Excel read back a format code with a space where the
18
+ * author wrote a tab. Our own reader preserved the raw character, so a round trip through this
19
+ * library returned it unchanged and no test here could see the loss.
20
+ */
21
+ export declare function escapeFormatCode(code: string): string;
16
22
  /**
17
23
  * Escape a cell value: the `_xHHHH_` convention first, then XML's own escapes.
18
24
  *
@@ -25,7 +31,7 @@ export declare function escapeAttr(value: string): string;
25
31
  * `escapeText` runs last and sees no unrepresentable character left, so its guard is a
26
32
  * standing proof that the escape was complete rather than a second check of the same thing.
27
33
  *
28
- * Reversed by `decodeSpreadsheetText` in `./xml-read.ts`, whose single left-to-right pass is what
34
+ * Reversed by `decodeSpreadsheetText` in `./xml-scan.ts`, whose single left-to-right pass is what
29
35
  * makes the `_x005F_` step above reversible. Change either and read its comment first.
30
36
  */
31
37
  export declare function escapeSpreadsheetText(value: string): string;
@@ -36,13 +42,6 @@ export declare function escapeSpreadsheetText(value: string): string;
36
42
  * identically on the way back.
37
43
  */
38
44
  export declare function textElement(value: string): string;
39
- /**
40
- * Render a formula operand for serialisation: a number becomes its literal, a string is stripped of
41
- * the single optional leading '=' an author may write (OOXML stores the expression without it, e.g.
42
- * `=A1>0` on disk is `A1>0`). The result is unescaped: the caller escapes it for its target,
43
- * whether that is element text or an attribute value.
44
- */
45
- export declare function stripFormulaEquals(value: string | number): string;
46
45
  /**
47
46
  * A boolean attribute rendered with a leading space (` name="1"` / ` name="0"`), or '' when the value
48
47
  * is undefined. OOXML booleans serialise as 1/0; emitting the explicit `="0"` lets a writer force a
@@ -85,18 +84,20 @@ export declare function checkedToken(value: string, isValid: (candidate: string)
85
84
  */
86
85
  export declare function numAttr(name: string, value: number | undefined): string;
87
86
  /**
88
- * Refuse a number OOXML cannot spell. Every numeric attribute in the format is `xsd:double`,
89
- * `xsd:unsignedInt` or a bounded flavour of one, and none of those lexical spaces has a form for a
90
- * NaN or an infinity, so a value that reaches the file as `NaN` is a package Excel reports as
91
- * damaged.
87
+ * Refuse a `Date` OOXML cannot spell, naming the property that carries it.
92
88
  *
93
- * Exported for the callers that must refuse before they write. A number can be unwritable and still
94
- * be read on the way to the bytes: compared, summed, walked. A comparison against `NaN` or an
95
- * infinity silently takes the wrong branch long before the value would have been serialised.
89
+ * Two ways a `Date` fails to have a `dcterms:W3CDTF` / `xsd:dateTime` spelling, and neither is
90
+ * caught by the types. An **Invalid Date** is truthy and is an instance of `Date`, so it passes
91
+ * every guard short of this one and reaches `toISOString()`, which throws a bare
92
+ * `RangeError: Invalid time value` -- outside this taxonomy, and naming neither the property nor
93
+ * the document it was being written into. A year **outside 0000-9999** has no four-digit form, so
94
+ * `toISOString()` falls back to ISO 8601's expanded `+275760-09-13T…` notation, which is not in
95
+ * the lexical space of either type; that one does not throw at all, it writes a package Excel
96
+ * offers to repair.
96
97
  *
97
- * @throws {AuthoringError} naming the value.
98
+ * @throws {AuthoringError} naming the property.
98
99
  */
99
- export declare function assertWritableNumber(value: number): void;
100
+ export declare function assertWritableDate(date: Date, property: string): void;
100
101
  /**
101
102
  * A finite number serialises as its shortest round-trippable decimal; a non-finite one
102
103
  * has no OOXML numeric representation, so the writer refuses it rather than emit `NaN`.
package/dist/xml/xml.js CHANGED
@@ -1,4 +1,5 @@
1
- import { AuthoringError } from '../errors.js';
1
+ import { assertWritableNumber, AuthoringError, codePointHex, invalidToken, unrepresentable, } from '../errors.js';
2
+ import { XML_UNREPRESENTABLE, XML_UNREPRESENTABLE_GLOBAL } from './xml-chars.js';
2
3
  const TEXT_ESCAPES = {
3
4
  '&': '&amp;',
4
5
  '<': '&lt;',
@@ -14,18 +15,12 @@ const ATTR_ESCAPES = {
14
15
  '\r': '&#13;',
15
16
  '\t': '&#9;',
16
17
  };
17
- const UNREPRESENTABLE = /[\u{0}-\u{8}\u{B}\u{C}\u{E}-\u{1F}\u{FFFE}\u{FFFF}\u{D800}-\u{DFFF}]/u;
18
- const UNREPRESENTABLE_GLOBAL = new RegExp(UNREPRESENTABLE.source, 'gu');
19
- function codePointHex(codePoint) {
20
- return codePoint.toString(16).toUpperCase().padStart(4, '0');
21
- }
22
- export function assertRepresentable(value) {
23
- const found = UNREPRESENTABLE.exec(value);
24
- if (found === null)
25
- return;
26
- throw new AuthoringError(`cannot write U+${codePointHex(value.codePointAt(found.index))} at offset ${found.index}: ` +
27
- 'XML 1.0 has no representation for it, and the _xHHHH_ escape that would carry it is a ' +
28
- 'convention of cell values only');
18
+ const XML_REFUSAL = 'XML 1.0 has no representation for it, and the _xHHHH_ escape that would carry it is a ' +
19
+ 'convention of cell values only';
20
+ function assertRepresentable(value) {
21
+ const error = unrepresentable(value, XML_UNREPRESENTABLE, XML_REFUSAL);
22
+ if (error !== undefined)
23
+ throw error;
29
24
  }
30
25
  export function escapeText(value) {
31
26
  assertRepresentable(value);
@@ -35,10 +30,14 @@ export function escapeAttr(value) {
35
30
  assertRepresentable(value);
36
31
  return value.replace(/[&<>"'\n\r\t]/g, (ch) => ATTR_ESCAPES[ch]);
37
32
  }
33
+ export function escapeFormatCode(code) {
34
+ assertRepresentable(code);
35
+ return code.replace(/[&<>"\n\r\t]/g, (ch) => ATTR_ESCAPES[ch]);
36
+ }
38
37
  export function escapeSpreadsheetText(value) {
39
38
  return escapeText(value
40
39
  .replace(/_(x[0-9A-Fa-f]{4}_)/g, '_x005F_$1')
41
- .replace(UNREPRESENTABLE_GLOBAL, (ch) => `_x${codePointHex(ch.codePointAt(0))}_`));
40
+ .replace(XML_UNREPRESENTABLE_GLOBAL, (ch) => `_x${codePointHex(ch.codePointAt(0))}_`));
42
41
  }
43
42
  function needsSpacePreserve(value) {
44
43
  return value.length > 0 && (value !== value.trim() || /[\n\r\t]/.test(value));
@@ -47,11 +46,6 @@ export function textElement(value) {
47
46
  const space = needsSpacePreserve(value) ? ' xml:space="preserve"' : '';
48
47
  return `<t${space}>${escapeSpreadsheetText(value)}</t>`;
49
48
  }
50
- export function stripFormulaEquals(value) {
51
- if (typeof value === 'number')
52
- return String(value);
53
- return value.startsWith('=') ? value.slice(1) : value;
54
- }
55
49
  export function boolAttr(name, value) {
56
50
  return value === undefined ? '' : ` ${name}="${value ? 1 : 0}"`;
57
51
  }
@@ -60,17 +54,23 @@ export function textAttr(name, value) {
60
54
  }
61
55
  export function checkedToken(value, isValid, kind) {
62
56
  if (!isValid(value)) {
63
- throw new AuthoringError(`Invalid ${kind} ${JSON.stringify(value)}: not a value the OOXML enumeration allows`);
57
+ throw invalidToken(kind, value);
64
58
  }
65
59
  return value;
66
60
  }
67
61
  export function numAttr(name, value) {
68
62
  return value === undefined ? '' : ` ${name}="${numberText(value)}"`;
69
63
  }
70
- export function assertWritableNumber(value) {
71
- if (Number.isFinite(value))
72
- return;
73
- throw new AuthoringError(`cannot write a non-finite number (${value}): it has no OOXML representation`);
64
+ export function assertWritableDate(date, property) {
65
+ const time = date.getTime();
66
+ if (Number.isNaN(time)) {
67
+ throw new AuthoringError(`cannot write ${property}: it is an Invalid Date`);
68
+ }
69
+ const year = date.getUTCFullYear();
70
+ if (year < 0 || year > 9999) {
71
+ throw new AuthoringError(`cannot write ${property}: the year ${year} is outside the 0000-9999 range a W3CDTF ` +
72
+ 'timestamp can spell');
73
+ }
74
74
  }
75
75
  export function numberText(value) {
76
76
  assertWritableNumber(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shbernal/ts-xlsx",
3
- "version": "2.1.0",
3
+ "version": "3.1.0",
4
4
  "description": "A TypeScript-first library for reading and writing xlsx (OOXML) spreadsheets.",
5
5
  "keywords": [
6
6
  "csv",
@@ -77,8 +77,7 @@
77
77
  },
78
78
  "scripts": {
79
79
  "prepare": "node scripts/install-hooks.ts",
80
- "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
81
- "build": "pnpm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.build.dts.json",
80
+ "build": "node scripts/build.ts",
82
81
  "size": "node scripts/size-budget.ts",
83
82
  "smoke:dist": "node scripts/smoke-dist.ts",
84
83
  "site:prepare": "node www/scripts/sync-docs.ts",
@@ -92,10 +91,14 @@
92
91
  "constitution:check": "node scripts/check-constitution.ts",
93
92
  "layering:check": "node scripts/check-layering.ts",
94
93
  "entries:check": "node scripts/check-entries.ts",
94
+ "public-types:check": "node scripts/check-public-types.ts",
95
+ "facet-register:check": "node scripts/check-facet-register.ts",
96
+ "tsconfig-coverage:check": "node scripts/check-tsconfig-coverage.ts",
95
97
  "browser:check": "node scripts/check-browser-safe.ts",
96
98
  "source-text:check": "node scripts/check-source-text.ts",
99
+ "error-messages:check": "node scripts/check-error-messages.ts",
97
100
  "chars:check": "charcheck",
98
- "lint": "node node_modules/oxlint/bin/oxlint src scripts test tools www charcheck.config.ts --type-aware --deny-warnings --report-unused-disable-directives",
101
+ "lint": "node scripts/lint.ts",
99
102
  "lint:fix": "pnpm run lint --fix",
100
103
  "format": "node scripts/format.ts --write",
101
104
  "format:check": "node scripts/format.ts --check",
@@ -104,7 +107,8 @@
104
107
  "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
105
108
  "typecheck:dist": "tsc --noEmit -p tsconfig.dist.json",
106
109
  "typecheck:site": "tsc --noEmit -p www/tsconfig.json",
107
- "test:src": "node --test \"src/**/*.test.ts\"",
110
+ "test:src": "node scripts/test-src.ts",
111
+ "test:harness": "node --test \"scripts/**/*.test.ts\"",
108
112
  "test:site": "node --test \"www/**/*.test.ts\"",
109
113
  "coverage": "node scripts/coverage.ts",
110
114
  "corpus": "node test/corpus/run.ts",
@@ -121,17 +125,17 @@
121
125
  },
122
126
  "devDependencies": {
123
127
  "@types/node": "^24.13.3",
124
- "charcheck": "^0.3.0",
128
+ "charcheck": "^0.4.0",
125
129
  "jszip": "^3.10.1",
126
- "lefthook": "^2.1.10",
130
+ "lefthook": "^2.1.12",
127
131
  "ooxml-validate": "^0.0.3",
128
- "oxfmt": "^0.64.0",
132
+ "oxfmt": "^0.65.0",
129
133
  "oxlint": "^1.80.0",
130
134
  "oxlint-tsgolint": "7.0.2001",
131
135
  "shiki": "^4.4.3",
132
136
  "typescript": "^7.0.2",
133
137
  "vitepress": "^1.6.4",
134
- "vue": "^3.5.41"
138
+ "vue": "^3.5.42"
135
139
  },
136
140
  "engines": {
137
141
  "node": ">=24"