wxml 里有这样的一种模板语法:
<view wx:if="{{a === 1}}" />
注意 wx:if 的 value 部分,其本质上是一个字符串,解析的结果应为一个 JavaScript expression: a === 1
<view class="primary {{ appTheme==="dark"?'dark':'light' }} enable" />
注意 class 的 value 部分,其本质上是一个字符串,解析的结果应该类似于
`primary ${ appTheme==="dark"?'dark':'light' } enable`
后文将模板语法简称为“模板”,其类型为字符串。
DFA 是什么
DFA(deterministic finite automaton,确定有限状态自动机) 是一个能实现状态转移的自动机,由五部分组成:
- 一个非空有限状态集合
Set<State> - 一个输入字母表(非空有限字符集合)
Set<string> - 一个转移函数
(state:State, char:string) => State - 一个初始状态
State - 一个接受状态集合
Set<State>
画出该模板语法的状态图
首先考虑输入字母表,对于一个模板来说,里面会影响解析的字符有 :
{: 两个{为表达式的开始}: 两个}为表达式的结束': 单引号,代表字符串的开始,字符串里的其他特殊字符应被忽略,作为字符串的一部分(考虑{{index===1 ? '}}' : '{{'}}这样一个模板,其引号内部的{{}}不应有任何含义)": 双引号,代表字符串的开始,性质同单引号
除了这4个特殊字符外,其余的统一视为 other
DFA 如图所示,具体过程为:
先画一个初始状态 1,其接受 { 时会进入到下一个状态 2,表示此时正在等待下一个 { 。而对于其他所有的输入,状态都会转移到 1 本身
对于状态 2,此时正在等待下一个 {,所以接受 { 时进入下一个状态 3。而对于其他所有的输入,状态会回到 1,表示之前接受的 { 只是一个普通的字符,不是一个表达式的开始
对于状态 3,此时已经接受完了两个 {,表示已经进入了一个表达式内部。对于 },3 会进入下一个状态 4, 表示此时正在等待下一个 };对于 ',3 会进入到状态 5,代表此时进入了表达式内的一个由单引号包围的字符串中;同理对于 ",3会进入到状态 6,代表此时进入了表达式内的一个由双引号包围的字符串中。而对于其他的输入,都会停留在状态3,表示一直在接受表达式
对于状态4,此时正在等待下一个 },所以接受 } 时代表一个表达式已经接收完毕,回到状态1循环整个过程。而对于其他所有的输入,都回到状态3,表示之前接受的 } 只是表达式的一部分,不是一个表达式的结束
对于状态 5 和 6,接受 ' 或者 " 时表示一个字符串结束了,对于其他所有输入都表示一直在接受字符串
一个 DFA 状态图中的每个状态,对于字母表中的每个字符输入,都必须有且仅有一个下个状态,即可构成一个状态转移表:
| { | } | ' | " | other | |
|---|---|---|---|---|---|
| 1 | 2 | 1 | 1 | 1 | 1 |
| 2 | 3 | 1 | 1 | 1 | 1 |
| 3 | 3 | 4 | 5 | 6 | 3 |
| 4 | 3 | 1 | 3 | 3 | 3 |
| 5 | 5 | 5 | 3 | 5 | 5 |
| 6 | 6 | 6 | 6 | 3 | 6 |
这个状态转移表即表示 DFA 中的状态转移函数。
目前为止,该 DFA 已经具备了
- 状态集合:1, 2, 3, 4, 5, 6
- 输入字母表
{}'"other - 转移函数
- 初始状态 1
- 接受状态集合
[1]接下来用代码实现就简单了:
代码实现
目标是设计一个函数,该函数接受一个模板,返回一个对象 { success:boolean, exprs: Array}
success 代表解析是否成功,即该模板是否合法
exprs 是一个数组, 其中每一项格式为 { type:string, value:string }
type 代表类型,有 normal 和 expr 两种,分别代表普通字符串和表达式字符串
value 为具体值
eg: 输入 primary {{ appTheme==='dark'?'dark':'light' }} enable 这样一个模板, 返回
{
"success": true,
"exprs": [
{ "type": "normal", "value": "primary " },
{ "type": "expr", "value": "{{ appTheme==='dark'?'dark':'light' }}" },
{ "type": "normal", "value": " enable" },
]
}
其中 type 为 expr 的字符串部分就可以交给 babel 去 parse
完整代码:
enum STATE { // 状态集合
INIT_OR_END = 1,
RECEIVED_ONE_LEFT_BRACE,
IN_EXPR,
RECEIVED_ONE_RIGHT_BRACE,
IN_EXPR_SINGLE_QUOTE,
IN_EXPR_DOUBLE_QUOTE,
}
enum ALPHABET { // 输入字母表
LEFT_BRACE = 1,
RIGHT_BRACE,
SINGLE_QUOTE,
DOUBLE_QUOTE,
OTHER,
}
const MOVE_MATRIX = [
// 状态转移矩阵
[2, 1, 1, 1, 1],
[3, 1, 1, 1, 1],
[3, 4, 5, 6, 3],
[3, 1, 3, 3, 3],
[5, 5, 3, 5, 5],
[6, 6, 6, 3, 6],
];
const move = (state: STATE, char: ALPHABET) => {
// 状态转移函数
return MOVE_MATRIX[state - 1][char - 1]; // 因为状态枚举中,数字是从1而不是从0开始的,所以用于数组索引时要-1
};
const FINAL_STATES = new Set([STATE.INIT_OR_END]); // 接受状态集合,这里就一个状态 1
const INIT_STATE = STATE.INIT_OR_END; // 初始状态 1
function mapCharToAlphabet(char: string): ALPHABET {
// 将每个字符映射到字母表里
switch (char) {
case `{`:
return ALPHABET.LEFT_BRACE;
case `}`:
return ALPHABET.RIGHT_BRACE;
case `'`:
return ALPHABET.SINGLE_QUOTE;
case `"`:
return ALPHABET.DOUBLE_QUOTE;
default:
return ALPHABET.OTHER;
}
}
interface Expr {
type: SLICE_TYPE;
value: string;
}
export enum SLICE_TYPE {
NORMAL = 'normal',
EXPR = 'expr',
}
export default function tokenize(text: string): {
success: boolean;
exprs: Expr[];
} {
let char = ALPHABET.OTHER;
let prevState = INIT_STATE;
let state = INIT_STATE;
let sectionStart = 0;
let sectionEnd = 0;
const exprs: Expr[] = [];
function emit(type: SLICE_TYPE) {
// 往 exprs 里塞一个元素
const slice = text.slice(sectionStart, sectionEnd + 1);
if (slice.length > 0) {
exprs.push({ type, value: slice });
}
}
for (let i = 0; i < text.length; i++) {
char = mapCharToAlphabet(text.charAt(i));
prevState = state;
state = move(state, char); // 状态转移
if (
prevState === STATE.RECEIVED_ONE_LEFT_BRACE &&
state === STATE.IN_EXPR
) {
// 从2转移到3,说明进入了一个 expr
sectionEnd = i - 2;
emit(SLICE_TYPE.NORMAL); // emit 该 expr 之前的部分,一定是 normal text(两个紧挨着的 expr 中间的空字符串会在 emit 中被过滤掉)
sectionStart = i - 1;
} else if (
prevState === STATE.RECEIVED_ONE_RIGHT_BRACE &&
state === STATE.INIT_OR_END
) {
// 从4转移到5,说明退出了一个 expr
sectionEnd = i;
emit(SLICE_TYPE.EXPR); // emit 该 expr
sectionStart = i + 1;
}
}
sectionEnd = text.length - 1;
emit(SLICE_TYPE.NORMAL); // emit 最后一部分 normal text(如果有)
if (FINAL_STATES.has(state)) {
// 如果最终状态在接受状态集合里,说明输入合法,解析成功
return {
success: true,
exprs,
};
} else {
return {
success: false,
exprs: [],
};
}
}
总结
DFA 是编译的词法分析中一个很强大很方便的工具。对于一个语法的解析,只需要找到 DFA 的五个部分(实际工作中这个过程有可能会非常困难,尤其是对于 GPL),剩下的工作就是在状态转移过程中不断 emit 接受完毕的 token 并做对应的处理。