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