@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.
Files changed (211) hide show
  1. package/dist/index.d.ts +4 -26
  2. package/dist/index.js +3 -23
  3. package/dist/tools/builtin.js +279 -8
  4. package/dist/tools/context.d.ts +54 -11
  5. package/dist/tools/context.js +28 -5
  6. package/dist/tools/download.js +68 -0
  7. package/dist/tools/export.worker.d.ts +16 -0
  8. package/dist/tools/export.worker.js +29 -0
  9. package/dist/tools/registry.d.ts +7 -4
  10. package/dist/tools/registry.js +1 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/sheet.d.ts +6 -5
  13. package/dist/vue/formula-bar.js +393 -0
  14. package/dist/vue/formula-suggest-list.js +66 -0
  15. package/dist/vue/import-file.js +140 -0
  16. package/dist/vue/insert-count-menu-item.js +68 -0
  17. package/dist/vue/insert-image.js +79 -0
  18. package/dist/vue/popup-helpers.js +73 -0
  19. package/dist/vue/popups/border-popup.js +75 -0
  20. package/dist/vue/popups/border-preset-glyph.js +106 -0
  21. package/dist/vue/popups/export-popup.js +51 -0
  22. package/dist/vue/popups/fill-color-popup.js +37 -0
  23. package/dist/vue/popups/find-popup.js +113 -0
  24. package/dist/vue/popups/font-color-popup.js +37 -0
  25. package/dist/vue/popups/font-size-popup.js +63 -0
  26. package/dist/vue/popups/import.worker.d.ts +21 -0
  27. package/dist/vue/popups/import.worker.js +41 -0
  28. package/dist/vue/popups/insert-image-popup.js +38 -0
  29. package/dist/vue/sheet-context-menu.js +200 -0
  30. package/dist/vue/sheet-tabs.js +162 -0
  31. package/dist/vue/sheet-toolbar.js +106 -0
  32. package/dist/vue/sheet.js +144 -164
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +10 -0
  35. package/dist/vue/style2.css +564 -3
  36. package/dist/vue/use-find-replace.js +161 -0
  37. package/dist/vue/use-formula-ref-select.js +37 -0
  38. package/dist/vue/use-formula-suggest.js +126 -0
  39. package/dist/vue/use-sheet-grid.js +180 -0
  40. package/dist/vue/use-sheet-state.js +122 -0
  41. package/dist/vue/use-sheet-tabs-bar.js +90 -0
  42. package/dist/vue/use-tool-groups.js +42 -0
  43. package/dist/vue/use-tool-popup.js +123 -0
  44. package/dist/vue/use-toolbar-scroll.js +72 -0
  45. package/package.json +12 -7
  46. package/src/env.d.ts +11 -0
  47. package/src/index.ts +203 -14
  48. package/src/tools/__test__/builtin.test.ts +208 -66
  49. package/src/tools/__test__/context.test.ts +62 -2
  50. package/src/tools/builtin.ts +350 -6
  51. package/src/tools/context.ts +89 -11
  52. package/src/tools/download.ts +82 -0
  53. package/src/tools/export.worker.ts +61 -0
  54. package/src/tools/registry.ts +16 -1
  55. package/src/types/index.ts +6 -0
  56. package/src/types/sheet.ts +5 -3
  57. package/src/vue/__test__/formula-bar.test.ts +455 -0
  58. package/src/vue/__test__/import-file.test.ts +183 -0
  59. package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
  60. package/src/vue/__test__/insert-image-popup.test.ts +89 -0
  61. package/src/vue/__test__/insert-image.test.ts +100 -0
  62. package/src/vue/__test__/sheet-component.test.ts +716 -27
  63. package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
  64. package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
  65. package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
  66. package/src/vue/formula-bar.vue +473 -0
  67. package/src/vue/formula-suggest-list.vue +64 -0
  68. package/src/vue/import-file.ts +173 -0
  69. package/src/vue/insert-count-menu-item.vue +69 -0
  70. package/src/vue/insert-image.ts +90 -0
  71. package/src/vue/parsing.ts +11 -0
  72. package/src/vue/popup-helpers.ts +50 -0
  73. package/src/vue/popups/border-popup.vue +93 -0
  74. package/src/vue/popups/border-preset-glyph.vue +111 -0
  75. package/src/vue/popups/export-popup.vue +52 -0
  76. package/src/vue/popups/fill-color-popup.vue +39 -0
  77. package/src/vue/popups/find-popup.vue +111 -0
  78. package/src/vue/popups/font-color-popup.vue +39 -0
  79. package/src/vue/popups/font-size-popup.vue +52 -0
  80. package/src/vue/popups/import.worker.ts +66 -0
  81. package/src/vue/popups/insert-image-popup.vue +39 -0
  82. package/src/vue/sheet-context-menu.ts +229 -0
  83. package/src/vue/sheet-tabs.vue +162 -0
  84. package/src/vue/sheet-toolbar.vue +91 -0
  85. package/src/vue/sheet.vue +161 -186
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +11 -0
  88. package/src/vue/use-find-replace.ts +200 -0
  89. package/src/vue/use-formula-ref-select.ts +34 -0
  90. package/src/vue/use-formula-suggest.ts +150 -0
  91. package/src/vue/use-sheet-grid.ts +243 -0
  92. package/src/vue/use-sheet-state.ts +161 -0
  93. package/src/vue/use-sheet-tabs-bar.ts +115 -0
  94. package/src/vue/use-tool-groups.ts +61 -0
  95. package/src/vue/use-tool-popup.ts +150 -0
  96. package/src/vue/use-toolbar-scroll.ts +95 -0
  97. package/dist/core/address.d.ts +0 -60
  98. package/dist/core/address.js +0 -142
  99. package/dist/core/address.js.map +0 -1
  100. package/dist/core/cell-store.d.ts +0 -61
  101. package/dist/core/cell-store.js +0 -135
  102. package/dist/core/cell-store.js.map +0 -1
  103. package/dist/core/command/default-registry.d.ts +0 -10
  104. package/dist/core/command/default-registry.js +0 -18
  105. package/dist/core/command/default-registry.js.map +0 -1
  106. package/dist/core/command/history.d.ts +0 -64
  107. package/dist/core/command/history.js +0 -127
  108. package/dist/core/command/history.js.map +0 -1
  109. package/dist/core/command/merge-cells.d.ts +0 -21
  110. package/dist/core/command/merge-cells.js +0 -74
  111. package/dist/core/command/merge-cells.js.map +0 -1
  112. package/dist/core/command/registry.d.ts +0 -17
  113. package/dist/core/command/registry.js +0 -26
  114. package/dist/core/command/registry.js.map +0 -1
  115. package/dist/core/command/set-cell-formula.d.ts +0 -17
  116. package/dist/core/command/set-cell-formula.js +0 -29
  117. package/dist/core/command/set-cell-formula.js.map +0 -1
  118. package/dist/core/command/set-cell-value.d.ts +0 -22
  119. package/dist/core/command/set-cell-value.js +0 -35
  120. package/dist/core/command/set-cell-value.js.map +0 -1
  121. package/dist/core/command/types.d.ts +0 -68
  122. package/dist/core/events.js +0 -29
  123. package/dist/core/events.js.map +0 -1
  124. package/dist/core/fill.d.ts +0 -34
  125. package/dist/core/fill.js +0 -320
  126. package/dist/core/fill.js.map +0 -1
  127. package/dist/core/formula/ast.d.ts +0 -58
  128. package/dist/core/formula/ast.js +0 -38
  129. package/dist/core/formula/ast.js.map +0 -1
  130. package/dist/core/formula/dependency-graph.d.ts +0 -70
  131. package/dist/core/formula/dependency-graph.js +0 -269
  132. package/dist/core/formula/dependency-graph.js.map +0 -1
  133. package/dist/core/formula/errors.d.ts +0 -25
  134. package/dist/core/formula/errors.js +0 -36
  135. package/dist/core/formula/errors.js.map +0 -1
  136. package/dist/core/formula/evaluator.d.ts +0 -41
  137. package/dist/core/formula/evaluator.js +0 -154
  138. package/dist/core/formula/evaluator.js.map +0 -1
  139. package/dist/core/formula/functions.d.ts +0 -30
  140. package/dist/core/formula/functions.js +0 -209
  141. package/dist/core/formula/functions.js.map +0 -1
  142. package/dist/core/formula/parser.d.ts +0 -7
  143. package/dist/core/formula/parser.js +0 -208
  144. package/dist/core/formula/parser.js.map +0 -1
  145. package/dist/core/formula/tokenizer.d.ts +0 -37
  146. package/dist/core/formula/tokenizer.js +0 -146
  147. package/dist/core/formula/tokenizer.js.map +0 -1
  148. package/dist/core/merge-manager.d.ts +0 -64
  149. package/dist/core/merge-manager.js +0 -111
  150. package/dist/core/merge-manager.js.map +0 -1
  151. package/dist/core/selection.d.ts +0 -31
  152. package/dist/core/selection.js +0 -62
  153. package/dist/core/selection.js.map +0 -1
  154. package/dist/core/sheet.d.ts +0 -116
  155. package/dist/core/sheet.js +0 -224
  156. package/dist/core/sheet.js.map +0 -1
  157. package/dist/core/workbook.d.ts +0 -43
  158. package/dist/core/workbook.js +0 -77
  159. package/dist/core/workbook.js.map +0 -1
  160. package/dist/grid/sheet-grid.d.ts +0 -81
  161. package/dist/grid/sheet-grid.js +0 -331
  162. package/dist/grid/sheet-grid.js.map +0 -1
  163. package/dist/grid/vtable-theme.js +0 -80
  164. package/dist/grid/vtable-theme.js.map +0 -1
  165. package/dist/tools/builtin.js.map +0 -1
  166. package/dist/tools/context.js.map +0 -1
  167. package/dist/tools/registry.js.map +0 -1
  168. package/dist/vue/index.d.ts +0 -2
  169. package/dist/vue/sheet.js.map +0 -1
  170. package/dist/vue/sheet.vue.d.ts +0 -24
  171. package/src/core/__test__/address.test.ts +0 -113
  172. package/src/core/__test__/cell-store.test.ts +0 -97
  173. package/src/core/__test__/command.test.ts +0 -377
  174. package/src/core/__test__/fill.test.ts +0 -140
  175. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  176. package/src/core/__test__/formula-engine.test.ts +0 -470
  177. package/src/core/__test__/formula-functions.test.ts +0 -178
  178. package/src/core/__test__/formula-parser.test.ts +0 -160
  179. package/src/core/__test__/history.test.ts +0 -225
  180. package/src/core/__test__/merge-manager.test.ts +0 -106
  181. package/src/core/__test__/sheet.test.ts +0 -142
  182. package/src/core/address.ts +0 -166
  183. package/src/core/cell-store.ts +0 -173
  184. package/src/core/command/default-registry.ts +0 -15
  185. package/src/core/command/history.ts +0 -167
  186. package/src/core/command/index.ts +0 -7
  187. package/src/core/command/merge-cells.ts +0 -74
  188. package/src/core/command/registry.ts +0 -30
  189. package/src/core/command/set-cell-formula.ts +0 -29
  190. package/src/core/command/set-cell-value.ts +0 -38
  191. package/src/core/command/types.ts +0 -73
  192. package/src/core/events.ts +0 -37
  193. package/src/core/fill.ts +0 -314
  194. package/src/core/formula/ast.ts +0 -66
  195. package/src/core/formula/dependency-graph.ts +0 -329
  196. package/src/core/formula/errors.ts +0 -41
  197. package/src/core/formula/evaluator.ts +0 -219
  198. package/src/core/formula/functions.ts +0 -266
  199. package/src/core/formula/index.ts +0 -7
  200. package/src/core/formula/parser.ts +0 -198
  201. package/src/core/formula/tokenizer.ts +0 -160
  202. package/src/core/merge-manager.ts +0 -158
  203. package/src/core/selection.ts +0 -66
  204. package/src/core/sheet.ts +0 -282
  205. package/src/core/workbook.ts +0 -99
  206. package/src/grid/__test__/canvas-mock.ts +0 -49
  207. package/src/grid/__test__/setup.ts +0 -3
  208. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  209. package/src/grid/__test__/sheet-grid.test.ts +0 -275
  210. package/src/grid/sheet-grid.ts +0 -407
  211. package/src/grid/vtable-theme.ts +0 -61
@@ -1,266 +0,0 @@
1
- import type { AstNode } from './ast'
2
- import { formulaError, isFormulaError, type FormulaError } from './errors'
3
- import {
4
- coerceToBoolean,
5
- coerceToNumber,
6
- coerceToText,
7
- type EvalValue,
8
- type ScalarValue
9
- } from './evaluator'
10
-
11
- /**
12
- * 函数注册表(可扩展)+ 基础函数集。
13
- *
14
- * 参数形态约定:区域引用求值为数组(只含稀疏存在的格),直接参数为标量。
15
- * 聚合函数据此区分 Excel 语义:区域内的文本/布尔被忽略,直接参数则强转(非法 → #VALUE!)。
16
- * lazy 函数自行求值参数(IF 的短路分支,未选分支的副作用/错误不产生)。
17
- */
18
-
19
- export type FormulaFunction =
20
- | { kind?: 'normal'; minArgs?: number; maxArgs?: number; impl: (args: EvalValue[]) => EvalValue }
21
- | {
22
- kind: 'lazy'
23
- minArgs?: number
24
- maxArgs?: number
25
- impl: (nodes: AstNode[], evalNode: (node: AstNode) => EvalValue) => EvalValue
26
- }
27
-
28
- const registry = new Map<string, FormulaFunction>()
29
-
30
- /** 注册函数(名称大小写不敏感;同名覆盖,供扩展/自定义函数) */
31
- export function registerFormulaFunction(name: string, def: FormulaFunction): void {
32
- registry.set(name.toUpperCase(), def)
33
- }
34
-
35
- /** 查询函数(大小写不敏感) */
36
- export function getFormulaFunction(name: string): FormulaFunction | undefined {
37
- return registry.get(name.toUpperCase())
38
- }
39
-
40
- /** 求值器回调:名称解析 + 参数个数校验 + 按 kind 分发 */
41
- export function invokeFormulaFunction(
42
- name: string,
43
- nodes: AstNode[],
44
- evalNode: (node: AstNode) => EvalValue
45
- ): EvalValue {
46
- const def = getFormulaFunction(name)
47
- if (!def) return formulaError('#NAME?')
48
- if (def.minArgs !== undefined && nodes.length < def.minArgs) return formulaError('#VALUE!')
49
- if (def.maxArgs !== undefined && nodes.length > def.maxArgs) return formulaError('#VALUE!')
50
- if (def.kind === 'lazy') return def.impl(nodes, evalNode)
51
- return def.impl(nodes.map(evalNode))
52
- }
53
-
54
- // ─── 内部工具 ────────────────────────────────────────────────
55
-
56
- interface FlatArg {
57
- value: ScalarValue | FormulaError
58
- /** 来自区域展开(区别于直接参数:区域内文本/布尔被聚合函数忽略) */
59
- fromRange: boolean
60
- }
61
-
62
- function flattenArgs(args: EvalValue[]): FlatArg[] {
63
- const out: FlatArg[] = []
64
- for (const arg of args) {
65
- if (Array.isArray(arg)) {
66
- for (const value of arg) out.push({ value, fromRange: true })
67
- } else {
68
- out.push({ value: arg, fromRange: false })
69
- }
70
- }
71
- return out
72
- }
73
-
74
- /** 收集数字:区域内只取数字格;直接参数强转(非法/错误 → 传播) */
75
- function collectNumbers(args: EvalValue[]): number[] | FormulaError {
76
- const numbers: number[] = []
77
- for (const { value, fromRange } of flattenArgs(args)) {
78
- if (isFormulaError(value)) return value
79
- if (fromRange) {
80
- if (typeof value === 'number') numbers.push(value)
81
- continue
82
- }
83
- const n = coerceToNumber(value)
84
- if (isFormulaError(n)) return n
85
- numbers.push(n)
86
- }
87
- return numbers
88
- }
89
-
90
- /** 收集布尔:区域内只取布尔格;直接参数强转(非法文本/错误 → 传播);空值跳过 */
91
- function collectBooleans(args: EvalValue[]): boolean[] | FormulaError {
92
- const booleans: boolean[] = []
93
- for (const { value, fromRange } of flattenArgs(args)) {
94
- if (isFormulaError(value)) return value
95
- if (fromRange) {
96
- if (typeof value === 'boolean') booleans.push(value)
97
- continue
98
- }
99
- if (value === null) continue
100
- const b = coerceToBoolean(value)
101
- if (isFormulaError(b)) return b
102
- booleans.push(b)
103
- }
104
- return booleans
105
- }
106
-
107
- // ─── 基础函数集 ─────────────────────────────────────────────
108
-
109
- registerFormulaFunction('SUM', {
110
- minArgs: 1,
111
- impl(args) {
112
- const numbers = collectNumbers(args)
113
- if (isFormulaError(numbers)) return numbers
114
- let sum = 0
115
- for (const n of numbers) sum += n
116
- return sum
117
- }
118
- })
119
-
120
- registerFormulaFunction('AVERAGE', {
121
- minArgs: 1,
122
- impl(args) {
123
- const numbers = collectNumbers(args)
124
- if (isFormulaError(numbers)) return numbers
125
- if (numbers.length === 0) return formulaError('#DIV/0!')
126
- let sum = 0
127
- for (const n of numbers) sum += n
128
- return sum / numbers.length
129
- }
130
- })
131
-
132
- registerFormulaFunction('MAX', {
133
- minArgs: 1,
134
- impl(args) {
135
- const numbers = collectNumbers(args)
136
- if (isFormulaError(numbers)) return numbers
137
- return numbers.length === 0 ? 0 : Math.max(...numbers)
138
- }
139
- })
140
-
141
- registerFormulaFunction('MIN', {
142
- minArgs: 1,
143
- impl(args) {
144
- const numbers = collectNumbers(args)
145
- if (isFormulaError(numbers)) return numbers
146
- return numbers.length === 0 ? 0 : Math.min(...numbers)
147
- }
148
- })
149
-
150
- registerFormulaFunction('COUNT', {
151
- minArgs: 1,
152
- impl(args) {
153
- let count = 0
154
- for (const { value, fromRange } of flattenArgs(args)) {
155
- if (isFormulaError(value)) return value
156
- if (fromRange) {
157
- if (typeof value === 'number') count++
158
- continue
159
- }
160
- // 直接参数:可强转为数字即计数(含 TRUE/FALSE、数字文本)
161
- if (!isFormulaError(coerceToNumber(value))) count++
162
- }
163
- return count
164
- }
165
- })
166
-
167
- registerFormulaFunction('COUNTA', {
168
- minArgs: 1,
169
- impl(args) {
170
- let count = 0
171
- for (const { value, fromRange } of flattenArgs(args)) {
172
- // 直接错误参数传播;区域内的错误格照常计数
173
- if (!fromRange && isFormulaError(value)) return value
174
- if (value === null) continue
175
- count++
176
- }
177
- return count
178
- }
179
- })
180
-
181
- registerFormulaFunction('IF', {
182
- kind: 'lazy',
183
- minArgs: 2,
184
- maxArgs: 3,
185
- impl(nodes, evalNode) {
186
- const cond = coerceToBoolean(evalNode(nodes[0]!))
187
- if (isFormulaError(cond)) return cond
188
- if (cond) return evalNode(nodes[1]!)
189
- return nodes[2] ? evalNode(nodes[2]) : false
190
- }
191
- })
192
-
193
- registerFormulaFunction('AND', {
194
- minArgs: 1,
195
- impl(args) {
196
- const booleans = collectBooleans(args)
197
- if (isFormulaError(booleans)) return booleans
198
- if (booleans.length === 0) return formulaError('#VALUE!')
199
- return booleans.every(Boolean)
200
- }
201
- })
202
-
203
- registerFormulaFunction('OR', {
204
- minArgs: 1,
205
- impl(args) {
206
- const booleans = collectBooleans(args)
207
- if (isFormulaError(booleans)) return booleans
208
- if (booleans.length === 0) return formulaError('#VALUE!')
209
- return booleans.some(Boolean)
210
- }
211
- })
212
-
213
- registerFormulaFunction('NOT', {
214
- minArgs: 1,
215
- maxArgs: 1,
216
- impl(args) {
217
- const b = coerceToBoolean(args[0]!)
218
- if (isFormulaError(b)) return b
219
- return !b
220
- }
221
- })
222
-
223
- registerFormulaFunction('ROUND', {
224
- minArgs: 2,
225
- maxArgs: 2,
226
- impl(args) {
227
- const n = coerceToNumber(args[0]!)
228
- if (isFormulaError(n)) return n
229
- const d = coerceToNumber(args[1]!)
230
- if (isFormulaError(d)) return d
231
- const factor = 10 ** Math.trunc(d)
232
- // 位数超出 double 精度时四舍五入是恒等/归零
233
- if (!Number.isFinite(factor)) return n
234
- if (factor === 0) return 0
235
- // 远离零方向取整;1e-12 补偿浮点表示误差(如 2.675 存为 2.67499…)
236
- const shifted = n * factor
237
- const rounded = Math.sign(shifted) * Math.round(Math.abs(shifted) + 1e-12)
238
- return rounded / factor
239
- }
240
- })
241
-
242
- registerFormulaFunction('ABS', {
243
- minArgs: 1,
244
- maxArgs: 1,
245
- impl(args) {
246
- const n = coerceToNumber(args[0]!)
247
- if (isFormulaError(n)) return n
248
- return Math.abs(n)
249
- }
250
- })
251
-
252
- registerFormulaFunction('CONCATENATE', {
253
- minArgs: 1,
254
- impl(args) {
255
- let text = ''
256
- for (const arg of args) {
257
- const values = Array.isArray(arg) ? arg : [arg]
258
- for (const value of values) {
259
- const piece = coerceToText(value)
260
- if (isFormulaError(piece)) return piece
261
- text += piece
262
- }
263
- }
264
- return text
265
- }
266
- })
@@ -1,7 +0,0 @@
1
- export * from './errors'
2
- export * from './ast'
3
- export * from './tokenizer'
4
- export * from './parser'
5
- export * from './evaluator'
6
- export * from './functions'
7
- export * from './dependency-graph'
@@ -1,198 +0,0 @@
1
- import { createRange, parseAddress, type CellAddress } from '../address'
2
- import type { AstNode, BinaryOperator } from './ast'
3
- import { FormulaParseError, tokenizeFormula, type FormulaToken } from './tokenizer'
4
-
5
- /**
6
- * Pratt parser:token 序列 → AST。
7
- *
8
- * 优先级(同 Excel,低到高):比较运算 < `&` < `+ -` < `* /` < 一元 `+ -` < `^`(右结合)< `%`。
9
- * 一元运算紧于幂次是 Excel 的特有行为:-2^2 = (-2)^2 = 4。
10
- * 引用消歧:ident 后随 `(` → 函数调用;后随 `!` → 表名前缀;匹配单元格形态 → 引用;
11
- * TRUE/FALSE → 布尔字面量;其余 → 未知名称节点(求值 #NAME?)。
12
- */
13
-
14
- /** 中缀运算符绑定力 [左, 右];右结合运算符右绑定力 = 左 - 1 */
15
- const INFIX_BP: Record<string, readonly [number, number]> = {
16
- '=': [1, 1],
17
- '<>': [1, 1],
18
- '<': [1, 1],
19
- '<=': [1, 1],
20
- '>': [1, 1],
21
- '>=': [1, 1],
22
- '&': [2, 2],
23
- '+': [3, 3],
24
- '-': [3, 3],
25
- '*': [4, 4],
26
- '/': [4, 4],
27
- '^': [6, 5]
28
- }
29
- /** 百分号后缀绑定力(最高) */
30
- const PERCENT_BP = 7
31
- /** 一元 +/- 操作数的绑定力:紧于 ^(6),松于 %(7) */
32
- const UNARY_OPERAND_BP = 6
33
-
34
- /** 单元格引用形态(列限 3 字母;超出按未知名称处理,如 Sheet2) */
35
- const CELL_REF_RE = /^\$?[A-Za-z]{1,3}\$?[1-9]\d*$/
36
-
37
- class Parser {
38
- private pos = 0
39
-
40
- constructor(private readonly tokens: FormulaToken[]) {}
41
-
42
- parse(): AstNode {
43
- const node = this.parseExpression(0)
44
- if (this.pos < this.tokens.length) {
45
- throw new FormulaParseError(`意外的多余输入: ${describeToken(this.tokens[this.pos]!)}`)
46
- }
47
- return node
48
- }
49
-
50
- private peek(): FormulaToken | undefined {
51
- return this.tokens[this.pos]
52
- }
53
-
54
- private next(): FormulaToken | undefined {
55
- return this.tokens[this.pos++]
56
- }
57
-
58
- private peekOp(op: string): boolean {
59
- const token = this.peek()
60
- return token?.type === 'op' && token.op === op
61
- }
62
-
63
- private expectOp(op: string): void {
64
- if (!this.peekOp(op)) {
65
- throw new FormulaParseError(`期望 "${op}",得到 ${describeToken(this.peek())}`)
66
- }
67
- this.pos++
68
- }
69
-
70
- private parseExpression(minBp: number): AstNode {
71
- let left = this.parsePrimary()
72
- for (;;) {
73
- const token = this.peek()
74
- if (token?.type !== 'op') break
75
- // 百分号后缀
76
- if (token.op === '%') {
77
- if (PERCENT_BP <= minBp) break
78
- this.pos++
79
- left = { kind: 'percent', operand: left }
80
- continue
81
- }
82
- const bp = INFIX_BP[token.op]
83
- if (!bp || bp[0] <= minBp) break
84
- this.pos++
85
- const right = this.parseExpression(bp[1])
86
- left = { kind: 'binary', op: token.op as BinaryOperator, left, right }
87
- }
88
- return left
89
- }
90
-
91
- private parsePrimary(): AstNode {
92
- const token = this.next()
93
- if (!token) throw new FormulaParseError('表达式不完整')
94
-
95
- if (token.type === 'number') return { kind: 'number', value: token.value }
96
- if (token.type === 'string') return { kind: 'string', value: token.value }
97
-
98
- if (token.type === 'quoted-name') {
99
- // 带引号表名只能作为跨表引用前缀
100
- this.expectOp('!')
101
- return this.parseCellRefOrRange(token.name)
102
- }
103
-
104
- if (token.type === 'ident') {
105
- const name = token.name
106
- if (this.peekOp('(')) return this.parseCall(name)
107
- if (this.peekOp('!')) {
108
- this.pos++
109
- return this.parseCellRefOrRange(name)
110
- }
111
- const addr = parseCellRef(name)
112
- if (addr) return this.parseRangeTail({ sheet: undefined, addr })
113
- if (name.toUpperCase() === 'TRUE') return { kind: 'boolean', value: true }
114
- if (name.toUpperCase() === 'FALSE') return { kind: 'boolean', value: false }
115
- return { kind: 'name', name }
116
- }
117
-
118
- // 运算符位置:括号 / 一元 +-
119
- if (token.type === 'op') {
120
- if (token.op === '(') {
121
- const inner = this.parseExpression(0)
122
- this.expectOp(')')
123
- return inner
124
- }
125
- if (token.op === '-' || token.op === '+') {
126
- return { kind: 'unary', op: token.op, operand: this.parseExpression(UNARY_OPERAND_BP) }
127
- }
128
- }
129
- throw new FormulaParseError(`意外的 ${describeToken(token)}`)
130
- }
131
-
132
- /** 函数调用:ident 已消费,当前 token 为 '(' */
133
- private parseCall(name: string): AstNode {
134
- this.pos++ // 消费 '('
135
- const args: AstNode[] = []
136
- if (!this.peekOp(')')) {
137
- for (;;) {
138
- args.push(this.parseExpression(0))
139
- if (this.peekOp(',')) {
140
- this.pos++
141
- continue
142
- }
143
- break
144
- }
145
- }
146
- this.expectOp(')')
147
- return { kind: 'call', name, args }
148
- }
149
-
150
- /** 表名前缀(Sheet2! / 'My Sheet'!)之后的单元格或区域引用 */
151
- private parseCellRefOrRange(sheet: string): AstNode {
152
- const token = this.next()
153
- const addr = token?.type === 'ident' ? parseCellRef(token.name) : null
154
- if (!addr) {
155
- throw new FormulaParseError(`表名 "${sheet}" 后应为单元格引用,得到 ${describeToken(token)}`)
156
- }
157
- return this.parseRangeTail({ sheet, addr })
158
- }
159
-
160
- /** 单元格引用之后的可选区域后缀(:B9) */
161
- private parseRangeTail(ref: { sheet: string | undefined; addr: CellAddress }): AstNode {
162
- if (!this.peekOp(':')) return { kind: 'cell', sheet: ref.sheet, addr: ref.addr }
163
- this.pos++
164
- const token = this.next()
165
- const end = token?.type === 'ident' ? parseCellRef(token.name) : null
166
- if (!end) {
167
- throw new FormulaParseError(`区域 ":" 后应为单元格引用,得到 ${describeToken(token)}`)
168
- }
169
- return { kind: 'range', sheet: ref.sheet, range: createRange(ref.addr, end) }
170
- }
171
- }
172
-
173
- /** 按单元格引用形态解析(列限 3 字母);不匹配返回 null */
174
- function parseCellRef(name: string): CellAddress | null {
175
- if (!CELL_REF_RE.test(name)) return null
176
- return parseAddress(name)
177
- }
178
-
179
- function describeToken(token: FormulaToken | undefined): string {
180
- if (!token) return '输入末尾'
181
- switch (token.type) {
182
- case 'number':
183
- return `数字 ${token.value}`
184
- case 'string':
185
- return `字符串 "${token.value}"`
186
- case 'ident':
187
- return `名称 "${token.name}"`
188
- case 'quoted-name':
189
- return `表名 '${token.name}'`
190
- case 'op':
191
- return `"${token.op}"`
192
- }
193
- }
194
-
195
- /** 解析公式文本(不含 '=')→ AST;非法输入抛 FormulaParseError */
196
- export function parseFormula(text: string): AstNode {
197
- return new Parser(tokenizeFormula(text)).parse()
198
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * 公式分词器:数字 / 字符串 / 布尔 / 标识符(函数名、引用、表名)/ 带引号表名 / 运算符。
3
- *
4
- * 不做语义消歧:`A1`(引用)、`SUM`(函数名)、`Sheet2`(表名)统一产出 ident,
5
- * 由 parser 依据后随 token(`(` / `!` / `:` / 运算位)判定。
6
- */
7
-
8
- /** 解析失败异常(Sheet 层捕获 → 单元格记为 #ERROR!) */
9
- export class FormulaParseError extends Error {
10
- constructor(message: string) {
11
- super(message)
12
- this.name = 'FormulaParseError'
13
- }
14
- }
15
-
16
- export type FormulaOperator =
17
- | '+'
18
- | '-'
19
- | '*'
20
- | '/'
21
- | '^'
22
- | '&'
23
- | '%'
24
- | '('
25
- | ')'
26
- | ','
27
- | '!'
28
- | ':'
29
- | '='
30
- | '<>'
31
- | '<'
32
- | '<='
33
- | '>'
34
- | '>='
35
-
36
- export type FormulaToken =
37
- | { type: 'number'; value: number }
38
- | { type: 'string'; value: string }
39
- /** 标识符:函数名 / 单元格引用形态 / 裸表名(含 $ 绝对引用写法);TRUE/FALSE 由 parser 归约为布尔 */
40
- | { type: 'ident'; name: string }
41
- /** 带单引号的表名('' 转义为字面单引号) */
42
- | { type: 'quoted-name'; name: string }
43
- | { type: 'op'; op: FormulaOperator }
44
-
45
- const NUMBER_RE = /^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/
46
-
47
- /** 标识符首字符:字母 / _ / $ / 非 ASCII(中文表名等) */
48
- function isIdentStart(ch: string): boolean {
49
- return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) >= 0x80
50
- }
51
-
52
- /** 标识符后续字符:首字符集 + 数字 + . */
53
- function isIdentPart(ch: string): boolean {
54
- return /[A-Za-z0-9_.$]/.test(ch) || ch.charCodeAt(0) >= 0x80
55
- }
56
-
57
- const TWO_CHAR_OPS = ['<>', '<=', '>='] as const
58
- const ONE_CHAR_OPS = [
59
- '+',
60
- '-',
61
- '*',
62
- '/',
63
- '^',
64
- '&',
65
- '%',
66
- '(',
67
- ')',
68
- ',',
69
- '!',
70
- ':',
71
- '=',
72
- '<',
73
- '>'
74
- ] as const
75
-
76
- /** 公式文本(不含 '=')→ token 序列;非法输入抛 FormulaParseError */
77
- export function tokenizeFormula(text: string): FormulaToken[] {
78
- const tokens: FormulaToken[] = []
79
- let i = 0
80
- while (i < text.length) {
81
- const ch = text[i]!
82
- // 空白忽略(不支持 Excel 的空格交集运算符)
83
- if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
84
- i++
85
- continue
86
- }
87
- // 数字
88
- if ((ch >= '0' && ch <= '9') || (ch === '.' && text[i + 1]! >= '0' && text[i + 1]! <= '9')) {
89
- const match = NUMBER_RE.exec(text.slice(i))
90
- if (!match) throw new FormulaParseError(`非法数字(位置 ${i + 1})`)
91
- tokens.push({ type: 'number', value: Number.parseFloat(match[0]) })
92
- i += match[0].length
93
- continue
94
- }
95
- // 字符串字面量("...","" 转义)
96
- if (ch === '"') {
97
- let value = ''
98
- let j = i + 1
99
- for (;;) {
100
- if (j >= text.length) throw new FormulaParseError('字符串缺少结束引号')
101
- if (text[j] === '"') {
102
- if (text[j + 1] === '"') {
103
- value += '"'
104
- j += 2
105
- continue
106
- }
107
- break
108
- }
109
- value += text[j]
110
- j++
111
- }
112
- tokens.push({ type: 'string', value })
113
- i = j + 1
114
- continue
115
- }
116
- // 带引号表名('...','' 转义)
117
- if (ch === "'") {
118
- let name = ''
119
- let j = i + 1
120
- for (;;) {
121
- if (j >= text.length) throw new FormulaParseError('表名缺少结束引号')
122
- if (text[j] === "'") {
123
- if (text[j + 1] === "'") {
124
- name += "'"
125
- j += 2
126
- continue
127
- }
128
- break
129
- }
130
- name += text[j]
131
- j++
132
- }
133
- tokens.push({ type: 'quoted-name', name })
134
- i = j + 1
135
- continue
136
- }
137
- // 标识符(函数名 / 引用 / 裸表名)
138
- if (isIdentStart(ch)) {
139
- let j = i + 1
140
- while (j < text.length && isIdentPart(text[j]!)) j++
141
- tokens.push({ type: 'ident', name: text.slice(i, j) })
142
- i = j
143
- continue
144
- }
145
- // 双字符运算符优先
146
- const two = text.slice(i, i + 2)
147
- if ((TWO_CHAR_OPS as readonly string[]).includes(two)) {
148
- tokens.push({ type: 'op', op: two as FormulaOperator })
149
- i += 2
150
- continue
151
- }
152
- if ((ONE_CHAR_OPS as readonly string[]).includes(ch)) {
153
- tokens.push({ type: 'op', op: ch as FormulaOperator })
154
- i++
155
- continue
156
- }
157
- throw new FormulaParseError(`无法识别的字符 "${ch}"(位置 ${i + 1})`)
158
- }
159
- return tokens
160
- }