Mozilla FireFox Gecko内核源代码解析
(4.nsHTMLTokens)
中科院计算技术研究所网络数据科学与工程研究中心
信息抽取小组
耿耘
之前我们分析了nsHTMLTokenizer(详见其解析篇),其中我们了解到了,其中设计了如何配合 nsScanner对输入流循环地解析流程,如怎么进行回溯等流式操作。实际上其中并没有包含具体的字符比对,以及正则表达式匹配等工作。同时也没有具体地声明“哪些HTML标签才是合法的标签”等具体信息。
而这些信息实际上都包含在了nsHTMLTokens这个文件中。在这个文件中除了对HTML中常见的各种Tag和他们最后生成的Token进行了一一的对应,并且对Tokens的类型,名称,状态,结构等进行了定义。并且对于一些类型的Token,都提供了相应的Consume方法,该种方法就是提供给在nsHTMLTokenizer中所调用的,真正对相应的Token所对应的HTML文本进行解析的方法。
通过学习这个nsHTMLTokens,还可以对Mozilla所推出的HTML标准进行了解,可以知道它都包含什么样的Tokens,哪些Tokens是合法的,以及他们的属性都是如何在浏览器中存储的。
首先,我们来看它的头文件nsHTMLTokens.h,其中包含了哪些声明和定义,这个头文件比较长,因此我们对其的内容逐个进行解析
首先来看他在文件头写下的一段注释:
这个文件中包含了我们的HTML Token的类型定义,这个定义能够被我们的DTD所理解。实际上,这一套Token定义也可以用于XML解析。目前我们拥有文本,注释,起始型标签,结束型标签,实体,属性,样式,脚本以及省略内容,这些个类型的Tokens。空格和换行符同样拥有他们所对应的Token,但是它们在未来的版本中可能会被消除掉。
如果你想查看HTML的标签定义,请查看叫做nsHTMLTags.h/cpp的文件。
大部分的Token类型都有相似的API。他们都有获取Token类型的方法(GetTokenType);那些代表HTML Tag的Tokens还有一个获取tag类型的方法(GetTypeID)。另外,大部分的Token都有一个在解析流程中帮助它们自己进行解析的调用方法(Consume)。我们同样还提供了一些调试用的代码。
下面我们来看代码:
#ifndefHTMLTOKENS_H #defineHTMLTOKENS_H #include "nsToken.h" #include "nsHTMLTags.h" #include "nsString.h" #include "nsScannerString.h" classnsScanner; /******************************************************************* * This enum defines the set of token typesthat we currently support. *******************************************************************/ //下面这个枚举类型,定义了我们目前所支持的Token类型,这个很重要,相当于HTML的规范定义其中的一部分,我们可以看到Mozilla一共将所有的Token分为了如下的14种类型,其中的两种类型unknown和last是浏览器内部使用的,不属于HTML规范 enumeHTMLTokenTypes { eToken_unknown=0, eToken_start=1, eToken_end, eToken_comment, eToken_entity, eToken_whitespace, eToken_newline, eToken_text, eToken_attribute, eToken_instruction, eToken_cdatasection, eToken_doctypeDecl, eToken_markupDecl, eToken_last //make sure this stays the lasttoken... }; //这个从字面上就能看出, nsresult ConsumeQuotedString(PRUnicharaChar,nsString& aString,nsScanner& aScanner); //这个从字面上就能看出是对属性文本进行解析的 nsresult ConsumeAttributeText(PRUnicharaChar,nsString& aString,nsScanner& aScanner); 这个是通过一个整形的标示位来获取TagName的 constPRUnichar* GetTagName(PRInt32 aTag); //PRInt32 FindEntityIndex(nsString&aString,PRInt32 aCount=-1); 下面我们来看一个HTMLToken的基类,他是用于为其他类进行继承而服务的,其中定义了一些基本的属性和方法,后面的几个Token类都是基于这个类建立的。 /** * Thisdeclares the basic token type used in the HTML DTD's. * @update gess 3/25/98 */ classCHTMLToken : public CToken { public: virtual ~CHTMLToken(); CHTMLToken(eHTMLTags aTag); //全部都是虚函数,用于重载 virtual eContainerInfo GetContainerInfo(void)const {return eFormUnknown;} virtual voidSetContainerInfo(eContainerInfo aInfo) { } protected: }; //下面我们来看非常重要的一个类CStartToken,顾名思义他是定义了起始型标签的数据结构,包括它所有的方法,属性等等。 /** * Thisdeclares start tokens, which always take the form <xxxx>. * Thisclass also knows how to consume related attributes. * * @update gess 3/25/98 */ //下面的这段代码声明了起始型的Tokens,它们都是采用<xxxx>的格式。这个类同样还知道如何对相关属性进行解析。 classCStartToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF public: CStartToken(eHTMLTags aTag=eHTMLTag_unknown); //构造方法,参数为eHTMLTags,其值默认为未知类型 CStartToken(const nsAString&aString); //第二种构造方法,用字符串作为参数 CStartToken(const nsAString&aName,eHTMLTags aTag); //第三种构造方法,用字符串外带一个aTag作为参数。 //下面的全部都是虚方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //解析扫描器中当前这段代码。 virtual PRInt32 GetTypeID(void); //获取类型ID virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual PRBool IsEmpty(void); //判断是否是空 virtual voidSetEmpty(PRBool aValue); //设置为空 virtual constnsSubstring& GetStringValue(); //获取字符串形式的值 virtual voidGetSource(nsString& anOutputString); //获取源代码,并输入到anOutputString中 virtual voidAppendSourceTo(nsAString& anOutputString); //添加源代码到anOutputString中 // the following info is used to set well-formedness stateon start tags... //下面这段信息是用来设置起始型Tags上的良构状态的 virtual eContainerInfo GetContainerInfo(void)const {return mContainerInfo;} //获取当前的容器信息 virtual voidSetContainerInfo(eContainerInfo aContainerInfo) { //设置当前的容器信息,如果当前容器信息为未知类型的花,那么就将其进行设置,意思是说只有在类型未知的情况下才能对其进行设置 if (eFormUnknown==mContainerInfo) { mContainerInfo=aContainerInfo; } } //判断是否是良构的 virtual PRBool IsWellFormed(void)const { return eWellFormed == mContainerInfo; } //注意这个数据成员是public的 nsString mTextValue; protected: eContainerInfo mContainerInfo; //存放当前Token的容器信息 PRPackedBool mEmpty; //存放当前Token是否是空 #ifdefDEBUG PRPackedBool mAttributed; #endif }; //下面我们来看和上面这个起始型Token相对应的结束型Token。 /** * Thisdeclares end tokens, which always take the * form</xxxx>. This class also knows how to consume * related attributes. * * @update gess 3/25/98 */ //下面这段代码声明了结束型的Token,它们都是采用</xxxx>的形式。这个类同时还知道如何去解析相关的属性。 classCEndToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF public: CEndToken(eHTMLTags aTag); //构造方法 CEndToken(const nsAString& aString); //构造方法,用字符串做参数 CEndToken(const nsAString&aName,eHTMLTags aTag); //和前面的起始型Token的构造方法结构一样 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //对当前扫描器中的字符进行解析 virtual PRInt32 GetTypeID(void); //获取类型ID virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual constnsSubstring& GetStringValue(); //获取字符值 virtual voidGetSource(nsString& anOutputString); //获取源代码 virtual voidAppendSourceTo(nsAString& anOutputString); //输出源代码 protected: nsString mTextValue; //注意这个数据成员变成受保护类型的了,起始型标签的是public,可以思考一下为什么 }; //下面这个声明的是注释型标签 /** * Thisdeclares comment tokens. Comments are usually * thought of as tokens, but we treat them that way * hereso that the parser can have a consistent view * ofall tokens. * * @update gess 3/25/98 */ //下面这段代码声明了注释型标签的数据结构。注释通常被当做Token来对待,但是我们在这里这样对他们进行处理,以便Parser能够对所有的Tokens都有一致的认识。 classCCommentToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF //参见nsToken.h文件,预先#define的一个语句 public: CCommentToken(); //构造方法 CCommentToken(const nsAString&aString); //构造方法,采用字符串作为参数 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //标准的解析方法,对扫描器当前的字符串进行解析 virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual constnsSubstring& GetStringValue(void); //获取字符串类型的值 virtual voidAppendSourceTo(nsAString& anOutputString); //输出源代码到字符串 nsresult ConsumeStrictComment(nsScanner& aScanner); //解析strict类型的注释 nsresult ConsumeQuirksComment(nsScanner& aScanner); //解析quirks类型的注释 //上面的strict和quirks是根据DTD不同而不同的两种解析模式。 protected: nsScannerSubstring mComment; // does notinclude MDO & MDC nsScannerSubstring mCommentDecl; // includesMDO & MDC }; //下面是用来存放实体类型的Token的类的声明。 /** * Thisclass declares entity tokens, which always take * theform &xxxx;. This class also offers a few utility * methods that allow you to easily reduce entities. * * @update gess 3/25/98 */ classCEntityToken : public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先的#define语句,请参见nsToken.h public: CEntityToken(); //构造方法 CEntityToken(const nsAString&aString); //构造方法,用字符串做为参数 virtual PRInt32 GetTokenType(void); //获取Token的类型 PRInt32 TranslateToUnicodeStr(nsString& aString); //将字符串编码转换为Unicode的方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要解析方法,从当前扫描器的当前位置开始解析 static nsresult ConsumeEntity(PRUnichar aChar,nsString& aString, nsScanner&aScanner); //解析实体用的方法,注意参数的区别 static PRInt32 TranslateToUnicodeStr(PRInt32aValue,nsString& aString); //转换字符串为Unicode编码,注意参数和前面方法的不同 virtual constnsSubstring& GetStringValue(void); //获取字符串的值 virtual voidGetSource(nsString& anOutputString); //获取源代码 virtual voidAppendSourceTo(nsAString& anOutputString); //输出源代码到目标字符串 protected: nsString mTextValue; //注意这里也是受保护的数据类型 }; //下面是存放空格字符类型Token的类。 /** * Whitespace tokens are used where whitespace can be * detected as distinct from text. This allows us to * easily skip leading/trailing whitespace when desired. * * @update gess 3/25/98 */ //空格类型字符的Token是在当空格能够被和文本区分地监测出来的时候所使用。这使得我们可以在需要的时候轻易地跳过那些开头/结尾的空格。 classCWhitespaceToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先#define的语句,请参考nsToken.h public: CWhitespaceToken(); //构造方法 CWhitespaceToken(const nsAString&aString); //构造方法,用字符串作为参数 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要解析方法 virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual constnsSubstring& GetStringValue(void); //获取字符串的值 protected: nsScannerSharedSubstring mTextValue; //受保护类型的数据成员,存放文本值 }; //下面是用来存放普通文本的Token的类的声明 /** * Texttokens contain the normalized form of html text. * Thesetokens are guaranteed not to contain entities, * startor end tags, or newlines. * * @update gess 3/25/98 */ //文本Tokens包含普通格式的html文本,这些Token保证不会包含任何的实体数据,起始或结束标签,或者新行。 classCTextToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先#define的方法,请参见nsToken.h public: CTextToken(); //构造方法 CTextToken(const nsAString&aString); //使用字符串作为参数的构造方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要解析方法,从当前扫描器的当前位置开始进行解析 virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual PRInt32 GetTextLength(void); //获取文本的长度 virtual voidCopyTo(nsAString& aStr); //拷贝文本到字符串 virtual constnsSubstring& GetStringValue(void); //获取字符串值 //绑定文本到扫描器的起始和结束位置 virtual voidBind(nsScanner* aScanner, nsScannerIterator& aStart, nsScannerIterator&aEnd); //绑定到字符串 virtual void Bind(const nsAString& aStr); //解析字符类型的数据,注意参数的作用 nsresult ConsumeCharacterData(PRBool aIgnoreComments, nsScanner&aScanner, const nsAString& aEndTagName, PRInt32 aFlag, PRBool&aFlushTokens); //解析经过预解析字符类型的数据,注意参数的作用,我们到具体实现的代码时再进行说明 nsresult ConsumeParsedCharacterData(PRBool aDiscardFirstNewline, PRBoolaConservativeConsume, nsScanner& aScanner, constnsAString& aEndTagName, PRInt32aFlag, PRBool& aFound); protected: nsScannerSubstring mTextValue; //注意这个数据类型也是受保护的 }; //下面是用来存放[!CDATA],即连续型段文本的数据的类。 /** * CDATASection tokens contain raw unescaped text content delimited by * a![CDATA[ and ]]. * XXXNot really a HTML construct - maybe we need a separation * * @update vidur 11/12/98 */ //CDATA类型的Token包含连续的段文本,它们由![CDATA[ 和 ]]符号进行划分,其实这并不能说是严格意义上的HTML结构—也许我们需要对他们进行一下区分 classCCDATASectionToken : public CHTMLToken { CTOKEN_IMPL_SIZEOF public: CCDATASectionToken(eHTMLTags aTag = eHTMLTag_unknown); //构造方法 CCDATASectionToken(const nsAString&aString); //构造方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要解析方法,从当前解析器的当前位置开始进行解析 virtual PRInt32 GetTokenType(void); //获取当前Token的类型 virtual constnsSubstring& GetStringValue(void); //获取字符串的值 protected: nsString mTextValue; //受保护类型的数据成员,存放当前文本值 }; //下面的类用来存放一些声明型的标签,他们也是被认为是连续型的段文本。 /** * Declaration tokens contain raw unescaped text content (not really, but * rightnow we use this only for view source). * XXXNot really a HTML construct - maybe we need a separation * */ //声明类型的Tokens包含连续的纯文本内容(其实并不一定,但是我们目前只有在阅览源代码方式时来使用它) //并不是一个HTML结构—也许我们需要对其进行一下区分 classCMarkupDeclToken : public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先#define的方法,详情请见nsToken.h public: CMarkupDeclToken(); //构造方法 CMarkupDeclToken(const nsAString&aString); //构造方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //解析方法,从当前解析器的当前位置开始进行解析 virtual PRInt32 GetTokenType(void); //获取Token类型 virtual constnsSubstring& GetStringValue(void); //获取字符串的值 protected: nsScannerSubstring mTextValue; //用来存放当前文本的值 }; //下面是用来存放属性的类。 /** * Attribute tokens are used to contain attribute key/value * pairswhereever they may occur. Typically, they should * occuronly in start tokens. However, we may expand that * ability when XML tokens become commonplace. * * @update gess 3/25/98 */ //属性tokens是用来包含属性的,其中需要存储属性名/属性值的序对。普通情况下,他们应当只在起始型Tokens中出现。然而,我们可能在今后遇到XML Token的时候扩展这一功能。 classCAttributeToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先#define的代码,详情请参见nsToken.h public: CAttributeToken(); //构造方法 CAttributeToken(const nsAString&aString); //构造方法 CAttributeToken(const nsAString&aKey,const nsAString& aString); //构造方法 ~CAttributeToken() {} //析构方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要解析方法,用来解析当前扫描器的当前位置的值 virtual PRInt32 GetTokenType(void); //获取Token的类型 const nsSubstring& GetKey(void){return mTextKey.AsString(); } //获取Key,即属性名的值 virtual void SetKey(const nsAString& aKey); //设置key,即属性名的值 //绑定Key至扫描器 virtual voidBindKey(nsScanner* aScanner, nsScannerIterator& aStart, nsScannerIterator&aEnd); //获取值,获取当前的文本值 const nsSubstring& GetValue(void) {returnmTextValue.str();} //获取字符串的值 virtual constnsSubstring& GetStringValue(void); //输出源代码到字符串 virtual voidGetSource(nsString& anOutputString); //输出源代码到字符串的尾端 virtual voidAppendSourceTo(nsAString& anOutputString); PRPackedBool mHasEqualWithoutValue; //一个BOOL判断位,从字面上来看应该是判断该属性是否是没有赋值的情况。 protected: nsScannerSharedSubstring mTextValue; //当前的文本值 nsScannerSubstring mTextKey; //当前的属性名 }; //下面是一个用来存放换行符的类的声明。 /** * Newline tokens contain, you guessed it, newlines. * Theyconsume newline (CR/LF) either alone or in pairs. * * @update gess 3/25/98 */ //换行符包含,你猜是什么,当然是换行符。 //他们包含成对或者单独的换行符(CR/LF) classCNewlineToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先#define的值,详情参见nsToken.h public: CNewlineToken(); //构造方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要的解析方法,从当前扫描器的当前位置进行扫描 virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual constnsSubstring& GetStringValue(void); //获取字符串的值 static void AllocNewline(); //分配新行 static voidFreeNewline(); //回收新行 }; //下面是用来存放指令类型的Token。 /** * Whitespace tokens are used where whitespace can be * detected as distinct from text. This allows us to * easily skip leading/trailing whitespace when desired. * * @update gess 3/25/98 */ //和空格字符Token的代码注释重复了 classCInstructionToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF public: CInstructionToken(); CInstructionToken(const nsAString&aString); virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); virtual PRInt32 GetTokenType(void); virtual constnsSubstring& GetStringValue(void); protected: nsString mTextValue; }; //最后一个,就是用来对DOCTYPE进行存放的: /** * This token is generated by the HTML andExpat tokenizers * when they see the doctype declaration("<!DOCTYPE ... >") * */ //这个Token是当HTML和Expat分词器看到doctype声明(”<!DOCTYPE…>”)的时候自动生成的 classCDoctypeDeclToken: public CHTMLToken { CTOKEN_IMPL_SIZEOF //预先#define的语句,详情请查看nsToken.h文件 public: CDoctypeDeclToken(eHTMLTags aTag=eHTMLTag_unknown); //构造方法 CDoctypeDeclToken(const nsAString&aString,eHTMLTags aTag=eHTMLTag_unknown); //构造方法 virtual nsresult Consume(PRUnicharaChar,nsScanner& aScanner,PRInt32 aMode); //主要解析方法,用来从当前的扫描器的当前位置开始进行解析 virtual PRInt32 GetTokenType(void); //获取Token的类型 virtual constnsSubstring& GetStringValue(void); //获取字符串的值 virtual voidSetStringValue(const nsAString& aStr); //设置字符串的值 protected: nsString mTextValue; //受保护的数据类型,用来存放字符值 }; #endif
以上就是所有nsHTMLToken.h头文件的内容了,可以看到,它主要采用了一个枚举类型集合来声明了所有的HTML标签的类型,这个枚举类型主要用在Allocator中创建并为每个新Token分配内存的时候所使用,并且对每个标签类型定义了它们的基本方法和数据成员,下面,我们就来看看这些方法的具体实现和数据成员的使用。
下面是nsHTMLToken.cpp文件的源代码解析:
#include <ctype.h> #include <time.h> #include <stdio.h> #include "nsScanner.h" #include "nsToken.h" include "nsHTMLTokens.h" #include "prtypes.h" #include "nsDebug.h" #include "nsHTMLTags.h" #include "nsHTMLEntities.h" #include "nsCRT.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "nsScanner.h" //之后有一些前置的函数声明和变量定义 //用户定义类型的名称userdefined,以字符数组方式存放 static const PRUnichar sUserdefined[] = {'u','s','e', 'r', 'd', 'e','f', 'i', 'n', 'e','d', 0}; //一个字符型数组,放置属性终止符 static const PRUnichar kAttributeTerminalChars[] = { PRUnichar('&'), PRUnichar('\t'), PRUnichar('\n'), PRUnichar('\r'), PRUnichar(' '), PRUnichar('>'), PRUnichar(0) }; //在字符串的最后面插入一个实体,NCR是实体字符的整型编码 static void AppendNCR(nsSubstring& aString, PRInt32aNCRValue); //下面我们来看两个通用的全局性方法,首先是处理实体标签用的ConsumeEntity: /** * Consumes an entity from aScanner and expandsit into aString. * * @param aString The target string to append the entity to. * @param aScanner Controller of underlying input source * @param aIECompatible Controls whether we respect entities with values > * 255 and no terminatingsemicolon. * @param aFlag If NS_IPARSER_FLAG_VIEW_SOURCE do not reduce entities... * @return error result */ //从扫描器中获取一个实体元素并将其附加到一个字符串中去 staticnsresult ConsumeEntity(nsScannerSharedSubstring&aString, nsScanner& aScanner, PRBool aIECompatible, PRInt32 aFlag) { nsresult result = NS_OK; //设置运行结果,默认为NS_OK PRUnichar ch; //设置一个字符变量,用来存放从扫描器中得到的字符 result = aScanner.Peek(ch, 1); //查看当前处于1位置的字符 if (NS_SUCCEEDED(result)) { //如果查看结果成功的话 //初始化若干个需要用到的变量 PRUnichar amp = 0; PRInt32 theNCRValue = 0; nsAutoString entity; //判断从扫描器查看到的字符是否是Ascii编码的字符,如果是Ascii编码的字符,且当前的运行模式不是查看源代码模式 if (nsCRT::IsAsciiAlpha(ch) &&!(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { //调用CEntityToken的解析实体元素ConsumeEntity方法,对当前解析器的当前位置进行解析,解析到的元素放入entity中 result = CEntityToken::ConsumeEntity(ch, entity, aScanner); //如果解析成功 if (NS_SUCCEEDED(result)) { //将entity转换为Unicode格式的一个整型,存放到NCRValue中去 theNCRValue = nsHTMLEntities::EntityToUnicode(entity); //获取entity最后的一个字符,放到theTermChar中去 PRUnichar theTermChar = entity.Last(); //如果实体的NCR值大于255,那么:Nav 4.x版本都不会将它作为一个实体来对待 //IE在它以一个单分号结束的时候会将其作为实体来进行处理 //这里很类似IE! // If an entity value is greater than 255then: // Nav 4.x does not treat it as an entity, // IE treats it as an entity if terminatedwith a semicolon. // Resembling IE!! //获取aString的可写入地址 nsSubstring &writable = aString.writable(); //判断,如果NCRValue小于0,或NCRValue大于255且结尾不为’;’号且是IE兼容模式 if (theNCRValue < 0 || (aIECompatible &&theNCRValue > 255 && theTermChar != ';')){ // Looks like we're not dealing with an entity //那么我们不将其作为实体进行处理 //我们单纯地在结果字符串的末尾加上一个’&’和entity的本体,即原封不动保持其原来的样子 writable.Append(kAmpersand); writable.Append(entity); } else { //其他情况下,那么说明这是一个正确的实体类型 // A valid entity so reduce it. //我们将其正确的实体字符添加到结果字符串的末尾 writable.Append(PRUnichar(theNCRValue)); } } //其他情况下,此时我们需要目前的ch字符如果不是ascii编码字符,那么是否是’#’符号,我们这里通过对其再进行判断 }else if (ch ==kHashsign && !(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { //设置 result = CEntityToken::ConsumeEntity(ch, entity, aScanner); //同样调用实体处理方法对其进行处理 if (NS_SUCCEEDED(result)) { //如果处理成功 nsSubstring &writable = aString.writable(); //获取存放结果的字符串的可读写部分 if (result ==NS_HTMLTOKENS_NOT_AN_ENTITY) { //如果处理结果判断为其不为实体类型 // Looked like an entity but it's not //看起来像是一个实体,但其实它不是 aScanner.GetChar(amp); //获取当前的字符 writable.Append(amp); //并且将其附加到存放结果的字符串之后 result = NS_OK; //设置结果为OK } else { //其他情况下,即处理结果不是“NOT_AN_ENTITY” PRInt32 err; //设置一个整型变量,后面用其作为参数 theNCRValue = entity.ToInteger(&err, kAutoDetect); //将entity转换为整形编码 AppendNCR(writable, theNCRValue); //将其编码所对应的字符添加到字符串的结尾处 } } } else { // What we thought as entity is not really anentity... //此时我们所认为是实体的东西,其实并不是一个实体 aScanner.GetChar(amp); //获取扫描器的当前字符,放到amp中 aString.writable().Append(amp); //直接将其附加到目标结果字符串的结尾 } } return result; //返回处理结果 } //通过分析以上方法可知,nsHTMLTokens中实现了一个全局的处理实体方法,该方法主要通过使用EntityToken的Consume方法来解析当前扫描器位置的字符串,并尝试将该字符串进行转换为“整型编码”(NCRValue)的方法,判断其值是否处于某个值域范围之内(0~255)并且是否以’;’结尾,如果判断成功,那么说明其确实为一个实体字符,否则则认为其不为实体字符。在其为实体字符的时候,我们直接将其对应的实体字符添加到目标字符串之后,如果不是,那么我们则自将其原封不动地添加到目标字符串之后。注意这里的扫描器的字符位置前进运行控制,是由Consume的处理结果而决定的,具体的后面可知。 //下面,我们来看第二个全局的处理方法: /* * Thisgeneral purpose method is used when you want to * consume attributed text value. * Note:It also reduces entities. * * @param aNewlineCount -- thenewline count to increment when hitting newlines * @param aScanner -- controller ofunderlying input source * @param aTerminalChars --characters that stop consuming attribute. * @param aAllowNewlines -- whetherto allow newlines in the value. * XXX it would benice to roll this info into * aTerminalCharssomehow.... * @param aIECompatEntities IE treatsentities with values > 255 as * entities only ifthey're terminated with a * semicolon. This istrue to follow that behavior * and false to treatall values as entities. * @param aFlag - containsinformation such as |dtd mode|view mode|doctype|etc... * @return error result */ //这个方法其实主要是用来处理带属性的文本值的 staticnsresult ConsumeUntil(nsScannerSharedSubstring&aString, PRInt32& aNewlineCount, nsScanner& aScanner, constnsReadEndCondition& aEndCondition, PRBool aAllowNewlines, PRBool aIECompatEntities, PRInt32 aFlag) { //设置两个中间变量,并初始化它们的默认值 nsresult result = NS_OK; PRBool done = PR_FALSE; do { //调用扫描器,不断获取字符,直到遇到了aEndCondition中规定的条件为止,并将获取来的字符放到aString中 result= aScanner.ReadUntil(aString, aEndCondition, PR_FALSE); //如果获取成功 if (NS_SUCCEEDED(result)) { PRUnichar ch; //一个字符型的中间变量 aScanner.Peek(ch); //察看扫描器的下一个字符 if (ch == kAmpersand) { //如果下一个字符是’&’,说明我们遇到了实体字符 result = ConsumeEntity(aString, aScanner, aIECompatEntities, aFlag); //调用前面的全局方法ConsumeEntity,对实体字符进行处理 } else if(ch == kCR && aAllowNewlines) { //否则,如果字符为’\r’并且当前参数为允许新行 aScanner.GetChar(ch); //获取当前字符,即’\r’ result = aScanner.Peek(ch); //尝试查看下一个字符 if (NS_SUCCEEDED(result)) { //如果查看成功 nsSubstring &writable =aString.writable(); //获取当前目标字符串的可读写部分 if (ch == kNewLine) { //如果字符为换行符’\n’ writable.AppendLiteral("\r\n"); //我们在目标字符串之后加上’\r\n’,也就是说如果是’\r\n’同时出现,我们需要将他们一起处理,它们只相当于新起了一行 aScanner.GetChar(ch); //并获取当前字符,即’\n’ } else { writable.Append(PRUnichar('\r')); //其他情况下,即当前字符不为’\n’则在目标字符串后 } ++aNewlineCount; //将新行记数加一 } } else if(ch == kNewLine && aAllowNewlines) { //其他情况下如果ch为’\n’且当前参数为允许新行 aScanner.GetChar(ch); //获取当前字符,放置到ch中 aString.writable().Append(PRUnichar('\n')); //在目标字符串之后附加上’\n’。 ++aNewlineCount; //添加新行 } else { done = PR_TRUE; //再其他情况下,直接设置done为TRUE,跳出循环 } } } while (NS_SUCCEEDED(result) &&!done); //循环判断条件 return result; //将结果返回 } //可以看到,这个方法主要工作就是调用扫描器一直进行读取,并且将遇到的实体字符全部进行转换处理,直到遇到了aEndCondition中规定的字符或者条件为止,在允许新行的情况下,途中还需要对途中遇到的换行符进行处理并记录下换行的次数。 //下面我们就开始解析各个Token和它们所对应的各自的方法了。 //首先,我们来看所有HTML的基类CHTMLToken的构造方法,因为是基类所以都是空方法: /************************************************************** And now for the token classes... **************************************************************/ /** * Constructor from tag id */ //因为只是基类,所以构造方法都是空方法 CHTMLToken::CHTMLToken(eHTMLTags aTag) : CToken(aTag) { } CHTMLToken::~CHTMLToken() { } //下面我们来看起始型标签即CStartToken的方法: /* * Constructor from tag id */ //构造方法,使用eHTMLTags类型(该类型的具体构造可以查看nsHTMLTagList.h)的参数来进行构造 CStartToken::CStartToken(eHTMLTags aTag) : CHTMLToken(aTag) { mEmpty = PR_FALSE; //设置是否为空为否 mContainerInfo = eFormUnknown; //设置容器信息为未知 #ifdefDEBUG mAttributed = PR_FALSE; #endif } CStartToken::CStartToken(const nsAString& aName) : CHTMLToken(eHTMLTag_unknown) { //初始化方法和前一个一样,只不过多了一个用aName将mTextValue赋值的操作 mEmpty = PR_FALSE; mContainerInfo = eFormUnknown; mTextValue.Assign(aName); #ifdefDEBUG mAttributed = PR_FALSE; #endif } /* * This method returns the typeid (the tagtype) for this token. */ //获取TypeID:如果mTypeID为未知类型的话,则采用到nsHTMLTags中根据mTextValue去寻找相应的mTypeID的方法,否则直接返回mTypeID PRInt32 CStartToken::GetTypeID() { if (eHTMLTag_unknown == mTypeID) { mTypeID = nsHTMLTags::LookupTag(mTextValue); } return mTypeID; } //获取Token类型 PRInt32 CStartToken::GetTokenType() { return eToken_start; //直接返回eToken_start,即起始型标签,注意其整型的默认值为1 } //设置标示是否为空的数据成员mEmpty void CStartToken::SetEmpty(PRBool aValue) { mEmpty = aValue; //设置数据成员mEmpty为指定的aValue } //判断是否为空 PRBool CStartToken::IsEmpty() { return mEmpty; //返回mEmpty的值 } //下面我们来看起始型标签CStartToken的解析方法Consume: /* * Consume the identifier portion of the starttag */ nsresult CStartToken::Consume(PRUnichar aChar,nsScanner& aScanner, PRInt32 aFlag) { // If you're here, we've already Consumed the < char,and are // ready to Consume the rest of the open tag identifier. // Stop consuming as soon as you see a space or a '>'. // NOTE: We don't Consume the tag attributes here, nor dowe eat the ">" //如果代码运行到了这里,说明我们已经处理了’<’字符,并且准备好处理剩下的标签标示符了,我们处理的结束条件是遇到一个空格或者一个’>’字符。注意:我们不在这里对标签属性进行处理,同样我们也不会处理’>’ //设置两个中间变量 nsresult result = NS_OK; nsScannerSharedSubstring tagIdent; //判断aFlag,如果当前是HTML模式 if (aFlag & NS_IPARSER_FLAG_HTML) { result = aScanner.ReadTagIdentifier(tagIdent); //调用扫描器的读取标示符操作,该操作会一直读取,直到遇到了’<’,’>’,’/’或者空格字符为止,并将读取结果存放在tagIdent中 mTypeID = (PRInt32)nsHTMLTags::LookupTag(tagIdent.str()); //获取该tagIdent标示符的对应类型标示TypeID // Save the original tag string if this isuser-defined or if we // are viewing source //如果当前是阅读源代码模式,或者我们获取的是一个用户自定义的标签类型(非用户自定义类型的标签都有固定的标签名),除了TypeID,我们还需要记录下其文本形式的标签名,以便日后使用 if (eHTMLTag_userdefined == mTypeID || (aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { mTextValue = tagIdent.str(); //获取标示符的字符串形式 } } else { //其他情况下,即非HTML模式 result= aScanner.ReadTagIdentifier(tagIdent); //获取其标示符,存放至tagIdent mTextValue= tagIdent.str(); //设置mTextValue为该tagIdent,存放字符串类型的标示符 mTypeID = nsHTMLTags::LookupTag(mTextValue); //获取该标示符对应的TypeID } //判断如果结果成功,且当前不为查看源代码模式 if (NS_SUCCEEDED(result) && !(aFlag &NS_IPARSER_FLAG_VIEW_SOURCE)) { result = aScanner.SkipWhitespace(mNewlineCount); //跳过当前的所有空格型字符,并且用mNewlineCount记录其间跳过的行数 } if (kEOF == result &&!aScanner.IsIncremental()) { //如果结果为到达文件末尾,且扫描器不为增量模式 // Take what we can get. result = NS_OK; //直接返回成功值即可,解析结束 } return result; } //获取字符串的值 constnsSubstring& CStartToken::GetStringValue() { //如果mTypeID大于0,且小于eHTMLTag_text的值,说明其实某个固有的,可识别的HTML标签 if (eHTMLTag_unknown < mTypeID && mTypeID< eHTMLTag_text) { if (!mTextValue.Length()) { //如果其TextValue为空 mTextValue.Assign(nsHTMLTags::GetStringValue((nsHTMLTag) mTypeID)); //我们去nsHTMLTag中根据其TypeID查找到对应的字符串型标签名,并赋值到mTextValue中去 } } return mTextValue; //返回mTextValue } //获取源代码 void CStartToken::GetSource(nsString&anOutputString) { anOutputString.Truncate(); //清空目标字符串 AppendSourceTo(anOutputString); //调用AppendSourceTo()方法将源代码复制到目标字符串anOutputString中去 } //复制源代码 void CStartToken::AppendSourceTo(nsAString&anOutputString) { anOutputString.Append(PRUnichar('<')); //因为是标签,所以肯定要以’<’开头 /* * Watch out for Bug 15204 */ if (!mTextValue.IsEmpty()) { //如果当前的字符串值不为空 anOutputString.Append(mTextValue); //将字符串值赋值进去 } else { anOutputString.Append(GetTagName(mTypeID)); //否则便去根据mTypeID获取相应的TagName,并且复制到anOutputString中去 } anOutputString.Append(PRUnichar('>')); //标签,必然需要以’>’结尾 } //以上就是起始型标签的一些方法,下面我们来看结束型标签的方法,其实这些标签的方法结构上都很类似,只不过在一些具体标签的具体属性方面有所区别。 //结束型标签的作用可以说没有起始型标签那么大,它们在解析时主要的作用就是为了给起始型标签划上结束符,因此它也不具备许多起始型标签所拥有的属性。 CEndToken::CEndToken(eHTMLTags aTag) : CHTMLToken(aTag) { } CEndToken::CEndToken(const nsAString& aName) : CHTMLToken(eHTMLTag_unknown) { mTextValue.Assign(aName); //用aName为当前字符串赋值 } CEndToken::CEndToken(const nsAString& aName, eHTMLTags aTag) : CHTMLToken(aTag) { mTextValue.Assign(aName); //同样,用aName为当前字符串赋值 } //EndToken的解析方法Consume() nsresult CEndToken::Consume(PRUnichar aChar, nsScanner&aScanner, PRInt32 aFlag) { //设置两个中间变量 nsresult result = NS_OK; nsScannerSharedSubstring tagIdent; if (aFlag & NS_IPARSER_FLAG_HTML) { result = aScanner.ReadTagIdentifier(tagIdent); //调用扫描器的ReadTagIdentifier获取当前的标示符 mTypeID = (PRInt32)nsHTMLTags::LookupTag(tagIdent.str()); //通过标示符查找到TypeID // Save the original tag string if this isuser-defined or if we // are viewing source //和起始型标签类似,如果是用户定义类型的Tag或者是查看源代码模式,我们需要存储字符串值 if (eHTMLTag_userdefined == mTypeID || (aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { mTextValue = tagIdent.str(); //存储字符串值 } } else { result = aScanner.ReadTagIdentifier(tagIdent); //其他情况下,说明非HTML模式 mTextValue = tagIdent.str(); //存储字符串值 mTypeID = nsHTMLTags::LookupTag(mTextValue); //获取TypeID并存储 } //如果结果成功,且当前模式不是阅读源代码模式 if (NS_SUCCEEDED(result) && !(aFlag &NS_IPARSER_FLAG_VIEW_SOURCE)) { result = aScanner.SkipWhitespace(mNewlineCount); } //如果当前已经到了文件结尾,且aScanner不是增量模式 if (kEOF == result &&!aScanner.IsIncremental()) { // Take what wecan get. result = NS_OK; //直接返回即可 } return result; //返回结果 } //下面是结束型标签获取TypeID的方式 /* * Asksthe token to determine the <i>HTMLTag type</i> of * thetoken. This turns around and looks up the tag name * inthe tag dictionary. */ PRInt32 CEndToken::GetTypeID() { if (eHTMLTag_unknown == mTypeID) { //如果当前TypeID为未知型eHTMLTag_unknown mTypeID = nsHTMLTags::LookupTag(mTextValue); //根据字符串的值去查找并获取一个TypeID switch (mTypeID) { //根据获取到的TypeID进行操作 case eHTMLTag_dir: case eHTMLTag_menu: mTypeID = eHTMLTag_ul; //如果是eHTMLTag_dir或eHTMLTag_menu,则统一转换为eHTMLTag_ul break; default: break; } } return mTypeID; //返回TypeID } PRInt32 CEndToken::GetTokenType() //获取标签的类型 { return eToken_end; //因为是结束型标签,所以返回类型为eToken_end } constnsSubstring& CEndToken::GetStringValue() { if (eHTMLTag_unknown < mTypeID && mTypeID< eHTMLTag_text) { //如果mTypeID的值位于可识别的范围之内 if (!mTextValue.Length()) { mTextValue.Assign(nsHTMLTags::GetStringValue((nsHTMLTag) mTypeID)); //我们直接去列表中查找该ID所对应的字符串值并复制到mTextValue中 } } return mTextValue; //返回mTextValue } void CEndToken::GetSource(nsString&anOutputString) { anOutputString.Truncate(); //清空目标字符串 AppendSourceTo(anOutputString); //调用AppendSourceTo()方法,复制源代码到目标字符串 } void CEndToken::AppendSourceTo(nsAString&anOutputString) { anOutputString.AppendLiteral("</"); //结束型标签必然以’</’作为开头 if (!mTextValue.IsEmpty()) { //如果字符串值不为空 anOutputString.Append(mTextValue); //将字符串的值赋值到anOutputString中 } else { anOutputString.Append(GetTagName(mTypeID)); //否则则根据mTypeID查找该标签的字符串值,并赋值到anOutputString中 } anOutputString.Append(PRUnichar('>')); //标签必然以’>’作为结尾 } //上面我们看过了起始型标签和结束型标签的具体数据结构和他们的方法实现。下面我们来看对于普通文本进行存储的类CTextToken,由于HTML的文本结构稍微有些复杂,因此CTextToken的实现也相对较为复杂。 //构造方法 CTextToken::CTextToken() : CHTMLToken(eHTMLTag_text) { } CTextToken::CTextToken(const nsAString& aName) : CHTMLToken(eHTMLTag_text) { mTextValue.Rebind(aName); //设置mTextValue为aName } PRInt32 CTextToken::GetTokenType() //获取当前的Token类型 { return eToken_text; //因为是文本型Token,因此返回eToken_text } //获取文本长度 PRInt32 CTextToken::GetTextLength() { return mTextValue.Length(); //返回mTextValue的长度即可 } //接下来的是TextToken的主要解析方法Consume,代码很长因此我们分段对其进行解析 nsresult CTextToken::Consume(PRUnichar aChar,nsScanner& aScanner, PRInt32 aFlag) { //解析方法 //设置终止字符数组,遇到这些字符就会中断扫描器 static constPRUnichar theTerminalsChars[] = { PRUnichar('\n'), PRUnichar('\r'), PRUnichar('&'),PRUnichar('<'), PRUnichar(0) }; //用该终止字符数组初始化一个中断条件,用于中断扫描器 static constnsReadEndCondition theEndCondition(theTerminalsChars); nsresult result = NS_OK; PRBool done = PR_FALSE; nsScannerIterator origin, start, end; //三个游标,用来记录扫描器的位置 // Start scanning after the first character, because weknow it to // be part of this text token (we wouldn't have come hereif it weren't) //我们从当前位置的下一个字符开始进行扫描,因为第一个字符必然是文本标签的一部分 aScanner.CurrentPosition(origin); //首先获取扫描器的当前位置,放入origin start = origin; //设置start =origin aScanner.EndReading(end); //获取扫描器的结束位置,放入end //判断起始位置是否是结束位置,如果是则报错,因为此时文档已经到了结尾 NS_ASSERTION(start != end, "CallingCTextToken::Consume when already at the " "end of a document is a bad idea."); //设置扫描器的当前位置加一 aScanner.SetPosition(++start); //下面是一个经典条件循环 while (NS_OK == result && !done) { //通过扫描器不断进行读取,直到遇到了theEndCondition为止 result= aScanner.ReadUntil(start, end, theEndCondition, PR_FALSE); //如果读取结果成功 if (NS_OK == result) { result = aScanner.Peek(aChar); //查看当前位置的字符,并放置到aChar中 if (NS_OK == result && (kCR ==aChar || kNewLine == aChar)) { switch (aChar) { case kCR: //是’\r’符号 { // It's acarriage return. See if this is part of a CR-LF pair (in //which case we need to treat it as one newline). If we're at the // edgeof a packet, then leave the CR on the scanner, since it // couldstill be part of a CR-LF pair. Otherwise, it isn't. //如果是’\r’回车的话。我们需要看看它是否是’\r\n’的组合使用(如果是这样的话我们需要将它们作为同一个换行符来处理)。如果我们处于某个数据包的末端,那么我们则不管这个’\r’符号,将其留在扫描器上,因为它仍有可能是某个’\r\n’的组合之一。其他的情况下,我们则认为它不是这样的组合 PRUnichar theNextChar; result = aScanner.Peek(theNextChar,1); //查看1位置的字符,放入theNextChar if(result == kEOF && aScanner.IsIncremental()) { //如果结果为文件末尾且扫描器是增量模式的,即当前处于某个数据包的末端 break; //退出循环 } if(NS_SUCCEEDED(result)) { //如果结果判定为成功 //Actually get the carriage return. aScanner.GetChar(aChar); //获取该字符放入aChar } if(kLF == theNextChar) { //如果下一个字符是’\n’ // Ifthe "\r" is followed by a "\n", don't replace it and let // itbe ignored by the layout system. end.advance(2); //将end游标前进两个位置 aScanner.GetChar(theNextChar); //获取当前位置字符并放到theNextChar中,这样就等于忽略了’\n’ } else{ //其他情况下,则说明该’\r’是单独的一个’\r’ // Ifit is standalone, replace the "\r" with a "\n" so that it // willbe considered by the layout system. aScanner.ReplaceCharacter(end,kLF); //将end位置的字符替换为’\n’ ++end; //将end游标位置前进一个位置 } ++mNewlineCount; //增加新行数 break; } case kLF: //如果是’\n’ aScanner.GetChar(aChar); //获取当前字符,放到aChar中 ++end; //将end游标前进一位 ++mNewlineCount; //增加新行数 break; } } else { done = PR_TRUE; //其他情况,我认为此时应当是碰到了文件末尾,或者遇到’&’和’<’符号,直接设置标示位,以跳出循环,注意这种情况下我们是不会获取该’&’或’<’字符的 } } } // Note: This function is only called fromnsHTMLTokenizer::ConsumeText. If // we return an error result from the final buffer, then itis responsible // for turning it into an NS_OK result. //这个功能只会被nsHTMLTokenizer::ConsumeText所调用。如果我们从最后一个缓冲区中获得了一个错误的返回值,那么它需要负责将其更改为NS_OK结果。 //获取origin到end的这段文本,复制到mTextValue中去。 aScanner.BindSubstring(mTextValue, origin, end); return result; } //从上面这个方法我们可以看出,它主要就是循环地对HTML中标准意义上的Text类型数据进行解析,即除了’\n’,’\r’,’&’和’<’会停止解析进行特殊处理,对其他字符都一视同仁。但是仅靠这一种方法也许还不足以满足所有的解析需求,因此后面还有对文本进行解析的两种方法ConsumeCharacterData()和ConsumeParsedCharacterData(),我们分别进行解析。 /* * Consume as much clear text from scanner as possible. * Thescanner is left on the < of the perceived end tag. * * @param aChar -- last charconsumed from stream * @param aConservativeConsume --controls our handling of content with no * terminatingstring. * @param aIgnoreComments --whether or not we should take comments into * account inlooking for the end tag. * @param aScanner -- controller ofunderlying input source * @param aEndTagname -- theterminal tag name. * @param aFlag -- dtd modes andsuch. * @param aFlushTokens -- PR_TRUE if we found theterminal tag. * @return error result */ //尽可能多地解析合法文本,此时扫描器应当位于“预期中”结束Tag的左边 nsresult CTextToken::ConsumeCharacterData(PRBoolaIgnoreComments, nsScanner&aScanner, constnsAString& aEndTagName, PRInt32 aFlag, PRBool&aFlushTokens) { nsresult result = NS_OK; //几个游标,用来进行记录:起始位置的偏移地址,当前的偏移地址,结束标签的位置,注释的起始位置,备用结束标签的位置,末尾位置 nsScannerIterator theStartOffset, theCurrOffset, theTermStrPos, theStartCommentPos,theAltTermStrPos, endPos; PRBool done = PR_FALSE; PRBool theLastIteration =PR_FALSE; //获取当前扫描器的位置,放到theStartOffset中 aScanner.CurrentPosition(theStartOffset); //初始化theCurrOffset为当前扫描器的位置 theCurrOffset = theStartOffset; //获取当前扫描器的结束位置,放到endPos中 aScanner.EndReading(endPos); //初始化theTermStrPos,theStartCommentPos,theAltTermStrPos为当前扫描器的结束位置 theTermStrPos = theStartCommentPos = theAltTermStrPos = endPos; //接下来,有一大段注释,主要是介绍的这个方法的核心算法,我们下面首先对其进行一下解释: //算法:本算法的前提是文档中的标签具有良构性。 //1:寻找一个’<’字符.这可能是: //a) 一段注释的开始位置(<!--) //b) 结束字符串的开始位置 //c) 一个标签的开始位置 //我们比较感兴趣的是a)和b)。而c)一般来讲可以忽略,因为在CDATA中我们不关心标签的存在与否。 //注意:从原理上来讲CDATA中我们应当同样也忽略注释内容,但是为了兼容性我们暂且不这样做。 //2:记录偏移值,对于’<’,从其位置开始寻找结束字符串,并且记录下该字符串的偏移值。 //3:同时对于该’<’,还要从其位置开始寻找是否是一个注释型标签’<!--’,如果找到,那么就开始在当前结束字符串和’<--’之间寻找该注释的结尾部分’-->’。如果此时没有找到该注释的结尾部分,那么说明当前解析的文档格式有错误,即是说,这个部分中有个过早的结束符号,如:<SCRIPT><!--document.write('</SCRIPT>')//--> </SCRIPT>(比如此时我们遇到<!--后,会在它和</SCRIPT>之间寻找结束型标签,而这肯定找不到,我们所希望的</SCRIPT>位于后面的-->之后,这也就说明当前的预期结束符</SCRIPT>不是我们真正的结束符)。但是我们仍需要记录下第一个预期结束字符串的偏移位置,并且更新当前的偏移位置到真正的结束字符串位置,之后转到步骤1。 //4: 恭喜…只有你找到了一对结束字符串和’-->’才算完成。否则的话需要回到步骤1。 //5:如果我们已经到了文档的结尾并且我们仍然没有达到第四步中的要求。那么我们便假设预期的结束标签就是真正的结束标签并转到第一步。这将是我们最后一次遍历。如果当前没有过早的结束符号,并且我们当前处于保守型的解析(aConservativeConsume),那么不要从扫描器获取任何内容。其他情况下,我们会一直解析并获取到结尾。 NS_NAMED_LITERAL_STRING(ltslash, "</"); //命名ltslash为”</” const nsString theTerminalString = ltslash +aEndTagName; //设置一个终结字符串为</加上当前的标签名,即我们一旦遇到</TAGNAME就停止 PRUint32 termStrLen = theTerminalString.Length(); //获取该终结字符串的长度 while (result == NS_OK && !done) { //经典循环方法,两个循环条件result和done PRBool found = PR_FALSE; nsScannerIteratorgtOffset, ltOffset = theCurrOffset; //设置两个游标为当前位置 //下面的这个循环条件可以解释为:从当前字节位置开始到结束位置之间拥有’<’字符,且当前字节位置到’<’字符的位置距离大于或等于termStrLen或’<’字符到结束位置之间的距离大于或等于termStrLen。termStrLen是终结字符串的长度,这里如果找到了’<’那么此时ltOffset的位置应当就是’<’的位置,如果没有找到,那么此时ltOffset的位置应该和endPos是一样的,这是因为在FindCharInReadable中对ltOffset进行了操作 while(FindCharInReadable(PRUnichar(kLessThan), ltOffset, endPos) && ((PRUint32)ltOffset.size_forward()>= termStrLen || Distance(ltOffset, endPos) >=termStrLen)) { // Make a copy of the (presumed) end tag and // do a case-insensitive comparison //这里我们要做的,是存一份“假设(预期)”结束标签的位置并进行一下大小写敏感的字符比较 nsScannerIterator start(ltOffset), end(ltOffset); //设置两个start,end游标为ltOffset的位置 end.advance(termStrLen); //将end前进终结字符串长度的距离 //如果找到了终结字符串,即”</TAGNAME”字符串,且之后紧接的字符要么是文件末尾,要么是一下几个字符之一:’>’,’ ’,’\t’,’\n’,’\r’ if(CaseInsensitiveFindInReadable(theTerminalString, start, end) && (end == endPos || (*end == '>' || *end ==' ' || *end == '\t' || *end == '\n'|| *end == '\r'))) { gtOffset = end; //设置gtOffset为终结字符串紧邻的下一个位置 // Note that aIgnoreComments is only not setfor <script>. We don't // want to execute scripts that aren't in theform of: <script\s.*> //这里需要注意到,我们只有在处理<script>时才不会设置忽略注释位aIgnoreComments。我们不希望处理如下格式之外的脚本:<script\s.*> //下面的判断条件可以解释为:如果紧邻的位置就是文件末尾,说明已经到了文件末尾且当前是忽略注释模式,或者在终结字符串末尾位置到文档结束位置之间找到了’>’字符 if ((end == endPos &&aIgnoreComments) || FindCharInReadable(PRUnichar(kGreaterThan), gtOffset, endPos)) { found = PR_TRUE; //设置found为true theTermStrPos = start; //并设置theTermStrPos,即终结字符串的位置为start游标所在位置 } break; //跳出内层的while循环 } ltOffset.advance(1); //我们将ltOffset前进一步再次进行寻找 } //笔者感觉上面这个算法也许效率还可以提升,查找的时候如果能够从最后开始向前进行查找,也许比这样从当前位置逐渐向后查找,从理论上更合理一点。不过这样的话处理一些错误的HTML文档可能要更花费时间。 if (found && theTermStrPos !=endPos) { //如果found为真,且所找到的结束字符的位置不为文档结尾,说明我们确实地找到了一个</TermString>,仅管此时我们还无法确定这个是否就是我们真正想要的TermString。 if (!(aFlag &NS_IPARSER_FLAG_STRICT_MODE) && //判断,如果当前不是STRICT模式,且theLastIteration为假(如果是第一次那么此时必定是假),且当前不为忽略注释模式 !theLastIteration && !aIgnoreComments) { nsScannerIterator endComment(ltOffset); //申请一个新的游标,位置为ltOffset endComment.advance(5); //将endComment前进5个位置,即确保它跳过了诸如”<!- -”等长度的字符,确保后面我们以它作为范围的下界时该范围一定能够包含<!--。 //如果theStartCommentPos为endPos(初始化值,说明第一次运行)且能够在theCurrOffset和theCurrOffset和endComment之间能找到”<!--” if ((theStartCommentPos == endPos)&& FindInReadable(NS_LITERAL_STRING("<!--"), theCurrOffset, endComment)){ theStartCommentPos = theCurrOffset; //那么设置theStartCommentPos为”<!--”的起始位置 } //如果找到了<!--标签 if (theStartCommentPos != endPos) { //在 </TERMINALSTRING>和 <!—之间寻找--> // Search for --> between <!-- and</TERMINALSTRING>. theCurrOffset = theStartCommentPos; //重新设置theCurrOffset为当前注释的起始位置 nsScannerIterator terminal(theTermStrPos); //申请一个新游标,记录下theTermStrPos的位置 if (!RFindInReadable(NS_LITERAL_STRING("-->"), theCurrOffset,terminal)) { //在theCurrOffset和terminal之间没有找到-->字符串,注意,这个RFIND的意思是从右向左进行寻找(前面笔者误解了意思,提到过自后向前改进效率的问题,看来它实现的时候确实是自后向前这么做的) // Ifyou're here it means that we have a bogus terminal string. // Eventhough it is bogus, the position of the terminal string // couldbe helpful in case we hit the rock bottom. //如果程序运行到了这里,说明我们找到了一个假的结束字符串。虽然它是假字符串,但是我们还是要记录下它的位置,避免如果后面直到结尾再也找不到结束字符串的时候,我们就会用这个字符串作为实际的结束字符串(即含有一定的纠错功能) if(theAltTermStrPos == endPos) { //如果是第一次运行 // Butwe only want to remember the first bogus terminal string. //我们只希望记住第一个出现的“虚假结束字符串”,注意是从右向左的第一个虚假结束字符串。 theAltTermStrPos = theTermStrPos; } // We didnot find '-->' so keep searching for terminal string. //由于我们没有找到-->,我们需要修改一下theCurrOffset为当前结束字符的位置,并跳过当前的结束字符继续向后寻找--> theCurrOffset = theTermStrPos; theCurrOffset.advance(termStrLen); continue; } } } //运行到了这里,说明我们已经找到了所需的Text起始和终结位置了,即找到了合适的终结字符的位置theTermStrPos,我们截取扫描器中theStartOffset和theTermStrPos之间的这段文本放置到mTextValue中。 aScanner.BindSubstring(mTextValue, theStartOffset, theTermStrPos); aScanner.SetPosition(ltOffset); //并记得设置一下扫描器的位置 // We found </SCRIPT> or</STYLE>...permit flushing -> Ref: Bug 22485 aFlushTokens = PR_TRUE; //这里貌似是对BUG 22485的一个修正。 done = PR_TRUE; //设置DONE为TRUE,跳出循环。 } else { //注意,这个判断条件是和前面的if(found && theTermStrPos != endPos)对应的 // We end up here if: // a) when the buffer runs out ot data. // b) when the terminal string is not found. //程序运行到这里只有两种可能: //a) buffer中没有数据了 //b) 没有找到终结字符串 if (!aScanner.IsIncremental()) { //如果当前扫描器不是增量模式的 if (theAltTermStrPos != endPos) { //如果找到了备选(虚假)的终结字符串 // If you're here it means that we hit therock bottom and therefore // switch to plan B, since we have analternative terminating string. //如果程序到达了这里说明我们找到头没有找到合适的终结字符串,但是我们可以采用B计划,即用前面的bogus终结字符串来替代。 theCurrOffset = theAltTermStrPos; //设置theCurrOffset为备选终结字符串的位置 theLastIteration = PR_TRUE; //设置theLastIteration标示位为真,说明当前已经是最后一次寻找了。 } else { // Oops, We fell all the way down to the endof the document. //比较遗憾,我们到达了文件末尾却啥也没找到 done = PR_TRUE; // Do this to fix Bug. 35456 //莫非有个BUG是因为没设置done以跳出循环而卡死么? result = kFakeEndTag; //设置返回结果为假标记 aScanner.BindSubstring(mTextValue,theStartOffset, endPos); //同样需要获取mTextValue,读者可以自己考虑一下这里究竟向mTextValue里存放了什么内容 aScanner.SetPosition(endPos); } } else { result = kEOF; //说明我们是增量模式,后面可能还会有数据到来,因此这里暂时返回空kEOF。 } } } if (result == NS_OK) { //如果结果为成功 mNewlineCount = mTextValue.CountChar(kNewLine); //记录下新解析内容的行数,加到已有行数中去,想想看,这个是否和前面的result=kFakeEndTag有关联? } return result; } nsresult CTextToken::ConsumeParsedCharacterData(PRBoolaDiscardFirstNewline, PRBoolaConservativeConsume, nsScanner& aScanner, const nsAString& aEndTagName, PRInt32aFlag, PRBool& aFound) { // This function is fairly straightforward except if thereis no terminating // string. If there is, we simply loop through all of theentities, reducing // them as necessary and skipping over non-terminal stringsstarting with <. // If there is *no* terminal string, then we examineaConservativeConsume. // If we want to be conservative, we backtrack to the firstplace in the // document that looked like the end of PCDATA (i.e., thefirst tag). This // is for compatibility and so we don't regress bug 42945.If we are not // conservative, then we consume everything, all the way upto the end of // the document. // 这个功能如果在有终结字符符存在的情况下还是很简单的,我们只需要循环遍历所有的实体,将他们进行适当的替换,并且跳过所有以<起始但不是终结符的字符串。如果终结符不存在的话,我们则需要检查一下aConservativeConsume位。如果我们采取保守型的策略的话,我们需要回退到文档中第一个看起来最想PCDATA末尾的位置(比如,第一个tag标签)。这主要是为了兼容性的考虑,这样我们就不会重蹈42945 BUG的覆辙。如果我们不是采用保守型的策略的话,我们则只需直接解析到文档末尾即可。 //首先设置一些终结字符 static constPRUnichar terminalChars[] = { PRUnichar('\r'), PRUnichar('\n'), PRUnichar('&'),PRUnichar('<'), PRUnichar(0) }; //和前面类似,根据这些终结字符设置一个终结条件以便后面使用。 static constnsReadEndCondition theEndCondition(terminalChars); //设置三个游标,从名称应该可以看出来,分别表示当前位置,结束位置,备用结束位置 nsScannerIterator currPos, endPos, altEndPos; //两个整型变量,后面会使用 PRUint32 truncPos = 0; PRInt32 truncNewlineCount = 0; aScanner.CurrentPosition(currPos); //获取当前扫描器的位置,放到currPos中 aScanner.EndReading(endPos); //获取当前扫描器的结束位置,放到endPos中 altEndPos = endPos; //首先初始化altEndPos为endPos,这也是可以做为判断找到终结字符串与否的一个条件 nsScannerSharedSubstring theContent; //设置一个新的字符串theContent PRUnichar ch = 0; //设置一个新的空字符 NS_NAMED_LITERAL_STRING(commentStart, "<!--"); //define常量,commentStart为”<!--” NS_NAMED_LITERAL_STRING(ltslash, "</");//define常量,ltslash为”</” const nsString theTerminalString = ltslash +aEndTagName; //设置结束字符串为</ENDTAGNAME> PRUint32 termStrLen = theTerminalString.Length(); //获取终结字符串的长度 PRUint32 commentStartLen = commentStart.Length(); //获取注释字符串的长度,即为4 nsresult result = NS_OK; //设置result结果判断位,默认为NS_OK // Note that if we're already at the end of the document,the ConsumeUntil // will fail, and we'll do the right thing. //注意到,如果此时我们已经处于文档的末尾了,ConsumeUntil方法会失败,我们会进行相应的正确处理 do { result = ConsumeUntil(theContent, mNewlineCount, aScanner, theEndCondition,PR_TRUE, PR_FALSE, aFlag); if(aDiscardFirstNewline && (NS_SUCCEEDED(result) || !aScanner.IsIncremental()) && !(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { // Check if the very first character is anewline, and if so discard it. // Note that we don't want to discard it inview source! // Also note that this has to happen here (asopposed to before the // ConsumeUntil) because we have to expand anyentities. // XXX It would be nice to be able to do thiswithout calling // writable()! //检查第一个字符是否是一个换行符,如果是的话则舍弃它,。注意到如果是view source模式则不应当舍弃它!同样注意到这应当在这里运行(相对于不应在ConsumeUntil之前),因为我们必须要对任何的实体进行处理。 //XXX期待这里可以不使用writable()来实现。 const nsSubstring &firstChunk = theContent.str(); //获取当前字符串的地址 if (!firstChunk.IsEmpty()) { //如果不为空 PRUint32 where = 0; //设置一个整型 PRUnichar newline = firstChunk.First(); //获取第一个字符,放置到newline中 if (newline == kCR || newline ==kNewLine) { //如果该字符为\r或者\n ++where; //将where加1 if (firstChunk.Length() > 1) { //如果当前字符串长度大于1(避免单字符的情况) if(newline == kCR && firstChunk.CharAt(1) == kNewLine) { //如果当前起始字符为\r且之后的第二个字符为\n //Handle \r\n = 1 newline. //对于\r\n,作为一行来进行处理 ++where; //则将where加1 } //注意:如果是\n\r的模式,则应当算作两行 // Note:\n\r = 2 newlines. } } if (where!= 0) { //如果where不为0 theContent.writable() = Substring(firstChunk, where); //则对字符串进行切分,舍弃开头第一个字符 } } } aDiscardFirstNewline= PR_FALSE; //设置舍弃新行标示位为PR_FALSE if (NS_FAILED(result)) { //如果result为失败,说明我们是位于文档末尾了 if (kEOF == result &&!aScanner.IsIncremental()) { //如果result为空且aScanner不为增量模式,即后面也不会有新内容到来 //没办法,设置aFound为TRUE aFound = PR_TRUE; // this is as good as itgets. //同时设置result为kFakeEndTag result = kFakeEndTag; if (aConservativeConsume &&altEndPos != endPos) { //如果解析采用的是保守型策略,且当前有备用的结束型节点 // We ran out of room looking for a </title>.Go back to the first // place that looked like a tag and use thatas our stopping point. //我们没有找到合适的</title>。因此我们回溯到第一个看起来像tag标签的地方,并且使用它作为我们的停止点。 theContent.writable().Truncate(truncPos); //将theContent的可写部分清零 mNewlineCount = truncNewlineCount; //清零mNewlineCount aScanner.SetPosition(altEndPos, PR_FALSE, PR_TRUE); //设置当前位置为altEndPos } // else we take everything we consumed. mTextValue.Rebind(theContent.str()); //拷贝所有的内容 } else { aFound = PR_FALSE; //其他情况下,设置aFound为假 } aScanner.CurrentPosition(currPos); //设置currPos为扫描器当前的位置 aScanner.GetChar(ch); // this character mustbe '&' or '<' //获取当前位置的字符,放置到ch中,理论上来讲这个字符必然是’&’或者’<’ if (ch == kLessThan && altEndPos== endPos) { //如果ch为<,且备用的结束字符串位置是endPos(即第一次运行) // Keep this position in case we need it forlater. altEndPos = currPos; //设置altEndPos为当前位置 truncPos = theContent.str().Length(); //获取当前内容的长度 truncNewlineCount =mNewlineCount; //获取当前的mNewlineCount } if (Distance(currPos, endPos) >=termStrLen) { //如果当前位置到结束位置的长度大于或等于结束型字符的长度 nsScannerIterator start(currPos), end(currPos); //设置两个游标,用来进行后面的字符串比对 end.advance(termStrLen); //将end前进结束型字符串的长度 if (CaseInsensitiveFindInReadable(theTerminalString,start, end)) { //在start,end之间进行比对,判断其如果是结束型字符串 if (end != endPos && (*end =='>' ||*end ==' ' || *end == '\t' || *end == '\n'|| *end == '\r')) { //并且判断其之后的字符是正确的字符之一 aFound = PR_TRUE; //设置aFound为TRUE mTextValue.Rebind(theContent.str()); //获取字符串的内容 // Note: This SetPosition() is actually goingbackwards from the // scanner's mCurrentPosition (so we passaReverse == PR_TRUE). This // is because we call GetChar() above after weget the current // position. //注意:这个SetPosition()实际上是将扫描器的当前位置向后倒退(因此我们设置aReverse==PR_TRUE)。这是因为我们在之前获取当前位置后调用了GetChar()。 aScanner.SetPosition(currPos, PR_FALSE, PR_TRUE); break; } } } // IE only consumes <!-- --> as commentsin PCDATA. if (Distance(currPos, endPos) >=commentStartLen) { //如果currPos到endPos的距离大于<!--的距离,即4 nsScannerIterator start(currPos), end(currPos); //同样采用start和end两个游标对其进行比对 end.advance(commentStartLen); if(CaseInsensitiveFindInReadable(commentStart, start, end)) { //如果确定其确实为<!— //申请一个新的注释型Token对其进行解析 CCommentToken consumer; // stack allocated. // CCommentToken expects us to be on the '-' aScanner.SetPosition(currPos.advance(2)); //前进两个位置,因为注释型Token需要我们从位于’-’的位置上开始解析。 // In quirks mode we consume too many thingsas comments, so pretend // that we're not by modifying aFlag. //在quirks模式中,我们将许多东西都作为注释进行处理,因此我们通过更改aFlag的方式来忽略他们的处理 result = consumer.Consume(*currPos, aScanner, (aFlag &~NS_IPARSER_FLAG_QUIRKS_MODE) | NS_IPARSER_FLAG_STRICT_MODE); //调用注释Token进行解析 if (kEOF == result) { //一般如果result是kEOF都说明文件到末尾 // This can only happen if we're really out ofspace. return kEOF; //返回kEOF } else if(kNotAComment == result) { //如果返回结果为kNotAComment,说明接下来的这段文本不是注释文本,我们需要将其作为普通的文本进行处理 // Fall through and consume this as text. aScanner.CurrentPosition(currPos); //获取当前位置 aScanner.SetPosition(currPos.advance(1)); //将其前进一位并重新设置给aScanner } else { consumer.AppendSourceTo(theContent.writable()); //将解析过的内容放入theContent中 mNewlineCount += consumer.GetNewlineCount(); //并且记录下新增的行数 continue; //继续循环,即继续解析后面的文本 } } } result = kEOF; //解析完毕,设置结果为kEOF // We did not find the terminal string yet so // include the character that stoppedconsumption. theContent.writable().Append(ch); //因此此时我们仍未找到终结字符,因此我们手动为其添加一个终结字符以结束解析 } while (currPos != endPos); //解析循环条件 return result; } constnsSubstring& CCDATASectionToken::GetStringValue() { return mTextValue; //获取mTextValue的Get方法 } //以上就是所有与CDATA相关的解析文档,下面我们要对Markup声明型的Token进行解析的文档进行分析,首先是几个比较简单的小方法。 //这个是MarkupDeclToken无参的构造方法 CMarkupDeclToken::CMarkupDeclToken() : CHTMLToken(eHTMLTag_markupDecl) { } CMarkupDeclToken::CMarkupDeclToken(const nsAString& aName) : CHTMLToken(eHTMLTag_markupDecl) { mTextValue.Rebind(aName); //这个是MarkupDeclToken使用字符串的构造方法,会将参数字符串赋值给mTextValue数据成员变量。 } PRInt32 CMarkupDeclToken::GetTokenType() { return eToken_markupDecl; //获取该Token的类型,以一个Int的标记类型返回 } //下面则是MarkupDeclToken的主要解析方法Consume: /* * Consume as much declaration from scanner as possible. * Declaration is a markup declaration of ELEMENT, ATTLIST, ENTITY or * NOTATION, which can span multiple lines and ends in >. * * @param aChar -- last charconsumed from stream * @param aScanner -- controller ofunderlying input source * @return error result */ //尽可能地从scanner获取声明型文本。声明型文本是一种声明ELEMENT(元素),ATTLIST(属性列表),ENTITY(实体数据),NOTATION(记号)的标记文本。它可能拥有若干行,并最终以>符号为结束。 nsresult CMarkupDeclToken::Consume(PRUnicharaChar, nsScanner& aScanner, PRInt32 aFlag) { //首先设置结束字符 static constPRUnichar theTerminalsChars[] = { PRUnichar('\n'), PRUnichar('\r'), PRUnichar('\''),PRUnichar('"'), PRUnichar('>'), PRUnichar(0) }; //使用这些字符创建结束判断条件 static constnsReadEndCondition theEndCondition(theTerminalsChars); nsresult result = NS_OK; PRBool done = PR_FALSE; PRUnichar quote = 0; //设置一些变量 nsScannerIterator origin, start, end; //获取扫描器当前的位置,赋值给origin aScanner.CurrentPosition(origin); start = origin; while (NS_OK == result && !done) { //经典循环条件 aScanner.SetPosition(start); //设置扫描器为start位置 result = aScanner.ReadUntil(start, end, theEndCondition, PR_FALSE); //从当前位置开始,一直读取直到遇到theEndCondition为止(或文件终结) if (NS_OK == result) { //如果读取成功 result = aScanner.Peek(aChar); //我们尝试查看下一个字符 if (NS_OK == result) { //如果查看成功 PRUnichar theNextChar = 0; //设置一个新的空字符 if (kCR == aChar || kNewLine == aChar) { //如果下一个字符为’\r’或者’\n’ //尝试获取该字符 result = aScanner.GetChar(aChar); // Strip offthe char //再次尝试查看下一个字符,即该字符之后的下一个字符 result = aScanner.Peek(theNextChar); // Thensee what's next. } switch(aChar) { //根据该字符的不同,我们进行相应的不同操作 case kCR: //如果是’\r’ // result= aScanner.GetChar(aChar); if(kLF == theNextChar) { //且该字符之后紧接着的是’\n’ //如果是一个’\r’后紧接着一个’\n’,我们不需要替换它并且可以允许它被渲染系统所忽略(即忽略’\n’只需换一行即可) // Ifthe "\r" is followed by a "\n", don't replace it and // letit be ignored by the layout system end.advance(2); //跳过这两个字符 result =aScanner.GetChar(theNextChar); //并从扫描器“拿走”’\n’字符 } else{ // Ifit standalone, replace the "\r" with a "\n" so that // itwill be considered by the layout system //其他情况下如果它是独立的,我们则需要使用’\n’字符来替换’\r’,这样它就会被渲染系统所考虑进去(’\r’是回到当前行起始,而不是换行) aScanner.ReplaceCharacter(end,kLF); //进行替换 ++end; //跳过这一个字符 } ++mNewlineCount; //添加了新行,因此记数加一 break; case kLF: //如果是’\n’ ++end; //直接跳过这个字符 ++mNewlineCount; //添加了新行,因此记数加一 break; case '\'': case '"': //如果是’\’或者’”’ ++end; //跳过这个字符 if(quote) { //判断现在的字符是否是转义后的字符,如果是此时quote应当是非零,如果不是则quote应当是0 if(quote == aChar) { //如果是转义后的字符 quote = 0; //将其清零,这样下一个字符就会被当做转义字符看待 } } else{ //说明此时是转义字符 quote = aChar; //这样做是为了将\和”区分对待 } break; case kGreaterThan: //如果是’>’符号 if(quote) { //如果此时是转义字符 ++end; //那么直接跳过这个字符 } else{ //其他情况下说明遇到结尾了,我们做一些善后工作并跳出循环 start = end; //设置start和end相等 // Notethat start is wrong after this, we just avoid temp var ++start; //并将start下移一个字符,即’>’的下一个字符 //设置扫描器的位置为start aScanner.SetPosition(start); // Skip the > done = PR_TRUE; //设置循环跳出位 } break; default: //此处不可能发生 NS_ABORT_IF_FALSE(0, "should not happen, switch is missing cases?"); break; } start = end; } else { //此处是前面的Peek aChar的操作失败了,什么操作也不做,但是为了后面的操作要设置done为true done = PR_TRUE; } } } aScanner.BindSubstring(mTextValue, origin, end); //获取从起始位置到读取结束位置的所有字符,放入TextValue if (kEOF == result) { //如果此时已经是文件末尾了 mInError = PR_TRUE; //设置错误位为true if (!aScanner.IsIncremental()) { //如果此时扫描器不是增量式的 // Hide this EOF. result = NS_OK; //设置结果为NS_OK,即全部结束 } } return result; //返回result } constnsSubstring& CMarkupDeclToken::GetStringValue() //小方法,获取该Token文本值的Get方法 { return mTextValue.AsString(); } //以上我们看完了声明型标签的内容,下面我们要处理的则是比较重要的注释型标签的文本,CommentToken。算法其实并不难,和前面的一些Token的处理并无大异,需要注意的是火狐对于CommentToken的处理分为了Strict和Quirks两种模式,两种模式的分别针对不同的DTD有着不同的处理,要注意区分。 CCommentToken::CCommentToken() //空构造方法 : CHTMLToken(eHTMLTag_comment) { } CCommentToken::CCommentToken(const nsAString& aName) //带文本串参数的构造方法 : CHTMLToken(eHTMLTag_comment) { mComment.Rebind(aName); //想当于设置文本值的set方法 } void CCommentToken::AppendSourceTo(nsAString&anOutputString) { AppendUnicodeTo(mCommentDecl, anOutputString); //一个简单的字符串拷贝,从mCommentDecl拷到anOutputString,具体请见nsScannerString.h } //下面是用来判断是否到达了注释末尾的方法。值得注意的是,这是火狐浏览器一个区别于IE的方法,因为火狐浏览器用这个方法规定了不允许在注释中出现“- -”的符号,否则会认为是注释的结束。很多人不喜欢这种规定,但是实际上在HTML4的规范中已经明确规定注释文本中不允许使用这样的写法。 staticPRBool IsCommentEnd(constnsScannerIterator& aCurrent,constnsScannerIterator& aEnd, nsScannerIterator& aGt) { nsScannerIterator current = aCurrent; //用来进行文本判断的游标 PRInt32 dashes = 0; //用来记录’-’的个数 while (current != aEnd && dashes != 2) { //循环条件,直到文件末尾或者遇到两个’-’为止 if (*current == kGreaterThan) { //如果当前是’>’符号 aGt = current; //用参数aGt返回当前的位置 return PR_TRUE; //返回true } if (*current == PRUnichar('-')) { //如果当前是’-’符号 ++dashes; //dash的个数加一 } else { dashes = 0; //其他情况下,清空dashes } ++current; //游标前进一个字符 } return PR_FALSE; //跳出了while循环,那么说明要么是文件结尾或遇到了两个dash,返回false } //下面我们就来看最关键的一个算法,即对注释节点真正进行处理的Consume方法,它针对Strict和Quirks两种不同的DTD模式分别进行处理。我们先来看Strick模式下的。 nsresult CCommentToken::ConsumeStrictComment(nsScanner&aScanner) { // <!--[... -- ... -- ...]*--> //这个算法对于格式良好的注释文本处理效果很好,但是对于格式不好的注释文本处理效果则不是很好,这个算法会对其“按块”进行处理 nsScannerIterator end, current, gt, lt; //设置四个算法用的游标 aScanner.EndReading(end); //获取当前Scanner的末尾位置,赋值给end aScanner.CurrentPosition(current); //获取当前Scanner的当前位置,赋值给current nsScannerIterator beginData = end; //申请一个新的游标,初始值赋值为end lt = current; //lt游标赋值为current //将lt游标倒退2个位置,即倒退过<!这两个字符 lt.advance(-2); // <! //将current游标倒退1个位置 current.advance(-1); //普通的注释必须以<!- -作为开端 if (*current == kExclamation && //对当前”<!--”四个字符进行判断 ++current != end && *current == kMinus && ++current != end && *current == kMinus && ++current != end) { //且之后不能是文档结尾 nsScannerIterator currentEnd = end; //用一个游标记录a下end的位置 PRBool balancedComment = PR_FALSE; //设置一个标示位,用来标示注释是否格式完整 NS_NAMED_LITERAL_STRING(dashes, "--"); //定义dashes文本替代符为两个连续的减号”- -” beginData= current; //设游标beginData为当前位置current while (FindInReadable(dashes, current,currentEnd)) { //在当前的current和currentEnd位置之间寻找”- -” current.advance(2); //此时一定是找到了,且current到达”--”符号的起始处,那么将current游标前进两位以跳过”- -” //我们需要将”- -”字符进行成对匹配,即最初是false,找到第二个变成true,之后找到找到偶数个即为true,奇数个为false balancedComment = !balancedComment; if(balancedComment && IsCommentEnd(current, end, gt)) { 判断此时是否已经是结尾型Comment(调用前面的IsCommentEnd函数),并且“- -”都已经匹配成对 //此时说明已经解析成功 current.advance(-2); //注意此时即使beginData == current也可以,(我们会拷贝一个空字符串过来),并且我们需要为mComment绑定值 aScanner.BindSubstring(mComment, beginData, current); //将beginData与current之间的内容放入mComment,注意这里的内容是<!- - 和- ->之间的那段文本,即注释的真正内容。 aScanner.BindSubstring(mCommentDecl, lt, ++gt); //将lt和gt之间的内容放入mCommentDecl中,注意这里的内容是包含<!- -和- ->整体的注释文本 aScanner.SetPosition(gt); //设置扫描器的当前位置为gt,即注释文本之后的位置 return NS_OK; //返回成功值 } //跳过”--”符号,并继续对后面的字符串进行处理 currentEnd = end; } } //如果beginData == end,那么说明我们没有找到一个’--’符号 if (beginData == end) { //这可能是由于空注释造成的:如<!> //也可能是一些完全莫名其妙的情况,如<!This is foobar> //下面对这两种情况都加以处理 aScanner.CurrentPosition(current); //获取扫描器当前的位置,赋值给current beginData = current; //设置beginData为current if (FindCharInReadable('>', current, end)) { //在当前current和end结束位置之间寻找’>’字符 //如果找到了,此时的Current应当指向’>’字符 aScanner.BindSubstring(mComment, beginData, current); //如果找到了,我们则认为beginData和current之间,即<!和>之间的这段文本为注释的内容,并将其赋值给mComment。 aScanner.BindSubstring(mCommentDecl, lt, ++current); //并且将包含<!和>的这段文本内容作为注释的文本,赋值给mCommentDecl aScanner.SetPosition(current); //设置扫描器的当前位置为current。 return NS_OK; //返回成功值 } } if (aScanner.IsIncremental()) { //如果程序运行到了这里,说明我们只看到了一段注释的开头,但是没有看到结尾,并且我们仍然在读取页面的过程中。这种情况下返回值会使程序回溯,并等待页面读取更多内容后再次尝试。 //未来的改进可能:为了提高效率,我们这里可以缓存一些环境信息,比如我们此时解析到哪了之类的值,以便于下次调用时提高速度。 return kEOF; //返回End of File值 } //如果程序运行到了这里,说明我们没有找到注释的结尾符,即没有- ->之类的结束符,此时我们就不能够将其作为注释,而是要将其作为普通文本进行处理。 aScanner.SetPosition(lt, PR_FALSE, PR_TRUE); //回溯扫描器的位置,并设置两个bool变量值,第一个表示是否为结尾,第二个表示当前是否回溯(请参考nsScanner的解析文档) return kNotAComment; //返回非注释标示值 //下面是和上面ConsumeStrictComment对应的ConsumeQuirkComment,即对一些非良构的注释文本进行处理,在处理的过程上有些相似,但是适应性非常强。 nsresult CCommentToken::ConsumeQuirksComment(nsScanner&aScanner) { // <![-[-]] ... [[-]-|--!]> //这个方法对于平时经常使用的注释文本处理效果很好,但是它并不是真正意义上“分批”对注释内容进行处理的,不过话说回来,IE或Nav浏览器也不是。 nsScannerIteratorend, current; //设置两个游标 aScanner.EndReading(end); //设置end游标为扫描器的结束位置 aScanner.CurrentPosition(current); //设置current为扫描器的当前位置 nsScannerIterator beginData = current, beginLastMinus = end, bestAltCommentEnd = end, lt = current; //设置四个游标,并分别初始化值 lt.advance(-2); //将lt游标后退两位,跳过“<!”文本 //当我们到达了这里,说明我们已经解析了<!文本 //跳过所有可能的减号”-” if (current != end && *current == kMinus) { //如果当前字符不为文档末尾且当前字符为减号 beginLastMinus = current; //用beginLastMinus记录下当前位置 ++current; //将当前游标前进一位 ++beginData; //将beginData游标同样前进一位 if (current != end && *current== kMinus) { //判断是否是减号“-”,如果是则说明此时是”<!--”文本 beginLastMinus = current; //记录下当前位置 ++current; //将当前游标前进一位 ++beginData; //将beginData游标同样前进一位 //长注释 nsScannerIterator currentEnd = end, gt = end; //设置两个游标,并赋初值 //下面要寻找注释的结尾符 while (FindCharInReadable(kGreaterThan,current, currentEnd)) { //在后面的文本中不断寻找”>”符号 gt = current; //设置gt为当前位置(即”>”符号的位置) if (bestAltCommentEnd == end) { //若bestAltCommentEnd未被改变 bestAltCommentEnd = gt; //将其设置为gt的位置,这个游标后面会用到,主要作用是在进行回溯的时候所用,即记录“备用的结束符” } --current; //将current后退一位,我们要判断其是否符号良好的结束符的格式 PRBool goodComment = PR_FALSE; //设置一个标示位,标示是否为良好格式的结束符 if (current != beginLastMinus &&*current == kMinus) { // 此时说明为->符号 --current; //继续后退一位 if (current != beginLastMinus &&*current == kMinus) { // 此时说明为-->符号 goodComment = PR_TRUE; //标示我们找到了良好格式的结束符 --current; //继续后退一位(为了后面进行注释内容拷贝) } } else if(current != beginLastMinus && *current == '!'){ //如果current不为减号而为’!’号 --current; //将current后退一位 if (current != beginLastMinus &&*current == kMinus) { //继续判断是否为减号 --current; if(current != beginLastMinus && *current == kMinus) { // 此时说明为--!>符号 --current; goodComment = PR_TRUE; //标示我们找到了良好格式的结束符 } } } else if(current == beginLastMinus) { //这里需要注意一下,运行到这里说明我们遇到了空注释,即<!-->,此时我们仍认为其是格式良好的注释,并设置标示位为true goodComment = PR_TRUE; } if (goodComment) { //如果良构标示位为true //此时说明我们已经完成了解析 //将注释的内容,注释的完整文本拷贝入mComment和mCommentDecl aScanner.BindSubstring(mComment, beginData, ++current); aScanner.BindSubstring(mCommentDecl, lt, ++gt); //设置扫描器的位置为gt aScanner.SetPosition(gt); return NS_OK; //返回成功值 } else { //说明没有找到合适的良构结束型标记,从上一个’>’符号开始重新尝试 current = ++gt; currentEnd = end; } } if (aScanner.IsIncremental()) { //如果程序运行到了这里,说明我们只看到了一段注释的开头,但是没有看到结尾,并且我们仍然在读取页面的过程中。这种情况下返回值会使程序回溯,并等待页面读取更多内容后再次尝试。 //未来的改进可能:为了提高效率,我们这里可以缓存一些环境信息,比如我们此时解析到哪了之类的值,以便于下次调用时提高速度。 returnkEOF; } //如果程序运行到了这个地方,我们则处于一个比较特殊的位置。 //此时的问题是我们已经到达了文档的末尾,但是还没有遇到”- ->”符号。在这种情况下,w我们应当尝试的第一件事就是看看我们能否在已经解析过的文本中寻找到一个替代的”>”符号。如果能的话,则回溯到那个位置,并且认为从起始直到这个位置之前的内容皆为注释,如果没有的话,我们则认为文档没有注释的结尾,应当被看作一大段注释来处理。 gt = bestAltCommentEnd; //回溯到上一个”>”的位置 //将起始位置与这个”>”之间的所有内容当作注释进行存储 aScanner.BindSubstring(mComment, beginData, gt); if (gt != end) { ++gt; } //同样将这个”>”作为注释结束符,一同作为注释文本拷贝起来 aScanner.BindSubstring(mCommentDecl, lt, gt); aScanner.SetPosition(gt); return NS_OK; //返回成功结果 } } //前面都是处理以<!- -开头的注释内容的,下面我们将要处理短类型的文本即以<!为开头的内容,我们和上面采用类似的方法进行处理,此处不多做介绍 current = beginData; if (FindCharInReadable(kGreaterThan, current, end)) { nsScannerIterator gt = current; if (current != beginData) { --current; if (current != beginData &&*current == kMinus) {// -> --current; if (current != beginData &&*current == kMinus) {// --> --current; } } else if(current != beginData && *current == '!'){// !> --current; if (current != beginData &&*current == kMinus) {// -!> --current; if (current != beginData &&*current == kMinus) {// --!> --current; } } } } if (current != gt) { aScanner.BindSubstring(mComment, beginData, ++current); }else { //此处值得稍微注意一下,和前面不同,此时同样为空注释,但我们不需要将current前进,因为此时是遇到了<!> aScanner.BindSubstring(mComment, beginData, current); } aScanner.BindSubstring(mCommentDecl, lt, ++gt); aScanner.SetPosition(gt); return NS_OK; //返回成功值 } //运行到此处说明没有找到合适的结束符 if (!aScanner.IsIncremental()) { //如果文档已经读取完毕 //此时说明这根本不是一个注释,将其全部作为普通文本处理 aScanner.SetPosition(lt, PR_FALSE, PR_TRUE); return kNotAComment; //返回“非注释”结果 } return kEOF; //说明文档未读取完毕,返回文档结尾符kEOF,并等待更多内容 } //下面有几个小方法,我们逐一来介绍 //一个用来处理注释中标识符部分的方法,请注意此时我们已经处理了“<!”部分 //这也是CommentToken的Consume主方法,通过其调用前面的strict和quirk两种解析方法 /* * Consume the identifier portion of the comment. * Notethat we've already eaten the "<!" portion. * * @param aChar -- last char consumed from stream * @param aScanner -- controller ofunderlying input source * @return error result */ nsresult CCommentToken::Consume(PRUnichar aChar,nsScanner& aScanner, PRInt32 aFlag) { nsresult result = PR_TRUE; if (aFlag & NS_IPARSER_FLAG_STRICT_MODE) { //鉴于BUG 53011和2749的反驳,此处使用Strict模式进行解析 result = ConsumeStrictComment(aScanner); //调用前面的strict模式解析 } else { result = ConsumeQuirksComment(aScanner); //调用前面的quirk模式解析 } if (NS_SUCCEEDED(result)) { //如果解析结果成功 mNewlineCount = mCommentDecl.CountChar(kNewLine); //记录一下有多少行 } return result; } //下面是几个小方法,此处就不一一介绍了 //获取注释内容mComment constnsSubstring& CCommentToken::GetStringValue() { return mComment.AsString(); } //获取标签类型,返回“注释标签”类型 PRInt32 CCommentToken::GetTokenType() { return eToken_comment; } CNewlineToken::CNewlineToken() //换行符Token,直接继承过来的构造方法,无改动 : CHTMLToken(eHTMLTag_newline) { } PRInt32 CNewlineToken::GetTokenType() //获取换行符的类型 { return eToken_newline; } staticnsScannerSubstring* gNewlineStr; void CNewlineToken::AllocNewline() //分配一个新行,其实就是在全局静态变量字符串gNewlineStr中赋值\n { gNewlineStr = newnsScannerSubstring(NS_LITERAL_STRING("\n")); } void CNewlineToken::FreeNewline() //删除新行,其实就是删除gNewlineStr { if (gNewlineStr) { delete gNewlineStr; gNewlineStr = nsnull; } } //获取gNewlineStr的值 constnsSubstring& CNewlineToken::GetStringValue() { return gNewlineStr->AsString(); } //下面是NewlineToken的Consume主方法 //解析一整行(cr/lf对) /* * Consume one newline (cr/lf pair). * * @param aChar -- last char consumedfrom stream * @param aScanner -- controller ofunderlying input source * @return error result */ nsresult CNewlineToken::Consume(PRUnichar aChar,nsScanner& aScanner, PRInt32 aFlag) { //首先介绍一下HTML定义中对于“新行”的定义: //一个行终结符定义为一个回车符( ),一个换行符( ),或者一个回车/换行对 //所有的行终结符都属于空白字符(White Space) nsresult rv = NS_OK; if (aChar == kCR) { //如果该字符为’\r’ PRUnichar theChar; rv = aScanner.Peek(theChar); //查看一下之后紧接着的字符 if (theChar == kNewLine) { //看看之后紧接着的字符是否是’\n’ rv = aScanner.GetChar(theChar); //如果是则将其获取 } else if(rv == kEOF && !aScanner.IsIncremental()) { //如果是文档结尾 //应当确保我们不会丢失这个换行信息 rv = NS_OK; } } mNewlineCount = 1; //设置新行数为1 return rv; //返回rv结果 } //下面是对节点属性进行解析的attributeToken,我们来看一下它的方法。 //无参构造方法 CAttributeToken::CAttributeToken() : CHTMLToken(eHTMLTag_unknown) { mHasEqualWithoutValue = PR_FALSE; } //基于字符串的构造方法 CAttributeToken::CAttributeToken(const nsAString& aName) : CHTMLToken(eHTMLTag_unknown) { mTextValue.writable().Assign(aName); //将参数赋值给mTextValue mHasEqualWithoutValue = PR_FALSE; } //基于键/值对的构造方法 CAttributeToken::CAttributeToken(const nsAString& aKey,constnsAString& aName) : CHTMLToken(eHTMLTag_unknown) { mTextValue.writable().Assign(aName); //为Value赋值 mTextKey.Rebind(aKey); //为Key赋值 mHasEqualWithoutValue = PR_FALSE; } //获取类型 PRInt32 CAttributeToken::GetTokenType() { return eToken_attribute; } //获取其字符串值,注意是获取的mTextValue的值(而不是mTextKey) constnsSubstring& CAttributeToken::GetStringValue() { return mTextValue.str(); } //获取Source,主要调用了后面的AppendSourceTo方法 void CAttributeToken::GetSource(nsString&anOutputString) { anOutputString.Truncate(); AppendSourceTo(anOutputString); } //将源代码字符串值赋值给参数anOutputString,其格式如class=”abc”,值得注意的是,这里实际上赋值的是解析后的值(这也就是说我们看到的比如class=”abc”的源代码都是解析过的) void CAttributeToken::AppendSourceTo(nsAString&anOutputString) { AppendUnicodeTo(mTextKey, anOutputString); if (mTextValue.str().Length() ||mHasEqualWithoutValue) { anOutputString.AppendLiteral("="); } anOutputString.Append(mTextValue.str()); // anOutputString.AppendLiteral(";"); } //下面是一个公共的通用方法,主要用来处理一段由单引号或者双引号包含起来的字段。 staticnsresult ConsumeQuotedString(PRUnichar aChar, nsScannerSharedSubstring& aString, PRInt32& aNewlineCount, nsScanner& aScanner, PRInt32 aFlag) { //首先对aChar的合法性做一下判断 NS_ASSERTION(aChar == kQuote || aChar == kApostrophe, "charis neither quote nor apostrophe"); //记录下原始字符串的长度 PRUint32 origLen = aString.str().Length(); //下面两个是用于进行字符串扫描的终结符组合 static constPRUnichar theTerminalCharsQuote[] = { //双引号情况 PRUnichar(kQuote), PRUnichar('&'),PRUnichar(kCR), PRUnichar(kNewLine),PRUnichar(0) }; static constPRUnichar theTerminalCharsApostrophe[] = { //单引号情况 PRUnichar(kApostrophe), PRUnichar('&'),PRUnichar(kCR), PRUnichar(kNewLine), PRUnichar(0) }; //利用上面的两个字符串数组设置两个终结条件 static constnsReadEndCondition theTerminateConditionQuote(theTerminalCharsQuote); static constnsReadEndCondition theTerminateConditionApostrophe(theTerminalCharsApostrophe); //假设默认是双引号情况,以便进行初始化 const nsReadEndCondition *terminateCondition =&theTerminateConditionQuote; if (aChar == kApostrophe) { //单引号情况判断 terminateCondition = &theTerminateConditionApostrophe; } nsresult result = NS_OK; //用来储存结果 nsScannerIterator theOffset; //游标 aScanner.CurrentPosition(theOffset); //获取扫描器当前位置 //下面是从扫描器当前位置开始,直到找到terminateCondition中规定的终结符为止 result = ConsumeUntil(aString, aNewlineCount, aScanner, *terminateCondition,PR_TRUE, PR_TRUE, aFlag); if (NS_SUCCEEDED(result)) { //如果找到了 result = aScanner.GetChar(aChar); //则将其进行保存,这里的aChar应当是单引号或者双引号 } // 引用: Bug 35806 // 一个备用方法,主要用来处理一些极端情况 // 如: <table> <trd="><td>hello</td></tr></table> if (!aString.str().IsEmpty() &&aString.str().Last() != aChar && !aScanner.IsIncremental() && result == kEOF) { //进行特殊情况的判断 static constnsReadEndCondition theAttributeTerminator(kAttributeTerminalChars); //重新设置终结符 aString.writable().Truncate(origLen); //复位 aScanner.SetPosition(theOffset, PR_FALSE, PR_TRUE); //复位 result = ConsumeUntil(aString, aNewlineCount, aScanner, //重新进行扫描 theAttributeTerminator, PR_FALSE, PR_TRUE, aFlag); if (NS_SUCCEEDED(result) &&(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { //如果成功找到,则将结果位设置为表示“这个字符串没有终结符” result = NS_ERROR_HTMLPARSER_UNTERMINATEDSTRINGLITERAL; } } return result; //返回结果 } //下面这个方法,主要是用来在view-source模式中处理非法的属性的,这个方法的目标是那些以’,或”,或/.为起始的属性,我们会消耗所有的’,”,或者/,以及接下来的空格 staticnsresult ConsumeInvalidAttribute(nsScanner&aScanner, PRUnichar aChar, nsScannerIterator&aCurrent, PRInt32&aNewlineCount) { //判断aChar必须是单引号,双引号,或者/ NS_ASSERTION(aChar == kApostrophe || aChar == kQuote || aChar ==kForwardSlash, "aCharmust be a quote or apostrophe"); nsScannerIterator end, wsbeg; aScanner.EndReading(end); //获取扫描器的结束位置 //不断向后查找,直到找到不为aChar的位置为止 while (aCurrent != end && *aCurrent == aChar){ ++aCurrent; } aScanner.SetPosition(aCurrent); //设置扫描器的当前位置为该位置 return aScanner.ReadWhitespace(wsbeg, aCurrent,aNewlineCount); //并且跳过所有的空格 } //处理并解析属性的键/值对 nsresult CAttributeToken::Consume(PRUnicharaChar, nsScanner& aScanner, PRInt32 aFlag) { nsresult result; nsScannerIterator wsstart, wsend; if (aFlag & NS_IPARSER_FLAG_VIEW_SOURCE) { //如果现在是viewsource模式 result = aScanner.ReadWhitespace(wsstart, wsend, mNewlineCount); //读取空白字符之前的文本 if (kEOF == result && wsstart !=wsend) { //我们这样做的原因是如果此时是文档中的最后一个token,我们不会失去空格 aScanner.BindSubstring(mTextKey, wsstart, wsend); //将这段文本拷贝入mTextKey } } else { result = aScanner.SkipWhitespace(mNewlineCount); //其他情况下我们将跳过所有的空格字符 } if (NS_OK == result) { //如果结果是成功,即运行了刚才的SkipWhitespace并且成功 static constPRUnichar theTerminalsChars[] = //设置一系列终结符 { PRUnichar(' '), PRUnichar('"'), PRUnichar('='), PRUnichar('\n'), PRUnichar('\r'), PRUnichar('\t'), PRUnichar('>'), PRUnichar('<'), PRUnichar('\''), PRUnichar('/'), PRUnichar(0) }; static const nsReadEndConditiontheEndCondition(theTerminalsChars); //利用这一系列终结符设置终结条件 nsScannerIterator start, end; result= aScanner.ReadUntil(start, end, theEndCondition, PR_FALSE); //处理字符,直到遇到终结条件为止。 if (!(aFlag &NS_IPARSER_FLAG_VIEW_SOURCE)) { //如果现在不是viewsource模式 aScanner.BindSubstring(mTextKey, start, end); //将start和end之间的字符拷贝作为Key值 }else if (kEOF== result && wsstart != end) { //如果到达了文档的结尾且有一些可解析的内容 //此时我们捕获所有的文本作为key(从空格的开始处直到文档的结尾) aScanner.BindSubstring(mTextKey, wsstart, end); } //现在是时候去解析和处理那“可能存在“的value值了 if (NS_OK == result) { //如果结果为成功 if (aFlag &NS_IPARSER_FLAG_VIEW_SOURCE) { //如果此时为view source模式 //读取直到空白字符,并且将这一段空白文本作为Key值保存 result = aScanner.ReadWhitespace(start, wsend, mNewlineCount); aScanner.BindSubstring(mTextKey, wsstart, wsend); } else { //否则则跳过这一段空白文本 result = aScanner.SkipWhitespace(mNewlineCount); } if (NS_OK == result) { //如果结果为成功 //此处我们要一直处理,直到找到”=”号或者”>”符号为止 result = aScanner.Peek(aChar); //读一下扫描器当前的字符 if (NS_OK == result) { if (kEqual == aChar) { //如果为”=”号 result = aScanner.GetChar(aChar); //跳过”=”号 if(NS_OK == result) { //如果结果为成功 if(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE) { //如果当前为view source模式 PRBool haveCR; result =aScanner.ReadWhitespace(mTextValue, mNewlineCount, haveCR); //读取所有的空白字符并放入mTextValue } else{ result =aScanner.SkipWhitespace(mNewlineCount); //否则跳过所有的空白字符 } if(NS_OK == result) { //如果结果未成功 result = aScanner.Peek(aChar); //再查看一下下一个字符 if(NS_OK == result) { //如果查看成功 if(kQuote == aChar || kApostrophe == aChar) { //判断该字符是否是单引号或者双引号 aScanner.GetChar(aChar); //如果是的话则获取该字符 if (aFlag& NS_IPARSER_FLAG_VIEW_SOURCE) { mTextValue.writable().Append(aChar); //将该字符预先拷贝到mTextValue中 } //之后将利用该引号作为参数,获取该单/双引号之间的所有内容并拷贝到mTextValue result =ConsumeQuotedString(aChar, mTextValue, mNewlineCount, aScanner, aFlag); if(NS_SUCCEEDED(result) && (aFlag &NS_IPARSER_FLAG_VIEW_SOURCE)) { //如果成功了且当前是view source模式 //将结束的单/双引号拷贝过来 mTextValue.writable().Append(aChar); } elseif (result == //如果结果是该属性没有结束的引号 NS_ERROR_HTMLPARSER_UNTERMINATEDSTRINGLITERAL) { result = NS_OK; //设置结果为成功 mInError = PR_TRUE; //同时标记一下,格式有错误 } //作者此处有一段注释: //根据定义。我们(我们是谁?)应当忽略换行的影响,但是即使是回车被消除了掉了(好奇怪为啥)-请查看bug 15204。好吧,根据定义中告诉我们的要忽略换行,但是bug 47535是怎么回事?我们是否应当保持他们全部的内容?好吧,我们按照这样来进行实现! } elseif (kGreaterThan == aChar) { //如果此时的字符是”>”符号 mHasEqualWithoutValue =PR_TRUE; //设置“有等号无配对“标示符为true mInError = PR_TRUE; //设置出错位为true } else{ //其他情况下 staticconst nsReadEndCondition //我们利用终结符设置终结条件 theAttributeTerminator(kAttributeTerminalChars); result = //读取直到遇到该终结条件位置 ConsumeUntil(mTextValue, mNewlineCount, aScanner, theAttributeTerminator, PR_FALSE, PR_TRUE, aFlag); } } if(NS_OK == result) { //如果结果为成功 if(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE) { //如果为view source模式 //获取所有的空白字符为mTextValue PRBool haveCR; result = aScanner.ReadWhitespace(mTextValue,mNewlineCount, haveCR); } else{ //其他的情况下跳过所有的空白字符 result =aScanner.SkipWhitespace(mNewlineCount); } } } else{ // 我们看到了”=”号,但是没有地方找到value值了 mHasEqualWithoutValue =PR_TRUE; mInError = PR_TRUE; } } } else { //这里就是我们处理最通常遇到的错误的地方了 //如果运行到这里,说明我们看到了一个属性名,但是无法找到接下来的“=“号。如<tag NAME… //在任何情况下都能够纠正这种错误是一件<i>非常</i>困难的事情。 //我最好的方法是获取接下来的非空白字符。我们知道它不是’=’,因此让我们来看看它是什么。如果它是一个‘”‘符,我们则假设此时正在从属性值的中央读取,我们将尝试慢慢向两侧将引号剥离并且继续…注意到这段代码同样会剥离斜杠”/”来处理诸如<tag NAME/>的情况。 if(kQuote == aChar || kApostrophe == aChar || kForwardSlash == aChar){ //在XML中,一个正斜杠不是错误 //如果下一个字符不为正斜杠, if(kForwardSlash != aChar || !(aFlag & NS_IPARSER_FLAG_XML)) { mInError = PR_TRUE; } if(!(aFlag & NS_IPARSER_FLAG_VIEW_SOURCE)) { //如果当前不是view source模式 result =aScanner.SkipOver(aChar); //去掉斜杠或者引号 if(NS_SUCCEEDED(result)) { //如果成功了 result =aScanner.SkipWhitespace(mNewlineCount); //跳过空白字符,并记录下跳过的行数 } } else{ //我们希望在这里收集空白字符,这样接下来的属性就可以有正确的行号了(并且为了和上面的非view source模式的代码相对应) result =ConsumeInvalidAttribute(aScanner, aChar, //调用前面的处理非法属性的方法 wsend, mNewlineCount); aScanner.BindSubstring(mTextKey,wsstart, wsend); //将处理好的文本(wsstart和wsend之间的文本)拷贝至mTextKey中 aScanner.SetPosition(wsend); //重设扫描器的当前位置为新位置,以便继续向下解析 } } } } } } if (NS_OK == result) { if (mTextValue.str().Length() == 0&& mTextKey.Length() == 0 && mNewlineCount == 0 && !mHasEqualWithoutValue) { //这个属性对我们一点用也没有,因此这里也没有必要对其进行保存。而那些虽然为空,但是包含有换行符的属性会被DTD所使用,以便可以得到正确的行号。 return NS_ERROR_HTMLPARSER_BADATTRIBUTE; //坏属性 } } } if (kEOF == result &&!aScanner.IsIncremental()) { //这是我们通常对“注意在文档结尾不要丢失内容”处理方法的一个略微改进版:我们不希望返回一个空的属性键值,即使这里已经是文档的末尾。 if (mTextKey.Length() == 0) { //如果键值为空 result = NS_ERROR_HTMLPARSER_BADATTRIBUTE; //坏属性 } else { //其他情况下 result = NS_OK; //返回成功 } } return result; } //下面是一些比较简短的小方法: //手动通过字符串设置属性的键值 void CAttributeToken::SetKey(const nsAString& aKey) { mTextKey.Rebind(aKey); } //手动通过扫描器中文档的位置来设置键值 void CAttributeToken::BindKey(nsScanner*aScanner, nsScannerIterator&aStart, nsScannerIterator&aEnd) { aScanner->BindSubstring(mTextKey, aStart, aEnd); } //空格符Token的无参构造方法 CWhitespaceToken::CWhitespaceToken() : CHTMLToken(eHTMLTag_whitespace) { } //通过字符串作为参数的构造方法 CWhitespaceToken::CWhitespaceToken(const nsAString& aName) : CHTMLToken(eHTMLTag_whitespace) { mTextValue.writable().Assign(aName); //将参数赋值给mTextValue } //获取Token的类型 PRInt32 CWhitespaceToken::GetTokenType() { return eToken_whitespace; } //下面这个方法的主要作用就是处理掉任意类型的空白字符 nsresult CWhitespaceToken::Consume(PRUnicharaChar, nsScanner& aScanner, PRInt32 aFlag) { //如果可能的话,我们希望仅仅是一个自aChar开始的独立字符串。扫描器已经前进了,因此我们需要将其放回原先的位置以易于处理。 nsScannerIterator start; aScanner.CurrentPosition(start); //记录下当前位置 aScanner.SetPosition(--start, PR_FALSE, PR_TRUE); //退回扫描器 PRBool haveCR; //是否有换行符 nsresult result = aScanner.ReadWhitespace(mTextValue, mNewlineCount,haveCR); //读取空白字符 if (result == kEOF &&!aScanner.IsIncremental()) { //如果到达了结尾,且当前文档已经加载完毕 //悲剧,我们已经到达了结尾,需要确保我们没有丢掉结尾的空白字符! result = NS_OK; } if (NS_OK == result && haveCR) { //如果发生了换行 mTextValue.writable().StripChar(kCR); //去掉所有的换行符 } return result; } //获取字符串值 constnsSubstring& CWhitespaceToken::GetStringValue() { return mTextValue.str(); } //下面来看一类非常重要的解析类,即Entity类,这代表了HTML中诸多的转义字符如 等 //EntityToken空参数的构造方法 CEntityToken::CEntityToken() : CHTMLToken(eHTMLTag_entity) { } //EntityToken带字符串参数的构造方法 CEntityToken::CEntityToken(const nsAString& aName) : CHTMLToken(eHTMLTag_entity) { mTextValue.Assign(aName); //将参数值赋值给mTextValue } //获取Token的类型 PRInt32 CEntityToken::GetTokenType() { return eToken_entity; } //下面我们来看一下对于实体类的解析方法: //这个方法的主要作用是当你想要处理类似于&XXXX的实体时所调用。请记住实体并<i>不是</i>缩小版的内嵌(inline)字符。 nsresult CEntityToken::ConsumeEntity(PRUnicharaChar, nsString&aString, nsScanner&aScanner) { nsresult result = NS_OK; if (kLeftBrace == aChar) { //说明你现在所处理的是一个脚本Entity… aScanner.GetChar(aChar); //处理 “&”字符 PRInt32 rightBraceCount = 0; //记录左括号’{‘的数量 PRInt32 leftBraceCount = 0; //记录右括号’}’的数量 do { //一个do-while循环,不断获取字符,直到左括号和右括号的数量相等为止,即形成{}的闭包 result = aScanner.GetChar(aChar); //获取下一个字符 if (NS_FAILED(result)) { //如果结果失败(一般是kEOF吧) return result; //直接返回 } aString.Append(aChar); //将该字符记录下来 if (aChar == kRightBrace) { ++rightBraceCount; //右括号加一 } else if(aChar == kLeftBrace) { ++leftBraceCount; //左括号加一 } } while (leftBraceCount !=rightBraceCount); } else { //不是SCRIPT Entity的情况下 PRUnichar theChar = 0; if (kHashsign == aChar) { //判断下一个字符是否是# result = aScanner.Peek(theChar, 2); //查看一下下下个字符是啥 if (NS_FAILED(result)) { //如果查看失败 if (kEOF == result &&!aScanner.IsIncremental()) { //如果结果是文件末尾且文档已经加载完毕 //此时说明我们遇到的绝对不是Entity。因为此时在&#后面已经没有字符了。Bug 188278。 return NS_HTMLTOKENS_NOT_AN_ENTITY; //返回信息“这不是一个Entity” } return result; } if (aChar == kSemicolon){ //获取到冒号’:’后就停止扫描 aString.Append(aChar); //将其附加到字符串结尾 result = aScanner.GetChar(aChar); //通过扫描器获取字符(其实就是扫描器前进一位) } return result; } //接下来作者很有意思地将一些常用被人在Entity中用错的字符代码列了出来,并且将他们映射到了正确的结果中去,也就是说即使你使用了错误的字符,但是属于下面一种错误,Firefox也会自动替你修正过来。 #defineNOT_USED 0xfffd static const PRUint16 PA_HackTable[] = { 0x20ac, /* EURO SIGN */ NOT_USED, 0x201a, /* SINGLE LOW-9QUOTATION MARK */ 0x0192, /* LATIN SMALLLETTER F WITH HOOK */ 0x201e, /* DOUBLE LOW-9QUOTATION MARK */ 0x2026, /* HORIZONTALELLIPSIS */ 0x2020, /* DAGGER */ 0x2021, /* DOUBLE DAGGER */ 0x02c6, /* MODIFIER LETTERCIRCUMFLEX ACCENT */ 0x2030, /* PER MILLE SIGN*/ 0x0160, /* LATIN CAPITALLETTER S WITH CARON */ 0x2039, /* SINGLELEFT-POINTING ANGLE QUOTATION MARK */ 0x0152, /* LATIN CAPITALLIGATURE OE */ NOT_USED, 0x017D, /* LATIN CAPITALLETTER Z WITH CARON */ NOT_USED, NOT_USED, 0x2018, /* LEFT SINGLEQUOTATION MARK */ 0x2019, /* RIGHT SINGLEQUOTATION MARK */ 0x201c, /* LEFT DOUBLEQUOTATION MARK */ 0x201d, /* RIGHT DOUBLEQUOTATION MARK */ 0x2022, /* BULLET */ 0x2013, /* EN DASH */ 0x2014, /* EM DASH */ 0x02dc, /* SMALL TILDE */ 0x2122, /* TRADE MARK SIGN*/ 0x0161, /* LATIN SMALLLETTER S WITH CARON */ 0x203a, /* SINGLERIGHT-POINTING ANGLE QUOTATION MARK */ 0x0153, /* LATIN SMALL LIGATUREOE */ NOT_USED, 0x017E, /* LATIN SMALLLETTER Z WITH CARON */ 0x0178 /* LATIN CAPITALLETTER Y WITH DIAERESIS */ }; //下面是一些小方法: //为字符串添加字符,并且对添加的字符进行一些校验和替换(利用上面的Table) static void AppendNCR(nsSubstring& aString,PRInt32 aNCRValue) { /* For some illegal, but popular usage */ if (aNCRValue >= 0x0080 && aNCRValue <=0x009f) { aNCRValue = PA_HackTable[aNCRValue - 0x0080]; } AppendUCS4ToUTF16(ENSURE_VALID_CHAR(aNCRValue), aString); } //这个方法将entity转换成等价的Unicode字符串,并且赋值给aString RInt32 CEntityToken::TranslateToUnicodeStr(nsString&aString) { PRInt32 value = 0; if (mTextValue.Length() > 1) { //长度不能小于2 PRUnichar theChar0 = mTextValue.CharAt(0); if (kHashsign == theChar0) { //首先判断是否由”#”开头 PRInt32 err = 0; value = mTextValue.ToInteger(&err, kAutoDetect); //将mTextValue转换为PRInt32数字类型 if (0 == err) { AppendNCR(aString, value); //如果没有出错,则将value附加到aString后面 } } else { value = nsHTMLEntities::EntityToUnicode(mTextValue); //获取mTextValue的数字值 if (-1 < value) { //如果value大于-1 //我们找到了一个已命名的Entity aString.Assign(PRUnichar(value));//将该Entity转义后的字符拷贝至aString } } } return value; } //获取Entity的字符串值 const nsSubstring&CEntityToken::GetStringValue() { return mTextValue; } //获取Entity的原始字符,赋值到anOutputString中去(其实就是加上&后的mTextValue) void CEntityToken::GetSource(nsString&anOutputString) { anOutputString.AppendLiteral("&"); anOutputString += mTextValue; //任何可能的冒号 : 都是我们文本值的一部分 } //同上,参数不同 void CEntityToken::AppendSourceTo(nsAString&anOutputString) { anOutputString.AppendLiteral("&"); anOutputString += mTextValue; //任何可能的冒号 : 都是我们文本值的一部分 } //通过一个整形PRInt32的aTag以获取Tag名 constPRUnichar* GetTagName(PRInt32 aTag) { const PRUnichar *result =nsHTMLTags::GetStringValue((nsHTMLTag) aTag); //就是这句去获取,具体请参考nsHTMLTags.h,以及HTMLTags的列表 if (result) { return result; } if (aTag >= eHTMLTag_userdefined) { //如果超出了类型定义的范围 return sUserdefined; //我们都将其作为用户定义的tag类型进行处理 } return 0; } //Instruction类型Token的无参构造方法 CInstructionToken::CInstructionToken() : CHTMLToken(eHTMLTag_instruction) { } //Instruction类型Token的带字符参数的构造方法 CInstructionToken::CInstructionToken(const nsAString& aString) : CHTMLToken(eHTMLTag_unknown) { mTextValue.Assign(aString); } //获取字符串的值 constnsSubstring& CInstructionToken::GetStringValue() { return mTextValue; } //下面是DoctypeDeclaration类型的Token //空参数构造方法 CDoctypeDeclToken::CDoctypeDeclToken(eHTMLTagsaTag) : CHTMLToken(aTag) { } //带两个参数(字符串,Tag类型)的构造方法,只是赋了两个初值 CDoctypeDeclToken::CDoctypeDeclToken(const nsAString& aString, eHTMLTags aTag) : CHTMLToken(aTag), mTextValue(aString) { } //下面这个方法将会处理一个doctype类型的元素 //注意:作者重写了这个方法来寻找第一个<,因为引号有的时候会导致很多错误。 //未来展望:也许这个方法会在XML或者strict模式下的效果更好? nsresult CDoctypeDeclToken::Consume(PRUnicharaChar, nsScanner& aScanner, PRInt32 aFlag) { static constPRUnichar terminalChars[] = //设置扫描器用的终结字符组 { PRUnichar('>'), PRUnichar('<'), PRUnichar(0) }; static constnsReadEndCondition theEndCondition(terminalChars); //利用终结字符组设立终结条件 nsScannerIterator start, end; aScanner.CurrentPosition(start); //获取扫描器的当前位置 aScanner.EndReading(end); //获取扫描器的结束位置 nsresult result = aScanner.ReadUntil(start, end, theEndCondition,PR_FALSE); //不断读取,直到遇到终结条件中的字符为止 if (NS_SUCCEEDED(result)){ //如果结果为成功 PRUnichar ch; aScanner.Peek(ch); //查看一下下一个字符 if (ch == kGreaterThan) { //如果下一个字符是”>” //将’>’包含进来,但不要包含’<’符号 //因为’<’可能属于另一个tag aScanner.GetChar(ch); //获取该字符 end.advance(1); //同时将end前进一位 } else { NS_ASSERTION(kLessThan == ch, "Makesure this doctype decl. is really in error."); //如果是‘<‘则判定其出错了 mInError = PR_TRUE; } } else if(!aScanner.IsIncremental()) { //如果读取失败了,且当前的文档已经读取完毕 //此时我们已经到达了文档的末尾,但是我们仍没有遇到一个’<’或者一个’>’。因此使用我们所有的一切作为内容。 mInError = PR_TRUE; //设置出错位 result = NS_OK; } if (NS_SUCCEEDED(result)) { //如果结果为成功 start.advance(-2); //将start后退两位,确保将’<!’包含进去 CopyUnicodeTo(start, end, mTextValue); //将start和end之间的内容拷贝到mTextValue中去 } return result; } //获取Token类型 PRInt32 CDoctypeDeclToken::GetTokenType() { return eToken_doctypeDecl; } //获取Token的字符串值 constnsSubstring& CDoctypeDeclToken::GetStringValue() { return mTextValue; } //设置该Token的mTextValue值 void CDoctypeDeclToken::SetStringValue(const nsAString& aStr) { mTextValue.Assign(aStr); }
后续:Mozilla Firefox Gecko内核源代码解析(5.CNavDTD)