reamkit 1.15.4 → 1.16.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 (94) hide show
  1. package/dist/esm/core/converter/facade.d.ts +5 -0
  2. package/dist/esm/core/converter/ream.d.ts +6 -0
  3. package/dist/esm/core/converter/ream.js +34 -8
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +187 -1
  6. package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
  7. package/dist/esm/core/drawingml/chart-geometry.js +371 -88
  8. package/dist/esm/core/drawingml/chart-parser.js +262 -11
  9. package/dist/esm/core/drawingml/colors.d.ts +6 -6
  10. package/dist/esm/core/drawingml/colors.js +58 -18
  11. package/dist/esm/core/drawingml/preset-geometry.js +7 -6
  12. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +74 -1
  15. package/dist/esm/core/font/ttf-subset.js +26 -1
  16. package/dist/esm/core/images.js +172 -43
  17. package/dist/esm/core/indexed-colors.d.ts +11 -0
  18. package/dist/esm/core/indexed-colors.js +81 -0
  19. package/dist/esm/core/ir/sheet.d.ts +61 -0
  20. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  21. package/dist/esm/core/number-format.js +800 -0
  22. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  23. package/dist/esm/core/opc/alternate-content.js +72 -0
  24. package/dist/esm/core/opc/core-properties.js +1 -0
  25. package/dist/esm/core/opc/package.d.ts +14 -1
  26. package/dist/esm/core/opc/package.js +43 -4
  27. package/dist/esm/core/opc/relationships.js +1 -0
  28. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  29. package/dist/esm/core/opc/xml-entities.js +91 -0
  30. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  31. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  32. package/dist/esm/core/vector.d.ts +13 -0
  33. package/dist/esm/excel/activex-parser.js +1 -0
  34. package/dist/esm/excel/column-bands.d.ts +26 -1
  35. package/dist/esm/excel/column-bands.js +156 -20
  36. package/dist/esm/excel/comments-parser.js +5 -3
  37. package/dist/esm/excel/conditional-format.d.ts +25 -2
  38. package/dist/esm/excel/conditional-format.js +63 -19
  39. package/dist/esm/excel/escaped-text.d.ts +9 -0
  40. package/dist/esm/excel/escaped-text.js +20 -0
  41. package/dist/esm/excel/form-control-parser.js +1 -0
  42. package/dist/esm/excel/formula/dates.js +1 -1
  43. package/dist/esm/excel/header-footer.d.ts +9 -1
  44. package/dist/esm/excel/header-footer.js +162 -14
  45. package/dist/esm/excel/index.d.ts +1 -1
  46. package/dist/esm/excel/index.js +2 -2
  47. package/dist/esm/excel/pivot-table-parser.js +1 -0
  48. package/dist/esm/excel/print-model.d.ts +75 -0
  49. package/dist/esm/excel/print-model.js +898 -76
  50. package/dist/esm/excel/printer-settings.d.ts +16 -0
  51. package/dist/esm/excel/printer-settings.js +36 -0
  52. package/dist/esm/excel/rich-value.d.ts +13 -0
  53. package/dist/esm/excel/rich-value.js +120 -0
  54. package/dist/esm/excel/shared-strings-parser.js +11 -7
  55. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  56. package/dist/esm/excel/sheet-drawing.js +26 -3
  57. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  58. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  59. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  60. package/dist/esm/excel/sheet-to-flow.js +551 -36
  61. package/dist/esm/excel/slicer-parser.js +1 -0
  62. package/dist/esm/excel/styles-parser.d.ts +30 -2
  63. package/dist/esm/excel/styles-parser.js +177 -40
  64. package/dist/esm/excel/table-parser.js +1 -0
  65. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  66. package/dist/esm/excel/vml-drawing.js +158 -0
  67. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  68. package/dist/esm/excel/workbook-parser.js +5 -1
  69. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  70. package/dist/esm/excel/worksheet-parser.js +150 -61
  71. package/dist/esm/excel/xlsx-reader.js +242 -11
  72. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  73. package/dist/esm/html/html-writer.js +11 -2
  74. package/dist/esm/layout/page-doc.d.ts +26 -0
  75. package/dist/esm/layout/styled-layout.d.ts +2 -0
  76. package/dist/esm/layout/styled-layout.js +493 -64
  77. package/dist/esm/pdf/cid-font.js +20 -9
  78. package/dist/esm/pdf/objects.d.ts +13 -0
  79. package/dist/esm/pdf/objects.js +18 -1
  80. package/dist/esm/pdf/styled-page-emitter.js +164 -32
  81. package/dist/esm/pdf/text-page-renderer.js +1 -0
  82. package/dist/esm/pdf/vector-graphics.d.ts +3 -1
  83. package/dist/esm/pdf/vector-graphics.js +19 -2
  84. package/dist/esm/pptx/pptx-reader.js +1 -0
  85. package/dist/esm/pptx/slide-parser.js +12 -1
  86. package/dist/esm/svg/svg-writer.js +23 -4
  87. package/dist/esm/word/document-parser.js +1 -0
  88. package/dist/esm/word/drawing-parser.d.ts +34 -1
  89. package/dist/esm/word/drawing-parser.js +52 -2
  90. package/dist/esm/word/numbering-parser.js +1 -0
  91. package/dist/esm/word/settings-parser.js +1 -0
  92. package/dist/esm/word/styles-parser.js +1 -0
  93. package/package.json +7 -3
  94. package/dist/esm/excel/number-format.js +0 -476
@@ -1,5 +1,74 @@
1
1
  import { twipsToPt } from "../core/ir/units.js";
2
2
  //#region src/excel/column-bands.ts
3
+ /** §18.3.1.70 — the width of the row-number column, in twips. */
4
+ var HEADING_COL_TWIPS = 460;
5
+ /** The A1-style letters of an absolute column index (0 → A, 26 → AA). */
6
+ function columnLetters(index) {
7
+ let n = index;
8
+ let out = "";
9
+ do {
10
+ out = String.fromCharCode(65 + n % 26) + out;
11
+ n = Math.floor(n / 26) - 1;
12
+ } while (n >= 0);
13
+ return out;
14
+ }
15
+ /**
16
+ * Wrap a grid in its printed row and column headings (§18.3.1.70 `headings`):
17
+ * the column letters across the top, the row numbers down the left, each in a
18
+ * thin box, the way the sheet looks on screen. A banded sheet gets its own
19
+ * letters per band, which is what both references print.
20
+ *
21
+ * @param rows The band's rows.
22
+ * @param widths The band's column widths in twips.
23
+ * @param colStart The absolute index of the band's first column.
24
+ * @param rowNumbers The absolute 1-based row number of each row.
25
+ * @returns The rows and widths with the heading band added.
26
+ */
27
+ function withHeadingBand(rows, widths, colStart, rowNumbers) {
28
+ const border = {
29
+ style: "single",
30
+ width: .5,
31
+ colorHex: "808080"
32
+ };
33
+ const borders = {
34
+ top: border,
35
+ bottom: border,
36
+ left: border,
37
+ right: border
38
+ };
39
+ const head = (text) => ({
40
+ properties: {
41
+ borders,
42
+ verticalAlign: "center"
43
+ },
44
+ content: [{
45
+ kind: "paragraph",
46
+ paragraph: {
47
+ properties: { alignment: "center" },
48
+ runs: [{
49
+ text,
50
+ properties: {}
51
+ }]
52
+ }
53
+ }]
54
+ });
55
+ const letters = [head("")];
56
+ for (let i = 0; i < widths.length; i++) letters.push(head(columnLetters(colStart + i)));
57
+ const out = [{
58
+ properties: { isHeader: true },
59
+ cells: letters
60
+ }];
61
+ rows.forEach((row, i) => {
62
+ out.push({
63
+ properties: row.properties,
64
+ cells: [head(String(rowNumbers[i] ?? i + 1)), ...row.cells]
65
+ });
66
+ });
67
+ return {
68
+ rows: out,
69
+ widths: [HEADING_COL_TWIPS, ...widths]
70
+ };
71
+ }
3
72
  /**
4
73
  * Greedy fill: accumulate column widths until the next column would overflow the
5
74
  * content width, then start a new band. A manual column break (`colBreaks` holds
@@ -50,27 +119,63 @@ function computeColumnBands(columnWidths, contentWidthTwips, colBreaks) {
50
119
  * @param columnWidths Per-column widths in twips, by local index.
51
120
  * @param bands The bands from {@link computeColumnBands}.
52
121
  * @param properties The shared table properties applied to every band.
122
+ * @param titleRowIndex The print-title row, or -1.
123
+ * @param drawingReachTwips How far right the sheet's drawings reach (0 if none).
124
+ * @param headings §18.3.1.70 — the printed row/column headings, when the sheet
125
+ * asks for them: the absolute index of the grid's first column and the sheet
126
+ * row number of each emitted row. Each band gets ITS OWN letters, which is
127
+ * what both references print across the top of a continuation page.
53
128
  * @returns One table body element per band, in band order.
54
129
  */
55
- function bandedTables(rows, columnWidths, bands, properties) {
56
- return bands.map((band, bandIndex) => {
57
- return {
58
- kind: "table",
59
- table: {
60
- properties,
61
- grid: columnWidths.slice(band.start, band.end + 1).map((w) => twipsToPt(w)),
62
- rows: rows.map((row, rowIndex) => {
63
- const cells = sliceRowCells(row.cells, band);
64
- return {
65
- properties: bandIndex > 0 && rowIndex === 0 ? {
66
- ...row.properties,
67
- pageBreakBefore: true
68
- } : row.properties,
69
- cells
70
- };
71
- })
72
- }
73
- };
130
+ function bandedTables(rows, columnWidths, bands, properties, titleRowIndex = -1, drawingReachTwips = 0, headings) {
131
+ return bands.flatMap((band, bandIndex) => {
132
+ const bandLeft = columnWidths.slice(0, band.start).reduce((sum, w) => sum + w, 0);
133
+ const bandTwips = columnWidths.slice(band.start, band.end + 1);
134
+ const grid = bandTwips.map((w) => twipsToPt(w));
135
+ const bandRowNumbers = [...headings?.rowNumbers ?? []];
136
+ const bandRows = rows.map((row, rowIndex) => {
137
+ const cells = sliceRowCells(row.cells, band);
138
+ return {
139
+ properties: bandIndex > 0 && rowIndex === 0 ? {
140
+ ...row.properties,
141
+ pageBreakBefore: true
142
+ } : row.properties,
143
+ cells
144
+ };
145
+ });
146
+ if (bandIndex > 0 && !(bandLeft < drawingReachTwips) && !bandRows.some(rowDrawsSomething)) return [];
147
+ while (bandRows.length > 1 && !rowDrawsSomething(bandRows[bandRows.length - 1])) {
148
+ bandRows.pop();
149
+ bandRowNumbers.pop();
150
+ }
151
+ return (titleRowIndex > 0 && titleRowIndex < bandRows.length ? [{
152
+ rows: bandRows.slice(0, titleRowIndex),
153
+ numbers: bandRowNumbers.slice(0, titleRowIndex)
154
+ }, {
155
+ rows: bandRows.slice(titleRowIndex),
156
+ numbers: bandRowNumbers.slice(titleRowIndex)
157
+ }] : [{
158
+ rows: bandRows,
159
+ numbers: bandRowNumbers
160
+ }]).map((part) => {
161
+ const headed = headings ? withHeadingBand(part.rows, bandTwips, headings.colStart + band.start, part.numbers) : void 0;
162
+ return {
163
+ kind: "table",
164
+ table: {
165
+ properties,
166
+ grid: headed ? headed.widths.map((w) => twipsToPt(w)) : grid,
167
+ rows: headed ? leadWithBandBreak(headed.rows) : part.rows
168
+ }
169
+ };
170
+ });
171
+ });
172
+ }
173
+ /** Whether a row would put anything on the page — content, fill, border or mark. */
174
+ function rowDrawsSomething(row) {
175
+ return row.cells.some((cell) => {
176
+ if (cell.content.length > 0) return true;
177
+ const p = cell.properties;
178
+ return p.shading !== void 0 || p.borders !== void 0 || p.dataBar !== void 0 || p.icon !== void 0 || p.sparkline !== void 0 || p.dropdown === true;
74
179
  });
75
180
  }
76
181
  function sliceRowCells(cells, band) {
@@ -103,5 +208,36 @@ function blankCell(span) {
103
208
  content: []
104
209
  };
105
210
  }
211
+ /**
212
+ * Move a `pageBreakBefore` that landed on the second row up to the first.
213
+ *
214
+ * The band's page break is set on the band's own leading row, before the
215
+ * heading band prepends the column-letter row in front of it. Left there, the
216
+ * letters print on one page and the band they label starts on the next.
217
+ *
218
+ * @param rows The band's rows, letters row first.
219
+ * @returns The same rows with the break carried by whichever leads them.
220
+ */
221
+ function leadWithBandBreak(rows) {
222
+ const second = rows[1];
223
+ if (!second?.properties.pageBreakBefore) return rows;
224
+ const [first, ...rest] = rows;
225
+ if (!first) return rows;
226
+ const { pageBreakBefore: _dropped, ...keep } = second.properties;
227
+ return [
228
+ {
229
+ ...first,
230
+ properties: {
231
+ ...first.properties,
232
+ pageBreakBefore: true
233
+ }
234
+ },
235
+ {
236
+ ...second,
237
+ properties: keep
238
+ },
239
+ ...rest.slice(1)
240
+ ];
241
+ }
106
242
  //#endregion
107
- export { bandedTables, computeColumnBands };
243
+ export { bandedTables, computeColumnBands, withHeadingBand };
@@ -1,7 +1,9 @@
1
+ import { resolveInternalEntities } from "../core/opc/xml-entities.js";
1
2
  import { XMLParser } from "fast-xml-parser";
2
3
  //#region src/excel/comments-parser.ts
3
4
  var decoder = new TextDecoder("utf-8");
4
5
  var parser = new XMLParser({
6
+ htmlEntities: true,
5
7
  ignoreAttributes: false,
6
8
  attributeNamePrefix: "@_",
7
9
  textNodeName: "#text",
@@ -17,7 +19,7 @@ var parser = new XMLParser({
17
19
  * of the text — stripped here so the resolved author is not shown twice.
18
20
  */
19
21
  function parseLegacyComments(data) {
20
- const root = asObject(parser.parse(decoder.decode(data))["comments"]);
22
+ const root = asObject(parser.parse(resolveInternalEntities(decoder.decode(data)))["comments"]);
21
23
  if (!root) return [];
22
24
  const authors = toArray(asObject(root["authors"])?.["author"]).map((a) => textOf(a));
23
25
  const out = [];
@@ -44,7 +46,7 @@ function parseLegacyComments(data) {
44
46
  * the authors of threaded comments.
45
47
  */
46
48
  function parsePersons(data) {
47
- const root = asObject(parser.parse(decoder.decode(data))["personList"]);
49
+ const root = asObject(parser.parse(resolveInternalEntities(decoder.decode(data)))["personList"]);
48
50
  const map = /* @__PURE__ */ new Map();
49
51
  if (!root) return map;
50
52
  for (const p of toArray(root["person"])) {
@@ -65,7 +67,7 @@ function parsePersons(data) {
65
67
  * @param persons The `id → displayName` map from {@link parsePersons}.
66
68
  */
67
69
  function parseThreadedComments(data, persons) {
68
- const root = asObject(parser.parse(decoder.decode(data))["ThreadedComments"]);
70
+ const root = asObject(parser.parse(resolveInternalEntities(decoder.decode(data)))["ThreadedComments"]);
69
71
  if (!root) return [];
70
72
  const out = [];
71
73
  for (const c of toArray(root["threadedComment"])) {
@@ -1,5 +1,5 @@
1
1
  import { CellIcon } from '../core/document-model/index.js';
2
- import { ConditionalFormat, DefinedName, WorksheetCell, XlsxStyles } from '../core/spreadsheet-model/index.js';
2
+ import { ConditionalFormat, DefinedName, WorksheetCell, XlsxBorder, XlsxStyles } from '../core/spreadsheet-model/index.js';
3
3
  /**
4
4
  * A resolved per-cell override the print model layers over a cell's base format: a
5
5
  * solid highlight fill, font tweaks, an in-cell data bar (fraction of the cell
@@ -7,16 +7,32 @@ import { ConditionalFormat, DefinedName, WorksheetCell, XlsxStyles } from '../co
7
7
  */
8
8
  export interface CfOverride {
9
9
  readonly fillHex?: string;
10
+ /**
11
+ * §18.8.9 — the edges the rule's dxf declares. A rule may format with nothing
12
+ * else, and dropping them made such a rule invisible.
13
+ */
14
+ readonly border?: XlsxBorder;
15
+ /** §18.8.9 — the format the rule renders the cell's value in. */
16
+ readonly numberFormat?: string;
10
17
  readonly fontColorHex?: string;
11
18
  readonly bold?: boolean;
12
19
  readonly italic?: boolean;
20
+ /** §18.8.37 — the rule strikes the cell's text through. */
21
+ readonly strike?: boolean;
13
22
  /** An in-cell data bar: its fraction of the cell width, colour, and optional left offset. */
14
23
  readonly dataBar?: {
15
24
  readonly fraction: number;
16
25
  readonly colorHex: string;
17
26
  readonly startFraction?: number;
27
+ readonly negative?: boolean;
18
28
  };
19
29
  readonly icon?: CellIcon;
30
+ /**
31
+ * §18.3.1.28 `<dataBar showValue="0">` — the cell shows its BAR and not its
32
+ * number. Excel's "Show Bar Only": the figure would otherwise sit on top of
33
+ * its own gauge.
34
+ */
35
+ readonly hideValue?: boolean;
20
36
  }
21
37
  /**
22
38
  * A per-cell lookup returning the {@link CfOverride} for the cell at `(row, col)`,
@@ -25,7 +41,14 @@ export interface CfOverride {
25
41
  * text tests and to key duplicate / unique comparisons for non-numeric cells
26
42
  * (empty/undefined for a blank cell).
27
43
  */
28
- export type CellConditionalFormatter = (row: number, col: number, numericValue: number | undefined, text: string | undefined) => CfOverride | undefined;
44
+ export type CellConditionalFormatter = (row: number, col: number, numericValue: number | undefined, text: string | undefined,
45
+ /**
46
+ * The cell holds nothing. It still COMPARES as zero — Excel and Calc both
47
+ * colour an empty cell for a rule written `cellIs equal 0`, which is how
48
+ * 48539.xlsx paints its whole Pass/Fail column red — but it is not a value
49
+ * that can repeat, so duplicate/uniqueValues must pass it over.
50
+ */
51
+ blank?: boolean) => CfOverride | undefined;
29
52
  /**
30
53
  * Compile a sheet's `<conditionalFormatting>` rules + the workbook's `<dxfs>` into
31
54
  * a per-cell {@link CellConditionalFormatter}. Returns undefined when the sheet has
@@ -89,22 +89,24 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
89
89
  definedNames: definedNames ?? []
90
90
  } : void 0;
91
91
  const ctx = hasExpr ? buildEvalContext(cells, resolveText, nowSerial, date1904, cross) : void 0;
92
- return (row, col, value, text) => {
92
+ return (row, col, value, text, blank) => {
93
93
  if (!hasExpr && value === void 0 && (text === void 0 || text.length === 0)) return;
94
- const dupKey = dupKeyOf(value, text);
94
+ const dupKey = blank === true ? void 0 : dupKeyOf(value, text);
95
95
  let textFmt;
96
96
  let textClaimed = false;
97
97
  let bar;
98
+ let hideValue = false;
98
99
  let icon;
99
- const claim = (dxfId) => {
100
- textFmt = dxfToOverride(dxfs[dxfId]);
100
+ const claim = (r) => {
101
+ textFmt = dxfToOverride(r.dxf ?? dxfs[r.dxfId]);
101
102
  textClaimed = true;
102
103
  };
103
104
  for (const { ranges, rule, scale, bar: resolved, iconThresholds, threshold, dupKeys, compiled, origin } of flat) {
104
105
  if (!coversCell(ranges, row, col)) continue;
105
106
  switch (rule.type) {
106
107
  case "cellIs":
107
- if (!textClaimed && value !== void 0 && cellIsMatches(rule.operator, value, rule.formulas)) claim(rule.dxfId);
108
+ if (textClaimed) break;
109
+ if (value !== void 0 && Number.isFinite(Number(operandText(rule.formulas[0]))) ? cellIsMatches(rule.operator, value, rule.formulas) : text !== void 0 && cellIsMatchesText(rule.operator, text, rule.formulas)) claim(rule);
108
110
  break;
109
111
  case "colorScale":
110
112
  if (!textClaimed && value !== void 0 && scale) {
@@ -113,7 +115,10 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
113
115
  }
114
116
  break;
115
117
  case "dataBar":
116
- if (!bar && value !== void 0 && resolved) bar = dataBarBar(resolved, value);
118
+ if (!bar && value !== void 0 && resolved) {
119
+ bar = dataBarBar(resolved, value);
120
+ if (rule.showValue === false) hideValue = true;
121
+ }
117
122
  break;
118
123
  case "iconSet":
119
124
  if (!icon && value !== void 0 && iconThresholds) {
@@ -123,17 +128,17 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
123
128
  break;
124
129
  case "top10":
125
130
  case "aboveAverage":
126
- if (!textClaimed && value !== void 0 && threshold && thresholdMatches(threshold, value)) claim(rule.dxfId);
131
+ if (!textClaimed && value !== void 0 && threshold && thresholdMatches(threshold, value)) claim(rule);
127
132
  break;
128
133
  case "duplicateValues":
129
134
  case "uniqueValues":
130
- if (!textClaimed && dupKey !== void 0 && dupKeys?.has(dupKey)) claim(rule.dxfId);
135
+ if (!textClaimed && dupKey !== void 0 && dupKeys?.has(dupKey)) claim(rule);
131
136
  break;
132
137
  case "containsText":
133
138
  case "notContainsText":
134
139
  case "beginsWith":
135
140
  case "endsWith":
136
- if (!textClaimed && text !== void 0 && textRuleMatches(rule, text)) claim(rule.dxfId);
141
+ if (!textClaimed && text !== void 0 && textRuleMatches(rule, text)) claim(rule);
137
142
  break;
138
143
  case "expression":
139
144
  if (!textClaimed && compiled && ctx && origin) {
@@ -142,11 +147,11 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
142
147
  dCol: col - origin.col,
143
148
  curRow: row,
144
149
  curCol: col
145
- })) claim(rule.dxfId);
150
+ })) claim(rule);
146
151
  }
147
152
  break;
148
153
  case "timePeriod":
149
- if (!textClaimed && value !== void 0 && nowSerial !== void 0 && timePeriodMatches(rule.timePeriod, value, nowSerial, date1904)) claim(rule.dxfId);
154
+ if (!textClaimed && value !== void 0 && nowSerial !== void 0 && timePeriodMatches(rule.timePeriod, value, nowSerial, date1904)) claim(rule);
150
155
  break;
151
156
  }
152
157
  }
@@ -154,7 +159,8 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
154
159
  return {
155
160
  ...textFmt ?? {},
156
161
  ...bar ? { dataBar: bar } : {},
157
- ...icon ? { icon } : {}
162
+ ...icon ? { icon } : {},
163
+ ...hideValue ? { hideValue: true } : {}
158
164
  };
159
165
  };
160
166
  }
@@ -277,6 +283,39 @@ function coversCell(ranges, row, col) {
277
283
  for (const r of ranges) if (row >= r.startRow && row <= r.endRow && col >= r.startColumn && col <= r.endColumn) return true;
278
284
  return false;
279
285
  }
286
+ /** A `cellIs` operand: a bare number, or a quoted string literal. */
287
+ function operandText(raw) {
288
+ const t = (raw ?? "").trim();
289
+ const quoted = /^"(.*)"$/su.exec(t);
290
+ return quoted ? quoted[1] ?? "" : t;
291
+ }
292
+ /**
293
+ * §18.3.1.11 — a `cellIs` rule compares the cell against one or two operands,
294
+ * and the comparison is not always arithmetic: `notEqual ""` over a sheet of
295
+ * words is how a document asks to format every cell that HOLDS something.
296
+ * Reading only numbers, that rule matched nothing, and
297
+ * tdf152581_bordercolorNotExportedToXLSX.xlsx lost the border and fill its two
298
+ * cells carry. Both sides numeric ⇒ compare numbers, as before; otherwise
299
+ * compare text, case-insensitively, the way Excel collates it.
300
+ */
301
+ function cellIsMatchesText(operator, text, formulas) {
302
+ const a = operandText(formulas[0]).toLowerCase();
303
+ const v = text.toLowerCase();
304
+ switch (operator) {
305
+ case "lessThan": return v < a;
306
+ case "lessThanOrEqual": return v <= a;
307
+ case "equal": return v === a;
308
+ case "notEqual": return v !== a;
309
+ case "greaterThanOrEqual": return v >= a;
310
+ case "greaterThan": return v > a;
311
+ case "between":
312
+ case "notBetween": {
313
+ const b = operandText(formulas[1]).toLowerCase();
314
+ const inside = v >= (a <= b ? a : b) && v <= (a <= b ? b : a);
315
+ return operator === "between" ? inside : !inside;
316
+ }
317
+ }
318
+ }
280
319
  function cellIsMatches(operator, value, formulas) {
281
320
  const a = Number(formulas[0]);
282
321
  if (!Number.isFinite(a)) return false;
@@ -304,6 +343,9 @@ function dxfToOverride(dxf) {
304
343
  if (dxf.font?.colorHex) out.fontColorHex = dxf.font.colorHex;
305
344
  if (dxf.font?.bold !== void 0) out.bold = dxf.font.bold;
306
345
  if (dxf.font?.italic !== void 0) out.italic = dxf.font.italic;
346
+ if (dxf.font?.strike !== void 0) out.strike = dxf.font.strike;
347
+ if (dxf.border) out.border = dxf.border;
348
+ if (dxf.numberFormat) out.numberFormat = dxf.numberFormat;
307
349
  return Object.keys(out).length > 0 ? out : void 0;
308
350
  }
309
351
  function collectRangeValues(cells, ranges) {
@@ -323,8 +365,8 @@ function resolveColorScale(rule, ranges, cells) {
323
365
  const vmin = sorted[0];
324
366
  const vmax = sorted[sorted.length - 1];
325
367
  const thresholds = [];
326
- for (const cfvo of rule.cfvos) {
327
- const t = resolveCfvo(cfvo, vmin, vmax, sorted);
368
+ for (const [i, cfvo] of rule.cfvos.entries()) {
369
+ const t = resolveCfvo(cfvo, vmin, vmax, sorted, i === rule.cfvos.length - 1);
328
370
  if (t === void 0) return void 0;
329
371
  thresholds.push(t);
330
372
  }
@@ -341,16 +383,16 @@ function resolveColorScale(rule, ranges, cells) {
341
383
  colors
342
384
  };
343
385
  }
344
- function resolveCfvo(cfvo, vmin, vmax, sorted) {
386
+ function resolveCfvo(cfvo, vmin, vmax, sorted, isLast = false) {
345
387
  switch (cfvo.type) {
346
388
  case "min":
347
389
  case "autoMin": return vmin;
348
390
  case "max":
349
391
  case "autoMax": return vmax;
350
- case "num":
392
+ case "num": return Number.isFinite(Number(cfvo.val)) ? Number(cfvo.val) : void 0;
351
393
  case "formula": {
352
394
  const n = Number(cfvo.val);
353
- return Number.isFinite(n) ? n : void 0;
395
+ return Number.isFinite(n) ? n : isLast ? vmax : vmin;
354
396
  }
355
397
  case "percent": {
356
398
  const p = Number(cfvo.val);
@@ -410,7 +452,7 @@ function resolveDataBar(rule, ranges, cells) {
410
452
  const vmin = sorted[0];
411
453
  const vmax = sorted[sorted.length - 1];
412
454
  const lower = resolveCfvo(rule.cfvos[0], vmin, vmax, sorted);
413
- const upper = resolveCfvo(rule.cfvos[1], vmin, vmax, sorted);
455
+ const upper = resolveCfvo(rule.cfvos[1], vmin, vmax, sorted, true);
414
456
  if (lower === void 0 || upper === void 0) return void 0;
415
457
  return {
416
458
  lower,
@@ -433,7 +475,8 @@ function dataBarBar(bar, value) {
433
475
  return {
434
476
  fraction: w,
435
477
  colorHex: bar.negativeColorHex,
436
- startFraction: axis - w
478
+ startFraction: axis - w,
479
+ negative: true
437
480
  };
438
481
  }
439
482
  return {
@@ -577,6 +620,7 @@ function resolveDupUnique(rule, ranges, cells, resolveText) {
577
620
  }
578
621
  function cellDupKey(cell, resolveText) {
579
622
  if (cell.type === "n") {
623
+ if (cell.rawValue.length === 0) return void 0;
580
624
  const n = Number(cell.rawValue);
581
625
  return Number.isFinite(n) ? `n:${n}` : void 0;
582
626
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Decode a SpreadsheetML `ST_Xstring` (§22.9.2.19): every `_xHHHH_` becomes the
3
+ * character it names. Text carrying no escape is returned unchanged, so the
4
+ * common path costs one failed match.
5
+ *
6
+ * @param text The raw `<t>` content.
7
+ * @returns The text with its escapes resolved.
8
+ */
9
+ export declare function decodeXstring(text: string): string;
@@ -0,0 +1,20 @@
1
+ //#region src/excel/escaped-text.ts
2
+ var ESCAPE = /_x([0-9A-Fa-f]{4})_/g;
3
+ /**
4
+ * Decode a SpreadsheetML `ST_Xstring` (§22.9.2.19): every `_xHHHH_` becomes the
5
+ * character it names. Text carrying no escape is returned unchanged, so the
6
+ * common path costs one failed match.
7
+ *
8
+ * @param text The raw `<t>` content.
9
+ * @returns The text with its escapes resolved.
10
+ */
11
+ function decodeXstring(text) {
12
+ if (!text.includes("_x")) return text;
13
+ return text.replace(ESCAPE, (whole, hex) => {
14
+ const code = Number.parseInt(hex, 16);
15
+ if (code >= 55296 && code <= 57343) return whole;
16
+ return String.fromCodePoint(code);
17
+ });
18
+ }
19
+ //#endregion
20
+ export { decodeXstring };
@@ -2,6 +2,7 @@ import { XMLParser } from "fast-xml-parser";
2
2
  //#region src/excel/form-control-parser.ts
3
3
  var decoder = new TextDecoder("utf-8");
4
4
  var parser = new XMLParser({
5
+ htmlEntities: true,
5
6
  ignoreAttributes: false,
6
7
  attributeNamePrefix: "@_",
7
8
  parseAttributeValue: false,
@@ -1,4 +1,4 @@
1
- import { excelSerialFromUtcParts, excelSerialToDate } from "../number-format.js";
1
+ import { excelSerialFromUtcParts, excelSerialToDate } from "../../core/number-format.js";
2
2
  //#region src/excel/formula/dates.ts
3
3
  /**
4
4
  * Decompose an Excel serial into UTC calendar parts. A fractional serial is
@@ -7,5 +7,13 @@ import { BodyElement } from '../core/document-model/index.js';
7
7
  *
8
8
  * @param formatString The raw `&`-code format string.
9
9
  * @param sheetName The worksheet tab name, substituted for `&A`.
10
+ * @param scale The sheet's print scale.
11
+ * @param basePt The default header font size.
12
+ * @param fileName The workbook's file name, substituted for `&F`. A
13
+ * byte-oriented API does not know it, so the caller supplies
14
+ * it; absent, `&F` is dropped as before.
15
+ * @param themePalette The workbook theme, resolving a `&K` theme reference.
16
+ * @param now The reference date `&D`/`&T` print. An explicit input,
17
+ * never the wall clock; absent, both are dropped as before.
10
18
  */
11
- export declare function buildHeaderFooterContent(formatString: string, sheetName: string): Array<BodyElement>;
19
+ export declare function buildHeaderFooterContent(formatString: string, sheetName: string, scale?: number, basePt?: number, fileName?: string, themePalette?: ReadonlyMap<string, string>, now?: Date): Array<BodyElement>;