@tkeron/html-parser 0.1.4 → 0.1.7
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/README.md +6 -6
- package/bun.lock +3 -3
- package/index.ts +0 -5
- package/package.json +7 -6
- package/src/css-selector.ts +45 -32
- package/src/dom-simulator.ts +243 -46
- package/src/parser.ts +0 -39
- package/src/tokenizer.ts +0 -116
- package/tests/advanced.test.ts +2 -2
- package/tests/cloneNode.test.ts +50 -50
- package/tests/custom-elements.test.ts +8 -8
- package/tests/dom-manipulation.test.ts +638 -0
- package/tests/official/acid/acid-tests.test.ts +6 -6
- package/tests/official/final-output/final-output.test.ts +15 -15
- package/tests/official/html5lib/tokenizer-utils.ts +19 -31
- package/tests/official/html5lib/tokenizer.test.ts +4 -4
- package/tests/official/html5lib/tree-construction-utils.ts +20 -34
- package/tests/official/html5lib/tree-construction.test.ts +5 -5
- package/tests/official/validator/validator-tests.test.ts +11 -11
- package/tests/official/wpt/wpt-tests.test.ts +5 -5
- package/tests/outerHTML-replacement.test.ts +208 -0
- package/tests/parser.test.ts +1 -1
- package/tests/selectors.test.ts +64 -1
- package/tests/test-page-0.txt +12 -355
- package/tests/tokenizer.test.ts +86 -0
- package/tests/void-elements.test.ts +471 -0
- package/tests/api-integration.test.ts +0 -114
- package/tests/cloneNode-bug-reproduction.test.ts +0 -325
- package/tests/cloneNode-interactive.ts +0 -235
- package/tests/dom-adoption.test.ts +0 -363
- package/tests/dom-synchronization.test.ts +0 -675
- package/tests/setAttribute-outerHTML.test.ts +0 -102
package/src/dom-simulator.ts
CHANGED
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
querySelectorAll as querySelectorAllFunction,
|
|
7
7
|
} from "./css-selector.js";
|
|
8
8
|
|
|
9
|
+
const VOID_ELEMENTS = new Set([
|
|
10
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
11
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
12
|
+
]);
|
|
13
|
+
|
|
9
14
|
export const enum NodeType {
|
|
10
15
|
ELEMENT_NODE = 1,
|
|
11
16
|
TEXT_NODE = 3,
|
|
@@ -22,9 +27,13 @@ export function createElement(
|
|
|
22
27
|
): any {
|
|
23
28
|
const innerHTML = "";
|
|
24
29
|
const tagNameLower = tagName.toLowerCase();
|
|
25
|
-
const
|
|
30
|
+
const isVoid = VOID_ELEMENTS.has(tagNameLower);
|
|
31
|
+
const attrsStr = Object.entries(attributes)
|
|
26
32
|
.map(([k, v]) => ` ${k}="${v}"`)
|
|
27
|
-
.join("")
|
|
33
|
+
.join("");
|
|
34
|
+
const initialOuterHTML = isVoid
|
|
35
|
+
? `<${tagNameLower}${attrsStr}>`
|
|
36
|
+
: `<${tagNameLower}${attrsStr}></${tagNameLower}>`;
|
|
28
37
|
const textContent = "";
|
|
29
38
|
|
|
30
39
|
const element: any = {
|
|
@@ -37,7 +46,7 @@ export function createElement(
|
|
|
37
46
|
children: [],
|
|
38
47
|
textContent,
|
|
39
48
|
innerHTML,
|
|
40
|
-
|
|
49
|
+
_internalOuterHTML: initialOuterHTML,
|
|
41
50
|
parentNode: null,
|
|
42
51
|
parentElement: null,
|
|
43
52
|
firstChild: null,
|
|
@@ -54,6 +63,18 @@ export function createElement(
|
|
|
54
63
|
return child;
|
|
55
64
|
},
|
|
56
65
|
|
|
66
|
+
prepend(...nodes: any[]): void {
|
|
67
|
+
prepend(element, ...nodes);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
append(...nodes: any[]): void {
|
|
71
|
+
append(element, ...nodes);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
remove(): void {
|
|
75
|
+
remove(element);
|
|
76
|
+
},
|
|
77
|
+
|
|
57
78
|
removeChild(child: any): any {
|
|
58
79
|
return removeChild(element, child);
|
|
59
80
|
},
|
|
@@ -96,6 +117,10 @@ export function createElement(
|
|
|
96
117
|
return querySelectorAllFunction(element, selector);
|
|
97
118
|
},
|
|
98
119
|
|
|
120
|
+
matches(selector: string): boolean {
|
|
121
|
+
return matches(element, selector);
|
|
122
|
+
},
|
|
123
|
+
|
|
99
124
|
cloneNode(deep: boolean = false): any {
|
|
100
125
|
return cloneNode(element, deep);
|
|
101
126
|
},
|
|
@@ -123,7 +148,6 @@ export function createElement(
|
|
|
123
148
|
configurable: true,
|
|
124
149
|
});
|
|
125
150
|
|
|
126
|
-
// Add className property
|
|
127
151
|
Object.defineProperty(element, "className", {
|
|
128
152
|
get() {
|
|
129
153
|
return element.attributes.class || "";
|
|
@@ -135,7 +159,6 @@ export function createElement(
|
|
|
135
159
|
configurable: true,
|
|
136
160
|
});
|
|
137
161
|
|
|
138
|
-
// Add id property
|
|
139
162
|
Object.defineProperty(element, "id", {
|
|
140
163
|
get() {
|
|
141
164
|
return element.attributes.id || "";
|
|
@@ -147,6 +170,17 @@ export function createElement(
|
|
|
147
170
|
configurable: true,
|
|
148
171
|
});
|
|
149
172
|
|
|
173
|
+
Object.defineProperty(element, "outerHTML", {
|
|
174
|
+
get() {
|
|
175
|
+
return element._internalOuterHTML || "";
|
|
176
|
+
},
|
|
177
|
+
set(value: string) {
|
|
178
|
+
setOuterHTML(element, value);
|
|
179
|
+
},
|
|
180
|
+
enumerable: true,
|
|
181
|
+
configurable: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
150
184
|
return element;
|
|
151
185
|
}
|
|
152
186
|
|
|
@@ -163,6 +197,10 @@ export function createTextNode(content: string): any {
|
|
|
163
197
|
lastChild: null,
|
|
164
198
|
nextSibling: null,
|
|
165
199
|
previousSibling: null,
|
|
200
|
+
|
|
201
|
+
remove(): void {
|
|
202
|
+
remove(textNode);
|
|
203
|
+
},
|
|
166
204
|
};
|
|
167
205
|
return textNode;
|
|
168
206
|
}
|
|
@@ -180,6 +218,10 @@ export function createComment(content: string): any {
|
|
|
180
218
|
lastChild: null,
|
|
181
219
|
nextSibling: null,
|
|
182
220
|
previousSibling: null,
|
|
221
|
+
|
|
222
|
+
remove(): void {
|
|
223
|
+
remove(commentNode);
|
|
224
|
+
},
|
|
183
225
|
};
|
|
184
226
|
return commentNode;
|
|
185
227
|
}
|
|
@@ -213,6 +255,14 @@ export function createDocument(): any {
|
|
|
213
255
|
return child;
|
|
214
256
|
},
|
|
215
257
|
|
|
258
|
+
prepend(...nodes: any[]): void {
|
|
259
|
+
prepend(document, ...nodes);
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
append(...nodes: any[]): void {
|
|
263
|
+
append(document, ...nodes);
|
|
264
|
+
},
|
|
265
|
+
|
|
216
266
|
removeChild(child: any): any {
|
|
217
267
|
return removeChild(document, child);
|
|
218
268
|
},
|
|
@@ -326,8 +376,6 @@ function convertASTNodeToDOM(astNode: ASTNode): any {
|
|
|
326
376
|
}
|
|
327
377
|
|
|
328
378
|
function appendChild(parent: any, child: any): void {
|
|
329
|
-
// Check for hierarchy request error: prevent circular references
|
|
330
|
-
// Check if parent is a descendant of child
|
|
331
379
|
if (child.nodeType === NodeType.ELEMENT_NODE || child.nodeType === NodeType.DOCUMENT_NODE) {
|
|
332
380
|
let ancestor = parent;
|
|
333
381
|
while (ancestor) {
|
|
@@ -338,7 +386,6 @@ function appendChild(parent: any, child: any): void {
|
|
|
338
386
|
}
|
|
339
387
|
}
|
|
340
388
|
|
|
341
|
-
// Remove child from its current parent if it has one
|
|
342
389
|
if (child.parentNode) {
|
|
343
390
|
removeChild(child.parentNode, child);
|
|
344
391
|
}
|
|
@@ -389,6 +436,83 @@ function appendChild(parent: any, child: any): void {
|
|
|
389
436
|
}
|
|
390
437
|
}
|
|
391
438
|
|
|
439
|
+
function prepend(parent: any, ...nodes: any[]): void {
|
|
440
|
+
if (nodes.length === 0) return;
|
|
441
|
+
|
|
442
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
443
|
+
const node = nodes[i];
|
|
444
|
+
let childNode: any;
|
|
445
|
+
|
|
446
|
+
if (typeof node === 'string') {
|
|
447
|
+
childNode = createTextNode(node);
|
|
448
|
+
} else {
|
|
449
|
+
childNode = node;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (parent.firstChild) {
|
|
453
|
+
insertBefore(parent, childNode, parent.firstChild);
|
|
454
|
+
} else {
|
|
455
|
+
appendChild(parent, childNode);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function append(parent: any, ...nodes: any[]): void {
|
|
461
|
+
if (nodes.length === 0) return;
|
|
462
|
+
|
|
463
|
+
for (const node of nodes) {
|
|
464
|
+
let childNode: any;
|
|
465
|
+
|
|
466
|
+
if (typeof node === 'string') {
|
|
467
|
+
childNode = createTextNode(node);
|
|
468
|
+
} else {
|
|
469
|
+
childNode = node;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
appendChild(parent, childNode);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function remove(node: any): void {
|
|
477
|
+
if (node.parentNode) {
|
|
478
|
+
removeChild(node.parentNode, node);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function matches(element: any, selector: string): boolean {
|
|
483
|
+
if (!selector || element.nodeType !== NodeType.ELEMENT_NODE) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
// Para selectores complejos con descendientes, necesitamos buscar desde un ancestro
|
|
489
|
+
if (selector.includes(' ') || selector.includes('>')) {
|
|
490
|
+
// Buscar desde la raíz del documento
|
|
491
|
+
let root = element;
|
|
492
|
+
while (root.parentNode) {
|
|
493
|
+
root = root.parentNode;
|
|
494
|
+
}
|
|
495
|
+
const results = querySelectorAllFunction(root, selector);
|
|
496
|
+
return results.includes(element);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Para selectores simples, usar el padre o crear uno temporal
|
|
500
|
+
const parent = element.parentNode || createTempParent(element);
|
|
501
|
+
const results = querySelectorAllFunction(parent, selector);
|
|
502
|
+
return results.includes(element);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function createTempParent(element: any): any {
|
|
509
|
+
const temp = createElement('div');
|
|
510
|
+
temp.childNodes.push(element);
|
|
511
|
+
temp.children.push(element);
|
|
512
|
+
element._tempParent = temp;
|
|
513
|
+
return temp;
|
|
514
|
+
}
|
|
515
|
+
|
|
392
516
|
function removeChild(parent: any, child: any): any {
|
|
393
517
|
const index = parent.childNodes.indexOf(child);
|
|
394
518
|
if (index === -1) {
|
|
@@ -411,7 +535,6 @@ function removeChild(parent: any, child: any): any {
|
|
|
411
535
|
parent.lastChild = child.previousSibling;
|
|
412
536
|
}
|
|
413
537
|
|
|
414
|
-
// Only handle element-specific relationships if parent is an element
|
|
415
538
|
if (parent.nodeType === NodeType.ELEMENT_NODE && child.nodeType === NodeType.ELEMENT_NODE) {
|
|
416
539
|
const childElement = child;
|
|
417
540
|
const elemIndex = parent.children.indexOf(childElement);
|
|
@@ -454,19 +577,16 @@ function removeChild(parent: any, child: any): any {
|
|
|
454
577
|
}
|
|
455
578
|
|
|
456
579
|
function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
457
|
-
// If referenceNode is null, append to the end
|
|
458
580
|
if (referenceNode === null) {
|
|
459
581
|
appendChild(parent, newNode);
|
|
460
582
|
return newNode;
|
|
461
583
|
}
|
|
462
584
|
|
|
463
|
-
// Verify referenceNode is actually a child of parent
|
|
464
585
|
const refIndex = parent.childNodes.indexOf(referenceNode);
|
|
465
586
|
if (refIndex === -1) {
|
|
466
587
|
throw new Error("Reference node is not a child of this node");
|
|
467
588
|
}
|
|
468
589
|
|
|
469
|
-
// Check for hierarchy request error: prevent circular references
|
|
470
590
|
if (newNode.nodeType === NodeType.ELEMENT_NODE || newNode.nodeType === NodeType.DOCUMENT_NODE) {
|
|
471
591
|
let ancestor = parent;
|
|
472
592
|
while (ancestor) {
|
|
@@ -477,16 +597,13 @@ function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
|
477
597
|
}
|
|
478
598
|
}
|
|
479
599
|
|
|
480
|
-
// Remove newNode from its current parent if it has one
|
|
481
600
|
if (newNode.parentNode) {
|
|
482
601
|
removeChild(newNode.parentNode, newNode);
|
|
483
602
|
}
|
|
484
603
|
|
|
485
|
-
// Insert into childNodes
|
|
486
604
|
parent.childNodes.splice(refIndex, 0, newNode);
|
|
487
605
|
newNode.parentNode = parent;
|
|
488
606
|
|
|
489
|
-
// Update sibling relationships for all nodes
|
|
490
607
|
newNode.previousSibling = referenceNode.previousSibling;
|
|
491
608
|
newNode.nextSibling = referenceNode;
|
|
492
609
|
|
|
@@ -495,12 +612,10 @@ function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
|
495
612
|
}
|
|
496
613
|
referenceNode.previousSibling = newNode;
|
|
497
614
|
|
|
498
|
-
// Update firstChild if inserting at the beginning
|
|
499
615
|
if (parent.firstChild === referenceNode) {
|
|
500
616
|
parent.firstChild = newNode;
|
|
501
617
|
}
|
|
502
618
|
|
|
503
|
-
// Handle element-specific relationships
|
|
504
619
|
if (
|
|
505
620
|
parent.nodeType === NodeType.ELEMENT_NODE &&
|
|
506
621
|
newNode.nodeType === NodeType.ELEMENT_NODE
|
|
@@ -510,12 +625,10 @@ function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
|
510
625
|
|
|
511
626
|
newElement.parentElement = parentElement;
|
|
512
627
|
|
|
513
|
-
// Find the reference node in the children array
|
|
514
628
|
let refElementIndex = -1;
|
|
515
629
|
if (referenceNode.nodeType === NodeType.ELEMENT_NODE) {
|
|
516
630
|
refElementIndex = parentElement.children.indexOf(referenceNode);
|
|
517
631
|
} else {
|
|
518
|
-
// Find the next element sibling
|
|
519
632
|
let nextElement = referenceNode.nextSibling;
|
|
520
633
|
while (nextElement && nextElement.nodeType !== NodeType.ELEMENT_NODE) {
|
|
521
634
|
nextElement = nextElement.nextSibling;
|
|
@@ -526,14 +639,11 @@ function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
|
526
639
|
}
|
|
527
640
|
|
|
528
641
|
if (refElementIndex === -1) {
|
|
529
|
-
// No element siblings after, append to children
|
|
530
642
|
parentElement.children.push(newElement);
|
|
531
643
|
} else {
|
|
532
|
-
// Insert before the reference element
|
|
533
644
|
parentElement.children.splice(refElementIndex, 0, newElement);
|
|
534
645
|
}
|
|
535
646
|
|
|
536
|
-
// Update element sibling relationships
|
|
537
647
|
const newElemIndex = parentElement.children.indexOf(newElement);
|
|
538
648
|
newElement.previousElementSibling =
|
|
539
649
|
newElemIndex > 0 ? parentElement.children[newElemIndex - 1] : null;
|
|
@@ -549,12 +659,9 @@ function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
|
549
659
|
newElement.nextElementSibling.previousElementSibling = newElement;
|
|
550
660
|
}
|
|
551
661
|
|
|
552
|
-
// Update firstElementChild if needed
|
|
553
662
|
if (newElemIndex === 0) {
|
|
554
663
|
parentElement.firstElementChild = newElement;
|
|
555
664
|
}
|
|
556
|
-
|
|
557
|
-
// lastElementChild is not affected since we're inserting before
|
|
558
665
|
}
|
|
559
666
|
|
|
560
667
|
if (parent.nodeType === NodeType.ELEMENT_NODE) {
|
|
@@ -565,13 +672,11 @@ function insertBefore(parent: any, newNode: any, referenceNode: any): any {
|
|
|
565
672
|
}
|
|
566
673
|
|
|
567
674
|
function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
568
|
-
// Verify oldChild is actually a child of parent
|
|
569
675
|
const oldIndex = parent.childNodes.indexOf(oldChild);
|
|
570
676
|
if (oldIndex === -1) {
|
|
571
677
|
throw new Error("Old child is not a child of this node");
|
|
572
678
|
}
|
|
573
679
|
|
|
574
|
-
// Check for hierarchy request error: prevent circular references
|
|
575
680
|
if (newChild.nodeType === NodeType.ELEMENT_NODE || newChild.nodeType === NodeType.DOCUMENT_NODE) {
|
|
576
681
|
let ancestor = parent;
|
|
577
682
|
while (ancestor) {
|
|
@@ -582,16 +687,13 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
582
687
|
}
|
|
583
688
|
}
|
|
584
689
|
|
|
585
|
-
// Remove newChild from its current parent if it has one
|
|
586
690
|
if (newChild.parentNode) {
|
|
587
691
|
removeChild(newChild.parentNode, newChild);
|
|
588
692
|
}
|
|
589
693
|
|
|
590
|
-
// Replace in childNodes array
|
|
591
694
|
parent.childNodes[oldIndex] = newChild;
|
|
592
695
|
newChild.parentNode = parent;
|
|
593
696
|
|
|
594
|
-
// Transfer sibling relationships
|
|
595
697
|
newChild.previousSibling = oldChild.previousSibling;
|
|
596
698
|
newChild.nextSibling = oldChild.nextSibling;
|
|
597
699
|
|
|
@@ -602,7 +704,6 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
602
704
|
oldChild.nextSibling.previousSibling = newChild;
|
|
603
705
|
}
|
|
604
706
|
|
|
605
|
-
// Update first/last child if needed
|
|
606
707
|
if (parent.firstChild === oldChild) {
|
|
607
708
|
parent.firstChild = newChild;
|
|
608
709
|
}
|
|
@@ -610,20 +711,16 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
610
711
|
parent.lastChild = newChild;
|
|
611
712
|
}
|
|
612
713
|
|
|
613
|
-
// Handle element-specific relationships
|
|
614
714
|
if (parent.nodeType === NodeType.ELEMENT_NODE) {
|
|
615
715
|
const parentElement = parent;
|
|
616
716
|
|
|
617
|
-
// Remove old element from children if it's an element
|
|
618
717
|
if (oldChild.nodeType === NodeType.ELEMENT_NODE) {
|
|
619
718
|
const oldElemIndex = parentElement.children.indexOf(oldChild);
|
|
620
719
|
if (oldElemIndex !== -1) {
|
|
621
720
|
if (newChild.nodeType === NodeType.ELEMENT_NODE) {
|
|
622
|
-
// Replace with new element
|
|
623
721
|
parentElement.children[oldElemIndex] = newChild;
|
|
624
722
|
newChild.parentElement = parentElement;
|
|
625
723
|
|
|
626
|
-
// Transfer element sibling relationships
|
|
627
724
|
newChild.previousElementSibling = oldChild.previousElementSibling;
|
|
628
725
|
newChild.nextElementSibling = oldChild.nextElementSibling;
|
|
629
726
|
|
|
@@ -641,7 +738,6 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
641
738
|
parentElement.lastElementChild = newChild;
|
|
642
739
|
}
|
|
643
740
|
} else {
|
|
644
|
-
// Replacing element with non-element, remove from children
|
|
645
741
|
parentElement.children.splice(oldElemIndex, 1);
|
|
646
742
|
|
|
647
743
|
if (oldChild.previousElementSibling) {
|
|
@@ -662,11 +758,9 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
662
758
|
}
|
|
663
759
|
}
|
|
664
760
|
} else if (newChild.nodeType === NodeType.ELEMENT_NODE) {
|
|
665
|
-
// Replacing non-element with element, need to insert into children array
|
|
666
761
|
const newElement = newChild;
|
|
667
762
|
newElement.parentElement = parentElement;
|
|
668
763
|
|
|
669
|
-
// Find correct position in children array
|
|
670
764
|
let insertIndex = 0;
|
|
671
765
|
for (let i = 0; i < oldIndex; i++) {
|
|
672
766
|
if (parent.childNodes[i].nodeType === NodeType.ELEMENT_NODE) {
|
|
@@ -676,7 +770,6 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
676
770
|
|
|
677
771
|
parentElement.children.splice(insertIndex, 0, newElement);
|
|
678
772
|
|
|
679
|
-
// Update element sibling relationships
|
|
680
773
|
newElement.previousElementSibling =
|
|
681
774
|
insertIndex > 0 ? parentElement.children[insertIndex - 1] : null;
|
|
682
775
|
newElement.nextElementSibling =
|
|
@@ -700,7 +793,6 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
700
793
|
}
|
|
701
794
|
}
|
|
702
795
|
|
|
703
|
-
// Clear oldChild's relationships
|
|
704
796
|
oldChild.parentNode = null;
|
|
705
797
|
if (oldChild.nodeType === NodeType.ELEMENT_NODE) {
|
|
706
798
|
oldChild.parentElement = null;
|
|
@@ -720,19 +812,16 @@ function replaceChild(parent: any, newChild: any, oldChild: any): any {
|
|
|
720
812
|
}
|
|
721
813
|
|
|
722
814
|
function insertAfter(parent: any, newNode: any, referenceNode: any): any {
|
|
723
|
-
// If referenceNode is null, insert at the beginning
|
|
724
815
|
if (referenceNode === null) {
|
|
725
816
|
insertBefore(parent, newNode, parent.firstChild);
|
|
726
817
|
return newNode;
|
|
727
818
|
}
|
|
728
819
|
|
|
729
|
-
// Verify referenceNode is actually a child of parent
|
|
730
820
|
const refIndex = parent.childNodes.indexOf(referenceNode);
|
|
731
821
|
if (refIndex === -1) {
|
|
732
822
|
throw new Error("Reference node is not a child of this node");
|
|
733
823
|
}
|
|
734
824
|
|
|
735
|
-
// Insert after means insert before the next sibling
|
|
736
825
|
const nextSibling = referenceNode.nextSibling;
|
|
737
826
|
return insertBefore(parent, newNode, nextSibling);
|
|
738
827
|
}
|
|
@@ -762,7 +851,14 @@ function updateElementContent(element: any): void {
|
|
|
762
851
|
.map(([k, v]) => ` ${k}="${v}"`)
|
|
763
852
|
.join("");
|
|
764
853
|
const tagNameLower = element.tagName.toLowerCase();
|
|
765
|
-
|
|
854
|
+
const isVoid = VOID_ELEMENTS.has(tagNameLower);
|
|
855
|
+
|
|
856
|
+
Object.defineProperty(element, "_internalOuterHTML", {
|
|
857
|
+
value: isVoid ? `<${tagNameLower}${attrs}>` : `<${tagNameLower}${attrs}>${innerHTML}</${tagNameLower}>`,
|
|
858
|
+
writable: true,
|
|
859
|
+
enumerable: false,
|
|
860
|
+
configurable: true,
|
|
861
|
+
});
|
|
766
862
|
|
|
767
863
|
const computedTextContent = getTextContent(element);
|
|
768
864
|
Object.defineProperty(element, "_internalTextContent", {
|
|
@@ -772,7 +868,6 @@ function updateElementContent(element: any): void {
|
|
|
772
868
|
configurable: true,
|
|
773
869
|
});
|
|
774
870
|
|
|
775
|
-
// Propagate changes up to parent elements
|
|
776
871
|
if (element.parentElement) {
|
|
777
872
|
updateElementContent(element.parentElement);
|
|
778
873
|
}
|
|
@@ -854,7 +949,109 @@ export function setInnerHTML(element: any, html: string): void {
|
|
|
854
949
|
.map(([k, v]) => ` ${k}="${v}"`)
|
|
855
950
|
.join("");
|
|
856
951
|
const tagNameLower = element.tagName.toLowerCase();
|
|
857
|
-
|
|
952
|
+
const isVoid = VOID_ELEMENTS.has(tagNameLower);
|
|
953
|
+
|
|
954
|
+
Object.defineProperty(element, "_internalOuterHTML", {
|
|
955
|
+
value: isVoid ? `<${tagNameLower}${attrs}>` : `<${tagNameLower}${attrs}>${actualInnerHTML}</${tagNameLower}>`,
|
|
956
|
+
writable: true,
|
|
957
|
+
enumerable: false,
|
|
958
|
+
configurable: true,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function setOuterHTML(element: any, html: string): void {
|
|
963
|
+
if (!element.parentNode) {
|
|
964
|
+
throw new Error("Cannot set outerHTML on element without a parent");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const parent = element.parentNode;
|
|
968
|
+
const indexInParent = parent.childNodes.indexOf(element);
|
|
969
|
+
|
|
970
|
+
if (indexInParent === -1) {
|
|
971
|
+
throw new Error("Element not found in parent's childNodes");
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let newNodes: any[] = [];
|
|
975
|
+
|
|
976
|
+
if (html.trim()) {
|
|
977
|
+
const tokens = tokenize(html);
|
|
978
|
+
const ast = parse(tokens);
|
|
979
|
+
|
|
980
|
+
if (ast.children) {
|
|
981
|
+
for (const child of ast.children) {
|
|
982
|
+
const domChild = convertASTNodeToDOM(child);
|
|
983
|
+
if (domChild) {
|
|
984
|
+
newNodes.push(domChild);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const previousSibling = element.previousSibling;
|
|
991
|
+
const nextSibling = element.nextSibling;
|
|
992
|
+
|
|
993
|
+
parent.childNodes.splice(indexInParent, 1);
|
|
994
|
+
|
|
995
|
+
if (newNodes.length > 0) {
|
|
996
|
+
parent.childNodes.splice(indexInParent, 0, ...newNodes);
|
|
997
|
+
|
|
998
|
+
for (const newNode of newNodes) {
|
|
999
|
+
newNode.parentNode = parent;
|
|
1000
|
+
newNode.parentElement = parent.nodeType === NodeType.ELEMENT_NODE ? parent : null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
for (let i = 0; i < newNodes.length; i++) {
|
|
1004
|
+
const currentNode = newNodes[i];
|
|
1005
|
+
|
|
1006
|
+
if (i === 0) {
|
|
1007
|
+
currentNode.previousSibling = previousSibling;
|
|
1008
|
+
if (previousSibling) {
|
|
1009
|
+
previousSibling.nextSibling = currentNode;
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
currentNode.previousSibling = newNodes[i - 1];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (i === newNodes.length - 1) {
|
|
1016
|
+
currentNode.nextSibling = nextSibling;
|
|
1017
|
+
if (nextSibling) {
|
|
1018
|
+
nextSibling.previousSibling = currentNode;
|
|
1019
|
+
}
|
|
1020
|
+
} else {
|
|
1021
|
+
currentNode.nextSibling = newNodes[i + 1];
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} else {
|
|
1025
|
+
if (previousSibling) {
|
|
1026
|
+
previousSibling.nextSibling = nextSibling;
|
|
1027
|
+
}
|
|
1028
|
+
if (nextSibling) {
|
|
1029
|
+
nextSibling.previousSibling = previousSibling;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
element.parentNode = null;
|
|
1034
|
+
element.parentElement = null;
|
|
1035
|
+
element.previousSibling = null;
|
|
1036
|
+
element.nextSibling = null;
|
|
1037
|
+
|
|
1038
|
+
parent.children = parent.childNodes.filter(
|
|
1039
|
+
(child: any) => child.nodeType === NodeType.ELEMENT_NODE
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
parent.firstChild = parent.childNodes.length > 0 ? parent.childNodes[0] : null;
|
|
1043
|
+
parent.lastChild = parent.childNodes.length > 0 ? parent.childNodes[parent.childNodes.length - 1] : null;
|
|
1044
|
+
|
|
1045
|
+
parent.firstElementChild = parent.children.length > 0 ? parent.children[0] : null;
|
|
1046
|
+
parent.lastElementChild = parent.children.length > 0 ? parent.children[parent.children.length - 1] : null;
|
|
1047
|
+
|
|
1048
|
+
for (let i = 0; i < parent.children.length; i++) {
|
|
1049
|
+
const child = parent.children[i];
|
|
1050
|
+
child.previousElementSibling = i > 0 ? parent.children[i - 1] : null;
|
|
1051
|
+
child.nextElementSibling = i < parent.children.length - 1 ? parent.children[i + 1] : null;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
updateElementContent(parent);
|
|
858
1055
|
}
|
|
859
1056
|
|
|
860
1057
|
function setTextContent(element: any, text: string): void {
|
package/src/parser.ts
CHANGED
|
@@ -314,42 +314,3 @@ function shouldSkipWhitespace(parent: ASTNode): boolean {
|
|
|
314
314
|
|
|
315
315
|
return parent.tagName ? skipWhitespaceIn.has(parent.tagName) : false;
|
|
316
316
|
}
|
|
317
|
-
|
|
318
|
-
export function traverseAST(node: ASTNode, callback: (node: ASTNode) => void): void {
|
|
319
|
-
callback(node);
|
|
320
|
-
|
|
321
|
-
if (node.children) {
|
|
322
|
-
for (const child of node.children) {
|
|
323
|
-
traverseAST(child, callback);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function findNodesByTagName(root: ASTNode, tagName: string): ASTNode[] {
|
|
329
|
-
const results: ASTNode[] = [];
|
|
330
|
-
|
|
331
|
-
traverseAST(root, (node) => {
|
|
332
|
-
if (node.type === ASTNodeType.ELEMENT && node.tagName === tagName.toLowerCase()) {
|
|
333
|
-
results.push(node);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
return results;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function findNodesByAttribute(root: ASTNode, attrName: string, attrValue?: string): ASTNode[] {
|
|
341
|
-
const results: ASTNode[] = [];
|
|
342
|
-
|
|
343
|
-
traverseAST(root, (node) => {
|
|
344
|
-
if (node.type === ASTNodeType.ELEMENT && node.attributes) {
|
|
345
|
-
const hasAttr = attrName in node.attributes;
|
|
346
|
-
const valueMatches = attrValue === undefined || node.attributes[attrName] === attrValue;
|
|
347
|
-
|
|
348
|
-
if (hasAttr && valueMatches) {
|
|
349
|
-
results.push(node);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
return results;
|
|
355
|
-
}
|