@xmldom/xmldom 0.8.12 → 0.8.13

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/CHANGELOG.md CHANGED
@@ -4,16 +4,39 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.8.13](https://github.com/xmldom/xmldom/compare/0.8.12...0.8.13)
8
+
9
+ ### Fixed
10
+
11
+ - Security: `XMLSerializer.serializeToString()` (and `Node.toString()`, `NodeList.toString()`) now accept a `requireWellFormed` option (fourth argument, after `isHtml` and `nodeFilter`). When `{ requireWellFormed: true }` is passed, the serializer throws `InvalidStateError` for injection-prone node content, preventing XML injection via attacker-controlled node data. [`GHSA-j759-j44w-7fr8`](https://github.com/xmldom/xmldom/security/advisories/GHSA-j759-j44w-7fr8) [`GHSA-x6wf-f3px-wcqx`](https://github.com/xmldom/xmldom/security/advisories/GHSA-x6wf-f3px-wcqx) [`GHSA-f6ww-3ggp-fr8h`](https://github.com/xmldom/xmldom/security/advisories/GHSA-f6ww-3ggp-fr8h)
12
+ - Comment: throws when `data` contains `-->`
13
+ - ProcessingInstruction: throws when `data` contains `?>`
14
+ - DocumentType: throws when `publicId` fails `PubidLiteral`, `systemId` fails `SystemLiteral`, or `internalSubset` contains `]>`
15
+ - Security: DOM traversal operations (`XMLSerializer.serializeToString()`, `Node.prototype.normalize()`, `Node.prototype.cloneNode(true)`, `Document.prototype.importNode(node, true)`, `node.textContent` getter, `getElementsByTagName()` / `getElementsByTagNameNS()` / `getElementsByClassName()` / `getElementById()`) are now iterative. Previously, deeply nested DOM trees would exhaust the JavaScript call stack and throw an unrecoverable `RangeError`. [`GHSA-2v35-w6hq-6mfw`](https://github.com/xmldom/xmldom/security/advisories/GHSA-2v35-w6hq-6mfw)
16
+
17
+ Thank you,
18
+ [@Jvr2022](https://github.com/Jvr2022),
19
+ [@praveen-kv](https://github.com/praveen-kv),
20
+ [@TharVid](https://github.com/TharVid),
21
+ [@decsecre583](https://github.com/decsecre583),
22
+ [@tlsbollei](https://github.com/tlsbollei),
23
+ [@KarimTantawey](https://github.com/KarimTantawey),
24
+ for your contributions
25
+
7
26
  ## [0.8.12](https://github.com/xmldom/xmldom/compare/0.8.11...0.8.12)
8
27
 
9
28
  ### Fixed
10
29
 
30
+ - preserve trailing whitespace in ProcessingInstruction data [`#962`](https://github.com/xmldom/xmldom/pull/962) / [`#42`](https://github.com/xmldom/xmldom/issues/42)
11
31
  - Security: `createCDATASection` now throws `InvalidCharacterError` when `data` contains `"]]>"`, as required by the [WHATWG DOM spec](https://dom.spec.whatwg.org/#dom-document-createcdatasection). [`GHSA-wh4c-j3r5-mjhp`](https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp)
12
32
  - Security: `XMLSerializer` now splits CDATASection nodes whose data contains `"]]>"` into adjacent CDATA sections at serialization time, preventing XML injection via mutation methods (`appendData`, `replaceData`, `.data =`, `.textContent =`). [`GHSA-wh4c-j3r5-mjhp`](https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp)
13
33
 
14
34
  Code that passes a string containing `"]]>"` to `createCDATASection` and relied on the previously unsafe behavior will now receive `InvalidCharacterError`. Use a mutation method such as `appendData` if you intentionally need `"]]>"` in a CDATASection node's data.
15
35
 
16
- Thank you, [@thesmartshadow](https://github.com/thesmartshadow), for your contributions
36
+ Thank you,
37
+ [@thesmartshadow](https://github.com/thesmartshadow),
38
+ [@stevenobiajulu](https://github.com/stevenobiajulu),
39
+ for your contributions
17
40
 
18
41
  ## [0.8.11](https://github.com/xmldom/xmldom/compare/0.8.10...0.8.11)
19
42
 
package/index.d.ts CHANGED
@@ -22,18 +22,52 @@ declare module "@xmldom/xmldom" {
22
22
  parseFromString(xmlsource: string, mimeType?: string): Document;
23
23
  }
24
24
 
25
+ /** Options accepted by `XMLSerializer.prototype.serializeToString`. */
26
+ interface XMLSerializerOptions {
27
+ /**
28
+ * When `true`, the serializer throws a DOMException with code `INVALID_STATE_ERR` if:
29
+ * - A CDATASection node's data contains `"]]>"`.
30
+ * - A Comment node's data contains `"-->"` (the injection sequence that terminates a
31
+ * comment). Comments whose data contains `"--"` but not `"-->"` are accepted on this
32
+ * branch — the 0.8.x parser does not validate bare `"--"` in comment content.
33
+ * - A ProcessingInstruction's data contains `"?>"` (W3C DOM Parsing §3.2.1.7).
34
+ *
35
+ * @default false
36
+ */
37
+ requireWellFormed?: boolean;
38
+ }
39
+
25
40
  interface XMLSerializer {
26
41
  /**
27
42
  * Returns the result of serializing `node` to XML.
28
43
  *
44
+ * When `options.requireWellFormed` is `true`, the serializer throws for content that would
45
+ * produce ill-formed XML.
46
+ *
29
47
  * __This implementation differs from the specification:__
30
48
  * - CDATASection nodes whose data contains `]]>` are serialized by splitting the section
31
49
  * at each `]]>` occurrence (following W3C DOM Level 3 Core `split-cdata-sections`
32
- * default behaviour). A configurable option is not yet implemented.
50
+ * default behaviour) unless `requireWellFormed` is `true`.
51
+ * - when `requireWellFormed` is `true`, `DOMException` with code `INVALID_STATE_ERR`
52
+ * is only thrown to prevent injection vectors, not for all the spec mandated checks.
33
53
  *
54
+ * @throws {DOMException}
55
+ * With code `INVALID_STATE_ERR` when `requireWellFormed` is `true` and:
56
+ * - a CDATASection node's data contains `"]]>"`,
57
+ * - a Comment node's data contains `"-->"` (bare `"--"` does not throw on this branch),
58
+ * - a ProcessingInstruction's data contains `"?>"`,
59
+ * - a DocumentType's `publicId` is non-empty and does not match the XML `PubidLiteral`
60
+ * production,
61
+ * - a DocumentType's `systemId` is non-empty and does not match the XML `SystemLiteral`
62
+ * production, or
63
+ * - a DocumentType's `internalSubset` contains `"]>"`.
64
+ * Note: xmldom does not enforce `readonly` on DocumentType fields — direct property
65
+ * writes succeed and are covered by the serializer-level checks above.
34
66
  * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring
67
+ * @see https://w3c.github.io/DOM-Parsing/#xml-serialization
68
+ * @see https://github.com/w3c/DOM-Parsing/issues/84
35
69
  */
36
- serializeToString(node: Node): string;
70
+ serializeToString(node: Node, isHtml?: boolean, nodeFilter?: (node: Node) => Node | null | undefined, options?: XMLSerializerOptions): string;
37
71
  }
38
72
 
39
73
  interface Options {
package/lib/dom.js CHANGED
@@ -171,9 +171,10 @@ NodeList.prototype = {
171
171
  item: function(index) {
172
172
  return index >= 0 && index < this.length ? this[index] : null;
173
173
  },
174
- toString:function(isHTML,nodeFilter){
174
+ toString:function(isHTML,nodeFilter,options){
175
+ var requireWellFormed = !!options && !!options.requireWellFormed;
175
176
  for(var buf = [], i = 0;i<this.length;i++){
176
- serializeToString(this[i],buf,isHTML,nodeFilter);
177
+ serializeToString(this[i],buf,isHTML,nodeFilter,null,requireWellFormed);
177
178
  }
178
179
  return buf.join('');
179
180
  },
@@ -419,13 +420,28 @@ DOMImplementation.prototype = {
419
420
  /**
420
421
  * Returns a doctype, with the given `qualifiedName`, `publicId`, and `systemId`.
421
422
  *
422
- * __This behavior is slightly different from the in the specs__:
423
+ * __This implementation differs from the specification:__
423
424
  * - this implementation is not validating names or qualified names
424
425
  * (when parsing XML strings, the SAX parser takes care of that)
425
426
  *
427
+ * Note: `internalSubset` can only be introduced via a direct property write to `node.internalSubset` after creation.
428
+ * Creation-time validation of `publicId`, `systemId` is not enforced.
429
+ * The serializer-level check covers all mutation vectors, including direct property writes.
430
+ * `internalSubset` is only serialized as `[ ... ]` when both `publicId` and `systemId` are
431
+ * absent (empty or `'.'`) — if either external identifier is present, `internalSubset` is
432
+ * silently omitted from the serialized output.
433
+ *
426
434
  * @param {string} qualifiedName
427
435
  * @param {string} [publicId]
436
+ * The external subset public identifier. Stored verbatim including surrounding quotes.
437
+ * When serialized with `requireWellFormed: true` (via the 4th-parameter options object),
438
+ * throws `DOMException` with code `INVALID_STATE_ERR` if the value is non-empty and does
439
+ * not match the XML `PubidLiteral` production (W3C DOM Parsing §3.2.1.3; XML 1.0 [12]).
428
440
  * @param {string} [systemId]
441
+ * The external subset system identifier. Stored verbatim including surrounding quotes.
442
+ * When serialized with `requireWellFormed: true`, throws `DOMException` with code
443
+ * `INVALID_STATE_ERR` if the value is non-empty and does not match the XML `SystemLiteral`
444
+ * production (W3C DOM Parsing §3.2.1.3; XML 1.0 [11]).
429
445
  * @returns {DocumentType} which can either be used with `DOMImplementation.createDocument` upon document creation
430
446
  * or can be put into the document via methods like `Node.insertBefore()` or `Node.replaceChild()`
431
447
  *
@@ -492,18 +508,44 @@ Node.prototype = {
492
508
  return cloneNode(this.ownerDocument||this,this,deep);
493
509
  },
494
510
  // Modified in DOM Level 2:
495
- normalize:function(){
496
- var child = this.firstChild;
497
- while(child){
498
- var next = child.nextSibling;
499
- if(next && next.nodeType == TEXT_NODE && child.nodeType == TEXT_NODE){
500
- this.removeChild(next);
501
- child.appendData(next.data);
502
- }else{
503
- child.normalize();
504
- child = next;
505
- }
506
- }
511
+ /**
512
+ * Puts the specified node and all of its subtree into a "normalized" form. In a normalized
513
+ * subtree, no text nodes in the subtree are empty and there are no adjacent text nodes.
514
+ *
515
+ * Specifically, this method merges any adjacent text nodes (i.e., nodes for which `nodeType`
516
+ * is `TEXT_NODE`) into a single node with the combined data. It also removes any empty text
517
+ * nodes.
518
+ *
519
+ * This method iteratively traverses all child nodes to normalize all descendant nodes within
520
+ * the subtree.
521
+ *
522
+ * @throws {DOMException}
523
+ * May throw a DOMException if operations within removeChild or appendData (which are
524
+ * potentially invoked in this method) do not meet their specific constraints.
525
+ * @see {@link Node.removeChild}
526
+ * @see {@link CharacterData.appendData}
527
+ * @see ../docs/walk-dom.md.
528
+ */
529
+ normalize: function () {
530
+ walkDOM(this, null, {
531
+ enter: function (node) {
532
+ // Merge adjacent text children of node before walkDOM schedules them.
533
+ // walkDOM reads lastChild/previousSibling after enter returns, so the
534
+ // surviving post-merge children are what it descends into.
535
+ var child = node.firstChild;
536
+ while (child) {
537
+ var next = child.nextSibling;
538
+ if (next !== null && next.nodeType === TEXT_NODE && child.nodeType === TEXT_NODE) {
539
+ node.removeChild(next);
540
+ child.appendData(next.data);
541
+ // Do not advance child: re-check new nextSibling for another text run
542
+ } else {
543
+ child = next;
544
+ }
545
+ }
546
+ return true; // descend into surviving children
547
+ },
548
+ });
507
549
  },
508
550
  // Introduced in DOM Level 2:
509
551
  isSupported:function(feature, version){
@@ -579,21 +621,103 @@ copy(NodeType,Node);
579
621
  copy(NodeType,Node.prototype);
580
622
 
581
623
  /**
582
- * @param callback return true for continue,false for break
583
- * @return boolean true: break visit;
624
+ * @param {Node} node
625
+ * Root of the subtree to visit.
626
+ * @param {function(Node): boolean} callback
627
+ * Called for each node in depth-first pre-order. Return a truthy value to stop traversal early.
628
+ * @return {boolean} `true` if traversal was aborted by the callback, `false` otherwise.
584
629
  */
585
- function _visitNode(node,callback){
586
- if(callback(node)){
587
- return true;
588
- }
589
- if(node = node.firstChild){
590
- do{
591
- if(_visitNode(node,callback)){return true}
592
- }while(node=node.nextSibling)
593
- }
630
+ function _visitNode(node, callback) {
631
+ return walkDOM(node, null, { enter: function (n) { return callback(n) ? walkDOM.STOP : true; } }) === walkDOM.STOP;
594
632
  }
595
633
 
634
+ /**
635
+ * Depth-first pre/post-order DOM tree walker.
636
+ *
637
+ * Visits every node in the subtree rooted at `node`. For each node:
638
+ *
639
+ * 1. Calls `callbacks.enter(node, context)` before descending into the node's children. The
640
+ * return value becomes the `context` passed to each child's `enter` call and to the matching
641
+ * `exit` call.
642
+ * 2. If `enter` returns `null` or `undefined`, the node's children are skipped;
643
+ * sibling traversal continues normally.
644
+ * 3. If `enter` returns `walkDOM.STOP`, the entire traversal is aborted immediately — no
645
+ * further `enter` or `exit` calls are made.
646
+ * 4. `lastChild` and `previousSibling` are read **after** `enter` returns, so `enter` may
647
+ * safely modify the node's own child list before the walker descends. Modifying siblings of
648
+ * the current node or any other part of the tree produces unpredictable results: nodes already
649
+ * queued on the stack are visited regardless of DOM changes, and newly inserted nodes outside
650
+ * the current child list are never visited.
651
+ * 5. Calls `callbacks.exit(node, context)` (if provided) after all of a node's children have
652
+ * been visited, passing the same `context` that `enter`
653
+ * returned for that node.
654
+ *
655
+ * This implementation uses an explicit stack and does not recurse — it is safe on arbitrarily
656
+ * deep trees.
657
+ *
658
+ * @param {Node} node
659
+ * Root of the subtree to walk.
660
+ * @param {*} context
661
+ * Initial context value passed to the root node's `enter`.
662
+ * @param {{ enter: function(Node, *): *, exit?: function(Node, *): void }} callbacks
663
+ * @returns {void | walkDOM.STOP}
664
+ * @see ../docs/walk-dom.md.
665
+ */
666
+ function walkDOM(node, context, callbacks) {
667
+ // Each stack frame is {node, context, phase}:
668
+ // walkDOM.ENTER — call enter, then push children
669
+ // walkDOM.EXIT — call exit
670
+ var stack = [{ node: node, context: context, phase: walkDOM.ENTER }];
671
+ while (stack.length > 0) {
672
+ var frame = stack.pop();
673
+ if (frame.phase === walkDOM.ENTER) {
674
+ var childContext = callbacks.enter(frame.node, frame.context);
675
+ if (childContext === walkDOM.STOP) {
676
+ return walkDOM.STOP;
677
+ }
678
+ // Push exit frame before children so it fires after all children are processed (Last In First Out)
679
+ stack.push({ node: frame.node, context: childContext, phase: walkDOM.EXIT });
680
+ if (childContext === null || childContext === undefined) {
681
+ continue; // skip children
682
+ }
683
+ // lastChild is read after enter returns, so enter may modify the child list.
684
+ var child = frame.node.lastChild;
685
+ // Traverse from lastChild backwards so that pushing onto the stack
686
+ // naturally yields firstChild on top (processed first).
687
+ while (child) {
688
+ stack.push({ node: child, context: childContext, phase: walkDOM.ENTER });
689
+ child = child.previousSibling;
690
+ }
691
+ } else {
692
+ // frame.phase === walkDOM.EXIT
693
+ if (callbacks.exit) {
694
+ callbacks.exit(frame.node, frame.context);
695
+ }
696
+ }
697
+ }
698
+ }
596
699
 
700
+ /**
701
+ * Sentinel value returned from a `walkDOM` `enter` callback to abort the entire traversal
702
+ * immediately.
703
+ *
704
+ * @type {symbol}
705
+ */
706
+ walkDOM.STOP = Symbol('walkDOM.STOP');
707
+ /**
708
+ * Phase constant for a stack frame that has not yet been visited.
709
+ * The `enter` callback is called and children are scheduled.
710
+ *
711
+ * @type {number}
712
+ */
713
+ walkDOM.ENTER = 0;
714
+ /**
715
+ * Phase constant for a stack frame whose subtree has been fully visited.
716
+ * The `exit` callback is called.
717
+ *
718
+ * @type {number}
719
+ */
720
+ walkDOM.EXIT = 1;
597
721
 
598
722
  function Document(){
599
723
  this.ownerDocument = this;
@@ -1218,6 +1342,23 @@ Document.prototype = {
1218
1342
  node.appendData(data)
1219
1343
  return node;
1220
1344
  },
1345
+ /**
1346
+ * Returns a ProcessingInstruction node whose target is target and data is data.
1347
+ *
1348
+ * __This implementation differs from the specification:__
1349
+ * - it does not do any input validation on the arguments and doesn't throw "InvalidCharacterError".
1350
+ *
1351
+ * Note: When the resulting document is serialized with `requireWellFormed: true`, the
1352
+ * serializer throws with code `INVALID_STATE_ERR` if `.data` contains `?>` (W3C DOM Parsing
1353
+ * §3.2.1.7). Without that option the data is emitted verbatim.
1354
+ *
1355
+ * @param {string} target
1356
+ * @param {string} data
1357
+ * @returns {ProcessingInstruction}
1358
+ * @see https://developer.mozilla.org/docs/Web/API/Document/createProcessingInstruction
1359
+ * @see https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction
1360
+ * @see https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml §3.2.1.7
1361
+ */
1221
1362
  createProcessingInstruction : function(target,data){
1222
1363
  var node = new ProcessingInstruction();
1223
1364
  node.ownerDocument = this;
@@ -1449,6 +1590,19 @@ CDATASection.prototype = {
1449
1590
  _extends(CDATASection,CharacterData);
1450
1591
 
1451
1592
 
1593
+ /**
1594
+ * Represents a DocumentType node (the `<!DOCTYPE ...>` declaration).
1595
+ *
1596
+ * `publicId`, `systemId`, and `internalSubset` are plain own-property assignments.
1597
+ * xmldom does not enforce the `readonly` constraint declared by the WHATWG DOM spec —
1598
+ * direct property writes succeed silently. Values are serialized verbatim when
1599
+ * `requireWellFormed` is false (the default). When the serializer is invoked with
1600
+ * `requireWellFormed: true` (via the 4th-parameter options object), it validates each
1601
+ * field and throws `DOMException` with code `INVALID_STATE_ERR` on invalid values.
1602
+ *
1603
+ * @class
1604
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/DocumentType MDN
1605
+ */
1452
1606
  function DocumentType() {
1453
1607
  };
1454
1608
  DocumentType.prototype.nodeType = DOCUMENT_TYPE_NODE;
@@ -1484,22 +1638,45 @@ function XMLSerializer(){}
1484
1638
  /**
1485
1639
  * Returns the result of serializing `node` to XML.
1486
1640
  *
1641
+ * When `options.requireWellFormed` is `true`, the serializer throws for content that would
1642
+ * produce ill-formed XML.
1643
+ *
1487
1644
  * __This implementation differs from the specification:__
1488
1645
  * - CDATASection nodes whose data contains `]]>` are serialized by splitting the section
1489
1646
  * at each `]]>` occurrence (following W3C DOM Level 3 Core `split-cdata-sections`
1490
- * default behaviour). A configurable option is not yet implemented.
1647
+ * default behaviour) unless `requireWellFormed` is `true`.
1648
+ * - when `requireWellFormed` is `true`, `DOMException` with code `INVALID_STATE_ERR`
1649
+ * is only thrown to prevent injection vectors, not for all the spec mandated checks.
1491
1650
  *
1492
1651
  * @param {Node} node
1493
1652
  * @param {boolean} [isHtml]
1494
1653
  * @param {function} [nodeFilter]
1654
+ * @param {Object} [options]
1655
+ * @param {boolean} [options.requireWellFormed=false]
1656
+ * When `true`, throws for content that would produce ill-formed XML.
1495
1657
  * @returns {string}
1658
+ * @throws {DOMException}
1659
+ * With code `INVALID_STATE_ERR` when `requireWellFormed` is `true` and:
1660
+ * - a CDATASection node's data contains `"]]>"`,
1661
+ * - a Comment node's data contains `"-->"` (bare `"--"` does not throw on this branch),
1662
+ * - a ProcessingInstruction's data contains `"?>"`,
1663
+ * - a DocumentType's `publicId` is non-empty and does not match the XML `PubidLiteral`
1664
+ * production,
1665
+ * - a DocumentType's `systemId` is non-empty and does not match the XML `SystemLiteral`
1666
+ * production, or
1667
+ * - a DocumentType's `internalSubset` contains `"]>"`.
1668
+ * Note: xmldom does not enforce `readonly` on DocumentType fields — direct property
1669
+ * writes succeed and are covered by the serializer-level checks above.
1496
1670
  * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring
1671
+ * @see https://w3c.github.io/DOM-Parsing/#xml-serialization
1672
+ * @see https://github.com/w3c/DOM-Parsing/issues/84
1497
1673
  */
1498
- XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter){
1499
- return nodeSerializeToString.call(node,isHtml,nodeFilter);
1674
+ XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter,options){
1675
+ return nodeSerializeToString.call(node,isHtml,nodeFilter,options);
1500
1676
  }
1501
1677
  Node.prototype.toString = nodeSerializeToString;
1502
- function nodeSerializeToString(isHtml,nodeFilter){
1678
+ function nodeSerializeToString(isHtml,nodeFilter,options){
1679
+ var requireWellFormed = !!options && !!options.requireWellFormed;
1503
1680
  var buf = [];
1504
1681
  var refNode = this.nodeType == 9 && this.documentElement || this;
1505
1682
  var prefix = refNode.prefix;
@@ -1516,7 +1693,7 @@ function nodeSerializeToString(isHtml,nodeFilter){
1516
1693
  ]
1517
1694
  }
1518
1695
  }
1519
- serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces);
1696
+ serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces,requireWellFormed);
1520
1697
  //console.log('###',this.nodeType,uri,prefix,buf.join(''))
1521
1698
  return buf.join('');
1522
1699
  }
@@ -1565,272 +1742,323 @@ function addSerializedAttribute(buf, qualifiedName, value) {
1565
1742
  buf.push(' ', qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"')
1566
1743
  }
1567
1744
 
1568
- function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1745
+ function serializeToString(node, buf, isHTML, nodeFilter, visibleNamespaces, requireWellFormed) {
1569
1746
  if (!visibleNamespaces) {
1570
1747
  visibleNamespaces = [];
1571
1748
  }
1572
-
1573
- if(nodeFilter){
1574
- node = nodeFilter(node);
1575
- if(node){
1576
- if(typeof node == 'string'){
1577
- buf.push(node);
1578
- return;
1579
- }
1580
- }else{
1581
- return;
1582
- }
1583
- //buf.sort.apply(attrs, attributeSorter);
1584
- }
1585
-
1586
- switch(node.nodeType){
1587
- case ELEMENT_NODE:
1588
- var attrs = node.attributes;
1589
- var len = attrs.length;
1590
- var child = node.firstChild;
1591
- var nodeName = node.tagName;
1592
-
1593
- isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML
1594
-
1595
- var prefixedNodeName = nodeName
1596
- if (!isHTML && !node.prefix && node.namespaceURI) {
1597
- var defaultNS
1598
- // lookup current default ns from `xmlns` attribute
1599
- for (var ai = 0; ai < attrs.length; ai++) {
1600
- if (attrs.item(ai).name === 'xmlns') {
1601
- defaultNS = attrs.item(ai).value
1602
- break
1603
- }
1604
- }
1605
- if (!defaultNS) {
1606
- // lookup current default ns in visibleNamespaces
1607
- for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
1608
- var namespace = visibleNamespaces[nsi]
1609
- if (namespace.prefix === '' && namespace.namespace === node.namespaceURI) {
1610
- defaultNS = namespace.namespace
1611
- break
1749
+ walkDOM(node, { ns: visibleNamespaces, isHTML: isHTML }, {
1750
+ enter: function (n, ctx) {
1751
+ var ns = ctx.ns;
1752
+ var html = ctx.isHTML;
1753
+
1754
+ if (nodeFilter) {
1755
+ n = nodeFilter(n);
1756
+ if (n) {
1757
+ if (typeof n == 'string') {
1758
+ buf.push(n);
1759
+ return null;
1612
1760
  }
1761
+ } else {
1762
+ return null;
1613
1763
  }
1614
1764
  }
1615
- if (defaultNS !== node.namespaceURI) {
1616
- for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
1617
- var namespace = visibleNamespaces[nsi]
1618
- if (namespace.namespace === node.namespaceURI) {
1619
- if (namespace.prefix) {
1620
- prefixedNodeName = namespace.prefix + ':' + nodeName
1765
+
1766
+ switch (n.nodeType) {
1767
+ case ELEMENT_NODE:
1768
+ var attrs = n.attributes;
1769
+ var len = attrs.length;
1770
+ var nodeName = n.tagName;
1771
+
1772
+ html = NAMESPACE.isHTML(n.namespaceURI) || html;
1773
+
1774
+ var prefixedNodeName = nodeName;
1775
+ if (!html && !n.prefix && n.namespaceURI) {
1776
+ var defaultNS;
1777
+ // lookup current default ns from `xmlns` attribute
1778
+ for (var ai = 0; ai < attrs.length; ai++) {
1779
+ if (attrs.item(ai).name === 'xmlns') {
1780
+ defaultNS = attrs.item(ai).value;
1781
+ break;
1782
+ }
1783
+ }
1784
+ if (!defaultNS) {
1785
+ // lookup current default ns in visibleNamespaces
1786
+ for (var nsi = ns.length - 1; nsi >= 0; nsi--) {
1787
+ var nsEntry = ns[nsi];
1788
+ if (nsEntry.prefix === '' && nsEntry.namespace === n.namespaceURI) {
1789
+ defaultNS = nsEntry.namespace;
1790
+ break;
1791
+ }
1792
+ }
1793
+ }
1794
+ if (defaultNS !== n.namespaceURI) {
1795
+ for (var nsi = ns.length - 1; nsi >= 0; nsi--) {
1796
+ var nsEntry = ns[nsi];
1797
+ if (nsEntry.namespace === n.namespaceURI) {
1798
+ if (nsEntry.prefix) {
1799
+ prefixedNodeName = nsEntry.prefix + ':' + nodeName;
1800
+ }
1801
+ break;
1802
+ }
1803
+ }
1621
1804
  }
1622
- break
1623
1805
  }
1624
- }
1625
- }
1626
- }
1627
1806
 
1628
- buf.push('<', prefixedNodeName);
1807
+ buf.push('<', prefixedNodeName);
1808
+
1809
+ // Build a fresh namespace snapshot for this element's children.
1810
+ // The slice prevents sibling elements from inheriting each other's declarations.
1811
+ var childNs = ns.slice();
1812
+ for (var i = 0; i < len; i++) {
1813
+ var attr = attrs.item(i);
1814
+ if (attr.prefix == 'xmlns') {
1815
+ childNs.push({ prefix: attr.localName, namespace: attr.value });
1816
+ } else if (attr.nodeName == 'xmlns') {
1817
+ childNs.push({ prefix: '', namespace: attr.value });
1818
+ }
1819
+ }
1629
1820
 
1630
- for(var i=0;i<len;i++){
1631
- // add namespaces for attributes
1632
- var attr = attrs.item(i);
1633
- if (attr.prefix == 'xmlns') {
1634
- visibleNamespaces.push({ prefix: attr.localName, namespace: attr.value });
1635
- }else if(attr.nodeName == 'xmlns'){
1636
- visibleNamespaces.push({ prefix: '', namespace: attr.value });
1637
- }
1638
- }
1821
+ for (var i = 0; i < len; i++) {
1822
+ var attr = attrs.item(i);
1823
+ if (needNamespaceDefine(attr, html, childNs)) {
1824
+ var attrPrefix = attr.prefix || '';
1825
+ var uri = attr.namespaceURI;
1826
+ addSerializedAttribute(buf, attrPrefix ? 'xmlns:' + attrPrefix : 'xmlns', uri);
1827
+ childNs.push({ prefix: attrPrefix, namespace: uri });
1828
+ }
1829
+ // Apply nodeFilter and serialize the attribute.
1830
+ var filteredAttr = nodeFilter ? nodeFilter(attr) : attr;
1831
+ if (filteredAttr) {
1832
+ if (typeof filteredAttr === 'string') {
1833
+ buf.push(filteredAttr);
1834
+ } else {
1835
+ addSerializedAttribute(buf, filteredAttr.name, filteredAttr.value);
1836
+ }
1837
+ }
1838
+ }
1639
1839
 
1640
- for(var i=0;i<len;i++){
1641
- var attr = attrs.item(i);
1642
- if (needNamespaceDefine(attr,isHTML, visibleNamespaces)) {
1643
- var prefix = attr.prefix||'';
1644
- var uri = attr.namespaceURI;
1645
- addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
1646
- visibleNamespaces.push({ prefix: prefix, namespace:uri });
1647
- }
1648
- serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
1649
- }
1840
+ // add namespace for current node
1841
+ if (nodeName === prefixedNodeName && needNamespaceDefine(n, html, childNs)) {
1842
+ var nodePrefix = n.prefix || '';
1843
+ var uri = n.namespaceURI;
1844
+ addSerializedAttribute(buf, nodePrefix ? 'xmlns:' + nodePrefix : 'xmlns', uri);
1845
+ childNs.push({ prefix: nodePrefix, namespace: uri });
1846
+ }
1650
1847
 
1651
- // add namespace for current node
1652
- if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
1653
- var prefix = node.prefix||'';
1654
- var uri = node.namespaceURI;
1655
- addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
1656
- visibleNamespaces.push({ prefix: prefix, namespace:uri });
1657
- }
1848
+ var child = n.firstChild;
1849
+ if (child || html && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)) {
1850
+ buf.push('>');
1851
+ if (html && /^script$/i.test(nodeName)) {
1852
+ // Inline serialization for <script> children; return null to skip walkDOM descent.
1853
+ while (child) {
1854
+ if (child.data) {
1855
+ buf.push(child.data);
1856
+ } else {
1857
+ serializeToString(child, buf, html, nodeFilter, childNs.slice(), requireWellFormed);
1858
+ }
1859
+ child = child.nextSibling;
1860
+ }
1861
+ buf.push('</', nodeName, '>');
1862
+ return null;
1863
+ }
1864
+ // Return child context; walkDOM descends and exit emits the closing tag.
1865
+ return { ns: childNs, isHTML: html, tag: prefixedNodeName };
1866
+ } else {
1867
+ buf.push('/>');
1868
+ return null;
1869
+ }
1658
1870
 
1659
- if(child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)){
1660
- buf.push('>');
1661
- //if is cdata child node
1662
- if(isHTML && /^script$/i.test(nodeName)){
1663
- while(child){
1664
- if(child.data){
1665
- buf.push(child.data);
1666
- }else{
1667
- serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
1871
+ case DOCUMENT_NODE:
1872
+ case DOCUMENT_FRAGMENT_NODE:
1873
+ // Descend into children; exit is a no-op (tag is null).
1874
+ return { ns: ns.slice(), isHTML: html, tag: null };
1875
+
1876
+ case ATTRIBUTE_NODE:
1877
+ addSerializedAttribute(buf, n.name, n.value);
1878
+ return null;
1879
+
1880
+ case TEXT_NODE:
1881
+ /**
1882
+ * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
1883
+ * except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
1884
+ * If they are needed elsewhere, they must be escaped using either numeric character references or the strings
1885
+ * `&amp;` and `&lt;` respectively.
1886
+ * The right angle bracket (>) may be represented using the string " &gt; ", and must, for compatibility,
1887
+ * be escaped using either `&gt;` or a character reference when it appears in the string `]]>` in content,
1888
+ * when that string is not marking the end of a CDATA section.
1889
+ *
1890
+ * In the content of elements, character data is any string of characters
1891
+ * which does not contain the start-delimiter of any markup
1892
+ * and does not include the CDATA-section-close delimiter, `]]>`.
1893
+ *
1894
+ * @see https://www.w3.org/TR/xml/#NT-CharData
1895
+ * @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
1896
+ */
1897
+ buf.push(n.data.replace(/[<&>]/g, _xmlEncoder));
1898
+ return null;
1899
+
1900
+ case CDATA_SECTION_NODE:
1901
+ if (requireWellFormed && n.data.indexOf(']]>') !== -1) {
1902
+ throw new DOMException(INVALID_STATE_ERR, 'The CDATASection data contains "]]>"');
1668
1903
  }
1669
- child = child.nextSibling;
1670
- }
1671
- }else
1672
- {
1673
- while(child){
1674
- serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
1675
- child = child.nextSibling;
1676
- }
1677
- }
1678
- buf.push('</',prefixedNodeName,'>');
1679
- }else{
1680
- buf.push('/>');
1681
- }
1682
- // remove added visible namespaces
1683
- //visibleNamespaces.length = startVisibleNamespaces;
1684
- return;
1685
- case DOCUMENT_NODE:
1686
- case DOCUMENT_FRAGMENT_NODE:
1687
- var child = node.firstChild;
1688
- while(child){
1689
- serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
1690
- child = child.nextSibling;
1691
- }
1692
- return;
1693
- case ATTRIBUTE_NODE:
1694
- return addSerializedAttribute(buf, node.name, node.value);
1695
- case TEXT_NODE:
1696
- /**
1697
- * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
1698
- * except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
1699
- * If they are needed elsewhere, they must be escaped using either numeric character references or the strings
1700
- * `&amp;` and `&lt;` respectively.
1701
- * The right angle bracket (>) may be represented using the string " &gt; ", and must, for compatibility,
1702
- * be escaped using either `&gt;` or a character reference when it appears in the string `]]>` in content,
1703
- * when that string is not marking the end of a CDATA section.
1704
- *
1705
- * In the content of elements, character data is any string of characters
1706
- * which does not contain the start-delimiter of any markup
1707
- * and does not include the CDATA-section-close delimiter, `]]>`.
1708
- *
1709
- * @see https://www.w3.org/TR/xml/#NT-CharData
1710
- * @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
1711
- */
1712
- return buf.push(node.data
1713
- .replace(/[<&>]/g,_xmlEncoder)
1714
- );
1715
- case CDATA_SECTION_NODE:
1716
- return buf.push('<![CDATA[', node.data.replace(/]]>/g, ']]]]><![CDATA[>'), ']]>');
1717
- case COMMENT_NODE:
1718
- return buf.push( "<!--",node.data,"-->");
1719
- case DOCUMENT_TYPE_NODE:
1720
- var pubid = node.publicId;
1721
- var sysid = node.systemId;
1722
- buf.push('<!DOCTYPE ',node.name);
1723
- if(pubid){
1724
- buf.push(' PUBLIC ', pubid);
1725
- if (sysid && sysid!='.') {
1726
- buf.push(' ', sysid);
1904
+ buf.push('<![CDATA[', n.data.replace(/]]>/g, ']]]]><![CDATA[>'), ']]>');
1905
+ return null;
1906
+
1907
+ case COMMENT_NODE:
1908
+ if (requireWellFormed && n.data.indexOf('-->') !== -1) {
1909
+ throw new DOMException(INVALID_STATE_ERR, 'The comment node data contains "-->"');
1910
+ }
1911
+ buf.push('<!--', n.data, '-->');
1912
+ return null;
1913
+
1914
+ case DOCUMENT_TYPE_NODE:
1915
+ if (requireWellFormed) {
1916
+ if (n.publicId && !/^("[\x20\r\na-zA-Z0-9\-()+,.\/:=?;!*#@$_%']*"|'[\x20\r\na-zA-Z0-9\-()+,.\/:=?;!*#@$_%'"]*')$/.test(n.publicId)) {
1917
+ throw new DOMException(INVALID_STATE_ERR, 'DocumentType publicId is not a valid PubidLiteral');
1918
+ }
1919
+ if (n.systemId && !/^("[^"]*"|'[^']*')$/.test(n.systemId)) {
1920
+ throw new DOMException(INVALID_STATE_ERR, 'DocumentType systemId is not a valid SystemLiteral');
1921
+ }
1922
+ if (n.internalSubset && n.internalSubset.indexOf(']>') !== -1) {
1923
+ throw new DOMException(INVALID_STATE_ERR, 'DocumentType internalSubset contains "]>"');
1924
+ }
1925
+ }
1926
+ var pubid = n.publicId;
1927
+ var sysid = n.systemId;
1928
+ buf.push('<!DOCTYPE ', n.name);
1929
+ if (pubid) {
1930
+ buf.push(' PUBLIC ', pubid);
1931
+ if (sysid && sysid != '.') {
1932
+ buf.push(' ', sysid);
1933
+ }
1934
+ buf.push('>');
1935
+ } else if (sysid && sysid != '.') {
1936
+ buf.push(' SYSTEM ', sysid, '>');
1937
+ } else {
1938
+ var sub = n.internalSubset;
1939
+ if (sub) {
1940
+ buf.push(' [', sub, ']');
1941
+ }
1942
+ buf.push('>');
1943
+ }
1944
+ return null;
1945
+
1946
+ case PROCESSING_INSTRUCTION_NODE:
1947
+ if (requireWellFormed && n.data.indexOf('?>') !== -1) {
1948
+ throw new DOMException(INVALID_STATE_ERR, 'The ProcessingInstruction data contains "?>"');
1949
+ }
1950
+ buf.push('<?', n.target, ' ', n.data, '?>');
1951
+ return null;
1952
+
1953
+ case ENTITY_REFERENCE_NODE:
1954
+ buf.push('&', n.nodeName, ';');
1955
+ return null;
1956
+
1957
+ //case ENTITY_NODE:
1958
+ //case NOTATION_NODE:
1959
+ default:
1960
+ buf.push('??', n.nodeName);
1961
+ return null;
1727
1962
  }
1728
- buf.push('>');
1729
- }else if(sysid && sysid!='.'){
1730
- buf.push(' SYSTEM ', sysid, '>');
1731
- }else{
1732
- var sub = node.internalSubset;
1733
- if(sub){
1734
- buf.push(" [",sub,"]");
1963
+ },
1964
+ exit: function (n, childCtx) {
1965
+ if (childCtx && childCtx.tag) {
1966
+ buf.push('</', childCtx.tag, '>');
1735
1967
  }
1736
- buf.push(">");
1737
- }
1738
- return;
1739
- case PROCESSING_INSTRUCTION_NODE:
1740
- return buf.push( "<?",node.target," ",node.data,"?>");
1741
- case ENTITY_REFERENCE_NODE:
1742
- return buf.push( '&',node.nodeName,';');
1743
- //case ENTITY_NODE:
1744
- //case NOTATION_NODE:
1745
- default:
1746
- buf.push('??',node.nodeName);
1747
- }
1968
+ },
1969
+ });
1748
1970
  }
1749
- function importNode(doc,node,deep){
1750
- var node2;
1751
- switch (node.nodeType) {
1752
- case ELEMENT_NODE:
1753
- node2 = node.cloneNode(false);
1754
- node2.ownerDocument = doc;
1755
- //var attrs = node2.attributes;
1756
- //var len = attrs.length;
1757
- //for(var i=0;i<len;i++){
1758
- //node2.setAttributeNodeNS(importNode(doc,attrs.item(i),deep));
1759
- //}
1760
- case DOCUMENT_FRAGMENT_NODE:
1761
- break;
1762
- case ATTRIBUTE_NODE:
1763
- deep = true;
1764
- break;
1765
- //case ENTITY_REFERENCE_NODE:
1766
- //case PROCESSING_INSTRUCTION_NODE:
1767
- ////case TEXT_NODE:
1768
- //case CDATA_SECTION_NODE:
1769
- //case COMMENT_NODE:
1770
- // deep = false;
1771
- // break;
1772
- //case DOCUMENT_NODE:
1773
- //case DOCUMENT_TYPE_NODE:
1774
- //cannot be imported.
1775
- //case ENTITY_NODE:
1776
- //case NOTATION_NODE:
1777
- //can not hit in level3
1778
- //default:throw e;
1779
- }
1780
- if(!node2){
1781
- node2 = node.cloneNode(false);//false
1782
- }
1783
- node2.ownerDocument = doc;
1784
- node2.parentNode = null;
1785
- if(deep){
1786
- var child = node.firstChild;
1787
- while(child){
1788
- node2.appendChild(importNode(doc,child,deep));
1789
- child = child.nextSibling;
1790
- }
1791
- }
1792
- return node2;
1971
+ /**
1972
+ * Imports a node from a different document into `doc`, creating a new copy.
1973
+ * Delegates to {@link walkDOM} for traversal. Each node in the subtree is shallow-cloned,
1974
+ * stamped with `doc` as its `ownerDocument`, and detached (`parentNode` set to `null`).
1975
+ * Children are imported recursively when `deep` is `true`; for {@link Attr} nodes `deep` is
1976
+ * always forced to `true`
1977
+ * because an attribute's value lives in a child text node.
1978
+ *
1979
+ * @param {Document} doc
1980
+ * The document that will own the imported node.
1981
+ * @param {Node} node
1982
+ * The node to import.
1983
+ * @param {boolean} deep
1984
+ * If `true`, descendants are imported recursively.
1985
+ * @returns {Node}
1986
+ * The newly imported node, now owned by `doc`.
1987
+ */
1988
+ function importNode(doc, node, deep) {
1989
+ var destRoot;
1990
+ walkDOM(node, null, {
1991
+ enter: function (srcNode, destParent) {
1992
+ // Shallow-clone the node and stamp it into the target document.
1993
+ var destNode = srcNode.cloneNode(false);
1994
+ destNode.ownerDocument = doc;
1995
+ destNode.parentNode = null;
1996
+ // capture as the root of the imported subtree or attach to parent.
1997
+ if (destParent === null) {
1998
+ destRoot = destNode;
1999
+ } else {
2000
+ destParent.appendChild(destNode);
2001
+ }
2002
+ // ATTRIBUTE_NODE must always be imported deeply: its value lives in a child text node.
2003
+ var shouldDeep = srcNode.nodeType === ATTRIBUTE_NODE || deep;
2004
+ return shouldDeep ? destNode : null;
2005
+ },
2006
+ });
2007
+ return destRoot;
1793
2008
  }
1794
2009
  //
1795
2010
  //var _relationMap = {firstChild:1,lastChild:1,previousSibling:1,nextSibling:1,
1796
2011
  // attributes:1,childNodes:1,parentNode:1,documentElement:1,doctype,};
1797
- function cloneNode(doc,node,deep){
1798
- var node2 = new node.constructor();
1799
- for (var n in node) {
1800
- if (Object.prototype.hasOwnProperty.call(node, n)) {
1801
- var v = node[n];
1802
- if (typeof v != "object") {
1803
- if (v != node2[n]) {
1804
- node2[n] = v;
2012
+ function cloneNode(doc, node, deep) {
2013
+ var destRoot;
2014
+ walkDOM(node, null, {
2015
+ enter: function (srcNode, destParent) {
2016
+ // 1. Create a blank node of the same type and copy all scalar own properties.
2017
+ var destNode = new srcNode.constructor();
2018
+ for (var n in srcNode) {
2019
+ if (Object.prototype.hasOwnProperty.call(srcNode, n)) {
2020
+ var v = srcNode[n];
2021
+ if (typeof v != 'object') {
2022
+ if (v != destNode[n]) {
2023
+ destNode[n] = v;
2024
+ }
2025
+ }
1805
2026
  }
1806
2027
  }
1807
- }
1808
- }
1809
- if(node.childNodes){
1810
- node2.childNodes = new NodeList();
1811
- }
1812
- node2.ownerDocument = doc;
1813
- switch (node2.nodeType) {
1814
- case ELEMENT_NODE:
1815
- var attrs = node.attributes;
1816
- var attrs2 = node2.attributes = new NamedNodeMap();
1817
- var len = attrs.length
1818
- attrs2._ownerElement = node2;
1819
- for(var i=0;i<len;i++){
1820
- node2.setAttributeNode(cloneNode(doc,attrs.item(i),true));
1821
- }
1822
- break;;
1823
- case ATTRIBUTE_NODE:
1824
- deep = true;
1825
- }
1826
- if(deep){
1827
- var child = node.firstChild;
1828
- while(child){
1829
- node2.appendChild(cloneNode(doc,child,deep));
1830
- child = child.nextSibling;
1831
- }
1832
- }
1833
- return node2;
2028
+ if (srcNode.childNodes) {
2029
+ destNode.childNodes = new NodeList();
2030
+ }
2031
+ destNode.ownerDocument = doc;
2032
+ // 2. Handle node-type-specific setup.
2033
+ // Attributes are not DOM children, so they are cloned inline here
2034
+ // rather than by walkDOM descent.
2035
+ // ATTRIBUTE_NODE forces deep=true so its own children are walked.
2036
+ var shouldDeep = deep;
2037
+ switch (destNode.nodeType) {
2038
+ case ELEMENT_NODE:
2039
+ var attrs = srcNode.attributes;
2040
+ var attrs2 = (destNode.attributes = new NamedNodeMap());
2041
+ var len = attrs.length;
2042
+ attrs2._ownerElement = destNode;
2043
+ for (var i = 0; i < len; i++) {
2044
+ destNode.setAttributeNode(cloneNode(doc, attrs.item(i), true));
2045
+ }
2046
+ break;
2047
+ case ATTRIBUTE_NODE:
2048
+ shouldDeep = true;
2049
+ }
2050
+ // 3. Attach to parent, or capture as the root of the cloned subtree.
2051
+ if (destParent !== null) {
2052
+ destParent.appendChild(destNode);
2053
+ } else {
2054
+ destRoot = destNode;
2055
+ }
2056
+ // 4. Return destNode as the context for children (causes walkDOM to descend),
2057
+ // or null to skip children (shallow clone).
2058
+ return shouldDeep ? destNode : null;
2059
+ },
2060
+ });
2061
+ return destRoot;
1834
2062
  }
1835
2063
 
1836
2064
  function __set__(object,key,value){
@@ -1846,49 +2074,55 @@ try{
1846
2074
  }
1847
2075
  });
1848
2076
 
1849
- Object.defineProperty(Node.prototype,'textContent',{
1850
- get:function(){
1851
- return getTextContent(this);
2077
+ /**
2078
+ * The text content of this node and its descendants.
2079
+ *
2080
+ * Setting `textContent` on an element or document fragment replaces all child nodes with a
2081
+ * single text node; on other nodes it sets `data`, `value`, and `nodeValue` directly.
2082
+ *
2083
+ * @type {string | null}
2084
+ * @see {@link https://dom.spec.whatwg.org/#dom-node-textcontent}
2085
+ */
2086
+ Object.defineProperty(Node.prototype, 'textContent', {
2087
+ get: function () {
2088
+ if (this.nodeType === ELEMENT_NODE || this.nodeType === DOCUMENT_FRAGMENT_NODE) {
2089
+ var buf = [];
2090
+ walkDOM(this, null, {
2091
+ enter: function (n) {
2092
+ if (n.nodeType === ELEMENT_NODE || n.nodeType === DOCUMENT_FRAGMENT_NODE) {
2093
+ return true; // enter children
2094
+ }
2095
+ if (n.nodeType === PROCESSING_INSTRUCTION_NODE || n.nodeType === COMMENT_NODE) {
2096
+ return null; // excluded from text content
2097
+ }
2098
+ buf.push(n.nodeValue);
2099
+ },
2100
+ });
2101
+ return buf.join('');
2102
+ }
2103
+ return this.nodeValue;
1852
2104
  },
1853
2105
 
1854
- set:function(data){
1855
- switch(this.nodeType){
1856
- case ELEMENT_NODE:
1857
- case DOCUMENT_FRAGMENT_NODE:
1858
- while(this.firstChild){
1859
- this.removeChild(this.firstChild);
1860
- }
1861
- if(data || String(data)){
1862
- this.appendChild(this.ownerDocument.createTextNode(data));
1863
- }
1864
- break;
2106
+ set: function (data) {
2107
+ switch (this.nodeType) {
2108
+ case ELEMENT_NODE:
2109
+ case DOCUMENT_FRAGMENT_NODE:
2110
+ while (this.firstChild) {
2111
+ this.removeChild(this.firstChild);
2112
+ }
2113
+ if (data || String(data)) {
2114
+ this.appendChild(this.ownerDocument.createTextNode(data));
2115
+ }
2116
+ break;
1865
2117
 
1866
- default:
1867
- this.data = data;
1868
- this.value = data;
1869
- this.nodeValue = data;
2118
+ default:
2119
+ this.data = data;
2120
+ this.value = data;
2121
+ this.nodeValue = data;
1870
2122
  }
1871
- }
2123
+ },
1872
2124
  })
1873
2125
 
1874
- function getTextContent(node){
1875
- switch(node.nodeType){
1876
- case ELEMENT_NODE:
1877
- case DOCUMENT_FRAGMENT_NODE:
1878
- var buf = [];
1879
- node = node.firstChild;
1880
- while(node){
1881
- if(node.nodeType!==7 && node.nodeType !==8){
1882
- buf.push(getTextContent(node));
1883
- }
1884
- node = node.nextSibling;
1885
- }
1886
- return buf.join('');
1887
- default:
1888
- return node.nodeValue;
1889
- }
1890
- }
1891
-
1892
2126
  __set__ = function(object,key,value){
1893
2127
  //console.log(value)
1894
2128
  object['$$'+key] = value
@@ -1904,5 +2138,6 @@ try{
1904
2138
  exports.Element = Element;
1905
2139
  exports.Node = Node;
1906
2140
  exports.NodeList = NodeList;
2141
+ exports.walkDOM = walkDOM;
1907
2142
  exports.XMLSerializer = XMLSerializer;
1908
2143
  //}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmldom/xmldom",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
4
4
  "description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.",
5
5
  "keywords": [
6
6
  "w3c",
@@ -27,6 +27,9 @@
27
27
  "index.d.ts",
28
28
  "lib"
29
29
  ],
30
+ "config": {
31
+ "test_stack_size": 256
32
+ },
30
33
  "scripts": {
31
34
  "lint": "eslint lib test",
32
35
  "format": "prettier --write test",
@@ -35,7 +38,7 @@
35
38
  "start": "nodemon --watch package.json --watch lib --watch test --exec 'npm --silent run test && npm --silent run lint'",
36
39
  "stryker": "stryker run",
37
40
  "stryker:dry-run": "stryker run -m '' --reporters progress",
38
- "test": "jest",
41
+ "test": "node --stack-size=$npm_package_config_test_stack_size ./node_modules/.bin/jest",
39
42
  "testrelease": "npm test && eslint lib",
40
43
  "version": "./changelog-has-version.sh",
41
44
  "release": "np --no-yarn --test-script testrelease --branch release-0.8.x patch"