@rockn/vue-formula-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/FormulaEditor.d.ts +70 -0
- package/dist/engine/evaluator.d.ts +14 -0
- package/dist/engine/format.d.ts +19 -0
- package/dist/engine/index.cjs +7 -0
- package/dist/engine/index.d.ts +26 -0
- package/dist/engine/index.js +1383 -0
- package/dist/engine/lexer.d.ts +2 -0
- package/dist/engine/parser.d.ts +21 -0
- package/dist/engine/types.d.ts +182 -0
- package/dist/index.cjs +39 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +1537 -0
- package/dist/style.css +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rockn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# @rockn/vue-formula-editor
|
|
2
|
+
|
|
3
|
+
可复用的 **Vue 3 公式编辑器组件** + 零依赖公式引擎。IF/ELSE 块状逻辑、点选变量与函数、**枚举标签-值**、**键值对成员访问**、语法高亮、实时求值与安全校验。引擎可脱离 Vue 单独使用,一套引擎同时服务编辑器与你的业务代码。
|
|
4
|
+
|
|
5
|
+
  
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 特性
|
|
10
|
+
|
|
11
|
+
- **块状 IF / ELSE IF / ELSE**:`IF 条件 THEN 真值 ELSE IF 条件 THEN 真值 … ELSE 假值`,贴近自然语言;也支持中文关键字 `如果 / 那么 / 否则 / 否则如果`
|
|
12
|
+
- **函数式兼容**:`IF(条件, 真值, 假值)`、`IFS(...)`、`a ? b : c`、中缀 `AND / OR` 依旧可用
|
|
13
|
+
- **枚举变量(标签 ↔ 值)**:公式里写/显示**标签**(`{会员等级} == '金卡'`),求值返回**代码值**(如 `GOLD`),比较时标签与值双向可匹配;变量面板可点选标签
|
|
14
|
+
- **键值对变量(成员访问)**:`{订单}.金额`、`{订单}['金额']`、`{订单}.商品[0]`,链式访问;字段定义优先、context 键兜底校验
|
|
15
|
+
- **47 个内置函数**:逻辑、数字、文本(含 `CONTAINS`/`REPLACE`/`SPLIT` 等)、类型转换、日期、聚合统计
|
|
16
|
+
- **悬停提示**:变量(类型/必填/描述/当前值/枚举标签/键值对字段)、函数(签名/参数说明/示例)、关键字、运算符
|
|
17
|
+
- **函数面板**:点选插入任意内置函数,占位参数自动选中
|
|
18
|
+
- **编辑体验**:变量/函数点选插入、`{变量}` 整块删除(仅光标在花括号内)、Enter 自动缩进 + 行尾内联 ELSE 拆行、一键格式化、括号配对高亮、当前行高亮
|
|
19
|
+
- **实时求值**:通过 `context` 传测试值,输入即算(击键防抖),结果实时显示
|
|
20
|
+
- **安全引擎**:递归下降解析 + AST 求值,无 `eval` / `new Function`,类型严格检查,错误带原文偏移
|
|
21
|
+
- **零依赖引擎**:`engine` 子路径无任何依赖,可在 Node / 其他框架直接使用
|
|
22
|
+
- **自定义函数**:`functions` prop 注入业务函数,`functionSpecs` prop 提供签名提示
|
|
23
|
+
- **双主题**:dark / light,全部样式用 `--fe-*` CSS 变量,可覆盖
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @rockn/vue-formula-editor
|
|
29
|
+
# 或
|
|
30
|
+
pnpm add @rockn/vue-formula-editor
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
### 1. 作为 Vue 组件使用
|
|
36
|
+
|
|
37
|
+
```vue
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
import { ref, reactive } from 'vue'
|
|
40
|
+
import { FormulaEditor } from '@rockn/vue-formula-editor'
|
|
41
|
+
import type { VariableDef } from '@rockn/vue-formula-editor'
|
|
42
|
+
|
|
43
|
+
const variables: VariableDef[] = [
|
|
44
|
+
{ name: '订单金额', type: 'number', description: '订单总金额(元)', required: true },
|
|
45
|
+
{ name: '会员等级', type: 'enum', description: '用户会员等级',
|
|
46
|
+
options: [
|
|
47
|
+
{ label: '金卡', value: 'GOLD' },
|
|
48
|
+
{ label: '黑金', value: 'BLACK' },
|
|
49
|
+
] },
|
|
50
|
+
{ name: '订单', type: 'object', description: '订单对象',
|
|
51
|
+
fields: [{ key: '金额' }, { key: '收货人' }] },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const formula = ref(`IF {会员等级} == '金卡' OR {会员等级} == '黑金' THEN
|
|
55
|
+
ROUND({订单金额} * 0.85, 2)
|
|
56
|
+
ELSE
|
|
57
|
+
{订单金额}`)
|
|
58
|
+
|
|
59
|
+
// 测试值:枚举存代码值,公式写标签
|
|
60
|
+
const context = reactive({
|
|
61
|
+
订单金额: 520,
|
|
62
|
+
会员等级: 'GOLD',
|
|
63
|
+
订单: { 金额: 520, 收货人: '张三' },
|
|
64
|
+
})
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<FormulaEditor
|
|
69
|
+
v-model="formula"
|
|
70
|
+
:variables="variables"
|
|
71
|
+
:context="context"
|
|
72
|
+
title="折扣规则"
|
|
73
|
+
subtitle="RULE FORMULA"
|
|
74
|
+
/>
|
|
75
|
+
</template>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> 组件样式已内联(`<style scoped>`),无需额外引入 CSS。若使用独立产物,可 `import '@rockn/vue-formula-editor/dist/style.css'`。
|
|
79
|
+
|
|
80
|
+
### 2. 单独使用引擎(不依赖 Vue)
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { evaluateFormula, validateFormula } from '@rockn/vue-formula-editor/engine'
|
|
84
|
+
|
|
85
|
+
// 枚举变量:传变量元数据即可做标签-值解析
|
|
86
|
+
const variables = [
|
|
87
|
+
{ name: '会员等级', type: 'enum', options: [{ label: '金卡', value: 'GOLD' }] },
|
|
88
|
+
]
|
|
89
|
+
const result = evaluateFormula(
|
|
90
|
+
`IF {会员等级} == '金卡' THEN {金额} - 20 ELSE {金额}`,
|
|
91
|
+
{ 金额: 520, 会员等级: 'GOLD' },
|
|
92
|
+
{},
|
|
93
|
+
variables,
|
|
94
|
+
)
|
|
95
|
+
if (result.ok) {
|
|
96
|
+
console.log(result.value) // 500
|
|
97
|
+
} else {
|
|
98
|
+
console.error(result.error.message, result.error.start, result.error.end)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 静态校验(未定义变量 / 字段存在性 / 函数参数个数)
|
|
102
|
+
const check = validateFormula(`{订单}.金额 > 0`, [
|
|
103
|
+
{ name: '订单', type: 'object', fields: [{ key: '金额' }] },
|
|
104
|
+
])
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. Node / CommonJS
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const { evaluateFormula } = require('@rockn/vue-formula-editor/engine')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Props
|
|
114
|
+
|
|
115
|
+
| Prop | 类型 | 默认 | 说明 |
|
|
116
|
+
| --- | --- | --- | --- |
|
|
117
|
+
| `modelValue` | `string` | `''` | 公式文本(配合 `v-model`) |
|
|
118
|
+
| `variables` | `VariableDef[]` | `[]` | 变量定义(支持枚举 / 键值对) |
|
|
119
|
+
| `context` | `Record<string, unknown>` | `{}` | 测试值,用于实时求值 |
|
|
120
|
+
| `functions` | `Record<string, (args: unknown[]) => unknown>` | `{}` | 自定义函数(参数已求值) |
|
|
121
|
+
| `functionSpecs` | `Record<string, CustomFunctionSpec>` | `{}` | 自定义函数文档(悬停提示/函数面板) |
|
|
122
|
+
| `title` / `subtitle` | `string` | 默认 | 头部标题 |
|
|
123
|
+
| `placeholder` | `string` | 默认提示 | 占位文案 |
|
|
124
|
+
| `height` | `string` | `'340px'` | 编辑区最小高度 |
|
|
125
|
+
| `disabled` / `readonly` | `boolean` | `false` | 禁用 / 只读 |
|
|
126
|
+
| `showVariablesPanel` | `boolean` | `true` | 显示变量面板 |
|
|
127
|
+
| `showToolbar` | `boolean` | `true` | 显示语法快捷工具条 |
|
|
128
|
+
| `showPreview` | `boolean` | `true` | 显示实时求值条 |
|
|
129
|
+
| `theme` | `'dark' \| 'light'` | `'dark'` | 主题 |
|
|
130
|
+
|
|
131
|
+
## Events
|
|
132
|
+
|
|
133
|
+
| 事件 | 参数 | 说明 |
|
|
134
|
+
| --- | --- | --- |
|
|
135
|
+
| `update:modelValue` | `string` | 内容变化(v-model) |
|
|
136
|
+
| `change` | `string` | 内容变化(与上面同时触发) |
|
|
137
|
+
| `error` | `ValidationResult` | 校验结果变化(`{ ok, errors }`) |
|
|
138
|
+
| `insert` | `{ type, value }` | 插入变量/函数/文本时触发 |
|
|
139
|
+
| `update:theme` | `'dark' \| 'light'` | 主题切换 |
|
|
140
|
+
|
|
141
|
+
## 暴露的实例方法(ref)
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const editor = ref<InstanceType<typeof FormulaEditor>>()
|
|
145
|
+
// editor.value.validate() → ValidationResult
|
|
146
|
+
// editor.value.evaluate(ctx?) → EvalResult
|
|
147
|
+
// editor.value.insertText('{金额}') → 在光标处插入
|
|
148
|
+
// editor.value.focus()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 公式语法速览
|
|
152
|
+
|
|
153
|
+
| 语法 | 示例 | 说明 |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| 变量 | `{订单金额}` | 花括号包裹,支持中文名 |
|
|
156
|
+
| 成员访问 | `{订单}.金额` / `{订单}['金额']` / `{订单}.商品[0]` | 键值对取字段,可链式 |
|
|
157
|
+
| 枚举比较 | `{会员等级} == '金卡'` | 写标签、算代码值,双向可匹配 |
|
|
158
|
+
| 字面量 | `1.5` `'文本'` `true` `null` `undefined` | 数字/字符串/布尔/空 |
|
|
159
|
+
| 算术 | `+ - * / % **` | `**` 为幂运算,右结合 |
|
|
160
|
+
| 比较 | `< <= > >= == != === !==` | `==` 宽松(`'5' == 5` 为真) |
|
|
161
|
+
| 逻辑 | `AND OR && \|\| !` | 中缀 `AND`/`OR` 大小写不敏感 |
|
|
162
|
+
| 块状条件 | `IF 条件 THEN 真值 ELSE 假值` | 支持多级 `ELSE IF`,短路求值 |
|
|
163
|
+
| 中文条件 | `如果 条件 那么 真值 否则 假值` | `否则如果` 为 else-if |
|
|
164
|
+
| 函数 | `ROUND({金额} * 0.9, 2)` | 参数逗号分隔 |
|
|
165
|
+
|
|
166
|
+
## 文档
|
|
167
|
+
|
|
168
|
+
- [内置函数表](docs/functions.md)
|
|
169
|
+
- [公式语法完整参考](docs/syntax.md)
|
|
170
|
+
- [引擎独立使用(Node / 其他框架)](docs/engine.md)
|
|
171
|
+
- [组件 API 参考](docs/component-api.md)
|
|
172
|
+
- [主题定制](docs/theming.md)
|
|
173
|
+
- [FAQ / 常见问题](docs/faq.md)
|
|
174
|
+
|
|
175
|
+
## 本地开发
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm install
|
|
179
|
+
npm run dev # 演示页 http://localhost:5162
|
|
180
|
+
npm run build # 类型检查 + 演示打包
|
|
181
|
+
npm run build:lib # 组件库打包(dist/,供发布)
|
|
182
|
+
npm run typecheck # 仅类型检查
|
|
183
|
+
npm run smoke # 引擎冒烟测试(152 项断言)
|
|
184
|
+
npm pack # 生成可发布的 tgz(会自动 build:lib)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 发布
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npm login # 登录你的 npm 账号(包名为 @rockn/vue-formula-editor)
|
|
191
|
+
npm run build:lib # 构建组件库
|
|
192
|
+
npm publish --access public
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
> 注意:Windows + Node 24 下 `fs.rmSync` 与 `vite-plugin-dts` 存在原生崩溃(STATUS_STACK_BUFFER_OVERRUN)。本项目构建脚本已改用异步 `rm` 规避,声明文件由 `vue-tsc` + 手写 `types/` 生成。
|
|
196
|
+
|
|
197
|
+
## 许可
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { DefineComponent } from 'vue'
|
|
2
|
+
import type { FunctionSpec, ValidationResult, EvalResult, VariableDef } from './engine'
|
|
3
|
+
|
|
4
|
+
/** 自定义函数的文档规格(悬停提示 / 函数面板使用) */
|
|
5
|
+
export interface CustomFunctionSpec {
|
|
6
|
+
signature: string
|
|
7
|
+
description: string
|
|
8
|
+
params?: string[]
|
|
9
|
+
example?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FormulaEditorProps {
|
|
13
|
+
/** v-model:公式文本 */
|
|
14
|
+
modelValue?: string
|
|
15
|
+
/** 可选变量列表 */
|
|
16
|
+
variables?: VariableDef[]
|
|
17
|
+
/** 测试值上下文:变量名 -> 值,用于实时求值 */
|
|
18
|
+
context?: Record<string, unknown>
|
|
19
|
+
/** 自定义函数扩展(可覆盖内置函数) */
|
|
20
|
+
functions?: Record<string, (args: unknown[]) => unknown>
|
|
21
|
+
/** 自定义函数的文档规格(悬停提示 / 函数面板使用;key 与 functions 同名) */
|
|
22
|
+
functionSpecs?: Record<string, CustomFunctionSpec>
|
|
23
|
+
/** 标题 */
|
|
24
|
+
title?: string
|
|
25
|
+
subtitle?: string
|
|
26
|
+
placeholder?: string
|
|
27
|
+
/** 编辑区高度 */
|
|
28
|
+
height?: string
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
readonly?: boolean
|
|
31
|
+
/** 是否显示变量面板 */
|
|
32
|
+
showVariablesPanel?: boolean
|
|
33
|
+
/** 是否显示模板工具条 */
|
|
34
|
+
showToolbar?: boolean
|
|
35
|
+
/** 是否显示底部实时求值条 */
|
|
36
|
+
showPreview?: boolean
|
|
37
|
+
/** 主题:dark(深色)/ light(浅色) */
|
|
38
|
+
theme?: 'dark' | 'light'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FormulaEditorExpose {
|
|
42
|
+
/** 手动校验,返回 { ok, errors, ast } */
|
|
43
|
+
validate: () => ValidationResult
|
|
44
|
+
/** 手动求值(可传自定义测试值,缺省用 props.context) */
|
|
45
|
+
evaluate: (ctx?: Record<string, unknown>) => EvalResult
|
|
46
|
+
/** 在光标处插入文本并聚焦 */
|
|
47
|
+
insertText: (fragment: string) => void
|
|
48
|
+
/** 聚焦编辑器 */
|
|
49
|
+
focus: () => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FormulaEditorEmits {
|
|
53
|
+
(e: 'update:modelValue', value: string): void
|
|
54
|
+
(e: 'change', value: string): void
|
|
55
|
+
(e: 'error', result: ValidationResult): void
|
|
56
|
+
(e: 'insert', payload: { type: 'variable' | 'function' | 'text'; value: string }): void
|
|
57
|
+
(e: 'update:theme', theme: 'dark' | 'light'): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const FormulaEditor: DefineComponent<
|
|
61
|
+
FormulaEditorProps,
|
|
62
|
+
{},
|
|
63
|
+
{},
|
|
64
|
+
{},
|
|
65
|
+
{},
|
|
66
|
+
{},
|
|
67
|
+
{},
|
|
68
|
+
FormulaEditorEmits,
|
|
69
|
+
FormulaEditorExpose
|
|
70
|
+
>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExprNode, FunctionSpec, VariableDef } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 安全求值器:基于 AST 递归求值,无 eval / new Function。
|
|
4
|
+
* 所有运算都做类型检查,错误携带原文中的偏移位置,便于编辑器定位。
|
|
5
|
+
*/
|
|
6
|
+
export declare class RuntimeError extends Error {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
constructor(message: string, start: number, end: number);
|
|
10
|
+
}
|
|
11
|
+
/** 自定义函数:参数已求值(兼容传入变量元数据后的扩展形态) */
|
|
12
|
+
export type FnMap = Record<string, (args: unknown[]) => unknown>;
|
|
13
|
+
export declare function evaluate(node: ExprNode, context: Record<string, unknown>, functions?: FnMap, variables?: VariableDef[]): unknown;
|
|
14
|
+
export declare const BUILTIN_FUNCTIONS: Record<string, FunctionSpec>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 公式格式化:解析感知的完整表达式排版。
|
|
3
|
+
*
|
|
4
|
+
* 规则:
|
|
5
|
+
* - 先尝试解析公式;解析成功时按 AST 重新排版:
|
|
6
|
+
* - 块状 IF:单行内联 `IF 条件 THEN 真值 ELSE 假值` → 展开为多行块;
|
|
7
|
+
* 每级缩进 2 空格,ELSE IF 与所属 IF 对齐,行尾内联 ELSE 自动拆行
|
|
8
|
+
* - 二元运算:`+ - * / % ** == != < <= > >= && ||` 两侧加空格
|
|
9
|
+
* - 逗号后加空格、三元 `? :` 两侧加空格、一元运算符紧贴操作数
|
|
10
|
+
* - 字符串/数字/变量/成员访问/布尔/空字面量:按原文保留(不改引号、不改成员访问的点)
|
|
11
|
+
* - 运算符原文保留(`AND`/`and`/`==`/`===` 等保持用户写法)
|
|
12
|
+
* - 解析失败(语法错误)时回退到行级缩进逻辑,保证不完整公式也能整理缩进。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 格式化公式文本。
|
|
16
|
+
* @param src 原始公式
|
|
17
|
+
* @returns 按块状 IF 层级与运算符空格重排后的公式
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatFormula(src: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";var P=Object.defineProperty;var V=(e,t,r)=>t in e?P(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var x=(e,t,r)=>V(e,typeof t!="symbol"?t+"":t,r);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const _=["===","!=="],Y=["==","!=","<=",">=","&&","||","**"],G="+-*/%<>!?=,():[]";function K(e){return e.replace(/\\u([0-9a-fA-F]{4})/g,(t,r)=>String.fromCharCode(parseInt(r,16))).replace(/\\(.)/g,(t,r)=>r==="n"?`
|
|
2
|
+
`:r==="t"?" ":r==="r"?"\r":r)}function C(e){const t=[],r=e.length;let n=0;const s=(i,l,u,m,p=!0)=>t.push({type:i,value:l,start:u,end:m,raw:e.slice(u,m),closed:p});for(;n<r;){const i=e[n];if(/\s/.test(i)){n++;continue}if(i==="{"){const m=n;let p=n+1,f=!1;for(;p<r&&e[p]!=="}";)p++;p<r&&(f=!0);const o=e.slice(n+1,p),N=f?p+1:p;s("variable",o,m,N,f),n=N;continue}if(/[0-9]/.test(i)||i==="."&&/[0-9]/.test(e[n+1]??"")){const m=/^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(e.slice(n));if(m){s("number",m[0],n,n+m[0].length),n+=m[0].length;continue}}if(i==="."){s("dot",i,n,n+1),n++;continue}if(i==='"'||i==="'"){const m=n,p=i;let f=n+1,o=!1,N=!1;for(;f<r;){const a=e[f];if(N)N=!1;else if(a==="\\")N=!0;else if(a===p){o=!0,f++;break}else if(a===`
|
|
3
|
+
`)break;f++}const g=e.slice(m+1,o?f-1:f);s("string",o?K(g):g,m,f,o),n=f;continue}if(/[A-Za-z_\u4e00-\u9fa5]/.test(i)){const m=/^[A-Za-z_\u4e00-\u9fa5][A-Za-z0-9_\u4e00-\u9fa5]*/.exec(e.slice(n));if(m){s("identifier",m[0],n,n+m[0].length),n+=m[0].length;continue}}const l=e.slice(n,n+3);if(_.includes(l)){s("operator",l,n,n+3),n+=3;continue}const u=e.slice(n,n+2);if(Y.includes(u)){s("operator",u,n,n+2),n+=2;continue}if(G.includes(i)){s(i==="("?"lparen":i===")"?"rparen":i==="["?"lbracket":i==="]"?"rbracket":i===","?"comma":i==="?"?"question":i===":"?"colon":"operator",i,n,n+1),n++;continue}s("error",i,n,n+1),n++}return t.push({type:"eof",value:"",start:r,end:r,raw:""}),t}class E extends Error{constructor(r,n,s){super(r);x(this,"start");x(this,"end");this.name="ParseError",this.start=n,this.end=s}}const M={OR:1,"||":1,AND:2,"&&":2,"==":3,"!=":3,"===":3,"!==":3,"<":4,"<=":4,">":4,">=":4,"+":5,"-":5,"*":6,"/":6,"%":6,"**":7};function d(e,...t){if(!e||e.type!=="identifier")return!1;for(const r of t)if(e.value===r||/^[A-Za-z]+$/.test(e.value)&&/^[A-Za-z]+$/.test(r)&&e.value.toUpperCase()===r.toUpperCase())return!0;return!1}function q(e){return d(e,"IF","如果")&&e!==void 0}function U(e){const t=C(e);let r=0;const n=(a=0)=>t[r+a]??t[t.length-1],s=()=>t[r++];function i(a,c){const b=n();if(b.type!==a)throw new E(c,b.start,b.end);return s()}function l(a=0){let c=f();for(;;){const b=n(),h=b.type==="operator"?b.value:b.type==="identifier"?b.value.toUpperCase():void 0;if(h!==void 0&&M[h]!==void 0){const I=M[h];if(I<a)break;s();const O=l(h==="**"?I:I+1);c={type:"binary",op:h,left:c,right:O,start:c.start,end:O.end}}else if(b.type==="question"&&a===0){s();const I=l(0);i("colon",'三元表达式缺少 ":" 分隔符');const O=l(0);c={type:"conditional",test:c,consequent:I,alternate:O,start:c.start,end:O.end}}else break}return c}function u(a){const c=[];let b=null;if(n().type==="eof"||d(n(),"THEN","那么","ELSE","否则","ELSEIF","否则如果"))throw new E(`IF/如果 后缺少条件表达式(遇到 "${n().raw}")`,n().start,n().end);let h=l(0);m();let I=l(0);c.push({condition:h,body:I});let O=I.end;for(;;){const L=n();if(d(L,"ELSE","否则","ELSEIF","否则如果")){if(s(),d(L,"ELSEIF","否则如果")||d(n(),"IF","如果")&&n(1).type!=="lparen"){if(d(L,"ELSEIF","否则如果")||s(),n().type==="eof"||d(n(),"THEN","那么","ELSE","否则","ELSEIF","否则如果"))throw new E(`ELSE IF/否则如果 后缺少条件表达式(遇到 "${n().raw}")`,n().start,n().end);h=l(0),m(),I=l(0),c.push({condition:h,body:I}),O=I.end;continue}if(n().type==="eof")throw new E("ELSE/否则 后缺少表达式",L.start,L.end);b=l(0),O=b.end;break}break}return{type:"if",branches:c,elseBody:b,start:a.start,end:O}}function m(){const a=n();if(!d(a,"THEN","那么"))throw new E(`IF 条件后缺少 "THEN/那么"(遇到 "${a.raw}",块状 IF 写法:IF 条件 THEN 真值 ELSE 假值)`,a.start,a.end);s()}function p(a){let c=a;for(;;){const b=n();if(b.type==="dot"){s();const h=n();if(h.type!=="identifier")throw new E(`"." 后应为成员名,遇到 "${h.raw}"`,h.start,h.end);s(),c={type:"member",object:c,prop:h.value,index:null,start:c.start,end:h.end};continue}if(b.type==="lbracket"){s();const h=l(0),I=i("rbracket",'缺少右括号 "]"');h.type==="string"?c={type:"member",object:c,prop:h.value,index:null,start:c.start,end:I.end}:c={type:"member",object:c,prop:"",index:h,start:c.start,end:I.end};continue}break}return c}function f(){const a=n();if(a.type==="operator"&&(a.value==="-"||a.value==="+"||a.value==="!")){s();const c=f();return{type:"unary",op:a.value,operand:c,start:a.start,end:c.end}}return p(o())}function o(){const a=n();if(a.type==="number")return s(),{type:"number",value:parseFloat(a.value),start:a.start,end:a.end};if(a.type==="string"){if(s(),!a.closed)throw new E(`字符串未闭合(缺少 ${a.raw[0]??"引号"})`,a.start,a.end);return{type:"string",value:a.value,start:a.start,end:a.end}}if(a.type==="variable"){if(s(),!a.closed)throw new E('变量缺少结束符 "}"',a.start,a.end);if(!a.value.trim())throw new E("变量名不能为空,例如 {订单金额}",a.start,a.end);return{type:"variable",name:a.value.trim(),start:a.start,end:a.end}}if(a.type==="identifier"){s();const c=a.value.toLowerCase();if(c==="true"||c==="false")return{type:"boolean",value:c==="true",start:a.start,end:a.end};if(c==="null"||c==="undefined")return{type:"null",start:a.start,end:a.end};if(q(a)&&n().type!=="lparen")return u(a);if(d(a,"THEN","那么"))throw new E('意外的 "THEN/那么"(缺少与之配对的 IF?)',a.start,a.end);if(d(a,"ELSE","否则","ELSEIF","否则如果"))throw new E('意外的 "ELSE/否则"(缺少与之配对的 IF?)',a.start,a.end);if(n().type==="lparen"){s();const b=[];if(n().type!=="rparen")for(b.push(l(0));n().type==="comma";)s(),b.push(l(0));const h=i("rparen",`函数 ${a.value}() 缺少右括号 ")"`);return{type:"call",name:a.value,args:b,start:a.start,end:h.end}}throw new E(`无法识别的标识符 "${a.value}"(变量请用 {名称} 引用;函数需要括号调用)`,a.start,a.end)}if(a.type==="lparen"){s();const c=l(0);return i("rparen",'缺少右括号 ")"'),c}throw a.type==="eof"?new E("表达式不完整,结尾缺少操作数",a.start,a.end):a.type==="operator"&&a.value==="="?new E('单个 "=" 不能用于比较,请使用 "==" 或 "==="',a.start,a.end):a.type==="operator"&&(a.value===")"||a.value==="]")?new E(`多余的 "${a.value}"`,a.start,a.end):a.type==="rbracket"?new E('多余的 "]"',a.start,a.end):a.type==="dot"?new E('"." 前缺少被访问的表达式',a.start,a.end):a.type==="comma"?new E('多余的逗号 ","',a.start,a.end):new E(`意外的符号 "${a.raw}"`,a.start,a.end)}const N=l(0),g=n();if(g.type!=="eof")throw g.type==="operator"&&g.value==="="?new E('单个 "=" 不能用于比较,请使用 "==" 或 "==="',g.start,g.end):new E(`意外的符号 "${g.raw}"(表达式已结束)`,g.start,g.end);return N}class A extends Error{constructor(r,n,s){super(r);x(this,"start");x(this,"end");this.name="RuntimeError",this.start=n,this.end=s}}function X(e){const t=new Map;for(const r of e??[]){if(r.type!=="enum"||!r.options)continue;const n=new Set,s=new Set,i=new Map;for(const l of r.options){const u=typeof l=="string"?{label:l,value:l}:l;n.add(u.label),s.add(u.value),i.set(u.label,u.value)}t.set(r.name,{labels:n,values:s,labelToValue:i})}return{byName:t}}function w(e){return e==null?"空值 NULL":typeof e=="number"?`数字 ${e}`:typeof e=="string"?`字符串 "${e.length>12?e.slice(0,12)+"…":e}"`:typeof e=="boolean"?`布尔值 ${e}`:Array.isArray(e)?`数组(${e.length} 项)`:typeof e=="object"?`对象(${Object.keys(e).length} 个键)`:"其他"}function k(e,t){if(typeof e=="number"&&!Number.isNaN(e))return e;throw new A(`${t} 需要数字,实际为 ${w(e)}`,0,0)}function S(e,t,r){if(typeof e=="number"&&!Number.isNaN(e))return e;if(typeof e=="string"&&e.trim()!==""&&!Number.isNaN(Number(e)))return Number(e);throw new A(`函数 ${t}() 的第 ${r+1} 个参数需要数字,实际为 ${w(e)}`,0,0)}function B(e){return typeof e=="number"&&!Number.isNaN(e)?e:typeof e=="string"&&e.trim()!==""&&!Number.isNaN(Number(e))?Number(e):0}function R(e,t){if(e===t)return!0;if(e==null||t===null||t===void 0)return!1;if(typeof e=="boolean"&&(e=e?1:0),typeof t=="boolean"&&(t=t?1:0),typeof e=="number"&&typeof t=="string"){const r=Number(t);return!Number.isNaN(r)&&e===r}if(typeof t=="number"&&typeof e=="string"){const r=Number(e);return!Number.isNaN(r)&&r===t}return!1}function z(e,t,r,n,s,i){const l=(u,m)=>s?Object.is(u,m):R(u,m);if(e.type==="variable"){const u=i.byName.get(e.name);if(u){const m=u.labelToValue.has(String(r))?u.labelToValue.get(String(r)):r;let p=n;return typeof n=="string"&&u.labelToValue.has(n)&&(p=u.labelToValue.get(n)),l(m,p)}}if(t.type==="variable"){const u=i.byName.get(t.name);if(u){const m=u.labelToValue.has(String(n))?u.labelToValue.get(String(n)):n;let p=r;return typeof r=="string"&&u.labelToValue.has(r)&&(p=u.labelToValue.get(r)),l(p,m)}}return null}function Z(e,t,r){let n,s;if(typeof t=="number"&&typeof r=="number")n=t,s=r;else if(typeof t=="string"&&typeof r=="string")n=t,s=r;else{const i=typeof t=="boolean"?t?1:0:typeof t=="string"?Number(t):t,l=typeof r=="boolean"?r?1:0:typeof r=="string"?Number(r):r;if(typeof i=="number"&&!Number.isNaN(i)||typeof l=="number"&&!Number.isNaN(l))if(typeof i=="number"&&typeof l=="number"&&!Number.isNaN(i)&&!Number.isNaN(l))n=i,s=l;else throw new A(`无法比较 ${w(t)} 与 ${w(r)}`,0,0);else throw new A(`无法比较 ${w(t)} 与 ${w(r)}`,0,0)}switch(e){case"<":return n<s;case"<=":return n<=s;case">":return n>s;case">=":return n>=s;default:return!1}}function J(e,t){return e===t?`${e}`:t===1/0?`至少 ${e}`:`${e}~${t}`}function Q(e,t,r,n){const s=y(e.object,t,r,n);let i;if(e.index!==null?i=y(e.index,t,r,n):i=e.prop,s==null)throw new A(`无法访问 ${String(i)}:左侧是 ${w(s)}`,e.start,e.end);if(Array.isArray(s)){const l=typeof i=="number"?i:Number(i);if(!Number.isInteger(l)||l<0||l>=s.length)throw new A(`数组下标 ${String(i)} 越界(长度 ${s.length})`,e.start,e.end);return s[l]}if(typeof s=="object"){const l=String(i);if(!(l in s))throw new A(`对象中不存在字段 "${l}"`,e.start,e.end);return s[l]}throw new A(`无法访问 ${String(i)}:${w(s)} 不是对象或数组`,e.start,e.end)}function H(e,t,r={},n){return y(e,t,r,X(n))}function y(e,t,r,n){switch(e.type){case"number":return e.value;case"string":return e.value;case"boolean":return e.value;case"null":return null;case"variable":{if(!(e.name in t))throw new A(`变量 {${e.name}} 未定义,请先在测试值中填写`,e.start,e.end);return t[e.name]}case"member":return Q(e,t,r,n);case"unary":{const s=y(e.operand,t,r,n);if(e.op==="!")return!s;if(e.op==="-")return-k(s,'一元 "-"');if(e.op==="+")return+k(s,'一元 "+"');throw new A(`未知一元运算符 "${e.op}"`,e.start,e.end)}case"binary":{if(e.op==="&&"||e.op==="AND")return y(e.left,t,r,n)?!!y(e.right,t,r,n):!1;if(e.op==="||"||e.op==="OR")return y(e.left,t,r,n)?!0:!!y(e.right,t,r,n);if(e.op==="=="||e.op==="!="||e.op==="==="||e.op==="!=="){const l=y(e.left,t,r,n),u=y(e.right,t,r,n),m=e.op==="==="||e.op==="!==",p=e.op==="!="||e.op==="!==",f=z(e.left,e.right,l,u,m,n),o=f!==null?f:m?Object.is(l,u):R(l,u);return p?!o:o}const s=y(e.left,t,r,n),i=y(e.right,t,r,n);switch(e.op){case"<":case"<=":case">":case">=":return Z(e.op,s,i);case"+":{if(typeof s=="string"||typeof i=="string")return String(s)+String(i);if(typeof s=="number"&&typeof i=="number")return s+i;throw new A(`"+" 需要数字或字符串操作数,得到 ${w(s)} 与 ${w(i)}`,e.start,e.end)}case"-":case"*":case"/":case"%":case"**":{if(typeof s!="number"||typeof i!="number")throw new A(`"${e.op}" 需要数字操作数,得到 ${w(s)} 与 ${w(i)}`,e.start,e.end);if(e.op==="/"&&i===0)throw new A("除法运算中除数不能为 0",e.start,e.end);if(e.op==="%"&&i===0)throw new A("取余运算中除数不能为 0",e.start,e.end);switch(e.op){case"-":return s-i;case"*":return s*i;case"/":return s/i;case"%":return s%i;case"**":return Math.pow(s,i);default:throw new A(`未知运算符 "${e.op}"`,e.start,e.end)}}default:throw new A(`未知运算符 "${e.op}"`,e.start,e.end)}}case"conditional":{const s=y(e.test,t,r,n);return y(s?e.consequent:e.alternate,t,r,n)}case"if":{for(const s of e.branches)if(y(s.condition,t,r,n))return y(s.body,t,r,n);return e.elseBody?y(e.elseBody,t,r,n):null}case"call":{const s=e.name,i=r[s],l=$[s];if(!i&&!l)throw new A(`未知函数 ${s}()(内置函数:${Object.keys($).join("、")})`,e.start,e.end);if(s==="IF"){if(e.args.length!==3)throw new A(`函数 IF() 需要 3 个参数:IF(条件, 真值, 假值),实际 ${e.args.length} 个`,e.start,e.end);const m=y(e.args[0],t,r,n);return y(m?e.args[1]:e.args[2],t,r,n)}if(s==="IFS"){if(e.args.length<3)throw new A("函数 IFS() 至少需要 3 个参数:IFS(条件1, 值1, 条件2, 值2, …, 默认值)",e.start,e.end);const m=e.args.length,p=(m-1)/2;if(!Number.isInteger(p))throw new A("函数 IFS() 参数应为“成对的条件/值”,最后可跟一个默认值",e.start,e.end);for(let f=0;f<p;f++)if(y(e.args[f*2],t,r,n))return y(e.args[f*2+1],t,r,n);return y(e.args[m-1],t,r,n)}const u=e.args.map(m=>y(m,t,r,n));if(l){if(u.length<l.minArgs||u.length>l.maxArgs)throw new A(`函数 ${s}() 需要 ${J(l.minArgs,l.maxArgs)} 个参数,实际 ${u.length} 个`,e.start,e.end);return l.fn(u)}return i(u)}default:{const s=e;throw new A("无法求值的表达式",s.start,s.end)}}}function ee(e){return e==null?"":String(e)}function j(e,t,r){if(Array.isArray(e))return e;throw new A(`函数 ${t}() 的第 ${r+1} 个参数需要数组,实际为 ${w(e)}`,0,0)}function v(e,t,r){if(e!==null&&typeof e=="object"&&!Array.isArray(e))return e;throw new A(`函数 ${t}() 的第 ${r+1} 个参数需要对象,实际为 ${w(e)}`,0,0)}const $={IF:{name:"IF",signature:"IF(条件, 真值, 假值)",description:"条件判断:条件成立返回真值,否则返回假值(短路求值)。",minArgs:3,maxArgs:3,params:["条件:布尔表达式","真值:条件成立时返回","假值:条件不成立时返回"],example:"IF({年龄} >= 18, '成年', '未成年')",fn:()=>null},IFS:{name:"IFS",signature:"IFS(条件1, 值1, 条件2, 值2, …, 默认值)",description:"多级条件判断:依次判断,命中即返回对应值;都不命中返回默认值。",minArgs:3,maxArgs:1/0,params:["条件/值成对出现","…","默认值(可选)"],example:"IFS({等级} == '黑金', 0.8, {等级} == '金卡', 0.85, 1)",fn:()=>null},AND:{name:"AND",signature:"AND(条件1, 条件2, …)",description:"逻辑与:全部为真才返回 true。",minArgs:1,maxArgs:1/0,params:["条件:布尔表达式,至少 1 个"],example:"AND({金额} > 100, {包邮})",fn:e=>e.every(Boolean)},OR:{name:"OR",signature:"OR(条件1, 条件2, …)",description:"逻辑或:任一为真即返回 true。",minArgs:1,maxArgs:1/0,params:["条件:布尔表达式,至少 1 个"],example:"OR({等级} == '金卡', {等级} == '黑金')",fn:e=>e.some(Boolean)},NOT:{name:"NOT",signature:"NOT(条件)",description:"逻辑非:取反。",minArgs:1,maxArgs:1,params:["条件:布尔表达式"],example:"NOT({包邮})",fn:e=>!e[0]},IFNULL:{name:"IFNULL",signature:"IFNULL(值, 兜底值)",description:"值为 NULL / 未定义时返回兜底值,否则返回值本身。",minArgs:2,maxArgs:2,params:["值:待判断的值","兜底值:为空时返回"],example:"IFNULL({优惠券码}, '无优惠')",fn:e=>e[0]===null||e[0]===void 0?e[1]:e[0]},COALESCE:{name:"COALESCE",signature:"COALESCE(值1, 值2, …)",description:"返回第一个非 NULL 的值;全部为空返回 NULL。",minArgs:1,maxArgs:1/0,params:["值列表:从前往后取第一个非空"],example:"COALESCE({备注}, {优惠券码}, '无')",fn:e=>{for(const t of e)if(t!=null)return t;return null}},SWITCH:{name:"SWITCH",signature:"SWITCH(值, 情况1, 结果1, …, 默认值)",description:"按值匹配:与某个情况相等则返回对应结果;都不匹配返回默认值(可选)。",minArgs:2,maxArgs:1/0,params:["值:待匹配值","情况/结果成对出现","…","默认值(可选)"],example:"SWITCH({会员等级}, '金卡', 0.85, '黑金', 0.8, 1)",fn:e=>{const t=e[0],r=e.slice(1),n=Math.floor(r.length/2);for(let s=0;s<n;s++)if(R(t,r[s*2]))return r[s*2+1];return r.length%2===1?r[r.length-1]:null}},ABS:{name:"ABS",signature:"ABS(数字)",description:"取绝对值。",minArgs:1,maxArgs:1,params:["数字"],example:"ABS(-9)",fn:e=>Math.abs(S(e[0],"ABS",0))},ROUND:{name:"ROUND",signature:"ROUND(数字[, 小数位])",description:"四舍五入到指定小数位(默认 0 位)。",minArgs:1,maxArgs:2,params:["数字","小数位(可选,默认 0)"],example:"ROUND(3.14159, 2)",fn:e=>{const t=S(e[0],"ROUND",0),r=e.length>1?S(e[1],"ROUND",1):0,n=Math.pow(10,r);return Math.round(t*n)/n}},FLOOR:{name:"FLOOR",signature:"FLOOR(数字)",description:"向下取整。",minArgs:1,maxArgs:1,params:["数字"],example:"FLOOR(3.7)",fn:e=>Math.floor(S(e[0],"FLOOR",0))},CEIL:{name:"CEIL",signature:"CEIL(数字)",description:"向上取整。",minArgs:1,maxArgs:1,params:["数字"],example:"CEIL(3.2)",fn:e=>Math.ceil(S(e[0],"CEIL",0))},MIN:{name:"MIN",signature:"MIN(数字1, 数字2, …)",description:"取最小值。",minArgs:1,maxArgs:1/0,params:["数字列表"],example:"MIN({金额}, 100)",fn:e=>Math.min(...e.map((t,r)=>S(t,"MIN",r)))},MAX:{name:"MAX",signature:"MAX(数字1, 数字2, …)",description:"取最大值。",minArgs:1,maxArgs:1/0,params:["数字列表"],example:"MAX({金额}, 0)",fn:e=>Math.max(...e.map((t,r)=>S(t,"MAX",r)))},SUM:{name:"SUM",signature:"SUM(数字1, 数字2, …)",description:"求数值总和(忽略非数字参数)。",minArgs:1,maxArgs:1/0,params:["数字列表(可引用数组变量)"],example:"SUM(1, 2, 3)",fn:e=>e.reduce((t,r)=>t+B(r),0)},AVERAGE:{name:"AVERAGE",signature:"AVERAGE(数字1, 数字2, …)",description:"求数值平均值(忽略非数字参数)。",minArgs:1,maxArgs:1/0,params:["数字列表"],example:"AVERAGE(2, 4, 6)",fn:e=>{const t=[];for(const r of e)typeof r=="number"?t.push(r):typeof r=="string"&&r.trim()!==""&&!Number.isNaN(Number(r))&&t.push(Number(r));return t.length?t.reduce((r,n)=>r+n,0)/t.length:0}},CONCAT:{name:"CONCAT",signature:"CONCAT(文本1, 文本2, …)",description:"拼接多个值为一个字符串。",minArgs:1,maxArgs:1/0,params:["值列表(非文本会转成字符串)"],example:"CONCAT('订单#', {订单编号})",fn:e=>e.map(t=>String(t??"")).join("")},LEN:{name:"LEN",signature:"LEN(文本)",description:"返回文本长度(按字符数)。",minArgs:1,maxArgs:1,params:["文本"],example:"LEN('你好abc')",fn:e=>String(e[0]??"").length},UPPER:{name:"UPPER",signature:"UPPER(文本)",description:"转大写。",minArgs:1,maxArgs:1,params:["文本"],example:"UPPER('ab')",fn:e=>String(e[0]??"").toUpperCase()},LOWER:{name:"LOWER",signature:"LOWER(文本)",description:"转小写。",minArgs:1,maxArgs:1,params:["文本"],example:"LOWER('AB')",fn:e=>String(e[0]??"").toLowerCase()},TRIM:{name:"TRIM",signature:"TRIM(文本)",description:"去除首尾空白。",minArgs:1,maxArgs:1,params:["文本"],example:"TRIM(' hi ')",fn:e=>String(e[0]??"").trim()},SUBSTR:{name:"SUBSTR",signature:"SUBSTR(文本, 起始下标[, 长度])",description:'截取子串(0 起始,如 SUBSTR("hello", 1, 3) = "ell")。',minArgs:2,maxArgs:3,params:["文本","起始下标(0 起始)","长度(可选)"],example:"SUBSTR('hello', 1, 3)",fn:e=>{const t=String(e[0]??""),r=S(e[1],"SUBSTR",1),n=e.length>2?S(e[2],"SUBSTR",2):void 0;return n===void 0?t.slice(r):t.slice(r,r+n)}},MID:{name:"MID",signature:"MID(文本, 起始位, 长度)",description:'Excel 风格截取(1 起始,如 MID("hello", 2, 3) = "ell")。',minArgs:3,maxArgs:3,params:["文本","起始位(1 起始)","长度"],example:"MID('hello', 2, 3)",fn:e=>{const t=String(e[0]??""),r=S(e[1],"MID",1),n=S(e[2],"MID",2);return t.slice(r-1,r-1+n)}},LEFT:{name:"LEFT",signature:"LEFT(文本, 长度)",description:"取左侧 N 个字符。",minArgs:2,maxArgs:2,params:["文本","长度"],example:"LEFT('hello', 2)",fn:e=>String(e[0]??"").slice(0,S(e[1],"LEFT",1))},RIGHT:{name:"RIGHT",signature:"RIGHT(文本, 长度)",description:"取右侧 N 个字符。",minArgs:2,maxArgs:2,params:["文本","长度"],example:"RIGHT('hello', 2)",fn:e=>String(e[0]??"").slice(-S(e[1],"RIGHT",1))},CONTAINS:{name:"CONTAINS",signature:"CONTAINS(文本, 子串)",description:"文本是否包含子串(大小写敏感)。",minArgs:2,maxArgs:2,params:["文本","子串"],example:"CONTAINS({优惠券码}, 'FM')",fn:e=>String(e[0]??"").includes(String(e[1]??""))},STARTSWITH:{name:"STARTSWITH",signature:"STARTSWITH(文本, 前缀)",description:"文本是否以指定前缀开头。",minArgs:2,maxArgs:2,params:["文本","前缀"],example:"STARTSWITH({订单编号}, 'A')",fn:e=>String(e[0]??"").startsWith(String(e[1]??""))},ENDSWITH:{name:"ENDSWITH",signature:"ENDSWITH(文本, 后缀)",description:"文本是否以指定后缀结尾。",minArgs:2,maxArgs:2,params:["文本","后缀"],example:"ENDSWITH({订单编号}, '8')",fn:e=>String(e[0]??"").endsWith(String(e[1]??""))},INDEXOF:{name:"INDEXOF",signature:"INDEXOF(文本, 子串[, 起始下标])",description:"返回子串首次出现的位置(0 起始),未找到返回 -1。",minArgs:2,maxArgs:3,params:["文本","子串","起始下标(可选,默认 0)"],example:"INDEXOF('hello', 'l')",fn:e=>{const t=String(e[0]??""),r=String(e[1]??""),n=e.length>2?S(e[2],"INDEXOF",2):0;return t.indexOf(r,n)}},REPLACE:{name:"REPLACE",signature:"REPLACE(文本, 被替换, 替换为)",description:"把文本中所有匹配的子串替换为新内容。",minArgs:3,maxArgs:3,params:["文本","被替换的子串","替换为"],example:"REPLACE('a-b-c', '-', '/')",fn:e=>String(e[0]??"").split(String(e[1]??"")).join(String(e[2]??""))},SPLIT:{name:"SPLIT",signature:"SPLIT(文本, 分隔符)",description:"按分隔符拆分为数组。",minArgs:2,maxArgs:2,params:["文本","分隔符"],example:"SPLIT('a,b,c', ',')",fn:e=>String(e[0]??"").split(String(e[1]??""))},TOSTRING:{name:"TOSTRING",signature:"TOSTRING(值)",description:"转字符串(NULL 转空串)。",minArgs:1,maxArgs:1,params:["值"],example:"TOSTRING({金额})",fn:e=>ee(e[0])},TONUMBER:{name:"TONUMBER",signature:"TONUMBER(值)",description:"转数字;无法转换时报错。",minArgs:1,maxArgs:1,params:["值(数字或数字字符串)"],example:"TONUMBER('42')",fn:e=>S(e[0],"TONUMBER",0)},TOBOOLEAN:{name:"TOBOOLEAN",signature:"TOBOOLEAN(值)",description:'转布尔:布尔原样;数字非 0 为 true;字符串 "true"/"false"(大小写不敏感)转换,其他为 true(非空)。',minArgs:1,maxArgs:1,params:["值"],example:"TOBOOLEAN('true')",fn:e=>{const t=e[0];if(typeof t=="boolean")return t;if(typeof t=="number")return t!==0;if(typeof t=="string"){const r=t.trim().toLowerCase();return r==="true"?!0:r==="false"?!1:t.trim()!==""}return t!=null}},NOW:{name:"NOW",signature:"NOW()",description:"返回当前时间戳(毫秒)。",minArgs:0,maxArgs:0,params:[],example:"NOW()",fn:()=>Date.now()},TODAY:{name:"TODAY",signature:"TODAY()",description:"返回今天零点的时间戳(毫秒)。",minArgs:0,maxArgs:0,params:[],example:"TODAY()",fn:()=>{const e=new Date;return e.setHours(0,0,0,0),e.getTime()}},DATEADD:{name:"DATEADD",signature:"DATEADD(时间戳, 天数)",description:"时间戳加 N 天,返回新时间戳。",minArgs:2,maxArgs:2,params:["时间戳(毫秒)","天数(可为负)"],example:"DATEADD(TODAY(), 7)",fn:e=>{const t=S(e[0],"DATEADD",0),r=S(e[1],"DATEADD",1);return t+r*864e5}},DATEDIF:{name:"DATEDIF",signature:"DATEDIF(起始时间戳, 结束时间戳)",description:"两个时间戳相隔的整天数。",minArgs:2,maxArgs:2,params:["起始时间戳","结束时间戳"],example:"DATEDIF(TODAY(), DATEADD(TODAY(), 3))",fn:e=>{const t=S(e[0],"DATEDIF",0),r=S(e[1],"DATEDIF",1);return Math.round((r-t)/864e5)}},ISNULL:{name:"ISNULL",signature:"ISNULL(值)",description:"是否为空值(NULL / 未定义)。",minArgs:1,maxArgs:1,params:["值"],example:"ISNULL({备注})",fn:e=>e[0]===null||e[0]===void 0},ISEMPTY:{name:"ISEMPTY",signature:"ISEMPTY(值)",description:"是否为空(NULL、空字符串、空数组/对象均视为空)。",minArgs:1,maxArgs:1,params:["值"],example:"ISEMPTY({优惠券码})",fn:e=>{const t=e[0];return t==null?!0:typeof t=="string"?t.trim()==="":Array.isArray(t)?t.length===0:typeof t=="object"?Object.keys(t).length===0:!1}},ISNUMERIC:{name:"ISNUMERIC",signature:"ISNUMERIC(值)",description:"是否为数字(含可转换的数字字符串)。",minArgs:1,maxArgs:1,params:["值"],example:"ISNUMERIC('42')",fn:e=>{const t=e[0];return typeof t=="number"?!Number.isNaN(t):typeof t=="string"?t.trim()!==""&&!Number.isNaN(Number(t)):!1}},BETWEEN:{name:"BETWEEN",signature:"BETWEEN(值, 下限, 上限)",description:"值是否在 [下限, 上限] 区间内(含边界)。",minArgs:3,maxArgs:3,params:["值","下限","上限"],example:"BETWEEN({重量}, 1, 5)",fn:e=>{const t=S(e[0],"BETWEEN",0),r=S(e[1],"BETWEEN",1),n=S(e[2],"BETWEEN",2);return t>=r&&t<=n}},COUNT:{name:"COUNT",signature:"COUNT(数组)",description:"返回数组长度;空值返回 0。",minArgs:1,maxArgs:1,params:["数组"],example:"COUNT({商品列表})",fn:e=>Array.isArray(e[0])?e[0].length:0},COUNTIF:{name:"COUNTIF",signature:"COUNTIF(数组, 目标值)",description:"统计数组中与目标值相等的元素个数(宽松比较)。",minArgs:2,maxArgs:2,params:["数组","目标值"],example:"COUNTIF({地区列表}, '华东')",fn:e=>j(e[0],"COUNTIF",0).filter(r=>R(r,e[1])).length},SUMIF:{name:"SUMIF",signature:"SUMIF(数组, 目标值)",description:"对数组中与目标值相等的数字元素求和(宽松比较,忽略非数字)。",minArgs:2,maxArgs:2,params:["数组","目标值"],example:"SUMIF({价格列表}, 10)",fn:e=>j(e[0],"SUMIF",0).reduce((r,n)=>R(n,e[1])?r+B(n):r,0)},KEYS:{name:"KEYS",signature:"KEYS(对象)",description:"返回对象的键名数组。",minArgs:1,maxArgs:1,params:["对象(键值对变量)"],example:"KEYS({订单})",fn:e=>Object.keys(v(e[0],"KEYS",0))},VALUES:{name:"VALUES",signature:"VALUES(对象)",description:"返回对象的键值数组。",minArgs:1,maxArgs:1,params:["对象(键值对变量)"],example:"VALUES({订单})",fn:e=>Object.values(v(e[0],"VALUES",0))}},D=2;function F(e,t,r){return r.slice(e.end,t.start).trim()}function T(e,t,r){switch(e.type){case"if":return te(e,t,r);case"number":case"string":case"boolean":case"null":case"variable":return t.slice(e.start,e.end).trim();case"member":{const n=T(e.object,t,r);return e.index!==null?`${n}[${T(e.index,t,r)}]`:`${n}.${e.prop}`}case"call":{const n=e.args.map(i=>T(i,t,r)).join(", ");return`${t.slice(e.start,e.start+e.name.length).trim()}(${n})`}case"unary":return`${e.op}${T(e.operand,t,r)}`;case"binary":{const n=t.slice(e.left.end,e.right.start).trim(),s=T(e.left,t,r),i=T(e.right,t,r),l=e.op==="**"&&e.right.type==="binary"&&e.right.op==="**"?`(${i})`:i;return`${s} ${n} ${l}`}case"conditional":{const n=T(e.test,t,r),s=T(e.consequent,t,r),i=T(e.alternate,t,r);return`${n} ? ${s} : ${i}`}default:{const n=e;return t.slice(n.start,n.end).trim()}}}function te(e,t,r){const n=" ".repeat(r*D),s=" ".repeat((r+1)*D),i=[],l=f=>{f.type==="if"?i.push(T(f,t,r+1)):i.push(`${s}${T(f,t,r+1)}`)},u=e.branches[0],m=F({end:e.start},u.condition,t),p=F(u.condition,u.body,t);i.push(`${n}${m} ${T(u.condition,t,r)} ${p}`),l(u.body);for(let f=1;f<e.branches.length;f++){const o=e.branches[f],N=e.branches[f-1],g=F(N.body,o.condition,t),a=F(o.condition,o.body,t);i.push(`${n}${g} ${T(o.condition,t,r)} ${a}`),l(o.body)}if(e.elseBody){const f=e.branches[e.branches.length-1],o=F(f.body,e.elseBody,t);i.push(`${n}${o}`),l(e.elseBody)}return i.join(`
|
|
4
|
+
`)}const re=/^(IF|如果)(?=\s|$)/i,ne=/^(ELSE\s+IF|否则如果)(?=\s|$)/i,se=/^(ELSE|否则)(?=\s|$)/i,ae=/(ELSE|否则)$/i;function W(e){const t=e.split(`
|
|
5
|
+
`);if(t.length<=1)return e.trim();const r=[],n=[];let s=0;const i=l=>{let u=s;if(ne.test(l))u=n.length?n[n.length-1]:0;else if(se.test(l)){const m=n.length?n[n.length-1]:0;u=m,n.length&&n.pop(),s=m+1}else re.test(l)&&(u=s,n.push(s),s+=1);r.push(" ".repeat(u*D)+l)};for(const l of t){const u=l.trim();if(!u){r.push("");continue}const m=ae.exec(u);if(m&&m.index>0){i(u.slice(0,m.index).trim()),i(u.slice(m.index));continue}i(u)}return r.join(`
|
|
6
|
+
`)}function ie(e){const t=e.replace(/\r\n/g,`
|
|
7
|
+
`);if(!t.trim())return"";try{const r=U(t);return T(r,t,0)||W(t)}catch{return W(t)}}function oe(e){return U(e)}function le(e,t={},r={},n=[]){try{const s=U(e);return{ok:!0,value:H(s,t,r,n)}}catch(s){if(s instanceof E)return{ok:!1,error:{message:s.message,start:s.start,end:s.end,severity:"error",kind:"parse"}};if(s instanceof A)return{ok:!1,error:{message:s.message,start:s.start,end:s.end,severity:"error",kind:"runtime"}};throw s}}function ue(e,t=[],r={},n=[]){const s=[];let i=null;if(!e.trim())return{ok:!1,errors:[],ast:null};try{i=U(e)}catch(o){if(o instanceof E)return{ok:!1,errors:[{message:o.message,start:o.start,end:o.end,severity:"error",kind:"parse"}],ast:null};throw o}const l=new Set([...t.map(o=>o.name),...n]),u=new Set(t.map(o=>o.name)),m=new Map;for(const o of t)o.type==="object"&&o.fields&&o.fields.length>0&&m.set(o.name,new Set(o.fields.map(N=>N.key)));const p=new Set([...Object.keys($),...Object.keys(r)]),f=o=>{switch(o.type){case"variable":{if(!l.has(o.name)){const N=u.size>0?`(已定义:${Array.from(u).join("、")})`:"(尚未定义任何变量)";s.push({message:`未定义的变量 {${o.name}}${N}`,start:o.start,end:o.end,severity:"error",kind:"semantic"})}return}case"member":{if(f(o.object),o.index===null&&o.object.type==="variable"){const N=o.object.name,g=m.get(N);g&&!g.has(o.prop)&&s.push({message:`变量 {${N}} 中不存在字段 "${o.prop}"(已定义:${Array.from(g).join("、")})`,start:o.start,end:o.end,severity:"error",kind:"semantic"})}else o.index&&f(o.index);return}case"call":{const N=r[o.name];if(!p.has(o.name))s.push({message:`未知函数 ${o.name}()(内置:${Object.keys($).join("、")})`,start:o.start,end:o.end,severity:"error",kind:"semantic"});else if(!N){const g=$[o.name];if(g&&(o.args.length<g.minArgs||o.args.length>g.maxArgs)){const a=g.minArgs===g.maxArgs?`${g.minArgs}`:g.maxArgs===1/0?`至少 ${g.minArgs}`:`${g.minArgs}~${g.maxArgs}`;s.push({message:`函数 ${o.name}() 需要 ${a} 个参数,实际 ${o.args.length} 个`,start:o.start,end:o.end,severity:"error",kind:"semantic"})}}o.args.forEach(f);return}case"binary":f(o.left),f(o.right);return;case"unary":f(o.operand);return;case"conditional":f(o.test),f(o.consequent),f(o.alternate);return;case"if":for(const N of o.branches)f(N.condition),f(N.body);o.elseBody&&f(o.elseBody);return;default:return}};return f(i),{ok:s.length===0,errors:s,ast:i}}function me(e,t=80){let r;if(e==null)r="NULL";else if(typeof e=="string")r=`"${e}"`;else if(typeof e=="number")r=Number.isInteger(e)?String(e):String(Math.round(e*1e6)/1e6);else if(typeof e=="boolean")r=String(e);else if(Array.isArray(e)||typeof e=="object")try{r=JSON.stringify(e)}catch{r=String(e)}else r=String(e);return r.length>t?r.slice(0,t)+"…":r}function fe(e){return e==null?"空":typeof e=="number"?"数字":typeof e=="string"?"文本":typeof e=="boolean"?"布尔":Array.isArray(e)?"数组":typeof e=="object"?"对象":"其他"}function ce(e){const t=[];for(const r of C(e))r.type==="error"&&t.push({message:`无法识别的字符 "${r.raw}"`,start:r.start,end:r.end,severity:"error",kind:"lex"});return t}exports.BUILTIN_FUNCTIONS=$;exports.ParseError=E;exports.RuntimeError=A;exports.evaluate=H;exports.evaluateFormula=le;exports.formatFormula=ie;exports.formatValue=me;exports.lexErrors=ce;exports.parse=U;exports.parseFormula=oe;exports.tokenize=C;exports.validateFormula=ue;exports.valueTypeName=fe;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FnMap } from './evaluator';
|
|
2
|
+
import type { EvalResult, ExprNode, FormulaError, ValidationResult, VariableDef } from './types';
|
|
3
|
+
export { tokenize } from './lexer';
|
|
4
|
+
export { parse, ParseError } from './parser';
|
|
5
|
+
export { evaluate, RuntimeError, BUILTIN_FUNCTIONS } from './evaluator';
|
|
6
|
+
export { formatFormula } from './format';
|
|
7
|
+
export type { FnMap } from './evaluator';
|
|
8
|
+
export type { Token, TokenType, ExprNode, FunctionSpec, FormulaError, ValidationResult, EvalResult, VariableDef, ValueType, } from './types';
|
|
9
|
+
/** 便捷入口:直接解析一条公式 */
|
|
10
|
+
export declare function parseFormula(src: string): ExprNode;
|
|
11
|
+
/** 便捷入口:解析并求值(variables 可选,供枚举标签-值解析与成员访问校验) */
|
|
12
|
+
export declare function evaluateFormula(src: string, context?: Record<string, unknown>, functions?: FnMap, variables?: VariableDef[]): EvalResult;
|
|
13
|
+
/**
|
|
14
|
+
* 静态校验:语法 + 语义(未定义变量、未知函数、参数个数)。
|
|
15
|
+
* @param src 公式文本
|
|
16
|
+
* @param variables 可选变量列表
|
|
17
|
+
* @param extraFunctions 自定义函数(可覆盖内置)
|
|
18
|
+
* @param contextKeys 运行时测试值中额外提供的键(视为已定义变量)
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateFormula(src: string, variables?: VariableDef[], extraFunctions?: FnMap, contextKeys?: string[]): ValidationResult;
|
|
21
|
+
/** 格式化求值结果为可读文本(供界面展示) */
|
|
22
|
+
export declare function formatValue(v: unknown, maxLen?: number): string;
|
|
23
|
+
/** 求值结果的人类可读类型名 */
|
|
24
|
+
export declare function valueTypeName(v: unknown): string;
|
|
25
|
+
/** 词法层错误(未闭合字符串等)——解析器已覆盖大部分,这里保留供高亮使用 */
|
|
26
|
+
export declare function lexErrors(src: string): FormulaError[];
|