使用 DFA 实现一个 wxml 模板解析器

·  阅读 298
使用 DFA 实现一个 wxml 模板解析器

wxml 里有这样的一种模板语法:

<view wx:if="{{a === 1}}" />
复制代码

注意 wx:ifvalue 部分,其本质上是一个字符串,解析的结果应为一个 JavaScript expression: a === 1

<view class="primary {{ appTheme==="dark"?'dark':'light' }} enable" />
复制代码

注意 classvalue 部分,其本质上是一个字符串,解析的结果应该类似于

`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
121111
231111
334563
431333
555355
666636

这个状态转移表即表示 DFA 中的状态转移函数。

目前为止,该 DFA 已经具备了

  • 状态集合:1, 2, 3, 4, 5, 6
  • 输入字母表 { } ' " other
  • 转移函数
  • 初始状态 1
  • 接受状态集合 [1] 接下来用代码实现就简单了:

代码实现

目标是设计一个函数,该函数接受一个模板,返回一个对象 { success:boolean, exprs: Array}

success 代表解析是否成功,即该模板是否合法

exprs 是一个数组, 其中每一项格式为 { type:string, value:string }

type 代表类型,有 normalexpr 两种,分别代表普通字符串和表达式字符串

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" },
  ]
}
复制代码

其中 typeexpr 的字符串部分就可以交给 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 并做对应的处理。

分类:
前端
收藏成功!
已添加到「」, 点击更改