@a3s-lab/office 0.35.0 → 0.36.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.
- package/README.md +27 -10
- package/dist/0~7048.js +497 -2
- package/dist/0~spreadsheet-editor.js +132 -14
- package/dist/0~work-office-diagnostics.js +1 -1
- package/dist/3266.js +189 -10
- package/dist/4121.js +257 -0
- package/dist/8715.js +299 -299
- package/dist/core.js +14 -4
- package/dist/internal/features/work/editors/spreadsheet-calculation-model.d.ts +1 -1
- package/dist/internal/features/work/editors/spreadsheet-calculation-projection.d.ts +5 -2
- package/dist/internal/features/work/editors/spreadsheet-table-calculated-columns.d.ts +42 -0
- package/dist/internal/features/work/work-types.d.ts +5 -0
- package/dist/internal/kernel/office-kernel-protocol.d.ts +1 -1
- package/dist/internal/kernel/office-kernel-spreadsheet-protocol.d.ts +18 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-structured-reference.d.ts +48 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/office-kernel.worker.js +505 -2
- package/docs/latest/en/browser-editor-architecture.md +21 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,9 +56,8 @@ workflows, and a separate Rust automation plane.
|
|
|
56
56
|
|
|
57
57
|
## Latest on `main`
|
|
58
58
|
|
|
59
|
-
The `0.
|
|
60
|
-
|
|
61
|
-
detail in the documentation:
|
|
59
|
+
The `0.36.0` release exposes these capabilities as normal user-facing
|
|
60
|
+
Playground templates, with matching implementation detail in the documentation:
|
|
62
61
|
|
|
63
62
|
| Editor | Latest capability | Public entry |
|
|
64
63
|
| --- | --- | --- |
|
|
@@ -69,12 +68,13 @@ detail in the documentation:
|
|
|
69
68
|
| Writer | Native character shading with exact `w:shd` patterns, foreground/background color identity, explicit resets, authoring, and DOCX reopen | Playground **Latest capabilities → 字符底纹** · [Document reference](docs/latest/en/components/document.mdx#native-character-shading) |
|
|
70
69
|
| Writer | Independent Latin, East Asian, and bidi proofing languages plus explicit `w:noProof` inclusion/exclusion | Playground **Latest capabilities → 校对语言** · [Document reference](docs/latest/en/components/document.mdx#native-proofing-languages) |
|
|
71
70
|
| Spreadsheet | Complete common Data Validation input, blank/dropdown, and Stop/Warning/Information error settings with native XLSX round trips | Playground **Latest capabilities → 数据验证** · [Spreadsheet reference](docs/latest/en/components/spreadsheet.mdx#data-validation) |
|
|
71
|
+
| Spreadsheet | Bounded structured-reference calculation plus automatic calculated-column fill for table names/display names, contiguous columns, header/data/totals selectors, qualified references, and table-local current-row formulas across Rust/WASM and JavaScript fallback | Playground **Latest capabilities → 结构化引用** · [Spreadsheet reference](docs/latest/en/components/spreadsheet.mdx#structured-reference-calculation) |
|
|
72
72
|
| PDF | Insert, delete, rotate, reorder, extract, merge, and split pages through a dedicated Web Worker, with Blob-level Undo/Redo and independent binary reopen verification | Playground **Latest capabilities → 组织 PDF 页面** · [PDF reference](docs/latest/en/components/pdf.mdx#page-organization) |
|
|
73
73
|
|
|
74
|
-
All
|
|
74
|
+
All nine entries are reachable from the first Playground viewport. The
|
|
75
75
|
filterable capability gallery groups releases by editor, publishes the source
|
|
76
76
|
release on every card, and grows from one shared metadata list instead of a
|
|
77
|
-
fixed-width strip. The
|
|
77
|
+
fixed-width strip. The eight content templates also remain under **新建**;
|
|
78
78
|
**组织 PDF 页面** opens the normal PDF file workflow because page organization
|
|
79
79
|
must operate on host-provided source bytes.
|
|
80
80
|
|
|
@@ -151,8 +151,8 @@ baseline rather than one specific release.
|
|
|
151
151
|
| Cells, sheets, navigation, and history | **Supported** — multiple sheets, sparse editing, search, clipboard, four-direction fill, exact formula/value copy from above, and undo/redo | Mature grid workflows across desktop and web |
|
|
152
152
|
| Formatting and style rendering | **Partial** — native fonts, colors, borders, alignment, number formats, cell styles, all 17 native non-solid OOXML pattern fills, native linear/path XLSX gradients, and one Format Cells surface that authors none, solid, pattern, or gradient fills with exact geometry and 2–256 ordered stops; static date/time entry, contrast-safe font preview, native XLSX rich-text runs, selected-text font formatting, direct formula-bar/F2 insertion or deletion, and bounded authenticated formatted-HTML paste | Disjoint multi-edit rich-text authoring, broader themes, locale formats, and advanced style effects |
|
|
153
153
|
| Ribbon and shortcuts | **Partial** — common Office-style Home/Data/View commands, grid-scoped shortcuts, and focused Font-dialog aliases | Larger command catalog and platform-specific accelerators |
|
|
154
|
-
| Formulas and recalculation | **Partial** — dependency-aware calculation and
|
|
155
|
-
| Tables, pivots, charts, and rules | **Partial** — native tables, pivots, charts, conditional formatting, and common validation rules with complete input/error-setting authoring |
|
|
154
|
+
| Formulas and recalculation | **Partial** — dependency-aware calculation plus bounded structured references (`Table[Column]`, contiguous ranges, `#All`/`#Headers`/`#Data`/`#Totals`/`#This Row`, and `[@Column]`) in the shared Rust/WASM and JavaScript paths | Wider functions, arrays, volatile semantics, and calculation parity |
|
|
155
|
+
| Tables, pivots, charts, and rules | **Partial** — native tables, bounded structured-reference formulas, calculated-column formulas with automatic fill for newly inserted body rows, pivots, charts, conditional formatting, and common validation rules with complete input/error-setting authoring | Complete totals-row authoring, slicers, pivot charts, custom/dependent validation rules, advanced analysis, and broader browser alert flows |
|
|
156
156
|
| Large worksheets | **Supported with boundaries** — maximum-dimension sparse import/editing and viewport-bounded Canvas painting | Highly optimized native grid with hardware-dependent limits |
|
|
157
157
|
| Files and printing | **Partial** — XLS/XLSX/ODS/CSV import, XLSX export, and PDF output | Broader round trips, external data, print fidelity, and legacy conversion |
|
|
158
158
|
| External data, macros, and specialist analysis | **Gap** — active macros are never executed; bounded models are still needed for data connections and solver-like tools | Established data, macro/add-in, scenario, and optimization ecosystems |
|
|
@@ -740,9 +740,25 @@ stable IDs, workbook-unique names, zero-based ranges, ordered columns, filters,
|
|
|
740
740
|
header/totals flags, and built-in style identity. XLSX import and export keep
|
|
741
741
|
native table parts and relationships. Table styling is resolved only for the
|
|
742
742
|
visible Canvas cells; converting a table to a range materializes the confirmed
|
|
743
|
-
appearance without densifying unrelated worksheet space.
|
|
744
|
-
|
|
745
|
-
|
|
743
|
+
appearance without densifying unrelated worksheet space.
|
|
744
|
+
|
|
745
|
+
The calculation kernel resolves a bounded common structured-reference subset:
|
|
746
|
+
table names or `displayName`, worksheet-qualified tables, contiguous column
|
|
747
|
+
ranges, `#All`, `#Headers`, `#Data`, `#Totals`, `#This Row`, and table-local
|
|
748
|
+
`[@Column]` formulas. Rust/WASM and JavaScript fallback share the parser,
|
|
749
|
+
catalog, dependency graph, and no-history projection path. Requests accept at
|
|
750
|
+
most 1,024 tables and materialize at most 100,000 cells per structured range;
|
|
751
|
+
whole-row/column, three-dimensional, external, disjoint, missing, or
|
|
752
|
+
over-budget references fail closed. A validated calculated-column rule is
|
|
753
|
+
inferred from matching current-row formulas and fills only newly inserted table
|
|
754
|
+
body rows. Existing values, formulas, and manual exceptions always win; a
|
|
755
|
+
conflicting column fails closed and drops its automatic rule. XLSX
|
|
756
|
+
`<calculatedColumnFormula>` metadata round-trips with the leading `=` restored
|
|
757
|
+
in the controlled model, while unsafe or external formulas are omitted.
|
|
758
|
+
The Playground's **结构化引用** template shows a calculated `Revenue` column,
|
|
759
|
+
an insertion hint, a totals row, and a worksheet-qualified summary. Complete
|
|
760
|
+
totals-row authoring, slicers, and external/query tables remain explicit
|
|
761
|
+
boundaries.
|
|
746
762
|
|
|
747
763
|
Use `downloadArtifact` to start a browser download or
|
|
748
764
|
`createArtifactBlob` when your application owns upload and persistence.
|
|
@@ -1625,6 +1641,7 @@ keep working without hard-coded return URLs.
|
|
|
1625
1641
|
|
|
1626
1642
|
- [Live Playground](https://a3s-lab.github.io/Office/playground/)
|
|
1627
1643
|
- [Documentation center](https://a3s-lab.github.io/Office/docs/)
|
|
1644
|
+
- [A3S Office 0.36.0 documentation](https://a3s-lab.github.io/Office/docs/0.36.0/)
|
|
1628
1645
|
- [A3S Office 0.34.0 documentation](https://a3s-lab.github.io/Office/docs/0.34.0/)
|
|
1629
1646
|
- [A3S Office 0.33.0 documentation](https://a3s-lab.github.io/Office/docs/0.33.0/)
|
|
1630
1647
|
- [A3S Office 0.32.0 documentation](https://a3s-lab.github.io/Office/docs/0.32.0/)
|
package/dist/0~7048.js
CHANGED
|
@@ -1462,6 +1462,7 @@ function validateSpreadsheetCalculationRequest(request) {
|
|
|
1462
1462
|
if (!sheet.id.trim() || utf8ByteLength(sheet.id) > MAX_SPREADSHEET_IDENTIFIER_BYTES || !sheet.name.trim() || utf8ByteLength(sheet.name) > MAX_SPREADSHEET_IDENTIFIER_BYTES || sheetIds.has(sheet.id) || sheetNames.has(normalizedName)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.sheet_invalid', 'Spreadsheet sheet IDs and names must be unique and non-empty.');
|
|
1463
1463
|
sheetIds.add(sheet.id);
|
|
1464
1464
|
sheetNames.add(normalizedName);
|
|
1465
|
+
validateSpreadsheetTables(sheet, request.sheets);
|
|
1465
1466
|
const coordinates = new Set();
|
|
1466
1467
|
for (const cell of sheet.cells){
|
|
1467
1468
|
const key = `${cell.row}:${cell.column}`;
|
|
@@ -1473,6 +1474,68 @@ function validateSpreadsheetCalculationRequest(request) {
|
|
|
1473
1474
|
}
|
|
1474
1475
|
for (const target of request.targets ?? [])if (!sheetIds.has(target.sheetId) || !boundedSpreadsheetIndex(target.row, office_kernel_spreadsheet_protocol_OFFICE_KERNEL_SPREADSHEET_MAX_ROWS) || !boundedSpreadsheetIndex(target.column, 16384)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.target_invalid', 'Spreadsheet calculation targets must reference an existing, bounded cell.');
|
|
1475
1476
|
}
|
|
1477
|
+
function validateSpreadsheetTables(sheet, sheets) {
|
|
1478
|
+
const tables = sheet.tables ?? [];
|
|
1479
|
+
const tableCount = sheets.reduce((count, candidate)=>count + (candidate.tables?.length ?? 0), 0);
|
|
1480
|
+
if (tableCount > 1024) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_limit_exceeded', 'A Spreadsheet calculation request may contain at most 1024 tables.');
|
|
1481
|
+
const aliases = new Map();
|
|
1482
|
+
for (const candidate of sheets)for (const [tableIndex, table] of (candidate.tables ?? []).entries()){
|
|
1483
|
+
const identity = `${candidate.id}\u0000${tableIndex}`;
|
|
1484
|
+
for (const alias of [
|
|
1485
|
+
table.name,
|
|
1486
|
+
table.displayName
|
|
1487
|
+
]){
|
|
1488
|
+
if (!alias) continue;
|
|
1489
|
+
const normalized = alias.toLocaleLowerCase();
|
|
1490
|
+
const existing = aliases.get(normalized);
|
|
1491
|
+
if (existing && existing !== identity) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table name '${alias}' is ambiguous.`);
|
|
1492
|
+
aliases.set(normalized, identity);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const ranges = [];
|
|
1496
|
+
for (const table of tables){
|
|
1497
|
+
for (const [kind, value] of [
|
|
1498
|
+
[
|
|
1499
|
+
'startRow',
|
|
1500
|
+
table.startRow
|
|
1501
|
+
],
|
|
1502
|
+
[
|
|
1503
|
+
'endRow',
|
|
1504
|
+
table.endRow
|
|
1505
|
+
],
|
|
1506
|
+
[
|
|
1507
|
+
'startColumn',
|
|
1508
|
+
table.startColumn
|
|
1509
|
+
],
|
|
1510
|
+
[
|
|
1511
|
+
'endColumn',
|
|
1512
|
+
table.endColumn
|
|
1513
|
+
]
|
|
1514
|
+
])if (!boundedSpreadsheetIndex(value, kind.endsWith('Row') ? office_kernel_spreadsheet_protocol_OFFICE_KERNEL_SPREADSHEET_MAX_ROWS : 16384)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' has an out-of-bounds ${kind}.`);
|
|
1515
|
+
if (table.startRow > table.endRow || table.startColumn > table.endColumn || table.columns.length !== table.endColumn - table.startColumn + 1) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' has an invalid range or column count.`);
|
|
1516
|
+
validateTableName(table.name, 'name');
|
|
1517
|
+
if (void 0 !== table.displayName) validateTableName(table.displayName, 'displayName');
|
|
1518
|
+
const columnNames = new Set();
|
|
1519
|
+
for (const column of table.columns){
|
|
1520
|
+
validateTableName(column, 'column');
|
|
1521
|
+
const normalized = column.toLocaleLowerCase();
|
|
1522
|
+
if (columnNames.has(normalized)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' contains duplicate column names.`);
|
|
1523
|
+
columnNames.add(normalized);
|
|
1524
|
+
}
|
|
1525
|
+
if (table.headerRow && table.totalsRow && table.startRow === table.endRow) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' cannot use header and totals rows in one row.`);
|
|
1526
|
+
for (const previous of ranges)if (table.startRow <= previous.endRow && table.endRow >= previous.startRow && table.startColumn <= previous.endColumn && table.endColumn >= previous.startColumn) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet tables '${previous.name}' and '${table.name}' overlap.`);
|
|
1527
|
+
ranges.push({
|
|
1528
|
+
startRow: table.startRow,
|
|
1529
|
+
endRow: table.endRow,
|
|
1530
|
+
startColumn: table.startColumn,
|
|
1531
|
+
endColumn: table.endColumn,
|
|
1532
|
+
name: table.name
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function validateTableName(value, kind) {
|
|
1537
|
+
if (!value.trim() || utf8ByteLength(value) > MAX_SPREADSHEET_IDENTIFIER_BYTES) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table ${kind} must contain 1-${MAX_SPREADSHEET_IDENTIFIER_BYTES} UTF-8 bytes.`);
|
|
1538
|
+
}
|
|
1476
1539
|
function boundedSpreadsheetIndex(value, exclusiveMaximum) {
|
|
1477
1540
|
return Number.isSafeInteger(value) && value >= 0 && value < exclusiveMaximum;
|
|
1478
1541
|
}
|
|
@@ -1566,6 +1629,428 @@ function isParserErrorValue(value) {
|
|
|
1566
1629
|
const withSuffix = '#DIV/0' === normalized ? '#DIV/0!' : normalized;
|
|
1567
1630
|
return isOfficeKernelSpreadsheetError(withSuffix);
|
|
1568
1631
|
}
|
|
1632
|
+
class SpreadsheetStructuredReferenceError extends Error {
|
|
1633
|
+
kind;
|
|
1634
|
+
constructor(kind, message){
|
|
1635
|
+
super(message);
|
|
1636
|
+
this.name = 'SpreadsheetStructuredReferenceError';
|
|
1637
|
+
this.kind = kind;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function parseSpreadsheetStructuredReference(reference) {
|
|
1641
|
+
const open = reference.indexOf('[');
|
|
1642
|
+
if (open < 0) throw invalidReference(reference);
|
|
1643
|
+
const tableName = reference.slice(0, open) || void 0;
|
|
1644
|
+
const content = outerGroup(reference.slice(open));
|
|
1645
|
+
if (null === content) throw invalidReference(reference);
|
|
1646
|
+
const rows = {
|
|
1647
|
+
all: false,
|
|
1648
|
+
headers: false,
|
|
1649
|
+
data: false,
|
|
1650
|
+
totals: false,
|
|
1651
|
+
current: false
|
|
1652
|
+
};
|
|
1653
|
+
let firstColumn;
|
|
1654
|
+
let lastColumn;
|
|
1655
|
+
if (content.startsWith('@')) {
|
|
1656
|
+
rows.current = true;
|
|
1657
|
+
const column = parseCurrentColumn(content.slice(1), reference);
|
|
1658
|
+
firstColumn = column;
|
|
1659
|
+
lastColumn = column;
|
|
1660
|
+
} else if (content.startsWith('[')) [firstColumn, lastColumn] = parseNestedSelection(content, reference, rows);
|
|
1661
|
+
else {
|
|
1662
|
+
const item = tableItem(content);
|
|
1663
|
+
if (item) applyTableItem(rows, item, reference);
|
|
1664
|
+
else {
|
|
1665
|
+
const column = parsePlainColumn(content, reference);
|
|
1666
|
+
firstColumn = column;
|
|
1667
|
+
lastColumn = column;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (!rows.all && !rows.headers && !rows.data && !rows.totals && !rows.current) rows.data = true;
|
|
1671
|
+
return {
|
|
1672
|
+
tableName,
|
|
1673
|
+
firstColumn,
|
|
1674
|
+
lastColumn,
|
|
1675
|
+
rows
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
class SpreadsheetStructuredReferenceCatalog {
|
|
1679
|
+
sheets;
|
|
1680
|
+
definitions = [];
|
|
1681
|
+
byName = new Map();
|
|
1682
|
+
bySheet = new Map();
|
|
1683
|
+
constructor(sheets){
|
|
1684
|
+
this.sheets = sheets;
|
|
1685
|
+
for (const sheet of sheets){
|
|
1686
|
+
const indexes = [];
|
|
1687
|
+
for (const table of sheet.tables ?? []){
|
|
1688
|
+
const definition = {
|
|
1689
|
+
...table,
|
|
1690
|
+
sheetId: sheet.id,
|
|
1691
|
+
sheetName: sheet.name
|
|
1692
|
+
};
|
|
1693
|
+
const index = this.definitions.length;
|
|
1694
|
+
this.definitions.push(definition);
|
|
1695
|
+
indexes.push(index);
|
|
1696
|
+
for (const alias of [
|
|
1697
|
+
table.name,
|
|
1698
|
+
table.displayName
|
|
1699
|
+
]){
|
|
1700
|
+
if (!alias) continue;
|
|
1701
|
+
const key = alias.toLocaleLowerCase();
|
|
1702
|
+
if (!this.byName.has(key)) this.byName.set(key, index);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
this.bySheet.set(sheet.id, indexes);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
resolve(qualifier, reference, currentSheet, currentColumn, currentRow) {
|
|
1709
|
+
const parsed = parseSpreadsheetStructuredReference(reference);
|
|
1710
|
+
const table = this.resolveTable(parsed, reference, currentSheet, currentColumn, currentRow);
|
|
1711
|
+
if (qualifier && table.sheetName.toLocaleLowerCase() !== qualifier.toLocaleLowerCase()) throw new SpreadsheetStructuredReferenceError('missing-table', `Spreadsheet table '${table.name}' is not on worksheet '${qualifier}'.`);
|
|
1712
|
+
if (parsed.rows.current && currentSheet.id !== table.sheetId) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference #This Row requires the current formula cell to be on table '${table.name}'.`);
|
|
1713
|
+
const [startColumn, endColumn] = this.resolveColumns(table, parsed);
|
|
1714
|
+
return this.resolveRows(table, parsed.rows, currentRow).map(([startRow, endRow])=>({
|
|
1715
|
+
sheetId: table.sheetId,
|
|
1716
|
+
sheetName: table.sheetName,
|
|
1717
|
+
startRow,
|
|
1718
|
+
endRow,
|
|
1719
|
+
startColumn,
|
|
1720
|
+
endColumn
|
|
1721
|
+
}));
|
|
1722
|
+
}
|
|
1723
|
+
resolveTable(parsed, reference, currentSheet, currentColumn, currentRow) {
|
|
1724
|
+
if (parsed.tableName) {
|
|
1725
|
+
const index = this.byName.get(parsed.tableName.toLocaleLowerCase());
|
|
1726
|
+
if (void 0 === index) throw new SpreadsheetStructuredReferenceError('missing-table', `Spreadsheet table '${parsed.tableName}' does not exist.`);
|
|
1727
|
+
const table = this.definitions[index];
|
|
1728
|
+
if (!table) throw invalidReference(reference);
|
|
1729
|
+
return table;
|
|
1730
|
+
}
|
|
1731
|
+
const matching = (this.bySheet.get(currentSheet.id) ?? []).map((index)=>this.definitions[index]).filter((table)=>void 0 !== table && currentRow >= table.startRow && currentRow <= table.endRow && currentColumn >= table.startColumn && currentColumn <= table.endColumn);
|
|
1732
|
+
if (!matching.length) throw new SpreadsheetStructuredReferenceError('missing-table', `Table-local structured reference '${reference}' requires the current formula cell to be inside a Spreadsheet table.`);
|
|
1733
|
+
if (matching.length > 1) throw new SpreadsheetStructuredReferenceError('unsupported', `Table-local structured reference '${reference}' is ambiguous at the current formula cell.`);
|
|
1734
|
+
return matching[0];
|
|
1735
|
+
}
|
|
1736
|
+
resolveColumns(table, parsed) {
|
|
1737
|
+
let first = 0;
|
|
1738
|
+
let last = table.columns.length - 1;
|
|
1739
|
+
if (void 0 !== parsed.firstColumn && void 0 !== parsed.lastColumn) {
|
|
1740
|
+
first = table.columns.findIndex((column)=>column.toLocaleLowerCase() === parsed.firstColumn.toLocaleLowerCase());
|
|
1741
|
+
last = table.columns.findIndex((column)=>column.toLocaleLowerCase() === parsed.lastColumn.toLocaleLowerCase());
|
|
1742
|
+
if (first < 0) throw new SpreadsheetStructuredReferenceError('missing-column', `Spreadsheet table '${table.name}' has no column '${parsed.firstColumn}'.`);
|
|
1743
|
+
if (last < 0) throw new SpreadsheetStructuredReferenceError('missing-column', `Spreadsheet table '${table.name}' has no column '${parsed.lastColumn}'.`);
|
|
1744
|
+
if (first > last) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured-reference column range '${parsed.firstColumn}:${parsed.lastColumn}' is reversed.`);
|
|
1745
|
+
}
|
|
1746
|
+
if (first < 0 || last < first || table.startColumn + last > table.endColumn) throw invalidReference(table.name);
|
|
1747
|
+
return [
|
|
1748
|
+
table.startColumn + first,
|
|
1749
|
+
table.startColumn + last
|
|
1750
|
+
];
|
|
1751
|
+
}
|
|
1752
|
+
resolveRows(table, rows, currentRow) {
|
|
1753
|
+
const selected = [];
|
|
1754
|
+
if (rows.all) selected.push([
|
|
1755
|
+
table.startRow,
|
|
1756
|
+
table.endRow
|
|
1757
|
+
]);
|
|
1758
|
+
if (rows.headers) {
|
|
1759
|
+
if (!table.headerRow) throw missingRows(table, '#Headers');
|
|
1760
|
+
selected.push([
|
|
1761
|
+
table.startRow,
|
|
1762
|
+
table.startRow
|
|
1763
|
+
]);
|
|
1764
|
+
}
|
|
1765
|
+
if (rows.data) selected.push(this.dataRows(table));
|
|
1766
|
+
if (rows.totals) {
|
|
1767
|
+
if (!table.totalsRow) throw missingRows(table, '#Totals');
|
|
1768
|
+
selected.push([
|
|
1769
|
+
table.endRow,
|
|
1770
|
+
table.endRow
|
|
1771
|
+
]);
|
|
1772
|
+
}
|
|
1773
|
+
if (rows.current) {
|
|
1774
|
+
const [start, end] = this.dataRows(table);
|
|
1775
|
+
if (currentRow < start || currentRow > end) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference #This Row requires the current formula row to be inside table '${table.name}'.`);
|
|
1776
|
+
selected.push([
|
|
1777
|
+
currentRow,
|
|
1778
|
+
currentRow
|
|
1779
|
+
]);
|
|
1780
|
+
}
|
|
1781
|
+
selected.sort((left, right)=>left[0] - right[0]);
|
|
1782
|
+
const merged = [];
|
|
1783
|
+
for (const [start, end] of selected){
|
|
1784
|
+
const previous = merged.at(-1);
|
|
1785
|
+
if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
|
|
1786
|
+
else merged.push([
|
|
1787
|
+
start,
|
|
1788
|
+
end
|
|
1789
|
+
]);
|
|
1790
|
+
}
|
|
1791
|
+
if (!merged.length) throw new SpreadsheetStructuredReferenceError('unsupported', 'Structured reference selects no table rows.');
|
|
1792
|
+
return merged;
|
|
1793
|
+
}
|
|
1794
|
+
dataRows(table) {
|
|
1795
|
+
const start = table.startRow + (table.headerRow ? 1 : 0);
|
|
1796
|
+
const end = table.endRow - (table.totalsRow ? 1 : 0);
|
|
1797
|
+
if (start > end) throw new SpreadsheetStructuredReferenceError('unsupported', `Spreadsheet table '${table.name}' has no data rows.`);
|
|
1798
|
+
return [
|
|
1799
|
+
start,
|
|
1800
|
+
end
|
|
1801
|
+
];
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function expandSpreadsheetStructuredReferences(formula, catalog, currentSheet, currentRow, currentColumn) {
|
|
1805
|
+
const hasEquals = formula.startsWith('=');
|
|
1806
|
+
const source = hasEquals ? formula.slice(1) : formula;
|
|
1807
|
+
let output = '';
|
|
1808
|
+
let cursor = 0;
|
|
1809
|
+
while(cursor < source.length){
|
|
1810
|
+
const character = source[cursor] ?? '';
|
|
1811
|
+
if ('"' === character) {
|
|
1812
|
+
const end = quotedStringEnd(source, cursor);
|
|
1813
|
+
output += source.slice(cursor, end);
|
|
1814
|
+
cursor = end;
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const token = scanStructuredReference(source, cursor);
|
|
1818
|
+
if (!token) {
|
|
1819
|
+
output += character;
|
|
1820
|
+
cursor += 1;
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
const areas = catalog.resolve(token.qualifier, token.reference, currentSheet, currentColumn, currentRow);
|
|
1824
|
+
if (areas.length > 1) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference '${token.reference}' resolves to disjoint row areas; the JavaScript fallback requires a contiguous range.`);
|
|
1825
|
+
const ranges = areas.map((area)=>{
|
|
1826
|
+
const prefix = area.sheetId === currentSheet.id ? '' : `${quoteSheetName(area.sheetName)}!`;
|
|
1827
|
+
const start = cellAddress(area.startRow, area.startColumn);
|
|
1828
|
+
const end = cellAddress(area.endRow, area.endColumn);
|
|
1829
|
+
return `${prefix}${start}${start === end ? '' : `:${end}`}`;
|
|
1830
|
+
});
|
|
1831
|
+
output += 1 === ranges.length ? ranges[0] : `(${ranges.join(',')})`;
|
|
1832
|
+
cursor = token.end;
|
|
1833
|
+
}
|
|
1834
|
+
return hasEquals ? `=${output}` : output;
|
|
1835
|
+
}
|
|
1836
|
+
function scanStructuredReference(source, start) {
|
|
1837
|
+
const qualified = scanQualifier(source, start);
|
|
1838
|
+
let cursor = qualified?.end ?? start;
|
|
1839
|
+
const qualifier = qualified?.name;
|
|
1840
|
+
if ('[' === source[cursor]) {
|
|
1841
|
+
const end = matchingBracket(source, cursor);
|
|
1842
|
+
if (null === end) return null;
|
|
1843
|
+
const content = source.slice(cursor + 1, end - 1);
|
|
1844
|
+
if (!content.startsWith('@') && !content.startsWith('#') && !content.startsWith('[')) return null;
|
|
1845
|
+
return {
|
|
1846
|
+
end,
|
|
1847
|
+
qualifier,
|
|
1848
|
+
reference: source.slice(cursor, end)
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
const nameStart = cursor;
|
|
1852
|
+
if (!isNameStart(source[cursor] ?? '')) return null;
|
|
1853
|
+
cursor += 1;
|
|
1854
|
+
while(cursor < source.length && isNameContinue(source[cursor]))cursor += 1;
|
|
1855
|
+
if ('[' !== source[cursor]) return null;
|
|
1856
|
+
const end = matchingBracket(source, cursor);
|
|
1857
|
+
if (null === end) throw invalidReference(source.slice(nameStart, source.length));
|
|
1858
|
+
return {
|
|
1859
|
+
end,
|
|
1860
|
+
qualifier,
|
|
1861
|
+
reference: source.slice(nameStart, end)
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function scanQualifier(source, start) {
|
|
1865
|
+
if ("'" === source[start]) {
|
|
1866
|
+
let cursor = start + 1;
|
|
1867
|
+
let decoded = '';
|
|
1868
|
+
while(cursor < source.length){
|
|
1869
|
+
const character = source[cursor];
|
|
1870
|
+
if ("'" === character) {
|
|
1871
|
+
if ("'" === source[cursor + 1]) {
|
|
1872
|
+
decoded += "'";
|
|
1873
|
+
cursor += 2;
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
if ('!' === source[cursor + 1]) return {
|
|
1877
|
+
name: decoded,
|
|
1878
|
+
end: cursor + 2
|
|
1879
|
+
};
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
decoded += character;
|
|
1883
|
+
cursor += 1;
|
|
1884
|
+
}
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
let cursor = start;
|
|
1888
|
+
while(cursor < source.length && isQualifierCharacter(source[cursor]))cursor += 1;
|
|
1889
|
+
if ('!' !== source[cursor] || cursor === start) return null;
|
|
1890
|
+
return {
|
|
1891
|
+
name: source.slice(start, cursor),
|
|
1892
|
+
end: cursor + 1
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function matchingBracket(source, start) {
|
|
1896
|
+
let depth = 0;
|
|
1897
|
+
for(let cursor = start; cursor < source.length; cursor += 1){
|
|
1898
|
+
const character = source[cursor];
|
|
1899
|
+
if ("'" === character) {
|
|
1900
|
+
cursor += "'" === source[cursor + 1] ? 1 : 0;
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
if ('[' === character) depth += 1;
|
|
1904
|
+
else if (']' === character) {
|
|
1905
|
+
depth -= 1;
|
|
1906
|
+
if (0 === depth) return cursor + 1;
|
|
1907
|
+
if (depth < 0) break;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
function outerGroup(value) {
|
|
1913
|
+
if (!value.startsWith('[')) return null;
|
|
1914
|
+
const end = matchingBracket(value, 0);
|
|
1915
|
+
return end === value.length ? value.slice(1, -1) : null;
|
|
1916
|
+
}
|
|
1917
|
+
function bracketAtom(value) {
|
|
1918
|
+
if (!value.startsWith('[')) return null;
|
|
1919
|
+
const end = matchingBracket(value, 0);
|
|
1920
|
+
return null === end ? null : {
|
|
1921
|
+
atom: value.slice(1, end - 1),
|
|
1922
|
+
consumed: end
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function parseCurrentColumn(value, reference) {
|
|
1926
|
+
if (value.startsWith('[')) {
|
|
1927
|
+
const atom = bracketAtom(value);
|
|
1928
|
+
if (!atom || atom.consumed !== value.length) throw invalidReference(reference);
|
|
1929
|
+
const column = decodeAtom(atom.atom);
|
|
1930
|
+
if (!column) throw invalidReference(reference);
|
|
1931
|
+
return column;
|
|
1932
|
+
}
|
|
1933
|
+
return parsePlainColumn(value, reference);
|
|
1934
|
+
}
|
|
1935
|
+
function parseNestedSelection(content, reference, rows) {
|
|
1936
|
+
const atoms = [];
|
|
1937
|
+
const separators = [];
|
|
1938
|
+
let cursor = 0;
|
|
1939
|
+
while(cursor < content.length){
|
|
1940
|
+
const atom = bracketAtom(content.slice(cursor));
|
|
1941
|
+
if (!atom) throw invalidReference(reference);
|
|
1942
|
+
atoms.push(atom.atom);
|
|
1943
|
+
cursor += atom.consumed;
|
|
1944
|
+
if (cursor === content.length) break;
|
|
1945
|
+
const separator = content[cursor];
|
|
1946
|
+
if (',' !== separator && ':' !== separator) throw invalidReference(reference);
|
|
1947
|
+
separators.push(separator);
|
|
1948
|
+
cursor += 1;
|
|
1949
|
+
}
|
|
1950
|
+
const columns = [];
|
|
1951
|
+
atoms.forEach((atom, index)=>{
|
|
1952
|
+
const item = tableItem(atom);
|
|
1953
|
+
if (item) applyTableItem(rows, item, reference);
|
|
1954
|
+
else {
|
|
1955
|
+
const column = decodeAtom(atom);
|
|
1956
|
+
if (!column) throw invalidReference(reference);
|
|
1957
|
+
columns.push({
|
|
1958
|
+
index,
|
|
1959
|
+
name: column
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
if (0 === columns.length) {
|
|
1964
|
+
if (separators.includes(':')) throw invalidReference(reference);
|
|
1965
|
+
return [
|
|
1966
|
+
void 0,
|
|
1967
|
+
void 0
|
|
1968
|
+
];
|
|
1969
|
+
}
|
|
1970
|
+
if (1 === columns.length) {
|
|
1971
|
+
if (separators.includes(':')) throw invalidReference(reference);
|
|
1972
|
+
return [
|
|
1973
|
+
columns[0].name,
|
|
1974
|
+
columns[0].name
|
|
1975
|
+
];
|
|
1976
|
+
}
|
|
1977
|
+
const first = columns[0];
|
|
1978
|
+
const last = columns[1];
|
|
1979
|
+
if (2 === columns.length && last.index === first.index + 1 && ':' === separators[first.index] && separators.every((separator, index)=>index === first.index || ',' === separator)) return [
|
|
1980
|
+
first.name,
|
|
1981
|
+
last.name
|
|
1982
|
+
];
|
|
1983
|
+
throw new SpreadsheetStructuredReferenceError('unsupported', 'Disjoint structured-reference columns are not supported.');
|
|
1984
|
+
}
|
|
1985
|
+
function tableItem(value) {
|
|
1986
|
+
const normalized = value.toLocaleLowerCase();
|
|
1987
|
+
if ('#all' === normalized) return 'all';
|
|
1988
|
+
if ('#headers' === normalized) return 'headers';
|
|
1989
|
+
if ('#data' === normalized) return 'data';
|
|
1990
|
+
if ('#totals' === normalized) return 'totals';
|
|
1991
|
+
if ('#this row' === normalized) return 'current';
|
|
1992
|
+
}
|
|
1993
|
+
function applyTableItem(rows, item, reference) {
|
|
1994
|
+
rows['current' === item ? 'current' : item] = true;
|
|
1995
|
+
if (rows.current && (rows.all || rows.headers || rows.data || rows.totals)) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference '${reference}' cannot combine #This Row with another item.`);
|
|
1996
|
+
}
|
|
1997
|
+
function parsePlainColumn(value, reference) {
|
|
1998
|
+
if (!value || /[[\],:]/u.test(value)) throw invalidReference(reference);
|
|
1999
|
+
const column = decodeAtom(value);
|
|
2000
|
+
if (!column) throw invalidReference(reference);
|
|
2001
|
+
return column;
|
|
2002
|
+
}
|
|
2003
|
+
function decodeAtom(value) {
|
|
2004
|
+
let output = '';
|
|
2005
|
+
for(let cursor = 0; cursor < value.length; cursor += 1){
|
|
2006
|
+
const character = value[cursor];
|
|
2007
|
+
if ("'" === character) {
|
|
2008
|
+
const escaped = value[cursor + 1];
|
|
2009
|
+
if (void 0 === escaped) throw invalidReference(value);
|
|
2010
|
+
output += escaped;
|
|
2011
|
+
cursor += 1;
|
|
2012
|
+
} else output += character;
|
|
2013
|
+
}
|
|
2014
|
+
return output;
|
|
2015
|
+
}
|
|
2016
|
+
function missingRows(table, item) {
|
|
2017
|
+
return new SpreadsheetStructuredReferenceError('unsupported', `Structured reference ${item} requires table '${table.name}' to contain that row.`);
|
|
2018
|
+
}
|
|
2019
|
+
function invalidReference(reference) {
|
|
2020
|
+
return new SpreadsheetStructuredReferenceError('invalid', `Structured reference '${reference}' is not in a supported canonical form.`);
|
|
2021
|
+
}
|
|
2022
|
+
function quotedStringEnd(source, start) {
|
|
2023
|
+
for(let cursor = start + 1; cursor < source.length; cursor += 1)if ('"' === source[cursor]) {
|
|
2024
|
+
if ('"' === source[cursor + 1]) {
|
|
2025
|
+
cursor += 1;
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
return cursor + 1;
|
|
2029
|
+
}
|
|
2030
|
+
return source.length;
|
|
2031
|
+
}
|
|
2032
|
+
function cellAddress(row, column) {
|
|
2033
|
+
let value = column + 1;
|
|
2034
|
+
let label = '';
|
|
2035
|
+
while(value > 0){
|
|
2036
|
+
value -= 1;
|
|
2037
|
+
label = String.fromCharCode(65 + value % 26) + label;
|
|
2038
|
+
value = Math.floor(value / 26);
|
|
2039
|
+
}
|
|
2040
|
+
return `${label}${row + 1}`;
|
|
2041
|
+
}
|
|
2042
|
+
function quoteSheetName(name) {
|
|
2043
|
+
return /^[A-Za-z_][A-Za-z0-9_.]*$/u.test(name) ? name : `'${name.replaceAll("'", "''")}'`;
|
|
2044
|
+
}
|
|
2045
|
+
function isNameStart(value) {
|
|
2046
|
+
return /^[A-Za-z_\\?]$/u.test(value);
|
|
2047
|
+
}
|
|
2048
|
+
function isNameContinue(value) {
|
|
2049
|
+
return /^[A-Za-z0-9_.\\?]$/u.test(value);
|
|
2050
|
+
}
|
|
2051
|
+
function isQualifierCharacter(value) {
|
|
2052
|
+
return /^[A-Za-z0-9_.\\?[\]:$]$/u.test(value);
|
|
2053
|
+
}
|
|
1569
2054
|
async function calculateSpreadsheetInJavaScript(request) {
|
|
1570
2055
|
validateSpreadsheetCalculationRequest(request);
|
|
1571
2056
|
const formulaParser = await import("@fortune-sheet/formula-parser");
|
|
@@ -1581,9 +2066,11 @@ class JavaScriptSpreadsheetEvaluator {
|
|
|
1581
2066
|
stack = [];
|
|
1582
2067
|
calculationOrder = [];
|
|
1583
2068
|
issues = [];
|
|
2069
|
+
tableCatalog;
|
|
1584
2070
|
constructor(request, formulaParser){
|
|
1585
2071
|
this.request = request;
|
|
1586
2072
|
this.formulaParser = formulaParser;
|
|
2073
|
+
this.tableCatalog = new SpreadsheetStructuredReferenceCatalog(request.sheets);
|
|
1587
2074
|
for (const sheet of request.sheets){
|
|
1588
2075
|
this.sheetsByName.set(sheet.name.toLowerCase(), sheet);
|
|
1589
2076
|
for (const cell of sheet.cells)this.cells.set(cellKey(sheet.id, cell.row, cell.column), {
|
|
@@ -1654,6 +2141,13 @@ class JavaScriptSpreadsheetEvaluator {
|
|
|
1654
2141
|
}
|
|
1655
2142
|
evaluateFormula(coordinate, indexed) {
|
|
1656
2143
|
const formula = indexed.cell.formula ?? '';
|
|
2144
|
+
let expandedFormula;
|
|
2145
|
+
try {
|
|
2146
|
+
expandedFormula = expandSpreadsheetStructuredReferences(formula, this.tableCatalog, indexed.sheet, coordinate.row, coordinate.column);
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
const message = error instanceof SpreadsheetStructuredReferenceError ? error.message : 'Structured reference expansion failed.';
|
|
2149
|
+
return failedEvaluation(indexed.cell.value, calculationIssue(coordinate, 'office.kernel.spreadsheet.formula_unsupported', message));
|
|
2150
|
+
}
|
|
1657
2151
|
const parser = new this.formulaParser.Parser();
|
|
1658
2152
|
let unresolvedDependency = false;
|
|
1659
2153
|
let unsupportedReference = false;
|
|
@@ -1687,7 +2181,7 @@ class JavaScriptSpreadsheetEvaluator {
|
|
|
1687
2181
|
return true;
|
|
1688
2182
|
}));
|
|
1689
2183
|
});
|
|
1690
|
-
const parsed = parser.parse(normalizeFormulaForFortuneParser(
|
|
2184
|
+
const parsed = parser.parse(normalizeFormulaForFortuneParser(expandedFormula), {
|
|
1691
2185
|
sheetId: indexed.sheet.id
|
|
1692
2186
|
});
|
|
1693
2187
|
if (unsupportedFunction) return failedEvaluation(indexed.cell.value, calculationIssue(coordinate, 'office.kernel.spreadsheet.formula_unsupported', `Formula function '${unsupportedFunction}' is not supported.`));
|
|
@@ -1711,7 +2205,8 @@ class JavaScriptSpreadsheetEvaluator {
|
|
|
1711
2205
|
}
|
|
1712
2206
|
rangeValues(currentSheet, start, end, onUnresolvedDependency, onUnsupportedReference, reserveRangeCells) {
|
|
1713
2207
|
const startCoordinate = this.resolveCoordinate(currentSheet, start);
|
|
1714
|
-
const
|
|
2208
|
+
const rangeSheet = start.sheetName ? this.sheetsByName.get(start.sheetName.toLowerCase()) : currentSheet;
|
|
2209
|
+
const endCoordinate = rangeSheet ? this.resolveCoordinate(rangeSheet, end) : null;
|
|
1715
2210
|
if (!startCoordinate || !endCoordinate || startCoordinate.sheetId !== endCoordinate.sheetId) {
|
|
1716
2211
|
onUnsupportedReference();
|
|
1717
2212
|
return [];
|