@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.
@@ -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 outerHTML = `<${tagNameLower}${Object.entries(attributes)
30
+ const isVoid = VOID_ELEMENTS.has(tagNameLower);
31
+ const attrsStr = Object.entries(attributes)
26
32
  .map(([k, v]) => ` ${k}="${v}"`)
27
- .join("")}></${tagNameLower}>`;
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
- outerHTML,
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
- element.outerHTML = `<${tagNameLower}${attrs}>${innerHTML}</${tagNameLower}>`;
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
- element.outerHTML = `<${tagNameLower}${attrs}>${actualInnerHTML}</${tagNameLower}>`;
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
- }