Unstyled XML viewer crashes when XML contains an element with id="tree"
[WebKit-https.git] / Source / WebCore / xml / XMLViewer.js
1 /*
2  * Copyright (C) 2011 Google Inc. All rights reserved.
3  * Copyright (C) 2013 Apple Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above
13  * copyright notice, this list of conditions and the following disclaimer
14  * in the documentation and/or other materials provided with the
15  * distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS
18  * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC.
21  * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 var nodeParentPairs = [];
31 var sourceXML;
32
33 // Script entry point.
34
35 function prepareWebKitXMLViewer(noStyleMessage)
36 {
37     var html = createHTMLElement('html');
38     var head = createHTMLElement('head');
39     html.appendChild(head);
40     var style = createHTMLElement('style');
41     style.id = 'xml-viewer-style';
42     head.appendChild(style);
43     var body = createHTMLElement('body');
44     html.appendChild(body);
45     sourceXML = createHTMLElement('div');
46
47     var child;
48     while (child = document.firstChild) {
49         document.removeChild(child);
50         if (child.nodeType != Node.DOCUMENT_TYPE_NODE)
51             sourceXML.appendChild(child);
52     }
53     document.appendChild(html);
54
55     var header = createHTMLElement('div');
56     body.appendChild(header);
57     header.classList.add('header');
58     var headerSpan = createHTMLElement('span');
59     header.appendChild(headerSpan);
60     headerSpan.textContent = noStyleMessage;
61     header.appendChild(createHTMLElement('br'));
62
63     var tree = createHTMLElement('div');
64     body.appendChild(tree);
65     tree.classList.add('pretty-print');
66     tree.id = 'tree';
67     window.onload = sourceXMLLoaded;
68 }
69
70 function sourceXMLLoaded()
71 {
72     var root = document.getElementById('tree');
73
74     for (var child = sourceXML.firstChild; child; child = child.nextSibling)
75         nodeParentPairs.push({parentElement: root, node: child});
76
77     for (var i = 0; i < nodeParentPairs.length; i++)
78         processNode(nodeParentPairs[i].parentElement, nodeParentPairs[i].node);
79
80     drawArrows();
81     initButtons();
82
83     if (typeof(onAfterWebkitXMLViewerLoaded) == 'function')
84       onAfterWebkitXMLViewerLoaded();
85 }
86
87 // Tree processing.
88
89 function processNode(parentElement, node)
90 {
91     if (!processNode.processorsMap) {
92         processNode.processorsMap = {};
93         processNode.processorsMap[Node.PROCESSING_INSTRUCTION_NODE] = processProcessingInstruction;
94         processNode.processorsMap[Node.ELEMENT_NODE] = processElement;
95         processNode.processorsMap[Node.COMMENT_NODE] = processComment;
96         processNode.processorsMap[Node.TEXT_NODE] = processText;
97         processNode.processorsMap[Node.CDATA_SECTION_NODE] = processCDATA;
98     }
99     if (processNode.processorsMap[node.nodeType])
100         processNode.processorsMap[node.nodeType].call(this, parentElement, node);
101 }
102
103 function processElement(parentElement, node)
104 {
105     if (!node.firstChild)
106         processEmptyElement(parentElement, node);
107     else {
108         var child = node.firstChild;
109         if (child.nodeType == Node.TEXT_NODE && isShort(child.nodeValue) && !child.nextSibling)
110             processShortTextOnlyElement(parentElement, node);
111         else
112             processComplexElement(parentElement, node);
113     }
114 }
115
116 function processEmptyElement(parentElement, node)
117 {
118     var line = createLine();
119     line.appendChild(createTag(node, false, true));
120     parentElement.appendChild(line);
121 }
122
123 function processShortTextOnlyElement(parentElement, node)
124 {
125     var line = createLine();
126     line.appendChild(createTag(node, false, false));
127     for (var child = node.firstChild; child; child = child.nextSibling)
128         line.appendChild(createText(child.nodeValue));
129     line.appendChild(createTag(node, true, false));
130     parentElement.appendChild(line);
131 }
132
133 function processComplexElement(parentElement, node)
134 {
135     var collapsible = createCollapsible();
136
137     collapsible.expanded.start.appendChild(createTag(node, false, false));
138     for (var child = node.firstChild; child; child = child.nextSibling)
139         nodeParentPairs.push({parentElement: collapsible.expanded.content, node: child});
140     collapsible.expanded.end.appendChild(createTag(node, true, false));
141
142     collapsible.collapsed.content.appendChild(createTag(node, false, false));
143     collapsible.collapsed.content.appendChild(createText('...'));
144     collapsible.collapsed.content.appendChild(createTag(node, true, false));
145     parentElement.appendChild(collapsible);
146 }
147
148 function processComment(parentElement, node)
149 {
150     if (isShort(node.nodeValue)) {
151         var line = createLine();
152         line.appendChild(createComment('<!-- ' + node.nodeValue + ' -->'));
153         parentElement.appendChild(line);
154     } else {
155         var collapsible = createCollapsible();
156
157         collapsible.expanded.start.appendChild(createComment('<!--'));
158         collapsible.expanded.content.appendChild(createComment(node.nodeValue));
159         collapsible.expanded.end.appendChild(createComment('-->'));
160
161         collapsible.collapsed.content.appendChild(createComment('<!--'));
162         collapsible.collapsed.content.appendChild(createComment('...'));
163         collapsible.collapsed.content.appendChild(createComment('-->'));
164         parentElement.appendChild(collapsible);
165     }
166 }
167
168 function processCDATA(parentElement, node)
169 {
170     if (isShort(node.nodeValue)) {
171         var line = createLine();
172         line.appendChild(createText('<![CDATA[ ' + node.nodeValue + ' ]]>'));
173         parentElement.appendChild(line);
174     } else {
175         var collapsible = createCollapsible();
176
177         collapsible.expanded.start.appendChild(createText('<![CDATA['));
178         collapsible.expanded.content.appendChild(createText(node.nodeValue));
179         collapsible.expanded.end.appendChild(createText(']]>'));
180
181         collapsible.collapsed.content.appendChild(createText('<![CDATA['));
182         collapsible.collapsed.content.appendChild(createText('...'));
183         collapsible.collapsed.content.appendChild(createText(']]>'));
184         parentElement.appendChild(collapsible);
185     }
186 }
187
188 function processProcessingInstruction(parentElement, node)
189 {
190     if (isShort(node.nodeValue)) {
191         var line = createLine();
192         line.appendChild(createComment('<?' + node.nodeName + ' ' + node.nodeValue + '?>'));
193         parentElement.appendChild(line);
194     } else {
195         var collapsible = createCollapsible();
196
197         collapsible.expanded.start.appendChild(createComment('<?' + node.nodeName));
198         collapsible.expanded.content.appendChild(createComment(node.nodeValue));
199         collapsible.expanded.end.appendChild(createComment('?>'));
200
201         collapsible.collapsed.content.appendChild(createComment('<?' + node.nodeName));
202         collapsible.collapsed.content.appendChild(createComment('...'));
203         collapsible.collapsed.content.appendChild(createComment('?>'));
204         parentElement.appendChild(collapsible);
205     }
206 }
207
208 function processText(parentElement, node)
209 {
210     parentElement.appendChild(createText(node.nodeValue));
211 }
212
213 // Processing utils.
214
215 function trim(value)
216 {
217     return value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
218 }
219
220 function isShort(value)
221 {
222     return trim(value).length <= 50;
223 }
224
225 // Tree rendering.
226
227 function createHTMLElement(elementName)
228 {
229     return document.createElementNS('http://www.w3.org/1999/xhtml', elementName)
230 }
231
232 function createCollapsible()
233 {
234     var collapsible = createHTMLElement('div');
235     collapsible.classList.add('collapsible');
236     collapsible.expanded = createHTMLElement('div');
237     collapsible.expanded.classList.add('expanded');
238     collapsible.appendChild(collapsible.expanded);
239
240     collapsible.expanded.start = createLine();
241     collapsible.expanded.start.appendChild(createCollapseButton());
242     collapsible.expanded.appendChild(collapsible.expanded.start);
243
244     collapsible.expanded.content = createHTMLElement('div');
245     collapsible.expanded.content.classList.add('collapsible-content');
246     collapsible.expanded.appendChild(collapsible.expanded.content);
247
248     collapsible.expanded.end = createLine();
249     collapsible.expanded.appendChild(collapsible.expanded.end);
250
251     collapsible.collapsed = createHTMLElement('div');
252     collapsible.collapsed.classList.add('collapsed');
253     collapsible.collapsed.classList.add('hidden');
254     collapsible.appendChild(collapsible.collapsed);
255     collapsible.collapsed.content = createLine();
256     collapsible.collapsed.content.appendChild(createExpandButton());
257     collapsible.collapsed.appendChild(collapsible.collapsed.content);
258
259     return collapsible;
260 }
261
262 function createButton()
263 {
264     var button = createHTMLElement('span');
265     button.classList.add('button');
266     return button;
267 }
268
269 function createCollapseButton(str)
270 {
271     var button = createButton();
272     button.classList.add('collapse-button');
273     return button;
274 }
275
276 function createExpandButton(str)
277 {
278     var button = createButton();
279     button.classList.add('expand-button');
280     return button;
281 }
282
283 function createComment(commentString)
284 {
285     var comment = createHTMLElement('span');
286     comment.classList.add('comment');
287     comment.classList.add('webkit-html-comment');
288     comment.textContent = commentString;
289     return comment;
290 }
291
292 function createText(value)
293 {
294     var text = createHTMLElement('span');
295     text.textContent = trim(value);
296     text.classList.add('text');
297     return text;
298 }
299
300 function createLine()
301 {
302     var line = createHTMLElement('div');
303     line.classList.add('line');
304     return line;
305 }
306
307 function createTag(node, isClosing, isEmpty)
308 {
309     var tag = createHTMLElement('span');
310     tag.classList.add('webkit-html-tag');
311
312     var stringBeforeAttrs = '<';
313     if (isClosing)
314         stringBeforeAttrs += '/';
315     stringBeforeAttrs += node.nodeName;
316     var textBeforeAttrs = document.createTextNode(stringBeforeAttrs);
317     tag.appendChild(textBeforeAttrs);
318
319     if (!isClosing) {
320         for (var i = 0; i < node.attributes.length; i++)
321             tag.appendChild(createAttribute(node.attributes[i]));
322     }
323
324     var stringAfterAttrs = '';
325     if (isEmpty)
326         stringAfterAttrs += '/';
327     stringAfterAttrs += '>';
328     var textAfterAttrs = document.createTextNode(stringAfterAttrs);
329     tag.appendChild(textAfterAttrs);
330
331     return tag;
332 }
333
334 function createAttribute(attributeNode)
335 {
336     var attribute = createHTMLElement('span');
337     attribute.classList.add('webkit-html-attribute');
338
339     var attributeName = createHTMLElement('span');
340     attributeName.classList.add('webkit-html-attribute-name');
341     attributeName.textContent = attributeNode.name;
342
343     var textBefore = document.createTextNode(' ');
344     var textBetween = document.createTextNode('="');
345
346     var attributeValue = createHTMLElement('span');
347     attributeValue.classList.add('webkit-html-attribute-value');
348     attributeValue.textContent = attributeNode.value;
349
350     var textAfter = document.createTextNode('"');
351
352     attribute.appendChild(textBefore);
353     attribute.appendChild(attributeName);
354     attribute.appendChild(textBetween);
355     attribute.appendChild(attributeValue);
356     attribute.appendChild(textAfter);
357     return attribute;
358 }
359
360 // Tree behaviour.
361
362 function drawArrows()
363 {
364     var ctx = document.getCSSCanvasContext("2d", "arrowRight", 10, 11);
365
366     ctx.fillStyle = "rgb(90,90,90)";
367     ctx.beginPath();
368     ctx.moveTo(0, 0);
369     ctx.lineTo(0, 8);
370     ctx.lineTo(7, 4);
371     ctx.lineTo(0, 0);
372     ctx.fill();
373     ctx.closePath();
374
375     var ctx = document.getCSSCanvasContext("2d", "arrowDown", 10, 10);
376
377     ctx.fillStyle = "rgb(90,90,90)";
378     ctx.beginPath();
379     ctx.moveTo(0, 0);
380     ctx.lineTo(8, 0);
381     ctx.lineTo(4, 7);
382     ctx.lineTo(0, 0);
383     ctx.fill();
384     ctx.closePath();
385 }
386
387 function expandFunction(sectionId)
388 {
389     return function()
390     {
391         document.querySelector('#' + sectionId + ' > .expanded').className = 'expanded';
392         document.querySelector('#' + sectionId + ' > .collapsed').className = 'collapsed hidden';
393     };
394 }
395
396 function collapseFunction(sectionId)
397 {
398     return function()
399     {
400         document.querySelector('#' + sectionId + ' > .expanded').className = 'expanded hidden';
401         document.querySelector('#' + sectionId + ' > .collapsed').className = 'collapsed';
402     };
403 }
404
405 function initButtons()
406 {
407     var sections = document.querySelectorAll('.collapsible');
408     for (var i = 0; i < sections.length; i++) {
409         var sectionId = 'collapsible' + i;
410         sections[i].id = sectionId;
411
412         var expandedPart = sections[i].querySelector('#' + sectionId + ' > .expanded');
413         var collapseButton = expandedPart.querySelector('.collapse-button');
414         collapseButton.onclick = collapseFunction(sectionId);
415         collapseButton.onmousedown = handleButtonMouseDown;
416
417         var collapsedPart = sections[i].querySelector('#' + sectionId + ' > .collapsed');
418         var expandButton = collapsedPart.querySelector('.expand-button');
419         expandButton.onclick = expandFunction(sectionId);
420         expandButton.onmousedown = handleButtonMouseDown;
421     }
422
423 }
424
425 function handleButtonMouseDown(e)
426 {
427    // To prevent selection on double click
428    e.preventDefault();
429 }