parse_html_to_node 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +626 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Martin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# parse_html
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/parse_html)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
一个不依赖浏览器环境的HTML字符串解析器,可将HTML转换为可操作的对象树,支持完整的DOM操作、属性管理和样式处理。
|
|
8
|
+
|
|
9
|
+
## 特性
|
|
10
|
+
|
|
11
|
+
- 🚀 **零依赖**:纯JavaScript实现,无需浏览器环境
|
|
12
|
+
- 🔧 **完整DOM操作**:支持`before`、`after`、`insert`等DOM操作方法
|
|
13
|
+
- 🏷️ **属性管理**:提供`getAttr`、`setAttr`、`setAttrs`等方法
|
|
14
|
+
- 🎨 **样式处理**:支持`getStyle`、`setStyle`、`setStyles`等方法
|
|
15
|
+
- 🌳 **多根节点支持**:可解析HTML片段(多个根节点)
|
|
16
|
+
- 📦 **TypeScript支持**:完整的类型定义
|
|
17
|
+
- ⚡ **高性能**:轻量级实现,快速解析
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install parse_html
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
或
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add parse_html
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
或
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add parse_html
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import Node from 'parse_html';
|
|
41
|
+
|
|
42
|
+
// 解析单根HTML
|
|
43
|
+
const html = `
|
|
44
|
+
<div class="container" id="main" style="color: red; background-color: #fff;">
|
|
45
|
+
<p>Hello World</p>
|
|
46
|
+
<img src="test.jpg" alt="test" />
|
|
47
|
+
</div>
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const node = new Node(html);
|
|
51
|
+
|
|
52
|
+
// 获取HTML内容
|
|
53
|
+
console.log(node.getHtml());
|
|
54
|
+
|
|
55
|
+
// 获取子节点
|
|
56
|
+
const children = node.child();
|
|
57
|
+
console.log(children.length); // 2
|
|
58
|
+
|
|
59
|
+
// 获取属性
|
|
60
|
+
console.log(node.getAttr('id')); // "main"
|
|
61
|
+
|
|
62
|
+
// 获取样式
|
|
63
|
+
console.log(node.getStyle('backgroundColor')); // "#fff"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API文档
|
|
67
|
+
|
|
68
|
+
### `Node` 类
|
|
69
|
+
|
|
70
|
+
#### 构造函数
|
|
71
|
+
```typescript
|
|
72
|
+
new Node(html: string): Node
|
|
73
|
+
```
|
|
74
|
+
通过HTML字符串创建节点对象,支持单根和多根HTML。
|
|
75
|
+
|
|
76
|
+
#### 属性
|
|
77
|
+
- `tagName: string` - 标签名(`#text`表示文本节点,`#fragment`表示片段节点)
|
|
78
|
+
- `attributes: IAttributeData` - 属性对象
|
|
79
|
+
- `styles: Record<string, string>` - 样式对象(驼峰格式)
|
|
80
|
+
- `textContent: string` - 文本内容
|
|
81
|
+
- `children: Node[]` - 子节点数组
|
|
82
|
+
- `parent: Node | null` - 父节点
|
|
83
|
+
|
|
84
|
+
### 方法
|
|
85
|
+
|
|
86
|
+
#### `child(): Node[]`
|
|
87
|
+
获取当前节点的所有子节点(浅拷贝)。
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
const children = node.child();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### `before(newNode: string | Node): Node`
|
|
94
|
+
在当前节点之前插入新节点。
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
node.before('<span>Before</span>');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### `after(newNode: string | Node): Node`
|
|
101
|
+
在当前节点之后插入新节点。
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
node.after('<span>After</span>');
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### `insert(position: number, newNode: string | Node): Node`
|
|
108
|
+
在指定位置插入新节点。
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// 在第一个子节点前插入
|
|
112
|
+
node.insert(0, '<span>First</span>');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `getAttr(attrName: string): string | null`
|
|
116
|
+
获取指定属性的值。
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const id = node.getAttr('id');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `setAttr(attrName: string, value: string | null | undefined): Node`
|
|
123
|
+
设置或删除属性。
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// 设置属性
|
|
127
|
+
node.setAttr('class', 'container');
|
|
128
|
+
|
|
129
|
+
// 删除属性
|
|
130
|
+
node.setAttr('class', null);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `setAttrs(attrs: Record<string, string | null | undefined>): Node`
|
|
134
|
+
批量设置属性。
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
node.setAttrs({
|
|
138
|
+
'id': 'main',
|
|
139
|
+
'class': 'container',
|
|
140
|
+
'data-test': null // 删除属性
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### `getStyle(styleProp: string): string | null`
|
|
145
|
+
获取指定样式的值。
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const color = node.getStyle('color');
|
|
149
|
+
const bgColor = node.getStyle('backgroundColor'); // 支持驼峰格式
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `setStyle(styleProp: string, value: string | null | undefined): Node`
|
|
153
|
+
设置或删除样式。
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// 设置样式
|
|
157
|
+
node.setStyle('color', 'red');
|
|
158
|
+
|
|
159
|
+
// 删除样式
|
|
160
|
+
node.setStyle('color', null);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### `setStyles(styles: Record<string, string | null | undefined>): Node`
|
|
164
|
+
批量设置样式。
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
node.setStyles({
|
|
168
|
+
'color': 'red',
|
|
169
|
+
'fontSize': '14px',
|
|
170
|
+
'backgroundColor': null // 删除样式
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### `getHtml(): string`
|
|
175
|
+
获取当前节点的完整HTML字符串。
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
const htmlString = node.getHtml();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 示例
|
|
182
|
+
|
|
183
|
+
### 1. 多根节点解析
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
import Node from 'parse_html';
|
|
187
|
+
|
|
188
|
+
const multiRootHtml = `
|
|
189
|
+
<p>第一段文本</p>
|
|
190
|
+
<div class="box" style="font-size: 14px;">第二段内容</div>
|
|
191
|
+
<span>第三段</span>
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const fragmentNode = new Node(multiRootHtml);
|
|
195
|
+
console.log(fragmentNode.tagName); // "#fragment"
|
|
196
|
+
console.log(fragmentNode.child().length); // 3
|
|
197
|
+
console.log(fragmentNode.getHtml()); // 输出拼接后的完整HTML
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 2. DOM操作
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
import Node from 'parse_html';
|
|
204
|
+
|
|
205
|
+
const parent = new Node('<div id="parent"></div>');
|
|
206
|
+
|
|
207
|
+
// 插入子节点
|
|
208
|
+
parent.insert(0, '<span>Child 1</span>');
|
|
209
|
+
parent.insert(1, '<span>Child 2</span>');
|
|
210
|
+
|
|
211
|
+
// 在子节点间插入
|
|
212
|
+
const firstChild = parent.child()[0];
|
|
213
|
+
firstChild.after('<span>Between</span>');
|
|
214
|
+
|
|
215
|
+
console.log(parent.getHtml());
|
|
216
|
+
// <div id="parent"><span>Child 1</span><span>Between</span><span>Child 2</span></div>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 3. 属性和样式操作
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
import Node from 'parse_html';
|
|
223
|
+
|
|
224
|
+
const node = new Node('<div>Test</div>');
|
|
225
|
+
|
|
226
|
+
// 设置属性和样式
|
|
227
|
+
node.setAttrs({
|
|
228
|
+
'id': 'test',
|
|
229
|
+
'class': 'container'
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
node.setStyles({
|
|
233
|
+
'color': 'blue',
|
|
234
|
+
'fontSize': '16px',
|
|
235
|
+
'padding': '10px'
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 获取属性和样式
|
|
239
|
+
console.log(node.getAttr('id')); // "test"
|
|
240
|
+
console.log(node.getStyle('fontSize')); // "16px"
|
|
241
|
+
|
|
242
|
+
// 更新样式
|
|
243
|
+
node.setStyle('color', 'green');
|
|
244
|
+
console.log(node.getStyle('color')); // "green"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 4. 文本节点处理
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
import Node from 'parse_html';
|
|
251
|
+
|
|
252
|
+
const html = 'Hello <strong>World</strong>!';
|
|
253
|
+
const node = new Node(html);
|
|
254
|
+
|
|
255
|
+
// 文本节点会被正确解析
|
|
256
|
+
console.log(node.child().length); // 3
|
|
257
|
+
console.log(node.child()[0].tagName); // "#text"
|
|
258
|
+
console.log(node.child()[0].textContent); // "Hello "
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 注意事项
|
|
262
|
+
|
|
263
|
+
1. **片段节点**:多根HTML会解析为`#fragment`节点,其属性/样式操作会返回`null`。
|
|
264
|
+
2. **自闭合标签**:支持`img`、`br`、`input`、`meta`等标准自闭合标签。
|
|
265
|
+
3. **样式格式**:内部使用驼峰格式,但解析时支持短横线格式。
|
|
266
|
+
4. **错误处理**:无效HTML会抛出错误,请确保HTML格式正确。
|
|
267
|
+
|
|
268
|
+
## 开发
|
|
269
|
+
|
|
270
|
+
### 构建项目
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# 安装依赖
|
|
274
|
+
pnpm install
|
|
275
|
+
|
|
276
|
+
# 开发模式(监听文件变化)
|
|
277
|
+
pnpm run dev
|
|
278
|
+
|
|
279
|
+
# 构建项目
|
|
280
|
+
pnpm run build
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 项目结构
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
parse_html/
|
|
287
|
+
├── lib/
|
|
288
|
+
│ └── index.ts # 源代码
|
|
289
|
+
├── dist/ # 构建输出
|
|
290
|
+
├── text.ts # 使用示例
|
|
291
|
+
├── package.json
|
|
292
|
+
└── tsconfig.json
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## 贡献
|
|
296
|
+
|
|
297
|
+
欢迎提交Issue和Pull Request!
|
|
298
|
+
|
|
299
|
+
1. Fork 项目
|
|
300
|
+
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
|
301
|
+
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
|
|
302
|
+
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
|
303
|
+
5. 开启 Pull Request
|
|
304
|
+
|
|
305
|
+
## 许可证
|
|
306
|
+
|
|
307
|
+
[MIT](LICENSE) © Huang Jingjing
|
|
308
|
+
|
|
309
|
+
## 相关链接
|
|
310
|
+
|
|
311
|
+
- [GitHub仓库](https://github.com/Easy-Martin/parse_html)
|
|
312
|
+
- [npm包](https://www.npmjs.com/package/parse_html)
|
|
313
|
+
- [问题反馈](https://github.com/Easy-Martin/parse_html/issues)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface IAttributeData {
|
|
2
|
+
[key: string]: string | Record<string, string> | undefined;
|
|
3
|
+
styleObj?: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
declare class Node {
|
|
6
|
+
#private;
|
|
7
|
+
tagName: string;
|
|
8
|
+
attributes: IAttributeData;
|
|
9
|
+
styles: Record<string, string>;
|
|
10
|
+
textContent: string;
|
|
11
|
+
children: Node[];
|
|
12
|
+
parent: Node | null;
|
|
13
|
+
/**
|
|
14
|
+
* 构造函数:通过HTML字符串初始化节点(支持单根/多根)
|
|
15
|
+
* @param {string} html - HTML字符串(单根/多根均可)
|
|
16
|
+
*/
|
|
17
|
+
constructor(html: string);
|
|
18
|
+
/**
|
|
19
|
+
* 子集管理:获取当前节点的所有子节点
|
|
20
|
+
* @returns {Node[]} 子节点数组(浅拷贝)
|
|
21
|
+
*/
|
|
22
|
+
child(): Node[];
|
|
23
|
+
/**
|
|
24
|
+
* DOM操作:在当前节点之后插入新元素
|
|
25
|
+
* @param {string|Node} newNode - 要插入的HTML字符串或Node实例
|
|
26
|
+
* @returns {Node} 当前节点(链式调用)
|
|
27
|
+
*/
|
|
28
|
+
before(newNode: string | Node): Node;
|
|
29
|
+
/**
|
|
30
|
+
* DOM操作:在当前节点之后插入新元素
|
|
31
|
+
* @param {string|Node} newNode - 要插入的HTML字符串或Node实例
|
|
32
|
+
* @returns {Node} 当前节点(链式调用)
|
|
33
|
+
*/
|
|
34
|
+
after(newNode: string | Node): Node;
|
|
35
|
+
/**
|
|
36
|
+
* DOM操作:在指定位置插入新元素
|
|
37
|
+
* @param {number} position - 插入位置(0 ~ children.length)
|
|
38
|
+
* @param {string|Node} newNode - 要插入的HTML字符串或Node实例
|
|
39
|
+
* @returns {Node} 当前节点(链式调用)
|
|
40
|
+
*/
|
|
41
|
+
insert(position: number, newNode: string | Node): Node;
|
|
42
|
+
/**
|
|
43
|
+
* 属性操作:获取指定属性的值
|
|
44
|
+
* @param {string} attrName - 属性名
|
|
45
|
+
* @returns {string|null} 属性值(不存在返回null)
|
|
46
|
+
*/
|
|
47
|
+
getAttr(attrName: string): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* 属性操作:设置指定属性的值
|
|
50
|
+
* @param {string} attrName - 属性名
|
|
51
|
+
* @param {string|null|undefined} value - 属性值(null/undefined删除属性)
|
|
52
|
+
* @returns {Node} 当前节点(链式调用)
|
|
53
|
+
*/
|
|
54
|
+
setAttr(attrName: string, value: string | null | undefined): Node;
|
|
55
|
+
/**
|
|
56
|
+
* 属性操作:批量设置多个属性
|
|
57
|
+
* @param {Record<string, string | null | undefined>} attrs - 包含属性名-属性值键值对的对象
|
|
58
|
+
* @returns {Node} 当前节点(链式调用)
|
|
59
|
+
*/
|
|
60
|
+
setAttrs(attrs: Record<string, string | null | undefined>): Node;
|
|
61
|
+
/**
|
|
62
|
+
* 样式操作:获取指定样式属性的值
|
|
63
|
+
* @param {string} styleProp - 样式属性名(支持驼峰/短横线)
|
|
64
|
+
* @returns {string|null} 样式值(不存在返回null)
|
|
65
|
+
*/
|
|
66
|
+
getStyle(styleProp: string): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* 样式操作:设置指定样式属性的值
|
|
69
|
+
* @param {string} styleProp - 样式属性名(支持驼峰/短横线)
|
|
70
|
+
* @param {string|null|undefined} value - 样式值(null/undefined删除样式)
|
|
71
|
+
* @returns {Node} 当前节点(链式调用)
|
|
72
|
+
*/
|
|
73
|
+
setStyle(styleProp: string, value: string | null | undefined): Node;
|
|
74
|
+
/**
|
|
75
|
+
* 样式操作:批量设置多个样式属性
|
|
76
|
+
* @param {Record<string, string | null | undefined>} styles - 包含样式属性名-样式值键值对的对象
|
|
77
|
+
* @returns {Node} 当前节点(链式调用)
|
|
78
|
+
*/
|
|
79
|
+
setStyles(styles: Record<string, string | null | undefined>): Node;
|
|
80
|
+
/**
|
|
81
|
+
* 获取当前节点的完整HTML文本
|
|
82
|
+
* @returns {string} HTML字符串
|
|
83
|
+
*/
|
|
84
|
+
getHtml(): string;
|
|
85
|
+
}
|
|
86
|
+
export default Node;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
5
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
6
|
+
};
|
|
7
|
+
var _Node_instances, _a, _Node_generateHTMLFromData, _Node_convertToNode;
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
// 自闭合标签常量(只读数组)
|
|
10
|
+
const SELF_CLOSING_TAGS = ["img", "br", "input", "meta", "link", "hr", "area", "base", "col", "embed", "param", "source", "track", "wbr"];
|
|
11
|
+
// 工具函数:驼峰转短横线(用于样式属性)
|
|
12
|
+
function camelToKebab(str) {
|
|
13
|
+
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
14
|
+
}
|
|
15
|
+
// 工具函数:短横线转驼峰(用于样式属性)
|
|
16
|
+
function kebabToCamel(str) {
|
|
17
|
+
return str.replace(/-([a-z])/g, (_, match) => match.toUpperCase());
|
|
18
|
+
}
|
|
19
|
+
// 工具函数:解析属性字符串为属性对象(修改返回类型为IAttributeData)
|
|
20
|
+
function parseAttributes(attrStr) {
|
|
21
|
+
const attrs = {}; // 修改:使用扩展后的属性接口
|
|
22
|
+
if (!attrStr)
|
|
23
|
+
return attrs;
|
|
24
|
+
// 匹配属性键值对:key="value" / key='value' / key=value
|
|
25
|
+
const attrRegex = /([a-zA-Z0-9-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = attrRegex.exec(attrStr)) !== null) {
|
|
28
|
+
const [, key, doubleVal, singleVal, noQuoteVal] = match;
|
|
29
|
+
const value = doubleVal || singleVal || noQuoteVal || "";
|
|
30
|
+
attrs[key] = value;
|
|
31
|
+
}
|
|
32
|
+
// 解析style属性为样式对象(内部属性,不对外输出)
|
|
33
|
+
if (attrs.style) {
|
|
34
|
+
const styleObj = {};
|
|
35
|
+
const styleRegex = /([a-zA-Z0-9-]+)\s*:\s*([^;]+)/g;
|
|
36
|
+
let styleMatch;
|
|
37
|
+
while ((styleMatch = styleRegex.exec(attrs.style)) !== null) {
|
|
38
|
+
const [, prop, val] = styleMatch;
|
|
39
|
+
styleObj[kebabToCamel(prop.trim())] = val.trim();
|
|
40
|
+
}
|
|
41
|
+
attrs.styleObj = styleObj; // 现在类型匹配,无TS错误
|
|
42
|
+
delete attrs.style; // 替换为结构化的样式对象
|
|
43
|
+
}
|
|
44
|
+
return attrs;
|
|
45
|
+
}
|
|
46
|
+
// 核心工具函数:解析单个节点(内部使用)
|
|
47
|
+
function parseSingleNode(html) {
|
|
48
|
+
html = html.trim();
|
|
49
|
+
if (!html)
|
|
50
|
+
return null;
|
|
51
|
+
// 文本节点(非标签内容)
|
|
52
|
+
if (!html.startsWith("<")) {
|
|
53
|
+
return {
|
|
54
|
+
tagName: "#text",
|
|
55
|
+
textContent: html,
|
|
56
|
+
attributes: {}, // 符合IAttributeData类型
|
|
57
|
+
styles: {},
|
|
58
|
+
children: [],
|
|
59
|
+
parent: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const selfClosingRegex = /<([a-zA-Z0-9]+)\s*(.*?)\/?>/;
|
|
63
|
+
const selfClosingMatch = html.match(selfClosingRegex);
|
|
64
|
+
// 处理自闭合标签
|
|
65
|
+
if (selfClosingMatch) {
|
|
66
|
+
const [, tagName, attrStr] = selfClosingMatch;
|
|
67
|
+
if (SELF_CLOSING_TAGS.includes(tagName.toLowerCase())) {
|
|
68
|
+
const attrs = parseAttributes(attrStr);
|
|
69
|
+
return {
|
|
70
|
+
tagName: tagName.toLowerCase(),
|
|
71
|
+
attributes: attrs, // 类型匹配
|
|
72
|
+
styles: attrs.styleObj || {}, // styleObj是Record<string, string>,符合styles类型
|
|
73
|
+
textContent: "",
|
|
74
|
+
children: [],
|
|
75
|
+
parent: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 匹配开始标签
|
|
80
|
+
const startTagRegex = /<([a-zA-Z0-9]+)\s*(.*?)>/;
|
|
81
|
+
const startTagMatch = html.match(startTagRegex);
|
|
82
|
+
if (!startTagMatch)
|
|
83
|
+
return null;
|
|
84
|
+
const [startTag, tagName, attrStr] = startTagMatch;
|
|
85
|
+
const lowerTagName = tagName.toLowerCase();
|
|
86
|
+
const endTag = `</${lowerTagName}>`;
|
|
87
|
+
// 查找匹配的结束标签(处理嵌套)
|
|
88
|
+
let endTagIndex = -1;
|
|
89
|
+
let tagCount = 1;
|
|
90
|
+
let currentIndex = startTag.length;
|
|
91
|
+
while (currentIndex < html.length && tagCount > 0) {
|
|
92
|
+
const nextStart = html.indexOf(`<${lowerTagName}`, currentIndex);
|
|
93
|
+
const nextEnd = html.indexOf(endTag, currentIndex);
|
|
94
|
+
if (nextEnd === -1)
|
|
95
|
+
break;
|
|
96
|
+
if (nextStart !== -1 && nextStart < nextEnd) {
|
|
97
|
+
tagCount++;
|
|
98
|
+
currentIndex = nextStart + `<${lowerTagName}`.length;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
tagCount--;
|
|
102
|
+
if (tagCount === 0)
|
|
103
|
+
endTagIndex = nextEnd;
|
|
104
|
+
currentIndex = nextEnd + endTag.length;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 提取标签内容(开始标签和结束标签之间)
|
|
108
|
+
const content = endTagIndex !== -1 ? html.slice(startTag.length, endTagIndex).trim() : html.slice(startTag.length).trim();
|
|
109
|
+
// 构建基础节点数据
|
|
110
|
+
const attrs = parseAttributes(attrStr);
|
|
111
|
+
const nodeData = {
|
|
112
|
+
tagName: lowerTagName,
|
|
113
|
+
attributes: attrs, // 类型匹配
|
|
114
|
+
styles: attrs.styleObj || {}, // 类型匹配
|
|
115
|
+
textContent: "",
|
|
116
|
+
children: [],
|
|
117
|
+
parent: null,
|
|
118
|
+
};
|
|
119
|
+
// 解析子节点
|
|
120
|
+
if (content) {
|
|
121
|
+
const childNodes = [];
|
|
122
|
+
let remaining = content;
|
|
123
|
+
while (remaining) {
|
|
124
|
+
const tagStart = remaining.indexOf("<");
|
|
125
|
+
if (tagStart === -1) {
|
|
126
|
+
// 纯文本内容
|
|
127
|
+
const textNode = parseSingleNode(remaining);
|
|
128
|
+
if (textNode)
|
|
129
|
+
childNodes.push(textNode);
|
|
130
|
+
remaining = "";
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (tagStart > 0) {
|
|
134
|
+
// 标签前的文本
|
|
135
|
+
const textNode = parseSingleNode(remaining.slice(0, tagStart));
|
|
136
|
+
if (textNode)
|
|
137
|
+
childNodes.push(textNode);
|
|
138
|
+
remaining = remaining.slice(tagStart);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// 解析嵌套标签
|
|
142
|
+
const tempTagMatch = remaining.match(startTagRegex);
|
|
143
|
+
if (!tempTagMatch) {
|
|
144
|
+
const textNode = parseSingleNode(remaining);
|
|
145
|
+
if (textNode)
|
|
146
|
+
childNodes.push(textNode);
|
|
147
|
+
remaining = "";
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const tempTagName = tempTagMatch[1].toLowerCase();
|
|
151
|
+
const tempEndTag = `</${tempTagName}>`;
|
|
152
|
+
// 自闭合标签直接处理
|
|
153
|
+
if (SELF_CLOSING_TAGS.includes(tempTagName)) {
|
|
154
|
+
const selfNode = parseSingleNode(remaining.slice(0, tempTagMatch[0].length));
|
|
155
|
+
if (selfNode)
|
|
156
|
+
childNodes.push(selfNode);
|
|
157
|
+
remaining = remaining.slice(tempTagMatch[0].length).trim();
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// 查找嵌套标签的结束位置
|
|
161
|
+
let tempEndIndex = -1;
|
|
162
|
+
let tempTagCount = 1;
|
|
163
|
+
let tempCurrentIndex = tempTagMatch[0].length;
|
|
164
|
+
while (tempCurrentIndex < remaining.length && tempTagCount > 0) {
|
|
165
|
+
const nextTempStart = remaining.indexOf(`<${tempTagName}`, tempCurrentIndex);
|
|
166
|
+
const nextTempEnd = remaining.indexOf(tempEndTag, tempCurrentIndex);
|
|
167
|
+
if (nextTempEnd === -1)
|
|
168
|
+
break;
|
|
169
|
+
if (nextTempStart !== -1 && nextTempStart < nextTempEnd) {
|
|
170
|
+
tempTagCount++;
|
|
171
|
+
tempCurrentIndex = nextTempStart + `<${tempTagName}`.length;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
tempTagCount--;
|
|
175
|
+
if (tempTagCount === 0)
|
|
176
|
+
tempEndIndex = nextTempEnd;
|
|
177
|
+
tempCurrentIndex = nextTempEnd + tempEndTag.length;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 提取子节点HTML并递归解析
|
|
181
|
+
if (tempEndIndex !== -1) {
|
|
182
|
+
const childHTML = remaining.slice(0, tempEndIndex + tempEndTag.length);
|
|
183
|
+
const childNode = parseSingleNode(childHTML);
|
|
184
|
+
if (childNode)
|
|
185
|
+
childNodes.push(childNode);
|
|
186
|
+
remaining = remaining.slice(tempEndIndex + tempEndTag.length).trim();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const childNode = parseSingleNode(remaining);
|
|
190
|
+
if (childNode)
|
|
191
|
+
childNodes.push(childNode);
|
|
192
|
+
remaining = "";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 过滤无效节点并赋值
|
|
198
|
+
nodeData.children = childNodes.filter(Boolean);
|
|
199
|
+
// 单一文本子节点直接合并到textContent
|
|
200
|
+
if (nodeData.children.length === 1 && nodeData.children[0].tagName === "#text") {
|
|
201
|
+
nodeData.textContent = nodeData.children[0].textContent;
|
|
202
|
+
nodeData.children = [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return nodeData;
|
|
206
|
+
}
|
|
207
|
+
// 新增:解析HTML片段(支持多根节点)
|
|
208
|
+
function parseHTMLFragment(html) {
|
|
209
|
+
html = html.trim();
|
|
210
|
+
if (!html)
|
|
211
|
+
return [];
|
|
212
|
+
const fragmentNodes = [];
|
|
213
|
+
let remaining = html;
|
|
214
|
+
while (remaining) {
|
|
215
|
+
const tagStart = remaining.indexOf("<");
|
|
216
|
+
if (tagStart === -1) {
|
|
217
|
+
// 剩余纯文本
|
|
218
|
+
const textNode = parseSingleNode(remaining);
|
|
219
|
+
if (textNode)
|
|
220
|
+
fragmentNodes.push(textNode);
|
|
221
|
+
remaining = "";
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (tagStart > 0) {
|
|
225
|
+
// 标签前的文本节点
|
|
226
|
+
const textNode = parseSingleNode(remaining.slice(0, tagStart));
|
|
227
|
+
if (textNode)
|
|
228
|
+
fragmentNodes.push(textNode);
|
|
229
|
+
remaining = remaining.slice(tagStart);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// 解析单个标签节点
|
|
233
|
+
const startTagRegex = /<([a-zA-Z0-9]+)\s*(.*?)>/;
|
|
234
|
+
const tempTagMatch = remaining.match(startTagRegex);
|
|
235
|
+
if (!tempTagMatch) {
|
|
236
|
+
const textNode = parseSingleNode(remaining);
|
|
237
|
+
if (textNode)
|
|
238
|
+
fragmentNodes.push(textNode);
|
|
239
|
+
remaining = "";
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const tempTagName = tempTagMatch[1].toLowerCase();
|
|
243
|
+
// 自闭合标签
|
|
244
|
+
if (SELF_CLOSING_TAGS.includes(tempTagName)) {
|
|
245
|
+
const selfClosingRegex = /<([a-zA-Z0-9]+)\s*(.*?)\/?>/;
|
|
246
|
+
const selfClosingMatch = remaining.match(selfClosingRegex);
|
|
247
|
+
if (selfClosingMatch) {
|
|
248
|
+
const selfNode = parseSingleNode(selfClosingMatch[0]);
|
|
249
|
+
if (selfNode)
|
|
250
|
+
fragmentNodes.push(selfNode);
|
|
251
|
+
remaining = remaining.slice(selfClosingMatch[0].length).trim();
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const textNode = parseSingleNode(remaining);
|
|
255
|
+
if (textNode)
|
|
256
|
+
fragmentNodes.push(textNode);
|
|
257
|
+
remaining = "";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// 非自闭合标签,找匹配的结束标签
|
|
262
|
+
const tempEndTag = `</${tempTagName}>`;
|
|
263
|
+
let tempEndIndex = -1;
|
|
264
|
+
let tempTagCount = 1;
|
|
265
|
+
let tempCurrentIndex = tempTagMatch[0].length;
|
|
266
|
+
while (tempCurrentIndex < remaining.length && tempTagCount > 0) {
|
|
267
|
+
const nextTempStart = remaining.indexOf(`<${tempTagName}`, tempCurrentIndex);
|
|
268
|
+
const nextTempEnd = remaining.indexOf(tempEndTag, tempCurrentIndex);
|
|
269
|
+
if (nextTempEnd === -1)
|
|
270
|
+
break;
|
|
271
|
+
if (nextTempStart !== -1 && nextTempStart < nextTempEnd) {
|
|
272
|
+
tempTagCount++;
|
|
273
|
+
tempCurrentIndex = nextTempStart + `<${tempTagName}`.length;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
tempTagCount--;
|
|
277
|
+
if (tempTagCount === 0)
|
|
278
|
+
tempEndIndex = nextTempEnd;
|
|
279
|
+
tempCurrentIndex = nextTempEnd + tempEndTag.length;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (tempEndIndex !== -1) {
|
|
283
|
+
const childHTML = remaining.slice(0, tempEndIndex + tempEndTag.length);
|
|
284
|
+
const childNode = parseSingleNode(childHTML);
|
|
285
|
+
if (childNode)
|
|
286
|
+
fragmentNodes.push(childNode);
|
|
287
|
+
remaining = remaining.slice(tempEndIndex + tempEndTag.length).trim();
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const childNode = parseSingleNode(remaining);
|
|
291
|
+
if (childNode)
|
|
292
|
+
fragmentNodes.push(childNode);
|
|
293
|
+
remaining = "";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// 过滤无效节点
|
|
300
|
+
return fragmentNodes.filter(Boolean);
|
|
301
|
+
}
|
|
302
|
+
// 核心Node类实现(支持片段/多根节点)
|
|
303
|
+
class Node {
|
|
304
|
+
/**
|
|
305
|
+
* 构造函数:通过HTML字符串初始化节点(支持单根/多根)
|
|
306
|
+
* @param {string} html - HTML字符串(单根/多根均可)
|
|
307
|
+
*/
|
|
308
|
+
constructor(html) {
|
|
309
|
+
_Node_instances.add(this);
|
|
310
|
+
if (typeof html !== "string") {
|
|
311
|
+
throw new Error("初始化Node必须传入HTML字符串");
|
|
312
|
+
}
|
|
313
|
+
const htmlTrimmed = html.trim();
|
|
314
|
+
if (!htmlTrimmed) {
|
|
315
|
+
throw new Error("无法解析空的HTML字符串");
|
|
316
|
+
}
|
|
317
|
+
// 尝试解析为单一节点
|
|
318
|
+
const singleNodeData = parseSingleNode(htmlTrimmed);
|
|
319
|
+
// 解析为片段(多根节点)
|
|
320
|
+
const fragmentNodeData = parseHTMLFragment(htmlTrimmed);
|
|
321
|
+
// 初始化默认属性
|
|
322
|
+
this.tagName = "";
|
|
323
|
+
this.attributes = {}; // 符合IAttributeData类型
|
|
324
|
+
this.styles = {};
|
|
325
|
+
this.textContent = "";
|
|
326
|
+
this.children = [];
|
|
327
|
+
this.parent = null;
|
|
328
|
+
// 节点核心属性赋值
|
|
329
|
+
if (singleNodeData && fragmentNodeData.length === 1) {
|
|
330
|
+
// 单根节点
|
|
331
|
+
this.tagName = singleNodeData.tagName;
|
|
332
|
+
this.attributes = { ...singleNodeData.attributes };
|
|
333
|
+
this.styles = { ...singleNodeData.styles };
|
|
334
|
+
this.textContent = singleNodeData.textContent || "";
|
|
335
|
+
// 子节点转换为Node实例
|
|
336
|
+
if (singleNodeData.children.length > 0) {
|
|
337
|
+
this.children = singleNodeData.children.map((childData) => {
|
|
338
|
+
const childNode = new _a(__classPrivateFieldGet(this, _Node_instances, "m", _Node_generateHTMLFromData).call(this, childData));
|
|
339
|
+
childNode.parent = this;
|
|
340
|
+
return childNode;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (fragmentNodeData.length > 0) {
|
|
345
|
+
// 多根节点 → 标记为片段节点
|
|
346
|
+
this.tagName = "#fragment";
|
|
347
|
+
this.attributes = {}; // 符合IAttributeData类型
|
|
348
|
+
this.styles = {};
|
|
349
|
+
this.textContent = "";
|
|
350
|
+
// 片段的子节点是多个根节点
|
|
351
|
+
this.children = fragmentNodeData.map((childData) => {
|
|
352
|
+
const childNode = new _a(__classPrivateFieldGet(this, _Node_instances, "m", _Node_generateHTMLFromData).call(this, childData));
|
|
353
|
+
childNode.parent = this;
|
|
354
|
+
return childNode;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
throw new Error("无法解析无效的HTML字符串");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 子集管理:获取当前节点的所有子节点
|
|
363
|
+
* @returns {Node[]} 子节点数组(浅拷贝)
|
|
364
|
+
*/
|
|
365
|
+
child() {
|
|
366
|
+
return [...this.children];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* DOM操作:在当前节点之后插入新元素
|
|
370
|
+
* @param {string|Node} newNode - 要插入的HTML字符串或Node实例
|
|
371
|
+
* @returns {Node} 当前节点(链式调用)
|
|
372
|
+
*/
|
|
373
|
+
before(newNode) {
|
|
374
|
+
if (!this.parent) {
|
|
375
|
+
throw new Error("当前节点没有父节点,无法执行before操作");
|
|
376
|
+
}
|
|
377
|
+
const nodeToInsert = __classPrivateFieldGet(this, _Node_instances, "m", _Node_convertToNode).call(this, newNode);
|
|
378
|
+
const currentIndex = this.parent.children.findIndex((child) => child === this);
|
|
379
|
+
if (currentIndex === -1) {
|
|
380
|
+
throw new Error("当前节点不在父节点的子节点列表中");
|
|
381
|
+
}
|
|
382
|
+
// 插入到当前节点前一个位置
|
|
383
|
+
this.parent.children.splice(currentIndex, 0, nodeToInsert);
|
|
384
|
+
nodeToInsert.parent = this.parent;
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* DOM操作:在当前节点之后插入新元素
|
|
389
|
+
* @param {string|Node} newNode - 要插入的HTML字符串或Node实例
|
|
390
|
+
* @returns {Node} 当前节点(链式调用)
|
|
391
|
+
*/
|
|
392
|
+
after(newNode) {
|
|
393
|
+
if (!this.parent) {
|
|
394
|
+
throw new Error("当前节点没有父节点,无法执行after操作");
|
|
395
|
+
}
|
|
396
|
+
const nodeToInsert = __classPrivateFieldGet(this, _Node_instances, "m", _Node_convertToNode).call(this, newNode);
|
|
397
|
+
const currentIndex = this.parent.children.findIndex((child) => child === this);
|
|
398
|
+
if (currentIndex === -1) {
|
|
399
|
+
throw new Error("当前节点不在父节点的子节点列表中");
|
|
400
|
+
}
|
|
401
|
+
// 插入到当前节点下一个位置
|
|
402
|
+
this.parent.children.splice(currentIndex + 1, 0, nodeToInsert);
|
|
403
|
+
nodeToInsert.parent = this.parent;
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* DOM操作:在指定位置插入新元素
|
|
408
|
+
* @param {number} position - 插入位置(0 ~ children.length)
|
|
409
|
+
* @param {string|Node} newNode - 要插入的HTML字符串或Node实例
|
|
410
|
+
* @returns {Node} 当前节点(链式调用)
|
|
411
|
+
*/
|
|
412
|
+
insert(position, newNode) {
|
|
413
|
+
if (typeof position !== "number" || position < 0 || position > this.children.length) {
|
|
414
|
+
throw new Error(`插入位置${position}无效,必须是0到${this.children.length}之间的整数`);
|
|
415
|
+
}
|
|
416
|
+
const nodeToInsert = __classPrivateFieldGet(this, _Node_instances, "m", _Node_convertToNode).call(this, newNode);
|
|
417
|
+
// 如果插入的是片段节点,展开其所有子节点(符合DOM标准)
|
|
418
|
+
if (nodeToInsert.tagName === "#fragment") {
|
|
419
|
+
nodeToInsert.children.forEach((child, index) => {
|
|
420
|
+
child.parent = this;
|
|
421
|
+
this.children.splice(position + index, 0, child);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
this.children.splice(position, 0, nodeToInsert);
|
|
426
|
+
nodeToInsert.parent = this;
|
|
427
|
+
}
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 属性操作:获取指定属性的值
|
|
432
|
+
* @param {string} attrName - 属性名
|
|
433
|
+
* @returns {string|null} 属性值(不存在返回null)
|
|
434
|
+
*/
|
|
435
|
+
getAttr(attrName) {
|
|
436
|
+
if (this.tagName === "#fragment" || this.tagName === "#text") {
|
|
437
|
+
return null; // 片段/文本节点无属性
|
|
438
|
+
}
|
|
439
|
+
if (typeof attrName !== "string") {
|
|
440
|
+
throw new Error("属性名必须是字符串");
|
|
441
|
+
}
|
|
442
|
+
// 只返回字符串类型的属性(排除styleObj)
|
|
443
|
+
return typeof this.attributes[attrName] === "string" ? this.attributes[attrName] : null;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 属性操作:设置指定属性的值
|
|
447
|
+
* @param {string} attrName - 属性名
|
|
448
|
+
* @param {string|null|undefined} value - 属性值(null/undefined删除属性)
|
|
449
|
+
* @returns {Node} 当前节点(链式调用)
|
|
450
|
+
*/
|
|
451
|
+
setAttr(attrName, value) {
|
|
452
|
+
if (this.tagName === "#fragment" || this.tagName === "#text") {
|
|
453
|
+
throw new Error("片段/文本节点不支持设置属性");
|
|
454
|
+
}
|
|
455
|
+
if (typeof attrName !== "string") {
|
|
456
|
+
throw new Error("属性名必须是字符串");
|
|
457
|
+
}
|
|
458
|
+
if (value === null || value === undefined) {
|
|
459
|
+
delete this.attributes[attrName];
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
// 确保属性值是字符串(符合IAttributeData的基础约束)
|
|
463
|
+
this.attributes[attrName] = String(value);
|
|
464
|
+
}
|
|
465
|
+
return this;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* 属性操作:批量设置多个属性
|
|
469
|
+
* @param {Record<string, string | null | undefined>} attrs - 包含属性名-属性值键值对的对象
|
|
470
|
+
* @returns {Node} 当前节点(链式调用)
|
|
471
|
+
*/
|
|
472
|
+
setAttrs(attrs) {
|
|
473
|
+
if (this.tagName === "#fragment" || this.tagName === "#text") {
|
|
474
|
+
throw new Error("片段/文本节点不支持设置属性");
|
|
475
|
+
}
|
|
476
|
+
if (typeof attrs !== "object" || attrs === null) {
|
|
477
|
+
throw new Error("属性对象必须是非空对象");
|
|
478
|
+
}
|
|
479
|
+
for (const [attrName, value] of Object.entries(attrs)) {
|
|
480
|
+
this.setAttr(attrName, value);
|
|
481
|
+
}
|
|
482
|
+
return this;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* 样式操作:获取指定样式属性的值
|
|
486
|
+
* @param {string} styleProp - 样式属性名(支持驼峰/短横线)
|
|
487
|
+
* @returns {string|null} 样式值(不存在返回null)
|
|
488
|
+
*/
|
|
489
|
+
getStyle(styleProp) {
|
|
490
|
+
if (this.tagName === "#fragment" || this.tagName === "#text") {
|
|
491
|
+
return null; // 片段/文本节点无样式
|
|
492
|
+
}
|
|
493
|
+
if (typeof styleProp !== "string") {
|
|
494
|
+
throw new Error("样式属性名必须是字符串");
|
|
495
|
+
}
|
|
496
|
+
const camelProp = kebabToCamel(styleProp);
|
|
497
|
+
return this.styles[camelProp] || this.styles[styleProp] || null;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 样式操作:设置指定样式属性的值
|
|
501
|
+
* @param {string} styleProp - 样式属性名(支持驼峰/短横线)
|
|
502
|
+
* @param {string|null|undefined} value - 样式值(null/undefined删除样式)
|
|
503
|
+
* @returns {Node} 当前节点(链式调用)
|
|
504
|
+
*/
|
|
505
|
+
setStyle(styleProp, value) {
|
|
506
|
+
if (this.tagName === "#fragment" || this.tagName === "#text") {
|
|
507
|
+
throw new Error("片段/文本节点不支持设置样式");
|
|
508
|
+
}
|
|
509
|
+
if (typeof styleProp !== "string") {
|
|
510
|
+
throw new Error("样式属性名必须是字符串");
|
|
511
|
+
}
|
|
512
|
+
const camelProp = kebabToCamel(styleProp);
|
|
513
|
+
if (value === null || value === undefined) {
|
|
514
|
+
delete this.styles[camelProp];
|
|
515
|
+
delete this.styles[styleProp];
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
this.styles[camelProp] = String(value);
|
|
519
|
+
}
|
|
520
|
+
return this;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* 样式操作:批量设置多个样式属性
|
|
524
|
+
* @param {Record<string, string | null | undefined>} styles - 包含样式属性名-样式值键值对的对象
|
|
525
|
+
* @returns {Node} 当前节点(链式调用)
|
|
526
|
+
*/
|
|
527
|
+
setStyles(styles) {
|
|
528
|
+
if (this.tagName === "#fragment" || this.tagName === "#text") {
|
|
529
|
+
throw new Error("片段/文本节点不支持设置样式");
|
|
530
|
+
}
|
|
531
|
+
if (typeof styles !== "object" || styles === null) {
|
|
532
|
+
throw new Error("样式对象必须是非空对象");
|
|
533
|
+
}
|
|
534
|
+
for (const [styleProp, value] of Object.entries(styles)) {
|
|
535
|
+
this.setStyle(styleProp, value);
|
|
536
|
+
}
|
|
537
|
+
return this;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* 获取当前节点的完整HTML文本
|
|
541
|
+
* @returns {string} HTML字符串
|
|
542
|
+
*/
|
|
543
|
+
getHtml() {
|
|
544
|
+
// 文本节点直接返回文本
|
|
545
|
+
if (this.tagName === "#text") {
|
|
546
|
+
return this.textContent;
|
|
547
|
+
}
|
|
548
|
+
// 片段节点返回所有子节点的HTML拼接
|
|
549
|
+
if (this.tagName === "#fragment") {
|
|
550
|
+
return this.children.map((child) => child.getHtml()).join("");
|
|
551
|
+
}
|
|
552
|
+
// 普通元素节点
|
|
553
|
+
let startTag = `<${this.tagName}`;
|
|
554
|
+
const attrs = { ...this.attributes };
|
|
555
|
+
// 拼接样式属性(覆盖原始style)
|
|
556
|
+
if (Object.keys(this.styles).length > 0) {
|
|
557
|
+
const styleStr = Object.entries(this.styles)
|
|
558
|
+
.map(([key, val]) => `${camelToKebab(key)}: ${val}`)
|
|
559
|
+
.join("; ");
|
|
560
|
+
attrs.style = styleStr;
|
|
561
|
+
}
|
|
562
|
+
// 拼接所有属性:过滤内部属性styleObj,确保值是字符串
|
|
563
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
564
|
+
if (key === "styleObj")
|
|
565
|
+
continue; // 核心修复:跳过内部样式对象属性
|
|
566
|
+
if (typeof val === "string") {
|
|
567
|
+
const safeVal = val.replace(/"/g, """);
|
|
568
|
+
startTag += ` ${key}="${safeVal}"`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// 自闭合标签处理
|
|
572
|
+
if (SELF_CLOSING_TAGS.includes(this.tagName)) {
|
|
573
|
+
startTag += "/>";
|
|
574
|
+
return startTag;
|
|
575
|
+
}
|
|
576
|
+
startTag += ">";
|
|
577
|
+
// 拼接内容(文本+子节点HTML)
|
|
578
|
+
let content = this.textContent;
|
|
579
|
+
if (this.children.length > 0) {
|
|
580
|
+
content += this.children.map((child) => child.getHtml()).join("");
|
|
581
|
+
}
|
|
582
|
+
return `${startTag}${content}</${this.tagName}>`;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
_a = Node, _Node_instances = new WeakSet(), _Node_generateHTMLFromData = function _Node_generateHTMLFromData(nodeData) {
|
|
586
|
+
if (nodeData.tagName === "#text")
|
|
587
|
+
return nodeData.textContent;
|
|
588
|
+
// 构建开始标签
|
|
589
|
+
let startTag = `<${nodeData.tagName}`;
|
|
590
|
+
const attrs = { ...nodeData.attributes };
|
|
591
|
+
// 拼接样式属性(覆盖原始style)
|
|
592
|
+
if (Object.keys(nodeData.styles).length > 0) {
|
|
593
|
+
const styleStr = Object.entries(nodeData.styles)
|
|
594
|
+
.map(([key, val]) => `${camelToKebab(key)}: ${val}`)
|
|
595
|
+
.join("; ");
|
|
596
|
+
attrs.style = styleStr;
|
|
597
|
+
}
|
|
598
|
+
// 拼接所有属性:过滤内部属性styleObj
|
|
599
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
600
|
+
if (key === "styleObj")
|
|
601
|
+
continue; // 核心修复:跳过内部样式对象属性
|
|
602
|
+
// 确保val是字符串(IAttributeData中除了styleObj都是string)
|
|
603
|
+
if (typeof val === "string") {
|
|
604
|
+
startTag += ` ${key}="${val}"`;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// 自闭合标签处理
|
|
608
|
+
if (SELF_CLOSING_TAGS.includes(nodeData.tagName)) {
|
|
609
|
+
startTag += "/>";
|
|
610
|
+
return startTag;
|
|
611
|
+
}
|
|
612
|
+
startTag += ">";
|
|
613
|
+
// 拼接内容(文本+子节点)
|
|
614
|
+
let content = nodeData.textContent;
|
|
615
|
+
if (nodeData.children.length > 0) {
|
|
616
|
+
content += nodeData.children.map((child) => __classPrivateFieldGet(this, _Node_instances, "m", _Node_generateHTMLFromData).call(this, child)).join("");
|
|
617
|
+
}
|
|
618
|
+
return `${startTag}${content}</${nodeData.tagName}>`;
|
|
619
|
+
}, _Node_convertToNode = function _Node_convertToNode(node) {
|
|
620
|
+
if (node instanceof _a)
|
|
621
|
+
return node;
|
|
622
|
+
if (typeof node === "string")
|
|
623
|
+
return new _a(node);
|
|
624
|
+
throw new Error("插入的节点必须是HTML字符串或Node实例");
|
|
625
|
+
};
|
|
626
|
+
exports.default = Node;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "parse_html_to_node",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "This is an HTML string parsed into an object, which does not depend on a browser environment and can be used in any JavaScript runtime.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Easy-Martin/parse_html.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Easy-Martin/parse_html",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Easy-Martin/parse_html/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"parse_html",
|
|
29
|
+
"html",
|
|
30
|
+
"parser"
|
|
31
|
+
],
|
|
32
|
+
"author": "Huang Jingjing",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tslib": "^2.8.1",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|