生态产品多个设计器现已发布候选版本

查看生态总览
跳转到内容

受限表达式 ​

@xihan-ui/expression 是一门受限的表达式语言:源码解析成可 JSON 序列化的语法树,静态检查后对数据求值。它服务于表单联动与公式、报表公式、打印格式化和流程条件这类「模板里写一小段计算」的场景。

ts
import { compile, evaluate, toPlain } from "@xihan-ui/expression";

const compiled = compile("ROUND(SUM(items.price) * (1 - discount), 2)", {
  identifiers: ["items", "discount"],
});
if (!compiled.ok)
  throw new Error(compiled.issues[0].code);

const result = evaluate(compiled.ast, { items: [{ price: 19.9 }, { price: 0.1 }], discount: 0.15 });
if (result.ok)
  toPlain(result.value); // 17

设计约束 ​

  • 模板只是数据。 不使用 eval、new Function,也不提供赋值、循环、方法调用或任何能执行宿主代码的语法;能调用的只有函数表里登记的函数。
  • 结果确定。 同样的语法树、同样的数据、同样的函数表与当前时间,永远得到同样的结果。语言本身不读时钟、不读随机数,NOW() 只返回宿主传入的时间。
  • 类型规则写死。 没有 JS 式的隐式转换:'1' + 1 是类型错误,不是 '11';条件必须是布尔或 null,没有「真值」「假值」。
  • 数值是十进制。 数字按十进制精确运算,0.1 + 0.2 == 0.3 为真,金额计算不会出现二进制浮点的尾差。
  • 可在其他语言复刻。 数值、日期、字符串的每一条规则都不依赖 JS 引擎的实现细节,包内附带两份跨实现一致性用例:求值的 tests/fixtures/conformance.json,按引用改写的 tests/fixtures/rename-references.json,其他语言的实现逐条比对即可。
  • 问题只给码与位置。 解析、检查、求值的问题都是 { code, start, end, data? },不带文案;界面按问题码自行组织提示语。

是否安装 ​

安装任意一个适配器即自动获得它:headless 与三个适配器都依赖它。只用表达式、不用组件时,也可以单独安装:

bash
pnpm add @xihan-ui/expression

零运行时依赖,import 无副作用,不碰 DOM,浏览器与服务端都能运行。

三步管线 ​

步骤函数结果
解析parse(source, options?){ ok: true, ast } 或 { ok: false, issue },只报第一条问题
静态检查check(ast, options?)问题数组:未知标识符、未知函数、实参个数、禁止的成员名、语法树结构
求值evaluate(ast, scope?, options?){ ok: true, value } 或 { ok: false, issue }

compile(source, options?) 把前两步合为一步,失败时带全部问题。三个函数对表达式和数据的问题一律以结果返回,从不抛异常;只有选项本身写错(上限不是正整数、函数表不是 createFunctionTable 创建的)才抛 TypeError。

语法树可以存进模板、跨端传输后再求值。来自不可信来源的语法树先过 check;evaluate 自身也逐节点校验结构,伪造的节点报 XH_EXPR_INVALID_AST,不会越权执行。

注入引擎 ​

打印、流程、表单、报表四款设计器不直接调用上面三步,而是接收一个形状统一的引擎,由宿主注入:

ts
interface ExpressionEngine {
  compile: (source: string, options?: { identifiers?: Iterable<string> | ((name: string) => boolean) }) => EngineCompileResult;
}

type EngineCompileResult
  = | { ok: true; references: readonly string[]; evaluate: (scope: Readonly<Record<string, unknown>>) => EngineEvaluation }
    | { ok: false; issues: readonly ExpressionIssue[] };

type EngineEvaluation = { ok: true; value: unknown } | { ok: false; issue: ExpressionIssue };

createExpressionEngine(options?) 直接给出这个形状,宿主不必再写转接代码:

ts
import { createExpressionEngine, ExprDate } from "@xihan-ui/expression";

const engine = createExpressionEngine({
  builtins: "all", // 或 "none",或 ["SUM", "ROUND"] 这样的名单
  functions: [], // 宿主函数,见「宿主函数」一节
  now: () => ExprDate.fromParts(2026, 10, 16), // 固定日期,或每次求值时调用的函数
  limits: { maxSteps: 50000 },
});

const compiled = engine.compile("price * qty", { identifiers: ["price", "qty"] });
if (compiled.ok) {
  compiled.references; // ["price", "qty"]
  compiled.evaluate({ price: 1.5, qty: 2 }); // { ok: true, value: 3 }
}
  • references 是引用的顶层变量名,按源码顺序去重,不含函数名,用于依赖分析。
  • evaluate 的结果是 toPlain 之后的普通 JS 值:数字转成 JS 数字,日期转成规范文本。它从不抛异常,结果里有不可表示的值或环时同样以问题返回。
  • 同一个引擎按源码缓存语法树(有条数上限),不同引擎互不影响。

模板里的表达式以源码字符串保存,求值处还有两步四款都要做,由两个函数直接给出:

ts
import { conditionOf, evaluateSource } from "@xihan-ui/expression";

const cache = new Map(); // 一轮批量求值建一个,同一源码只编译一次
const outcome = evaluateSource(engine, "qty > 0", { qty: 3 }, cache);
conditionOf(outcome); // true
  • evaluateSource(engine, source, scope, cache?):编译后求值;编译失败时以第一条问题作为求值失败返回,形状与 evaluate 的结果相同。不传 cache 时每次都编译。
  • conditionOf(evaluation):把求值结果解释成条件。true 成立,false 与 null 不成立(与 ?:、IF 中 null 走否定分支一致),其余结果为 'not-boolean',求值失败为 'failed'。联动、网关、条件格式、显隐等条件位置都按这一条判定,后两种结果由宿主报成问题。

依赖排序 ​

计算字段、联动赋值这类「名字 = 表达式」会互相引用,要按依赖先后求值,并在保存模板时查出循环:

ts
import { checkExpressions, evaluatePlan, planExpressions } from "@xihan-ui/expression";

const entries = [
  { name: "total", expression: "subtotal + tax" },
  { name: "tax", expression: "subtotal * 0.1" },
  { name: "subtotal", expression: "price * qty" },
];

const plan = planExpressions(engine, entries, { identifiers: ["price", "qty"] });
if (plan.ok)
  evaluatePlan(plan.steps, { price: 10, qty: 3 }); // { ok: true, values: { subtotal: 30, tax: 3, total: 33 } }

checkExpressions(engine, entries, { identifiers: ["price"] }); // 保存时校验:[{ code: "XH_EXPR_UNKNOWN_IDENTIFIER", name: "subtotal", ... }]
  • planExpressions(engine, entries, { identifiers? }):编译每个条目并按相互引用排序,被引用的排在前面,互不相关的保持输入顺序。有问题时 ok 为 false,一次给出全部问题,不给步骤。
  • 问题有三类:条目编译不通过(带条目名 name 与引擎给出的问题码、源码区间);两个条目同名(XH_EXPR_DUPLICATE_NAME);循环引用(XH_EXPR_CYCLE,names 按引用顺序列出环上的名字,同一个环只报一次,自己引用自己也算)。
  • identifiers 是表达式之外可引用的名字,各条目自己的名字总是已知;不给时不检查未知名字。
  • checkExpressions 只返回问题,没有问题时为空数组。
  • evaluatePlan(steps, scope):按步骤依次求值,每一步可以引用 scope 与此前各步的结果,同名时以步骤结果为准;遇到求值失败即停,返回失败的条目名与问题。不修改 scope。

文本模板 ​

标题、说明文字、页眉页脚这类「文字里嵌几个数据」的位置用文本模板,占位里只写数据路径,不写表达式;需要计算的内容另用表达式算好再交给模板。它在 @xihan-ui/core 的子入口 ./template,不依赖表达式:

ts
import { fillTextTemplate, parseTextTemplate } from "@xihan-ui/core/template";

fillTextTemplate("订单 {{order.items[0].name}},第 {{$page}} / {{$pages}} 页", { order: { items: [{ name: "笔" }] }, $page: 2, $pages: 7 });
// { text: "订单 笔,第 2 / 7 页", nonScalar: [] }

parseTextTemplate("{{ price * qty }}").issues; // [{ code: "invalid-path", start: 0, end: 17 }]
  • 路径:写法同 @xihan-ui/core/json 的 formatJsonPath,由 parseJsonPath 解析:标识符以 . 相连,下标写成 [0],不是标识符的键写成 ["unit price"];$ 开头的宿主变量与中文名都按普通键处理。占位两侧的空白忽略。属性面板校验路径用 isJsonPathText。
  • 问题:parseTextTemplate 一次给出全部问题,{ code, start, end } 的区间是整个占位;问题码为 unclosed-placeholder(没有闭合)、empty-placeholder(空占位)、invalid-path(路径不合法,写了表达式也是这一条,与 core/json 的 JsonPathError 同名同义)。有问题的占位按原样文本保留。
  • 读值:同 getJsonAt,只读自有属性,下标只读数组,读不到的路径得到空串。
  • 显示:缺省规则即导出的 displayText(value):null 与 undefined 为空串,字符串原样,有限数字、布尔与 bigint 转字符串,非有限数字与无效日期为空串,Date 转 ISO 文本;对象与数组不能直接显示,displayText 返回 undefined,占位得到空串并按出现顺序记入 nonScalar。在模板之外显示单个数据值时直接调用 displayText,与模板保持一致。传 display(value, path) 可以换掉这套规则,返回 undefined 表示不能显示。
  • 页码:页码不另设记号,模板里放宿主变量 $page 与 $pages 的占位;要罗马数字或中文数字时,宿主先用 formatValue(n, { kind: "numeral", style }) 转好再放进数据。

文法 ​

text
expression   = conditional
conditional  = or [ "?" conditional ":" conditional ]          右结合
or           = and { "||" and }
and          = equality { "&&" equality }
equality     = relational [ ("==" | "!=") relational ]          不能连写
relational   = additive [ ("<" | "<=" | ">" | ">=") additive ]  不能连写
additive     = multiplicative { ("+" | "-") multiplicative }
multiplicative = unary { ("*" | "/" | "%") unary }
unary        = ("-" | "!") unary | postfix
postfix      = primary { "." name | "[" expression "]" }
primary      = number | string | "true" | "false" | "null"
             | identifier | call | "(" expression ")" | "[" [ expression { "," expression } ] "]"
call         = function-name "(" [ expression { "," expression } ] ")"
  • 数字:12、1.5、1e3、2.5E-4。必须以数字开头(.5 不合法),没有负数字面量,-1 是一元负号作用于 1。
  • 字符串:单引号或双引号,转义只有 \\ \' \" \n \r \t \uXXXX,其余反斜杠组合报 XH_EXPR_INVALID_ESCAPE。
  • 标识符:ASCII 字母、_、$ 开头,后接字母、数字、_、$;非 ASCII 字符(中文等)也可直接使用,但全角标点、全角空格、通用标点区不算标识符字符,误输入时报 XH_EXPR_UNEXPECTED_CHARACTER。名字含空格等字符时用反引号括起:`订单 金额`,反引号内只转义 \` 与 \\。
  • 关键字:只有小写的 true、false、null,TRUE 是普通标识符。
  • 函数名:ASCII 字母或 _ 开头,后接字母、数字、_,查找不区分 ASCII 大小写,sum(...) 与 SUM(...) 相同。只能直接调用函数名,a.b()、(f)() 报 XH_EXPR_INVALID_CALLEE。
  • 空白:空格、制表、换行、回车、垂直制表、换页、不换行空格(U+00A0)、全角空格(U+3000)、BOM。
  • 比较不能连写:a < b < c、a == b == c 报 XH_EXPR_CHAINED_COMPARISON,要写成 a < b && b < c。
  • 没有 =、&、| 单字符运算符,写错时在该字符处报 XH_EXPR_UNEXPECTED_CHARACTER。

位置是 UTF-16 码元偏移,左闭右开,与 JS 字符串下标一致。

值与类型 ​

类型说明来自宿主数据
null缺失null、undefined、不存在的字段
boolean布尔布尔
number十进制数,见下节有限的 JS 数字(取最短往返十进制表示)、bigint、ExprNumber
string字符串字符串
date不带时区的日期时间,见下下节只接受 ExprDate;文本日期在表达式里用 TO_DATE 转换
list只读列表数组
record只读记录原型是 Object.prototype 或 null 的普通对象

NaN、Infinity、Date、Map、函数、类实例等不可表示,读到时报 XH_EXPR_UNSUPPORTED_VALUE。宿主数据按需转换,只读自有、可枚举的数据属性:原型链上的成员、getter 都读不到,也不会被触发。

__proto__、constructor、prototype 三个名字一律禁止,作标识符、点号成员或下标(含运行时算出的下标)时报 XH_EXPR_FORBIDDEN_MEMBER。

运算规则 ​

运算规则
+ - * / %两侧都是数字;+ 另允许两侧都是字符串(拼接)。任一侧为 null 时结果为 null。% 是截断取余,符号随被除数。除数为 0 报 XH_EXPR_DIVISION_BY_ZERO
一元 -数字取反,null 得 null
== !=任意类型;类型不同即不等,null == null 为真;数字按数值、日期按时刻、列表逐项、记录按字段集合深比较
< <= > >=两侧是同类的数字、字符串或日期;字符串按 UTF-16 码元比较;任一侧为 null 时结果为 null
!布尔取反,null 得 null
&& ||短路的三值逻辑:false && x 为 false、true || x 为 true,都不求 x;null && false 为 false,null || true 为 true,其余含 null 的组合为 null
c ? a : bc 为 true 取 a,为 false 或 null 取 b,只求所选分支
a.ba 是记录时取字段,缺失得 null;a 为 null 时得 null;a 是列表时逐元素取字段(投影),嵌套列表逐层投影
a[i]列表取整数下标,越界或负数得 null,非整数报 XH_EXPR_INVALID_INDEX;记录取字符串键

运算数的类型不符时报 XH_EXPR_TYPE_MISMATCH,位置指向出错的那个运算数,data 带 operator 与实际类型 actual。

投影让子表合计可以直接写:SUM(items.amount)。

数值 ​

数值是十进制:值为「系数 × 10^指数」。

  • 每个运算结果保留 34 位有效数字,超出部分按四舍六入五成双舍入;除法、平方根先求足够多位再舍入一次,结果是正确舍入的。
  • 绝对值必须小于 10^65,否则报 XH_EXPR_NUMBER_OVERFLOW;最小非零绝对值是 10^-64,更小的部分按同一规则舍入,可能得到 0。
  • 规范文本不用科学计数法、不带多余的 0:1.50 写作 1.5,1e3 写作 1000。
  • ROUND 用四舍五入(远离零),FLOOR 向负无穷,CEIL 向正无穷,TRUNC 向零;位数可为负数,表示舍到十位、百位。
  • 来自宿主的 JS 数字先取最短往返十进制表示:0.1 就是十进制的 0.1。

ExprNumber 可在宿主代码里使用:ExprNumber.parse(text)、ExprNumber.fromJs(value)、toString()、toJs()、compare()、equals(),以及只读的 coefficient(bigint)与 exponent。

日期 ​

日期不带时区,精确到毫秒,范围 0001-01-01 至 9999-12-31,按前推格里历计算,不经过运行环境的时区。

  • 规范文本:零点时是 YYYY-MM-DD,否则是 YYYY-MM-DDTHH:mm:ss,毫秒不为 0 时再带 .SSS。
  • TO_DATE 接受 YYYY-MM-DD,可带 T 或空格分隔的 HH:mm、HH:mm:ss、HH:mm:ss.fff(小数 1 至 9 位,截到毫秒);时间后可带 Z 或 ±HH:mm 偏移,偏移被丢弃,保留墙钟时间。不合法得到 null。
  • 按月、按年加减时,日号超过目标月的天数就取月末:1 月 31 日加一个月是 2 月 28 日或 29 日。
  • DATE_DIFF 数完整单位,带符号,向零截断;按月计时,从起点加上该月数仍不超过终点才算一个完整的月。

宿主用 ExprDate.fromParts(年, 月, 日, 时?, 分?, 秒?, 毫秒?) 或 ExprDate.parse(text) 构造日期。当前时间由宿主经 evaluate 的 now 选项传入:取哪个时区的墙钟时间由宿主决定。

字符串 ​

  • LEN、LEFT、RIGHT、MID 按 Unicode 码点计数与截取,一个表情符号算一个字符;落单的代理项也算一个。
  • UPPER、LOWER 只转换 ASCII 字母,其余字符原样保留。
  • TRIM 去掉的空白与词法中的空白是同一集合。
  • 字符串结果长度超过 maxStringLength 报 XH_EXPR_STRING_TOO_LONG;REPLACE 在分配之前就按结果长度判断。

函数 ​

除特别说明,任一实参为 null 时结果为 null。实参类型不符报 XH_EXPR_TYPE_MISMATCH,取值不合法报 XH_EXPR_INVALID_ARGUMENT,位置都指向那个实参,data 带 function 与 index。

逻辑与空值 ​

函数说明
IF(条件, 是[, 否])只求所选分支;条件为 null 取「否」,缺省的「否」是 null
COALESCE(值, …)第一个非 null 的值,全为 null 时为 null
ISBLANK(值)null、空串、空列表为 true

数学 ​

函数说明
ABS(x)绝对值
ROUND(x[, 位数])四舍五入,位数缺省 0,取 -64 至 64 的整数
FLOOR(x[, 位数]) / CEIL(x[, 位数]) / TRUNC(x[, 位数])向下、向上、向零
SQRT(x)平方根,负数报不合法实参
POWER(x, n)整数次幂,n 取 -999 至 999 的整数

聚合 ​

实参里的列表递归展开,null 跳过。

函数说明
SUM(…)数字之和,没有数时为 0
AVG(…)平均值,没有数时为 null
COUNT(…)非 null 值的个数,任意类型
MIN(…) / MAX(…)同类的数字、字符串或日期中的极值,没有值时为 null

文本 ​

函数说明
CONCAT(值, …)按规范文本拼接,null 是空串;列表与记录报类型不符
TEXT(值)规范文本,null 是空串
LEN(s)码点个数
LEFT(s, n) / RIGHT(s, n)前 / 后 n 个码点
MID(s, 起点, 长度)起点从 1 开始
UPPER(s) / LOWER(s) / TRIM(s)只转 ASCII 大小写;去首尾空白
CONTAINS(s, 子串) / CONTAINS(列表, 值)子串包含,或列表成员(按 == 比较);列表时允许查 null
STARTS_WITH(s, 前缀) / ENDS_WITH(s, 后缀)前后缀判断
REPLACE(s, 查找, 替换)全部替换,按字面文本匹配;查找串不能为空
SPLIT(s, 分隔符)拆成字符串列表;分隔符不能为空
JOIN(列表[, 分隔符])元素按规范文本拼接,null 是空串
FORMAT_NUMBER(x, 小数位[, 千分位])四舍五入到 0 至 34 位小数并补齐 0;千分位用 ,,小数点用 .,与语言环境无关
TO_NUMBER(值)去首尾空白后按十进制文本解析,不合法得到 null;不接受千分位、十六进制、Infinity

日期 ​

函数说明
NOW() / TODAY()宿主传入的当前时间 / 当天零点;没有传入时报 XH_EXPR_NOW_UNAVAILABLE
DATE(年, 月, 日[, 时, 分, 秒, 毫秒])各部分必须是范围内的整数
TO_DATE(值)文本转日期,不合法得到 null
YEAR MONTH DAY HOUR MINUTE SECOND(日期)取组成部分
WEEKDAY(日期)ISO 星期:周一为 1,周日为 7
DATE_ADD(日期, n, 单位)单位是 'year' 'month' 'week' 'day' 'hour' 'minute' 'second',n 是整数
DATE_DIFF(起, 止, 单位)完整单位数,带符号
FORMAT_DATE(日期, 模式)占位符 yyyy yy MM M dd d HH H mm m ss s SSS;单引号括起的内容原样输出,'' 输出一个单引号

宿主函数 ​

ts
import { createFunctionTable } from "@xihan-ui/expression";

const functions = createFunctionTable({
  functions: [
    {
      name: "MASK_PHONE",
      minArgs: 1,
      maxArgs: 1,
      params: ["phone"],
      call: ([phone], context) => {
        if (phone === null)
          return null;
        if (typeof phone !== "string")
          return context.typeMismatch(0);
        return phone.length === 11 ? `${phone.slice(0, 3)}****${phone.slice(7)}` : phone;
      },
    },
  ],
});
  • 名字是大写 ASCII 字母开头的 A-Z、0-9、_,不能与已启用的内置函数重名;builtins 选项可只启用部分内置函数(['SUM', 'IF'])或全部不启用('none')。
  • call 收到的是表达式值,返回值可以是表达式值,也可以是能转换的 JS 值(数字、字符串、数组、普通对象)。
  • context.invalidArgument(i)、context.typeMismatch(i) 返回失败标记,直接 return 即可;抛出的异常记为 XH_EXPR_FUNCTION_FAILED。
  • call 必须是纯函数:只依赖实参与 context.now,不读时钟、随机数或外部状态,否则「结果确定」的承诺不成立。
  • params 是参数名,可省略:给出时是小写字母开头的英文键,互不重复,个数等于 maxArgs,不限个数时等于 max(minArgs, 1),最后一个表示可重复。编辑器按键取显示文字,省略时显示为「参数 1」「参数 2」。内置函数的参数名都已给出,如 IF 是 condition、whenTrue、whenFalse。

check 与 evaluate 都接受同一个 functions 选项。table.names 列出全部函数名,table.lookup(name) 返回签名(名字、实参个数与 params),可用来做编辑器里的自动补全与参数提示。

格式化 ​

formatValue(value, spec, { locale?, timeZone? }) 把值转成展示用的文本,四款设计器的数字、日期与金额展示共用它。数字类一律按十进制精确舍入:1.005 保留两位得到 1.01,2.675 得到 2.68。null 与 undefined 得到空串。

spec说明例
{ kind: "number", decimals?, minDecimals?, maxDecimals?, grouping?, rounding? }decimals 固定小数位并补 0;或 maxDecimals 舍入后去掉末尾的 0、至少保留 minDecimals 位;都不给时不舍入1234567.891 按 { decimals: 2, grouping: true } 得 1,234,567.89
{ kind: "percent", …同上 }乘 100 后按数字输出并接 %0.1234 按 { decimals: 1 } 得 12.3%
{ kind: "date", pattern }占位符同 FORMAT_DATEyyyy年M月d日
{ kind: "rmb-upper", rounding? }人民币大写,舍入到分,用「元」不用「圆」1001.5 得 壹仟零壹元伍角,100000 得 壹拾万元整
{ kind: "numeral", style }整数的字形:roman-upper / roman-lower 罗马数字,cn-lower / cn-upper 中文小写 / 大写数字1994 得 MCMXCIV,110 得 一百一十 / 壹佰壹拾,15 得 十五
  • 舍入方式 rounding:half-up(四舍五入,缺省)、half-even、floor、ceil、down。
  • 数字输入:ExprNumber、有限的 JS 数字(取最短往返十进制表示)、bigint、十进制文本。
  • 日期输入:ExprDate、日期文本、毫秒时间戳、Date。不带偏移的文本按墙钟时间原样输出;带偏移的文本在给了 timeZone 时换算到该时区,没给时保留它自己的墙钟时间;时间戳与 Date 换算到 timeZone,缺省 UTC。
  • timeZone:取 UTC、±HH:mm 或 IANA 名称(如 Asia/Shanghai)。IANA 名称经运行环境的时区数据换算,不同环境的时区数据版本可能不同。
  • locale:只决定小数点与千分位符号,数字始终是 ASCII 数字、每三位一组;不给时用 . 与 ,。
  • 人民币大写:
    • 零的写法:段内连续的 0 只写一个「零」,整段为 0 时只在后面还有非零段时补「零」,如 100010001 得 壹亿零壹万零壹元整。
    • 角分:有角无分时不写「整」;无角有分时写「零×分」;整数部分为 0 时省去「元」,如 0.5 得 伍角。
    • 范围:负数前加「负」,金额绝对值须小于一亿亿。
  • 数字字形:只接受整数,小数报值不合法。罗马数字取 1 至 3999;中文数字的零写法同人民币大写,小写以「一十」开头时省去「一」(10 得 十、100000 得 十万),大写保留(壹拾),0 得 零,负数前加「负」,绝对值须小于一亿亿。
  • 保存时校验:checkFormatSpec(spec, { kinds? }) 不抛异常,返回全部问题 { code, path }[],path 是出问题的那一项的键路径。问题码:不是普通对象 XH_EXPR_FORMAT_INVALID_SPEC;种类缺失、不认识或不在 kinds 里 XH_EXPR_FORMAT_INVALID_KIND;取值不合法或必填项缺失 XH_EXPR_FORMAT_INVALID_OPTION;decimals 与 minDecimals / maxDecimals 同给、或 minDecimals 大于 maxDecimals XH_EXPR_FORMAT_CONFLICT;该种类没有的键 XH_EXPR_FORMAT_UNKNOWN_KEY(formatValue 本身不理会多余的键)。图表数值这类只收数字的位置传 kinds: ["number", "percent"]。属性面板列选项用 FORMAT_KINDS、FORMAT_ROUNDINGS、NUMERAL_STYLES,小数位上限是 FORMAT_DECIMALS_LIMIT(34)。
  • 注入类型:设计器把格式化作为注入项时用 ValueFormatter 声明,签名与 formatValue 相同((value, spec, options) => string),宿主直接传 formatValue。
  • 出错:格式说明不合法时抛 TypeError("XH_EXPR_FORMAT_INVALID_SPEC"),值不合法时抛 TypeError("XH_EXPR_FORMAT_INVALID_VALUE"),大写金额、中文数字或罗马数字超出范围时抛 TypeError("XH_EXPR_FORMAT_OUT_OF_RANGE")。

上限 ​

选项缺省作用于
maxLength10000源码长度
maxDepth100语法树深度;求值时也约束嵌套数据的层数
maxSteps100000求值步数:每个节点一步,遍历列表时每个元素再一步
maxStringLength100000字符串结果长度

这些上限让任何输入都在有界的时间与内存内结束:深层括号、超长源码、巨大的数字字面量、带环的数据、十万行的列表、层层嵌套的 REPLACE 都会以问题码返回,不会耗尽调用栈或内存。

辅助函数 ​

函数说明
print(ast)打印成规范源码;结果再解析得到同一棵树
collectReferences(ast)按源码顺序列出字段引用的最长静态路径,用于依赖分析与改名
renameReferences(source, rename, options?)按引用改写源码,见下文「按引用改名」
toPlain(value)转成 JSON 能表示的值:数字转 JS 数字,日期转规范文本
toExpressionValue(raw)把宿主值转成表达式值,不可表示时抛 TypeError
typeOf(value)值的类型名
isPlainName(text)能否不加反引号写成标识符
createExpressionEngine(options?)四款设计器统一注入的引擎,见「注入引擎」
formatValue(value, spec, options?)数字、百分比、日期、人民币大写的格式化,见「格式化」
scanTokens(source)容错的词法扫描:把源码切成名字、数字、字符串、运算符、标点与 unknown 六类词,遇到错误不中断,未闭合的字符串到源码末尾;给编辑器判断光标所在的词
createExpressionCheck(engine, options?)用引擎的 compile 做检查,返回 (source) => issues:失败时是全部问题,成功时为空;可直接作为表达式输入框的 check,identifiers 同 compile

按引用改名 ​

字段、参数改名时,用 renameReferences 把公式里对它的引用一并改掉:

ts
import { renameReferences } from "@xihan-ui/expression";

const result = renameReferences("IF($year > 2025, $row.amount, 0)", path =>
  path[0] === "$year" ? ["$fiscalYear", ...path.slice(1)] : undefined);
// { ok: true, source: "IF($fiscalYear > 2025, $row.amount, 0)", renamed: 1 }
  • rename 收到每一处引用的路径(与 collectReferences 相同),返回新路径;返回 undefined 或原路径表示不改。
  • 只重写名字变了的那几段,其余源码一字不动:字符串字面量与函数名里的同名文字、空白、括号、原来的点号或下标写法都保留。
  • 新名字不合标识符规则或是关键字时加反引号,写进字符串下标时按字符串转义;改写后的源码再解析,引用路径就是 rename 给的新路径。
  • 新路径更长时多出的段接成点号成员,更短时去掉多出的段。
  • 不改入参。源码解析失败时返回 { ok: false, source, issue },source 原样不动,不做半截改写。

问题码 ​

阶段问题码
解析XH_EXPR_EMPTY XH_EXPR_TOO_LONG XH_EXPR_TOO_DEEP XH_EXPR_UNEXPECTED_CHARACTER XH_EXPR_UNEXPECTED_TOKEN XH_EXPR_UNEXPECTED_END XH_EXPR_UNTERMINATED_STRING XH_EXPR_INVALID_ESCAPE XH_EXPR_INVALID_NUMBER XH_EXPR_INVALID_IDENTIFIER XH_EXPR_CHAINED_COMPARISON XH_EXPR_INVALID_CALLEE XH_EXPR_NUMBER_OVERFLOW
检查XH_EXPR_UNKNOWN_IDENTIFIER XH_EXPR_UNKNOWN_FUNCTION XH_EXPR_ARGUMENT_COUNT XH_EXPR_FORBIDDEN_MEMBER XH_EXPR_INVALID_AST XH_EXPR_TOO_DEEP XH_EXPR_NUMBER_OVERFLOW
依赖排序XH_EXPR_DUPLICATE_NAME XH_EXPR_CYCLE,见「依赖排序」一节
求值XH_EXPR_TYPE_MISMATCH XH_EXPR_INVALID_ARGUMENT XH_EXPR_INVALID_INDEX XH_EXPR_DIVISION_BY_ZERO XH_EXPR_NUMBER_OVERFLOW XH_EXPR_DATE_OUT_OF_RANGE XH_EXPR_STRING_TOO_LONG XH_EXPR_STEP_LIMIT XH_EXPR_DEPTH_LIMIT XH_EXPR_UNSUPPORTED_VALUE XH_EXPR_NOW_UNAVAILABLE XH_EXPR_FUNCTION_FAILED,以及运行时遇到的 XH_EXPR_UNKNOWN_FUNCTION XH_EXPR_ARGUMENT_COUNT XH_EXPR_FORBIDDEN_MEMBER XH_EXPR_INVALID_AST

data 里的键:name(标识符、函数或成员名)、function(出错的函数)、index(第几个实参,从 0 起)、operator、actual(实际类型)、min / max / actual(实参个数)、max(上限)、char(不认识的字符)、kind / token(意外的记号)。

状态 ​

这个包属于实验级:破坏性变更可能出现在 minor 中。语言的文法与语义一经发布,修改会同步更新一致性用例,并在更新日志中逐条列出。

Released under The MIT License