@shbernal/ts-xlsx 1.3.1 → 2.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 (186) hide show
  1. package/README.md +20 -10
  2. package/dist/bytes.d.ts +24 -0
  3. package/dist/bytes.js +34 -0
  4. package/dist/core/address.d.ts +72 -3
  5. package/dist/core/address.js +50 -10
  6. package/dist/core/autofilter.d.ts +13 -2
  7. package/dist/core/autofilter.js +35 -10
  8. package/dist/core/cell.d.ts +17 -16
  9. package/dist/core/cell.js +3 -7
  10. package/dist/core/color-resolution.d.ts +8 -8
  11. package/dist/core/color-resolution.js +7 -8
  12. package/dist/core/column.d.ts +8 -9
  13. package/dist/core/column.js +2 -4
  14. package/dist/core/comment-thread.d.ts +32 -23
  15. package/dist/core/comment-thread.js +4 -4
  16. package/dist/core/conditional-formatting-overlay.d.ts +6 -1
  17. package/dist/core/conditional-formatting-overlay.js +11 -0
  18. package/dist/core/conditional-formatting.d.ts +48 -8
  19. package/dist/core/conditional-formatting.js +88 -0
  20. package/dist/core/data-validation-overlay.d.ts +12 -1
  21. package/dist/core/data-validation-overlay.js +21 -2
  22. package/dist/core/data-validation.d.ts +12 -5
  23. package/dist/core/data-validation.js +34 -0
  24. package/dist/core/date.d.ts +3 -3
  25. package/dist/core/formula.d.ts +6 -6
  26. package/dist/core/grid-edits.d.ts +16 -0
  27. package/dist/core/grid-edits.js +29 -11
  28. package/dist/core/grid-shift.d.ts +19 -0
  29. package/dist/core/grid-shift.js +8 -0
  30. package/dist/core/image.d.ts +39 -2
  31. package/dist/core/image.js +10 -0
  32. package/dist/core/internal.d.ts +9 -1
  33. package/dist/core/limits.d.ts +4 -4
  34. package/dist/core/merge.d.ts +17 -12
  35. package/dist/core/merge.js +36 -5
  36. package/dist/core/page-setup.d.ts +20 -6
  37. package/dist/core/page-setup.js +8 -1
  38. package/dist/core/pivot-table.d.ts +4 -4
  39. package/dist/core/pivot-table.js +29 -20
  40. package/dist/core/preserved.d.ts +3 -3
  41. package/dist/core/protection.d.ts +7 -4
  42. package/dist/core/protection.js +20 -14
  43. package/dist/core/range.d.ts +16 -14
  44. package/dist/core/range.js +5 -5
  45. package/dist/core/row-input.d.ts +9 -8
  46. package/dist/core/row-input.js +12 -10
  47. package/dist/core/row.d.ts +7 -8
  48. package/dist/core/row.js +2 -4
  49. package/dist/core/style.d.ts +41 -14
  50. package/dist/core/style.js +82 -67
  51. package/dist/core/table-style.d.ts +9 -9
  52. package/dist/core/table-style.js +1 -1
  53. package/dist/core/table.d.ts +53 -51
  54. package/dist/core/table.js +83 -41
  55. package/dist/core/theme.d.ts +16 -44
  56. package/dist/core/theme.js +6 -148
  57. package/dist/core/value.d.ts +7 -7
  58. package/dist/core/workbook-protection.d.ts +2 -2
  59. package/dist/core/workbook-styles.d.ts +26 -0
  60. package/dist/core/workbook-styles.js +48 -0
  61. package/dist/core/workbook-theme.d.ts +25 -0
  62. package/dist/core/workbook-theme.js +49 -0
  63. package/dist/core/workbook-vba.d.ts +17 -0
  64. package/dist/core/workbook-vba.js +79 -0
  65. package/dist/core/workbook.d.ts +133 -87
  66. package/dist/core/workbook.js +74 -133
  67. package/dist/core/worksheet-comments.d.ts +17 -0
  68. package/dist/core/worksheet-comments.js +70 -0
  69. package/dist/core/worksheet-model.d.ts +4 -4
  70. package/dist/core/worksheet-model.js +1 -0
  71. package/dist/core/worksheet-pictures.d.ts +29 -0
  72. package/dist/core/worksheet-pictures.js +42 -0
  73. package/dist/core/worksheet.d.ts +100 -79
  74. package/dist/core/worksheet.js +63 -142
  75. package/dist/customui/errors.d.ts +1 -1
  76. package/dist/customui/ribbon.d.ts +3 -3
  77. package/dist/customui/ribbon.js +24 -23
  78. package/dist/entries/core.d.ts +7 -7
  79. package/dist/entries/core.js +1 -1
  80. package/dist/entries/csv.d.ts +1 -1
  81. package/dist/entries/node-unavailable.d.ts +15 -0
  82. package/dist/entries/node-unavailable.js +20 -0
  83. package/dist/entries/node.d.ts +1 -0
  84. package/dist/entries/node.js +1 -0
  85. package/dist/entries/vba.d.ts +1 -1
  86. package/dist/entries/vba.js +1 -1
  87. package/dist/entries/xlsx.d.ts +1 -1
  88. package/dist/entries/xlsx.js +1 -1
  89. package/dist/errors.d.ts +12 -12
  90. package/dist/io/csv/read.js +2 -3
  91. package/dist/io/csv/write.d.ts +19 -4
  92. package/dist/io/csv/write.js +34 -4
  93. package/dist/io/opc/errors.d.ts +6 -6
  94. package/dist/io/opc/inflate.d.ts +1 -1
  95. package/dist/io/opc/inflate.js +2 -12
  96. package/dist/io/opc/namespaces.d.ts +1 -1
  97. package/dist/io/opc/read-opc.d.ts +19 -3
  98. package/dist/io/opc/read-opc.js +14 -8
  99. package/dist/io/opc/read-options.d.ts +2 -2
  100. package/dist/io/opc/rels.d.ts +11 -0
  101. package/dist/io/opc/rels.js +3 -2
  102. package/dist/io/opc/sniff-format.d.ts +2 -2
  103. package/dist/io/style/xf-style.d.ts +44 -7
  104. package/dist/io/style/xf-style.js +27 -0
  105. package/dist/io/xlsb/errors.d.ts +1 -1
  106. package/dist/io/xlsb/formula.d.ts +4 -4
  107. package/dist/io/xlsb/primitives.d.ts +8 -8
  108. package/dist/io/xlsb/ptg-functions.d.ts +1 -1
  109. package/dist/io/xlsb/read-styles.js +2 -20
  110. package/dist/io/xlsb/read.d.ts +2 -2
  111. package/dist/io/xlsx/cell-accumulator.d.ts +26 -9
  112. package/dist/io/xlsx/cell-accumulator.js +89 -46
  113. package/dist/io/xlsx/cell-value.d.ts +3 -3
  114. package/dist/io/xlsx/cell-value.js +9 -6
  115. package/dist/io/xlsx/color-xml.d.ts +1 -1
  116. package/dist/io/xlsx/color-xml.js +18 -23
  117. package/dist/io/xlsx/comments.d.ts +5 -5
  118. package/dist/io/xlsx/comments.js +17 -8
  119. package/dist/io/xlsx/conditional-formatting.d.ts +4 -4
  120. package/dist/io/xlsx/conditional-formatting.js +64 -58
  121. package/dist/io/xlsx/data-validation.d.ts +3 -3
  122. package/dist/io/xlsx/data-validation.js +35 -33
  123. package/dist/io/xlsx/errors.d.ts +3 -3
  124. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  125. package/dist/io/xlsx/hyperlinks.js +10 -18
  126. package/dist/io/xlsx/images.d.ts +1 -1
  127. package/dist/io/xlsx/images.js +25 -28
  128. package/dist/io/xlsx/namespaces.d.ts +2 -2
  129. package/dist/io/xlsx/package-plan.js +5 -2
  130. package/dist/io/xlsx/read-pivot.js +4 -13
  131. package/dist/io/xlsx/read-rows.d.ts +13 -13
  132. package/dist/io/xlsx/read-rows.js +19 -55
  133. package/dist/io/xlsx/read-shared-strings.js +7 -44
  134. package/dist/io/xlsx/read-styles.d.ts +3 -3
  135. package/dist/io/xlsx/read-styles.js +42 -66
  136. package/dist/io/xlsx/read-worksheet.js +73 -129
  137. package/dist/io/xlsx/read.d.ts +4 -6
  138. package/dist/io/xlsx/read.js +104 -142
  139. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  140. package/dist/io/xlsx/rich-runs.js +94 -30
  141. package/dist/io/xlsx/rich-text.d.ts +1 -1
  142. package/dist/io/xlsx/shared-strings.d.ts +2 -2
  143. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  144. package/dist/io/xlsx/sheet-properties.js +14 -10
  145. package/dist/io/xlsx/styles.d.ts +9 -22
  146. package/dist/io/xlsx/styles.js +65 -89
  147. package/dist/io/xlsx/tables.d.ts +2 -2
  148. package/dist/io/xlsx/tables.js +20 -25
  149. package/dist/io/xlsx/theme-xml.d.ts +37 -0
  150. package/dist/io/xlsx/theme-xml.js +145 -0
  151. package/dist/io/xlsx/threaded-comments.d.ts +18 -27
  152. package/dist/io/xlsx/threaded-comments.js +26 -38
  153. package/dist/io/xlsx/workbook-xml.js +14 -8
  154. package/dist/io/xlsx/worksheet-xml.d.ts +2 -2
  155. package/dist/io/xlsx/worksheet-xml.js +40 -48
  156. package/dist/io/xlsx/write-stream.d.ts +23 -16
  157. package/dist/io/xlsx/write-stream.js +13 -14
  158. package/dist/io/xlsx/write.d.ts +8 -7
  159. package/dist/io/xlsx/write.js +6 -3
  160. package/dist/io/xlsx/x14-ext.d.ts +1 -1
  161. package/dist/sha512.d.ts +2 -0
  162. package/dist/sha512.js +146 -0
  163. package/dist/vba/bytes.d.ts +11 -0
  164. package/dist/vba/bytes.js +29 -0
  165. package/dist/vba/cfb-writer.d.ts +1 -1
  166. package/dist/vba/cfb.d.ts +1 -1
  167. package/dist/vba/cfb.js +15 -51
  168. package/dist/vba/codepage.d.ts +2 -2
  169. package/dist/vba/dir-records.d.ts +53 -0
  170. package/dist/vba/dir-records.js +28 -0
  171. package/dist/vba/errors.d.ts +2 -2
  172. package/dist/vba/index.d.ts +2 -2
  173. package/dist/vba/index.js +2 -2
  174. package/dist/vba/ms-ovba.d.ts +1 -1
  175. package/dist/vba/ms-ovba.js +1 -3
  176. package/dist/vba/project-editor.d.ts +9 -9
  177. package/dist/vba/project-editor.js +6 -44
  178. package/dist/vba/project.d.ts +5 -5
  179. package/dist/vba/project.js +3 -28
  180. package/dist/vba/vba-encoding.d.ts +1 -1
  181. package/dist/xml/errors.d.ts +1 -1
  182. package/dist/xml/xml-read.d.ts +98 -9
  183. package/dist/xml/xml-read.js +70 -0
  184. package/dist/xml/xml.d.ts +74 -10
  185. package/dist/xml/xml.js +39 -7
  186. package/package.json +59 -36
@@ -0,0 +1,145 @@
1
+ import { DEFAULT_THEME_COLOR_SCHEME, isThemeColorSlot, normalizeThemeColor, } from '../../core/theme.js';
2
+ import { decodeEntities } from '../../xml/xml-read.js';
3
+ import { escapeAttr } from '../../xml/xml.js';
4
+ const SCHEME_SLOT = new RegExp('<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)\\b[^>]*>' + '\\s*<a:(srgbClr|sysClr)\\b([^>]*)>', 'g');
5
+ export function parseThemeColorScheme(themeXml) {
6
+ const block = /<a:clrScheme\b[^>]*>([\s\S]*?)<\/a:clrScheme>/.exec(themeXml);
7
+ if (block === null)
8
+ return {};
9
+ const scheme = {};
10
+ for (const match of (block[1] ?? '').matchAll(SCHEME_SLOT)) {
11
+ const slot = match[1];
12
+ if (slot === undefined || !isThemeColorSlot(slot))
13
+ continue;
14
+ const attrs = match[3] ?? '';
15
+ const source = match[2] === 'sysClr' ? /\blastClr="([^"]*)"/ : /\bval="([^"]*)"/;
16
+ const value = source.exec(attrs)?.[1];
17
+ if (value !== undefined && /^[0-9a-fA-F]{6}$/.test(value))
18
+ scheme[slot] = value;
19
+ }
20
+ return scheme;
21
+ }
22
+ export function parseThemeFontScheme(themeXml) {
23
+ const block = /<a:fontScheme\b[^>]*>([\s\S]*?)<\/a:fontScheme>/.exec(themeXml);
24
+ if (block === null)
25
+ return {};
26
+ const face = (which) => {
27
+ const font = new RegExp(`<a:${which}\\b[^>]*>([\\s\\S]*?)</a:${which}>`).exec(block[1] ?? '');
28
+ const typeface = /<a:latin\b[^>]*\btypeface="([^"]*)"/.exec(font?.[1] ?? '')?.[1];
29
+ return typeface === undefined ? undefined : decodeEntities(typeface);
30
+ };
31
+ const scheme = {};
32
+ const major = face('majorFont');
33
+ const minor = face('minorFont');
34
+ if (major !== undefined)
35
+ scheme.major = major;
36
+ if (minor !== undefined)
37
+ scheme.minor = minor;
38
+ return scheme;
39
+ }
40
+ const SCHEME_ELEMENT_ORDER = [
41
+ 'dk1',
42
+ 'lt1',
43
+ 'dk2',
44
+ 'lt2',
45
+ 'accent1',
46
+ 'accent2',
47
+ 'accent3',
48
+ 'accent4',
49
+ 'accent5',
50
+ 'accent6',
51
+ 'hlink',
52
+ 'folHlink',
53
+ ];
54
+ export function applyThemeOverrides(baseXml, overrides) {
55
+ let xml = baseXml;
56
+ const colors = overrides.colors ?? {};
57
+ if (Object.keys(colors).length > 0) {
58
+ const sourceElements = parseThemeColorElements(baseXml);
59
+ const body = SCHEME_ELEMENT_ORDER.map((slot) => {
60
+ const authored = colors[slot];
61
+ const inner = authored !== undefined
62
+ ? `<a:srgbClr val="${normalizeThemeColor(authored)}"/>`
63
+ : (sourceElements[slot] ?? `<a:srgbClr val="${DEFAULT_THEME_COLOR_SCHEME[slot]}"/>`);
64
+ return `<a:${slot}>${inner}</a:${slot}>`;
65
+ }).join('');
66
+ xml = replaceBlockBody(xml, 'clrScheme', body);
67
+ }
68
+ const { major, minor } = overrides.fonts ?? {};
69
+ if (major !== undefined)
70
+ xml = replaceLatinTypeface(xml, 'majorFont', major);
71
+ if (minor !== undefined)
72
+ xml = replaceLatinTypeface(xml, 'minorFont', minor);
73
+ return xml;
74
+ }
75
+ function parseThemeColorElements(themeXml) {
76
+ const block = /<a:clrScheme\b[^>]*>([\s\S]*?)<\/a:clrScheme>/.exec(themeXml);
77
+ if (block === null)
78
+ return {};
79
+ const elements = {};
80
+ const pattern = /<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)>([\s\S]*?)<\/a:\1>|<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)\/>/g;
81
+ for (const match of (block[1] ?? '').matchAll(pattern)) {
82
+ const slot = match[1] ?? match[3];
83
+ if (slot === undefined || !isThemeColorSlot(slot))
84
+ continue;
85
+ const inner = match[2];
86
+ if (inner !== undefined && inner !== '')
87
+ elements[slot] = inner;
88
+ }
89
+ return elements;
90
+ }
91
+ function replaceBlockBody(xml, name, body) {
92
+ const pattern = new RegExp(`(<a:${name}\\b[^>]*>)[\\s\\S]*?(</a:${name}>)`);
93
+ return xml.replace(pattern, (_all, open, close) => `${open}${body}${close}`);
94
+ }
95
+ function replaceLatinTypeface(xml, which, typeface) {
96
+ const pattern = new RegExp(`(<a:${which}\\b[^>]*>[\\s\\S]*?<a:latin\\b)[^>]*(/>)`);
97
+ return xml.replace(pattern, (_all, open, close) => `${open} typeface="${escapeAttr(typeface)}"${close}`);
98
+ }
99
+ export const DEFAULT_THEME_XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
100
+ '<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">' +
101
+ '<a:themeElements>' +
102
+ '<a:clrScheme name="Office">' +
103
+ '<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>' +
104
+ '<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>' +
105
+ '<a:dk2><a:srgbClr val="44546A"/></a:dk2>' +
106
+ '<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>' +
107
+ '<a:accent1><a:srgbClr val="4472C4"/></a:accent1>' +
108
+ '<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>' +
109
+ '<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>' +
110
+ '<a:accent4><a:srgbClr val="FFC000"/></a:accent4>' +
111
+ '<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>' +
112
+ '<a:accent6><a:srgbClr val="70AD47"/></a:accent6>' +
113
+ '<a:hlink><a:srgbClr val="0563C1"/></a:hlink>' +
114
+ '<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>' +
115
+ '</a:clrScheme>' +
116
+ '<a:fontScheme name="Office">' +
117
+ '<a:majorFont><a:latin typeface="Calibri Light" panose="020F0302020204030204"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>' +
118
+ '<a:minorFont><a:latin typeface="Calibri" panose="020F0502020204030204"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>' +
119
+ '</a:fontScheme>' +
120
+ '<a:fmtScheme name="Office">' +
121
+ '<a:fillStyleLst>' +
122
+ '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
123
+ '<a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>' +
124
+ '<a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>' +
125
+ '</a:fillStyleLst>' +
126
+ '<a:lnStyleLst>' +
127
+ '<a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>' +
128
+ '<a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>' +
129
+ '<a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>' +
130
+ '</a:lnStyleLst>' +
131
+ '<a:effectStyleLst>' +
132
+ '<a:effectStyle><a:effectLst/></a:effectStyle>' +
133
+ '<a:effectStyle><a:effectLst/></a:effectStyle>' +
134
+ '<a:effectStyle><a:effectLst><a:outerShdw blurRad="57150" dist="19050" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle>' +
135
+ '</a:effectStyleLst>' +
136
+ '<a:bgFillStyleLst>' +
137
+ '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
138
+ '<a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>' +
139
+ '<a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill>' +
140
+ '</a:bgFillStyleLst>' +
141
+ '</a:fmtScheme>' +
142
+ '</a:themeElements>' +
143
+ '<a:objectDefaults/>' +
144
+ '<a:extraClrSchemeLst/>' +
145
+ '</a:theme>';
@@ -1,5 +1,5 @@
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`. */
1
+ import { type CommentThread, type MentionRef, type Person } from '../../core/comment-thread.ts';
2
+ /** A registered author of threaded comments: one `<person>` of `xl/persons/person.xml`. */
3
3
  export interface ParsedPerson {
4
4
  /** Brace-wrapped GUID a message's `personId` points at. */
5
5
  readonly id: string;
@@ -10,26 +10,17 @@ export interface ParsedPerson {
10
10
  readonly providerId?: string;
11
11
  }
12
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.
13
+ * One `<mention>` of a message's `<mentions>` block: the model's own {@link MentionRef}, which is
14
+ * exactly the four wire attributes with nothing resolved yet. Declaring it a second time here would
15
+ * be two places for the offset convention to drift, and the offsets are what a spreadsheet app
16
+ * highlights with.
15
17
  *
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
+ * All four attributes are required (Excel rejects a file missing any), and note the lowercase `p`
19
+ * in `mentionpersonId`; the capitalised spelling is not a declared attribute. The person id is that
20
+ * attribute's value; every other field is named as the model names it.
18
21
  */
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`. */
22
+ export type ParsedMention = MentionRef;
23
+ /** One message of a threaded conversation: a `<threadedComment>` of a `threadedComment{n}.xml`. */
33
24
  export interface ParsedThreadedComment {
34
25
  /** A1 reference of the cell the whole thread anchors to; every message of a thread repeats it. */
35
26
  readonly ref: string;
@@ -39,7 +30,7 @@ export interface ParsedThreadedComment {
39
30
  readonly personId?: string;
40
31
  /**
41
32
  * 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
33
+ * timezone (`2026-07-24T10:56:41.72`), which is not a round-trippable instant. Keeping the
43
34
  * string spares the reader from inventing a zone the file never stated.
44
35
  */
45
36
  readonly date?: string;
@@ -48,7 +39,7 @@ export interface ParsedThreadedComment {
48
39
  readonly parentId?: string;
49
40
  /**
50
41
  * 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
42
+ * open thread (never `done="0"`), so read a thread's resolved state off its head: a reply's
52
43
  * `false` here means "did not say", not "not resolved".
53
44
  */
54
45
  readonly done: boolean;
@@ -57,12 +48,12 @@ export interface ParsedThreadedComment {
57
48
  }
58
49
  /**
59
50
  * 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
51
+ * meaning, because Excel re-sorts the list by person id when it saves, so nothing may depend on it. An
61
52
  * entry without an `id` is skipped: no message could reference it.
62
53
  */
63
54
  export declare function parsePersons(xml: string): ParsedPerson[];
64
55
  /**
65
- * Parse a `threadedComment{n}.xml` part into its messages, in document order thread order, with
56
+ * Parse a `threadedComment{n}.xml` part into its messages, in document order: thread order, with
66
57
  * each thread's replies following its head. Grouping into threads is {@link buildCommentThreads}'s
67
58
  * job; this stays faithful to the part. A message without a `ref` or `id` cannot be anchored or
68
59
  * replied to and is skipped.
@@ -74,14 +65,14 @@ export declare function parseThreadedComments(xml: string): ParsedThreadedCommen
74
65
  *
75
66
  * Document order is thread order with each thread's replies following its head, so one pass suffices: a
76
67
  * 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
68
+ * whose parent is unknown (a dangling `parentId` no Excel file produces) opens a thread of its own
78
69
  * rather than being dropped, so a foreign generator's damage costs structure, never content.
79
70
  */
80
71
  export declare function buildCommentThreads(messages: readonly ParsedThreadedComment[], personById: (id: string) => Person | undefined): CommentThread[];
81
72
  /**
82
73
  * Serialise one sheet's conversations into its `xl/threadedComments/threadedComment{n}.xml` part.
83
74
  *
84
- * Messages are written flat, in thread order with each thread's replies after its head the shape
75
+ * Messages are written flat, in thread order with each thread's replies after its head: the shape
85
76
  * {@link parseThreadedComments} reads back. The head/reply distinction the model holds as array position
86
77
  * becomes `parentId` on every reply but the head, and `done="1"` goes on the head alone: only the head
87
78
  * carries the flag on the wire, so a reply can never contradict the thread it belongs to. An open thread
@@ -92,7 +83,7 @@ export declare function buildCommentThreads(messages: readonly ParsedThreadedCom
92
83
  */
93
84
  export declare function threadedCommentsXml(threads: readonly CommentThread[]): string;
94
85
  /**
95
- * Serialise the workbook's identity registry into `xl/persons/person.xml` singular and unnumbered,
86
+ * Serialise the workbook's identity registry into `xl/persons/person.xml`: singular and unnumbered,
96
87
  * unlike the per-sheet thread parts.
97
88
  *
98
89
  * Entries are written in registry order, which carries no meaning: Excel re-sorts the list by person id
@@ -1,7 +1,7 @@
1
- import { decodeAddress } from '../../core/address.js';
1
+ import { encodeAddress, tryDecodeCellRef } from '../../core/address.js';
2
2
  import { MENTION_OFFSET_MAX, } from '../../core/comment-thread.js';
3
- import { escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
4
- import { boolStrict, localName, parseXml } from '../../xml/xml-read.js';
3
+ import { boolStrict, decodeSpreadsheetText, localName, numInteger, parseXml, TextCapture, } from '../../xml/xml-read.js';
4
+ import { escapeAttr, escapeSpreadsheetText, textAttr, XML_DECLARATION } from '../../xml/xml.js';
5
5
  import { THREADED_COMMENTS_NS } from './namespaces.js';
6
6
  export function parsePersons(xml) {
7
7
  const persons = [];
@@ -27,33 +27,33 @@ export function parseThreadedComments(xml) {
27
27
  const messages = [];
28
28
  let open;
29
29
  let mentions = [];
30
- let inText = false;
31
30
  let text = '';
31
+ const capture = new TextCapture('text');
32
32
  parseXml(xml, {
33
- onOpen(name, attrs) {
33
+ onOpen(name, attrs, selfClosing) {
34
34
  const local = localName(name);
35
35
  if (local === 'threadedComment') {
36
36
  open = attrs;
37
37
  text = '';
38
38
  mentions = [];
39
39
  }
40
- else if (local === 'text' && open !== undefined) {
41
- inText = true;
42
- }
43
- else if (local === 'mention' && open !== undefined) {
44
- const mention = mentionFrom(attrs);
45
- if (mention !== undefined)
46
- mentions.push(mention);
40
+ else if (open !== undefined) {
41
+ capture.open(local, selfClosing);
42
+ if (local === 'mention') {
43
+ const mention = mentionFrom(attrs);
44
+ if (mention !== undefined)
45
+ mentions.push(mention);
46
+ }
47
47
  }
48
48
  },
49
49
  onText(chunk) {
50
- if (inText)
51
- text += chunk;
50
+ capture.text(chunk);
52
51
  },
53
52
  onClose(name) {
54
53
  const local = localName(name);
55
- if (local === 'text') {
56
- inText = false;
54
+ const body = capture.close(local);
55
+ if (body !== undefined) {
56
+ text = body;
57
57
  }
58
58
  else if (local === 'threadedComment') {
59
59
  if (open !== undefined) {
@@ -76,7 +76,7 @@ function threadedCommentFrom(attrs, text, mentions) {
76
76
  return {
77
77
  ref,
78
78
  id,
79
- text,
79
+ text: decodeSpreadsheetText(text),
80
80
  done: boolStrict(attrs.done),
81
81
  mentions: [...mentions],
82
82
  ...(personId !== undefined ? { personId } : {}),
@@ -86,8 +86,8 @@ function threadedCommentFrom(attrs, text, mentions) {
86
86
  }
87
87
  function mentionFrom(attrs) {
88
88
  const personId = attrs.mentionpersonId;
89
- const startIndex = integerAttribute(attrs.startIndex);
90
- const length = integerAttribute(attrs.length);
89
+ const startIndex = numInteger(attrs.startIndex);
90
+ const length = numInteger(attrs.length);
91
91
  if (personId === undefined || startIndex === undefined || length === undefined)
92
92
  return undefined;
93
93
  if (startIndex < 0 || length <= 0)
@@ -101,12 +101,6 @@ function mentionFrom(attrs) {
101
101
  ...(attrs.mentionId !== undefined ? { mentionId: attrs.mentionId } : {}),
102
102
  };
103
103
  }
104
- function integerAttribute(raw) {
105
- if (raw === undefined || raw.trim() === '')
106
- return undefined;
107
- const value = Number(raw);
108
- return Number.isInteger(value) ? value : undefined;
109
- }
110
104
  export function buildCommentThreads(messages, personById) {
111
105
  const threads = [];
112
106
  const commentsByHeadId = new Map();
@@ -127,14 +121,8 @@ export function buildCommentThreads(messages, personById) {
127
121
  return threads;
128
122
  }
129
123
  function anchorRef(reference) {
130
- let decoded;
131
- try {
132
- decoded = decodeAddress(reference);
133
- }
134
- catch {
135
- return undefined;
136
- }
137
- return decoded.col === undefined || decoded.row === undefined ? undefined : decoded.address;
124
+ const cell = tryDecodeCellRef(reference);
125
+ return cell === undefined ? undefined : encodeAddress(cell.col, cell.row);
138
126
  }
139
127
  function commentFrom(message, personById) {
140
128
  const author = message.personId === undefined ? undefined : personById(message.personId);
@@ -171,10 +159,10 @@ export function threadedCommentsXml(threads) {
171
159
  `<ThreadedComments xmlns="${THREADED_COMMENTS_NS}">${messages.join('')}</ThreadedComments>`);
172
160
  }
173
161
  function threadedCommentXml(ref, comment, tail) {
174
- const date = comment.date === undefined ? '' : ` dT="${escapeAttr(comment.date)}"`;
175
- const person = comment.personId === undefined ? '' : ` personId="${escapeAttr(comment.personId)}"`;
162
+ const date = textAttr('dT', comment.date);
163
+ const person = textAttr('personId', comment.personId);
176
164
  return (`<threadedComment ref="${escapeAttr(ref)}"${date}${person} id="${escapeAttr(comment.id)}"${tail}>` +
177
- `<text>${escapeText(comment.text)}</text>` +
165
+ `<text>${escapeSpreadsheetText(comment.text)}</text>` +
178
166
  mentionsXml(comment.mentions) +
179
167
  '</threadedComment>');
180
168
  }
@@ -196,8 +184,8 @@ function writableOffset(value) {
196
184
  }
197
185
  export function personsXml(persons) {
198
186
  const entries = persons.map((person) => {
199
- const userId = person.userId === undefined ? '' : ` userId="${escapeAttr(person.userId)}"`;
200
- const providerId = person.providerId === undefined ? '' : ` providerId="${escapeAttr(person.providerId)}"`;
187
+ const userId = textAttr('userId', person.userId);
188
+ const providerId = textAttr('providerId', person.providerId);
201
189
  return (`<person displayName="${escapeAttr(person.displayName)}"` +
202
190
  ` id="${escapeAttr(person.id)}"${userId}${providerId}/>`);
203
191
  });
@@ -1,6 +1,7 @@
1
1
  import { mangleFormula, quoteSheetName } from '../../core/formula.js';
2
2
  import { WORKBOOK_PROTECTION_CREDENTIAL_ATTRS } from '../../core/workbook-protection.js';
3
- import { escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
3
+ import { isVisibility } from '../../core/worksheet.js';
4
+ import { checkedToken, escapeAttr, escapeText, numAttr, textAttr, XML_DECLARATION, } from '../../xml/xml.js';
4
5
  import { extensionOf, relativePartPath, THEME_PART_PATH } from '../opc/part-paths.js';
5
6
  import { relationship, relationshipsPart } from '../opc/rels.js';
6
7
  import { imageContentType } from './images.js';
@@ -102,14 +103,16 @@ export function rootRelsXml(rootRefs) {
102
103
  relationship('rId1', REL.officeDocument, 'xl/workbook.xml'),
103
104
  relationship('rId2', REL.coreProps, 'docProps/core.xml'),
104
105
  relationship('rId3', REL.extProps, 'docProps/app.xml'),
105
- ...rootRefs.map((ref, i) => relationship(`rId${4 + i}`, ref.relType, escapeAttr(ref.entryPath))),
106
+ ...rootRefs.map((ref, i) => relationship(`rId${4 + i}`, ref.relType, ref.entryPath)),
106
107
  ]);
107
108
  }
108
109
  export function workbookXml(workbook, preservedRels, pivots) {
109
110
  const sheets = workbook.worksheets;
110
111
  const entries = sheets
111
112
  .map((sheet, i) => {
112
- const state = sheet.state === 'visible' ? '' : ` state="${sheet.state}"`;
113
+ const state = sheet.state === 'visible'
114
+ ? ''
115
+ : ` state="${checkedToken(sheet.state, isVisibility, 'sheet visibility')}"`;
113
116
  return `<sheet name="${escapeAttr(sheet.name)}" sheetId="${sheet.id}"${state} r:id="rId${i + 1}"/>`;
114
117
  })
115
118
  .join('');
@@ -128,11 +131,14 @@ export function workbookXml(workbook, preservedRels, pivots) {
128
131
  function bookViewsXml(workbook) {
129
132
  const view = workbook.view;
130
133
  const activeTab = workbook.activeTabIndex;
131
- const attrs = (view.visibility !== undefined && view.visibility !== 'visible'
132
- ? ` visibility="${view.visibility}"`
133
- : '') +
134
+ const attrs = (view.visibility === undefined || view.visibility === 'visible'
135
+ ? ''
136
+ : ` visibility="${checkedToken(view.visibility, isVisibility, 'window visibility')}"`) +
134
137
  (view.minimized ? ' minimized="1"' : '') +
135
- ` xWindow="${view.x}" yWindow="${view.y}" windowWidth="${view.width}" windowHeight="${view.height}"` +
138
+ numAttr('xWindow', view.x) +
139
+ numAttr('yWindow', view.y) +
140
+ numAttr('windowWidth', view.width) +
141
+ numAttr('windowHeight', view.height) +
136
142
  (activeTab === 0 ? '' : ` activeTab="${activeTab}"`);
137
143
  return `<bookViews><workbookView${attrs}/></bookViews>`;
138
144
  }
@@ -192,7 +198,7 @@ function definedNamesXml(workbook) {
192
198
  const scopeAttr = name.scope === undefined
193
199
  ? ''
194
200
  : ` localSheetId="${sheets.findIndex((sheet) => sheet.name === name.scope)}"`;
195
- const commentAttr = name.comment === undefined ? '' : ` comment="${escapeAttr(name.comment)}"`;
201
+ const commentAttr = textAttr('comment', name.comment);
196
202
  const hiddenAttr = name.hidden ? ' hidden="1"' : '';
197
203
  return (`<definedName name="${escapeAttr(name.name)}"${scopeAttr}${commentAttr}${hiddenAttr}>` +
198
204
  `${escapeText(mangleFormula(name.refersTo))}</definedName>`);
@@ -6,7 +6,7 @@ import { type SharedFormulaRole } from './shared-formulas.ts';
6
6
  import type { SharedStringTable } from './shared-strings.ts';
7
7
  import type { StyleRegistry } from './styles.ts';
8
8
  /**
9
- * The used-cell extent of a sheet the top-left/bottom-right grid bounds that fold into the
9
+ * The used-cell extent of a sheet: the top-left/bottom-right grid bounds that fold into the
10
10
  * `<dimension>`. Rows carrying only formatting (a row height, an outline level) do not extend the
11
11
  * used range, matching how Excel records `<dimension>`, so {@link add} ignores them. A fresh extent
12
12
  * holds the `Infinity`/`-Infinity` sentinels; {@link isEmpty} reports that no used cell has been seen.
@@ -17,7 +17,7 @@ export declare class Extent {
17
17
  bottom: number;
18
18
  right: number;
19
19
  constructor(seed?: Extent);
20
- /** Whether no used cell has been folded in yet the sheet's dimension is then the lone cell `A1`. */
20
+ /** Whether no used cell has been folded in yet, in which case the dimension is the lone cell `A1`. */
21
21
  get isEmpty(): boolean;
22
22
  /** Fold a rendered row's used-column span into the extent. `minCol` is `Infinity` when the row
23
23
  * carried no cells (only formatting), which extends nothing. */
@@ -1,10 +1,11 @@
1
- import { decodeRange, encodeAddress, MAX_COLUMN } from '../../core/address.js';
1
+ import { decodeRange, encodeAddress } from '../../core/address.js';
2
2
  import { DEFAULT_DATE_NUMFMT, dateToSerial } from '../../core/date.js';
3
3
  import { mangleFormula } from '../../core/formula.js';
4
4
  import { NAMED_STYLE_ID } from '../../core/internal.js';
5
+ import { CELL_STYLE_FACETS, pickStyleFacets } from '../../core/style.js';
5
6
  import { detectValueType, isDataTableFormulaValue, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, } from '../../core/value.js';
6
7
  import { AuthoringError, InternalError } from '../../errors.js';
7
- import { escapeAttr, escapeText, numberText, textElement, XML_DECLARATION } from '../../xml/xml.js';
8
+ import { assertWritableNumber, escapeAttr, escapeSpreadsheetText, escapeText, numberText, textAttr, textElement, XML_DECLARATION, } from '../../xml/xml.js';
8
9
  import { relativePartPath } from '../opc/part-paths.js';
9
10
  import { relationship, relationshipsPart } from '../opc/rels.js';
10
11
  import { conditionalFormattingsExtXml, conditionalFormattingsXml } from './conditional-formatting.js';
@@ -136,16 +137,20 @@ export function renderRow(entry, ctx) {
136
137
  return { xml: `<row r="${number}"${attrs}>${cellsXml}</row>`, minCol, maxCol };
137
138
  }
138
139
  function composeCellStyle(cell, rowFill, colDef) {
139
- return {
140
- fill: cell.fill ?? rowFill ?? colDef?.fill,
141
- numFmt: cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value),
142
- font: cell.font ?? colDef?.font,
143
- border: cell.border ?? colDef?.border,
144
- alignment: cell.alignment ?? colDef?.alignment,
145
- protection: cell.protection ?? colDef?.protection,
146
- quotePrefix: cell.quotePrefix,
147
- xfId: cell[NAMED_STYLE_ID],
148
- };
140
+ const style = {};
141
+ for (const facet of CELL_STYLE_FACETS)
142
+ inheritFacet(style, facet, cell, colDef);
143
+ style.fill = cell.fill ?? rowFill ?? colDef?.fill;
144
+ style.numFmt = cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value);
145
+ if (cell.quotePrefix !== undefined)
146
+ style.quotePrefix = cell.quotePrefix;
147
+ const xfId = cell[NAMED_STYLE_ID];
148
+ if (xfId !== undefined)
149
+ style.xfId = xfId;
150
+ return style;
151
+ }
152
+ function inheritFacet(target, key, cell, colDef) {
153
+ target[key] = cell[key] ?? colDef?.[key];
149
154
  }
150
155
  function worksheetExtLstXml(sheet, slicerRelIds) {
151
156
  const exts = [
@@ -175,7 +180,7 @@ function validateMerges(sheet) {
175
180
  top <= region.bottom &&
176
181
  bottom >= region.top;
177
182
  if (overlaps) {
178
- throw new AuthoringError(`merged range ${merge} overlaps table "${table.name}" (${table.range}) Excel forbids a merge inside a table`);
183
+ throw new AuthoringError(`merged range ${merge} overlaps table "${table.name}" (${table.range}): Excel forbids a merge inside a table`);
179
184
  }
180
185
  }
181
186
  }
@@ -228,7 +233,7 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
228
233
  ...preservedReferences.map((reference) => relationship(reference.relId, reference.relType, escapeAttr(relativePartPath('xl/worksheets/sheet1.xml', reference.entryPath)))),
229
234
  ...hyperlinks
230
235
  .filter((link) => link.relId !== undefined && link.target !== undefined)
231
- .map((link) => relationship(link.relId, REL.hyperlink, escapeAttr(link.target), {
236
+ .map((link) => relationship(link.relId, REL.hyperlink, link.target, {
232
237
  external: true,
233
238
  })),
234
239
  ];
@@ -243,26 +248,25 @@ function sheetFormatPr(properties, outlineLevel) {
243
248
  }
244
249
  if (properties.defaultRowHeight !== undefined)
245
250
  attrs += ' customHeight="1"';
246
- if (outlineLevel.col > 0)
247
- attrs += ` outlineLevelCol="${outlineLevel.col}"`;
248
- if (outlineLevel.row > 0)
249
- attrs += ` outlineLevelRow="${outlineLevel.row}"`;
251
+ attrs += outlineAttr('outlineLevelCol', outlineLevel.col);
252
+ attrs += outlineAttr('outlineLevelRow', outlineLevel.row);
250
253
  return `<sheetFormatPr${attrs}/>`;
251
254
  }
255
+ function outlineAttr(name, level) {
256
+ if (level === undefined)
257
+ return '';
258
+ const text = numberText(level);
259
+ return level > 0 ? ` ${name}="${text}"` : '';
260
+ }
252
261
  function maxColumnOutlineLevel(sheet) {
253
262
  let max = 0;
254
- for (const column of sheet.columns()) {
255
- if (column.index > MAX_COLUMN)
256
- continue;
263
+ for (const column of sheet.columns())
257
264
  max = Math.max(max, column.outlineLevel ?? 0);
258
- }
259
265
  return max;
260
266
  }
261
267
  function colsXml(sheet, styles) {
262
268
  const runs = [];
263
269
  for (const { index, properties } of sheet.columns()) {
264
- if (index > MAX_COLUMN)
265
- continue;
266
270
  const body = colBody(properties ?? {}, styles);
267
271
  if (body === null)
268
272
  continue;
@@ -290,22 +294,16 @@ function colBody(properties, styles) {
290
294
  attrs += ' hidden="1"';
291
295
  meaningful = true;
292
296
  }
293
- if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
294
- attrs += ` outlineLevel="${properties.outlineLevel}"`;
297
+ const outline = outlineAttr('outlineLevel', properties.outlineLevel);
298
+ if (outline !== '') {
299
+ attrs += outline;
295
300
  meaningful = true;
296
301
  }
297
302
  if (properties.collapsed) {
298
303
  attrs += ' collapsed="1"';
299
304
  meaningful = true;
300
305
  }
301
- const style = styles.styleId({
302
- fill: properties.fill,
303
- numFmt: properties.numFmt,
304
- font: properties.font,
305
- border: properties.border,
306
- alignment: properties.alignment,
307
- protection: properties.protection,
308
- });
306
+ const style = styles.styleId(pickStyleFacets(properties));
309
307
  if (style !== 0) {
310
308
  attrs += ` style="${style}"`;
311
309
  meaningful = true;
@@ -320,9 +318,7 @@ function rowAttrs(properties, styles, collapsedSummary) {
320
318
  attrs += ` ht="${numberText(properties.height)}" customHeight="1"`;
321
319
  if (properties.hidden)
322
320
  attrs += ' hidden="1"';
323
- if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
324
- attrs += ` outlineLevel="${properties.outlineLevel}"`;
325
- }
321
+ attrs += outlineAttr('outlineLevel', properties.outlineLevel);
326
322
  if (properties.collapsed || collapsedSummary)
327
323
  attrs += ' collapsed="1"';
328
324
  const style = styles.styleId({ fill: properties.fill });
@@ -336,6 +332,7 @@ function scanRowOutline(sheet) {
336
332
  let maxLevel = 0;
337
333
  for (const { number, properties } of sheet.rows()) {
338
334
  const rowLevel = properties?.outlineLevel ?? 0;
335
+ assertWritableNumber(rowLevel);
339
336
  level.set(number, rowLevel);
340
337
  hidden.set(number, properties?.hidden ?? false);
341
338
  if (rowLevel > maxLevel)
@@ -410,15 +407,10 @@ function cellXml(cell, style, shared, sharedStrings) {
410
407
  }
411
408
  if (value === null)
412
409
  return `<c r="${ref}"${s}/>`;
413
- throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm every CellValue kind is handled above`);
410
+ throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm: every CellValue kind is handled above`);
414
411
  }
415
412
  function hasOwnStyle(cell) {
416
- return (cell.fill !== undefined ||
417
- cell.numFmt !== undefined ||
418
- cell.font !== undefined ||
419
- cell.border !== undefined ||
420
- cell.alignment !== undefined ||
421
- cell.protection !== undefined ||
413
+ return (CELL_STYLE_FACETS.some((facet) => cell[facet] !== undefined) ||
422
414
  cell.quotePrefix === true ||
423
415
  cell[NAMED_STYLE_ID] !== undefined);
424
416
  }
@@ -435,8 +427,8 @@ function cellFormulaXml(ref, s, value, shared) {
435
427
  const attrs = `ref="${escapeAttr(value.ref)}"` +
436
428
  ` dt2D="${value.dataTable2D ? 1 : 0}"` +
437
429
  ` dtr="${value.dataTableRow ? 1 : 0}"` +
438
- (value.r1 !== undefined ? ` r1="${escapeAttr(value.r1)}"` : '') +
439
- (value.r2 !== undefined ? ` r2="${escapeAttr(value.r2)}"` : '');
430
+ textAttr('r1', value.r1) +
431
+ textAttr('r2', value.r2);
440
432
  return formulaBodyXml(ref, s, `<f t="dataTable" ${attrs}/>`, value.result);
441
433
  }
442
434
  if (isFormulaValue(value)) {
@@ -455,7 +447,7 @@ function formulaBodyXml(ref, s, f, result) {
455
447
  return `<c r="${ref}"${s} t="b">${f}<v>${result ? 1 : 0}</v></c>`;
456
448
  }
457
449
  if (typeof result === 'string') {
458
- return `<c r="${ref}"${s} t="str">${f}<v>${escapeText(result)}</v></c>`;
450
+ return `<c r="${ref}"${s} t="str">${f}<v>${escapeSpreadsheetText(result)}</v></c>`;
459
451
  }
460
452
  if (isErrorValue(result)) {
461
453
  return `<c r="${ref}"${s} t="e">${f}<v>${result.error}</v></c>`;
@@ -465,5 +457,5 @@ function formulaBodyXml(ref, s, f, result) {
465
457
  return `<c r="${ref}"${s}>${f}</c>`;
466
458
  return `<c r="${ref}"${s}>${f}<v>${numberText(dateToSerial(result))}</v></c>`;
467
459
  }
468
- throw new InternalError('writing a non-primitive formula result has no arm every FormulaResult kind is handled above');
460
+ throw new InternalError('writing a non-primitive formula result has no arm: every FormulaResult kind is handled above');
469
461
  }