@winged/core
Version:
Morden webapp framekwork made only for ts developers. (UNDER DEVELOPMENT, PLEASE DO NOT USE)
429 lines (418 loc) • 13.8 kB
text/typescript
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]
}
}