@a3s-lab/office 0.15.0 → 0.17.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.
Files changed (35) hide show
  1. package/COLLABORATION_ROADMAP.md +15 -5
  2. package/README.md +28 -2
  3. package/dist/0~6090.js +7 -8
  4. package/dist/0~spreadsheet-editor.js +2347 -406
  5. package/dist/2180.js +370 -34
  6. package/dist/4104.js +657 -22
  7. package/dist/5184.js +1 -1
  8. package/dist/8715.js +156 -156
  9. package/dist/internal/collaboration/office-spreadsheet-collaboration-records.d.ts +2 -0
  10. package/dist/internal/collaboration/office-spreadsheet-collaboration-validation-support.d.ts +6 -0
  11. package/dist/internal/features/work/editors/spreadsheet-auto-filter.d.ts +1 -0
  12. package/dist/internal/features/work/editors/spreadsheet-command-catalog.d.ts +59 -1
  13. package/dist/internal/features/work/editors/spreadsheet-command-controller.d.ts +12 -0
  14. package/dist/internal/features/work/editors/spreadsheet-editor-ribbon.d.ts +4 -2
  15. package/dist/internal/features/work/editors/spreadsheet-font-size-command.d.ts +18 -0
  16. package/dist/internal/features/work/editors/spreadsheet-font-size.d.ts +4 -0
  17. package/dist/internal/features/work/editors/spreadsheet-table-command.d.ts +3 -0
  18. package/dist/internal/features/work/editors/spreadsheet-table-conversion.d.ts +7 -0
  19. package/dist/internal/features/work/editors/spreadsheet-table-dialog.d.ts +8 -0
  20. package/dist/internal/features/work/editors/spreadsheet-table-limits.d.ts +1 -0
  21. package/dist/internal/features/work/editors/spreadsheet-table-reconciliation.d.ts +21 -0
  22. package/dist/internal/features/work/editors/spreadsheet-table-render.d.ts +16 -0
  23. package/dist/internal/features/work/editors/spreadsheet-table-ribbon.d.ts +8 -0
  24. package/dist/internal/features/work/editors/spreadsheet-table-style.d.ts +31 -0
  25. package/dist/internal/features/work/editors/spreadsheet-table.d.ts +55 -0
  26. package/dist/internal/features/work/editors/use-spreadsheet-table.d.ts +23 -0
  27. package/dist/internal/features/work/work-types.d.ts +100 -0
  28. package/dist/internal/features/work/work-xlsx-interop.d.ts +2 -0
  29. package/dist/internal/features/work/work-xlsx-table-filters.d.ts +3 -0
  30. package/dist/internal/features/work/work-xlsx-tables.d.ts +4 -0
  31. package/dist/office-kernel.wasm +0 -0
  32. package/dist/styles.css +204 -3
  33. package/dist/work-spreadsheet-package-scan.worker.js +3 -2
  34. package/docs/latest/en/browser-editor-architecture.md +43 -0
  35. package/package.json +6 -1
package/dist/2180.js CHANGED
@@ -1,11 +1,78 @@
1
+ import { workOfficeCollaborationJsonEqual, canonicalWorkOfficeCollaborationJson, isWorkOfficeCollaborationRecord, cloneWorkOfficeCollaborationJson } from "./4650.js";
1
2
  import { initializeWorkOfficeCollaborationMetadata, assertWorkOfficeCollaborationEditable, OfficeCollaborationError as WorkOfficeCollaborationError, readOfficeCollaborationMetadata as readWorkOfficeCollaborationMetadata, registerWorkOfficeCollaborationInitializer, markWorkOfficeCollaborationInitialized, assertWorkOfficeCollaborationOrigin } from "./9787.js";
2
3
  import { OFFICE_KERNEL_SPREADSHEET_MAX_ROWS } from "./5184.js";
3
- import { workOfficeCollaborationJsonEqual, canonicalWorkOfficeCollaborationJson, isWorkOfficeCollaborationRecord, cloneWorkOfficeCollaborationJson } from "./4650.js";
4
- import { sparseMatrixColumnCount, cloneSparseMatrix, sparseArrayEntries as spreadsheet_sparse_sparseArrayEntries, sparseArrayIndexes, formatSpreadsheetCellRanges, parseSpreadsheetCellRanges as work_spreadsheet_ranges_parseSpreadsheetCellRanges } from "./8715.js";
4
+ import { sparseMatrixColumnCount, cloneSparseMatrix, sparseArrayEntries as spreadsheet_sparse_sparseArrayEntries, formatSpreadsheetCellRanges, isValidSpreadsheetDefinedName, parseSpreadsheetCellRanges as work_spreadsheet_ranges_parseSpreadsheetCellRanges, sparseArrayIndexes } from "./8715.js";
5
5
  import { patchWorkOfficeCollaborationFlatJsonMap, readWorkOfficeCollaborationFlatJsonMap } from "./7060.js";
6
6
  import * as __rspack_external_yjs from "yjs";
7
+ function requiredCoordinate(value, maximum, label, sheetId) {
8
+ if (!Number.isSafeInteger(value) || value < 0 || value >= maximum) invalidWorkOfficeSpreadsheetInput(`a valid ${label} coordinate in sheet '${sheetId}'`);
9
+ return value;
10
+ }
11
+ function requiredInputRecord(value, label) {
12
+ if (!isWorkOfficeCollaborationRecord(value)) invalidWorkOfficeSpreadsheetInput(`a valid ${label} record`);
13
+ return value;
14
+ }
15
+ function requiredIdentifier(value, label) {
16
+ const result = requiredNonEmptyString(value, `${label} ID`);
17
+ if (result !== result.trim() || result.length > 256) invalidWorkOfficeSpreadsheetInput(`a ${label} ID containing 1 to 256 characters`);
18
+ return result;
19
+ }
20
+ function requiredNonEmptyString(value, label) {
21
+ if ('string' != typeof value || !value.trim() || value.length > 256) invalidWorkOfficeSpreadsheetInput(`a non-empty string of at most 256 characters for ${label}`);
22
+ return value;
23
+ }
24
+ function validateJsonRecord(value, label) {
25
+ try {
26
+ return cloneWorkOfficeCollaborationJson(value);
27
+ } catch (error) {
28
+ throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `Spreadsheet collaboration requires a JSON-compatible ${label}: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ }
31
+ function invalidWorkOfficeSpreadsheetInput(expected) {
32
+ throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `Spreadsheet collaboration requires ${expected}.`);
33
+ }
7
34
  const MAX_POPULATED_CELLS = 1000000;
8
35
  const MAX_DENSE_MATRIX_CELLS = 1000000;
36
+ const MAX_FILTER_VALUES_PER_COLUMN = 10000;
37
+ const MAX_FILTER_VALUE_CHARACTERS = 32767;
38
+ const MAX_FILTER_TEXT_BYTES = 1048576;
39
+ const FILTER_TEXT_ENCODER = new TextEncoder();
40
+ const SPREADSHEET_DYNAMIC_FILTERS = new Set([
41
+ 'above-average',
42
+ 'below-average',
43
+ 'tomorrow',
44
+ 'today',
45
+ 'yesterday',
46
+ 'next-week',
47
+ 'this-week',
48
+ 'last-week',
49
+ 'next-month',
50
+ 'this-month',
51
+ 'last-month',
52
+ 'next-quarter',
53
+ 'this-quarter',
54
+ 'last-quarter',
55
+ 'next-year',
56
+ 'this-year',
57
+ 'last-year',
58
+ 'year-to-date',
59
+ 'quarter-1',
60
+ 'quarter-2',
61
+ 'quarter-3',
62
+ 'quarter-4',
63
+ 'month-1',
64
+ 'month-2',
65
+ 'month-3',
66
+ 'month-4',
67
+ 'month-5',
68
+ 'month-6',
69
+ 'month-7',
70
+ 'month-8',
71
+ 'month-9',
72
+ 'month-10',
73
+ 'month-11',
74
+ 'month-12'
75
+ ]);
9
76
  const TRANSIENT_SHEET_FIELDS = [
10
77
  'calcChain',
11
78
  'dynamicArray_compute',
@@ -74,6 +141,7 @@ function validateSheet(value) {
74
141
  if (void 0 !== source.images) result.images = validateIdRecords(source.images, `image in sheet '${result.id}'`).filter(({ id })=>!id.startsWith('work-chart-preview-'));
75
142
  if (void 0 !== source.charts) result.charts = validateIdRecords(source.charts, `chart in sheet '${result.id}'`);
76
143
  if (void 0 !== source.pivotTables) result.pivotTables = validateIdRecords(source.pivotTables, `pivot table in sheet '${result.id}'`);
144
+ if (void 0 !== source.tables) result.tables = validateSpreadsheetTables(source.tables, result.id);
77
145
  if (void 0 !== source.formulaMetadata) result.formulaMetadata = validateJsonRecord(requiredInputRecord(source.formulaMetadata, 'formula metadata'), `formula metadata in sheet '${result.id}'`);
78
146
  return result;
79
147
  }
@@ -128,6 +196,262 @@ function validateIdRecords(value, label) {
128
196
  return result;
129
197
  });
130
198
  }
199
+ function validateSpreadsheetTables(value, sheetId) {
200
+ const records = validateIdRecords(value, `table in sheet '${sheetId}'`);
201
+ return records.map((record)=>{
202
+ const label = `table '${record.id}' in sheet '${sheetId}'`;
203
+ const range = requiredSpreadsheetTableRange(record.range, label, sheetId);
204
+ const width = range.column[1] - range.column[0] + 1;
205
+ const columns = requiredSpreadsheetTableColumns(record.columns, width, label);
206
+ const filters = requiredSpreadsheetTableFilters(record.filters, width, label);
207
+ const headerRow = requiredBoolean(record.headerRow, `${label} header row`);
208
+ const totalsRow = requiredBoolean(record.totalsRow, `${label} totals row`);
209
+ const height = range.row[1] - range.row[0] + 1;
210
+ if (height <= Number(headerRow) + Number(totalsRow)) invalidWorkOfficeSpreadsheetInput(`${label} to include at least one body row`);
211
+ const result = record;
212
+ result.name = requiredSpreadsheetTableName(record.name, `${label} name`);
213
+ if (void 0 !== record.displayName) result.displayName = requiredSpreadsheetTableName(record.displayName, `${label} display name`);
214
+ if (void 0 !== record.ooxmlId) {
215
+ if (!Number.isSafeInteger(record.ooxmlId) || Number(record.ooxmlId) < 1) invalidWorkOfficeSpreadsheetInput(`a positive OOXML ID for ${label}`);
216
+ result.ooxmlId = Number(record.ooxmlId);
217
+ }
218
+ result.range = range;
219
+ result.columns = columns;
220
+ result.filters = filters;
221
+ result.headerRow = headerRow;
222
+ result.totalsRow = totalsRow;
223
+ const style = requiredSpreadsheetTableStyle(record.style, label);
224
+ const showFirstColumn = requiredBoolean(record.showFirstColumn, `${label} first-column emphasis`);
225
+ const showLastColumn = requiredBoolean(record.showLastColumn, `${label} last-column emphasis`);
226
+ const showRowStripes = requiredBoolean(record.showRowStripes, `${label} row stripes`);
227
+ const showColumnStripes = requiredBoolean(record.showColumnStripes, `${label} column stripes`);
228
+ if (!headerRow && filters.length > 0) invalidWorkOfficeSpreadsheetInput(`${label} filters to require an enabled header row`);
229
+ if ('none' === style.family && (showFirstColumn || showLastColumn || showRowStripes || showColumnStripes)) invalidWorkOfficeSpreadsheetInput(`${label} style flags to require a built-in style`);
230
+ result.style = style;
231
+ result.showFirstColumn = showFirstColumn;
232
+ result.showLastColumn = showLastColumn;
233
+ result.showRowStripes = showRowStripes;
234
+ result.showColumnStripes = showColumnStripes;
235
+ return result;
236
+ });
237
+ }
238
+ function requiredSpreadsheetTableRange(value, label, sheetId) {
239
+ const record = requiredInputRecord(value, `${label} range`);
240
+ const readAxis = (candidate, maximum, axis)=>{
241
+ if (!Array.isArray(candidate) || 2 !== candidate.length) invalidWorkOfficeSpreadsheetInput(`a bounded ${axis} range for ${label}`);
242
+ const start = requiredCoordinate(candidate[0], maximum, `${axis} start`, sheetId);
243
+ const end = requiredCoordinate(candidate[1], maximum, `${axis} end`, sheetId);
244
+ if (start > end) invalidWorkOfficeSpreadsheetInput(`an ordered ${axis} range for ${label}`);
245
+ return [
246
+ start,
247
+ end
248
+ ];
249
+ };
250
+ return {
251
+ row: readAxis(record.row, OFFICE_KERNEL_SPREADSHEET_MAX_ROWS, 'row'),
252
+ column: readAxis(record.column, 16384, 'column')
253
+ };
254
+ }
255
+ function requiredSpreadsheetTableColumns(value, width, label) {
256
+ if (!Array.isArray(value) || value.length !== width) invalidWorkOfficeSpreadsheetInput(`${label} to have one column definition per worksheet column`);
257
+ const names = new Set();
258
+ return value.map((candidate)=>{
259
+ const record = requiredInputRecord(candidate, `${label} column`);
260
+ assertExactRecordKeys(record, [
261
+ 'name'
262
+ ], `column for ${label}`);
263
+ const name = requiredTableColumnName(record.name, label);
264
+ const normalized = name.toLocaleLowerCase();
265
+ if (names.has(normalized)) invalidWorkOfficeSpreadsheetInput(`unique column names for ${label}`);
266
+ names.add(normalized);
267
+ return {
268
+ name
269
+ };
270
+ });
271
+ }
272
+ function requiredSpreadsheetTableFilters(value, width, label) {
273
+ if (!Array.isArray(value)) invalidWorkOfficeSpreadsheetInput(`an array of filters for ${label}`);
274
+ const columns = new Set();
275
+ const textBudget = {
276
+ bytes: 0
277
+ };
278
+ return value.map((candidate)=>{
279
+ const record = validateJsonRecord(requiredInputRecord(candidate, `${label} filter`), `${label} filter`);
280
+ assertExactRecordKeys(record, [
281
+ 'column',
282
+ 'criteria'
283
+ ], `filter for ${label}`);
284
+ if (!Number.isSafeInteger(record.column) || Number(record.column) < 0 || Number(record.column) >= width || columns.has(Number(record.column))) invalidWorkOfficeSpreadsheetInput(`unique in-range filter columns for ${label}`);
285
+ const column = Number(record.column);
286
+ const criteria = requiredSpreadsheetTableFilterCriteria(record.criteria, label, textBudget);
287
+ columns.add(column);
288
+ return {
289
+ column,
290
+ criteria
291
+ };
292
+ });
293
+ }
294
+ function requiredSpreadsheetTableFilterCriteria(value, label, textBudget) {
295
+ const record = validateJsonRecord(requiredInputRecord(value, `filter criteria for ${label}`), `filter criteria for ${label}`);
296
+ const type = record.type;
297
+ switch(type){
298
+ case 'values':
299
+ {
300
+ assertExactRecordKeys(record, [
301
+ 'type',
302
+ 'values',
303
+ 'includeBlanks'
304
+ ], `filter criteria for ${label}`);
305
+ if (!Array.isArray(record.values) || record.values.length > MAX_FILTER_VALUES_PER_COLUMN) invalidWorkOfficeSpreadsheetInput(`at most ${MAX_FILTER_VALUES_PER_COLUMN.toLocaleString()} values in filter criteria for ${label}`);
306
+ const includeBlanks = requiredBoolean(record.includeBlanks, `filter blank inclusion for ${label}`);
307
+ if (0 === record.values.length && !includeBlanks) invalidWorkOfficeSpreadsheetInput(`at least one value or includeBlanks=true in filter criteria for ${label}`);
308
+ const observed = new Set();
309
+ const values = [];
310
+ for (const candidate of record.values){
311
+ const filterValue = requiredSpreadsheetFilterText(candidate, label, textBudget);
312
+ if (observed.has(filterValue)) invalidWorkOfficeSpreadsheetInput(`unique filter values in criteria for ${label}`);
313
+ observed.add(filterValue);
314
+ values.push(filterValue);
315
+ }
316
+ return {
317
+ type,
318
+ values,
319
+ includeBlanks
320
+ };
321
+ }
322
+ case 'equals':
323
+ case 'not-equals':
324
+ case 'contains':
325
+ case 'does-not-contain':
326
+ case 'begins-with':
327
+ case 'ends-with':
328
+ case 'greater-than':
329
+ case 'greater-than-or-equal':
330
+ case 'less-than':
331
+ case 'less-than-or-equal':
332
+ assertExactRecordKeys(record, [
333
+ 'type',
334
+ 'value'
335
+ ], `filter criteria for ${label}`);
336
+ return {
337
+ type,
338
+ value: requiredSpreadsheetFilterText(record.value, label, textBudget)
339
+ };
340
+ case 'between':
341
+ case 'not-between':
342
+ assertExactRecordKeys(record, [
343
+ 'type',
344
+ 'lower',
345
+ 'upper'
346
+ ], `filter criteria for ${label}`);
347
+ return {
348
+ type,
349
+ lower: requiredSpreadsheetFilterText(record.lower, label, textBudget),
350
+ upper: requiredSpreadsheetFilterText(record.upper, label, textBudget)
351
+ };
352
+ case 'blanks':
353
+ case 'non-blanks':
354
+ assertExactRecordKeys(record, [
355
+ 'type'
356
+ ], `filter criteria for ${label}`);
357
+ return {
358
+ type
359
+ };
360
+ case 'top':
361
+ case 'bottom':
362
+ assertExactRecordKeys(record, [
363
+ 'type',
364
+ 'count'
365
+ ], `filter criteria for ${label}`);
366
+ return {
367
+ type,
368
+ count: requiredSpreadsheetFilterInteger(record.count, 1, 500, `a ${type} filter count from 1 through 500 for ${label}`)
369
+ };
370
+ case 'top-percent':
371
+ case 'bottom-percent':
372
+ assertExactRecordKeys(record, [
373
+ 'type',
374
+ 'percent'
375
+ ], `filter criteria for ${label}`);
376
+ return {
377
+ type,
378
+ percent: requiredSpreadsheetFilterInteger(record.percent, 1, 100, `a ${type} filter percentage from 1 through 100 for ${label}`)
379
+ };
380
+ case 'dynamic':
381
+ assertExactRecordKeys(record, [
382
+ 'type',
383
+ 'kind'
384
+ ], `filter criteria for ${label}`);
385
+ if ('string' != typeof record.kind || !SPREADSHEET_DYNAMIC_FILTERS.has(record.kind)) invalidWorkOfficeSpreadsheetInput(`a supported dynamic filter for ${label}`);
386
+ return {
387
+ type,
388
+ kind: record.kind
389
+ };
390
+ default:
391
+ invalidWorkOfficeSpreadsheetInput(`supported filter criteria for ${label}`);
392
+ }
393
+ }
394
+ function requiredSpreadsheetFilterText(value, label, textBudget) {
395
+ if ('string' != typeof value) invalidWorkOfficeSpreadsheetInput(`filter text containing 1 to ${MAX_FILTER_VALUE_CHARACTERS.toLocaleString()} characters for ${label}`);
396
+ let characters = 0;
397
+ for (const character of value){
398
+ characters += 1;
399
+ const codePoint = character.codePointAt(0);
400
+ if (void 0 === codePoint || !(0x9 === codePoint || 0xa === codePoint || 0xd === codePoint || codePoint >= 0x20 && codePoint <= 0xd7ff || codePoint >= 0xe000 && codePoint <= 0xfffd || codePoint >= 0x10000 && codePoint <= 0x10ffff)) invalidWorkOfficeSpreadsheetInput(`XML-compatible filter text for ${label}`);
401
+ }
402
+ if (characters < 1 || characters > MAX_FILTER_VALUE_CHARACTERS) invalidWorkOfficeSpreadsheetInput(`filter text containing 1 to ${MAX_FILTER_VALUE_CHARACTERS.toLocaleString()} characters for ${label}`);
403
+ textBudget.bytes += FILTER_TEXT_ENCODER.encode(value).byteLength;
404
+ if (textBudget.bytes > MAX_FILTER_TEXT_BYTES) invalidWorkOfficeSpreadsheetInput(`filter text totaling at most ${MAX_FILTER_TEXT_BYTES.toLocaleString()} bytes for ${label}`);
405
+ return value;
406
+ }
407
+ function requiredSpreadsheetFilterInteger(value, minimum, maximum, expected) {
408
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) invalidWorkOfficeSpreadsheetInput(expected);
409
+ return Number(value);
410
+ }
411
+ function assertExactRecordKeys(record, keys, label) {
412
+ const allowed = new Set(keys);
413
+ if (keys.some((key)=>!Object.hasOwn(record, key)) || Object.keys(record).some((key)=>!allowed.has(key))) invalidWorkOfficeSpreadsheetInput(`a complete ${label} record without unknown fields`);
414
+ }
415
+ function requiredSpreadsheetTableStyle(value, label) {
416
+ const record = validateJsonRecord(requiredInputRecord(value, `${label} style`), `${label} style`);
417
+ const family = record.family;
418
+ if ('none' === family) {
419
+ assertExactRecordKeys(record, [
420
+ 'family'
421
+ ], `style for ${label}`);
422
+ return {
423
+ family
424
+ };
425
+ }
426
+ assertExactRecordKeys(record, [
427
+ 'family',
428
+ 'number'
429
+ ], `style for ${label}`);
430
+ const maximum = 'light' === family ? 21 : 'medium' === family ? 28 : 11;
431
+ if ('light' !== family && 'medium' !== family && 'dark' !== family || !Number.isSafeInteger(record.number) || Number(record.number) < 1 || Number(record.number) > maximum) invalidWorkOfficeSpreadsheetInput(`a built-in table style for ${label}`);
432
+ return {
433
+ family,
434
+ number: Number(record.number)
435
+ };
436
+ }
437
+ function requiredSpreadsheetTableName(value, label) {
438
+ if ('string' != typeof value || value.trim() !== value || Array.from(value).length < 1 || Array.from(value).length > 255) invalidWorkOfficeSpreadsheetInput(`a valid table name for ${label}`);
439
+ const result = value;
440
+ if (!isValidSpreadsheetDefinedName(result) || [
441
+ 'r',
442
+ 'c'
443
+ ].includes(result.toLocaleLowerCase())) invalidWorkOfficeSpreadsheetInput(`a valid table name for ${label}`);
444
+ return result;
445
+ }
446
+ function requiredTableColumnName(value, label) {
447
+ const characters = 'string' == typeof value ? Array.from(value) : [];
448
+ if ('string' != typeof value || characters.length < 1 || characters.length > 255 || value.trim() !== value || characters.some((character)=>/\p{Cc}/u.test(character) || '\uFFFE' === character || '\uFFFF' === character)) invalidWorkOfficeSpreadsheetInput(`a valid column name for ${label}`);
449
+ return value;
450
+ }
451
+ function requiredBoolean(value, label) {
452
+ if ('boolean' != typeof value) invalidWorkOfficeSpreadsheetInput(`a Boolean value for ${label}`);
453
+ return value;
454
+ }
131
455
  function copyOptionalSheetSidecars(source, target, key, label) {
132
456
  const value = source[key];
133
457
  if (void 0 === value) return;
@@ -151,8 +475,25 @@ function copyOptionalJsonField(source, target, key, label) {
151
475
  }
152
476
  function assertSpreadsheetReferences(content) {
153
477
  const sheetIds = new Set(content.sheets.map((sheet)=>sheet.id));
478
+ const definedNames = new Set((content.namedRanges ?? []).map((range)=>range.name.trim().toLocaleLowerCase()));
479
+ const tableNames = new Set();
154
480
  for (const range of content.namedRanges ?? [])if (void 0 !== range.scopeSheetId && !sheetIds.has(range.scopeSheetId)) invalidWorkOfficeSpreadsheetInput(`named range '${range.id}' to reference an existing scope sheet`);
155
- for (const sheet of content.sheets)for (const pivot of sheet.pivotTables ?? [])if (!sheetIds.has(pivot.sourceSheetId)) invalidWorkOfficeSpreadsheetInput(`pivot table '${pivot.id}' to reference an existing source sheet`);
481
+ for (const sheet of content.sheets){
482
+ for (const pivot of sheet.pivotTables ?? [])if (!sheetIds.has(pivot.sourceSheetId)) invalidWorkOfficeSpreadsheetInput(`pivot table '${pivot.id}' to reference an existing source sheet`);
483
+ for (const [index, table] of (sheet.tables ?? []).entries()){
484
+ const names = [
485
+ table.name,
486
+ table.displayName
487
+ ].filter((value)=>Boolean(value));
488
+ for (const name of names){
489
+ const normalized = name.toLocaleLowerCase();
490
+ if (definedNames.has(normalized) || tableNames.has(normalized)) invalidWorkOfficeSpreadsheetInput(`unique table and defined names; '${name}' is repeated`);
491
+ tableNames.add(normalized);
492
+ }
493
+ if (table.range.row[1] >= (sheet.row ?? OFFICE_KERNEL_SPREADSHEET_MAX_ROWS) || table.range.column[1] >= (sheet.column ?? 16384)) invalidWorkOfficeSpreadsheetInput(`table '${table.id}' to fit within sheet '${sheet.id}'`);
494
+ for (const other of (sheet.tables ?? []).slice(0, index))if (spreadsheetTableRangesIntersect(table.range, other.range)) invalidWorkOfficeSpreadsheetInput(`non-overlapping tables in sheet '${sheet.id}'`);
495
+ }
496
+ }
156
497
  for (const key of [
157
498
  'printAreas',
158
499
  'printTitles',
@@ -160,42 +501,18 @@ function assertSpreadsheetReferences(content) {
160
501
  'pageSetups'
161
502
  ])for (const record of content[key] ?? [])if (!sheetIds.has(record.sheetId)) invalidWorkOfficeSpreadsheetInput(`${key} to reference an existing sheet '${record.sheetId}'`);
162
503
  }
504
+ function spreadsheetTableRangesIntersect(left, right) {
505
+ return left.row[0] <= right.row[1] && left.row[1] >= right.row[0] && left.column[0] <= right.column[1] && left.column[1] >= right.column[0];
506
+ }
163
507
  function validateOptionalDimension(sheet, key, maximum) {
164
508
  const value = sheet[key];
165
509
  if (void 0 === value) return;
166
510
  if (!Number.isSafeInteger(value) || value < 0 || value > maximum) invalidWorkOfficeSpreadsheetInput(`a ${key} count between 0 and ${maximum} in sheet '${sheet.id}'`);
167
511
  }
168
- function requiredCoordinate(value, maximum, label, sheetId) {
169
- if (!Number.isSafeInteger(value) || value < 0 || value >= maximum) invalidWorkOfficeSpreadsheetInput(`a valid ${label} coordinate in sheet '${sheetId}'`);
170
- return value;
171
- }
172
- function requiredInputRecord(value, label) {
173
- if (!isWorkOfficeCollaborationRecord(value)) invalidWorkOfficeSpreadsheetInput(`a valid ${label} record`);
174
- return value;
175
- }
176
- function requiredIdentifier(value, label) {
177
- const result = requiredNonEmptyString(value, `${label} ID`);
178
- if (result !== result.trim() || result.length > 256) invalidWorkOfficeSpreadsheetInput(`a ${label} ID containing 1 to 256 characters`);
179
- return result;
180
- }
181
- function requiredNonEmptyString(value, label) {
182
- if ('string' != typeof value || !value.trim() || value.length > 256) invalidWorkOfficeSpreadsheetInput(`a non-empty string of at most 256 characters for ${label}`);
183
- return value;
184
- }
185
- function validateJsonRecord(value, label) {
186
- try {
187
- return cloneWorkOfficeCollaborationJson(value);
188
- } catch (error) {
189
- throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `Spreadsheet collaboration requires a JSON-compatible ${label}: ${error instanceof Error ? error.message : String(error)}`);
190
- }
191
- }
192
512
  function assertPopulatedCellLimit(count, sheetId) {
193
513
  if (count <= MAX_POPULATED_CELLS) return;
194
514
  invalidWorkOfficeSpreadsheetInput(`at most ${MAX_POPULATED_CELLS.toLocaleString()} populated cells in sheet '${sheetId}'`);
195
515
  }
196
- function invalidWorkOfficeSpreadsheetInput(expected) {
197
- throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `Spreadsheet collaboration requires ${expected}.`);
198
- }
199
516
  function appendWorkOfficeSpreadsheetRecordClaims(claims, previous, next) {
200
517
  const previousSheets = new Map((previous?.sheets ?? []).map((sheet)=>[
201
518
  sheet.id,
@@ -207,6 +524,7 @@ function appendWorkOfficeSpreadsheetRecordClaims(claims, previous, next) {
207
524
  appendChildClaims(claims, before, sheet, 'image', 'images');
208
525
  appendChildClaims(claims, before, sheet, 'chart', 'charts');
209
526
  appendChildClaims(claims, before, sheet, 'pivot-table', 'pivotTables');
527
+ appendChildClaims(claims, before, sheet, 'table', 'tables');
210
528
  }
211
529
  const previousNamedRanges = new Set((previous?.namedRanges ?? []).map(({ id })=>id));
212
530
  for (const range of next.namedRanges ?? [])if (!previousNamedRanges.has(range.id)) appendClaim(claims, 'named-range', range.id, range);
@@ -225,6 +543,7 @@ function assertWorkOfficeSpreadsheetRecordClaims(claims, content) {
225
543
  for (const image of sheet.images ?? [])assertClaimExists(fingerprints, 'image', image.id, sheet.id);
226
544
  for (const chart of sheet.charts ?? [])assertClaimExists(fingerprints, 'chart', chart.id, sheet.id);
227
545
  for (const pivot of sheet.pivotTables ?? [])assertClaimExists(fingerprints, 'pivot-table', pivot.id, sheet.id);
546
+ for (const table of sheet.tables ?? [])assertClaimExists(fingerprints, 'table', table.id, sheet.id);
228
547
  }
229
548
  for (const range of content.namedRanges ?? [])assertClaimExists(fingerprints, 'named-range', range.id);
230
549
  }
@@ -275,7 +594,7 @@ function parsedClaim(raw) {
275
594
  };
276
595
  }
277
596
  function requiredClaimKind(value) {
278
- if ('chart' === value || 'image' === value || 'named-range' === value || 'pivot-table' === value || 'sheet' === value) return value;
597
+ if ('chart' === value || 'image' === value || 'named-range' === value || 'pivot-table' === value || 'table' === value || 'sheet' === value) return value;
279
598
  invalidWorkOfficeSpreadsheetShared('record claim kind');
280
599
  }
281
600
  function requiredClaimIdentifier(value, label) {
@@ -304,6 +623,7 @@ function claimIdentity(kind, id, parentId) {
304
623
  function claimLabel(claim) {
305
624
  if ('named-range' === claim.kind) return 'named range';
306
625
  if ('pivot-table' === claim.kind) return `pivot table in sheet '${claim.parentId}'`;
626
+ if ('table' === claim.kind) return `table in sheet '${claim.parentId}'`;
307
627
  if ('sheet' === claim.kind) return 'sheet';
308
628
  return `${claim.kind} in sheet '${claim.parentId}'`;
309
629
  }
@@ -327,7 +647,8 @@ function assertCompatibleSheet(previous, next, shared, label) {
327
647
  'formulaMetadata',
328
648
  'images',
329
649
  'charts',
330
- 'pivotTables'
650
+ 'pivotTables',
651
+ 'tables'
331
652
  ]);
332
653
  assertCompatibleValue(withoutKeys(previous, omitted), withoutKeys(next, omitted), withoutKeys(shared, omitted), label);
333
654
  assertCompatibleValue(previous.config, next.config, shared.config, `${label} config`);
@@ -335,6 +656,7 @@ function assertCompatibleSheet(previous, next, shared, label) {
335
656
  assertRecordCollection(previous.images ?? [], next.images ?? [], shared.images ?? [], (image)=>image.id, `${label} image`);
336
657
  assertRecordCollection(previous.charts ?? [], next.charts ?? [], shared.charts ?? [], (chart)=>chart.id, `${label} chart`);
337
658
  assertRecordCollection(previous.pivotTables ?? [], next.pivotTables ?? [], shared.pivotTables ?? [], (pivot)=>pivot.id, `${label} pivot table`);
659
+ assertRecordCollection(previous.tables ?? [], next.tables ?? [], shared.tables ?? [], (table)=>table.id, `${label} table`);
338
660
  assertCellCollection(spreadsheetCells(previous), spreadsheetCells(next), spreadsheetCells(shared), label);
339
661
  }
340
662
  function assertCellCollection(previous, next, shared, sheetLabel) {
@@ -700,6 +1022,8 @@ const SPREADSHEET_RECORD_CHARTS = 'charts';
700
1022
  const SPREADSHEET_RECORD_CHART_ORDER = 'chartOrder';
701
1023
  const SPREADSHEET_RECORD_PIVOTS = 'pivotTables';
702
1024
  const SPREADSHEET_RECORD_PIVOT_ORDER = 'pivotOrder';
1025
+ const SPREADSHEET_RECORD_TABLES = 'tables';
1026
+ const SPREADSHEET_RECORD_TABLE_ORDER = 'tableOrder';
703
1027
  const NESTED_SHEET_FIELDS = new Set([
704
1028
  "cells",
705
1029
  SPREADSHEET_RECORD_CELL_PRESENCE,
@@ -712,7 +1036,9 @@ const NESTED_SHEET_FIELDS = new Set([
712
1036
  SPREADSHEET_RECORD_CHARTS,
713
1037
  SPREADSHEET_RECORD_CHART_ORDER,
714
1038
  SPREADSHEET_RECORD_PIVOTS,
715
- SPREADSHEET_RECORD_PIVOT_ORDER
1039
+ SPREADSHEET_RECORD_PIVOT_ORDER,
1040
+ SPREADSHEET_RECORD_TABLES,
1041
+ SPREADSHEET_RECORD_TABLE_ORDER
716
1042
  ]);
717
1043
  function initializeSpreadsheetSheetRecord(record, sheet) {
718
1044
  office_spreadsheet_collaboration_records_nestedSpreadsheetMap(record, SPREADSHEET_RECORD_CONFIG, 'sheet config');
@@ -720,6 +1046,7 @@ function initializeSpreadsheetSheetRecord(record, sheet) {
720
1046
  initializeIdCollection(record, SPREADSHEET_RECORD_IMAGES, SPREADSHEET_RECORD_IMAGE_ORDER);
721
1047
  initializeIdCollection(record, SPREADSHEET_RECORD_CHARTS, SPREADSHEET_RECORD_CHART_ORDER);
722
1048
  initializeIdCollection(record, SPREADSHEET_RECORD_PIVOTS, SPREADSHEET_RECORD_PIVOT_ORDER);
1049
+ initializeIdCollection(record, SPREADSHEET_RECORD_TABLES, SPREADSHEET_RECORD_TABLE_ORDER);
723
1050
  patchSpreadsheetSheetRecord(record, void 0, sheet);
724
1051
  }
725
1052
  function patchSpreadsheetSheetRecord(record, previous, next) {
@@ -729,13 +1056,14 @@ function patchSpreadsheetSheetRecord(record, previous, next) {
729
1056
  ...Object.keys(before),
730
1057
  ...Object.keys(after)
731
1058
  ]);
732
- for (const key of keys)if ('data' !== key && 'celldata' !== key && 'config' !== key && 'formulaMetadata' !== key && 'images' !== key && 'charts' !== key && 'pivotTables' !== key) patchOptionalJson(record, key, before[key], after[key], void 0 !== previous);
1059
+ for (const key of keys)if ('data' !== key && 'celldata' !== key && 'config' !== key && 'formulaMetadata' !== key && 'images' !== key && 'charts' !== key && 'pivotTables' !== key && 'tables' !== key) patchOptionalJson(record, key, before[key], after[key], void 0 !== previous);
733
1060
  patchJsonMap(office_spreadsheet_collaboration_records_nestedSpreadsheetMap(record, SPREADSHEET_RECORD_CONFIG, 'sheet config'), previous?.config, next.config);
734
1061
  patchJsonMap(office_spreadsheet_collaboration_records_nestedSpreadsheetMap(record, SPREADSHEET_RECORD_FORMULA_METADATA, 'sheet formula metadata'), previous?.formulaMetadata, next.formulaMetadata);
735
1062
  patchSpreadsheetCells(record, previous, next);
736
1063
  patchIdCollection(record, SPREADSHEET_RECORD_IMAGES, SPREADSHEET_RECORD_IMAGE_ORDER, previous?.images ?? [], next.images ?? [], `image in sheet '${next.id}'`);
737
1064
  patchIdCollection(record, SPREADSHEET_RECORD_CHARTS, SPREADSHEET_RECORD_CHART_ORDER, previous?.charts ?? [], next.charts ?? [], `chart in sheet '${next.id}'`);
738
1065
  patchIdCollection(record, SPREADSHEET_RECORD_PIVOTS, SPREADSHEET_RECORD_PIVOT_ORDER, previous?.pivotTables ?? [], next.pivotTables ?? [], `pivot table in sheet '${next.id}'`);
1066
+ patchIdCollection(record, SPREADSHEET_RECORD_TABLES, SPREADSHEET_RECORD_TABLE_ORDER, previous?.tables ?? [], next.tables ?? [], `table in sheet '${next.id}'`);
739
1067
  }
740
1068
  function readSpreadsheetSheetRecord(record, id) {
741
1069
  const result = {};
@@ -753,6 +1081,7 @@ function readSpreadsheetSheetRecord(record, id) {
753
1081
  readOptionalCollection(record, SPREADSHEET_RECORD_IMAGES, SPREADSHEET_RECORD_IMAGE_ORDER, `image in sheet '${id}'`, 'images', result);
754
1082
  readOptionalCollection(record, SPREADSHEET_RECORD_CHARTS, SPREADSHEET_RECORD_CHART_ORDER, `chart in sheet '${id}'`, 'charts', result);
755
1083
  readOptionalCollection(record, SPREADSHEET_RECORD_PIVOTS, SPREADSHEET_RECORD_PIVOT_ORDER, `pivot table in sheet '${id}'`, 'pivotTables', result);
1084
+ readBackwardCompatibleCollection(record, SPREADSHEET_RECORD_TABLES, SPREADSHEET_RECORD_TABLE_ORDER, `table in sheet '${id}'`, 'tables', result);
756
1085
  return sheet;
757
1086
  }
758
1087
  function patchSpreadsheetOrder(order, previous, next) {
@@ -843,6 +1172,13 @@ function readOptionalCollection(parent, recordsKey, orderKey, label, outputKey,
843
1172
  });
844
1173
  if (result.length > 0) output[outputKey] = result;
845
1174
  }
1175
+ function readBackwardCompatibleCollection(parent, recordsKey, orderKey, label, outputKey, output) {
1176
+ const hasRecords = parent.has(recordsKey);
1177
+ const hasOrder = parent.has(orderKey);
1178
+ if (!hasRecords && !hasOrder) return;
1179
+ if (hasRecords !== hasOrder) invalidWorkOfficeSpreadsheetShared(label);
1180
+ readOptionalCollection(parent, recordsKey, orderKey, label, outputKey, output);
1181
+ }
846
1182
  function patchJsonMap(target, previous, next) {
847
1183
  patchSpreadsheetFlatJsonMap(target, previous, next, 'Spreadsheet record');
848
1184
  }