前言虚拟语法树(AbstractSyntaxTree,AST)是解释器/编译器进行语法分析的基础,也是很多前端编译工具的基础工具,比如webpack、postcss、less等。对于ECMAScript来说,由于前端轮子多,人手太多了,早就累死人了。语法分析器有uglify,acorn,babyon,typescript,esprima等几种类型,还有一个AST的社区标准:ESTree。本文主要介绍如何编写一个AST解析器,但不是通过分析JavaScript,而是通过分析html5的语法树。使用html5的原因有两个:一是它的语法简单,总结起来只有两种:Text和Tag,二是因为JavaScript的语法分析器已经太多了,重新构建也没有意义另一个轮子。对于html5,虽然有很多AST分析器,比如htmlparser2、parser5等,但是都没有ESTree那么标准。同时,这些分析器存在一个问题:就是无法在定义的语法树中操作标签属性。所以为了解决这个问题,我写了一个html语法分析器,并定义了一个完整的AST结构,然后本文Article.AST定义为了跟踪每个节点的position属性,先定义一个基节点,所有节点继承从这个节点:exportinterfaceIBaseNode{start:number;//节点起始位置end:number;//节点结束位置}上面说了html5的语法类型可以分为两种,一种是Text,一种是Tag,这里用一个枚举类型来标记。exportenumSyntaxKind{Text='Text',//文本类型Tag='Tag',//标签类型}对于文本,它的属性只有一个原始字符串值,所以结构如下:exportinterfaceITextextendsIBaseNode{类型:SyntaxKind.Text;//类型值:字符串;//originalstring}对于Tag,它应该包括打开的标签开始部分,属性列表属性、标签名称、子标签/文本主体和标签结束部分close:exportinterfaceITagextendsIBaseNode{type:SyntaxKind.Tag;//输入open:IText;//标签开始部分,如name:string;//标签名称,全部转为小写属性:IAttribute[];//属性列表主体:Array//子节点列表,如果是非自闭合标签,且起始标签已经结束,则为数组|void//如果是自闭标签则为void0|无效的;//如果开始标签没有关闭,则为nullclose:IText//关闭标签部分,如果存在,则为文本节点|void//自闭标签没有结束部分|无效的;//非自闭合标签,但没有闭合标签部分}标签的属性是一个键值对,包括名称name和值value部分,定义结构如下:exportinterfaceIAttributeextendsIBaseNode{名称:IText;//名称值:IAttributeValue|空白;//value}其中name是一个普通的文本节点,但是value比较特殊,可能会被单/双引号括起来,引号是没有意义的,所以定义一个标签值结构体:exportinterfaceIAttributeValueextendsIBaseNode{value:细绳;//值,不包括引号quote:'\''|'"'|void;//引号类型,可能是',",也可能不是}Token解析AST解析首先需要解析原文得到符号列表,再通过上下文分析得到最终结果语法树。与JSON相比,html虽然看起来简单,但是需要context,所以虽然JSON可以直接通过token解析得到最终的结果,而html则不行。令牌分析是第一步,这是必需的。(JSON解析可以参考我的另一篇文章:写意写一个JSON解析器(Golang))。解析token时,需要根据当前状态分析token的含义,然后得到一个token列表。首先,定义令牌的结构:exportinterfaceIToken{start:number;//起始位置end:number;//结束位置值:字符串;//代币类型:TokenKind;//type}Token类型有以下类型:exportenumTokenKind{Literal='Literal',//textOpenTag='OpenTag',//标签名OpenTagEnd='OpenTagEnd',//开始标签结束,可能是'/',or'','--'CloseTag='CloseTag',//关闭标签Whitespace='Whitespace',//开始标签类属性值之间的空格AttrValueEq='AttrValueEq',//=inattributeAttrValueNq='AttrValueNq',//属性中不带引号的值AttrValueSq='AttrValueSq',//单引号包裹的属性值AttrValueDq='AttrValueDq',//双引号包裹的属性值}Token不考虑key/分析时属性的值关系,统一作为属性中的一个分片,同时作为一个特殊的独立分片,交给上层解析器进行key-value分析关系。这样做的原因是为了避免令牌分析时进行上下文处理,简化状态机状态表。状态列表如下:enumState{Literal='Literal',BeforeOpenTag='BeforeOpenTag',OpeningTag='OpeningTag',AfterOpenTag='AfterOpenTag',InValueNq='InValueNq',InValueSq='InValueSq',InValueDq='InValueDq',ClosingOpenTag='ClosingOpenTag',OpeningSpecial='OpeningSpecialtype',OpeningDoc'OpeningDoctype',OpeningNormalComment='OpeningNormalComment',InNormalComment='InNormalComment',InShortComment='InShortComment',ClosingNormalComment='ClosingNormalComment',ClosingTag='ClosingTag',}整个分析采用函数式编程,不使用OO,以简化function由于是同步操作,所以这里使用了JavaScript的事件模型,使用全局变量来保存状态。Token解析需要的全局变量列表如下:letstate:State//当前状态letbuffer:string//输入字符串letbufSize:number//输入字符串长度letsectionStart:number//Token的起始位置正在解析letindex:number//当前解析字符的位置lettokens:IToken[]//解析后的token列表letchar:number//当前解析位置的字符的UnicodePoint需要在开始解析前初始化全局变量:functioninit(input:string){state=State.Literalbuffer=inputbufSize=input.lengthsectionStart=0index=0tokens=[]}然后开始解析。解析时,需要遍历输入字符串中的所有字符,并根据当前状态(改变状态、输出token等)进行相应的处理,解析完成后,清除全局变量,返回结尾。exportfunctiontokenize(input:string):IToken[]{init(input)while(index