手把手教你写一个 AST 抽象语法树

您所在的位置:网站首页 编译原理抽象语法树怎么画 手把手教你写一个 AST 抽象语法树

手把手教你写一个 AST 抽象语法树

2024-05-17 06:16| 来源: 网络整理| 查看: 265

AST 解析器工作中经常用到,Vue.js 中的 VNode 就是如此! 其实如果有需要将 非结构化数据转 换成 结构化对象用 来分析、处理、渲染的场景,我们都可以用此思想做转换。

如何解析成 AST ?

我们知道 HTML 源码只是一个文本数据,尽管它里面包含复杂的含义和嵌套节点逻辑,但是对于浏览器,Babel 或者 Vue 来说,输入的就是一个长字符串,显然,纯粹的一个字符串是表示不出来啥含义,那么就需要转换成结构化的数据,能够清晰的表达每一节点是干嘛的。字符串的处理,自然而然就是强大的正则表达式了。

本文阐述 AST 解析器的实现方法和主要细节,简单易懂~~~~~~~~,总共解析器代码不过百行!

目标

本次目标,一步一步将如下 HTML 结构文档转换成 AST 抽象语法树

代码语言:javascript复制我是外层div 我是内层span

结构比较简单,外层一个 div,内层嵌套一个 span,外层有 class,data,stye 等属性。 麻雀虽小,五脏俱全,基本包含我们经常用到的了。其中转换后的 AST 结构 有哪些属性,需要怎样的形式显示,都可以根据需要自己定义即可。 本次转换后的结构:

代码语言:javascript复制{ "node": "root", "child": [{ "node": "element", "tag": "div", "class": "classAttr", "dataset": { "type": "dataType", "id": "dataId" }, "attrs": [{ "name": "style", "value": "color:red" }], "child": [{ "node": "text", "text": "我是外层div" }, { "node": "element", "tag": "span", "dataset": {}, "attrs": [], "child": [{ "node": "text", "text": "我是内层span" }] }] }] }

不难发现,外层是根节点,然后内层用 child 一层一层标记子节点,有 attr 标记节点的属性,classStr 来标记 class 属性,data 来标记 data- 属性,type 来标记节点类型,比如自定义的 data-type="title" 等。

回顾正则表达式

先来看几组简单的正则表达式:

^ 匹配一个输入或一行的开头,/^a/匹配"ab",而不匹配"ba"匹配一个输入或一行的结尾,/匹配"ba",而不匹配"ab"匹配前面元字符 0 次或多次,/ab*/将匹配 a,ab,abb,abbb匹配前面元字符 1 次或多次,/ab+/将匹配 ab,abb,但是不匹配 a[ab] 字符集匹配,匹配这个集合中的任一一个字符(或元字符),/[ab]/将匹配 a,b,ab\w 组成单词匹配,匹配字母,数字,下划线,等于[a-zA-Z0-9]匹配标签元素

首先我们将如下的 HTML 字符串用正则表达式表示出来:

代码语言:javascript复制我是一个div

这个字符串用正则描述大致如下:

以 < 开头 跟着 div 字符,然后接着 > ,然后是中文 “我是一个 div”,再跟着 结尾。

1. div 是 HTML 的标签,我们知道 HTML 标签是已字母和下划线开头,包含字母、数字、下滑线、中划线、点号组成的,对应正则如下:

代码语言:javascript复制const ncname = '[a-zA-Z_][\w-.]*'

于是组合的正则表达式如下:

代码语言:javascript复制``

2. 根据上面分析,很容易得出正则表达式为下:

代码语言:javascript复制``

3. 我是一个div

标签内可以是任意字符,那么任意字符如何描述呢? \s 匹配一个空白字符 \S 匹配一个非空白字符 \w 是字母数字数字下划线 \W 是非\w 的 同理还有\d 和\D 等。 我们通常采用\s 和\S 来描述任何字符(1、通用,2、规则简单,利于正则匹配):

代码语言:javascript复制`[\s\S]*`匹配标签属性

HTML 标签上的属性名称有哪些呢,常见的有 class,id,style,data-属性,当然也可以用户随便定义。但是属性名称我们也需要遵循原则,通常是用字母、下划线、冒号开头(Vue 的绑定属性用:开头,通常我们不会这么定义)的,然后包含字母数字下划线中划线冒号和点的。正则描述如下:

代码语言:javascript复制const attrKey = /[a-zA-Z_:][-a-zA-Z0-9_:.]*/

HTML 的属性的写法目前有以下几种:

class="title"class='title'class=title代码语言:javascript复制const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)=("([^"]*)"|'([^']*)'|([^\s"'=`]+)/

attrKey 跟着 = ,然后跟着三种情况:

” 开头 跟着多个不是 " 的字符,然后跟着 ” 结尾' 开头 跟着多个不是 ‘ 的字符,然后跟着 ' 结尾不是(空格,”,’,=,)的多个字符

我们测试一下 attr 的正则

代码语言:javascript复制"class=abc".match(attr); // output (6) ["class=abc", "class", "abc", undefined, undefined, "abc", index: 0, input: "class=abc", groups: undefined] "class='abc'".match(attr); // output (6) ["class='abc'", "class", "'abc'", undefined, "abc", undefined, index: 0, input: "class='abc'", groups: undefined]

我们发现,第二个带单引号的,匹配的结果是"‘abc’",多了一个单引号‘,因此我们需要用到正则里面的非匹配获取(?:)了。

例子:

代码语言:javascript复制"abcde".match(/a(?:b)c(.*)/); 输出 ["abcde", "de", index: 0, input: "abcde"]

这里匹配到了 b,但是在 output 的结果里面并没有 b 字符。 场景:正则需要匹配到存在 b,但是输出结果中不需要有该匹配的字符。 于是我么增加空格和非匹配获取的属性匹配表达式如下:

代码语言:javascript复制const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=`]+))/

= 两边可以增加零或多个空格,= 号右边的匹配括号使用非匹配获取,那么类似 = 号右侧的最外层大括号的获取匹配失效,而内层的括号获取匹配的是在双引号和单引号里面。效果如下:

从图中我们清晰看到,匹配的结果的数组的第二位是属性名称,第三位如果有值就是双引号的,第四位如果有值就是单引号的,第五位如果有值就是没有引号的。

匹配节点

有了上面的标签匹配和属性匹配之后,那么将两者合起来就是如下:

代码语言:javascript复制//; const endTag = //; const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=`]+))/g // 其他的就是标签里面的内容了

不难发现,标签已 < 开头,为标签起始标识位置,已



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3