@a3s-lab/office 0.36.0 → 0.37.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/dist/core.js CHANGED
@@ -5,7 +5,7 @@ import { OFFICE_COLLABORATION_PROTOCOL as WORK_OFFICE_COLLABORATION_PROTOCOL, Of
5
5
  import { directChildren, OoxmlPackage as work_ooxml_package_OoxmlPackage, parseXml, firstDescendant, directChild, descendants, createArtifact as createWorkArtifact, xmlContainsAnyElement, createOfficeId as createWorkId, attribute as work_ooxml_package_attribute } from "./4121.js";
6
6
  import { fileNameWithoutExtension, work_file_download_downloadBlob, mountWorkLiveDocumentCapture, importWorkDocumentFile, moveWorkSourceBlob, materializeWorkDocumentContent as work_document_model_codec_materializeWorkDocumentContent, createWorkDocumentModel, work_file_download_safeFileName, documentPageSurfaceGeometryForElement, registerSourceBlob as rememberWorkSourceBlob, documentModelForContent, readSourceBlob as readWorkSourceBlob, createWorkDocumentExtensions, positionWorkLiveDocumentCapture, createWorkDocumentBlob } from "./4174.js";
7
7
  import { normalizeSpreadsheetPrintTitleRows, sparseMatrixColumnCount, normalizeSpreadsheetPrintArea, parseSpreadsheetPrintTitles, isValidSpreadsheetDefinedName, formatSpreadsheetCellRanges, stripSpreadsheetSheetQualifier, editableSpreadsheetFormula, sparseArrayEntries, qualifySpreadsheetRange, refreshSpreadsheetPivotTables, parseSpreadsheetCellRanges, normalizeSpreadsheetPrintTitleColumns } from "./8715.js";
8
- import { xlsxColorMatches, xlsxRichTextCellText, spreadsheetCellValueWithDiagonalBorder, xlsxRichTextStyleOrigins, normalizeSpreadsheetDataValidationErrorStyle, directXlsxAlignment, applyImportedXlsxRichText, spreadsheetTextOrientationFromXlsx, directXlsxFontStyle, hasXlsxDirectFontStyle, xlsxStyleCollectionIndex, spreadsheetExplicitTextOrientationFromCell, sheetHasXlsxRichTextCells, activeXlsxNativeFill, readXlsxRichTextCells, normalizeSpreadsheetDateValidationBoundary, createXlsxRichTextReadContext, boundedSpreadsheetDataValidationText, xlsxNativeFillKey, xlsxBooleanAttribute, xlsxAlignmentMatches, xlsxBorderLineMatches, spreadsheetTextOrientationCellStyle, spreadsheetDateValidationFormula, spreadsheetDiagonalBorderFromCellValue, xlsxUnderlineStyle, spreadsheetUnderlineCellValueFromXlsx, xlsxToggleEnabled, writeXlsxRichTextCells, activeXlsxSemanticColorOrigin, xlsxNativeFillSemanticColors, normalizeSpreadsheetTableCalculatedFormula, spreadsheetUnderlineCellValueFromSheetJs } from "./3266.js";
8
+ import { xlsxColorMatches, xlsxRichTextCellText, spreadsheetCellValueWithDiagonalBorder, xlsxRichTextStyleOrigins, normalizeSpreadsheetDataValidationErrorStyle, directXlsxAlignment, spreadsheetTableTotalsFunctionToOoxml, spreadsheetTextOrientationFromXlsx, directXlsxFontStyle, hasXlsxDirectFontStyle, applyImportedXlsxRichText, xlsxStyleCollectionIndex, sheetHasXlsxRichTextCells, spreadsheetExplicitTextOrientationFromCell, activeXlsxNativeFill, readXlsxRichTextCells, normalizeSpreadsheetDateValidationBoundary, createXlsxRichTextReadContext, boundedSpreadsheetDataValidationText, xlsxNativeFillKey, xlsxBooleanAttribute, xlsxAlignmentMatches, xlsxBorderLineMatches, spreadsheetTextOrientationCellStyle, spreadsheetTableTotalsFunctionFromOoxml, spreadsheetDateValidationFormula, spreadsheetDiagonalBorderFromCellValue, xlsxUnderlineStyle, spreadsheetUnderlineCellValueFromXlsx, xlsxToggleEnabled, normalizeSpreadsheetTableTotalsLabel, activeXlsxSemanticColorOrigin, writeXlsxRichTextCells, xlsxNativeFillSemanticColors, normalizeSpreadsheetTableCalculatedFormula, normalizeSpreadsheetTableTotalsFormula, spreadsheetUnderlineCellValueFromSheetJs } from "./3266.js";
9
9
  import { freezeImportedSpreadsheetCell, readXlsxGradientFill, withXlsxPatternFill, xlsxCellStyleOriginSemanticColors, setXlsxToggleChild, withXlsxCellStyleOrigin, normalizeSpreadsheetConditionalVisualOptions, xlsxColorElementMatchesOrigin, withXlsxGradientFill, ensureXlsxStyleCollection, xlsxCellStyleOrigin, xlsxSemanticColorOriginKey, readXlsxPatternFill, isSpreadsheetConditionalComparisonOperator, registerImportedSpreadsheetMatrix, defaultSpreadsheetConditionalIconThresholds, readXlsxSemanticColorOrigin, setXlsxUnderlineChild, xlsxGradientFillFallbackColor, setXlsxColorChild, createXlsxColorResolver, resolveXlsxColor, setXlsxBorderLine, normalizeSpreadsheetConditionalIconSetFormat, defaultSpreadsheetDataBarOptions, defaultXlsxBorder, prepareXlsxSemanticPalette, importedSheetProtectionAuthority, defaultSpreadsheetColorScaleThresholds, xlsxRgbColor, writeXlsxAlignment, xlsxGradientFillKey, applyXlsxSemanticColorOrigin, spreadsheetConditionalComparisonNeedsUpperValue, sheetHasProtectionState, xlsxWorksheetCellEntries, isSpreadsheetConditionalIconSetName, setXlsxValueChild, defaultXlsxFill, spreadsheetConditionalThresholdsEqual, spreadsheetConditionalIconSetCount } from "./9333.js";
10
10
  import { updateSpreadsheetWorksheetCompatibilitySummary, inspectXlsxPivotTables, readXlsxWorksheetCharts, createXlsxFormulaCell, patchXlsxWorksheetDrawings, readXlsxWorksheetImages, writeXlsxProtection, xlsxWorksheetChartsToSheet, readXlsxFormulaFeaturesFromPackage, emptySpreadsheetWorksheetCompatibilitySummary, createSpreadsheetFormulaMetadata, createXlsxErrorCell, xlsxWorksheetImagesToSheet, readXlsxProtection, patchXlsxFormulaFeatures, patchXlsxPivotTables, writeXlsxPageSetup, XlsxCellProtectionWriter, applyImportedXlsxPivotTables, readXlsxPageSetup } from "./4166.js";
11
11
  import { WorkFileImportController, materializeWorkFileSource } from "./9356.js";
@@ -2702,11 +2702,24 @@ function parseXlsxTable(document1, partPath) {
2702
2702
  const columnName = work_ooxml_package_attribute(column, 'name');
2703
2703
  if (null === columnName) return [];
2704
2704
  const calculatedFormula = normalizeSpreadsheetTableCalculatedFormula(directChild(column, 'calculatedColumnFormula')?.textContent);
2705
+ const totalsFunction = spreadsheetTableTotalsFunctionFromOoxml(work_ooxml_package_attribute(column, 'totalsRowFunction'));
2706
+ const totalsLabel = normalizeSpreadsheetTableTotalsLabel(work_ooxml_package_attribute(column, 'totalsRowLabel'));
2707
+ const totalsFormula = normalizeSpreadsheetTableTotalsFormula(directChild(column, 'totalsRowFormula')?.textContent);
2708
+ const normalizedTotalsFunction = totalsFormula ? 'custom' : totalsFunction;
2705
2709
  return [
2706
2710
  {
2707
2711
  name: columnName,
2708
2712
  ...calculatedFormula ? {
2709
2713
  calculatedFormula
2714
+ } : {},
2715
+ ...normalizedTotalsFunction ? {
2716
+ totalsFunction: normalizedTotalsFunction
2717
+ } : {},
2718
+ ...totalsLabel && !totalsFormula ? {
2719
+ totalsLabel
2720
+ } : {},
2721
+ ...totalsFormula ? {
2722
+ totalsFormula
2710
2723
  } : {}
2711
2724
  }
2712
2725
  ];
@@ -2750,8 +2763,22 @@ function xlsxTableXml(table, tableId, namespace) {
2750
2763
  const columns = table.columns.map((column, index)=>{
2751
2764
  const formula = normalizeSpreadsheetTableCalculatedFormula(column.calculatedFormula);
2752
2765
  const formulaElement = formula ? `<calculatedColumnFormula>${work_xlsx_tables_escapeXml(formula.replace(/^=/, ''))}</calculatedColumnFormula>` : '';
2753
- const opening = `<tableColumn id="${index + 1}" name="${work_xlsx_tables_escapeXml(column.name)}"`;
2754
- return formulaElement ? `${opening}>${formulaElement}</tableColumn>` : `${opening}/>`;
2766
+ const totalsFunction = spreadsheetTableTotalsFunctionToOoxml(column.totalsFunction);
2767
+ const totalsLabel = normalizeSpreadsheetTableTotalsLabel(column.totalsLabel);
2768
+ const totalsFormula = normalizeSpreadsheetTableTotalsFormula(column.totalsFormula);
2769
+ const attributes = [
2770
+ `id="${index + 1}"`,
2771
+ `name="${work_xlsx_tables_escapeXml(column.name)}"`,
2772
+ ...totalsFunction ? [
2773
+ `totalsRowFunction="${work_xlsx_tables_escapeXml(totalsFunction)}"`
2774
+ ] : [],
2775
+ ...totalsLabel && !totalsFormula ? [
2776
+ `totalsRowLabel="${work_xlsx_tables_escapeXml(totalsLabel)}"`
2777
+ ] : []
2778
+ ].join(' ');
2779
+ const totalsFormulaElement = totalsFormula ? `<totalsRowFormula>${work_xlsx_tables_escapeXml(totalsFormula.replace(/^=/, ''))}</totalsRowFormula>` : '';
2780
+ const children = `${formulaElement}${totalsFormulaElement}`;
2781
+ return children ? `<tableColumn ${attributes}>${children}</tableColumn>` : `<tableColumn ${attributes}/>`;
2755
2782
  }).join('');
2756
2783
  const styleName = tableStyleName(table.style);
2757
2784
  const style = styleName ? `<tableStyleInfo name="${styleName}" showColumnStripes="${booleanToken(table.showColumnStripes)}" showFirstColumn="${booleanToken(table.showFirstColumn)}" showLastColumn="${booleanToken(table.showLastColumn)}" showRowStripes="${booleanToken(table.showRowStripes)}"/>` : '';
@@ -0,0 +1,39 @@
1
+ import type { WorkSpreadsheetSheet, WorkSpreadsheetTable, WorkSpreadsheetTableColumn, WorkSpreadsheetTableTotalsFunction } from '../work-types';
2
+ export declare const MAX_SPREADSHEET_TABLE_TOTALS_FORMULA_LENGTH = 8192;
3
+ export declare const MAX_SPREADSHEET_TABLE_TOTALS_LABEL_LENGTH = 255;
4
+ export declare const DEFAULT_SPREADSHEET_TABLE_TOTALS_LABEL = "Total";
5
+ /** The closed function set exposed by the Table Design totals menu. */
6
+ export declare const SPREADSHEET_TABLE_TOTALS_FUNCTIONS: readonly ["sum", "average", "count", "countNums", "max", "min", "stdDev", "stdDevP", "var", "varP", "custom"];
7
+ export interface SpreadsheetTableTotalsColumnPatch {
8
+ totalsFormula?: string | null;
9
+ totalsFunction?: WorkSpreadsheetTableTotalsFunction | null;
10
+ totalsLabel?: string | null;
11
+ }
12
+ export declare function spreadsheetTableTotalsFunctionLabel(value: WorkSpreadsheetTableTotalsFunction | undefined): string;
13
+ export declare function normalizeSpreadsheetTableTotalsFunction(value: unknown): WorkSpreadsheetTableTotalsFunction | undefined;
14
+ /** Normalize a table totals formula to the editable leading-equals form. */
15
+ export declare function normalizeSpreadsheetTableTotalsFormula(value: unknown): string | undefined;
16
+ export declare function normalizeSpreadsheetTableTotalsLabel(value: unknown): string | undefined;
17
+ export declare function spreadsheetTableTotalsFunctionFromOoxml(value: unknown): WorkSpreadsheetTableTotalsFunction | undefined;
18
+ export declare function spreadsheetTableTotalsFunctionToOoxml(value: WorkSpreadsheetTableTotalsFunction | undefined): string | undefined;
19
+ /**
20
+ * Returns the formula that should be placed in one totals-row cell. Native
21
+ * aggregate functions use SUBTOTAL so filtered-out rows stay excluded; a
22
+ * custom rule uses its explicit formula.
23
+ */
24
+ export declare function spreadsheetTableTotalsFormula(table: WorkSpreadsheetTable, columnOffset: number): string | undefined;
25
+ export declare function spreadsheetTableTotalsColumnPatch(columns: readonly WorkSpreadsheetTableColumn[], patches: Readonly<Record<string, SpreadsheetTableTotalsColumnPatch>> | readonly (SpreadsheetTableTotalsColumnPatch | null | undefined)[] | undefined): WorkSpreadsheetTableColumn[] | null;
26
+ /** Apply the native default label used when a totals row is first enabled. */
27
+ export declare function withDefaultSpreadsheetTableTotalsLabel(columns: readonly WorkSpreadsheetTableColumn[], totalsRow: boolean): WorkSpreadsheetTableColumn[];
28
+ /** True when a proposed totals row would overwrite a populated cell. */
29
+ export declare function spreadsheetTableTotalsRowHasContent(sheet: WorkSpreadsheetSheet, table: WorkSpreadsheetTable, row?: number): boolean;
30
+ /**
31
+ * Synchronize totals-row cells after a table metadata change. Explicitly
32
+ * changed column metadata owns the target cell; untouched manual cells remain
33
+ * authoritative. The helper works with Fortune's dense and sparse forms.
34
+ */
35
+ export declare function synchronizeSpreadsheetTableTotalsRow(sheet: WorkSpreadsheetSheet, previousTable: WorkSpreadsheetTable | undefined, nextTable: WorkSpreadsheetTable, options?: {
36
+ force?: boolean;
37
+ }): WorkSpreadsheetSheet;
38
+ /** Reconcile metadata after a user edits a totals-row cell directly. */
39
+ export declare function reconcileSpreadsheetTableTotalsColumns(sheet: WorkSpreadsheetSheet, table: WorkSpreadsheetTable, editedOffsets?: ReadonlySet<number>): WorkSpreadsheetTableColumn[];
@@ -1,6 +1,7 @@
1
1
  import type { Selection } from '@fortune-sheet/core';
2
2
  import type { WorkSpreadsheetContent, WorkSpreadsheetSheet, WorkSpreadsheetTable, WorkSpreadsheetTableStyle } from '../work-types';
3
3
  import { type SpreadsheetCellRange } from './spreadsheet-cell-range';
4
+ import { type SpreadsheetTableTotalsColumnPatch } from './spreadsheet-table-totals';
4
5
  export { MAX_SPREADSHEET_TABLE_CELLS } from './spreadsheet-table-limits';
5
6
  export interface SpreadsheetTableTarget {
6
7
  sheetId: string;
@@ -9,6 +10,7 @@ export interface SpreadsheetTableTarget {
9
10
  export interface SpreadsheetTableDialogValue {
10
11
  headerRow: boolean;
11
12
  rangeReference: string;
13
+ totalsRow?: boolean;
12
14
  }
13
15
  export interface SpreadsheetTableDialogSource {
14
16
  name: string;
@@ -24,6 +26,10 @@ export interface SpreadsheetTableRequest {
24
26
  range: SpreadsheetCellRange;
25
27
  sheetId: string;
26
28
  style?: WorkSpreadsheetTableStyle;
29
+ /** Create the table with a native totals row appended to the range. */
30
+ totalsRow?: boolean;
31
+ /** Initial per-column totals metadata, keyed by zero-based table offset. */
32
+ totalsColumns?: Readonly<Record<string, SpreadsheetTableTotalsColumnPatch>> | readonly (SpreadsheetTableTotalsColumnPatch | null | undefined)[];
27
33
  }
28
34
  export type SpreadsheetTableValidation = {
29
35
  columns: string[];
@@ -36,7 +42,7 @@ export type SpreadsheetTableValidation = {
36
42
  message: string;
37
43
  ok: false;
38
44
  };
39
- export type SpreadsheetTableErrorCode = 'auto-filter-overlap' | 'invalid-column-name' | 'invalid-name' | 'invalid-range' | 'invalid-style' | 'merged-range' | 'name-conflict' | 'out-of-bounds' | 'protected-range' | 'pivot-table' | 'range-too-large' | 'sheet-not-found' | 'table-overlap';
45
+ export type SpreadsheetTableErrorCode = 'auto-filter-overlap' | 'invalid-column-name' | 'invalid-name' | 'invalid-range' | 'invalid-style' | 'invalid-totals' | 'merged-range' | 'name-conflict' | 'out-of-bounds' | 'protected-range' | 'pivot-table' | 'range-too-large' | 'sheet-not-found' | 'table-overlap' | 'totals-row-content';
40
46
  export interface SpreadsheetTableDesignPatch {
41
47
  name?: string;
42
48
  showColumnStripes?: boolean;
@@ -44,6 +50,10 @@ export interface SpreadsheetTableDesignPatch {
44
50
  showLastColumn?: boolean;
45
51
  showRowStripes?: boolean;
46
52
  style?: WorkSpreadsheetTableStyle;
53
+ /** Toggle the native totals row. Enabling appends one row to the table. */
54
+ totalsRow?: boolean;
55
+ /** Update per-column totals function, label, or custom formula. */
56
+ totalsColumns?: Readonly<Record<string, SpreadsheetTableTotalsColumnPatch>> | readonly (SpreadsheetTableTotalsColumnPatch | null | undefined)[];
47
57
  }
48
58
  export declare function createSpreadsheetTableDialogSource(content: WorkSpreadsheetContent, target: SpreadsheetTableTarget): SpreadsheetTableDialogSource | null;
49
59
  export declare function spreadsheetTableRangeFromText(value: string): SpreadsheetCellRange | null;
@@ -241,7 +241,21 @@ export interface WorkSpreadsheetTableColumn {
241
241
  * stores the editable form with a leading `=`; XLSX table parts omit it.
242
242
  */
243
243
  calculatedFormula?: string;
244
+ /**
245
+ * Native totals-row aggregation. An omitted value means that the totals
246
+ * cell is not owned by the table. The controlled model uses camelCase names
247
+ * while XLSX table parts use their OOXML spellings.
248
+ */
249
+ totalsFunction?: WorkSpreadsheetTableTotalsFunction;
250
+ /** Optional text label rendered in this column's totals-row cell. */
251
+ totalsLabel?: string;
252
+ /**
253
+ * A bounded custom totals-row formula. The controlled model stores the
254
+ * editable form with a leading `=`; XLSX table parts omit it.
255
+ */
256
+ totalsFormula?: string;
244
257
  }
258
+ export type WorkSpreadsheetTableTotalsFunction = 'sum' | 'average' | 'count' | 'countNums' | 'max' | 'min' | 'stdDev' | 'stdDevP' | 'var' | 'varP' | 'custom';
245
259
  export type WorkSpreadsheetDynamicFilter = 'above-average' | 'below-average' | 'tomorrow' | 'today' | 'yesterday' | 'next-week' | 'this-week' | 'last-week' | 'next-month' | 'this-month' | 'last-month' | 'next-quarter' | 'this-quarter' | 'last-quarter' | 'next-year' | 'this-year' | 'last-year' | 'year-to-date' | 'quarter-1' | 'quarter-2' | 'quarter-3' | 'quarter-4' | 'month-1' | 'month-2' | 'month-3' | 'month-4' | 'month-5' | 'month-6' | 'month-7' | 'month-8' | 'month-9' | 'month-10' | 'month-11' | 'month-12';
246
260
  export type WorkSpreadsheetTableFilterCriteria = {
247
261
  type: 'values';
@@ -1,3 +1,14 @@
1
1
  export declare const browserScalarFunctionArities: Map<string, readonly [minimum: number, maximum: number]>;
2
2
  export declare function normalizeSpreadsheetFunctionName(name: string): string;
3
+ /**
4
+ * Evaluate the bounded browser implementation of `SUBTOTAL`.
5
+ *
6
+ * Fortune delegates unknown functions to Formula.js. Formula.js returns an
7
+ * undefined value for an unknown function number, which the parser can then
8
+ * coerce into a boolean. Keeping this small implementation here makes the
9
+ * JavaScript fallback deterministic and aligned with the Rust kernel. The
10
+ * calculation request does not carry hidden-row metadata yet, so the 1–11
11
+ * and 101–111 function-number families intentionally have the same result.
12
+ */
13
+ export declare function evaluateParserSubtotal(parameters: readonly unknown[]): unknown;
3
14
  export declare function normalizeFormulaForFortuneParser(formula: string): string;
Binary file
@@ -13727,6 +13727,13 @@
13727
13727
  1
13728
13728
  ]
13729
13729
  ],
13730
+ [
13731
+ 'SUBTOTAL',
13732
+ [
13733
+ 2,
13734
+ 255
13735
+ ]
13736
+ ],
13730
13737
  [
13731
13738
  'SUM',
13732
13739
  [
@@ -13747,6 +13754,90 @@
13747
13754
  while(normalized.startsWith('_XLFN.') || normalized.startsWith('_XLWS.'))normalized = normalized.slice(6);
13748
13755
  return normalized;
13749
13756
  }
13757
+ function evaluateParserSubtotal(parameters) {
13758
+ const codeValue = parserNumericValue(parameters[0]);
13759
+ if (void 0 === codeValue) return 'VALUE!';
13760
+ const code = Math.trunc(codeValue);
13761
+ const values = parameters.slice(1).flatMap((parameter)=>flattenParserValues(parameter));
13762
+ for (const value of values){
13763
+ const error = parserErrorValue(value);
13764
+ if (error) return error;
13765
+ }
13766
+ switch(code){
13767
+ case 1:
13768
+ case 101:
13769
+ return parserSubtotalNumeric(values, 'average');
13770
+ case 2:
13771
+ case 102:
13772
+ return values.filter((value)=>'number' == typeof value).length;
13773
+ case 3:
13774
+ case 103:
13775
+ return values.filter((value)=>null != value).length;
13776
+ case 4:
13777
+ case 104:
13778
+ return parserSubtotalNumeric(values, 'max');
13779
+ case 5:
13780
+ case 105:
13781
+ return parserSubtotalNumeric(values, 'min');
13782
+ case 6:
13783
+ case 106:
13784
+ return parserSubtotalNumeric(values, 'product');
13785
+ case 7:
13786
+ case 107:
13787
+ return parserSubtotalNumeric(values, 'stddev');
13788
+ case 8:
13789
+ case 108:
13790
+ return parserSubtotalNumeric(values, 'stddevp');
13791
+ case 9:
13792
+ case 109:
13793
+ return parserSubtotalNumeric(values, 'sum');
13794
+ case 10:
13795
+ case 110:
13796
+ return parserSubtotalNumeric(values, 'var');
13797
+ case 11:
13798
+ case 111:
13799
+ return parserSubtotalNumeric(values, 'varp');
13800
+ default:
13801
+ return 'VALUE!';
13802
+ }
13803
+ }
13804
+ function parserSubtotalNumeric(values, operation) {
13805
+ const numbers = values.filter((value)=>'number' == typeof value && Number.isFinite(value));
13806
+ const count = numbers.length;
13807
+ const sum = numbers.reduce((total, value)=>total + value, 0);
13808
+ if ('sum' === operation) return finiteParserNumber(sum);
13809
+ if ('average' === operation) return count ? finiteParserNumber(sum / count) : 'DIV/0!';
13810
+ if ('max' === operation) return finiteParserNumber(Math.max(...numbers, 0));
13811
+ if ('min' === operation) return finiteParserNumber(Math.min(...numbers, 0));
13812
+ if ('product' === operation) return finiteParserNumber(count ? numbers.reduce((total, value)=>total * value, 1) : 0);
13813
+ if ('stddev' === operation || 'var' === operation) {
13814
+ if (count < 2) return 'DIV/0!';
13815
+ } else if (0 === count) return 'DIV/0!';
13816
+ const mean = sum / count;
13817
+ const divisor = 'stddev' === operation || 'var' === operation ? count - 1 : count;
13818
+ const variance = numbers.reduce((total, value)=>total + (value - mean) ** 2, 0) / divisor;
13819
+ return finiteParserNumber('stddev' === operation || 'stddevp' === operation ? Math.sqrt(variance) : variance);
13820
+ }
13821
+ function flattenParserValues(value) {
13822
+ if (!Array.isArray(value)) return [
13823
+ value
13824
+ ];
13825
+ return value.flatMap((entry)=>flattenParserValues(entry));
13826
+ }
13827
+ function parserNumericValue(value) {
13828
+ if ('number' == typeof value) return Number.isFinite(value) ? value : void 0;
13829
+ if ('boolean' == typeof value) return value ? 1 : 0;
13830
+ if ('string' != typeof value || !value.trim()) return;
13831
+ const parsed = Number(value);
13832
+ return Number.isFinite(parsed) ? parsed : void 0;
13833
+ }
13834
+ function parserErrorValue(value) {
13835
+ if (value instanceof Error) return value.message.startsWith('#') ? value.message.slice(1) : 'VALUE!';
13836
+ return 'string' == typeof value && value.startsWith('#') ? value.slice(1) : void 0;
13837
+ }
13838
+ function finiteParserNumber(value) {
13839
+ return Number.isFinite(value) ? value : 'NUM!';
13840
+ }
13750
13841
  function normalizeFormulaForFortuneParser(formula) {
13751
13842
  const source = formula.replace(/^=/, '');
13752
13843
  let output = '';
@@ -14608,10 +14699,11 @@
14608
14699
  let unsupportedFunction;
14609
14700
  let materializedRangeCells = 0;
14610
14701
  parser.setFunction('IFERROR', evaluateParserIfError).setFunction('ROW', (parameters)=>parameters.length ? null : coordinate.row + 1).setFunction('COLUMN', (parameters)=>parameters.length ? null : coordinate.column + 1);
14611
- parser.on('callFunction', (name, parameters)=>{
14702
+ parser.on('callFunction', (name, parameters, done)=>{
14612
14703
  const normalized = normalizeSpreadsheetFunctionName(name);
14613
14704
  const arity = browserScalarFunctionArities.get(normalized);
14614
14705
  if (!arity || parameters.length < arity[0] || parameters.length > arity[1]) unsupportedFunction ??= name.toUpperCase();
14706
+ if ('SUBTOTAL' === normalized) done(evaluateParserSubtotal(parameters));
14615
14707
  });
14616
14708
  parser.on('callCellValue', (cell, _options, done)=>{
14617
14709
  const dependency = this.resolveCoordinate(indexed.sheet, cell);
package/dist/styles.css CHANGED
@@ -18364,6 +18364,113 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
18364
18364
  min-width: 76px;
18365
18365
  }
18366
18366
 
18367
+ .work-spreadsheet-table-totals-root {
18368
+ display: flex;
18369
+ }
18370
+
18371
+ .work-office-ribbon .work-spreadsheet-table-totals-trigger {
18372
+ min-width: 70px;
18373
+ }
18374
+
18375
+ .work-spreadsheet-table-totals-menu {
18376
+ border: 1px solid var(--a3s-overlay-border);
18377
+ border-radius: var(--a3s-overlay-radius);
18378
+ background: var(--a3s-overlay-surface);
18379
+ width: min(560px, 100vw - 24px);
18380
+ max-height: min(650px, 100vh - 32px);
18381
+ box-shadow: var(--a3s-shadow-soft);
18382
+ padding: 14px;
18383
+ overflow: auto;
18384
+ }
18385
+
18386
+ .work-spreadsheet-table-totals-content {
18387
+ gap: 10px;
18388
+ display: grid;
18389
+ }
18390
+
18391
+ .work-spreadsheet-table-totals-hint {
18392
+ color: var(--a3s-muted);
18393
+ margin: -4px 0 2px;
18394
+ font-size: 11px;
18395
+ line-height: 1.45;
18396
+ }
18397
+
18398
+ .work-spreadsheet-table-totals-columns {
18399
+ gap: 8px;
18400
+ max-height: 440px;
18401
+ padding-right: 2px;
18402
+ display: grid;
18403
+ overflow: auto;
18404
+ }
18405
+
18406
+ .work-spreadsheet-table-totals-column {
18407
+ border: 1px solid var(--a3s-line);
18408
+ background: var(--a3s-panel-soft);
18409
+ border-radius: 7px;
18410
+ grid-template-columns: minmax(92px, 1fr) minmax(100px, 1fr) minmax(100px, 1fr);
18411
+ align-items: end;
18412
+ gap: 7px;
18413
+ padding: 9px;
18414
+ display: grid;
18415
+ }
18416
+
18417
+ .work-spreadsheet-table-totals-column > strong {
18418
+ color: var(--a3s-ink);
18419
+ text-overflow: ellipsis;
18420
+ white-space: nowrap;
18421
+ grid-column: 1 / -1;
18422
+ font-size: 11px;
18423
+ font-weight: 650;
18424
+ overflow: hidden;
18425
+ }
18426
+
18427
+ .work-spreadsheet-table-totals-column label {
18428
+ gap: 3px;
18429
+ min-width: 0;
18430
+ display: grid;
18431
+ }
18432
+
18433
+ .work-spreadsheet-table-totals-column label > span {
18434
+ color: var(--a3s-muted);
18435
+ font-size: 9px;
18436
+ line-height: 1;
18437
+ }
18438
+
18439
+ .work-spreadsheet-table-totals-column select, .work-spreadsheet-table-totals-column input {
18440
+ box-sizing: border-box;
18441
+ border: 1px solid var(--a3s-line-strong);
18442
+ background: var(--a3s-panel);
18443
+ width: 100%;
18444
+ min-width: 0;
18445
+ height: 28px;
18446
+ color: var(--a3s-ink);
18447
+ border-radius: 5px;
18448
+ padding: 0 7px;
18449
+ font-size: 11px;
18450
+ }
18451
+
18452
+ .work-spreadsheet-table-totals-column select:disabled, .work-spreadsheet-table-totals-column input:disabled {
18453
+ cursor: not-allowed;
18454
+ opacity: .55;
18455
+ }
18456
+
18457
+ .work-spreadsheet-table-totals-close {
18458
+ border: 1px solid var(--work-spreadsheet-accent, #159469);
18459
+ background: var(--work-spreadsheet-accent, #159469);
18460
+ color: #fff;
18461
+ cursor: pointer;
18462
+ border-radius: 5px;
18463
+ justify-self: end;
18464
+ min-height: 29px;
18465
+ padding: 0 13px;
18466
+ font-size: 11px;
18467
+ font-weight: 650;
18468
+ }
18469
+
18470
+ .work-spreadsheet-table-totals-close:hover, .work-spreadsheet-table-totals-close:focus-visible {
18471
+ filter: brightness(.94);
18472
+ }
18473
+
18367
18474
  .work-spreadsheet-table-style-preview {
18368
18475
  background: #fff;
18369
18476
  border: 1px solid #0f172a29;
@@ -18455,6 +18562,19 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
18455
18562
  padding: 8px;
18456
18563
  }
18457
18564
 
18565
+ .work-spreadsheet-table-totals-menu {
18566
+ width: min(440px, 100vw - 16px);
18567
+ padding: 10px;
18568
+ }
18569
+
18570
+ .work-spreadsheet-table-totals-column {
18571
+ grid-template-columns: 1fr 1fr;
18572
+ }
18573
+
18574
+ .work-spreadsheet-table-totals-column label:last-child {
18575
+ grid-column: 1 / -1;
18576
+ }
18577
+
18458
18578
  .work-spreadsheet-table-style-family > div {
18459
18579
  grid-template-columns: repeat(5, minmax(0, 1fr));
18460
18580
  }
@@ -1430,8 +1430,14 @@ formulas authored on data rows and totals/header/data selectors. A consistent
1430
1430
  current-row rule is persisted in the table metadata and fills only empty cells
1431
1431
  in newly inserted body rows; manual values and conflicting formulas fail closed.
1432
1432
  Dense and sparse sheet representations use the same path, and native XLSX
1433
- `<calculatedColumnFormula>` metadata round-trips. Complete totals-row
1434
- authoring, slicers, and external/query tables remain open gates.
1433
+ `<calculatedColumnFormula>` metadata round-trips. Native totals-row authoring
1434
+ now extends the same model: Table Design controls the row and each column's
1435
+ label, aggregate, or custom formula; native aggregates map to filter-aware
1436
+ `SUBTOTAL` codes; generated cells are reconciled after direct edits, row/column
1437
+ changes, and table renames; and `totalsRowFunction`, `totalsRowLabel`, and
1438
+ `totalsRowFormula` survive XLSX and Yjs round trips. Occupied, merged,
1439
+ protected, overlapping, AutoFilter, unsafe, external, and over-budget targets
1440
+ fail closed. Slicers and external/query tables remain open gates.
1435
1441
 
1436
1442
  Exit criteria: scrolling and selection do not scale with total row count;
1437
1443
  incremental recalculation touches only affected dependency subgraphs; XLSX
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",
@@ -114,6 +114,7 @@
114
114
  "playground:visual:spreadsheet-rich-text": "playwright test visual-tests/spreadsheet-rich-text.functional.spec.ts --config playwright.config.ts",
115
115
  "playground:visual:spreadsheet-underline": "playwright test visual-tests/spreadsheet-underline.functional.spec.ts --config playwright.config.ts",
116
116
  "playground:visual:spreadsheet-table": "playwright test visual-tests/spreadsheet-table.functional.spec.ts --config playwright.config.ts",
117
+ "playground:visual:spreadsheet-table-totals": "playwright test visual-tests/spreadsheet-table-totals.functional.spec.ts --config playwright.config.ts",
117
118
  "playground:visual:presentation-animation": "playwright test visual-tests/presentation-animation.functional.spec.ts --config playwright.config.ts",
118
119
  "playground:visual:pdf-page-organization": "playwright test visual-tests/pdf-page-organization.functional.spec.ts --config playwright.config.ts",
119
120
  "playground:visual:update": "playwright test --config playwright.config.ts --update-snapshots",
@@ -178,6 +179,8 @@
178
179
  "test:e2e:spreadsheet-underline-styles:check": "a3s-test check tests/e2e/spreadsheet-underline-styles.acl --json",
179
180
  "test:e2e:spreadsheet-table": "a3s-test run tests/e2e/spreadsheet-table.acl --json",
180
181
  "test:e2e:spreadsheet-table:check": "a3s-test check tests/e2e/spreadsheet-table.acl --json",
182
+ "test:e2e:spreadsheet-table-totals": "a3s-test run tests/e2e/spreadsheet-table-totals.acl --json",
183
+ "test:e2e:spreadsheet-table-totals:check": "a3s-test check tests/e2e/spreadsheet-table-totals.acl --json",
181
184
  "test:e2e:spreadsheet-cell-fill": "a3s-test run tests/e2e/spreadsheet-cell-fill.acl --json",
182
185
  "test:e2e:spreadsheet-cell-fill:check": "a3s-test check tests/e2e/spreadsheet-cell-fill.acl --json",
183
186
  "test:e2e:spreadsheet-auto-sum": "a3s-test run tests/e2e/spreadsheet-auto-sum.acl --json",