Mozilla FireFox Gecko内核源代码解析
(1.nsTokenizer)
中科院计算技术研究所网络数据科学与工程研究中心
信息抽取小组
耿耘
前面我们大体介绍了nsParser的主控流程(nsParser.cpp),可知HTML解析一般分为两个阶段,即文法阶段的分词操作,和语法阶段的解析操作,前者一般来讲就是将HTML的标签分开,分成一个个的Token,而在Mozilla Firefox中,这个工作的主要流程是由nsHTMLTokenizer(分词器)控制下的nsHTMLToken来完成的。nsHTMLTokenizer负责响应nsParser传输过来的分析请求,并调用相应的nsHTMLToken,具体的词法,属性分析等都是放在后者nsHTMLTokens中完成的。其中还要用到对字符串进行流式扫描和读取进行支持的nsScanner。
值得注意的是nsHTMLTokenizer继承自nsITokenizer接口,实际上Mozilla还针对将来可能出现的其他XML格式文档进行了接口的制定,也许是说如过HTML5,6,7出来后,我们依然可以复用一部分接口来制定新的Tokenizer。
而目前我们主要使用的就是HTMLTokenizer,它主要针对各种HTML的标签进行解析,它将HTML的标签划分为了13类(实际上没这么多),这在一个叫做eHTMLTokenTypes的枚举类型中进行了定义,我们在之后的nsHTMLToken分析中会详细进行解析,这里我们为了助于理解Tokenizer的工作原理,先来看一下这个类型的集合:
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...
};
可以看到,其中eToken_last,eToken_unknow等是为了进行一些其他处理而存在的。其他的就是我们Mozilla中队常用的HTML标签的类型进行的分类。
观察头文件可以看出,它主要的方法是以ConsumeXXX()的样式来命名的方法,并可以看出,所有方法的参数中都要添加nsScanner类型的参数,这其实潜在地表示,Tokenizer和Scanner不必一对一配套使用。
它还提供了一个很重要的ScanDocStructure()方法,通过一个栈来对文档中所有Tokens的良构性进行一个判断,即基本的文法正确性检查。
首先,我们来看它的头文件:
#ifndef__NSHTMLTOKENIZER
#define__NSHTMLTOKENIZER
#include "nsISupports.h"
#include "nsITokenizer.h"
#include "nsIDTD.h"
#include "prtypes.h"
#include "nsDeque.h"
#include "nsScanner.h"
#include "nsHTMLTokens.h"
#include "nsDTDUtils.h"
/***************************************************************
Notes:
***************************************************************/
#ifdef_MSC_VER
#pragma warning( disable :4275 )
#endif
classnsHTMLTokenizer : public nsITokenizer {
public:
NS_DECL_ISUPPORTS //之前构件代码中#Define的方法
NS_DECL_NSITOKENIZER
nsHTMLTokenizer(nsDTDMode aParseMode = eDTDMode_quirks, //构造方法
eParserDocType aDocType =eHTML_Quirks,
eParserCommands aCommand =eViewNormal,
PRUint32 aFlags = 0);
virtual ~nsHTMLTokenizer(); //析构方法
static PRUint32 GetFlags(constnsIContentSink* aSink); //获取本Tokenizer的标示位
protected:
//下面的方法都是针对不同的HTMl词条类型来进行解析处理的
nsresult ConsumeTag(PRUnichar aChar,CToken*& aToken,nsScanner&aScanner,PRBool& aFlushTokens);
nsresult ConsumeStartTag(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner,PRBool& aFlushTokens);
nsresult ConsumeEndTag(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
nsresult ConsumeAttributes(PRUnichar aChar, CToken* aToken,nsScanner& aScanner);
nsresult ConsumeEntity(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
nsresult ConsumeWhitespace(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
nsresult ConsumeComment(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
nsresult ConsumeNewline(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
nsresult ConsumeText(CToken*& aToken,nsScanner& aScanner);
nsresult ConsumeSpecialMarkup(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
nsresult ConsumeProcessingInstruction(PRUnichar aChar,CToken*&aToken,nsScanner& aScanner);
//这个方法是对当前词条队列中所有词条进行良构分析的方法
nsresult ScanDocStructure(PRBool aIsFinalChunk);
//添加新的Token到队列中去
static void AddToken(CToken*& aToken,nsresultaResult,nsDeque* aDeque,nsTokenAllocator* aTokenAllocator);
nsDeque mTokenDeque; //存放Token的队列
PRPackedBool mIsFinalChunk; //标注是否是最后一个数据块
nsTokenAllocator* mTokenAllocator; //这个是用来分配Token的Allocator,在Mozilla中,为了节省内存资源,对于Token我们都是通过TokenAllocator进行分配的,这个我们在相应代码的解析之中会进行分析的
// This variable saves the position of the last tag weinspected in
// ScanDocStructure. We start scanning the generalwell-formedness of the
// document starting at this position each time.
//下面这个变量记录了我们在ScanDocStructure中所处理到的最后一个tag的位置。我们每次对文档进行良构性扫描的时候都会从这个位置开始。
PRInt32 mTokenScanPos;
//下面这个变量是用来记录分词器状态的标示位
PRUint32 mFlags;
};
#endif
以上就是nsHTMLTokenizer的头文件,下面我们就来看其cpp文件的真正实现部分。
这主要是对nsITokenizer接口的实现。这个文件包含了一个对HTML文档进行分词的分词器的实现。它尝试在对老版本的解析器的兼容性和对SGML标准的支持之上进行这些工作。注意到,大部分真正的“分词”过程是在nsHTMLTokens.cpp中进行的。
#include "nsIAtom.h"
#include "nsHTMLTokenizer.h"
#include "nsScanner.h"
#include "nsElementTable.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
/************************************************************************
And now for the main class --nsHTMLTokenizer...
************************************************************************/
/**
* Satisfy the nsISupports interface.
*/
//下面这个主要是为了实现nsISupports接口,具体实现在之前的#Define中实现了
NS_IMPL_ISUPPORTS1(nsHTMLTokenizer,nsITokenizer)
//下面是nsHTMLTokenizer的默认构造方法:
/**
* Default constructor
*
* @param aParseMode The current mode the document is in (quirks, etc.)
* @param aDocType Thedocument type of the current document
* @param aCommand Whatwe are trying to do (view-source, parse a fragment, etc.)
*/
nsHTMLTokenizer::nsHTMLTokenizer(nsDTDModeaParseMode,
eParserDocTypeaDocType,
eParserCommands aCommand,
PRUint32aFlags)
: mTokenDeque(0), mFlags(aFlags)
//构造方法,初始化两个变量,清空Token存放队列,并用aFlags设置Tokenizer的状态位
{
//首先,要根据aParseMode来设置mFlags
if (aParseMode == eDTDMode_full_standards ||
aParseMode == eDTDMode_almost_standards) {
mFlags |= NS_IPARSER_FLAG_STRICT_MODE;
} else if(aParseMode == eDTDMode_quirks) {
mFlags |= NS_IPARSER_FLAG_QUIRKS_MODE;
} else if(aParseMode == eDTDMode_autodetect) {
mFlags |= NS_IPARSER_FLAG_AUTO_DETECT_MODE;
} else {
mFlags |= NS_IPARSER_FLAG_UNKNOWN_MODE;
}
//之后还要根据aDocType来对mFlags进行设置
if (aDocType == ePlainText) {
mFlags |= NS_IPARSER_FLAG_PLAIN_TEXT;
} else if(aDocType == eXML) {
mFlags |= NS_IPARSER_FLAG_XML;
} else if(aDocType == eHTML_Quirks ||
aDocType == eHTML_Strict) {
mFlags |= NS_IPARSER_FLAG_HTML;
}
//根据aCommand来设置mFlag标示位是VIEW_SOURCE或VIEW_NORMAL
mFlags |= aCommand == eViewSource
? NS_IPARSER_FLAG_VIEW_SOURCE
: NS_IPARSER_FLAG_VIEW_NORMAL;
//判断,不能为XML模式,而且必须为VIEW_SOURCE模式?
NS_ASSERTION(!(mFlags & NS_IPARSER_FLAG_XML) ||
(mFlags &NS_IPARSER_FLAG_VIEW_SOURCE),
"Whyisn't this XML document going through our XML parser?");
//初始化,清空另两个数据成员变量
mTokenAllocator = nsnull;
mTokenScanPos = 0;
}
//下面是nsHTMLTokenizer默认的析构方法,注意到里面需要用到一个叫做ArenaPool的内存分配机制,这个机制是Mozilla中推出的一种内存分配机制,具体的方法我们在其他的代码解析文档中会说,有兴趣的读者也可以自己去看一下。就是为了尽可能减少内存碎片而设计的一种机制,FireFox的JSEngine即SpiderMonkey中也用到了这个机制。
/**
* The destructor ensures that we don't leakany left over tokens.
*/
nsHTMLTokenizer::~nsHTMLTokenizer()
{
if (mTokenDeque.GetSize()) { //如果当前的Token队列存在
CTokenDeallocator theDeallocator(mTokenAllocator->GetArenaPool()); //获取对应的Deallocator
mTokenDeque.ForEach(theDeallocator); //对每个mTokenDeque里的Token运行theDeallocator
}
}
//获取nsHTMLTokenizer的mFlag标示位。
/*static*/PRUint32
nsHTMLTokenizer::GetFlags(const nsIContentSink* aSink)
{
PRUint32 flags = 0;
nsCOMPtr<nsIHTMLContentSink> sink = //这种构建方法需要了解
do_QueryInterface(const_cast<nsIContentSink*>(aSink));
if (sink) { //如果获取Sink成功
PRBool enabled = PR_TRUE; //申请一个BOOL变量enabled,默认为为TRUE
sink->IsEnabled(eHTMLTag_frameset, &enabled); //获取sink是否启用了Tag_frameset的标示
if (enabled) { //如果启用了
flags |= NS_IPARSER_FLAG_FRAMES_ENABLED; //设置相应的标示位
}
sink->IsEnabled(eHTMLTag_script, &enabled); //获取sink是否启用了Tag_sript的标示
if (enabled) { //如果启用了
flags |= NS_IPARSER_FLAG_SCRIPT_ENABLED; //设置相应的标示位
}
}
return flags;
}
//上面一些方法都是对分词过程进行支持的,下面我们来看看真正的分词方法。
/*******************************************************************
Here begins the real working methods for thetokenizer.
*******************************************************************/
/**
* Adds a token onto the end of the deque ifaResult is a successful result.
* Otherwise, this function frees aToken andsets it to nsnull.
*
* @param aToken The token that wants to beadded.
* @param aResult The error code that will beused to determine if we actually
* want to push this token.
* @param aDeque The deque we want to pushaToken onto.
* @param aTokenAllocator The allocator we useto free aToken in case aResult
* is not a success code.
*/
/* static */
//AddToken顾名思义,就是添加一个新的Token到存放Tokens的队列尾部。其他情况下,即如果不成功的话(aResult不为TRUE),则我们会释放aToken并将其设置为nsnull。
void
nsHTMLTokenizer::AddToken(CToken*& aToken,
nsresult aResult,
nsDeque* aDeque,
nsTokenAllocator*aTokenAllocator)
{
if (aToken && aDeque) {
if (NS_SUCCEEDED(aResult)) { //首先判断aResult是否成功
aDeque->Push(aToken); //将aToken推入队列
} else { //其他情况下,即aResult不成功
IF_FREE(aToken, aTokenAllocator); //释放aToken
}
}
}
//以上方法和接下来的几个方法需要注意到的是,aToken是存放在一中叫做nsDeque的队列型数据结构中的,因此其会提供相应的push(),peek()方法等,具体的可以去看具体的数据结构,我们这里只需要调用该数据结构提供的方法即可。
/**
* Retrieve a pointer to the global tokenrecycler...
*
* @return Pointer to recycler (or null)
*/
nsTokenAllocator* //获取全局的token回收器
nsHTMLTokenizer::GetTokenAllocator()
{
return mTokenAllocator; //返回mTokenAllocator
}
//查看队列头部Token的PeekToken方法
/**
* This method provides access to the topmosttoken in the tokenDeque.
* The token is not really removed from thelist.
*
* @return Pointer to token
*/
CToken*
nsHTMLTokenizer::PeekToken()
{
return (CToken*)mTokenDeque.PeekFront(); //查看队列头部的Token,该Token不会出队
}
//获取队列头部Token,并将其出队的PopToken方法
/**
* This method provides access to the topmosttoken in the tokenDeque.
* The token is really removed from the list;if the list is empty we return 0.
*
* @return Pointer to token or NULL
*/
CToken*
nsHTMLTokenizer::PopToken()
{
return (CToken*)mTokenDeque.PopFront(); //直接获取头部Token,如果是空的队列,则会返回0
}
//将Token压入到队列的头部,并且返回这个Token(我个人感觉应当返回压入操作的成功与否)
/**
* Pushes a token onto the front of our dequesuch that the next call to
* PopToken() or PeekToken() will return thattoken.
*
* @param theToken The next token to beprocessed
* @return theToken
*/
CToken*
nsHTMLTokenizer::PushTokenFront(CToken*theToken)
{
mTokenDeque.PushFront(theToken); //压入操作
return theToken; //返回该Token
}
//将Token压入队列的尾部,并返回相应的Token(操作结果就不判断了么?)
/**
* Pushes a token onto the front of our dequesuch that the next call to
* PopToken() or PeekToken() will return thattoken.
*
* @param theToken The next token to beprocessed
* @return theToken
*/
CToken*
nsHTMLTokenizer::PushTokenFront(CToken*theToken)
{
mTokenDeque.PushFront(theToken); //压入操作
return theToken; //返回该Token