@a3s-lab/office 0.36.0 → 0.37.1

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 (59) hide show
  1. package/README.md +18 -6
  2. package/dist/0~3557.js +2 -1
  3. package/dist/0~3635.js +4 -1
  4. package/dist/0~7048.js +93 -1
  5. package/dist/0~9073.js +7 -2
  6. package/dist/0~presentation-editor.js +45 -9
  7. package/dist/0~spreadsheet-editor.js +20321 -16966
  8. package/dist/4121.js +1942 -1939
  9. package/dist/{3266.js → 5416.js} +1504 -20
  10. package/dist/9333.js +1 -1
  11. package/dist/core.d.ts +1 -0
  12. package/dist/core.js +141 -28
  13. package/dist/index.js +1 -1
  14. package/dist/internal/features/work/editors/office-shortcuts.d.ts +1 -0
  15. package/dist/internal/features/work/editors/spreadsheet-auto-filter-command.d.ts +3 -0
  16. package/dist/internal/features/work/editors/spreadsheet-auto-filter-condition-dialog-model.d.ts +27 -0
  17. package/dist/internal/features/work/editors/spreadsheet-auto-filter-condition-dialog.d.ts +17 -0
  18. package/dist/internal/features/work/editors/spreadsheet-auto-filter-menu.d.ts +10 -0
  19. package/dist/internal/features/work/editors/spreadsheet-auto-filter.d.ts +16 -4
  20. package/dist/internal/features/work/editors/spreadsheet-cell-range.d.ts +1 -0
  21. package/dist/internal/features/work/editors/spreadsheet-command-catalog.d.ts +9 -0
  22. package/dist/internal/features/work/editors/spreadsheet-command-controller.d.ts +13 -1
  23. package/dist/internal/features/work/editors/spreadsheet-current-region.d.ts +12 -0
  24. package/dist/internal/features/work/editors/spreadsheet-editor.d.ts +3 -0
  25. package/dist/internal/features/work/editors/spreadsheet-sort-appearance.d.ts +45 -0
  26. package/dist/internal/features/work/editors/spreadsheet-sort-collation.d.ts +8 -0
  27. package/dist/internal/features/work/editors/spreadsheet-sort-command.d.ts +3 -0
  28. package/dist/internal/features/work/editors/spreadsheet-sort-custom-list-editor.d.ts +8 -0
  29. package/dist/internal/features/work/editors/spreadsheet-sort-custom-list-store.d.ts +13 -0
  30. package/dist/internal/features/work/editors/spreadsheet-sort-custom-list.d.ts +26 -0
  31. package/dist/internal/features/work/editors/spreadsheet-sort-dialog.d.ts +9 -0
  32. package/dist/internal/features/work/editors/spreadsheet-sort-matrix.d.ts +4 -0
  33. package/dist/internal/features/work/editors/spreadsheet-sort-options-dialog.d.ts +7 -0
  34. package/dist/internal/features/work/editors/spreadsheet-sort-order-controls.d.ts +16 -0
  35. package/dist/internal/features/work/editors/spreadsheet-sort-range-dialog.d.ts +7 -0
  36. package/dist/internal/features/work/editors/spreadsheet-sort.d.ts +138 -0
  37. package/dist/internal/features/work/editors/spreadsheet-table-totals.d.ts +39 -0
  38. package/dist/internal/features/work/editors/spreadsheet-table.d.ts +11 -1
  39. package/dist/internal/features/work/editors/use-spreadsheet-auto-filter.d.ts +12 -3
  40. package/dist/internal/features/work/editors/use-spreadsheet-sort.d.ts +26 -0
  41. package/dist/internal/features/work/editors/work-office-chrome.d.ts +3 -0
  42. package/dist/internal/features/work/work-spreadsheet-auto-filter.d.ts +14 -0
  43. package/dist/internal/features/work/work-spreadsheet-dynamic-filter.d.ts +7 -0
  44. package/dist/internal/features/work/work-spreadsheet-filter-contract.d.ts +3 -0
  45. package/dist/internal/features/work/work-spreadsheet-wildcard.d.ts +7 -0
  46. package/dist/internal/features/work/work-types.d.ts +45 -7
  47. package/dist/internal/features/work/work-xlsx-interop.d.ts +2 -1
  48. package/dist/internal/features/work/work-xlsx-table-filters.d.ts +3 -1
  49. package/dist/internal/kernel/office-kernel-spreadsheet-fallback-formula.d.ts +11 -0
  50. package/dist/office-kernel.wasm +0 -0
  51. package/dist/office-kernel.worker.js +93 -1
  52. package/dist/styles.css +789 -145
  53. package/dist/vue.d.ts +3 -1
  54. package/dist/vue.js +2 -0
  55. package/dist/web-component.d.ts +3 -1
  56. package/dist/web-component.js +9 -0
  57. package/dist/work-spreadsheet-package-scan.worker.js +2 -1
  58. package/docs/latest/en/browser-editor-architecture.md +136 -2
  59. package/package.json +24 -1
@@ -173,6 +173,340 @@ function nearestSpreadsheetTableCell(sheet, table, row, column) {
173
173
  }
174
174
  return null;
175
175
  }
176
+ const MAX_SPREADSHEET_TABLE_TOTALS_FORMULA_LENGTH = 8192;
177
+ const MAX_SPREADSHEET_TABLE_TOTALS_LABEL_LENGTH = 255;
178
+ const DEFAULT_SPREADSHEET_TABLE_TOTALS_LABEL = 'Total';
179
+ const SPREADSHEET_TABLE_TOTALS_FUNCTIONS = Object.freeze([
180
+ 'sum',
181
+ 'average',
182
+ 'count',
183
+ 'countNums',
184
+ 'max',
185
+ 'min',
186
+ 'stdDev',
187
+ 'stdDevP',
188
+ 'var',
189
+ 'varP',
190
+ 'custom'
191
+ ]);
192
+ const UNSAFE_TOTALS_FUNCTIONS = new Set([
193
+ 'CALL',
194
+ 'DDE',
195
+ 'DDE.REQUEST',
196
+ 'DDE.POKE',
197
+ 'EXEC',
198
+ 'HYPERLINK',
199
+ 'INDIRECT',
200
+ 'OFFSET',
201
+ 'REGISTER.ID',
202
+ 'RTD',
203
+ 'WEBSERVICE'
204
+ ]);
205
+ const SUBTOTAL_CODES = {
206
+ average: 101,
207
+ count: 103,
208
+ countNums: 102,
209
+ max: 104,
210
+ min: 105,
211
+ stdDev: 107,
212
+ stdDevP: 108,
213
+ sum: 109,
214
+ var: 110,
215
+ varP: 111
216
+ };
217
+ const OOXML_TOTALS_FUNCTIONS = {
218
+ average: 'average',
219
+ count: 'count',
220
+ countNums: 'countNums',
221
+ max: 'max',
222
+ min: 'min',
223
+ stdDev: 'stdDev',
224
+ stdDevP: 'stdDevp',
225
+ sum: 'sum',
226
+ var: 'var',
227
+ varP: 'varp'
228
+ };
229
+ const TOTALS_FUNCTION_LABELS = {
230
+ sum: '求和',
231
+ average: '平均值',
232
+ count: '计数',
233
+ countNums: '数值计数',
234
+ max: '最大值',
235
+ min: '最小值',
236
+ stdDev: '标准差',
237
+ stdDevP: '总体标准差',
238
+ var: '方差',
239
+ varP: '总体方差',
240
+ custom: '自定义'
241
+ };
242
+ function spreadsheetTableTotalsFunctionLabel(value) {
243
+ return value ? TOTALS_FUNCTION_LABELS[value] : '不汇总';
244
+ }
245
+ function normalizeSpreadsheetTableTotalsFunction(value) {
246
+ if ('string' != typeof value) return;
247
+ if ('none' === value || '' === value) return;
248
+ return SPREADSHEET_TABLE_TOTALS_FUNCTIONS.includes(value) ? value : void 0;
249
+ }
250
+ function normalizeSpreadsheetTableTotalsFormula(value) {
251
+ if ('string' != typeof value) return;
252
+ const trimmed = value.trim();
253
+ if (!trimmed) return;
254
+ const formula = trimmed.startsWith('=') ? trimmed : `=${trimmed}`;
255
+ if (formula.startsWith('==') || formula.length > MAX_SPREADSHEET_TABLE_TOTALS_FORMULA_LENGTH || /[\u0000-\u001f\u007f]/u.test(formula) || formulaHasExternalReference(formula) || spreadsheetFormulaFunctions(formula).some((name)=>UNSAFE_TOTALS_FUNCTIONS.has(name))) return;
256
+ const editable = editableSpreadsheetFormula(formula).trim();
257
+ return editable || void 0;
258
+ }
259
+ function normalizeSpreadsheetTableTotalsLabel(value) {
260
+ if ('string' != typeof value) return;
261
+ const label = value.trim();
262
+ if (!label || Array.from(label).length > MAX_SPREADSHEET_TABLE_TOTALS_LABEL_LENGTH || /[\u0000-\u001f\u007f]/u.test(label)) return;
263
+ return label;
264
+ }
265
+ function spreadsheetTableTotalsFunctionFromOoxml(value) {
266
+ if ('string' != typeof value) return;
267
+ const normalized = value.trim().toLowerCase();
268
+ if ('custom' === normalized) return 'custom';
269
+ const entry = Object.entries(OOXML_TOTALS_FUNCTIONS).find(([, token])=>token.toLowerCase() === normalized);
270
+ return entry?.[0];
271
+ }
272
+ function spreadsheetTableTotalsFunctionToOoxml(value) {
273
+ if (!value || 'custom' === value) return 'custom' === value ? 'custom' : void 0;
274
+ return OOXML_TOTALS_FUNCTIONS[value];
275
+ }
276
+ function spreadsheetTableTotalsFormula(table, columnOffset) {
277
+ const column = table.columns[columnOffset];
278
+ if (!column) return;
279
+ const custom = normalizeSpreadsheetTableTotalsFormula(column.totalsFormula);
280
+ if (custom) return custom;
281
+ const functionName = normalizeSpreadsheetTableTotalsFunction(column.totalsFunction);
282
+ if (!functionName || 'custom' === functionName) return;
283
+ const code = SUBTOTAL_CODES[functionName];
284
+ if (!code) return;
285
+ return `=SUBTOTAL(${code},${table.name}[${escapeStructuredColumnName(column.name)}])`;
286
+ }
287
+ function spreadsheetTableTotalsColumnPatch(columns, patches) {
288
+ if (void 0 === patches) return columns.map((column)=>({
289
+ ...column
290
+ }));
291
+ const entries = Array.isArray(patches) ? patches.flatMap((patch, offset)=>patch ? [
292
+ [
293
+ String(offset),
294
+ patch
295
+ ]
296
+ ] : []) : Object.entries(patches);
297
+ const next = columns.map((column)=>({
298
+ ...column
299
+ }));
300
+ for (const [rawOffset, patch] of entries){
301
+ const offset = Number(rawOffset);
302
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset >= next.length || !patch || 'object' != typeof patch || Array.isArray(patch)) return null;
303
+ const current = next[offset];
304
+ if (!current) return null;
305
+ const candidate = {
306
+ ...current
307
+ };
308
+ if (Object.hasOwn(patch, 'totalsFunction')) if (null === patch.totalsFunction || '' === patch.totalsFunction) delete candidate.totalsFunction;
309
+ else {
310
+ const functionName = normalizeSpreadsheetTableTotalsFunction(patch.totalsFunction);
311
+ if (!functionName) return null;
312
+ candidate.totalsFunction = functionName;
313
+ }
314
+ if (Object.hasOwn(patch, 'totalsLabel')) if (null === patch.totalsLabel || '' === patch.totalsLabel) delete candidate.totalsLabel;
315
+ else {
316
+ const label = normalizeSpreadsheetTableTotalsLabel(patch.totalsLabel);
317
+ if (!label) return null;
318
+ candidate.totalsLabel = label;
319
+ }
320
+ if (Object.hasOwn(patch, 'totalsFormula')) if (null === patch.totalsFormula || '' === patch.totalsFormula) delete candidate.totalsFormula;
321
+ else {
322
+ const formula = normalizeSpreadsheetTableTotalsFormula(patch.totalsFormula);
323
+ if (!formula) return null;
324
+ candidate.totalsFormula = formula;
325
+ }
326
+ if (candidate.totalsFormula) {
327
+ if (void 0 !== candidate.totalsFunction && 'custom' !== candidate.totalsFunction) return null;
328
+ candidate.totalsFunction = 'custom';
329
+ } else if ('custom' === candidate.totalsFunction) return null;
330
+ if (void 0 !== candidate.totalsFunction && 'custom' !== candidate.totalsFunction) delete candidate.totalsFormula;
331
+ if (void 0 !== candidate.totalsFunction && candidate.totalsLabel) delete candidate.totalsLabel;
332
+ next[offset] = candidate;
333
+ }
334
+ return next;
335
+ }
336
+ function withDefaultSpreadsheetTableTotalsLabel(columns, totalsRow) {
337
+ const next = columns.map((column)=>({
338
+ ...column
339
+ }));
340
+ if (!totalsRow || !next.length) return next;
341
+ const hasExplicitLabel = next.some((column)=>column.totalsLabel);
342
+ if (hasExplicitLabel) return next;
343
+ const first = next[0];
344
+ if (first && !first.totalsFunction && !first.totalsFormula) first.totalsLabel = DEFAULT_SPREADSHEET_TABLE_TOTALS_LABEL;
345
+ return next;
346
+ }
347
+ function spreadsheetTableTotalsRowHasContent(sheet, table, row = table.totalsRow ? table.range.row[1] : table.range.row[1] + 1) {
348
+ for(let column = table.range.column[0]; column <= table.range.column[1]; column += 1)if (!spreadsheetTableTotalsCellIsEmpty(spreadsheetTableTotalsCellAt(sheet, row, column))) return true;
349
+ return false;
350
+ }
351
+ function synchronizeSpreadsheetTableTotalsRow(sheet, previousTable, nextTable, options = {}) {
352
+ if (!nextTable.totalsRow) return sheet;
353
+ const force = true === options.force;
354
+ const previousRow = previousTable?.range.row[1];
355
+ const nextRow = nextTable.range.row[1];
356
+ const width = nextTable.range.column[1] - nextTable.range.column[0] + 1;
357
+ const updates = new Map();
358
+ for(let offset = 0; offset < width; offset += 1){
359
+ const column = nextTable.range.column[0] + offset;
360
+ const nextColumn = nextTable.columns[offset];
361
+ if (!nextColumn) continue;
362
+ const previousColumn = previousTable?.columns[offset];
363
+ const oldFormula = previousTable ? spreadsheetTableTotalsFormula(previousTable, offset) : void 0;
364
+ const newFormula = spreadsheetTableTotalsFormula(nextTable, offset);
365
+ const oldLabel = normalizeSpreadsheetTableTotalsLabel(previousColumn?.totalsLabel);
366
+ const newLabel = normalizeSpreadsheetTableTotalsLabel(nextColumn.totalsLabel);
367
+ const changed = force || void 0 === previousTable || previousTable.name !== nextTable.name || previousTable.range.row[1] !== nextTable.range.row[1] || previousColumn?.totalsFunction !== nextColumn.totalsFunction || previousColumn?.totalsFormula !== nextColumn.totalsFormula || previousColumn?.totalsLabel !== nextColumn.totalsLabel || previousColumn?.name !== nextColumn.name;
368
+ const current = spreadsheetTableTotalsCellAt(sheet, nextRow, column);
369
+ const previousCell = void 0 === previousRow ? null : spreadsheetTableTotalsCellAt(sheet, previousRow, column);
370
+ const styleSource = current ?? previousCell;
371
+ const empty = spreadsheetTableTotalsCellIsEmpty(current);
372
+ const currentFormula = normalizeSpreadsheetTableTotalsFormula(current?.f);
373
+ const generatedFormula = Boolean(oldFormula && currentFormula && formulasEqual(currentFormula, oldFormula));
374
+ const generatedLabel = Boolean(oldLabel && !current?.f && spreadsheetTableCellText(current) === oldLabel);
375
+ const replaceable = force || empty || changed && (generatedFormula || generatedLabel);
376
+ if (newFormula && replaceable) updates.set(column, spreadsheetCellWithTotalsFormula(styleSource, newFormula));
377
+ else if (newLabel && replaceable) updates.set(column, spreadsheetCellWithTotalsLabel(styleSource, newLabel));
378
+ else if (!newFormula && !newLabel && changed && (generatedFormula || generatedLabel)) updates.set(column, clearSpreadsheetTableTotalsCell(current));
379
+ }
380
+ return applySpreadsheetTableTotalsCellUpdates(sheet, nextRow, updates);
381
+ }
382
+ function reconcileSpreadsheetTableTotalsColumns(sheet, table, editedOffsets) {
383
+ return table.columns.map((column, offset)=>{
384
+ const candidate = {
385
+ ...column
386
+ };
387
+ const declaredFunction = normalizeSpreadsheetTableTotalsFunction(candidate.totalsFunction);
388
+ const declaredFormula = normalizeSpreadsheetTableTotalsFormula(candidate.totalsFormula);
389
+ if (declaredFunction) candidate.totalsFunction = declaredFunction;
390
+ else delete candidate.totalsFunction;
391
+ if (declaredFormula) candidate.totalsFormula = declaredFormula;
392
+ else delete candidate.totalsFormula;
393
+ const label = normalizeSpreadsheetTableTotalsLabel(candidate.totalsLabel);
394
+ if (label) candidate.totalsLabel = label;
395
+ else delete candidate.totalsLabel;
396
+ if (!table.totalsRow) return candidate;
397
+ const cell = spreadsheetTableTotalsCellAt(sheet, table.range.row[1], table.range.column[0] + offset);
398
+ if (editedOffsets && !editedOffsets.has(offset)) return candidate;
399
+ const cellFormula = normalizeSpreadsheetTableTotalsFormula(cell?.f);
400
+ const expected = spreadsheetTableTotalsFormula({
401
+ ...table,
402
+ columns: [
403
+ {
404
+ ...candidate
405
+ }
406
+ ]
407
+ }, 0);
408
+ if (cellFormula) {
409
+ if (expected && formulasEqual(cellFormula, expected)) return candidate;
410
+ delete candidate.totalsFunction;
411
+ delete candidate.totalsFormula;
412
+ candidate.totalsFunction = 'custom';
413
+ candidate.totalsFormula = cellFormula;
414
+ return candidate;
415
+ }
416
+ delete candidate.totalsFunction;
417
+ delete candidate.totalsFormula;
418
+ const cellLabel = normalizeSpreadsheetTableTotalsLabel(spreadsheetTableCellText(cell));
419
+ if (cellLabel) if (candidate.totalsLabel && candidate.totalsLabel !== cellLabel) delete candidate.totalsLabel;
420
+ else candidate.totalsLabel = cellLabel;
421
+ else delete candidate.totalsLabel;
422
+ return candidate;
423
+ });
424
+ }
425
+ function escapeStructuredColumnName(value) {
426
+ return value.replaceAll(']', ']]');
427
+ }
428
+ function formulasEqual(left, right) {
429
+ return left.trim().replace(/\s+/g, '').toLocaleLowerCase() === right.trim().replace(/\s+/g, '').toLocaleLowerCase();
430
+ }
431
+ function spreadsheetTableTotalsCellAt(sheet, row, column) {
432
+ return sheet.data?.[row]?.[column] ?? sheet.celldata?.find((entry)=>entry.r === row && entry.c === column)?.v ?? null;
433
+ }
434
+ function spreadsheetTableTotalsCellIsEmpty(cell) {
435
+ return !cell?.f && (cell?.v === void 0 || null === cell.v || '' === cell.v) && (cell?.m === void 0 || null === cell.m || '' === cell.m);
436
+ }
437
+ function spreadsheetTableCellText(cell) {
438
+ const value = cell?.m ?? cell?.v;
439
+ return null == value ? '' : String(value);
440
+ }
441
+ function spreadsheetCellWithTotalsFormula(source, formula) {
442
+ const { f: _formula, m: _display, v: _value, ...presentation } = source ?? {};
443
+ return {
444
+ ...presentation,
445
+ f: formula
446
+ };
447
+ }
448
+ function spreadsheetCellWithTotalsLabel(source, label) {
449
+ const { f: _formula, ...withoutFormula } = source ?? {};
450
+ return {
451
+ ...withoutFormula,
452
+ m: label,
453
+ v: label
454
+ };
455
+ }
456
+ function clearSpreadsheetTableTotalsCell(source) {
457
+ if (!source) return null;
458
+ const { f: _formula, m: _display, v: _value, ...presentation } = source;
459
+ return Object.keys(presentation).length ? presentation : null;
460
+ }
461
+ function applySpreadsheetTableTotalsCellUpdates(sheet, rowIndex, updates) {
462
+ if (!updates.size) return sheet;
463
+ if (void 0 !== sheet.data) {
464
+ const data = sheet.data.slice();
465
+ const source = data[rowIndex];
466
+ const row = source ? source.slice() : [];
467
+ for (const [column, cell] of updates)row[column] = cell;
468
+ data[rowIndex] = row;
469
+ return {
470
+ ...sheet,
471
+ data
472
+ };
473
+ }
474
+ const entries = [
475
+ ...sheet.celldata ?? []
476
+ ];
477
+ const indexes = new Map(entries.map((entry, index)=>[
478
+ `${entry.r}:${entry.c}`,
479
+ index
480
+ ]));
481
+ for (const [column, cell] of updates){
482
+ const key = `${rowIndex}:${column}`;
483
+ const index = indexes.get(key);
484
+ if (null === cell) {
485
+ if (void 0 !== index) entries.splice(index, 1);
486
+ if (void 0 !== index) {
487
+ indexes.clear();
488
+ entries.forEach((entry, nextIndex)=>{
489
+ indexes.set(`${entry.r}:${entry.c}`, nextIndex);
490
+ });
491
+ }
492
+ continue;
493
+ }
494
+ const entry = {
495
+ r: rowIndex,
496
+ c: column,
497
+ v: cell
498
+ };
499
+ if (void 0 === index) {
500
+ indexes.set(key, entries.length);
501
+ entries.push(entry);
502
+ } else entries[index] = entry;
503
+ }
504
+ entries.sort((left, right)=>left.r - right.r || left.c - right.c);
505
+ return {
506
+ ...sheet,
507
+ celldata: entries
508
+ };
509
+ }
176
510
  function requiredCoordinate(value, maximum, label, sheetId) {
177
511
  if (!Number.isSafeInteger(value) || value < 0 || value >= maximum) invalidWorkOfficeSpreadsheetInput(`a valid ${label} coordinate in sheet '${sheetId}'`);
178
512
  return value;
@@ -203,7 +537,6 @@ function invalidWorkOfficeSpreadsheetInput(expected) {
203
537
  const MAX_POPULATED_CELLS = 1000000;
204
538
  const MAX_DENSE_MATRIX_CELLS = 1000000;
205
539
  const MAX_FILTER_VALUES_PER_COLUMN = 10000;
206
- const MAX_FILTER_VALUE_CHARACTERS = 32767;
207
540
  const MAX_FILTER_TEXT_BYTES = 1048576;
208
541
  const FILTER_TEXT_ENCODER = new TextEncoder();
209
542
  const SPREADSHEET_DYNAMIC_FILTERS = new Set([
@@ -428,20 +761,46 @@ function requiredSpreadsheetTableColumns(value, width, label) {
428
761
  const record = requiredInputRecord(candidate, `${label} column`);
429
762
  assertOptionalRecordKeys(record, [
430
763
  'name',
431
- 'calculatedFormula'
764
+ 'calculatedFormula',
765
+ 'totalsFunction',
766
+ 'totalsLabel',
767
+ 'totalsFormula'
432
768
  ], `column for ${label}`);
433
769
  const name = requiredTableColumnName(record.name, label);
434
770
  const normalized = name.toLocaleLowerCase();
435
771
  if (names.has(normalized)) invalidWorkOfficeSpreadsheetInput(`unique column names for ${label}`);
436
772
  names.add(normalized);
437
- if (void 0 === record.calculatedFormula) return {
773
+ let calculatedFormula;
774
+ if (void 0 !== record.calculatedFormula) {
775
+ calculatedFormula = normalizeSpreadsheetTableCalculatedFormula(record.calculatedFormula);
776
+ if (!calculatedFormula || calculatedFormula !== record.calculatedFormula) invalidWorkOfficeSpreadsheetInput(`a bounded structured calculated-column formula for ${label}`);
777
+ }
778
+ const totalsFunction = void 0 === record.totalsFunction ? void 0 : normalizeSpreadsheetTableTotalsFunction(record.totalsFunction);
779
+ if (void 0 !== record.totalsFunction && (!totalsFunction || totalsFunction !== record.totalsFunction)) invalidWorkOfficeSpreadsheetInput(`a supported totals-row function for ${label}`);
780
+ const totalsLabel = void 0 === record.totalsLabel ? void 0 : normalizeSpreadsheetTableTotalsLabel(record.totalsLabel);
781
+ if (void 0 !== record.totalsLabel && (!totalsLabel || totalsLabel !== record.totalsLabel)) invalidWorkOfficeSpreadsheetInput(`a bounded totals-row label for ${label}`);
782
+ const totalsFormula = void 0 === record.totalsFormula ? void 0 : normalizeSpreadsheetTableTotalsFormula(record.totalsFormula);
783
+ if (void 0 !== record.totalsFormula && (!totalsFormula || totalsFormula !== record.totalsFormula)) invalidWorkOfficeSpreadsheetInput(`a bounded totals-row formula for ${label}`);
784
+ if (totalsFormula && 'custom' !== totalsFunction) invalidWorkOfficeSpreadsheetInput(`custom totals-row formulas to declare the custom function for ${label}`);
785
+ if ('custom' === totalsFunction && !totalsFormula) invalidWorkOfficeSpreadsheetInput(`custom totals-row functions to include a formula for ${label}`);
786
+ if ((totalsFunction || totalsFormula) && totalsLabel) invalidWorkOfficeSpreadsheetInput(`totals-row labels not to share a cell with a formula for ${label}`);
787
+ if (!calculatedFormula && !totalsFunction && !totalsLabel && !totalsFormula) return {
438
788
  name
439
789
  };
440
- const calculatedFormula = normalizeSpreadsheetTableCalculatedFormula(record.calculatedFormula);
441
- if (!calculatedFormula || calculatedFormula !== record.calculatedFormula) invalidWorkOfficeSpreadsheetInput(`a bounded structured calculated-column formula for ${label}`);
442
790
  return {
443
791
  name,
444
- calculatedFormula
792
+ ...calculatedFormula ? {
793
+ calculatedFormula
794
+ } : {},
795
+ ...totalsFunction ? {
796
+ totalsFunction
797
+ } : {},
798
+ ...totalsLabel ? {
799
+ totalsLabel
800
+ } : {},
801
+ ...totalsFormula ? {
802
+ totalsFormula
803
+ } : {}
445
804
  };
446
805
  });
447
806
  }
@@ -504,19 +863,16 @@ function requiredSpreadsheetTableFilterCriteria(value, label, textBudget) {
504
863
  case 'contains':
505
864
  case 'does-not-contain':
506
865
  case 'begins-with':
866
+ case 'does-not-begin-with':
507
867
  case 'ends-with':
868
+ case 'does-not-end-with':
869
+ case 'matches-wildcard':
870
+ case 'does-not-match-wildcard':
508
871
  case 'greater-than':
509
872
  case 'greater-than-or-equal':
510
873
  case 'less-than':
511
874
  case 'less-than-or-equal':
512
- assertExactRecordKeys(record, [
513
- 'type',
514
- 'value'
515
- ], `filter criteria for ${label}`);
516
- return {
517
- type,
518
- value: requiredSpreadsheetFilterText(record.value, label, textBudget)
519
- };
875
+ return requiredSpreadsheetCustomFilterCondition(record, label, textBudget);
520
876
  case 'between':
521
877
  case 'not-between':
522
878
  assertExactRecordKeys(record, [
@@ -537,6 +893,22 @@ function requiredSpreadsheetTableFilterCriteria(value, label, textBudget) {
537
893
  return {
538
894
  type
539
895
  };
896
+ case 'compound':
897
+ assertExactRecordKeys(record, [
898
+ 'type',
899
+ 'conjunction',
900
+ 'conditions'
901
+ ], `filter criteria for ${label}`);
902
+ if ('and' !== record.conjunction && 'or' !== record.conjunction) invalidWorkOfficeSpreadsheetInput(`an 'and' or 'or' conjunction for compound filter criteria for ${label}`);
903
+ if (!Array.isArray(record.conditions) || 2 !== record.conditions.length) invalidWorkOfficeSpreadsheetInput(`exactly two custom filter conditions for ${label}`);
904
+ return {
905
+ type,
906
+ conjunction: record.conjunction,
907
+ conditions: [
908
+ requiredSpreadsheetCustomFilterCondition(record.conditions[0], label, textBudget),
909
+ requiredSpreadsheetCustomFilterCondition(record.conditions[1], label, textBudget)
910
+ ]
911
+ };
540
912
  case 'top':
541
913
  case 'bottom':
542
914
  assertExactRecordKeys(record, [
@@ -571,15 +943,44 @@ function requiredSpreadsheetTableFilterCriteria(value, label, textBudget) {
571
943
  invalidWorkOfficeSpreadsheetInput(`supported filter criteria for ${label}`);
572
944
  }
573
945
  }
946
+ function requiredSpreadsheetCustomFilterCondition(value, label, textBudget) {
947
+ const record = validateJsonRecord(requiredInputRecord(value, `custom filter condition for ${label}`), `custom filter condition for ${label}`);
948
+ switch(record.type){
949
+ case 'equals':
950
+ case 'not-equals':
951
+ case 'contains':
952
+ case 'does-not-contain':
953
+ case 'begins-with':
954
+ case 'does-not-begin-with':
955
+ case 'ends-with':
956
+ case 'does-not-end-with':
957
+ case 'matches-wildcard':
958
+ case 'does-not-match-wildcard':
959
+ case 'greater-than':
960
+ case 'greater-than-or-equal':
961
+ case 'less-than':
962
+ case 'less-than-or-equal':
963
+ assertExactRecordKeys(record, [
964
+ 'type',
965
+ 'value'
966
+ ], `custom filter condition for ${label}`);
967
+ return {
968
+ type: record.type,
969
+ value: requiredSpreadsheetFilterText(record.value, label, textBudget)
970
+ };
971
+ default:
972
+ invalidWorkOfficeSpreadsheetInput(`supported custom filter condition for ${label}`);
973
+ }
974
+ }
574
975
  function requiredSpreadsheetFilterText(value, label, textBudget) {
575
- if ('string' != typeof value) invalidWorkOfficeSpreadsheetInput(`filter text containing 1 to ${MAX_FILTER_VALUE_CHARACTERS.toLocaleString()} characters for ${label}`);
976
+ if ('string' != typeof value) invalidWorkOfficeSpreadsheetInput(`filter text containing 1 to ${32767..toLocaleString()} characters for ${label}`);
576
977
  let characters = 0;
577
978
  for (const character of value){
578
979
  characters += 1;
579
980
  const codePoint = character.codePointAt(0);
580
981
  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}`);
581
982
  }
582
- if (characters < 1 || characters > MAX_FILTER_VALUE_CHARACTERS) invalidWorkOfficeSpreadsheetInput(`filter text containing 1 to ${MAX_FILTER_VALUE_CHARACTERS.toLocaleString()} characters for ${label}`);
983
+ if (characters < 1 || characters > 32767) invalidWorkOfficeSpreadsheetInput(`filter text containing 1 to ${32767..toLocaleString()} characters for ${label}`);
583
984
  textBudget.bytes += FILTER_TEXT_ENCODER.encode(value).byteLength;
584
985
  if (textBudget.bytes > MAX_FILTER_TEXT_BYTES) invalidWorkOfficeSpreadsheetInput(`filter text totaling at most ${MAX_FILTER_TEXT_BYTES.toLocaleString()} bytes for ${label}`);
585
986
  return value;
@@ -1800,7 +2201,867 @@ function assertInitializedSpreadsheetSession(session) {
1800
2201
  if (metadata?.initialized) return;
1801
2202
  throw new WorkOfficeCollaborationError('office.collaboration.not_initialized', 'The Spreadsheet collaboration session has not been initialized.');
1802
2203
  }
2204
+ const WORK_SPREADSHEET_FILTER_TEXT_MAX_CHARACTERS = 32767;
2205
+ function workSpreadsheetFilterTextCharacters(value) {
2206
+ let characters = 0;
2207
+ for (const _character of value)characters += 1;
2208
+ return characters;
2209
+ }
2210
+ function workSpreadsheetFilterTextIsBounded(value) {
2211
+ const characters = workSpreadsheetFilterTextCharacters(value);
2212
+ return characters >= 1 && characters <= WORK_SPREADSHEET_FILTER_TEXT_MAX_CHARACTERS;
2213
+ }
1803
2214
  const MILLISECONDS_PER_DAY = 86400000;
2215
+ const EXCEL_1900_EPOCH_UTC = Date.UTC(1899, 11, 30);
2216
+ const EXCEL_1900_CIVIL_EPOCH_UTC = Date.UTC(1899, 11, 31);
2217
+ const EXCEL_1900_LOCAL_BASE = new Date(1899, 11, 30);
2218
+ const SERIAL_INTEGER_EPSILON = 1e-7;
2219
+ const CALENDAR_MONTH_FILTERS = [
2220
+ 'month-1',
2221
+ 'month-2',
2222
+ 'month-3',
2223
+ 'month-4',
2224
+ 'month-5',
2225
+ 'month-6',
2226
+ 'month-7',
2227
+ 'month-8',
2228
+ 'month-9',
2229
+ 'month-10',
2230
+ 'month-11',
2231
+ 'month-12'
2232
+ ];
2233
+ const CALENDAR_QUARTER_FILTERS = [
2234
+ 'quarter-1',
2235
+ 'quarter-2',
2236
+ 'quarter-3',
2237
+ 'quarter-4'
2238
+ ];
2239
+ function workSpreadsheetDynamicFilterMatcher(kind, cells, context) {
2240
+ if ('above-average' === kind || 'below-average' === kind) return averageMatcher(kind, cells);
2241
+ const month = CALENDAR_MONTH_FILTERS.indexOf(kind);
2242
+ if (month >= 0) return (cell)=>spreadsheetDateParts(cell)?.month === month;
2243
+ const quarter = CALENDAR_QUARTER_FILTERS.indexOf(kind);
2244
+ if (quarter >= 0) return (cell)=>spreadsheetDateParts(cell)?.quarter === quarter;
2245
+ const bounds = relativeDateBounds(kind, context.now);
2246
+ if (!bounds) return null;
2247
+ return (cell)=>{
2248
+ const day = spreadsheetDateSerialDay(cell);
2249
+ return null !== day && day >= bounds.start && day < bounds.end;
2250
+ };
2251
+ }
2252
+ function workSpreadsheetCellIsDate(cell) {
2253
+ return null !== spreadsheetDateSerialDay(cell);
2254
+ }
2255
+ function averageMatcher(kind, cells) {
2256
+ const average = finiteArithmeticMean(cells);
2257
+ if (null === average) return ()=>false;
2258
+ return 'above-average' === kind ? (cell)=>{
2259
+ const value = spreadsheetNumericCellValue(cell);
2260
+ return null !== value && value > average;
2261
+ } : (cell)=>{
2262
+ const value = spreadsheetNumericCellValue(cell);
2263
+ return null !== value && value < average;
2264
+ };
2265
+ }
2266
+ function finiteArithmeticMean(cells) {
2267
+ let count = 0;
2268
+ let scale = 0;
2269
+ let scaledTotal = 0;
2270
+ for (const cell of cells){
2271
+ const value = spreadsheetNumericCellValue(cell);
2272
+ if (null === value) continue;
2273
+ count += 1;
2274
+ const magnitude = Math.abs(value);
2275
+ if (magnitude > scale) {
2276
+ scaledTotal = scale / magnitude * scaledTotal + value / magnitude;
2277
+ scale = magnitude;
2278
+ } else if (scale > 0) scaledTotal += value / scale;
2279
+ }
2280
+ return count ? scaledTotal / count * scale : null;
2281
+ }
2282
+ function relativeDateBounds(kind, now) {
2283
+ const timestamp = now.getTime();
2284
+ const year = now.getFullYear();
2285
+ if (!Number.isFinite(timestamp) || !Number.isSafeInteger(year) || year < 1900 || year > 9999) return null;
2286
+ const month = now.getMonth();
2287
+ const today = excelSerialFromCivilDate(year, month, now.getDate());
2288
+ if ('yesterday' === kind) return dayBounds(today - 1);
2289
+ if ('today' === kind) return dayBounds(today);
2290
+ if ('tomorrow' === kind) return dayBounds(today + 1);
2291
+ const weekStart = today - now.getDay();
2292
+ if ('last-week' === kind) return rangeBounds(weekStart - 7, weekStart);
2293
+ if ('this-week' === kind) return rangeBounds(weekStart, weekStart + 7);
2294
+ if ('next-week' === kind) return rangeBounds(weekStart + 7, weekStart + 14);
2295
+ const monthStart = excelSerialFromCivilDate(year, month, 1);
2296
+ if ('last-month' === kind) return rangeBounds(excelSerialFromCivilDate(year, month - 1, 1), monthStart);
2297
+ if ('this-month' === kind) return rangeBounds(monthStart, excelSerialFromCivilDate(year, month + 1, 1));
2298
+ if ('next-month' === kind) return rangeBounds(excelSerialFromCivilDate(year, month + 1, 1), excelSerialFromCivilDate(year, month + 2, 1));
2299
+ const quarterStartMonth = 3 * Math.floor(month / 3);
2300
+ const quarterStart = excelSerialFromCivilDate(year, quarterStartMonth, 1);
2301
+ if ('last-quarter' === kind) return rangeBounds(excelSerialFromCivilDate(year, quarterStartMonth - 3, 1), quarterStart);
2302
+ if ('this-quarter' === kind) return rangeBounds(quarterStart, excelSerialFromCivilDate(year, quarterStartMonth + 3, 1));
2303
+ if ('next-quarter' === kind) return rangeBounds(excelSerialFromCivilDate(year, quarterStartMonth + 3, 1), excelSerialFromCivilDate(year, quarterStartMonth + 6, 1));
2304
+ const yearStart = excelSerialFromCivilDate(year, 0, 1);
2305
+ if ('last-year' === kind) return rangeBounds(excelSerialFromCivilDate(year - 1, 0, 1), yearStart);
2306
+ if ('this-year' === kind) return rangeBounds(yearStart, excelSerialFromCivilDate(year + 1, 0, 1));
2307
+ if ('next-year' === kind) return rangeBounds(excelSerialFromCivilDate(year + 1, 0, 1), excelSerialFromCivilDate(year + 2, 0, 1));
2308
+ if ('year-to-date' === kind) return rangeBounds(yearStart, today + 1);
2309
+ return null;
2310
+ }
2311
+ function spreadsheetDateParts(cell) {
2312
+ const day = spreadsheetDateSerialDay(cell);
2313
+ if (null === day) return null;
2314
+ const adjustedDay = day > 60 ? day - 1 : day;
2315
+ const date = new Date(EXCEL_1900_CIVIL_EPOCH_UTC + adjustedDay * MILLISECONDS_PER_DAY);
2316
+ const timestamp = date.getTime();
2317
+ const year = date.getUTCFullYear();
2318
+ if (!Number.isFinite(timestamp) || year < 1900 || year > 9999) return null;
2319
+ const month = date.getUTCMonth();
2320
+ return {
2321
+ month,
2322
+ quarter: Math.floor(month / 3)
2323
+ };
2324
+ }
2325
+ function spreadsheetDateSerialDay(cell) {
2326
+ const serial = spreadsheetDateCellSerial(cell);
2327
+ if (null === serial) return null;
2328
+ const rounded = Math.round(serial);
2329
+ const normalized = Math.abs(serial - rounded) <= SERIAL_INTEGER_EPSILON ? rounded : serial;
2330
+ const day = Math.floor(normalized);
2331
+ return day >= 1 && 60 !== day ? day : null;
2332
+ }
2333
+ function spreadsheetDateCellSerial(cell) {
2334
+ const value = spreadsheetCellRawValue(cell);
2335
+ if (value instanceof Date) {
2336
+ if (!Number.isFinite(value.getTime())) return null;
2337
+ const threshold = EXCEL_1900_LOCAL_BASE.getTime() + (value.getTimezoneOffset() - EXCEL_1900_LOCAL_BASE.getTimezoneOffset()) * 60000;
2338
+ const serial = (value.getTime() - threshold) / MILLISECONDS_PER_DAY;
2339
+ return Number.isFinite(serial) ? serial : null;
2340
+ }
2341
+ return 'number' == typeof value && Number.isFinite(value) && cell?.ct?.t === 'd' ? value : null;
2342
+ }
2343
+ function spreadsheetNumericCellValue(cell) {
2344
+ if (null !== spreadsheetDateCellSerial(cell)) return null;
2345
+ const value = spreadsheetCellRawValue(cell);
2346
+ return 'number' == typeof value && Number.isFinite(value) ? value : null;
2347
+ }
2348
+ function spreadsheetCellRawValue(cell) {
2349
+ const candidate = cell;
2350
+ return candidate?.v ?? candidate?.m;
2351
+ }
2352
+ function excelSerialFromCivilDate(year, month, day) {
2353
+ return (Date.UTC(year, month, day) - EXCEL_1900_EPOCH_UTC) / MILLISECONDS_PER_DAY;
2354
+ }
2355
+ function dayBounds(day) {
2356
+ return {
2357
+ start: day,
2358
+ end: day + 1
2359
+ };
2360
+ }
2361
+ function rangeBounds(start, end) {
2362
+ return {
2363
+ start,
2364
+ end
2365
+ };
2366
+ }
2367
+ const WILDCARD_ESCAPES = new Set([
2368
+ '*',
2369
+ '?',
2370
+ '~'
2371
+ ]);
2372
+ const BITSET_MINIMUM_SEARCH_WORK = 1000000;
2373
+ const BITSET_MINIMUM_TOKENS = 256;
2374
+ const BITSET_MAXIMUM_LITERAL_VALUES = 256;
2375
+ function workSpreadsheetWildcardMatcher(source) {
2376
+ const tokens = wildcardTokens(source);
2377
+ const literalMatcher = literalWildcardMatcher(tokens);
2378
+ if (literalMatcher) return (value)=>literalMatcher(comparableWildcardText(value));
2379
+ const fixedWidthMatcher = fixedWidthWildcardMatcher(tokens);
2380
+ return (value)=>fixedWidthMatcher(comparableWildcardText(value));
2381
+ }
2382
+ function workSpreadsheetHasUnescapedWildcard(source) {
2383
+ const characters = [
2384
+ ...source
2385
+ ];
2386
+ for(let index = 0; index < characters.length; index += 1){
2387
+ const character = characters[index];
2388
+ if ('~' === character && WILDCARD_ESCAPES.has(characters[index + 1] ?? '')) {
2389
+ index += 1;
2390
+ continue;
2391
+ }
2392
+ if ('*' === character || '?' === character) return true;
2393
+ }
2394
+ return false;
2395
+ }
2396
+ function wildcardTokens(source) {
2397
+ const characters = [
2398
+ ...source
2399
+ ];
2400
+ const tokens = [];
2401
+ let literal = '';
2402
+ const flushLiteral = ()=>{
2403
+ if (!literal) return;
2404
+ for (const value of comparableWildcardText(literal))tokens.push({
2405
+ kind: 'literal',
2406
+ value
2407
+ });
2408
+ literal = '';
2409
+ };
2410
+ for(let index = 0; index < characters.length; index += 1){
2411
+ const character = characters[index] ?? '';
2412
+ const escaped = characters[index + 1] ?? '';
2413
+ if ('~' === character && WILDCARD_ESCAPES.has(escaped)) {
2414
+ literal += escaped;
2415
+ index += 1;
2416
+ continue;
2417
+ }
2418
+ if ('*' === character) {
2419
+ flushLiteral();
2420
+ if (tokens.at(-1)?.kind !== 'many') tokens.push({
2421
+ kind: 'many'
2422
+ });
2423
+ continue;
2424
+ }
2425
+ if ('?' === character) {
2426
+ flushLiteral();
2427
+ tokens.push({
2428
+ kind: 'single'
2429
+ });
2430
+ continue;
2431
+ }
2432
+ literal += character;
2433
+ }
2434
+ flushLiteral();
2435
+ return tokens;
2436
+ }
2437
+ function literalWildcardMatcher(tokens) {
2438
+ if (tokens.some((token)=>'single' === token.kind)) return null;
2439
+ const startsWithMany = tokens[0]?.kind === 'many';
2440
+ const endsWithMany = tokens.at(-1)?.kind === 'many';
2441
+ const segments = [];
2442
+ let segment = '';
2443
+ for (const token of tokens){
2444
+ if ('many' === token.kind) {
2445
+ if (segment) segments.push(segment);
2446
+ segment = '';
2447
+ continue;
2448
+ }
2449
+ if ('literal' !== token.kind) return null;
2450
+ segment += token.value;
2451
+ }
2452
+ if (segment) segments.push(segment);
2453
+ if (!segments.length) return tokens.length ? ()=>true : (value)=>'' === value;
2454
+ if (!startsWithMany && !endsWithMany && 1 === segments.length) return (value)=>value === segments[0];
2455
+ return (value)=>{
2456
+ let minimumIndex = 0;
2457
+ let segmentIndex = 0;
2458
+ if (!startsWithMany) {
2459
+ const first = segments[0] ?? '';
2460
+ if (!value.startsWith(first)) return false;
2461
+ minimumIndex = first.length;
2462
+ segmentIndex = 1;
2463
+ }
2464
+ const searchableSegments = endsWithMany ? segments.length : segments.length - 1;
2465
+ for(; segmentIndex < searchableSegments; segmentIndex += 1){
2466
+ const current = segments[segmentIndex] ?? '';
2467
+ const found = value.indexOf(current, minimumIndex);
2468
+ if (found < 0) return false;
2469
+ minimumIndex = found + current.length;
2470
+ }
2471
+ if (endsWithMany) return true;
2472
+ const last = segments.at(-1) ?? '';
2473
+ return value.endsWith(last) && value.length - last.length >= minimumIndex;
2474
+ };
2475
+ }
2476
+ function fixedWidthWildcardMatcher(tokens) {
2477
+ const startsWithMany = tokens[0]?.kind === 'many';
2478
+ const endsWithMany = tokens.at(-1)?.kind === 'many';
2479
+ const segments = [];
2480
+ let current = [];
2481
+ for (const token of tokens){
2482
+ if ('many' === token.kind) {
2483
+ if (current.length) segments.push(fixedWidthWildcardSegment(current));
2484
+ current = [];
2485
+ continue;
2486
+ }
2487
+ current.push(token);
2488
+ }
2489
+ if (current.length) segments.push(fixedWidthWildcardSegment(current));
2490
+ if (!segments.length) return ()=>true;
2491
+ if (!startsWithMany && !endsWithMany && 1 === segments.length) {
2492
+ const only = segments[0];
2493
+ return (value)=>{
2494
+ const characters = [
2495
+ ...value
2496
+ ];
2497
+ return characters.length === only.tokens.length && fixedWidthWildcardSegmentMatches(only, characters, 0);
2498
+ };
2499
+ }
2500
+ return (value)=>{
2501
+ const characters = [
2502
+ ...value
2503
+ ];
2504
+ let minimumIndex = 0;
2505
+ let segmentIndex = 0;
2506
+ let maximumEnd = characters.length;
2507
+ let finalSegment = segments.length;
2508
+ if (!startsWithMany) {
2509
+ const first = segments[0];
2510
+ if (!fixedWidthWildcardSegmentMatches(first, characters, 0)) return false;
2511
+ minimumIndex = first.tokens.length;
2512
+ segmentIndex = 1;
2513
+ }
2514
+ if (!endsWithMany) {
2515
+ finalSegment -= 1;
2516
+ const last = segments[finalSegment];
2517
+ const start = characters.length - last.tokens.length;
2518
+ if (start < minimumIndex || !fixedWidthWildcardSegmentMatches(last, characters, start)) return false;
2519
+ maximumEnd = start;
2520
+ }
2521
+ if (segmentIndex >= finalSegment) return true;
2522
+ const offsets = wildcardCodePointOffsets(characters);
2523
+ for(; segmentIndex < finalSegment; segmentIndex += 1){
2524
+ const segment = segments[segmentIndex];
2525
+ const found = findFixedWidthWildcardSegment(segment, value, characters, offsets, minimumIndex, maximumEnd);
2526
+ if (found < 0) return false;
2527
+ minimumIndex = found + segment.tokens.length;
2528
+ }
2529
+ return true;
2530
+ };
2531
+ }
2532
+ function fixedWidthWildcardSegment(tokens) {
2533
+ let anchor = null;
2534
+ let literalStart = -1;
2535
+ const considerLiteralRun = (end)=>{
2536
+ if (literalStart < 0) return;
2537
+ const text = tokens.slice(literalStart, end).map((token)=>'literal' === token.kind ? token.value : '').join('');
2538
+ if (!anchor || end - literalStart >= [
2539
+ ...anchor.text
2540
+ ].length) anchor = {
2541
+ offset: literalStart,
2542
+ text
2543
+ };
2544
+ literalStart = -1;
2545
+ };
2546
+ for(let index = 0; index <= tokens.length; index += 1)if (tokens[index]?.kind === 'literal') {
2547
+ if (literalStart < 0) literalStart = index;
2548
+ } else considerLiteralRun(index);
2549
+ return {
2550
+ anchor,
2551
+ bitset: fixedWidthWildcardBitset(tokens),
2552
+ tokens
2553
+ };
2554
+ }
2555
+ function findFixedWidthWildcardSegment(segment, value, characters, offsets, minimumIndex, maximumEnd) {
2556
+ if (minimumIndex + segment.tokens.length > maximumEnd) return -1;
2557
+ if (!segment.anchor) return minimumIndex;
2558
+ if (segment.bitset && segment.tokens.length * (maximumEnd - minimumIndex) >= BITSET_MINIMUM_SEARCH_WORK) return findFixedWidthWildcardSegmentWithBitset(segment, characters, minimumIndex, maximumEnd);
2559
+ const minimumAnchorIndex = minimumIndex + segment.anchor.offset;
2560
+ let anchorOffset = value.indexOf(segment.anchor.text, offsets[minimumAnchorIndex] ?? value.length);
2561
+ while(anchorOffset >= 0){
2562
+ const anchorIndex = codePointIndexAtOffset(offsets, anchorOffset);
2563
+ if (anchorIndex >= 0) {
2564
+ const start = anchorIndex - segment.anchor.offset;
2565
+ if (start + segment.tokens.length > maximumEnd) return -1;
2566
+ if (start >= minimumIndex && fixedWidthWildcardSegmentMatches(segment, characters, start)) return start;
2567
+ }
2568
+ anchorOffset = value.indexOf(segment.anchor.text, anchorOffset + 1);
2569
+ }
2570
+ return -1;
2571
+ }
2572
+ function fixedWidthWildcardBitset(tokens) {
2573
+ if (tokens.length < BITSET_MINIMUM_TOKENS) return null;
2574
+ const literalValues = new Set(tokens.flatMap((token)=>'literal' === token.kind ? [
2575
+ token.value
2576
+ ] : []));
2577
+ if (literalValues.size > BITSET_MAXIMUM_LITERAL_VALUES) return null;
2578
+ const literalMasks = new Map();
2579
+ let singleMask = 0n;
2580
+ for(let index = 0; index < tokens.length; index += 1){
2581
+ const token = tokens[index];
2582
+ const bit = 1n << BigInt(index);
2583
+ if ('single' === token.kind) {
2584
+ singleMask |= bit;
2585
+ continue;
2586
+ }
2587
+ literalMasks.set(token.value, (literalMasks.get(token.value) ?? 0n) | bit);
2588
+ }
2589
+ return {
2590
+ literalMasks,
2591
+ matchBit: 1n << BigInt(tokens.length - 1),
2592
+ singleMask
2593
+ };
2594
+ }
2595
+ function findFixedWidthWildcardSegmentWithBitset(segment, characters, minimumIndex, maximumEnd) {
2596
+ const bitset = segment.bitset;
2597
+ if (!bitset) return -1;
2598
+ let state = 0n;
2599
+ for(let index = minimumIndex; index < maximumEnd; index += 1){
2600
+ const mask = bitset.singleMask | (bitset.literalMasks.get(characters[index]) ?? 0n);
2601
+ state = (state << 1n | 1n) & mask;
2602
+ if ((state & bitset.matchBit) !== 0n) return index - segment.tokens.length + 1;
2603
+ }
2604
+ return -1;
2605
+ }
2606
+ function fixedWidthWildcardSegmentMatches(segment, characters, start) {
2607
+ if (start < 0 || start + segment.tokens.length > characters.length) return false;
2608
+ return segment.tokens.every((token, index)=>'single' === token.kind || token.value === characters[start + index]);
2609
+ }
2610
+ function wildcardCodePointOffsets(characters) {
2611
+ const offsets = [
2612
+ 0
2613
+ ];
2614
+ for (const character of characters)offsets.push((offsets.at(-1) ?? 0) + character.length);
2615
+ return offsets;
2616
+ }
2617
+ function codePointIndexAtOffset(offsets, target) {
2618
+ let low = 0;
2619
+ let high = offsets.length - 1;
2620
+ while(low <= high){
2621
+ const middle = Math.floor((low + high) / 2);
2622
+ const value = offsets[middle] ?? -1;
2623
+ if (value === target) return middle;
2624
+ if (value < target) low = middle + 1;
2625
+ else high = middle - 1;
2626
+ }
2627
+ return -1;
2628
+ }
2629
+ function comparableWildcardText(value) {
2630
+ return value.normalize('NFKC').toLocaleLowerCase('zh-CN');
2631
+ }
2632
+ const AUTO_FILTER_CRITERIA_KIND = 'a3s-office-auto-filter-criteria';
2633
+ const AUTO_FILTER_CRITERIA_VERSION = 1;
2634
+ const SINGLE_VALUE_FILTER_TYPES = new Set([
2635
+ 'equals',
2636
+ 'not-equals',
2637
+ 'contains',
2638
+ 'does-not-contain',
2639
+ 'begins-with',
2640
+ 'does-not-begin-with',
2641
+ 'ends-with',
2642
+ 'does-not-end-with',
2643
+ 'matches-wildcard',
2644
+ 'does-not-match-wildcard',
2645
+ 'greater-than',
2646
+ 'greater-than-or-equal',
2647
+ 'less-than',
2648
+ 'less-than-or-equal'
2649
+ ]);
2650
+ const DYNAMIC_FILTER_KINDS = new Set([
2651
+ 'above-average',
2652
+ 'below-average',
2653
+ 'tomorrow',
2654
+ 'today',
2655
+ 'yesterday',
2656
+ 'next-week',
2657
+ 'this-week',
2658
+ 'last-week',
2659
+ 'next-month',
2660
+ 'this-month',
2661
+ 'last-month',
2662
+ 'next-quarter',
2663
+ 'this-quarter',
2664
+ 'last-quarter',
2665
+ 'next-year',
2666
+ 'this-year',
2667
+ 'last-year',
2668
+ 'year-to-date',
2669
+ 'quarter-1',
2670
+ 'quarter-2',
2671
+ 'quarter-3',
2672
+ 'quarter-4',
2673
+ 'month-1',
2674
+ 'month-2',
2675
+ 'month-3',
2676
+ 'month-4',
2677
+ 'month-5',
2678
+ 'month-6',
2679
+ 'month-7',
2680
+ 'month-8',
2681
+ 'month-9',
2682
+ 'month-10',
2683
+ 'month-11',
2684
+ 'month-12'
2685
+ ]);
2686
+ function normalizedWorkSpreadsheetAutoFilterRange(selection) {
2687
+ const rowStart = finiteIndex(selection?.row?.[0]);
2688
+ const rowEnd = finiteIndex(selection?.row?.[1]);
2689
+ const columnStart = finiteIndex(selection?.column?.[0]);
2690
+ const columnEnd = finiteIndex(selection?.column?.[1]);
2691
+ if (null === rowStart || null === rowEnd || null === columnStart || null === columnEnd) return null;
2692
+ return {
2693
+ row: [
2694
+ Math.min(rowStart, rowEnd),
2695
+ Math.max(rowStart, rowEnd)
2696
+ ],
2697
+ column: [
2698
+ Math.min(columnStart, columnEnd),
2699
+ Math.max(columnStart, columnEnd)
2700
+ ]
2701
+ };
2702
+ }
2703
+ function workSpreadsheetAutoFilterCriteria(sheet, column) {
2704
+ const range = normalizedWorkSpreadsheetAutoFilterRange(sheet?.filter_select);
2705
+ if (!sheet || !range || !columnInRange(range, column)) return null;
2706
+ return criteriaFromEntry(sheet.filter?.[String(column - range.column[0])]);
2707
+ }
2708
+ function workSpreadsheetAutoFilterCriteriaEntries(sheet) {
2709
+ const range = normalizedWorkSpreadsheetAutoFilterRange(sheet.filter_select);
2710
+ if (!range) return [];
2711
+ const entries = [];
2712
+ for (const [key, value] of Object.entries(sheet.filter ?? {})){
2713
+ const column = Number(key);
2714
+ const criteria = criteriaFromEntry(value);
2715
+ if (criteria && Number.isSafeInteger(column) && !(column < 0) && !(column > range.column[1] - range.column[0])) entries.push({
2716
+ column,
2717
+ criteria
2718
+ });
2719
+ }
2720
+ return entries.sort((left, right)=>left.column - right.column);
2721
+ }
2722
+ function workSpreadsheetSheetWithAutoFilterCriteria(sheet, column, criteria, context = {
2723
+ now: new Date()
2724
+ }) {
2725
+ const normalizedCriteria = normalizeWorkSpreadsheetFilterCriteria(criteria);
2726
+ const range = normalizedWorkSpreadsheetAutoFilterRange(sheet.filter_select);
2727
+ if (!normalizedCriteria || !range || !columnInRange(range, column)) return null;
2728
+ const rowhidden = hiddenRowsForCriteria(sheet, range, column, normalizedCriteria, context);
2729
+ if (!rowhidden) return null;
2730
+ const manuallyHiddenRows = workSpreadsheetAutoFilterManuallyHiddenRows(sheet);
2731
+ const filter = {
2732
+ ...sheet.filter ?? {},
2733
+ [String(column - range.column[0])]: filterColumnState(range, column, normalizedCriteria, rowhidden)
2734
+ };
2735
+ return sheetWithFilterState(sheet, filter, manuallyHiddenRows);
2736
+ }
2737
+ function workSpreadsheetSheetWithoutAutoFilterCriteria(sheet, column) {
2738
+ const range = normalizedWorkSpreadsheetAutoFilterRange(sheet.filter_select);
2739
+ if (!range || !columnInRange(range, column)) return null;
2740
+ const key = String(column - range.column[0]);
2741
+ if (!Object.hasOwn(sheet.filter ?? {}, key)) return null;
2742
+ const filter = {
2743
+ ...sheet.filter ?? {}
2744
+ };
2745
+ delete filter[key];
2746
+ return sheetWithFilterState(sheet, filter, workSpreadsheetAutoFilterManuallyHiddenRows(sheet));
2747
+ }
2748
+ function workSpreadsheetAutoFilterManuallyHiddenRows(sheet) {
2749
+ const owned = filterHiddenRows(sheet.filter);
2750
+ const manuallyHidden = new Set(Object.keys(sheet.config?.rowhidden ?? {}).filter((row)=>!owned.has(row)));
2751
+ for (const entry of Object.values(sheet.filter ?? {})){
2752
+ const overlap = criteriaPayloadFromEntry(entry)?.manualHiddenOverlap;
2753
+ if (Array.isArray(overlap)) {
2754
+ for (const row of overlap)if (validHiddenRowKey(row)) manuallyHidden.add(row);
2755
+ }
2756
+ }
2757
+ return manuallyHidden;
2758
+ }
2759
+ function workSpreadsheetSheetWithImportedAutoFilterCriteria(sheet, entries, context = {
2760
+ now: new Date()
2761
+ }) {
2762
+ const range = normalizedWorkSpreadsheetAutoFilterRange(sheet.filter_select);
2763
+ if (!range || !entries.length) return sheet;
2764
+ const width = range.column[1] - range.column[0] + 1;
2765
+ const filter = {};
2766
+ const owned = new Set();
2767
+ for (const entry of entries){
2768
+ const criteria = normalizeWorkSpreadsheetFilterCriteria(entry.criteria);
2769
+ if (!criteria || !Number.isSafeInteger(entry.column) || entry.column < 0 || entry.column >= width) continue;
2770
+ const column = range.column[0] + entry.column;
2771
+ const rowhidden = hiddenRowsForCriteria(sheet, range, column, criteria, context) ?? {};
2772
+ for (const row of Object.keys(rowhidden))owned.add(row);
2773
+ filter[String(entry.column)] = filterColumnState(range, column, criteria, rowhidden);
2774
+ }
2775
+ if (!Object.keys(filter).length) return sheet;
2776
+ const manuallyHiddenRows = new Set(Object.keys(sheet.config?.rowhidden ?? {}).filter((row)=>!owned.has(row)));
2777
+ return sheetWithFilterState(sheet, filter, manuallyHiddenRows);
2778
+ }
2779
+ function normalizeWorkSpreadsheetFilterCriteria(criteria) {
2780
+ if (!criteria || 'object' != typeof criteria || Array.isArray(criteria)) return null;
2781
+ const value = criteria;
2782
+ const type = value.type;
2783
+ if ('blanks' === type || 'non-blanks' === type) return {
2784
+ type
2785
+ };
2786
+ if ('values' === type) {
2787
+ if (!Array.isArray(value.values) || 'boolean' != typeof value.includeBlanks) return null;
2788
+ const values = value.values.filter((item)=>'string' == typeof item);
2789
+ if (values.length !== value.values.length || !values.length && !value.includeBlanks) return null;
2790
+ return {
2791
+ type,
2792
+ values: [
2793
+ ...new Set(values)
2794
+ ],
2795
+ includeBlanks: value.includeBlanks
2796
+ };
2797
+ }
2798
+ if ('between' === type || 'not-between' === type) return 'string' == typeof value.lower && 'string' == typeof value.upper ? {
2799
+ type,
2800
+ lower: value.lower,
2801
+ upper: value.upper
2802
+ } : null;
2803
+ if ('compound' === type) {
2804
+ if ('and' !== value.conjunction && 'or' !== value.conjunction || !Array.isArray(value.conditions) || 2 !== value.conditions.length) return null;
2805
+ const first = normalizeWorkSpreadsheetCustomFilterCondition(value.conditions[0]);
2806
+ const second = normalizeWorkSpreadsheetCustomFilterCondition(value.conditions[1]);
2807
+ return first && second ? {
2808
+ type,
2809
+ conjunction: value.conjunction,
2810
+ conditions: [
2811
+ first,
2812
+ second
2813
+ ]
2814
+ } : null;
2815
+ }
2816
+ if ('top' === type || 'bottom' === type) return Number.isSafeInteger(value.count) && Number(value.count) >= 1 && Number(value.count) <= 500 ? {
2817
+ type,
2818
+ count: Number(value.count)
2819
+ } : null;
2820
+ if ('top-percent' === type || 'bottom-percent' === type) return Number.isSafeInteger(value.percent) && Number(value.percent) >= 1 && Number(value.percent) <= 100 ? {
2821
+ type,
2822
+ percent: Number(value.percent)
2823
+ } : null;
2824
+ if ('dynamic' === type) return 'string' == typeof value.kind && DYNAMIC_FILTER_KINDS.has(value.kind) ? {
2825
+ type,
2826
+ kind: value.kind
2827
+ } : null;
2828
+ return normalizeWorkSpreadsheetCustomFilterCondition(value);
2829
+ }
2830
+ function normalizeWorkSpreadsheetCustomFilterCondition(condition) {
2831
+ if (!condition || 'object' != typeof condition || Array.isArray(condition)) return null;
2832
+ const value = condition;
2833
+ if ('string' != typeof value.type || !SINGLE_VALUE_FILTER_TYPES.has(value.type) || 'string' != typeof value.value) return null;
2834
+ if (('matches-wildcard' === value.type || 'does-not-match-wildcard' === value.type) && !workSpreadsheetFilterTextIsBounded(value.value)) return null;
2835
+ return {
2836
+ type: value.type,
2837
+ value: value.value
2838
+ };
2839
+ }
2840
+ function sheetWithFilterState(sheet, filter, manuallyHiddenRows) {
2841
+ const owned = filterHiddenRows(filter);
2842
+ const overlap = [
2843
+ ...manuallyHiddenRows
2844
+ ].filter((row)=>owned.has(row)).sort(compareHiddenRowKeys);
2845
+ const rowhidden = {};
2846
+ for (const row of manuallyHiddenRows)rowhidden[row] = 0;
2847
+ for (const row of owned)rowhidden[row] = 0;
2848
+ return {
2849
+ ...sheet,
2850
+ filter: filterWithManualHiddenOverlap(filter, overlap),
2851
+ config: {
2852
+ ...sheet.config ?? {},
2853
+ rowhidden
2854
+ }
2855
+ };
2856
+ }
2857
+ function filterWithManualHiddenOverlap(filter, manualHiddenOverlap) {
2858
+ return Object.fromEntries(Object.entries(filter).map(([column, entry])=>{
2859
+ const payload = criteriaPayloadFromEntry(entry);
2860
+ if (!payload || !entry || 'object' != typeof entry) return [
2861
+ column,
2862
+ entry
2863
+ ];
2864
+ return [
2865
+ column,
2866
+ {
2867
+ ...entry,
2868
+ caljs: {
2869
+ ...payload,
2870
+ manualHiddenOverlap: [
2871
+ ...manualHiddenOverlap
2872
+ ]
2873
+ }
2874
+ }
2875
+ ];
2876
+ }));
2877
+ }
2878
+ function filterColumnState(range, column, criteria, rowhidden) {
2879
+ return {
2880
+ caljs: {
2881
+ criteria,
2882
+ kind: AUTO_FILTER_CRITERIA_KIND,
2883
+ version: AUTO_FILTER_CRITERIA_VERSION
2884
+ },
2885
+ cindex: column,
2886
+ edc: range.column[1],
2887
+ edr: range.row[1],
2888
+ optionstate: true,
2889
+ rowhidden,
2890
+ stc: range.column[0],
2891
+ str: range.row[0]
2892
+ };
2893
+ }
2894
+ function hiddenRowsForCriteria(sheet, range, column, criteria, context) {
2895
+ if ('top' === criteria.type || 'top-percent' === criteria.type || 'bottom' === criteria.type || 'bottom-percent' === criteria.type) return hiddenRowsForRankCriteria(sheet, range, column, criteria);
2896
+ if ('dynamic' === criteria.type) return hiddenRowsForDynamicCriteria(sheet, range, column, criteria.kind, context);
2897
+ const matches = filterMatcher(criteria);
2898
+ if (!matches) return null;
2899
+ const cellAt = sheetCellReader(sheet);
2900
+ const hidden = {};
2901
+ for(let row = range.row[0] + 1; row <= range.row[1]; row += 1)if (!matches(cellAt(row, column))) hidden[String(row)] = 0;
2902
+ return hidden;
2903
+ }
2904
+ function hiddenRowsForDynamicCriteria(sheet, range, column, kind, context) {
2905
+ const cellAt = sheetCellReader(sheet);
2906
+ const cells = {
2907
+ *[Symbol.iterator] () {
2908
+ for(let row = range.row[0] + 1; row <= range.row[1]; row += 1)yield cellAt(row, column);
2909
+ }
2910
+ };
2911
+ const matches = workSpreadsheetDynamicFilterMatcher(kind, cells, context);
2912
+ if (!matches) return null;
2913
+ const hidden = {};
2914
+ for(let row = range.row[0] + 1; row <= range.row[1]; row += 1)if (!matches(cellAt(row, column))) hidden[String(row)] = 0;
2915
+ return hidden;
2916
+ }
2917
+ function hiddenRowsForRankCriteria(sheet, range, column, criteria) {
2918
+ const cellAt = sheetCellReader(sheet);
2919
+ const values = [];
2920
+ for(let row = range.row[0] + 1; row <= range.row[1]; row += 1){
2921
+ const value = numericCellValue(cellAt(row, column));
2922
+ if (null !== value) values.push(value);
2923
+ }
2924
+ const top = 'top' === criteria.type || 'top-percent' === criteria.type;
2925
+ values.sort((left, right)=>top ? right - left : left - right);
2926
+ const requested = 'top' === criteria.type || 'bottom' === criteria.type ? criteria.count : Math.ceil(values.length * criteria.percent / 100);
2927
+ const boundary = values[Math.min(requested, values.length) - 1];
2928
+ const hidden = {};
2929
+ for(let row = range.row[0] + 1; row <= range.row[1]; row += 1){
2930
+ const value = numericCellValue(cellAt(row, column));
2931
+ if (null === value || void 0 === boundary || (top ? value < boundary : value > boundary)) hidden[String(row)] = 0;
2932
+ }
2933
+ return hidden;
2934
+ }
2935
+ function filterMatcher(criteria) {
2936
+ if ('blanks' === criteria.type) return cellIsBlank;
2937
+ if ('non-blanks' === criteria.type) return (cell)=>!cellIsBlank(cell);
2938
+ if ('values' === criteria.type) {
2939
+ const values = new Set(criteria.values.map(comparableText));
2940
+ return (cell)=>{
2941
+ const value = cellValue(cell);
2942
+ if (null === value) return criteria.includeBlanks;
2943
+ return values.has(comparableText(String(value)));
2944
+ };
2945
+ }
2946
+ if ('between' === criteria.type || 'not-between' === criteria.type) return (cell)=>{
2947
+ const lower = compareCell(cell, criteria.lower);
2948
+ const upper = compareCell(cell, criteria.upper);
2949
+ const between = null !== lower && null !== upper && lower >= 0 && upper <= 0;
2950
+ return 'between' === criteria.type ? between : !between;
2951
+ };
2952
+ if ('compound' === criteria.type) {
2953
+ const first = customFilterMatcher(criteria.conditions[0]);
2954
+ const second = customFilterMatcher(criteria.conditions[1]);
2955
+ return 'and' === criteria.conjunction ? (cell)=>first(cell) && second(cell) : (cell)=>first(cell) || second(cell);
2956
+ }
2957
+ if ('top' === criteria.type || 'top-percent' === criteria.type || 'bottom' === criteria.type || 'bottom-percent' === criteria.type || 'dynamic' === criteria.type) return null;
2958
+ return customFilterMatcher(criteria);
2959
+ }
2960
+ function customFilterMatcher(criteria) {
2961
+ if ('matches-wildcard' === criteria.type || 'does-not-match-wildcard' === criteria.type) {
2962
+ const matchesWildcard = workSpreadsheetWildcardMatcher(criteria.value);
2963
+ const matches = (cell)=>{
2964
+ const value = cellValue(cell);
2965
+ return null !== value && matchesWildcard(String(value));
2966
+ };
2967
+ return 'matches-wildcard' === criteria.type ? matches : (cell)=>!matches(cell);
2968
+ }
2969
+ const expected = criteria.value;
2970
+ if ('contains' === criteria.type) return (cell)=>cellText(cell).includes(comparableText(expected));
2971
+ if ('does-not-contain' === criteria.type) return (cell)=>!cellText(cell).includes(comparableText(expected));
2972
+ if ('begins-with' === criteria.type) return (cell)=>cellText(cell).startsWith(comparableText(expected));
2973
+ if ('does-not-begin-with' === criteria.type) return (cell)=>!cellText(cell).startsWith(comparableText(expected));
2974
+ if ('ends-with' === criteria.type) return (cell)=>cellText(cell).endsWith(comparableText(expected));
2975
+ if ('does-not-end-with' === criteria.type) return (cell)=>!cellText(cell).endsWith(comparableText(expected));
2976
+ return (cell)=>{
2977
+ const order = compareCell(cell, expected);
2978
+ if (null === order) return false;
2979
+ if ('equals' === criteria.type) return 0 === order;
2980
+ if ('not-equals' === criteria.type) return 0 !== order;
2981
+ if ('greater-than' === criteria.type) return order > 0;
2982
+ if ('greater-than-or-equal' === criteria.type) return order >= 0;
2983
+ if ('less-than' === criteria.type) return order < 0;
2984
+ return order <= 0;
2985
+ };
2986
+ }
2987
+ function compareCell(cell, expected) {
2988
+ const value = cellValue(cell);
2989
+ if (null === value) return null;
2990
+ const numericExpected = finiteNumber(expected);
2991
+ if ('number' == typeof value && null !== numericExpected) return value - numericExpected;
2992
+ return comparableText(String(value)).localeCompare(comparableText(expected), 'zh-CN', {
2993
+ numeric: true,
2994
+ sensitivity: 'base'
2995
+ });
2996
+ }
2997
+ function cellIsBlank(cell) {
2998
+ return null === cellValue(cell);
2999
+ }
3000
+ function cellText(cell) {
3001
+ const value = cellValue(cell);
3002
+ return null === value ? '' : comparableText(String(value));
3003
+ }
3004
+ function cellValue(cell) {
3005
+ const value = cell?.v ?? cell?.m;
3006
+ if ('number' == typeof value && Number.isFinite(value)) return value;
3007
+ if ('string' == typeof value) return '' === value ? null : value;
3008
+ if ('boolean' == typeof value) return value;
3009
+ return null;
3010
+ }
3011
+ function numericCellValue(cell) {
3012
+ const value = cellValue(cell);
3013
+ return 'number' == typeof value ? value : null;
3014
+ }
3015
+ function comparableText(value) {
3016
+ return value.normalize('NFKC').toLocaleLowerCase('zh-CN');
3017
+ }
3018
+ function finiteNumber(value) {
3019
+ if (!value.trim()) return null;
3020
+ const numeric = Number(value);
3021
+ return Number.isFinite(numeric) ? numeric : null;
3022
+ }
3023
+ function criteriaFromEntry(entry) {
3024
+ const payload = criteriaPayloadFromEntry(entry);
3025
+ return payload ? normalizeWorkSpreadsheetFilterCriteria(payload.criteria) : null;
3026
+ }
3027
+ function criteriaPayloadFromEntry(entry) {
3028
+ if (!entry || 'object' != typeof entry) return null;
3029
+ const caljs = entry.caljs;
3030
+ if (!caljs || 'object' != typeof caljs) return null;
3031
+ const payload = caljs;
3032
+ if (payload.kind !== AUTO_FILTER_CRITERIA_KIND || payload.version !== AUTO_FILTER_CRITERIA_VERSION) return null;
3033
+ return normalizeWorkSpreadsheetFilterCriteria(payload.criteria) ? payload : null;
3034
+ }
3035
+ function filterHiddenRows(filter) {
3036
+ const rows = new Set();
3037
+ for (const value of Object.values(filter ?? {})){
3038
+ if (!value || 'object' != typeof value) continue;
3039
+ const rowhidden = value.rowhidden;
3040
+ if (rowhidden && 'object' == typeof rowhidden) for (const row of Object.keys(rowhidden))rows.add(row);
3041
+ }
3042
+ return rows;
3043
+ }
3044
+ function sheetCellReader(sheet) {
3045
+ if (sheet.data) return (row, column)=>sheet.data?.[row]?.[column] ?? null;
3046
+ const cells = new Map((sheet.celldata ?? []).map((entry)=>[
3047
+ `${entry.r}:${entry.c}`,
3048
+ entry.v ?? null
3049
+ ]));
3050
+ return (row, column)=>cells.get(`${row}:${column}`) ?? null;
3051
+ }
3052
+ function columnInRange(range, column) {
3053
+ return Number.isSafeInteger(column) && column >= range.column[0] && column <= range.column[1];
3054
+ }
3055
+ function finiteIndex(value) {
3056
+ return 'number' == typeof value && Number.isSafeInteger(value) && value >= 0 ? value : null;
3057
+ }
3058
+ function validHiddenRowKey(value) {
3059
+ return 'string' == typeof value && /^(?:0|[1-9]\d*)$/.test(value) && Number.isSafeInteger(Number(value));
3060
+ }
3061
+ function compareHiddenRowKeys(left, right) {
3062
+ return Number(left) - Number(right);
3063
+ }
3064
+ const work_spreadsheet_data_validation_MILLISECONDS_PER_DAY = 86400000;
1804
3065
  const SPREADSHEET_DATA_VALIDATION_ERROR_STYLES = [
1805
3066
  'stop',
1806
3067
  'warning',
@@ -1824,7 +3085,7 @@ function normalizeSpreadsheetDateValidationBoundary(value, uses1904DateSystem =
1824
3085
  if (!uses1904DateSystem && (0 === serial || 60 === serial)) return null;
1825
3086
  const adjustedSerial = uses1904DateSystem ? serial : serial > 60 ? serial - 1 : serial;
1826
3087
  const epoch = uses1904DateSystem ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 31);
1827
- return isoDateFromTimestamp(epoch + adjustedSerial * MILLISECONDS_PER_DAY);
3088
+ return isoDateFromTimestamp(epoch + adjustedSerial * work_spreadsheet_data_validation_MILLISECONDS_PER_DAY);
1828
3089
  }
1829
3090
  function spreadsheetDateValidationFormula(value) {
1830
3091
  const normalized = normalizeIsoDate(value.trim());
@@ -2478,7 +3739,7 @@ function readRichTextRun(element, value, colors) {
2478
3739
  if (work_xlsx_rich_text_xlsxToggleEnabled(directChild(properties, 'b'))) run.bl = 1;
2479
3740
  if (work_xlsx_rich_text_xlsxToggleEnabled(directChild(properties, 'i'))) run.it = 1;
2480
3741
  if (work_xlsx_rich_text_xlsxToggleEnabled(directChild(properties, 'strike'))) run.cl = 1;
2481
- const size = finiteNumber(attribute(directChild(properties, 'sz') ?? properties, 'val'));
3742
+ const size = work_xlsx_rich_text_finiteNumber(attribute(directChild(properties, 'sz') ?? properties, 'val'));
2482
3743
  if (null !== size && size >= 1 && size <= 409) run.fs = size;
2483
3744
  const underline = directChild(properties, 'u');
2484
3745
  if (underline) {
@@ -2550,7 +3811,7 @@ function work_xlsx_rich_text_xlsxToggleEnabled(element) {
2550
3811
  const value = attribute(element, 'val')?.trim().toLowerCase();
2551
3812
  return '0' !== value && 'false' !== value && 'off' !== value;
2552
3813
  }
2553
- function finiteNumber(value) {
3814
+ function work_xlsx_rich_text_finiteNumber(value) {
2554
3815
  if (null === value || !value.trim()) return null;
2555
3816
  const parsed = Number(value);
2556
3817
  return Number.isFinite(parsed) ? parsed : null;
@@ -2563,4 +3824,227 @@ function work_xlsx_rich_text_nonNegativeInteger(value) {
2563
3824
  function work_xlsx_rich_text_isRecord(value) {
2564
3825
  return 'object' == typeof value && null !== value && !Array.isArray(value);
2565
3826
  }
2566
- export { activeXlsxNativeFill, activeXlsxSemanticColorOrigin, applyImportedXlsxRichText, boundedSpreadsheetDataValidationText, coalesceXlsxRichTextRuns, createWorkOfficeSpreadsheetCollaborationBinding as createOfficeSpreadsheetCollaborationBinding, createXlsxRichTextReadContext, deleteXlsxNativeFills, directXlsxAlignment, directXlsxFontStyle, fillSpreadsheetTableCalculatedColumns, hasXlsxDirectFontStyle, initializeWorkOfficeSpreadsheetCollaboration as initializeOfficeSpreadsheetCollaboration, isHighSurrogate, isLowSurrogate, isSpreadsheetTextOrientationId, isSpreadsheetUnderlineStyle, normalizeSpreadsheetDataValidationErrorStyle, normalizeSpreadsheetDateValidationBoundary, normalizeSpreadsheetTableCalculatedFormula, normalizeXlsxRichTextCell, normalizeXlsxRichTextColor, normalizeXlsxRichTextEditSource, normalizeXlsxRichTextRun, patchSpreadsheetRichTextFontRuns, readWorkOfficeSpreadsheetCollaboration as readOfficeSpreadsheetCollaboration, readXlsxRichTextCells, reconcileSpreadsheetTableCalculatedColumns, replaceWorkOfficeSpreadsheetCollaboration as replaceOfficeSpreadsheetCollaboration, sameXlsxRichTextRunStyle, sheetHasXlsxRichTextCells, spreadsheetCellValueWithDiagonalBorder, spreadsheetDateValidationFormula, spreadsheetDiagonalBorderFromCellValue, spreadsheetExplicitTextOrientationFromCell, spreadsheetTextOrientationCellStyle, spreadsheetTextOrientationChoiceFromCell, spreadsheetTextOrientationFromAngle, spreadsheetTextOrientationFromCell, spreadsheetTextOrientationFromChoice, spreadsheetTextOrientationFromXlsx, spreadsheetUnderlineCellValue, spreadsheetUnderlineCellValueFromSheetJs, spreadsheetUnderlineCellValueFromXlsx, spreadsheetUnderlineStyle, spreadsheetVisibleTextRotationFromCell, validXlsxRichText, writeXlsxRichTextCells, xlsxAlignmentMatches, xlsxBooleanAttribute, xlsxBorderLineMatches, xlsxColorMatches, xlsxNativeFillCellKeys, xlsxNativeFillKey, xlsxNativeFillSemanticColors, xlsxRichTextCellText, xlsxRichTextStyleOrigins, xlsxStyleCollectionIndex, xlsxToggleEnabled, xlsxUnderlineStyle };
3827
+ const MAX_SPREADSHEET_SORT_CUSTOM_LIST_ENTRIES = 256;
3828
+ const MAX_SPREADSHEET_SORT_CUSTOM_LIST_ENTRY_CODE_POINTS = 128;
3829
+ const MAX_SPREADSHEET_SORT_CUSTOM_LIST_CODE_POINTS = 4096;
3830
+ const MAX_SPREADSHEET_SORT_USER_CUSTOM_LISTS = 32;
3831
+ function parseSpreadsheetSortCustomList(text) {
3832
+ return validateSpreadsheetSortCustomList(text.split(/\r?\n|,|,/u).map((entry)=>entry.trim()).filter(Boolean));
3833
+ }
3834
+ function validateSpreadsheetSortCustomList(input) {
3835
+ if (!Array.isArray(input)) return customListError('invalid-entry');
3836
+ if (input.length < 2) return customListError('not-enough-entries');
3837
+ if (input.length > MAX_SPREADSHEET_SORT_CUSTOM_LIST_ENTRIES) return customListError('too-many-entries');
3838
+ const entries = [];
3839
+ const seen = new Set();
3840
+ let totalCodePoints = 0;
3841
+ for (const candidate of input){
3842
+ if ('string' != typeof candidate) return customListError('invalid-entry');
3843
+ const entry = candidate.trim();
3844
+ if (!entry) return customListError('invalid-entry');
3845
+ const codePoints = Array.from(entry).length;
3846
+ if (codePoints > MAX_SPREADSHEET_SORT_CUSTOM_LIST_ENTRY_CODE_POINTS) return customListError('entry-too-long');
3847
+ totalCodePoints += codePoints;
3848
+ if (totalCodePoints > MAX_SPREADSHEET_SORT_CUSTOM_LIST_CODE_POINTS) return customListError('list-too-long');
3849
+ const matchKey = spreadsheetSortCustomListMatchKey(entry);
3850
+ if (seen.has(matchKey)) return customListError('duplicate-entry');
3851
+ seen.add(matchKey);
3852
+ entries.push(entry);
3853
+ }
3854
+ return {
3855
+ ok: true,
3856
+ entries
3857
+ };
3858
+ }
3859
+ function createSpreadsheetSortCustomList(entries, source = 'session') {
3860
+ const validation = validateSpreadsheetSortCustomList(entries);
3861
+ if (!validation.ok) return null;
3862
+ const normalized = Object.freeze([
3863
+ ...validation.entries
3864
+ ]);
3865
+ return Object.freeze({
3866
+ source,
3867
+ entries: normalized,
3868
+ label: spreadsheetSortCustomListLabel(normalized)
3869
+ });
3870
+ }
3871
+ function mergeSpreadsheetSortCustomLists(lists) {
3872
+ const merged = [
3873
+ ...SPREADSHEET_SORT_BUILT_IN_CUSTOM_LISTS
3874
+ ];
3875
+ let userListCount = 0;
3876
+ for (const candidate of lists){
3877
+ if ('stored' !== candidate.source && 'session' !== candidate.source) continue;
3878
+ const list = createSpreadsheetSortCustomList(candidate.entries, candidate.source);
3879
+ if (!(!list || merged.some((item)=>spreadsheetSortCustomListsEqual(item.entries, list.entries)))) {
3880
+ if (userListCount >= MAX_SPREADSHEET_SORT_USER_CUSTOM_LISTS) break;
3881
+ merged.push(list);
3882
+ userListCount += 1;
3883
+ }
3884
+ }
3885
+ return Object.freeze(merged);
3886
+ }
3887
+ function spreadsheetSortCustomListsEqual(left, right) {
3888
+ return left.length === right.length && left.every((entry, index)=>spreadsheetSortCustomListMatchKey(entry) === spreadsheetSortCustomListMatchKey(right[index] ?? ''));
3889
+ }
3890
+ function spreadsheetSortCustomListMatchKey(value) {
3891
+ return String(value).normalize('NFKC').trim().toLocaleLowerCase('zh-CN');
3892
+ }
3893
+ function spreadsheetSortCustomListLabel(entries) {
3894
+ const visible = entries.slice(0, 3).map((entry)=>truncateSpreadsheetSortCustomListLabelEntry(entry));
3895
+ return `${visible.join(' → ')}${entries.length > 3 ? ' → …' : ''}`;
3896
+ }
3897
+ function truncateSpreadsheetSortCustomListLabelEntry(entry) {
3898
+ const characters = Array.from(entry);
3899
+ return characters.length > 18 ? `${characters.slice(0, 17).join('')}…` : entry;
3900
+ }
3901
+ function customListError(code) {
3902
+ const messages = {
3903
+ 'duplicate-entry': '自定义序列不能包含重复项(忽略大小写和全半角差异)。',
3904
+ 'entry-too-long': `每个序列项最多包含 ${MAX_SPREADSHEET_SORT_CUSTOM_LIST_ENTRY_CODE_POINTS} 个字符。`,
3905
+ 'invalid-entry': '自定义序列只能包含非空文本项。',
3906
+ 'list-too-long': `一个自定义序列最多包含 ${MAX_SPREADSHEET_SORT_CUSTOM_LIST_CODE_POINTS.toLocaleString('en-US')} 个字符。`,
3907
+ 'not-enough-entries': '自定义序列至少需要两个项目。',
3908
+ 'too-many-entries': `一个自定义序列最多包含 ${MAX_SPREADSHEET_SORT_CUSTOM_LIST_ENTRIES} 个项目。`
3909
+ };
3910
+ return {
3911
+ ok: false,
3912
+ code,
3913
+ message: messages[code]
3914
+ };
3915
+ }
3916
+ function builtInCustomList(label, entries) {
3917
+ return Object.freeze({
3918
+ source: 'built-in',
3919
+ entries: Object.freeze([
3920
+ ...entries
3921
+ ]),
3922
+ label
3923
+ });
3924
+ }
3925
+ const SPREADSHEET_SORT_BUILT_IN_CUSTOM_LISTS = Object.freeze([
3926
+ builtInCustomList('月份(一月 → 十二月)', [
3927
+ '一月',
3928
+ '二月',
3929
+ '三月',
3930
+ '四月',
3931
+ '五月',
3932
+ '六月',
3933
+ '七月',
3934
+ '八月',
3935
+ '九月',
3936
+ '十月',
3937
+ '十一月',
3938
+ '十二月'
3939
+ ]),
3940
+ builtInCustomList('星期(星期日 → 星期六)', [
3941
+ '星期日',
3942
+ '星期一',
3943
+ '星期二',
3944
+ '星期三',
3945
+ '星期四',
3946
+ '星期五',
3947
+ '星期六'
3948
+ ]),
3949
+ builtInCustomList('周(周日 → 周六)', [
3950
+ '周日',
3951
+ '周一',
3952
+ '周二',
3953
+ '周三',
3954
+ '周四',
3955
+ '周五',
3956
+ '周六'
3957
+ ]),
3958
+ builtInCustomList('月份(January → December)', [
3959
+ 'January',
3960
+ 'February',
3961
+ 'March',
3962
+ 'April',
3963
+ 'May',
3964
+ 'June',
3965
+ 'July',
3966
+ 'August',
3967
+ 'September',
3968
+ 'October',
3969
+ 'November',
3970
+ 'December'
3971
+ ]),
3972
+ builtInCustomList('月份(Jan → Dec)', [
3973
+ 'Jan',
3974
+ 'Feb',
3975
+ 'Mar',
3976
+ 'Apr',
3977
+ 'May',
3978
+ 'Jun',
3979
+ 'Jul',
3980
+ 'Aug',
3981
+ 'Sep',
3982
+ 'Oct',
3983
+ 'Nov',
3984
+ 'Dec'
3985
+ ]),
3986
+ builtInCustomList('星期(Sunday → Saturday)', [
3987
+ 'Sunday',
3988
+ 'Monday',
3989
+ 'Tuesday',
3990
+ 'Wednesday',
3991
+ 'Thursday',
3992
+ 'Friday',
3993
+ 'Saturday'
3994
+ ]),
3995
+ builtInCustomList('星期(Sun → Sat)', [
3996
+ 'Sun',
3997
+ 'Mon',
3998
+ 'Tue',
3999
+ 'Wed',
4000
+ 'Thu',
4001
+ 'Fri',
4002
+ 'Sat'
4003
+ ])
4004
+ ]);
4005
+ const SPREADSHEET_SORT_CUSTOM_LIST_STORAGE_VERSION = 1;
4006
+ const DEFAULT_SPREADSHEET_SORT_CUSTOM_LIST_STORAGE_KEY = 'a3s-office.spreadsheet-sort-custom-lists.v1';
4007
+ class LocalStorageSpreadsheetSortCustomListStore {
4008
+ storage;
4009
+ key;
4010
+ constructor(storage, key = DEFAULT_SPREADSHEET_SORT_CUSTOM_LIST_STORAGE_KEY){
4011
+ this.storage = storage;
4012
+ this.key = key;
4013
+ }
4014
+ load() {
4015
+ try {
4016
+ const source = this.storage.getItem(this.key);
4017
+ if (!source) return Object.freeze([]);
4018
+ const payload = JSON.parse(source);
4019
+ if (!spreadsheet_sort_custom_list_store_isRecord(payload) || payload.version !== SPREADSHEET_SORT_CUSTOM_LIST_STORAGE_VERSION) return Object.freeze([]);
4020
+ return normalizeStoredSpreadsheetSortCustomLists(payload.lists);
4021
+ } catch {
4022
+ return Object.freeze([]);
4023
+ }
4024
+ }
4025
+ save(lists) {
4026
+ const normalized = normalizeStoredSpreadsheetSortCustomLists(lists);
4027
+ this.storage.setItem(this.key, JSON.stringify({
4028
+ version: SPREADSHEET_SORT_CUSTOM_LIST_STORAGE_VERSION,
4029
+ lists: normalized
4030
+ }));
4031
+ }
4032
+ }
4033
+ function normalizeStoredSpreadsheetSortCustomLists(input) {
4034
+ if (!Array.isArray(input)) return Object.freeze([]);
4035
+ const lists = [];
4036
+ for (const candidate of input){
4037
+ const validation = validateSpreadsheetSortCustomList(candidate);
4038
+ if (!(!validation.ok || lists.some((entries)=>spreadsheetSortCustomListsEqual(entries, validation.entries)))) {
4039
+ lists.push(Object.freeze([
4040
+ ...validation.entries
4041
+ ]));
4042
+ if (lists.length >= 32) break;
4043
+ }
4044
+ }
4045
+ return Object.freeze(lists);
4046
+ }
4047
+ function spreadsheet_sort_custom_list_store_isRecord(value) {
4048
+ return Boolean(value && 'object' == typeof value && !Array.isArray(value));
4049
+ }
4050
+ export { DEFAULT_SPREADSHEET_SORT_CUSTOM_LIST_STORAGE_KEY, LocalStorageSpreadsheetSortCustomListStore, SPREADSHEET_TABLE_TOTALS_FUNCTIONS, activeXlsxNativeFill, activeXlsxSemanticColorOrigin, applyImportedXlsxRichText, boundedSpreadsheetDataValidationText, coalesceXlsxRichTextRuns, createSpreadsheetSortCustomList, createWorkOfficeSpreadsheetCollaborationBinding as createOfficeSpreadsheetCollaborationBinding, createXlsxRichTextReadContext, deleteXlsxNativeFills, directXlsxAlignment, directXlsxFontStyle, fillSpreadsheetTableCalculatedColumns, hasXlsxDirectFontStyle, initializeWorkOfficeSpreadsheetCollaboration as initializeOfficeSpreadsheetCollaboration, isHighSurrogate, isLowSurrogate, isSpreadsheetTextOrientationId, isSpreadsheetUnderlineStyle, mergeSpreadsheetSortCustomLists, normalizeSpreadsheetDataValidationErrorStyle, normalizeSpreadsheetDateValidationBoundary, normalizeSpreadsheetTableCalculatedFormula, normalizeSpreadsheetTableTotalsFormula, normalizeSpreadsheetTableTotalsLabel, normalizeStoredSpreadsheetSortCustomLists, normalizeWorkSpreadsheetFilterCriteria, normalizeXlsxRichTextCell, normalizeXlsxRichTextColor, normalizeXlsxRichTextEditSource, normalizeXlsxRichTextRun, normalizedWorkSpreadsheetAutoFilterRange, parseSpreadsheetSortCustomList, patchSpreadsheetRichTextFontRuns, readWorkOfficeSpreadsheetCollaboration as readOfficeSpreadsheetCollaboration, readXlsxRichTextCells, reconcileSpreadsheetTableCalculatedColumns, reconcileSpreadsheetTableTotalsColumns, replaceWorkOfficeSpreadsheetCollaboration as replaceOfficeSpreadsheetCollaboration, sameXlsxRichTextRunStyle, sheetHasXlsxRichTextCells, spreadsheetCellValueWithDiagonalBorder, spreadsheetDateValidationFormula, spreadsheetDiagonalBorderFromCellValue, spreadsheetExplicitTextOrientationFromCell, spreadsheetSortCustomListMatchKey, spreadsheetSortCustomListsEqual, spreadsheetTableTotalsColumnPatch, spreadsheetTableTotalsFunctionFromOoxml, spreadsheetTableTotalsFunctionLabel, spreadsheetTableTotalsFunctionToOoxml, spreadsheetTableTotalsRowHasContent, spreadsheetTextOrientationCellStyle, spreadsheetTextOrientationChoiceFromCell, spreadsheetTextOrientationFromAngle, spreadsheetTextOrientationFromCell, spreadsheetTextOrientationFromChoice, spreadsheetTextOrientationFromXlsx, spreadsheetUnderlineCellValue, spreadsheetUnderlineCellValueFromSheetJs, spreadsheetUnderlineCellValueFromXlsx, spreadsheetUnderlineStyle, spreadsheetVisibleTextRotationFromCell, synchronizeSpreadsheetTableTotalsRow, validXlsxRichText, validateSpreadsheetSortCustomList, withDefaultSpreadsheetTableTotalsLabel, workSpreadsheetAutoFilterCriteria, workSpreadsheetAutoFilterCriteriaEntries, workSpreadsheetAutoFilterManuallyHiddenRows, workSpreadsheetCellIsDate, workSpreadsheetFilterTextCharacters, workSpreadsheetHasUnescapedWildcard, workSpreadsheetSheetWithAutoFilterCriteria, workSpreadsheetSheetWithImportedAutoFilterCriteria, workSpreadsheetSheetWithoutAutoFilterCriteria, writeXlsxRichTextCells, xlsxAlignmentMatches, xlsxBooleanAttribute, xlsxBorderLineMatches, xlsxColorMatches, xlsxNativeFillCellKeys, xlsxNativeFillKey, xlsxNativeFillSemanticColors, xlsxRichTextCellText, xlsxRichTextStyleOrigins, xlsxStyleCollectionIndex, xlsxToggleEnabled, xlsxUnderlineStyle };