diff --git a/dist/index.js b/dist/index.js index 9e7c354..1e8f82a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -717,9 +717,10 @@ NodeList.prototype = { item: function(index) { return index >= 0 && index < this.length ? this[index] : null; }, - toString:function(isHTML,nodeFilter){ + toString:function(isHTML,nodeFilter,options){ + var requireWellFormed = !!options && !!options.requireWellFormed; for(var buf = [], i = 0;i 0) { + var frame = stack.pop(); + if (frame.phase === walkDOM.ENTER) { + var childContext = callbacks.enter(frame.node, frame.context); + if (childContext === walkDOM.STOP) { + return walkDOM.STOP; + } + // Push exit frame before children so it fires after all children are processed (Last In First Out) + stack.push({ node: frame.node, context: childContext, phase: walkDOM.EXIT }); + if (childContext === null || childContext === undefined) { + continue; // skip children + } + // lastChild is read after enter returns, so enter may modify the child list. + var child = frame.node.lastChild; + // Traverse from lastChild backwards so that pushing onto the stack + // naturally yields firstChild on top (processed first). + while (child) { + stack.push({ node: child, context: childContext, phase: walkDOM.ENTER }); + child = child.previousSibling; + } + } else { + // frame.phase === walkDOM.EXIT + if (callbacks.exit) { + callbacks.exit(frame.node, frame.context); + } + } + } +} +/** + * Sentinel value returned from a `walkDOM` `enter` callback to abort the entire traversal + * immediately. + * + * @type {symbol} + */ +walkDOM.STOP = Symbol('walkDOM.STOP'); +/** + * Phase constant for a stack frame that has not yet been visited. + * The `enter` callback is called and children are scheduled. + * + * @type {number} + */ +walkDOM.ENTER = 0; +/** + * Phase constant for a stack frame whose subtree has been fully visited. + * The `exit` callback is called. + * + * @type {number} + */ +walkDOM.EXIT = 1; function Document(){ this.ownerDocument = this; @@ -1764,6 +1888,23 @@ Document.prototype = { node.appendData(data) return node; }, + /** + * Returns a ProcessingInstruction node whose target is target and data is data. + * + * __This implementation differs from the specification:__ + * - it does not do any input validation on the arguments and doesn't throw "InvalidCharacterError". + * + * Note: When the resulting document is serialized with `requireWellFormed: true`, the + * serializer throws with code `INVALID_STATE_ERR` if `.data` contains `?>` (W3C DOM Parsing + * §3.2.1.7). Without that option the data is emitted verbatim. + * + * @param {string} target + * @param {string} data + * @returns {ProcessingInstruction} + * @see https://developer.mozilla.org/docs/Web/API/Document/createProcessingInstruction + * @see https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction + * @see https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml §3.2.1.7 + */ createProcessingInstruction : function(target,data){ var node = new ProcessingInstruction(); node.ownerDocument = this; @@ -1995,6 +2136,19 @@ CDATASection.prototype = { _extends(CDATASection,CharacterData); +/** + * Represents a DocumentType node (the `` declaration). + * + * `publicId`, `systemId`, and `internalSubset` are plain own-property assignments. + * xmldom does not enforce the `readonly` constraint declared by the WHATWG DOM spec — + * direct property writes succeed silently. Values are serialized verbatim when + * `requireWellFormed` is false (the default). When the serializer is invoked with + * `requireWellFormed: true` (via the 4th-parameter options object), it validates each + * field and throws `DOMException` with code `INVALID_STATE_ERR` on invalid values. + * + * @class + * @see https://developer.mozilla.org/en-US/docs/Web/API/DocumentType MDN + */ function DocumentType() { }; DocumentType.prototype.nodeType = DOCUMENT_TYPE_NODE; @@ -2030,22 +2184,45 @@ function XMLSerializer(){} /** * Returns the result of serializing `node` to XML. * + * When `options.requireWellFormed` is `true`, the serializer throws for content that would + * produce ill-formed XML. + * * __This implementation differs from the specification:__ * - CDATASection nodes whose data contains `]]>` are serialized by splitting the section * at each `]]>` occurrence (following W3C DOM Level 3 Core `split-cdata-sections` - * default behaviour). A configurable option is not yet implemented. + * default behaviour) unless `requireWellFormed` is `true`. + * - when `requireWellFormed` is `true`, `DOMException` with code `INVALID_STATE_ERR` + * is only thrown to prevent injection vectors, not for all the spec mandated checks. * * @param {Node} node * @param {boolean} [isHtml] * @param {function} [nodeFilter] + * @param {Object} [options] + * @param {boolean} [options.requireWellFormed=false] + * When `true`, throws for content that would produce ill-formed XML. * @returns {string} + * @throws {DOMException} + * With code `INVALID_STATE_ERR` when `requireWellFormed` is `true` and: + * - a CDATASection node's data contains `"]]>"`, + * - a Comment node's data contains `"-->"` (bare `"--"` does not throw on this branch), + * - a ProcessingInstruction's data contains `"?>"`, + * - a DocumentType's `publicId` is non-empty and does not match the XML `PubidLiteral` + * production, + * - a DocumentType's `systemId` is non-empty and does not match the XML `SystemLiteral` + * production, or + * - a DocumentType's `internalSubset` contains `"]>"`. + * Note: xmldom does not enforce `readonly` on DocumentType fields — direct property + * writes succeed and are covered by the serializer-level checks above. * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring + * @see https://w3c.github.io/DOM-Parsing/#xml-serialization + * @see https://github.com/w3c/DOM-Parsing/issues/84 */ -XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter){ - return nodeSerializeToString.call(node,isHtml,nodeFilter); +XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter,options){ + return nodeSerializeToString.call(node,isHtml,nodeFilter,options); } Node.prototype.toString = nodeSerializeToString; -function nodeSerializeToString(isHtml,nodeFilter){ +function nodeSerializeToString(isHtml,nodeFilter,options){ + var requireWellFormed = !!options && !!options.requireWellFormed; var buf = []; var refNode = this.nodeType == 9 && this.documentElement || this; var prefix = refNode.prefix; @@ -2062,7 +2239,7 @@ function nodeSerializeToString(isHtml,nodeFilter){ ] } } - serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces); + serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces,requireWellFormed); //console.log('###',this.nodeType,uri,prefix,buf.join('')) return buf.join(''); } @@ -2111,272 +2288,323 @@ function addSerializedAttribute(buf, qualifiedName, value) { buf.push(' ', qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"') } -function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){ +function serializeToString(node, buf, isHTML, nodeFilter, visibleNamespaces, requireWellFormed) { if (!visibleNamespaces) { visibleNamespaces = []; } - - if(nodeFilter){ - node = nodeFilter(node); - if(node){ - if(typeof node == 'string'){ - buf.push(node); - return; - } - }else{ - return; - } - //buf.sort.apply(attrs, attributeSorter); - } - - switch(node.nodeType){ - case ELEMENT_NODE: - var attrs = node.attributes; - var len = attrs.length; - var child = node.firstChild; - var nodeName = node.tagName; - - isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML - - var prefixedNodeName = nodeName - if (!isHTML && !node.prefix && node.namespaceURI) { - var defaultNS - // lookup current default ns from `xmlns` attribute - for (var ai = 0; ai < attrs.length; ai++) { - if (attrs.item(ai).name === 'xmlns') { - defaultNS = attrs.item(ai).value - break - } - } - if (!defaultNS) { - // lookup current default ns in visibleNamespaces - for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) { - var namespace = visibleNamespaces[nsi] - if (namespace.prefix === '' && namespace.namespace === node.namespaceURI) { - defaultNS = namespace.namespace - break + walkDOM(node, { ns: visibleNamespaces, isHTML: isHTML }, { + enter: function (n, ctx) { + var ns = ctx.ns; + var html = ctx.isHTML; + + if (nodeFilter) { + n = nodeFilter(n); + if (n) { + if (typeof n == 'string') { + buf.push(n); + return null; } + } else { + return null; } } - if (defaultNS !== node.namespaceURI) { - for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) { - var namespace = visibleNamespaces[nsi] - if (namespace.namespace === node.namespaceURI) { - if (namespace.prefix) { - prefixedNodeName = namespace.prefix + ':' + nodeName + + switch (n.nodeType) { + case ELEMENT_NODE: + var attrs = n.attributes; + var len = attrs.length; + var nodeName = n.tagName; + + html = NAMESPACE.isHTML(n.namespaceURI) || html; + + var prefixedNodeName = nodeName; + if (!html && !n.prefix && n.namespaceURI) { + var defaultNS; + // lookup current default ns from `xmlns` attribute + for (var ai = 0; ai < attrs.length; ai++) { + if (attrs.item(ai).name === 'xmlns') { + defaultNS = attrs.item(ai).value; + break; + } + } + if (!defaultNS) { + // lookup current default ns in visibleNamespaces + for (var nsi = ns.length - 1; nsi >= 0; nsi--) { + var nsEntry = ns[nsi]; + if (nsEntry.prefix === '' && nsEntry.namespace === n.namespaceURI) { + defaultNS = nsEntry.namespace; + break; + } + } + } + if (defaultNS !== n.namespaceURI) { + for (var nsi = ns.length - 1; nsi >= 0; nsi--) { + var nsEntry = ns[nsi]; + if (nsEntry.namespace === n.namespaceURI) { + if (nsEntry.prefix) { + prefixedNodeName = nsEntry.prefix + ':' + nodeName; + } + break; + } + } } - break } - } - } - } - buf.push('<', prefixedNodeName); + buf.push('<', prefixedNodeName); + + // Build a fresh namespace snapshot for this element's children. + // The slice prevents sibling elements from inheriting each other's declarations. + var childNs = ns.slice(); + for (var i = 0; i < len; i++) { + var attr = attrs.item(i); + if (attr.prefix == 'xmlns') { + childNs.push({ prefix: attr.localName, namespace: attr.value }); + } else if (attr.nodeName == 'xmlns') { + childNs.push({ prefix: '', namespace: attr.value }); + } + } - for(var i=0;i'); + if (html && /^script$/i.test(nodeName)) { + // Inline serialization for