最近需要写一个编辑扩展组件,主要功能类似于Excel的单元格编辑框,主要针对单元格输入内容的处理。要知道在Excel中,每个单元格除了可以输入文本内容(包括字符、数字、日期等)外,还有包括函数。 那么在输入函数时,如果聚焦到函数(比如IF VLOOKUP)或单元格(比如A1 A2)上,都会有对应的响应,那么我们输入的文本,是如何识别到其中的函数,常量,单元格的呢?并且在被聚焦后能够做出对应的响应?

基于这样的需求,经过分析后发现真正需要做的事情,其实是将输入的内容进行解析,然后根据解析的结果,进行相应的处理,最终生成一段HTML内容,而所有的点击响应都是对DOM元素的事件监听而已, 而这里面的关键则是如何将输入的内容,解析成对应的AST,后面生成HTML的过程也就简单了。

现在有这样的一个Excel公式,调用了IF、VLOOKUP函数,引用了单元格与外部sheet区域,包括操作符号,常量等等元素:

IF(VLOOKUP(A1,"sheet1"!$A$1:$C$50,1,2) > 3, 4, 5)

接下来我们需要做的是将上面的文本转换成下面的存储结构,也就是AST(抽象语法树):

{
  type: 'Program',
  body: [{
 type: 'function',
 name: 'IF',
 params: [
        {
            type: 'expression,
            body: [
                {
                    type: 'function',
                    name: 'VLOOKUP',
                    params: [
                        {
                            type: 'cell',
                            value: 'A1',
                        }, 
                        {
                            type: 'range',
                            ref: 'sheet1'
                            value: '$A$1:$C$50',
                        },
                        {
                            type: 'number',
                            value: '1'
                        },
                        {
                            type: 'number',
                            value: '2'
                        }
                    ]
                },
                {
                    type: 'operator',
                    value: '>'
                },
                {
                    type: 'number',
                    value: '3'
                }
            ]
        },
        {
            type: 'number',
            value: '4',
        }, 
        {
            type: 'NumberLiteral',
            value: '5',
        }
    ]
  }]
}

基于结构化的树,可以很放标转换成类似下面的HTML形式,要知道对html的操作是非常方便的:

<div class="editor">
    <span class="function">IF</span>
    <span>(</span>
    <span class="exception">
        <span>VLOOKUP</span>
        <span>(</span>
        <span class="cell">A1</span>
        <span>,</span>
        <span class="ref">sheet1!</span>
        <span class="range">"sheet"!$A$1:$C$50</span>
        <span>,</span>
        <span class="number">1</span>
        <span>,</span>
        <span class="number">2</span>
        <span>)</span>
        <span class="operator">></span>
        <span class="number">3</span>
    </span>
    <span class="number">4</span>
    <span>,</span>
    <span class="number">5</span>
    <span>)</span>
</div>

最后为为上面的html内容添加样式,并为不同类型的元素绑定事件与处理逻辑,这样不仅可以对输入内容高亮,在点击函数、单元格时,还可以给出对应的提示信息或响应。


上面的内容仅仅是实现一个功能的思考与假设,实际的实现可能有所差异,但是实现的过程是类似的。

在我们使用的所有编程语言,比如Java、C、javascript等,我们都会编写文本格式的源代码,编译器或解释器会将源代码按照语言语法解析成对应的语法结构树,基于该结构一来可以实现语法检查、代码高亮等功能,同时针对特定代码块可以有很多其他操作。

实现

通过上面的分析,可以看到核心要实现的是将文本内容解析成语法树的过程。在完成这部分功能的过程中,看到一个基于javascript实现的类似功能的例子the-super-tiny-compiler.js,通过几百行的代码,把这这一过程清晰地表现出来。

其过程主要包括下面几个步骤:

  1. tokenizer:词法分析的,以字符为单位逐个遍历文本,构建成一个token数组来描述文本内容,其中每一个token主要包含type,value
  2. parser:语法分析,将上面的token数组转换树结构
  3. transformer:遍历上面语法树各种类型的节点,针对不同的节点类型,执行不同的操作,生成最终带有语言特性标识的语法树
  4. codeGenerator:基于语法树生成最终的代码

下面是具体的实现过程:

tokenizer

function tokenizer(input) {

  let current = 0;

  let tokens = [];

  while (current < input.length) {

    let char = input[current];

    if (char === '(') {

      tokens.push({
        type: 'paren',
        value: '(',
      });

      current++;

      continue;
    }

    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }

    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {

      let value = '';

      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({ type: 'number', value });

      continue;
    }

    if (char === '"') {
      let value = '';

      char = input[++current];

      while (char !== '"') {
        value += char;
        char = input[++current];
      }

      char = input[++current];

      tokens.push({ type: 'string', value });

      continue;
    }

    let LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      let value = '';

      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({ type: 'name', value });

      continue;
    }

    throw new TypeError('I dont know what this character is: ' + char);
  }

  return tokens;
}

parser

function parser(tokens) {

  let current = 0;

  function walk() {

    let token = tokens[current];

    if (token.type === 'number') {

      current++;

      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    if (token.type === 'string') {
      current++;

      return {
        type: 'StringLiteral',
        value: token.value,
      };
    }

    if (
      token.type === 'paren' &&
      token.value === '('
    ) {

      token = tokens[++current];

      let node = {
        type: 'CallExpression',
        name: token.value,
        params: [],
      };

      token = tokens[++current];


      while (
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        node.params.push(walk());
        token = tokens[current];
      }

      current++;

      return node;
    }

    throw new TypeError(token.type);
  }

  let ast = {
    type: 'Program',
    body: [],
  };

  while (current < tokens.length) {
    ast.body.push(walk());
  }

  return ast;
}

traverser

function traverser(ast, visitor) {

  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  function traverseNode(node, parent) {

    let methods = visitor[node.type];

    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    switch (node.type) {

      case 'Program':
        traverseArray(node.body, node);
        break;

      case 'CallExpression':
        traverseArray(node.params, node);
        break;

      case 'NumberLiteral':
      case 'StringLiteral':
        break;

      default:
        throw new TypeError(node.type);
    }

    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }

  traverseNode(ast, null);
}

transformer

function transformer(ast) {

  let newAst = {
    type: 'Program',
    body: [],
  };

  ast._context = newAst.body;

  traverser(ast, {

    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },

    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },

    CallExpression: {
      enter(node, parent) {

        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };

        node._context = expression.arguments;

        if (parent.type !== 'CallExpression') {

          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }

        parent._context.push(expression);
      },
    }
  });

  return newAst;
}

codeGenerator

function codeGenerator(node) {

  switch (node.type) {

    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');

    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) +
      );

    case 'CallExpression':
      return (
        codeGenerator(node.callee) +
        '(' +
        node.arguments.map(codeGenerator)
          .join(', ') +
        ')'
      );

    case 'Identifier':
      return node.name;

    case 'NumberLiteral':
      return node.value;

    case 'StringLiteral':
      return '"' + node.value + '"';

    default:
      throw new TypeError(node.type);
  }
}

compiler

function compiler(input) {
  let tokens = tokenizer(input);
  let ast    = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);

  return output;
}
const input  = '(add 2 (subtract 4 2))';
compiler(input);

感兴趣的可以查看相关的源代码,上面的解释比代码还多,逐行都有非常详细的描述。虽然只是一段简单的示例,但是将一段代码的编译过程都清晰地展现出来,对理解编译过程非常有帮助。像Vue React这样的框架,其编译过程也是基于这个思路,通过一系列的处理流程,最终生成带有语言特性的内容。

引用与其他博主,如有侵权请联系删除。