@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
|
@@ -13916,6 +13916,7 @@
|
|
|
13916
13916
|
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.');
|
|
13917
13917
|
sheetIds.add(sheet.id);
|
|
13918
13918
|
sheetNames.add(normalizedName);
|
|
13919
|
+
validateSpreadsheetTables(sheet, request.sheets);
|
|
13919
13920
|
const coordinates = new Set();
|
|
13920
13921
|
for (const cell of sheet.cells){
|
|
13921
13922
|
const key = `${cell.row}:${cell.column}`;
|
|
@@ -13927,6 +13928,68 @@
|
|
|
13927
13928
|
}
|
|
13928
13929
|
for (const target of request.targets ?? [])if (!sheetIds.has(target.sheetId) || !boundedSpreadsheetIndex(target.row, 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.');
|
|
13929
13930
|
}
|
|
13931
|
+
function validateSpreadsheetTables(sheet, sheets) {
|
|
13932
|
+
const tables = sheet.tables ?? [];
|
|
13933
|
+
const tableCount = sheets.reduce((count, candidate)=>count + (candidate.tables?.length ?? 0), 0);
|
|
13934
|
+
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.');
|
|
13935
|
+
const aliases = new Map();
|
|
13936
|
+
for (const candidate of sheets)for (const [tableIndex, table] of (candidate.tables ?? []).entries()){
|
|
13937
|
+
const identity = `${candidate.id}\u0000${tableIndex}`;
|
|
13938
|
+
for (const alias of [
|
|
13939
|
+
table.name,
|
|
13940
|
+
table.displayName
|
|
13941
|
+
]){
|
|
13942
|
+
if (!alias) continue;
|
|
13943
|
+
const normalized = alias.toLocaleLowerCase();
|
|
13944
|
+
const existing = aliases.get(normalized);
|
|
13945
|
+
if (existing && existing !== identity) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table name '${alias}' is ambiguous.`);
|
|
13946
|
+
aliases.set(normalized, identity);
|
|
13947
|
+
}
|
|
13948
|
+
}
|
|
13949
|
+
const ranges = [];
|
|
13950
|
+
for (const table of tables){
|
|
13951
|
+
for (const [kind, value] of [
|
|
13952
|
+
[
|
|
13953
|
+
'startRow',
|
|
13954
|
+
table.startRow
|
|
13955
|
+
],
|
|
13956
|
+
[
|
|
13957
|
+
'endRow',
|
|
13958
|
+
table.endRow
|
|
13959
|
+
],
|
|
13960
|
+
[
|
|
13961
|
+
'startColumn',
|
|
13962
|
+
table.startColumn
|
|
13963
|
+
],
|
|
13964
|
+
[
|
|
13965
|
+
'endColumn',
|
|
13966
|
+
table.endColumn
|
|
13967
|
+
]
|
|
13968
|
+
])if (!boundedSpreadsheetIndex(value, kind.endsWith('Row') ? 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}.`);
|
|
13969
|
+
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.`);
|
|
13970
|
+
validateTableName(table.name, 'name');
|
|
13971
|
+
if (void 0 !== table.displayName) validateTableName(table.displayName, 'displayName');
|
|
13972
|
+
const columnNames = new Set();
|
|
13973
|
+
for (const column of table.columns){
|
|
13974
|
+
validateTableName(column, 'column');
|
|
13975
|
+
const normalized = column.toLocaleLowerCase();
|
|
13976
|
+
if (columnNames.has(normalized)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' contains duplicate column names.`);
|
|
13977
|
+
columnNames.add(normalized);
|
|
13978
|
+
}
|
|
13979
|
+
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.`);
|
|
13980
|
+
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.`);
|
|
13981
|
+
ranges.push({
|
|
13982
|
+
startRow: table.startRow,
|
|
13983
|
+
endRow: table.endRow,
|
|
13984
|
+
startColumn: table.startColumn,
|
|
13985
|
+
endColumn: table.endColumn,
|
|
13986
|
+
name: table.name
|
|
13987
|
+
});
|
|
13988
|
+
}
|
|
13989
|
+
}
|
|
13990
|
+
function validateTableName(value, kind) {
|
|
13991
|
+
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.`);
|
|
13992
|
+
}
|
|
13930
13993
|
function boundedSpreadsheetIndex(value, exclusiveMaximum) {
|
|
13931
13994
|
return Number.isSafeInteger(value) && value >= 0 && value < exclusiveMaximum;
|
|
13932
13995
|
}
|
|
@@ -14020,6 +14083,428 @@
|
|
|
14020
14083
|
const withSuffix = '#DIV/0' === normalized ? '#DIV/0!' : normalized;
|
|
14021
14084
|
return isOfficeKernelSpreadsheetError(withSuffix);
|
|
14022
14085
|
}
|
|
14086
|
+
class SpreadsheetStructuredReferenceError extends Error {
|
|
14087
|
+
kind;
|
|
14088
|
+
constructor(kind, message){
|
|
14089
|
+
super(message);
|
|
14090
|
+
this.name = 'SpreadsheetStructuredReferenceError';
|
|
14091
|
+
this.kind = kind;
|
|
14092
|
+
}
|
|
14093
|
+
}
|
|
14094
|
+
function parseSpreadsheetStructuredReference(reference) {
|
|
14095
|
+
const open = reference.indexOf('[');
|
|
14096
|
+
if (open < 0) throw invalidReference(reference);
|
|
14097
|
+
const tableName = reference.slice(0, open) || void 0;
|
|
14098
|
+
const content = outerGroup(reference.slice(open));
|
|
14099
|
+
if (null === content) throw invalidReference(reference);
|
|
14100
|
+
const rows = {
|
|
14101
|
+
all: false,
|
|
14102
|
+
headers: false,
|
|
14103
|
+
data: false,
|
|
14104
|
+
totals: false,
|
|
14105
|
+
current: false
|
|
14106
|
+
};
|
|
14107
|
+
let firstColumn;
|
|
14108
|
+
let lastColumn;
|
|
14109
|
+
if (content.startsWith('@')) {
|
|
14110
|
+
rows.current = true;
|
|
14111
|
+
const column = parseCurrentColumn(content.slice(1), reference);
|
|
14112
|
+
firstColumn = column;
|
|
14113
|
+
lastColumn = column;
|
|
14114
|
+
} else if (content.startsWith('[')) [firstColumn, lastColumn] = parseNestedSelection(content, reference, rows);
|
|
14115
|
+
else {
|
|
14116
|
+
const item = tableItem(content);
|
|
14117
|
+
if (item) applyTableItem(rows, item, reference);
|
|
14118
|
+
else {
|
|
14119
|
+
const column = parsePlainColumn(content, reference);
|
|
14120
|
+
firstColumn = column;
|
|
14121
|
+
lastColumn = column;
|
|
14122
|
+
}
|
|
14123
|
+
}
|
|
14124
|
+
if (!rows.all && !rows.headers && !rows.data && !rows.totals && !rows.current) rows.data = true;
|
|
14125
|
+
return {
|
|
14126
|
+
tableName,
|
|
14127
|
+
firstColumn,
|
|
14128
|
+
lastColumn,
|
|
14129
|
+
rows
|
|
14130
|
+
};
|
|
14131
|
+
}
|
|
14132
|
+
class SpreadsheetStructuredReferenceCatalog {
|
|
14133
|
+
sheets;
|
|
14134
|
+
definitions = [];
|
|
14135
|
+
byName = new Map();
|
|
14136
|
+
bySheet = new Map();
|
|
14137
|
+
constructor(sheets){
|
|
14138
|
+
this.sheets = sheets;
|
|
14139
|
+
for (const sheet of sheets){
|
|
14140
|
+
const indexes = [];
|
|
14141
|
+
for (const table of sheet.tables ?? []){
|
|
14142
|
+
const definition = {
|
|
14143
|
+
...table,
|
|
14144
|
+
sheetId: sheet.id,
|
|
14145
|
+
sheetName: sheet.name
|
|
14146
|
+
};
|
|
14147
|
+
const index = this.definitions.length;
|
|
14148
|
+
this.definitions.push(definition);
|
|
14149
|
+
indexes.push(index);
|
|
14150
|
+
for (const alias of [
|
|
14151
|
+
table.name,
|
|
14152
|
+
table.displayName
|
|
14153
|
+
]){
|
|
14154
|
+
if (!alias) continue;
|
|
14155
|
+
const key = alias.toLocaleLowerCase();
|
|
14156
|
+
if (!this.byName.has(key)) this.byName.set(key, index);
|
|
14157
|
+
}
|
|
14158
|
+
}
|
|
14159
|
+
this.bySheet.set(sheet.id, indexes);
|
|
14160
|
+
}
|
|
14161
|
+
}
|
|
14162
|
+
resolve(qualifier, reference, currentSheet, currentColumn, currentRow) {
|
|
14163
|
+
const parsed = parseSpreadsheetStructuredReference(reference);
|
|
14164
|
+
const table = this.resolveTable(parsed, reference, currentSheet, currentColumn, currentRow);
|
|
14165
|
+
if (qualifier && table.sheetName.toLocaleLowerCase() !== qualifier.toLocaleLowerCase()) throw new SpreadsheetStructuredReferenceError('missing-table', `Spreadsheet table '${table.name}' is not on worksheet '${qualifier}'.`);
|
|
14166
|
+
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}'.`);
|
|
14167
|
+
const [startColumn, endColumn] = this.resolveColumns(table, parsed);
|
|
14168
|
+
return this.resolveRows(table, parsed.rows, currentRow).map(([startRow, endRow])=>({
|
|
14169
|
+
sheetId: table.sheetId,
|
|
14170
|
+
sheetName: table.sheetName,
|
|
14171
|
+
startRow,
|
|
14172
|
+
endRow,
|
|
14173
|
+
startColumn,
|
|
14174
|
+
endColumn
|
|
14175
|
+
}));
|
|
14176
|
+
}
|
|
14177
|
+
resolveTable(parsed, reference, currentSheet, currentColumn, currentRow) {
|
|
14178
|
+
if (parsed.tableName) {
|
|
14179
|
+
const index = this.byName.get(parsed.tableName.toLocaleLowerCase());
|
|
14180
|
+
if (void 0 === index) throw new SpreadsheetStructuredReferenceError('missing-table', `Spreadsheet table '${parsed.tableName}' does not exist.`);
|
|
14181
|
+
const table = this.definitions[index];
|
|
14182
|
+
if (!table) throw invalidReference(reference);
|
|
14183
|
+
return table;
|
|
14184
|
+
}
|
|
14185
|
+
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);
|
|
14186
|
+
if (!matching.length) throw new SpreadsheetStructuredReferenceError('missing-table', `Table-local structured reference '${reference}' requires the current formula cell to be inside a Spreadsheet table.`);
|
|
14187
|
+
if (matching.length > 1) throw new SpreadsheetStructuredReferenceError('unsupported', `Table-local structured reference '${reference}' is ambiguous at the current formula cell.`);
|
|
14188
|
+
return matching[0];
|
|
14189
|
+
}
|
|
14190
|
+
resolveColumns(table, parsed) {
|
|
14191
|
+
let first = 0;
|
|
14192
|
+
let last = table.columns.length - 1;
|
|
14193
|
+
if (void 0 !== parsed.firstColumn && void 0 !== parsed.lastColumn) {
|
|
14194
|
+
first = table.columns.findIndex((column)=>column.toLocaleLowerCase() === parsed.firstColumn.toLocaleLowerCase());
|
|
14195
|
+
last = table.columns.findIndex((column)=>column.toLocaleLowerCase() === parsed.lastColumn.toLocaleLowerCase());
|
|
14196
|
+
if (first < 0) throw new SpreadsheetStructuredReferenceError('missing-column', `Spreadsheet table '${table.name}' has no column '${parsed.firstColumn}'.`);
|
|
14197
|
+
if (last < 0) throw new SpreadsheetStructuredReferenceError('missing-column', `Spreadsheet table '${table.name}' has no column '${parsed.lastColumn}'.`);
|
|
14198
|
+
if (first > last) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured-reference column range '${parsed.firstColumn}:${parsed.lastColumn}' is reversed.`);
|
|
14199
|
+
}
|
|
14200
|
+
if (first < 0 || last < first || table.startColumn + last > table.endColumn) throw invalidReference(table.name);
|
|
14201
|
+
return [
|
|
14202
|
+
table.startColumn + first,
|
|
14203
|
+
table.startColumn + last
|
|
14204
|
+
];
|
|
14205
|
+
}
|
|
14206
|
+
resolveRows(table, rows, currentRow) {
|
|
14207
|
+
const selected = [];
|
|
14208
|
+
if (rows.all) selected.push([
|
|
14209
|
+
table.startRow,
|
|
14210
|
+
table.endRow
|
|
14211
|
+
]);
|
|
14212
|
+
if (rows.headers) {
|
|
14213
|
+
if (!table.headerRow) throw missingRows(table, '#Headers');
|
|
14214
|
+
selected.push([
|
|
14215
|
+
table.startRow,
|
|
14216
|
+
table.startRow
|
|
14217
|
+
]);
|
|
14218
|
+
}
|
|
14219
|
+
if (rows.data) selected.push(this.dataRows(table));
|
|
14220
|
+
if (rows.totals) {
|
|
14221
|
+
if (!table.totalsRow) throw missingRows(table, '#Totals');
|
|
14222
|
+
selected.push([
|
|
14223
|
+
table.endRow,
|
|
14224
|
+
table.endRow
|
|
14225
|
+
]);
|
|
14226
|
+
}
|
|
14227
|
+
if (rows.current) {
|
|
14228
|
+
const [start, end] = this.dataRows(table);
|
|
14229
|
+
if (currentRow < start || currentRow > end) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference #This Row requires the current formula row to be inside table '${table.name}'.`);
|
|
14230
|
+
selected.push([
|
|
14231
|
+
currentRow,
|
|
14232
|
+
currentRow
|
|
14233
|
+
]);
|
|
14234
|
+
}
|
|
14235
|
+
selected.sort((left, right)=>left[0] - right[0]);
|
|
14236
|
+
const merged = [];
|
|
14237
|
+
for (const [start, end] of selected){
|
|
14238
|
+
const previous = merged.at(-1);
|
|
14239
|
+
if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
|
|
14240
|
+
else merged.push([
|
|
14241
|
+
start,
|
|
14242
|
+
end
|
|
14243
|
+
]);
|
|
14244
|
+
}
|
|
14245
|
+
if (!merged.length) throw new SpreadsheetStructuredReferenceError('unsupported', 'Structured reference selects no table rows.');
|
|
14246
|
+
return merged;
|
|
14247
|
+
}
|
|
14248
|
+
dataRows(table) {
|
|
14249
|
+
const start = table.startRow + (table.headerRow ? 1 : 0);
|
|
14250
|
+
const end = table.endRow - (table.totalsRow ? 1 : 0);
|
|
14251
|
+
if (start > end) throw new SpreadsheetStructuredReferenceError('unsupported', `Spreadsheet table '${table.name}' has no data rows.`);
|
|
14252
|
+
return [
|
|
14253
|
+
start,
|
|
14254
|
+
end
|
|
14255
|
+
];
|
|
14256
|
+
}
|
|
14257
|
+
}
|
|
14258
|
+
function expandSpreadsheetStructuredReferences(formula, catalog, currentSheet, currentRow, currentColumn) {
|
|
14259
|
+
const hasEquals = formula.startsWith('=');
|
|
14260
|
+
const source = hasEquals ? formula.slice(1) : formula;
|
|
14261
|
+
let output = '';
|
|
14262
|
+
let cursor = 0;
|
|
14263
|
+
while(cursor < source.length){
|
|
14264
|
+
const character = source[cursor] ?? '';
|
|
14265
|
+
if ('"' === character) {
|
|
14266
|
+
const end = quotedStringEnd(source, cursor);
|
|
14267
|
+
output += source.slice(cursor, end);
|
|
14268
|
+
cursor = end;
|
|
14269
|
+
continue;
|
|
14270
|
+
}
|
|
14271
|
+
const token = scanStructuredReference(source, cursor);
|
|
14272
|
+
if (!token) {
|
|
14273
|
+
output += character;
|
|
14274
|
+
cursor += 1;
|
|
14275
|
+
continue;
|
|
14276
|
+
}
|
|
14277
|
+
const areas = catalog.resolve(token.qualifier, token.reference, currentSheet, currentColumn, currentRow);
|
|
14278
|
+
if (areas.length > 1) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference '${token.reference}' resolves to disjoint row areas; the JavaScript fallback requires a contiguous range.`);
|
|
14279
|
+
const ranges = areas.map((area)=>{
|
|
14280
|
+
const prefix = area.sheetId === currentSheet.id ? '' : `${quoteSheetName(area.sheetName)}!`;
|
|
14281
|
+
const start = cellAddress(area.startRow, area.startColumn);
|
|
14282
|
+
const end = cellAddress(area.endRow, area.endColumn);
|
|
14283
|
+
return `${prefix}${start}${start === end ? '' : `:${end}`}`;
|
|
14284
|
+
});
|
|
14285
|
+
output += 1 === ranges.length ? ranges[0] : `(${ranges.join(',')})`;
|
|
14286
|
+
cursor = token.end;
|
|
14287
|
+
}
|
|
14288
|
+
return hasEquals ? `=${output}` : output;
|
|
14289
|
+
}
|
|
14290
|
+
function scanStructuredReference(source, start) {
|
|
14291
|
+
const qualified = scanQualifier(source, start);
|
|
14292
|
+
let cursor = qualified?.end ?? start;
|
|
14293
|
+
const qualifier = qualified?.name;
|
|
14294
|
+
if ('[' === source[cursor]) {
|
|
14295
|
+
const end = matchingBracket(source, cursor);
|
|
14296
|
+
if (null === end) return null;
|
|
14297
|
+
const content = source.slice(cursor + 1, end - 1);
|
|
14298
|
+
if (!content.startsWith('@') && !content.startsWith('#') && !content.startsWith('[')) return null;
|
|
14299
|
+
return {
|
|
14300
|
+
end,
|
|
14301
|
+
qualifier,
|
|
14302
|
+
reference: source.slice(cursor, end)
|
|
14303
|
+
};
|
|
14304
|
+
}
|
|
14305
|
+
const nameStart = cursor;
|
|
14306
|
+
if (!isNameStart(source[cursor] ?? '')) return null;
|
|
14307
|
+
cursor += 1;
|
|
14308
|
+
while(cursor < source.length && isNameContinue(source[cursor]))cursor += 1;
|
|
14309
|
+
if ('[' !== source[cursor]) return null;
|
|
14310
|
+
const end = matchingBracket(source, cursor);
|
|
14311
|
+
if (null === end) throw invalidReference(source.slice(nameStart, source.length));
|
|
14312
|
+
return {
|
|
14313
|
+
end,
|
|
14314
|
+
qualifier,
|
|
14315
|
+
reference: source.slice(nameStart, end)
|
|
14316
|
+
};
|
|
14317
|
+
}
|
|
14318
|
+
function scanQualifier(source, start) {
|
|
14319
|
+
if ("'" === source[start]) {
|
|
14320
|
+
let cursor = start + 1;
|
|
14321
|
+
let decoded = '';
|
|
14322
|
+
while(cursor < source.length){
|
|
14323
|
+
const character = source[cursor];
|
|
14324
|
+
if ("'" === character) {
|
|
14325
|
+
if ("'" === source[cursor + 1]) {
|
|
14326
|
+
decoded += "'";
|
|
14327
|
+
cursor += 2;
|
|
14328
|
+
continue;
|
|
14329
|
+
}
|
|
14330
|
+
if ('!' === source[cursor + 1]) return {
|
|
14331
|
+
name: decoded,
|
|
14332
|
+
end: cursor + 2
|
|
14333
|
+
};
|
|
14334
|
+
break;
|
|
14335
|
+
}
|
|
14336
|
+
decoded += character;
|
|
14337
|
+
cursor += 1;
|
|
14338
|
+
}
|
|
14339
|
+
return null;
|
|
14340
|
+
}
|
|
14341
|
+
let cursor = start;
|
|
14342
|
+
while(cursor < source.length && isQualifierCharacter(source[cursor]))cursor += 1;
|
|
14343
|
+
if ('!' !== source[cursor] || cursor === start) return null;
|
|
14344
|
+
return {
|
|
14345
|
+
name: source.slice(start, cursor),
|
|
14346
|
+
end: cursor + 1
|
|
14347
|
+
};
|
|
14348
|
+
}
|
|
14349
|
+
function matchingBracket(source, start) {
|
|
14350
|
+
let depth = 0;
|
|
14351
|
+
for(let cursor = start; cursor < source.length; cursor += 1){
|
|
14352
|
+
const character = source[cursor];
|
|
14353
|
+
if ("'" === character) {
|
|
14354
|
+
cursor += "'" === source[cursor + 1] ? 1 : 0;
|
|
14355
|
+
continue;
|
|
14356
|
+
}
|
|
14357
|
+
if ('[' === character) depth += 1;
|
|
14358
|
+
else if (']' === character) {
|
|
14359
|
+
depth -= 1;
|
|
14360
|
+
if (0 === depth) return cursor + 1;
|
|
14361
|
+
if (depth < 0) break;
|
|
14362
|
+
}
|
|
14363
|
+
}
|
|
14364
|
+
return null;
|
|
14365
|
+
}
|
|
14366
|
+
function outerGroup(value) {
|
|
14367
|
+
if (!value.startsWith('[')) return null;
|
|
14368
|
+
const end = matchingBracket(value, 0);
|
|
14369
|
+
return end === value.length ? value.slice(1, -1) : null;
|
|
14370
|
+
}
|
|
14371
|
+
function bracketAtom(value) {
|
|
14372
|
+
if (!value.startsWith('[')) return null;
|
|
14373
|
+
const end = matchingBracket(value, 0);
|
|
14374
|
+
return null === end ? null : {
|
|
14375
|
+
atom: value.slice(1, end - 1),
|
|
14376
|
+
consumed: end
|
|
14377
|
+
};
|
|
14378
|
+
}
|
|
14379
|
+
function parseCurrentColumn(value, reference) {
|
|
14380
|
+
if (value.startsWith('[')) {
|
|
14381
|
+
const atom = bracketAtom(value);
|
|
14382
|
+
if (!atom || atom.consumed !== value.length) throw invalidReference(reference);
|
|
14383
|
+
const column = decodeAtom(atom.atom);
|
|
14384
|
+
if (!column) throw invalidReference(reference);
|
|
14385
|
+
return column;
|
|
14386
|
+
}
|
|
14387
|
+
return parsePlainColumn(value, reference);
|
|
14388
|
+
}
|
|
14389
|
+
function parseNestedSelection(content, reference, rows) {
|
|
14390
|
+
const atoms = [];
|
|
14391
|
+
const separators = [];
|
|
14392
|
+
let cursor = 0;
|
|
14393
|
+
while(cursor < content.length){
|
|
14394
|
+
const atom = bracketAtom(content.slice(cursor));
|
|
14395
|
+
if (!atom) throw invalidReference(reference);
|
|
14396
|
+
atoms.push(atom.atom);
|
|
14397
|
+
cursor += atom.consumed;
|
|
14398
|
+
if (cursor === content.length) break;
|
|
14399
|
+
const separator = content[cursor];
|
|
14400
|
+
if (',' !== separator && ':' !== separator) throw invalidReference(reference);
|
|
14401
|
+
separators.push(separator);
|
|
14402
|
+
cursor += 1;
|
|
14403
|
+
}
|
|
14404
|
+
const columns = [];
|
|
14405
|
+
atoms.forEach((atom, index)=>{
|
|
14406
|
+
const item = tableItem(atom);
|
|
14407
|
+
if (item) applyTableItem(rows, item, reference);
|
|
14408
|
+
else {
|
|
14409
|
+
const column = decodeAtom(atom);
|
|
14410
|
+
if (!column) throw invalidReference(reference);
|
|
14411
|
+
columns.push({
|
|
14412
|
+
index,
|
|
14413
|
+
name: column
|
|
14414
|
+
});
|
|
14415
|
+
}
|
|
14416
|
+
});
|
|
14417
|
+
if (0 === columns.length) {
|
|
14418
|
+
if (separators.includes(':')) throw invalidReference(reference);
|
|
14419
|
+
return [
|
|
14420
|
+
void 0,
|
|
14421
|
+
void 0
|
|
14422
|
+
];
|
|
14423
|
+
}
|
|
14424
|
+
if (1 === columns.length) {
|
|
14425
|
+
if (separators.includes(':')) throw invalidReference(reference);
|
|
14426
|
+
return [
|
|
14427
|
+
columns[0].name,
|
|
14428
|
+
columns[0].name
|
|
14429
|
+
];
|
|
14430
|
+
}
|
|
14431
|
+
const first = columns[0];
|
|
14432
|
+
const last = columns[1];
|
|
14433
|
+
if (2 === columns.length && last.index === first.index + 1 && ':' === separators[first.index] && separators.every((separator, index)=>index === first.index || ',' === separator)) return [
|
|
14434
|
+
first.name,
|
|
14435
|
+
last.name
|
|
14436
|
+
];
|
|
14437
|
+
throw new SpreadsheetStructuredReferenceError('unsupported', 'Disjoint structured-reference columns are not supported.');
|
|
14438
|
+
}
|
|
14439
|
+
function tableItem(value) {
|
|
14440
|
+
const normalized = value.toLocaleLowerCase();
|
|
14441
|
+
if ('#all' === normalized) return 'all';
|
|
14442
|
+
if ('#headers' === normalized) return 'headers';
|
|
14443
|
+
if ('#data' === normalized) return 'data';
|
|
14444
|
+
if ('#totals' === normalized) return 'totals';
|
|
14445
|
+
if ('#this row' === normalized) return 'current';
|
|
14446
|
+
}
|
|
14447
|
+
function applyTableItem(rows, item, reference) {
|
|
14448
|
+
rows['current' === item ? 'current' : item] = true;
|
|
14449
|
+
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.`);
|
|
14450
|
+
}
|
|
14451
|
+
function parsePlainColumn(value, reference) {
|
|
14452
|
+
if (!value || /[[\],:]/u.test(value)) throw invalidReference(reference);
|
|
14453
|
+
const column = decodeAtom(value);
|
|
14454
|
+
if (!column) throw invalidReference(reference);
|
|
14455
|
+
return column;
|
|
14456
|
+
}
|
|
14457
|
+
function decodeAtom(value) {
|
|
14458
|
+
let output = '';
|
|
14459
|
+
for(let cursor = 0; cursor < value.length; cursor += 1){
|
|
14460
|
+
const character = value[cursor];
|
|
14461
|
+
if ("'" === character) {
|
|
14462
|
+
const escaped = value[cursor + 1];
|
|
14463
|
+
if (void 0 === escaped) throw invalidReference(value);
|
|
14464
|
+
output += escaped;
|
|
14465
|
+
cursor += 1;
|
|
14466
|
+
} else output += character;
|
|
14467
|
+
}
|
|
14468
|
+
return output;
|
|
14469
|
+
}
|
|
14470
|
+
function missingRows(table, item) {
|
|
14471
|
+
return new SpreadsheetStructuredReferenceError('unsupported', `Structured reference ${item} requires table '${table.name}' to contain that row.`);
|
|
14472
|
+
}
|
|
14473
|
+
function invalidReference(reference) {
|
|
14474
|
+
return new SpreadsheetStructuredReferenceError('invalid', `Structured reference '${reference}' is not in a supported canonical form.`);
|
|
14475
|
+
}
|
|
14476
|
+
function quotedStringEnd(source, start) {
|
|
14477
|
+
for(let cursor = start + 1; cursor < source.length; cursor += 1)if ('"' === source[cursor]) {
|
|
14478
|
+
if ('"' === source[cursor + 1]) {
|
|
14479
|
+
cursor += 1;
|
|
14480
|
+
continue;
|
|
14481
|
+
}
|
|
14482
|
+
return cursor + 1;
|
|
14483
|
+
}
|
|
14484
|
+
return source.length;
|
|
14485
|
+
}
|
|
14486
|
+
function cellAddress(row, column) {
|
|
14487
|
+
let value = column + 1;
|
|
14488
|
+
let label = '';
|
|
14489
|
+
while(value > 0){
|
|
14490
|
+
value -= 1;
|
|
14491
|
+
label = String.fromCharCode(65 + value % 26) + label;
|
|
14492
|
+
value = Math.floor(value / 26);
|
|
14493
|
+
}
|
|
14494
|
+
return `${label}${row + 1}`;
|
|
14495
|
+
}
|
|
14496
|
+
function quoteSheetName(name) {
|
|
14497
|
+
return /^[A-Za-z_][A-Za-z0-9_.]*$/u.test(name) ? name : `'${name.replaceAll("'", "''")}'`;
|
|
14498
|
+
}
|
|
14499
|
+
function isNameStart(value) {
|
|
14500
|
+
return /^[A-Za-z_\\?]$/u.test(value);
|
|
14501
|
+
}
|
|
14502
|
+
function isNameContinue(value) {
|
|
14503
|
+
return /^[A-Za-z0-9_.\\?]$/u.test(value);
|
|
14504
|
+
}
|
|
14505
|
+
function isQualifierCharacter(value) {
|
|
14506
|
+
return /^[A-Za-z0-9_.\\?[\]:$]$/u.test(value);
|
|
14507
|
+
}
|
|
14023
14508
|
async function calculateSpreadsheetInJavaScript(request) {
|
|
14024
14509
|
validateSpreadsheetCalculationRequest(request);
|
|
14025
14510
|
const formulaParser = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, 909));
|
|
@@ -14035,9 +14520,11 @@
|
|
|
14035
14520
|
stack = [];
|
|
14036
14521
|
calculationOrder = [];
|
|
14037
14522
|
issues = [];
|
|
14523
|
+
tableCatalog;
|
|
14038
14524
|
constructor(request, formulaParser){
|
|
14039
14525
|
this.request = request;
|
|
14040
14526
|
this.formulaParser = formulaParser;
|
|
14527
|
+
this.tableCatalog = new SpreadsheetStructuredReferenceCatalog(request.sheets);
|
|
14041
14528
|
for (const sheet of request.sheets){
|
|
14042
14529
|
this.sheetsByName.set(sheet.name.toLowerCase(), sheet);
|
|
14043
14530
|
for (const cell of sheet.cells)this.cells.set(cellKey(sheet.id, cell.row, cell.column), {
|
|
@@ -14108,6 +14595,13 @@
|
|
|
14108
14595
|
}
|
|
14109
14596
|
evaluateFormula(coordinate, indexed) {
|
|
14110
14597
|
const formula = indexed.cell.formula ?? '';
|
|
14598
|
+
let expandedFormula;
|
|
14599
|
+
try {
|
|
14600
|
+
expandedFormula = expandSpreadsheetStructuredReferences(formula, this.tableCatalog, indexed.sheet, coordinate.row, coordinate.column);
|
|
14601
|
+
} catch (error) {
|
|
14602
|
+
const message = error instanceof SpreadsheetStructuredReferenceError ? error.message : 'Structured reference expansion failed.';
|
|
14603
|
+
return failedEvaluation(indexed.cell.value, calculationIssue(coordinate, 'office.kernel.spreadsheet.formula_unsupported', message));
|
|
14604
|
+
}
|
|
14111
14605
|
const parser = new this.formulaParser.Parser();
|
|
14112
14606
|
let unresolvedDependency = false;
|
|
14113
14607
|
let unsupportedReference = false;
|
|
@@ -14141,7 +14635,7 @@
|
|
|
14141
14635
|
return true;
|
|
14142
14636
|
}));
|
|
14143
14637
|
});
|
|
14144
|
-
const parsed = parser.parse(normalizeFormulaForFortuneParser(
|
|
14638
|
+
const parsed = parser.parse(normalizeFormulaForFortuneParser(expandedFormula), {
|
|
14145
14639
|
sheetId: indexed.sheet.id
|
|
14146
14640
|
});
|
|
14147
14641
|
if (unsupportedFunction) return failedEvaluation(indexed.cell.value, calculationIssue(coordinate, 'office.kernel.spreadsheet.formula_unsupported', `Formula function '${unsupportedFunction}' is not supported.`));
|
|
@@ -14165,7 +14659,8 @@
|
|
|
14165
14659
|
}
|
|
14166
14660
|
rangeValues(currentSheet, start, end, onUnresolvedDependency, onUnsupportedReference, reserveRangeCells) {
|
|
14167
14661
|
const startCoordinate = this.resolveCoordinate(currentSheet, start);
|
|
14168
|
-
const
|
|
14662
|
+
const rangeSheet = start.sheetName ? this.sheetsByName.get(start.sheetName.toLowerCase()) : currentSheet;
|
|
14663
|
+
const endCoordinate = rangeSheet ? this.resolveCoordinate(rangeSheet, end) : null;
|
|
14169
14664
|
if (!startCoordinate || !endCoordinate || startCoordinate.sheetId !== endCoordinate.sheetId) {
|
|
14170
14665
|
onUnsupportedReference();
|
|
14171
14666
|
return [];
|
|
@@ -14344,6 +14839,14 @@
|
|
|
14344
14839
|
return sheets.map((sheet)=>({
|
|
14345
14840
|
id: sheet.id,
|
|
14346
14841
|
name: sheet.name,
|
|
14842
|
+
...sheet.tables?.length ? {
|
|
14843
|
+
tables: sheet.tables.map((table)=>({
|
|
14844
|
+
...table,
|
|
14845
|
+
columns: [
|
|
14846
|
+
...table.columns
|
|
14847
|
+
]
|
|
14848
|
+
}))
|
|
14849
|
+
} : {},
|
|
14347
14850
|
cells: sheet.cells.map((cell)=>({
|
|
14348
14851
|
...cell,
|
|
14349
14852
|
value: {
|
|
@@ -603,11 +603,16 @@ tests. Successful scalar results are applied without adding undo history,
|
|
|
603
603
|
known grouped formulas refresh before their dependents, and unresolved
|
|
604
604
|
dependencies enter an ordered, cell-scoped Fortune Sheet compatibility pass.
|
|
605
605
|
The shared Rust parser handles the same bounded formula grammar in the native
|
|
606
|
-
core and browser kernel.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
606
|
+
core and browser kernel. The current table slice resolves names/display names,
|
|
607
|
+
worksheet-qualified tables, contiguous column ranges, `#All`, `#Headers`,
|
|
608
|
+
`#Data`, `#Totals`, `#This Row`, and table-local `[@Column]` formulas. It is not
|
|
609
|
+
a complete Excel engine: Fortune Sheet remains the canonical grid, initial and
|
|
610
|
+
replacement sparse projection still run on the main thread, and the kernel
|
|
611
|
+
does not materialize whole-row or whole-column ranges, calculate arrays, spills,
|
|
612
|
+
external workbooks, own number formatting, or own print layout. Structured
|
|
613
|
+
areas are capped at 100,000 cells and requests at 1,024 tables; disjoint,
|
|
614
|
+
three-dimensional, missing, or over-budget references fail closed with a
|
|
615
|
+
cell-scoped diagnostic.
|
|
611
616
|
Presentation sends alignment plus move and resize snapping to Rust/WASM. The
|
|
612
617
|
main thread treats an ordered selection as one bounded geometry frame, paints
|
|
613
618
|
at most one transient preview per animation frame, and ignores stale geometry
|
|
@@ -1419,8 +1424,14 @@ row/column operations reconcile table structure, and Convert to Range
|
|
|
1419
1424
|
materializes appearance through a sparse-safe path. XLSX table parts,
|
|
1420
1425
|
relationships, content types, styles, and supported filters round-trip, while
|
|
1421
1426
|
Yjs uses ordered ID-keyed records and creation claims for two-client
|
|
1422
|
-
convergence.
|
|
1423
|
-
|
|
1427
|
+
convergence. The follow-up structured-reference slice now shares the parser and
|
|
1428
|
+
dependency session with native calculation, including calculated-column
|
|
1429
|
+
formulas authored on data rows and totals/header/data selectors. A consistent
|
|
1430
|
+
current-row rule is persisted in the table metadata and fills only empty cells
|
|
1431
|
+
in newly inserted body rows; manual values and conflicting formulas fail closed.
|
|
1432
|
+
Dense and sparse sheet representations use the same path, and native XLSX
|
|
1433
|
+
`<calculatedColumnFormula>` metadata round-trips. Complete totals-row
|
|
1434
|
+
authoring, slicers, and external/query tables remain open gates.
|
|
1424
1435
|
|
|
1425
1436
|
Exit criteria: scrolling and selection do not scale with total row count;
|
|
1426
1437
|
incremental recalculation touches only affected dependency subgraphs; XLSX
|
|
@@ -1642,7 +1653,9 @@ The XLSX row above is a rectangular one-million-cell data fixture, not a
|
|
|
1642
1653
|
semantic `WorkSpreadsheetTable`/OOXML ListObject benchmark. It proves the
|
|
1643
1654
|
plain-workbook import and visible-range Canvas path; it must not be used to
|
|
1644
1655
|
claim that structured-reference calculation, calculated columns, totals,
|
|
1645
|
-
filters, or table conversion have the same profile.
|
|
1656
|
+
filters, or table conversion have the same profile. Calculated-column fill is
|
|
1657
|
+
bounded by the inserted-row operation and has correctness coverage, but no
|
|
1658
|
+
large-ListObject latency or memory number is published yet. A dedicated large
|
|
1646
1659
|
ListObject matrix remains required before publishing table-specific load,
|
|
1647
1660
|
mutation, conversion, and export budgets.
|
|
1648
1661
|
|