代码编译初探 (中)- 前端必看

9,188 阅读3分钟

前言

这一篇来简单的实践一下编译器的词法分析,考虑到词法分析实现的复杂度,这里选择以 markdown 文本语言作为对象进行简单介绍。

正文

1. 什么是 markdown?

定义:

Markdown是一种轻量级标记语言,创始人为约翰·格鲁伯。它允许人们使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档。

由于Markdown的轻量化、易读易写特性,并且对于图片、图表、数学式、代码块都有支持,目前许多网站都广泛使用Markdown来撰写帮助文档或是用于论坛上发表消息。

标准化:

随着时间的推移,Markdown 已经成为典型的转换为HTML的非正式规范和参考实现,出现了许多Markdown实现。人们开发这些主要是由于在基本语法之上需要额外的功能 - 如表格,脚注,定义列表(技术上的HTML描述列表)和HTML块内的Markdown。与此同时,非正式规范中的一些含糊不清引起了人们的注意。这些问题促使Markdown解析器的一些开发人员努力实现标准化。

2016年3月发布了RFC 7763和RFC 7764。RFC 7763 从原始变体引入了MIME类型 text/markdown。RFC 7764讨论并注册了MultiMarkdown、GitHub Flavored Markdown (GFM)、Pandoc、CommonMark及Markdown等变体。

2. markdowm 与 html 的关系

下面列举了 部分 markdown语法 与 html 语法的对应关系

markdown 语法html 语法
# 标题1<h1>标题</h1>
## 标题2<h2> 标题2 </h2>
空白行分隔 表示段落<p></p>
行末尾 两个空格 表示换行<p> <br /> </p>
_斜体_<em>斜体</em>
粗体<strong>粗体</strong>
`代码`<code>代码<code>
---<hr />
* 1<ul> <li> 1 </li> </ul>

由于篇幅有限 ,这里先列举这么多语法,其他更多语法可以参考《了不起的Markdown》一书。

现在已经知道来 markdown 与 html 的语法对应关系出来,那么应该如何实现一个 compiler 编译器(解析器) 去进行语法识别和转换呢 ?

3. markdowm 词法分析

有了 markdown 的语法标准,就可以进行语法的分析,也就是这里的词法分析,下面以 heading 为例,

首先需要一个 词法解析器,按照 heading 的语法规则 生产出对应的 tokens 序列。

// 词法解析器
Class Lexer {
   constructor(options) {
        this.tokens = [];
        this.tokens.links = Object.create(null);
        this.options = options || {};
        this.rules = {
           // heading 匹配规则
           heading: /^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/
           // ...
        };
    }
    // 生成 token 序列
    token(src) {
        while (src) {
            // heading
            if (cap = this.rules.heading.exec(src)) {
                src = src.substring(cap[0].length);
                this.tokens.push({
                    type: 'heading',
                    depth: cap[1].length,
                    text: cap[2]
                });
                continue;
            }
            // other

            // ...
        }
        return this.tokens;
    }
}

4. markdown 编译(解析)成 html

上面已经拿到了 markdown 的 tokens 序列,下面还需要把它按照 html 的语法解析成对应的标签, 如下

// 解析器  - Parsing & Compiling
class Parser {
    constructor(options) {
        this.tokens = [];
        this.token = null;
        this.options = options || {};
        this.renderer = new Renderer();
    }
    parse(src) {
        this.tokens = src.reverse(); // 先进先出
        var out = '';
        while (this.next()) {
            out += this.tok();
        }
        return out;
    }
    next() {
        this.token = this.tokens.pop();
        return this.token;
    }
    tok() {
        switch (this.token.type) {
         // ...
        case 'heading': {
            return this.renderer.heading(
                this.token.text,
                this.token.depth
            )
        }
        // ...
        }
    }
}
// 渲染器(代码生产器) 
class Renderer {
    constructor(options) {
        this.options = options || {};
    }
    heading(text, level, raw) {
        if (this.options.headerIds) {
            return '<h'
            + level
            + ' id="'
            + this.options.headerPrefix
            + '">'
            + text
            + '</h'
            + level
            + '>\n';
        }
        return '<h' + level + '>' + text + '</h' + level + '>\n';
    };
}

后记

由于 markdown 是一种标记文本语言,因此这里代码解析转换时候 不需要对其进行 抽象语法树的分析(生成/分析 AST 的环节),相当于只进行了词法分析和代码生成的过程。其编译解析过程相对 图灵完备的语言 较简单。

思考题:

根据 javascript 语法 加减乘除 优先级,是否可以构建一个简单的文法解析器呢?

例如:var a = 1 + 3 * 2;


  • 相关文章

代码编译初探(下)