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.
- package/README.md +10 -0
- package/dist/esm/core/bidi/index.d.ts +10 -0
- package/dist/esm/core/bidi/index.js +18 -2
- package/dist/esm/core/converter/facade.js +10 -5
- package/dist/esm/core/converter/project.js +1 -0
- package/dist/esm/core/converter/ream.js +17 -6
- package/dist/esm/core/converter/sheet-images.d.ts +17 -0
- package/dist/esm/core/converter/sheet-images.js +33 -0
- package/dist/esm/core/crypto/offcrypto.js +56 -12
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +112 -0
- package/dist/esm/core/drawingml/chart-geometry.d.ts +36 -13
- package/dist/esm/core/drawingml/chart-geometry.js +372 -146
- package/dist/esm/core/drawingml/chart-parser.d.ts +19 -0
- package/dist/esm/core/drawingml/chart-parser.js +126 -18
- package/dist/esm/core/drawingml/colors.d.ts +6 -0
- package/dist/esm/core/drawingml/colors.js +23 -3
- package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
- package/dist/esm/core/drawingml/shape-render.js +4 -2
- package/dist/esm/core/font/measure.d.ts +9 -0
- package/dist/esm/core/font/measure.js +9 -1
- package/dist/esm/core/fonts/families.js +4 -2
- package/dist/esm/core/fonts/scripts.js +4 -2
- package/dist/esm/core/ir/flow.d.ts +6 -0
- package/dist/esm/core/ir/sheet.d.ts +43 -0
- package/dist/esm/core/number-format.d.ts +40 -15
- package/dist/esm/core/number-format.js +295 -83
- package/dist/esm/core/opc/tag-scan.d.ts +15 -0
- package/dist/esm/core/opc/tag-scan.js +41 -0
- package/dist/esm/core/opc/xml-entities.js +39 -2
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
- package/dist/esm/core/style-cascade/resolver.js +4 -2
- package/dist/esm/excel/column-bands.d.ts +39 -6
- package/dist/esm/excel/column-bands.js +41 -17
- package/dist/esm/excel/comments-parser.js +19 -0
- package/dist/esm/excel/conditional-format.d.ts +7 -3
- package/dist/esm/excel/conditional-format.js +77 -39
- package/dist/esm/excel/formula/context.d.ts +18 -0
- package/dist/esm/excel/formula/eval.js +51 -0
- package/dist/esm/excel/formula/functions.js +37 -14
- package/dist/esm/excel/formula/index.d.ts +2 -2
- package/dist/esm/excel/formula/lexer.d.ts +1 -1
- package/dist/esm/excel/formula/lexer.js +41 -0
- package/dist/esm/excel/formula/parser.d.ts +25 -0
- package/dist/esm/excel/formula/parser.js +95 -0
- package/dist/esm/excel/print-model.d.ts +38 -1
- package/dist/esm/excel/print-model.js +536 -83
- package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -1
- package/dist/esm/excel/sheet-drawing.d.ts +38 -2
- package/dist/esm/excel/sheet-drawing.js +47 -13
- package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
- package/dist/esm/excel/sheet-shape-parser.js +18 -6
- package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
- package/dist/esm/excel/sheet-to-flow.js +434 -47
- package/dist/esm/excel/styles-parser.d.ts +9 -0
- package/dist/esm/excel/styles-parser.js +80 -26
- package/dist/esm/excel/table-parser.js +5 -0
- package/dist/esm/excel/table-style-presets.d.ts +26 -0
- package/dist/esm/excel/table-style-presets.js +1555 -0
- package/dist/esm/excel/table-style.d.ts +13 -0
- package/dist/esm/excel/table-style.js +75 -0
- package/dist/esm/excel/tint.d.ts +10 -0
- package/dist/esm/excel/tint.js +75 -0
- package/dist/esm/excel/vml-drawing.d.ts +41 -3
- package/dist/esm/excel/vml-drawing.js +144 -11
- package/dist/esm/excel/worksheet-parser.js +89 -15
- package/dist/esm/excel/xls/biff-reader.js +10 -2
- package/dist/esm/excel/xlsx-reader.js +234 -19
- package/dist/esm/excel/xlsx-writer.js +8 -7
- package/dist/esm/html/html-writer.js +202 -20
- package/dist/esm/layout/fit-pages.d.ts +11 -0
- package/dist/esm/layout/fit-pages.js +86 -0
- package/dist/esm/layout/line-paint.d.ts +58 -0
- package/dist/esm/layout/line-paint.js +107 -0
- package/dist/esm/layout/page-doc.d.ts +51 -0
- package/dist/esm/layout/page-doc.js +141 -1
- package/dist/esm/layout/styled-layout.d.ts +7 -0
- package/dist/esm/layout/styled-layout.js +275 -52
- package/dist/esm/layout/turned-section.js +14 -2
- package/dist/esm/markdown/markdown-writer.js +75 -13
- package/dist/esm/pdf/styled-page-emitter.js +49 -71
- package/dist/esm/pdf/styled-page-renderer.js +1 -1
- package/dist/esm/pdf-reader/annot-draw.js +2 -2
- package/dist/esm/pdf-reader/annots.js +3 -1
- package/dist/esm/pdf-reader/font.js +4 -4
- package/dist/esm/pdf-reader/layout.js +1 -1
- package/dist/esm/svg/svg-text.d.ts +58 -0
- package/dist/esm/svg/svg-text.js +322 -0
- package/dist/esm/svg/svg-writer.js +135 -71
- package/dist/esm/word/document-parser.js +1 -1
- package/dist/esm/word/docx-reader.js +13 -8
- package/dist/esm/word/docx-to-pdf.js +3 -3
- package/dist/esm/word/docx-writer.js +11 -2
- package/dist/esm/word/font-table.js +89 -10
- package/dist/esm/word/numbering-parser.js +1 -1
- 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`)
|
|
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
|
|
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 =
|
|
399
|
-
return
|
|
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 =
|
|
403
|
-
return
|
|
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 ??
|
|
463
|
-
maxLen: (rule.maxLength ??
|
|
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
|
-
|
|
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.
|
|
558
|
+
const hit = crit.wildcard.test(text);
|
|
559
559
|
return crit.negate ? !hit : hit;
|
|
560
560
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
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
|
-
|
|
567
|
+
atoms.push(escapeRe(pattern[i + 1]));
|
|
567
568
|
i++;
|
|
568
|
-
} else if (ch === "*")
|
|
569
|
-
else
|
|
570
|
-
else out += escapeRe(ch);
|
|
569
|
+
} else if (ch === "*") pieces.push([]);
|
|
570
|
+
else atoms.push(ch === "?" ? "." : escapeRe(ch));
|
|
571
571
|
}
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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 };
|