@tkeron/html-parser 0.1.0
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/.github/workflows/npm_deploy.yml +24 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/bun.lock +29 -0
- package/index.ts +18 -0
- package/package.json +25 -0
- package/src/css-selector.ts +172 -0
- package/src/dom-simulator.ts +592 -0
- package/src/dom-types.ts +78 -0
- package/src/parser.ts +355 -0
- package/src/tokenizer.ts +413 -0
- package/tests/advanced.test.ts +487 -0
- package/tests/api-integration.test.ts +114 -0
- package/tests/dom-extended.test.ts +173 -0
- package/tests/dom.test.ts +482 -0
- package/tests/google-dom.test.ts +118 -0
- package/tests/google-homepage.txt +13 -0
- package/tests/official/README.md +87 -0
- package/tests/official/acid/acid-tests.test.ts +309 -0
- package/tests/official/final-output/final-output.test.ts +361 -0
- package/tests/official/html5lib/tokenizer-utils.ts +204 -0
- package/tests/official/html5lib/tokenizer.test.ts +184 -0
- package/tests/official/html5lib/tree-construction-utils.ts +208 -0
- package/tests/official/html5lib/tree-construction.test.ts +250 -0
- package/tests/official/validator/validator-tests.test.ts +237 -0
- package/tests/official/validator-nu/validator-nu.test.ts +335 -0
- package/tests/official/whatwg/whatwg-tests.test.ts +205 -0
- package/tests/official/wpt/wpt-tests.test.ts +409 -0
- package/tests/parser.test.ts +642 -0
- package/tests/selectors.test.ts +65 -0
- package/tests/test-page-0.txt +362 -0
- package/tests/tokenizer.test.ts +666 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import type { ASTNode, ASTNodeType } from "./parser.js";
|
|
2
|
+
import { parse } from "./parser.js";
|
|
3
|
+
import { tokenize } from "./tokenizer.js";
|
|
4
|
+
import {
|
|
5
|
+
querySelector as querySelectorFunction,
|
|
6
|
+
querySelectorAll as querySelectorAllFunction,
|
|
7
|
+
} from "./css-selector.js";
|
|
8
|
+
|
|
9
|
+
export const enum NodeType {
|
|
10
|
+
ELEMENT_NODE = 1,
|
|
11
|
+
TEXT_NODE = 3,
|
|
12
|
+
COMMENT_NODE = 8,
|
|
13
|
+
DOCUMENT_NODE = 9,
|
|
14
|
+
DOCUMENT_TYPE_NODE = 10,
|
|
15
|
+
PROCESSING_INSTRUCTION_NODE = 7,
|
|
16
|
+
CDATA_SECTION_NODE = 4,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createElement(
|
|
20
|
+
tagName: string,
|
|
21
|
+
attributes: Record<string, string> = {}
|
|
22
|
+
): any {
|
|
23
|
+
const innerHTML = "";
|
|
24
|
+
const tagNameLower = tagName.toLowerCase();
|
|
25
|
+
const outerHTML = `<${tagNameLower}${Object.entries(attributes)
|
|
26
|
+
.map(([k, v]) => ` ${k}="${v}"`)
|
|
27
|
+
.join("")}></${tagNameLower}>`;
|
|
28
|
+
const textContent = "";
|
|
29
|
+
|
|
30
|
+
const element: any = {
|
|
31
|
+
nodeType: NodeType.ELEMENT_NODE,
|
|
32
|
+
nodeName: tagName.toUpperCase(),
|
|
33
|
+
nodeValue: null,
|
|
34
|
+
tagName: tagName.toUpperCase(),
|
|
35
|
+
attributes: { ...attributes },
|
|
36
|
+
childNodes: [],
|
|
37
|
+
children: [],
|
|
38
|
+
textContent,
|
|
39
|
+
innerHTML,
|
|
40
|
+
outerHTML,
|
|
41
|
+
parentNode: null,
|
|
42
|
+
parentElement: null,
|
|
43
|
+
firstChild: null,
|
|
44
|
+
lastChild: null,
|
|
45
|
+
nextSibling: null,
|
|
46
|
+
previousSibling: null,
|
|
47
|
+
firstElementChild: null,
|
|
48
|
+
lastElementChild: null,
|
|
49
|
+
nextElementSibling: null,
|
|
50
|
+
previousElementSibling: null,
|
|
51
|
+
|
|
52
|
+
appendChild(child: any): any {
|
|
53
|
+
appendChild(element, child);
|
|
54
|
+
return child;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
removeChild(child: any): any {
|
|
58
|
+
const index = element.childNodes.indexOf(child);
|
|
59
|
+
if (index === -1) {
|
|
60
|
+
throw new Error("Child not found");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
element.childNodes.splice(index, 1);
|
|
64
|
+
|
|
65
|
+
if (child.previousSibling) {
|
|
66
|
+
child.previousSibling.nextSibling = child.nextSibling;
|
|
67
|
+
}
|
|
68
|
+
if (child.nextSibling) {
|
|
69
|
+
child.nextSibling.previousSibling = child.previousSibling;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (element.firstChild === child) {
|
|
73
|
+
element.firstChild = child.nextSibling;
|
|
74
|
+
}
|
|
75
|
+
if (element.lastChild === child) {
|
|
76
|
+
element.lastChild = child.previousSibling;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (child.nodeType === NodeType.ELEMENT_NODE) {
|
|
80
|
+
const childElement = child;
|
|
81
|
+
const elemIndex = element.children.indexOf(childElement);
|
|
82
|
+
if (elemIndex !== -1) {
|
|
83
|
+
element.children.splice(elemIndex, 1);
|
|
84
|
+
|
|
85
|
+
if (childElement.previousElementSibling) {
|
|
86
|
+
childElement.previousElementSibling.nextElementSibling =
|
|
87
|
+
childElement.nextElementSibling;
|
|
88
|
+
}
|
|
89
|
+
if (childElement.nextElementSibling) {
|
|
90
|
+
childElement.nextElementSibling.previousElementSibling =
|
|
91
|
+
childElement.previousElementSibling;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (element.firstElementChild === childElement) {
|
|
95
|
+
element.firstElementChild = childElement.nextElementSibling;
|
|
96
|
+
}
|
|
97
|
+
if (element.lastElementChild === childElement) {
|
|
98
|
+
element.lastElementChild = childElement.previousElementSibling;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
child.parentNode = null;
|
|
104
|
+
if (child.nodeType === NodeType.ELEMENT_NODE) {
|
|
105
|
+
child.parentElement = null;
|
|
106
|
+
}
|
|
107
|
+
child.previousSibling = null;
|
|
108
|
+
child.nextSibling = null;
|
|
109
|
+
|
|
110
|
+
updateElementContent(element);
|
|
111
|
+
return child;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
setAttribute(name: string, value: string): void {
|
|
115
|
+
element.attributes[name] = value;
|
|
116
|
+
updateElementContent(element);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
getAttribute(name: string): string | null {
|
|
120
|
+
return element.attributes[name] || null;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
hasAttribute(name: string): boolean {
|
|
124
|
+
return name in element.attributes;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
removeAttribute(name: string): void {
|
|
128
|
+
delete element.attributes[name];
|
|
129
|
+
updateElementContent(element);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
querySelector(selector: string): any {
|
|
133
|
+
return querySelectorFunction(element, selector);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
querySelectorAll(selector: string): any[] {
|
|
137
|
+
return querySelectorAllFunction(element, selector);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
cloneNode(deep: boolean = false): any {
|
|
141
|
+
return cloneNode(element, deep);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
Object.defineProperty(element, "textContent", {
|
|
146
|
+
get() {
|
|
147
|
+
return (element as any)._internalTextContent || getTextContent(element);
|
|
148
|
+
},
|
|
149
|
+
set(value: string) {
|
|
150
|
+
setTextContent(element, value);
|
|
151
|
+
},
|
|
152
|
+
enumerable: true,
|
|
153
|
+
configurable: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
Object.defineProperty(element, "innerHTML", {
|
|
157
|
+
get() {
|
|
158
|
+
return (element as any)._internalInnerHTML || getInnerHTML(element);
|
|
159
|
+
},
|
|
160
|
+
set(value: string) {
|
|
161
|
+
setInnerHTML(element, value);
|
|
162
|
+
},
|
|
163
|
+
enumerable: true,
|
|
164
|
+
configurable: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Add className property
|
|
168
|
+
Object.defineProperty(element, "className", {
|
|
169
|
+
get() {
|
|
170
|
+
return element.attributes.class || "";
|
|
171
|
+
},
|
|
172
|
+
set(value: string) {
|
|
173
|
+
element.attributes.class = value;
|
|
174
|
+
},
|
|
175
|
+
enumerable: true,
|
|
176
|
+
configurable: true,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Add id property
|
|
180
|
+
Object.defineProperty(element, "id", {
|
|
181
|
+
get() {
|
|
182
|
+
return element.attributes.id || "";
|
|
183
|
+
},
|
|
184
|
+
set(value: string) {
|
|
185
|
+
element.attributes.id = value;
|
|
186
|
+
},
|
|
187
|
+
enumerable: true,
|
|
188
|
+
configurable: true,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return element;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createTextNode(content: string): any {
|
|
195
|
+
const textNode: any = {
|
|
196
|
+
nodeType: NodeType.TEXT_NODE,
|
|
197
|
+
nodeName: "#text",
|
|
198
|
+
nodeValue: content,
|
|
199
|
+
textContent: content,
|
|
200
|
+
data: content,
|
|
201
|
+
childNodes: [],
|
|
202
|
+
parentNode: null,
|
|
203
|
+
firstChild: null,
|
|
204
|
+
lastChild: null,
|
|
205
|
+
nextSibling: null,
|
|
206
|
+
previousSibling: null,
|
|
207
|
+
};
|
|
208
|
+
return textNode;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createComment(content: string): any {
|
|
212
|
+
const commentNode: any = {
|
|
213
|
+
nodeType: NodeType.COMMENT_NODE,
|
|
214
|
+
nodeName: "#comment",
|
|
215
|
+
nodeValue: content,
|
|
216
|
+
textContent: "",
|
|
217
|
+
data: content,
|
|
218
|
+
childNodes: [],
|
|
219
|
+
parentNode: null,
|
|
220
|
+
firstChild: null,
|
|
221
|
+
lastChild: null,
|
|
222
|
+
nextSibling: null,
|
|
223
|
+
previousSibling: null,
|
|
224
|
+
};
|
|
225
|
+
return commentNode;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function createDocument(): any {
|
|
229
|
+
const document: any = {
|
|
230
|
+
nodeType: NodeType.DOCUMENT_NODE,
|
|
231
|
+
nodeName: "#document",
|
|
232
|
+
nodeValue: null,
|
|
233
|
+
textContent: "",
|
|
234
|
+
childNodes: [],
|
|
235
|
+
parentNode: null,
|
|
236
|
+
firstChild: null,
|
|
237
|
+
lastChild: null,
|
|
238
|
+
nextSibling: null,
|
|
239
|
+
previousSibling: null,
|
|
240
|
+
documentElement: null,
|
|
241
|
+
body: null,
|
|
242
|
+
head: null,
|
|
243
|
+
|
|
244
|
+
createElement(tagName: string): any {
|
|
245
|
+
return createElement(tagName, {});
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
createTextNode(data: string): any {
|
|
249
|
+
return createTextNode(data);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
querySelector(selector: string): any {
|
|
253
|
+
return querySelectorFunction(document, selector);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
querySelectorAll(selector: string): any[] {
|
|
257
|
+
return querySelectorAllFunction(document, selector);
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
getElementById(id: string): any {
|
|
261
|
+
return querySelectorFunction(document, `#${id}`);
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
return document;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function astToDOM(ast: ASTNode): Document {
|
|
268
|
+
const document = createDocument();
|
|
269
|
+
if (ast.children) {
|
|
270
|
+
for (const child of ast.children) {
|
|
271
|
+
const Node = convertASTNodeToDOM(child);
|
|
272
|
+
if (Node) {
|
|
273
|
+
appendChild(document, Node);
|
|
274
|
+
if (Node.nodeType === NodeType.ELEMENT_NODE) {
|
|
275
|
+
const element = Node as any;
|
|
276
|
+
if (element.tagName === "HTML") {
|
|
277
|
+
document.documentElement = element;
|
|
278
|
+
findSpecialElements(document, element);
|
|
279
|
+
} else if (element.tagName === "BODY") {
|
|
280
|
+
document.body = element;
|
|
281
|
+
} else if (element.tagName === "HEAD") {
|
|
282
|
+
document.head = element;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!document.body) {
|
|
290
|
+
const bodyElement = createElement("body");
|
|
291
|
+
document.body = bodyElement;
|
|
292
|
+
|
|
293
|
+
bodyElement.querySelector = function (selector: string) {
|
|
294
|
+
return querySelectorFunction(document, selector);
|
|
295
|
+
};
|
|
296
|
+
bodyElement.querySelectorAll = function (selector: string) {
|
|
297
|
+
return querySelectorAllFunction(document, selector);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return document;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function findSpecialElements(document: any, htmlElement: any): void {
|
|
305
|
+
for (const child of htmlElement.childNodes) {
|
|
306
|
+
if (child.nodeType === NodeType.ELEMENT_NODE) {
|
|
307
|
+
const element = child;
|
|
308
|
+
if (element.tagName === "BODY") {
|
|
309
|
+
document.body = element;
|
|
310
|
+
} else if (element.tagName === "HEAD") {
|
|
311
|
+
document.head = element;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function convertASTNodeToDOM(astNode: ASTNode): any {
|
|
318
|
+
switch (astNode.type) {
|
|
319
|
+
case "ELEMENT":
|
|
320
|
+
const tagName = astNode.tagName || "div";
|
|
321
|
+
const element = createElement(tagName, astNode.attributes || {});
|
|
322
|
+
|
|
323
|
+
if (astNode.children) {
|
|
324
|
+
for (const child of astNode.children) {
|
|
325
|
+
const domChild = convertASTNodeToDOM(child);
|
|
326
|
+
if (domChild) {
|
|
327
|
+
appendChild(element, domChild);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
updateElementContent(element);
|
|
333
|
+
return element;
|
|
334
|
+
case "TEXT":
|
|
335
|
+
return createTextNode(astNode.content || "");
|
|
336
|
+
case "COMMENT":
|
|
337
|
+
return createComment(astNode.content || "");
|
|
338
|
+
case "CDATA":
|
|
339
|
+
return createTextNode(astNode.content || "");
|
|
340
|
+
case "DOCTYPE":
|
|
341
|
+
case "PROCESSING_INSTRUCTION":
|
|
342
|
+
return null;
|
|
343
|
+
default:
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function appendChild(parent: any, child: any): void {
|
|
349
|
+
child.parentNode = parent;
|
|
350
|
+
parent.childNodes.push(child);
|
|
351
|
+
|
|
352
|
+
if (parent.childNodes.length > 1) {
|
|
353
|
+
const previousSibling = parent.childNodes[parent.childNodes.length - 2];
|
|
354
|
+
if (previousSibling) {
|
|
355
|
+
previousSibling.nextSibling = child;
|
|
356
|
+
child.previousSibling = previousSibling;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (parent.childNodes.length === 1) {
|
|
361
|
+
parent.firstChild = child;
|
|
362
|
+
}
|
|
363
|
+
parent.lastChild = child;
|
|
364
|
+
|
|
365
|
+
if (
|
|
366
|
+
parent.nodeType === NodeType.ELEMENT_NODE &&
|
|
367
|
+
child.nodeType === NodeType.ELEMENT_NODE
|
|
368
|
+
) {
|
|
369
|
+
const parentElement = parent;
|
|
370
|
+
const childElement = child;
|
|
371
|
+
|
|
372
|
+
childElement.parentElement = parentElement;
|
|
373
|
+
parentElement.children.push(childElement);
|
|
374
|
+
|
|
375
|
+
if (parentElement.children.length === 1) {
|
|
376
|
+
parentElement.firstElementChild = childElement;
|
|
377
|
+
}
|
|
378
|
+
parentElement.lastElementChild = childElement;
|
|
379
|
+
|
|
380
|
+
if (parentElement.children.length > 1) {
|
|
381
|
+
const previousElementSibling =
|
|
382
|
+
parentElement.children[parentElement.children.length - 2];
|
|
383
|
+
if (previousElementSibling) {
|
|
384
|
+
previousElementSibling.nextElementSibling = childElement;
|
|
385
|
+
childElement.previousElementSibling = previousElementSibling;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (parent.nodeType === NodeType.ELEMENT_NODE) {
|
|
391
|
+
updateElementContent(parent);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function updateElementContent(element: any): void {
|
|
396
|
+
const innerHTML = element.childNodes
|
|
397
|
+
.map((child: any) => {
|
|
398
|
+
if (child.nodeType === NodeType.TEXT_NODE) {
|
|
399
|
+
return child.textContent;
|
|
400
|
+
} else if (child.nodeType === NodeType.ELEMENT_NODE) {
|
|
401
|
+
return child.outerHTML;
|
|
402
|
+
} else if (child.nodeType === NodeType.COMMENT_NODE) {
|
|
403
|
+
return `<!--${child.data}-->`;
|
|
404
|
+
}
|
|
405
|
+
return "";
|
|
406
|
+
})
|
|
407
|
+
.join("");
|
|
408
|
+
|
|
409
|
+
Object.defineProperty(element, "_internalInnerHTML", {
|
|
410
|
+
value: innerHTML,
|
|
411
|
+
writable: true,
|
|
412
|
+
enumerable: false,
|
|
413
|
+
configurable: true,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const attrs = Object.entries(element.attributes)
|
|
417
|
+
.map(([k, v]) => ` ${k}="${v}"`)
|
|
418
|
+
.join("");
|
|
419
|
+
const tagNameLower = element.tagName.toLowerCase();
|
|
420
|
+
element.outerHTML = `<${tagNameLower}${attrs}>${innerHTML}</${tagNameLower}>`;
|
|
421
|
+
|
|
422
|
+
const computedTextContent = getTextContent(element);
|
|
423
|
+
Object.defineProperty(element, "_internalTextContent", {
|
|
424
|
+
value: computedTextContent,
|
|
425
|
+
writable: true,
|
|
426
|
+
enumerable: false,
|
|
427
|
+
configurable: true,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function getTextContent(node: any): string {
|
|
432
|
+
if (node.nodeType === NodeType.TEXT_NODE) {
|
|
433
|
+
return node.textContent || "";
|
|
434
|
+
}
|
|
435
|
+
if (
|
|
436
|
+
node.nodeType !== NodeType.ELEMENT_NODE &&
|
|
437
|
+
node.nodeType !== NodeType.DOCUMENT_NODE
|
|
438
|
+
) {
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
let textContent = "";
|
|
442
|
+
for (const child of node.childNodes) {
|
|
443
|
+
textContent += getTextContent(child);
|
|
444
|
+
}
|
|
445
|
+
return textContent;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function getAttribute(element: any, name: string): string | null {
|
|
449
|
+
return element.attributes[name] || null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function hasAttribute(element: any, name: string): boolean {
|
|
453
|
+
return name in element.attributes;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function setAttribute(element: any, name: string, value: string): void {
|
|
457
|
+
element.attributes[name] = value;
|
|
458
|
+
updateElementContent(element);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function removeAttribute(element: any, name: string): void {
|
|
462
|
+
delete element.attributes[name];
|
|
463
|
+
updateElementContent(element);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function setInnerHTML(element: any, html: string): void {
|
|
467
|
+
element.childNodes = [];
|
|
468
|
+
element.children = [];
|
|
469
|
+
element.firstChild = null;
|
|
470
|
+
element.lastChild = null;
|
|
471
|
+
element.firstElementChild = null;
|
|
472
|
+
element.lastElementChild = null;
|
|
473
|
+
|
|
474
|
+
if (html.trim()) {
|
|
475
|
+
const tokens = tokenize(html);
|
|
476
|
+
const ast = parse(tokens);
|
|
477
|
+
if (ast.children) {
|
|
478
|
+
for (const child of ast.children) {
|
|
479
|
+
const domChild = convertASTNodeToDOM(child);
|
|
480
|
+
if (domChild) {
|
|
481
|
+
appendChild(element, domChild);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const actualInnerHTML = getInnerHTML(element);
|
|
488
|
+
Object.defineProperty(element, "_internalInnerHTML", {
|
|
489
|
+
value: actualInnerHTML,
|
|
490
|
+
writable: true,
|
|
491
|
+
enumerable: false,
|
|
492
|
+
configurable: true,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const textContent = getTextContent(element);
|
|
496
|
+
Object.defineProperty(element, "_internalTextContent", {
|
|
497
|
+
value: textContent,
|
|
498
|
+
writable: true,
|
|
499
|
+
enumerable: false,
|
|
500
|
+
configurable: true,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const attrs = Object.entries(element.attributes)
|
|
504
|
+
.map(([k, v]) => ` ${k}="${v}"`)
|
|
505
|
+
.join("");
|
|
506
|
+
const tagNameLower = element.tagName.toLowerCase();
|
|
507
|
+
element.outerHTML = `<${tagNameLower}${attrs}>${actualInnerHTML}</${tagNameLower}>`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function setTextContent(element: any, text: string): void {
|
|
511
|
+
element.childNodes = [];
|
|
512
|
+
element.children = [];
|
|
513
|
+
element.firstChild = null;
|
|
514
|
+
element.lastChild = null;
|
|
515
|
+
element.firstElementChild = null;
|
|
516
|
+
element.lastElementChild = null;
|
|
517
|
+
|
|
518
|
+
if (text) {
|
|
519
|
+
const textNode: any = {
|
|
520
|
+
nodeType: NodeType.TEXT_NODE,
|
|
521
|
+
nodeName: "#text",
|
|
522
|
+
nodeValue: text,
|
|
523
|
+
textContent: text,
|
|
524
|
+
data: text,
|
|
525
|
+
childNodes: [],
|
|
526
|
+
parentNode: element,
|
|
527
|
+
firstChild: null,
|
|
528
|
+
lastChild: null,
|
|
529
|
+
nextSibling: null,
|
|
530
|
+
previousSibling: null,
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
element.childNodes.push(textNode);
|
|
534
|
+
element.firstChild = textNode;
|
|
535
|
+
element.lastChild = textNode;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
updateElementContent(element);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function cloneNode(node: any, deep: boolean = false): any {
|
|
542
|
+
if (node.nodeType === NodeType.ELEMENT_NODE) {
|
|
543
|
+
const element = node;
|
|
544
|
+
const cloned = createElement(element.tagName, element.attributes || {});
|
|
545
|
+
|
|
546
|
+
if (deep && element.childNodes && element.childNodes.length > 0) {
|
|
547
|
+
for (let i = 0; i < element.childNodes.length; i++) {
|
|
548
|
+
const child = element.childNodes[i];
|
|
549
|
+
const childClone = cloneNode(child, true);
|
|
550
|
+
appendChild(cloned, childClone);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return cloned;
|
|
555
|
+
} else if (node.nodeType === NodeType.TEXT_NODE) {
|
|
556
|
+
return createTextNode(node.textContent || "");
|
|
557
|
+
} else if (node.nodeType === NodeType.COMMENT_NODE) {
|
|
558
|
+
return createComment(node.data || "");
|
|
559
|
+
} else if (node.nodeType === NodeType.DOCUMENT_NODE) {
|
|
560
|
+
const doc = createDocument();
|
|
561
|
+
if (deep && node.childNodes && node.childNodes.length > 0) {
|
|
562
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
563
|
+
const child = node.childNodes[i];
|
|
564
|
+
const childClone = cloneNode(child, true);
|
|
565
|
+
appendChild(doc, childClone);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return doc;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return createTextNode("");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function getInnerHTML(element: any): string {
|
|
575
|
+
if (element.nodeType !== NodeType.ELEMENT_NODE) {
|
|
576
|
+
return "";
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let innerHTML = "";
|
|
580
|
+
for (const child of element.childNodes) {
|
|
581
|
+
if (child.nodeType === NodeType.ELEMENT_NODE) {
|
|
582
|
+
innerHTML += child.outerHTML;
|
|
583
|
+
} else if (child.nodeType === NodeType.TEXT_NODE) {
|
|
584
|
+
innerHTML += child.textContent || "";
|
|
585
|
+
} else if (child.nodeType === NodeType.COMMENT_NODE) {
|
|
586
|
+
innerHTML += `<!--${child.data || ""}-->`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return innerHTML;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export { querySelector, querySelectorAll } from "./css-selector.js";
|
package/src/dom-types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface ChildNode extends Node {
|
|
2
|
+
nextSibling: ChildNode | null;
|
|
3
|
+
previousSibling: ChildNode | null;
|
|
4
|
+
parentNode: ParentNode | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ParentNode extends Node {
|
|
8
|
+
childNodes: ChildNode[];
|
|
9
|
+
firstChild: ChildNode | null;
|
|
10
|
+
lastChild: ChildNode | null;
|
|
11
|
+
appendChild(child: ChildNode): ChildNode;
|
|
12
|
+
removeChild(child: ChildNode): ChildNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Text extends ChildNode {
|
|
16
|
+
nodeType: 3;
|
|
17
|
+
nodeName: "#text";
|
|
18
|
+
nodeValue: string;
|
|
19
|
+
textContent: string;
|
|
20
|
+
data: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Comment extends ChildNode {
|
|
24
|
+
nodeType: 8;
|
|
25
|
+
nodeName: "#comment";
|
|
26
|
+
nodeValue: string;
|
|
27
|
+
textContent: string;
|
|
28
|
+
data: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HTMLElement extends ChildNode, ParentNode {
|
|
32
|
+
nodeType: 1;
|
|
33
|
+
tagName: string;
|
|
34
|
+
attributes: Record<string, string>;
|
|
35
|
+
innerHTML: string;
|
|
36
|
+
outerHTML: string;
|
|
37
|
+
textContent: string;
|
|
38
|
+
children: HTMLElement[];
|
|
39
|
+
parentElement: HTMLElement | null;
|
|
40
|
+
firstElementChild: HTMLElement | null;
|
|
41
|
+
lastElementChild: HTMLElement | null;
|
|
42
|
+
nextElementSibling: HTMLElement | null;
|
|
43
|
+
previousElementSibling: HTMLElement | null;
|
|
44
|
+
|
|
45
|
+
getAttribute(name: string): string | null;
|
|
46
|
+
setAttribute(name: string, value: string): void;
|
|
47
|
+
hasAttribute(name: string): boolean;
|
|
48
|
+
removeAttribute(name: string): void;
|
|
49
|
+
querySelector(selector: string): HTMLElement | null;
|
|
50
|
+
querySelectorAll(selector: string): HTMLElement[];
|
|
51
|
+
cloneNode(deep?: boolean): HTMLElement;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Document extends ParentNode {
|
|
55
|
+
nodeType: 9;
|
|
56
|
+
nodeName: "#document";
|
|
57
|
+
documentElement: HTMLElement | null;
|
|
58
|
+
head: HTMLElement | null;
|
|
59
|
+
body: HTMLElement | null;
|
|
60
|
+
|
|
61
|
+
createElement(tagName: string): HTMLElement;
|
|
62
|
+
createTextNode(data: string): Text;
|
|
63
|
+
querySelector(selector: string): HTMLElement | null;
|
|
64
|
+
querySelectorAll(selector: string): HTMLElement[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Node {
|
|
68
|
+
nodeType: number;
|
|
69
|
+
nodeName: string;
|
|
70
|
+
nodeValue: string | null;
|
|
71
|
+
textContent: string;
|
|
72
|
+
childNodes: ChildNode[];
|
|
73
|
+
parentNode: ParentNode | null;
|
|
74
|
+
firstChild: ChildNode | null;
|
|
75
|
+
lastChild: ChildNode | null;
|
|
76
|
+
nextSibling: ChildNode | null;
|
|
77
|
+
previousSibling: ChildNode | null;
|
|
78
|
+
}
|