@xmldom/xmldom 0.9.9 → 0.9.10

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/lib/dom.js CHANGED
@@ -330,14 +330,31 @@ NodeList.prototype = {
330
330
  /**
331
331
  * Returns a string representation of the NodeList.
332
332
  *
333
- * @param {unknown} nodeFilter
334
- * __A filter function? Not implemented according to the spec?__.
333
+ * Accepts the same `options` object as `XMLSerializer.prototype.serializeToString`
334
+ * (`requireWellFormed`, `splitCDATASections`, `nodeFilter`). Passing a function is treated as
335
+ * a legacy `nodeFilter` for backward compatibility.
336
+ *
337
+ * @param {Object | function} [options]
338
+ * @param {boolean} [options.requireWellFormed=false]
339
+ * @param {boolean} [options.splitCDATASections=true]
340
+ * @param {function} [options.nodeFilter]
335
341
  * @returns {string}
336
- * A string representation of the NodeList.
337
342
  */
338
- toString: function (nodeFilter) {
343
+ toString: function (options) {
344
+ var opts;
345
+ if (typeof options === 'function') {
346
+ opts = { requireWellFormed: false, splitCDATASections: true, nodeFilter: options };
347
+ } else if (!!options) {
348
+ opts = {
349
+ requireWellFormed: !!options.requireWellFormed,
350
+ splitCDATASections: options.splitCDATASections !== false,
351
+ nodeFilter: options.nodeFilter || null,
352
+ };
353
+ } else {
354
+ opts = { requireWellFormed: false, splitCDATASections: true, nodeFilter: null };
355
+ }
339
356
  for (var buf = [], i = 0; i < this.length; i++) {
340
- serializeToString(this[i], buf, nodeFilter);
357
+ serializeToString(this[i], buf, null, opts);
341
358
  }
342
359
  return buf.join('');
343
360
  },
@@ -866,11 +883,21 @@ DOMImplementation.prototype = {
866
883
  * The {@link https://www.w3.org/TR/DOM-Level-3-Core/glossary.html#dt-qualifiedname qualified
867
884
  * name} of the document type to be created.
868
885
  * @param {string} [publicId]
869
- * The external subset public identifier.
886
+ * The external subset public identifier. Stored verbatim including surrounding quotes.
887
+ * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
888
+ * if the value is non-empty and does not match the XML `PubidLiteral` production
889
+ * (W3C DOM Parsing §3.2.1.3; XML 1.0 production [12]). Creation-time validation is not
890
+ * enforced — deferred to a future breaking release.
870
891
  * @param {string} [systemId]
871
- * The external subset system identifier.
892
+ * The external subset system identifier. Stored verbatim including surrounding quotes.
893
+ * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
894
+ * if the value is non-empty and does not match the XML `SystemLiteral` production
895
+ * (W3C DOM Parsing §3.2.1.3; XML 1.0 production [11]). Creation-time validation is not
896
+ * enforced — deferred to a future breaking release.
872
897
  * @param {string} [internalSubset]
873
- * the internal subset or an empty string if it is not present
898
+ * The internal subset or an empty string if it is not present. Stored verbatim.
899
+ * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
900
+ * if the value contains `"]>"`. Creation-time validation is not enforced.
874
901
  * @returns {DocumentType}
875
902
  * A new {@link DocumentType} node with {@link Node#ownerDocument} set to null.
876
903
  * @throws {DOMException}
@@ -1114,56 +1141,68 @@ Node.prototype = {
1114
1141
  /**
1115
1142
  * Checks whether the given node is equal to this node.
1116
1143
  *
1144
+ * Two nodes are equal when they have the same type, defining characteristics (for the type),
1145
+ * and the same childNodes. The comparison is iterative to avoid stack overflows on
1146
+ * deeply-nested trees. Attribute nodes of each Element pair are also pushed onto the stack
1147
+ * and compared the same way.
1148
+ *
1117
1149
  * @param {Node} [otherNode]
1150
+ * @returns {boolean}
1118
1151
  * @see https://dom.spec.whatwg.org/#concept-node-equals
1152
+ * @see ../docs/walk-dom.md.
1119
1153
  */
1120
1154
  isEqualNode: function (otherNode) {
1121
1155
  if (!otherNode) return false;
1122
1156
 
1123
- if (this.nodeType !== otherNode.nodeType) return false;
1124
-
1125
- switch (this.nodeType) {
1126
- case this.DOCUMENT_TYPE_NODE:
1127
- if (this.name !== otherNode.name) return false;
1128
- if (this.publicId !== otherNode.publicId) return false;
1129
- if (this.systemId !== otherNode.systemId) return false;
1130
- break;
1131
- case this.ELEMENT_NODE:
1132
- if (this.namespaceURI !== otherNode.namespaceURI) return false;
1133
- if (this.prefix !== otherNode.prefix) return false;
1134
- if (this.localName !== otherNode.localName) return false;
1135
- if (this.attributes.length !== otherNode.attributes.length) return false;
1136
- for (var i = 0; i < this.attributes.length; i++) {
1137
- var attr = this.attributes.item(i);
1138
- if (!attr.isEqualNode(otherNode.getAttributeNodeNS(attr.namespaceURI, attr.localName))) {
1139
- return false;
1157
+ // Use an explicit {node, other} pair stack to avoid call-stack overflow on deep trees.
1158
+ // walkDOM cannot be used here — parallel two-tree traversal requires pairing
1159
+ // corresponding nodes at each step across both trees simultaneously.
1160
+ var stack = [{ node: this, other: otherNode }];
1161
+ while (stack.length > 0) {
1162
+ var pair = stack.pop();
1163
+ var node = pair.node;
1164
+ var other = pair.other;
1165
+
1166
+ if (node.nodeType !== other.nodeType) return false;
1167
+
1168
+ switch (node.nodeType) {
1169
+ case node.DOCUMENT_TYPE_NODE:
1170
+ if (node.name !== other.name) return false;
1171
+ if (node.publicId !== other.publicId) return false;
1172
+ if (node.systemId !== other.systemId) return false;
1173
+ break;
1174
+ case node.ELEMENT_NODE:
1175
+ if (node.namespaceURI !== other.namespaceURI) return false;
1176
+ if (node.prefix !== other.prefix) return false;
1177
+ if (node.localName !== other.localName) return false;
1178
+ if (node.attributes.length !== other.attributes.length) return false;
1179
+ for (var i = 0; i < node.attributes.length; i++) {
1180
+ var attr = node.attributes.item(i);
1181
+ var otherAttr = other.getAttributeNodeNS(attr.namespaceURI, attr.localName);
1182
+ if (!otherAttr) return false;
1183
+ stack.push({ node: attr, other: otherAttr });
1140
1184
  }
1141
- }
1142
- break;
1143
- case this.ATTRIBUTE_NODE:
1144
- if (this.namespaceURI !== otherNode.namespaceURI) return false;
1145
- if (this.localName !== otherNode.localName) return false;
1146
- if (this.value !== otherNode.value) return false;
1147
-
1148
- break;
1149
- case this.PROCESSING_INSTRUCTION_NODE:
1150
- if (this.target !== otherNode.target || this.data !== otherNode.data) {
1151
- return false;
1152
- }
1153
- break;
1154
- case this.TEXT_NODE:
1155
- case this.COMMENT_NODE:
1156
- if (this.data !== otherNode.data) return false;
1157
- break;
1158
- }
1185
+ break;
1186
+ case node.ATTRIBUTE_NODE:
1187
+ if (node.namespaceURI !== other.namespaceURI) return false;
1188
+ if (node.localName !== other.localName) return false;
1189
+ if (node.value !== other.value) return false;
1190
+ break;
1191
+ case node.PROCESSING_INSTRUCTION_NODE:
1192
+ if (node.target !== other.target || node.data !== other.data) return false;
1193
+ break;
1194
+ case node.TEXT_NODE:
1195
+ case node.CDATA_SECTION_NODE:
1196
+ case node.COMMENT_NODE:
1197
+ if (node.data !== other.data) return false;
1198
+ break;
1199
+ }
1159
1200
 
1160
- if (this.childNodes.length !== otherNode.childNodes.length) {
1161
- return false;
1162
- }
1201
+ if (node.childNodes.length !== other.childNodes.length) return false;
1163
1202
 
1164
- for (var i = 0; i < this.childNodes.length; i++) {
1165
- if (!this.childNodes[i].isEqualNode(otherNode.childNodes[i])) {
1166
- return false;
1203
+ // Push children in reverse order so index 0 is processed first (LIFO).
1204
+ for (var i = node.childNodes.length - 1; i >= 0; i--) {
1205
+ stack.push({ node: node.childNodes[i], other: other.childNodes[i] });
1167
1206
  }
1168
1207
  }
1169
1208
 
@@ -1283,7 +1322,7 @@ Node.prototype = {
1283
1322
  * is `TEXT_NODE`) into a single node with the combined data. It also removes any empty text
1284
1323
  * nodes.
1285
1324
  *
1286
- * This method operates recursively, so it also normalizes any and all descendent nodes within
1325
+ * This method iterativly traverses all child nodes to normalize all descendent nodes within
1287
1326
  * the subtree.
1288
1327
  *
1289
1328
  * @throws {DOMException}
@@ -1292,19 +1331,28 @@ Node.prototype = {
1292
1331
  * @since Modified in DOM Level 2
1293
1332
  * @see {@link Node.removeChild}
1294
1333
  * @see {@link CharacterData.appendData}
1334
+ * @see ../docs/walk-dom.md.
1295
1335
  */
1296
1336
  normalize: function () {
1297
- var child = this.firstChild;
1298
- while (child) {
1299
- var next = child.nextSibling;
1300
- if (next && next.nodeType == TEXT_NODE && child.nodeType == TEXT_NODE) {
1301
- this.removeChild(next);
1302
- child.appendData(next.data);
1303
- } else {
1304
- child.normalize();
1305
- child = next;
1306
- }
1307
- }
1337
+ walkDOM(this, null, {
1338
+ enter: function (node) {
1339
+ // Merge adjacent text children of node before walkDOM schedules them.
1340
+ // walkDOM reads lastChild/previousSibling after enter returns, so the
1341
+ // surviving post-merge children are what it descends into.
1342
+ var child = node.firstChild;
1343
+ while (child) {
1344
+ var next = child.nextSibling;
1345
+ if (next !== null && next.nodeType === TEXT_NODE && child.nodeType === TEXT_NODE) {
1346
+ node.removeChild(next);
1347
+ child.appendData(next.data);
1348
+ // Do not advance child: re-check new nextSibling for another text run
1349
+ } else {
1350
+ child = next;
1351
+ }
1352
+ }
1353
+ return true; // descend into surviving children
1354
+ },
1355
+ });
1308
1356
  },
1309
1357
  /**
1310
1358
  * Checks whether the DOM implementation implements a specific feature and its version.
@@ -1521,24 +1569,112 @@ copy(DocumentPosition, Node);
1521
1569
  copy(DocumentPosition, Node.prototype);
1522
1570
 
1523
1571
  /**
1524
- * @param callback
1525
- * Return true for continue,false for break.
1526
- * @returns
1527
- * boolean true: break visit;
1572
+ * Visits every node in the subtree rooted at `node` in depth-first pre-order.
1573
+ *
1574
+ * Delegates to {@link walkDOM} for traversal. The `callback` is called on each node;
1575
+ * if it returns a truthy value, traversal stops immediately.
1576
+ *
1577
+ * @param {Node} node
1578
+ * Root of the subtree to visit.
1579
+ * @param {function(Node): *} callback
1580
+ * Called for each node. A truthy return value stops traversal early.
1528
1581
  */
1529
1582
  function _visitNode(node, callback) {
1530
- if (callback(node)) {
1531
- return true;
1532
- }
1533
- if ((node = node.firstChild)) {
1534
- do {
1535
- if (_visitNode(node, callback)) {
1536
- return true;
1583
+ walkDOM(node, null, {
1584
+ enter: function (n) {
1585
+ return callback(n) ? walkDOM.STOP : true;
1586
+ },
1587
+ });
1588
+ }
1589
+
1590
+ /**
1591
+ * Depth-first pre/post-order DOM tree walker.
1592
+ *
1593
+ * Visits every node in the subtree rooted at `node`. For each node:
1594
+ *
1595
+ * 1. Calls `callbacks.enter(node, context)` before descending into the node's children. The
1596
+ * return value becomes the `context` passed to each child's `enter` call and to the matching
1597
+ * `exit` call.
1598
+ * 2. If `enter` returns `null` or `undefined`, the node's children are skipped;
1599
+ * sibling traversal continues normally.
1600
+ * 3. If `enter` returns `walkDOM.STOP`, the entire traversal is aborted immediately — no
1601
+ * further `enter` or `exit` calls are made.
1602
+ * 4. `lastChild` and `previousSibling` are read **after** `enter` returns, so `enter` may
1603
+ * safely modify the node's own child list before the walker descends. Modifying siblings of
1604
+ * the current node or any other part of the tree produces unpredictable results: nodes already
1605
+ * queued on the stack are visited regardless of DOM changes, and newly inserted nodes outside
1606
+ * the current child list are never visited.
1607
+ * 5. Calls `callbacks.exit(node, context)` (if provided) after all of a node's children have
1608
+ * been visited, passing the same `context` that `enter`
1609
+ * returned for that node.
1610
+ *
1611
+ * This implementation uses an explicit stack and does not recurse — it is safe on arbitrarily
1612
+ * deep trees.
1613
+ *
1614
+ * @param {Node} node
1615
+ * Root of the subtree to walk.
1616
+ * @param {*} context
1617
+ * Initial context value passed to the root node's `enter`.
1618
+ * @param {{ enter: function(Node, *): *, exit?: function(Node, *): void }} callbacks
1619
+ * @returns {void | walkDOM.STOP}
1620
+ * @see ../docs/walk-dom.md.
1621
+ */
1622
+ function walkDOM(node, context, callbacks) {
1623
+ // Each stack frame is {node, context, phase}:
1624
+ // walkDOM.ENTER — call enter, then push children
1625
+ // walkDOM.EXIT — call exit
1626
+ var stack = [{ node: node, context: context, phase: walkDOM.ENTER }];
1627
+ while (stack.length > 0) {
1628
+ var frame = stack.pop();
1629
+ if (frame.phase === walkDOM.ENTER) {
1630
+ var childContext = callbacks.enter(frame.node, frame.context);
1631
+ if (childContext === walkDOM.STOP) {
1632
+ return walkDOM.STOP;
1633
+ }
1634
+ // Push exit frame before children so it fires after all children are processed (Last In First Out)
1635
+ stack.push({ node: frame.node, context: childContext, phase: walkDOM.EXIT });
1636
+ if (childContext === null || childContext === undefined) {
1637
+ continue; // skip children
1638
+ }
1639
+ // lastChild is read after enter returns, so enter may modify the child list.
1640
+ var child = frame.node.lastChild;
1641
+ // Traverse from lastChild backwards so that pushing onto the stack
1642
+ // naturally yields firstChild on top (processed first).
1643
+ while (child) {
1644
+ stack.push({ node: child, context: childContext, phase: walkDOM.ENTER });
1645
+ child = child.previousSibling;
1646
+ }
1647
+ } else {
1648
+ // frame.phase === walkDOM.EXIT
1649
+ if (callbacks.exit) {
1650
+ callbacks.exit(frame.node, frame.context);
1537
1651
  }
1538
- } while ((node = node.nextSibling));
1652
+ }
1539
1653
  }
1540
1654
  }
1541
1655
 
1656
+ /**
1657
+ * Sentinel value returned from a `walkDOM` `enter` callback to abort the entire traversal
1658
+ * immediately.
1659
+ *
1660
+ * @type {symbol}
1661
+ */
1662
+ walkDOM.STOP = Symbol('walkDOM.STOP');
1663
+ /**
1664
+ * Phase constant for a stack frame that has not yet been visited.
1665
+ * The `enter` callback is called and children are scheduled.
1666
+ *
1667
+ * @type {number}
1668
+ */
1669
+ walkDOM.ENTER = 0;
1670
+ /**
1671
+ * Phase constant for a stack frame whose subtree has been fully visited.
1672
+ * The `exit` callback is called.
1673
+ *
1674
+ * @type {number}
1675
+ */
1676
+ walkDOM.EXIT = 1;
1677
+
1542
1678
  /**
1543
1679
  * @typedef DocumentOptions
1544
1680
  * @property {string} [contentType=MIME_TYPE.XML_APPLICATION]
@@ -2125,7 +2261,20 @@ Document.prototype = {
2125
2261
  this.documentElement = newChild;
2126
2262
  }
2127
2263
  },
2128
- // Introduced in DOM Level 2:
2264
+ /**
2265
+ * Imports a node from another document into this document, creating a new copy owned by this
2266
+ * document. The source node and its subtree are not modified.
2267
+ *
2268
+ * @param {Node} importedNode
2269
+ * The node to import.
2270
+ * @param {boolean} deep
2271
+ * If true, the contents of the node are recursively imported.
2272
+ * If false, only the node itself (and its attributes, if it is an element) are imported.
2273
+ * @returns {Node}
2274
+ * Returns the newly created import of the node.
2275
+ * @see {@link importNode}
2276
+ * @see {@link https://dom.spec.whatwg.org/#dom-document-importnode}
2277
+ */
2129
2278
  importNode: function (importedNode, deep) {
2130
2279
  return importNode(this, importedNode, deep);
2131
2280
  },
@@ -2201,6 +2350,15 @@ Document.prototype = {
2201
2350
  /**
2202
2351
  * @param {string} data
2203
2352
  * @returns {Comment}
2353
+ * @see https://dom.spec.whatwg.org/#dom-document-createcomment
2354
+ * @see https://www.w3.org/TR/xml/#NT-Comment XML 1.0 production [15]
2355
+ * @see https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml §3.2.1.3
2356
+ *
2357
+ * Note: no validation is performed at creation time. When the resulting document is
2358
+ * serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
2359
+ * if the comment data contains `--` anywhere, ends with `-`, or contains characters
2360
+ * outside the XML Char production (W3C DOM Parsing §3.2.1.3). Without that option the
2361
+ * data is emitted verbatim.
2204
2362
  */
2205
2363
  createComment: function (data) {
2206
2364
  var node = new Comment(PDC);
@@ -2233,9 +2391,24 @@ Document.prototype = {
2233
2391
  return node;
2234
2392
  },
2235
2393
  /**
2394
+ * Returns a ProcessingInstruction node whose target is target and data is data.
2395
+ *
2396
+ * __This behavior is slightly different from the in the specs__:
2397
+ * - it does not do any input validation on the arguments and doesn't throw
2398
+ * "InvalidCharacterError".
2399
+ *
2400
+ * Note: When the resulting document is serialized with `requireWellFormed: true`, the
2401
+ * serializer throws `InvalidStateError` if `.target` contains `:` or is an ASCII
2402
+ * case-insensitive match for `"xml"`, or if `.data` contains `?>` or characters outside the
2403
+ * XML Char production (W3C DOM Parsing §3.2.1.7). Without that option the data is emitted
2404
+ * verbatim.
2405
+ *
2236
2406
  * @param {string} target
2237
2407
  * @param {string} data
2238
2408
  * @returns {ProcessingInstruction}
2409
+ * @see https://developer.mozilla.org/docs/Web/API/Document/createProcessingInstruction
2410
+ * @see https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction
2411
+ * @see https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml §3.2.1.7
2239
2412
  */
2240
2413
  createProcessingInstruction: function (target, data) {
2241
2414
  var node = new ProcessingInstruction(PDC);
@@ -2668,6 +2841,31 @@ CDATASection.prototype = {
2668
2841
  };
2669
2842
  _extends(CDATASection, Text);
2670
2843
 
2844
+ /**
2845
+ * @class DocumentType
2846
+ * @augments Node
2847
+ * @property {string} publicId
2848
+ * The external subset public identifier, stored verbatim (including surrounding quotes).
2849
+ * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this constraint —
2850
+ * direct property writes succeed and the written value is serialized verbatim.
2851
+ * When serialized with `requireWellFormed: true`, the serializer validates the value against
2852
+ * the XML `PubidLiteral` production and throws `InvalidStateError` if it does not match.
2853
+ * @property {string} systemId
2854
+ * The external subset system identifier, stored verbatim (including surrounding quotes).
2855
+ * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this constraint —
2856
+ * direct property writes succeed and the written value is serialized verbatim.
2857
+ * When serialized with `requireWellFormed: true`, the serializer validates the value against
2858
+ * the XML `SystemLiteral` production and throws `InvalidStateError` if it does not match.
2859
+ * @property {string} internalSubset
2860
+ * The internal subset string (the raw content between `[` and `]`), or an empty string.
2861
+ * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this constraint —
2862
+ * direct property writes succeed and the written value is serialized verbatim.
2863
+ * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
2864
+ * if the value contains `"]>"`.
2865
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/DocumentType MDN
2866
+ * @see https://dom.spec.whatwg.org/#interface-documenttype WHATWG DOM
2867
+ * @prettierignore
2868
+ */
2671
2869
  function DocumentType(symbol) {
2672
2870
  checkSymbol(symbol);
2673
2871
  }
@@ -2708,21 +2906,79 @@ function XMLSerializer() {}
2708
2906
  /**
2709
2907
  * Returns the result of serializing `node` to XML.
2710
2908
  *
2711
- * __This implementation differs from the specification:__ - CDATASection nodes whose data
2712
- * contains `]]>` are serialized by splitting the section at each `]]>` occurrence (following
2713
- * W3C DOM Level 3 Core `split-cdata-sections`
2714
- * default behaviour). A configurable option is not yet implemented.
2909
+ * When `options.requireWellFormed` is `true`, the serializer throws `InvalidStateError` for
2910
+ * content that would produce ill-formed XML (e.g. CDATASection data containing `"]]>"`, Text
2911
+ * data containing characters outside the XML Char production, or a Document with no
2912
+ * `documentElement`).
2913
+ *
2914
+ * When `options.splitCDATASections` is `false`, CDATASection data is emitted verbatim even
2915
+ * when it contains `"]]>"`. When `true` (the default), `"]]>"` sequences are split across
2916
+ * concatenated CDATA sections — this behavior is **deprecated** and will be removed in the
2917
+ * next breaking release. Callers should migrate to `{ requireWellFormed: true }`, which throws
2918
+ * `InvalidStateError` instead of transforming.
2919
+ *
2920
+ * __This implementation differs from the specification:__ - CDATASection serialization is not
2921
+ * specified by W3C DOM Parsing or WHATWG DOM Parsing (see
2922
+ * {@link https://github.com/w3c/DOM-Parsing/issues/38 w3c/DOM-Parsing#38}).
2923
+ * When `splitCDATASections` is `true` (the default), `"]]>"` sequences in CDATASection data
2924
+ * are split across concatenated CDATA sections — this mechanism is derived from DOM Level 3
2925
+ * Core and is **deprecated**. The split mechanics will be removed in the next breaking
2926
+ * release. Callers that rely on this behavior should migrate to `{ requireWellFormed: true }`.
2927
+ * - W3C DOM Parsing §3.2.1.1 requires well-formedness checks on Element `localName`s,
2928
+ * prefixes,
2929
+ * and attribute serialization (duplicate attributes, namespace declarations, attribute value
2930
+ * characters) when `requireWellFormed` is `true`. These checks are **not implemented** in this
2931
+ * release — see the tracking issue filed against the next breaking milestone.
2715
2932
  *
2716
2933
  * @param {Node} node
2717
- * @param {function} [nodeFilter]
2934
+ * @param {Object | function} [options]
2935
+ * Options object, or a legacy nodeFilter function (backward compatible).
2936
+ * @param {boolean} [options.requireWellFormed=false]
2937
+ * When `true`, throws `InvalidStateError` for content that would produce ill-formed XML.
2938
+ * @param {boolean} [options.splitCDATASections=true]
2939
+ * When `true` (default), splits `"]]>"` sequences in CDATASection data across concatenated
2940
+ * CDATA sections. **Deprecated** — will be removed in the next breaking release.
2941
+ * @param {function} [options.nodeFilter]
2942
+ * A filter function applied to each node before serialization.
2718
2943
  * @returns {string}
2944
+ * @throws {DOMException}
2945
+ * With name `InvalidStateError` when `requireWellFormed` is `true` and any of the following
2946
+ * conditions hold:
2947
+ * - CDATASection data contains `"]]>"`
2948
+ * - Text data contains characters outside the XML Char production
2949
+ * - a Comment node's data contains `--` anywhere or ends with `-`
2950
+ * - a ProcessingInstruction's target contains `:` or is an ASCII case-insensitive match for
2951
+ * `"xml"`, or its data contains `?>` or characters outside the XML Char production
2952
+ * - a DocumentType's `publicId` is non-empty and does not match the XML `PubidLiteral`
2953
+ * production (W3C DOM Parsing §3.2.1.3; XML 1.0 production [12])
2954
+ * - a DocumentType's `systemId` is non-empty and does not match the XML `SystemLiteral`
2955
+ * production (W3C DOM Parsing §3.2.1.3; XML 1.0 production [11])
2956
+ * - a DocumentType's `internalSubset` contains `"]>"`
2957
+ * - the Document has no `documentElement`
2958
+ * @see https://developer.mozilla.org/docs/Web/API/XMLSerializer/serializeToString
2719
2959
  * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring
2960
+ * @see https://github.com/w3c/DOM-Parsing/issues/84
2961
+ * @prettierignore
2720
2962
  */
2721
- XMLSerializer.prototype.serializeToString = function (node, nodeFilter) {
2722
- return nodeSerializeToString.call(node, nodeFilter);
2963
+ XMLSerializer.prototype.serializeToString = function (node, options) {
2964
+ return nodeSerializeToString.call(node, options);
2723
2965
  };
2724
2966
  Node.prototype.toString = nodeSerializeToString;
2725
- function nodeSerializeToString(nodeFilter) {
2967
+ function nodeSerializeToString(options) {
2968
+ // Normalize the user-supplied options into a single internal opts object so that the
2969
+ // internal serializer always works with a consistent shape rather than positional flags.
2970
+ var opts;
2971
+ if (typeof options === 'function') {
2972
+ opts = { requireWellFormed: false, splitCDATASections: true, nodeFilter: options };
2973
+ } else if (options != null) {
2974
+ opts = {
2975
+ requireWellFormed: !!options.requireWellFormed,
2976
+ splitCDATASections: options.splitCDATASections !== false,
2977
+ nodeFilter: options.nodeFilter || null,
2978
+ };
2979
+ } else {
2980
+ opts = { requireWellFormed: false, splitCDATASections: true, nodeFilter: null };
2981
+ }
2726
2982
  var buf = [];
2727
2983
  var refNode = (this.nodeType === DOCUMENT_NODE && this.documentElement) || this;
2728
2984
  var prefix = refNode.prefix;
@@ -2737,7 +2993,7 @@ function nodeSerializeToString(nodeFilter) {
2737
2993
  ];
2738
2994
  }
2739
2995
  }
2740
- serializeToString(this, buf, nodeFilter, visibleNamespaces);
2996
+ serializeToString(this, buf, visibleNamespaces, opts);
2741
2997
  return buf.join('');
2742
2998
  }
2743
2999
 
@@ -2787,235 +3043,317 @@ function addSerializedAttribute(buf, qualifiedName, value) {
2787
3043
  buf.push(' ', qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"');
2788
3044
  }
2789
3045
 
2790
- function serializeToString(node, buf, nodeFilter, visibleNamespaces) {
3046
+ function serializeToString(node, buf, visibleNamespaces, opts) {
2791
3047
  if (!visibleNamespaces) {
2792
3048
  visibleNamespaces = [];
2793
3049
  }
3050
+ var nodeFilter = opts.nodeFilter;
3051
+ var requireWellFormed = opts.requireWellFormed;
3052
+ var splitCDATASections = opts.splitCDATASections;
2794
3053
  var doc = node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
2795
3054
  var isHTML = doc.type === 'html';
2796
3055
 
2797
- if (nodeFilter) {
2798
- node = nodeFilter(node);
2799
- if (node) {
2800
- if (typeof node == 'string') {
2801
- buf.push(node);
2802
- return;
2803
- }
2804
- } else {
2805
- return;
2806
- }
2807
- //buf.sort.apply(attrs, attributeSorter);
2808
- }
2809
-
2810
- switch (node.nodeType) {
2811
- case ELEMENT_NODE:
2812
- var attrs = node.attributes;
2813
- var len = attrs.length;
2814
- var child = node.firstChild;
2815
- var nodeName = node.tagName;
2816
-
2817
- var prefixedNodeName = nodeName;
2818
- if (!isHTML && !node.prefix && node.namespaceURI) {
2819
- var defaultNS;
2820
- // lookup current default ns from `xmlns` attribute
2821
- for (var ai = 0; ai < attrs.length; ai++) {
2822
- if (attrs.item(ai).name === 'xmlns') {
2823
- defaultNS = attrs.item(ai).value;
2824
- break;
2825
- }
2826
- }
2827
- if (!defaultNS) {
2828
- // lookup current default ns in visibleNamespaces
2829
- for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
2830
- var namespace = visibleNamespaces[nsi];
2831
- if (namespace.prefix === '' && namespace.namespace === node.namespaceURI) {
2832
- defaultNS = namespace.namespace;
2833
- break;
3056
+ walkDOM(
3057
+ node,
3058
+ { ns: visibleNamespaces },
3059
+ {
3060
+ enter: function (n, ctx) {
3061
+ var namespaces = ctx.ns;
3062
+
3063
+ if (nodeFilter) {
3064
+ n = nodeFilter(n);
3065
+ if (n) {
3066
+ if (typeof n == 'string') {
3067
+ buf.push(n);
3068
+ return null;
2834
3069
  }
3070
+ } else {
3071
+ return null;
2835
3072
  }
2836
3073
  }
2837
- if (defaultNS !== node.namespaceURI) {
2838
- for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
2839
- var namespace = visibleNamespaces[nsi];
2840
- if (namespace.namespace === node.namespaceURI) {
2841
- if (namespace.prefix) {
2842
- prefixedNodeName = namespace.prefix + ':' + nodeName;
3074
+
3075
+ switch (n.nodeType) {
3076
+ case ELEMENT_NODE:
3077
+ var attrs = n.attributes;
3078
+ var len = attrs.length;
3079
+ var nodeName = n.tagName;
3080
+
3081
+ var prefixedNodeName = nodeName;
3082
+ if (!isHTML && !n.prefix && n.namespaceURI) {
3083
+ var defaultNS;
3084
+ // lookup current default ns from `xmlns` attribute
3085
+ for (var ai = 0; ai < attrs.length; ai++) {
3086
+ if (attrs.item(ai).name === 'xmlns') {
3087
+ defaultNS = attrs.item(ai).value;
3088
+ break;
3089
+ }
3090
+ }
3091
+ if (!defaultNS) {
3092
+ // lookup current default ns in visibleNamespaces
3093
+ for (var nsi = namespaces.length - 1; nsi >= 0; nsi--) {
3094
+ var nsEntry = namespaces[nsi];
3095
+ if (nsEntry.prefix === '' && nsEntry.namespace === n.namespaceURI) {
3096
+ defaultNS = nsEntry.namespace;
3097
+ break;
3098
+ }
3099
+ }
3100
+ }
3101
+ if (defaultNS !== n.namespaceURI) {
3102
+ for (var nsi = namespaces.length - 1; nsi >= 0; nsi--) {
3103
+ var nsEntry = namespaces[nsi];
3104
+ if (nsEntry.namespace === n.namespaceURI) {
3105
+ if (nsEntry.prefix) {
3106
+ prefixedNodeName = nsEntry.prefix + ':' + nodeName;
3107
+ }
3108
+ break;
3109
+ }
3110
+ }
2843
3111
  }
2844
- break;
2845
3112
  }
2846
- }
2847
- }
2848
- }
2849
3113
 
2850
- buf.push('<', prefixedNodeName);
3114
+ buf.push('<', prefixedNodeName);
3115
+
3116
+ // Build a fresh namespace snapshot for this element's children.
3117
+ // The slice prevents sibling elements from inheriting each other's declarations.
3118
+ var childNamespaces = namespaces.slice();
3119
+
3120
+ for (var i = 0; i < len; i++) {
3121
+ // add namespaces for attributes
3122
+ var attr = attrs.item(i);
3123
+ if (attr.prefix == 'xmlns') {
3124
+ childNamespaces.push({
3125
+ prefix: attr.localName,
3126
+ namespace: attr.value,
3127
+ });
3128
+ } else if (attr.nodeName == 'xmlns') {
3129
+ childNamespaces.push({ prefix: '', namespace: attr.value });
3130
+ }
3131
+ }
2851
3132
 
2852
- for (var i = 0; i < len; i++) {
2853
- // add namespaces for attributes
2854
- var attr = attrs.item(i);
2855
- if (attr.prefix == 'xmlns') {
2856
- visibleNamespaces.push({
2857
- prefix: attr.localName,
2858
- namespace: attr.value,
2859
- });
2860
- } else if (attr.nodeName == 'xmlns') {
2861
- visibleNamespaces.push({ prefix: '', namespace: attr.value });
2862
- }
2863
- }
3133
+ for (var i = 0; i < len; i++) {
3134
+ var attr = attrs.item(i);
3135
+ if (needNamespaceDefine(attr, isHTML, childNamespaces)) {
3136
+ var attrPrefix = attr.prefix || '';
3137
+ var uri = attr.namespaceURI;
3138
+ addSerializedAttribute(buf, attrPrefix ? 'xmlns:' + attrPrefix : 'xmlns', uri);
3139
+ childNamespaces.push({ prefix: attrPrefix, namespace: uri });
3140
+ }
3141
+ // Apply nodeFilter and serialize the attribute.
3142
+ var filteredAttr = nodeFilter ? nodeFilter(attr) : attr;
3143
+ if (filteredAttr) {
3144
+ if (typeof filteredAttr === 'string') {
3145
+ buf.push(filteredAttr);
3146
+ } else {
3147
+ addSerializedAttribute(buf, filteredAttr.name, filteredAttr.value);
3148
+ }
3149
+ }
3150
+ }
2864
3151
 
2865
- for (var i = 0; i < len; i++) {
2866
- var attr = attrs.item(i);
2867
- if (needNamespaceDefine(attr, isHTML, visibleNamespaces)) {
2868
- var prefix = attr.prefix || '';
2869
- var uri = attr.namespaceURI;
2870
- addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : 'xmlns', uri);
2871
- visibleNamespaces.push({ prefix: prefix, namespace: uri });
2872
- }
2873
- serializeToString(attr, buf, nodeFilter, visibleNamespaces);
2874
- }
3152
+ // add namespace for current node
3153
+ if (nodeName === prefixedNodeName && needNamespaceDefine(n, isHTML, childNamespaces)) {
3154
+ var nodePrefix = n.prefix || '';
3155
+ var uri = n.namespaceURI;
3156
+ addSerializedAttribute(buf, nodePrefix ? 'xmlns:' + nodePrefix : 'xmlns', uri);
3157
+ childNamespaces.push({ prefix: nodePrefix, namespace: uri });
3158
+ }
2875
3159
 
2876
- // add namespace for current node
2877
- if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
2878
- var prefix = node.prefix || '';
2879
- var uri = node.namespaceURI;
2880
- addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : 'xmlns', uri);
2881
- visibleNamespaces.push({ prefix: prefix, namespace: uri });
2882
- }
2883
- // in XML elements can be closed when they have no children
2884
- var canCloseTag = !child;
2885
- if (canCloseTag && (isHTML || node.namespaceURI === NAMESPACE.HTML)) {
2886
- // in HTML (doc or ns) only void elements can be closed right away
2887
- canCloseTag = isHTMLVoidElement(nodeName);
2888
- }
2889
- if (canCloseTag) {
2890
- buf.push('/>');
2891
- } else {
2892
- buf.push('>');
2893
- //if is cdata child node
2894
- if (isHTML && isHTMLRawTextElement(nodeName)) {
2895
- while (child) {
2896
- if (child.data) {
2897
- buf.push(child.data);
3160
+ // in XML elements can be closed when they have no children
3161
+ var canCloseTag = !n.firstChild;
3162
+ if (canCloseTag && (isHTML || n.namespaceURI === NAMESPACE.HTML)) {
3163
+ // in HTML (doc or ns) only void elements can be closed right away
3164
+ canCloseTag = isHTMLVoidElement(nodeName);
3165
+ }
3166
+ if (canCloseTag) {
3167
+ buf.push('/>');
3168
+ // Self-closing: no children and no closing tag needed from exit.
3169
+ return null;
3170
+ }
3171
+
3172
+ buf.push('>');
3173
+
3174
+ // HTML raw text elements: serialize children as raw data without further descent.
3175
+ if (isHTML && isHTMLRawTextElement(nodeName)) {
3176
+ var child = n.firstChild;
3177
+ while (child) {
3178
+ if (child.data) {
3179
+ buf.push(child.data);
3180
+ } else {
3181
+ serializeToString(child, buf, childNamespaces.slice(), opts);
3182
+ }
3183
+ child = child.nextSibling;
3184
+ }
3185
+ buf.push('</', prefixedNodeName, '>');
3186
+ // Children handled manually above; prevent walkDOM from also traversing them.
3187
+ return null;
3188
+ }
3189
+
3190
+ // Return child context so walkDOM descends; exit will emit the closing tag.
3191
+ return { ns: childNamespaces, tag: prefixedNodeName };
3192
+ case DOCUMENT_NODE:
3193
+ case DOCUMENT_FRAGMENT_NODE:
3194
+ if (requireWellFormed && n.nodeType === DOCUMENT_NODE && n.documentElement == null) {
3195
+ throw new DOMException('The Document has no documentElement', DOMExceptionName.InvalidStateError);
3196
+ }
3197
+ // Pass namespaces through; each child element will slice independently.
3198
+ return { ns: namespaces };
3199
+ case ATTRIBUTE_NODE:
3200
+ addSerializedAttribute(buf, n.name, n.value);
3201
+ return null;
3202
+ case TEXT_NODE:
3203
+ /*
3204
+ * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
3205
+ * except when used as markup delimiters, or within a comment, a processing instruction,
3206
+ * or a CDATA section.
3207
+ * If they are needed elsewhere, they must be escaped using either numeric character
3208
+ * references or the strings `&amp;` and `&lt;` respectively.
3209
+ * The right angle bracket (>) may be represented using the string " &gt; ",
3210
+ * and must, for compatibility, be escaped using either `&gt;`,
3211
+ * or a character reference when it appears in the string `]]>` in content,
3212
+ * when that string is not marking the end of a CDATA section.
3213
+ *
3214
+ * In the content of elements, character data is any string of characters which does not
3215
+ * contain the start-delimiter of any markup and does not include the CDATA-section-close
3216
+ * delimiter, `]]>`.
3217
+ *
3218
+ * @see https://www.w3.org/TR/xml/#NT-CharData
3219
+ * @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
3220
+ */
3221
+ if (requireWellFormed && g.InvalidChar.test(n.data)) {
3222
+ throw new DOMException(
3223
+ 'The Text node data contains characters outside the XML Char production',
3224
+ DOMExceptionName.InvalidStateError
3225
+ );
3226
+ }
3227
+ buf.push(n.data.replace(/[<&>]/g, _xmlEncoder));
3228
+ return null;
3229
+ case CDATA_SECTION_NODE:
3230
+ if (requireWellFormed && n.data.indexOf(']]>') !== -1) {
3231
+ throw new DOMException('The CDATASection data contains "]]>"', DOMExceptionName.InvalidStateError);
3232
+ }
3233
+ if (splitCDATASections) {
3234
+ buf.push(g.CDATA_START, n.data.replace(/]]>/g, ']]]]><![CDATA[>'), g.CDATA_END);
2898
3235
  } else {
2899
- serializeToString(child, buf, nodeFilter, visibleNamespaces.slice());
3236
+ buf.push(g.CDATA_START, n.data, g.CDATA_END);
2900
3237
  }
2901
- child = child.nextSibling;
2902
- }
2903
- } else {
2904
- while (child) {
2905
- serializeToString(child, buf, nodeFilter, visibleNamespaces.slice());
2906
- child = child.nextSibling;
2907
- }
3238
+ return null;
3239
+ case COMMENT_NODE:
3240
+ if (requireWellFormed) {
3241
+ if (g.InvalidChar.test(n.data)) {
3242
+ throw new DOMException(
3243
+ 'The comment node data contains characters outside the XML Char production',
3244
+ DOMExceptionName.InvalidStateError
3245
+ );
3246
+ }
3247
+ if (n.data.indexOf('--') !== -1 || n.data[n.data.length - 1] === '-') {
3248
+ throw new DOMException(
3249
+ 'The comment node data contains "--" or ends with "-"',
3250
+ DOMExceptionName.InvalidStateError
3251
+ );
3252
+ }
3253
+ }
3254
+ buf.push(g.COMMENT_START, n.data, g.COMMENT_END);
3255
+ return null;
3256
+ case DOCUMENT_TYPE_NODE:
3257
+ var pubid = n.publicId;
3258
+ var sysid = n.systemId;
3259
+ if (requireWellFormed) {
3260
+ if (pubid && !g.PubidLiteral_match.test(pubid)) {
3261
+ throw new DOMException('DocumentType publicId is not a valid PubidLiteral', DOMExceptionName.InvalidStateError);
3262
+ }
3263
+ if (sysid && sysid !== '.' && !g.SystemLiteral_match.test(sysid)) {
3264
+ throw new DOMException('DocumentType systemId is not a valid SystemLiteral', DOMExceptionName.InvalidStateError);
3265
+ }
3266
+ if (n.internalSubset && n.internalSubset.indexOf(']>') !== -1) {
3267
+ throw new DOMException('DocumentType internalSubset contains "]>"', DOMExceptionName.InvalidStateError);
3268
+ }
3269
+ }
3270
+ buf.push(g.DOCTYPE_DECL_START, ' ', n.name);
3271
+ if (pubid) {
3272
+ buf.push(' ', g.PUBLIC, ' ', pubid);
3273
+ if (sysid && sysid !== '.') {
3274
+ buf.push(' ', sysid);
3275
+ }
3276
+ } else if (sysid && sysid !== '.') {
3277
+ buf.push(' ', g.SYSTEM, ' ', sysid);
3278
+ }
3279
+ if (n.internalSubset) {
3280
+ buf.push(' [', n.internalSubset, ']');
3281
+ }
3282
+ buf.push('>');
3283
+ return null;
3284
+ case PROCESSING_INSTRUCTION_NODE:
3285
+ if (requireWellFormed) {
3286
+ if (n.target.indexOf(':') !== -1 || n.target.toLowerCase() === 'xml') {
3287
+ throw new DOMException('The ProcessingInstruction target is not well-formed', DOMExceptionName.InvalidStateError);
3288
+ }
3289
+ if (g.InvalidChar.test(n.data)) {
3290
+ throw new DOMException(
3291
+ 'The ProcessingInstruction data contains characters outside the XML Char production',
3292
+ DOMExceptionName.InvalidStateError
3293
+ );
3294
+ }
3295
+ if (n.data.indexOf('?>') !== -1) {
3296
+ throw new DOMException('The ProcessingInstruction data contains "?>"', DOMExceptionName.InvalidStateError);
3297
+ }
3298
+ }
3299
+ buf.push('<?', n.target, ' ', n.data, '?>');
3300
+ return null;
3301
+ case ENTITY_REFERENCE_NODE:
3302
+ buf.push('&', n.nodeName, ';');
3303
+ return null;
3304
+ //case ENTITY_NODE:
3305
+ //case NOTATION_NODE:
3306
+ default:
3307
+ buf.push('??', n.nodeName);
3308
+ return null;
2908
3309
  }
2909
- buf.push('</', prefixedNodeName, '>');
2910
- }
2911
- // remove added visible namespaces
2912
- //visibleNamespaces.length = startVisibleNamespaces;
2913
- return;
2914
- case DOCUMENT_NODE:
2915
- case DOCUMENT_FRAGMENT_NODE:
2916
- var child = node.firstChild;
2917
- while (child) {
2918
- serializeToString(child, buf, nodeFilter, visibleNamespaces.slice());
2919
- child = child.nextSibling;
2920
- }
2921
- return;
2922
- case ATTRIBUTE_NODE:
2923
- return addSerializedAttribute(buf, node.name, node.value);
2924
- case TEXT_NODE:
2925
- /*
2926
- * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
2927
- * except when used as markup delimiters, or within a comment, a processing instruction,
2928
- * or a CDATA section.
2929
- * If they are needed elsewhere, they must be escaped using either numeric character
2930
- * references or the strings `&amp;` and `&lt;` respectively.
2931
- * The right angle bracket (>) may be represented using the string " &gt; ",
2932
- * and must, for compatibility, be escaped using either `&gt;`,
2933
- * or a character reference when it appears in the string `]]>` in content,
2934
- * when that string is not marking the end of a CDATA section.
2935
- *
2936
- * In the content of elements, character data is any string of characters which does not
2937
- * contain the start-delimiter of any markup and does not include the CDATA-section-close
2938
- * delimiter, `]]>`.
2939
- *
2940
- * @see https://www.w3.org/TR/xml/#NT-CharData
2941
- * @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
2942
- */
2943
- return buf.push(node.data.replace(/[<&>]/g, _xmlEncoder));
2944
- case CDATA_SECTION_NODE:
2945
- return buf.push(g.CDATA_START, node.data.replace(/]]>/g, ']]]]><![CDATA[>'), g.CDATA_END);
2946
- case COMMENT_NODE:
2947
- return buf.push(g.COMMENT_START, node.data, g.COMMENT_END);
2948
- case DOCUMENT_TYPE_NODE:
2949
- var pubid = node.publicId;
2950
- var sysid = node.systemId;
2951
- buf.push(g.DOCTYPE_DECL_START, ' ', node.name);
2952
- if (pubid) {
2953
- buf.push(' ', g.PUBLIC, ' ', pubid);
2954
- if (sysid && sysid !== '.') {
2955
- buf.push(' ', sysid);
3310
+ },
3311
+ exit: function (n, childCtx) {
3312
+ // Emit the closing tag for elements that were opened (not self-closed, not raw text).
3313
+ if (childCtx && childCtx.tag) {
3314
+ buf.push('</', childCtx.tag, '>');
2956
3315
  }
2957
- } else if (sysid && sysid !== '.') {
2958
- buf.push(' ', g.SYSTEM, ' ', sysid);
2959
- }
2960
- if (node.internalSubset) {
2961
- buf.push(' [', node.internalSubset, ']');
2962
- }
2963
- buf.push('>');
2964
- return;
2965
- case PROCESSING_INSTRUCTION_NODE:
2966
- return buf.push('<?', node.target, ' ', node.data, '?>');
2967
- case ENTITY_REFERENCE_NODE:
2968
- return buf.push('&', node.nodeName, ';');
2969
- //case ENTITY_NODE:
2970
- //case NOTATION_NODE:
2971
- default:
2972
- buf.push('??', node.nodeName);
2973
- }
3316
+ },
3317
+ }
3318
+ );
2974
3319
  }
3320
+ /**
3321
+ * Imports a node from a different document into `doc`, creating a new copy.
3322
+ * Delegates to {@link walkDOM} for traversal. Each node in the subtree is shallow-cloned,
3323
+ * stamped with `doc` as its `ownerDocument`, and detached (`parentNode` set to `null`).
3324
+ * Children are imported recursively when `deep` is `true`; for {@link Attr} nodes `deep` is
3325
+ * always forced to `true`
3326
+ * because an attribute's value lives in a child text node.
3327
+ *
3328
+ * @param {Document} doc
3329
+ * The document that will own the imported node.
3330
+ * @param {Node} node
3331
+ * The node to import.
3332
+ * @param {boolean} deep
3333
+ * If `true`, descendants are imported recursively.
3334
+ * @returns {Node}
3335
+ * The newly imported node, now owned by `doc`.
3336
+ */
2975
3337
  function importNode(doc, node, deep) {
2976
- var node2;
2977
- switch (node.nodeType) {
2978
- case ELEMENT_NODE:
2979
- node2 = node.cloneNode(false);
2980
- node2.ownerDocument = doc;
2981
- //var attrs = node2.attributes;
2982
- //var len = attrs.length;
2983
- //for(var i=0;i<len;i++){
2984
- //node2.setAttributeNodeNS(importNode(doc,attrs.item(i),deep));
2985
- //}
2986
- case DOCUMENT_FRAGMENT_NODE:
2987
- break;
2988
- case ATTRIBUTE_NODE:
2989
- deep = true;
2990
- break;
2991
- //case ENTITY_REFERENCE_NODE:
2992
- //case PROCESSING_INSTRUCTION_NODE:
2993
- ////case TEXT_NODE:
2994
- //case CDATA_SECTION_NODE:
2995
- //case COMMENT_NODE:
2996
- // deep = false;
2997
- // break;
2998
- //case DOCUMENT_NODE:
2999
- //case DOCUMENT_TYPE_NODE:
3000
- //cannot be imported.
3001
- //case ENTITY_NODE:
3002
- //case NOTATION_NODE:
3003
- //can not hit in level3
3004
- //default:throw e;
3005
- }
3006
- if (!node2) {
3007
- node2 = node.cloneNode(false); //false
3008
- }
3009
- node2.ownerDocument = doc;
3010
- node2.parentNode = null;
3011
- if (deep) {
3012
- var child = node.firstChild;
3013
- while (child) {
3014
- node2.appendChild(importNode(doc, child, deep));
3015
- child = child.nextSibling;
3016
- }
3017
- }
3018
- return node2;
3338
+ var destRoot;
3339
+ walkDOM(node, null, {
3340
+ enter: function (srcNode, destParent) {
3341
+ // Shallow-clone the node and stamp it into the target document.
3342
+ var destNode = srcNode.cloneNode(false);
3343
+ destNode.ownerDocument = doc;
3344
+ destNode.parentNode = null;
3345
+ // capture as the root of the imported subtree or attach to parent.
3346
+ if (destParent === null) {
3347
+ destRoot = destNode;
3348
+ } else {
3349
+ destParent.appendChild(destNode);
3350
+ }
3351
+ // ATTRIBUTE_NODE must always be imported deeply: its value lives in a child text node.
3352
+ var shouldDeep = srcNode.nodeType === ATTRIBUTE_NODE || deep;
3353
+ return shouldDeep ? destNode : null;
3354
+ },
3355
+ });
3356
+ return destRoot;
3019
3357
  }
3020
3358
 
3021
3359
  /**
@@ -3035,42 +3373,55 @@ function importNode(doc, node, deep) {
3035
3373
  * potentially invoked in this function) do not meet their specific constraints.
3036
3374
  */
3037
3375
  function cloneNode(doc, node, deep) {
3038
- var node2 = new node.constructor(PDC);
3039
- for (var n in node) {
3040
- if (hasOwn(node, n)) {
3041
- var v = node[n];
3042
- if (typeof v != 'object') {
3043
- if (v != node2[n]) {
3044
- node2[n] = v;
3376
+ var destRoot;
3377
+ walkDOM(node, null, {
3378
+ enter: function (srcNode, destParent) {
3379
+ // 1. Create a blank node of the same type and copy all scalar own properties.
3380
+ var destNode = new srcNode.constructor(PDC);
3381
+ for (var n in srcNode) {
3382
+ if (hasOwn(srcNode, n)) {
3383
+ var v = srcNode[n];
3384
+ if (typeof v != 'object') {
3385
+ if (v != destNode[n]) {
3386
+ destNode[n] = v;
3387
+ }
3388
+ }
3045
3389
  }
3046
3390
  }
3047
- }
3048
- }
3049
- if (node.childNodes) {
3050
- node2.childNodes = new NodeList();
3051
- }
3052
- node2.ownerDocument = doc;
3053
- switch (node2.nodeType) {
3054
- case ELEMENT_NODE:
3055
- var attrs = node.attributes;
3056
- var attrs2 = (node2.attributes = new NamedNodeMap());
3057
- var len = attrs.length;
3058
- attrs2._ownerElement = node2;
3059
- for (var i = 0; i < len; i++) {
3060
- node2.setAttributeNode(cloneNode(doc, attrs.item(i), true));
3391
+ if (srcNode.childNodes) {
3392
+ destNode.childNodes = new NodeList();
3061
3393
  }
3062
- break;
3063
- case ATTRIBUTE_NODE:
3064
- deep = true;
3065
- }
3066
- if (deep) {
3067
- var child = node.firstChild;
3068
- while (child) {
3069
- node2.appendChild(cloneNode(doc, child, deep));
3070
- child = child.nextSibling;
3071
- }
3072
- }
3073
- return node2;
3394
+ destNode.ownerDocument = doc;
3395
+ // 2. Handle node-type-specific setup.
3396
+ // Attributes are not DOM children, so they are cloned inline here
3397
+ // rather than by walkDOM descent.
3398
+ // ATTRIBUTE_NODE forces deep=true so its own children are walked.
3399
+ var shouldDeep = deep;
3400
+ switch (destNode.nodeType) {
3401
+ case ELEMENT_NODE:
3402
+ var attrs = srcNode.attributes;
3403
+ var attrs2 = (destNode.attributes = new NamedNodeMap());
3404
+ var len = attrs.length;
3405
+ attrs2._ownerElement = destNode;
3406
+ for (var i = 0; i < len; i++) {
3407
+ destNode.setAttributeNode(cloneNode(doc, attrs.item(i), true));
3408
+ }
3409
+ break;
3410
+ case ATTRIBUTE_NODE:
3411
+ shouldDeep = true;
3412
+ }
3413
+ // 3. Attach to parent, or capture as the root of the cloned subtree.
3414
+ if (destParent !== null) {
3415
+ destParent.appendChild(destNode);
3416
+ } else {
3417
+ destRoot = destNode;
3418
+ }
3419
+ // 4. Return destNode as the context for children (causes walkDOM to descend),
3420
+ // or null to skip children (shallow clone).
3421
+ return shouldDeep ? destNode : null;
3422
+ },
3423
+ });
3424
+ return destRoot;
3074
3425
  }
3075
3426
 
3076
3427
  function __set__(object, key, value) {
@@ -3102,9 +3453,37 @@ try {
3102
3453
  },
3103
3454
  });
3104
3455
 
3456
+ /**
3457
+ * The text content of this node and its descendants.
3458
+ *
3459
+ * For {@link Element} and {@link DocumentFragment} nodes, returns the concatenation of the
3460
+ * `nodeValue` of every descendant text node, excluding processing instruction and comment
3461
+ * nodes. For all other node types, returns `nodeValue`.
3462
+ *
3463
+ * Setting `textContent` on an element or document fragment replaces all child nodes with a
3464
+ * single text node; on other nodes it sets `data`, `value`, and `nodeValue` directly.
3465
+ *
3466
+ * @type {string | null}
3467
+ * @see {@link https://dom.spec.whatwg.org/#dom-node-textcontent}
3468
+ */
3105
3469
  Object.defineProperty(Node.prototype, 'textContent', {
3106
3470
  get: function () {
3107
- return getTextContent(this);
3471
+ if (this.nodeType === ELEMENT_NODE || this.nodeType === DOCUMENT_FRAGMENT_NODE) {
3472
+ var buf = [];
3473
+ walkDOM(this, null, {
3474
+ enter: function (n) {
3475
+ if (n.nodeType === ELEMENT_NODE || n.nodeType === DOCUMENT_FRAGMENT_NODE) {
3476
+ return true; // enter children
3477
+ }
3478
+ if (n.nodeType === PROCESSING_INSTRUCTION_NODE || n.nodeType === COMMENT_NODE) {
3479
+ return null; // excluded from text content
3480
+ }
3481
+ buf.push(n.nodeValue);
3482
+ },
3483
+ });
3484
+ return buf.join('');
3485
+ }
3486
+ return this.nodeValue;
3108
3487
  },
3109
3488
 
3110
3489
  set: function (data) {
@@ -3127,24 +3506,6 @@ try {
3127
3506
  },
3128
3507
  });
3129
3508
 
3130
- function getTextContent(node) {
3131
- switch (node.nodeType) {
3132
- case ELEMENT_NODE:
3133
- case DOCUMENT_FRAGMENT_NODE:
3134
- var buf = [];
3135
- node = node.firstChild;
3136
- while (node) {
3137
- if (node.nodeType !== 7 && node.nodeType !== 8) {
3138
- buf.push(getTextContent(node));
3139
- }
3140
- node = node.nextSibling;
3141
- }
3142
- return buf.join('');
3143
- default:
3144
- return node.nodeValue;
3145
- }
3146
- }
3147
-
3148
3509
  Object.defineProperty(Element.prototype, 'children', {
3149
3510
  get: function () {
3150
3511
  return new LiveNodeList(this, childrenRefresh);
@@ -3189,4 +3550,5 @@ exports.NodeList = NodeList;
3189
3550
  exports.Notation = Notation;
3190
3551
  exports.Text = Text;
3191
3552
  exports.ProcessingInstruction = ProcessingInstruction;
3553
+ exports.walkDOM = walkDOM;
3192
3554
  exports.XMLSerializer = XMLSerializer;