@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
@@ -1,41 +1,85 @@
1
1
  import { DEFAULT_THEME_COLOR_SCHEME, isThemeColorSlot, normalizeThemeColor, } from '../../core/theme.js';
2
- import { decodeEntities } from '../../xml/xml-read.js';
2
+ import { elementRange, parseXml } from '../../xml/xml-read.js';
3
+ import { localName } from '../../xml/xml-scan.js';
3
4
  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 {};
5
+ const COLOR_ELEMENTS = new Set(['srgbClr', 'sysClr']);
6
+ function readThemeScheme(themeXml) {
7
+ const colors = {};
8
+ const elements = {};
9
+ const fonts = {};
10
+ let prefix = '';
11
+ let container;
12
+ let slot;
13
+ let fontSlot;
14
+ parseXml(themeXml, {
15
+ onOpen(name, attrs, _selfClosing, scope) {
16
+ const local = localName(name);
17
+ if (local === 'theme') {
18
+ const colon = name.indexOf(':');
19
+ prefix = colon === -1 ? '' : name.slice(0, colon + 1);
20
+ return;
21
+ }
22
+ if (local === 'clrScheme' || local === 'fontScheme') {
23
+ container = local;
24
+ return;
25
+ }
26
+ if (container === 'clrScheme') {
27
+ if (isThemeColorSlot(local)) {
28
+ slot = local;
29
+ }
30
+ else if (slot !== undefined && COLOR_ELEMENTS.has(local)) {
31
+ const value = local === 'sysClr' ? attrs.lastClr : attrs.val;
32
+ if (value !== undefined && /^[0-9a-fA-F]{6}$/.test(value))
33
+ colors[slot] = value;
34
+ elements[slot] = emptyElement(name, attrs);
35
+ slot = undefined;
36
+ }
37
+ return;
38
+ }
39
+ if (container === 'fontScheme') {
40
+ if (local === 'majorFont')
41
+ fontSlot = 'major';
42
+ else if (local === 'minorFont')
43
+ fontSlot = 'minor';
44
+ else if (local === 'latin' && fontSlot !== undefined && attrs.typeface !== undefined) {
45
+ fonts[fontSlot] ??= attrs.typeface;
46
+ }
47
+ }
48
+ void scope;
49
+ },
50
+ onClose(name) {
51
+ const local = localName(name);
52
+ if (local === 'clrScheme' || local === 'fontScheme') {
53
+ container = undefined;
54
+ slot = undefined;
55
+ fontSlot = undefined;
56
+ }
57
+ else if (local === 'majorFont' || local === 'minorFont') {
58
+ fontSlot = undefined;
59
+ }
60
+ else if (slot !== undefined && local === slot) {
61
+ slot = undefined;
62
+ }
63
+ },
64
+ });
9
65
  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;
66
+ if (fonts.major !== undefined)
67
+ scheme.major = fonts.major;
68
+ if (fonts.minor !== undefined)
69
+ scheme.minor = fonts.minor;
70
+ return { colors, elements, fonts: scheme, prefix };
71
+ }
72
+ function emptyElement(name, attrs) {
73
+ let out = `<${name}`;
74
+ for (const key in attrs)
75
+ out += ` ${key}="${escapeAttr(attrs[key] ?? '')}"`;
76
+ return `${out}/>`;
77
+ }
78
+ export function parseThemeColorScheme(themeXml) {
79
+ return readThemeScheme(themeXml).colors;
21
80
  }
22
81
  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;
82
+ return readThemeScheme(themeXml).fonts;
39
83
  }
40
84
  const SCHEME_ELEMENT_ORDER = [
41
85
  'dk1',
@@ -52,49 +96,43 @@ const SCHEME_ELEMENT_ORDER = [
52
96
  'folHlink',
53
97
  ];
54
98
  export function applyThemeOverrides(baseXml, overrides) {
55
- let xml = baseXml;
99
+ const { elements: sourceElements, prefix } = readThemeScheme(baseXml);
100
+ const edits = [];
56
101
  const colors = overrides.colors ?? {};
57
102
  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);
103
+ const scheme = elementRange(baseXml, ['clrScheme']);
104
+ if (scheme !== undefined) {
105
+ const body = SCHEME_ELEMENT_ORDER.map((slot) => {
106
+ const authored = colors[slot];
107
+ const inner = authored !== undefined
108
+ ? `<${prefix}srgbClr val="${normalizeThemeColor(authored)}"/>`
109
+ : (sourceElements[slot] ??
110
+ `<${prefix}srgbClr val="${DEFAULT_THEME_COLOR_SCHEME[slot]}"/>`);
111
+ return `<${prefix}${slot}>${inner}</${prefix}${slot}>`;
112
+ }).join('');
113
+ edits.push({ start: scheme.contentStart, end: scheme.contentEnd, text: body });
114
+ }
67
115
  }
68
116
  const { major, minor } = overrides.fonts ?? {};
117
+ const latin = (which, typeface) => {
118
+ const found = elementRange(baseXml, [which, 'latin']);
119
+ if (found === undefined)
120
+ return;
121
+ const attrs = { ...found.attrs, typeface };
122
+ let rendered = `<${found.name}`;
123
+ for (const key in attrs)
124
+ rendered += ` ${key}="${escapeAttr(attrs[key] ?? '')}"`;
125
+ edits.push({ start: found.start, end: found.end, text: `${rendered}/>` });
126
+ };
69
127
  if (major !== undefined)
70
- xml = replaceLatinTypeface(xml, 'majorFont', major);
128
+ latin('majorFont', major);
71
129
  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;
130
+ latin('minorFont', minor);
131
+ let xml = baseXml;
132
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
133
+ xml = xml.slice(0, edit.start) + edit.text + xml.slice(edit.end);
88
134
  }
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}`);
135
+ return xml;
98
136
  }
99
137
  export const DEFAULT_THEME_XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
100
138
  '<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">' +
@@ -1,14 +1,12 @@
1
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
- 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
- }
2
+ /**
3
+ * A registered author of threaded comments: one `<person>` of `xl/persons/person.xml`, which is the
4
+ * model's own {@link Person} with nothing added. Declared as an alias for the same reason
5
+ * {@link ParsedMention} is: this file's write half already emits `Person` directly, so re-declaring
6
+ * the four fields here made one feature module name one shape two ways, with nothing keeping the two
7
+ * in step.
8
+ */
9
+ export type ParsedPerson = Person;
12
10
  /**
13
11
  * One `<mention>` of a message's `<mentions>` block: the model's own {@link MentionRef}, which is
14
12
  * exactly the four wire attributes with nothing resolved yet. Declaring it a second time here would
@@ -1,6 +1,8 @@
1
1
  import { encodeAddress, tryDecodeCellRef } from '../../core/address.js';
2
2
  import { MENTION_OFFSET_MAX, } from '../../core/comment-thread.js';
3
- import { boolStrict, decodeSpreadsheetText, localName, numInteger, parseXml, TextCapture, } from '../../xml/xml-read.js';
3
+ import { decodeSpreadsheetText, numInteger } from '../../xml/xml-attrs.js';
4
+ import { parseXml, TextCapture } from '../../xml/xml-read.js';
5
+ import { boolStrict, localName } from '../../xml/xml-scan.js';
4
6
  import { escapeAttr, escapeSpreadsheetText, textAttr, XML_DECLARATION } from '../../xml/xml.js';
5
7
  import { THREADED_COMMENTS_NS } from './namespaces.js';
6
8
  export function parsePersons(xml) {
@@ -3,10 +3,60 @@ import type { PivotPlan, PreservedPartPlan, PreservedRootReferencePlan, Preserve
3
3
  export type PreservedWorkbookRel = PreservedWorkbookReferencePlan & {
4
4
  readonly relId: string;
5
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;
6
+ /**
7
+ * The workbook part's relationship ids, drawn once by the writer's `assignWorkbookRelIds` and handed
8
+ * to every consumer rather than re-derived by any of them.
9
+ *
10
+ * Declared here because both consumers are here: the rels part wires the ids, and the workbook body
11
+ * *references* them from `<sheet r:id>`. The body used to spell that reference `rId${i + 1}`, which
12
+ * agreed with the allocator only because sheets happen to be the first ids it hands out. Anything
13
+ * laid ahead of them would have re-pointed every sheet at the wrong part, and the package would have
14
+ * stayed schema-valid while doing it -- a class of corruption no validator catches and no round-trip
15
+ * through this library notices, because the reader resolves the same wrong ids consistently.
16
+ */
17
+ export interface WorkbookRelPlan {
18
+ readonly sheetRelIds: readonly string[];
19
+ readonly stylesRelId: string;
20
+ readonly themeRelId: string;
21
+ readonly sharedStringsRelId: string | null;
22
+ readonly personsRelId: string | null;
23
+ readonly preservedWorkbookRels: readonly PreservedWorkbookRel[];
24
+ }
25
+ /**
26
+ * What `[Content_Types].xml` needs to know about a package: every part family it declares.
27
+ *
28
+ * One object rather than twelve positional parameters, eight of them arrays and four of those bare
29
+ * `readonly number[]`, which two adjacent ones could be silently transposed between. That is the same
30
+ * hazard `SheetReferences`, `SheetPlan` and `WorksheetXmlInputs` remove elsewhere in this writer, and
31
+ * it travels the whole way down: the two functions that render from this take it as it stands rather
32
+ * than re-scattering its fields across their own parameter lists.
33
+ */
34
+ export interface ContentTypeInputs {
35
+ readonly sheetCount: number;
36
+ readonly tables: readonly TablePlan[];
37
+ readonly commentNumbers: readonly number[];
38
+ readonly drawingNumbers: readonly number[];
39
+ readonly printerSettingsNumbers: readonly number[];
40
+ readonly mediaExtensions: readonly string[];
41
+ readonly hasSharedStrings: boolean;
42
+ readonly preservedParts: readonly PreservedPartPlan[];
43
+ readonly pivots: readonly PivotPlan[];
44
+ readonly preservedWorkbookRefs: readonly PreservedWorkbookReferencePlan[];
45
+ readonly threadedCommentNumbers: readonly number[];
46
+ readonly hasPersons: boolean;
47
+ }
48
+ export declare function contentTypesXml(inputs: ContentTypeInputs): string;
7
49
  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;
50
+ export declare function workbookXml(workbook: Workbook, plan: WorkbookRelPlan, pivots: readonly PivotPlan[]): string;
51
+ /**
52
+ * The workbook part's `.rels`, rendered from the ids the planner drew.
53
+ *
54
+ * Every id arrives in `plan`; none is computed here. It used to re-derive the styles, theme and
55
+ * shared-strings ids from the sheet count and a shared constant while `write.ts` independently summed
56
+ * the same inputs to place everything after them, so one sequence was spelled in two files and only
57
+ * its fixed part was shared. The relationship *order* is still stated here, because that is what this
58
+ * function is; the numbering is not, because two numberings of one sequence is how they drift apart.
59
+ */
60
+ export declare function workbookRelsXml(plan: WorkbookRelPlan, pivots: readonly PivotPlan[]): string;
11
61
  export declare function corePropsXml(properties: WorkbookProperties): string;
12
62
  export declare function appPropsXml(properties: WorkbookProperties): string;
@@ -1,11 +1,14 @@
1
1
  import { mangleFormula, quoteSheetName } from '../../core/formula.js';
2
2
  import { WORKBOOK_PROTECTION_CREDENTIAL_ATTRS } from '../../core/workbook-protection.js';
3
3
  import { isVisibility } from '../../core/worksheet.js';
4
- import { checkedToken, escapeAttr, escapeText, numAttr, textAttr, XML_DECLARATION, } from '../../xml/xml.js';
5
- import { extensionOf, relativePartPath, THEME_PART_PATH } from '../opc/part-paths.js';
4
+ import { AuthoringError, InternalError, quoted } from '../../errors.js';
5
+ import { isRelType } from '../../rel-type.js';
6
+ import { assertWritableDate, checkedToken, escapeAttr, escapeText, numAttr, textAttr, XML_DECLARATION, } from '../../xml/xml.js';
7
+ import { extensionOf, THEME_PART_PATH } from '../opc/part-paths.js';
6
8
  import { relationship, relationshipsPart } from '../opc/rels.js';
7
9
  import { imageContentType } from './images.js';
8
10
  import { SLICER_CACHES_EXT_URI } from './namespaces.js';
11
+ import { APP_PROPS_PART, commentsPart, CORE_PROPS_PART, drawingPart, PERSONS_PART, pivotCacheDefinitionPart, pivotCacheRecordsPart, pivotTablePart, SHARED_STRINGS_PART, STYLES_PART, tablePart, targetFromWorkbook, threadedCommentsPart, WORKBOOK_PART, worksheetPart, } from './part-names.js';
9
12
  import { NS, REL } from './relationships.js';
10
13
  import { x14Ext } from './x14-ext.js';
11
14
  const CT = {
@@ -28,15 +31,16 @@ const CT = {
28
31
  threadedComments: 'application/vnd.ms-excel.threadedcomments+xml',
29
32
  person: 'application/vnd.ms-excel.person+xml',
30
33
  };
31
- export function contentTypesXml(sheetCount, tables, commentNumbers, drawingNumbers, printerSettingsNumbers, mediaExtensions, hasSharedStrings, preservedParts, pivots, preservedWorkbookRefs, threadedCommentNumbers, hasPersons) {
32
- const extensionDefaults = buildExtensionDefaults(commentNumbers, printerSettingsNumbers, mediaExtensions, preservedParts);
34
+ export function contentTypesXml(inputs) {
35
+ const extensionDefaults = buildExtensionDefaults(inputs);
33
36
  return (XML_DECLARATION +
34
37
  `<Types xmlns="${NS.contentTypes}">` +
35
38
  contentTypeDefaults(extensionDefaults) +
36
- contentTypeOverrides(sheetCount, tables, drawingNumbers, commentNumbers, hasSharedStrings, preservedParts, pivots, preservedWorkbookRefs, extensionDefaults, threadedCommentNumbers, hasPersons) +
39
+ contentTypeOverrides(inputs, extensionDefaults) +
37
40
  '</Types>');
38
41
  }
39
- function buildExtensionDefaults(commentNumbers, printerSettingsNumbers, mediaExtensions, preservedParts) {
42
+ function buildExtensionDefaults(inputs) {
43
+ const { commentNumbers, printerSettingsNumbers, mediaExtensions, preservedParts } = inputs;
40
44
  const defaults = new Map();
41
45
  const add = (extension, contentType) => {
42
46
  const key = extension.toLowerCase();
@@ -53,71 +57,78 @@ function buildExtensionDefaults(commentNumbers, printerSettingsNumbers, mediaExt
53
57
  add(ext, imageContentType(ext));
54
58
  for (const part of preservedParts) {
55
59
  const ext = extensionOf(part.path);
56
- if (ext.toLowerCase() === 'xml')
60
+ if (ext === 'xml')
57
61
  continue;
58
62
  add(ext, part.contentType);
59
63
  }
60
64
  return defaults;
61
65
  }
62
66
  function isMacroEnabled(preservedWorkbookRefs) {
63
- return preservedWorkbookRefs.some((ref) => ref.relType.endsWith('/vbaProject'));
67
+ return preservedWorkbookRefs.some((ref) => isRelType(ref.relType, 'vbaProject'));
64
68
  }
65
69
  function contentTypeDefaults(extensionDefaults) {
66
70
  return [...extensionDefaults.values()]
67
71
  .map(({ extension, contentType }) => defaultType(extension, contentType))
68
72
  .join('');
69
73
  }
70
- function contentTypeOverrides(sheetCount, tables, drawingNumbers, commentNumbers, hasSharedStrings, preservedParts, pivots, preservedWorkbookRefs, extensionDefaults, threadedCommentNumbers, hasPersons) {
74
+ function contentTypeOverrides(inputs, extensionDefaults) {
75
+ const { sheetCount, tables, drawingNumbers, commentNumbers, threadedCommentNumbers, pivots, hasSharedStrings, hasPersons, preservedParts, preservedWorkbookRefs, } = inputs;
71
76
  const preservedOverrides = preservedParts
72
77
  .filter((part) => part.path !== THEME_PART_PATH)
73
- .filter((part) => extensionDefaults.get(extensionOf(part.path).toLowerCase())?.contentType !==
74
- part.contentType)
75
- .map((part) => override(`/${part.path}`, part.contentType));
78
+ .filter((part) => extensionDefaults.get(extensionOf(part.path))?.contentType !== part.contentType)
79
+ .map((part) => override(part.path, part.contentType));
76
80
  return [
77
- override('/xl/workbook.xml', isMacroEnabled(preservedWorkbookRefs) ? CT.macroEnabledWorkbook : CT.workbook),
78
- ...Array.from({ length: sheetCount }, (_, i) => override(`/xl/worksheets/sheet${i + 1}.xml`, CT.worksheet)),
79
- ...tables.map(({ number }) => override(`/xl/tables/table${number}.xml`, CT.table)),
80
- ...drawingNumbers.map((number) => override(`/xl/drawings/drawing${number}.xml`, CT.drawing)),
81
- ...commentNumbers.map((number) => override(`/xl/comments${number}.xml`, CT.comments)),
82
- ...threadedCommentNumbers.map((number) => override(`/xl/threadedComments/threadedComment${number}.xml`, CT.threadedComments)),
83
- ...pivots.map(({ number }) => override(`/xl/pivotTables/pivotTable${number}.xml`, CT.pivotTable)),
84
- ...pivots.map(({ number }) => override(`/xl/pivotCache/pivotCacheDefinition${number}.xml`, CT.pivotCacheDefinition)),
85
- ...pivots.map(({ number }) => override(`/xl/pivotCache/pivotCacheRecords${number}.xml`, CT.pivotCacheRecords)),
86
- override(`/${THEME_PART_PATH}`, CT.theme),
87
- override('/xl/styles.xml', CT.styles),
88
- ...(hasSharedStrings ? [override('/xl/sharedStrings.xml', CT.sharedStrings)] : []),
89
- ...(hasPersons ? [override('/xl/persons/person.xml', CT.person)] : []),
90
- override('/docProps/core.xml', CT.core),
91
- override('/docProps/app.xml', CT.app),
81
+ override(WORKBOOK_PART, isMacroEnabled(preservedWorkbookRefs) ? CT.macroEnabledWorkbook : CT.workbook),
82
+ ...Array.from({ length: sheetCount }, (_, i) => override(worksheetPart(i + 1), CT.worksheet)),
83
+ ...tables.map(({ number }) => override(tablePart(number), CT.table)),
84
+ ...drawingNumbers.map((number) => override(drawingPart(number), CT.drawing)),
85
+ ...commentNumbers.map((number) => override(commentsPart(number), CT.comments)),
86
+ ...threadedCommentNumbers.map((number) => override(threadedCommentsPart(number), CT.threadedComments)),
87
+ ...pivots.map(({ number }) => override(pivotTablePart(number), CT.pivotTable)),
88
+ ...pivots.map(({ number }) => override(pivotCacheDefinitionPart(number), CT.pivotCacheDefinition)),
89
+ ...pivots.map(({ number }) => override(pivotCacheRecordsPart(number), CT.pivotCacheRecords)),
90
+ override(THEME_PART_PATH, CT.theme),
91
+ override(STYLES_PART, CT.styles),
92
+ ...(hasSharedStrings ? [override(SHARED_STRINGS_PART, CT.sharedStrings)] : []),
93
+ ...(hasPersons ? [override(PERSONS_PART, CT.person)] : []),
94
+ override(CORE_PROPS_PART, CT.core),
95
+ override(APP_PROPS_PART, CT.app),
92
96
  ...preservedOverrides,
93
97
  ].join('');
94
98
  }
95
- function override(partName, contentType) {
96
- return `<Override PartName="${partName}" ContentType="${contentType}"/>`;
99
+ function override(partPath, contentType) {
100
+ return `<Override PartName="/${escapeAttr(partPath)}" ContentType="${escapeAttr(contentType)}"/>`;
97
101
  }
98
102
  function defaultType(extension, contentType) {
99
- return `<Default Extension="${extension}" ContentType="${contentType}"/>`;
103
+ return `<Default Extension="${escapeAttr(extension)}" ContentType="${escapeAttr(contentType)}"/>`;
100
104
  }
101
105
  export function rootRelsXml(rootRefs) {
102
106
  return relationshipsPart([
103
- relationship('rId1', REL.officeDocument, 'xl/workbook.xml'),
104
- relationship('rId2', REL.coreProps, 'docProps/core.xml'),
105
- relationship('rId3', REL.extProps, 'docProps/app.xml'),
107
+ relationship('rId1', REL.officeDocument, WORKBOOK_PART),
108
+ relationship('rId2', REL.coreProps, CORE_PROPS_PART),
109
+ relationship('rId3', REL.extProps, APP_PROPS_PART),
106
110
  ...rootRefs.map((ref, i) => relationship(`rId${4 + i}`, ref.relType, ref.entryPath)),
107
111
  ]);
108
112
  }
109
- export function workbookXml(workbook, preservedRels, pivots) {
113
+ export function workbookXml(workbook, plan, pivots) {
114
+ const preservedRels = plan.preservedWorkbookRels;
110
115
  const sheets = workbook.worksheets;
111
116
  const entries = sheets
112
117
  .map((sheet, i) => {
118
+ const relId = plan.sheetRelIds[i];
119
+ if (relId === undefined) {
120
+ throw new InternalError(`worksheet ${i + 1} of ${sheets.length} has no relationship id: the plan was drawn for ` +
121
+ `${plan.sheetRelIds.length} sheet(s)`);
122
+ }
113
123
  const state = sheet.state === 'visible'
114
124
  ? ''
115
125
  : ` state="${checkedToken(sheet.state, isVisibility, 'sheet visibility')}"`;
116
- return `<sheet name="${escapeAttr(sheet.name)}" sheetId="${sheet.id}"${state} r:id="rId${i + 1}"/>`;
126
+ return `<sheet name="${escapeAttr(sheet.name)}" sheetId="${sheet.id}"${state} r:id="${relId}"/>`;
117
127
  })
118
128
  .join('');
119
129
  return (XML_DECLARATION +
120
130
  `<workbook xmlns="${NS.main}" xmlns:r="${NS.docRels}">` +
131
+ workbookPrXml(workbook) +
121
132
  workbookProtectionXml(workbook) +
122
133
  bookViewsXml(workbook) +
123
134
  `<sheets>${entries}</sheets>` +
@@ -128,6 +139,11 @@ export function workbookXml(workbook, preservedRels, pivots) {
128
139
  workbookExtLstXml(preservedRels) +
129
140
  '</workbook>');
130
141
  }
142
+ function workbookPrXml(workbook) {
143
+ const attrs = (workbook.dateEpoch === 1904 ? ' date1904="1"' : '') +
144
+ (workbook.codeName === undefined ? '' : ` codeName="${escapeAttr(workbook.codeName)}"`);
145
+ return attrs === '' ? '' : `<workbookPr${attrs}/>`;
146
+ }
131
147
  function bookViewsXml(workbook) {
132
148
  const view = workbook.view;
133
149
  const activeTab = workbook.activeTabIndex;
@@ -145,23 +161,23 @@ function bookViewsXml(workbook) {
145
161
  function externalReferencesXml(preservedRels) {
146
162
  const links = preservedRels
147
163
  .filter((ref) => ref.externalReferenceIndex !== undefined)
148
- .sort((a, b) => a.externalReferenceIndex - b.externalReferenceIndex);
164
+ .sort((a, b) => (a.externalReferenceIndex ?? 0) - (b.externalReferenceIndex ?? 0));
149
165
  if (links.length === 0)
150
166
  return '';
151
167
  const entries = links.map((ref) => `<externalReference r:id="${ref.relId}"/>`).join('');
152
168
  return `<externalReferences>${entries}</externalReferences>`;
153
169
  }
154
170
  function workbookExtLstXml(preservedRels) {
155
- const caches = preservedRels.filter((ref) => ref.relType.endsWith('/slicerCache'));
171
+ const caches = preservedRels.filter((ref) => isRelType(ref.relType, 'slicerCache'));
156
172
  if (caches.length === 0)
157
173
  return '';
158
174
  const entries = caches.map((ref) => `<x14:slicerCache r:id="${ref.relId}"/>`).join('');
159
175
  return `<extLst>${x14Ext(SLICER_CACHES_EXT_URI, `<x14:slicerCaches>${entries}</x14:slicerCaches>`)}</extLst>`;
160
176
  }
161
177
  function pivotCachesXml(preservedRels, pivots) {
162
- const preserved = preservedRels
163
- .filter((ref) => ref.pivotCacheId !== undefined)
164
- .map((ref) => `<pivotCache cacheId="${escapeAttr(ref.pivotCacheId)}" r:id="${ref.relId}"/>`);
178
+ const preserved = preservedRels.flatMap((ref) => ref.pivotCacheId === undefined
179
+ ? []
180
+ : [`<pivotCache cacheId="${escapeAttr(ref.pivotCacheId)}" r:id="${ref.relId}"/>`]);
165
181
  const generated = pivots.map((pivot) => `<pivotCache cacheId="${escapeAttr(pivot.cacheId)}" r:id="${pivot.workbookRelId}"/>`);
166
182
  const entries = [...preserved, ...generated];
167
183
  if (entries.length === 0)
@@ -192,12 +208,20 @@ const EXCEL_CALC_ID = '171027';
192
208
  function calcPrXml(workbook) {
193
209
  return workbook.fullCalcOnLoad ? `<calcPr calcId="${EXCEL_CALC_ID}" fullCalcOnLoad="1"/>` : '';
194
210
  }
211
+ function localSheetId(sheets, name, scope) {
212
+ const wanted = scope.toLowerCase();
213
+ const index = sheets.findIndex((sheet) => sheet.name.toLowerCase() === wanted);
214
+ if (index === -1) {
215
+ throw new AuthoringError(`defined name ${quoted(name)} is scoped to worksheet ${quoted(scope)}, which this workbook does not contain`);
216
+ }
217
+ return index;
218
+ }
195
219
  function definedNamesXml(workbook) {
196
220
  const sheets = workbook.worksheets;
197
221
  const userEntries = workbook.definedNames.map((name) => {
198
222
  const scopeAttr = name.scope === undefined
199
223
  ? ''
200
- : ` localSheetId="${sheets.findIndex((sheet) => sheet.name === name.scope)}"`;
224
+ : ` localSheetId="${localSheetId(sheets, name.name, name.scope)}"`;
201
225
  const commentAttr = textAttr('comment', name.comment);
202
226
  const hiddenAttr = name.hidden ? ' hidden="1"' : '';
203
227
  return (`<definedName name="${escapeAttr(name.name)}"${scopeAttr}${commentAttr}${hiddenAttr}>` +
@@ -218,22 +242,23 @@ function filterDatabaseRefersTo(sheetName, range) {
218
242
  const absolute = range.replace(/([A-Z]+)(\d+)/g, '$$$1$$$2');
219
243
  return `${quoteSheetName(sheetName)}!${absolute}`;
220
244
  }
221
- export const FIXED_WORKBOOK_REL_COUNT = 2;
222
- export function workbookRelsXml(sheetCount, hasSharedStrings, personsRelId, preservedRels, pivots) {
245
+ export function workbookRelsXml(plan, pivots) {
246
+ const { personsRelId } = plan;
247
+ const preservedRels = plan.preservedWorkbookRels;
223
248
  return relationshipsPart([
224
- ...Array.from({ length: sheetCount }, (_, i) => relationship(`rId${i + 1}`, REL.worksheet, `worksheets/sheet${i + 1}.xml`)),
225
- relationship(`rId${sheetCount + 1}`, REL.styles, 'styles.xml'),
226
- relationship(`rId${sheetCount + FIXED_WORKBOOK_REL_COUNT}`, REL.theme, relativePartPath('xl/workbook.xml', THEME_PART_PATH)),
227
- ...(hasSharedStrings
228
- ? [
229
- relationship(`rId${sheetCount + FIXED_WORKBOOK_REL_COUNT + 1}`, REL.sharedStrings, 'sharedStrings.xml'),
230
- ]
231
- : []),
249
+ ...plan.sheetRelIds.map((relId, i) => relationship(relId, REL.worksheet, targetFromWorkbook(worksheetPart(i + 1)))),
250
+ relationship(plan.stylesRelId, REL.styles, targetFromWorkbook(STYLES_PART)),
251
+ relationship(plan.themeRelId, REL.theme, targetFromWorkbook(THEME_PART_PATH)),
252
+ ...(plan.sharedStringsRelId === null
253
+ ? []
254
+ : [
255
+ relationship(plan.sharedStringsRelId, REL.sharedStrings, targetFromWorkbook(SHARED_STRINGS_PART)),
256
+ ]),
232
257
  ...(personsRelId === null
233
258
  ? []
234
- : [relationship(personsRelId, REL.person, 'persons/person.xml')]),
235
- ...preservedRels.map((ref) => relationship(ref.relId, ref.relType, escapeAttr(relativePartPath('xl/workbook.xml', ref.entryPath)))),
236
- ...pivots.map((pivot) => relationship(pivot.workbookRelId, REL.pivotCacheDefinition, `pivotCache/pivotCacheDefinition${pivot.number}.xml`)),
259
+ : [relationship(personsRelId, REL.person, targetFromWorkbook(PERSONS_PART))]),
260
+ ...preservedRels.map((ref) => relationship(ref.relId, ref.relType, targetFromWorkbook(ref.entryPath))),
261
+ ...pivots.map((pivot) => relationship(pivot.workbookRelId, REL.pivotCacheDefinition, targetFromWorkbook(pivotCacheDefinitionPart(pivot.number)))),
237
262
  ]);
238
263
  }
239
264
  export function corePropsXml(properties) {
@@ -247,10 +272,10 @@ export function corePropsXml(properties) {
247
272
  if (properties.lastModifiedBy !== undefined) {
248
273
  parts.push(`<cp:lastModifiedBy>${escapeText(properties.lastModifiedBy)}</cp:lastModifiedBy>`);
249
274
  }
250
- if (properties.created) {
275
+ if (properties.created !== undefined) {
251
276
  parts.push(w3cdtf('created', properties.created));
252
277
  }
253
- if (properties.modified) {
278
+ if (properties.modified !== undefined) {
254
279
  parts.push(w3cdtf('modified', properties.modified));
255
280
  }
256
281
  return (XML_DECLARATION +
@@ -260,7 +285,9 @@ export function corePropsXml(properties) {
260
285
  '</cp:coreProperties>');
261
286
  }
262
287
  function w3cdtf(element, date) {
263
- return `<dcterms:${element} xsi:type="dcterms:W3CDTF">${date.toISOString()}</dcterms:${element}>`;
288
+ assertWritableDate(date, `the document property ${quoted(element)}`);
289
+ const stamp = date.toISOString().replace(/\.\d{3}Z$/, 'Z');
290
+ return `<dcterms:${element} xsi:type="dcterms:W3CDTF">${stamp}</dcterms:${element}>`;
264
291
  }
265
292
  export function appPropsXml(properties) {
266
293
  const company = properties.company === undefined ? '' : `<Company>${escapeText(properties.company)}</Company>`;