| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- /**
- * 表达式与条件求值:算术表达式、条件表达式(不依赖 eval)
- */
- const { parseValue, resolveValue } = require('./actions/set-parser.js')
- /** 在 str 中查找 needle 的起始下标,忽略单/双引号字符串内的匹配(支持 \\ 转义) */
- function findOutsideQuotes(str, needle, fromIndex = 0) {
- let i = fromIndex
- let inSingle = false
- let inDouble = false
- while (i < str.length) {
- const c = str[i]
- if (inSingle) {
- if (c === '\\' && i + 1 < str.length) {
- i += 2
- continue
- }
- if (c === "'") inSingle = false
- i++
- continue
- }
- if (inDouble) {
- if (c === '\\' && i + 1 < str.length) {
- i += 2
- continue
- }
- if (c === '"') inDouble = false
- i++
- continue
- }
- if (c === "'") {
- inSingle = true
- i++
- continue
- }
- if (c === '"') {
- inDouble = true
- i++
- continue
- }
- if (str.startsWith(needle, i)) return i
- i++
- }
- return -1
- }
- function splitTopLevelFirst(expr, delimiter) {
- const idx = findOutsideQuotes(expr, delimiter)
- if (idx === -1) return null
- return [expr.slice(0, idx).trim(), expr.slice(idx + delimiter.length).trim()]
- }
- /**
- * 在引号外查找第一个比较运算符(长运算符优先,避免 >= 被拆成 >)
- */
- function findBinaryComparisonOp(expr, operators) {
- const sorted = [...operators].sort((a, b) => b.op.length - a.op.length)
- let i = 0
- let inSingle = false
- let inDouble = false
- while (i < expr.length) {
- const c = expr[i]
- if (inSingle) {
- if (c === '\\' && i + 1 < expr.length) {
- i += 2
- continue
- }
- if (c === "'") inSingle = false
- i++
- continue
- }
- if (inDouble) {
- if (c === '\\' && i + 1 < expr.length) {
- i += 2
- continue
- }
- if (c === '"') inDouble = false
- i++
- continue
- }
- if (c === "'") {
- inSingle = true
- i++
- continue
- }
- if (c === '"') {
- inDouble = true
- i++
- continue
- }
- for (const row of sorted) {
- if (expr.startsWith(row.op, i)) return { index: i, op: row.op, fn: row.fn }
- }
- i++
- }
- return null
- }
- function parseArithmeticExpression(expr) {
- let index = 0
- const skipWhitespace = () => {
- while (index < expr.length && /\s/.test(expr[index])) index++
- }
- const parseNumber = () => {
- skipWhitespace()
- let numStr = ''
- let hasDot = false
- while (index < expr.length) {
- const c = expr[index]
- if (c >= '0' && c <= '9') { numStr += c; index++ }
- else if (c === '.' && !hasDot) { numStr += c; hasDot = true; index++ }
- else break
- }
- if (!numStr) throw new Error('期望数字')
- const n = parseFloat(numStr)
- if (isNaN(n)) throw new Error(`无效的数字: ${numStr}`)
- return n
- }
- const parseFactor = () => {
- skipWhitespace()
- if (index >= expr.length) throw new Error('表达式不完整')
- let neg = false
- if (expr[index] === '-') { neg = true; index++; skipWhitespace() }
- else if (expr[index] === '+') { index++; skipWhitespace() }
- let r
- if (expr[index] === '(') {
- index++
- r = parseExpression()
- skipWhitespace()
- if (index >= expr.length || expr[index] !== ')') throw new Error('缺少右括号')
- index++
- } else {
- r = parseNumber()
- }
- return neg ? -r : r
- }
- const parseTerm = () => {
- let r = parseFactor()
- skipWhitespace()
- while (index < expr.length) {
- const op = expr[index]
- if (op === '*') { index++; r *= parseFactor() }
- else if (op === '/') { index++; const d = parseFactor(); if (d === 0) throw new Error('除以零'); r /= d }
- else break
- skipWhitespace()
- }
- return r
- }
- const parseExpression = () => {
- let r = parseTerm()
- skipWhitespace()
- while (index < expr.length) {
- const op = expr[index]
- if (op === '+') { index++; r += parseTerm() }
- else if (op === '-') { index++; r -= parseTerm() }
- else break
- skipWhitespace()
- }
- return r
- }
- const r = parseExpression()
- skipWhitespace()
- if (index < expr.length) throw new Error(`表达式解析不完整: ${expr.substring(index)}`)
- return r
- }
- function evaluateExpression(expression, context) {
- if (typeof expression !== 'string') return expression
- try {
- let expr = expression.trim()
- const varPattern = /\{(\w+)\}(?!\})/g
- const originalExpr = expr
- let hasVars = false
- expr = expr.replace(varPattern, (match, varName) => {
- hasVars = true
- const v = context[varName]
- if (v === undefined || v === null) return '0'
- if (typeof v === 'number') return String(v)
- if (typeof v === 'boolean') return v ? '1' : '0'
- if (typeof v === 'string') {
- const n = Number(v)
- if (!isNaN(n) && v.trim() !== '') return String(n)
- return '0'
- }
- const n = Number(v)
- return !isNaN(n) ? String(n) : '0'
- })
- if (!hasVars && !/[+\-*/]/.test(expr)) {
- const n = Number(expr)
- if (!isNaN(n) && expr.trim() !== '') return n
- return expr
- }
- if (!/[+\-*/]/.test(expr)) {
- const n = Number(expr)
- if (!isNaN(n) && expr.trim() !== '') return n
- return expr
- }
- expr = expr.replace(/\s+/g, '')
- if (!/^[0-9+\-*/().]+$/.test(expr)) return resolveValue(originalExpr, context)
- if (!/^[0-9(]/.test(expr) || !/[0-9)]$/.test(expr)) return resolveValue(originalExpr, context)
- const result = parseArithmeticExpression(expr)
- if (typeof result === 'number' && !isNaN(result) && isFinite(result)) return result
- return resolveValue(originalExpr, context)
- } catch (e) {
- return resolveValue(expression, context)
- }
- }
- function parseConditionExpression(expr) {
- expr = expr.trim()
- const orPart = splitTopLevelFirst(expr, '||')
- if (orPart) {
- return parseConditionExpression(orPart[0]) || parseConditionExpression(orPart[1])
- }
- const andPart = splitTopLevelFirst(expr, '&&')
- if (andPart) {
- return parseConditionExpression(andPart[0]) && parseConditionExpression(andPart[1])
- }
- const operators = [
- { op: '!=', fn: (a, b) => a != b },
- { op: '==', fn: (a, b) => {
- if (typeof a === 'string' && typeof b !== 'string') b = String(b)
- else if (typeof b === 'string' && typeof a !== 'string') a = String(a)
- if (a === '' && b === '') return true
- if ((a === '' && typeof b === 'string') || (b === '' && typeof a === 'string')) {
- try {
- const p = JSON.parse(a === '' ? b : a)
- if (Array.isArray(p)) return p.length === 0
- } catch (e) {}
- }
- return a == b
- }},
- { op: '>=', fn: (a, b) => Number(a) >= Number(b) },
- { op: '<=', fn: (a, b) => Number(a) <= Number(b) },
- { op: '>', fn: (a, b) => Number(a) > Number(b) },
- { op: '<', fn: (a, b) => Number(a) < Number(b) },
- ]
- const binary = findBinaryComparisonOp(expr, operators)
- if (binary) {
- const left = expr.slice(0, binary.index).trim()
- const right = expr.slice(binary.index + binary.op.length).trim()
- return binary.fn(parseValue(left), parseValue(right))
- }
- const value = parseValue(expr)
- if (typeof value === 'boolean') return value
- if (typeof value === 'string') {
- if (value === '' || value === 'undefined') return false
- try {
- const p = JSON.parse(value)
- if (Array.isArray(p)) return p.length > 0
- if (typeof p === 'object' && Object.keys(p).length === 0) return false
- } catch (e) {}
- return true
- }
- if (typeof value === 'number') return value !== 0
- return Boolean(value)
- }
- function evaluateCondition(condition, context) {
- if (!condition) return true
- try {
- let expr = condition
- const varPattern = /\{([\w-]+)\}/g
- expr = expr.replace(varPattern, (match, varName) => {
- const v = context[varName]
- if (v === undefined || v === null || v === '' || v === 'undefined' || v === 'null') return '""'
- if (typeof v === 'string') {
- const trimmed = v.trim()
- if (trimmed === '' || trimmed === 'undefined' || trimmed === 'null') return '""'
- try {
- const p = JSON.parse(trimmed)
- if (Array.isArray(p)) return `"${v.replace(/"/g, '\\"')}"`
- } catch (e) {}
- return `"${v.replace(/"/g, '\\"')}"`
- }
- if (Array.isArray(v)) {
- try {
- return `"${JSON.stringify(v).replace(/"/g, '\\"')}"`
- } catch (e) { return '"[]"' }
- }
- if (typeof v === 'number' || typeof v === 'boolean') return v
- return `"${String(v)}"`
- })
- return parseConditionExpression(expr)
- } catch (e) {
- return false
- }
- }
- module.exports = {
- parseArithmeticExpression,
- evaluateExpression,
- evaluateCondition,
- parseConditionExpression,
- }
|