@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.
- package/COLLABORATION_ROADMAP.md +15 -5
- package/README.md +28 -2
- package/dist/0~6090.js +7 -8
- package/dist/0~spreadsheet-editor.js +2347 -406
- package/dist/2180.js +370 -34
- package/dist/4104.js +657 -22
- package/dist/5184.js +1 -1
- package/dist/8715.js +156 -156
- package/dist/internal/collaboration/office-spreadsheet-collaboration-records.d.ts +2 -0
- package/dist/internal/collaboration/office-spreadsheet-collaboration-validation-support.d.ts +6 -0
- package/dist/internal/features/work/editors/spreadsheet-auto-filter.d.ts +1 -0
- package/dist/internal/features/work/editors/spreadsheet-command-catalog.d.ts +59 -1
- package/dist/internal/features/work/editors/spreadsheet-command-controller.d.ts +12 -0
- package/dist/internal/features/work/editors/spreadsheet-editor-ribbon.d.ts +4 -2
- package/dist/internal/features/work/editors/spreadsheet-font-size-command.d.ts +18 -0
- package/dist/internal/features/work/editors/spreadsheet-font-size.d.ts +4 -0
- package/dist/internal/features/work/editors/spreadsheet-table-command.d.ts +3 -0
- package/dist/internal/features/work/editors/spreadsheet-table-conversion.d.ts +7 -0
- package/dist/internal/features/work/editors/spreadsheet-table-dialog.d.ts +8 -0
- package/dist/internal/features/work/editors/spreadsheet-table-limits.d.ts +1 -0
- package/dist/internal/features/work/editors/spreadsheet-table-reconciliation.d.ts +21 -0
- package/dist/internal/features/work/editors/spreadsheet-table-render.d.ts +16 -0
- package/dist/internal/features/work/editors/spreadsheet-table-ribbon.d.ts +8 -0
- package/dist/internal/features/work/editors/spreadsheet-table-style.d.ts +31 -0
- package/dist/internal/features/work/editors/spreadsheet-table.d.ts +55 -0
- package/dist/internal/features/work/editors/use-spreadsheet-table.d.ts +23 -0
- package/dist/internal/features/work/work-types.d.ts +100 -0
- package/dist/internal/features/work/work-xlsx-interop.d.ts +2 -0
- package/dist/internal/features/work/work-xlsx-table-filters.d.ts +3 -0
- package/dist/internal/features/work/work-xlsx-tables.d.ts +4 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/styles.css +204 -3
- package/dist/work-spreadsheet-package-scan.worker.js +3 -2
- package/docs/latest/en/browser-editor-architecture.md +43 -0
- 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 {
|
|
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)
|
|
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
|
}
|