手搓一个HTML解释器(HTMLParser )
Author:zhoulujun Date:
js如何解释html文本?
使用 DOM 解析 HTML,可是使用原生的DOMParser,具体参看 https://developer.mozilla.org/zh-CN/docs/Web/API/DOMParser
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.example.com');
xhr.onload = function() {
if (xhr.status === 200) {
const doc = new DOMParser().parseFromString(xhr.responseText, 'text/html');
const title = doc.getElementsByTagName('title')[0];
console.log(title.textContent);
} else {
console.error('Error loading HTML document');
}
};
xhr.send();
实际上,我们一般使用第三方库
流行的 HTML 解析器库包括:
Cheerio:https://github.com/cheeriojs
htmlparser2:https://github.com/fb55/htmlparser2
当然,我们可以使用 innerHTML 、outerHTML、insertAdjacentHTML HTML 字符串插入到已存在的 DOM 元素中。
const element = document.getElementById('someElement');
element.innerHTML = '<p>Hello, World!</p>';
element.outerHTML = '<div class="newClass">New content</div>';
element.insertAdjacentHTML('beforeend', '<p>New paragraph</p>');
这些,无法防止 XSS攻击,为了防止 XSS 攻击,你需要在将解析后的 DOM 插入到页面之前对其进行净化。这通常涉及到以下步骤:
实体编码:将任何来自用户输入的文本转换为 HTML 实体,以防止它们被解析为 HTML 或 JavaScript。
属性净化:移除或限制 HTML 标签和属性,特别是那些可能引起安全问题的,比如 on* 事件处理器或 src 属性。
脚本移除:确保 <script> 和 <iframe> 等标签不会被执行。
有许多库可以帮助你完成这些任务,例如:
DOMPurify:这是一个专门设计用于清理 HTML,防止 XSS 攻击的库。
JS Html Sanitizer:另一个用于防止 XSS 的 HTML 清洗库。
如果我们手工是如何实现一个html parser 呢?
手搓HTML解析器
实现 html parser 主要分为词法分析和语法分析两步。
词法分析
词法分析需要把每一种类型的 token 识别出来,具体的类型有:
开始标签,如 <div>
结束标签,如 </div>
注释标签,如 <!--comment-->
doctype 标签,如 <!doctype html>
text,如 aaa
这是最外层的 token,开始标签内部还要分出属性,如 id="aaa" 这种。
也就是有这几种情况:
第一层判断是否包含 <,如果不包含则是 text,如果包含则再判断是哪一种,如果是开始标签,还要对其内容再取属性,直到遇到 > 就重新判断。
语法分析
法分析就是对上面分出的 token 进行组装,生成 ast。
html 的 ast 的组装主要是考虑父子关系,记录当前的 parent,然后 text、children 都设置到当前 parent 上。
实现分析
首先我想到的是正则表达式
const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/;
const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/;
const commentReg = /^<!\-\-[^(-->)]*\-\->/;
const docTypeReg = /^<!doctype [^>]+>/;
const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^>]+))/;
实现:https://github.com/QuarkGluonPlasma/tiny-browser/blob/master/src/htmlParser.js
const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/;
const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^> ]+))/;
const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/;
const commentReg = /^<!\-\-[^(-->)]*\-\->/;
const docTypeReg = /^<!doctype [^>]+>/;
function parse(html, options) {
function advance(num) {
html = html.slice(num);
}
while(html){
if(html.startsWith('<')) {
const commentMatch = html.match(commentReg);
if (commentMatch) {
options.onComment({
type: 'comment',
value: commentMatch[0]
})
advance(commentMatch[0].length);
continue;
}
const docTypeMatch = html.match(docTypeReg);
if (docTypeMatch) {
options.onDoctype({
type: 'docType',
value: docTypeMatch[0]
});
advance(docTypeMatch[0].length);
continue;
}
const endTagMatch = html.match(endTagReg);
if (endTagMatch) {
options.onEndTag({
type: 'tagEnd',
value: endTagMatch[1]
});
advance(endTagMatch[0].length);
continue;
}
const startTagMatch = html.match(startTagReg);
if(startTagMatch) {
options.onStartTag({
type: 'tagStart',
value: startTagMatch[1]
});
advance(startTagMatch[1].length + 1);
let attributeMath;
while(attributeMath = html.match(attributeReg)) {
options.onAttribute({
type: 'attribute',
value: attributeMath[1]
});
advance(attributeMath[0].length);
}
advance(1);
continue;
}
} else {
let textEndIndex = html.indexOf('<');
options.onText({
type: 'text',
value: html.slice(0, textEndIndex)
});
textEndIndex = textEndIndex === -1 ? html.length: textEndIndex;
advance(textEndIndex);
}
}
}
module.exports = function htmlParser(str) {
const ast = {
children: []
};
let curParent = ast;
let prevParent = null;
const domTree = parse(str,{
onComment(node) {
},
onStartTag(token) {
const tag = {
tagName: token.value,
attributes: [],
text: '',
children: []
};
curParent.children.push(tag);
prevParent = curParent;
curParent = tag;
},
onAttribute(token) {
const [ name, value ] = token.value.split('=');
curParent.attributes.push({
name,
value: value.replace(/^['"]/, '').replace(/['"]$/, '')
});
},
onEndTag(token) {
curParent = prevParent;
},
onDoctype(token) {
},
onText(token) {
curParent.text = token.value;
}
});
return ast.children[0];
}
如果不用正则表达式呢?
class Node {
constructor(type, content) {
this.type = type;
this.content = content;
}
}
class ElementNode extends Node {
constructor(tag, attributes, children) {
super('element');
this.tag = tag;
this.attributes = attributes;
this.children = children;
}
}
class HTMLParser {
constructor() {
this.pos = 0;
this.tokens = [];
}
parse(html) {
this.pos = 0;
this.tokens = [];
while (this.pos < html.length) {
if (html[this.pos] === '<') {
this.parseTag(html);
} else {
this.parseText(html);
}
}
// Build DOM tree from tokens
const rootNode = this.buildDOMTree();
return rootNode;
}
parseTag(html) {
let start = this.pos;
this.pos++; // Skip '<'
let tagType;
if (html[this.pos] === '/') {
this.pos++; // Skip '/'
tagType = 'end_tag';
} else {
tagType = 'start_tag';
}
let tagName = '';
while (this.pos < html.length && html[this.pos].match(/[a-zA-Z0-9]/)) {
tagName += html[this.pos];
this.pos++;
}
let attributes = [];
while (this.pos < html.length && html[this.pos] !== '>') {
let { attrName, attrValue } = this.parseAttribute(html);
if (attrName) {
attributes.push({ name: attrName, value: attrValue });
}
}
this.pos++; // Skip '>'
this.tokens.push({
type: tagType,
tag: tagName,
attributes: attributes,
});
}
parseAttribute(html) {
let attrName = '';
let attrValue = '';
while (this.pos < html.length && html[this.pos].match(/\s/)) {
this.pos++;
}
if (this.pos < html.length && html[this.pos] !== '>') {
while (this.pos < html.length && html[this.pos].match(/[a-zA-Z0-9]/)) {
attrName += html[this.pos];
this.pos++;
}
while (this.pos < html.length && html[this.pos] !== '=') {
this.pos++;
}
if (this.pos < html.length && html[this.pos] === '=') {
this.pos++; // Skip '='
let quoteChar = html[this.pos];
this.pos++; // Skip quote character
while (this.pos < html.length && html[this.pos] !== quoteChar) {
attrValue += html[this.pos];
this.pos++;
}
this.pos++; // Skip closing quote
}
}
return { attrName, attrValue };
}
parseText(html) {
let start = this.pos;
while (this.pos < html.length && html[this.pos] !== '<') {
this.pos++;
}
let text = html.substring(start, this.pos);
this.tokens.push({
type: 'text',
content: text,
});
}
buildDOMTree() {
let stack = [];
let rootNode = null;
for (let token of this.tokens) {
if (token.type === 'start_tag') {
let elementNode = new ElementNode(token.tag, token.attributes, []);
if (stack.length > 0) {
let parent = stack[stack.length - 1];
parent.children.push(elementNode);
} else {
rootNode = elementNode;
}
stack.push(elementNode);
} else if (token.type === 'end_tag') {
stack.pop();
} else if (token.type === 'text') {
if (stack.length > 0) {
let parent = stack[stack.length - 1];
parent.children.push(new Node('text', token.content));
}
}
}
if (rootNode === null && stack.length > 0) {
rootNode = stack[0];
}
return rootNode;
}
}
参考文章:
人问我能不能写一个 HTML Parser? https://cloud.tencent.com/developer/article/1842858
转载本站文章《手搓一个HTML解释器(HTMLParser )》,
请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/htmlBase/2024_0715_9170.html