OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 'use strict'; | |
6 | |
7 installClass('XMLSerializer', function(XMLSerializerPrototype) { | |
8 | |
9 var _EMPTY = ''; | |
10 | |
11 var _XML_NAMESPACE_URI = 'http://www.w3.org/XML/1998/namespace'; | |
12 var _XLINK_NAMESPACE_URI = 'http://www.w3.org/1999/xlink'; | |
13 var _XMLNS_NAMESPACE_URI = 'http://www.w3.org/2000/xmlns'; | |
14 var _HTML_NAMESPACE_URI = 'http://www.w3.org/1999/xhtml'; | |
15 | |
16 var _SERIALIZATION_TYPE_AS_OWNER_DOCUMENT = 0; | |
17 var _SERIALIZATION_TYPE_FORCED_XML = 1; | |
18 | |
19 function escapePCDATA(text) { | |
20 return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, ' >'); | |
21 } | |
22 | |
23 function escapeHTMLPCDATA(text) { | |
24 return escapePCDATA(text).replace(/\u00A0/g, ' '); | |
25 } | |
26 | |
27 function escapeAttributeValue(text) { | |
28 return text.replace(/&/g, '&').replace(/\u0022/g, '"').replace( /</g, '<').replace(/>/g, '>'); | |
29 } | |
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.
| |
30 | |
31 function escapeHTMLAttributeValue(text) { | |
32 return text.replace(/&/g, '&').replace(/\u0022/g, '"').replace( /\u00A0/g, ' '); | |
33 } | |
34 | |
35 function validateURI(uri) { | |
36 return uri ? uri.replace(/\/$/g, '') : ''; | |
37 } | |
38 | |
39 function cloneNamespace(obj) { | |
40 var clone = {}; | |
41 for (var i in obj) | |
42 clone[i] = obj[i]; | |
43 return clone; | |
44 } | |
45 | |
46 function isTemplateElement(node) { | |
47 return isHTMLElement(node) && node.tagName.toLowerCase() == 'template'; | |
48 } | |
49 | |
50 function generatePrefix(namespaces) { | |
51 var nsRegex = new RegExp('^ns([0-9]+)$', 'g'); | |
52 var maxValue = 0; | |
53 for (var key in namespaces) { | |
54 var match = nsRegex.exec(key); | |
55 if (match) { | |
56 var value = parseInt(match[1]); | |
57 if (maxValue < value) | |
58 maxValue = value; | |
59 } | |
60 } | |
61 return 'ns' + (maxValue + 1); | |
62 } | |
63 | |
64 function qualifiedTagName(node) { | |
65 if (isHTMLElement(node)) | |
66 return node.tagName.toLowerCase(); | |
67 return node.tagName; | |
68 } | |
69 | |
70 function attributeIsInSerializedNamespace(attr) { | |
71 var _SERIALIZED_NAMESPACES = { | |
72 _XML_NAMESPACE_URI:true, _XLINK_NAMESPACE_URI:true, | |
73 _XMLNS_NAMESPACE_URI:true | |
74 }; | |
75 return _SERIALIZED_NAMESPACES[validateURI(attr.namespaceURI)]; | |
76 } | |
77 | |
78 function appendNamespace(prefix, namespaceURI, namespaces) { | |
79 if (!namespaceURI) | |
80 return ''; | |
81 | |
82 var result = ''; | |
83 var lookupKey = _EMPTY; | |
84 if (prefix) | |
85 lookupKey = prefix; | |
86 if (namespaces[lookupKey] != namespaceURI) { | |
87 namespaces[lookupKey] = namespaceURI; | |
88 result += ' xmlns'; | |
89 if (prefix) | |
90 result += ':' + prefix; | |
91 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.
| |
92 } | |
93 return result; | |
94 } | |
95 | |
96 function shouldAddNamespaceAttribute(attr, node) { | |
97 if (!attr.namespaceURI) | |
98 return false; | |
99 if (!attr.prefix) | |
100 return true; | |
101 if (!node.hasAttribute('xmlns:' + attr.prefix)) | |
102 return true; | |
103 return false; | |
104 } | |
105 | |
106 function shouldAddNamespaceElement(node, namespaces) { | |
107 var prefix = node.prefix; | |
108 if (!node.prefix) { | |
109 if (node.hasAttribute('xmlns')) { | |
110 namespaces[_EMPTY] = node.namespaceURI; | |
111 return false; | |
112 } | |
113 return true; | |
114 } | |
115 if (!node.hasAttribute('xmlns:' + node.prefix)) | |
116 return true; | |
117 return false; | |
118 } | |
119 | |
120 function isHTMLElement(node) { | |
121 if (node.nodeType != Node.ELEMENT_NODE) | |
122 return false; | |
123 | |
124 for (var runner = node; runner; runner = runner.parentNode) { | |
125 if (runner.nodeType == Node.ELEMENT_NODE && runner.namespaceURI != n ull) | |
126 return validateURI(runner.namespaceURI) == _HTML_NAMESPACE_URI; | |
127 } | |
128 return false; | |
129 } | |
130 | |
131 function isEndTagOmissible(node) { | |
132 if (node.nodeType != Node.ELEMENT_NODE) | |
133 return false; | |
134 // This tables comes from HTMLElement::ieForbidsInsertHTML(). | |
135 var _END_TAG_OMISSIBLE_TAGS = { | |
136 'area':true, 'base':true, 'basefont':true, 'br':true, 'col':true, 'e mbed':true, | |
137 'frame':true, 'hr':true, 'image':true, 'img':true, 'input':true, 'li nk':true, | |
138 'meta':true, 'param':true, 'source':true, 'wbr':true | |
139 }; | |
140 return _END_TAG_OMISSIBLE_TAGS[node.tagName.toLowerCase()]; | |
141 } | |
142 | |
143 XMLSerializerPrototype.serializeAsHTMLDocument_ = function(node) { | |
144 if (this.serializationType_ == _SERIALIZATION_TYPE_FORCED_XML) | |
145 return false; | |
146 return node.ownerDocument && (node.ownerDocument instanceof HTMLDocument ); | |
147 } | |
148 | |
149 XMLSerializerPrototype.serializeAttribute_ = function(node, attr, namespaces ) { | |
150 var documentIsHTML = this.serializeAsHTMLDocument_(node); | |
151 var result = ''; | |
152 | |
153 var attrPrefix = attr.prefix; | |
154 var attrName = attr.localName; | |
155 if (documentIsHTML && !attributeIsInSerializedNamespace(attr)) { | |
156 result += ' ' + attr.localName; | |
157 } else { | |
158 var namespaceURI = validateURI(attr.namespaceURI); | |
159 if (namespaceURI == _XMLNS_NAMESPACE_URI) { | |
160 if (!attr.prefix && attr.localName != 'xmlns') | |
161 attrPrefix = 'xmlns'; | |
162 if (namespaces) { | |
163 var lookupKey = _EMPTY; | |
164 if (attr.prefix) | |
165 lookupKey = attr.localName; | |
166 namespaces[lookupKey] = attr.value; | |
167 } | |
168 } else if (namespaceURI == _XML_NAMESPACE_URI) { | |
169 if (!attr.prefix) | |
170 attrPrefix = 'xml'; | |
171 } else { | |
172 if (namespaceURI == _XLINK_NAMESPACE_URI) { | |
173 if (!attr.prefix) | |
174 attrPrefix = 'xlink'; | |
175 } | |
176 if (namespaces && shouldAddNamespaceAttribute(attr, node)) { | |
177 if (!attrPrefix) { | |
178 for (var i in namespaces) { | |
179 if (namespaces[i] == namespaceURI && i) { | |
180 attrPrefix = i; | |
181 break; | |
182 } | |
183 } | |
184 if (!attrPrefix) | |
185 attrPrefix = generatePrefix(namespaces); | |
186 } | |
187 result += appendNamespace(attrPrefix, namespaceURI, namespac es); | |
188 } | |
189 } | |
190 if (attrPrefix) { | |
191 result += ' ' + attrPrefix + ':' + attrName; | |
192 } else { | |
193 result += ' ' + attrName; | |
194 } | |
195 } | |
196 if (attr.value) { | |
197 var attrValue; | |
198 if (documentIsHTML) { | |
199 attrValue = escapeHTMLAttributeValue(String(attr.value)); | |
200 } else { | |
201 attrValue = escapeAttributeValue(String(attr.value)); | |
202 } | |
203 result += '="' + attrValue + '"'; | |
204 } | |
205 return result; | |
206 } | |
207 | |
208 XMLSerializerPrototype.serializeAttributes_ = function(node, namespaces) { | |
209 var attributes = node.attributes; | |
210 if (!attributes) | |
211 return ''; | |
212 | |
213 var sink = ''; | |
214 var attrs = []; | |
215 for (var index = 0; index < attributes.length; ++index) | |
216 attrs.push(attributes[index]); | |
217 | |
218 var serializer = this; | |
219 attrs.sort(function(a, b) { | |
220 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
| |
221 }).forEach(function(attr) { | |
222 if (attr.name.indexOf('_moz') == 0) | |
223 return; | |
224 sink += serializer.serializeAttribute_(node, attr, namespaces); | |
225 }); | |
226 return sink; | |
227 } | |
228 | |
229 XMLSerializerPrototype.serializeOpenTag_ = function(node, namespaces) { | |
230 var sink = '<' + qualifiedTagName(node); | |
231 if (!this.serializeAsHTMLDocument_(node) && namespaces && shouldAddNames paceElement(node, namespaces)) | |
232 sink += appendNamespace(node.prefix, node.namespaceURI, namespaces); | |
233 return sink; | |
234 } | |
235 | |
236 XMLSerializerPrototype.serializeCloseTag_ = function(node, namespaces) { | |
237 var sink = ''; | |
238 if (this.shouldSelfClose_(node)) { | |
239 if (isHTMLElement(node)) | |
240 sink += ' '; | |
241 sink += '/'; | |
242 } | |
243 sink += '>'; | |
244 return sink; | |
245 } | |
246 | |
247 XMLSerializerPrototype.serializeElement_ = function(node, namespaces) { | |
248 var sink = this.serializeOpenTag_(node, namespaces); | |
249 | |
250 sink += this.serializeAttributes_(node, namespaces); | |
251 sink += this.serializeCloseTag_(node, namespaces); | |
252 return sink; | |
253 } | |
254 | |
255 XMLSerializerPrototype.serializeText_ = function(node) { | |
256 if (!node.nodeValue) | |
257 return ''; | |
258 | |
259 if (this.serializeAsHTMLDocument_(node)) { | |
260 if (node.parentNode && isHTMLElement(node.parentNode)) { | |
261 var tagName = node.parentNode.tagName.toLowerCase(); | |
262 if (tagName == 'script' || tagName == 'style' || tagName == 'xmp ') | |
263 return node.nodeValue; | |
264 } | |
265 return escapeHTMLPCDATA(node.nodeValue); | |
266 } | |
267 return escapePCDATA(node.nodeValue); | |
268 } | |
269 | |
270 XMLSerializerPrototype.serializeComment_ = function(node) { | |
271 return '<!--' + node.nodeValue + '-->'; | |
272 } | |
273 | |
274 XMLSerializerPrototype.serializeDocument_ = function(node) { | |
275 return this.documentXMLDeclaration_; | |
276 } | |
277 | |
278 XMLSerializerPrototype.serializeDocumentType_ = function(node) { | |
279 var s = '<!DOCTYPE ' + node.name; | |
280 if (node.publicId) { | |
281 s += ' PUBLIC "' + node.publicId + '"'; | |
282 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.
| |
283 s += ' "' + node.systemId + '"'; | |
284 } | |
285 s += '>'; | |
286 return s; | |
287 } | |
288 | |
289 XMLSerializerPrototype.serializeProcessingInstruction_ = function(node) { | |
290 var s = '<?' + node.target; | |
291 if (node.nodeValue) | |
292 s += ' ' + node.nodeValue; | |
293 s += '?>'; | |
294 return s; | |
295 } | |
296 | |
297 XMLSerializerPrototype.serializeCDataSection_ = function(node) { | |
298 return '<![CDATA[' + node.nodeValue + ']]>'; | |
299 } | |
300 | |
301 XMLSerializerPrototype.serializeStartTag = function(node, namespaces) { | |
302 if (node.nodeType == Node.TEXT_NODE) { | |
303 return this.serializeText_(node); | |
304 } else if (node.nodeType == Node.COMMENT_NODE) { | |
305 return this.serializeComment_(node); | |
306 } else if (node.nodeType == Node.DOCUMENT_NODE) { | |
307 return this.serializeDocument_(node); | |
308 } else if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { | |
309 return this.serializeDocumentType_(node); | |
310 } else if (node.nodeType == Node.DOCUMENT_FRAGMENT_NODE) { | |
311 return ''; | |
312 } else if (node.nodeType == Node.PROCESSING_INSTRUCTION_NODE) { | |
313 return this.serializeProcessingInstruction_(node); | |
314 } else if (node.nodeType == Node.ELEMENT_NODE) { | |
315 return this.serializeElement_(node, namespaces); | |
316 } else if (node.nodeType == Node.CDATA_SECTION_NODE) { | |
317 return this.serializeCDataSection_(node); | |
318 } else { | |
319 return ''; | |
320 } | |
321 } | |
322 | |
323 XMLSerializerPrototype.shouldSelfClose_ = function(node) { | |
324 if (this.serializeAsHTMLDocument_(node)) | |
325 return false; | |
326 if (node.hasChildNodes()) | |
327 return false; | |
328 if (isTemplateElement(node) && node.content && node.content.hasChildNode s()) | |
329 return false; | |
330 if (isHTMLElement(node) && !isEndTagOmissible(node)) | |
331 return false; | |
332 return true; | |
333 } | |
334 | |
335 XMLSerializerPrototype.serializeEndTag_ = function(node) { | |
336 if (this.shouldSelfClose_(node) || (!node.hasChildNodes() && isEndTagOmi ssible(node))) | |
337 return ''; | |
338 return '</' + qualifiedTagName(node) + '>'; | |
339 } | |
340 | |
341 XMLSerializerPrototype.serializeNodesWithNamespaces_ = function(node, namesp aces) { | |
342 var sink = ''; | |
343 var localNamespaces = cloneNamespace(namespaces); | |
344 sink += this.serializeStartTag(node, localNamespaces); | |
345 | |
346 if (!(this.serializeAsHTMLDocument_(node) && isEndTagOmissible(node))) { | |
347 var current = node; | |
348 if (isTemplateElement(node)) | |
349 current = node.content; | |
350 current = current.firstChild; | |
351 while (current) { | |
352 sink += this.serializeNodesWithNamespaces_(current, localNamespa ces); | |
353 current = current.nextSibling; | |
354 } | |
355 } | |
356 if (node.nodeType == Node.ELEMENT_NODE) | |
357 sink += this.serializeEndTag_(node); | |
358 return sink; | |
359 } | |
360 | |
361 XMLSerializerPrototype.serializeNodes_ = function(node) { | |
362 var namespaces = {}; | |
363 if (!this.serializeAsHTMLDocument_(node)) | |
364 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.
| |
365 | |
366 return this.serializeNodesWithNamespaces_(node, namespaces); | |
367 } | |
368 | |
369 XMLSerializerPrototype.serializeToStringInternal = function(root, xmlDeclara tion) { | |
370 this.serializationType_ = _SERIALIZATION_TYPE_FORCED_XML; | |
371 this.documentXMLDeclaration_ = xmlDeclaration ? xmlDeclaration : ''; | |
372 return this.serializeNodes_(root); | |
373 } | |
374 }); | |
OLD | NEW |