2024-09-18 15:08:22 +08:00

168 lines
6.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// HTML模板字符串
const template = `<div><p>Vue</p><p>React</p></div>`;
// 定义不同的解析状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始的状态,例如遇到 '<'
tagName: 3, // 解析标签名称的状态
text: 4, // 文本节点的状态
tagEnd: 5, // 结束标签的初始状态,例如遇到 '</'
tagEndName: 6, // 解析结束标签名称的状态
};
// 判断字符是否为字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
function tokenize(str) {
let currentState = State.initial; // 初始状态
const chars = []; // 用于存储字符
const tokens = []; // 存储生成的令牌
// 循环处理字符串中的每个字符
while (str) {
const char = str[0]; // 获取当前字符
switch (currentState) {
case State.initial:
// 初始状态:检查当前字符
if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示标签开始
str = str.slice(1); // 移除已处理的字符
} else if (isAlpha(char)) {
currentState = State.text; // 如果是字母,表示文本开始
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagOpen:
// 标签开启状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagName; // 如果是字母,表示进入标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "/") {
currentState = State.tagEnd; // 如果字符是 '/',表示结束标签开始
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagName:
// 解析标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',标签名称结束,返回初始状态
tokens.push({ type: "tag", name: chars.join("") }); // 创建标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.text:
// 解析文本节点状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === "<") {
currentState = State.tagOpen; // 如果字符是 '<',表示遇到新的标签,返回标签开启状态
tokens.push({ type: "text", content: chars.join("") }); // 创建文本类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEnd:
// 结束标签的开始状态:检查当前字符
if (isAlpha(char)) {
currentState = State.tagEndName; // 如果是字母,表示进入结束标签名称状态
chars.push(char); // 将字符添加到chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
case State.tagEndName:
// 解析结束标签名称状态:检查当前字符
if (isAlpha(char)) {
chars.push(char); // 如果是字母继续添加到chars数组
str = str.slice(1); // 移除已处理的字符
} else if (char === ">") {
currentState = State.initial; // 如果字符是 '>',结束标签名称结束,返回初始状态
tokens.push({ type: "tagEnd", name: chars.join("") }); // 创建结束标签类型的token
chars.length = 0; // 清空chars数组
str = str.slice(1); // 移除已处理的字符
}
break;
}
}
// 返回生成的令牌列表
return tokens;
}
// 解析函数,将 HTML 字符串转换为 AST
function parse(str) {
// 使用tokenize函数将字符串转换为令牌
const tokens = tokenize(str);
// 创建一个根节点对象用于存储解析后的HTML结构
const root = {
type: "Root",
children: [],
};
// 使用一个栈来跟踪当前处理的元素节点
const elementStack = [root];
// 当仍有令牌时,继续处理
while (tokens.length) {
// 获取当前的父元素(栈顶元素)
const parent = elementStack[elementStack.length - 1];
// 获取当前要处理的令牌
const t = tokens[0];
// 根据令牌类型处理不同情况
switch (t.type) {
case "tag":
// 如果是开始标签,创建一个新的元素节点
const elementNode = {
type: "Element",
tag: t.name,
children: [],
};
// 将新节点添加到父元素的子节点中
parent.children.push(elementNode);
// 将新节点压入栈中,成为下一个父节点
elementStack.push(elementNode);
break;
case "text":
// 如果是文本,创建一个文本节点
const textNode = {
type: "Text",
content: t.content,
};
// 将文本节点添加到当前父元素的子节点中
parent.children.push(textNode);
break;
case "tagEnd":
// 如果是结束标签,从栈中弹出当前处理的元素
elementStack.pop();
break;
}
// 移除已处理的令牌
tokens.shift();
}
// 返回解析后的根节点它包含了整个HTML结构
return root;
}
console.log(parse(template));
</script>
</body>
</html>