@veltra/sheet 2.0.1 → 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 -26
- package/dist/index.js +3 -23
- 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 -164
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +10 -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 -7
- package/src/env.d.ts +11 -0
- package/src/index.ts +203 -14
- 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 +161 -186
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +11 -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/fill.d.ts +0 -34
- package/dist/core/fill.js +0 -320
- package/dist/core/fill.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 -116
- package/dist/core/sheet.js +0 -224
- 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 -81
- package/dist/grid/sheet-grid.js +0 -331
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/grid/vtable-theme.js +0 -80
- package/dist/grid/vtable-theme.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__/fill.test.ts +0 -140
- 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/fill.ts +0 -314
- 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 -282
- 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 -275
- package/src/grid/sheet-grid.ts +0 -407
- package/src/grid/vtable-theme.ts +0 -61
|
@@ -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/fill.ts
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import type { CellAddress, CellRange } from './address'
|
|
2
|
-
import { colIndexToName, colNameToIndex, createRange, iterateRange } from './address'
|
|
3
|
-
import type { CellData, CellType } from './cell-store'
|
|
4
|
-
import type { SetCellValueItem } from './command/set-cell-value'
|
|
5
|
-
|
|
6
|
-
/** 填充柄拖拽方向(与 VTable drag_fill_handle_end 一致) */
|
|
7
|
-
export type FillDirection = 'top' | 'bottom' | 'left' | 'right'
|
|
8
|
-
|
|
9
|
-
export interface GenerateFillOptions {
|
|
10
|
-
/** 拖拽前的源选区(模型坐标) */
|
|
11
|
-
source: CellRange
|
|
12
|
-
/** 需要写入的目标区(不含源区,模型坐标) */
|
|
13
|
-
target: CellRange
|
|
14
|
-
/** 填充方向 */
|
|
15
|
-
direction: FillDirection
|
|
16
|
-
/** 读取源区单元格(原始存储语义) */
|
|
17
|
-
getCellData: (addr: CellAddress) => CellData | undefined
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 由源选区 + 拖拽后扩展选区 + 方向,计算需要填充的目标区(模型坐标)。
|
|
22
|
-
* 算法对齐 VTable 官方 fill-handle 示例(扩展选区含源区,目标为扩展部分)。
|
|
23
|
-
*/
|
|
24
|
-
export function computeFillTargetRange(
|
|
25
|
-
source: CellRange,
|
|
26
|
-
direction: FillDirection,
|
|
27
|
-
expanded: CellRange
|
|
28
|
-
): CellRange | null {
|
|
29
|
-
const src = createRange(source.start, source.end)
|
|
30
|
-
const exp = createRange(expanded.start, expanded.end)
|
|
31
|
-
|
|
32
|
-
if (direction === 'bottom') {
|
|
33
|
-
if (exp.end.row <= src.end.row) return null
|
|
34
|
-
return {
|
|
35
|
-
start: { row: src.end.row + 1, col: src.start.col },
|
|
36
|
-
end: { row: exp.end.row, col: src.end.col }
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (direction === 'top') {
|
|
40
|
-
if (exp.start.row >= src.start.row) return null
|
|
41
|
-
return {
|
|
42
|
-
start: { row: exp.start.row, col: src.start.col },
|
|
43
|
-
end: { row: src.start.row - 1, col: src.end.col }
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (direction === 'right') {
|
|
47
|
-
if (exp.end.col <= src.end.col) return null
|
|
48
|
-
return {
|
|
49
|
-
start: { row: src.start.row, col: src.end.col + 1 },
|
|
50
|
-
end: { row: src.end.row, col: exp.end.col }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// left
|
|
54
|
-
if (exp.start.col >= src.start.col) return null
|
|
55
|
-
return {
|
|
56
|
-
start: { row: src.start.row, col: exp.start.col },
|
|
57
|
-
end: { row: src.end.row, col: src.start.col - 1 }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 生成填充写入项(tile / 数字日期等差 / 公式相对引用位移)。
|
|
63
|
-
* 调用方一次 `sheet.setCells(items)` 即可(单 undo 单元)。
|
|
64
|
-
*/
|
|
65
|
-
export function generateFill(options: GenerateFillOptions): SetCellValueItem[] {
|
|
66
|
-
const { source, target, direction, getCellData } = options
|
|
67
|
-
const src = createRange(source.start, source.end)
|
|
68
|
-
const tgt = createRange(target.start, target.end)
|
|
69
|
-
const srcRows = src.end.row - src.start.row + 1
|
|
70
|
-
const srcCols = src.end.col - src.start.col + 1
|
|
71
|
-
const vertical = direction === 'top' || direction === 'bottom'
|
|
72
|
-
|
|
73
|
-
const seriesByLane = new Map<number, SeriesInfo | null>()
|
|
74
|
-
if (vertical) {
|
|
75
|
-
for (let col = src.start.col; col <= src.end.col; col++) {
|
|
76
|
-
const values: number[] = []
|
|
77
|
-
let cellType: CellType | undefined
|
|
78
|
-
let ok = true
|
|
79
|
-
for (let row = src.start.row; row <= src.end.row; row++) {
|
|
80
|
-
const data = getCellData({ row, col })
|
|
81
|
-
const num = readSeriesNumber(data)
|
|
82
|
-
if (num == null) {
|
|
83
|
-
ok = false
|
|
84
|
-
break
|
|
85
|
-
}
|
|
86
|
-
values.push(num.value)
|
|
87
|
-
cellType = num.t
|
|
88
|
-
}
|
|
89
|
-
seriesByLane.set(col, ok ? makeSeriesInfo(values, cellType) : null)
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
for (let row = src.start.row; row <= src.end.row; row++) {
|
|
93
|
-
const values: number[] = []
|
|
94
|
-
let cellType: CellType | undefined
|
|
95
|
-
let ok = true
|
|
96
|
-
for (let col = src.start.col; col <= src.end.col; col++) {
|
|
97
|
-
const data = getCellData({ row, col })
|
|
98
|
-
const num = readSeriesNumber(data)
|
|
99
|
-
if (num == null) {
|
|
100
|
-
ok = false
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
values.push(num.value)
|
|
104
|
-
cellType = num.t
|
|
105
|
-
}
|
|
106
|
-
seriesByLane.set(row, ok ? makeSeriesInfo(values, cellType) : null)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const items: SetCellValueItem[] = []
|
|
111
|
-
for (const addr of iterateRange(tgt)) {
|
|
112
|
-
const rowOffset = positiveMod(addr.row - src.start.row, srcRows)
|
|
113
|
-
const colOffset = positiveMod(addr.col - src.start.col, srcCols)
|
|
114
|
-
const srcAddr: CellAddress = { row: src.start.row + rowOffset, col: src.start.col + colOffset }
|
|
115
|
-
const srcData = getCellData(srcAddr)
|
|
116
|
-
|
|
117
|
-
if (srcData?.f != null && srcData.f !== '') {
|
|
118
|
-
const deltaRow = addr.row - srcAddr.row
|
|
119
|
-
const deltaCol = addr.col - srcAddr.col
|
|
120
|
-
items.push({ addr, data: { f: shiftFormulaRefs(srcData.f, deltaRow, deltaCol) } })
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const lane = vertical ? srcAddr.col : srcAddr.row
|
|
125
|
-
const series = seriesByLane.get(lane)
|
|
126
|
-
if (series) {
|
|
127
|
-
const index = vertical ? addr.row - src.start.row : addr.col - src.start.col
|
|
128
|
-
const value = series.first + index * series.delta
|
|
129
|
-
items.push({ addr, data: { v: value, t: series.t ?? 'n' } })
|
|
130
|
-
continue
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// tile 复制(含空格 → 清除)
|
|
134
|
-
items.push({ addr, data: cloneCellData(srcData) })
|
|
135
|
-
}
|
|
136
|
-
return items
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
interface SeriesInfo {
|
|
140
|
-
first: number
|
|
141
|
-
delta: number
|
|
142
|
-
t?: CellType
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function makeSeriesInfo(values: number[], t?: CellType): SeriesInfo {
|
|
146
|
-
const first = values[0]!
|
|
147
|
-
if (values.length === 1) {
|
|
148
|
-
return { first, delta: 1, t }
|
|
149
|
-
}
|
|
150
|
-
const delta = (values[values.length - 1]! - first) / (values.length - 1)
|
|
151
|
-
return { first, delta, t }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 可参与等差序列的数字/日期格;公式格与其它类型返回 null */
|
|
155
|
-
function readSeriesNumber(data: CellData | undefined): { value: number; t?: CellType } | null {
|
|
156
|
-
if (!data) return null
|
|
157
|
-
if (data.f != null && data.f !== '') return null
|
|
158
|
-
if (typeof data.v !== 'number' || !Number.isFinite(data.v)) return null
|
|
159
|
-
if (data.t != null && data.t !== 'n' && data.t !== 'd') return null
|
|
160
|
-
return { value: data.v, t: data.t }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function cloneCellData(data: CellData | undefined): CellData | undefined {
|
|
164
|
-
if (!data) return undefined
|
|
165
|
-
const cloned: CellData = {}
|
|
166
|
-
if (data.v !== undefined) cloned.v = data.v
|
|
167
|
-
if (data.t !== undefined) cloned.t = data.t
|
|
168
|
-
if (data.f !== undefined) cloned.f = data.f
|
|
169
|
-
return cloned
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function positiveMod(n: number, mod: number): number {
|
|
173
|
-
return ((n % mod) + mod) % mod
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 公式引用字符串级位移:尊重 `$` 绝对行列;出界 → `#REF!`。
|
|
178
|
-
* 跳过双引号字符串字面量,避免改写文本中的 A1 片段。
|
|
179
|
-
*/
|
|
180
|
-
export function shiftFormulaRefs(formula: string, deltaRow: number, deltaCol: number): string {
|
|
181
|
-
if (deltaRow === 0 && deltaCol === 0) return formula
|
|
182
|
-
|
|
183
|
-
let result = ''
|
|
184
|
-
let i = 0
|
|
185
|
-
while (i < formula.length) {
|
|
186
|
-
const ch = formula[i]!
|
|
187
|
-
if (ch === '"') {
|
|
188
|
-
const end = skipExcelString(formula, i)
|
|
189
|
-
result += formula.slice(i, end)
|
|
190
|
-
i = end
|
|
191
|
-
continue
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const sheetMatch = matchSheetPrefix(formula, i)
|
|
195
|
-
const addrStart = sheetMatch ? sheetMatch.end : i
|
|
196
|
-
const addrMatch = matchCellOrRange(formula, addrStart)
|
|
197
|
-
if (addrMatch && (sheetMatch || isRefBoundary(formula, i - 1))) {
|
|
198
|
-
const sheetPrefix = sheetMatch ? formula.slice(sheetMatch.start, sheetMatch.end) : ''
|
|
199
|
-
// 有 sheet 前缀时,前缀前也需是边界
|
|
200
|
-
if (sheetMatch && !isRefBoundary(formula, sheetMatch.start - 1)) {
|
|
201
|
-
result += ch
|
|
202
|
-
i++
|
|
203
|
-
continue
|
|
204
|
-
}
|
|
205
|
-
const shifted = shiftAddrPart(addrMatch.text, deltaRow, deltaCol)
|
|
206
|
-
result += sheetPrefix + shifted
|
|
207
|
-
i = addrMatch.end
|
|
208
|
-
continue
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
result += ch
|
|
212
|
-
i++
|
|
213
|
-
}
|
|
214
|
-
return result
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function skipExcelString(text: string, start: number): number {
|
|
218
|
-
// start 指向开引号;Excel 字符串内 "" 为转义
|
|
219
|
-
let i = start + 1
|
|
220
|
-
while (i < text.length) {
|
|
221
|
-
if (text[i] === '"') {
|
|
222
|
-
if (text[i + 1] === '"') {
|
|
223
|
-
i += 2
|
|
224
|
-
continue
|
|
225
|
-
}
|
|
226
|
-
return i + 1
|
|
227
|
-
}
|
|
228
|
-
i++
|
|
229
|
-
}
|
|
230
|
-
return text.length
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function matchSheetPrefix(text: string, start: number): { start: number; end: number } | null {
|
|
234
|
-
if (text[start] === "'") {
|
|
235
|
-
let i = start + 1
|
|
236
|
-
while (i < text.length) {
|
|
237
|
-
if (text[i] === "'") {
|
|
238
|
-
if (text[i + 1] === "'") {
|
|
239
|
-
i += 2
|
|
240
|
-
continue
|
|
241
|
-
}
|
|
242
|
-
if (text[i + 1] === '!') {
|
|
243
|
-
return { start, end: i + 2 }
|
|
244
|
-
}
|
|
245
|
-
return null
|
|
246
|
-
}
|
|
247
|
-
i++
|
|
248
|
-
}
|
|
249
|
-
return null
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 简单表名:字母/下划线开头
|
|
253
|
-
if (!/[A-Za-z_]/.test(text[start] ?? '')) return null
|
|
254
|
-
let i = start + 1
|
|
255
|
-
while (i < text.length && /[\w.]/.test(text[i]!)) i++
|
|
256
|
-
if (text[i] === '!') return { start, end: i + 1 }
|
|
257
|
-
return null
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const CELL_REF_RE = /^(\$?)([A-Za-z]+)(\$?)([1-9]\d*)/
|
|
261
|
-
|
|
262
|
-
function matchCellOrRange(text: string, start: number): { text: string; end: number } | null {
|
|
263
|
-
const first = matchOneCell(text, start)
|
|
264
|
-
if (!first) return null
|
|
265
|
-
if (text[first.end] === ':') {
|
|
266
|
-
const second = matchOneCell(text, first.end + 1)
|
|
267
|
-
if (!second) return { text: first.text, end: first.end }
|
|
268
|
-
return { text: text.slice(start, second.end), end: second.end }
|
|
269
|
-
}
|
|
270
|
-
return first
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function matchOneCell(text: string, start: number): { text: string; end: number } | null {
|
|
274
|
-
const slice = text.slice(start)
|
|
275
|
-
const m = CELL_REF_RE.exec(slice)
|
|
276
|
-
if (!m) return null
|
|
277
|
-
const end = start + m[0].length
|
|
278
|
-
const next = text[end]
|
|
279
|
-
// 行号后紧跟字母 → 不是 A1;紧跟 `(` → 函数名(如 LOG10()
|
|
280
|
-
if (next && (/[A-Za-z]/.test(next) || next === '(')) return null
|
|
281
|
-
return { text: m[0], end }
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function isRefBoundary(text: string, index: number): boolean {
|
|
285
|
-
if (index < 0) return true
|
|
286
|
-
const ch = text[index]!
|
|
287
|
-
// 引用前不能是字母/数字(避免 FOOA1);`!` 已由 sheet 前缀消费
|
|
288
|
-
return !/[A-Za-z0-9_$]/.test(ch)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function shiftAddrPart(part: string, deltaRow: number, deltaCol: number): string {
|
|
292
|
-
if (part.includes(':')) {
|
|
293
|
-
const [a, b] = part.split(':')
|
|
294
|
-
const left = shiftOneRef(a!, deltaRow, deltaCol)
|
|
295
|
-
const right = shiftOneRef(b!, deltaRow, deltaCol)
|
|
296
|
-
if (left === '#REF!' || right === '#REF!') return '#REF!'
|
|
297
|
-
return `${left}:${right}`
|
|
298
|
-
}
|
|
299
|
-
return shiftOneRef(part, deltaRow, deltaCol)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function shiftOneRef(ref: string, deltaRow: number, deltaCol: number): string {
|
|
303
|
-
const m = CELL_REF_RE.exec(ref)
|
|
304
|
-
if (!m) return ref
|
|
305
|
-
const colAbs = m[1] === '$'
|
|
306
|
-
const rowAbs = m[3] === '$'
|
|
307
|
-
let col = colNameToIndex(m[2]!)
|
|
308
|
-
let row = Number.parseInt(m[4]!, 10) - 1
|
|
309
|
-
if (col < 0 || row < 0) return '#REF!'
|
|
310
|
-
if (!colAbs) col += deltaCol
|
|
311
|
-
if (!rowAbs) row += deltaRow
|
|
312
|
-
if (col < 0 || row < 0) return '#REF!'
|
|
313
|
-
return `${colAbs ? '$' : ''}${colIndexToName(col)}${rowAbs ? '$' : ''}${row + 1}`
|
|
314
|
-
}
|
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
|
-
}
|