HTML简单解析器:token的生成

638 阅读2分钟

HTMl解析

此文,我们目的是要实现一个简单的的HTML解析器。
我们先梳理一下整个解析的过程。
HTML字节流--->解码器解码---->HTML字符流---->分词(通过各种状态机)---->token (最小的有意义的单元)---->栈(分析层级和所属关系)------>构建出DOM树;

  1. 解析代码
    把HTML字符流解析成一个个token,一个需要指定分词规则,这一块要依据状态机。然后就是要定义token种类,没有种类,也就区分不出差别,那这样的词也就没有意义。
    token的种类不多,有DOCTYPE,开始标签(EtartTag),结束标签(EndTag),元素内容(Character),注释(Commend),文档结束(EndOfFile)几种。

那什么是状态机呢?
状态机的功能简单来说就是输入一个字符流,输出的是一个个词语。
HTML规范大致有好几十个状态机。参考HTML官方文档;

这里笔者理解,状态机是在整个字符逐个扫描过程中,一个个的判断条件,最后通过一个个条件判断,确定具体类型。

比如:

<div class='node'> node</div>

“<div” 是开始标签的开始;
”class='node' “是元素内容的属性节点;
“ >”是开始标签的结束;
“node" 是元素内容的文本节点;
""是结束标签;

具体拆分token方法如下:

const str=`<HTML meta="test" >
    <head>
        <title> 
		test</title>
    </head>
    <body>
        <img src="..." />
		<div class="cls">
					parseH  tml
		</div>
    </body>
</HTML>`;
/**
简单的html解析器,
主要处理开始标签<** >,结束标签</ **>,自闭标签<**  />
*/
function parseHtml(str){
	let tokenStack=[];
	
	let token={}
	let  state = data;
	this.str=str;

	this.start=function(){
		for (let char of this.str) {
			state = state(char);
		}
		return tokenStack;
	}

	function data(c){
		if(c===null){
			throw Error('not ')
		}
		if(c==='<'){
			return tagOpen;
		}
		if(c.match(/\s/)){
			return data;
		}
		if(c.match(/[a-zA-Z0-9\s+-=)]/)){
			token={};
			token.type='text';
			token.name=c;
			return textName			
		}
		return data;
	}

	function tagOpen(c){
		if(c==='/'){
			return endTagOpen;
		}
		if(c.match(/[a-zA-Z]/)){
			token = {}; 
			token.type = 'startTag';
			token.name = c.toLowerCase();
			 return tagNameState;
		}
		throw new Error('tagOpen未匹配');

	}

	function tagNameState(c){

		if(c.match(/[a-zA-Z]/)){
			token.name =token.name+ c.toLowerCase();
			 return tagNameState;
		}
		if(c.match(/\s/)){
			emitToken(token);
			return beforeAttributeName;
		}
		if(c==='/'){
			emitToken(token);
			return endTagOpen;
		}
		if(c==='>'){
			emitToken(token);
			return data;
		}
		throw new Error('tagNameState未匹配');

	}

	function beforeAttributeName(c){
		if(c==='/'){
			return endTagOpen;
		}
		if(c==='>'){
			return data;
		}
		if(c.match(/\S/)){
			token={};
			token.type='attr';
			token.name =c;
			return attrNameState;
		}
		if(c.match(/\s/)){
			return beforeAttributeName;
		}

		throw new Error('beforeAttributeName未匹配');
	}

	function textName(c){
		if(c.match(/\n|\t/)){
			return textName;
		}
		if(c.match(/\w|\s/)){
			token.name =token.name+ c;
			return textName;
		}
		if(c==='<'){
			emitToken(token);
			return tagOpen;
		}
		console.log(c)
		throw new Error('textName未匹配');
	}
	function attrNameState(c){
		// todo 匹配字符可以丰富
		if(c.match(/[a-zA-Z0-9-=_"'.]/)){
			token.name =token.name+ c;
			 return attrNameState;
		}
		if(c.match(/\s/)){
			emitToken(token);
			token={};
			token.type='attr';
			token.name =c;
			return beforeAttributeName;
		}
		if(c==='/'){
			emitToken(token);
			return endTagOpen;
		}
		if(c==='>'){
			emitToken(token);
			return data;
		}
		throw new Error('attrNameState未匹配');
	}

	function endTagOpen(c){
		if(c.match(/[a-zA-Z]/)){
			token={};
			token.type='endTag';
			token.name =c.toLowerCase();
			 return endTagName;
		}
		if(c==='>'){
			token={};
			token.type='endTag';
			emitToken(token);
			return data;
		}
		throw new Error('endTagOpen未匹配');
	}

	function endTagName(c){
		if(c.match(/[a-zA-Z]/)){
			token.name =token.name+c.toLowerCase();
			 return endTagName;
		}
		if(c==='>'){
			emitToken(token);
			return data;
		}
		throw new Error('endTagName未匹配');
	}

	function emitToken(t){
		tokenStack.push(t);
		token={};
	}
}



const parse=new parseHtml(str);
const tokenStack=parse.start(parse.str);
console.log(tokenStack)

获取到了token栈之后,根据token栈再来生成ast树。
原来很简单,观察规律,下一次更新。
------todo------ 直接上代码


// 依据的type类型有:startTag,attr,text,endTag
function getAst(tokenList){
	let ast={};
	let cacheNode=[];
	const len=tokenList.length;

	let count=0;
	while(count<len){
		const node =tokenList[count];

		if(node.type==='startTag'){
			let newNode={
				name:node.name
			}
			cacheNode.push(newNode);
		}
		if(node.type==='endTag'){
			const currentNode=cacheNode.pop();

			const lastNode=cacheNode[cacheNode.length-1];

			if(lastNode){
				if(lastNode.children){
					lastNode.children.push(currentNode);
				}else{
					lastNode.children=[];
					lastNode.children.push(currentNode);
				}

			}else{
				ast.root=currentNode;
				// 结束
				return JSON.stringify(ast);
			}
		}
		if(node.type==='attr'){
			const currentNode=cacheNode[cacheNode.length-1];
			let attrValue={};
			const nodeAttr=node.name.split('=');
			attrValue.name=nodeAttr[0];
			attrValue.value=nodeAttr[1]||true;
			if(currentNode.attr){
				currentNode.attr.push(attrValue);
			}else{
				currentNode.attr=[];
				currentNode.attr.push(attrValue);

			}
		}
		if(node.type==='text'){
			const currentNode=cacheNode[cacheNode.length-1];
			currentNode.text=node.name;
		}
		count++;
	}

}