@veltra/sheet 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -25
- package/dist/index.js +3 -22
- package/dist/tools/builtin.js +279 -8
- package/dist/tools/context.d.ts +54 -11
- package/dist/tools/context.js +28 -5
- package/dist/tools/download.js +68 -0
- package/dist/tools/export.worker.d.ts +16 -0
- package/dist/tools/export.worker.js +29 -0
- package/dist/tools/registry.d.ts +7 -4
- package/dist/tools/registry.js +1 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/sheet.d.ts +6 -5
- package/dist/vue/formula-bar.js +393 -0
- package/dist/vue/formula-suggest-list.js +66 -0
- package/dist/vue/import-file.js +140 -0
- package/dist/vue/insert-count-menu-item.js +68 -0
- package/dist/vue/insert-image.js +79 -0
- package/dist/vue/popup-helpers.js +73 -0
- package/dist/vue/popups/border-popup.js +75 -0
- package/dist/vue/popups/border-preset-glyph.js +106 -0
- package/dist/vue/popups/export-popup.js +51 -0
- package/dist/vue/popups/fill-color-popup.js +37 -0
- package/dist/vue/popups/find-popup.js +113 -0
- package/dist/vue/popups/font-color-popup.js +37 -0
- package/dist/vue/popups/font-size-popup.js +63 -0
- package/dist/vue/popups/import.worker.d.ts +21 -0
- package/dist/vue/popups/import.worker.js +41 -0
- package/dist/vue/popups/insert-image-popup.js +38 -0
- package/dist/vue/sheet-context-menu.js +200 -0
- package/dist/vue/sheet-tabs.js +162 -0
- package/dist/vue/sheet-toolbar.js +106 -0
- package/dist/vue/sheet.js +144 -138
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +11 -0
- package/dist/vue/style2.css +564 -3
- package/dist/vue/use-find-replace.js +161 -0
- package/dist/vue/use-formula-ref-select.js +37 -0
- package/dist/vue/use-formula-suggest.js +126 -0
- package/dist/vue/use-sheet-grid.js +180 -0
- package/dist/vue/use-sheet-state.js +122 -0
- package/dist/vue/use-sheet-tabs-bar.js +90 -0
- package/dist/vue/use-tool-groups.js +42 -0
- package/dist/vue/use-tool-popup.js +123 -0
- package/dist/vue/use-toolbar-scroll.js +72 -0
- package/package.json +12 -6
- package/src/env.d.ts +14 -0
- package/src/index.ts +203 -13
- package/src/tools/__test__/builtin.test.ts +208 -66
- package/src/tools/__test__/context.test.ts +62 -2
- package/src/tools/builtin.ts +350 -6
- package/src/tools/context.ts +89 -11
- package/src/tools/download.ts +82 -0
- package/src/tools/export.worker.ts +61 -0
- package/src/tools/registry.ts +16 -1
- package/src/types/index.ts +6 -0
- package/src/types/sheet.ts +5 -3
- package/src/vue/__test__/formula-bar.test.ts +455 -0
- package/src/vue/__test__/import-file.test.ts +183 -0
- package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
- package/src/vue/__test__/insert-image-popup.test.ts +89 -0
- package/src/vue/__test__/insert-image.test.ts +100 -0
- package/src/vue/__test__/sheet-component.test.ts +716 -27
- package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
- package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
- package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
- package/src/vue/formula-bar.vue +473 -0
- package/src/vue/formula-suggest-list.vue +64 -0
- package/src/vue/import-file.ts +173 -0
- package/src/vue/insert-count-menu-item.vue +69 -0
- package/src/vue/insert-image.ts +90 -0
- package/src/vue/parsing.ts +11 -0
- package/src/vue/popup-helpers.ts +50 -0
- package/src/vue/popups/border-popup.vue +93 -0
- package/src/vue/popups/border-preset-glyph.vue +111 -0
- package/src/vue/popups/export-popup.vue +52 -0
- package/src/vue/popups/fill-color-popup.vue +39 -0
- package/src/vue/popups/find-popup.vue +111 -0
- package/src/vue/popups/font-color-popup.vue +39 -0
- package/src/vue/popups/font-size-popup.vue +52 -0
- package/src/vue/popups/import.worker.ts +66 -0
- package/src/vue/popups/insert-image-popup.vue +39 -0
- package/src/vue/sheet-context-menu.ts +229 -0
- package/src/vue/sheet-tabs.vue +162 -0
- package/src/vue/sheet-toolbar.vue +91 -0
- package/src/vue/sheet.vue +163 -148
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +12 -0
- package/src/vue/use-find-replace.ts +200 -0
- package/src/vue/use-formula-ref-select.ts +34 -0
- package/src/vue/use-formula-suggest.ts +150 -0
- package/src/vue/use-sheet-grid.ts +243 -0
- package/src/vue/use-sheet-state.ts +161 -0
- package/src/vue/use-sheet-tabs-bar.ts +115 -0
- package/src/vue/use-tool-groups.ts +61 -0
- package/src/vue/use-tool-popup.ts +150 -0
- package/src/vue/use-toolbar-scroll.ts +95 -0
- package/dist/core/address.d.ts +0 -60
- package/dist/core/address.js +0 -142
- package/dist/core/address.js.map +0 -1
- package/dist/core/cell-store.d.ts +0 -61
- package/dist/core/cell-store.js +0 -135
- package/dist/core/cell-store.js.map +0 -1
- package/dist/core/command/default-registry.d.ts +0 -10
- package/dist/core/command/default-registry.js +0 -18
- package/dist/core/command/default-registry.js.map +0 -1
- package/dist/core/command/history.d.ts +0 -64
- package/dist/core/command/history.js +0 -127
- package/dist/core/command/history.js.map +0 -1
- package/dist/core/command/merge-cells.d.ts +0 -21
- package/dist/core/command/merge-cells.js +0 -74
- package/dist/core/command/merge-cells.js.map +0 -1
- package/dist/core/command/registry.d.ts +0 -17
- package/dist/core/command/registry.js +0 -26
- package/dist/core/command/registry.js.map +0 -1
- package/dist/core/command/set-cell-formula.d.ts +0 -17
- package/dist/core/command/set-cell-formula.js +0 -29
- package/dist/core/command/set-cell-formula.js.map +0 -1
- package/dist/core/command/set-cell-value.d.ts +0 -22
- package/dist/core/command/set-cell-value.js +0 -35
- package/dist/core/command/set-cell-value.js.map +0 -1
- package/dist/core/command/types.d.ts +0 -68
- package/dist/core/events.js +0 -29
- package/dist/core/events.js.map +0 -1
- package/dist/core/formula/ast.d.ts +0 -58
- package/dist/core/formula/ast.js +0 -38
- package/dist/core/formula/ast.js.map +0 -1
- package/dist/core/formula/dependency-graph.d.ts +0 -70
- package/dist/core/formula/dependency-graph.js +0 -269
- package/dist/core/formula/dependency-graph.js.map +0 -1
- package/dist/core/formula/errors.d.ts +0 -25
- package/dist/core/formula/errors.js +0 -36
- package/dist/core/formula/errors.js.map +0 -1
- package/dist/core/formula/evaluator.d.ts +0 -41
- package/dist/core/formula/evaluator.js +0 -154
- package/dist/core/formula/evaluator.js.map +0 -1
- package/dist/core/formula/functions.d.ts +0 -30
- package/dist/core/formula/functions.js +0 -209
- package/dist/core/formula/functions.js.map +0 -1
- package/dist/core/formula/parser.d.ts +0 -7
- package/dist/core/formula/parser.js +0 -208
- package/dist/core/formula/parser.js.map +0 -1
- package/dist/core/formula/tokenizer.d.ts +0 -37
- package/dist/core/formula/tokenizer.js +0 -146
- package/dist/core/formula/tokenizer.js.map +0 -1
- package/dist/core/merge-manager.d.ts +0 -64
- package/dist/core/merge-manager.js +0 -111
- package/dist/core/merge-manager.js.map +0 -1
- package/dist/core/selection.d.ts +0 -31
- package/dist/core/selection.js +0 -62
- package/dist/core/selection.js.map +0 -1
- package/dist/core/sheet.d.ts +0 -105
- package/dist/core/sheet.js +0 -202
- package/dist/core/sheet.js.map +0 -1
- package/dist/core/workbook.d.ts +0 -43
- package/dist/core/workbook.js +0 -77
- package/dist/core/workbook.js.map +0 -1
- package/dist/grid/sheet-grid.d.ts +0 -59
- package/dist/grid/sheet-grid.js +0 -230
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/tools/builtin.js.map +0 -1
- package/dist/tools/context.js.map +0 -1
- package/dist/tools/registry.js.map +0 -1
- package/dist/vue/index.d.ts +0 -2
- package/dist/vue/sheet.js.map +0 -1
- package/dist/vue/sheet.vue.d.ts +0 -24
- package/src/core/__test__/address.test.ts +0 -113
- package/src/core/__test__/cell-store.test.ts +0 -97
- package/src/core/__test__/command.test.ts +0 -377
- package/src/core/__test__/formula-adversarial.test.ts +0 -265
- package/src/core/__test__/formula-engine.test.ts +0 -470
- package/src/core/__test__/formula-functions.test.ts +0 -178
- package/src/core/__test__/formula-parser.test.ts +0 -160
- package/src/core/__test__/history.test.ts +0 -225
- package/src/core/__test__/merge-manager.test.ts +0 -106
- package/src/core/__test__/sheet.test.ts +0 -142
- package/src/core/address.ts +0 -166
- package/src/core/cell-store.ts +0 -173
- package/src/core/command/default-registry.ts +0 -15
- package/src/core/command/history.ts +0 -167
- package/src/core/command/index.ts +0 -7
- package/src/core/command/merge-cells.ts +0 -74
- package/src/core/command/registry.ts +0 -30
- package/src/core/command/set-cell-formula.ts +0 -29
- package/src/core/command/set-cell-value.ts +0 -38
- package/src/core/command/types.ts +0 -73
- package/src/core/events.ts +0 -37
- package/src/core/formula/ast.ts +0 -66
- package/src/core/formula/dependency-graph.ts +0 -329
- package/src/core/formula/errors.ts +0 -41
- package/src/core/formula/evaluator.ts +0 -219
- package/src/core/formula/functions.ts +0 -266
- package/src/core/formula/index.ts +0 -7
- package/src/core/formula/parser.ts +0 -198
- package/src/core/formula/tokenizer.ts +0 -160
- package/src/core/merge-manager.ts +0 -158
- package/src/core/selection.ts +0 -66
- package/src/core/sheet.ts +0 -257
- package/src/core/workbook.ts +0 -99
- package/src/grid/__test__/canvas-mock.ts +0 -49
- package/src/grid/__test__/setup.ts +0 -3
- package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
- package/src/grid/__test__/sheet-grid.test.ts +0 -177
- package/src/grid/sheet-grid.ts +0 -283
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { CellAddress } from '../address'
|
|
2
|
-
import { cellDataEqual, isEmptyCellData, type CellData } from '../cell-store'
|
|
3
|
-
import type { CellPatch, Command, CommandResult } from './types'
|
|
4
|
-
|
|
5
|
-
/** SetCellValueCommand 的批量写入项 */
|
|
6
|
-
export interface SetCellValueItem {
|
|
7
|
-
addr: CellAddress
|
|
8
|
-
/** 目标数据;空数据(无公式且 v 为 null/undefined/'')= 清除 */
|
|
9
|
-
data?: CellData
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface SetCellValueParams {
|
|
13
|
-
/** 批量写入(供粘贴/填充复用);一次调用 = 一个 undo 单元 */
|
|
14
|
-
items: SetCellValueItem[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 单元格写入命令:逐项解析锚点、捕获 before/after 差量并立即应用。
|
|
19
|
-
* 与 before 相等的项跳过(不产生补丁);全部无变化时不入历史。
|
|
20
|
-
*/
|
|
21
|
-
export const SetCellValueCommand: Command<SetCellValueParams> = {
|
|
22
|
-
id: 'sheet.command.set-cell-value',
|
|
23
|
-
|
|
24
|
-
handler(ctx, params): CommandResult {
|
|
25
|
-
const patches: CellPatch[] = []
|
|
26
|
-
for (const item of params.items) {
|
|
27
|
-
const anchor = ctx.sheet.merges.resolveAnchor(item.addr)
|
|
28
|
-
const before = ctx.sheet.store.getCell(anchor)
|
|
29
|
-
const after = isEmptyCellData(item.data) ? undefined : item.data
|
|
30
|
-
if (cellDataEqual(before, after)) continue
|
|
31
|
-
const patch: CellPatch = { kind: 'cell', addr: anchor, before, after }
|
|
32
|
-
ctx.applyPatch(patch, 'redo')
|
|
33
|
-
patches.push(patch)
|
|
34
|
-
}
|
|
35
|
-
if (patches.length === 0) return { mutations: [] }
|
|
36
|
-
return { mutations: [{ redo: patches, undo: [...patches].reverse() }] }
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { CellAddress, CellRange } from '../address'
|
|
2
|
-
import type { CellData } from '../cell-store'
|
|
3
|
-
import type { Sheet } from '../sheet'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 命令系统类型定义(决策 5:命令 + 逆操作补丁)。
|
|
7
|
-
*
|
|
8
|
-
* - 一切模型变更都是 Command,执行时产出 Mutation
|
|
9
|
-
* - Patch 是受影响单元格 / 合并记录的 before/after 差量,不是全量快照
|
|
10
|
-
* - 同一批 Patch 双向回放:redo 方向应用 after,undo 方向应用 before
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** 补丁回放方向 */
|
|
14
|
-
export type PatchDirection = 'undo' | 'redo'
|
|
15
|
-
|
|
16
|
-
/** 单元格数据差量补丁(原始存储语义;undefined = 该侧无此格) */
|
|
17
|
-
export interface CellPatch {
|
|
18
|
-
kind: 'cell'
|
|
19
|
-
addr: CellAddress
|
|
20
|
-
/**
|
|
21
|
-
* 目标 sheet(跨表公式重算的派生补丁会落在其它 sheet 上);
|
|
22
|
-
* 缺省 = 命令所在 sheet。回放时按此字段路由到目标 sheet 的变更通道。
|
|
23
|
-
*/
|
|
24
|
-
sheet?: Sheet
|
|
25
|
-
/** 变更前的数据(undefined = 原本为空格) */
|
|
26
|
-
before?: CellData
|
|
27
|
-
/** 变更后的数据(undefined = 变更后为空格) */
|
|
28
|
-
after?: CellData
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 合并记录差量补丁(before/after = 该侧是否存在此合并) */
|
|
32
|
-
export interface MergePatch {
|
|
33
|
-
kind: 'merge'
|
|
34
|
-
range: CellRange
|
|
35
|
-
before: boolean
|
|
36
|
-
after: boolean
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type Patch = CellPatch | MergePatch
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 一次命令执行产生的变更单元。
|
|
43
|
-
* `redo` 按列表顺序应用;`undo` 取同一批补丁的逆序(命令实现负责构造,
|
|
44
|
-
* 保证 undo 先撤销后登记的副作用,例如先移除新合并再恢复旧合并)。
|
|
45
|
-
*/
|
|
46
|
-
export interface Mutation {
|
|
47
|
-
redo: Patch[]
|
|
48
|
-
undo: Patch[]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** 命令执行结果 */
|
|
52
|
-
export interface CommandResult<R = unknown> {
|
|
53
|
-
/** 产生的 mutation 列表(空数组 = 无实际变更,不入历史) */
|
|
54
|
-
mutations: Mutation[]
|
|
55
|
-
/** 命令附带返回值(如 MergeCellsCommand 返回最终生效区域) */
|
|
56
|
-
result?: R
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 命令执行上下文 */
|
|
60
|
-
export interface CommandContext {
|
|
61
|
-
sheet: Sheet
|
|
62
|
-
/**
|
|
63
|
-
* 应用单个补丁。命令 handler 执行时对 redo 补丁逐项调用;
|
|
64
|
-
* HistoryManager 回放 undo/redo 亦走此路径,保证只有一条变更通道。
|
|
65
|
-
*/
|
|
66
|
-
applyPatch(patch: Patch, direction: PatchDirection): void
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 命令:id + handler */
|
|
70
|
-
export interface Command<P = unknown, R = unknown> {
|
|
71
|
-
readonly id: string
|
|
72
|
-
handler(ctx: CommandContext, params: P): CommandResult<R> | undefined
|
|
73
|
-
}
|
package/src/core/events.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 包内轻量类型化事件发射器(core 内部基建,不对外导出实现细节)。
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type EventHandler<T> = (payload: T) => void
|
|
6
|
-
|
|
7
|
-
export class TypedEventEmitter<EventMap extends Record<string, unknown>> {
|
|
8
|
-
private handlers = new Map<keyof EventMap, Set<EventHandler<EventMap[keyof EventMap]>>>()
|
|
9
|
-
|
|
10
|
-
/** 订阅事件,返回取消订阅函数 */
|
|
11
|
-
on<K extends keyof EventMap>(type: K, handler: EventHandler<EventMap[K]>): () => void {
|
|
12
|
-
let set = this.handlers.get(type)
|
|
13
|
-
if (!set) {
|
|
14
|
-
set = new Set()
|
|
15
|
-
this.handlers.set(type, set)
|
|
16
|
-
}
|
|
17
|
-
set.add(handler as EventHandler<EventMap[keyof EventMap]>)
|
|
18
|
-
return () => this.off(type, handler)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
off<K extends keyof EventMap>(type: K, handler: EventHandler<EventMap[K]>): void {
|
|
22
|
-
this.handlers.get(type)?.delete(handler as EventHandler<EventMap[keyof EventMap]>)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
emit<K extends keyof EventMap>(type: K, payload: EventMap[K]): void {
|
|
26
|
-
const set = this.handlers.get(type)
|
|
27
|
-
if (!set) return
|
|
28
|
-
// Set 迭代天然容忍遍历中取消订阅
|
|
29
|
-
for (const handler of set) {
|
|
30
|
-
;(handler as EventHandler<EventMap[K]>)(payload)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
clear(): void {
|
|
35
|
-
this.handlers.clear()
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/core/formula/ast.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { CellAddress, CellRange } from '../address'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 公式 AST 节点。
|
|
5
|
-
* 引用节点携带可选 sheet 名(跨表引用);缺省 = 公式所在 sheet。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type BinaryOperator =
|
|
9
|
-
| '+'
|
|
10
|
-
| '-'
|
|
11
|
-
| '*'
|
|
12
|
-
| '/'
|
|
13
|
-
| '^'
|
|
14
|
-
| '&'
|
|
15
|
-
| '='
|
|
16
|
-
| '<>'
|
|
17
|
-
| '<'
|
|
18
|
-
| '<='
|
|
19
|
-
| '>'
|
|
20
|
-
| '>='
|
|
21
|
-
|
|
22
|
-
export type AstNode =
|
|
23
|
-
| { kind: 'number'; value: number }
|
|
24
|
-
| { kind: 'string'; value: string }
|
|
25
|
-
| { kind: 'boolean'; value: boolean }
|
|
26
|
-
/** 未知名称(如裸写的 Sheet2 / 未定义命名);求值为 #NAME? */
|
|
27
|
-
| { kind: 'name'; name: string }
|
|
28
|
-
| { kind: 'cell'; sheet?: string; addr: CellAddress }
|
|
29
|
-
| { kind: 'range'; sheet?: string; range: CellRange }
|
|
30
|
-
| { kind: 'unary'; op: '-' | '+'; operand: AstNode }
|
|
31
|
-
/** 百分号后缀运算(除以 100) */
|
|
32
|
-
| { kind: 'percent'; operand: AstNode }
|
|
33
|
-
| { kind: 'binary'; op: BinaryOperator; left: AstNode; right: AstNode }
|
|
34
|
-
| { kind: 'call'; name: string; args: AstNode[] }
|
|
35
|
-
|
|
36
|
-
/** 公式中的引用(依赖图提取用):sheet 缺省由所属公式节点补齐 */
|
|
37
|
-
export interface AstReference {
|
|
38
|
-
sheet?: string
|
|
39
|
-
range: CellRange
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 遍历 AST,收集全部引用(单格视为 1×1 区域) */
|
|
43
|
-
export function collectReferences(node: AstNode, out: AstReference[] = []): AstReference[] {
|
|
44
|
-
switch (node.kind) {
|
|
45
|
-
case 'cell':
|
|
46
|
-
out.push({ sheet: node.sheet, range: { start: node.addr, end: node.addr } })
|
|
47
|
-
break
|
|
48
|
-
case 'range':
|
|
49
|
-
out.push({ sheet: node.sheet, range: node.range })
|
|
50
|
-
break
|
|
51
|
-
case 'unary':
|
|
52
|
-
case 'percent':
|
|
53
|
-
collectReferences(node.operand, out)
|
|
54
|
-
break
|
|
55
|
-
case 'binary':
|
|
56
|
-
collectReferences(node.left, out)
|
|
57
|
-
collectReferences(node.right, out)
|
|
58
|
-
break
|
|
59
|
-
case 'call':
|
|
60
|
-
for (const arg of node.args) collectReferences(arg, out)
|
|
61
|
-
break
|
|
62
|
-
default:
|
|
63
|
-
break
|
|
64
|
-
}
|
|
65
|
-
return out
|
|
66
|
-
}
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { cellKey, rangeContainsAddress, type CellAddress, type CellRange } from '../address'
|
|
2
|
-
import type { CellData, CellType, CellValue } from '../cell-store'
|
|
3
|
-
import type { CellPatch } from '../command/types'
|
|
4
|
-
import type { Sheet } from '../sheet'
|
|
5
|
-
import { collectReferences, type AstNode } from './ast'
|
|
6
|
-
import { formulaError, isFormulaError, isFormulaErrorCode, type FormulaError } from './errors'
|
|
7
|
-
import { evaluateAst, type ScalarValue } from './evaluator'
|
|
8
|
-
import { invokeFormulaFunction } from './functions'
|
|
9
|
-
import { parseFormula } from './parser'
|
|
10
|
-
import { FormulaParseError } from './tokenizer'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 依赖图(工作簿级计算中枢):
|
|
14
|
-
* - sheet 注册表:跨表引用按名解析(不存在 → #REF!)
|
|
15
|
-
* - 正向索引:公式格 → 引用格集合(节点 deps);反向索引:引用格 → 依赖者
|
|
16
|
-
* (单格引用走 cellKey 精确索引 O(1),区域引用走线性扫描)
|
|
17
|
-
* - 变更时标脏(向上 BFS 收集全部依赖者)+ 拓扑序增量重算(递归向下,memo 去重)
|
|
18
|
-
* - 循环引用检测:求值在途栈遇回边 → 环上所有格 #CYCLE!;依赖环外格只传播错误,
|
|
19
|
-
* 打破循环(编辑环上格)后经标脏重算自动恢复
|
|
20
|
-
*
|
|
21
|
-
* 图状态与单元格存储严格同步:所有 f 变更(命令/undo/redo/rollback 回放)都经
|
|
22
|
-
* Sheet.applyPatch → syncCell 维护节点;重算只发生在命令执行后(见 Sheet.executeCommand),
|
|
23
|
-
* undo/redo 靠派生补丁精确回放缓存值,不重算。
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/** 公式节点的一个依赖(sheet 名已补齐为具体值) */
|
|
27
|
-
export interface FormulaDependency {
|
|
28
|
-
sheetName: string
|
|
29
|
-
range: CellRange
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 公式节点(依赖图管理的公式格状态;依赖图外部只读使用) */
|
|
33
|
-
export interface FormulaNode {
|
|
34
|
-
readonly sheetName: string
|
|
35
|
-
readonly addr: CellAddress
|
|
36
|
-
readonly formula: string
|
|
37
|
-
/** null = 解析失败(求值 #ERROR!,无依赖) */
|
|
38
|
-
readonly ast: AstNode | null
|
|
39
|
-
readonly deps: FormulaDependency[]
|
|
40
|
-
/** 反向索引清理器(removeNode 时执行) */
|
|
41
|
-
readonly cleanups: (() => void)[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class DependencyGraph {
|
|
45
|
-
private readonly sheets = new Map<string, Sheet>()
|
|
46
|
-
/** 正向索引宿主:sheetName → cellKey → 公式节点 */
|
|
47
|
-
private readonly nodes = new Map<string, Map<number, FormulaNode>>()
|
|
48
|
-
/** 反向索引(单格引用):sheetName → cellKey → 依赖者集合 */
|
|
49
|
-
private readonly exact = new Map<string, Map<number, Set<FormulaNode>>>()
|
|
50
|
-
/** 反向索引(区域引用):sheetName → { 区域, 依赖者 } 集合(线性扫描) */
|
|
51
|
-
private readonly ranged = new Map<string, Set<{ range: CellRange; node: FormulaNode }>>()
|
|
52
|
-
|
|
53
|
-
// ─── sheet 注册表 ─────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
registerSheet(sheet: Sheet): void {
|
|
56
|
-
this.sheets.set(sheet.name, sheet)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 注销 sheet 并移除其全部公式节点(引用它的公式在下次重算时得 #REF!) */
|
|
60
|
-
unregisterSheet(sheet: Sheet): void {
|
|
61
|
-
if (this.sheets.get(sheet.name) !== sheet) return
|
|
62
|
-
this.sheets.delete(sheet.name)
|
|
63
|
-
const sheetNodes = this.nodes.get(sheet.name)
|
|
64
|
-
if (sheetNodes) {
|
|
65
|
-
for (const node of sheetNodes.values()) this.removeNode(node)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
getSheet(name: string): Sheet | undefined {
|
|
70
|
-
return this.sheets.get(name)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ─── 节点查询 ─────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
getNode(sheetName: string, addr: CellAddress): FormulaNode | undefined {
|
|
76
|
-
return this.nodes.get(sheetName)?.get(cellKey(addr))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 公式节点总数(测试/调试用) */
|
|
80
|
-
get nodeCount(): number {
|
|
81
|
-
let count = 0
|
|
82
|
-
for (const sheetNodes of this.nodes.values()) count += sheetNodes.size
|
|
83
|
-
return count
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─── 图同步 ───────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 单元格补丁后的图同步:f 增/删/改 → 节点增/删/重建。
|
|
90
|
-
* 由 Sheet.applyPatch(唯一变更通道)调用,命令与 undo/redo 回放均覆盖。
|
|
91
|
-
*/
|
|
92
|
-
syncCell(sheet: Sheet, addr: CellAddress, before?: CellData, after?: CellData): void {
|
|
93
|
-
const beforeF = before?.f
|
|
94
|
-
const afterF = after?.f
|
|
95
|
-
if (beforeF === afterF) return
|
|
96
|
-
const existing = this.getNode(sheet.name, addr)
|
|
97
|
-
if (existing) this.removeNode(existing)
|
|
98
|
-
if (afterF != null && afterF !== '') this.addNode(sheet, addr, afterF)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ─── 增量重算 ─────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 变更集 → 标脏 + 拓扑序重算 → 派生补丁(未应用)。
|
|
105
|
-
* 调用方(Sheet.executeCommand)负责应用补丁并并入同一 undo 单元。
|
|
106
|
-
*/
|
|
107
|
-
recalc(changed: readonly { sheet: Sheet; addr: CellAddress }[]): CellPatch[] {
|
|
108
|
-
// 1. 标脏:变更格自身(若是公式)+ 向上 BFS 全部传递依赖者
|
|
109
|
-
const dirty = new Set<FormulaNode>()
|
|
110
|
-
const queue: FormulaNode[] = []
|
|
111
|
-
const mark = (node: FormulaNode | undefined): void => {
|
|
112
|
-
if (node && !dirty.has(node)) {
|
|
113
|
-
dirty.add(node)
|
|
114
|
-
queue.push(node)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const { sheet, addr } of changed) {
|
|
118
|
-
mark(this.getNode(sheet.name, addr))
|
|
119
|
-
for (const dep of this.dependentsOf(sheet.name, addr)) mark(dep)
|
|
120
|
-
}
|
|
121
|
-
for (let i = 0; i < queue.length; i++) {
|
|
122
|
-
const node = queue[i]!
|
|
123
|
-
for (const dep of this.dependentsOf(node.sheetName, node.addr)) mark(dep)
|
|
124
|
-
}
|
|
125
|
-
if (dirty.size === 0) return []
|
|
126
|
-
|
|
127
|
-
// 2. 拓扑序求值:递归向下(依赖先于使用求值),memo 保证每格最多算一次;
|
|
128
|
-
// 在途栈遇回边 → 栈中自回边点起全部入环(#CYCLE!)
|
|
129
|
-
const passValues = new Map<FormulaNode, ScalarValue | FormulaError>()
|
|
130
|
-
const inProgress = new Set<FormulaNode>()
|
|
131
|
-
const stack: FormulaNode[] = []
|
|
132
|
-
const cycleMembers = new Set<FormulaNode>()
|
|
133
|
-
const cycleError = formulaError('#CYCLE!')
|
|
134
|
-
|
|
135
|
-
const evaluateNode = (node: FormulaNode): ScalarValue | FormulaError => {
|
|
136
|
-
const cached = passValues.get(node)
|
|
137
|
-
if (cached !== undefined) return cached
|
|
138
|
-
if (inProgress.has(node)) {
|
|
139
|
-
const start = stack.indexOf(node)
|
|
140
|
-
for (let i = start; i < stack.length; i++) cycleMembers.add(stack[i]!)
|
|
141
|
-
return cycleError
|
|
142
|
-
}
|
|
143
|
-
if (!node.ast) {
|
|
144
|
-
const error = formulaError('#ERROR!')
|
|
145
|
-
passValues.set(node, error)
|
|
146
|
-
return error
|
|
147
|
-
}
|
|
148
|
-
inProgress.add(node)
|
|
149
|
-
stack.push(node)
|
|
150
|
-
let value: ScalarValue | FormulaError | (ScalarValue | FormulaError)[]
|
|
151
|
-
try {
|
|
152
|
-
value = evaluateAst(node.ast, {
|
|
153
|
-
currentSheet: node.sheetName,
|
|
154
|
-
readCell,
|
|
155
|
-
readRange,
|
|
156
|
-
callFunction: invokeFormulaFunction
|
|
157
|
-
})
|
|
158
|
-
} catch {
|
|
159
|
-
// 求值期异常(如自定义函数抛错)不应击穿重算 → 记为 #ERROR!
|
|
160
|
-
value = formulaError('#ERROR!')
|
|
161
|
-
}
|
|
162
|
-
inProgress.delete(node)
|
|
163
|
-
stack.pop()
|
|
164
|
-
// 区域直接作为公式结果('=A1:B2')→ #VALUE!
|
|
165
|
-
if (Array.isArray(value)) value = formulaError('#VALUE!')
|
|
166
|
-
if (cycleMembers.has(node)) value = cycleError
|
|
167
|
-
passValues.set(node, value)
|
|
168
|
-
return value
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const readCell = (sheetName: string, addr: CellAddress): ScalarValue | FormulaError => {
|
|
172
|
-
const sheet = this.sheets.get(sheetName)
|
|
173
|
-
if (!sheet) return formulaError('#REF!')
|
|
174
|
-
const dep = this.getNode(sheetName, addr)
|
|
175
|
-
if (dep && dirty.has(dep)) return evaluateNode(dep)
|
|
176
|
-
return cellDataToScalar(sheet.store.getCell(addr))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const readRange = (
|
|
180
|
-
sheetName: string,
|
|
181
|
-
range: CellRange
|
|
182
|
-
): (ScalarValue | FormulaError)[] | FormulaError => {
|
|
183
|
-
const sheet = this.sheets.get(sheetName)
|
|
184
|
-
if (!sheet) return formulaError('#REF!')
|
|
185
|
-
const values: (ScalarValue | FormulaError)[] = []
|
|
186
|
-
// 只迭代稀疏存在的格
|
|
187
|
-
for (const [addr, data] of sheet.store.entriesInRange(range)) {
|
|
188
|
-
const dep = this.getNode(sheetName, addr)
|
|
189
|
-
values.push(dep && dirty.has(dep) ? evaluateNode(dep) : cellDataToScalar(data))
|
|
190
|
-
}
|
|
191
|
-
return values
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
for (const node of dirty) evaluateNode(node)
|
|
195
|
-
|
|
196
|
-
// 3. 派生补丁:缓存值与重算结果一致的跳过(收敛时不产生冗余变更)
|
|
197
|
-
const patches: CellPatch[] = []
|
|
198
|
-
for (const node of dirty) {
|
|
199
|
-
const value = passValues.get(node)!
|
|
200
|
-
const { v, t } = serializeResult(value)
|
|
201
|
-
const sheet = this.sheets.get(node.sheetName)
|
|
202
|
-
if (!sheet) continue
|
|
203
|
-
const before = sheet.store.getCell(node.addr)
|
|
204
|
-
if (before && before.f === node.formula && before.v === v && before.t === t) continue
|
|
205
|
-
patches.push({
|
|
206
|
-
kind: 'cell',
|
|
207
|
-
sheet,
|
|
208
|
-
addr: node.addr,
|
|
209
|
-
before,
|
|
210
|
-
after: { f: node.formula, v, t }
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
return patches
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ─── 内部 ─────────────────────────────────────────────────
|
|
217
|
-
|
|
218
|
-
private addNode(sheet: Sheet, addr: CellAddress, formula: string): void {
|
|
219
|
-
let ast: AstNode | null = null
|
|
220
|
-
try {
|
|
221
|
-
ast = parseFormula(formula)
|
|
222
|
-
} catch (error) {
|
|
223
|
-
if (!(error instanceof FormulaParseError)) throw error
|
|
224
|
-
// 解析失败:ast 保持 null(求值 #ERROR!,无依赖)
|
|
225
|
-
}
|
|
226
|
-
const deps: FormulaDependency[] = []
|
|
227
|
-
if (ast) {
|
|
228
|
-
const seen = new Set<string>()
|
|
229
|
-
for (const ref of collectReferences(ast)) {
|
|
230
|
-
const sheetName = ref.sheet ?? sheet.name
|
|
231
|
-
const { start, end } = ref.range
|
|
232
|
-
const key = `${sheetName}|${start.row},${start.col},${end.row},${end.col}`
|
|
233
|
-
if (seen.has(key)) continue
|
|
234
|
-
seen.add(key)
|
|
235
|
-
deps.push({ sheetName, range: ref.range })
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const node: FormulaNode = {
|
|
239
|
-
sheetName: sheet.name,
|
|
240
|
-
addr: { ...addr },
|
|
241
|
-
formula,
|
|
242
|
-
ast,
|
|
243
|
-
deps,
|
|
244
|
-
cleanups: []
|
|
245
|
-
}
|
|
246
|
-
let sheetNodes = this.nodes.get(sheet.name)
|
|
247
|
-
if (!sheetNodes) {
|
|
248
|
-
sheetNodes = new Map()
|
|
249
|
-
this.nodes.set(sheet.name, sheetNodes)
|
|
250
|
-
}
|
|
251
|
-
sheetNodes.set(cellKey(addr), node)
|
|
252
|
-
|
|
253
|
-
for (const dep of deps) {
|
|
254
|
-
const isSingleCell =
|
|
255
|
-
dep.range.start.row === dep.range.end.row && dep.range.start.col === dep.range.end.col
|
|
256
|
-
if (isSingleCell) {
|
|
257
|
-
let sheetExact = this.exact.get(dep.sheetName)
|
|
258
|
-
if (!sheetExact) {
|
|
259
|
-
sheetExact = new Map()
|
|
260
|
-
this.exact.set(dep.sheetName, sheetExact)
|
|
261
|
-
}
|
|
262
|
-
const key = cellKey(dep.range.start)
|
|
263
|
-
let set = sheetExact.get(key)
|
|
264
|
-
if (!set) {
|
|
265
|
-
set = new Set()
|
|
266
|
-
sheetExact.set(key, set)
|
|
267
|
-
}
|
|
268
|
-
set.add(node)
|
|
269
|
-
node.cleanups.push(() => {
|
|
270
|
-
set.delete(node)
|
|
271
|
-
if (set.size === 0) sheetExact.delete(key)
|
|
272
|
-
})
|
|
273
|
-
} else {
|
|
274
|
-
let set = this.ranged.get(dep.sheetName)
|
|
275
|
-
if (!set) {
|
|
276
|
-
set = new Set()
|
|
277
|
-
this.ranged.set(dep.sheetName, set)
|
|
278
|
-
}
|
|
279
|
-
const entry = { range: dep.range, node }
|
|
280
|
-
set.add(entry)
|
|
281
|
-
node.cleanups.push(() => set.delete(entry))
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private removeNode(node: FormulaNode): void {
|
|
287
|
-
for (const cleanup of node.cleanups) cleanup()
|
|
288
|
-
const sheetNodes = this.nodes.get(node.sheetName)
|
|
289
|
-
if (!sheetNodes) return
|
|
290
|
-
sheetNodes.delete(cellKey(node.addr))
|
|
291
|
-
if (sheetNodes.size === 0) this.nodes.delete(node.sheetName)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private dependentsOf(sheetName: string, addr: CellAddress): Set<FormulaNode> {
|
|
295
|
-
const result = new Set<FormulaNode>()
|
|
296
|
-
const exactSet = this.exact.get(sheetName)?.get(cellKey(addr))
|
|
297
|
-
if (exactSet) for (const node of exactSet) result.add(node)
|
|
298
|
-
const rangedSet = this.ranged.get(sheetName)
|
|
299
|
-
if (rangedSet) {
|
|
300
|
-
for (const entry of rangedSet) {
|
|
301
|
-
if (rangeContainsAddress(entry.range, addr)) result.add(entry.node)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
return result
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** 单元格存储 → 求值标量(t='e' 还原为错误标记;空 → null) */
|
|
309
|
-
function cellDataToScalar(data: CellData | undefined): ScalarValue | FormulaError {
|
|
310
|
-
if (!data || data.v == null) return null
|
|
311
|
-
if (data.t === 'e') {
|
|
312
|
-
return formulaError(isFormulaErrorCode(data.v) ? data.v : '#ERROR!')
|
|
313
|
-
}
|
|
314
|
-
return data.v
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** 求值结果 → 存储形态(str = 公式结果字符串,e = 错误;与决策 2 一致) */
|
|
318
|
-
function serializeResult(value: ScalarValue | FormulaError): { v: CellValue; t: CellType } {
|
|
319
|
-
if (isFormulaError(value)) return { v: value.code, t: 'e' }
|
|
320
|
-
if (value === null) return { v: 0, t: 'n' } // 防御:公式结果不会是 null
|
|
321
|
-
switch (typeof value) {
|
|
322
|
-
case 'number':
|
|
323
|
-
return { v: value, t: 'n' }
|
|
324
|
-
case 'boolean':
|
|
325
|
-
return { v: value, t: 'b' }
|
|
326
|
-
default:
|
|
327
|
-
return { v: value, t: 'str' }
|
|
328
|
-
}
|
|
329
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 公式错误值体系。
|
|
3
|
-
*
|
|
4
|
-
* 求值器内部以 FormulaError 标记对象传递错误(随运算传播);
|
|
5
|
-
* 写入单元格时序列化为 `v = 错误码, t = 'e'`(决策 2 的 CellData 格式)。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** 公式错误码(Excel 子集 + 解析失败 #ERROR!、循环引用 #CYCLE!) */
|
|
9
|
-
export const FORMULA_ERROR_CODES = [
|
|
10
|
-
'#DIV/0!',
|
|
11
|
-
'#VALUE!',
|
|
12
|
-
'#NAME?',
|
|
13
|
-
'#REF!',
|
|
14
|
-
'#ERROR!',
|
|
15
|
-
'#CYCLE!'
|
|
16
|
-
] as const
|
|
17
|
-
|
|
18
|
-
export type FormulaErrorCode = (typeof FORMULA_ERROR_CODES)[number]
|
|
19
|
-
|
|
20
|
-
const ERROR_BRAND: unique symbol = Symbol('veltra-sheet.formula-error')
|
|
21
|
-
|
|
22
|
-
/** 求值过程中的错误标记(不参与普通值运算,遇运算即传播) */
|
|
23
|
-
export interface FormulaError {
|
|
24
|
-
readonly [ERROR_BRAND]: true
|
|
25
|
-
readonly code: FormulaErrorCode
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 构造错误标记 */
|
|
29
|
-
export function formulaError(code: FormulaErrorCode): FormulaError {
|
|
30
|
-
return { [ERROR_BRAND]: true, code }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 判定错误标记 */
|
|
34
|
-
export function isFormulaError(value: unknown): value is FormulaError {
|
|
35
|
-
return typeof value === 'object' && value !== null && ERROR_BRAND in value
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 判定合法错误码(读取 t='e' 的单元格时校验) */
|
|
39
|
-
export function isFormulaErrorCode(value: unknown): value is FormulaErrorCode {
|
|
40
|
-
return typeof value === 'string' && (FORMULA_ERROR_CODES as readonly string[]).includes(value)
|
|
41
|
-
}
|