Chromium Code Reviews| Index: Source/core/xml/XMLSerializer.js |
| diff --git a/Source/core/xml/XMLSerializer.js b/Source/core/xml/XMLSerializer.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..5792432ac804644f0203c17232d1373b5a518bd0 |
| --- /dev/null |
| +++ b/Source/core/xml/XMLSerializer.js |
| @@ -0,0 +1,374 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +'use strict'; |
| + |
| +installClass('XMLSerializer', function(XMLSerializerPrototype) { |
| + |
| + var _EMPTY = ''; |
| + |
| + var _XML_NAMESPACE_URI = 'http://www.w3.org/XML/1998/namespace'; |
| + var _XLINK_NAMESPACE_URI = 'http://www.w3.org/1999/xlink'; |
| + var _XMLNS_NAMESPACE_URI = 'http://www.w3.org/2000/xmlns'; |
| + var _HTML_NAMESPACE_URI = 'http://www.w3.org/1999/xhtml'; |
| + |
| + var _SERIALIZATION_TYPE_AS_OWNER_DOCUMENT = 0; |
| + var _SERIALIZATION_TYPE_FORCED_XML = 1; |
| + |
| + function escapePCDATA(text) { |
| + return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
| + } |
| + |
| + function escapeHTMLPCDATA(text) { |
| + return escapePCDATA(text).replace(/\u00A0/g, ' '); |
| + } |
| + |
| + function escapeAttributeValue(text) { |
| + return text.replace(/&/g, '&').replace(/\u0022/g, '"').replace(/</g, '<').replace(/>/g, '>'); |
| + } |
|
Jens Widell
2014/09/03 10:29:33
I think \u0022 can be written as ". What's the rea
tasak
2014/09/03 11:40:47
Done.
|
| + |
| + function escapeHTMLAttributeValue(text) { |
| + return text.replace(/&/g, '&').replace(/\u0022/g, '"').replace(/\u00A0/g, ' '); |
| + } |
| + |
| + function validateURI(uri) { |
| + return uri ? uri.replace(/\/$/g, '') : ''; |
| + } |
| + |
| + function cloneNamespace(obj) { |
| + var clone = {}; |
| + for (var i in obj) |
| + clone[i] = obj[i]; |
| + return clone; |
| + } |
| + |
| + function isTemplateElement(node) { |
| + return isHTMLElement(node) && node.tagName.toLowerCase() == 'template'; |
| + } |
| + |
| + function generatePrefix(namespaces) { |
| + var nsRegex = new RegExp('^ns([0-9]+)$', 'g'); |
| + var maxValue = 0; |
| + for (var key in namespaces) { |
| + var match = nsRegex.exec(key); |
| + if (match) { |
| + var value = parseInt(match[1]); |
| + if (maxValue < value) |
| + maxValue = value; |
| + } |
| + } |
| + return 'ns' + (maxValue + 1); |
| + } |
| + |
| + function qualifiedTagName(node) { |
| + if (isHTMLElement(node)) |
| + return node.tagName.toLowerCase(); |
| + return node.tagName; |
| + } |
| + |
| + function attributeIsInSerializedNamespace(attr) { |
| + var _SERIALIZED_NAMESPACES = { |
| + _XML_NAMESPACE_URI:true, _XLINK_NAMESPACE_URI:true, |
| + _XMLNS_NAMESPACE_URI:true |
| + }; |
| + return _SERIALIZED_NAMESPACES[validateURI(attr.namespaceURI)]; |
| + } |
| + |
| + function appendNamespace(prefix, namespaceURI, namespaces) { |
| + if (!namespaceURI) |
| + return ''; |
| + |
| + var result = ''; |
| + var lookupKey = _EMPTY; |
| + if (prefix) |
| + lookupKey = prefix; |
| + if (namespaces[lookupKey] != namespaceURI) { |
| + namespaces[lookupKey] = namespaceURI; |
| + result += ' xmlns'; |
| + if (prefix) |
| + result += ':' + prefix; |
| + result += '=\"' + namespaceURI + '"'; |
|
Jens Widell
2014/09/03 10:29:33
No need to escape " in either string here.
tasak
2014/09/03 11:40:46
Done.
|
| + } |
| + return result; |
| + } |
| + |
| + function shouldAddNamespaceAttribute(attr, node) { |
| + if (!attr.namespaceURI) |
| + return false; |
| + if (!attr.prefix) |
| + return true; |
| + if (!node.hasAttribute('xmlns:' + attr.prefix)) |
| + return true; |
| + return false; |
| + } |
| + |
| + function shouldAddNamespaceElement(node, namespaces) { |
| + var prefix = node.prefix; |
| + if (!node.prefix) { |
| + if (node.hasAttribute('xmlns')) { |
| + namespaces[_EMPTY] = node.namespaceURI; |
| + return false; |
| + } |
| + return true; |
| + } |
| + if (!node.hasAttribute('xmlns:' + node.prefix)) |
| + return true; |
| + return false; |
| + } |
| + |
| + function isHTMLElement(node) { |
| + if (node.nodeType != Node.ELEMENT_NODE) |
| + return false; |
| + |
| + for (var runner = node; runner; runner = runner.parentNode) { |
| + if (runner.nodeType == Node.ELEMENT_NODE && runner.namespaceURI != null) |
| + return validateURI(runner.namespaceURI) == _HTML_NAMESPACE_URI; |
| + } |
| + return false; |
| + } |
| + |
| + function isEndTagOmissible(node) { |
| + if (node.nodeType != Node.ELEMENT_NODE) |
| + return false; |
| + // This tables comes from HTMLElement::ieForbidsInsertHTML(). |
| + var _END_TAG_OMISSIBLE_TAGS = { |
| + 'area':true, 'base':true, 'basefont':true, 'br':true, 'col':true, 'embed':true, |
| + 'frame':true, 'hr':true, 'image':true, 'img':true, 'input':true, 'link':true, |
| + 'meta':true, 'param':true, 'source':true, 'wbr':true |
| + }; |
| + return _END_TAG_OMISSIBLE_TAGS[node.tagName.toLowerCase()]; |
| + } |
| + |
| + XMLSerializerPrototype.serializeAsHTMLDocument_ = function(node) { |
| + if (this.serializationType_ == _SERIALIZATION_TYPE_FORCED_XML) |
| + return false; |
| + return node.ownerDocument && (node.ownerDocument instanceof HTMLDocument); |
| + } |
| + |
| + XMLSerializerPrototype.serializeAttribute_ = function(node, attr, namespaces) { |
| + var documentIsHTML = this.serializeAsHTMLDocument_(node); |
| + var result = ''; |
| + |
| + var attrPrefix = attr.prefix; |
| + var attrName = attr.localName; |
| + if (documentIsHTML && !attributeIsInSerializedNamespace(attr)) { |
| + result += ' ' + attr.localName; |
| + } else { |
| + var namespaceURI = validateURI(attr.namespaceURI); |
| + if (namespaceURI == _XMLNS_NAMESPACE_URI) { |
| + if (!attr.prefix && attr.localName != 'xmlns') |
| + attrPrefix = 'xmlns'; |
| + if (namespaces) { |
| + var lookupKey = _EMPTY; |
| + if (attr.prefix) |
| + lookupKey = attr.localName; |
| + namespaces[lookupKey] = attr.value; |
| + } |
| + } else if (namespaceURI == _XML_NAMESPACE_URI) { |
| + if (!attr.prefix) |
| + attrPrefix = 'xml'; |
| + } else { |
| + if (namespaceURI == _XLINK_NAMESPACE_URI) { |
| + if (!attr.prefix) |
| + attrPrefix = 'xlink'; |
| + } |
| + if (namespaces && shouldAddNamespaceAttribute(attr, node)) { |
| + if (!attrPrefix) { |
| + for (var i in namespaces) { |
| + if (namespaces[i] == namespaceURI && i) { |
| + attrPrefix = i; |
| + break; |
| + } |
| + } |
| + if (!attrPrefix) |
| + attrPrefix = generatePrefix(namespaces); |
| + } |
| + result += appendNamespace(attrPrefix, namespaceURI, namespaces); |
| + } |
| + } |
| + if (attrPrefix) { |
| + result += ' ' + attrPrefix + ':' + attrName; |
| + } else { |
| + result += ' ' + attrName; |
| + } |
| + } |
| + if (attr.value) { |
| + var attrValue; |
| + if (documentIsHTML) { |
| + attrValue = escapeHTMLAttributeValue(String(attr.value)); |
| + } else { |
| + attrValue = escapeAttributeValue(String(attr.value)); |
| + } |
| + result += '="' + attrValue + '"'; |
| + } |
| + return result; |
| + } |
| + |
| + XMLSerializerPrototype.serializeAttributes_ = function(node, namespaces) { |
| + var attributes = node.attributes; |
| + if (!attributes) |
| + return ''; |
| + |
| + var sink = ''; |
| + var attrs = []; |
| + for (var index = 0; index < attributes.length; ++index) |
| + attrs.push(attributes[index]); |
| + |
| + var serializer = this; |
| + attrs.sort(function(a, b) { |
| + return a.name <= b.name ? -1 : 0; |
|
Jens Widell
2014/09/03 10:29:33
This is an unusual comparison function:
If a < b,
tasak
2014/09/03 11:40:46
I see. I found that we don't need to sort attribut
|
| + }).forEach(function(attr) { |
| + if (attr.name.indexOf('_moz') == 0) |
| + return; |
| + sink += serializer.serializeAttribute_(node, attr, namespaces); |
| + }); |
| + return sink; |
| + } |
| + |
| + XMLSerializerPrototype.serializeOpenTag_ = function(node, namespaces) { |
| + var sink = '<' + qualifiedTagName(node); |
| + if (!this.serializeAsHTMLDocument_(node) && namespaces && shouldAddNamespaceElement(node, namespaces)) |
| + sink += appendNamespace(node.prefix, node.namespaceURI, namespaces); |
| + return sink; |
| + } |
| + |
| + XMLSerializerPrototype.serializeCloseTag_ = function(node, namespaces) { |
| + var sink = ''; |
| + if (this.shouldSelfClose_(node)) { |
| + if (isHTMLElement(node)) |
| + sink += ' '; |
| + sink += '/'; |
| + } |
| + sink += '>'; |
| + return sink; |
| + } |
| + |
| + XMLSerializerPrototype.serializeElement_ = function(node, namespaces) { |
| + var sink = this.serializeOpenTag_(node, namespaces); |
| + |
| + sink += this.serializeAttributes_(node, namespaces); |
| + sink += this.serializeCloseTag_(node, namespaces); |
| + return sink; |
| + } |
| + |
| + XMLSerializerPrototype.serializeText_ = function(node) { |
| + if (!node.nodeValue) |
| + return ''; |
| + |
| + if (this.serializeAsHTMLDocument_(node)) { |
| + if (node.parentNode && isHTMLElement(node.parentNode)) { |
| + var tagName = node.parentNode.tagName.toLowerCase(); |
| + if (tagName == 'script' || tagName == 'style' || tagName == 'xmp') |
| + return node.nodeValue; |
| + } |
| + return escapeHTMLPCDATA(node.nodeValue); |
| + } |
| + return escapePCDATA(node.nodeValue); |
| + } |
| + |
| + XMLSerializerPrototype.serializeComment_ = function(node) { |
| + return '<!--' + node.nodeValue + '-->'; |
| + } |
| + |
| + XMLSerializerPrototype.serializeDocument_ = function(node) { |
| + return this.documentXMLDeclaration_; |
| + } |
| + |
| + XMLSerializerPrototype.serializeDocumentType_ = function(node) { |
| + var s = '<!DOCTYPE ' + node.name; |
| + if (node.publicId) { |
| + s += ' PUBLIC "' + node.publicId + '"'; |
| + if (node.systemId) |
|
Jens Widell
2014/09/03 10:29:33
There can also be just a system id, in which case
tasak
2014/09/03 11:40:47
Done.
|
| + s += ' "' + node.systemId + '"'; |
| + } |
| + s += '>'; |
| + return s; |
| + } |
| + |
| + XMLSerializerPrototype.serializeProcessingInstruction_ = function(node) { |
| + var s = '<?' + node.target; |
| + if (node.nodeValue) |
| + s += ' ' + node.nodeValue; |
| + s += '?>'; |
| + return s; |
| + } |
| + |
| + XMLSerializerPrototype.serializeCDataSection_ = function(node) { |
| + return '<![CDATA[' + node.nodeValue + ']]>'; |
| + } |
| + |
| + XMLSerializerPrototype.serializeStartTag = function(node, namespaces) { |
| + if (node.nodeType == Node.TEXT_NODE) { |
| + return this.serializeText_(node); |
| + } else if (node.nodeType == Node.COMMENT_NODE) { |
| + return this.serializeComment_(node); |
| + } else if (node.nodeType == Node.DOCUMENT_NODE) { |
| + return this.serializeDocument_(node); |
| + } else if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { |
| + return this.serializeDocumentType_(node); |
| + } else if (node.nodeType == Node.DOCUMENT_FRAGMENT_NODE) { |
| + return ''; |
| + } else if (node.nodeType == Node.PROCESSING_INSTRUCTION_NODE) { |
| + return this.serializeProcessingInstruction_(node); |
| + } else if (node.nodeType == Node.ELEMENT_NODE) { |
| + return this.serializeElement_(node, namespaces); |
| + } else if (node.nodeType == Node.CDATA_SECTION_NODE) { |
| + return this.serializeCDataSection_(node); |
| + } else { |
| + return ''; |
| + } |
| + } |
| + |
| + XMLSerializerPrototype.shouldSelfClose_ = function(node) { |
| + if (this.serializeAsHTMLDocument_(node)) |
| + return false; |
| + if (node.hasChildNodes()) |
| + return false; |
| + if (isTemplateElement(node) && node.content && node.content.hasChildNodes()) |
| + return false; |
| + if (isHTMLElement(node) && !isEndTagOmissible(node)) |
| + return false; |
| + return true; |
| + } |
| + |
| + XMLSerializerPrototype.serializeEndTag_ = function(node) { |
| + if (this.shouldSelfClose_(node) || (!node.hasChildNodes() && isEndTagOmissible(node))) |
| + return ''; |
| + return '</' + qualifiedTagName(node) + '>'; |
| + } |
| + |
| + XMLSerializerPrototype.serializeNodesWithNamespaces_ = function(node, namespaces) { |
| + var sink = ''; |
| + var localNamespaces = cloneNamespace(namespaces); |
| + sink += this.serializeStartTag(node, localNamespaces); |
| + |
| + if (!(this.serializeAsHTMLDocument_(node) && isEndTagOmissible(node))) { |
| + var current = node; |
| + if (isTemplateElement(node)) |
| + current = node.content; |
| + current = current.firstChild; |
| + while (current) { |
| + sink += this.serializeNodesWithNamespaces_(current, localNamespaces); |
| + current = current.nextSibling; |
| + } |
| + } |
| + if (node.nodeType == Node.ELEMENT_NODE) |
| + sink += this.serializeEndTag_(node); |
| + return sink; |
| + } |
| + |
| + XMLSerializerPrototype.serializeNodes_ = function(node) { |
| + var namespaces = {}; |
| + if (!this.serializeAsHTMLDocument_(node)) |
| + namespaces['xml'] = _XML_NAMESPACE_URI; |
|
Jens Widell
2014/09/03 10:29:34
Could add 'xmlns' as well, I guess. It too is impl
tasak
2014/09/03 11:40:47
Done.
|
| + |
| + return this.serializeNodesWithNamespaces_(node, namespaces); |
| + } |
| + |
| + XMLSerializerPrototype.serializeToStringInternal = function(root, xmlDeclaration) { |
| + this.serializationType_ = _SERIALIZATION_TYPE_FORCED_XML; |
| + this.documentXMLDeclaration_ = xmlDeclaration ? xmlDeclaration : ''; |
| + return this.serializeNodes_(root); |
| + } |
| +}); |