@xmldom/xmldom 0.9.8 → 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}
@@ -1082,7 +1109,7 @@ Node.prototype = {
1082
1109
  var parent = other;
1083
1110
  do {
1084
1111
  if (this === parent) return true;
1085
- parent = other.parentNode;
1112
+ parent = parent.parentNode;
1086
1113
  } while (parent);
1087
1114
  return false;
1088
1115
  },
@@ -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
1537
1638
  }
1538
- } while ((node = node.nextSibling));
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);
1651
+ }
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);
@@ -2210,10 +2368,22 @@ Document.prototype = {
2210
2368
  return node;
2211
2369
  },
2212
2370
  /**
2371
+ * Returns a new CDATASection node whose data is `data`.
2372
+ *
2373
+ * __This implementation differs from the specification:__ - calling this method on an HTML
2374
+ * document does not throw `NotSupportedError`.
2375
+ *
2213
2376
  * @param {string} data
2214
2377
  * @returns {CDATASection}
2378
+ * @throws {DOMException}
2379
+ * With code `INVALID_CHARACTER_ERR` if `data` contains `"]]>"`.
2380
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection
2381
+ * @see https://dom.spec.whatwg.org/#dom-document-createcdatasection
2215
2382
  */
2216
2383
  createCDATASection: function (data) {
2384
+ if (data.indexOf(']]>') !== -1) {
2385
+ throw new DOMException(DOMException.INVALID_CHARACTER_ERR, 'data contains "]]>"');
2386
+ }
2217
2387
  var node = new CDATASection(PDC);
2218
2388
  node.ownerDocument = this;
2219
2389
  node.childNodes = new NodeList();
@@ -2221,9 +2391,24 @@ Document.prototype = {
2221
2391
  return node;
2222
2392
  },
2223
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
+ *
2224
2406
  * @param {string} target
2225
2407
  * @param {string} data
2226
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
2227
2412
  */
2228
2413
  createProcessingInstruction: function (target, data) {
2229
2414
  var node = new ProcessingInstruction(PDC);
@@ -2656,6 +2841,31 @@ CDATASection.prototype = {
2656
2841
  };
2657
2842
  _extends(CDATASection, Text);
2658
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
+ */
2659
2869
  function DocumentType(symbol) {
2660
2870
  checkSymbol(symbol);
2661
2871
  }
@@ -2693,11 +2903,82 @@ function ProcessingInstruction(symbol) {
2693
2903
  ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE;
2694
2904
  _extends(ProcessingInstruction, CharacterData);
2695
2905
  function XMLSerializer() {}
2696
- XMLSerializer.prototype.serializeToString = function (node, nodeFilter) {
2697
- return nodeSerializeToString.call(node, nodeFilter);
2906
+ /**
2907
+ * Returns the result of serializing `node` to XML.
2908
+ *
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.
2932
+ *
2933
+ * @param {Node} node
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.
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
2959
+ * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring
2960
+ * @see https://github.com/w3c/DOM-Parsing/issues/84
2961
+ * @prettierignore
2962
+ */
2963
+ XMLSerializer.prototype.serializeToString = function (node, options) {
2964
+ return nodeSerializeToString.call(node, options);
2698
2965
  };
2699
2966
  Node.prototype.toString = nodeSerializeToString;
2700
- 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
+ }
2701
2982
  var buf = [];
2702
2983
  var refNode = (this.nodeType === DOCUMENT_NODE && this.documentElement) || this;
2703
2984
  var prefix = refNode.prefix;
@@ -2712,7 +2993,7 @@ function nodeSerializeToString(nodeFilter) {
2712
2993
  ];
2713
2994
  }
2714
2995
  }
2715
- serializeToString(this, buf, nodeFilter, visibleNamespaces);
2996
+ serializeToString(this, buf, visibleNamespaces, opts);
2716
2997
  return buf.join('');
2717
2998
  }
2718
2999
 
@@ -2762,235 +3043,317 @@ function addSerializedAttribute(buf, qualifiedName, value) {
2762
3043
  buf.push(' ', qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"');
2763
3044
  }
2764
3045
 
2765
- function serializeToString(node, buf, nodeFilter, visibleNamespaces) {
3046
+ function serializeToString(node, buf, visibleNamespaces, opts) {
2766
3047
  if (!visibleNamespaces) {
2767
3048
  visibleNamespaces = [];
2768
3049
  }
3050
+ var nodeFilter = opts.nodeFilter;
3051
+ var requireWellFormed = opts.requireWellFormed;
3052
+ var splitCDATASections = opts.splitCDATASections;
2769
3053
  var doc = node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
2770
3054
  var isHTML = doc.type === 'html';
2771
3055
 
2772
- if (nodeFilter) {
2773
- node = nodeFilter(node);
2774
- if (node) {
2775
- if (typeof node == 'string') {
2776
- buf.push(node);
2777
- return;
2778
- }
2779
- } else {
2780
- return;
2781
- }
2782
- //buf.sort.apply(attrs, attributeSorter);
2783
- }
2784
-
2785
- switch (node.nodeType) {
2786
- case ELEMENT_NODE:
2787
- var attrs = node.attributes;
2788
- var len = attrs.length;
2789
- var child = node.firstChild;
2790
- var nodeName = node.tagName;
2791
-
2792
- var prefixedNodeName = nodeName;
2793
- if (!isHTML && !node.prefix && node.namespaceURI) {
2794
- var defaultNS;
2795
- // lookup current default ns from `xmlns` attribute
2796
- for (var ai = 0; ai < attrs.length; ai++) {
2797
- if (attrs.item(ai).name === 'xmlns') {
2798
- defaultNS = attrs.item(ai).value;
2799
- break;
2800
- }
2801
- }
2802
- if (!defaultNS) {
2803
- // lookup current default ns in visibleNamespaces
2804
- for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
2805
- var namespace = visibleNamespaces[nsi];
2806
- if (namespace.prefix === '' && namespace.namespace === node.namespaceURI) {
2807
- defaultNS = namespace.namespace;
2808
- 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;
2809
3069
  }
3070
+ } else {
3071
+ return null;
2810
3072
  }
2811
3073
  }
2812
- if (defaultNS !== node.namespaceURI) {
2813
- for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
2814
- var namespace = visibleNamespaces[nsi];
2815
- if (namespace.namespace === node.namespaceURI) {
2816
- if (namespace.prefix) {
2817
- 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
+ }
2818
3111
  }
2819
- break;
2820
3112
  }
2821
- }
2822
- }
2823
- }
2824
3113
 
2825
- 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
+ }
2826
3132
 
2827
- for (var i = 0; i < len; i++) {
2828
- // add namespaces for attributes
2829
- var attr = attrs.item(i);
2830
- if (attr.prefix == 'xmlns') {
2831
- visibleNamespaces.push({
2832
- prefix: attr.localName,
2833
- namespace: attr.value,
2834
- });
2835
- } else if (attr.nodeName == 'xmlns') {
2836
- visibleNamespaces.push({ prefix: '', namespace: attr.value });
2837
- }
2838
- }
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
+ }
2839
3151
 
2840
- for (var i = 0; i < len; i++) {
2841
- var attr = attrs.item(i);
2842
- if (needNamespaceDefine(attr, isHTML, visibleNamespaces)) {
2843
- var prefix = attr.prefix || '';
2844
- var uri = attr.namespaceURI;
2845
- addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : 'xmlns', uri);
2846
- visibleNamespaces.push({ prefix: prefix, namespace: uri });
2847
- }
2848
- serializeToString(attr, buf, nodeFilter, visibleNamespaces);
2849
- }
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
+ }
2850
3159
 
2851
- // add namespace for current node
2852
- if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
2853
- var prefix = node.prefix || '';
2854
- var uri = node.namespaceURI;
2855
- addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : 'xmlns', uri);
2856
- visibleNamespaces.push({ prefix: prefix, namespace: uri });
2857
- }
2858
- // in XML elements can be closed when they have no children
2859
- var canCloseTag = !child;
2860
- if (canCloseTag && (isHTML || node.namespaceURI === NAMESPACE.HTML)) {
2861
- // in HTML (doc or ns) only void elements can be closed right away
2862
- canCloseTag = isHTMLVoidElement(nodeName);
2863
- }
2864
- if (canCloseTag) {
2865
- buf.push('/>');
2866
- } else {
2867
- buf.push('>');
2868
- //if is cdata child node
2869
- if (isHTML && isHTMLRawTextElement(nodeName)) {
2870
- while (child) {
2871
- if (child.data) {
2872
- 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);
2873
3235
  } else {
2874
- serializeToString(child, buf, nodeFilter, visibleNamespaces.slice());
3236
+ buf.push(g.CDATA_START, n.data, g.CDATA_END);
2875
3237
  }
2876
- child = child.nextSibling;
2877
- }
2878
- } else {
2879
- while (child) {
2880
- serializeToString(child, buf, nodeFilter, visibleNamespaces.slice());
2881
- child = child.nextSibling;
2882
- }
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;
2883
3309
  }
2884
- buf.push('</', prefixedNodeName, '>');
2885
- }
2886
- // remove added visible namespaces
2887
- //visibleNamespaces.length = startVisibleNamespaces;
2888
- return;
2889
- case DOCUMENT_NODE:
2890
- case DOCUMENT_FRAGMENT_NODE:
2891
- var child = node.firstChild;
2892
- while (child) {
2893
- serializeToString(child, buf, nodeFilter, visibleNamespaces.slice());
2894
- child = child.nextSibling;
2895
- }
2896
- return;
2897
- case ATTRIBUTE_NODE:
2898
- return addSerializedAttribute(buf, node.name, node.value);
2899
- case TEXT_NODE:
2900
- /*
2901
- * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
2902
- * except when used as markup delimiters, or within a comment, a processing instruction,
2903
- * or a CDATA section.
2904
- * If they are needed elsewhere, they must be escaped using either numeric character
2905
- * references or the strings `&amp;` and `&lt;` respectively.
2906
- * The right angle bracket (>) may be represented using the string " &gt; ",
2907
- * and must, for compatibility, be escaped using either `&gt;`,
2908
- * or a character reference when it appears in the string `]]>` in content,
2909
- * when that string is not marking the end of a CDATA section.
2910
- *
2911
- * In the content of elements, character data is any string of characters which does not
2912
- * contain the start-delimiter of any markup and does not include the CDATA-section-close
2913
- * delimiter, `]]>`.
2914
- *
2915
- * @see https://www.w3.org/TR/xml/#NT-CharData
2916
- * @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
2917
- */
2918
- return buf.push(node.data.replace(/[<&>]/g, _xmlEncoder));
2919
- case CDATA_SECTION_NODE:
2920
- return buf.push(g.CDATA_START, node.data, g.CDATA_END);
2921
- case COMMENT_NODE:
2922
- return buf.push(g.COMMENT_START, node.data, g.COMMENT_END);
2923
- case DOCUMENT_TYPE_NODE:
2924
- var pubid = node.publicId;
2925
- var sysid = node.systemId;
2926
- buf.push(g.DOCTYPE_DECL_START, ' ', node.name);
2927
- if (pubid) {
2928
- buf.push(' ', g.PUBLIC, ' ', pubid);
2929
- if (sysid && sysid !== '.') {
2930
- 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, '>');
2931
3315
  }
2932
- } else if (sysid && sysid !== '.') {
2933
- buf.push(' ', g.SYSTEM, ' ', sysid);
2934
- }
2935
- if (node.internalSubset) {
2936
- buf.push(' [', node.internalSubset, ']');
2937
- }
2938
- buf.push('>');
2939
- return;
2940
- case PROCESSING_INSTRUCTION_NODE:
2941
- return buf.push('<?', node.target, ' ', node.data, '?>');
2942
- case ENTITY_REFERENCE_NODE:
2943
- return buf.push('&', node.nodeName, ';');
2944
- //case ENTITY_NODE:
2945
- //case NOTATION_NODE:
2946
- default:
2947
- buf.push('??', node.nodeName);
2948
- }
3316
+ },
3317
+ }
3318
+ );
2949
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
+ */
2950
3337
  function importNode(doc, node, deep) {
2951
- var node2;
2952
- switch (node.nodeType) {
2953
- case ELEMENT_NODE:
2954
- node2 = node.cloneNode(false);
2955
- node2.ownerDocument = doc;
2956
- //var attrs = node2.attributes;
2957
- //var len = attrs.length;
2958
- //for(var i=0;i<len;i++){
2959
- //node2.setAttributeNodeNS(importNode(doc,attrs.item(i),deep));
2960
- //}
2961
- case DOCUMENT_FRAGMENT_NODE:
2962
- break;
2963
- case ATTRIBUTE_NODE:
2964
- deep = true;
2965
- break;
2966
- //case ENTITY_REFERENCE_NODE:
2967
- //case PROCESSING_INSTRUCTION_NODE:
2968
- ////case TEXT_NODE:
2969
- //case CDATA_SECTION_NODE:
2970
- //case COMMENT_NODE:
2971
- // deep = false;
2972
- // break;
2973
- //case DOCUMENT_NODE:
2974
- //case DOCUMENT_TYPE_NODE:
2975
- //cannot be imported.
2976
- //case ENTITY_NODE:
2977
- //case NOTATION_NODE:
2978
- //can not hit in level3
2979
- //default:throw e;
2980
- }
2981
- if (!node2) {
2982
- node2 = node.cloneNode(false); //false
2983
- }
2984
- node2.ownerDocument = doc;
2985
- node2.parentNode = null;
2986
- if (deep) {
2987
- var child = node.firstChild;
2988
- while (child) {
2989
- node2.appendChild(importNode(doc, child, deep));
2990
- child = child.nextSibling;
2991
- }
2992
- }
2993
- 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;
2994
3357
  }
2995
3358
 
2996
3359
  /**
@@ -3010,47 +3373,76 @@ function importNode(doc, node, deep) {
3010
3373
  * potentially invoked in this function) do not meet their specific constraints.
3011
3374
  */
3012
3375
  function cloneNode(doc, node, deep) {
3013
- var node2 = new node.constructor(PDC);
3014
- for (var n in node) {
3015
- if (hasOwn(node, n)) {
3016
- var v = node[n];
3017
- if (typeof v != 'object') {
3018
- if (v != node2[n]) {
3019
- 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
+ }
3020
3389
  }
3021
3390
  }
3022
- }
3023
- }
3024
- if (node.childNodes) {
3025
- node2.childNodes = new NodeList();
3026
- }
3027
- node2.ownerDocument = doc;
3028
- switch (node2.nodeType) {
3029
- case ELEMENT_NODE:
3030
- var attrs = node.attributes;
3031
- var attrs2 = (node2.attributes = new NamedNodeMap());
3032
- var len = attrs.length;
3033
- attrs2._ownerElement = node2;
3034
- for (var i = 0; i < len; i++) {
3035
- node2.setAttributeNode(cloneNode(doc, attrs.item(i), true));
3391
+ if (srcNode.childNodes) {
3392
+ destNode.childNodes = new NodeList();
3036
3393
  }
3037
- break;
3038
- case ATTRIBUTE_NODE:
3039
- deep = true;
3040
- }
3041
- if (deep) {
3042
- var child = node.firstChild;
3043
- while (child) {
3044
- node2.appendChild(cloneNode(doc, child, deep));
3045
- child = child.nextSibling;
3046
- }
3047
- }
3048
- 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;
3049
3425
  }
3050
3426
 
3051
3427
  function __set__(object, key, value) {
3052
3428
  object[key] = value;
3053
3429
  }
3430
+
3431
+ // Returns a new array of direct Element children.
3432
+ // Passed to LiveNodeList to implement ParentNode.children.
3433
+ // https://dom.spec.whatwg.org/#dom-parentnode-children
3434
+ function childrenRefresh(node) {
3435
+ var ls = [];
3436
+ var child = node.firstChild;
3437
+ while (child) {
3438
+ if (child.nodeType === ELEMENT_NODE) {
3439
+ ls.push(child);
3440
+ }
3441
+ child = child.nextSibling;
3442
+ }
3443
+ return ls;
3444
+ }
3445
+
3054
3446
  //do dynamic
3055
3447
  try {
3056
3448
  if (Object.defineProperty) {
@@ -3061,9 +3453,37 @@ try {
3061
3453
  },
3062
3454
  });
3063
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
+ */
3064
3469
  Object.defineProperty(Node.prototype, 'textContent', {
3065
3470
  get: function () {
3066
- 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;
3067
3487
  },
3068
3488
 
3069
3489
  set: function (data) {
@@ -3086,23 +3506,21 @@ try {
3086
3506
  },
3087
3507
  });
3088
3508
 
3089
- function getTextContent(node) {
3090
- switch (node.nodeType) {
3091
- case ELEMENT_NODE:
3092
- case DOCUMENT_FRAGMENT_NODE:
3093
- var buf = [];
3094
- node = node.firstChild;
3095
- while (node) {
3096
- if (node.nodeType !== 7 && node.nodeType !== 8) {
3097
- buf.push(getTextContent(node));
3098
- }
3099
- node = node.nextSibling;
3100
- }
3101
- return buf.join('');
3102
- default:
3103
- return node.nodeValue;
3104
- }
3105
- }
3509
+ Object.defineProperty(Element.prototype, 'children', {
3510
+ get: function () {
3511
+ return new LiveNodeList(this, childrenRefresh);
3512
+ },
3513
+ });
3514
+ Object.defineProperty(Document.prototype, 'children', {
3515
+ get: function () {
3516
+ return new LiveNodeList(this, childrenRefresh);
3517
+ },
3518
+ });
3519
+ Object.defineProperty(DocumentFragment.prototype, 'children', {
3520
+ get: function () {
3521
+ return new LiveNodeList(this, childrenRefresh);
3522
+ },
3523
+ });
3106
3524
 
3107
3525
  __set__ = function (object, key, value) {
3108
3526
  //console.log(value)
@@ -3132,4 +3550,5 @@ exports.NodeList = NodeList;
3132
3550
  exports.Notation = Notation;
3133
3551
  exports.Text = Text;
3134
3552
  exports.ProcessingInstruction = ProcessingInstruction;
3553
+ exports.walkDOM = walkDOM;
3135
3554
  exports.XMLSerializer = XMLSerializer;