UNPKG

@winged/core

Version:

Morden webapp framekwork made only for ts developers. (UNDER DEVELOPMENT, PLEASE DO NOT USE)

429 lines (418 loc) 13.8 kB
import { StateDependencies, ViewState } from '../../types' import { utils } from '../../utils' import { vdomUtils } from '../vdomUtils' export class ExpressionCompileError extends Error { public readonly expression: string public readonly index: number constructor(expression: string, index: number, message: string) { super(message) this.expression = expression this.index = index } } const Regs = { validNameStarterChar: /[a-zA-Z_]/, validNameChar: /[a-zA-Z0-9_]/, nameMatcher: /([a-zA-Z_]\w*)/g, stringMatcher: /(["'])((?:[^\1\\]|\\.)*?)\1/g } type DataSet = ViewState const enum LexType { String, Name, Operator } interface LexicialPart { type: LexType /** exists when type is string | name */ value?: string /** exists when type is operator */ operator?: '==' | '!=' | '||' | '.' | ':' | '?' | '!' } abstract class GrammarNode { public abstract getValue(dataSet: DataSet): any } class NameNode extends GrammarNode { public dataName: string // TODO: rewrite to use DataPath object instead of child tree public child?: NameNode public getValue(dataSet: DataSet): any { const v = dataSet[this.dataName] if (v === null || v === undefined) { return null } if (this.child) { if ((v as any)[this.child.dataName]) { return this.child.getValue(v as ViewState) } else { return null } } else { if (utils.listContains(['string', 'number', 'boolean'], typeof v)) { return v } else { return true } } } public toStateDependencies() { const depsTree: StateDependencies = { [this.dataName]: {} } let node: NameNode = this let pathNode = depsTree[this.dataName] while (node.child) { node = node.child pathNode[node.dataName] = {} pathNode = pathNode[node.dataName] } return depsTree } } class ValueNode extends GrammarNode { public type: 'string' | 'name' // no number and bool public nameNode?: NameNode public stringValue?: string public getValue(dataSet: DataSet): any { if (this.type === 'string') { return this.stringValue } else { return (this.nameNode as NameNode).getValue(dataSet) } } } class ConditionNode extends GrammarNode { public cond: CalculationNode public trueValue: ValueNode public falseValue?: ValueNode public getValue(dataSet: DataSet): any { if (this.cond.getValue(dataSet)) { return this.trueValue.getValue(dataSet) } else { if (this.falseValue) { return this.falseValue.getValue(dataSet) } else { return null } } } } export class CalculationNode extends GrammarNode { public operation?: '==' | '!=' | '!' | '||' public leftHand: ValueNode public rightHand?: ValueNode // exists when operation not null public getValue(dataSet: DataSet): boolean { if (!this.operation) { return this.leftHand.getValue(dataSet) } if (this.operation === '||') { const leftValue = this.leftHand.getValue(dataSet) if (leftValue !== null && leftValue !== undefined) { return leftValue } else { return (this.rightHand as ValueNode).getValue(dataSet) } } if (this.operation === '!=') { if (this.leftHand.getValue(dataSet) !== (this.rightHand as ValueNode).getValue(dataSet)) { return true } else { return false } } if (this.operation === '==') { if (this.leftHand.getValue(dataSet) === (this.rightHand as ValueNode).getValue(dataSet)) { return true } else { return false } } if (this.operation === '!') { return !(this.leftHand.getValue(dataSet)) } return false } } export class DataExpression { protected get fullExpression() { return `{{${this.expression}}}` } // ** 数据依赖 */ public stateDependencies: StateDependencies = {} /** 求值表达式, 如 `a?1:2` */ protected expression: string /** 求值表达式, 包括包裹符, 如 "{{a?1:2}}" */ protected lexicialParts: LexicialPart[] = [] protected rootGrammarNode: ValueNode | CalculationNode | ConditionNode constructor(expression: string) { this.expression = expression this.compile() } public evaluate(dataSet: DataSet): string { return this.rootGrammarNode.getValue(dataSet) } public getExpression() { return this.expression } public reCompile(expression: string) { this.expression = expression this.lexicialParts = [] delete this.rootGrammarNode this.compile() } /** * 在调用每个 getNode 方法时,传入的 index 是下一个需要取用的 lexicialPart 的下标 * 每个 getNode 方法都会返回一个 [LexicialNode, offset] 结果数组 * 其中的 offset 代表此方法最后取用的 lexicialPart 的下标,需要由调用方手动处理向后偏移(通常 +1 即可) */ protected getRootNode(): DataExpression['rootGrammarNode'] | null { let rootNode: GrammarNode let offset: number // {{Value}} [rootNode, offset] = this.getValueNode(0) if (rootNode && offset === this.lexicialParts.length - 1) { return rootNode as ValueNode } // {{Calc}} [rootNode, offset] = this.getCalculationNode(0) if (rootNode && offset === this.lexicialParts.length - 1) { return rootNode as CalculationNode } // {{Cond}} [rootNode, offset] = this.getConditionNode(0) if (rootNode && offset === this.lexicialParts.length - 1) { return rootNode as ConditionNode } return null } protected getConditionNode(index: number): [ConditionNode, number] { // Calc?Value:Value // Calc?Value const rootNode = new ConditionNode() let offset: number [rootNode.cond, offset] = this.getCalculationNode(index) if (!rootNode.cond) { return [null, null] as any } index = offset + 1 if (this.getLexicialPart(index).operator !== '?') { return [null, null] as any } index += 1; [rootNode.trueValue, offset] = this.getValueNode(index) if (!rootNode.trueValue) { throw new ExpressionCompileError( this.expression, index, `Invalid condition expression in data point ${this.fullExpression}, expected a value or name after "?"` ) } index = offset + 1 if (this.getLexicialPart(index).operator !== ':') { return [rootNode, index - 1] } index += 1; [rootNode.falseValue, offset] = this.getValueNode(index) if (!rootNode.falseValue) { throw new ExpressionCompileError( this.expression, index, `Invalid condition expression in data point ${this.fullExpression}, expected a value or name after ":"` ) } return [rootNode, offset] } protected getCalculationNode(index: number): [CalculationNode, number] { // Value==Value // Value!=Value // Value||Value // !Value // Value const rootNode = new CalculationNode() let offset: number if (this.getLexicialPart(index).operator === '!') { // !Value index += 1; [rootNode.leftHand, offset] = this.getValueNode(index) if (!rootNode.leftHand) { throw new ExpressionCompileError( this.expression, index, `Invalid calculation expression in data point ${this.fullExpression}, expected a value or name after "!"` ) } rootNode.operation = '!' return [rootNode, offset] } else { // Value [rootNode.leftHand, offset] = this.getValueNode(index) if (!rootNode.leftHand) { return [null, null] as any } // Value==Value or Value||Value or Value!=Value index = offset + 1 const operator = this.getLexicialPart(index).operator if (operator !== '==' && operator !== '||' && operator !== '!=') { return [rootNode, index - 1] } else { rootNode.operation = operator } index += 1; [rootNode.rightHand, offset] = this.getValueNode(index) if (!rootNode.rightHand) { throw new ExpressionCompileError( this.expression, index, `Invalid calculation expression in data point ${this.fullExpression}, expected a value or name after "=="` ) } return [rootNode, offset] } } protected getValueNode(index: number): [ValueNode, number] { const rootNode = new ValueNode() if (this.getLexicialPart(index).type === LexType.String) { rootNode.type = 'string' rootNode.stringValue = this.getLexicialPart(index).value return [rootNode, index] } let offset: number [rootNode.nameNode, offset] = this.getNameNode(index) vdomUtils.mergeStateDependenciesN(this.stateDependencies, rootNode.nameNode.toStateDependencies()) if (rootNode.nameNode) { rootNode.type = 'name' return [rootNode, offset] } return [null, null] as any } protected getNameNode(index: number): [NameNode, number] { const rootNode = new NameNode() let offset: number const part = this.getLexicialPart(index) if (part.type !== LexType.Name) { return [null, null] as any } rootNode.dataName = part.value as string index += 1 if (this.getLexicialPart(index).operator !== '.') { return [rootNode, index - 1] } index += 1; [rootNode.child, offset] = this.getNameNode(index) if (!rootNode.child) { throw new ExpressionCompileError( this.expression, index, `Invalid data getter in data point ${this.fullExpression}, expected a name after "."` ) } return [rootNode, offset] } private compile() { // lexicial analysis let state: ('name' | 'string' | 'none') = 'none' let stringQuote: '"' | '\'' | null = null let nameBuffer: string[] = [] for (let arr = this.expression, i = 0; i < arr.length; i++) { const c = arr[i] if (state === 'name') { if (!Regs.validNameChar.test(c)) { state = 'none' const name = nameBuffer.join('') this.checkName(name, i - 1) this.lexicialParts.push({ type: LexType.Name, value: name }) // roll back 1 turn to check this char i -= 1 } else { nameBuffer.push(c) } } else if (state === 'string') { if (c === stringQuote) { state = 'none' stringQuote = null this.lexicialParts.push({ type: LexType.String, value: nameBuffer.join('') }) } else { nameBuffer.push(c) } } else { // null if (c === '"' || c === '\'') { // handle string starter state = 'string' stringQuote = c nameBuffer = [] } else if (c === '=' || c === '|') { // handle operator "==" and "||" if (arr[i + 1] === c) { if (c === '=') { this.lexicialParts.push({ type: LexType.Operator, operator: '==' }) } else { this.lexicialParts.push({ type: LexType.Operator, operator: '||' }) } } else { throw new ExpressionCompileError( this.expression, i, `Invalid char "${c}", do you mean "${c}${c}"?` ) } // step over next '=' or '|' i += 1 } else if (c === '!') { if (arr[i + 1] === '=') { this.lexicialParts.push({ type: LexType.Operator, operator: '!=' }) // step over next '=' i += 1 } else { this.lexicialParts.push({ type: LexType.Operator, operator: '!' }) } } else if (c === ' ') { // skip space continue } else if (c === '.' || c === '?' || c === ':') { // handle other operators this.lexicialParts.push({ type: LexType.Operator, operator: c }) } else if (Regs.validNameStarterChar.test(c)) { // handle name starter state = 'name' nameBuffer = [c] } else { // handle standalone digital if (c.match('[0-9]')) { throw new ExpressionCompileError( this.expression, i, `Invalid char "${c}" in ${this.fullExpression}, the use of number value was not permitted.` ) } else if (c === '>') { throw new ExpressionCompileError( this.expression, i, `Invalid char "${c}" in ${this.fullExpression},` + 'If you wan\'t to use ViewPoint/ViewListPoint, make sure it\'s placed on the right place;' ) } else { throw new ExpressionCompileError( this.expression, i, `Invalid char "${c}" in ${this.fullExpression}` ) } } } } if (state === 'name') { const name = nameBuffer.join('') this.checkName(name, 0) this.lexicialParts.push({ type: LexType.Name, value: name }) } // grammar analysis const rootNode = this.getRootNode() if (!rootNode) { throw new ExpressionCompileError( this.expression, 0, `Can't parse data expression "${this.fullExpression}"` ) } this.rootGrammarNode = rootNode } private checkName(name: string, index: number): void { if (name === 'true' || name === 'false') { throw new ExpressionCompileError( this.expression, index, `Invalid name ${name}. in ${this.fullExpression}.` + ' If you want to use conditional render, use grammar like {{flag?\'res\'}} or {{!flag?\'res\'}} instead' ) } } private getLexicialPart(index: number): LexicialPart { if (!this.lexicialParts[index]) { return { type: null } as any } return this.lexicialParts[index] } }