2024-09-11 10:55:44 +08:00

295 lines
11 KiB
HTML
Raw 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;
}
// 辅助方法用于按照层次结构打印AST的节点信息
function dump(node, indent = 0) {
// 获取当前节点的类型
const type = node.type;
// 根据节点类型构建描述信息
// 对于根节点,描述为空;对于元素节点,使用标签名;对于文本节点,使用内容
const desc =
node.type === "Root"
? ""
: node.type === "Element"
? node.tag
: node.content;
// 打印当前节点信息,包括类型和描述
// 使用重复的"-"字符来表示缩进(层级)
console.log(`${"-".repeat(indent)}${type}: ${desc}`);
// 如果当前节点有子节点递归调用dump函数打印每个子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2));
}
}
const templateAST = parse(template);
// 需要将之前的转换方法全部提出来,每一种转换提取成一个单独的方法
function transformElement(node, context) {
// if (node.type === "Element" && node.tag === "p") {
// node.tag = "h1";
// }
}
function transformText(node, context) {
// if (node.type === "Text") {
// node.content = node.content.toUpperCase();
// }
// 测试替换将文本节点替换为span元素
// if (node.type === "Text") {
// context.replaceNode({
// type: "Element",
// tag: "span",
// });
// }
// 测试删除:删除 React 文本节点
// if (node.type === "Text" && node.content === "React") {
// context.removeNode();
// }
return () => {
// 对节点再次进行处理
console.log("可以再次处理节点", node.type, node.tag || node.content);
};
}
function tranverseNode(ast, context) {
// 刚进去时进行处理
console.log("处理节点:", ast.type, ast.tag || ast.content);
// 获取到当前的节点
context.currentNode = ast;
// 1. 增加一个数组,用于存储转换函数返回的函数
const exitFns = [];
// 从上下文对象里面拿到所有的转换方法
const transforms = context.nodeTransforms;
for (let i = 0; i < transforms.length; i++) {
// 执行转换函数的时候,接收其返回值
const onExit = transforms[i](context.currentNode, context);
if (onExit) {
exitFns.push(onExit);
}
// 由于删除节点的时候当前节点会被置为null所以需要判断
// 如果当前节点为null直接返回
if (!context.currentNode) return;
}
// 获取当前节点的子节点
const children = context.currentNode.children;
if (children) {
for (let i = 0; i < children.length; i++) {
// 更新上下文里面的信息
context.parent = context.currentNode;
context.childIndex = i;
tranverseNode(children[i], context);
}
}
// 在节点处理完成之后执行exitFns里面所有的函数
// 执行的顺序是从后往前依次执行
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
function transform(ast) {
// 上下文对象:包含一些重要信息
const context = {
currentNode: null, // 存储当前正在转换的节点
childIndex: 0, // 子节点在父节点的 children 数组中的索引
parent: null, // 存储父节点
// 替换节点
replaceNode(node) {
context.parent.children[context.childIndex] = node;
context.currentNode = node;
},
// 删除节点
removeNode() {
if (context.parent) {
context.parent.children.splice(context.childIndex, 1);
context.currentNode = null;
}
},
nodeTransforms: [transformElement, transformText], // 存储具体的转换方法
};
// 在遍历模板AST树的时候可以针对部分节点作出一些修改
tranverseNode(ast, context);
console.log(dump(ast));
}
transform(templateAST);
</script>
</body>
</html>