reamkit 1.31.0 → 1.32.1

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 (98) 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/crypto/offcrypto.js +56 -12
  10. package/dist/esm/core/document-model/index.d.ts +1 -1
  11. package/dist/esm/core/document-model/types.d.ts +112 -0
  12. package/dist/esm/core/drawingml/chart-geometry.d.ts +36 -13
  13. package/dist/esm/core/drawingml/chart-geometry.js +372 -146
  14. package/dist/esm/core/drawingml/chart-parser.d.ts +19 -0
  15. package/dist/esm/core/drawingml/chart-parser.js +126 -18
  16. package/dist/esm/core/drawingml/colors.d.ts +6 -0
  17. package/dist/esm/core/drawingml/colors.js +23 -3
  18. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  19. package/dist/esm/core/drawingml/shape-render.js +4 -2
  20. package/dist/esm/core/font/measure.d.ts +9 -0
  21. package/dist/esm/core/font/measure.js +9 -1
  22. package/dist/esm/core/fonts/families.js +4 -2
  23. package/dist/esm/core/fonts/scripts.js +4 -2
  24. package/dist/esm/core/ir/flow.d.ts +6 -0
  25. package/dist/esm/core/ir/sheet.d.ts +43 -0
  26. package/dist/esm/core/number-format.d.ts +40 -15
  27. package/dist/esm/core/number-format.js +295 -83
  28. package/dist/esm/core/opc/tag-scan.d.ts +15 -0
  29. package/dist/esm/core/opc/tag-scan.js +41 -0
  30. package/dist/esm/core/opc/xml-entities.js +39 -2
  31. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  32. package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
  33. package/dist/esm/core/style-cascade/resolver.js +4 -2
  34. package/dist/esm/excel/column-bands.d.ts +39 -6
  35. package/dist/esm/excel/column-bands.js +41 -17
  36. package/dist/esm/excel/comments-parser.js +19 -0
  37. package/dist/esm/excel/conditional-format.d.ts +7 -3
  38. package/dist/esm/excel/conditional-format.js +77 -39
  39. package/dist/esm/excel/formula/context.d.ts +18 -0
  40. package/dist/esm/excel/formula/eval.js +51 -0
  41. package/dist/esm/excel/formula/functions.js +37 -14
  42. package/dist/esm/excel/formula/index.d.ts +2 -2
  43. package/dist/esm/excel/formula/lexer.d.ts +1 -1
  44. package/dist/esm/excel/formula/lexer.js +41 -0
  45. package/dist/esm/excel/formula/parser.d.ts +25 -0
  46. package/dist/esm/excel/formula/parser.js +95 -0
  47. package/dist/esm/excel/print-model.d.ts +38 -1
  48. package/dist/esm/excel/print-model.js +536 -83
  49. package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
  50. package/dist/esm/excel/shared-strings-parser.js +11 -1
  51. package/dist/esm/excel/sheet-drawing.d.ts +38 -2
  52. package/dist/esm/excel/sheet-drawing.js +47 -13
  53. package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
  54. package/dist/esm/excel/sheet-shape-parser.js +18 -6
  55. package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
  56. package/dist/esm/excel/sheet-to-flow.js +434 -47
  57. package/dist/esm/excel/styles-parser.d.ts +9 -0
  58. package/dist/esm/excel/styles-parser.js +80 -26
  59. package/dist/esm/excel/table-parser.js +5 -0
  60. package/dist/esm/excel/table-style-presets.d.ts +26 -0
  61. package/dist/esm/excel/table-style-presets.js +1555 -0
  62. package/dist/esm/excel/table-style.d.ts +13 -0
  63. package/dist/esm/excel/table-style.js +75 -0
  64. package/dist/esm/excel/tint.d.ts +10 -0
  65. package/dist/esm/excel/tint.js +75 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +41 -3
  67. package/dist/esm/excel/vml-drawing.js +144 -11
  68. package/dist/esm/excel/worksheet-parser.js +89 -15
  69. package/dist/esm/excel/xls/biff-reader.js +10 -2
  70. package/dist/esm/excel/xlsx-reader.js +234 -19
  71. package/dist/esm/excel/xlsx-writer.js +8 -7
  72. package/dist/esm/html/html-writer.js +202 -20
  73. package/dist/esm/layout/fit-pages.d.ts +11 -0
  74. package/dist/esm/layout/fit-pages.js +86 -0
  75. package/dist/esm/layout/line-paint.d.ts +58 -0
  76. package/dist/esm/layout/line-paint.js +107 -0
  77. package/dist/esm/layout/page-doc.d.ts +51 -0
  78. package/dist/esm/layout/page-doc.js +141 -1
  79. package/dist/esm/layout/styled-layout.d.ts +7 -0
  80. package/dist/esm/layout/styled-layout.js +275 -52
  81. package/dist/esm/layout/turned-section.js +14 -2
  82. package/dist/esm/markdown/markdown-writer.js +75 -13
  83. package/dist/esm/pdf/styled-page-emitter.js +49 -71
  84. package/dist/esm/pdf/styled-page-renderer.js +1 -1
  85. package/dist/esm/pdf-reader/annot-draw.js +2 -2
  86. package/dist/esm/pdf-reader/annots.js +3 -1
  87. package/dist/esm/pdf-reader/font.js +4 -4
  88. package/dist/esm/pdf-reader/layout.js +1 -1
  89. package/dist/esm/svg/svg-text.d.ts +58 -0
  90. package/dist/esm/svg/svg-text.js +322 -0
  91. package/dist/esm/svg/svg-writer.js +135 -71
  92. package/dist/esm/word/document-parser.js +1 -1
  93. package/dist/esm/word/docx-reader.js +13 -8
  94. package/dist/esm/word/docx-to-pdf.js +3 -3
  95. package/dist/esm/word/docx-writer.js +11 -2
  96. package/dist/esm/word/font-table.js +89 -10
  97. package/dist/esm/word/numbering-parser.js +1 -1
  98. package/package.json +7 -3
@@ -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;
@@ -537,7 +537,7 @@ function parseCriteria(s) {
537
537
  return {
538
538
  kind: "text",
539
539
  negate: op === "<>",
540
- re: wildcardToRegExp(text)
540
+ wildcard: wildcard(text)
541
541
  };
542
542
  }
543
543
  function matchCriteria(cell, crit) {
@@ -555,22 +555,45 @@ function matchCriteria(cell, crit) {
555
555
  }
556
556
  const text = cell.t === "str" ? cell.v : cell.t === "blank" ? "" : void 0;
557
557
  if (text === void 0) return false;
558
- const hit = crit.re.test(text);
558
+ const hit = crit.wildcard.test(text);
559
559
  return crit.negate ? !hit : hit;
560
560
  }
561
- function wildcardToRegExp(pattern) {
562
- let out = "^";
561
+ function wildcard(pattern) {
562
+ const pieces = [[]];
563
563
  for (let i = 0; i < pattern.length; i++) {
564
564
  const ch = pattern[i];
565
+ const atoms = pieces[pieces.length - 1];
565
566
  if (ch === "~" && (pattern[i + 1] === "*" || pattern[i + 1] === "?" || pattern[i + 1] === "~")) {
566
- out += escapeRe(pattern[i + 1]);
567
+ atoms.push(escapeRe(pattern[i + 1]));
567
568
  i++;
568
- } else if (ch === "*") out += ".*";
569
- else if (ch === "?") out += ".";
570
- else out += escapeRe(ch);
569
+ } else if (ch === "*") pieces.push([]);
570
+ else atoms.push(ch === "?" ? "." : escapeRe(ch));
571
571
  }
572
- out += "$";
573
- return new RegExp(out, "is");
572
+ const all = pieces.map((atoms) => ({
573
+ at: new RegExp(atoms.join(""), "isy"),
574
+ search: new RegExp(atoms.join(""), "gis"),
575
+ width: atoms.length
576
+ }));
577
+ const fits = (piece, text, at) => {
578
+ piece.at.lastIndex = at;
579
+ return piece.at.test(text);
580
+ };
581
+ const first = all[0];
582
+ if (all.length === 1) return { test: (text) => text.length === first.width && fits(first, text, 0) };
583
+ const last = all[all.length - 1];
584
+ const between = all.slice(1, -1).filter((piece) => piece.width > 0);
585
+ return { test(text) {
586
+ const end = text.length - last.width;
587
+ if (end < first.width || !fits(first, text, 0) || !fits(last, text, end)) return false;
588
+ let at = first.width;
589
+ for (const piece of between) {
590
+ piece.search.lastIndex = at;
591
+ const found = piece.search.exec(text);
592
+ if (found === null || found.index + piece.width > end) return false;
593
+ at = found.index + piece.width;
594
+ }
595
+ return true;
596
+ } };
574
597
  }
575
598
  function escapeRe(ch) {
576
599
  return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
@@ -1090,10 +1113,10 @@ function matchFn(args, ev, ctx) {
1090
1113
  if (len > MAX_SCAN) return err("#N/A");
1091
1114
  const at = (i) => horizontal ? refGet(range.sheet, ctx, rect.r0, rect.c0 + i) : refGet(range.sheet, ctx, rect.r0 + i, rect.c0);
1092
1115
  if (type === 0) {
1093
- const re = target.t === "str" ? wildcardToRegExp(target.v) : void 0;
1116
+ const wild = target.t === "str" ? wildcard(target.v) : void 0;
1094
1117
  for (let i = 0; i < len; i++) {
1095
1118
  const cell = at(i);
1096
- if (re ? cell.t === "str" && re.test(cell.v) : scalarEquals(cell, target)) return num(i + 1);
1119
+ if (wild ? cell.t === "str" && wild.test(cell.v) : scalarEquals(cell, target)) return num(i + 1);
1097
1120
  }
1098
1121
  return err("#N/A");
1099
1122
  }
@@ -1156,10 +1179,10 @@ function lookupFn(args, ev, ctx, vh) {
1156
1179
  const keyAt = (i) => vertical ? refGet(sh, ctx, rect.r0 + i, rect.c0) : refGet(sh, ctx, rect.r0, rect.c0 + i);
1157
1180
  const resultAt = (i) => vertical ? refGet(sh, ctx, rect.r0 + i, rect.c0 + line - 1) : refGet(sh, ctx, rect.r0 + line - 1, rect.c0 + i);
1158
1181
  if (!approx) {
1159
- const re = target.t === "str" ? wildcardToRegExp(target.v) : void 0;
1182
+ const wild = target.t === "str" ? wildcard(target.v) : void 0;
1160
1183
  for (let i = 0; i < vecLen; i++) {
1161
1184
  const k = keyAt(i);
1162
- if (re ? k.t === "str" && re.test(k.v) : scalarEquals(k, target)) return resultAt(i);
1185
+ if (wild ? k.t === "str" && wild.test(k.v) : scalarEquals(k, target)) return resultAt(i);
1163
1186
  }
1164
1187
  return err("#N/A");
1165
1188
  }
@@ -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;
@@ -148,6 +148,10 @@ var Parser = class {
148
148
  case "sheetq":
149
149
  this.expect("!");
150
150
  return this.parseSheetCell(t.text);
151
+ case "sref": return {
152
+ k: "sref",
153
+ ref: parseStructuredRef(t.text)
154
+ };
151
155
  case "op":
152
156
  if (t.text === "(") {
153
157
  const e = this.parseExpr(0);
@@ -323,5 +327,96 @@ function columnToIndex(letters) {
323
327
  for (let i = 0; i < up.length; i++) idx = idx * 26 + (up.charCodeAt(i) - 64);
324
328
  return idx - 1;
325
329
  }
330
+ var AREAS = new Map([
331
+ ["#all", "#All"],
332
+ ["#data", "#Data"],
333
+ ["#headers", "#Headers"],
334
+ ["#totals", "#Totals"],
335
+ ["#this row", "#This Row"]
336
+ ]);
337
+ /**
338
+ * §18.17.6.4 — read a structured reference's text: `Table[Col]`, `Table[]`,
339
+ * `Table[#Totals]`, `Table[[#Headers],[A]:[C]]`, `[@Col]`, `[@[Col 2]]`.
340
+ *
341
+ * @param text The token: the table's name, if any, and its brackets.
342
+ * @returns The reference.
343
+ * @throws ParseError when the brackets name nothing a table has.
344
+ */
345
+ function parseStructuredRef(text) {
346
+ const open = text.indexOf("[");
347
+ const table = text.slice(0, open);
348
+ let inner = text.slice(open + 1, -1).trim();
349
+ const areas = [];
350
+ let columns;
351
+ if (inner.startsWith("@")) {
352
+ areas.push("#This Row");
353
+ inner = inner.slice(1).trim();
354
+ if (inner.length > 0 && !inner.startsWith("[")) inner = `[${inner}]`;
355
+ } else if (inner.length > 0 && !inner.startsWith("[")) inner = `[${inner}]`;
356
+ for (const item of splitTopLevel(inner)) {
357
+ const range = splitRange(item);
358
+ if (range) {
359
+ columns = [unescapeName(range[0]), unescapeName(range[1])];
360
+ continue;
361
+ }
362
+ const body = item.slice(1, -1);
363
+ const area = AREAS.get(body.trim().toLowerCase());
364
+ if (area) areas.push(area);
365
+ else {
366
+ const name = unescapeName(body);
367
+ columns = [name, name];
368
+ }
369
+ }
370
+ return {
371
+ table,
372
+ areas,
373
+ ...columns ? { columns } : {}
374
+ };
375
+ }
376
+ /** The bracket groups of a list `[a],[b]:[c]`, each with its `:` partner. */
377
+ function splitTopLevel(inner) {
378
+ const out = [];
379
+ let depth = 0;
380
+ let start = 0;
381
+ for (let i = 0; i < inner.length; i++) {
382
+ const c = inner[i];
383
+ if (c === "'") {
384
+ i++;
385
+ continue;
386
+ }
387
+ if (c === "[") depth++;
388
+ else if (c === "]") depth--;
389
+ else if (c === "," && depth === 0) {
390
+ out.push(inner.slice(start, i).trim());
391
+ start = i + 1;
392
+ }
393
+ }
394
+ const last = inner.slice(start).trim();
395
+ if (last.length > 0) out.push(last);
396
+ for (const item of out) if (!item.startsWith("[") || !item.endsWith("]")) throw new ParseError(`bad structured reference part ${item}`);
397
+ return out;
398
+ }
399
+ /** `[A]:[C]` → the two columns' bracketed bodies, or undefined for one group. */
400
+ function splitRange(item) {
401
+ let depth = 0;
402
+ for (let i = 0; i < item.length; i++) {
403
+ const c = item[i];
404
+ if (c === "'") {
405
+ i++;
406
+ continue;
407
+ }
408
+ if (c === "[") depth++;
409
+ else if (c === "]") depth--;
410
+ else if (c === ":" && depth === 0) {
411
+ const a = item.slice(0, i).trim();
412
+ const b = item.slice(i + 1).trim();
413
+ return [a.slice(1, -1), b.slice(1, -1)];
414
+ }
415
+ }
416
+ }
417
+ /** A column name as written in a reference, its `'` escapes undone. */
418
+ function unescapeName(name) {
419
+ return name.trim().replace(/'(.)/g, "$1");
420
+ }
326
421
  //#endregion
327
422
  export { parse };