reamkit 1.31.0 → 1.32.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 (83) hide show
  1. package/README.md +10 -0
  2. package/dist/esm/core/bidi/index.d.ts +10 -0
  3. package/dist/esm/core/bidi/index.js +18 -2
  4. package/dist/esm/core/converter/facade.js +10 -5
  5. package/dist/esm/core/converter/project.js +1 -0
  6. package/dist/esm/core/converter/ream.js +17 -6
  7. package/dist/esm/core/converter/sheet-images.d.ts +17 -0
  8. package/dist/esm/core/converter/sheet-images.js +33 -0
  9. package/dist/esm/core/document-model/index.d.ts +1 -1
  10. package/dist/esm/core/document-model/types.d.ts +112 -0
  11. package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -8
  12. package/dist/esm/core/drawingml/chart-geometry.js +198 -108
  13. package/dist/esm/core/drawingml/chart-parser.js +73 -2
  14. package/dist/esm/core/drawingml/colors.d.ts +6 -0
  15. package/dist/esm/core/drawingml/colors.js +23 -3
  16. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  17. package/dist/esm/core/drawingml/shape-render.js +4 -2
  18. package/dist/esm/core/font/measure.d.ts +9 -0
  19. package/dist/esm/core/font/measure.js +9 -1
  20. package/dist/esm/core/fonts/families.js +4 -2
  21. package/dist/esm/core/fonts/scripts.js +4 -2
  22. package/dist/esm/core/ir/flow.d.ts +6 -0
  23. package/dist/esm/core/ir/sheet.d.ts +43 -0
  24. package/dist/esm/core/number-format.js +16 -11
  25. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  26. package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
  27. package/dist/esm/core/style-cascade/resolver.js +4 -2
  28. package/dist/esm/excel/column-bands.d.ts +39 -6
  29. package/dist/esm/excel/column-bands.js +41 -17
  30. package/dist/esm/excel/comments-parser.js +19 -0
  31. package/dist/esm/excel/conditional-format.d.ts +7 -3
  32. package/dist/esm/excel/conditional-format.js +77 -39
  33. package/dist/esm/excel/formula/context.d.ts +18 -0
  34. package/dist/esm/excel/formula/eval.js +51 -0
  35. package/dist/esm/excel/formula/index.d.ts +2 -2
  36. package/dist/esm/excel/formula/lexer.d.ts +1 -1
  37. package/dist/esm/excel/formula/lexer.js +41 -0
  38. package/dist/esm/excel/formula/parser.d.ts +25 -0
  39. package/dist/esm/excel/formula/parser.js +95 -0
  40. package/dist/esm/excel/print-model.d.ts +38 -1
  41. package/dist/esm/excel/print-model.js +522 -80
  42. package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
  43. package/dist/esm/excel/shared-strings-parser.js +11 -1
  44. package/dist/esm/excel/sheet-drawing.d.ts +38 -2
  45. package/dist/esm/excel/sheet-drawing.js +47 -13
  46. package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
  47. package/dist/esm/excel/sheet-shape-parser.js +18 -6
  48. package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
  49. package/dist/esm/excel/sheet-to-flow.js +434 -47
  50. package/dist/esm/excel/styles-parser.d.ts +9 -0
  51. package/dist/esm/excel/styles-parser.js +80 -26
  52. package/dist/esm/excel/table-parser.js +5 -0
  53. package/dist/esm/excel/table-style-presets.d.ts +26 -0
  54. package/dist/esm/excel/table-style-presets.js +1555 -0
  55. package/dist/esm/excel/table-style.d.ts +13 -0
  56. package/dist/esm/excel/table-style.js +75 -0
  57. package/dist/esm/excel/tint.d.ts +10 -0
  58. package/dist/esm/excel/tint.js +75 -0
  59. package/dist/esm/excel/vml-drawing.d.ts +41 -3
  60. package/dist/esm/excel/vml-drawing.js +143 -10
  61. package/dist/esm/excel/worksheet-parser.js +89 -15
  62. package/dist/esm/excel/xls/biff-reader.js +10 -2
  63. package/dist/esm/excel/xlsx-reader.js +195 -14
  64. package/dist/esm/excel/xlsx-writer.js +8 -7
  65. package/dist/esm/html/html-writer.js +202 -20
  66. package/dist/esm/layout/fit-pages.d.ts +11 -0
  67. package/dist/esm/layout/fit-pages.js +86 -0
  68. package/dist/esm/layout/line-paint.d.ts +58 -0
  69. package/dist/esm/layout/line-paint.js +107 -0
  70. package/dist/esm/layout/page-doc.d.ts +51 -0
  71. package/dist/esm/layout/page-doc.js +141 -1
  72. package/dist/esm/layout/styled-layout.d.ts +7 -0
  73. package/dist/esm/layout/styled-layout.js +274 -51
  74. package/dist/esm/layout/turned-section.js +14 -2
  75. package/dist/esm/markdown/markdown-writer.js +17 -6
  76. package/dist/esm/pdf/styled-page-emitter.js +49 -71
  77. package/dist/esm/pdf/styled-page-renderer.js +1 -1
  78. package/dist/esm/pdf-reader/font.js +3 -3
  79. package/dist/esm/svg/svg-text.d.ts +58 -0
  80. package/dist/esm/svg/svg-text.js +322 -0
  81. package/dist/esm/svg/svg-writer.js +135 -71
  82. package/dist/esm/word/docx-writer.js +11 -2
  83. package/package.json +5 -1
@@ -1,7 +1,4 @@
1
- import { twipsToPt } from "../core/ir/units.js";
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;
1
+ import { pt, twipsToPt } from "../core/ir/units.js";
5
2
  /** The A1-style letters of an absolute column index (0 → A, 26 → AA). */
6
3
  function columnLetters(index) {
7
4
  let n = index;
@@ -20,14 +17,17 @@ function columnLetters(index) {
20
17
  *
21
18
  * @param rows The band's rows.
22
19
  * @param widths The band's column widths in twips.
23
- * @param colStart The absolute index of the band's first column.
20
+ * @param columns The absolute index of each of those columns — a hidden
21
+ * one between them is skipped, and so is its letter.
24
22
  * @param rowNumbers The absolute 1-based row number of each row.
23
+ * @param band How the headings stand, at the print scale.
25
24
  * @returns The rows and widths with the heading band added.
26
25
  */
27
- function withHeadingBand(rows, widths, colStart, rowNumbers) {
26
+ function withHeadingBand(rows, widths, columns, rowNumbers, band = {}) {
27
+ const { lettersPt, colTwips = 460, fontPt, lineWidthPt = .5 } = band;
28
28
  const border = {
29
29
  style: "single",
30
- width: .5,
30
+ width: lineWidthPt,
31
31
  colorHex: "808080"
32
32
  };
33
33
  const borders = {
@@ -47,15 +47,21 @@ function withHeadingBand(rows, widths, colStart, rowNumbers) {
47
47
  properties: { alignment: "center" },
48
48
  runs: [{
49
49
  text,
50
- properties: {}
50
+ properties: fontPt !== void 0 ? { fontSizePt: pt(fontPt) } : {}
51
51
  }]
52
52
  }
53
53
  }]
54
54
  });
55
55
  const letters = [head("")];
56
- for (let i = 0; i < widths.length; i++) letters.push(head(columnLetters(colStart + i)));
56
+ for (let i = 0; i < widths.length; i++) letters.push(head(columnLetters(columns[i] ?? i)));
57
57
  const out = [{
58
- properties: { isHeader: true },
58
+ properties: {
59
+ isHeader: true,
60
+ ...lettersPt !== void 0 ? {
61
+ height: pt(lettersPt),
62
+ heightRule: "exact"
63
+ } : {}
64
+ },
59
65
  cells: letters
60
66
  }];
61
67
  rows.forEach((row, i) => {
@@ -66,7 +72,7 @@ function withHeadingBand(rows, widths, colStart, rowNumbers) {
66
72
  });
67
73
  return {
68
74
  rows: out,
69
- widths: [HEADING_COL_TWIPS, ...widths]
75
+ widths: [colTwips, ...widths]
70
76
  };
71
77
  }
72
78
  /**
@@ -122,16 +128,33 @@ function computeColumnBands(columnWidths, contentWidthTwips, colBreaks) {
122
128
  * @param titleRowIndex The print-title row, or -1.
123
129
  * @param drawingReachTwips How far right the sheet's drawings reach (0 if none).
124
130
  * @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.
131
+ * asks for them: the absolute index of each column and the sheet row number
132
+ * of each emitted row. Each band gets ITS OWN letters, which is what both
133
+ * references print across the top of a continuation page.
134
+ * @param sheet §20.5 — where the grid's first column stands on the sheet and
135
+ * where its printed drawings lie, at the print scale: each band is framed
136
+ * for the drawings over it (TableOverlay) and keeps the rows they cover.
128
137
  * @returns One table body element per band, in band order.
129
138
  */
130
- function bandedTables(rows, columnWidths, bands, properties, titleRowIndex = -1, drawingReachTwips = 0, headings) {
139
+ function bandedTables(rows, columnWidths, bands, properties, titleRowIndex = -1, drawingReachTwips = 0, headings, sheet) {
131
140
  return bands.flatMap((band, bandIndex) => {
132
141
  const bandLeft = columnWidths.slice(0, band.start).reduce((sum, w) => sum + w, 0);
133
142
  const bandTwips = columnWidths.slice(band.start, band.end + 1);
134
143
  const grid = bandTwips.map((w) => twipsToPt(w));
144
+ const sheetLeftPt = sheet ? sheet.sheetLeftPt + twipsToPt(bandLeft) : 0;
145
+ const sheetRightPt = sheetLeftPt + grid.reduce((sum, w) => sum + w, 0);
146
+ const overlayAt = (leftPt) => sheet ? { overlay: {
147
+ sheetLeftPt: pt(sheetLeftPt),
148
+ widthPt: pt(sheetRightPt - sheetLeftPt),
149
+ leftPt: pt(leftPt),
150
+ drawings: []
151
+ } } : {};
152
+ const underDrawing = (row) => {
153
+ const top = row.properties.sheetTopPt;
154
+ if (top === void 0 || !sheet) return false;
155
+ const bottom = top + (row.properties.height ?? 0);
156
+ return sheet.drawings.some((d) => d.xPt < sheetRightPt && d.xPt + d.widthPt > sheetLeftPt && d.yPt < bottom && d.yPt + d.heightPt > top);
157
+ };
135
158
  const bandRowNumbers = [...headings?.rowNumbers ?? []];
136
159
  const bandRows = rows.map((row, rowIndex) => {
137
160
  const cells = sliceRowCells(row.cells, band);
@@ -144,7 +167,7 @@ function bandedTables(rows, columnWidths, bands, properties, titleRowIndex = -1,
144
167
  };
145
168
  });
146
169
  if (bandIndex > 0 && !(bandLeft < drawingReachTwips) && !bandRows.some(rowDrawsSomething)) return [];
147
- while (bandRows.length > 1 && !rowDrawsSomething(bandRows[bandRows.length - 1])) {
170
+ while (bandRows.length > 1 && !rowDrawsSomething(bandRows[bandRows.length - 1]) && !underDrawing(bandRows[bandRows.length - 1])) {
148
171
  bandRows.pop();
149
172
  bandRowNumbers.pop();
150
173
  }
@@ -158,11 +181,12 @@ function bandedTables(rows, columnWidths, bands, properties, titleRowIndex = -1,
158
181
  rows: bandRows,
159
182
  numbers: bandRowNumbers
160
183
  }]).map((part) => {
161
- const headed = headings ? withHeadingBand(part.rows, bandTwips, headings.colStart + band.start, part.numbers) : void 0;
184
+ const headed = headings ? withHeadingBand(part.rows, bandTwips, headings.columns.slice(band.start, band.end + 1), part.numbers, headings.band) : void 0;
162
185
  return {
163
186
  kind: "table",
164
187
  table: {
165
188
  properties,
189
+ ...overlayAt(headed ? twipsToPt(headings?.band?.colTwips ?? 460) : 0),
166
190
  grid: headed ? headed.widths.map((w) => twipsToPt(w)) : grid,
167
191
  rows: headed ? leadWithBandBreak(headed.rows) : part.rows
168
192
  }
@@ -1,4 +1,5 @@
1
1
  import { resolveInternalEntities } from "../core/opc/xml-entities.js";
2
+ import { richRun } from "./shared-strings-parser.js";
2
3
  import { XMLParser } from "fast-xml-parser";
3
4
  //#region src/excel/comments-parser.ts
4
5
  var decoder = new TextDecoder("utf-8");
@@ -32,10 +33,12 @@ function parseLegacyComments(data) {
32
33
  const author = Number.isInteger(authorIdx) ? authors[authorIdx] : void 0;
33
34
  const text = stripAuthorPrefix(richText(obj["text"]), author);
34
35
  if (text.length === 0 && !author) continue;
36
+ const runs = richRuns(obj["text"]);
35
37
  out.push({
36
38
  ref,
37
39
  ...author ? { author } : {},
38
40
  text,
41
+ ...runs.length > 0 ? { runs } : {},
39
42
  threaded: false
40
43
  });
41
44
  }
@@ -95,6 +98,22 @@ function richText(node) {
95
98
  if (direct) return direct;
96
99
  return toArray(obj["r"]).map((r) => textOf(asObject(r)?.["t"])).join("");
97
100
  }
101
+ function richRuns(node) {
102
+ const obj = asObject(node);
103
+ if (!obj) {
104
+ const plain = textOf(node);
105
+ return plain ? [{ text: plain }] : [];
106
+ }
107
+ const direct = textOf(obj["t"]);
108
+ if (direct) return [{ text: direct }];
109
+ const out = [];
110
+ for (const r of toArray(obj["r"])) {
111
+ const run = asObject(r);
112
+ const text = textOf(run?.["t"]);
113
+ if (text.length > 0) out.push(richRun(text, run?.["rPr"]));
114
+ }
115
+ return out;
116
+ }
98
117
  function stripAuthorPrefix(text, author) {
99
118
  if (!author) return text;
100
119
  const prefix = `${author}:`;
@@ -1,5 +1,5 @@
1
1
  import { CellIcon } from '../core/document-model/index.js';
2
- import { ConditionalFormat, DefinedName, WorksheetCell, XlsxBorder, XlsxStyles } from '../core/spreadsheet-model/index.js';
2
+ import { ConditionalFormat, DefinedName, ExcelTable, 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
@@ -25,12 +25,14 @@ export interface CfOverride {
25
25
  readonly colorHex: string;
26
26
  readonly startFraction?: number;
27
27
  readonly negative?: boolean;
28
+ readonly solid?: boolean;
28
29
  };
29
30
  readonly icon?: CellIcon;
30
31
  /**
31
32
  * §18.3.1.28 `<dataBar showValue="0">` — the cell shows its BAR and not its
32
33
  * number. Excel's "Show Bar Only": the figure would otherwise sit on top of
33
- * its own gauge.
34
+ * its own gauge. §18.3.1.49 `<iconSet showValue="0">` is the same for an
35
+ * icon: "Show Icon Only".
34
36
  */
35
37
  readonly hideValue?: boolean;
36
38
  }
@@ -69,11 +71,13 @@ blank?: boolean) => CfOverride | undefined;
69
71
  * driving `TODAY()`/`NOW()` and the `timePeriod` windows;
70
72
  * absent ⇒ those constructs no-op (deterministic output).
71
73
  * @param sheetGrids The whole workbook, for an `expression` rule that reaches
72
- * another sheet (`Sheet2!A1`) or a defined name; absent ⇒
74
+ * another sheet (`Sheet2!A1`), a defined name or a table
75
+ * by its columns (`tblIncome[Amount]`); absent ⇒
73
76
  * same-sheet references only.
74
77
  * @param currentSheet The rule sheet's name, for resolving sheet-local names.
75
78
  * @param definedNames The workbook defined names visible to `expression` rules.
76
79
  */
77
80
  export declare function buildConditionalFormatter(conditionalFormats: ReadonlyArray<ConditionalFormat> | undefined, styles: XlsxStyles, cells: ReadonlyArray<WorksheetCell>, resolveText?: (cell: WorksheetCell) => string, date1904?: boolean, now?: Date, sheetGrids?: ReadonlyMap<string, {
78
81
  readonly cells: ReadonlyArray<WorksheetCell>;
82
+ readonly tables?: ReadonlyArray<ExcelTable>;
79
83
  }>, currentSheet?: string, definedNames?: ReadonlyArray<DefinedName>): CellConditionalFormatter | undefined;
@@ -1,5 +1,5 @@
1
1
  import { serialFromDate, timePeriodMatches } from "./formula/dates.js";
2
- import { BLANK, bool, err, num, str } from "./formula/value.js";
2
+ import { BLANK, bool, err, num, str, toNumber } from "./formula/value.js";
3
3
  import { NO_SHIFT, evaluate } from "./formula/eval.js";
4
4
  import { compileFormula, evaluateToBool } from "./formula/index.js";
5
5
  //#region src/excel/conditional-format.ts
@@ -23,7 +23,8 @@ import { compileFormula, evaluateToBool } from "./formula/index.js";
23
23
  * driving `TODAY()`/`NOW()` and the `timePeriod` windows;
24
24
  * absent ⇒ those constructs no-op (deterministic output).
25
25
  * @param sheetGrids The whole workbook, for an `expression` rule that reaches
26
- * another sheet (`Sheet2!A1`) or a defined name; absent ⇒
26
+ * another sheet (`Sheet2!A1`), a defined name or a table
27
+ * by its columns (`tblIncome[Amount]`); absent ⇒
27
28
  * same-sheet references only.
28
29
  * @param currentSheet The rule sheet's name, for resolving sheet-local names.
29
30
  * @param definedNames The workbook defined names visible to `expression` rules.
@@ -31,21 +32,40 @@ import { compileFormula, evaluateToBool } from "./formula/index.js";
31
32
  function buildConditionalFormatter(conditionalFormats, styles, cells, resolveText, date1904 = false, now, sheetGrids, currentSheet, definedNames) {
32
33
  if (!conditionalFormats || conditionalFormats.length === 0) return void 0;
33
34
  const dxfs = styles.dxfs ?? [];
35
+ const hasExpr = conditionalFormats.some((cf) => cf.rules.some((r) => r.type === "expression"));
36
+ const formulaStops = conditionalFormats.some((cf) => cf.rules.some((r) => (r.type === "colorScale" || r.type === "dataBar" || r.type === "iconSet") && r.cfvos.some((c) => c.val !== void 0 && !Number.isFinite(Number(c.val)))));
37
+ const nowSerial = now !== void 0 ? serialFromDate(now, date1904) : void 0;
38
+ const cross = (hasExpr || formulaStops) && sheetGrids ? {
39
+ sheets: [...sheetGrids].map(([name, ws]) => ({
40
+ name,
41
+ cells: ws.cells,
42
+ ...ws.tables ? { tables: ws.tables } : {}
43
+ })),
44
+ ...currentSheet !== void 0 ? { currentSheet } : {},
45
+ definedNames: definedNames ?? []
46
+ } : void 0;
47
+ const ctx = hasExpr || formulaStops ? buildEvalContext(cells, resolveText, nowSerial, date1904, cross) : void 0;
48
+ const stop = ctx ? (formula) => {
49
+ const compiled = compileFormula(formula.startsWith("=") ? formula.slice(1) : formula);
50
+ if (!compiled) return void 0;
51
+ const n = toNumber(evaluate(compiled.ast, ctx, NO_SHIFT), ctx);
52
+ return typeof n === "number" && Number.isFinite(n) ? n : void 0;
53
+ } : void 0;
34
54
  const flat = [];
35
55
  for (const cf of conditionalFormats) for (const rule of cf.rules) if (rule.type === "colorScale") flat.push({
36
56
  ranges: cf.ranges,
37
57
  rule,
38
- scale: resolveColorScale(rule, cf.ranges, cells)
58
+ scale: resolveColorScale(rule, cf.ranges, cells, stop)
39
59
  });
40
60
  else if (rule.type === "dataBar") flat.push({
41
61
  ranges: cf.ranges,
42
62
  rule,
43
- bar: resolveDataBar(rule, cf.ranges, cells)
63
+ bar: resolveDataBar(rule, cf.ranges, cells, stop)
44
64
  });
45
65
  else if (rule.type === "iconSet") flat.push({
46
66
  ranges: cf.ranges,
47
67
  rule,
48
- iconThresholds: resolveIconThresholds(rule, cf.ranges, cells)
68
+ iconThresholds: resolveIconThresholds(rule, cf.ranges, cells, stop)
49
69
  });
50
70
  else if (rule.type === "top10") flat.push({
51
71
  ranges: cf.ranges,
@@ -78,17 +98,6 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
78
98
  rule
79
99
  });
80
100
  flat.sort((a, b) => a.rule.priority - b.rule.priority);
81
- const hasExpr = flat.some((f) => f.rule.type === "expression");
82
- const nowSerial = now !== void 0 ? serialFromDate(now, date1904) : void 0;
83
- const cross = hasExpr && sheetGrids ? {
84
- sheets: [...sheetGrids].map(([name, ws]) => ({
85
- name,
86
- cells: ws.cells
87
- })),
88
- ...currentSheet !== void 0 ? { currentSheet } : {},
89
- definedNames: definedNames ?? []
90
- } : void 0;
91
- const ctx = hasExpr ? buildEvalContext(cells, resolveText, nowSerial, date1904, cross) : void 0;
92
101
  return (row, col, value, text, blank) => {
93
102
  if (!hasExpr && value === void 0 && (text === void 0 || text.length === 0)) return;
94
103
  const dupKey = blank === true ? void 0 : dupKeyOf(value, text);
@@ -124,6 +133,7 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
124
133
  if (!icon && value !== void 0 && iconThresholds) {
125
134
  const bucket = iconBucket(iconThresholds, value);
126
135
  icon = iconToCell(rule.iconSet, iconThresholds.length, bucket, rule.reverse ?? false);
136
+ if (rule.showValue === false) hideValue = true;
127
137
  }
128
138
  break;
129
139
  case "top10":
@@ -206,12 +216,34 @@ function buildEvalContext(cells, resolveText, nowSerial, date1904, cross) {
206
216
  const key = dn.name.toLowerCase();
207
217
  if (dn.localSheetId !== void 0 || !nameMap.has(key)) nameMap.set(key, dn);
208
218
  }
219
+ const tables = /* @__PURE__ */ new Map();
220
+ const onThisSheet = [];
221
+ cross.sheets.forEach((s, i) => {
222
+ for (const t of s.tables ?? []) {
223
+ const table = {
224
+ ...i !== currentIdx ? { sheet: i } : {},
225
+ ref: {
226
+ r0: t.ref.startRow,
227
+ c0: t.ref.startColumn,
228
+ r1: t.ref.endRow,
229
+ c1: t.ref.endColumn
230
+ },
231
+ headerRowCount: t.headerRowCount,
232
+ totalsRowCount: t.totalsRowCount ?? 0,
233
+ columns: t.columns ?? []
234
+ };
235
+ if (t.name !== void 0) tables.set(t.name.toLowerCase(), table);
236
+ if (i === currentIdx) onThisSheet.push(table);
237
+ }
238
+ });
209
239
  const resolving = /* @__PURE__ */ new Set();
210
240
  const ctx = {
211
241
  ...base,
212
242
  sheetIndex: (name) => sheetByName.get(name.toLowerCase()),
213
243
  getCellOn: (sheet, row, col) => sheetData(sheet).byKey.get(row * COLS + col) ?? BLANK,
214
244
  eachCellOn: (sheet, rect, visit) => eachInMap(sheetData(sheet), rect, visit),
245
+ table: (name) => tables.get(name.toLowerCase()),
246
+ tableAt: (row, col) => onThisSheet.find((t) => row >= t.ref.r0 && row <= t.ref.r1 && col >= t.ref.c0 && col <= t.ref.c1),
215
247
  resolveName: (rawName) => {
216
248
  const key = rawName.toLowerCase();
217
249
  const dn = nameMap.get(key);
@@ -222,7 +254,6 @@ function buildEvalContext(cells, resolveText, nowSerial, date1904, cross) {
222
254
  if (ast.k === "num") return num(ast.v);
223
255
  if (ast.k === "str") return str(ast.v);
224
256
  if (ast.k === "bool") return bool(ast.v);
225
- if (ast.k !== "cell" && ast.k !== "range") return void 0;
226
257
  resolving.add(key);
227
258
  try {
228
259
  return evaluate(ast, ctx, NO_SHIFT);
@@ -358,7 +389,7 @@ function collectRangeValues(cells, ranges) {
358
389
  }
359
390
  return out;
360
391
  }
361
- function resolveColorScale(rule, ranges, cells) {
392
+ function resolveColorScale(rule, ranges, cells, stop) {
362
393
  const vals = collectRangeValues(cells, ranges);
363
394
  if (vals.length === 0) return void 0;
364
395
  const sorted = [...vals].sort((a, b) => a - b);
@@ -366,7 +397,7 @@ function resolveColorScale(rule, ranges, cells) {
366
397
  const vmax = sorted[sorted.length - 1];
367
398
  const thresholds = [];
368
399
  for (const [i, cfvo] of rule.cfvos.entries()) {
369
- const t = resolveCfvo(cfvo, vmin, vmax, sorted, i === rule.cfvos.length - 1);
400
+ const t = resolveCfvo(cfvo, vmin, vmax, sorted, i === rule.cfvos.length - 1, stop);
370
401
  if (t === void 0) return void 0;
371
402
  thresholds.push(t);
372
403
  }
@@ -383,24 +414,26 @@ function resolveColorScale(rule, ranges, cells) {
383
414
  colors
384
415
  };
385
416
  }
386
- function resolveCfvo(cfvo, vmin, vmax, sorted, isLast = false) {
417
+ function resolveCfvo(cfvo, vmin, vmax, sorted, isLast = false, stop) {
418
+ const given = () => {
419
+ const n = Number(cfvo.val);
420
+ if (Number.isFinite(n)) return n;
421
+ return cfvo.val !== void 0 ? stop?.(cfvo.val) : void 0;
422
+ };
387
423
  switch (cfvo.type) {
388
424
  case "min":
389
425
  case "autoMin": return vmin;
390
426
  case "max":
391
427
  case "autoMax": return vmax;
392
- case "num": return Number.isFinite(Number(cfvo.val)) ? Number(cfvo.val) : void 0;
393
- case "formula": {
394
- const n = Number(cfvo.val);
395
- return Number.isFinite(n) ? n : isLast ? vmax : vmin;
396
- }
428
+ case "num": return given();
429
+ case "formula": return given() ?? (isLast ? vmax : vmin);
397
430
  case "percent": {
398
- const p = Number(cfvo.val);
399
- return Number.isFinite(p) ? vmin + (vmax - vmin) * (p / 100) : void 0;
431
+ const p = given();
432
+ return p !== void 0 ? vmin + (vmax - vmin) * (p / 100) : void 0;
400
433
  }
401
434
  case "percentile": {
402
- const p = Number(cfvo.val);
403
- return Number.isFinite(p) ? percentile(sorted, p) : void 0;
435
+ const p = given();
436
+ return p !== void 0 ? percentile(sorted, p) : void 0;
404
437
  }
405
438
  }
406
439
  }
@@ -445,50 +478,55 @@ function rgbHex([r, g, b]) {
445
478
  const h = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0").toUpperCase();
446
479
  return `${h(r)}${h(g)}${h(b)}`;
447
480
  }
448
- function resolveDataBar(rule, ranges, cells) {
481
+ function resolveDataBar(rule, ranges, cells, stop) {
449
482
  const vals = collectRangeValues(cells, ranges);
450
483
  if (vals.length === 0) return void 0;
451
484
  const sorted = [...vals].sort((a, b) => a - b);
452
485
  const vmin = sorted[0];
453
486
  const vmax = sorted[sorted.length - 1];
454
- const lower = resolveCfvo(rule.cfvos[0], vmin, vmax, sorted);
455
- const upper = resolveCfvo(rule.cfvos[1], vmin, vmax, sorted, true);
487
+ const lower = resolveCfvo(rule.cfvos[0], vmin, vmax, sorted, false, stop);
488
+ const upper = resolveCfvo(rule.cfvos[1], vmin, vmax, sorted, true, stop);
456
489
  if (lower === void 0 || upper === void 0) return void 0;
457
490
  return {
458
491
  lower,
459
492
  upper,
460
493
  colorHex: rule.colorHex,
461
494
  negativeColorHex: "FF0000",
462
- minLen: (rule.minLength ?? 0) / 100,
463
- maxLen: (rule.maxLength ?? 100) / 100
495
+ minLen: (rule.minLength ?? 10) / 100,
496
+ maxLen: (rule.maxLength ?? 90) / 100,
497
+ solid: rule.gradient === false
464
498
  };
465
499
  }
466
500
  function dataBarBar(bar, value) {
501
+ const solid = bar.solid ? { solid: true } : {};
467
502
  if (bar.lower < 0 && bar.upper > 0) {
468
503
  const axis = -bar.lower / (bar.upper - bar.lower);
469
504
  if (value >= 0) return {
470
505
  fraction: value / bar.upper * (1 - axis),
471
506
  colorHex: bar.colorHex,
472
- startFraction: axis
507
+ startFraction: axis,
508
+ ...solid
473
509
  };
474
510
  const w = Math.abs(value) / Math.abs(bar.lower) * axis;
475
511
  return {
476
512
  fraction: w,
477
513
  colorHex: bar.negativeColorHex,
478
514
  startFraction: axis - w,
479
- negative: true
515
+ negative: true,
516
+ ...solid
480
517
  };
481
518
  }
482
519
  return {
483
520
  fraction: dataBarFraction(bar, value),
484
- colorHex: bar.colorHex
521
+ colorHex: bar.colorHex,
522
+ ...solid
485
523
  };
486
524
  }
487
525
  function dataBarFraction(bar, value) {
488
526
  const t = bar.upper > bar.lower ? Math.min(1, Math.max(0, (value - bar.lower) / (bar.upper - bar.lower))) : value >= bar.upper ? 1 : 0;
489
527
  return bar.minLen + t * (bar.maxLen - bar.minLen);
490
528
  }
491
- function resolveIconThresholds(rule, ranges, cells) {
529
+ function resolveIconThresholds(rule, ranges, cells, stop) {
492
530
  const vals = collectRangeValues(cells, ranges);
493
531
  if (vals.length === 0) return void 0;
494
532
  const sorted = [...vals].sort((a, b) => a - b);
@@ -496,7 +534,7 @@ function resolveIconThresholds(rule, ranges, cells) {
496
534
  const vmax = sorted[sorted.length - 1];
497
535
  const out = [];
498
536
  for (const cfvo of rule.cfvos) {
499
- const t = resolveCfvo(cfvo, vmin, vmax, sorted);
537
+ const t = resolveCfvo(cfvo, vmin, vmax, sorted, false, stop);
500
538
  if (t === void 0) return void 0;
501
539
  out.push(t);
502
540
  }
@@ -45,4 +45,22 @@ export interface EvalContext {
45
45
  * Rect, optionally on another sheet) or a literal scalar; undefined ⇒ `#NAME?`.
46
46
  */
47
47
  readonly resolveName?: (name: string) => FValue | undefined;
48
+ /**
49
+ * §18.5 — a table by its name (case-insensitive), for a structured reference
50
+ * (`tblIncome[Amount]`); undefined ⇒ `#REF!`.
51
+ */
52
+ readonly table?: (name: string) => FormulaTable | undefined;
53
+ /** The table a cell stands in, for a reference that names none (`[@Amount]`). */
54
+ readonly tableAt?: (row: number, col: number) => FormulaTable | undefined;
55
+ }
56
+ /** What a structured reference needs of a table (§18.5.1.2). */
57
+ export interface FormulaTable {
58
+ /** The sheet it is on, when not the formula's own; undefined ⇒ the formula's sheet. */
59
+ readonly sheet?: number;
60
+ /** Its whole range — headers, data and totals. */
61
+ readonly ref: Rect;
62
+ readonly headerRowCount: number;
63
+ readonly totalsRowCount: number;
64
+ /** Its columns' names, left to right. */
65
+ readonly columns: ReadonlyArray<string>;
48
66
  }
@@ -56,6 +56,7 @@ function evaluate(ast, ctx, shift) {
56
56
  };
57
57
  }
58
58
  case "name": return ctx.resolveName?.(ast.name) ?? err("#NAME?");
59
+ case "sref": return structuredRef(ast.ref, ctx, shift);
59
60
  case "array": return {
60
61
  t: "arr",
61
62
  rows: ast.rows.map((row) => row.map((el) => deref(evaluate(el, ctx, shift), ctx)))
@@ -69,6 +70,56 @@ function evaluate(ast, ctx, shift) {
69
70
  case "call": return callFn(ast.name, ast.args, (a) => evaluate(a, ctx, shift), ctx, shift);
70
71
  }
71
72
  }
73
+ /**
74
+ * §18.17.6.4 — the cells a structured reference names: the parts of the table
75
+ * it asks for (its data rows when it names none), across the columns it names
76
+ * (all of them when it names none). `#This Row` is the current cell's row.
77
+ *
78
+ * @param ref The reference.
79
+ * @param ctx The evaluation context, which knows the tables.
80
+ * @param shift The current cell, for `#This Row` and for a reference naming no table.
81
+ * @returns The range, or `#REF!` for a table, column or part that is not there.
82
+ */
83
+ function structuredRef(ref, ctx, shift) {
84
+ const table = ref.table.length > 0 ? ctx.table?.(ref.table) : shift.curRow !== void 0 && shift.curCol !== void 0 ? ctx.tableAt?.(shift.curRow, shift.curCol) : void 0;
85
+ if (!table) return err("#REF!");
86
+ const { r0, c0, r1, c1 } = table.ref;
87
+ const headerEnd = r0 + table.headerRowCount - 1;
88
+ const totalsStart = r1 - table.totalsRowCount + 1;
89
+ const rows = (area) => {
90
+ switch (area) {
91
+ case "#All": return [r0, r1];
92
+ case "#Data": return [headerEnd + 1, totalsStart - 1];
93
+ case "#Headers": return table.headerRowCount > 0 ? [r0, headerEnd] : void 0;
94
+ case "#Totals": return table.totalsRowCount > 0 ? [totalsStart, r1] : void 0;
95
+ case "#This Row": return shift.curRow !== void 0 ? [shift.curRow, shift.curRow] : void 0;
96
+ }
97
+ };
98
+ const spans = (ref.areas.length > 0 ? ref.areas : ["#Data"]).map(rows);
99
+ if (spans.some((s) => s === void 0 || s[1] < s[0])) return err("#REF!");
100
+ const top = Math.min(...spans.map((s) => s[0]));
101
+ const bottom = Math.max(...spans.map((s) => s[1]));
102
+ let left = c0;
103
+ let right = c1;
104
+ if (ref.columns) {
105
+ const at = (name) => table.columns.findIndex((c) => c.toLowerCase() === name.toLowerCase());
106
+ const a = at(ref.columns[0]);
107
+ const b = at(ref.columns[1]);
108
+ if (a < 0 || b < 0) return err("#REF!");
109
+ left = c0 + Math.min(a, b);
110
+ right = c0 + Math.max(a, b);
111
+ }
112
+ return {
113
+ t: "ref",
114
+ rect: {
115
+ r0: top,
116
+ c0: left,
117
+ r1: bottom,
118
+ c1: right
119
+ },
120
+ ...table.sheet !== void 0 ? { sheet: table.sheet } : {}
121
+ };
122
+ }
72
123
  function resolveRef(ref, shift) {
73
124
  const row = ref.row.abs ? ref.row.index : ref.row.index + shift.dRow;
74
125
  const col = ref.col.abs ? ref.col.index : ref.col.index + shift.dCol;
@@ -1,10 +1,10 @@
1
1
  import { Ast } from './parser.js';
2
2
  import { EvalContext } from './context.js';
3
3
  import { Shift } from './eval.js';
4
- export type { EvalContext } from './context.js';
4
+ export type { EvalContext, FormulaTable } from './context.js';
5
5
  export type { Shift } from './eval.js';
6
6
  export type { Scalar, FValue, Rect, FErr } from './value.js';
7
- export { num, str, bool, err, BLANK } from './value.js';
7
+ export { num, str, bool, err, BLANK, toNumber } from './value.js';
8
8
  export { serialFromDate, serialToParts, timePeriodMatches } from './dates.js';
9
9
  export { NO_SHIFT, evaluate } from './eval.js';
10
10
  /**
@@ -1,5 +1,5 @@
1
1
  /** The lexical categories the {@link tokenize} pass emits. */
2
- export type TokenKind = 'num' | 'str' | 'err' | 'word' | 'sheetq' | 'op' | 'eof';
2
+ export type TokenKind = 'num' | 'str' | 'err' | 'word' | 'sheetq' | 'sref' | 'op' | 'eof';
3
3
  /** One token: its {@link TokenKind} and the matched source text. */
4
4
  export interface Token {
5
5
  readonly kind: TokenKind;
@@ -21,6 +21,29 @@ function isWordPart(ch) {
21
21
  return isWordStart(ch) || isDigit(ch) || ch === ".";
22
22
  }
23
23
  /**
24
+ * Where the bracket group that opens at `start` closes: past its matching `]`,
25
+ * nested groups included. A `'` escapes the character after it — a column
26
+ * named `Q[1]` is written `[Q'[1']]`.
27
+ *
28
+ * @param src The formula source.
29
+ * @param start The index of the opening `[`.
30
+ * @returns The index just past the closing `]`.
31
+ * @throws LexError when the group never closes.
32
+ */
33
+ function bracketsEnd(src, start) {
34
+ let depth = 0;
35
+ for (let j = start; j < src.length; j++) {
36
+ const c = src[j];
37
+ if (c === "'") {
38
+ j++;
39
+ continue;
40
+ }
41
+ if (c === "[") depth++;
42
+ else if (c === "]" && --depth === 0) return j + 1;
43
+ }
44
+ throw new LexError("unclosed [");
45
+ }
46
+ /**
24
47
  * Tokenize a formula string into a flat token stream terminated by an `eof`
25
48
  * token. Whitespace separates tokens but is otherwise dropped.
26
49
  *
@@ -122,6 +145,15 @@ function tokenize(src) {
122
145
  if (isWordStart(ch)) {
123
146
  let j = i + 1;
124
147
  while (j < n && isWordPart(src[j])) j++;
148
+ if (src[j] === "[") {
149
+ const end = bracketsEnd(src, j);
150
+ out.push({
151
+ kind: "sref",
152
+ text: src.slice(i, end)
153
+ });
154
+ i = end;
155
+ continue;
156
+ }
125
157
  out.push({
126
158
  kind: "word",
127
159
  text: src.slice(i, j)
@@ -129,6 +161,15 @@ function tokenize(src) {
129
161
  i = j;
130
162
  continue;
131
163
  }
164
+ if (ch === "[") {
165
+ const end = bracketsEnd(src, i);
166
+ out.push({
167
+ kind: "sref",
168
+ text: src.slice(i, end)
169
+ });
170
+ i = end;
171
+ continue;
172
+ }
132
173
  if (ch === "<" && src[i + 1] === "=") {
133
174
  out.push({
134
175
  kind: "op",
@@ -15,6 +15,19 @@ export interface CellRef {
15
15
  readonly col: Axis;
16
16
  readonly row: Axis;
17
17
  }
18
+ /** A table's part a structured reference names (§18.17.6.4); none named ⇒ `#Data`. */
19
+ export type TableArea = '#All' | '#Data' | '#Headers' | '#Totals' | '#This Row';
20
+ /**
21
+ * §18.17.6.4 — a structured reference: the table (empty for the one the cell
22
+ * is in), the parts of it, and the columns, first to last (all of them when
23
+ * undefined). `tblIncome[Amount]` is `{ table: 'tblIncome', areas: [],
24
+ * columns: ['Amount', 'Amount'] }`.
25
+ */
26
+ export interface StructuredRef {
27
+ readonly table: string;
28
+ readonly areas: ReadonlyArray<TableArea>;
29
+ readonly columns?: readonly [string, string];
30
+ }
18
31
  /** The formula syntax tree — a discriminated union over the node kind `k`. */
19
32
  export type Ast = {
20
33
  readonly k: 'num';
@@ -40,6 +53,9 @@ export type Ast = {
40
53
  } | {
41
54
  readonly k: 'name';
42
55
  readonly name: string;
56
+ } | {
57
+ readonly k: 'sref';
58
+ readonly ref: StructuredRef;
43
59
  } | {
44
60
  readonly k: 'array';
45
61
  readonly rows: ReadonlyArray<ReadonlyArray<Ast>>;
@@ -75,3 +91,12 @@ export declare class ParseError extends Error {
75
91
  * @throws ParseError on a malformed formula (or {@link LexError} from tokenizing).
76
92
  */
77
93
  export declare function parse(src: string): Ast;
94
+ /**
95
+ * §18.17.6.4 — read a structured reference's text: `Table[Col]`, `Table[]`,
96
+ * `Table[#Totals]`, `Table[[#Headers],[A]:[C]]`, `[@Col]`, `[@[Col 2]]`.
97
+ *
98
+ * @param text The token: the table's name, if any, and its brackets.
99
+ * @returns The reference.
100
+ * @throws ParseError when the brackets name nothing a table has.
101
+ */
102
+ export declare function parseStructuredRef(text: string): StructuredRef;