@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.
@@ -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(formula), {
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 endCoordinate = this.resolveCoordinate(currentSheet, end);
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. This is not a complete Excel engine: Fortune Sheet
607
- remains the canonical grid, initial and replacement sparse projection still
608
- run on the main thread, and the kernel does not materialize whole-row or
609
- whole-column ranges, calculate arrays, spills, structured references or
610
- external workbooks, own number formatting, or own print layout.
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. Structured-reference calculation, calculated columns, complete
1423
- totals authoring, slicers, and external/query tables remain open gates.
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. A dedicated large
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",