@shbernal/ts-xlsx 1.0.3 → 1.2.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 (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -0,0 +1,6 @@
1
+ import type { Color } from '../../core/style.ts';
2
+ /** Serialise a {@link Color} as the attribute list a `<color>`-shaped element carries. */
3
+ export declare function colorAttrs(color: Color): string;
4
+ export declare function parseColor(attrs: {
5
+ readonly [k: string]: string;
6
+ }): Color;
@@ -0,0 +1,42 @@
1
+ import { AuthoringError } from '../../errors.js';
2
+ function normalizeArgb(argb) {
3
+ const hex = argb.startsWith('#') ? argb.slice(1) : argb;
4
+ const rgb = hex.length === 6 ? `FF${hex}` : hex;
5
+ if (!/^[0-9a-fA-F]{8}$/.test(rgb)) {
6
+ throw new AuthoringError(`Invalid ARGB colour ${JSON.stringify(argb)}: expected 6 or 8 hexadecimal digits`);
7
+ }
8
+ return rgb;
9
+ }
10
+ export function colorAttrs(color) {
11
+ const parts = [];
12
+ if (color.argb !== undefined)
13
+ parts.push(`rgb="${normalizeArgb(color.argb)}"`);
14
+ if (color.theme !== undefined)
15
+ parts.push(`theme="${color.theme}"`);
16
+ if (color.tint !== undefined)
17
+ parts.push(`tint="${color.tint}"`);
18
+ if (color.indexed !== undefined)
19
+ parts.push(`indexed="${color.indexed}"`);
20
+ return parts.join(' ');
21
+ }
22
+ export function parseColor(attrs) {
23
+ const color = {};
24
+ if (attrs.rgb !== undefined)
25
+ color.argb = attrs.rgb;
26
+ if (attrs.theme !== undefined) {
27
+ const theme = Number(attrs.theme);
28
+ if (Number.isInteger(theme))
29
+ color.theme = theme;
30
+ }
31
+ if (attrs.tint !== undefined) {
32
+ const tint = Number(attrs.tint);
33
+ if (Number.isFinite(tint))
34
+ color.tint = tint;
35
+ }
36
+ if (attrs.indexed !== undefined) {
37
+ const indexed = Number(attrs.indexed);
38
+ if (Number.isInteger(indexed))
39
+ color.indexed = indexed;
40
+ }
41
+ return color;
42
+ }
@@ -1,49 +1,13 @@
1
- // Cell comments — the `xl/comments{n}.xml` part, its `xl/drawings/vmlDrawing{n}.vml` companion, and the
2
- // reader that maps a comment back onto its cell.
3
- //
4
- // A comment is anchored to a cell by A1 reference and rendered by Excel as a floating box. The box's
5
- // geometry lives in a legacy VML drawing (the pre-DrawingML shape format Excel still requires here);
6
- // the text lives in the comments part. Both are emitted together — a comments part with no matching
7
- // `<legacyDrawing>`/VML reads as text but renders nothing, so we never split them.
8
- //
9
- // Two different things share this one wire form:
10
- // • a user's **note** (`cell.note`) — a single anonymous annotation, the whole of what the part held
11
- // before 2018;
12
- // • the legacy **fallback** Excel writes beside every modern threaded comment (see
13
- // `threaded-comments.ts`), so a pre-2018 reader still sees the conversation. Its text is a fixed
14
- // boilerplate wrapping a copy of the thread, and its author is a synthetic `tc={headId}` entry.
15
- //
16
- // That `tc=` author and the comment's `xr:uid` are how Excel binds a cell back to its thread — not
17
- // decoration. Verified against desktop Excel: a package whose threadedComment part, persons registry,
18
- // relationships and content types all survive intact still reads back as ordinary notes with zero
19
- // threads once those two are lost. So the fallback is *derived from the thread model* on write and
20
- // *suppressed on read*, rather than round-tripped as a plain note.
21
1
  import { decodeAddress } from '../../core/address.js';
22
2
  import { escapeAttr, escapeText, textElement, XML_DECLARATION } from '../../xml/xml.js';
23
3
  import { localName, parseXml } from '../../xml/xml-read.js';
24
4
  import { MARKUP_COMPATIBILITY_NS, REVISION_NS, SPREADSHEETML_NS } from './namespaces.js';
25
- /**
26
- * Gather every comment a sheet must write: its cells' notes, plus one legacy fallback per conversation
27
- * in `threads`. A comment anchors to its cell regardless of the cell's value, so a note (or a thread) on
28
- * an otherwise-empty cell is collected too.
29
- *
30
- * `threads` is the conversations the *package* will carry, not simply the ones the sheet holds — the
31
- * caller decides, because a fallback beside a thread whose `threadedComment` part is missing is worse
32
- * than no fallback at all: verified against desktop Excel, such a comment shows as neither a thread nor
33
- * a note, so the text disappears entirely.
34
- *
35
- * Ordered by cell, row-major, the way Excel writes the list — so a fallback lands interleaved among the
36
- * notes rather than appended after them, and the VML shapes follow the same order.
37
- */
38
5
  export function collectComments(sheet, threads) {
39
6
  const fallbacks = threadFallbacks(threads);
40
7
  const anchored = new Set(fallbacks.map((fallback) => fallback.ref));
41
8
  const comments = [...fallbacks];
42
9
  for (const { cells } of sheet.rows()) {
43
10
  for (const cell of cells) {
44
- // Excel refuses to put a note and a thread on one cell, so a file carrying both (only a foreign
45
- // generator or a hand-edit makes one) is written back as the thread alone: two comments on one ref
46
- // is a shape Excel repairs by dropping both, which would lose the conversation as well as the note.
47
11
  if (cell.note === undefined || anchored.has(cell.address))
48
12
  continue;
49
13
  comments.push({ ref: cell.address, row: cell.row, col: cell.col, text: cell.note });
@@ -51,8 +15,6 @@ export function collectComments(sheet, threads) {
51
15
  }
52
16
  return comments.sort((a, b) => a.row - b.row || a.col - b.col);
53
17
  }
54
- // One legacy fallback per conversation, keyed to the thread head whose id binds it. A thread with no
55
- // messages has nothing to write and no id to bind by, so it contributes none.
56
18
  function threadFallbacks(threads) {
57
19
  const fallbacks = [];
58
20
  for (const thread of threads) {
@@ -64,31 +26,15 @@ function threadFallbacks(threads) {
64
26
  }
65
27
  return fallbacks;
66
28
  }
67
- // The boilerplate Excel puts in front of every fallback, captured verbatim from an Excel-authored file.
68
- // It is what a pre-2018 reader shows the user, so it is reproduced exactly rather than paraphrased.
69
29
  const FALLBACK_PREAMBLE = '[Threaded comment]\n\nYour version of Excel allows you to read this threaded comment; however, any ' +
70
30
  'edits to it will get removed if the file is opened in a newer version of Excel. Learn more: ' +
71
31
  'https://go.microsoft.com/fwlink/?linkid=870924\n\n';
72
- // A whole conversation flattened into the one comment a pre-2018 reader can render: the opening message
73
- // under `Comment:`, then each reply under its own `Reply:`, every body indented four spaces. Verified
74
- // against desktop Excel for a thread with three replies — `Reply:` repeats per reply rather than the
75
- // replies being joined under one heading.
76
32
  function fallbackText(thread) {
77
33
  const [head, ...replies] = thread.comments;
78
34
  const body = replies.map((reply) => `\nReply:\n ${reply.text}`).join('');
79
35
  return `${FALLBACK_PREAMBLE}Comment:\n ${head?.text ?? ''}${body}`;
80
36
  }
81
- // `xr:uid` lives in the 2014 revision namespace, declared `mc:Ignorable` exactly as Excel declares it so
82
- // a consumer that does not know the prefix skips the attribute instead of rejecting the part.
83
37
  const REVISION_NS_ATTRS = ` xmlns:mc="${MARKUP_COMPATIBILITY_NS}" mc:Ignorable="xr" xmlns:xr="${REVISION_NS}"`;
84
- /**
85
- * The `xl/comments{n}.xml` part.
86
- *
87
- * Authors are laid out the way Excel lays them out: one synthetic `tc={headId}` entry per threaded
88
- * conversation first, then a single anonymous author shared by every note (the model carries no note
89
- * author). Each comment points at its own author by index, and a fallback additionally carries the
90
- * `xr:uid` naming its thread — the pair that keeps Excel treating the cell as threaded.
91
- */
92
38
  export function commentsXml(comments) {
93
39
  const authorIdByThreadId = new Map();
94
40
  for (const { threadId } of comments) {
@@ -116,7 +62,6 @@ export function commentsXml(comments) {
116
62
  `<commentList>${list}</commentList>` +
117
63
  '</comments>');
118
64
  }
119
- // VML namespaces and the one shape type (a text box) every comment reuses.
120
65
  const VML_HEADER = '<xml xmlns:v="urn:schemas-microsoft-com:vml" ' +
121
66
  'xmlns:o="urn:schemas-microsoft-com:office:office" ' +
122
67
  'xmlns:x="urn:schemas-microsoft-com:office:excel">' +
@@ -124,11 +69,6 @@ const VML_HEADER = '<xml xmlns:v="urn:schemas-microsoft-com:vml" ' +
124
69
  '<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" ' +
125
70
  'path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/>' +
126
71
  '<v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>';
127
- /** The `xl/drawings/vmlDrawing{n}.vml` companion: one hidden text-box shape per comment, in the same
128
- * order as the comments part. Anchor coordinates place the box a couple of cells down-and-right of its
129
- * owner; Excel refines them on open, so the values are a sensible starting geometry rather than a
130
- * pixel-exact layout. A thread's fallback shape is `ObjectType="Note"` like any other — Excel draws the
131
- * threaded-comment card itself and only needs the shape to exist. */
132
72
  export function vmlDrawingXml(comments) {
133
73
  const shapes = comments
134
74
  .map((comment, i) => {
@@ -148,18 +88,8 @@ export function vmlDrawingXml(comments) {
148
88
  .join('');
149
89
  return `${VML_HEADER}${shapes}</xml>`;
150
90
  }
151
- // A comment names its author by index into `<authors>`, so an empty entry must still occupy its slot —
152
- // presenting the self-closing `<author/>` an author-less file writes as an empty element gives it the
153
- // close that pushes it. Without this every later index would shift by one and a note could inherit a
154
- // thread's `tc=` author.
155
91
  const COMMENT_EMPTY_CLOSES = new Set(['author']);
156
- // The author string marking a comment as a thread's legacy fallback: `tc={headThreadId}`.
157
92
  const THREAD_AUTHOR_PREFIX = 'tc=';
158
- /**
159
- * Parse a `comments{n}.xml` part into a map of A1 reference → comment. Text runs within one comment are
160
- * concatenated; an author-name run is Excel's own convention and is not stripped, so a note reads back
161
- * as exactly the text that was written.
162
- */
163
93
  export function parseComments(xml) {
164
94
  const comments = new Map();
165
95
  const authors = [];
@@ -210,26 +140,12 @@ export function parseComments(xml) {
210
140
  }, { closeEmptyElements: COMMENT_EMPTY_CLOSES });
211
141
  return comments;
212
142
  }
213
- // A missing or non-numeric `authorId` indexes nothing, so `authors[NaN]` is undefined and the comment
214
- // reads as a plain note — the safe direction, since mistaking a note for a fallback would delete it.
215
143
  function threadIdOf(author) {
216
144
  if (author === undefined || !author.startsWith(THREAD_AUTHOR_PREFIX))
217
145
  return undefined;
218
146
  const id = author.slice(THREAD_AUTHOR_PREFIX.length);
219
147
  return id === '' ? undefined : id;
220
148
  }
221
- /**
222
- * Apply a parsed comments part onto a sheet's cells as notes, addressing each by its A1 reference.
223
- *
224
- * A thread's legacy fallback is not a note and does not become one: its text is boilerplate wrapping a
225
- * copy of the conversation, so surfacing it as `cell.note` hands the caller garbage — and on write it
226
- * would be re-emitted as a plain note, destroying the `tc=`/`xr:uid` binding and leaving Excel unable to
227
- * see the thread at all.
228
- *
229
- * Suppressed only for a conversation the reader actually holds: a file whose thread part is missing or
230
- * damaged has nothing else left, so there the boilerplate is kept rather than the content lost. Call
231
- * after the sheet's threads are restored, since that is what this reads to decide.
232
- */
233
149
  export function applyNotes(sheet, comments) {
234
150
  const headIds = new Set(sheet.commentThreads.flatMap((thread) => {
235
151
  const head = thread.comments[0];
@@ -1,5 +1,5 @@
1
1
  import type { ConditionalFormatting } from '../../core/conditional-formatting.ts';
2
- import { type StyleRegistry } from './styles.ts';
2
+ import type { StyleRegistry } from './styles.ts';
3
3
  /**
4
4
  * Serialise every conditional formatting on a sheet into its `<conditionalFormatting>` blocks, in
5
5
  * insertion order. Returns '' when the sheet has none. A rule missing a `priority` is assigned the
@@ -1,47 +1,18 @@
1
- // Serialising conditional formatting to the classic `<conditionalFormatting>` worksheet element.
2
- //
3
- // Each block names its target range(s) in a `sqref` attribute and holds one or more `<cfRule>`
4
- // children. A rule's shape depends on its type: a `dataBar`/`colorScale`/`iconSet` carries a scale
5
- // element (its `<cfvo>` anchors and colours), while a `cellIs`/`expression`/`top10`/… carries its
6
- // operands as `<formula>` children and points at a differential style by `dxfId`. A rule the library
7
- // does not model in depth still round-trips its attributes, so nothing is silently dropped on save.
8
- //
9
- // A data bar's richer facets — its gradient fill, its negative-value fill colour, its axis colour —
10
- // have no home in the classic `<dataBar>` element; Excel stores them only in the 2009 x14 extension.
11
- // So a data-bar rule carrying any of them is written twice: the classic element (its anchors and bar
12
- // colour, understood by every consumer) plus an `<x14:dataBar>` in the worksheet `<extLst>` carrying
13
- // the extras, the two linked by a shared id. The reader folds the extension back onto the classic
14
- // rule, so the gradient flag and the two extra colours survive a round-trip rather than being dropped.
15
1
  import { boolAttr, escapeAttr, escapeText, stripFormulaEquals } from '../../xml/xml.js';
16
2
  import { boolStrict, coerceNumericLiteral, localName, parseXml } from '../../xml/xml-read.js';
17
- // The x14/xm extension namespaces and ext-URI GUIDs are declared inline on the `<ext>` elements
18
- // exactly as Excel writes them, so no worksheet-root xmlns is needed. `CF_EXT_URI` scopes the
19
- // worksheet's x14 conditional formattings; `DATABAR_LINK_EXT_URI` scopes the `<x14:id>` link a
20
- // classic cfRule carries to name its extension.
3
+ import { colorAttrs, parseColor } from './color-xml.js';
21
4
  import { CF_EXT_URI, DATABAR_LINK_EXT_URI, XM_NS } from './namespaces.js';
22
- import { colorAttrs, parseColor } from './styles.js';
23
5
  import { x14Ext } from './x14-ext.js';
24
- // Excel's default data bar when the author supplies none: a min/max anchor pair and its standard blue.
25
6
  const DEFAULT_DATABAR_CFVO = [{ type: 'min' }, { type: 'max' }];
26
7
  const DEFAULT_DATABAR_COLOR = { argb: 'FF638EC6' };
27
- // A data bar needs the x14 extension only when it carries a facet the classic element cannot express.
28
- // A plain data bar (anchors and bar colour alone) stays classic-only, so an unadorned rule never
29
- // fabricates an empty extension block.
30
8
  function needsDataBarExt(rule) {
31
9
  return (rule.gradient !== undefined ||
32
10
  rule.negativeFillColor !== undefined ||
33
11
  rule.axisColor !== undefined);
34
12
  }
35
- // The synthetic id linking a classic cfRule to its x14 extension. Excel uses a random GUID; any unique
36
- // token that matches on both ends works, so a deterministic per-sheet index keeps the output stable
37
- // and testable.
38
13
  function dataBarExtGuid(index) {
39
14
  return `{00000000-0000-0000-0000-${String(index + 1).padStart(12, '0')}}`;
40
15
  }
41
- // Assign every extended data bar its link id once, keyed by rule identity. The classic pass (which
42
- // emits the `<extLst>` link on the cfRule) and the extension pass (which emits the `<x14:cfRule id>`)
43
- // both read the id from this map, so the two ends of a link agree by construction — not by the two
44
- // passes happening to walk the rules in the same order. A rule absent from the map needs no extension.
45
16
  function dataBarExtLinks(formattings) {
46
17
  const links = new Map();
47
18
  let index = 0;
@@ -53,16 +24,8 @@ function dataBarExtLinks(formattings) {
53
24
  }
54
25
  return links;
55
26
  }
56
- // The three built-in visual rules. Each renders a built-in visual and carries no differential
57
- // style (so the write side skips the dxf), and each nests its `<color>` children differently (so
58
- // the read side, {@link ScaleKind}, tracks which one it is inside). This tuple drives both.
59
27
  const SCALE_KINDS = ['dataBar', 'colorScale', 'iconSet'];
60
28
  const SCALE_TYPES = new Set(SCALE_KINDS);
61
- /**
62
- * Serialise every conditional formatting on a sheet into its `<conditionalFormatting>` blocks, in
63
- * insertion order. Returns '' when the sheet has none. A rule missing a `priority` is assigned the
64
- * next free one, so the output always satisfies Excel's requirement that every cfRule carry one.
65
- */
66
29
  export function conditionalFormattingsXml(formattings, styles) {
67
30
  if (formattings.length === 0)
68
31
  return '';
@@ -70,13 +33,6 @@ export function conditionalFormattingsXml(formattings, styles) {
70
33
  const extLinks = dataBarExtLinks(formattings);
71
34
  return formattings.map((cf) => blockXml(cf, styles, priority, extLinks)).join('');
72
35
  }
73
- /**
74
- * The worksheet `<extLst>` `<ext>` carrying the x14 data-bar extensions, or '' when no data bar needs
75
- * one. Each extension's `<x14:cfRule id>` is read from the same {@link dataBarExtLinks} map the classic
76
- * pass uses for the `<x14:id>` its cfRule carries, so the two ends of a link always match. Emitted bare
77
- * (no `<extLst>` wrapper) so the worksheet serialiser can gather it into a single `<extLst>` beside the
78
- * data-validation extension.
79
- */
80
36
  export function conditionalFormattingsExtXml(formattings) {
81
37
  const extLinks = dataBarExtLinks(formattings);
82
38
  const items = [];
@@ -91,9 +47,6 @@ export function conditionalFormattingsExtXml(formattings) {
91
47
  return '';
92
48
  return x14Ext(CF_EXT_URI, `<x14:conditionalFormattings>${items.join('')}</x14:conditionalFormattings>`);
93
49
  }
94
- // One `<x14:conditionalFormatting>`: an `<x14:cfRule type="dataBar" id>` mirroring the classic anchors
95
- // as `<x14:cfvo>` and adding the facets the classic element cannot carry (gradient, negative-fill and
96
- // axis colours), with the target range in an `<xm:sqref>` child — the shape Excel writes.
97
50
  function x14DataBarXml(ref, rule, guid) {
98
51
  const cfvo = rule.cfvo && rule.cfvo.length > 0 ? rule.cfvo : DEFAULT_DATABAR_CFVO;
99
52
  const anchors = cfvo.map(x14CfvoXml).join('');
@@ -107,15 +60,12 @@ function x14DataBarXml(ref, rule, guid) {
107
60
  `<x14:dataBar${gradient}>${anchors}${negative}${axis}</x14:dataBar>` +
108
61
  `</x14:cfRule><xm:sqref>${escapeText(ref)}</xm:sqref></x14:conditionalFormatting>`);
109
62
  }
110
- // An x14 scale anchor. A `min`/`max` carries no value and self-closes; the rest wrap their value in an
111
- // `<xm:f>` (the extension form stores every anchor value as a formula).
112
63
  function x14CfvoXml(cfvo) {
113
64
  const type = escapeAttr(cfvo.type);
114
65
  if (cfvo.value === undefined)
115
66
  return `<x14:cfvo type="${type}"/>`;
116
67
  return `<x14:cfvo type="${type}"><xm:f>${escapeText(String(cfvo.value))}</xm:f></x14:cfvo>`;
117
68
  }
118
- // The `<extLst>` a classic data-bar cfRule carries to name its x14 extension by shared id.
119
69
  function cfRuleExtLinkXml(guid) {
120
70
  return `<extLst>${x14Ext(DATABAR_LINK_EXT_URI, `<x14:id>${guid}</x14:id>`)}</extLst>`;
121
71
  }
@@ -125,7 +75,6 @@ function blockXml(cf, styles, priority, extLinks) {
125
75
  }
126
76
  function ruleXml(rule, styles, priority, extLinks) {
127
77
  const p = rule.priority ?? priority.next;
128
- // Keep the running counter ahead of any explicit priority so later auto-assigned ones stay unique.
129
78
  priority.next = Math.max(priority.next, p) + 1;
130
79
  const attrs = [`type="${escapeAttr(rule.type)}"`];
131
80
  const dxfId = resolveDxfId(rule, styles);
@@ -153,9 +102,6 @@ function ruleXml(rule, styles, priority, extLinks) {
153
102
  if (rule.stdDev !== undefined)
154
103
  attrs.push(`stdDev="${rule.stdDev}"`);
155
104
  let body = SCALE_TYPES.has(rule.type) ? scaleXml(rule) : formulaeXml(rule.formulae);
156
- // A data bar with x14-only facets links to its extension by the id assigned in dataBarExtLinks; the
157
- // extension itself rides in the worksheet <extLst>. The link is the cfRule's last child, after the
158
- // dataBar. A rule absent from the map carries no extension.
159
105
  const extGuid = extLinks.get(rule);
160
106
  if (extGuid !== undefined)
161
107
  body += cfRuleExtLinkXml(extGuid);
@@ -163,9 +109,6 @@ function ruleXml(rule, styles, priority, extLinks) {
163
109
  ? `<cfRule ${attrs.join(' ')}/>`
164
110
  : `<cfRule ${attrs.join(' ')}>${body}</cfRule>`;
165
111
  }
166
- // A rule points at a differential style either by a preserved index read from a file (`dxfId`) or by
167
- // a style authored on the rule (interned here). The preserved index wins — it references the original
168
- // file's dxf table, which the writer re-emits verbatim. Scale rules never carry one.
169
112
  function resolveDxfId(rule, styles) {
170
113
  if (SCALE_TYPES.has(rule.type))
171
114
  return undefined;
@@ -187,19 +130,12 @@ function scaleXml(rule) {
187
130
  return colorScaleXml(rule);
188
131
  return iconSetXml(rule);
189
132
  }
190
- // A data bar states its low and high anchors and its bar colour. The minimal call (no cfvo, no colour)
191
- // gains Excel's own defaults — a min/max anchor pair and the standard blue — rather than an invalid
192
- // empty element. The gradient flag and the negative-fill/axis colours have no home in this classic
193
- // element; they ride in the x14 extension (see {@link conditionalFormattingsExtXml}), linked from the
194
- // cfRule that wraps this by a shared id.
195
133
  function dataBarXml(rule) {
196
134
  const cfvo = rule.cfvo && rule.cfvo.length > 0 ? rule.cfvo : DEFAULT_DATABAR_CFVO;
197
135
  const color = rule.color ?? DEFAULT_DATABAR_COLOR;
198
136
  const anchors = cfvo.map(cfvoXml).join('');
199
137
  return `<dataBar>${anchors}<color ${colorAttrs(color)}/></dataBar>`;
200
138
  }
201
- // A colour scale pairs each anchor with a colour; a missing colour list falls back to none, still a
202
- // well-formed (if plain) element.
203
139
  function colorScaleXml(rule) {
204
140
  const anchors = (rule.cfvo ?? []).map(cfvoXml).join('');
205
141
  const colors = (rule.colors ?? []).map((c) => `<color ${colorAttrs(c)}/>`).join('');
@@ -210,20 +146,10 @@ function iconSetXml(rule) {
210
146
  const anchors = (rule.cfvo ?? []).map(cfvoXml).join('');
211
147
  return `<iconSet${name}>${anchors}</iconSet>`;
212
148
  }
213
- // One scale anchor. `min`/`max` carry no value; the rest state theirs in `val` (a formula anchor's
214
- // value is its formula text, escaped like any attribute).
215
149
  function cfvoXml(cfvo) {
216
150
  const val = cfvo.value !== undefined ? ` val="${escapeAttr(String(cfvo.value))}"` : '';
217
151
  return `<cfvo type="${escapeAttr(cfvo.type)}"${val}/>`;
218
152
  }
219
- /**
220
- * Parse a worksheet's conditional formatting into the model. The classic `<conditionalFormatting>`
221
- * blocks supply every rule; the x14 extension (`<x14:conditionalFormatting>` inside `<extLst>`) is
222
- * read only to enrich a classic data bar with the facets the classic element cannot carry — the
223
- * gradient flag and the negative-fill and axis colours — matched by the shared id the two ends link
224
- * on. An extension rule with no classic counterpart (a rule that lives only in x14) is ignored, so it
225
- * is never half-read into a broken classic rule.
226
- */
227
153
  export function parseConditionalFormattings(xml) {
228
154
  const blocks = [];
229
155
  let block;
@@ -231,9 +157,6 @@ export function parseConditionalFormattings(xml) {
231
157
  let scale;
232
158
  let capturingFormula = false;
233
159
  let formulaText = '';
234
- // Classic data-bar rules that named an extension, paired with the id they linked on, plus the
235
- // extensions gathered from the worksheet <extLst>. The two are married after the pass — the
236
- // extension always follows the classic blocks in document order, so it is known by then.
237
160
  const linked = [];
238
161
  const extById = new Map();
239
162
  let x14Ext;
@@ -244,8 +167,6 @@ export function parseConditionalFormattings(xml) {
244
167
  onOpen(name, attrs, selfClosing) {
245
168
  const ln = localName(name);
246
169
  if (name.includes(':')) {
247
- // The `<x14:id>` a classic data bar carries to name its extension: capture its text into the
248
- // open draft. The rest are the worksheet extension's own elements.
249
170
  if (ln === 'id' && draft !== undefined) {
250
171
  capturingX14Id = true;
251
172
  x14IdText = '';
@@ -255,7 +176,6 @@ export function parseConditionalFormattings(xml) {
255
176
  x14ExtId = attrs.id;
256
177
  }
257
178
  else if (x14Ext !== undefined && ln === 'dataBar') {
258
- // gradient defaults to true in the x14 schema, so an absent attribute reads as a gradient.
259
179
  x14Ext.gradient = attrs.gradient !== '0';
260
180
  }
261
181
  else if (x14Ext !== undefined && ln === 'negativeFillColor') {
@@ -270,8 +190,6 @@ export function parseConditionalFormattings(xml) {
270
190
  block = { ref: attrs.sqref ?? '', rules: [] };
271
191
  }
272
192
  else if (ln === 'cfRule' && block !== undefined) {
273
- // A rule with no operands (e.g. duplicateValues) is a self-closing element that fires no
274
- // close event, so it must be finalised here; one with children waits for its </cfRule>.
275
193
  if (selfClosing) {
276
194
  block.rules.push(finalizeRule(newDraft(attrs)));
277
195
  }
@@ -360,12 +278,6 @@ export function parseConditionalFormattings(xml) {
360
278
  function emptyExt() {
361
279
  return { gradient: undefined, negativeFillColor: undefined, axisColor: undefined };
362
280
  }
363
- /**
364
- * Extract the differential-style (`<dxf>`) fragments from styles.xml, each verbatim. Preserving the
365
- * raw XML — rather than reparsing and re-serialising — is what keeps a foreign dxf's number format a
366
- * real format code on re-write instead of a coerced `"[object Object]"`, and keeps every conditional
367
- * formatting's `dxfId` index pointing at the same style it did in the source file.
368
- */
369
281
  export function parseDxfs(stylesXml) {
370
282
  const block = /<dxfs\b[^>]*>([\s\S]*?)<\/dxfs>/.exec(stylesXml);
371
283
  if (block === null)
@@ -385,7 +297,6 @@ function newDraft(attrs) {
385
297
  stdDev: parseFiniteAttr(attrs.stdDev),
386
298
  percent: boolStrict(attrs.percent),
387
299
  bottom: boolStrict(attrs.bottom),
388
- // aboveAverage defaults to true in OOXML; only an explicit "0" means below-average.
389
300
  aboveAverage: attrs.aboveAverage === undefined ? undefined : attrs.aboveAverage !== '0',
390
301
  equalAverage: boolStrict(attrs.equalAverage),
391
302
  dxfId: parseIndexAttr(attrs.dxfId),
@@ -397,18 +308,12 @@ function newDraft(attrs) {
397
308
  x14Id: undefined,
398
309
  };
399
310
  }
400
- // priority/rank/stdDev must be finite; a malformed value is dropped rather than propagated as NaN —
401
- // `priority` in particular feeds the writer's running priority counter (see `ruleXml`), so one bad
402
- // value would otherwise poison every later rule's auto-assigned priority on the same sheet.
403
311
  function parseFiniteAttr(value) {
404
312
  if (value === undefined)
405
313
  return undefined;
406
314
  const n = Number(value);
407
315
  return Number.isFinite(n) ? n : undefined;
408
316
  }
409
- // dxfId is preserved as the raw string (not renumbered) so it keeps pointing at the same slot in the
410
- // dxf table on re-write; it must still be a non-negative integer, so a malformed value is dropped
411
- // rather than later coercing to `dxfId="NaN"` in {@link resolveDxfId}.
412
317
  function parseIndexAttr(value) {
413
318
  if (value === undefined)
414
319
  return undefined;
@@ -457,7 +362,6 @@ function parseCfvo(attrs) {
457
362
  const type = (attrs.type ?? 'num');
458
363
  const cfvo = { type };
459
364
  if (attrs.val !== undefined) {
460
- // A `formula` anchor's value is an expression and stays a string; the rest are numeric.
461
365
  cfvo.value = type === 'formula' ? attrs.val : coerceNumericLiteral(attrs.val);
462
366
  }
463
367
  return cfvo;
@@ -1,30 +1,8 @@
1
- // Data validation I/O — the sheet-level `<dataValidations>` element and the reader that folds each
2
- // rule back onto its range.
3
- //
4
- // A validation stores its operands in `<formula1>`/`<formula2>` child elements and its target in a
5
- // `sqref` attribute. The operand text carries NO leading '=' (the '=' is an authoring convention, not
6
- // part of the stored formula), so the writer strips one and the reader keeps whatever it finds. A
7
- // numeric-typed rule's literal operand parses to a number; a cell reference, defined name, or list
8
- // source keeps its string, so a reference is never coerced to NaN and lost.
9
- //
10
- // The extended `<x14:dataValidation>` form (2009 extension schema) carries the validations a legacy
11
- // element cannot express — chiefly a list whose source lives on another sheet. It lives in the
12
- // worksheet `<extLst>`, keeps its target in a `<xm:sqref>` child rather than a `sqref` attribute, and
13
- // wraps each operand in an `<xm:f>` under `<x14:formula1>`/`<x14:formula2>`. A rule read from that
14
- // form is tagged `extended` so it is written back there; the two forms are parsed and serialised by
15
- // prefix so neither reader mistakes one for the other.
16
1
  import { escapeAttr, escapeText, stripFormulaEquals } from '../../xml/xml.js';
17
2
  import { boolStrict, coerceNumericLiteral, localName, parseXml } from '../../xml/xml-read.js';
18
- // The x14/xm extension namespaces and `DATA_VALIDATION_EXT_URI` are declared inline on the elements
19
- // that need them, exactly as Excel writes them, so the block is self-contained and the worksheet root
20
- // needs no extra namespace declaration.
21
3
  import { DATA_VALIDATION_EXT_URI, XM_NS } from './namespaces.js';
22
4
  import { x14Ext } from './x14-ext.js';
23
- // The typed validations whose literal operands are numbers; `list`/`custom` operands stay strings.
24
5
  const TYPED = new Set(['whole', 'decimal', 'date', 'time', 'textLength']);
25
- /** The standard `<dataValidations>` element for the rules stored in the legacy form, or '' when the
26
- * sheet has none of them — so a sheet with only extended (or no) validations stays byte-clean here.
27
- * The extended rules are emitted separately by {@link dataValidationsExtXml}. */
28
6
  export function dataValidationsXml(entries) {
29
7
  const standard = entries.filter((entry) => !entry.extended);
30
8
  if (standard.length === 0)
@@ -32,9 +10,6 @@ export function dataValidationsXml(entries) {
32
10
  const items = standard.map(({ sqref, rule }) => dataValidationXml(sqref, rule)).join('');
33
11
  return `<dataValidations count="${standard.length}">${items}</dataValidations>`;
34
12
  }
35
- /** The `<ext>` carrying the extended (`<x14:dataValidation>`) rules, or '' when the sheet declares
36
- * none. Emitted bare (no `<extLst>` wrapper) so the worksheet serialiser can gather it into a single
37
- * `<extLst>` beside the conditional-formatting extension — a worksheet may carry at most one. */
38
13
  export function dataValidationsExtXml(entries) {
39
14
  const extended = entries.filter((entry) => entry.extended);
40
15
  if (extended.length === 0)
@@ -42,10 +17,6 @@ export function dataValidationsExtXml(entries) {
42
17
  const items = extended.map(({ sqref, rule }) => extendedDataValidationXml(sqref, rule)).join('');
43
18
  return x14Ext(DATA_VALIDATION_EXT_URI, `<x14:dataValidations count="${extended.length}" xmlns:xm="${XM_NS}">${items}</x14:dataValidations>`);
44
19
  }
45
- // The shared attributes of a validation, in CT_DataValidation order: type, errorStyle, operator,
46
- // allowBlank, showInputMessage, showErrorMessage, errorTitle, error, promptTitle, prompt. The target
47
- // range differs between the two forms (a `sqref` attribute vs an `<xm:sqref>` child), so it is not
48
- // part of this shared prefix.
49
20
  function ruleAttrs(rule) {
50
21
  return (` type="${rule.type}"` +
51
22
  (rule.errorStyle !== undefined ? ` errorStyle="${rule.errorStyle}"` : '') +
@@ -58,25 +29,17 @@ function ruleAttrs(rule) {
58
29
  (rule.promptTitle !== undefined ? ` promptTitle="${escapeAttr(rule.promptTitle)}"` : '') +
59
30
  (rule.prompt !== undefined ? ` prompt="${escapeAttr(rule.prompt)}"` : ''));
60
31
  }
61
- // The standard element: shared attributes, then `sqref` last, then `<formula1>`/`<formula2>` bodies.
62
32
  function dataValidationXml(sqref, rule) {
63
33
  const [f1, f2] = operands(rule);
64
34
  const body = (f1 !== undefined ? `<formula1>${escapeText(stripFormulaEquals(f1))}</formula1>` : '') +
65
35
  (f2 !== undefined ? `<formula2>${escapeText(stripFormulaEquals(f2))}</formula2>` : '');
66
36
  return `<dataValidation${ruleAttrs(rule)} sqref="${escapeAttr(sqref)}">${body}</dataValidation>`;
67
37
  }
68
- // A rule's two operands with any non-finite numeric bound dropped: a NaN/±Infinity operand (e.g. a
69
- // date validation whose bound failed to coerce to a serial) has no OOXML representation, so it is
70
- // omitted rather than serialised as the literal "NaN" — the same graceful degradation a non-finite
71
- // cell value gets.
72
38
  function operands(rule) {
73
39
  const drop = (v) => typeof v === 'number' && !Number.isFinite(v) ? undefined : v;
74
40
  const [f1, f2] = rule.formulae ?? [];
75
41
  return [drop(f1), drop(f2)];
76
42
  }
77
- // The extended element: same shared attributes, but each operand wraps in `<x14:formula1><xm:f>…` and
78
- // the target range is an `<xm:sqref>` child that follows the formulae. The `xr:uid` Excel adds is
79
- // revision metadata it regenerates freely, so it is not modelled or re-emitted.
80
43
  function extendedDataValidationXml(sqref, rule) {
81
44
  const [f1, f2] = operands(rule);
82
45
  const body = (f1 !== undefined
@@ -88,7 +51,6 @@ function extendedDataValidationXml(sqref, rule) {
88
51
  `<xm:sqref>${escapeText(sqref)}</xm:sqref>`;
89
52
  return `<x14:dataValidation${ruleAttrs(rule)}>${body}</x14:dataValidation>`;
90
53
  }
91
- /** Parse every standard `<dataValidation>` out of a worksheet part into range-bound rules. */
92
54
  export function parseDataValidations(xml) {
93
55
  const entries = [];
94
56
  let current;
@@ -96,7 +58,6 @@ export function parseDataValidations(xml) {
96
58
  parseXml(xml, {
97
59
  onOpen(name, attrs) {
98
60
  const ln = localName(name);
99
- // Only the standard, unprefixed element — an `x14:dataValidation` is left for the extended path.
100
61
  if (ln === 'dataValidation' && !name.includes(':')) {
101
62
  current = { attrs, formulae: [] };
102
63
  }
@@ -137,9 +98,6 @@ function buildEntry(attrs, formulae) {
137
98
  const rule = buildRule(attrs, formulae);
138
99
  return rule === undefined ? undefined : { sqref, rule };
139
100
  }
140
- // The rule carried by a validation element of either form: its attributes decide the type, operator,
141
- // flags, and messages; its `<formula1>`/`<formula2>` operands become `formulae`. The target range is
142
- // supplied separately by each form's caller, so it is not read here.
143
101
  function buildRule(attrs, formulae) {
144
102
  const { type } = attrs;
145
103
  if (type === undefined)
@@ -149,8 +107,6 @@ function buildRule(attrs, formulae) {
149
107
  rule.operator = attrs.operator;
150
108
  }
151
109
  else if (TYPED.has(type)) {
152
- // Excel omits `operator="between"` because it is the default for a typed rule; restore it so a
153
- // reader sees the operator the rule actually enforces.
154
110
  rule.operator = 'between';
155
111
  }
156
112
  if (boolStrict(attrs.allowBlank))
@@ -169,9 +125,6 @@ function buildRule(attrs, formulae) {
169
125
  rule.prompt = attrs.prompt;
170
126
  if (attrs.promptTitle !== undefined)
171
127
  rule.promptTitle = attrs.promptTitle;
172
- // A `list`/`custom` operand is always a string (a source list or an expression); every other
173
- // type's literal operand coerces to a number when it is one, so a numeric bound reads back as a
174
- // number while a cell reference or defined name survives as its verbatim string.
175
128
  const parsed = formulae
176
129
  .filter((f) => f !== undefined)
177
130
  .map((f) => (type === 'list' || type === 'custom' ? f : coerceNumericLiteral(f)));
@@ -179,15 +132,9 @@ function buildRule(attrs, formulae) {
179
132
  rule.formulae = parsed;
180
133
  return rule;
181
134
  }
182
- /** Parse every extended `<x14:dataValidation>` out of a worksheet's `<extLst>` into range-bound
183
- * rules tagged `extended`, so a cross-sheet or whole-column list validation Excel stored only in the
184
- * 2009 extension form is read back rather than dropped. The standard parser ignores these (they are
185
- * prefixed); this one, symmetrically, handles only the prefixed elements. */
186
135
  export function parseExtendedDataValidations(xml) {
187
136
  const entries = [];
188
137
  let current;
189
- // Which operand an `<xm:f>` feeds (set by the enclosing `<x14:formula1>`/`<x14:formula2>`), and
190
- // which child element's text is currently being gathered.
191
138
  let slot;
192
139
  let capture;
193
140
  let text = '';
@@ -195,7 +142,6 @@ export function parseExtendedDataValidations(xml) {
195
142
  onOpen(name, attrs) {
196
143
  const ln = localName(name);
197
144
  const prefixed = name.includes(':');
198
- // A `<x14:dataValidation>`; its attributes (type, flags, messages) build the rule.
199
145
  if (ln === 'dataValidation' && prefixed) {
200
146
  current = { attrs, formulae: [], sqref: '' };
201
147
  }
@@ -249,8 +195,6 @@ function buildExtendedEntry(attrs, formulae, sqref) {
249
195
  const rule = buildRule(attrs, formulae);
250
196
  return rule === undefined ? undefined : { sqref, rule, extended: true };
251
197
  }
252
- /** Fold parsed validations onto a sheet, each bound to its original range and carrying its form: an
253
- * `extended` entry is re-attached as extended so a round-trip writes it back to the x14 block. */
254
198
  export function applyDataValidations(sheet, entries) {
255
199
  for (const { sqref, rule, extended } of entries) {
256
200
  sheet.addDataValidation(sqref, rule, extended ? { extended: true } : {});