浏览器探究——webkit部分——解析HTML(3)HTMLToken的处理



HTMLTokenizer的处理篇

上篇学习到HTMLTokenizer的处理,它是利用有穷状态自动机来完成词法解析的,把解码后的字符串作为输入,输出一个个的HTMLToken的。

测试页面:

<html>

<head>

<metahttp-equiv="content-type" content="text/html;charset=UTF-8" />

</head>

<body>

<p>First name: </p>

<input type="text"name="fname" />

Last name: <input type="text"name="lname" />

</body>

</html>

它的调用栈如下:

看下当前完整的调用栈:

#0WebCore::HTMLTokenizer::nextToken

#1WebCore::HTMLDocumentParser::pumpTokenizer

#2WebCore::HTMLDocumentParser::pumpTokenizerIfPossible

#3WebCore::HTMLDocumentParser::append

#4WebCore::DecodedDataDocumentParser::appendBytes

#5WebCore::DocumentWriter::addData

#6WebCore::DocumentLoader::commitData

#7android::FrameLoaderClientAndroid::committedLoad

#8WebCore::DocumentLoader::commitLoad

#9WebCore::DocumentLoader::receivedData

#10WebCore::MainResourceLoader::addData

#11WebCore::ResourceLoader::didReceiveData

#12WebCore::MainResourceLoader::didReceiveData

#13WebCore::ResourceLoader::didReceiveData

#14android::WebUrlLoaderClient::didReceiveData

 

在HTMLTokenizer中有成员HTMLToken* m_token;用于生成,配置,并输出的HTMLToken。

HTMLToken

首先在HTMLToken中定义了几种HTMLToken的类型。

enum Type {

       Uninitialized,  //未定义,默认

       DOCTYPE,    //文档类型

       StartTag,     //起始标签

       EndTag,      //结束标签

       Comment,    //注释

       Character,    //元素内容

       EndOfFile,    //文档结束

};

并有如下成员

Type m_type;    //类型

   Range m_range; // Always starts at zero.   //在字节流中的偏移

   int m_baseOffset;        

   // "name" for DOCTYPE, StartTag, and EndTag

   // "characters" for Character

   // "data" for Comment

   DataVector m_data;  //数据

   // For DOCTYPE

   OwnPtr<DoctypeData> m_doctypeData; //文档类型

   // For StartTag and EndTag

   bool m_selfClosing;  //是否自封闭

   AttributeList m_attributes;   //属性列表

   // A pointer into m_attributes used during lexing.

Attribute* m_currentAttribute;   //当前属性

另外以上的Range和Attribute类型的定义

class Range {

    public:

        int m_start;

        int m_end;

    };

 

    class Attribute {

    public:

        Range m_nameRange;

        Range m_valueRange;

        WTF::Vector<UChar, 32> m_name;

        WTF::Vector<UChar, 32> m_value;

    };

有了以上的信息,大致可以知道了HTMLToken中其实就是保存了输入流中被分出的几段数据,这些数据会用于构建DOM的节点。这些数据抽象为类型,数据,属性结合的一条记录。

另外在HTMLDocumentParser中也有成员HTMLToken m_token;// We hold m_token here because it might bepartially complete.

<html>的处理

在HTMLTokenizer::nextToken中会把HTMLDocumentParser::m_token传入,这里是引用方式传的,即他们使用的是同一个HTMLToken。

在初始时,HTMLTokenizer::m_state为DataState。该HTMLToken类型为Uninitialized。

读取了’<’时,HTMLTokenizer::m_state变为TagOpenState。HTMLToken类型Uninitialized。

读取了’h’时,HTMLTokenizer::m_state变为TagNameState。HTMLToken类型变为StartTag,并清除了属性列表和当前属性,把当前字符’h’加入到HTMLToken::m_data中。

读取了’t’时,HTMLTokenizer::m_state为TagNameState。HTMLToken类型为StartTag,把当前字符’t’加入到HTMLToken::m_data中。

读取了’m’,’l’时,同上。

读取了’>’时,HTMLTokenizer::m_state变为DataState。HTMLToken类型为StartTag 。HTMLTokenizer::nextToken函数执行了return操作。

经过上述,HTMLTokenizer::nextToken完成了一个“词”的分析,即经过对”<html>”的读取后,得到了一个类型为StartTag的HTMLToken,并且其m_data为”html”。

之后通过HTMLTreeBuilder:: constructTreeFromToken把该HTMLToken进行语法分析。

HTMLTokenizer::nextToken是在HTMLDocumentParser::pumpTokenizer中运行的,HTMLDocumentParser有成员OwnPtr<HTMLTreeBuilder>m_treeBuilder;该成员是在HTMLDocumentParser构造时一并创建的。

这里回顾一下,Document类有成员DocumentParser,而HTMLDocument类是Document的子类,HTMLDocumentParser是DocumentParser的子类。即HTMLDocument类中有HTMLDocumentParser成员。这两个是相互对应的。在HTMLDocumentParser创建时,HTMLDocument把自身作为参数传入。HTMLDocumentParser的构造函数中会同时创建HTMLTreeBuilder,HTMLTreeBuilder会收到HTMLDocument的指针,并记录在其成员HTMLTreeBuilder:: m_document。另外还有成员HTMLTreeBuilder::m_parser记录了HTMLDocumentParser的指针。

有了上诉的关系,这里继续看HTMLTreeBuilder:: constructTreeFromToken。

HTMLTreeBuilder::constructTreeFromToken

先利用参数传入的HTMLToken创建一个AtomicHTMLToken。这个AtomicHTMLToken跟HTMLToken成员非常类似,只是HTMLToken中一些信息如m_data,m_attributes中存的都是该信息在输入流数据中的范围(起始位置和终止位置),而在AtomicHTMLToken中存的都是跟输入流数据无关的数据类型了,并且根据HTMLToken把一些数据转换成有具体含义的成员,比如这里的类型是StartTag,那么它的m_data就转换成AtomicHTMLToken:: m_name的值,即HTMLToken::m_data在该类型下是标签名称的含义。

在转换完AtomicHTMLToken后会做这样的判断,如果参数HTMLToken的类型不是Character则对其做clear操作即重置其成员。

之后把调用HTMLTreeBuilder::constructTreeFromAtomicToken。该函数通过HTMLTreeBuilder::processToken来处理AtomicHTMLToken。

HTMLTreeBuilder::processToken

该函数对每种类型的AtomicHTMLToken做不同的分发处理。当前是StartTag,则进入HTMLTreeBuilder ::processStartTag。

HTMLTreeBuilder中有成员InsertionModem_insertionMode;这个模式是保存当前插入模式,插入模式是什么?http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#insertion-mode中有对他的说明,他其实还是实现一个有穷自动状态机,他根据输入的Token,来转换自身的状态,在状态转换函数中完成对Token的语法分析。

在执行了状态转换后,开始利用Token进行DOM的构建时,调用了HTMLConstructionSite::insertHTMLXXX函数。

在HTMLTreeBuilder中有成员HTMLConstructionSite m_tree; HTMLTreeBuilder实际上完成的是对Token的识别,状态机的维护。根据传入的Token来运转状态机,通过状态机的转换函数,来找到需要执行哪种节点的创建,具体的节点的创建则是通过HTMLConstructionSite来完成的。

当前的Token类型是StartTag,通过类型的识别,进入HTMLTreeBuilder::processStartTag的处理。通过状态机的处理,使得状态机的状态变成BeforeHTMLMode,在该状态下对StartTag类型的Token处理,就是执行HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML。

看下HTMLTreeBuilder对所有的Token的处理流程大体如此:

1.      HTMLTreeBuilder::processToken作为总入口。

2.      HTMLTreeBuilder::processToken中根据AtomicHTMLToken的类型,做转发处理,即调用相应的processXXX函数来处理与之对应的类型的AtomicHTMLToken。

3.      processXXX是对某种类型的AtomicHTMLToken的处理。某些根据InsertionModem_insertionMode;这个状态机的运转来决定状态转换后具体的AtomicHTMLToken处理流程。某些则不经过状态机,直接在当前状态下执行AtomicHTMLToken的处理流程。

4.      在决定新状态下AtomicHTMLToken的处理流程后,根据AtomicHTMLToken的类型执行相应的HTMLConstructionSite::insertXXX。

看下当前的栈情况:

#0WebCore::HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML

#1WebCore::HTMLTreeBuilder::processStartTag

#2WebCore::HTMLTreeBuilder::processToken

#3 WebCore::HTMLTreeBuilder::constructTreeFromAtomicToken

#4WebCore::HTMLTreeBuilder::constructTreeFromToken

#5WebCore::HTMLDocumentParser::pumpTokenizer

#6WebCore::HTMLDocumentParser::pumpTokenizerIfPossible

#7WebCore::HTMLDocumentParser::append

#8WebCore::DecodedDataDocumentParser::appendBytes

到了这里开始真正的Node的创建了。

Node的创建

HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML

该函数通过HTMLHtmlElement::create创建了一个HTMLHtmlElement,把HTMLConstructionSite::m_document作为参数传入,回忆一下Node中有成员Document*m_document;用于指明该Node属于哪个Document中。所以这里创建Node时,通过参数告诉它它的Document是谁。

看下HTMLHtmlElement的继承体系:

Node

ContainerNode

Element

StyledElement

HTMLElement

HTMLHtmlElement

其中在Node中定义了成员Document* m_document;和RenderObject* m_renderer;

在Element中定义了成员QualifiedName m_tagName;和mutableRefPtr<NamedNodeMap> m_attributeMap;

这几个都是很重要的成员,Document标识了该Node位于哪个Document下,每个Node只能在一个Document下,Document是该DOM树的根。

RenderObject标识了跟该Node对应的RenderObject是哪个,每个Node有一个跟其一一对应的RenderObject。

QualifiedName m_tagName标识了该Element的类型。

NamedNodeMap m_attributeMap标识了该Element的属性。

另外Node中还有成员负责构造树结构。

了解了这些成员信息后,继续看HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML。

在创建了HTMLHtmlElement后,把AtomicHTMLToken中的属性设置给HTMLHtmlElement。

之后把该HTMLHtmlElement压入一个HTMLElementStack中。

该函数结束。经过上述的过程,从输入流中找到了一个<html>标签,该标签被识别成一个StartTag的HTMLToken,利用该HTMLToken在HTMLConstructionSite的处理中创建了一个HTMLHtmlElement,并把它压入栈HTMLConstructionSite:: m_openElements中。

<html>后面的换行符的处理

回到HTMLDocumentParser::pumpTokenizer中。

在While循环中再次进入HTMLTokenizer::nextToken。

此时, HTMLTokenizer::m_state为DataState。该HTMLToken类型为Uninitialized。

读取了’换行符’时,HTMLTokenizer::m_state为DataState。HTMLToken类型变为Character。把当前字符’换行符’加入到HTMLToken::m_data中。

读取了’<’时,HTMLTokenizer::m_state为DataState。HTMLToken类型为Character。HTMLTokenizer::nextToken返回。

在HTMLDocumentParser::pumpTokenizer中进入HTMLTreeBuilder::constructTreeFromToken。

经过下面的调用栈

#0 WebCore::HTMLTreeBuilder::processCharacterBuffer

#1WebCore::HTMLTreeBuilder::processCharacter

#2WebCore::HTMLTreeBuilder::processToken

#3WebCore::HTMLTreeBuilder::constructTreeFromAtomicToken

#4WebCore::HTMLTreeBuilder::constructTreeFromToken

#5 WebCore::HTMLDocumentParser::pumpTokenizer

在HTMLTreeBuilder::processCharacterBuffer中,判断m_insertionMode为BeforeHeadMode后,在处理流程中,会对字符串进行检测,发现除了空白字符没有其他字符后,直接返回。

以下换行符的处理都相同,不再讨论。

<head>的处理

回到HTMLDocumentParser::pumpTokenizer中。

此时, HTMLTokenizer::m_state为DataState。该HTMLToken类型为Uninitialized。

状态机的处理类似对<html>的处理

这里生成的Token的名字是headTag。用该Token创建了一个HTMLElement。

之后调用了一个HTMLConstructionSite::attachToCurrent。这个Current是谁呢?它就是HTMLConstructionSite::m_openElements这个栈的栈顶元素,记得刚才创建HTMLHtmlElement的过程中,把HTMLHtmlElement压入栈了,现在获取到的栈顶元素就是刚才的HTMLHtmlElement。这个attach的过程就是把两个 Node建立成父子关系。

再之后把新的HTMLElement压入栈。

由此可见,每次收到一个开始标签时,创建一个新的HTMLElement后,会把这个新的HTMLElement入栈,这样通过栈情况就能够找到待插入节点的父节点(栈顶元素)。即新的元素都是属于这个最近的开始标签的子Node。由此可知,当收到结束标签时,应该会有出栈的操作,这样栈顶元素还是标识这新Node的父节点。即栈维护了标签的开关情况,这个栈类似于函数调用,当进入某个函数时,函数入栈,当继续进入子函数时,子函数入栈。当函数返回时,该函数出栈。通过函数栈能够识别某个函数的嵌套位置,同样通过这个标签栈可以识别某个标签的嵌套位置。

<metahttp-equiv="content-type" content="text/html;charset=UTF-8" />的处理

回到HTMLDocumentParser::pumpTokenizer中。

在处理<meta时跟<html>标签前面一样,此时HTMLTokenizer::m_state为TagNameState。HTMLToken类型为StartTag

读取了’空格’时,HTMLTokenizer::m_state变为BeforeAttributeNameState。HTMLToken类型为StartTag。

读取了’h’时,HTMLTokenizer::m_state为BeforeAttributeNameState。HTMLToken类型为StartTag。通过HTMLToken::addNewAttribute让HTMLToken::m_attributes中开辟一个新的属性空间,HTMLToken中有个成员Attribute* m_currentAttribute;用于指向当前的属性的,即它现在指向新开辟的属性空间的地址。通过HTMLToken:: beginAttributeName设置该属性名字的起始范围,通过HTMLToken::appendToAttributeName把字符’h’加入。可见在HTMLToken中有一些专门用来维护属性信息的函数,在HTMLTokenizer解析到属性信息时,他的HTMLToken没有发生变化,仍然是StartTag,但是状态机的状态变成了BeforeAttributeNameState,并且状态处理函数中,对数据的解析会导致HTMLToken属性的设置。

读取了’t’时,HTMLTokenizer::m_state变为AttributeNameState。HTMLToken类型为StartTag。把当前字符’t’加入到HTMLToken的当前属性名中。

读取”tp-equiv”跟读取上面的’t’字符流程一样。

读取了’=’时,HTMLTokenizer::m_state变为BeforeAttributeValueState。HTMLToken类型为StartTag。通过HTMLToken::endAttributeName设置属性名字的结束偏移和属性值的开始及结束便宜。这里属性值还没有开始也没结束呢,这里设置可能是为了避免html页面中没写属性值的情况吧。

读取了’双引号’时,HTMLTokenizer::m_state变为AttributeValueDoubleQuotedState。HTMLToken类型为StartTag。通过HTMLToken::beginAttributeValue设置属性开始偏移。

读取了’c’时,HTMLTokenizer::m_state为AttributeValueDoubleQuotedState。HTMLToken类型为StartTag。把当前字符’c’加入到HTMLToken的当前属性值中。

读取”ontent-type”跟读取上面的’c’字符流程一样。

读取了’双引号’时,HTMLTokenizer::m_state变为AfterAttributeValueDoubleQuotedState。HTMLToken类型为StartTag。通过HTMLToken::endAttributeValue设置属性结束偏移。

读取了’空格’时,HTMLTokenizer::m_state变为BeforeAttributeValueState。HTMLToken类型为StartTag。

读取了’c’时,HTMLTokenizer::m_state变为AttributeValueState。HTMLToken类型为StartTag。执行跟上面读取了’h’时同样的创建属性的流程。

读取了”ontent="text/html; charset=UTF-8"”的流程跟上述对应的流程一样。

读取了’空格’时,HTMLTokenizer::m_state变为BeforeAttributeValueState。HTMLToken类型为StartTag。

读取了’/’时,HTMLTokenizer::m_state变为SelfClosingStartTagState。HTMLToken类型为StartTag。

读取了’>’时,通过HTMLToken::setSelfClosing设置其自关闭性质。之后执行返回,退出HTMLTokenizer::nextToken。

这里生成的Token的名字是metaTag。用该Token创建了一个HTMLElement。设置他的属性,把他attach到之前的<head>标签创建的HTMLElement上。

看下调用栈

#0WebCore::HTMLConstructionSite::insertSelfClosingHTMLElement

#1WebCore::HTMLTreeBuilder::processStartTagForInHead

#2WebCore::HTMLTreeBuilder::processStartTag

#3WebCore::HTMLTreeBuilder::processToken

#4 WebCore::HTMLTreeBuilder::constructTreeFromAtomicToken

#5WebCore::HTMLTreeBuilder::constructTreeFromToken

#6WebCore::HTMLDocumentParser::pumpTokenizer

#7WebCore::HTMLDocumentParser::pumpTokenizerIfPossible

#8WebCore::HTMLDocumentParser::append

</head>的处理

回到HTMLDocumentParser::pumpTokenizer中。

有了上述的基础,看这个标签就比较容易了,跟<head>非常类似,只是对这个标签的处理时,HTMLToken类型为EndTag。

在HTMLTreeBuilder对该HTMLToken的处理时,会对HTMLConstructionStie::m_openElements执行出栈操作,即之前所讲到的,并更新HTMLTreeBuilder的状态机情况。这里没有再创建新的Node了。

之后的其他标签的处理都是类似的,以下显示几个处理标签的对应的HTMLToken的栈情况:

<p>标签的处理栈:

#0 WebCore::HTMLConstructionSite::insertHTMLElement

#1 WebCore::HTMLTreeBuilder::processStartTagForInBody

#2 WebCore::HTMLTreeBuilder::processStartTag

#3 WebCore::HTMLTreeBuilder::processToken

#4 WebCore::HTMLTreeBuilder::constructTreeFromAtomicToken

#5 WebCore::HTMLTreeBuilder::constructTreeFromToken

#6WebCore::HTMLDocumentParser::pumpTokenizer

First name:”字符串的处理栈:

#0 WebCore::HTMLConstructionSite::insertTextNode

#1 WebCore::HTMLTreeBuilder::processCharacterBuffer

#2 WebCore::HTMLTreeBuilder::processCharacter

#3 WebCore::HTMLTreeBuilder::processToken

#4 WebCore::HTMLTreeBuilder::constructTreeFromAtomicToken

#5 WebCore::HTMLTreeBuilder::constructTreeFromToken

#6WebCore::HTMLDocumentParser::pumpTokenizer

其他标签基本上都可以在上述找到类似的流程了。

最终该页面生成的DOM树情况如下:

*#document    0x7e6a08

    HTML   0x8beb80

        HEAD   0x8e5238

            META    0x8e7358

        BODY   0x8e7758

            P   0x8e7800

                #text    0x8f2648 "First name: "

            INPUT    0x8f0420 NAME=fname

            #text    0x90aaa8 "\nLast name: "

            INPUT    0x90d650 NAME=lname

通过上述对Token的处理可知,在HTMLTokenizer::nextToken中根据状态机进行词法分析能够分离出一个个的HTMLToken,这些利用HTMLTreeBuilder::constructTreeFromToken进行语法分析能够识别它在当前的状态下要创建什么类型的Node。

HTMLTreeBuilder用于做这期间的语法分析,他会先根据HTMLToken的类型做分发处理(即通过processXXX),在某个分支处理中,又会根据HTMLTreeBuilder自身维护的状态机的状态情况做判断,进一步分发处理。也就是说HTMLTreeBuilder即会考虑当前的状态,也会考虑HTMLToken的类型,他的状态相当于该HTMLToken当前所处的环境。

HTMLTreeBuilder最终会通过HTMLConstructionSite来执行相应Node的创建(即通过insertXXX)。

另外注意在HTMLConstructionSite中有成员mutable HTMLElementStack m_openElements;用于维护一个HTMLElement的栈,这个栈用户维护标签的开始和结束的情况,即维护标签的层级关系,用于确定某个Node插入到哪个Node下面,即确定相互的父子关系。

而这里创建的所有的节点,都有一个公共的Document,即HTMLDocument中的成员HTMLDocumentParser中的成员HTMLTreeBuilder中的Document* m_document;这个Document就对应开始的HTMLDocument。

由此可见,后续创建的所有的Node都是HTMLDocument的后代节点。

该篇很多的说明还是在讲述HTMLTokenizer的状态机,HTML的词法分析就是利用HTMLTokenizer中的状态机来实现的。而HTML的语法分析则是根据HTMLToken的类型,以及HTMLTreeBuilder中的状态机来识别的,然后根据识别到的类型情况,来创建一个具体的Node,并在创建该Node后,把其插入到DOM树的相应的位置上,完成语法分析,生成一个DOM树作为语法树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值