@pixerate/editor 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +71 -1
  2. package/THIRD_PARTY_NOTICES.md +29 -0
  3. package/dist/agent/index.cjs +10 -9
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +1 -1
  6. package/dist/agent/index.d.ts +1 -1
  7. package/dist/agent/index.js +4 -3
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +3173 -340
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +617 -4
  12. package/dist/canvas/index.d.ts +617 -4
  13. package/dist/canvas/index.js +3105 -272
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/chunk-4BVP64JJ.cjs +1406 -0
  16. package/dist/chunk-4BVP64JJ.cjs.map +1 -0
  17. package/dist/{chunk-XNORY7M6.cjs → chunk-7WISYZFO.cjs} +105 -50
  18. package/dist/chunk-7WISYZFO.cjs.map +1 -0
  19. package/dist/{chunk-OLZJXL4R.cjs → chunk-AL6JMGWW.cjs} +59 -25
  20. package/dist/chunk-AL6JMGWW.cjs.map +1 -0
  21. package/dist/chunk-BVP3TSLH.cjs +69 -0
  22. package/dist/chunk-BVP3TSLH.cjs.map +1 -0
  23. package/dist/{chunk-MCSJFWAO.cjs → chunk-C7P22VIP.cjs} +670 -96
  24. package/dist/chunk-C7P22VIP.cjs.map +1 -0
  25. package/dist/{chunk-PGNZKFQS.js → chunk-ECCCYKGL.js} +76 -21
  26. package/dist/chunk-ECCCYKGL.js.map +1 -0
  27. package/dist/{chunk-3CN2WD3X.js → chunk-FH6F67DN.js} +646 -72
  28. package/dist/chunk-FH6F67DN.js.map +1 -0
  29. package/dist/chunk-HRMEDT2L.js +1406 -0
  30. package/dist/chunk-HRMEDT2L.js.map +1 -0
  31. package/dist/{chunk-ITBSKI26.js → chunk-IXHSYOVY.js} +941 -9
  32. package/dist/chunk-IXHSYOVY.js.map +1 -0
  33. package/dist/{chunk-FODNLX5N.js → chunk-NAXVVS5I.js} +38 -4
  34. package/dist/chunk-NAXVVS5I.js.map +1 -0
  35. package/dist/chunk-O5RYH2PG.js +69 -0
  36. package/dist/chunk-O5RYH2PG.js.map +1 -0
  37. package/dist/{chunk-SBCZ6RN5.cjs → chunk-PO6A6QQH.cjs} +1039 -107
  38. package/dist/chunk-PO6A6QQH.cjs.map +1 -0
  39. package/dist/{chunk-3LV6RRPH.js → chunk-QGYDWPRM.js} +2 -2
  40. package/dist/{chunk-U5KWZU4X.js → chunk-RR7F5GUM.js} +2632 -129
  41. package/dist/chunk-RR7F5GUM.js.map +1 -0
  42. package/dist/{chunk-5S3Z4PPF.js → chunk-UAKDG62Z.js} +232 -25
  43. package/dist/chunk-UAKDG62Z.js.map +1 -0
  44. package/dist/{chunk-SML64VJG.cjs → chunk-V3CHHWOE.cjs} +4 -4
  45. package/dist/{chunk-SML64VJG.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
  46. package/dist/chunk-WSQKHXW7.cjs +5726 -0
  47. package/dist/chunk-WSQKHXW7.cjs.map +1 -0
  48. package/dist/{chunk-D27YFC46.cjs → chunk-X4N4WUKB.cjs} +267 -60
  49. package/dist/chunk-X4N4WUKB.cjs.map +1 -0
  50. package/dist/commands/index.cjs +74 -1
  51. package/dist/commands/index.cjs.map +1 -1
  52. package/dist/commands/index.d.cts +34 -1
  53. package/dist/commands/index.d.ts +34 -1
  54. package/dist/commands/index.js +73 -0
  55. package/dist/commands/index.js.map +1 -1
  56. package/dist/compare/index.cjs +3 -3
  57. package/dist/compare/index.d.cts +1 -1
  58. package/dist/compare/index.d.ts +1 -1
  59. package/dist/compare/index.js +2 -2
  60. package/dist/controller-BIJiNoVs.d.cts +1842 -0
  61. package/dist/controller-BIJiNoVs.d.ts +1842 -0
  62. package/dist/{documents-BQjzJNe2.d.ts → documents-CHT1U5Vs.d.ts} +10 -0
  63. package/dist/{documents-KRTPhvst.d.cts → documents-Gp7wb0Me.d.cts} +10 -0
  64. package/dist/extensions/index.cjs +53 -6
  65. package/dist/extensions/index.cjs.map +1 -1
  66. package/dist/extensions/index.d.cts +12 -4
  67. package/dist/extensions/index.d.ts +12 -4
  68. package/dist/extensions/index.js +53 -6
  69. package/dist/generation/index.cjs +1013 -69
  70. package/dist/generation/index.cjs.map +1 -1
  71. package/dist/generation/index.d.cts +474 -13
  72. package/dist/generation/index.d.ts +474 -13
  73. package/dist/generation/index.js +976 -32
  74. package/dist/generation/index.js.map +1 -1
  75. package/dist/image-editor/index.cjs +3385 -1275
  76. package/dist/image-editor/index.cjs.map +1 -1
  77. package/dist/image-editor/index.d.cts +693 -128
  78. package/dist/image-editor/index.d.ts +693 -128
  79. package/dist/image-editor/index.js +3325 -1215
  80. package/dist/image-editor/index.js.map +1 -1
  81. package/dist/{index-fJrlkJCd.d.ts → index-7a6bHTLy.d.ts} +494 -5
  82. package/dist/{index-BhJBSm2d.d.cts → index-Csd5qI8u.d.cts} +494 -5
  83. package/dist/index.cjs +1491 -45
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +110 -19
  86. package/dist/index.d.ts +110 -19
  87. package/dist/index.js +1464 -18
  88. package/dist/index.js.map +1 -1
  89. package/dist/mention-text-CDnCFd3c.d.cts +53 -0
  90. package/dist/mention-text-CDnCFd3c.d.ts +53 -0
  91. package/dist/model-C1zApQM8.d.cts +219 -0
  92. package/dist/model-C1zApQM8.d.ts +219 -0
  93. package/dist/persistence/index.d.cts +3 -120
  94. package/dist/persistence/index.d.ts +3 -120
  95. package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.cts} +12 -1
  96. package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.ts} +12 -1
  97. package/dist/serializers/index.cjs +12 -4
  98. package/dist/serializers/index.cjs.map +1 -1
  99. package/dist/serializers/index.d.cts +39 -12
  100. package/dist/serializers/index.d.ts +39 -12
  101. package/dist/serializers/index.js +12 -4
  102. package/dist/spreadsheet/index.cjs +127 -3
  103. package/dist/spreadsheet/index.cjs.map +1 -1
  104. package/dist/spreadsheet/index.d.cts +431 -11
  105. package/dist/spreadsheet/index.d.ts +431 -11
  106. package/dist/spreadsheet/index.js +127 -3
  107. package/dist/storage-BpFfspHm.d.cts +120 -0
  108. package/dist/storage-BpFfspHm.d.ts +120 -0
  109. package/dist/tokens/index.cjs +115 -0
  110. package/dist/tokens/index.cjs.map +1 -0
  111. package/dist/tokens/index.d.cts +212 -0
  112. package/dist/tokens/index.d.ts +212 -0
  113. package/dist/tokens/index.js +115 -0
  114. package/dist/tokens/index.js.map +1 -0
  115. package/dist/{track-changes-BK5sVf4_.d.cts → track-changes-D_VAPELi.d.cts} +95 -11
  116. package/dist/{track-changes-CDjCrwKe.d.ts → track-changes-Db_fGEvx.d.ts} +95 -11
  117. package/package.json +6 -1
  118. package/dist/chunk-3CN2WD3X.js.map +0 -1
  119. package/dist/chunk-5S3Z4PPF.js.map +0 -1
  120. package/dist/chunk-D27YFC46.cjs.map +0 -1
  121. package/dist/chunk-FODNLX5N.js.map +0 -1
  122. package/dist/chunk-ITBSKI26.js.map +0 -1
  123. package/dist/chunk-MA2F5KHF.cjs +0 -3223
  124. package/dist/chunk-MA2F5KHF.cjs.map +0 -1
  125. package/dist/chunk-MCSJFWAO.cjs.map +0 -1
  126. package/dist/chunk-OLZJXL4R.cjs.map +0 -1
  127. package/dist/chunk-PGNZKFQS.js.map +0 -1
  128. package/dist/chunk-SBCZ6RN5.cjs.map +0 -1
  129. package/dist/chunk-U5KWZU4X.js.map +0 -1
  130. package/dist/chunk-XNORY7M6.cjs.map +0 -1
  131. package/dist/controller-BhLLAoi8.d.cts +0 -1000
  132. package/dist/controller-BhLLAoi8.d.ts +0 -1000
  133. /package/dist/{chunk-3LV6RRPH.js.map → chunk-QGYDWPRM.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-WSQKHXW7.cjs","../src/spreadsheet/formula/dependency-graph.ts","../src/spreadsheet/formula/references.ts","../src/spreadsheet/formula/caret.ts","../src/spreadsheet/numfmt/presets.ts","../src/spreadsheet/formula/format.ts","../src/spreadsheet/datasource/index.ts","../src/spreadsheet/workbook.ts","../src/spreadsheet/table.ts","../src/spreadsheet/clipboard.ts","../src/spreadsheet/rule-ranges.ts","../src/spreadsheet/conditional-format/icons.ts","../src/spreadsheet/conditional-format/evaluate.ts","../src/spreadsheet/conditional-format/describe.ts","../src/spreadsheet/conditional-format/presets.ts","../src/spreadsheet/conditional-format/render.ts","../src/spreadsheet/validation/validate.ts","../src/spreadsheet/view.ts","../src/spreadsheet/controller.ts","../src/spreadsheet/menu.ts","../src/spreadsheet/actions.ts","../src/spreadsheet/import.ts","../src/spreadsheet/sheet-tabs.ts"],"names":["quote","sameName","n","isBlank","cellKey","invalid"],"mappings":"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AACe,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACe,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACU,MAAA;AACK,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACoB,MAAA;AACD,MAAA;AACD,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACgB,MAAA;AACD,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACoB,MAAA;AACP,MAAA;AACK,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACe,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACU,MAAA;AACN,MAAA;AACE,QAAA;AACA,QAAA;AACb,MAAA;AACF,IAAA;AACA,IAAA;AACM,MAAA;AACiB,MAAA;AACC,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AAC0B,MAAA;AACR,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACqB,MAAA;AACR,MAAA;AACK,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACqB,MAAA;AACH,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACI,MAAA;AACW,MAAA;AACxB,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACU,MAAA;AACW,MAAA;AACjB,MAAA;AACa,QAAA;AACxB,MAAA;AACF,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACI,MAAA;AACA,MAAA;AACe,QAAA;AAC1B,MAAA;AACF,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACqB,MAAA;AACjB,MAAA;AACiB,QAAA;AAC5B,MAAA;AACF,IAAA;AACF,EAAA;AAG+B,EAAA;AACX,EAAA;AACG,IAAA;AACvB,EAAA;AAC+B,EAAA;AAClB,IAAA;AACqB,MAAA;AACN,MAAA;AACD,MAAA;AACE,MAAA;AACd,MAAA;AACE,QAAA;AACb,MAAA;AACD,IAAA;AACF,EAAA;AACU,EAAA;AACL,IAAA;AACG,IAAA;AACG,IAAA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AACvB,MAAA;AACH,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACuB,MAAA;AAEhB,MAAA;AACgB,QAAA;AACC,QAAA;AAC1B,MAAA;AACP,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACU,MAAA;AACW,QAAA;AAC3B,MAAA;AACH,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACmB,MAAA;AACT,MAAA;AACN,MAAA;AACY,QAAA;AACvB,MAAA;AACF,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACmB,MAAA;AACf,MAAA;AACY,QAAA;AACvB,MAAA;AACF,IAAA;AACA,IAAA;AACM,MAAA;AACG,MAAA;AACoB,MAAA;AACV,MAAA;AACa,MAAA;AACnB,MAAA;AACc,QAAA;AACzB,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAOkD;AAC3B,EAAA;AACD,EAAA;AACb,EAAA;AACT;AAiBmC;AAGzB,EAAA;AACA,IAAA;AACY,IAAA;AACN,MAAA;AACH,IAAA;AACqB,MAAA;AAElB,MAAA;AAIV,IAAA;AACO,IAAA;AACG,MAAA;AACI,MAAA;AACe,MAAA;AACE,MAAA;AAC7B,MAAA;AAC6B,MAAA;AACjB,MAAA;AACd,IAAA;AAEqB,EAAA;AAC3B;AtB41KoC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-WSQKHXW7.cjs","sourcesContent":[null,"import { ASTNode, parseFormula, WHOLE_COLUMN_END_ROW } from './parser';\nimport { colNameToIndex, indexToColName } from './evaluator';\n\nexport interface CellDep {\n col: string;\n /** Row number; 0 for a whole-column dependency (`A:A`). */\n row: number;\n /** Sheet of a sheet-qualified reference (`Sheet2!A1`); unset for the formula's own sheet. */\n sheet?: string;\n /** Last sheet of a 3-D reference (`Sheet1:Sheet3!A1`). */\n sheetEnd?: string;\n}\n\n/** A name used by a formula: a defined name, LET/LAMBDA variable or (with `call`) a function. */\nexport interface NameDep {\n /** Upper-case name. */\n name: string;\n /** Sheet of a sheet-qualified name (`Data!Rate`). */\n sheet?: string;\n /** Used as a function (`Double(4)`). */\n call: boolean;\n}\n\n/**\n * Names a formula uses (bare names and calls), for dependencies on defined\n * names. LET/LAMBDA variables are included; callers ignore names that are\n * not defined.\n */\nexport function extractNameDependencies(node: ASTNode): NameDep[] {\n const names: NameDep[] = [];\n const stack: ASTNode[] = [node];\n while (stack.length > 0) {\n const curr = stack.pop()!;\n switch (curr.type) {\n case 'Name':\n names.push(curr.sheet === undefined ? { name: curr.name, call: false } : { name: curr.name, sheet: curr.sheet, call: false });\n break;\n case 'FunctionCall':\n names.push({ name: curr.name, call: true });\n for (const arg of curr.args) stack.push(arg);\n break;\n case 'Invoke':\n stack.push(curr.callee);\n for (const arg of curr.args) stack.push(arg);\n break;\n case 'BinaryOp':\n stack.push(curr.left, curr.right);\n break;\n case 'UnaryOp':\n stack.push(curr.argument);\n break;\n default:\n break;\n }\n }\n return names;\n}\n\n/**\n * Dependency key for \"every cell in column `col`\", used by whole-column ranges\n * (`A:A`) instead of expanding a million cell keys.\n */\nexport const columnDependencyKey = (col: string): string => `${col.toUpperCase()}:`;\n\nexport function extractCellDependencies(node: ASTNode): CellDep[] {\n const deps: CellDep[] = [];\n\n const visit = (curr: ASTNode) => {\n const sheet: Pick<CellDep, 'sheet' | 'sheetEnd'> =\n 'sheet' in curr && curr.sheet !== undefined\n ? 'sheetEnd' in curr && curr.sheetEnd !== undefined\n ? { sheet: curr.sheet, sheetEnd: curr.sheetEnd }\n : { sheet: curr.sheet }\n : {};\n if (curr.type === 'CellRef') {\n deps.push({ col: curr.col, row: curr.row, ...sheet });\n } else if (curr.type === 'SpillRef') {\n // A1# is computed by A1: depending on the anchor orders it correctly.\n deps.push({ col: curr.col, row: curr.row, ...sheet });\n } else if (curr.type === 'Range' && curr.endRow >= WHOLE_COLUMN_END_ROW) {\n // Whole-column range: depend on the columns, not on every cell.\n const startC = colNameToIndex(curr.startCol);\n const endC = colNameToIndex(curr.endCol);\n for (let c = Math.min(startC, endC); c <= Math.max(startC, endC); c++) {\n deps.push({ col: indexToColName(c), row: 0, ...sheet });\n }\n } else if (curr.type === 'Range') {\n const startC = colNameToIndex(curr.startCol);\n const endC = colNameToIndex(curr.endCol);\n const minC = Math.min(startC, endC);\n const maxC = Math.max(startC, endC);\n const minR = Math.min(curr.startRow, curr.endRow);\n const maxR = Math.max(curr.startRow, curr.endRow);\n\n for (let c = minC; c <= maxC; c++) {\n for (let r = minR; r <= maxR; r++) {\n deps.push({ col: indexToColName(c), row: r, ...sheet });\n }\n }\n } else if (curr.type === 'BinaryOp') {\n visit(curr.left);\n visit(curr.right);\n } else if (curr.type === 'UnaryOp') {\n visit(curr.argument);\n } else if (curr.type === 'FunctionCall') {\n curr.args.forEach(visit);\n } else if (curr.type === 'Invoke') {\n visit(curr.callee);\n curr.args.forEach(visit);\n }\n };\n\n visit(node);\n return deps;\n}\n\nexport class DependencyGraph {\n // Map of cellKey (e.g. \"B2\") -> Set of cellKeys that this cell depends ON\n private dependencies = new Map<string, Set<string>>();\n // Map of cellKey (e.g. \"A1\") -> Set of cellKeys that depend ON this cell (dependents)\n private dependents = new Map<string, Set<string>>();\n // Dynamic-array edges, set by the controller after evaluating an anchor:\n // spilled cells depend on their anchor, and a blocked (#SPILL!) anchor\n // depends on the cells blocking it. Kept apart from formula dependencies,\n // so re-entering a formula doesn't drop them.\n private spillDependencies = new Map<string, Set<string>>();\n private spillDependents = new Map<string, Set<string>>();\n // anchor -> the edges it registered (so they can be replaced).\n private spillEdges = new Map<string, Array<[from: string, to: string]>>();\n\n /**\n * Sets the cells `cellKey` depends on directly (keys such as `B2`, or a\n * column key from `columnDependencyKey`), replacing its previous\n * dependencies. Used by spreadsheets whose keys carry more than A1 text\n * (e.g. sheet-qualified keys); `setCellFormula` derives them from a formula.\n */\n public setCellDependencies(cellKey: string, dependencyKeys: Iterable<string>): void {\n const key = cellKey.toUpperCase();\n for (const dep of this.dependencies.get(key) ?? []) {\n this.dependents.get(dep)?.delete(key);\n }\n this.dependencies.delete(key);\n const deps = new Set<string>();\n for (const raw of dependencyKeys) {\n const dep = raw.toUpperCase();\n deps.add(dep);\n if (!this.dependents.has(dep)) this.dependents.set(dep, new Set());\n this.dependents.get(dep)!.add(key);\n }\n if (deps.size > 0) this.dependencies.set(key, deps);\n }\n\n public setCellFormula(cellKey: string, formulaStr: string | null): void {\n const key = cellKey.toUpperCase();\n\n // Clear existing dependencies\n const oldDeps = this.dependencies.get(key) || new Set();\n for (const dep of oldDeps) {\n this.dependents.get(dep)?.delete(key);\n }\n this.dependencies.delete(key);\n\n if (!formulaStr || !formulaStr.startsWith('=')) {\n return;\n }\n\n try {\n const ast = parseFormula(formulaStr);\n const deps = extractCellDependencies(ast);\n const newDeps = new Set<string>();\n\n for (const d of deps) {\n const depKey = d.row === 0 ? columnDependencyKey(d.col) : `${d.col}${d.row}`.toUpperCase();\n newDeps.add(depKey);\n\n if (!this.dependents.has(depKey)) {\n this.dependents.set(depKey, new Set());\n }\n this.dependents.get(depKey)!.add(key);\n }\n\n this.dependencies.set(key, newDeps);\n } catch {\n // Syntax errors in formula have no valid dependencies\n }\n }\n\n /** Removes every registered formula and dependency. */\n public clear(): void {\n this.dependencies.clear();\n this.dependents.clear();\n this.spillDependencies.clear();\n this.spillDependents.clear();\n this.spillEdges.clear();\n }\n\n /**\n * Records the dynamic-array edges of the anchor `anchorKey`, replacing its\n * previous ones: every key in `spilledKeys` (cells holding its spilled\n * values) depends on the anchor, and the anchor depends on every key in\n * `blockingKeys` (cells that stop it spilling). Pass empty lists to remove\n * the anchor's edges.\n */\n public setSpillEdges(anchorKey: string, spilledKeys: Iterable<string>, blockingKeys: Iterable<string> = []): void {\n const anchor = anchorKey.toUpperCase();\n for (const [from, to] of this.spillEdges.get(anchor) ?? []) {\n this.spillDependencies.get(from)?.delete(to);\n this.spillDependents.get(to)?.delete(from);\n }\n const edges: Array<[string, string]> = [];\n for (const key of spilledKeys) edges.push([key.toUpperCase(), anchor]);\n for (const key of blockingKeys) edges.push([anchor, key.toUpperCase()]);\n for (const [from, to] of edges) {\n if (!this.spillDependencies.has(from)) this.spillDependencies.set(from, new Set());\n this.spillDependencies.get(from)!.add(to);\n if (!this.spillDependents.has(to)) this.spillDependents.set(to, new Set());\n this.spillDependents.get(to)!.add(from);\n }\n if (edges.length > 0) this.spillEdges.set(anchor, edges);\n else this.spillEdges.delete(anchor);\n }\n\n /** The spilled cells registered for `anchorKey` by `setSpillEdges`. */\n public getSpilledKeys(anchorKey: string): string[] {\n const anchor = anchorKey.toUpperCase();\n return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);\n }\n\n /** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */\n public getDependencies(cellKey: string): string[] {\n const key = cellKey.toUpperCase();\n return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));\n }\n\n /** Cells depending on `cellKey`, through formulas or spills. */\n public getDependents(cellKey: string): string[] {\n const key = cellKey.toUpperCase();\n return unionOf(this.dependents.get(key), this.spillDependents.get(key));\n }\n\n /**\n * Detects if there is a cycle containing cellKey.\n */\n public hasCycle(cellKey: string): boolean {\n const key = cellKey.toUpperCase();\n const visited = new Set<string>();\n const recStack = new Set<string>();\n\n const dfs = (node: string): boolean => {\n visited.add(node);\n recStack.add(node);\n\n const deps = this.getDependencies(node);\n for (const neighbor of deps) {\n if (!visited.has(neighbor)) {\n if (dfs(neighbor)) return true;\n } else if (recStack.has(neighbor)) {\n return true;\n }\n }\n\n recStack.delete(node);\n return false;\n };\n\n return dfs(key);\n }\n\n /**\n * Returns cells affected by updating cellKey in topological order.\n */\n public getEvaluationOrder(startCellKey: string): { order: string[]; hasCycle: boolean } {\n const start = startCellKey.toUpperCase();\n const visited = new Set<string>();\n const recStack = new Set<string>();\n const order: string[] = [];\n let cycleDetected = false;\n\n // Collect all downstream affected cells (reachability via dependents)\n const affected = new Set<string>();\n const collectAffected = (node: string) => {\n affected.add(node);\n const directDependents = this.dependents.get(node) || new Set();\n for (const dep of directDependents) {\n if (!affected.has(dep)) {\n collectAffected(dep);\n }\n }\n };\n collectAffected(start);\n\n // Topological sort among affected cells\n const visit = (node: string) => {\n if (recStack.has(node)) {\n cycleDetected = true;\n return;\n }\n if (visited.has(node)) return;\n\n recStack.add(node);\n visited.add(node);\n\n // Visit nodes that this node depends on, IF they are in the affected set\n const deps = this.dependencies.get(node) || new Set();\n for (const neighbor of deps) {\n if (affected.has(neighbor)) {\n visit(neighbor);\n }\n }\n\n recStack.delete(node);\n order.push(node);\n };\n\n for (const node of affected) {\n if (!visited.has(node)) {\n visit(node);\n }\n }\n\n return { order, hasCycle: cycleDetected };\n }\n\n /**\n * Plans a recalculation over `cellKeys` (and, with `includeDependents`, every\n * cell that transitively depends on them). Returns the keys in evaluation\n * order (dependencies first) and the set of keys that are part of a cycle,\n * which must be marked `#CYCLE!` instead of evaluated. Every member of a\n * cycle is reported, not just the cell that closed it.\n */\n public getEvaluationPlan(\n cellKeys: Iterable<string>,\n options: { includeDependents?: boolean } = {},\n ): { order: string[]; cyclic: Set<string> } {\n const nodes = new Set<string>();\n const queue: string[] = [];\n for (const key of cellKeys) {\n const upper = key.toUpperCase();\n if (!nodes.has(upper)) {\n nodes.add(upper);\n queue.push(upper);\n }\n }\n if (options.includeDependents) {\n while (queue.length > 0) {\n const node = queue.pop()!;\n const direct = [\n ...(this.dependents.get(node) ?? []),\n ...(this.spillDependents.get(node) ?? []),\n ...(this.dependents.get(columnDependencyKey(node.replace(/\\d+$/, ''))) ?? []),\n ];\n for (const dep of direct) {\n if (!nodes.has(dep)) {\n nodes.add(dep);\n queue.push(dep);\n }\n }\n }\n }\n\n // Edges point from a cell to the cells it depends on, restricted to `nodes`.\n // Whole-column dependencies expand to the planned cells in that column.\n const neighbors = (node: string): string[] => {\n const result: string[] = [];\n for (const dep of this.getDependencies(node)) {\n if (dep.endsWith(':')) {\n const col = dep.slice(0, -1);\n for (const candidate of nodes) {\n if (candidate.replace(/\\d+$/, '') === col) result.push(candidate);\n }\n } else if (nodes.has(dep)) {\n result.push(dep);\n }\n }\n return result;\n };\n\n // Iterative Tarjan: SCCs are emitted dependencies-first.\n const order: string[] = [];\n const cyclic = new Set<string>();\n const index = new Map<string, number>();\n const low = new Map<string, number>();\n const stack: string[] = [];\n const onStack = new Set<string>();\n let counter = 0;\n\n for (const root of nodes) {\n if (index.has(root)) continue;\n const work: Array<{ node: string; edges: string[]; next: number }> = [];\n const enter = (node: string) => {\n index.set(node, counter);\n low.set(node, counter);\n counter++;\n stack.push(node);\n onStack.add(node);\n work.push({ node, edges: neighbors(node), next: 0 });\n };\n enter(root);\n\n while (work.length > 0) {\n const frame = work[work.length - 1];\n if (frame.next < frame.edges.length) {\n const target = frame.edges[frame.next++];\n if (!index.has(target)) {\n enter(target);\n } else if (onStack.has(target)) {\n low.set(frame.node, Math.min(low.get(frame.node)!, index.get(target)!));\n }\n continue;\n }\n\n work.pop();\n if (work.length > 0) {\n const parent = work[work.length - 1].node;\n low.set(parent, Math.min(low.get(parent)!, low.get(frame.node)!));\n }\n\n if (low.get(frame.node) === index.get(frame.node)) {\n const component: string[] = [];\n let member: string;\n do {\n member = stack.pop()!;\n onStack.delete(member);\n component.push(member);\n } while (member !== frame.node);\n\n const isCycle = component.length > 1 || frame.edges.includes(frame.node);\n for (const key of component) {\n order.push(key);\n if (isCycle) cyclic.add(key);\n }\n }\n }\n }\n\n return { order, cyclic };\n }\n}\n\nfunction unionOf(a: Set<string> | undefined, b: Set<string> | undefined): string[] {\n if (!b || b.size === 0) return Array.from(a ?? []);\n if (!a || a.size === 0) return Array.from(b);\n return Array.from(new Set([...a, ...b]));\n}\n","// Portions ported from gridcraft/crates/formula/src/adjust.rs and\n// gridcraft/crates/engine/src/cmd/formulas.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport { tokenizeFormula, type FormulaToken } from './lexer';\nimport { colNameToIndex, indexToColName } from './evaluator';\n\n/**\n * A row or column insertion/deletion. `index` is the 0-based position of the\n * inserted or deleted row/column.\n */\nexport interface StructuralChange {\n axis: 'row' | 'col';\n index: number;\n type: 'insert' | 'delete';\n}\n\nconst CELL_PART = /^(\\$?)([A-Za-z]+)(\\$?)(\\d+)$/;\nconst COLUMN_PART = /^(\\$?)([A-Za-z]+)$/;\nconst REF_CHAR = /[A-Za-z0-9_$.:]/;\n\ninterface RefPart {\n colAbs: string;\n col: number; // 0-based column index\n rowAbs: string;\n row: number | null; // 0-based row index; null for whole-column parts (A:A)\n}\n\nfunction parsePart(text: string): RefPart | null {\n const cell = CELL_PART.exec(text);\n if (cell) {\n return {\n colAbs: cell[1],\n col: colNameToIndex(cell[2]),\n rowAbs: cell[3],\n row: parseInt(cell[4], 10) - 1,\n };\n }\n const column = COLUMN_PART.exec(text);\n if (column) {\n return { colAbs: column[1], col: colNameToIndex(column[2]), rowAbs: '', row: null };\n }\n return null;\n}\n\nfunction formatPart(part: RefPart): string {\n const col = `${part.colAbs}${indexToColName(part.col)}`;\n return part.row === null ? col : `${col}${part.rowAbs}${part.row + 1}`;\n}\n\n/** Shifts one coordinate; returns null when it points at a deleted row/column. */\nfunction shiftCoordinate(value: number, change: StructuralChange): number | null {\n if (change.type === 'insert') {\n return value >= change.index ? value + 1 : value;\n }\n if (value === change.index) return null;\n return value > change.index ? value - 1 : value;\n}\n\n/** Shifts a range's [lo, hi] bounds; returns null when the whole span is deleted. */\nfunction shiftSpan(lo: number, hi: number, change: StructuralChange): [number, number] | null {\n if (change.type === 'insert') {\n return [lo >= change.index ? lo + 1 : lo, hi >= change.index ? hi + 1 : hi];\n }\n if (lo === change.index && hi === change.index) return null;\n // Deleting inside or at the edge of a range shrinks it, like spreadsheets do.\n return [lo > change.index ? lo - 1 : lo, hi >= change.index ? hi - 1 : hi];\n}\n\nfunction rewriteReference(text: string, change: StructuralChange): string {\n const [startText, endText] = text.split(':');\n const start = parsePart(startText);\n if (!start) return text;\n\n const key = change.axis === 'row' ? 'row' : 'col';\n\n if (endText === undefined) {\n if (start[key] === null) return text;\n const shifted = shiftCoordinate(start[key] as number, change);\n if (shifted === null) return '#REF!';\n return formatPart({ ...start, [key]: shifted });\n }\n\n const end = parsePart(endText);\n if (!end) return text;\n if (start[key] === null || end[key] === null) return text; // e.g. rows of A:A\n\n const startValue = start[key] as number;\n const endValue = end[key] as number;\n const span = shiftSpan(Math.min(startValue, endValue), Math.max(startValue, endValue), change);\n if (span === null) return '#REF!';\n const [lo, hi] = span;\n const newStart = { ...start, [key]: startValue <= endValue ? lo : hi };\n const newEnd = { ...end, [key]: startValue <= endValue ? hi : lo };\n return `${formatPart(newStart)}:${formatPart(newEnd)}`;\n}\n\n/**\n * Rewrites the cell and range references in `formula` after a row or column\n * is inserted or deleted, the way spreadsheet apps do: references after the\n * change shift, ranges grow or shrink, and references to a deleted row/column\n * become `#REF!`. Absolute (`$`) markers are kept. String literals, property\n * references and function names are left untouched.\n */\nexport function shiftFormulaReferences(\n formula: string,\n change: StructuralChange,\n options: FormulaReferenceFilter = {},\n): string {\n return mapFormulaReferences(formula, (text, info) =>\n !options.appliesTo || options.appliesTo(info.sheet, info.sheetEnd) ? rewriteReference(text, change) : text,\n );\n}\n\n/**\n * Limits a rewrite to some references, e.g. only those pointing at the sheet\n * that changed: `appliesTo(sheet)` gets the reference's sheet (undefined for\n * the formula's own sheet) and, for 3-D references, the last sheet.\n */\nexport interface FormulaReferenceFilter {\n appliesTo?: (sheet: string | undefined, sheetEnd: string | undefined) => boolean;\n}\n\n/** Where a reference passed to a `mapFormulaReferences` callback points. */\nexport interface FormulaReferenceInfo {\n /** Sheet of a qualified reference (`Sheet2!A1`), unquoted; undefined for the formula's own sheet. */\n sheet?: string;\n /** Last sheet of a 3-D reference. */\n sheetEnd?: string;\n}\n\n/**\n * Calls `rewrite` with the source text of every cell or range reference in\n * `formula` (e.g. `\"$A1\"`, `\"B2:C10\"`, `\"A:A\"`) and splices in its result.\n * For a spill reference (`A1#`) it receives the anchor (`\"A1\"`) and the `#`\n * is kept, unless the rewrite returns `#REF!`. A sheet prefix (`Sheet2!`,\n * `'My Sheet'!`) is not part of the text: `info` says which sheet the\n * reference points at, and the prefix is kept unless the rewrite returns\n * `#REF!`. String literals, property references and function names (e.g.\n * `LOG10(`) are skipped. Formulas that fail to tokenize are returned unchanged.\n */\nexport function mapFormulaReferences(\n formula: string,\n rewrite: (reference: string, info: FormulaReferenceInfo) => string,\n): string {\n if (!formula.startsWith('=')) return formula;\n\n let tokens;\n try {\n tokens = tokenizeFormula(formula);\n } catch {\n return formula;\n }\n\n let result = '';\n let cursor = 0;\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (token.type !== 'CELL_REF' && token.type !== 'RANGE' && token.type !== 'SPILL_REF') continue;\n // Function names that look like cell references (e.g. LOG10) are calls.\n if (tokens[i + 1]?.type === 'LPAREN') continue;\n\n const refStart = token.refStart ?? token.position;\n let end = refStart;\n while (end < formula.length && REF_CHAR.test(formula[end])) end++;\n const original = formula.slice(refStart, end);\n const info: FormulaReferenceInfo =\n token.sheet === undefined ? {} : token.sheetEnd === undefined ? { sheet: token.sheet } : { sheet: token.sheet, sheetEnd: token.sheetEnd };\n const prefix = formula.slice(token.position, refStart);\n\n if (token.type === 'SPILL_REF') {\n // A1#: the anchor is rewritten like a cell reference; `#` stays.\n const rewritten = rewrite(original, info);\n result += formula.slice(cursor, token.position) + (rewritten === '#REF!' ? rewritten : `${prefix}${rewritten}#`);\n cursor = end + 1;\n continue;\n }\n\n const rewritten = rewrite(original, info);\n result += formula.slice(cursor, token.position) + (rewritten === '#REF!' ? rewritten : prefix + rewritten);\n cursor = end;\n }\n return result + formula.slice(cursor);\n}\n\n/**\n * Moves a formula by (`rowOffset`, `colOffset`), as copy/paste and autofill\n * do: relative row/column parts shift, `$`-absolute parts stay. A reference\n * pushed above row 1 or left of column A becomes `#REF!`.\n */\nexport function offsetFormulaReferences(formula: string, rowOffset: number, colOffset: number): string {\n if (rowOffset === 0 && colOffset === 0) return formula;\n return mapFormulaReferences(formula, (text) => {\n const parts = text.split(':').map(parsePart);\n if (parts.some((p) => p === null)) return text;\n const moved: RefPart[] = [];\n for (const part of parts as RefPart[]) {\n const col = part.colAbs ? part.col : part.col + colOffset;\n const row = part.row === null || part.rowAbs ? part.row : part.row + rowOffset;\n if (col < 0 || (row !== null && row < 0)) return '#REF!';\n moved.push({ ...part, col, row });\n }\n return moved.map(formatPart).join(':');\n });\n}\n\n/** A 0-based cell position. */\nexport interface CellPosition {\n row: number;\n col: number;\n}\n\n// Ranges larger than this are never remapped cell by cell.\nconst MAX_REMAP_RANGE_CELLS = 100_000;\n\n/**\n * Rewrites references when cells *move* (sort, transpose): references follow\n * the cell they point at, whether relative or absolute. `moveCell` returns a\n * cell's new position, or null when it did not move. A range is remapped only\n * when its cells moved together as a rectangle of the same size (e.g. a\n * one-row range inside a sorted row, or a range inside a transposed block);\n * otherwise it is kept, since it still covers the same rows and columns.\n */\nexport function remapFormulaReferences(\n formula: string,\n moveCell: (position: CellPosition) => CellPosition | null,\n options: FormulaReferenceFilter = {},\n): string {\n return mapFormulaReferences(formula, (text, info) => {\n if (options.appliesTo && !options.appliesTo(info.sheet, info.sheetEnd)) return text;\n const parts = text.split(':').map(parsePart);\n if (parts.some((p) => p === null)) return text;\n const [start, end] = parts as RefPart[];\n if (start.row === null || (end && end.row === null)) return text; // whole columns\n\n if (!end) {\n const target = moveCell({ row: start.row, col: start.col });\n return target ? formatPart({ ...start, row: target.row, col: target.col }) : text;\n }\n\n const lo = { row: Math.min(start.row, end.row!), col: Math.min(start.col, end.col) };\n const hi = { row: Math.max(start.row, end.row!), col: Math.max(start.col, end.col) };\n const area = (hi.row - lo.row + 1) * (hi.col - lo.col + 1);\n if (area > MAX_REMAP_RANGE_CELLS) return text;\n\n let minR = Infinity;\n let maxR = -Infinity;\n let minC = Infinity;\n let maxC = -Infinity;\n let movedAny = false;\n const targets = new Set<string>();\n for (let r = lo.row; r <= hi.row; r++) {\n for (let c = lo.col; c <= hi.col; c++) {\n const moved = moveCell({ row: r, col: c });\n if (moved) movedAny = true;\n const t = moved ?? { row: r, col: c };\n targets.add(`${t.row}:${t.col}`);\n minR = Math.min(minR, t.row);\n maxR = Math.max(maxR, t.row);\n minC = Math.min(minC, t.col);\n maxC = Math.max(maxC, t.col);\n }\n }\n // The cells must still form a filled rectangle of the same area.\n if (!movedAny || targets.size !== area || (maxR - minR + 1) * (maxC - minC + 1) !== area) {\n return text;\n }\n return `${formatPart({ ...start, row: minR, col: minC })}:${formatPart({ ...end, row: maxR, col: maxC })}`;\n });\n}\n\n// --- Sheets and defined names ---\n\n// A1-style reference within Excel's grid (columns up to XFD).\nconst CELL_LIKE = /^[A-Za-z]{1,3}\\d{1,7}$/;\nconst isCellLike = (name: string) => {\n if (!CELL_LIKE.test(name)) return false;\n const letters = name.match(/^[A-Za-z]+/)![0].toUpperCase();\n const col = Array.from(letters).reduce((n, ch) => n * 26 + ch.charCodeAt(0) - 64, 0);\n const row = Number(name.slice(letters.length));\n return col <= 16384 && row >= 1 && row <= 1048576;\n};\nconst R1C1_LIKE = /^[Rr]\\d*(?:[Cc]\\d*)?$|^[Cc]\\d*$/;\n\n/**\n * Whether a sheet name must be quoted in a formula (`'My Sheet'!A1`): names\n * with spaces or punctuation, starting with a digit, or that look like a\n * cell reference (`A1`, `R1C1`) or a boolean.\n */\nexport function sheetNameNeedsQuotes(name: string): boolean {\n if (!/^[A-Za-z_\\p{L}][\\p{L}\\p{N}_.]*$/u.test(name)) return true;\n const upper = name.toUpperCase();\n return isCellLike(name) || R1C1_LIKE.test(name) || upper === 'TRUE' || upper === 'FALSE';\n}\n\n/** A sheet name as written in formulas: `Data`, `'My Sheet'`, `'Bob''s'`. */\nexport function quoteSheetName(name: string): string {\n return sheetNameNeedsQuotes(name) ? `'${name.replace(/'/g, \"''\")}'` : name;\n}\n\n/** The prefix of a reference to `sheet` (or the 3-D span `sheet:sheetEnd`), with `!`. */\nexport function formatSheetPrefix(sheet: string, sheetEnd?: string): string {\n if (sheetEnd === undefined) return `${quoteSheetName(sheet)}!`;\n const quoted = sheetNameNeedsQuotes(sheet) || sheetNameNeedsQuotes(sheetEnd);\n return quoted ? `'${`${sheet}:${sheetEnd}`.replace(/'/g, \"''\")}'!` : `${sheet}:${sheetEnd}!`;\n}\n\nfunction tokensOf(formula: string): FormulaToken[] | null {\n if (!formula.startsWith('=')) return null;\n try {\n return tokenizeFormula(formula);\n } catch {\n return null;\n }\n}\n\n/** Replaces token spans: `edit` returns the new text for a token, or null to keep it. */\nfunction rewriteTokens(formula: string, edit: (token: FormulaToken) => { from: number; to: number; text: string } | null): string {\n const tokens = tokensOf(formula);\n if (!tokens) return formula;\n let result = '';\n let cursor = 0;\n for (const token of tokens) {\n const change = edit(token);\n if (!change) continue;\n result += formula.slice(cursor, change.from) + change.text;\n cursor = change.to;\n }\n return cursor === 0 ? formula : result + formula.slice(cursor);\n}\n\nconst sameName = (a: string, b: string) => a.toUpperCase() === b.toUpperCase();\n\n/**\n * Rewrites references to the sheet `oldName` (cell, range, spill and 3-D\n * references, and sheet-qualified names such as `Old!Rate`) after it is\n * renamed to `newName`, quoting the new name when needed.\n */\nexport function renameSheetInFormula(formula: string, oldName: string, newName: string): string {\n return rewriteTokens(formula, (token) => {\n if (token.sheet === undefined || token.refStart === undefined) return null;\n const first = sameName(token.sheet, oldName);\n const last = token.sheetEnd !== undefined && sameName(token.sheetEnd, oldName);\n if (!first && !last) return null;\n const sheet = first ? newName : token.sheet;\n const sheetEnd = token.sheetEnd === undefined ? undefined : last ? newName : token.sheetEnd;\n return { from: token.position, to: token.refStart, text: formatSheetPrefix(sheet, sheetEnd) };\n });\n}\n\n/**\n * Rewrites a formula after the sheet `name` is deleted: references to it\n * become `#REF!`. `order` lists the sheet names before the deletion, in tab\n * order: a 3-D reference losing an end sheet shrinks to the neighbouring\n * sheet, as in Excel (`Sheet1:Sheet3!A1` without Sheet1 is `Sheet2:Sheet3!A1`).\n */\nexport function deleteSheetInFormula(formula: string, name: string, order: readonly string[] = []): string {\n const position = (sheet: string) => order.findIndex((s) => sameName(s, sheet));\n return rewriteTokens(formula, (token) => {\n if (token.sheet === undefined || token.refStart === undefined) return null;\n const end = token.end ?? token.refStart;\n if (token.sheetEnd === undefined) {\n return sameName(token.sheet, name) ? { from: token.position, to: end, text: '#REF!' } : null;\n }\n const first = sameName(token.sheet, name);\n const last = sameName(token.sheetEnd, name);\n if (!first && !last) return null;\n const a = position(token.sheet);\n const b = position(token.sheetEnd);\n if (a < 0 || b < 0 || a === b) return { from: token.position, to: end, text: '#REF!' };\n // The deleted end moves one sheet towards the other end.\n const inward = (from: number, to: number) => order[from < to ? from + 1 : from - 1];\n const newFirst = first ? inward(a, b) : token.sheet;\n const newLast = last ? inward(b, a) : token.sheetEnd;\n const prefix = sameName(newFirst, newLast) ? formatSheetPrefix(newFirst) : formatSheetPrefix(newFirst, newLast);\n return { from: token.position, to: token.refStart, text: prefix };\n });\n}\n\n/**\n * Renames uses of the defined name `oldName` (`Rate`, `Rate*2`, `Double(4)`,\n * `Data!Rate`) to `newName`. `appliesTo(sheet)` decides, for a\n * sheet-qualified use, whether that sheet's name is the renamed one; unset,\n * every use is renamed. With `qualifiedOnly`, unqualified uses are kept (the\n * formula is on a sheet where the name means something else).\n */\nexport function renameNameInFormula(\n formula: string,\n oldName: string,\n newName: string,\n options: { appliesTo?: (sheet: string) => boolean; qualifiedOnly?: boolean } = {},\n): string {\n return rewriteTokens(formula, (token) => {\n if (token.type !== 'IDENTIFIER' || !sameName(token.value, oldName)) return null;\n if (token.sheet !== undefined ? options.appliesTo && !options.appliesTo(token.sheet) : options.qualifiedOnly) return null;\n const from = token.refStart ?? token.position;\n return { from, to: token.end ?? from + oldName.length, text: newName };\n });\n}\n\n/** Upper-case names used in a formula (bare names, calls and sheet-qualified names). */\nexport function formulaIdentifiers(formula: string): Array<{ name: string; sheet?: string; call: boolean }> {\n const tokens = tokensOf(formula);\n if (!tokens) return [];\n const out: Array<{ name: string; sheet?: string; call: boolean }> = [];\n tokens.forEach((token, i) => {\n if (token.type !== 'IDENTIFIER') return;\n const call = tokens[i + 1]?.type === 'LPAREN';\n out.push(token.sheet === undefined ? { name: token.value, call } : { name: token.value, sheet: token.sheet, call });\n });\n return out;\n}\n","import {\n getFormulaArgumentSpecIndex,\n getFormulaFunction,\n getFormulaSignature,\n getFormulaSignatureParts,\n searchFormulaFunctions,\n type FormulaArgumentSpec,\n type FormulaFunctionDefinition,\n type FormulaSignaturePart,\n} from './registry';\n\n/**\n * Caret analysis for formula autocomplete. The input is scanned tolerantly\n * (unterminated strings and unbalanced parentheses are expected while typing),\n * so this never throws.\n */\nexport interface FormulaCaretContext {\n /** The function-name prefix being typed right before the caret, if any. */\n prefix: { text: string; start: number; end: number } | null;\n /** The innermost function call containing the caret, if any. */\n call: {\n name: string;\n /** 0-based index of the argument the caret is in. */\n argIndex: number;\n /** Offset of the call's opening parenthesis. */\n openParen: number;\n } | null;\n}\n\nconst EMPTY: FormulaCaretContext = { prefix: null, call: null };\n\n/** Determines what is being typed at `caret` in `input` (a formula starting with `=`). */\nexport function getFormulaCaretContext(input: string, caret: number = input.length): FormulaCaretContext {\n const leading = input.length - input.trimStart().length;\n if (input[leading] !== '=') return EMPTY;\n const end = Math.max(0, Math.min(caret, input.length));\n if (end <= leading) return EMPTY;\n\n const stack: Array<{ name: string | null; argIndex: number; openParen: number }> = [];\n let quote: string | null = null;\n let inProperty = false;\n\n for (let i = leading + 1; i < end; i++) {\n const char = input[i];\n if (quote) {\n if (char === '\\\\') i++;\n else if (char === quote) quote = null;\n continue;\n }\n if (inProperty) {\n if (char === ']') inProperty = false;\n continue;\n }\n if (char === '\"' || char === \"'\") {\n quote = char;\n } else if (char === '[') {\n inProperty = true;\n } else if (char === '(') {\n const name = /([A-Za-z_][A-Za-z0-9_.]*)\\s*$/.exec(input.slice(leading + 1, i));\n const isFunction = name && !/^\\$?[A-Za-z]+\\$?\\d+$/.test(name[1]) && !/[:$]$/.test(input.slice(0, i - name[0].length));\n stack.push({ name: isFunction ? name![1].toUpperCase() : null, argIndex: 0, openParen: i });\n } else if (char === ')') {\n stack.pop();\n } else if (char === ',' && stack.length > 0) {\n stack[stack.length - 1].argIndex++;\n }\n }\n\n if (quote || inProperty) {\n // Inside a string or [property]: no name completion, but keep the call hint.\n return { prefix: null, call: innermostCall(stack) };\n }\n\n let prefix: FormulaCaretContext['prefix'] = null;\n const before = input.slice(leading + 1, end);\n const word = /[A-Za-z_][A-Za-z0-9_.]*$/.exec(before);\n if (word) {\n const start = leading + 1 + word.index;\n const preceding = input[start - 1] ?? '';\n // Not a range end (A1:B), an absolute ref ($A) or part of a number (1E5).\n if (!/[:$0-9.]/.test(preceding)) {\n prefix = { text: word[0], start, end };\n }\n }\n\n return { prefix, call: innermostCall(stack) };\n}\n\nfunction innermostCall(stack: Array<{ name: string | null; argIndex: number; openParen: number }>) {\n for (let i = stack.length - 1; i >= 0; i--) {\n const frame = stack[i];\n if (frame.name) return { name: frame.name, argIndex: frame.argIndex, openParen: frame.openParen };\n }\n return null;\n}\n\n/** A function suggestion for autocomplete. */\nexport interface FormulaFunctionSuggestion {\n name: string;\n signature: string;\n description: string;\n category: FormulaFunctionDefinition['category'];\n /**\n * `name` for a defined name (accepting it inserts the name as defined,\n * followed by `(` only for a LAMBDA name); unset for functions.\n */\n kind?: 'name';\n /** For a defined name: it holds a LAMBDA, so it is called like a function. */\n isLambda?: boolean;\n}\n\n/** A defined name offered by autocomplete (see `SpreadsheetController.getNameSuggestions`). */\nexport interface FormulaNameSuggestionSource {\n name: string;\n description?: string;\n /** A LAMBDA name is called like a function. */\n isLambda?: boolean;\n}\n\nexport interface FormulaActiveCall {\n name: string;\n signature: string;\n description: string;\n /** Signature split into parts, with the active argument marked. */\n parts: FormulaSignaturePart[];\n /** 0-based argument index at the caret. */\n argIndex: number;\n /** Spec of the active argument, if the function takes an argument there. */\n argument: FormulaArgumentSpec | null;\n}\n\nexport interface FormulaHints {\n context: FormulaCaretContext;\n /** Functions matching the name prefix at the caret (empty when not typing a name). */\n suggestions: FormulaFunctionSuggestion[];\n /** The known function whose arguments the caret is in. */\n activeCall: FormulaActiveCall | null;\n}\n\nconst toSuggestion = (def: FormulaFunctionDefinition): FormulaFunctionSuggestion => ({\n name: def.name,\n signature: getFormulaSignature(def),\n description: def.description,\n category: def.category,\n});\n\n/** Everything formula autocomplete UIs need for `input` with the caret at `caret`. */\nexport function getFormulaHints(\n input: string,\n caret: number = input.length,\n options: { limit?: number; names?: readonly FormulaNameSuggestionSource[] } = {},\n): FormulaHints {\n const context = getFormulaCaretContext(input, caret);\n let suggestions: FormulaFunctionSuggestion[] = [];\n if (context.prefix) {\n const query = context.prefix.text.toUpperCase();\n // Suggest names that start with what was typed; hide an exact, complete\n // match. Defined names come first: they are specific to this workbook.\n const names: FormulaFunctionSuggestion[] = (options.names ?? [])\n .filter((n) => n.name.toUpperCase().startsWith(query))\n .map((n) => ({\n name: n.name,\n signature: n.isLambda ? `${n.name}(…)` : n.name,\n description: n.description ?? '',\n category: 'lookup' as const,\n kind: 'name' as const,\n ...(n.isLambda ? { isLambda: true } : {}),\n }));\n suggestions = [\n ...names,\n ...searchFormulaFunctions(query, { limit: undefined })\n .filter((def) => def.name.startsWith(query) && !names.some((n) => n.name.toUpperCase() === def.name))\n .map(toSuggestion),\n ].slice(0, options.limit ?? 8);\n const followedByParen = input.slice(context.prefix.end).trimStart().startsWith('(');\n if (followedByParen || (suggestions.length === 1 && suggestions[0].name.toUpperCase() === query)) suggestions = [];\n }\n\n let activeCall: FormulaActiveCall | null = null;\n if (context.call) {\n const def = getFormulaFunction(context.call.name);\n if (def) {\n const specIndex = getFormulaArgumentSpecIndex(def, context.call.argIndex);\n activeCall = {\n name: def.name,\n signature: getFormulaSignature(def),\n description: def.description,\n parts: getFormulaSignatureParts(def, context.call.argIndex),\n argIndex: context.call.argIndex,\n argument: specIndex >= 0 ? def.args[specIndex] : null,\n };\n }\n }\n\n return { context, suggestions, activeCall };\n}\n\n/**\n * Replaces the name prefix at the caret with `NAME(`, returning the new input\n * and caret position (just inside the parenthesis). Given a defined-name\n * suggestion (`kind: 'name'`), the name keeps its case and gets a `(` only\n * when it holds a LAMBDA; `call: false` inserts any name without `(`.\n */\nexport function applyFormulaSuggestion(\n input: string,\n caret: number,\n suggestion: string | Pick<FormulaFunctionSuggestion, 'name' | 'kind' | 'isLambda'>,\n options: { call?: boolean } = {},\n): { value: string; caret: number } {\n const context = getFormulaCaretContext(input, caret);\n const start = context.prefix?.start ?? caret;\n const end = context.prefix?.end ?? caret;\n const definedName = typeof suggestion !== 'string' && suggestion.kind === 'name';\n const name = typeof suggestion === 'string' ? suggestion : suggestion.name;\n const call = options.call ?? (definedName ? !!(suggestion as FormulaFunctionSuggestion).isLambda : true);\n if (!call) {\n // A defined name: inserted as defined, without a parenthesis.\n const value = input.slice(0, start) + name + input.slice(end);\n return { value, caret: start + name.length };\n }\n // Skip an existing \"(\" so accepting inside \"SU|(\" doesn't double it.\n const hasParen = input[end] === '(';\n const spelled = definedName ? name : name.toUpperCase();\n const insert = hasParen ? spelled : `${spelled}(`;\n const value = input.slice(0, start) + insert + input.slice(end);\n return { value, caret: start + insert.length + (hasParen ? 1 : 0) };\n}\n","/**\n * Common number formats offered by the \"Format cells\" menu, and the mapping\n * between format codes and the coarser `CellFormat` kinds.\n */\nimport type { CellFormat } from '../types';\nimport { NumberFormat } from './index';\n\nexport interface NumberFormatPreset {\n id: 'general' | 'number' | 'currency' | 'percent' | 'date' | 'time';\n label: string;\n code: string;\n}\n\n/** Presets of the \"Format cells\" menu (en-US). */\nexport const NUMBER_FORMAT_PRESETS: readonly NumberFormatPreset[] = [\n { id: 'general', label: 'General', code: 'General' },\n { id: 'number', label: 'Number (0.00)', code: '0.00' },\n { id: 'currency', label: 'Currency ($1,234.50)', code: '\"$\"#,##0.00' },\n { id: 'percent', label: 'Percent (12.5%)', code: '0.0%' },\n { id: 'date', label: 'Date (3/14/2026)', code: 'm/d/yyyy' },\n { id: 'time', label: 'Time (1:30 PM)', code: 'h:mm AM/PM' },\n];\n\n/**\n * Format code for a `CellFormat` kind, used to display numbers in cells that\n * have a kind but no format code: `currency` → `\"$\"#,##0.00`, `percentage` →\n * `0%`, `date` → `m/d/yyyy`. Other kinds (`number`, `text` — the default of\n * new columns —, `badge`, `status`, `assignee`) display as General (undefined).\n */\nexport function numberFormatForCellFormat(format: CellFormat | undefined): string | undefined {\n switch (format) {\n case 'currency':\n return '\"$\"#,##0.00';\n case 'percentage':\n return '0%';\n case 'date':\n return 'm/d/yyyy';\n default:\n return undefined;\n }\n}\n\n/**\n * `CellFormat` kind matching a format code (`\"$\"#,##0.00` → `currency`,\n * `0%` → `percentage`, `m/d/yyyy` → `date`, `0.00` → `number`, `@` → `text`);\n * undefined for General, times and custom codes.\n */\nexport function cellFormatForNumberFormat(code: string): CellFormat | undefined {\n switch (NumberFormat.parseCached(code).kind()) {\n case 'number':\n case 'fraction':\n case 'scientific':\n case 'special':\n return 'number';\n case 'currency':\n case 'accounting':\n return 'currency';\n case 'percentage':\n return 'percentage';\n case 'date':\n return 'date';\n case 'text':\n return 'text';\n default:\n return undefined;\n }\n}\n\n/** True for an empty or `General` code. */\nexport const isGeneralNumberFormat = (code: string | undefined): boolean =>\n code === undefined || NumberFormat.parseCached(code).isGeneral();\n","import type { CellData } from '../types';\nimport { formatTextFunction, NumberFormat, type FormattedNumberValue } from '../numfmt';\nimport { isGeneralNumberFormat, numberFormatForCellFormat } from '../numfmt/presets';\nimport { isBlank, toText } from './values';\n\ntype DisplayCell = Pick<CellData, 'value' | 'error' | 'numberFormat' | 'format'>;\n\n/**\n * How a cell is shown: its error, or its value formatted with its number\n * format code (`numberFormat`: set explicitly, inherited from the column, or\n * chosen for typed input, e.g. `\"$\"#,##0.00` for `$1,200.50`). Numbers in a\n * cell with a `currency` / `percentage` / `date` `CellFormat` but no code use\n * that kind's code. Without a code (General), numbers use up to 15\n * significant digits and booleans show as TRUE/FALSE.\n *\n * `numeric` is true for numbers (and dates shown through a date format):\n * grids right-align those. `color` comes from a `[Red]` / `[ColorN]` tag.\n */\nexport function formatCellForDisplay(cell: DisplayCell | null | undefined): FormattedNumberValue {\n if (!cell) return { text: '', numeric: false };\n if (cell.error) return { text: cell.error, numeric: false };\n const { value } = cell;\n const code = cell.numberFormat ?? (typeof value === 'number' ? numberFormatForCellFormat(cell.format) : undefined);\n if (code !== undefined && !isGeneralNumberFormat(code) && !isBlank(value) && typeof value !== 'boolean') {\n return NumberFormat.parseCached(code).format(value);\n }\n return { text: toText(value), numeric: typeof value === 'number' };\n}\n\n/** The text a cell shows (see `formatCellForDisplay`). Used by grids, CSV export and the clipboard. */\nexport function formatCellDisplay(cell: DisplayCell | null | undefined): string {\n return formatCellForDisplay(cell).text;\n}\n\n/**\n * Formats a value with a number format code, as `TEXT()` does (see\n * `formatTextFunction`). Null when the value can't be shown with that code\n * (a date out of range, a code over 255 characters, an error value).\n */\nexport function formatValueWithCode(value: unknown, format: string): string | null {\n const result = formatTextFunction(value, format);\n return 'text' in result ? result.text : null;\n}\n","import { SpreadsheetDataSource, SpreadsheetRow, SpreadsheetColumn } from '../types';\n\nexport class DataSourceBinder {\n private dataSources = new Map<string, SpreadsheetDataSource>();\n\n public register(dataSource: SpreadsheetDataSource): void {\n this.dataSources.set(dataSource.id, dataSource);\n }\n\n public unregister(dataSourceId: string): void {\n this.dataSources.delete(dataSourceId);\n }\n\n public getDataSource(id: string): SpreadsheetDataSource | undefined {\n return this.dataSources.get(id);\n }\n\n /**\n * Generates or reconciles spreadsheet rows from a primary data source.\n */\n public syncRows(\n currentRows: SpreadsheetRow[],\n dataSourceId: string\n ): SpreadsheetRow[] {\n const ds = this.dataSources.get(dataSourceId);\n if (!ds) return currentRows;\n\n const records = ds.getRecords();\n const existingRecordRowMap = new Map<string, SpreadsheetRow>();\n const freeformRows: SpreadsheetRow[] = [];\n\n for (const row of currentRows) {\n if (row.type === 'record' && row.recordId) {\n existingRecordRowMap.set(row.recordId, row);\n } else {\n freeformRows.push(row);\n }\n }\n\n const nextRows: SpreadsheetRow[] = [];\n let rowIndex = 0;\n\n for (const record of records) {\n const existing = existingRecordRowMap.get(record.id);\n nextRows.push({\n ...(existing?.hidden ? { hidden: true } : {}),\n id: existing?.id || `row_${record.id}`,\n index: rowIndex++,\n height: existing?.height || 32,\n recordId: record.id,\n type: 'record'\n });\n existingRecordRowMap.delete(record.id);\n }\n\n // Append preserved freeform rows\n for (const freeform of freeformRows) {\n nextRows.push({\n ...freeform,\n index: rowIndex++\n });\n }\n\n return nextRows;\n }\n\n /**\n * Resolves a record's bound field value.\n */\n public resolveBoundValue(\n dataSourceId: string,\n recordId: string,\n field: string\n ): any {\n const ds = this.dataSources.get(dataSourceId);\n if (!ds) return undefined;\n\n const records = ds.getRecords();\n const record = records.find((r) => r.id === recordId);\n return record ? record[field] : undefined;\n }\n\n /**\n * Dispatches an update to the underlying data source.\n */\n public async commitBoundValue(\n dataSourceId: string,\n recordId: string,\n field: string,\n value: any\n ): Promise<void> {\n const ds = this.dataSources.get(dataSourceId);\n if (!ds) return;\n await ds.updateRecord(recordId, field, value);\n }\n}\n","// Portions ported from gridcraft/crates/model/src/workbook.rs and\n// gridcraft/crates/engine/src/cmd/formulas.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Workbook rules shared by the controller and the framework bindings: sheet\n * and defined-name validation, and default sheet names.\n */\nimport type { SpreadsheetDefinedName } from './types';\n\n/** Longest sheet name Excel accepts. */\nexport const MAX_SHEET_NAME_LENGTH = 31;\n/** Longest defined name Excel accepts. */\nexport const MAX_DEFINED_NAME_LENGTH = 255;\n\nconst sameName = (a: string, b: string) => a.toUpperCase() === b.toUpperCase();\n\n/**\n * Checks a sheet name like Excel: 1–31 characters, none of `: \\ / ? * [ ]`,\n * not starting or ending with `'`, not the reserved `History`, and not used\n * by another sheet (case-insensitively). Returns why it is invalid, or null.\n */\nexport function validateSheetName(name: string, existingNames: readonly string[] = []): string | null {\n const length = Array.from(name).length;\n if (length === 0 || name.trim() === '') return \"A sheet name can't be blank.\";\n if (length > MAX_SHEET_NAME_LENGTH) return `A sheet name can't be longer than ${MAX_SHEET_NAME_LENGTH} characters.`;\n if (/[:\\\\/?*[\\]]/.test(name)) return \"A sheet name can't contain any of these characters: : \\\\ / ? * [ ]\";\n if (name.startsWith(\"'\") || name.endsWith(\"'\")) return \"A sheet name can't begin or end with an apostrophe.\";\n if (sameName(name, 'History')) return '\"History\" is a reserved name.';\n if (existingNames.some((existing) => sameName(existing, name))) return 'That name is already taken. Try a different one.';\n return null;\n}\n\n/** The first `Sheet N` not used by `existingNames`. */\nexport function nextSheetName(existingNames: readonly string[], base = 'Sheet'): string {\n for (let n = 1; ; n++) {\n const candidate = `${base} ${n}`;\n if (!existingNames.some((existing) => sameName(existing, candidate))) return candidate;\n }\n}\n\n/**\n * A name for a copy of the sheet `name`: `Name (2)`, `Name (3)`, …, shortened\n * so the suffix fits in 31 characters.\n */\nexport function copySheetName(name: string, existingNames: readonly string[]): string {\n for (let k = 2; ; k++) {\n const suffix = ` (${k})`;\n const keep = Math.max(0, MAX_SHEET_NAME_LENGTH - suffix.length);\n const candidate = `${Array.from(name).slice(0, keep).join('').trimEnd()}${suffix}`;\n if (!existingNames.some((existing) => sameName(existing, candidate))) return candidate;\n }\n}\n\n/** Looks like an R1C1 reference (`R`, `C`, `RC`, `R2`, `C3`, `R1C1`), which Excel refuses as a name. */\nfunction isR1C1Like(name: string): boolean {\n let i = 0;\n if (/[Rr]/.test(name[i] ?? '')) {\n i++;\n while (i < name.length && /\\d/.test(name[i])) i++;\n }\n if (/[Cc]/.test(name[i] ?? '')) {\n i++;\n while (i < name.length && /\\d/.test(name[i])) i++;\n }\n return name.length > 0 && i === name.length;\n}\n\n/**\n * Checks a defined name like Excel: starts with a letter, `_` or `\\`;\n * continues with letters, digits, `_`, `.`, `\\` or `?`; at most 255\n * characters; and must not look like a cell reference (`A1`, `TAX2024`), an\n * R1C1 reference (`R1C1`, `R`, `C3`) or a boolean. Returns why it is invalid,\n * or null.\n */\nexport function validateDefinedName(name: string): string | null {\n if (name === '') return \"A name can't be blank.\";\n if (Array.from(name).length > MAX_DEFINED_NAME_LENGTH) return `A name can't be longer than ${MAX_DEFINED_NAME_LENGTH} characters.`;\n if (!/^[\\p{L}_\\\\][\\p{L}\\p{N}_.\\\\?]*$/u.test(name)) {\n return /^[\\p{L}_\\\\]/u.test(name)\n ? 'A name can only contain letters, numbers, periods, underscores, backslashes and question marks.'\n : 'A name must start with a letter, an underscore or a backslash.';\n }\n if (/^\\$?[A-Za-z]+\\$?\\d+$/.test(name)) return \"A name can't look like a cell reference (such as A1).\";\n if (isR1C1Like(name)) return \"A name can't look like an R1C1 reference (such as R1C1, R or C).\";\n if (sameName(name, 'TRUE') || sameName(name, 'FALSE')) return \"A name can't be TRUE or FALSE.\";\n return null;\n}\n\n/** Whether a defined name holds a LAMBDA (and so is called like a function). */\nexport function isLambdaDefinition(definition: Pick<SpreadsheetDefinedName, 'refersTo'>): boolean {\n return /^=?\\s*LAMBDA\\s*\\(/i.test(definition.refersTo);\n}\n","/**\n * Pure helpers behind the controller's table operations: value comparison\n * for sorting, filter matching, group aggregates, autofill series and range\n * arithmetic. Framework packages use these through `SpreadsheetController`.\n */\nimport type { CellRange, FilterRule, GroupAggregate, SortDirection } from './types';\nimport type { CellPosition } from './formula';\nimport { compareValues, isFormulaError, numericValue, parseIsoDate, sortCompareValues } from './formula/values';\nimport { parseCellInput } from './formula/parse-input';\n\n// --- Ranges ---\n\n/** Returns `range` with start <= end on both axes. */\nexport function normalizeRange(range: CellRange): CellRange {\n return {\n startRow: Math.min(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endRow: Math.max(range.startRow, range.endRow),\n endCol: Math.max(range.startCol, range.endCol),\n };\n}\n\nexport function rangeContains(range: CellRange, row: number, col: number): boolean {\n const r = normalizeRange(range);\n return row >= r.startRow && row <= r.endRow && col >= r.startCol && col <= r.endCol;\n}\n\nexport function rangesIntersect(a: CellRange, b: CellRange): boolean {\n const x = normalizeRange(a);\n const y = normalizeRange(b);\n return x.startRow <= y.endRow && y.startRow <= x.endRow && x.startCol <= y.endCol && y.startCol <= x.endCol;\n}\n\n/** True when `inner` lies entirely inside `outer`. */\nexport function rangeContainsRange(outer: CellRange, inner: CellRange): boolean {\n const o = normalizeRange(outer);\n const i = normalizeRange(inner);\n return i.startRow >= o.startRow && i.endRow <= o.endRow && i.startCol >= o.startCol && i.endCol <= o.endCol;\n}\n\nexport function isSingleCellRange(range: CellRange): boolean {\n const r = normalizeRange(range);\n return r.startRow === r.endRow && r.startCol === r.endCol;\n}\n\n// --- Values ---\n\nconst NUMBER_PATTERN = /^[-+]?(\\d+\\.?\\d*|\\.\\d+)(e[-+]?\\d+)?$/i;\nconst ISO_DATE_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\nexport function isBlankValue(value: unknown): boolean {\n return value === null || value === undefined || (typeof value === 'string' && value.trim() === '');\n}\n\n/** The numeric value of a number or numeric string, or null. */\nexport function toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (NUMBER_PATTERN.test(trimmed)) return parseFloat(trimmed);\n }\n return null;\n}\n\nconst collator =\n typeof Intl !== 'undefined' ? new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) : null;\n\nfunction compareText(a: string, b: string): number {\n return collator ? collator.compare(a, b) : a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Sort order for cell values, as in Excel: numbers (ascending), then text,\n * booleans and errors; `desc` reverses that order. Numbers compare at 15\n * significant digits and ISO dates chronologically. Text uses a natural,\n * case-insensitive order (\"Item 2\" before \"Item 10\"). Blanks always sort\n * last, whatever the direction.\n */\nexport function compareCellValues(a: unknown, b: unknown, direction: SortDirection = 'asc'): number {\n const aBlank = isBlankValue(a);\n const bBlank = isBlankValue(b);\n if (aBlank || bBlank) return aBlank === bBlank ? 0 : aBlank ? 1 : -1;\n\n let result: number;\n if (isPlainText(a) && isPlainText(b) && !(parseIsoDate(a) && parseIsoDate(b))) {\n result = compareText(a, b);\n } else {\n result = sortCompareValues(a, b);\n }\n if (result === 0) return 0;\n return direction === 'desc' ? -Math.sign(result) : Math.sign(result);\n}\n\nconst isPlainText = (value: unknown): value is string => typeof value === 'string' && !isFormulaError(value);\n\n// --- Filters ---\n\ntype FilterKind = 'number' | 'text' | 'boolean';\n\n/**\n * A cell value for filtering: numeric text (e.g. from a data source) counts\n * as a number, like criteria in COUNTIF. ISO dates order with numbers\n * (as serial dates).\n */\nfunction filterValue(value: unknown): { value: unknown; kind: FilterKind } | null {\n if (isBlankValue(value) || isFormulaError(value)) return null;\n if (typeof value === 'number') return Number.isFinite(value) ? { value, kind: 'number' } : null;\n if (typeof value === 'boolean') return { value, kind: 'boolean' };\n const num = numericValue(value);\n if (num !== null) return { value: num, kind: 'number' };\n const text = String(value);\n return { value: text, kind: parseIsoDate(text) ? 'number' : 'text' };\n}\n\n/** A filter operand, read with the typed-input rules (\"1,000\", \"10%\", \"3/4/2026\", \"TRUE\"). */\nfunction filterOperand(operand: unknown): { value: unknown; kind: FilterKind } | null {\n if (typeof operand !== 'string') return filterValue(operand);\n if (operand.trim() === '') return null;\n const parsed = parseCellInput(operand.trim());\n if (parsed.type === 'number' || parsed.type === 'date' || parsed.type === 'boolean') return filterValue(parsed.value);\n return { value: operand, kind: 'text' };\n}\n\nfunction compareForFilter(value: unknown, operand: unknown): number | null {\n const a = filterValue(value);\n const b = filterOperand(operand);\n // Numbers (and dates), text and booleans never compare with each other.\n if (!a || !b || a.kind !== b.kind) return null;\n return compareValues(a.value, b.value);\n}\n\nfunction looselyEquals(value: unknown, operand: unknown): boolean {\n const c = compareForFilter(value, operand);\n if (c !== null) return c === 0;\n return String(value ?? '').trim().toLowerCase() === String(operand ?? '').trim().toLowerCase();\n}\n\n/** Whether a cell value passes one filter rule. */\nexport function matchesFilterRule(value: unknown, rule: FilterRule): boolean {\n const text = String(value ?? '').toLowerCase();\n switch (rule.operator) {\n case 'equals':\n return looselyEquals(value, rule.value);\n case 'notEquals':\n return !looselyEquals(value, rule.value);\n case 'contains':\n return text.includes(String(rule.value ?? '').toLowerCase());\n case 'notContains':\n return !text.includes(String(rule.value ?? '').toLowerCase());\n case 'greaterThan': {\n const c = compareForFilter(value, rule.value);\n return c !== null && c > 0;\n }\n case 'greaterThanOrEqual': {\n const c = compareForFilter(value, rule.value);\n return c !== null && c >= 0;\n }\n case 'lessThan': {\n const c = compareForFilter(value, rule.value);\n return c !== null && c < 0;\n }\n case 'lessThanOrEqual': {\n const c = compareForFilter(value, rule.value);\n return c !== null && c <= 0;\n }\n case 'between': {\n const lo = compareForFilter(value, rule.value);\n const hi = compareForFilter(value, rule.value2);\n return lo !== null && hi !== null && lo >= 0 && hi <= 0;\n }\n case 'isBlank':\n return isBlankValue(value);\n case 'isNotBlank':\n return !isBlankValue(value);\n case 'oneOf':\n return (rule.values ?? []).some((candidate) => String(candidate) === String(value ?? ''));\n default:\n return true;\n }\n}\n\n// --- Group summaries ---\n\n/**\n * Aggregates values the way category summaries do: `count` counts non-blank\n * values; the other aggregates use numeric values only and return null when\n * there are none.\n */\nexport function aggregateValues(values: unknown[], aggregate: GroupAggregate): number | null {\n if (aggregate === 'count') return values.filter((v) => !isBlankValue(v)).length;\n const numbers: number[] = [];\n for (const v of values) {\n const n = toNumber(v);\n if (n !== null) numbers.push(n);\n }\n if (numbers.length === 0) return aggregate === 'sum' ? 0 : null;\n switch (aggregate) {\n case 'sum':\n return roundFloat(numbers.reduce((s, n) => s + n, 0));\n case 'average':\n return roundFloat(numbers.reduce((s, n) => s + n, 0) / numbers.length);\n case 'min':\n return numbers.reduce((m, n) => (n < m ? n : m), numbers[0]);\n case 'max':\n return numbers.reduce((m, n) => (n > m ? n : m), numbers[0]);\n default:\n return null;\n }\n}\n\n/** Removes binary floating point noise (0.1 + 0.2 -> 0.3). */\nexport function roundFloat(value: number): number {\n return Number.isInteger(value) ? value : parseFloat(value.toPrecision(12));\n}\n\n// --- Autofill ---\n\ntype FillValue =\n | { kind: 'blank' }\n | { kind: 'number'; value: number }\n | { kind: 'date'; time: number; year: number; month: number; day: number }\n | { kind: 'textNumber'; prefix: string; value: number; width: number; suffix: string }\n | { kind: 'text'; text: string };\n\nconst DAY_MS = 86_400_000;\nconst TRAILING_NUMBER = /^(.*?)(\\d+)(\\D*)$/;\n\nfunction classifyFillValue(raw: string): FillValue {\n if (raw.trim() === '') return { kind: 'blank' };\n const num = toNumber(raw);\n if (num !== null) return { kind: 'number', value: num };\n // Typed dates (\"3/4/2026\", \"Mar 4, 2026\") continue as ISO dates.\n const typed = parseCellInput(raw);\n const date = ISO_DATE_PATTERN.exec(typed.type === 'date' ? String(typed.value) : raw.trim());\n if (date) {\n const [year, month, day] = [Number(date[1]), Number(date[2]), Number(date[3])];\n const time = Date.UTC(year, month - 1, day);\n const check = new Date(time);\n if (check.getUTCMonth() === month - 1 && check.getUTCDate() === day) {\n return { kind: 'date', time, year, month, day };\n }\n }\n const textNumber = TRAILING_NUMBER.exec(raw);\n if (textNumber && textNumber[1] !== '') {\n return {\n kind: 'textNumber',\n prefix: textNumber[1],\n value: parseInt(textNumber[2], 10),\n width: textNumber[2].length,\n suffix: textNumber[3],\n };\n }\n return { kind: 'text', text: raw };\n}\n\nconst pad = (n: number, width: number) => String(Math.abs(n)).padStart(width, '0');\n\nfunction formatDate(time: number): string {\n const d = new Date(time);\n return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1, 2)}-${pad(d.getUTCDate(), 2)}`;\n}\n\n/** Constant difference between consecutive values, or null. */\nfunction constantStep(values: number[]): number | null {\n const step = values[1] - values[0];\n for (let i = 2; i < values.length; i++) {\n if (Math.abs(values[i] - values[i - 1] - step) > 1e-9) return null;\n }\n return step;\n}\n\n/** Least-squares line through (i, values[i]), evaluated at `x`. */\nfunction linearTrend(values: number[], x: number): number {\n const n = values.length;\n const meanX = (n - 1) / 2;\n const meanY = values.reduce((s, v) => s + v, 0) / n;\n let num = 0;\n let den = 0;\n values.forEach((v, i) => {\n num += (i - meanX) * (v - meanY);\n den += (i - meanX) ** 2;\n });\n const slope = den === 0 ? 0 : num / den;\n return meanY + slope * (x - meanX);\n}\n\n/**\n * Extends `source` (raw values in fill order: for an upward or leftward fill,\n * nearest-first) by `count` values, the way a fill handle does:\n *\n * - numbers: one value repeats; two or more continue their linear trend\n * - dates (ISO `YYYY-MM-DD`, or typed like `3/4/2026`): one date counts up\n * by a day; two or more continue their day or month step (as ISO dates)\n * - text ending in a number (\"Item 1\"): the number counts up (or continues\n * the step of several values with the same prefix/suffix)\n * - anything else (or a mix) repeats the source cyclically\n *\n * Formulas are not handled here; the controller shifts their references.\n */\nexport function fillSeries(source: string[], count: number): string[] {\n if (count <= 0) return [];\n if (source.length === 0) return Array(count).fill('');\n const repeat = () => Array.from({ length: count }, (_, i) => source[i % source.length]);\n\n const parsed = source.map(classifyFillValue);\n const kind = parsed[0].kind;\n if (!parsed.every((p) => p.kind === kind)) return repeat();\n const n = source.length;\n\n if (kind === 'number') {\n if (n === 1) return repeat();\n const values = parsed.map((p) => (p as { value: number }).value);\n return Array.from({ length: count }, (_, i) => String(roundFloat(linearTrend(values, n + i))));\n }\n\n if (kind === 'date') {\n const dates = parsed as Array<Extract<FillValue, { kind: 'date' }>>;\n if (n === 1) {\n return Array.from({ length: count }, (_, i) => formatDate(dates[0].time + (i + 1) * DAY_MS));\n }\n // Same day of month, constant month step: step by months (Jan 31 -> Feb 28 clamps).\n const monthIndex = dates.map((d) => d.year * 12 + (d.month - 1));\n const monthStep = constantStep(monthIndex);\n if (monthStep !== null && monthStep !== 0 && dates.every((d) => d.day === dates[0].day)) {\n const last = monthIndex[n - 1];\n return Array.from({ length: count }, (_, i) => {\n const m = last + monthStep * (i + 1);\n const year = Math.floor(m / 12);\n const month = m - year * 12;\n const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();\n return formatDate(Date.UTC(year, month, Math.min(dates[0].day, daysInMonth)));\n });\n }\n const dayStep = constantStep(dates.map((d) => Math.round(d.time / DAY_MS)));\n if (dayStep === null) return repeat();\n const lastTime = dates[n - 1].time;\n return Array.from({ length: count }, (_, i) => formatDate(lastTime + dayStep * (i + 1) * DAY_MS));\n }\n\n if (kind === 'textNumber') {\n const items = parsed as Array<Extract<FillValue, { kind: 'textNumber' }>>;\n const { prefix, suffix } = items[0];\n if (!items.every((t) => t.prefix === prefix && t.suffix === suffix)) return repeat();\n const step = n === 1 ? 1 : constantStep(items.map((t) => t.value));\n if (step === null) return repeat();\n const last = items[n - 1];\n return Array.from({ length: count }, (_, i) => {\n const value = last.value + step * (i + 1);\n const digits = value < 0 ? `-${pad(value, last.width)}` : pad(value, last.width);\n return `${prefix}${digits}${suffix}`;\n });\n }\n\n return repeat();\n}\n\nexport type FillDirection = 'down' | 'up' | 'right' | 'left';\n\n/**\n * Works out where a fill handle drag ends. Dragging further vertically than\n * horizontally fills down/up the source's columns; otherwise right/left\n * across its rows. Returns the full target range (source included), or null\n * while the pointer is still inside the source.\n */\nexport function computeFillTarget(source: CellRange, pointer: CellPosition): CellRange | null {\n const s = normalizeRange(source);\n const rowOverflow = pointer.row > s.endRow ? pointer.row - s.endRow : pointer.row < s.startRow ? s.startRow - pointer.row : 0;\n const colOverflow = pointer.col > s.endCol ? pointer.col - s.endCol : pointer.col < s.startCol ? s.startCol - pointer.col : 0;\n if (rowOverflow === 0 && colOverflow === 0) return null;\n if (rowOverflow >= colOverflow) {\n return pointer.row > s.endRow ? { ...s, endRow: pointer.row } : { ...s, startRow: pointer.row };\n }\n return pointer.col > s.endCol ? { ...s, endCol: pointer.col } : { ...s, startCol: pointer.col };\n}\n\n/**\n * The direction of a fill from `source` into `target` (the extended range),\n * or null when `target` does not extend `source` along exactly one edge.\n */\nexport function getFillDirection(source: CellRange, target: CellRange): FillDirection | null {\n const s = normalizeRange(source);\n const t = normalizeRange(target);\n const sameCols = s.startCol === t.startCol && s.endCol === t.endCol;\n const sameRows = s.startRow === t.startRow && s.endRow === t.endRow;\n if (sameCols && t.startRow === s.startRow && t.endRow > s.endRow) return 'down';\n if (sameCols && t.endRow === s.endRow && t.startRow < s.startRow) return 'up';\n if (sameRows && t.startCol === s.startCol && t.endCol > s.endCol) return 'right';\n if (sameRows && t.endCol === s.endCol && t.startCol < s.startCol) return 'left';\n return null;\n}\n","/**\n * Spreadsheet clipboard formats. Copying writes three representations:\n *\n * - `text/plain`: TSV of the displayed values (pastes into any app)\n * - `text/html`: an HTML `<table>` (pastes into docs, mail, other sheets)\n * - `application/x.pixerate.spreadsheet+json`: raw inputs and formulas, so a\n * paste into a pixerate spreadsheet is lossless; formulas are re-based to\n * the paste position like a spreadsheet copy/paste\n *\n * Pasting prefers the custom format, then an HTML table, then TSV. All\n * functions here are pure and DOM-free.\n */\nimport { offsetFormulaReferences, type CellPosition } from './formula';\nimport { parseTsv, serializeTsv } from './csv';\n\nexport const SPREADSHEET_CLIPBOARD_MIME = 'application/x.pixerate.spreadsheet+json';\n\nexport interface SpreadsheetClipboardCell {\n raw: string; // input as typed (formula text for formulas)\n value: unknown; // displayed value\n /** Display text for `text/plain` and `text/html` (e.g. `10%` for 0.1); defaults to the value as text. Not stored in the JSON payload. */\n text?: string;\n}\n\nexport interface SpreadsheetClipboardPayload {\n type: 'pixerate-spreadsheet';\n version: 1;\n /** Position of the copied block's top-left cell, used to re-base formulas. */\n origin: CellPosition;\n cells: SpreadsheetClipboardCell[][];\n}\n\n/** MIME type -> data, as written to `DataTransfer.setData`. */\nexport type SpreadsheetClipboardData = Record<string, string>;\n\n/** Anything clipboard data can be read from: a `DataTransfer` or a plain record. */\nexport type SpreadsheetClipboardSource =\n | { getData(format: string): string }\n | Record<string, string | undefined>;\n\nexport interface ParsedSpreadsheetClipboard {\n format: 'internal' | 'html' | 'text';\n /** Raw inputs to write, row by row. */\n rows: string[][];\n /** For internal pastes: where the block was copied from. */\n origin?: CellPosition;\n}\n\nconst displayText = (value: unknown): string => (value === null || value === undefined ? '' : String(value));\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/** Serializes displayed values as an HTML table. Line breaks become `<br>`. */\nexport function serializeHtmlTable(values: ReadonlyArray<ReadonlyArray<unknown>>): string {\n const body = values\n .map(\n (row) =>\n `<tr>${row.map((v) => `<td>${escapeHtml(displayText(v)).replace(/\\r?\\n/g, '<br>')}</td>`).join('')}</tr>`,\n )\n .join('');\n return `<table><tbody>${body}</tbody></table>`;\n}\n\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: ' ',\n};\n\nfunction decodeEntities(text: string): string {\n return text.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);/gi, (match, entity: string) => {\n if (entity[0] === '#') {\n const code = entity[1] === 'x' || entity[1] === 'X' ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);\n return Number.isFinite(code) ? String.fromCodePoint(code) : match;\n }\n return NAMED_ENTITIES[entity.toLowerCase()] ?? match;\n });\n}\n\n// Private-use placeholder for line breaks while HTML whitespace is collapsed.\nconst LINE_BREAK = '';\n\nfunction cellHtmlToText(html: string): string {\n const text = html\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<(style|script)[\\s\\S]*?<\\/\\1\\s*>/gi, '')\n .replace(/<br\\s*\\/?>/gi, LINE_BREAK)\n .replace(/<\\/(p|div)\\s*>/gi, LINE_BREAK)\n .replace(/<[^>]*>/g, '')\n .replace(/\\s+/g, ' ');\n return decodeEntities(text)\n .replace(/ /g, ' ')\n .split(LINE_BREAK)\n .map((line) => line.trim())\n .join('\\n')\n .replace(/^\\n+|\\n+$/g, '');\n}\n\n/**\n * Extracts cell texts from the first `<table>` in `html` (as written by\n * spreadsheet apps and browsers). `colspan`/`rowspan` cells are expanded with\n * blank cells so positions line up. Returns null when there is no table.\n */\nexport function parseHtmlTable(html: string): string[][] | null {\n const tableMatch = /<table[\\s>][\\s\\S]*?<\\/table\\s*>/i.exec(html);\n if (!tableMatch) return null;\n const table = tableMatch[0];\n\n const rows: string[][] = [];\n // Cells still occupied by a rowspan from an earlier row: row -> cols.\n const occupied = new Map<number, Set<number>>();\n const rowPattern = /<tr[\\s>]([\\s\\S]*?)<\\/tr\\s*>/gi;\n const cellPattern = /<(td|th)(\\s[^>]*)?>([\\s\\S]*?)<\\/\\1\\s*>/gi;\n let rowMatch: RegExpExecArray | null;\n let rowIndex = 0;\n while ((rowMatch = rowPattern.exec(table))) {\n const row: string[] = [];\n const taken = occupied.get(rowIndex) ?? new Set<number>();\n let col = 0;\n let cellMatch: RegExpExecArray | null;\n cellPattern.lastIndex = 0;\n while ((cellMatch = cellPattern.exec(rowMatch[1]))) {\n while (taken.has(col)) row[col++] = '';\n const attrs = cellMatch[2] ?? '';\n const colspan = Math.max(1, parseInt(/colspan\\s*=\\s*[\"']?(\\d+)/i.exec(attrs)?.[1] ?? '1', 10));\n const rowspan = Math.max(1, parseInt(/rowspan\\s*=\\s*[\"']?(\\d+)/i.exec(attrs)?.[1] ?? '1', 10));\n row[col] = cellHtmlToText(cellMatch[3]);\n for (let c = col; c < col + colspan; c++) {\n if (c > col) row[c] = '';\n for (let r = rowIndex + 1; r < rowIndex + rowspan; r++) {\n if (!occupied.has(r)) occupied.set(r, new Set());\n occupied.get(r)!.add(c);\n }\n }\n col += colspan;\n }\n while (taken.has(col)) row[col++] = '';\n rows.push(Array.from(row, (v) => v ?? ''));\n rowIndex++;\n }\n return rows.length > 0 ? rows : null;\n}\n\n/** Builds the three clipboard representations of a copied block. */\nexport function createSpreadsheetClipboardData(\n cells: SpreadsheetClipboardCell[][],\n origin: CellPosition,\n): SpreadsheetClipboardData {\n const texts = cells.map((row) => row.map((cell) => cell.text ?? displayText(cell.value)));\n const payload: SpreadsheetClipboardPayload = {\n type: 'pixerate-spreadsheet',\n version: 1,\n origin,\n cells: cells.map((row) => row.map(({ raw, value }) => ({ raw, value }))),\n };\n return {\n 'text/plain': serializeTsv(texts),\n 'text/html': serializeHtmlTable(texts),\n [SPREADSHEET_CLIPBOARD_MIME]: JSON.stringify(payload),\n };\n}\n\nfunction readFormat(source: SpreadsheetClipboardSource, format: string): string {\n try {\n if (typeof (source as { getData?: unknown }).getData === 'function') {\n return (source as { getData(format: string): string }).getData(format) ?? '';\n }\n return (source as Record<string, string | undefined>)[format] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction parsePayload(json: string): SpreadsheetClipboardPayload | null {\n try {\n const payload = JSON.parse(json);\n if (\n payload?.type !== 'pixerate-spreadsheet' ||\n !Array.isArray(payload.cells) ||\n !payload.cells.every((row: unknown) => Array.isArray(row)) ||\n typeof payload.origin?.row !== 'number' ||\n typeof payload.origin?.col !== 'number'\n ) {\n return null;\n }\n return payload as SpreadsheetClipboardPayload;\n } catch {\n return null;\n }\n}\n\n/**\n * Reads clipboard data, preferring the lossless internal format, then an\n * HTML table, then TSV text. Returns null when there is nothing to paste.\n */\nexport function readSpreadsheetClipboard(source: SpreadsheetClipboardSource): ParsedSpreadsheetClipboard | null {\n const payload = parsePayload(readFormat(source, SPREADSHEET_CLIPBOARD_MIME));\n if (payload) {\n return {\n format: 'internal',\n origin: payload.origin,\n rows: payload.cells.map((row) => row.map((cell) => (typeof cell?.raw === 'string' ? cell.raw : displayText(cell?.value)))),\n };\n }\n\n const html = readFormat(source, 'text/html');\n if (html) {\n const rows = parseHtmlTable(html);\n if (rows) return { format: 'html', rows };\n }\n\n const text = readFormat(source, 'text/plain');\n if (text) {\n const rows = parseTsv(text);\n if (rows.length > 0) return { format: 'text', rows };\n }\n return null;\n}\n\n/**\n * Moves formulas copied from `origin` so they keep their relative references\n * when pasted at `target`. Non-formula inputs are returned unchanged.\n */\nexport function rebaseClipboardRows(rows: string[][], origin: CellPosition, target: CellPosition): string[][] {\n const rowOffset = target.row - origin.row;\n const colOffset = target.col - origin.col;\n return rows.map((row) =>\n row.map((raw) => (raw.startsWith('=') ? offsetFormulaReferences(raw, rowOffset, colOffset) : raw)),\n );\n}\n","/**\n * Range helpers shared by conditional formatting and data validation: rules\n * apply to a list of rectangular ranges (\"applies to\") that follow row and\n * column insertions/deletions, and can be cut out of a selection.\n *\n * Portions ported from gridcraft/crates/engine/src/cmd/sheet.rs and\n * gridcraft/crates/engine/src/cmd/data.rs (MIT OR Apache-2.0, © ArtCraft Team\n * and contributors). See THIRD_PARTY_NOTICES.md.\n */\nimport type { CellRange } from './types';\nimport type { StructuralChange } from './formula';\nimport { colNameToIndex, indexToColName, offsetFormulaReferences, shiftFormulaReferences } from './formula';\nimport { normalizeRange, rangesIntersect } from './table';\n\n/**\n * Adjusts one range for an inserted or deleted row/column, the way merges\n * and formula ranges move: ranges after the change shift, ranges across it\n * grow or shrink, and a range whose rows (or columns) are all deleted is\n * gone (null).\n */\nexport function adjustRangeForStructuralChange(range: CellRange, change: StructuralChange): CellRange | null {\n const r = normalizeRange(range);\n const startKey = change.axis === 'row' ? 'startRow' : 'startCol';\n const endKey = change.axis === 'row' ? 'endRow' : 'endCol';\n if (change.type === 'insert') {\n if (r[startKey] >= change.index) r[startKey]++;\n if (r[endKey] >= change.index) r[endKey]++;\n return r;\n }\n if (r[startKey] === change.index && r[endKey] === change.index) return null;\n if (r[startKey] > change.index) r[startKey]--;\n if (r[endKey] >= change.index) r[endKey]--;\n return r;\n}\n\n/** `adjustRangeForStructuralChange` over a list, dropping ranges that are gone. */\nexport function adjustRangesForStructuralChange(ranges: CellRange[], change: StructuralChange): CellRange[] {\n const out: CellRange[] = [];\n for (const range of ranges) {\n const next = adjustRangeForStructuralChange(range, change);\n if (next) out.push(next);\n }\n return out;\n}\n\n/** The parts of `range` outside `cut` (up to four rectangles). */\nexport function subtractRange(range: CellRange, cut: CellRange): CellRange[] {\n const r = normalizeRange(range);\n const c = normalizeRange(cut);\n if (!rangesIntersect(r, c)) return [r];\n const out: CellRange[] = [];\n const top = Math.max(r.startRow, c.startRow);\n const bottom = Math.min(r.endRow, c.endRow);\n if (r.startRow < c.startRow) out.push({ startRow: r.startRow, startCol: r.startCol, endRow: c.startRow - 1, endCol: r.endCol });\n if (r.endRow > c.endRow) out.push({ startRow: c.endRow + 1, startCol: r.startCol, endRow: r.endRow, endCol: r.endCol });\n if (r.startCol < c.startCol) out.push({ startRow: top, startCol: r.startCol, endRow: bottom, endCol: c.startCol - 1 });\n if (r.endCol > c.endCol) out.push({ startRow: top, startCol: c.endCol + 1, endRow: bottom, endCol: r.endCol });\n return out;\n}\n\n/** Removes the cells of `cut` from every range of the list. */\nexport function subtractRangeFromRanges(ranges: CellRange[], cut: CellRange): CellRange[] {\n return ranges.flatMap((range) => subtractRange(range, cut));\n}\n\n/** Whether any range of the list contains (row, col). */\nexport function rangesContain(ranges: CellRange[], row: number, col: number): boolean {\n for (const r of ranges) {\n const minR = Math.min(r.startRow, r.endRow);\n const maxR = Math.max(r.startRow, r.endRow);\n const minC = Math.min(r.startCol, r.endCol);\n const maxC = Math.max(r.startCol, r.endCol);\n if (row >= minR && row <= maxR && col >= minC && col <= maxC) return true;\n }\n return false;\n}\n\n/** The top-left cell of the first range: rule formulas are written for it. */\nexport function ruleAnchor(ranges: CellRange[]): { row: number; col: number } {\n const first = ranges[0];\n if (!first) return { row: 0, col: 0 };\n return { row: Math.min(first.startRow, first.endRow), col: Math.min(first.startCol, first.endCol) };\n}\n\nconst withEquals = (formula: string) => (formula.trimStart().startsWith('=') ? formula.trimStart() : `=${formula}`);\n\n/**\n * A rule formula written for `anchor`, as seen from (row, col): relative\n * references shift by the distance, `$` parts stay (Excel's conditional\n * formatting and validation formulas work this way). Returned with a\n * leading `=`.\n */\nexport function ruleFormulaAt(formula: string, anchor: { row: number; col: number }, row: number, col: number): string {\n return offsetFormulaReferences(withEquals(formula), row - anchor.row, col - anchor.col);\n}\n\n/**\n * Rewrites a rule formula after a row/column insertion or deletion so it\n * keeps pointing at the same cells. Keeps the caller's leading `=` (or its\n * absence).\n */\nexport function shiftRuleFormula(formula: string, change: StructuralChange): string {\n const hadEquals = formula.trimStart().startsWith('=');\n const shifted = shiftFormulaReferences(withEquals(formula), change);\n return hadEquals ? shifted : shifted.slice(1);\n}\n\n/** Every cell of a list of ranges, clamped to the sheet, each once, in row-major order per range. */\nexport function forEachRangeCell(\n ranges: CellRange[],\n rowCount: number,\n colCount: number,\n fn: (row: number, col: number) => void\n): void {\n const seen = ranges.length > 1 ? new Set<number>() : null;\n for (const range of ranges) {\n const r = normalizeRange(range);\n const endRow = Math.min(r.endRow, rowCount - 1);\n const endCol = Math.min(r.endCol, colCount - 1);\n for (let row = Math.max(0, r.startRow); row <= endRow; row++) {\n for (let col = Math.max(0, r.startCol); col <= endCol; col++) {\n if (seen) {\n const key = row * (colCount + 1) + col;\n if (seen.has(key)) continue;\n seen.add(key);\n }\n fn(row, col);\n }\n }\n }\n}\n\n/** A1 text of a list of ranges: `A1:B5, D2`. */\nexport function formatRuleRanges(ranges: CellRange[]): string {\n return ranges\n .map((range) => {\n const r = normalizeRange(range);\n const start = `${indexToColName(r.startCol)}${r.startRow + 1}`;\n const end = `${indexToColName(r.endCol)}${r.endRow + 1}`;\n return start === end ? start : `${start}:${end}`;\n })\n .join(', ');\n}\n\nconst A1_RANGE = /^\\$?([A-Za-z]{1,3})\\$?(\\d{1,7})(?::\\$?([A-Za-z]{1,3})\\$?(\\d{1,7}))?$/;\n\n/** Parses `A1:B5, D2` (commas, semicolons or spaces between ranges; `$` ignored). Null when any part is not a cell or range. */\nexport function parseRuleRanges(text: string): CellRange[] | null {\n const parts = text.split(/[,;\\s]+/).filter((p) => p !== '');\n if (parts.length === 0) return null;\n const out: CellRange[] = [];\n for (const part of parts) {\n const m = A1_RANGE.exec(part);\n if (!m) return null;\n const startRow = Number(m[2]) - 1;\n const startCol = colNameToIndex(m[1]);\n const endRow = m[4] ? Number(m[4]) - 1 : startRow;\n const endCol = m[3] ? colNameToIndex(m[3]) : startCol;\n if (startRow < 0 || endRow < 0 || startCol < 0 || endCol < 0) return null;\n out.push(normalizeRange({ startRow, startCol, endRow, endCol }));\n }\n return out;\n}\n","/**\n * Glyphs for conditional-format icon sets, so React and Svelte draw the same\n * icons without image assets. Index 0 is the lowest icon.\n */\nimport type { ConditionalFormatIconSet } from './types';\n\nexport interface ConditionalFormatIconGlyph {\n /** Text glyph to draw. */\n glyph: string;\n /** CSS colour of the glyph. */\n color: string;\n /** Accessible name, e.g. \"Up arrow\". */\n label: string;\n}\n\nconst RED = '#d9534f';\nconst YELLOW = '#e0a800';\nconst GREEN = '#2e9d4f';\nconst GRAY = '#7a7f87';\nconst BLACK = '#3b3f45';\nconst BLUE = '#3b73b9';\n\nconst g = (glyph: string, color: string, label: string): ConditionalFormatIconGlyph => ({ glyph, color, label });\n\nconst ICON_SETS: Record<ConditionalFormatIconSet, ConditionalFormatIconGlyph[]> = {\n '3Arrows': [g('↓', RED, 'Down arrow'), g('→', YELLOW, 'Side arrow'), g('↑', GREEN, 'Up arrow')],\n '3ArrowsGray': [g('↓', GRAY, 'Down arrow'), g('→', GRAY, 'Side arrow'), g('↑', GRAY, 'Up arrow')],\n '3TrafficLights1': [g('●', RED, 'Red light'), g('●', YELLOW, 'Yellow light'), g('●', GREEN, 'Green light')],\n '3Flags': [g('⚑', RED, 'Red flag'), g('⚑', YELLOW, 'Yellow flag'), g('⚑', GREEN, 'Green flag')],\n '3Symbols': [g('✖', RED, 'Cross'), g('!', YELLOW, 'Exclamation'), g('✔', GREEN, 'Check')],\n '3Stars': [g('☆', YELLOW, 'Empty star'), g('✬', YELLOW, 'Half star'), g('★', YELLOW, 'Full star')],\n '4Arrows': [g('↓', RED, 'Down arrow'), g('↘', YELLOW, 'Down-right arrow'), g('↗', YELLOW, 'Up-right arrow'), g('↑', GREEN, 'Up arrow')],\n '4Rating': [g('▂', BLUE, 'Rating 1 of 4'), g('▄', BLUE, 'Rating 2 of 4'), g('▆', BLUE, 'Rating 3 of 4'), g('█', BLUE, 'Rating 4 of 4')],\n '4TrafficLights': [g('●', BLACK, 'Black light'), g('●', RED, 'Red light'), g('●', YELLOW, 'Yellow light'), g('●', GREEN, 'Green light')],\n '5Arrows': [\n g('↓', RED, 'Down arrow'),\n g('↘', YELLOW, 'Down-right arrow'),\n g('→', YELLOW, 'Side arrow'),\n g('↗', GREEN, 'Up-right arrow'),\n g('↑', GREEN, 'Up arrow'),\n ],\n '5Rating': [\n g('▁', BLUE, 'Rating 1 of 5'),\n g('▂', BLUE, 'Rating 2 of 5'),\n g('▄', BLUE, 'Rating 3 of 5'),\n g('▆', BLUE, 'Rating 4 of 5'),\n g('█', BLUE, 'Rating 5 of 5'),\n ],\n '5Quarters': [g('○', BLACK, 'Empty'), g('◔', BLACK, 'Quarter'), g('◑', BLACK, 'Half'), g('◕', BLACK, 'Three quarters'), g('●', BLACK, 'Full')],\n};\n\n/** Names of the built-in icon sets. */\nexport const CONDITIONAL_FORMAT_ICON_SETS = Object.keys(ICON_SETS) as ConditionalFormatIconSet[];\n\n/** Number of icons in a set (`3Arrows` → 3), always 3 to 5. */\nexport function conditionalFormatIconCount(set: string): number {\n const n = Number(set.charAt(0));\n return Number.isFinite(n) ? Math.min(5, Math.max(3, n)) : 3;\n}\n\n/** The glyph of icon `index` of `set` (unknown sets use traffic lights). */\nexport function getConditionalFormatIcon(set: string, index: number): ConditionalFormatIconGlyph {\n const icons = ICON_SETS[set as ConditionalFormatIconSet] ?? ICON_SETS['3TrafficLights1'];\n return icons[Math.max(0, Math.min(icons.length - 1, index))];\n}\n","/**\n * Conditional formatting evaluation: which fill, font colour, emphasis, data\n * bar or icon each cell gets.\n *\n * Portions ported from gridcraft/crates/engine/src/cf.rs (MIT OR Apache-2.0,\n * © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n */\nimport { compareValues, numberToText, parseCellInput, serialFromYmd } from '../formula';\nimport { isFormulaError, serialToDate, toBoolean } from '../formula/values';\nimport { forEachRangeCell, rangesContain, ruleAnchor, ruleFormulaAt } from '../rule-ranges';\nimport { rangesIntersect } from '../table';\nimport { conditionalFormatIconCount } from './icons';\nimport type {\n ConditionalCellFormat,\n ConditionalFormatOverlay,\n ConditionalFormatRule,\n ConditionalFormatSheet,\n ConditionalFormatStyle,\n ConditionalFormatValue,\n} from './types';\n\nconst DEFAULT_BAR_COLOR = '#638ec6';\n\ninterface RuleStats {\n sorted: number[];\n avg: number;\n std: number;\n /** Lower-cased text of each non-blank value -> occurrences. */\n counts: Map<string, number>;\n}\n\nconst isBlank = (v: unknown) => v === '' || v === null || v === undefined;\n\nconst ISO_DATE = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?: (\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$/;\n\n/**\n * The number a value counts as for scales, bars, icons, top/bottom and\n * averages: numbers, and dates (ISO text) as their serial number. Text,\n * booleans, blanks and errors are not numbers.\n */\nexport function conditionalFormatNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value !== 'string' || value.length < 8 || value.length > 19) return null;\n const m = ISO_DATE.exec(value);\n if (!m) return null;\n const serial = serialFromYmd(Number(m[1]), Number(m[2]), Number(m[3]));\n if (serial === null) return null;\n const time = m[4] ? (Number(m[4]) * 3600 + Number(m[5]) * 60 + Number(m[6] ?? 0)) / 86400 : 0;\n return serial + time;\n}\n\n/** A value as text, for text rules and duplicates. */\nfunction valueText(value: unknown): string {\n if (isBlank(value)) return '';\n if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';\n if (typeof value === 'number') return numberToText(value);\n return String(value);\n}\n\nfunction firstValue(value: unknown): unknown {\n return Array.isArray(value) ? (value.length ? value[0] : '') : value;\n}\n\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const rank = (Math.min(Math.max(p, 0), 100) / 100) * (sorted.length - 1);\n const lo = Math.floor(rank);\n const hi = Math.ceil(rank);\n const a = sorted[lo] ?? 0;\n const b = sorted[hi] ?? a;\n return a + (b - a) * (rank - lo);\n}\n\n/** Parses `#rgb` / `#rrggbb` (with or without `#`). */\nfunction parseHexColor(color: string | undefined): [number, number, number] | null {\n if (!color) return null;\n let hex = color.trim().replace(/^#/, '');\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;\n return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];\n}\n\nconst toHex = (rgb: [number, number, number]) => `#${rgb.map((c) => c.toString(16).padStart(2, '0')).join('')}`;\n\n/** Blends two colours: `t` 0 is `a`, 1 is `b`. */\nexport function blendConditionalFormatColors(a: string, b: string, t: number): string {\n const ca = parseHexColor(a) ?? [255, 255, 255];\n const cb = parseHexColor(b) ?? [255, 255, 255];\n const k = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);\n return toHex([0, 1, 2].map((i) => Math.round(ca[i] + (cb[i] - ca[i]) * k)) as [number, number, number]);\n}\n\n/** Excel's default icon thresholds: percent 0, 33, 67 for three icons. */\nexport function defaultIconThresholds(set: string): ConditionalFormatValue[] {\n const k = conditionalFormatIconCount(set);\n return Array.from({ length: k }, (_, i) => ({ type: 'percent' as const, value: Math.round((i * 100) / k) }));\n}\n\nconst usesStats = (rule: ConditionalFormatRule): boolean =>\n rule.type === 'colorScale' ||\n rule.type === 'dataBar' ||\n rule.type === 'iconSet' ||\n rule.type === 'top10' ||\n rule.type === 'aboveAverage' ||\n rule.type === 'duplicate' ||\n rule.type === 'unique';\n\nconst isFormulaText = (text: string | undefined) => !!text && text.trimStart().startsWith('=');\n\n/** Whether a rule reads cells through formulas (so any edit may change it). */\nfunction usesFormulas(rule: ConditionalFormatRule): boolean {\n switch (rule.type) {\n case 'expression':\n return true;\n case 'cellIs':\n return isFormulaText(rule.value) || isFormulaText(rule.value2);\n case 'colorScale':\n return rule.stops.some((s) => s.value.type === 'formula');\n case 'dataBar':\n return rule.min?.type === 'formula' || rule.max?.type === 'formula';\n case 'iconSet':\n return !!rule.thresholds?.some((t) => t.type === 'formula');\n default:\n return false;\n }\n}\n\n/** Adds the parts of `style` that `out` leaves unset (higher-priority rules come first). */\nfunction layerStyle(out: ConditionalCellFormat, style: ConditionalFormatStyle): void {\n if (out.fill === undefined && style.fill) out.fill = style.fill;\n if (out.color === undefined && style.color) out.color = style.color;\n if (style.bold) out.bold = true;\n if (style.italic) out.italic = true;\n if (style.underline) out.underline = true;\n if (style.strikethrough) out.strikethrough = true;\n}\n\n// Numeric cell keys (much faster than strings in large maps); columns stay below 2^20.\nconst COL_SPAN = 1048576;\nconst cellKey = (row: number, col: number) => row * COL_SPAN + col;\n\nclass Overlay implements ConditionalFormatOverlay {\n constructor(private readonly cells: Map<number, ConditionalCellFormat>) {}\n get(row: number, col: number): ConditionalCellFormat | undefined {\n return this.cells.size === 0 ? undefined : this.cells.get(cellKey(row, col));\n }\n get size(): number {\n return this.cells.size;\n }\n *entries(): IterableIterator<[{ row: number; col: number }, ConditionalCellFormat]> {\n for (const [key, format] of this.cells) {\n const row = Math.floor(key / COL_SPAN);\n const col = key - row * COL_SPAN;\n yield [{ row, col }, format];\n }\n }\n}\n\n/** An overlay with no formatted cells. */\nexport const EMPTY_CONDITIONAL_FORMAT_OVERLAY: ConditionalFormatOverlay = new Overlay(new Map());\n\n/**\n * Evaluates conditional formats and keeps what it needs to re-evaluate only\n * the affected cells after an edit (`update`). Rules are applied in order\n * (the first has the highest priority): every matching rule adds the parts\n * of its style that higher-priority rules left unset, and a matching rule\n * with `stopIfTrue` hides lower-priority rules. Colour scales, data bars and\n * icon sets apply to numbers only and never stop other rules.\n */\nexport class ConditionalFormatEngine {\n private cells = new Map<number, ConditionalCellFormat>();\n private stats = new Map<string, RuleStats>();\n private rulesKey: string | null = null;\n private current: ConditionalFormatOverlay = EMPTY_CONDITIONAL_FORMAT_OVERLAY;\n\n constructor(private sheet: ConditionalFormatSheet) {}\n\n /** Replaces the sheet accessor (e.g. after rows or columns were added). */\n setSheet(sheet: ConditionalFormatSheet): void {\n this.sheet = sheet;\n }\n\n /** The last evaluated overlay. */\n get overlay(): ConditionalFormatOverlay {\n return this.current;\n }\n\n /** Evaluates every rule over every cell it applies to. */\n evaluate(rules: ConditionalFormatRule[]): ConditionalFormatOverlay {\n this.rulesKey = JSON.stringify(rules);\n this.stats.clear();\n this.cells = new Map();\n const { rowCount, colCount } = this.sheet;\n const done = new Set<number>();\n for (const rule of rules) {\n forEachRangeCell(rule.ranges, rowCount, colCount, (row, col) => {\n const key = cellKey(row, col);\n if (done.has(key)) return;\n done.add(key);\n const look = this.computeLook(rules, row, col);\n if (look) this.cells.set(key, look);\n });\n }\n this.current = new Overlay(this.cells);\n return this.current;\n }\n\n /**\n * Re-evaluates after the values of `changed` cells changed (the edited\n * cells and the formulas that depend on them). Rules that look at the whole\n * range (scales, bars, icons, top/bottom, average, duplicates) are\n * re-evaluated over their range when a changed cell is inside it; formula\n * rules always are, since their formulas may read any cell; other rules\n * only at the changed cells. Falls back to `evaluate` when the rules\n * changed.\n */\n update(rules: ConditionalFormatRule[], changed: Iterable<{ row: number; col: number }>): ConditionalFormatOverlay {\n const key = JSON.stringify(rules);\n if (key !== this.rulesKey) return this.evaluate(rules);\n if (rules.length === 0) return this.current;\n const changedCells = Array.from(changed);\n const { rowCount, colCount } = this.sheet;\n const affected = new Map<number, { row: number; col: number }>();\n const addCell = (row: number, col: number) => affected.set(cellKey(row, col), { row, col });\n for (const rule of rules) {\n const wholeRange = usesFormulas(rule);\n const touched = changedCells.some((c) => rangesContain(rule.ranges, c.row, c.col));\n if (usesStats(rule) && touched) this.stats.delete(rule.id);\n if (wholeRange || (usesStats(rule) && touched)) {\n forEachRangeCell(rule.ranges, rowCount, colCount, addCell);\n } else if (touched) {\n for (const c of changedCells) {\n if (c.row < rowCount && c.col < colCount && rangesContain(rule.ranges, c.row, c.col)) addCell(c.row, c.col);\n }\n }\n }\n if (affected.size === 0) return this.current;\n const next = new Map(this.cells);\n for (const [k, { row, col }] of affected) {\n const look = this.computeLook(rules, row, col);\n if (look) next.set(k, look);\n else next.delete(k);\n }\n this.cells = next;\n this.current = new Overlay(next);\n return this.current;\n }\n\n private statsFor(rule: ConditionalFormatRule): RuleStats {\n let st = this.stats.get(rule.id);\n if (st) return st;\n const sorted: number[] = [];\n const counts = new Map<string, number>();\n forEachRangeCell(rule.ranges, this.sheet.rowCount, this.sheet.colCount, (row, col) => {\n const v = this.sheet.getValue(row, col);\n const n = conditionalFormatNumber(v);\n if (n !== null) sorted.push(n);\n if (!isBlank(v)) {\n const text = valueText(v).toLowerCase();\n counts.set(text, (counts.get(text) ?? 0) + 1);\n }\n });\n sorted.sort((a, b) => a - b);\n const n = Math.max(sorted.length, 1);\n let sum = 0;\n for (const x of sorted) sum += x;\n const avg = sum / n;\n let sq = 0;\n for (const x of sorted) sq += (x - avg) * (x - avg);\n st = { sorted, avg, std: Math.sqrt(sq / n), counts };\n this.stats.set(rule.id, st);\n return st;\n }\n\n private evalFormula(formula: string, rule: ConditionalFormatRule, row: number, col: number): unknown {\n if (!this.sheet.evaluateFormula) return '#NAME?';\n try {\n return firstValue(this.sheet.evaluateFormula(ruleFormulaAt(formula, ruleAnchor(rule.ranges), row, col), row, col));\n } catch {\n return '#ERROR!';\n }\n }\n\n private threshold(rule: ConditionalFormatRule, st: RuleStats, k: ConditionalFormatValue, row: number, col: number): number {\n const min = st.sorted[0] ?? 0;\n const max = st.sorted[st.sorted.length - 1] ?? 0;\n switch (k.type) {\n case 'min':\n return min;\n case 'max':\n return max;\n case 'number':\n return k.value;\n case 'percent':\n return min + ((max - min) * k.value) / 100;\n case 'percentile':\n return percentile(st.sorted, k.value);\n case 'formula': {\n const n = conditionalFormatNumber(this.evalFormula(k.formula, rule, row, col));\n return n ?? 0;\n }\n }\n }\n\n /** The operand of a `cellIs` rule: a formula result, or typed input. */\n private operand(text: string | undefined, rule: ConditionalFormatRule, row: number, col: number): unknown {\n if (text === undefined) return '';\n if (isFormulaText(text)) return this.evalFormula(text, rule, row, col);\n return parseCellInput(text).value;\n }\n\n /** Combines every rule that applies to (row, col). */\n computeLook(rules: ConditionalFormatRule[], row: number, col: number): ConditionalCellFormat | undefined {\n const out: ConditionalCellFormat = {};\n let any = false;\n let v: unknown;\n let loaded = false;\n const value = () => {\n if (!loaded) {\n v = this.sheet.getValue(row, col);\n loaded = true;\n }\n return v;\n };\n for (const rule of rules) {\n if (!rangesContain(rule.ranges, row, col)) continue;\n const matched = this.matchRule(rule, value(), row, col, out);\n if (matched === 'look') any = true;\n if (matched && matched !== 'look') {\n any = true;\n layerStyle(out, matched);\n if (rule.stopIfTrue) break;\n }\n }\n return any ? out : undefined;\n }\n\n /**\n * Checks one rule: returns its style when it matches, 'look' when it set a\n * scale fill, bar or icon on `out`, or null.\n */\n private matchRule(\n rule: ConditionalFormatRule,\n v: unknown,\n row: number,\n col: number,\n out: ConditionalCellFormat\n ): ConditionalFormatStyle | 'look' | null {\n const error = isFormulaError(v);\n switch (rule.type) {\n case 'cellIs': {\n if (error) return null;\n const a = this.operand(rule.value, rule, row, col);\n let ok: boolean;\n if (rule.operator === 'between' || rule.operator === 'notBetween') {\n const b = this.operand(rule.value2, rule, row, col);\n const [lo, hi] = compareValues(a, b) > 0 ? [b, a] : [a, b];\n const inside = compareValues(v, lo) >= 0 && compareValues(v, hi) <= 0;\n ok = rule.operator === 'between' ? inside : !inside;\n } else {\n if (isFormulaError(a)) return null;\n const cmp = compareValues(v, a);\n ok =\n rule.operator === 'equal'\n ? cmp === 0\n : rule.operator === 'notEqual'\n ? cmp !== 0\n : rule.operator === 'greaterThan'\n ? cmp > 0\n : rule.operator === 'lessThan'\n ? cmp < 0\n : rule.operator === 'greaterThanOrEqual'\n ? cmp >= 0\n : cmp <= 0;\n }\n return ok ? rule.style : null;\n }\n case 'expression': {\n const result = toBoolean(this.evalFormula(rule.formula, rule, row, col));\n return result === true ? rule.style : null;\n }\n case 'containsText':\n case 'notContainsText':\n case 'beginsWith':\n case 'endsWith': {\n const text = valueText(v).toLowerCase();\n const t = rule.text.toLowerCase();\n const ok =\n rule.type === 'containsText'\n ? text.includes(t)\n : rule.type === 'notContainsText'\n ? !text.includes(t)\n : rule.type === 'beginsWith'\n ? text.startsWith(t)\n : text.endsWith(t);\n return ok ? rule.style : null;\n }\n case 'blanks':\n return isBlank(v) || valueText(v).trim() === '' ? rule.style : null;\n case 'noBlanks':\n return !isBlank(v) && valueText(v).trim() !== '' ? rule.style : null;\n case 'errors':\n return error ? rule.style : null;\n case 'noErrors':\n return !error ? rule.style : null;\n case 'duplicate':\n case 'unique': {\n if (isBlank(v)) return null;\n const count = this.statsFor(rule).counts.get(valueText(v).toLowerCase()) ?? 0;\n return (rule.type === 'duplicate' ? count > 1 : count === 1) ? rule.style : null;\n }\n case 'top10': {\n const n = conditionalFormatNumber(v);\n const st = this.statsFor(rule);\n if (n === null || st.sorted.length === 0) return null;\n const rank = Math.max(1, Math.floor(rule.rank || 1));\n let k = rule.percent ? Math.max(1, Math.floor((st.sorted.length * rank) / 100)) : rank;\n k = Math.min(Math.max(k, 1), st.sorted.length);\n const ok = rule.bottom ? n <= st.sorted[k - 1] : n >= st.sorted[st.sorted.length - k];\n return ok ? rule.style : null;\n }\n case 'aboveAverage': {\n const n = conditionalFormatNumber(v);\n if (n === null) return null;\n const st = this.statsFor(rule);\n const thr = st.avg + (rule.below ? -1 : 1) * st.std * (rule.stdDev ?? 0);\n const ok = rule.below ? n < thr || (!!rule.equal && n === thr) : n > thr || (!!rule.equal && n === thr);\n return ok ? rule.style : null;\n }\n case 'timePeriod': {\n const n = conditionalFormatNumber(v);\n if (n === null) return null;\n return matchesTimePeriod(rule.period, Math.floor(n), this.sheet.today?.() ?? new Date()) ? rule.style : null;\n }\n case 'colorScale': {\n const n = conditionalFormatNumber(v);\n if (n === null || out.fill !== undefined) return null;\n const st = this.statsFor(rule);\n const pts = rule.stops.map((s) => ({ at: this.threshold(rule, st, s.value, row, col), color: s.color }));\n let fill: string | undefined;\n if (pts.length === 2) {\n const [a, b] = pts;\n fill = blendConditionalFormatColors(a.color, b.color, b.at > a.at ? (n - a.at) / (b.at - a.at) : 0);\n } else if (pts.length >= 3) {\n const [a, m, b] = pts;\n fill =\n n <= m.at\n ? blendConditionalFormatColors(a.color, m.color, m.at > a.at ? (n - a.at) / (m.at - a.at) : 0)\n : blendConditionalFormatColors(m.color, b.color, b.at > m.at ? (n - m.at) / (b.at - m.at) : 1);\n }\n if (!fill) return null;\n out.fill = fill;\n return 'look';\n }\n case 'dataBar': {\n const n = conditionalFormatNumber(v);\n if (n === null || out.dataBar) return null;\n const st = this.statsFor(rule);\n let lo = this.threshold(rule, st, rule.min ?? { type: 'min' }, row, col);\n const hi = this.threshold(rule, st, rule.max ?? { type: 'max' }, row, col);\n // Bars of positive numbers start at zero.\n if (lo > 0) lo = 0;\n const frac = hi > lo ? Math.min(Math.max((n - lo) / (hi - lo), 0), 1) : 1;\n out.dataBar = {\n // A minimum length keeps small positive values visible.\n fraction: frac * 0.9 + (frac > 0 ? 0.1 : 0),\n color: rule.color ?? DEFAULT_BAR_COLOR,\n gradient: rule.gradient ?? true,\n };\n if (rule.showValue === false) out.hideValue = true;\n return 'look';\n }\n case 'iconSet': {\n const n = conditionalFormatNumber(v);\n if (n === null || out.icon) return null;\n const set = rule.set ?? '3TrafficLights1';\n const k = conditionalFormatIconCount(set);\n const thresholds = rule.thresholds ?? [];\n // One threshold per icon (the first is the lower bound), or one fewer.\n const lower =\n thresholds.length === k\n ? thresholds.slice(1)\n : thresholds.length + 1 === k\n ? thresholds\n : defaultIconThresholds(set).slice(1);\n const st = this.statsFor(rule);\n let idx = 0;\n for (const t of lower) if (n >= this.threshold(rule, st, t, row, col)) idx++;\n if (rule.reverse) idx = k - 1 - Math.min(idx, k - 1);\n idx = Math.min(idx, k - 1);\n out.icon = { set, index: idx, id: `${set}:${idx}` };\n if (rule.showValue === false) out.hideValue = true;\n return 'look';\n }\n }\n return null;\n }\n}\n\n/** Whether serial day `d` falls in `period` relative to `today`. */\nexport function matchesTimePeriod(period: string, d: number, today: Date): boolean {\n const t = serialFromYmd(today.getFullYear(), today.getMonth() + 1, today.getDate());\n if (t === null) return false;\n const weekStart = t - today.getDay();\n const date = serialToDate(d);\n const monthOffset = (k: number) => {\n if (!date) return false;\n const m = today.getMonth() + k;\n const y = today.getFullYear() + Math.floor(m / 12);\n const mm = ((m % 12) + 12) % 12;\n return date.getFullYear() === y && date.getMonth() === mm;\n };\n switch (period) {\n case 'yesterday':\n return d === t - 1;\n case 'today':\n return d === t;\n case 'tomorrow':\n return d === t + 1;\n case 'last7Days':\n return d > t - 7 && d <= t;\n case 'lastWeek':\n return d >= weekStart - 7 && d < weekStart;\n case 'thisWeek':\n return d >= weekStart && d < weekStart + 7;\n case 'nextWeek':\n return d >= weekStart + 7 && d < weekStart + 14;\n case 'lastMonth':\n return monthOffset(-1);\n case 'thisMonth':\n return monthOffset(0);\n case 'nextMonth':\n return monthOffset(1);\n default:\n return false;\n }\n}\n\n/** Evaluates `rules` over a sheet: the conditional look of every formatted cell. */\nexport function evaluateConditionalFormats(sheet: ConditionalFormatSheet, rules: ConditionalFormatRule[]): ConditionalFormatOverlay {\n return new ConditionalFormatEngine(sheet).evaluate(rules);\n}\n\n/** Whether a rule's ranges intersect `range` (for \"clear rules from selection\"). */\nexport function conditionalFormatTouchesRange(\n rule: { ranges: { startRow: number; startCol: number; endRow: number; endCol: number }[] },\n range: { startRow: number; startCol: number; endRow: number; endCol: number }\n): boolean {\n return rule.ranges.some((r) => rangesIntersect(r, range));\n}\n","/**\n * One-line descriptions of rules for the conditional formatting and data\n * validation managers, shared by React and Svelte.\n */\nimport type { ConditionalFormatOperator, ConditionalFormatRule, ConditionalFormatValue } from './types';\nimport type { DataValidationRule } from '../validation/types';\n\nconst OPERATOR_TEXT: Record<ConditionalFormatOperator, string> = {\n between: 'between',\n notBetween: 'not between',\n equal: '=',\n notEqual: '≠',\n greaterThan: '>',\n lessThan: '<',\n greaterThanOrEqual: '≥',\n lessThanOrEqual: '≤',\n};\n\nconst anchorText = (v: ConditionalFormatValue | undefined, fallback: string): string => {\n if (!v) return fallback;\n switch (v.type) {\n case 'min':\n return 'min';\n case 'max':\n return 'max';\n case 'number':\n return String(v.value);\n case 'percent':\n return `${v.value}%`;\n case 'percentile':\n return `P${v.value}`;\n case 'formula':\n return v.formula;\n }\n};\n\n/** E.g. `Cell value > 100`, `Color scale (min → P50 → max)`, `Formula: =$B2>10`. */\nexport function describeConditionalFormat(rule: ConditionalFormatRule): string {\n switch (rule.type) {\n case 'cellIs':\n return rule.operator === 'between' || rule.operator === 'notBetween'\n ? `Cell value ${OPERATOR_TEXT[rule.operator]} ${rule.value} and ${rule.value2 ?? ''}`\n : `Cell value ${OPERATOR_TEXT[rule.operator]} ${rule.value}`;\n case 'expression':\n return `Formula: ${rule.formula.startsWith('=') ? rule.formula : `=${rule.formula}`}`;\n case 'containsText':\n return `Text contains \"${rule.text}\"`;\n case 'notContainsText':\n return `Text does not contain \"${rule.text}\"`;\n case 'beginsWith':\n return `Text begins with \"${rule.text}\"`;\n case 'endsWith':\n return `Text ends with \"${rule.text}\"`;\n case 'blanks':\n return 'Blank cells';\n case 'noBlanks':\n return 'Non-blank cells';\n case 'errors':\n return 'Errors';\n case 'noErrors':\n return 'No errors';\n case 'duplicate':\n return 'Duplicate values';\n case 'unique':\n return 'Unique values';\n case 'top10':\n return `${rule.bottom ? 'Bottom' : 'Top'} ${rule.rank}${rule.percent ? '%' : ''}`;\n case 'aboveAverage':\n return `${rule.below ? 'Below' : 'Above'} average${rule.stdDev ? ` by ${rule.stdDev} std dev` : ''}`;\n case 'timePeriod':\n return `Date occurring: ${rule.period}`;\n case 'colorScale':\n return `Color scale (${rule.stops.map((s) => anchorText(s.value, '')).join(' → ')})`;\n case 'dataBar':\n return `Data bar (${anchorText(rule.min, 'min')} → ${anchorText(rule.max, 'max')})`;\n case 'iconSet':\n return `Icon set ${rule.set ?? '3TrafficLights1'}${rule.reverse ? ' (reversed)' : ''}`;\n }\n}\n\nconst VALIDATION_OPERATOR_TEXT: Record<string, string> = OPERATOR_TEXT;\n\n/** E.g. `Whole number between 1 and 10`, `List: Low,Medium,High`, `Custom: =A1>0`. */\nexport function describeDataValidation(rule: DataValidationRule): string {\n const op = rule.operator ?? 'between';\n const bounds =\n op === 'between' || op === 'notBetween'\n ? `${VALIDATION_OPERATOR_TEXT[op]} ${rule.formula1 ?? ''} and ${rule.formula2 ?? ''}`\n : `${VALIDATION_OPERATOR_TEXT[op]} ${rule.formula1 ?? ''}`;\n switch (rule.type) {\n case 'any':\n return 'Any value';\n case 'whole':\n return `Whole number ${bounds}`;\n case 'decimal':\n return `Decimal ${bounds}`;\n case 'date':\n return `Date ${bounds}`;\n case 'time':\n return `Time ${bounds}`;\n case 'textLength':\n return `Text length ${bounds}`;\n case 'list':\n return `List: ${rule.formula1 ?? ''}`;\n case 'custom':\n return `Custom: ${rule.formula1 ?? ''}`;\n }\n}\n","/**\n * Ready-made rules for the conditional formatting and data validation\n * managers and the context menu, so React and Svelte offer the same choices.\n */\nimport type { CellRange } from '../types';\nimport type { DataValidationErrorStyle, DataValidationOperator, DataValidationType } from '../validation/types';\nimport type {\n ConditionalFormatIconSet,\n ConditionalFormatStyle,\n ConditionalFormatTimePeriod,\n NewConditionalFormatRule,\n} from './types';\n\n/** Excel's highlight styles. */\nexport const CONDITIONAL_FORMAT_STYLE_PRESETS: Array<{ id: string; label: string; style: ConditionalFormatStyle }> = [\n { id: 'lightRed', label: 'Light red fill, dark red text', style: { fill: '#ffc7ce', color: '#9c0006' } },\n { id: 'yellow', label: 'Yellow fill, dark yellow text', style: { fill: '#ffeb9c', color: '#9c5700' } },\n { id: 'green', label: 'Green fill, dark green text', style: { fill: '#c6efce', color: '#006100' } },\n { id: 'redText', label: 'Red text', style: { color: '#9c0006' } },\n { id: 'bold', label: 'Bold', style: { bold: true } },\n];\n\nexport const DEFAULT_CONDITIONAL_FORMAT_STYLE: ConditionalFormatStyle = CONDITIONAL_FORMAT_STYLE_PRESETS[0].style;\n\nexport type ConditionalFormatPresetKind =\n | 'greaterThan'\n | 'lessThan'\n | 'between'\n | 'equal'\n | 'textContains'\n | 'dateOccurring'\n | 'duplicate'\n | 'unique'\n | 'top10'\n | 'bottom10'\n | 'aboveAverage'\n | 'belowAverage'\n | 'blanks'\n | 'errors'\n | 'formula'\n | 'colorScale2'\n | 'colorScale3'\n | 'dataBar'\n | 'iconSet';\n\n/** Inputs a preset needs, besides the ranges. */\nexport type ConditionalFormatPresetField = 'value' | 'value2' | 'text' | 'formula' | 'rank' | 'period' | 'iconSet' | 'color';\n\nexport interface ConditionalFormatPresetInfo {\n kind: ConditionalFormatPresetKind;\n label: string;\n fields: ConditionalFormatPresetField[];\n /** Whether the rule takes a highlight style (scales, bars and icons don't). */\n usesStyle: boolean;\n}\n\nexport const CONDITIONAL_FORMAT_PRESETS: ConditionalFormatPresetInfo[] = [\n { kind: 'greaterThan', label: 'Cell value greater than', fields: ['value'], usesStyle: true },\n { kind: 'lessThan', label: 'Cell value less than', fields: ['value'], usesStyle: true },\n { kind: 'between', label: 'Cell value between', fields: ['value', 'value2'], usesStyle: true },\n { kind: 'equal', label: 'Cell value equal to', fields: ['value'], usesStyle: true },\n { kind: 'textContains', label: 'Text that contains', fields: ['text'], usesStyle: true },\n { kind: 'dateOccurring', label: 'A date occurring', fields: ['period'], usesStyle: true },\n { kind: 'duplicate', label: 'Duplicate values', fields: [], usesStyle: true },\n { kind: 'unique', label: 'Unique values', fields: [], usesStyle: true },\n { kind: 'top10', label: 'Top N items', fields: ['rank'], usesStyle: true },\n { kind: 'bottom10', label: 'Bottom N items', fields: ['rank'], usesStyle: true },\n { kind: 'aboveAverage', label: 'Above average', fields: [], usesStyle: true },\n { kind: 'belowAverage', label: 'Below average', fields: [], usesStyle: true },\n { kind: 'blanks', label: 'Blank cells', fields: [], usesStyle: true },\n { kind: 'errors', label: 'Errors', fields: [], usesStyle: true },\n { kind: 'formula', label: 'Use a formula', fields: ['formula'], usesStyle: true },\n { kind: 'colorScale2', label: '2-color scale', fields: [], usesStyle: false },\n { kind: 'colorScale3', label: '3-color scale', fields: [], usesStyle: false },\n { kind: 'dataBar', label: 'Data bars', fields: ['color'], usesStyle: false },\n { kind: 'iconSet', label: 'Icon set', fields: ['iconSet'], usesStyle: false },\n];\n\nexport const CONDITIONAL_FORMAT_TIME_PERIODS: Array<{ period: ConditionalFormatTimePeriod; label: string }> = [\n { period: 'yesterday', label: 'Yesterday' },\n { period: 'today', label: 'Today' },\n { period: 'tomorrow', label: 'Tomorrow' },\n { period: 'last7Days', label: 'In the last 7 days' },\n { period: 'lastWeek', label: 'Last week' },\n { period: 'thisWeek', label: 'This week' },\n { period: 'nextWeek', label: 'Next week' },\n { period: 'lastMonth', label: 'Last month' },\n { period: 'thisMonth', label: 'This month' },\n { period: 'nextMonth', label: 'Next month' },\n];\n\nexport interface ConditionalFormatPresetInput {\n kind: ConditionalFormatPresetKind;\n ranges: CellRange[];\n value?: string;\n value2?: string;\n text?: string;\n formula?: string;\n rank?: number;\n period?: ConditionalFormatTimePeriod;\n iconSet?: ConditionalFormatIconSet;\n /** Data bar colour. */\n color?: string;\n /** Highlight style (default light red fill, dark red text). */\n style?: ConditionalFormatStyle;\n stopIfTrue?: boolean;\n}\n\n/** Builds a rule from a preset and its inputs. */\nexport function createConditionalFormatRule(input: ConditionalFormatPresetInput): NewConditionalFormatRule {\n const style = input.style ?? DEFAULT_CONDITIONAL_FORMAT_STYLE;\n const base = { ranges: input.ranges, ...(input.stopIfTrue ? { stopIfTrue: true } : {}) };\n switch (input.kind) {\n case 'greaterThan':\n case 'lessThan':\n case 'equal':\n return { ...base, type: 'cellIs', operator: input.kind, value: input.value ?? '0', style };\n case 'between':\n return { ...base, type: 'cellIs', operator: 'between', value: input.value ?? '0', value2: input.value2 ?? '0', style };\n case 'textContains':\n return { ...base, type: 'containsText', text: input.text ?? '', style };\n case 'dateOccurring':\n return { ...base, type: 'timePeriod', period: input.period ?? 'today', style };\n case 'duplicate':\n case 'unique':\n case 'blanks':\n case 'errors':\n return { ...base, type: input.kind, style };\n case 'top10':\n case 'bottom10':\n return { ...base, type: 'top10', bottom: input.kind === 'bottom10', rank: input.rank ?? 10, style };\n case 'aboveAverage':\n case 'belowAverage':\n return { ...base, type: 'aboveAverage', below: input.kind === 'belowAverage', style };\n case 'formula':\n return { ...base, type: 'expression', formula: input.formula ?? '=TRUE', style };\n case 'colorScale2':\n return {\n ...base,\n type: 'colorScale',\n stops: [\n { value: { type: 'min' }, color: '#ffffff' },\n { value: { type: 'max' }, color: '#63be7b' },\n ],\n };\n case 'colorScale3':\n return {\n ...base,\n type: 'colorScale',\n stops: [\n { value: { type: 'min' }, color: '#f8696b' },\n { value: { type: 'percentile', value: 50 }, color: '#ffeb84' },\n { value: { type: 'max' }, color: '#63be7b' },\n ],\n };\n case 'dataBar':\n return { ...base, type: 'dataBar', color: input.color ?? '#638ec6' };\n case 'iconSet':\n return { ...base, type: 'iconSet', set: input.iconSet ?? '3TrafficLights1' };\n }\n}\n\nexport const DATA_VALIDATION_TYPE_OPTIONS: Array<{ type: DataValidationType; label: string }> = [\n { type: 'any', label: 'Any value' },\n { type: 'whole', label: 'Whole number' },\n { type: 'decimal', label: 'Decimal' },\n { type: 'list', label: 'List' },\n { type: 'date', label: 'Date' },\n { type: 'time', label: 'Time' },\n { type: 'textLength', label: 'Text length' },\n { type: 'custom', label: 'Custom formula' },\n];\n\nexport const DATA_VALIDATION_OPERATOR_OPTIONS: Array<{ operator: DataValidationOperator; label: string }> = [\n { operator: 'between', label: 'between' },\n { operator: 'notBetween', label: 'not between' },\n { operator: 'equal', label: 'equal to' },\n { operator: 'notEqual', label: 'not equal to' },\n { operator: 'greaterThan', label: 'greater than' },\n { operator: 'lessThan', label: 'less than' },\n { operator: 'greaterThanOrEqual', label: 'greater than or equal to' },\n { operator: 'lessThanOrEqual', label: 'less than or equal to' },\n];\n\nexport const DATA_VALIDATION_ERROR_STYLE_OPTIONS: Array<{ style: DataValidationErrorStyle; label: string }> = [\n { style: 'stop', label: 'Stop' },\n { style: 'warning', label: 'Warning' },\n { style: 'information', label: 'Information' },\n];\n","/**\n * CSS for the conditional look of a cell, shared by the React and Svelte\n * grids (React uses the objects, Svelte the `style` strings).\n */\nimport type { ConditionalCellFormat } from './types';\n\nexport interface ConditionalFormatCss {\n background?: string;\n color?: string;\n fontWeight?: string;\n fontStyle?: string;\n textDecoration?: string;\n}\n\n/**\n * Black or white, whichever reads better on `fill` (`#rgb` / `#rrggbb`), or\n * undefined for other colour syntaxes.\n */\nexport function conditionalFormatContrastColor(fill: string): string | undefined {\n let hex = fill.trim().replace(/^#/, '');\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n if (!/^[0-9a-fA-F]{6}$/.test(hex)) return undefined;\n const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);\n // Perceived brightness (ITU-R BT.601 weights).\n return 0.299 * r + 0.587 * g + 0.114 * b > 0.55 ? '#1f2328' : '#ffffff';\n}\n\n/**\n * Fill, font colour and emphasis of a cell (no data bar or icon). A fill\n * without a font colour of its own gets black or white text, so the value\n * stays readable whatever the theme's text colour (Excel's fills assume\n * dark text).\n */\nexport function getConditionalFormatCss(format: ConditionalCellFormat | undefined): ConditionalFormatCss {\n if (!format) return {};\n const css: ConditionalFormatCss = {};\n if (format.fill) css.background = format.fill;\n if (format.color) css.color = format.color;\n else if (format.fill) {\n const contrast = conditionalFormatContrastColor(format.fill);\n if (contrast) css.color = contrast;\n }\n if (format.bold) css.fontWeight = '700';\n if (format.italic) css.fontStyle = 'italic';\n const decorations = [format.underline ? 'underline' : '', format.strikethrough ? 'line-through' : ''].filter(Boolean);\n if (decorations.length) css.textDecoration = decorations.join(' ');\n return css;\n}\n\n/** The bar element of a data bar: absolutely placed from the cell's left edge. */\nexport function getDataBarCss(bar: NonNullable<ConditionalCellFormat['dataBar']>): Record<string, string> {\n const width = `${Math.round(Math.min(Math.max(bar.fraction, 0), 1) * 1000) / 10}%`;\n return {\n position: 'absolute',\n left: '0',\n top: '2px',\n bottom: '2px',\n width,\n pointerEvents: 'none',\n borderRadius: '1px',\n background: bar.gradient\n ? `linear-gradient(90deg, ${bar.color}, color-mix(in srgb, ${bar.color} 20%, transparent))`\n : bar.color,\n opacity: bar.gradient ? '0.9' : '0.75',\n };\n}\n\nconst kebab = (name: string) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);\n\n/** `{ fontWeight: '700' }` → `font-weight: 700;` for `style` attributes. */\nexport function conditionalFormatStyleString(css: Record<string, string | undefined> | ConditionalFormatCss): string {\n let out = '';\n for (const [key, value] of Object.entries(css)) {\n if (value !== undefined && value !== '') out += `${kebab(key)}: ${value}; `;\n }\n return out.trim();\n}\n","/**\n * Data validation: checks typed input against a cell's rule, lists dropdown\n * items and finds invalid cells (\"circle invalid data\").\n *\n * Portions ported from gridcraft/crates/engine/src/cmd/data.rs (MIT OR\n * Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n */\nimport { compareValues, numberToText, parseCellInput } from '../formula';\nimport { isFormulaError, toBoolean } from '../formula/values';\nimport { conditionalFormatNumber } from '../conditional-format/evaluate';\nimport { forEachRangeCell, rangesContain, ruleAnchor, ruleFormulaAt } from '../rule-ranges';\nimport type { CellPosition } from '../formula';\nimport type {\n DataValidationOperator,\n DataValidationResult,\n DataValidationRule,\n DataValidationSheet,\n} from './types';\n\n/** Excel's message when a rule has no error message of its own. */\nexport const DEFAULT_DATA_VALIDATION_MESSAGE =\n \"This value doesn't match the data validation restrictions defined for this cell.\";\nexport const DEFAULT_DATA_VALIDATION_TITLE = 'Invalid value';\n\n// A bare reference (`H1:H5`, `$H$1:$H$5`) used as a list source without `=`.\nconst BARE_REFERENCE = /^\\$?[A-Za-z]{1,3}\\$?\\d{1,7}(?::\\$?[A-Za-z]{1,3}\\$?\\d{1,7})?$/;\n\nconst isBlank = (v: unknown) => v === '' || v === null || v === undefined;\n\nfunction valueText(value: unknown): string {\n if (isBlank(value)) return '';\n if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';\n if (typeof value === 'number') return numberToText(value);\n return String(value);\n}\n\n/** The rule that applies to (row, col): the first one containing it. */\nexport function findDataValidation(rules: DataValidationRule[] | undefined, row: number, col: number): DataValidationRule | undefined {\n if (!rules) return undefined;\n for (const rule of rules) if (rangesContain(rule.ranges, row, col)) return rule;\n return undefined;\n}\n\ntype Entered = { row: number; col: number; value: unknown };\n\nfunction evaluate(sheet: DataValidationSheet, rule: DataValidationRule, formula: string, row: number, col: number, entered?: Entered): unknown {\n if (!sheet.evaluateFormula) return '#NAME?';\n try {\n return sheet.evaluateFormula(ruleFormulaAt(formula, ruleAnchor(rule.ranges), row, col), row, col, entered);\n } catch {\n return '#ERROR!';\n }\n}\n\nconst first = (v: unknown) => (Array.isArray(v) ? (v.length ? v[0] : '') : v);\n\n/**\n * The items of a list rule: literal (`Low, Medium, High`, optionally in\n * quotes) or read from a range or formula (`=$H$1:$H$5`), blanks skipped.\n * Formulas are evaluated at the range's top-left cell.\n */\nexport function getValidationListValues(sheet: DataValidationSheet, rule: DataValidationRule): string[] {\n const f = (rule.formula1 ?? '').trim();\n if (f === '') return [];\n if (f.startsWith('=') || BARE_REFERENCE.test(f)) {\n const anchor = ruleAnchor(rule.ranges);\n const result = evaluate(sheet, rule, f, anchor.row, anchor.col);\n const values = Array.isArray(result) ? result : [result];\n const out: string[] = [];\n for (const v of values) if (!isBlank(v) && !isFormulaError(v)) out.push(valueText(v));\n return out;\n }\n const unquoted = f.length >= 2 && f.startsWith('\"') && f.endsWith('\"') ? f.slice(1, -1) : f;\n return unquoted\n .split(',')\n .map((x) => x.trim())\n .filter((x) => x !== '');\n}\n\nfunction compareOp(op: DataValidationOperator, n: number, a: number | null, b: number | null): boolean {\n if (a === null) return false;\n switch (op) {\n case 'between':\n return b !== null && n >= Math.min(a, b) && n <= Math.max(a, b);\n case 'notBetween':\n return b !== null && (n < Math.min(a, b) || n > Math.max(a, b));\n case 'equal':\n return n === a;\n case 'notEqual':\n return n !== a;\n case 'greaterThan':\n return n > a;\n case 'lessThan':\n return n < a;\n case 'greaterThanOrEqual':\n return n >= a;\n case 'lessThanOrEqual':\n return n <= a;\n }\n}\n\n/** A bound (`formula1` / `formula2`): a formula, or typed input such as `10`, `1/31/2026`, `9:00`. */\nfunction bound(sheet: DataValidationSheet, rule: DataValidationRule, text: string | undefined, row: number, col: number, entered?: Entered): number | null {\n if (text === undefined || text.trim() === '') return null;\n const value = text.trim().startsWith('=') ? first(evaluate(sheet, rule, text, row, col, entered)) : parseCellInput(text.trim()).value;\n return conditionalFormatNumber(value);\n}\n\n/** Whether `value` (typed as `text`) satisfies `rule` at (row, col). */\nfunction satisfies(\n sheet: DataValidationSheet,\n rule: DataValidationRule,\n row: number,\n col: number,\n value: unknown,\n text: string,\n entered?: Entered\n): boolean {\n const op = rule.operator ?? 'between';\n switch (rule.type) {\n case 'any':\n return true;\n case 'list': {\n const typed = text.trim().toLowerCase();\n return getValidationListValues(sheet, rule).some(\n (item) => item.toLowerCase() === typed || compareValues(parseCellInput(item).value, value) === 0\n );\n }\n case 'custom':\n return toBoolean(first(evaluate(sheet, rule, rule.formula1 ?? '', row, col, entered))) === true;\n case 'textLength':\n return compareOp(op, Array.from(text).length, bound(sheet, rule, rule.formula1, row, col, entered), bound(sheet, rule, rule.formula2, row, col, entered));\n case 'whole':\n case 'decimal':\n case 'date':\n case 'time': {\n if (isFormulaError(value)) return false;\n let n = conditionalFormatNumber(value);\n if (n === null) return false;\n if (rule.type === 'whole' && !Number.isInteger(n)) return false;\n // A date-time typed into a time rule counts by its time of day.\n if (rule.type === 'time' && typeof value === 'string') n -= Math.floor(n);\n return compareOp(op, n, bound(sheet, rule, rule.formula1, row, col, entered), bound(sheet, rule, rule.formula2, row, col, entered));\n }\n }\n}\n\nfunction invalid(rule: DataValidationRule): DataValidationResult {\n return {\n valid: false,\n rule,\n errorStyle: rule.errorStyle ?? 'stop',\n title: rule.errorTitle || DEFAULT_DATA_VALIDATION_TITLE,\n message: rule.errorMessage || DEFAULT_DATA_VALIDATION_MESSAGE,\n };\n}\n\n/**\n * Checks `input` typed into (row, col) against the cell's rule. Typed input\n * is parsed like a cell commit (`10%` is 0.1, `3/4/2026` a date); a formula\n * is checked through its result. Custom and bound formulas are relative to\n * the range's top-left cell and see the value being entered. Blank input is\n * valid unless `ignoreBlank` is false; rules with `showErrorAlert: false`\n * accept anything.\n */\nexport function validateCellInput(\n sheet: DataValidationSheet,\n rules: DataValidationRule[] | undefined,\n row: number,\n col: number,\n input: string\n): DataValidationResult {\n const rule = findDataValidation(rules, row, col);\n if (!rule) return { valid: true };\n if (rule.type === 'any' || rule.showErrorAlert === false) return { valid: true, rule };\n if (input.trim() === '' && (rule.ignoreBlank ?? true)) return { valid: true, rule };\n let value: unknown;\n let text = input;\n if (input.startsWith('=')) {\n value = first(sheet.evaluateFormula ? safeEval(sheet, input, row, col) : '#NAME?');\n text = valueText(value);\n } else {\n value = parseCellInput(input).value;\n if (input.startsWith(\"'\")) text = input.slice(1);\n }\n const entered = { row, col, value };\n return satisfies(sheet, rule, row, col, value, text, entered) ? { valid: true, rule } : invalid(rule);\n}\n\nfunction safeEval(sheet: DataValidationSheet, formula: string, row: number, col: number): unknown {\n try {\n return sheet.evaluateFormula!(formula, row, col);\n } catch {\n return '#ERROR!';\n }\n}\n\n/**\n * Cells whose current value breaks their rule (\"circle invalid data\").\n * Blank cells are skipped, as are `any` rules. Alerts being off does not\n * matter here. Each cell is listed once, rule by rule, in row-major order.\n */\nexport function findInvalidCells(sheet: DataValidationSheet, rules: DataValidationRule[] | undefined): CellPosition[] {\n const out: CellPosition[] = [];\n if (!rules?.length) return out;\n const seen = new Set<string>();\n for (const rule of rules) {\n if (rule.type === 'any') continue;\n forEachRangeCell(rule.ranges, sheet.rowCount, sheet.colCount, (row, col) => {\n const key = `${row}:${col}`;\n if (seen.has(key)) return;\n seen.add(key);\n // Only the first rule containing a cell applies to it.\n if (findDataValidation(rules, row, col) !== rule) return;\n const value = sheet.getValue(row, col);\n if (isBlank(value)) return;\n const raw = sheet.getInput?.(row, col);\n const text = raw !== undefined && raw !== '' && !raw.startsWith('=') ? (raw.startsWith(\"'\") ? raw.slice(1) : raw) : valueText(value);\n if (!satisfies(sheet, rule, row, col, value, text)) out.push({ row, col });\n });\n }\n return out;\n}\n","/**\n * The render model of a spreadsheet: which rows and columns are shown, in\n * which order, with group header rows, frozen (sticky) offsets and merged\n * cell spans. Filters, hidden rows/columns and collapsed groups only affect\n * this view; the document keeps every row.\n */\nimport type {\n CellRange,\n SpreadsheetColumn,\n SpreadsheetDocument,\n SpreadsheetRow,\n} from './types';\nimport type { CellPosition } from './formula';\nimport { aggregateValues, compareCellValues, isBlankValue, matchesFilterRule, normalizeRange } from './table';\n\nexport const DEFAULT_ROW_HEIGHT = 32;\nexport const DEFAULT_COLUMN_WIDTH = 130;\n\nexport type SpreadsheetRowSection = 'header' | 'body' | 'footer';\n\nexport interface SpreadsheetRowGroup {\n key: string; // grouping value as text ('' for blanks)\n label: string;\n rowIndices: number[]; // member rows that pass the filters, in document order\n collapsed: boolean;\n summaries: Record<string, number | null>; // colId -> aggregate\n}\n\nexport type SpreadsheetViewRow =\n | {\n kind: 'row';\n row: SpreadsheetRow;\n index: number; // document row index\n section: SpreadsheetRowSection;\n frozen: boolean;\n stickyTop: number; // offset from the first frozen row (frozen rows only)\n groupKey?: string;\n }\n | { kind: 'group'; group: SpreadsheetRowGroup };\n\nexport interface SpreadsheetViewColumn {\n column: SpreadsheetColumn;\n index: number; // document column index\n header: boolean; // inside the header columns\n frozen: boolean;\n stickyLeft: number; // offset from the first frozen column (frozen columns only)\n}\n\nexport interface SpreadsheetCellSpan {\n rowSpan: number;\n colSpan: number;\n /** The cell whose content is shown (a merge's top-left cell). */\n source: CellPosition;\n merge?: CellRange;\n}\n\nexport interface SpreadsheetViewModel {\n rows: SpreadsheetViewRow[];\n columns: SpreadsheetViewColumn[];\n groups: SpreadsheetRowGroup[];\n /** Visible document row indices in display order (for keyboard navigation). */\n rowOrder: number[];\n /** Visible document column indices in display order. */\n columnOrder: number[];\n filteredOutCount: number;\n hiddenRowCount: number;\n hiddenColumnCount: number;\n /**\n * Span of the cell rendered at (row, col): `{ rowSpan: 1, colSpan: 1 }` for\n * an ordinary cell, the visible extent for a merge's first visible cell, and\n * null for cells covered by a merge (render nothing).\n */\n getCellSpan(row: number, col: number): SpreadsheetCellSpan | null;\n}\n\nexport type CellValueReader = (rowIndex: number, colIndex: number) => unknown;\n\nconst clampCount = (value: number | undefined, max: number) => Math.max(0, Math.min(Math.floor(value ?? 0), max));\n\n/** Header and footer row counts, clamped so they never overlap. */\nexport function getTableSections(doc: SpreadsheetDocument): { headerRows: number; footerRows: number } {\n const total = doc.rows.length;\n const headerRows = clampCount(doc.table?.headerRowCount, total);\n const footerRows = clampCount(doc.table?.footerRowCount, total - headerRows);\n return { headerRows, footerRows };\n}\n\nexport function getRowSection(doc: SpreadsheetDocument, rowIndex: number): SpreadsheetRowSection {\n const { headerRows, footerRows } = getTableSections(doc);\n if (rowIndex < headerRows) return 'header';\n if (rowIndex >= doc.rows.length - footerRows) return 'footer';\n return 'body';\n}\n\n/** Whether a body row passes the active filters. Header/footer rows always do. */\nexport function rowPassesFilters(doc: SpreadsheetDocument, rowIndex: number, getValue: CellValueReader): boolean {\n const filter = doc.table?.filter;\n if (!filter?.enabled || filter.rules.length === 0) return true;\n if (getRowSection(doc, rowIndex) !== 'body') return true;\n return filter.rules.every((rule) => {\n const colIndex = doc.columns.findIndex((c) => c.id === rule.colId);\n if (colIndex === -1) return true; // rules on deleted columns are ignored\n return matchesFilterRule(getValue(rowIndex, colIndex), rule);\n });\n}\n\n/** Category groups of the (unhidden, unfiltered) body rows. */\nexport function computeRowGroups(doc: SpreadsheetDocument, getValue: CellValueReader): SpreadsheetRowGroup[] {\n const grouping = doc.table?.grouping;\n if (!grouping) return [];\n const groupCol = doc.columns.findIndex((c) => c.id === grouping.colId);\n if (groupCol === -1) return [];\n\n const { headerRows, footerRows } = getTableSections(doc);\n const byKey = new Map<string, number[]>();\n for (let r = headerRows; r < doc.rows.length - footerRows; r++) {\n if (doc.rows[r].hidden || !rowPassesFilters(doc, r, getValue)) continue;\n const value = getValue(r, groupCol);\n const key = isBlankValue(value) ? '' : String(value).trim();\n if (!byKey.has(key)) byKey.set(key, []);\n byKey.get(key)!.push(r);\n }\n\n const collapsed = new Set(grouping.collapsed);\n return Array.from(byKey.keys())\n .sort((a, b) => compareCellValues(a, b))\n .map((key) => {\n const rowIndices = byKey.get(key)!;\n const summaries: Record<string, number | null> = {};\n for (const summary of grouping.summaries) {\n const colIndex = doc.columns.findIndex((c) => c.id === summary.colId);\n if (colIndex === -1) continue;\n summaries[summary.colId] = aggregateValues(\n rowIndices.map((r) => getValue(r, colIndex)),\n summary.aggregate,\n );\n }\n return { key, label: key === '' ? '(blank)' : key, rowIndices, collapsed: collapsed.has(key), summaries };\n });\n}\n\nexport function buildSpreadsheetView(doc: SpreadsheetDocument, getValue: CellValueReader): SpreadsheetViewModel {\n const { headerRows, footerRows } = getTableSections(doc);\n const frozenRows = clampCount(doc.table?.frozenRowCount, doc.rows.length);\n const frozenCols = clampCount(doc.table?.frozenColumnCount, doc.columns.length);\n const headerCols = clampCount(doc.table?.headerColumnCount, doc.columns.length);\n\n // Columns\n const columns: SpreadsheetViewColumn[] = [];\n let left = 0;\n let hiddenColumnCount = 0;\n doc.columns.forEach((column, index) => {\n if (column.hidden) {\n hiddenColumnCount++;\n return;\n }\n const frozen = index < frozenCols;\n columns.push({ column, index, header: index < headerCols, frozen, stickyLeft: frozen ? left : 0 });\n if (frozen) left += column.width || DEFAULT_COLUMN_WIDTH;\n });\n\n // Rows\n const rows: SpreadsheetViewRow[] = [];\n let hiddenRowCount = 0;\n let filteredOutCount = 0;\n const pushRow = (index: number, section: SpreadsheetRowSection, groupKey?: string) => {\n const row = doc.rows[index];\n rows.push({ kind: 'row', row, index, section, frozen: false, stickyTop: 0, groupKey });\n };\n\n for (let r = 0; r < headerRows; r++) {\n if (doc.rows[r].hidden) hiddenRowCount++;\n else pushRow(r, 'header');\n }\n\n const bodyEnd = doc.rows.length - footerRows;\n const groups = computeRowGroups(doc, getValue);\n for (let r = headerRows; r < bodyEnd; r++) {\n if (doc.rows[r].hidden) hiddenRowCount++;\n else if (!rowPassesFilters(doc, r, getValue)) filteredOutCount++;\n }\n const grouping = doc.table?.grouping;\n if (grouping && doc.columns.some((c) => c.id === grouping.colId)) {\n for (const group of groups) {\n rows.push({ kind: 'group', group });\n if (!group.collapsed) group.rowIndices.forEach((r) => pushRow(r, 'body', group.key));\n }\n } else {\n for (let r = headerRows; r < bodyEnd; r++) {\n if (!doc.rows[r].hidden && rowPassesFilters(doc, r, getValue)) pushRow(r, 'body');\n }\n }\n\n for (let r = bodyEnd; r < doc.rows.length; r++) {\n if (doc.rows[r].hidden) hiddenRowCount++;\n else pushRow(r, 'footer');\n }\n\n // Frozen rows: the leading run of displayed rows inside the frozen count.\n let top = 0;\n for (const viewRow of rows) {\n if (viewRow.kind !== 'row' || viewRow.index >= frozenRows) break;\n viewRow.frozen = true;\n viewRow.stickyTop = top;\n top += viewRow.row.height || DEFAULT_ROW_HEIGHT;\n }\n\n const rowOrder = rows.flatMap((r) => (r.kind === 'row' ? [r.index] : []));\n const columnOrder = columns.map((c) => c.index);\n\n // Merged cells: span the visible, display-contiguous part of each merge.\n const rowPos = new Map(rowOrder.map((index, pos) => [index, pos]));\n const colPos = new Map(columnOrder.map((index, pos) => [index, pos]));\n const spans = new Map<string, SpreadsheetCellSpan | null>();\n const contiguousRun = (indices: number[], positions: Map<number, number>) => {\n const visible = indices\n .filter((i) => positions.has(i))\n .sort((a, b) => positions.get(a)! - positions.get(b)!);\n if (visible.length === 0) return null;\n let run = 1;\n while (run < visible.length && positions.get(visible[run])! === positions.get(visible[0])! + run) run++;\n return { first: visible[0], run };\n };\n for (const raw of doc.table?.merges ?? []) {\n const merge = normalizeRange(raw);\n const rowIdx: number[] = [];\n const colIdx: number[] = [];\n for (let r = merge.startRow; r <= merge.endRow; r++) rowIdx.push(r);\n for (let c = merge.startCol; c <= merge.endCol; c++) colIdx.push(c);\n const rowRun = contiguousRun(rowIdx, rowPos);\n const colRun = contiguousRun(colIdx, colPos);\n for (const r of rowIdx) for (const c of colIdx) spans.set(`${r}:${c}`, null);\n if (rowRun && colRun) {\n spans.set(`${rowRun.first}:${colRun.first}`, {\n rowSpan: rowRun.run,\n colSpan: colRun.run,\n source: { row: merge.startRow, col: merge.startCol },\n merge,\n });\n }\n }\n\n return {\n rows,\n columns,\n groups,\n rowOrder,\n columnOrder,\n filteredOutCount,\n hiddenRowCount,\n hiddenColumnCount,\n getCellSpan(row, col) {\n const span = spans.get(`${row}:${col}`);\n if (span === undefined) return { rowSpan: 1, colSpan: 1, source: { row, col } };\n return span;\n },\n };\n}\n","// Portions ported from gridcraft/crates/engine/src/cmd/sheet.rs and\n// gridcraft/crates/engine/src/cmd/formulas.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport {\n SpreadsheetDocument,\n SpreadsheetColumn,\n SpreadsheetRow,\n CellCoordinate,\n CellRange,\n CellData,\n SpreadsheetDataSource,\n SpreadsheetTableSettings,\n SortKey,\n FilterRule,\n SpreadsheetFilterState,\n GroupAggregate,\n GroupSummary,\n SpreadsheetSpill,\n SpreadsheetWorkbook,\n SpreadsheetDefinedName,\n SpreadsheetSheetInfo\n} from './types';\nimport {\n FormulaEvaluator,\n colNameToIndex,\n indexToColName,\n DependencyGraph,\n shiftFormulaReferences,\n remapFormulaReferences,\n offsetFormulaReferences,\n WHOLE_COLUMN_END_ROW,\n describeFormulaError,\n isFormulaError,\n parseFormula,\n extractCellDependencies,\n extractNameDependencies,\n columnDependencyKey,\n getFormulaFunction,\n renameSheetInFormula,\n deleteSheetInFormula,\n renameNameInFormula,\n type ASTNode,\n type FormulaRange,\n type FormulaNameDefinition,\n type FormulaEvaluationResult,\n type StructuralChange,\n type CellPosition,\n type FormulaReferenceFilter\n} from './formula';\nimport {\n copySheetName,\n isLambdaDefinition,\n nextSheetName,\n validateDefinedName,\n validateSheetName\n} from './workbook';\nimport { parseCellInput } from './formula/parse-input';\nimport { formatCellDisplay } from './formula/format';\nimport { cellFormatForNumberFormat, isGeneralNumberFormat } from './numfmt/presets';\nimport { DataSourceBinder } from './datasource';\nimport {\n compareCellValues,\n fillSeries,\n getFillDirection,\n isSingleCellRange,\n normalizeRange,\n rangeContains,\n rangeContainsRange,\n rangesIntersect\n} from './table';\nimport { parseCsv, serializeCsv } from './csv';\nimport {\n createSpreadsheetClipboardData,\n readSpreadsheetClipboard,\n rebaseClipboardRows,\n type SpreadsheetClipboardData,\n type SpreadsheetClipboardSource\n} from './clipboard';\nimport {\n ConditionalFormatEngine,\n EMPTY_CONDITIONAL_FORMAT_OVERLAY,\n type ConditionalCellFormat,\n type ConditionalFormatOverlay,\n type ConditionalFormatRule,\n type ConditionalFormatSheet,\n type ConditionalFormatValue,\n type NewConditionalFormatRule\n} from './conditional-format';\nimport {\n findDataValidation,\n findInvalidCells,\n getValidationListValues,\n validateCellInput,\n type DataValidationResult,\n type DataValidationRule,\n type DataValidationSheet,\n type NewDataValidationRule\n} from './validation';\nimport { adjustRangesForStructuralChange, shiftRuleFormula, subtractRangeFromRanges } from './rule-ranges';\nimport {\n buildSpreadsheetView,\n computeRowGroups,\n getTableSections,\n type SpreadsheetRowGroup,\n type SpreadsheetViewModel\n} from './view';\n\nexport interface CsvExportOptions {\n range?: CellRange | null;\n delimiter?: string;\n lineEnding?: string;\n bom?: boolean;\n /** Export formulas as typed (`=SUM(A1:A3)`) instead of their values. */\n formulas?: boolean;\n}\n\nexport interface CsvImportOptions {\n startRow?: number;\n startCol?: number;\n delimiter?: string;\n /** Sheet (id or name) to import into; default the active sheet. */\n sheet?: string;\n}\n\nexport type NewFilterRule = Omit<FilterRule, 'id'> & { id?: string };\n\n/**\n * Invalid input waiting for the user: a `stop` alert (retry or cancel) or a\n * `warning` / `information` prompt (accept, retry or cancel). See\n * `SpreadsheetController.resolveValidationPrompt`.\n */\nexport interface SpreadsheetValidationPrompt {\n rowId: string;\n colId: string;\n row: number;\n col: number;\n /** The input that failed validation. */\n raw: string;\n /** The failed check: rule, alert style, title and message. */\n result: DataValidationResult;\n /** True when the input came from the cell editor (which stays open meanwhile). */\n editing: boolean;\n}\n\n/**\n * Outcome of committing input: `committed`; `rejected` (a `stop` rule) or\n * `confirm` (a `warning` / `information` rule), with `prompt` now waiting\n * in `validationPrompt`; `pending` when an earlier prompt is still open;\n * `none` when nothing was being edited.\n */\nexport type SpreadsheetCommitResult =\n | { status: 'committed' | 'none' }\n | { status: 'rejected' | 'confirm' | 'pending'; prompt: SpreadsheetValidationPrompt };\n\n/** How the user answered a validation prompt. */\nexport type SpreadsheetValidationAction = 'accept' | 'retry' | 'cancel';\n\nexport interface SpreadsheetControllerOptions {\n document?: Partial<SpreadsheetDocument>;\n dataSources?: SpreadsheetDataSource[];\n primaryDataSourceId?: string;\n onDocumentChange?: (doc: SpreadsheetDocument) => void;\n onSelectionChange?: (activeCell: CellCoordinate | null, range: CellRange | null) => void;\n onCellCommit?: (rowId: string, colId: string, raw: string, value: any) => void;\n /**\n * Start from a workbook with several sheets and defined names instead of\n * `document`. `document` then names nothing; the active sheet is\n * `workbook.activeSheetId` (default the first sheet).\n */\n workbook?: SpreadsheetWorkbook;\n /**\n * Called after any change, like `onDocumentChange`, with the whole\n * workbook. The object holds the live sheet documents: copy it (or use\n * `getWorkbook()`) before keeping it.\n */\n onWorkbookChange?: (workbook: SpreadsheetWorkbook) => void;\n /** Called when another sheet becomes active. */\n onActiveSheetChange?: (sheetId: string) => void;\n /**\n * Approximate memory budget for undo/redo snapshots, in bytes (UTF-16 size of\n * the serialized documents). The oldest undo steps beyond it are dropped, but\n * the most recent one is always kept. Defaults to 64 MiB; 0 = unlimited.\n */\n maxHistoryBytes?: number;\n}\n\n/** Module-wide sequence that makes generated row, column and filter ids unique. */\nlet nextIdSequence = 0;\n\n/** A placed dynamic array: derived state, rebuilt by every recalculation. */\ninterface SpillRecord {\n anchor: string;\n row: number;\n col: number;\n rows: number;\n cols: number;\n /** Column-major values, anchor (top-left) first. */\n values: any[];\n /** The spilled cells (anchor excluded). */\n cells: Array<{ key: string; rowId: string; colId: string }>;\n}\n\n/**\n * One sheet of the workbook: its document plus derived state (spilled\n * arrays) and, while it is not active, its saved selection.\n */\ninterface SheetRuntime {\n doc: SpreadsheetDocument;\n spills: Map<string, SpillRecord>; // anchor key -> spill\n spilledBy: Map<string, string>; // spilled cell key -> anchor key\n spillCells: Map<string, CellData>; // spillCellId -> displayed cell\n activeCell: CellCoordinate | null;\n selectedRange: CellRange | null;\n}\n\nconst createRuntime = (doc: SpreadsheetDocument): SheetRuntime => ({\n doc,\n spills: new Map(),\n spilledBy: new Map(),\n spillCells: new Map(),\n activeCell: null,\n selectedRange: null\n});\n\n/**\n * An undo/redo step: a snapshot of one sheet (cell edits, table operations)\n * or of the whole workbook (sheet and name operations).\n */\ntype HistoryEntry = { kind: 'sheet'; sheetId: string; data: string } | { kind: 'workbook'; data: string };\n\ninterface WorkbookSnapshot {\n sheets: SpreadsheetDocument[];\n activeSheetId: string;\n names: SpreadsheetDefinedName[];\n}\n\nconst sameName = (a: string, b: string) => a.toUpperCase() === b.toUpperCase();\n\n/** A defined name as listed by `getNames()`. */\nexport interface SpreadsheetNameInfo extends SpreadsheetDefinedName {\n /** Name of the sheet a sheet-scoped name belongs to; unset for workbook scope. */\n sheetName?: string;\n /** Whether the name holds a LAMBDA (it is called like a function). */\n isLambda: boolean;\n}\n\n/** Where a defined name lives: `sheet` (id or name) for a sheet-scoped name. */\nexport interface SpreadsheetNameScope {\n sheet?: string;\n}\n\n// Default undo/redo snapshot budget (see SpreadsheetControllerOptions.maxHistoryBytes).\nconst DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;\n\n// Recalculation passes after spill ranges change (see recalculateKeys).\nconst MAX_SPILL_PASSES = 25;\n\nconst spillCellId = (rowId: string, colId: string) => `${rowId}\\u0000${colId}`;\n\nexport class SpreadsheetController {\n public activeCell: CellCoordinate | null = null;\n public selectedRange: CellRange | null = null;\n public editingCell: CellCoordinate | null = null;\n public draftValue = '';\n\n public readonly binder = new DataSourceBinder();\n public readonly depGraph = new DependencyGraph();\n public readonly evaluator: FormulaEvaluator;\n\n private primaryDataSourceId?: string;\n private onDocumentChange?: (doc: SpreadsheetDocument) => void;\n private onWorkbookChange?: (workbook: SpreadsheetWorkbook) => void;\n private onActiveSheetChange?: (sheetId: string) => void;\n\n // Workbook: sheets in tab order, the active one, and the one being worked\n // on (the active sheet, or another sheet while one of its cells is read or\n // evaluated). Everything that uses `this.document` works on `ctx`.\n private runtimes: SheetRuntime[] = [];\n private active!: SheetRuntime;\n private ctx!: SheetRuntime;\n // The sheet bound to the primary data source (the first sheet).\n private boundSheetId: string | null = null;\n private names: SpreadsheetDefinedName[] = [];\n private onSelectionChange?: (activeCell: CellCoordinate | null, range: CellRange | null) => void;\n private onCellCommit?: (rowId: string, colId: string, raw: string, value: any) => void;\n\n // History stack for undo/redo\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private isApplyingHistory = false;\n private readonly maxHistoryBytes: number;\n // Set while importing TSV: history and recalculation are done once at the end.\n private batching = false;\n private pendingCommits: Array<[string, string, string]> = [];\n // Row being evaluated, so [property] references resolve per row.\n private evaluatingRowIndex: number | null = null;\n // Cell being evaluated, for implicit intersection (`=@A:A`).\n private evaluatingCell: { col: string; row: number } | null = null;\n\n // Dynamic arrays. Spilled values are derived state: they live in the sheet\n // runtimes, not in the documents, and every recalculation rebuilds them.\n // Cells (sheet-qualified keys) whose spill ownership changed during the current pass.\n private spillChanges: Set<string> | null = null;\n\n /** The active sheet's document (the sheet the editor shows). */\n public get document(): SpreadsheetDocument {\n return this.ctx.doc;\n }\n\n public set document(doc: SpreadsheetDocument) {\n this.ctx.doc = doc;\n }\n\n private get spills(): Map<string, SpillRecord> {\n return this.ctx.spills;\n }\n\n private get spilledBy(): Map<string, string> {\n return this.ctx.spilledBy;\n }\n\n private get spillCells(): Map<string, CellData> {\n return this.ctx.spillCells;\n }\n\n // Conditional formatting is evaluated lazily (getConditionalFormatOverlay).\n // Cell keys whose values changed since, or 'all' to evaluate from scratch.\n private cfEngine: ConditionalFormatEngine | null = null;\n private cfDirty: Set<string> | 'all' = 'all';\n // Set while a single cell edit notifies, so the change stays incremental.\n private cfIncremental = false;\n // Value being entered, seen by validation formulas instead of the cell's value.\n private valueOverride: { row: number; col: number; value: unknown } | null = null;\n // \"Circle invalid data\": on/off and the cached cells.\n private circlesShown = false;\n private invalidCircles: Set<string> | null = null;\n /** Invalid input waiting for the user's answer (see `resolveValidationPrompt`). */\n public validationPrompt: SpreadsheetValidationPrompt | null = null;\n\n constructor(options: SpreadsheetControllerOptions = {}) {\n const defaultCols: SpreadsheetColumn[] = [\n { id: 'col_a', key: 'A', title: 'A', width: 120, type: 'freeform' },\n { id: 'col_b', key: 'B', title: 'B', width: 120, type: 'freeform' },\n { id: 'col_c', key: 'C', title: 'C', width: 120, type: 'freeform' },\n { id: 'col_d', key: 'D', title: 'D', width: 120, type: 'freeform' }\n ];\n\n const defaultRows: SpreadsheetRow[] = options.primaryDataSourceId ? [] : Array.from({ length: 10 }, (_, i) => ({\n id: `row_${i + 1}`,\n index: i,\n height: 32,\n type: 'freeform'\n }));\n\n const first: SpreadsheetDocument = {\n id: options.document?.id || `sheet_${Date.now()}`,\n name: options.document?.name || 'Sheet 1',\n columns: options.document?.columns || defaultCols,\n rows: options.document?.rows || defaultRows,\n cells: options.document?.cells || {},\n createdAt: options.document?.createdAt || Date.now(),\n updatedAt: options.document?.updatedAt || Date.now(),\n ...(options.document?.table ? { table: options.document.table } : {})\n };\n if (options.workbook && options.workbook.sheets.length > 0) {\n this.setWorkbookState(options.workbook, false);\n } else {\n this.runtimes = [createRuntime(first)];\n this.active = this.ctx = this.runtimes[0];\n }\n this.boundSheetId = this.runtimes[0].doc.id;\n\n this.primaryDataSourceId = options.primaryDataSourceId;\n this.maxHistoryBytes = Math.max(0, options.maxHistoryBytes ?? DEFAULT_MAX_HISTORY_BYTES);\n this.onDocumentChange = options.onDocumentChange;\n this.onSelectionChange = options.onSelectionChange;\n this.onCellCommit = options.onCellCommit;\n this.onWorkbookChange = options.onWorkbookChange;\n this.onActiveSheetChange = options.onActiveSheetChange;\n\n if (options.dataSources) {\n options.dataSources.forEach((ds) => this.binder.register(ds));\n }\n\n this.evaluator = new FormulaEvaluator({\n resolveCell: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveCellByCoord(colStr, rowNum)),\n resolveRange: (sCol, sRow, eCol, eRow, sheet) =>\n this.onSheet(sheet, () => this.resolveRangeByCoords(sCol, sRow, eCol, eRow)),\n resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),\n resolveSpill: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveSpillByCoord(colStr, rowNum)),\n resolveSheetSpan: (first, last) => this.resolveSheetSpan(first, last),\n resolveName: (name, sheet) => this.resolveDefinedName(name, sheet),\n getFormulaCell: () => this.evaluatingCell\n });\n\n this.syncWithDataSource();\n for (const rt of this.runtimes) this.withSheet(rt, () => this.rederiveTypedValues());\n this.recalculateAll();\n }\n\n // --- Workbook: sheet runtimes ---\n\n private get boundRuntime(): SheetRuntime | null {\n return this.boundSheetId === null ? null : this.findRuntimeById(this.boundSheetId) ?? null;\n }\n\n /** Runs `fn` with `rt` as the sheet `this.document` (and spills) refer to. */\n private withSheet<T>(rt: SheetRuntime, fn: () => T): T {\n if (rt === this.ctx) return fn();\n const saved = this.ctx;\n this.ctx = rt;\n try {\n return fn();\n } finally {\n this.ctx = saved;\n }\n }\n\n /**\n * Runs `fn` on the sheet named `sheet` (a sheet-qualified reference), or\n * on the current sheet when undefined. `#REF!` when no sheet has that name.\n */\n private onSheet<T>(sheet: string | undefined, fn: () => T): T | string {\n if (sheet === undefined) return fn();\n const rt = this.findRuntimeByName(sheet);\n return rt ? this.withSheet(rt, fn) : '#REF!';\n }\n\n private findRuntimeByName(name: string): SheetRuntime | undefined {\n return this.runtimes.find((rt) => sameName(rt.doc.name, name));\n }\n\n private findRuntimeById(id: string): SheetRuntime | undefined {\n return this.runtimes.find((rt) => rt.doc.id === id) ?? this.runtimes.find((rt) => sameName(rt.doc.id, id));\n }\n\n /** A sheet by id or name. */\n private findRuntime(idOrName: string): SheetRuntime | undefined {\n return this.runtimes.find((rt) => rt.doc.id === idOrName) ?? this.findRuntimeByName(idOrName);\n }\n\n /** Dependency-graph key of a cell (`A1`) on a sheet: sheet-qualified, so keys are unique in the workbook. */\n private qualify(key: string, rt: SheetRuntime = this.ctx): string {\n return `${rt.doc.id}!${key}`.toUpperCase();\n }\n\n /** The sheet and A1 key of a qualified key. */\n private unqualify(qualified: string): { rt: SheetRuntime; key: string } | null {\n const bang = qualified.lastIndexOf('!');\n if (bang < 0) return null;\n const id = qualified.slice(0, bang);\n const rt = this.runtimes.find((r) => r.doc.id.toUpperCase() === id);\n return rt ? { rt, key: qualified.slice(bang + 1) } : null;\n }\n\n private resolveSheetSpan(first: string, last: string): string[] | string {\n const a = this.runtimes.findIndex((rt) => sameName(rt.doc.name, first));\n const b = this.runtimes.findIndex((rt) => sameName(rt.doc.name, last));\n if (a < 0 || b < 0) return '#REF!';\n return this.runtimes.slice(Math.min(a, b), Math.max(a, b) + 1).map((rt) => rt.doc.name);\n }\n\n /** The defined name `name` visible from `rt`: its sheet-scoped name first, then the workbook one. */\n private findName(name: string, rt: SheetRuntime | null): SpreadsheetDefinedName | undefined {\n return (\n (rt ? this.names.find((n) => n.sheetId === rt.doc.id && sameName(n.name, name)) : undefined) ??\n this.names.find((n) => n.sheetId === undefined && sameName(n.name, name))\n );\n }\n\n private resolveDefinedName(name: string, sheet?: string): FormulaNameDefinition | null {\n const rt = sheet === undefined ? this.ctx : this.findRuntimeByName(sheet);\n if (!rt) return null;\n const def = this.findName(name, rt);\n if (!def) return null;\n const scope = def.sheetId !== undefined ? this.findRuntimeById(def.sheetId) : undefined;\n return {\n formula: def.refersTo,\n ...(scope ? { sheet: scope.doc.name } : {}),\n key: `${def.sheetId ?? ''}!${def.name}`\n };\n }\n\n /**\n * Replaces the active sheet with `document` (its name becomes the sheet's\n * name, made unique). Clears undo history.\n */\n public loadDocument(document: SpreadsheetDocument): void {\n const loaded: SpreadsheetDocument = JSON.parse(JSON.stringify(document));\n const others = this.runtimes.filter((rt) => rt !== this.active);\n if (others.some((rt) => rt.doc.id === loaded.id)) loaded.id = this.generateSheetId();\n const otherNames = others.map((rt) => rt.doc.name);\n if (!loaded.name || otherNames.some((n) => sameName(n, loaded.name))) {\n loaded.name = loaded.name ? copySheetName(loaded.name, otherNames) : nextSheetName(otherNames);\n }\n const previousId = this.active.doc.id;\n this.ctx = this.active;\n this.document = loaded;\n if (previousId !== loaded.id) {\n for (const n of this.names) if (n.sheetId === previousId) n.sheetId = loaded.id;\n if (this.boundSheetId === previousId) this.boundSheetId = loaded.id;\n }\n this.activeCell = null;\n this.selectedRange = null;\n this.editingCell = null;\n this.draftValue = '';\n this.undoStack = [];\n this.redoStack = [];\n this.syncWithDataSource();\n this.rederiveTypedValues();\n this.recalculateAll();\n this.notifyChange();\n }\n\n // --- Workbook: loading and saving ---\n\n /** A deep copy of the workbook: every sheet, the active sheet and the defined names. */\n public getWorkbook(): SpreadsheetWorkbook {\n return JSON.parse(JSON.stringify(this.workbookSnapshot()));\n }\n\n /** Replaces the whole workbook (sheets, names, active sheet). Clears undo history. */\n public loadWorkbook(workbook: SpreadsheetWorkbook): void {\n if (!workbook.sheets.length) return;\n this.setWorkbookState(JSON.parse(JSON.stringify(workbook)), true);\n this.boundSheetId = this.runtimes[0].doc.id;\n this.editingCell = null;\n this.draftValue = '';\n this.undoStack = [];\n this.redoStack = [];\n this.syncWithDataSource();\n for (const rt of this.runtimes) this.withSheet(rt, () => this.rederiveTypedValues());\n this.recalculateAll();\n this.notifyChange();\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n }\n\n private workbookSnapshot(): WorkbookSnapshot {\n return {\n sheets: this.runtimes.map((rt) => rt.doc),\n activeSheetId: this.active.doc.id,\n names: this.names\n };\n }\n\n /**\n * Installs a workbook's sheets and names (taking ownership of the objects).\n * Duplicate sheet ids and names are made unique.\n */\n private setWorkbookState(workbook: SpreadsheetWorkbook, resetSelection: boolean): void {\n const ids = new Set<string>();\n const names: string[] = [];\n const runtimes = workbook.sheets.map((doc) => {\n if (!doc.id || ids.has(doc.id)) doc.id = this.generateSheetId();\n ids.add(doc.id);\n if (!doc.name || names.some((n) => sameName(n, doc.name))) {\n doc.name = doc.name ? copySheetName(doc.name, names) : nextSheetName(names);\n }\n names.push(doc.name);\n doc.columns ??= [];\n doc.rows ??= [];\n doc.cells ??= {};\n return createRuntime(doc);\n });\n this.runtimes = runtimes;\n this.active = this.ctx =\n runtimes.find((rt) => rt.doc.id === workbook.activeSheetId) ?? runtimes[0];\n this.names = (workbook.names ?? [])\n .filter((n) => n.sheetId === undefined || ids.has(n.sheetId))\n .map((n) => ({ ...n, refersTo: n.refersTo.startsWith('=') ? n.refersTo : `=${n.refersTo}` }));\n if (resetSelection) {\n this.activeCell = null;\n this.selectedRange = null;\n }\n }\n\n /**\n * Re-parses the value of every stored typed (non-formula) cell from its\n * `raw` input, so documents saved before typed input was parsed (whose\n * values are text such as `\"120\"`) compare and sort as numbers. Cells of\n * bound columns in record rows keep their value: it belongs to the data\n * source.\n */\n private rederiveTypedValues(): void {\n for (const row of this.document.rows) {\n const cells = this.document.cells[row.id];\n if (!cells) continue;\n for (const col of this.document.columns) {\n const cell = cells[col.id];\n if (!cell || typeof cell.raw !== 'string' || cell.raw.startsWith('=')) continue;\n if (col.type === 'bound' && col.binding && row.recordId) continue;\n const typed = this.typedCell(cell.raw, cell.format, col, cell);\n cell.value = typed.value;\n if (typed.numberFormat) cell.numberFormat = typed.numberFormat;\n else delete cell.numberFormat;\n if (typed.format) cell.format = typed.format;\n }\n }\n }\n\n /**\n * Value of typed (non-formula) input: a number, boolean, ISO date, error\n * or text (see `parseCellInput`), its automatic number format, and the\n * `CellFormat` to use: the automatic one (currency, percentage, date,\n * number) when `format` is unset or `text` (the default of new columns).\n */\n private typedCell(\n raw: string,\n format: CellData['format'],\n col?: SpreadsheetColumn,\n existing?: CellData\n ): Pick<CellData, 'value' | 'numberFormat' | 'explicitNumberFormat' | 'format'> {\n const parsed = parseCellInput(raw);\n const fixed = this.fixedNumberFormat(col, existing);\n const numberFormat = fixed ?? parsed.numberFormat;\n return {\n value: parsed.value,\n ...(numberFormat ? { numberFormat } : {}),\n ...(existing?.explicitNumberFormat ? { explicitNumberFormat: true } : {}),\n format: fixed === undefined && parsed.cellFormat && (!format || format === 'text') ? parsed.cellFormat : format\n };\n }\n\n /**\n * The number format a cell keeps whatever is typed into it: its explicit\n * one, else its column's. Undefined lets typed input choose one.\n */\n private fixedNumberFormat(col: SpreadsheetColumn | undefined, existing: CellData | undefined): string | undefined {\n if (existing?.explicitNumberFormat && existing.numberFormat !== undefined) return existing.numberFormat;\n return col?.numberFormat && !isGeneralNumberFormat(col.numberFormat) ? col.numberFormat : undefined;\n }\n\n /** `numberFormat` / `explicitNumberFormat` fields for a formula, bound or spilled cell. */\n private fixedNumberFormatFields(col: SpreadsheetColumn | undefined, existing: CellData | undefined): Partial<CellData> {\n const numberFormat = this.fixedNumberFormat(col, existing);\n if (numberFormat === undefined) return {};\n return existing?.explicitNumberFormat ? { numberFormat, explicitNumberFormat: true } : { numberFormat };\n }\n\n /**\n * Typed input for a bound field, coerced by the field's declared type:\n * number, boolean and date fields get a number, boolean or ISO date when\n * the input parses as one; string and enum fields (and unknown fields)\n * keep the text exactly as typed.\n */\n private boundInputValue(dataSourceId: string, field: string, raw: string): unknown {\n if (raw.startsWith('=')) return raw;\n const type = this.binder.getDataSource(dataSourceId)?.getFields().find((f) => f.key === field)?.type;\n if (type !== 'number' && type !== 'boolean' && type !== 'date') return raw;\n const parsed = parseCellInput(raw);\n if (type === 'number' && parsed.type === 'number') return parsed.value;\n if (type === 'boolean' && parsed.type === 'boolean') return parsed.value;\n if (type === 'date' && parsed.type === 'date') return parsed.value;\n return raw;\n }\n\n public registerDataSource(ds: SpreadsheetDataSource): void {\n this.binder.register(ds);\n this.syncWithDataSource();\n this.recalculateAll();\n }\n\n public setPrimaryDataSource(dataSourceId: string | undefined): void {\n this.primaryDataSourceId = dataSourceId;\n this.syncWithDataSource();\n this.recalculateAll();\n }\n\n public syncWithDataSource(): void {\n if (!this.primaryDataSourceId || !this.boundRuntime) return;\n const id = this.primaryDataSourceId;\n // Records are bound to the first sheet (the one the controller was created with).\n this.withSheet(this.boundRuntime, () => {\n this.document.rows = this.binder.syncRows(this.document.rows, id);\n this.recomputeRowIndices();\n });\n }\n\n private recomputeRowIndices(): void {\n this.document.rows.forEach((r, idx) => {\n r.index = idx;\n });\n this.document.columns.forEach((c, idx) => {\n c.key = indexToColName(idx);\n });\n }\n\n public getColumn(colIdOrKey: string): SpreadsheetColumn | undefined {\n return this.document.columns.find(\n (c) => c.id === colIdOrKey || c.key.toUpperCase() === colIdOrKey.toUpperCase()\n );\n }\n\n public getRow(rowIdOrIndex: string | number): SpreadsheetRow | undefined {\n if (typeof rowIdOrIndex === 'number') {\n return this.document.rows[rowIdOrIndex];\n }\n return this.document.rows.find((r) => r.id === rowIdOrIndex);\n }\n\n public getCell(rowId: string, colId: string): CellData {\n const existing = this.document.cells[rowId]?.[colId];\n if (existing && existing.raw !== '') return existing;\n if (this.spillCells.size > 0) {\n const spilled = this.spillCells.get(spillCellId(rowId, colId));\n if (spilled) return spilled;\n }\n if (existing && !existing.explicitNumberFormat) return existing;\n\n const col = this.document.columns.find((c) => c.id === colId);\n const row = this.document.rows.find((r) => r.id === rowId);\n const derived = this.getDerivedCell(row, col);\n if (!existing) return derived;\n // A blank cell holding only an explicit number format, over a bound\n // record value or a column formula: show that value with the format.\n if (derived.raw === '') return existing;\n return { ...derived, numberFormat: existing.numberFormat, explicitNumberFormat: true };\n }\n\n /** Value of a cell with no stored data: bound record value, column formula, or blank. */\n private getDerivedCell(row: SpreadsheetRow | undefined, col: SpreadsheetColumn | undefined): CellData {\n // Check if column is bound to record\n if (col && row && col.type === 'bound' && col.binding && row.recordId) {\n const boundVal = this.binder.resolveBoundValue(\n col.binding.dataSource,\n row.recordId,\n col.binding.field\n );\n return {\n raw: boundVal !== undefined ? String(boundVal) : '',\n value: boundVal ?? '',\n format: col.format,\n ...this.fixedNumberFormatFields(col, undefined)\n };\n }\n\n // Check if column has a column-level formula\n if (col && col.type === 'formula' && col.formula) {\n return {\n raw: col.formula,\n value: '',\n format: col.format,\n ...this.fixedNumberFormatFields(col, undefined)\n };\n }\n\n return { raw: '', value: '', format: col?.format, ...this.fixedNumberFormatFields(col, undefined) };\n }\n\n public setCellValue(rowId: string, colId: string, raw: string): void {\n this.recordSnapshot();\n\n if (!this.document.cells[rowId]) {\n this.document.cells[rowId] = {};\n }\n\n const col = this.document.columns.find((c) => c.id === colId);\n const row = this.document.rows.find((r) => r.id === rowId);\n\n const isFormula = raw.startsWith('=');\n const existing = this.document.cells[rowId][colId];\n let cell: CellData;\n // If this is a bound column and row is bound to record, dispatch back to data source\n if (col?.type === 'bound' && col.binding && row?.recordId) {\n const value = this.boundInputValue(col.binding.dataSource, col.binding.field, raw);\n this.binder.commitBoundValue(col.binding.dataSource, row.recordId, col.binding.field, value);\n cell = { raw, value, format: col.format, ...this.fixedNumberFormatFields(col, existing) };\n } else if (isFormula) {\n cell = { raw, value: raw, format: col?.format, ...this.fixedNumberFormatFields(col, existing) };\n } else {\n cell = { raw, ...this.typedCell(raw, col?.format, col, existing) };\n }\n\n const cellKey = this.getCellKey(rowId, colId);\n this.registerFormula(cellKey, isFormula ? raw : null);\n\n this.document.cells[rowId][colId] = cell;\n\n this.document.updatedAt = Date.now();\n if (this.batching) {\n this.pendingCommits.push([rowId, colId, raw]);\n return;\n }\n if (this.cfDirty !== 'all') this.cfDirty.add(cellKey);\n this.recalculateCell(rowId, colId);\n this.cfIncremental = true;\n try {\n this.notifyChange();\n } finally {\n this.cfIncremental = false;\n }\n\n const evaluatedVal = this.document.cells[rowId][colId]?.value;\n this.onCellCommit?.(rowId, colId, raw, evaluatedVal);\n }\n\n private getCellKey(rowId: string, colId: string): string {\n const row = this.document.rows.find((r) => r.id === rowId);\n const col = this.document.columns.find((c) => c.id === colId);\n if (!row || !col) return `${colId}_${rowId}`;\n return `${col.key}${row.index + 1}`.toUpperCase();\n }\n\n /**\n * Recalculates a cell and everything that depends on it, in dependency\n * order. Every cell in a reference cycle is marked `#CYCLE!`.\n */\n public recalculateCell(rowId: string, colId: string): void {\n const cellKey = this.getCellKey(rowId, colId);\n // Typing into a spilled cell blocks its array: the anchor recalculates (#SPILL!).\n const anchor = this.spilledBy.get(cellKey);\n this.recalculateKeys(anchor ? [this.qualify(cellKey), this.qualify(anchor)] : [this.qualify(cellKey)], true);\n }\n\n /**\n * Registers the dependencies of the formula in `cellKey` (A1 key on the\n * current sheet), or removes them when `formula` is null: its references\n * (sheet-qualified ones on their sheet, 3-D ones on every sheet of the\n * span) and, through defined names, the cells those names refer to.\n */\n private registerFormula(cellKey: string, formula: string | null): void {\n const key = this.qualify(cellKey);\n if (!formula || !formula.startsWith('=')) {\n this.depGraph.setCellDependencies(key, []);\n return;\n }\n let ast: ASTNode;\n try {\n ast = parseFormula(formula);\n } catch {\n this.depGraph.setCellDependencies(key, []);\n return;\n }\n const deps = new Set<string>();\n this.collectDependencies(ast, this.ctx, deps, new Set());\n this.depGraph.setCellDependencies(key, deps);\n }\n\n /** Adds the qualified keys `ast` depends on, with unqualified references on `home`. */\n private collectDependencies(ast: ASTNode, home: SheetRuntime, out: Set<string>, seenNames: Set<string>): void {\n for (const dep of extractCellDependencies(ast)) {\n let sheets: SheetRuntime[];\n if (dep.sheet === undefined) {\n sheets = [home];\n } else if (dep.sheetEnd === undefined) {\n const rt = this.findRuntimeByName(dep.sheet);\n sheets = rt ? [rt] : [];\n } else {\n const span = this.resolveSheetSpan(dep.sheet, dep.sheetEnd);\n sheets = Array.isArray(span) ? span.map((name) => this.findRuntimeByName(name)!) : [];\n }\n for (const rt of sheets) {\n out.add(dep.row === 0 ? columnDependencyKey(`${rt.doc.id}!${dep.col}`) : this.qualify(`${dep.col}${dep.row}`, rt));\n }\n }\n for (const used of extractNameDependencies(ast)) {\n // A call of a built-in function is not a name.\n if (used.call && getFormulaFunction(used.name)) continue;\n const lookupSheet = used.sheet === undefined ? home : this.findRuntimeByName(used.sheet);\n if (!lookupSheet) continue;\n const def = this.findName(used.name, lookupSheet);\n if (!def) continue;\n const scope = def.sheetId !== undefined ? this.findRuntimeById(def.sheetId) : undefined;\n // Unqualified references in a workbook-scoped name refer to the using sheet.\n const nameHome = scope ?? home;\n const seenKey = `${def.sheetId ?? ''}!${def.name.toUpperCase()}@${nameHome.doc.id}`;\n if (seenNames.has(seenKey)) continue;\n seenNames.add(seenKey);\n try {\n this.collectDependencies(parseFormula(def.refersTo), nameHome, out, seenNames);\n } catch {\n // A name whose formula can't be parsed has no dependencies.\n }\n }\n }\n\n /**\n * Recalculates `keys` (and, with `includeDependents`, their dependents) in\n * dependency order. Spill ranges are only known after evaluating their\n * anchors, so when a pass changes which cells hold spilled values (and so\n * the dependency graph), the dependents of those cells are recalculated in\n * another pass, until nothing changes.\n */\n private recalculateKeys(keys: Iterable<string>, includeDependents: boolean): void {\n let pending = Array.from(keys);\n let withDependents = includeDependents;\n for (let pass = 0; pass < MAX_SPILL_PASSES && pending.length > 0; pass++) {\n const changed = new Set<string>();\n this.spillChanges = changed;\n try {\n this.runEvaluationPlan(this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents }));\n } finally {\n this.spillChanges = null;\n }\n pending = Array.from(changed);\n withDependents = true;\n }\n }\n\n /**\n * Rebuilds the dependency graph from scratch and recalculates every formula\n * in dependency order.\n */\n public recalculateAll(): void {\n this.cfDirty = 'all';\n this.depGraph.clear();\n const formulaKeys: string[] = [];\n for (const rt of this.runtimes) {\n this.withSheet(rt, () => {\n this.spills.clear();\n this.spilledBy.clear();\n this.spillCells.clear();\n for (const row of this.document.rows) {\n for (const col of this.document.columns) {\n // Avoid getCell's id lookups: this loop visits every cell.\n const cell = this.document.cells[row.id]?.[col.id] ?? this.getDerivedCell(row, col);\n if (cell.raw.startsWith('=')) {\n const cellKey = `${col.key}${row.index + 1}`.toUpperCase();\n this.registerFormula(cellKey, cell.raw);\n formulaKeys.push(this.qualify(cellKey));\n }\n }\n }\n });\n }\n this.recalculateKeys(formulaKeys, false);\n }\n\n private runEvaluationPlan(plan: { order: string[]; cyclic: Set<string> }): void {\n for (const qualified of plan.order) {\n const located = this.unqualify(qualified);\n if (!located) continue;\n // Conditional formatting tracks changed cells on the active sheet only; other\n // sheets are evaluated from scratch when they are activated.\n const onActive = located.rt === this.active;\n this.withSheet(located.rt, () => {\n if (onActive) this.markValueChanged(located.key); // before: cells it spilled into\n if (plan.cyclic.has(qualified)) {\n this.markCycle(located.key);\n } else {\n this.evalSingleCellByKey(located.key);\n }\n if (onActive) this.markValueChanged(located.key); // after: cells it spills into now\n });\n }\n }\n\n private locateCellKey(cellKey: string): { row: SpreadsheetRow; col: SpreadsheetColumn } | null {\n const match = cellKey.match(/^([A-Za-z]+)(\\d+)$/);\n if (!match) return null;\n const col = this.document.columns.find((c) => c.key.toUpperCase() === match[1].toUpperCase());\n const row = this.document.rows[parseInt(match[2], 10) - 1];\n return col && row ? { row, col } : null;\n }\n\n private markCycle(cellKey: string): void {\n const located = this.locateCellKey(cellKey);\n if (!located) return;\n const { row, col } = located;\n const cell = this.getCell(row.id, col.id);\n if (!cell.raw.startsWith('=')) return;\n if (!this.document.cells[row.id]) this.document.cells[row.id] = {};\n // A direct self reference (A1 = A1+1, or SUM(A:A) in column A) gets its own message.\n const qualified = this.qualify(cellKey);\n const deps = this.depGraph.getDependencies(qualified);\n const selfReference = deps.includes(qualified) || deps.includes(columnDependencyKey(`${this.document.id}!${col.key}`));\n const spilledKeys = this.depGraph.getSpilledKeys(qualified);\n const ownSpill = deps.find((dep) => spilledKeys.includes(dep));\n const errorMessage = selfReference\n ? \"This formula can't reference its own cell.\"\n : ownSpill\n ? `This formula refers to ${ownSpill.slice(ownSpill.lastIndexOf('!') + 1)}, which holds its own spilled results (a circular reference).`\n : 'This formula is part of a circular reference: it depends on its own result through other cells.';\n // Keep the spill edges, so the cycle stays detected on the next pass.\n this.clearSpill(cellKey.toUpperCase(), true);\n this.document.cells[row.id][col.id] = { ...cell, value: '#CYCLE!', error: '#CYCLE!', errorMessage };\n }\n\n private evalSingleCellByKey(cellKey: string): void {\n const match = cellKey.match(/^([A-Za-z]+)(\\d+)$/);\n if (!match) return;\n const colName = match[1].toUpperCase();\n const rowNum = parseInt(match[2], 10);\n\n const col = this.document.columns.find((c) => c.key.toUpperCase() === colName);\n const row = this.document.rows[rowNum - 1];\n if (!col || !row) return;\n\n const cell = this.getCell(row.id, col.id);\n if (!cell.raw.startsWith('=')) {\n if (cell.error === '#CYCLE!') {\n cell.error = undefined;\n cell.errorMessage = undefined;\n }\n // No longer a formula: drop any array it spilled.\n this.clearSpill(`${colName}${rowNum}`);\n return;\n }\n\n try {\n this.evaluatingRowIndex = rowNum - 1;\n this.evaluatingCell = { col: colName, row: rowNum };\n const result = this.evaluator.evaluateWithDiagnostics(cell.raw);\n if (!this.document.cells[row.id]) {\n this.document.cells[row.id] = {};\n }\n let { value, error, message } = result;\n if (result.array) {\n const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, this.document.columns.indexOf(col), result.array);\n if (blocked) {\n value = '#SPILL!';\n error = '#SPILL!';\n message = blocked;\n }\n } else {\n this.clearSpill(`${colName}${rowNum}`);\n }\n this.document.cells[row.id][col.id] = {\n ...cell,\n value,\n // `#ERROR!` (malformed formula, wrong argument count) keeps its\n // message in `error`, as before; other errors store their code.\n error: error === '#ERROR!' ? message : error,\n errorMessage: message\n };\n } finally {\n this.evaluatingRowIndex = null;\n this.evaluatingCell = null;\n }\n }\n\n // --- Dynamic arrays (spilling) ---\n\n /**\n * Spills `array` from the anchor at (row, col) into the cells below and to\n * the right. Returns why it can't (the anchor then shows `#SPILL!`), or\n * null once placed. The grid never grows to fit an array, as in Excel.\n */\n private placeSpill(anchor: string, row: number, col: number, array: FormulaRange): string | null {\n const { rows, cols } = array;\n const area: CellRange = { startRow: row, startCol: col, endRow: row + rows - 1, endCol: col + cols - 1 };\n const label = `${indexToColName(area.startCol)}${area.startRow + 1}:${indexToColName(area.endCol)}${area.endRow + 1}`;\n let reason: string | null = null;\n const blockers: string[] = [];\n\n if (area.endRow >= this.document.rows.length || area.endCol >= this.document.columns.length) {\n reason = `The ${rows}×${cols} result needs ${label}, which extends past the edge of the sheet.`;\n } else if (this.getMerges().some((m) => rangesIntersect(m, area))) {\n reason = `The result can't spill into merged cells (it needs ${label}).`;\n } else {\n for (let c = area.startCol; c <= area.endCol; c++) {\n const column = this.document.columns[c];\n for (let r = area.startRow; r <= area.endRow; r++) {\n if (r === row && c === col) continue;\n const rowObj = this.document.rows[r];\n const key = `${column.key}${r + 1}`.toUpperCase();\n const stored = this.document.cells[rowObj.id]?.[column.id];\n const raw = stored ? stored.raw : this.getDerivedCell(rowObj, column).raw;\n const owner = this.spilledBy.get(key);\n if (raw !== '') {\n blockers.push(key);\n reason ??= `The result needs ${label}, but ${key} isn't empty.`;\n } else if (owner && owner !== anchor) {\n blockers.push(key);\n reason ??= `The result needs ${label}, but ${key} already holds values spilled from ${owner}.`;\n }\n }\n }\n }\n\n if (reason) {\n this.clearSpill(anchor);\n // Clearing a blocking cell recalculates this anchor.\n this.depGraph.setSpillEdges(this.qualify(anchor), [], blockers.map((key) => this.qualify(key)));\n return reason;\n }\n\n const previous = this.spills.get(anchor);\n const cells: SpillRecord['cells'] = [];\n for (let c = 0; c < cols; c++) {\n const column = this.document.columns[col + c];\n for (let r = 0; r < rows; r++) {\n if (r === 0 && c === 0) continue;\n const rowObj = this.document.rows[row + r];\n const key = `${column.key}${row + r + 1}`.toUpperCase();\n const value = array.values[c * rows + r] ?? '';\n const display: CellData = {\n raw: '',\n value,\n format: column.format,\n spillAnchor: anchor,\n ...this.fixedNumberFormatFields(column, this.document.cells[rowObj.id]?.[column.id])\n };\n if (isFormulaError(value)) {\n display.error = value;\n display.errorMessage = describeFormulaError(value);\n }\n this.spillCells.set(spillCellId(rowObj.id, column.id), display);\n if (this.spilledBy.get(key) !== anchor) {\n this.spilledBy.set(key, anchor);\n this.spillChanges?.add(this.qualify(key));\n }\n cells.push({ key, rowId: rowObj.id, colId: column.id });\n }\n }\n if (previous) {\n const kept = new Set(cells.map((cell) => cell.key));\n for (const old of previous.cells) {\n if (kept.has(old.key)) continue;\n this.forgetSpilledCell(anchor, old);\n }\n }\n this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });\n this.depGraph.setSpillEdges(this.qualify(anchor), cells.map((cell) => this.qualify(cell.key)));\n return null;\n }\n\n /** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */\n private clearSpill(anchor: string, keepEdges = false): void {\n const record = this.spills.get(anchor);\n if (record) {\n for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);\n this.spills.delete(anchor);\n }\n if (!keepEdges) this.depGraph.setSpillEdges(this.qualify(anchor), []);\n }\n\n private forgetSpilledCell(anchor: string, cell: SpillRecord['cells'][number]): void {\n if (this.spilledBy.get(cell.key) !== anchor) return;\n this.spilledBy.delete(cell.key);\n this.spillCells.delete(spillCellId(cell.rowId, cell.colId));\n this.spillChanges?.add(this.qualify(cell.key));\n }\n\n private resolveSpillByCoord(colStr: string, rowNum: number): FormulaRange | string {\n const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());\n if (record) return { rows: record.rows, cols: record.cols, values: record.values };\n const value = this.resolveCellByCoord(colStr, rowNum);\n return isFormulaError(value) ? value : '#REF!';\n }\n\n private toSpill(record: SpillRecord): SpreadsheetSpill {\n const row = this.document.rows[record.row];\n const col = this.document.columns[record.col];\n return {\n key: record.anchor,\n anchor: { row: record.row, col: record.col },\n range: {\n startRow: record.row,\n startCol: record.col,\n endRow: record.row + record.rows - 1,\n endCol: record.col + record.cols - 1\n },\n formula: row && col ? this.getCell(row.id, col.id).raw : ''\n };\n }\n\n /**\n * The dynamic array covering (row, col), whether it is the anchor (the cell\n * holding the formula) or a spilled cell; null when there is none.\n */\n public getSpillAt(row: number, col: number): SpreadsheetSpill | null {\n if (this.spills.size === 0) return null;\n const key = `${indexToColName(col)}${row + 1}`;\n const record = this.spills.get(key) ?? this.spills.get(this.spilledBy.get(key) ?? '');\n return record ? this.toSpill(record) : null;\n }\n\n /** Every dynamic array currently spilled. */\n public getSpills(): SpreadsheetSpill[] {\n return Array.from(this.spills.values(), (record) => this.toSpill(record));\n }\n\n /** Whether `range` overlaps a spilled array (its anchor or spilled cells). */\n private intersectsSpill(range: CellRange): boolean {\n for (const record of this.spills.values()) {\n const area: CellRange = {\n startRow: record.row,\n startCol: record.col,\n endRow: record.row + record.rows - 1,\n endCol: record.col + record.cols - 1\n };\n if (rangesIntersect(area, range)) return true;\n }\n return false;\n }\n\n private resolveCellByCoord(colStr: string, rowNum: number): any {\n const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());\n const row = this.document.rows[rowNum - 1];\n if (!col || !row) return 0;\n const override = this.valueOverride;\n if (override && override.row === rowNum - 1 && this.document.columns[override.col] === col) return override.value;\n\n // Blank cells resolve to '' (0 in arithmetic, \"\" in text), as in spreadsheets.\n return this.getCell(row.id, col.id).value ?? '';\n }\n\n private resolveRangeByCoords(sCol: string, sRow: number, eCol: string, eRow: number): any[] {\n const startC = colNameToIndex(sCol);\n const endC = colNameToIndex(eCol);\n const minC = Math.min(startC, endC);\n const maxC = Math.max(startC, endC);\n const minR = Math.min(sRow, eRow);\n // Whole-column ranges (A:A) stop at the last row.\n const maxR = Math.min(Math.max(sRow, eRow), WHOLE_COLUMN_END_ROW, this.document.rows.length);\n\n const results: any[] = [];\n for (let c = minC; c <= maxC; c++) {\n const colName = indexToColName(c);\n const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);\n if (!col) continue;\n\n const override = this.valueOverride && this.document.columns[this.valueOverride.col] === col ? this.valueOverride : null;\n for (let r = minR; r <= maxR; r++) {\n const row = this.document.rows[r - 1];\n if (!row) continue;\n if (override && override.row === r - 1) {\n results.push(override.value);\n continue;\n }\n results.push(this.getCell(row.id, col.id).value ?? '');\n }\n }\n return results;\n }\n\n private resolveCurrentRowProperty(prop: string): any {\n // Resolve against the row being evaluated, not the selected cell's row.\n const rowIndex = this.evaluatingRowIndex ?? this.activeCell?.row;\n if (rowIndex === undefined || rowIndex === null) return 0;\n const row = this.document.rows[rowIndex];\n if (!row || !row.recordId || !this.primaryDataSourceId) return 0;\n return this.binder.resolveBoundValue(this.primaryDataSourceId, row.recordId, prop) ?? 0;\n }\n\n // --- Selection and Navigation ---\n\n public selectCell(row: number, col: number, extendSelection = false): void {\n const clampedRow = Math.max(0, Math.min(row, this.document.rows.length - 1));\n const clampedCol = Math.max(0, Math.min(col, this.document.columns.length - 1));\n\n // A merged cell is selected as a whole, through its top-left cell.\n const merge = extendSelection ? null : this.getMergeAt(clampedRow, clampedCol);\n const activeRow = merge ? merge.startRow : clampedRow;\n const activeCol = merge ? merge.startCol : clampedCol;\n const rowObj = this.document.rows[activeRow];\n const colObj = this.document.columns[activeCol];\n\n this.activeCell = {\n row: activeRow,\n col: activeCol,\n rowId: rowObj?.id,\n colId: colObj?.id\n };\n\n if (extendSelection && this.selectedRange) {\n this.selectedRange = this.expandRangeToMerges({\n startRow: this.selectedRange.startRow,\n startCol: this.selectedRange.startCol,\n endRow: clampedRow,\n endCol: clampedCol\n });\n } else if (merge) {\n this.selectedRange = { ...merge };\n } else {\n this.selectedRange = {\n startRow: clampedRow,\n startCol: clampedCol,\n endRow: clampedRow,\n endCol: clampedCol\n };\n }\n\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n }\n\n public selectRange(range: CellRange): void {\n this.selectedRange = range;\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n }\n\n public startEditing(row?: number, col?: number, initialValue?: string): void {\n this.selectCell(row ?? this.activeCell?.row ?? 0, col ?? this.activeCell?.col ?? 0);\n // selectCell moves into a merge's top-left cell, which holds its content.\n const targetRow = this.activeCell?.row ?? 0;\n const targetCol = this.activeCell?.col ?? 0;\n\n const rowObj = this.document.rows[targetRow];\n const colObj = this.document.columns[targetCol];\n if (!rowObj || !colObj) return;\n\n this.editingCell = {\n row: targetRow,\n col: targetCol,\n rowId: rowObj.id,\n colId: colObj.id\n };\n\n const cell = this.getCell(rowObj.id, colObj.id);\n this.draftValue = initialValue !== undefined ? initialValue : cell.raw;\n }\n\n /**\n * Commits the cell editor's draft (or `valueToCommit`). Input that breaks\n * the cell's data validation is not committed: the editor stays open and\n * `validationPrompt` holds the alert until `resolveValidationPrompt`.\n * Unchanged input is not validated.\n */\n public commitEditing(valueToCommit?: string): SpreadsheetCommitResult {\n if (!this.editingCell) return { status: 'none' };\n if (this.validationPrompt) return { status: 'pending', prompt: this.validationPrompt };\n const val = valueToCommit !== undefined ? valueToCommit : this.draftValue;\n const { rowId, colId, row, col } = this.editingCell;\n if (rowId && colId) {\n const failed = this.checkInput(rowId, colId, row, col, val, true);\n if (failed) return failed;\n this.setCellValue(rowId, colId, val);\n }\n this.editingCell = null;\n this.draftValue = '';\n return { status: 'committed' };\n }\n\n /**\n * Commits typed input to a cell (e.g. from the formula bar or a list\n * dropdown) with data validation, like `commitEditing` without an open\n * editor. `setCellValue` never validates (paste, import and code bypass\n * validation, as in Excel).\n */\n public commitCellInput(rowId: string, colId: string, raw: string): SpreadsheetCommitResult {\n if (this.validationPrompt) return { status: 'pending', prompt: this.validationPrompt };\n const row = this.document.rows.findIndex((r) => r.id === rowId);\n const col = this.document.columns.findIndex((c) => c.id === colId);\n if (row === -1 || col === -1) return { status: 'none' };\n const failed = this.checkInput(rowId, colId, row, col, raw, false);\n if (failed) return failed;\n this.setCellValue(rowId, colId, raw);\n return { status: 'committed' };\n }\n\n /** Validates input; when it fails, records and returns the prompt. */\n private checkInput(\n rowId: string,\n colId: string,\n row: number,\n col: number,\n raw: string,\n editing: boolean\n ): SpreadsheetCommitResult | null {\n if (!this.document.table?.dataValidations?.length) return null;\n if (this.getCell(rowId, colId).raw === raw) return null;\n const result = this.validateCellInput(row, col, raw);\n if (result.valid) return null;\n const prompt: SpreadsheetValidationPrompt = { rowId, colId, row, col, raw, result, editing };\n this.validationPrompt = prompt;\n return { status: result.errorStyle === 'stop' ? 'rejected' : 'confirm', prompt };\n }\n\n /**\n * Answers `validationPrompt`: `accept` keeps the input (warning and\n * information alerts only; for a stop alert it acts as `retry`), `retry`\n * returns to editing it, `cancel` discards it.\n */\n public resolveValidationPrompt(action: SpreadsheetValidationAction): void {\n const prompt = this.validationPrompt;\n if (!prompt) return;\n this.validationPrompt = null;\n const editingSameCell =\n prompt.editing && this.editingCell?.rowId === prompt.rowId && this.editingCell?.colId === prompt.colId;\n if (action === 'accept' && prompt.result.errorStyle !== 'stop') {\n this.setCellValue(prompt.rowId, prompt.colId, prompt.raw);\n if (editingSameCell) {\n this.editingCell = null;\n this.draftValue = '';\n }\n } else if (action === 'cancel') {\n if (editingSameCell) this.cancelEditing();\n } else if (editingSameCell) {\n this.draftValue = prompt.raw;\n } else {\n this.startEditing(prompt.row, prompt.col, prompt.raw);\n }\n }\n\n public cancelEditing(): void {\n this.editingCell = null;\n this.draftValue = '';\n }\n\n public moveCursor(\n direction: 'up' | 'down' | 'left' | 'right',\n extendSelection = false\n ): void {\n if (!this.activeCell) {\n this.selectCell(0, 0);\n return;\n }\n\n // Move in display order: skip hidden, filtered and collapsed rows, hidden\n // columns, and step over merged cells as a whole.\n const { rowOrder, columnOrder } = this.getViewModel();\n let nextRow = this.activeCell.row;\n let nextCol = this.activeCell.col;\n const merge = extendSelection ? null : this.getMergeAt(nextRow, nextCol);\n\n switch (direction) {\n case 'up':\n nextRow = stepInOrder(rowOrder, merge ? merge.startRow : nextRow, -1);\n break;\n case 'down':\n nextRow = stepInOrder(rowOrder, merge ? merge.endRow : nextRow, 1);\n break;\n case 'left':\n nextCol = stepInOrder(columnOrder, merge ? merge.startCol : nextCol, -1);\n break;\n case 'right':\n nextCol = stepInOrder(columnOrder, merge ? merge.endCol : nextCol, 1);\n break;\n }\n\n this.selectCell(nextRow, nextCol, extendSelection);\n }\n\n // --- Grid Structure Mutations ---\n\n public insertColumn(index: number, config?: Partial<SpreadsheetColumn>): void {\n this.recordSnapshot();\n const newId = config?.id || this.generateId('col');\n const newCol: SpreadsheetColumn = {\n id: newId,\n key: indexToColName(index),\n title: config?.title || indexToColName(index),\n width: config?.width || 130,\n type: config?.type || 'freeform',\n binding: config?.binding,\n formula: config?.formula,\n format: config?.format || 'text',\n ...(config?.numberFormat ? { numberFormat: config.numberFormat } : {}),\n readOnly: config?.readOnly\n };\n\n this.shiftReferences({ axis: 'col', index, type: 'insert' });\n this.adjustTableForStructuralChange({ axis: 'col', index, type: 'insert' });\n this.document.columns.splice(index, 0, newCol);\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public deleteColumn(colId: string): void {\n this.recordSnapshot();\n const colIndex = this.document.columns.findIndex((c) => c.id === colId);\n if (colIndex === -1) return;\n this.shiftReferences({ axis: 'col', index: colIndex, type: 'delete' });\n this.adjustTableForStructuralChange({ axis: 'col', index: colIndex, type: 'delete' });\n this.document.columns = this.document.columns.filter((c) => c.id !== colId);\n for (const rowId of Object.keys(this.document.cells)) {\n delete this.document.cells[rowId][colId];\n }\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public insertRow(index: number, config?: Partial<SpreadsheetRow>): void {\n this.recordSnapshot();\n const newId = config?.id || this.generateId('row');\n const newRow: SpreadsheetRow = {\n id: newId,\n index,\n height: config?.height || 32,\n recordId: config?.recordId,\n type: config?.type || 'freeform'\n };\n\n this.shiftReferences({ axis: 'row', index, type: 'insert' });\n this.adjustTableForStructuralChange({ axis: 'row', index, type: 'insert' });\n this.document.rows.splice(index, 0, newRow);\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public deleteRow(rowId: string): void {\n this.recordSnapshot();\n const rowIndex = this.document.rows.findIndex((r) => r.id === rowId);\n if (rowIndex === -1) return;\n this.shiftReferences({ axis: 'row', index: rowIndex, type: 'delete' });\n this.adjustTableForStructuralChange({ axis: 'row', index: rowIndex, type: 'delete' });\n this.document.rows = this.document.rows.filter((r) => r.id !== rowId);\n delete this.document.cells[rowId];\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n private generateId(prefix: 'row' | 'col'): string {\n // The counter keeps ids unique when many rows are created in the same\n // millisecond (e.g. growing the grid on import); duplicate row ids would\n // share cell storage.\n return `${prefix}_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;\n }\n\n /**\n * Rewrites references in every cell formula (and, for column changes, every\n * column formula) so they keep pointing at the same cells.\n */\n private shiftReferences(change: StructuralChange): void {\n // On every sheet: references to this sheet (unqualified ones on this\n // sheet, `ThisSheet!A1` anywhere) shift. 3-D references don't.\n const changed = this.ctx;\n for (const rt of this.runtimes) {\n const filter = this.referencesTo(changed, rt);\n for (const rowCells of Object.values(rt.doc.cells)) {\n for (const cell of Object.values(rowCells)) {\n if (cell.raw?.startsWith('=')) {\n cell.raw = shiftFormulaReferences(cell.raw, change, filter);\n }\n }\n }\n if (change.axis === 'col') {\n for (const col of rt.doc.columns) {\n if (col.formula?.startsWith('=')) {\n col.formula = shiftFormulaReferences(col.formula, change, filter);\n }\n }\n }\n }\n for (const n of this.names) {\n const scope = n.sheetId !== undefined ? this.findRuntimeById(n.sheetId) : undefined;\n n.refersTo = shiftFormulaReferences(n.refersTo, change, {\n appliesTo: (sheet, sheetEnd) =>\n sheetEnd === undefined && (sheet === undefined ? scope === changed : sameName(sheet, changed.doc.name))\n });\n }\n }\n\n /** Filter for references, in formulas on sheet `on`, that point at sheet `target`. */\n private referencesTo(target: SheetRuntime, on: SheetRuntime): FormulaReferenceFilter {\n return {\n appliesTo: (sheet, sheetEnd) =>\n sheetEnd === undefined && (sheet === undefined ? on === target : sameName(sheet, target.doc.name))\n };\n }\n\n public setColumnWidth(colId: string, width: number): void {\n const col = this.document.columns.find((c) => c.id === colId);\n if (col) {\n col.width = Math.max(30, width);\n this.notifyChange();\n }\n }\n\n public autoFitColumnWidth(colId: string, customWidth?: number): void {\n if (customWidth !== undefined) {\n this.setColumnWidth(colId, customWidth);\n return;\n }\n const col = this.document.columns.find((c) => c.id === colId);\n if (!col) return;\n\n let maxLen = (col.title || '').length + (col.key ? col.key.length + 3 : 0);\n for (const row of this.document.rows) {\n const cell = this.getCell(row.id, col.id);\n const valStr = formatCellDisplay(cell);\n if (valStr.length > maxLen) {\n maxLen = valStr.length;\n }\n }\n const estimatedWidth = Math.min(600, Math.max(50, Math.ceil(maxLen * 8.5 + 32)));\n this.setColumnWidth(colId, estimatedWidth);\n }\n\n // --- Clipboard TSV/CSV ---\n\n /**\n * Exports cell values as TSV: the whole sheet, or only `range` (e.g. the\n * current selection) when given.\n */\n public exportToTsv(range?: CellRange | null): string {\n const rows = range\n ? this.document.rows.slice(\n Math.min(range.startRow, range.endRow),\n Math.max(range.startRow, range.endRow) + 1,\n )\n : this.document.rows;\n const columns = range\n ? this.document.columns.slice(\n Math.min(range.startCol, range.endCol),\n Math.max(range.startCol, range.endCol) + 1,\n )\n : this.document.columns;\n return rows\n .map((row) =>\n columns\n .map((col) => {\n return formatCellDisplay(this.getCell(row.id, col.id)).replace(/\\t/g, ' ');\n })\n .join('\\t')\n )\n .join('\\n');\n }\n\n /**\n * Pastes tab-separated values starting at (startRow, startCol), adding rows\n * and columns as needed. The whole paste is a single undo step.\n */\n public importFromTsv(tsvData: string, startRow = 0, startCol = 0): void {\n // Only drop the final line break: leading tabs are empty cells.\n const normalized = tsvData.replace(/\\r\\n?/g, '\\n').replace(/\\n$/, '');\n if (normalized === '') return;\n this.writeMatrix(normalized.split('\\n').map((line) => line.split('\\t')), startRow, startCol);\n }\n\n /** Adds rows/columns at the end until (rowIndex, colIndex) exists. */\n private growGrid(rowIndex: number, colIndex: number): void {\n // Growing the grid is not an insertion: no references shift.\n while (rowIndex >= this.document.rows.length) {\n const index = this.document.rows.length;\n this.document.rows.push({ id: this.generateId('row'), index, height: 32, type: 'freeform' });\n }\n while (colIndex >= this.document.columns.length) {\n const key = indexToColName(this.document.columns.length);\n this.document.columns.push({\n id: this.generateId('col'),\n key,\n title: key,\n width: 130,\n type: 'freeform',\n format: 'text'\n });\n }\n }\n\n /**\n * Writes raw inputs starting at (startRow, startCol), growing the grid as\n * needed, as a single undo step with one recalculation.\n */\n private writeMatrix(matrix: string[][], startRow: number, startCol: number): void {\n if (matrix.length === 0) return;\n this.runBatch(() => {\n matrix.forEach((line, rIdx) => {\n line.forEach((val, cIdx) => {\n this.growGrid(startRow + rIdx, startCol + cIdx);\n const row = this.document.rows[startRow + rIdx];\n this.setCellValue(row.id, this.document.columns[startCol + cIdx].id, val);\n });\n });\n });\n }\n\n /**\n * Runs `fn` as one undo step: cell writes inside it are batched and\n * recalculated once, then listeners are notified.\n */\n private runBatch(fn: () => void): void {\n this.recordSnapshot();\n this.batching = true;\n this.pendingCommits = [];\n try {\n fn();\n } finally {\n this.batching = false;\n }\n\n this.recomputeRowIndices();\n this.recalculateAll();\n this.document.updatedAt = Date.now();\n this.notifyChange();\n const commits = this.pendingCommits;\n this.pendingCommits = [];\n for (const [rowId, colId, raw] of commits) {\n this.onCellCommit?.(rowId, colId, raw, this.document.cells[rowId]?.[colId]?.value);\n }\n }\n\n // --- CSV ---\n\n /**\n * Exports the sheet (or `range`) as RFC 4180 CSV. Values are exported as\n * displayed unless `formulas` is set.\n */\n public exportToCsv(options: CsvExportOptions = {}): string {\n const matrix = this.readMatrix(options.range ?? null).map((row) =>\n row.map((cell) => (options.formulas ? cell.raw : formatCellDisplay(cell)))\n );\n return serializeCsv(matrix, {\n delimiter: options.delimiter,\n lineEnding: options.lineEnding,\n bom: options.bom\n });\n }\n\n /**\n * Imports CSV text at (startRow, startCol) (default A1), growing the grid\n * as needed. The whole import is a single undo step.\n */\n public importFromCsv(csv: string, options: CsvImportOptions = {}): void {\n const matrix = parseCsv(csv, { delimiter: options.delimiter });\n this.onTargetSheet(options.sheet, () => this.writeMatrix(matrix, options.startRow ?? 0, options.startCol ?? 0));\n }\n\n /** Runs `fn` on the sheet `sheet` (id or name), default the active one. Throws for an unknown sheet. */\n private onTargetSheet(sheet: string | undefined, fn: () => void): void {\n if (sheet === undefined) {\n fn();\n return;\n }\n const rt = this.findRuntime(sheet);\n if (!rt) throw new Error(`There is no sheet \"${sheet}\".`);\n this.withSheet(rt, fn);\n }\n\n /**\n * Writes already-parsed rows of raw inputs (e.g. from an import adapter)\n * starting at (startRow, startCol), as a single undo step.\n */\n public importRows(rows: string[][], options: Omit<CsvImportOptions, 'delimiter'> = {}): void {\n this.onTargetSheet(options.sheet, () => this.writeMatrix(rows, options.startRow ?? 0, options.startCol ?? 0));\n }\n\n /** The cells of `range` (or the whole sheet), row by row. */\n private readMatrix(range: CellRange | null): CellData[][] {\n const r = range ? normalizeRange(range) : null;\n const rows = r ? this.document.rows.slice(r.startRow, r.endRow + 1) : this.document.rows;\n const columns = r ? this.document.columns.slice(r.startCol, r.endCol + 1) : this.document.columns;\n return rows.map((row) => columns.map((col) => this.getCell(row.id, col.id)));\n }\n\n // --- Spreadsheet clipboard ---\n\n /**\n * Builds clipboard data for `range` (default: the selection) as TSV,\n * an HTML table and the lossless internal JSON format. Write every entry\n * with `clipboardData.setData(type, data)` in a `copy` event.\n */\n public copyToClipboardData(range: CellRange | null = this.selectedRange): SpreadsheetClipboardData {\n const r = range\n ? normalizeRange(range)\n : { startRow: 0, startCol: 0, endRow: this.document.rows.length - 1, endCol: this.document.columns.length - 1 };\n const cells = this.readMatrix(r).map((row) =>\n row.map((cell) => {\n const value = cell.error || cell.value;\n const text = formatCellDisplay(cell);\n // A spilled value is pasted as a constant unless its anchor (whose\n // formula re-spills) is copied too.\n if (cell.spillAnchor && !this.spillAnchorInRange(cell.spillAnchor, r)) {\n return { raw: text, value, text };\n }\n return { raw: cell.raw, value, text };\n })\n );\n return createSpreadsheetClipboardData(cells, { row: r.startRow, col: r.startCol });\n }\n\n /**\n * Pastes clipboard data at `target` (default: the selection's top-left).\n * Prefers the internal format (formulas re-based to the target), then an\n * HTML table, then TSV. Returns false when there was nothing to paste.\n */\n public pasteClipboardData(source: SpreadsheetClipboardSource, target?: CellPosition): boolean {\n const parsed = readSpreadsheetClipboard(source);\n if (!parsed) return false;\n const at = target ?? this.getSelectionTopLeft();\n const rows = parsed.origin ? rebaseClipboardRows(parsed.rows, parsed.origin, at) : parsed.rows;\n this.writeMatrix(rows, at.row, at.col);\n const r = normalizeRange({\n startRow: at.row,\n startCol: at.col,\n endRow: at.row + rows.length - 1,\n endCol: at.col + rows.reduce((max, row) => Math.max(max, row.length), 1) - 1\n });\n this.selectRange(r);\n return true;\n }\n\n private spillAnchorInRange(anchor: string, range: CellRange): boolean {\n const record = this.spills.get(anchor);\n return !!record && rangeContains(range, record.row, record.col);\n }\n\n private getSelectionTopLeft(): CellPosition {\n if (this.selectedRange) {\n const r = normalizeRange(this.selectedRange);\n return { row: r.startRow, col: r.startCol };\n }\n return { row: this.activeCell?.row ?? 0, col: this.activeCell?.col ?? 0 };\n }\n\n // --- Table operations: shared helpers ---\n\n /** The table settings object, created on first use. */\n private get table(): SpreadsheetTableSettings {\n if (!this.document.table) this.document.table = {};\n return this.document.table;\n }\n\n /** Records an undo step, applies `fn`, then notifies listeners. */\n private applyTableChange(fn: () => void, options: { recalculate?: boolean } = {}): void {\n this.recordSnapshot();\n fn();\n if (options.recalculate) {\n this.recomputeRowIndices();\n this.recalculateAll();\n }\n this.document.updatedAt = Date.now();\n this.notifyChange();\n }\n\n /** First and last (inclusive) body row: rows between header and footer rows. */\n private getBodyRowBounds(): { start: number; end: number } {\n const { headerRows, footerRows } = getTableSections(this.document);\n return { start: headerRows, end: this.document.rows.length - footerRows - 1 };\n }\n\n private getCellValueAt = (rowIndex: number, colIndex: number): unknown => {\n const row = this.document.rows[rowIndex];\n const col = this.document.columns[colIndex];\n if (!row || !col) return '';\n return this.getCell(row.id, col.id).value;\n };\n\n /** Rewrites every formula so references follow cells that moved. */\n private remapAllFormulas(moveCell: (position: CellPosition) => CellPosition | null): void {\n const moved = this.ctx;\n for (const rt of this.runtimes) {\n const filter = this.referencesTo(moved, rt);\n for (const rowCells of Object.values(rt.doc.cells)) {\n for (const cell of Object.values(rowCells)) {\n if (cell.raw?.startsWith('=')) {\n cell.raw = remapFormulaReferences(cell.raw, moveCell, filter);\n }\n }\n }\n }\n }\n\n /**\n * Keeps merges in place when rows/columns are inserted or deleted:\n * merges after the change shift, merges across it grow or shrink, and\n * merges reduced to a single cell are dropped.\n */\n private adjustTableForStructuralChange(change: StructuralChange): void {\n this.adjustRulesForStructuralChange(change);\n const merges = this.document.table?.merges;\n if (!merges?.length) return;\n const startKey = change.axis === 'row' ? 'startRow' : 'startCol';\n const endKey = change.axis === 'row' ? 'endRow' : 'endCol';\n const next: CellRange[] = [];\n for (const merge of merges) {\n const m = { ...merge };\n if (change.type === 'insert') {\n if (m[startKey] >= change.index) m[startKey]++;\n if (m[endKey] >= change.index) m[endKey]++;\n } else {\n if (m[startKey] === change.index && m[endKey] === change.index) continue;\n if (m[startKey] > change.index) m[startKey]--;\n if (m[endKey] >= change.index) m[endKey]--;\n }\n if (!isSingleCellRange(m)) next.push(m);\n }\n this.document.table!.merges = next;\n }\n\n // --- Sort ---\n\n /**\n * Sorts rows by one or more columns (stable; numbers before text, blanks\n * last). Without a range (or with a range spanning every column) whole rows\n * move; with a range only the cells inside it move. Header and footer rows\n * never move. References follow the moved cells. Returns false when the\n * sort is rejected (no valid keys, a key outside the range, or merged\n * cells in the sorted area).\n */\n public sortRows(keys: SortKey[], options: { range?: CellRange | null } = {}): boolean {\n const colCount = this.document.columns.length;\n const body = this.getBodyRowBounds();\n const range = options.range ? normalizeRange(options.range) : null;\n const startRow = Math.max(range?.startRow ?? body.start, body.start);\n const endRow = Math.min(range?.endRow ?? body.end, body.end);\n const startCol = range ? Math.max(0, range.startCol) : 0;\n const endCol = range ? Math.min(colCount - 1, range.endCol) : colCount - 1;\n if (endRow - startRow < 1 || startCol > endCol) return false;\n\n const keyCols = keys.map((key) => ({\n index: this.document.columns.findIndex((c) => c.id === key.colId),\n direction: key.direction ?? 'asc'\n }));\n if (keyCols.length === 0 || keyCols.some((k) => k.index < startCol || k.index > endCol)) return false;\n\n const area: CellRange = { startRow, startCol, endRow, endCol };\n if ((this.document.table?.merges ?? []).some((m) => rangesIntersect(m, area))) return false;\n // Part of a dynamic array can't move (Excel: \"You can't change part of an array\").\n if (this.intersectsSpill(area)) return false;\n\n const sourceRows: number[] = [];\n for (let r = startRow; r <= endRow; r++) sourceRows.push(r);\n const sorted = sourceRows\n .map((r) => ({ r, values: keyCols.map((k) => this.getCellValueAt(r, k.index)) }))\n .sort((a, b) => {\n for (let i = 0; i < keyCols.length; i++) {\n const result = compareCellValues(a.values[i], b.values[i], keyCols[i].direction);\n if (result !== 0) return result;\n }\n return a.r - b.r; // stable\n })\n .map((entry) => entry.r);\n if (sorted.every((r, i) => r === sourceRows[i])) return true; // already sorted\n\n // newIndexOf[oldRow] = newRow\n const newIndexOf = new Map<number, number>();\n sorted.forEach((oldRow, i) => newIndexOf.set(oldRow, startRow + i));\n const wholeRows = startCol === 0 && endCol === colCount - 1;\n\n this.applyTableChange(() => {\n this.remapAllFormulas(({ row, col }) => {\n const target = newIndexOf.get(row);\n if (target === undefined || col < startCol || col > endCol) return null;\n return { row: target, col };\n });\n\n if (wholeRows) {\n const moved = sorted.map((r) => this.document.rows[r]);\n this.document.rows.splice(startRow, moved.length, ...moved);\n } else {\n const colIds = this.document.columns.slice(startCol, endCol + 1).map((c) => c.id);\n const snapshot = sorted.map((oldRow) => {\n const rowId = this.document.rows[oldRow].id;\n return colIds.map((colId) => this.document.cells[rowId]?.[colId]);\n });\n snapshot.forEach((cells, i) => {\n const rowId = this.document.rows[startRow + i].id;\n if (!this.document.cells[rowId]) this.document.cells[rowId] = {};\n colIds.forEach((colId, c) => {\n if (cells[c]) this.document.cells[rowId][colId] = cells[c]!;\n else delete this.document.cells[rowId][colId];\n });\n });\n }\n }, { recalculate: true });\n return true;\n }\n\n // --- Filters ---\n\n public getFilterState(): SpreadsheetFilterState {\n return this.document.table?.filter ?? { enabled: false, rules: [] };\n }\n\n /** Replaces all filter rules (and turns filters on when there are any). */\n public setFilterRules(rules: NewFilterRule[]): void {\n this.applyTableChange(() => {\n this.table.filter = {\n enabled: rules.length > 0,\n rules: rules.map((rule) => ({ ...rule, id: rule.id ?? this.generateFilterId() }))\n };\n });\n }\n\n /** Adds a filter rule (AND-combined with the others) and turns filters on. Returns its id. */\n public addFilterRule(rule: NewFilterRule): string {\n const id = rule.id ?? this.generateFilterId();\n this.applyTableChange(() => {\n const current = this.getFilterState();\n this.table.filter = { enabled: true, rules: [...current.rules, { ...rule, id }] };\n });\n return id;\n }\n\n public updateFilterRule(ruleId: string, changes: Partial<Omit<FilterRule, 'id'>>): void {\n const current = this.getFilterState();\n if (!current.rules.some((r) => r.id === ruleId)) return;\n this.applyTableChange(() => {\n this.table.filter = {\n ...current,\n rules: current.rules.map((r) => (r.id === ruleId ? { ...r, ...changes } : r))\n };\n });\n }\n\n public removeFilterRule(ruleId: string): void {\n const current = this.getFilterState();\n if (!current.rules.some((r) => r.id === ruleId)) return;\n this.applyTableChange(() => {\n this.table.filter = { ...current, rules: current.rules.filter((r) => r.id !== ruleId) };\n });\n }\n\n public clearFilters(): void {\n if (this.getFilterState().rules.length === 0) return;\n this.applyTableChange(() => {\n this.table.filter = { enabled: false, rules: [] };\n });\n }\n\n /** Turns filtering on or off, keeping the rules. */\n public setFiltersEnabled(enabled: boolean): void {\n const current = this.getFilterState();\n if (current.enabled === enabled) return;\n this.applyTableChange(() => {\n this.table.filter = { ...current, enabled };\n });\n }\n\n public toggleFilters(): void {\n this.setFiltersEnabled(!this.getFilterState().enabled);\n }\n\n private generateFilterId(): string {\n return `filter_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;\n }\n\n // --- Header, footer and frozen rows/columns ---\n\n public setHeaderRowCount(count: number): void {\n this.setTableCount('headerRowCount', count, this.document.rows.length);\n }\n\n public setFooterRowCount(count: number): void {\n this.setTableCount('footerRowCount', count, this.document.rows.length);\n }\n\n public setHeaderColumnCount(count: number): void {\n this.setTableCount('headerColumnCount', count, this.document.columns.length);\n }\n\n /** Keeps the first `count` rows visible while scrolling. 0 unfreezes. */\n public setFrozenRowCount(count: number): void {\n this.setTableCount('frozenRowCount', count, this.document.rows.length);\n }\n\n /** Keeps the first `count` columns visible while scrolling. 0 unfreezes. */\n public setFrozenColumnCount(count: number): void {\n this.setTableCount('frozenColumnCount', count, this.document.columns.length);\n }\n\n private setTableCount(\n key: 'headerRowCount' | 'footerRowCount' | 'headerColumnCount' | 'frozenRowCount' | 'frozenColumnCount',\n count: number,\n max: number\n ): void {\n const value = Math.max(0, Math.min(Math.floor(count), max));\n if ((this.document.table?.[key] ?? 0) === value) return;\n this.applyTableChange(() => {\n this.table[key] = value;\n });\n }\n\n // --- Number formats ---\n //\n // Cells are replaced, never mutated in place: framework bindings compare\n // cell objects by identity to decide what to re-render.\n\n /**\n * Sets an explicit number format code (e.g. `0.00`, `\"$\"#,##0.00`,\n * `m/d/yyyy`, `[Red]0;[Blue]-0`) on every cell in `range`, including blank\n * cells, so values typed later keep it. `null` removes explicit formats: the\n * cells go back to their column's format or the one chosen for typed input.\n * Undoable as one step. Returns false when there is no range.\n */\n public setCellNumberFormat(range: CellRange | null, code: string | null): boolean {\n if (!range) return false;\n const r = normalizeRange(range);\n this.recordSnapshot();\n const kind = code === null ? undefined : cellFormatForNumberFormat(code);\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n const row = this.document.rows[ri];\n if (!row) continue;\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n const col = this.document.columns[ci];\n if (!col) continue;\n const cells = (this.document.cells[row.id] ??= {});\n const existing = cells[col.id];\n if (code === null) {\n if (!existing?.explicitNumberFormat) continue;\n if (existing.raw === '' && existing.value === '' && !existing.error) {\n delete cells[col.id];\n } else {\n const { explicitNumberFormat: _explicit, ...rest } = existing;\n cells[col.id] = this.withNumberFormat(rest, this.implicitNumberFormat(rest, col, row));\n }\n } else {\n cells[col.id] = {\n ...(existing ?? { raw: '', value: '' }),\n format: kind ?? existing?.format ?? col.format,\n numberFormat: code,\n explicitNumberFormat: true\n };\n }\n this.refreshSpilledNumberFormat(row, col);\n }\n }\n this.document.updatedAt = Date.now();\n this.notifyChange();\n return true;\n }\n\n /**\n * Sets the number format code of a column (`null` or `General` clears it).\n * Its cells without an explicit format of their own use it, including\n * values typed later; when cleared they go back to the format chosen for\n * typed input. Undoable.\n */\n public setColumnNumberFormat(colId: string, code: string | null): void {\n const col = this.getColumn(colId);\n if (!col) return;\n this.recordSnapshot();\n if (code === null || isGeneralNumberFormat(code)) delete col.numberFormat;\n else col.numberFormat = code;\n for (const row of this.document.rows) {\n const cells = this.document.cells[row.id];\n const cell = cells?.[col.id];\n if (cells && cell && !cell.explicitNumberFormat) {\n cells[col.id] = this.withNumberFormat(cell, this.implicitNumberFormat(cell, col, row));\n }\n this.refreshSpilledNumberFormat(row, col);\n }\n this.document.updatedAt = Date.now();\n this.notifyChange();\n }\n\n /** The column's format, or the one typed input chose (typed cells only). */\n private implicitNumberFormat(cell: CellData, col: SpreadsheetColumn, row: SpreadsheetRow): string | undefined {\n const typed = cell.raw !== '' && !cell.raw.startsWith('=') && !(col.type === 'bound' && col.binding && row.recordId);\n return this.fixedNumberFormat(col, undefined) ?? (typed ? parseCellInput(cell.raw).numberFormat : undefined);\n }\n\n /** A copy of `cell` with `numberFormat` set, or removed when undefined. */\n private withNumberFormat(cell: CellData, numberFormat: string | undefined): CellData {\n const { numberFormat: _old, ...rest } = cell;\n return numberFormat === undefined ? rest : { ...rest, numberFormat };\n }\n\n /** Re-applies the stored cell's (or the column's) format to a spilled value shown there. */\n private refreshSpilledNumberFormat(row: SpreadsheetRow, col: SpreadsheetColumn): void {\n const id = spillCellId(row.id, col.id);\n const spilled = this.spillCells.get(id);\n if (!spilled) return;\n const { numberFormat: _old, explicitNumberFormat: _explicit, ...rest } = spilled;\n this.spillCells.set(id, { ...rest, ...this.fixedNumberFormatFields(col, this.document.cells[row.id]?.[col.id]) });\n }\n\n // --- Hide / unhide ---\n\n public hideRows(indices: number[]): void {\n this.setHidden('row', indices, true);\n }\n\n public unhideRows(indices: number[]): void {\n this.setHidden('row', indices, false);\n }\n\n public hideColumns(indices: number[]): void {\n this.setHidden('col', indices, true);\n }\n\n public unhideColumns(indices: number[]): void {\n this.setHidden('col', indices, false);\n }\n\n /** Unhides every hidden row and column (filters are unaffected). */\n public unhideAll(): void {\n const rows = this.document.rows.flatMap((r, i) => (r.hidden ? [i] : []));\n const cols = this.document.columns.flatMap((c, i) => (c.hidden ? [i] : []));\n if (rows.length === 0 && cols.length === 0) return;\n this.applyTableChange(() => {\n rows.forEach((i) => delete this.document.rows[i].hidden);\n cols.forEach((i) => delete this.document.columns[i].hidden);\n });\n }\n\n private setHidden(axis: 'row' | 'col', indices: number[], hidden: boolean): void {\n const items: Array<SpreadsheetRow | SpreadsheetColumn> = axis === 'row' ? this.document.rows : this.document.columns;\n const targets = indices.filter((i) => items[i] && !!items[i].hidden !== hidden);\n if (targets.length === 0) return;\n this.applyTableChange(() => {\n for (const i of targets) {\n if (hidden) items[i].hidden = true;\n else delete items[i].hidden;\n }\n });\n }\n\n // --- Merged cells ---\n\n public getMerges(): CellRange[] {\n return this.document.table?.merges ?? [];\n }\n\n /** The merge containing (row, col), or null. */\n public getMergeAt(row: number, col: number): CellRange | null {\n return this.getMerges().find((m) => rangeContains(m, row, col)) ?? null;\n }\n\n /** Grows `range` until no merge crosses its edge. Keeps the drag direction. */\n public expandRangeToMerges(range: CellRange): CellRange {\n let r = normalizeRange(range);\n let changed = true;\n while (changed) {\n changed = false;\n for (const m of this.getMerges()) {\n if (rangesIntersect(m, r) && !rangeContainsRange(r, m)) {\n r = {\n startRow: Math.min(r.startRow, m.startRow),\n startCol: Math.min(r.startCol, m.startCol),\n endRow: Math.max(r.endRow, m.endRow),\n endCol: Math.max(r.endCol, m.endCol)\n };\n changed = true;\n }\n }\n }\n const rowsReversed = range.startRow > range.endRow;\n const colsReversed = range.startCol > range.endCol;\n return {\n startRow: rowsReversed ? r.endRow : r.startRow,\n endRow: rowsReversed ? r.startRow : r.endRow,\n startCol: colsReversed ? r.endCol : r.startCol,\n endCol: colsReversed ? r.startCol : r.endCol\n };\n }\n\n /**\n * Merges `range` (default: the selection) into one cell anchored on its\n * top-left cell; the other cells are cleared. Merges fully inside the range\n * are absorbed. Returns false for single cells and for ranges that\n * partially overlap an existing merge.\n */\n public mergeCells(range: CellRange | null = this.selectedRange): boolean {\n if (!range) return false;\n const r = normalizeRange(range);\n if (\n isSingleCellRange(r) ||\n r.startRow < 0 ||\n r.startCol < 0 ||\n r.endRow >= this.document.rows.length ||\n r.endCol >= this.document.columns.length\n ) {\n return false;\n }\n const merges = this.getMerges();\n if (merges.some((m) => rangesIntersect(m, r) && !rangeContainsRange(r, m))) return false;\n if (this.intersectsSpill(r)) return false;\n\n this.applyTableChange(() => {\n for (let row = r.startRow; row <= r.endRow; row++) {\n const rowId = this.document.rows[row].id;\n for (let col = r.startCol; col <= r.endCol; col++) {\n if (row === r.startRow && col === r.startCol) continue;\n delete this.document.cells[rowId]?.[this.document.columns[col].id];\n }\n }\n this.table.merges = [...merges.filter((m) => !rangeContainsRange(r, m)), r];\n }, { recalculate: true });\n this.selectCell(r.startRow, r.startCol);\n return true;\n }\n\n /** Removes every merge intersecting `range` (default: the selection). */\n public unmergeCells(range: CellRange | null = this.selectedRange): boolean {\n if (!range) return false;\n const merges = this.getMerges();\n const remaining = merges.filter((m) => !rangesIntersect(m, range));\n if (remaining.length === merges.length) return false;\n // Recalculate: the merge may have blocked a dynamic array (#SPILL!).\n this.applyTableChange(() => {\n this.table.merges = remaining;\n }, { recalculate: true });\n return true;\n }\n\n // --- Autofill ---\n\n /**\n * Fills `target` from `source` like dragging the fill handle. `target` is\n * the extended range (source included) and must extend `source` down, up,\n * right or left. Numbers and dates continue their series, text ending in a\n * number counts up, other text repeats, and formulas are copied with\n * their relative references shifted (`$` parts stay). Returns false when\n * the ranges don't line up.\n */\n public autofill(source: CellRange, target: CellRange): boolean {\n const s = normalizeRange(source);\n const t = normalizeRange(target);\n const direction = getFillDirection(s, t);\n if (!direction || t.startRow < 0 || t.startCol < 0) return false;\n if (s.endRow >= this.document.rows.length || s.endCol >= this.document.columns.length) return false;\n\n const vertical = direction === 'down' || direction === 'up';\n const forward = direction === 'down' || direction === 'right';\n // Source positions along the fill axis, nearest to the fill area last.\n const sourceSteps: number[] = [];\n const [sLo, sHi] = vertical ? [s.startRow, s.endRow] : [s.startCol, s.endCol];\n for (let i = sLo; i <= sHi; i++) sourceSteps.push(i);\n if (!forward) sourceSteps.reverse();\n const fillSteps: number[] = [];\n if (forward) for (let i = sHi + 1; i <= (vertical ? t.endRow : t.endCol); i++) fillSteps.push(i);\n else for (let i = sLo - 1; i >= (vertical ? t.startRow : t.startCol); i--) fillSteps.push(i);\n\n const [lLo, lHi] = vertical ? [s.startCol, s.endCol] : [s.startRow, s.endRow];\n this.runBatch(() => {\n this.growGrid(vertical ? Math.max(t.endRow, 0) : s.endRow, vertical ? s.endCol : Math.max(t.endCol, 0));\n for (let line = lLo; line <= lHi; line++) {\n const at = (step: number) => (vertical ? { row: step, col: line } : { row: line, col: step });\n const raws = sourceSteps.map((step) => {\n const p = at(step);\n return this.getCell(this.document.rows[p.row].id, this.document.columns[p.col].id).raw;\n });\n const hasFormula = raws.some((raw) => raw.startsWith('='));\n const series = hasFormula ? null : fillSeries(raws, fillSteps.length);\n fillSteps.forEach((step, i) => {\n const p = at(step);\n let raw: string;\n if (series) {\n raw = series[i];\n } else {\n const from = sourceSteps[i % sourceSteps.length];\n raw = raws[i % raws.length];\n if (raw.startsWith('=')) {\n const delta = step - from;\n raw = vertical ? offsetFormulaReferences(raw, delta, 0) : offsetFormulaReferences(raw, 0, delta);\n }\n }\n this.setCellValue(this.document.rows[p.row].id, this.document.columns[p.col].id, raw);\n });\n }\n });\n this.selectRange(t);\n return true;\n }\n\n // --- Categories (row groups) ---\n\n /**\n * Groups body rows by the value in `colId`, with optional per-group\n * summaries (e.g. `[{ colId, aggregate: 'sum' }]`).\n */\n public groupRowsBy(colId: string, summaries: GroupSummary[] = []): void {\n if (!this.getColumn(colId)) return;\n this.applyTableChange(() => {\n this.table.grouping = { colId, summaries, collapsed: [] };\n });\n }\n\n public ungroupRows(): void {\n if (!this.document.table?.grouping) return;\n this.applyTableChange(() => {\n delete this.table.grouping;\n });\n }\n\n /** Sets (or with `null`, removes) the group summary for a column. */\n public setGroupSummary(colId: string, aggregate: GroupAggregate | null): void {\n const grouping = this.document.table?.grouping;\n if (!grouping) return;\n this.applyTableChange(() => {\n const others = grouping.summaries.filter((s) => s.colId !== colId);\n this.table.grouping = {\n ...grouping,\n summaries: aggregate ? [...others, { colId, aggregate }] : others\n };\n });\n }\n\n public setGroupCollapsed(key: string, collapsed: boolean): void {\n const grouping = this.document.table?.grouping;\n if (!grouping || grouping.collapsed.includes(key) === collapsed) return;\n this.applyTableChange(() => {\n this.table.grouping = {\n ...grouping,\n collapsed: collapsed ? [...grouping.collapsed, key] : grouping.collapsed.filter((k) => k !== key)\n };\n });\n }\n\n public toggleGroupCollapsed(key: string): void {\n const grouping = this.document.table?.grouping;\n if (!grouping) return;\n this.setGroupCollapsed(key, !grouping.collapsed.includes(key));\n }\n\n /** The current row groups with their summaries (empty when not grouped). */\n public getGroups(): SpreadsheetRowGroup[] {\n return computeRowGroups(this.document, this.getCellValueAt);\n }\n\n // --- Transpose ---\n\n /**\n * Transposes `range` (default: the selection) in place around its top-left\n * cell: rows become columns. Cells left uncovered are cleared, the grid\n * grows if needed, and references follow the moved cells. Returns false\n * for single cells or when merged cells are involved.\n */\n public transposeRange(range: CellRange | null = this.selectedRange): boolean {\n if (!range) return false;\n const s = normalizeRange(range);\n if (isSingleCellRange(s)) return false;\n const height = s.endRow - s.startRow + 1;\n const width = s.endCol - s.startCol + 1;\n const t: CellRange = { startRow: s.startRow, startCol: s.startCol, endRow: s.startRow + width - 1, endCol: s.startCol + height - 1 };\n if (this.getMerges().some((m) => rangesIntersect(m, s) || rangesIntersect(m, t))) return false;\n if (this.intersectsSpill(s) || this.intersectsSpill(t)) return false;\n\n this.applyTableChange(() => {\n this.growGrid(t.endRow, t.endCol);\n this.remapAllFormulas(({ row, col }) => {\n if (!rangeContains(s, row, col)) return null;\n return { row: s.startRow + (col - s.startCol), col: s.startCol + (row - s.startRow) };\n });\n\n const rowId = (r: number) => this.document.rows[r].id;\n const colId = (c: number) => this.document.columns[c].id;\n const moved: Array<{ row: number; col: number; cell: CellData | undefined }> = [];\n for (let r = s.startRow; r <= s.endRow; r++) {\n for (let c = s.startCol; c <= s.endCol; c++) {\n moved.push({\n row: s.startRow + (c - s.startCol),\n col: s.startCol + (r - s.startRow),\n cell: this.document.cells[rowId(r)]?.[colId(c)]\n });\n delete this.document.cells[rowId(r)]?.[colId(c)];\n }\n }\n for (const { row, col, cell } of moved) {\n if (!cell) continue;\n if (!this.document.cells[rowId(row)]) this.document.cells[rowId(row)] = {};\n this.document.cells[rowId(row)][colId(col)] = cell;\n }\n }, { recalculate: true });\n this.selectRange(t);\n return true;\n }\n\n // --- View model ---\n\n /**\n * What to render: visible rows (with group headers) and columns in display\n * order, frozen offsets and merged-cell spans. Filters, hidden rows/columns\n * and collapsed groups only affect this view, never the document.\n */\n public getViewModel(): SpreadsheetViewModel {\n return buildSpreadsheetView(this.document, this.getCellValueAt);\n }\n\n // --- Sheets ---\n\n /** The sheets in tab order. */\n public getSheets(): SpreadsheetSheetInfo[] {\n return this.runtimes.map((rt, index) => ({ id: rt.doc.id, name: rt.doc.name, index, active: rt === this.active }));\n }\n\n /** Id of the active sheet (the one `document` is). */\n public get activeSheetId(): string {\n return this.active.doc.id;\n }\n\n /** A sheet's document by id or name (live: don't mutate it directly). */\n public getSheetDocument(idOrName: string): SpreadsheetDocument | undefined {\n return this.findRuntime(idOrName)?.doc;\n }\n\n /** Why `name` can't name a sheet (or null): see `validateSheetName`. `exceptSheet` is the sheet being renamed. */\n public validateSheetName(name: string, exceptSheet?: string): string | null {\n const except = exceptSheet === undefined ? undefined : this.findRuntime(exceptSheet);\n return validateSheetName(\n name,\n this.runtimes.filter((rt) => rt !== except).map((rt) => rt.doc.name)\n );\n }\n\n /**\n * Shows another sheet (by id or name). The current edit is committed and\n * each sheet keeps its own selection. Returns false for an unknown sheet.\n */\n public setActiveSheet(idOrName: string): boolean {\n const rt = this.findRuntime(idOrName);\n if (!rt) return false;\n if (rt === this.active) return true;\n if (this.editingCell) this.commitEditing();\n this.activateRuntime(rt);\n this.notifyChange();\n return true;\n }\n\n private activateRuntime(rt: SheetRuntime): void {\n if (rt === this.active) return;\n if (this.runtimes.includes(this.active)) {\n this.active.activeCell = this.activeCell;\n this.active.selectedRange = this.selectedRange;\n }\n this.active = this.ctx = rt;\n this.activeCell = rt.activeCell;\n this.selectedRange = rt.selectedRange;\n this.editingCell = null;\n this.draftValue = '';\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n this.onActiveSheetChange?.(rt.doc.id);\n }\n\n /**\n * Adds a sheet (default: `Sheet N`, empty, after the last sheet) and\n * activates it unless `activate` is false. Returns its id. Throws when\n * `name` is invalid or taken. Undoable.\n */\n public addSheet(\n options: { name?: string; index?: number; document?: Partial<SpreadsheetDocument>; activate?: boolean } = {}\n ): string {\n const existing = this.runtimes.map((rt) => rt.doc.name);\n const name = options.name ?? options.document?.name ?? nextSheetName(existing);\n const invalid = validateSheetName(name, existing);\n if (invalid) throw new Error(invalid);\n this.recordWorkbookSnapshot();\n const id = options.document?.id && !this.findRuntimeById(options.document.id) ? options.document.id : this.generateSheetId();\n const doc: SpreadsheetDocument = {\n columns: Array.from({ length: 4 }, (_, i) => ({\n id: `col_${String.fromCharCode(97 + i)}`,\n key: indexToColName(i),\n title: indexToColName(i),\n width: 120,\n type: 'freeform' as const\n })),\n rows: Array.from({ length: 10 }, (_, i) => ({ id: `row_${i + 1}`, index: i, height: 32, type: 'freeform' as const })),\n cells: {},\n createdAt: Date.now(),\n updatedAt: Date.now(),\n ...JSON.parse(JSON.stringify(options.document ?? {})),\n id,\n name\n };\n const rt = createRuntime(doc);\n const index = Math.max(0, Math.min(options.index ?? this.runtimes.length, this.runtimes.length));\n this.runtimes.splice(index, 0, rt);\n this.withSheet(rt, () => {\n this.recomputeRowIndices();\n this.rederiveTypedValues();\n });\n if (options.activate !== false) this.activateRuntime(rt);\n // Formulas that referred to a sheet of this name (#REF!) now find it.\n this.recalculateAll();\n this.notifyChange();\n return id;\n }\n\n /**\n * Renames a sheet. Formulas on every sheet and defined names that refer to\n * it follow (`=Old!A1` becomes `='New Name'!A1`). Throws when the name is\n * invalid or taken. Undoable.\n */\n public renameSheet(idOrName: string, newName: string): void {\n const rt = this.findRuntime(idOrName);\n if (!rt) throw new Error(`There is no sheet \"${idOrName}\".`);\n if (rt.doc.name === newName) return;\n const invalid = this.validateSheetName(newName, rt.doc.id);\n if (invalid) throw new Error(invalid);\n this.recordWorkbookSnapshot();\n const oldName = rt.doc.name;\n this.rewriteAllFormulas((formula) => renameSheetInFormula(formula, oldName, newName));\n rt.doc.name = newName;\n rt.doc.updatedAt = Date.now();\n this.recalculateAll();\n this.notifyChange();\n }\n\n /** Moves a sheet to position `toIndex` in tab order. Returns false for an unknown sheet. Undoable. */\n public moveSheet(idOrName: string, toIndex: number): boolean {\n const rt = this.findRuntime(idOrName);\n if (!rt) return false;\n const from = this.runtimes.indexOf(rt);\n const to = Math.max(0, Math.min(Math.floor(toIndex), this.runtimes.length - 1));\n if (from === to) return true;\n this.recordWorkbookSnapshot();\n this.runtimes.splice(from, 1);\n this.runtimes.splice(to, 0, rt);\n // 3-D references (Sheet1:Sheet3!A1) cover the sheets between their ends.\n this.recalculateAll();\n this.notifyChange();\n return true;\n }\n\n /**\n * Deletes a sheet and its sheet-scoped names. References to it become\n * `#REF!` (a 3-D reference losing an end sheet shrinks). The last sheet\n * can't be deleted (returns false). Undoable.\n */\n public deleteSheet(idOrName: string): boolean {\n const rt = this.findRuntime(idOrName);\n if (!rt || this.runtimes.length <= 1) return false;\n this.recordWorkbookSnapshot();\n const order = this.runtimes.map((r) => r.doc.name);\n const index = this.runtimes.indexOf(rt);\n this.runtimes.splice(index, 1);\n this.names = this.names.filter((n) => n.sheetId !== rt.doc.id);\n this.rewriteAllFormulas((formula) => deleteSheetInFormula(formula, rt.doc.name, order));\n if (this.active === rt) {\n this.ctx = this.active;\n this.activateRuntime(this.runtimes[Math.min(index, this.runtimes.length - 1)]);\n } else if (this.ctx === rt) {\n this.ctx = this.active;\n }\n this.recalculateAll();\n this.notifyChange();\n return true;\n }\n\n /**\n * Copies a sheet (default name `Name (2)`, placed after it) with its\n * sheet-scoped names, which refer to the copy. Activates the copy unless\n * `activate` is false. Returns the new sheet's id, or null for an unknown\n * sheet. Undoable.\n */\n public duplicateSheet(idOrName: string, options: { name?: string; index?: number; activate?: boolean } = {}): string | null {\n const source = this.findRuntime(idOrName);\n if (!source) return null;\n const existing = this.runtimes.map((rt) => rt.doc.name);\n const name = options.name ?? copySheetName(source.doc.name, existing);\n const invalid = validateSheetName(name, existing);\n if (invalid) throw new Error(invalid);\n this.recordWorkbookSnapshot();\n const doc: SpreadsheetDocument = JSON.parse(JSON.stringify(source.doc));\n doc.id = this.generateSheetId();\n doc.name = name;\n doc.createdAt = doc.updatedAt = Date.now();\n const copies = this.names\n .filter((n) => n.sheetId === source.doc.id)\n .map((n) => ({ ...n, sheetId: doc.id, refersTo: renameSheetInFormula(n.refersTo, source.doc.name, name) }));\n this.names.push(...copies);\n const rt = createRuntime(doc);\n const index = Math.max(0, Math.min(options.index ?? this.runtimes.indexOf(source) + 1, this.runtimes.length));\n this.runtimes.splice(index, 0, rt);\n if (options.activate !== false) this.activateRuntime(rt);\n this.recalculateAll();\n this.notifyChange();\n return doc.id;\n }\n\n /** Applies `rewrite` to every formula: cells and column formulas on every sheet, and defined names. */\n private rewriteAllFormulas(rewrite: (formula: string) => string): void {\n for (const rt of this.runtimes) {\n for (const [rowId, rowCells] of Object.entries(rt.doc.cells)) {\n for (const [colId, cell] of Object.entries(rowCells)) {\n if (!cell.raw?.startsWith('=')) continue;\n const raw = rewrite(cell.raw);\n // Replace the cell object (bindings compare cells by identity).\n if (raw !== cell.raw) rt.doc.cells[rowId][colId] = { ...cell, raw };\n }\n }\n for (const col of rt.doc.columns) {\n if (col.formula?.startsWith('=')) col.formula = rewrite(col.formula);\n }\n }\n for (const n of this.names) n.refersTo = rewrite(n.refersTo);\n }\n\n private generateSheetId(): string {\n return `sheet_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;\n }\n\n // --- Defined names (Name Manager) ---\n\n /** Every defined name, workbook-scoped and sheet-scoped. */\n public getNames(): SpreadsheetNameInfo[] {\n return this.names.map((n) => {\n const scope = n.sheetId !== undefined ? this.findRuntimeById(n.sheetId) : undefined;\n return {\n ...n,\n ...(scope ? { sheetName: scope.doc.name } : {}),\n isLambda: isLambdaDefinition(n)\n };\n });\n }\n\n /** The defined name `name` visible from the active sheet (its own sheet-scoped name first), or undefined. */\n public getName(name: string, scope?: SpreadsheetNameScope): SpreadsheetNameInfo | undefined {\n const rt = scope?.sheet !== undefined ? this.findRuntime(scope.sheet) : undefined;\n const def =\n scope?.sheet !== undefined\n ? rt\n ? this.names.find((n) => n.sheetId === rt.doc.id && sameName(n.name, name))\n : undefined\n : this.findName(name, this.active);\n return def ? this.getNames().find((n) => n.name === def.name && n.sheetId === def.sheetId) : undefined;\n }\n\n /** Why `name` can't be a defined name (or null): see `validateDefinedName`. */\n public validateDefinedName(name: string): string | null {\n return validateDefinedName(name);\n }\n\n private scopeRuntime(scope: SpreadsheetNameScope | undefined): SheetRuntime | undefined {\n if (scope?.sheet === undefined) return undefined;\n const rt = this.findRuntime(scope.sheet);\n if (!rt) throw new Error(`There is no sheet \"${scope.sheet}\".`);\n return rt;\n }\n\n /**\n * Defines (or redefines) a name in the workbook, or on one sheet with\n * `sheet`. `refersTo` is a formula: a range (`=Data!$A$2:$A$20`), a\n * constant (`=0.07`), a formula or a LAMBDA (`=LAMBDA(x, x*2)`, called as\n * `=Double(4)`). Formulas using the name recalculate. Throws when the name\n * is invalid or the formula can't be read. Undoable.\n */\n public defineName(name: string, refersTo: string, options: SpreadsheetNameScope & { comment?: string } = {}): void {\n const invalid = validateDefinedName(name);\n if (invalid) throw new Error(invalid);\n const formula = refersTo.trim().startsWith('=') ? refersTo.trim() : `=${refersTo.trim()}`;\n try {\n parseFormula(formula);\n } catch (err: any) {\n throw new Error(`There's a problem with this formula: ${err?.message ?? formula}`);\n }\n const rt = this.scopeRuntime(options);\n this.recordWorkbookSnapshot();\n const sheetId = rt?.doc.id;\n const index = this.names.findIndex((n) => n.sheetId === sheetId && sameName(n.name, name));\n const def: SpreadsheetDefinedName = {\n name,\n refersTo: formula,\n ...(sheetId !== undefined ? { sheetId } : {}),\n ...(options.comment ? { comment: options.comment } : {})\n };\n if (index >= 0) this.names[index] = def;\n else this.names.push(def);\n this.recalculateAll();\n this.notifyChange();\n }\n\n /**\n * Renames a defined name. Formulas that use it (cells, column formulas and\n * other names) follow, on the sheets where the old name meant this one.\n * Throws when the name doesn't exist or the new name is invalid or taken.\n * Undoable.\n */\n public renameName(oldName: string, newName: string, options: SpreadsheetNameScope = {}): void {\n const rt = this.scopeRuntime(options);\n const sheetId = rt?.doc.id;\n const def = this.names.find((n) => n.sheetId === sheetId && sameName(n.name, oldName));\n if (!def) throw new Error(`There is no name \"${oldName}\"${rt ? ` on sheet ${rt.doc.name}` : ''}.`);\n if (def.name === newName) return;\n const invalid = validateDefinedName(newName);\n if (invalid) throw new Error(invalid);\n if (!sameName(oldName, newName) && this.names.some((n) => n.sheetId === sheetId && sameName(n.name, newName))) {\n throw new Error('The name entered already exists. Enter a unique name.');\n }\n this.recordWorkbookSnapshot();\n // Sheets where the old name means this definition.\n const shadowedOn = new Set(this.names.filter((n) => n.sheetId !== undefined && sameName(n.name, oldName)).map((n) => n.sheetId));\n const appliesOn = (sheet: SheetRuntime | undefined) =>\n sheetId !== undefined ? sheet?.doc.id === sheetId : !sheet || !shadowedOn.has(sheet.doc.id);\n const appliesToSheetName = (sheetName: string) => appliesOn(this.findRuntimeByName(sheetName));\n for (const sheet of this.runtimes) {\n const options = { appliesTo: appliesToSheetName, qualifiedOnly: !appliesOn(sheet) };\n for (const [rowId, rowCells] of Object.entries(sheet.doc.cells)) {\n for (const [colId, cell] of Object.entries(rowCells)) {\n if (!cell.raw?.startsWith('=')) continue;\n const raw = renameNameInFormula(cell.raw, oldName, newName, options);\n if (raw !== cell.raw) sheet.doc.cells[rowId][colId] = { ...cell, raw };\n }\n }\n for (const col of sheet.doc.columns) {\n if (col.formula?.startsWith('=')) col.formula = renameNameInFormula(col.formula, oldName, newName, options);\n }\n }\n for (const n of this.names) {\n const scope = n.sheetId !== undefined ? this.findRuntimeById(n.sheetId) : undefined;\n n.refersTo = renameNameInFormula(n.refersTo, oldName, newName, {\n appliesTo: appliesToSheetName,\n qualifiedOnly: scope ? !appliesOn(scope) : sheetId !== undefined\n });\n }\n def.name = newName;\n this.recalculateAll();\n this.notifyChange();\n }\n\n /**\n * Deletes a defined name. Formulas still using it show `#NAME?`. Returns\n * false when there is no such name. Undoable.\n */\n public deleteName(name: string, options: SpreadsheetNameScope = {}): boolean {\n const rt = this.scopeRuntime(options);\n const sheetId = rt?.doc.id;\n const index = this.names.findIndex((n) => n.sheetId === sheetId && sameName(n.name, name));\n if (index < 0) return false;\n this.recordWorkbookSnapshot();\n this.names.splice(index, 1);\n this.recalculateAll();\n this.notifyChange();\n return true;\n }\n\n /**\n * Names formula autocomplete can offer on the active sheet: its\n * sheet-scoped names and the workbook names they don't hide.\n */\n public getNameSuggestions(): Array<{ name: string; description: string; isLambda: boolean }> {\n const visible = new Map<string, SpreadsheetDefinedName>();\n for (const n of this.names) {\n if (n.sheetId === this.active.doc.id) visible.set(n.name.toUpperCase(), n);\n }\n for (const n of this.names) {\n if (n.sheetId === undefined && !visible.has(n.name.toUpperCase())) visible.set(n.name.toUpperCase(), n);\n }\n return Array.from(visible.values(), (n) => ({\n name: n.name,\n description: n.comment ? `${n.comment} (${n.refersTo})` : n.refersTo,\n isLambda: isLambdaDefinition(n)\n })).sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /**\n * Evaluates a formula (e.g. `=Rate*2`, `=SUM(Sales)`) as if it were in a\n * cell of `sheet` (id or name; default the active sheet), without storing\n * it. Used by the Name Manager to show a name's current value.\n */\n public evaluateFormula(formula: string, options: { sheet?: string } = {}): FormulaEvaluationResult {\n const rt = options.sheet === undefined ? this.active : this.findRuntime(options.sheet);\n if (!rt) return { value: '#REF!', error: '#REF!', message: `There is no sheet \"${options.sheet}\".` };\n const text = formula.startsWith('=') ? formula : `=${formula}`;\n return this.withSheet(rt, () => this.evaluator.evaluateWithDiagnostics(text));\n }\n\n // --- Undo/Redo Snapshots ---\n //\n // Most steps snapshot one sheet (the one changed); sheet and name\n // operations snapshot the whole workbook. Undoing a sheet's step shows\n // that sheet.\n\n private recordSnapshot(): void {\n if (this.isApplyingHistory || this.batching) return;\n this.pushUndo({ kind: 'sheet', sheetId: this.ctx.doc.id, data: JSON.stringify(this.ctx.doc) });\n }\n\n private recordWorkbookSnapshot(): void {\n if (this.isApplyingHistory || this.batching) return;\n this.pushUndo({ kind: 'workbook', data: JSON.stringify(this.workbookSnapshot()) });\n }\n\n private pushUndo(entry: HistoryEntry): void {\n this.undoStack.push(entry);\n if (this.undoStack.length > 50) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistory();\n }\n\n /** Drops the oldest snapshots beyond `maxHistoryBytes`, always keeping the newest. */\n private trimHistory(): void {\n if (this.maxHistoryBytes <= 0 || this.undoStack.length <= 1) return;\n let total = this.getHistoryByteSize();\n while (total > this.maxHistoryBytes && this.undoStack.length > 1) {\n total -= this.undoStack.shift()!.data.length * 2;\n }\n }\n\n /** Approximate bytes held by the undo/redo snapshots (UTF-16 size). */\n public getHistoryByteSize(): number {\n let total = 0;\n for (const s of this.undoStack) total += s.data.length * 2;\n for (const s of this.redoStack) total += s.data.length * 2;\n return total;\n }\n\n /** Number of undo / redo snapshots currently held. */\n public getHistoryDepth(): { undo: number; redo: number } {\n return { undo: this.undoStack.length, redo: this.redoStack.length };\n }\n\n public undo(): void {\n this.applyHistory(this.undoStack, this.redoStack);\n }\n\n public redo(): void {\n this.applyHistory(this.redoStack, this.undoStack);\n }\n\n /** Restores the newest entry of `from`, saving the current state of the same scope on `to`. */\n private applyHistory(from: HistoryEntry[], to: HistoryEntry[]): void {\n const entry = from.pop();\n if (!entry) return;\n this.isApplyingHistory = true;\n try {\n if (entry.kind === 'sheet') {\n const rt = this.findRuntimeById(entry.sheetId);\n if (!rt) return;\n to.push({ kind: 'sheet', sheetId: rt.doc.id, data: JSON.stringify(rt.doc) });\n rt.doc = JSON.parse(entry.data);\n if (rt !== this.active) {\n this.ctx = this.active;\n this.activateRuntime(rt);\n }\n } else {\n to.push({ kind: 'workbook', data: JSON.stringify(this.workbookSnapshot()) });\n const previousActive = this.active.doc.id;\n const selection = { activeCell: this.activeCell, selectedRange: this.selectedRange };\n const saved = new Map(this.runtimes.map((rt) => [rt.doc.id, rt]));\n this.setWorkbookState(JSON.parse(entry.data), false);\n // Sheets that survive keep their saved selections.\n for (const rt of this.runtimes) {\n const old = saved.get(rt.doc.id);\n if (old) {\n rt.activeCell = old.activeCell;\n rt.selectedRange = old.selectedRange;\n }\n }\n if (this.active.doc.id === previousActive) {\n this.activeCell = selection.activeCell;\n this.selectedRange = selection.selectedRange;\n } else {\n this.activeCell = this.active.activeCell;\n this.selectedRange = this.active.selectedRange;\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n this.onActiveSheetChange?.(this.active.doc.id);\n }\n }\n this.recalculateAll();\n this.notifyChange();\n } finally {\n this.isApplyingHistory = false;\n }\n }\n\n private notifyChange(): void {\n if (!this.cfIncremental) this.cfDirty = 'all';\n this.invalidCircles = null;\n this.onDocumentChange?.(this.active.doc);\n if (this.onWorkbookChange) {\n const snapshot = this.workbookSnapshot();\n this.onWorkbookChange({ sheets: snapshot.sheets, activeSheetId: snapshot.activeSheetId, names: snapshot.names });\n }\n }\n\n // --- Conditional formatting and data validation ---\n\n /** The sheet as rule evaluation sees it. */\n private ruleSheet(): ConditionalFormatSheet & DataValidationSheet {\n return {\n rowCount: this.document.rows.length,\n colCount: this.document.columns.length,\n getValue: (row, col) => this.getCellValueAt(row, col) ?? '',\n getInput: (row, col) => {\n const r = this.document.rows[row];\n const c = this.document.columns[col];\n return r && c ? this.getCell(r.id, c.id).raw : '';\n },\n evaluateFormula: (formula: string, row: number, col: number, entered?: { row: number; col: number; value: unknown }) =>\n this.evaluateFormulaAt(formula, row, col, entered)\n };\n }\n\n /**\n * Evaluates `formula` as if it were in (row, col), without storing it:\n * relative references and `[property]` resolve from that cell. With\n * `entered`, that cell reads as the given value (input being validated).\n * Returns the value, or an array's values (column-major) for a range.\n */\n public evaluateFormulaAt(\n formula: string,\n row: number,\n col: number,\n entered?: { row: number; col: number; value: unknown }\n ): unknown {\n const saved = { rowIndex: this.evaluatingRowIndex, cell: this.evaluatingCell, override: this.valueOverride };\n this.evaluatingRowIndex = row;\n this.evaluatingCell = { col: this.document.columns[col]?.key ?? indexToColName(col), row: row + 1 };\n this.valueOverride = entered ?? null;\n try {\n const result = this.evaluator.evaluateWithDiagnostics(formula.startsWith('=') ? formula : `=${formula}`);\n return result.array ? result.array.values : result.value;\n } finally {\n this.evaluatingRowIndex = saved.rowIndex;\n this.evaluatingCell = saved.cell;\n this.valueOverride = saved.override;\n }\n }\n\n /** Records that the value of `cellKey` (and of the array it spills) may have changed. */\n private markValueChanged(cellKey: string): void {\n if (this.cfDirty === 'all') return;\n this.cfDirty.add(cellKey);\n const spill = this.spills.get(cellKey);\n if (spill) for (const cell of spill.cells) this.cfDirty.add(cell.key);\n }\n\n private generateRuleId(prefix: 'cf' | 'dv'): string {\n return `${prefix}_${Date.now()}_${(nextIdSequence++).toString(36)}`;\n }\n\n /** Moves rule ranges and rule formulas with inserted / deleted rows and columns. */\n private adjustRulesForStructuralChange(change: StructuralChange): void {\n const table = this.document.table;\n if (!table) return;\n const shiftIfFormula = (text: string | undefined) =>\n text !== undefined && text.trimStart().startsWith('=') ? shiftRuleFormula(text, change) : text;\n const shiftAnchor = (value: ConditionalFormatValue): ConditionalFormatValue =>\n value.type === 'formula' ? { ...value, formula: shiftRuleFormula(value.formula, change) } : value;\n if (table.conditionalFormats?.length) {\n table.conditionalFormats = table.conditionalFormats\n .map((rule) => {\n const next = { ...rule, ranges: adjustRangesForStructuralChange(rule.ranges, change) } as ConditionalFormatRule;\n if (next.type === 'expression') next.formula = shiftRuleFormula(next.formula, change);\n if (next.type === 'cellIs') {\n next.value = shiftIfFormula(next.value) ?? '';\n if (next.value2 !== undefined) next.value2 = shiftIfFormula(next.value2);\n }\n if (next.type === 'colorScale') next.stops = next.stops.map((s) => ({ ...s, value: shiftAnchor(s.value) }));\n if (next.type === 'dataBar') {\n if (next.min) next.min = shiftAnchor(next.min);\n if (next.max) next.max = shiftAnchor(next.max);\n }\n if (next.type === 'iconSet' && next.thresholds) next.thresholds = next.thresholds.map(shiftAnchor);\n return next;\n })\n .filter((rule) => rule.ranges.length > 0);\n }\n if (table.dataValidations?.length) {\n table.dataValidations = table.dataValidations\n .map((rule) => {\n const next: DataValidationRule = { ...rule, ranges: adjustRangesForStructuralChange(rule.ranges, change) };\n if (rule.formula1 !== undefined) next.formula1 = shiftIfFormula(rule.formula1);\n if (rule.formula2 !== undefined) next.formula2 = shiftIfFormula(rule.formula2);\n return next;\n })\n .filter((rule) => rule.ranges.length > 0);\n }\n }\n\n /** Conditional formats of the sheet, highest priority first. */\n public getConditionalFormats(): ConditionalFormatRule[] {\n return this.document.table?.conditionalFormats ?? [];\n }\n\n /**\n * Adds a conditional format (one undo step). New rules get the highest\n * priority, as in Excel; pass `priority: 'last'` to add it below the\n * others. Returns the rule id.\n */\n public addConditionalFormat(rule: NewConditionalFormatRule, options: { priority?: 'first' | 'last' } = {}): string {\n const id = rule.id || this.generateRuleId('cf');\n const stored = { ...rule, id, ranges: rule.ranges.map((r) => normalizeRange(r)) } as ConditionalFormatRule;\n this.applyTableChange(() => {\n const rules = [...(this.table.conditionalFormats ?? [])];\n if (options.priority === 'last') rules.push(stored);\n else rules.unshift(stored);\n this.table.conditionalFormats = rules;\n });\n return id;\n }\n\n /** Changes a conditional format (its condition, ranges or stop-if-true). Returns false when not found. */\n public updateConditionalFormat(id: string, changes: Partial<NewConditionalFormatRule>): boolean {\n const rules = this.document.table?.conditionalFormats ?? [];\n const index = rules.findIndex((r) => r.id === id);\n if (index === -1) return false;\n this.applyTableChange(() => {\n const next = [...rules];\n const updated = { ...rules[index], ...changes, id } as ConditionalFormatRule;\n if (changes.ranges) updated.ranges = changes.ranges.map((r) => normalizeRange(r));\n next[index] = updated;\n this.table.conditionalFormats = next;\n });\n return true;\n }\n\n /** Removes a conditional format. Returns false when not found. */\n public deleteConditionalFormat(id: string): boolean {\n const rules = this.document.table?.conditionalFormats ?? [];\n if (!rules.some((r) => r.id === id)) return false;\n this.applyTableChange(() => {\n this.table.conditionalFormats = rules.filter((r) => r.id !== id);\n });\n return true;\n }\n\n /**\n * Moves a conditional format to priority `to` (0 = highest), or by\n * `{ by: -1 }` (up) / `{ by: 1 }` (down). Returns false when nothing moved.\n */\n public moveConditionalFormat(id: string, to: number | { by: number }): boolean {\n const rules = this.document.table?.conditionalFormats ?? [];\n const from = rules.findIndex((r) => r.id === id);\n if (from === -1) return false;\n const target = Math.max(0, Math.min(rules.length - 1, typeof to === 'number' ? to : from + to.by));\n if (target === from) return false;\n this.applyTableChange(() => {\n const next = [...rules];\n const [moved] = next.splice(from, 1);\n next.splice(target, 0, moved);\n this.table.conditionalFormats = next;\n });\n return true;\n }\n\n /**\n * Removes conditional formatting from the cells of `range` (rules keep\n * their other cells; rules left without cells are deleted), or every rule\n * when `range` is null.\n */\n public clearConditionalFormats(range: CellRange | null = null): void {\n const rules = this.document.table?.conditionalFormats ?? [];\n if (rules.length === 0) return;\n this.applyTableChange(() => {\n this.table.conditionalFormats = range\n ? rules\n .map((rule) => ({ ...rule, ranges: subtractRangeFromRanges(rule.ranges, range) }) as ConditionalFormatRule)\n .filter((rule) => rule.ranges.length > 0)\n : [];\n });\n }\n\n /**\n * The conditional look of every formatted cell. Evaluated on demand and\n * cached: after a cell edit only the affected cells are re-evaluated.\n */\n public getConditionalFormatOverlay(): ConditionalFormatOverlay {\n const rules = this.document.table?.conditionalFormats ?? [];\n if (rules.length === 0) {\n this.cfEngine = null;\n this.cfDirty = 'all';\n return EMPTY_CONDITIONAL_FORMAT_OVERLAY;\n }\n const sheet = this.ruleSheet();\n if (!this.cfEngine || this.cfDirty === 'all') {\n this.cfEngine = new ConditionalFormatEngine(sheet);\n this.cfEngine.evaluate(rules);\n } else if (this.cfDirty.size > 0) {\n const changed: Array<{ row: number; col: number }> = [];\n for (const key of this.cfDirty) {\n const located = this.locateCellKey(key);\n if (located) changed.push({ row: located.row.index, col: this.document.columns.indexOf(located.col) });\n }\n this.cfEngine.setSheet(sheet);\n this.cfEngine.update(rules, changed);\n }\n this.cfDirty = new Set();\n return this.cfEngine.overlay;\n }\n\n /** The conditional look of (row, col), if any. */\n public getConditionalFormat(row: number, col: number): ConditionalCellFormat | undefined {\n return this.getConditionalFormatOverlay().get(row, col);\n }\n\n /** Data validation rules of the sheet. */\n public getDataValidations(): DataValidationRule[] {\n return this.document.table?.dataValidations ?? [];\n }\n\n /** The rule that applies to (row, col), if any. */\n public getDataValidationAt(row: number, col: number): DataValidationRule | undefined {\n return findDataValidation(this.document.table?.dataValidations, row, col);\n }\n\n /**\n * Adds a data validation rule (one undo step). Its cells are first\n * removed from other rules, so each cell has at most one. Returns the id.\n */\n public addDataValidation(rule: NewDataValidationRule): string {\n const id = rule.id || this.generateRuleId('dv');\n const ranges = rule.ranges.map((r) => normalizeRange(r));\n this.applyTableChange(() => {\n let rules = this.table.dataValidations ?? [];\n for (const cut of ranges) {\n rules = rules\n .map((r) => ({ ...r, ranges: subtractRangeFromRanges(r.ranges, cut) }))\n .filter((r) => r.ranges.length > 0);\n }\n this.table.dataValidations = [...rules, { ...rule, id, ranges }];\n });\n return id;\n }\n\n /** Changes a data validation rule. Returns false when not found. */\n public updateDataValidation(id: string, changes: Partial<NewDataValidationRule>): boolean {\n const rules = this.document.table?.dataValidations ?? [];\n const index = rules.findIndex((r) => r.id === id);\n if (index === -1) return false;\n this.applyTableChange(() => {\n const next = [...rules];\n const updated: DataValidationRule = { ...rules[index], ...changes, id };\n if (changes.ranges) updated.ranges = changes.ranges.map((r) => normalizeRange(r));\n next[index] = updated;\n this.table.dataValidations = next;\n });\n return true;\n }\n\n /** Removes a data validation rule. Returns false when not found. */\n public deleteDataValidation(id: string): boolean {\n const rules = this.document.table?.dataValidations ?? [];\n if (!rules.some((r) => r.id === id)) return false;\n this.applyTableChange(() => {\n this.table.dataValidations = rules.filter((r) => r.id !== id);\n });\n return true;\n }\n\n /** Removes validation from the cells of `range`, or every rule when `range` is null. */\n public clearDataValidation(range: CellRange | null = null): void {\n const rules = this.document.table?.dataValidations ?? [];\n if (rules.length === 0) return;\n this.applyTableChange(() => {\n this.table.dataValidations = range\n ? rules.map((r) => ({ ...r, ranges: subtractRangeFromRanges(r.ranges, range) })).filter((r) => r.ranges.length > 0)\n : [];\n });\n }\n\n /** Checks input for (row, col) against its data validation, without committing it. */\n public validateCellInput(row: number, col: number, raw: string): DataValidationResult {\n return validateCellInput(this.ruleSheet(), this.document.table?.dataValidations, row, col, raw);\n }\n\n /**\n * Dropdown items of a list-validated cell, or null when the cell has no\n * list rule (or its in-cell dropdown is off).\n */\n public getValidationListValues(row: number, col: number): string[] | null {\n const rule = this.getDataValidationAt(row, col);\n if (!rule || rule.type !== 'list' || rule.inCellDropdown === false) return null;\n return getValidationListValues(this.ruleSheet(), rule);\n }\n\n /** Cells whose current value breaks their validation rule. */\n public findInvalidCells(): CellPosition[] {\n return findInvalidCells(this.ruleSheet(), this.document.table?.dataValidations);\n }\n\n /**\n * Turns \"circle invalid data\" on or off. View state, not part of the\n * document (not undoable, not saved); listeners are notified so the grid\n * redraws.\n */\n public setInvalidDataCircles(show: boolean): void {\n if (this.circlesShown === show) return;\n this.circlesShown = show;\n this.invalidCircles = null;\n this.onDocumentChange?.(this.document);\n }\n\n /** Whether invalid cells are circled. */\n public getInvalidDataCircles(): boolean {\n return this.circlesShown;\n }\n\n /** Whether (row, col) is circled as invalid (always false while circles are off). */\n public isInvalidDataCircled(row: number, col: number): boolean {\n if (!this.circlesShown) return false;\n if (!this.invalidCircles) {\n this.invalidCircles = new Set(this.findInvalidCells().map((c) => `${c.row}:${c.col}`));\n }\n return this.invalidCircles.has(`${row}:${col}`);\n }\n}\n\n/**\n * The index after (`delta` = 1) or before (-1) `from` in a display order,\n * staying put at the edges. When `from` is not displayed (hidden), moves to\n * the nearest displayed index in that direction.\n */\nfunction stepInOrder(order: number[], from: number, delta: 1 | -1): number {\n if (order.length === 0) return from;\n const pos = order.indexOf(from);\n if (pos === -1) {\n const candidates = order.filter((i) => (delta > 0 ? i > from : i < from));\n if (candidates.length === 0) return from;\n return candidates.reduce((best, i) => (delta > 0 ? Math.min(best, i) : Math.max(best, i)));\n }\n return order[Math.max(0, Math.min(order.length - 1, pos + delta))];\n}\n","/**\n * The spreadsheet's table context menu, defined once so the React and Svelte\n * editors show the same actions. Frameworks render the items and call\n * `run()`; the controller's change listeners refresh their state.\n */\nimport type { SpreadsheetController } from './controller';\nimport type { CellRange } from './types';\nimport type { CellPosition } from './formula';\nimport { formatCellDisplay, indexToColName } from './formula';\nimport { isSingleCellRange, normalizeRange, rangeContains, rangesIntersect } from './table';\nimport { NUMBER_FORMAT_PRESETS, isGeneralNumberFormat } from './numfmt/presets';\nimport { createConditionalFormatRule, type ConditionalFormatPresetInput } from './conditional-format/presets';\n\nexport interface SpreadsheetMenuItem {\n id: string;\n label: string;\n disabled?: boolean;\n /** Draw a divider before this item. */\n separatorBefore?: boolean;\n /** The item is the current choice of its group (e.g. the cell's number format). */\n checked?: boolean;\n /**\n * Ask for text before running (e.g. a custom number format code): the\n * editor prompts with `label` and `defaultValue`, then calls `run(text)`;\n * cancelling skips `run`.\n */\n input?: { label: string; defaultValue?: string };\n /** Ask the user to confirm before running (e.g. deleting a sheet); the message to show. */\n confirm?: string;\n /**\n * Opens one of the editor's panels instead of running an action: the\n * editors open it (`run` does nothing).\n */\n panel?: SpreadsheetPanelId;\n run: (input?: string) => void;\n}\n\n/** Panels the spreadsheet editors can open from the context menu. */\nexport type SpreadsheetPanelId = 'conditionalFormatting' | 'dataValidation';\n\n/**\n * Runs a menu item the way the editors do: an item with `confirm` first asks\n * for confirmation with `confirm` (default `window.confirm`), an item with\n * `input` asks for text with `ask` (default `window.prompt`); either is\n * skipped when cancelled. Returns whether `run` was called.\n */\nexport function runSpreadsheetMenuItem(\n item: SpreadsheetMenuItem,\n ask?: (label: string, defaultValue: string) => string | null | undefined,\n confirm?: (message: string) => boolean\n): boolean {\n if (item.confirm) {\n const confirmed = confirm\n ? confirm(item.confirm)\n : typeof window !== 'undefined' && typeof window.confirm === 'function'\n ? window.confirm(item.confirm)\n : true;\n if (!confirmed) return false;\n }\n if (!item.input) {\n item.run();\n return true;\n }\n const prompt =\n ask ?? ((label: string, value: string) => (typeof window !== 'undefined' && window.prompt ? window.prompt(label, value) : null));\n const text = prompt(item.input.label, item.input.defaultValue ?? '');\n if (text === null || text === undefined) return false;\n item.run(text);\n return true;\n}\n\n/**\n * Where to place a `position: fixed` menu opened at `point` so it stays inside\n * the viewport: it flips to the left of / above the pointer when it would\n * overflow the right / bottom edge, or, when the flipped menu would not fit\n * either, slides back just enough to fit. It is kept at least `margin` px from\n * every edge (a menu taller than the viewport is pinned to the top and should\n * scroll, e.g. with `max-height: calc(100vh - 2 * margin)`).\n */\nexport function clampMenuPosition(\n point: { x: number; y: number },\n size: { width: number; height: number },\n viewport: { width: number; height: number },\n margin = 4\n): { x: number; y: number } {\n const place = (start: number, length: number, limit: number) => {\n if (start + length <= limit - margin) return Math.max(margin, start);\n const flipped = start - length;\n // Flip when that fits; otherwise slide it back just enough to fit.\n return Math.max(margin, flipped >= margin ? flipped : limit - margin - length);\n };\n return {\n x: place(point.x, size.width, viewport.width),\n y: place(point.y, size.height, viewport.height)\n };\n}\n\nconst quote = (value: unknown) => {\n const text = String(value ?? '');\n return text.length > 24 ? `\"${text.slice(0, 23)}…\"` : `\"${text}\"`;\n};\n\n/**\n * Menu items for a right-click on `cell`. When the cell is inside the\n * selection, range actions (hide, merge, transpose) apply to the selection;\n * otherwise to the clicked cell.\n */\nexport function getSpreadsheetMenuItems(controller: SpreadsheetController, cell: CellPosition): SpreadsheetMenuItem[] {\n const doc = controller.document;\n const column = doc.columns[cell.col];\n const row = doc.rows[cell.row];\n if (!column || !row) return [];\n\n const selection: CellRange =\n controller.selectedRange && rangeContains(controller.selectedRange, cell.row, cell.col)\n ? normalizeRange(controller.selectedRange)\n : { startRow: cell.row, startCol: cell.col, endRow: cell.row, endCol: cell.col };\n const colName = indexToColName(cell.col);\n const cellData = controller.getCell(row.id, column.id);\n const value = cellData.value;\n const table = doc.table ?? {};\n const filter = controller.getFilterState();\n const grouping = table.grouping;\n const rows = Array.from({ length: selection.endRow - selection.startRow + 1 }, (_, i) => selection.startRow + i);\n const cols = Array.from({ length: selection.endCol - selection.startCol + 1 }, (_, i) => selection.startCol + i);\n const anyHidden = doc.rows.some((r) => r.hidden) || doc.columns.some((c) => c.hidden);\n const hasMerge = controller.getMerges().some((m) => rangesIntersect(m, selection));\n const sortRange = isSingleCellRange(selection) ? null : selection;\n\n const items: SpreadsheetMenuItem[] = [\n {\n id: 'sort-asc',\n label: `Sort ascending by column ${colName}`,\n run: () => controller.sortRows([{ colId: column.id, direction: 'asc' }], { range: sortRange }),\n },\n {\n id: 'sort-desc',\n label: `Sort descending by column ${colName}`,\n run: () => controller.sortRows([{ colId: column.id, direction: 'desc' }], { range: sortRange }),\n },\n {\n id: 'filter-equals',\n label: `Show only rows where ${colName} = ${quote(formatCellDisplay(cellData))}`,\n separatorBefore: true,\n run: () => controller.addFilterRule({ colId: column.id, operator: 'equals', value: String(value ?? '') }),\n },\n {\n id: 'filter-toggle',\n label: filter.enabled ? 'Turn filters off' : 'Turn filters on',\n disabled: filter.rules.length === 0,\n run: () => controller.toggleFilters(),\n },\n {\n id: 'filter-clear',\n label: 'Clear filters',\n disabled: filter.rules.length === 0,\n run: () => controller.clearFilters(),\n },\n {\n id: 'freeze-rows',\n label: `Freeze rows 1–${cell.row + 1}`,\n separatorBefore: true,\n run: () => controller.setFrozenRowCount(cell.row + 1),\n },\n {\n id: 'freeze-columns',\n label: `Freeze columns A–${colName}`,\n run: () => controller.setFrozenColumnCount(cell.col + 1),\n },\n {\n id: 'unfreeze',\n label: 'Unfreeze rows and columns',\n disabled: !table.frozenRowCount && !table.frozenColumnCount,\n run: () => {\n controller.setFrozenRowCount(0);\n controller.setFrozenColumnCount(0);\n },\n },\n {\n id: 'header-rows',\n label: `Use rows 1–${cell.row + 1} as header rows`,\n run: () => controller.setHeaderRowCount(cell.row + 1),\n },\n {\n id: 'footer-rows',\n label: `Use rows ${cell.row + 1}–${doc.rows.length} as footer rows`,\n run: () => controller.setFooterRowCount(doc.rows.length - cell.row),\n },\n {\n id: 'hide-rows',\n label: rows.length > 1 ? `Hide ${rows.length} rows` : `Hide row ${cell.row + 1}`,\n separatorBefore: true,\n run: () => controller.hideRows(rows),\n },\n {\n id: 'hide-columns',\n label: cols.length > 1 ? `Hide ${cols.length} columns` : `Hide column ${colName}`,\n run: () => controller.hideColumns(cols),\n },\n {\n id: 'unhide-all',\n label: 'Unhide all rows and columns',\n disabled: !anyHidden,\n run: () => controller.unhideAll(),\n },\n {\n id: 'merge',\n label: 'Merge cells',\n separatorBefore: true,\n disabled: isSingleCellRange(selection),\n run: () => {\n controller.mergeCells(selection);\n },\n },\n {\n id: 'unmerge',\n label: 'Unmerge cells',\n disabled: !hasMerge,\n run: () => {\n controller.unmergeCells(selection);\n },\n },\n {\n id: 'transpose',\n label: 'Transpose',\n disabled: isSingleCellRange(selection),\n run: () => {\n controller.transposeRange(selection);\n },\n },\n ];\n\n // Format cells: number format presets and a custom code, for the selection.\n const currentFormat = cellData.numberFormat;\n const presetMatch = NUMBER_FORMAT_PRESETS.find((p) =>\n p.id === 'general' ? isGeneralNumberFormat(currentFormat) : p.code === currentFormat\n );\n NUMBER_FORMAT_PRESETS.forEach((preset, i) => {\n items.push({\n id: `number-format-${preset.id}`,\n label: `Format cells: ${preset.label}`,\n separatorBefore: i === 0,\n checked: presetMatch === preset,\n run: () => {\n controller.setCellNumberFormat(selection, preset.code);\n },\n });\n });\n items.push({\n id: 'number-format-custom',\n label: 'Format cells: Custom…',\n checked: !presetMatch,\n input: { label: 'Number format code, e.g. #,##0.00;[Red](#,##0.00)', defaultValue: currentFormat ?? 'General' },\n run: (code) => {\n if (code === undefined) return;\n controller.setCellNumberFormat(selection, code.trim() === '' ? null : code);\n },\n });\n\n if (grouping) {\n items.push(\n {\n id: 'ungroup',\n label: 'Remove categories',\n separatorBefore: true,\n run: () => controller.ungroupRows(),\n },\n ...(['sum', 'count', 'average'] as const).map((aggregate) => ({\n id: `summary-${aggregate}`,\n label: `Summarize column ${colName}: ${aggregate.toUpperCase()}`,\n run: () => controller.setGroupSummary(column.id, aggregate),\n })),\n );\n } else {\n items.push({\n id: 'group-by',\n label: `Categorize by column ${colName}`,\n separatorBefore: true,\n run: () => controller.groupRowsBy(column.id),\n });\n }\n\n items.push(...getRuleMenuItems(controller, selection));\n\n return items;\n}\n\n/** Conditional formatting and data validation items for the selection. */\nfunction getRuleMenuItems(controller: SpreadsheetController, selection: CellRange): SpreadsheetMenuItem[] {\n const ranges = [selection];\n const hasFormats = controller.getConditionalFormats().some((rule) => rule.ranges.some((r) => rangesIntersect(r, selection)));\n const hasValidation = controller.getDataValidations().some((rule) => rule.ranges.some((r) => rangesIntersect(r, selection)));\n const add = (input: Omit<ConditionalFormatPresetInput, 'ranges'>) => {\n controller.addConditionalFormat(createConditionalFormatRule({ ...input, ranges }));\n };\n const circles = controller.getInvalidDataCircles();\n return [\n {\n id: 'conditional-format-manage',\n label: 'Conditional formatting…',\n separatorBefore: true,\n panel: 'conditionalFormatting',\n run: () => {},\n },\n {\n id: 'conditional-format-greater-than',\n label: 'Conditional formatting: Highlight cells greater than…',\n input: { label: 'Highlight cells greater than', defaultValue: '0' },\n run: (value) => {\n if (value !== undefined && value.trim() !== '') add({ kind: 'greaterThan', value: value.trim() });\n },\n },\n {\n id: 'conditional-format-duplicates',\n label: 'Conditional formatting: Highlight duplicate values',\n run: () => add({ kind: 'duplicate' }),\n },\n {\n id: 'conditional-format-color-scale',\n label: 'Conditional formatting: Color scale',\n run: () => add({ kind: 'colorScale3' }),\n },\n {\n id: 'conditional-format-data-bars',\n label: 'Conditional formatting: Data bars',\n run: () => add({ kind: 'dataBar' }),\n },\n {\n id: 'conditional-format-icon-set',\n label: 'Conditional formatting: Icon set',\n run: () => add({ kind: 'iconSet', iconSet: '3Arrows' }),\n },\n {\n id: 'conditional-format-clear',\n label: 'Clear conditional formatting from selection',\n disabled: !hasFormats,\n run: () => controller.clearConditionalFormats(selection),\n },\n {\n id: 'data-validation-manage',\n label: 'Data validation…',\n separatorBefore: true,\n panel: 'dataValidation',\n run: () => {},\n },\n {\n id: 'data-validation-list',\n label: 'Data validation: List of values…',\n input: { label: 'Allowed values, separated by commas (or a range such as =$H$1:$H$5)', defaultValue: '' },\n run: (value) => {\n if (value === undefined || value.trim() === '') return;\n controller.addDataValidation({ ranges, type: 'list', formula1: value.trim() });\n },\n },\n {\n id: 'data-validation-clear',\n label: 'Clear data validation from selection',\n disabled: !hasValidation,\n run: () => controller.clearDataValidation(selection),\n },\n {\n id: 'data-validation-circles',\n label: circles ? 'Clear validation circles' : 'Circle invalid data',\n disabled: !circles && controller.getDataValidations().length === 0,\n run: () => controller.setInvalidDataCircles(!circles),\n },\n ];\n}\n","import type { SpreadsheetController } from './controller';\n\n/**\n * Controller methods the React and Svelte spreadsheet bindings expose as-is,\n * so both frameworks offer the same table operations. They need no extra\n * state syncing: the controller reports changes through `onDocumentChange`\n * and `onSelectionChange`.\n */\nexport const SPREADSHEET_TABLE_ACTIONS = [\n 'sortRows',\n 'getFilterState',\n 'setFilterRules',\n 'addFilterRule',\n 'updateFilterRule',\n 'removeFilterRule',\n 'clearFilters',\n 'setFiltersEnabled',\n 'toggleFilters',\n 'setHeaderRowCount',\n 'setFooterRowCount',\n 'setHeaderColumnCount',\n 'setFrozenRowCount',\n 'setFrozenColumnCount',\n 'hideRows',\n 'unhideRows',\n 'hideColumns',\n 'unhideColumns',\n 'unhideAll',\n 'getMerges',\n 'getMergeAt',\n 'mergeCells',\n 'unmergeCells',\n 'autofill',\n 'groupRowsBy',\n 'ungroupRows',\n 'setGroupSummary',\n 'setGroupCollapsed',\n 'toggleGroupCollapsed',\n 'getGroups',\n 'transposeRange',\n 'setCellNumberFormat',\n 'setColumnNumberFormat',\n 'exportToCsv',\n 'importFromCsv',\n 'copyToClipboardData',\n 'pasteClipboardData',\n // Conditional formatting and data validation\n 'getConditionalFormats',\n 'addConditionalFormat',\n 'updateConditionalFormat',\n 'deleteConditionalFormat',\n 'moveConditionalFormat',\n 'clearConditionalFormats',\n 'getConditionalFormatOverlay',\n 'getConditionalFormat',\n 'getDataValidations',\n 'getDataValidationAt',\n 'addDataValidation',\n 'updateDataValidation',\n 'deleteDataValidation',\n 'clearDataValidation',\n 'validateCellInput',\n 'getValidationListValues',\n 'findInvalidCells',\n 'setInvalidDataCircles',\n 'getInvalidDataCircles',\n 'isInvalidDataCircled'\n] as const;\n\nexport type SpreadsheetTableActionName = (typeof SPREADSHEET_TABLE_ACTIONS)[number];\nexport type SpreadsheetTableActions = Pick<SpreadsheetController, SpreadsheetTableActionName>;\n\n/** The table operations of `controller`, bound so they can be passed around. */\nexport function bindSpreadsheetTableActions(controller: SpreadsheetController): SpreadsheetTableActions {\n const actions = {} as Record<string, unknown>;\n for (const name of SPREADSHEET_TABLE_ACTIONS) {\n actions[name] = (controller[name] as (...args: unknown[]) => unknown).bind(controller);\n }\n return actions as SpreadsheetTableActions;\n}\n","/**\n * `spreadsheet` import adapters for the import registry: CSV and TSV files\n * parsed with the RFC 4180 parser in `csv.ts`.\n *\n * They are not registered by default (the default registry only knows\n * `document` adapters). Registries are passed in explicitly so this entry\n * point does not pull in the document adapters (markdown, TipTap):\n *\n * registerSpreadsheetImportAdapters(defaultImportRegistry);\n * await importFileIntoSpreadsheet(controller, file, { registry: defaultImportRegistry });\n */\nimport type {\n ImportAdapter,\n ImportAdapterRegistry,\n ImportInput,\n ImportOutcome,\n} from '../import/registry';\nimport type { SpreadsheetController } from './controller';\nimport { parseCsv } from './csv';\n\n/** Result of every `spreadsheet` adapter. */\nexport interface SpreadsheetImport {\n /** Parsed records, one array of raw cell inputs per row. */\n rows: string[][];\n /** Field separator the file was parsed with. */\n delimiter: string;\n}\n\nexport type SpreadsheetImportAdapter = ImportAdapter<SpreadsheetImport>;\n\nfunction delimitedAdapter(\n id: string,\n label: string,\n delimiter: string,\n mimeTypes: string[],\n extensions: string[],\n): SpreadsheetImportAdapter {\n return {\n id,\n label,\n mimeTypes,\n extensions,\n target: 'spreadsheet',\n async import(_input, context) {\n return { rows: parseCsv(await context.readText(), { delimiter }), delimiter };\n },\n };\n}\n\nexport const csvImportAdapter = delimitedAdapter(\n 'csv',\n 'CSV',\n ',',\n ['text/csv', 'application/csv', 'text/comma-separated-values'],\n ['.csv'],\n);\n\nexport const tsvImportAdapter = delimitedAdapter(\n 'tsv',\n 'Tab-separated values',\n '\\t',\n ['text/tab-separated-values'],\n ['.tsv', '.tab'],\n);\n\nexport const SPREADSHEET_IMPORT_ADAPTERS: readonly SpreadsheetImportAdapter[] = [\n csvImportAdapter,\n tsvImportAdapter,\n];\n\n/** Registers the CSV and TSV adapters; returns a function that unregisters them. */\nexport function registerSpreadsheetImportAdapters(registry: ImportAdapterRegistry): () => void {\n const unregisters = SPREADSHEET_IMPORT_ADAPTERS.map((adapter) =>\n registry.register(adapter, { replace: true }),\n );\n return () => unregisters.forEach((unregister) => unregister());\n}\n\nexport interface ImportFileIntoSpreadsheetOptions {\n /** Registry to resolve the adapter from (e.g. `defaultImportRegistry`). */\n registry: ImportAdapterRegistry;\n /** Force an adapter instead of matching by file name / MIME type. */\n adapterId?: string;\n fileName?: string;\n mimeType?: string;\n /** Top-left cell to write to (0-based). Default A1. */\n startRow?: number;\n startCol?: number;\n /** Sheet (id or name) to import into; default the active sheet. */\n sheet?: string;\n}\n\n/**\n * Imports a CSV/TSV file (or any registered `spreadsheet` adapter that\n * returns `SpreadsheetImport`) into the controller as one undoable change.\n */\nexport async function importFileIntoSpreadsheet(\n controller: SpreadsheetController,\n input: ImportInput,\n options: ImportFileIntoSpreadsheetOptions,\n): Promise<ImportOutcome<SpreadsheetImport>> {\n const { registry, startRow, startCol, sheet, ...rest } = options;\n const outcome = await registry.importFile<SpreadsheetImport>(input, { ...rest, target: 'spreadsheet' });\n controller.importRows(outcome.result.rows, { startRow, startCol, sheet });\n return outcome;\n}\n","/**\n * Sheet-tab and Name Manager behaviour shared by the React and Svelte\n * bindings: the tab context menu, drag-to-reorder targets and the rows a\n * Name Manager panel lists.\n */\nimport type { SpreadsheetController } from './controller';\nimport type { SpreadsheetMenuItem } from './menu';\nimport { formatCellDisplay } from './formula/format';\nimport { isFormulaError } from './formula';\n\n/**\n * Context-menu items for a sheet tab: insert, rename (asks for the name),\n * duplicate, move left/right and delete (asks to confirm; disabled for the\n * last sheet). Run them with `runSpreadsheetMenuItem`. A rename to an\n * invalid name throws; pass `onError` to report it instead.\n */\nexport function getSheetTabMenuItems(\n controller: SpreadsheetController,\n sheetId: string,\n options: { onError?: (message: string) => void } = {},\n): SpreadsheetMenuItem[] {\n const sheets = controller.getSheets();\n const sheet = sheets.find((s) => s.id === sheetId);\n if (!sheet) return [];\n const guard = (fn: () => void) => () => {\n try {\n fn();\n } catch (err: any) {\n if (!options.onError) throw err;\n options.onError(err?.message ?? String(err));\n }\n };\n return [\n {\n id: 'sheet-insert',\n label: 'Insert sheet',\n run: guard(() => {\n controller.addSheet({ index: sheet.index + 1 });\n }),\n },\n {\n id: 'sheet-rename',\n label: 'Rename…',\n input: { label: 'Sheet name', defaultValue: sheet.name },\n run: (name) =>\n guard(() => {\n const next = (name ?? '').trim();\n if (next && next !== sheet.name) controller.renameSheet(sheetId, next);\n })(),\n },\n {\n id: 'sheet-duplicate',\n label: 'Duplicate',\n run: guard(() => {\n controller.duplicateSheet(sheetId);\n }),\n },\n {\n id: 'sheet-move-left',\n label: 'Move left',\n disabled: sheet.index === 0,\n separatorBefore: true,\n run: () => {\n controller.moveSheet(sheetId, sheet.index - 1);\n },\n },\n {\n id: 'sheet-move-right',\n label: 'Move right',\n disabled: sheet.index === sheets.length - 1,\n run: () => {\n controller.moveSheet(sheetId, sheet.index + 1);\n },\n },\n {\n id: 'sheet-delete',\n label: 'Delete',\n disabled: sheets.length <= 1,\n separatorBefore: true,\n confirm: `Delete the sheet \"${sheet.name}\"? Formulas that refer to it will show #REF!.`,\n run: () => {\n controller.deleteSheet(sheetId);\n },\n },\n ];\n}\n\n/**\n * The tab index a dragged tab should move to when dropped on the tab at\n * `targetIndex`: dropping on the left half of a tab places it before that\n * tab, on the right half after it.\n */\nexport function sheetDropIndex(fromIndex: number, targetIndex: number, dropAfter: boolean): number {\n let to = dropAfter ? targetIndex + 1 : targetIndex;\n if (fromIndex < to) to--;\n return to;\n}\n\n/** A row of a Name Manager panel. */\nexport interface SpreadsheetNameRow {\n name: string;\n refersTo: string;\n /** `Workbook` or the sheet's name. */\n scopeLabel: string;\n /** Sheet id for a sheet-scoped name. */\n sheetId?: string;\n /** The name's current value, as displayed (`#REF!`, `[LAMBDA]`, `{3×1 array}`). */\n value: string;\n comment?: string;\n isLambda: boolean;\n}\n\n/** Every defined name with its scope and current value, sorted by name then scope. */\nexport function getNameManagerRows(controller: SpreadsheetController): SpreadsheetNameRow[] {\n return controller\n .getNames()\n .map((n) => {\n let value: string;\n if (n.isLambda) {\n value = '[LAMBDA]';\n } else {\n const result = controller.evaluateFormula(`=${n.name}`, n.sheetId ? { sheet: n.sheetId } : {});\n value = result.array\n ? `{${result.array.rows}×${result.array.cols} array}`\n : isFormulaError(result.value)\n ? String(result.value)\n : formatCellDisplay({ value: result.value });\n }\n return {\n name: n.name,\n refersTo: n.refersTo,\n scopeLabel: n.sheetName ?? 'Workbook',\n ...(n.sheetId ? { sheetId: n.sheetId } : {}),\n value,\n ...(n.comment ? { comment: n.comment } : {}),\n isLambda: n.isLambda,\n };\n })\n .sort((a, b) => a.name.localeCompare(b.name) || a.scopeLabel.localeCompare(b.scopeLabel));\n}\n"]}