Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 Components.utils.import("resource://gre/modules/Services.jsm");
9 var gDebug = 0;
10 var gLineCount = 0;
11 var gStartTargetLine = 0;
12 var gEndTargetLine = 0;
13 var gTargetNode = null;
15 var gEntityConverter = null;
16 var gWrapLongLines = false;
17 const gViewSourceCSS = 'resource://gre-resources/viewsource.css';
18 const NS_XHTML = 'http://www.w3.org/1999/xhtml';
20 // These are markers used to delimit the selection during processing. They
21 // are removed from the final rendering, but we pick space-like characters for
22 // safety (and futhermore, these are known to be mapped to a 0-length string
23 // in transliterate.properties). It is okay to set start=end, we use findNext()
24 // U+200B ZERO WIDTH SPACE
25 const MARK_SELECTION_START = '\u200B\u200B\u200B\u200B\u200B';
26 const MARK_SELECTION_END = '\u200B\u200B\u200B\u200B\u200B';
28 function onLoadViewPartialSource()
29 {
30 // check the view_source.wrap_long_lines pref
31 // and set the menuitem's checked attribute accordingly
32 gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
33 document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines);
34 document.getElementById("menu_highlightSyntax")
35 .setAttribute("checked",
36 Services.prefs.getBoolPref("view_source.syntax_highlight"));
38 if (window.arguments[3] == 'selection')
39 viewPartialSourceForSelection(window.arguments[2]);
40 else
41 viewPartialSourceForFragment(window.arguments[2], window.arguments[3]);
43 gBrowser.droppedLinkHandler = function (event, url, name) {
44 viewSource(url)
45 event.preventDefault();
46 }
48 window.content.focus();
49 }
51 ////////////////////////////////////////////////////////////////////////////////
52 // view-source of a selection with the special effect of remapping the selection
53 // to the underlying view-source output
54 function viewPartialSourceForSelection(selection)
55 {
56 var range = selection.getRangeAt(0);
57 var ancestorContainer = range.commonAncestorContainer;
58 var doc = ancestorContainer.ownerDocument;
60 var startContainer = range.startContainer;
61 var endContainer = range.endContainer;
62 var startOffset = range.startOffset;
63 var endOffset = range.endOffset;
65 // let the ancestor be an element
66 if (ancestorContainer.nodeType == Node.TEXT_NODE ||
67 ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
68 ancestorContainer = ancestorContainer.parentNode;
70 // for selectAll, let's use the entire document, including <html>...</html>
71 // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
72 try {
73 if (ancestorContainer == doc.body)
74 ancestorContainer = doc.documentElement;
75 } catch (e) { }
77 // each path is a "child sequence" (a.k.a. "tumbler") that
78 // descends from the ancestor down to the boundary point
79 var startPath = getPath(ancestorContainer, startContainer);
80 var endPath = getPath(ancestorContainer, endContainer);
82 // clone the fragment of interest and reset everything to be relative to it
83 // note: it is with the clone that we operate/munge from now on. Also note
84 // that we clone into a data document to prevent images in the fragment from
85 // loading and the like. The use of importNode here, as opposed to adoptNode,
86 // is _very_ important.
87 // XXXbz wish there were a less hacky way to create an untrusted document here
88 var isHTML = (doc.createElement("div").tagName == "DIV");
89 var dataDoc = isHTML ?
90 ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
91 ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
92 ancestorContainer = dataDoc.importNode(ancestorContainer, true);
93 startContainer = ancestorContainer;
94 endContainer = ancestorContainer;
96 // Only bother with the selection if it can be remapped. Don't mess with
97 // leaf elements (such as <isindex>) that secretly use anynomous content
98 // for their display appearance.
99 var canDrawSelection = ancestorContainer.hasChildNodes();
100 if (canDrawSelection) {
101 var i;
102 for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
103 startContainer = startContainer.childNodes.item(startPath[i]);
104 }
105 for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
106 endContainer = endContainer.childNodes.item(endPath[i]);
107 }
109 // add special markers to record the extent of the selection
110 // note: |startOffset| and |endOffset| are interpreted either as
111 // offsets in the text data or as child indices (see the Range spec)
112 // (here, munging the end point first to keep the start point safe...)
113 var tmpNode;
114 if (endContainer.nodeType == Node.TEXT_NODE ||
115 endContainer.nodeType == Node.CDATA_SECTION_NODE) {
116 // do some extra tweaks to try to avoid the view-source output to look like
117 // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
118 // To get a neat output, the idea here is to remap the end point from:
119 // 1. ...<tag>]... to ...]<tag>...
120 // 2. ...]</tag>... to ...</tag>]...
121 if ((endOffset > 0 && endOffset < endContainer.data.length) ||
122 !endContainer.parentNode || !endContainer.parentNode.parentNode)
123 endContainer.insertData(endOffset, MARK_SELECTION_END);
124 else {
125 tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
126 endContainer = endContainer.parentNode;
127 if (endOffset == 0)
128 endContainer.parentNode.insertBefore(tmpNode, endContainer);
129 else
130 endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
131 }
132 }
133 else {
134 tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
135 endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
136 }
138 if (startContainer.nodeType == Node.TEXT_NODE ||
139 startContainer.nodeType == Node.CDATA_SECTION_NODE) {
140 // do some extra tweaks to try to avoid the view-source output to look like
141 // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
142 // To get a neat output, the idea here is to remap the start point from:
143 // 1. ...<tag>[... to ...[<tag>...
144 // 2. ...[</tag>... to ...</tag>[...
145 if ((startOffset > 0 && startOffset < startContainer.data.length) ||
146 !startContainer.parentNode || !startContainer.parentNode.parentNode ||
147 startContainer != startContainer.parentNode.lastChild)
148 startContainer.insertData(startOffset, MARK_SELECTION_START);
149 else {
150 tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
151 startContainer = startContainer.parentNode;
152 if (startOffset == 0)
153 startContainer.parentNode.insertBefore(tmpNode, startContainer);
154 else
155 startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
156 }
157 }
158 else {
159 tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
160 startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
161 }
162 }
164 // now extract and display the syntax highlighted source
165 tmpNode = dataDoc.createElementNS(NS_XHTML, 'div');
166 tmpNode.appendChild(ancestorContainer);
168 // the load is aynchronous and so we will wait until the view-source DOM is done
169 // before drawing the selection.
170 if (canDrawSelection) {
171 window.document.getElementById("content").addEventListener("load", drawSelection, true);
172 }
174 // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
175 var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
176 getWebNavigation().loadURIWithBase((isHTML ?
177 "view-source:data:text/html;charset=utf-8," :
178 "view-source:data:application/xml;charset=utf-8,")
179 + encodeURIComponent(tmpNode.innerHTML),
180 loadFlags, null, null, null,
181 Services.io.newURI(doc.baseURI, null, null));
182 }
184 ////////////////////////////////////////////////////////////////////////////////
185 // helper to get a path like FIXptr, but with an array instead of the "tumbler" notation
186 // see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
187 function getPath(ancestor, node)
188 {
189 var n = node;
190 var p = n.parentNode;
191 if (n == ancestor || !p)
192 return null;
193 var path = new Array();
194 if (!path)
195 return null;
196 do {
197 for (var i = 0; i < p.childNodes.length; i++) {
198 if (p.childNodes.item(i) == n) {
199 path.push(i);
200 break;
201 }
202 }
203 n = p;
204 p = n.parentNode;
205 } while (n != ancestor && p);
206 return path;
207 }
209 ////////////////////////////////////////////////////////////////////////////////
210 // using special markers left in the serialized source, this helper makes the
211 // underlying markup of the selected fragment to automatically appear as selected
212 // on the inflated view-source DOM
213 function drawSelection()
214 {
215 gBrowser.contentDocument.title =
216 gViewSourceBundle.getString("viewSelectionSourceTitle");
218 // find the special selection markers that we added earlier, and
219 // draw the selection between the two...
220 var findService = null;
221 try {
222 // get the find service which stores the global find state
223 findService = Components.classes["@mozilla.org/find/find_service;1"]
224 .getService(Components.interfaces.nsIFindService);
225 } catch(e) { }
226 if (!findService)
227 return;
229 // cache the current global find state
230 var matchCase = findService.matchCase;
231 var entireWord = findService.entireWord;
232 var wrapFind = findService.wrapFind;
233 var findBackwards = findService.findBackwards;
234 var searchString = findService.searchString;
235 var replaceString = findService.replaceString;
237 // setup our find instance
238 var findInst = gBrowser.webBrowserFind;
239 findInst.matchCase = true;
240 findInst.entireWord = false;
241 findInst.wrapFind = true;
242 findInst.findBackwards = false;
244 // ...lookup the start mark
245 findInst.searchString = MARK_SELECTION_START;
246 var startLength = MARK_SELECTION_START.length;
247 findInst.findNext();
249 var selection = content.getSelection();
250 if (!selection.rangeCount)
251 return;
253 var range = selection.getRangeAt(0);
255 var startContainer = range.startContainer;
256 var startOffset = range.startOffset;
258 // ...lookup the end mark
259 findInst.searchString = MARK_SELECTION_END;
260 var endLength = MARK_SELECTION_END.length;
261 findInst.findNext();
263 var endContainer = selection.anchorNode;
264 var endOffset = selection.anchorOffset;
266 // reset the selection that find has left
267 selection.removeAllRanges();
269 // delete the special markers now...
270 endContainer.deleteData(endOffset, endLength);
271 startContainer.deleteData(startOffset, startLength);
272 if (startContainer == endContainer)
273 endOffset -= startLength; // has shrunk if on same text node...
274 range.setEnd(endContainer, endOffset);
276 // show the selection and scroll it into view
277 selection.addRange(range);
278 // the default behavior of the selection is to scroll at the end of
279 // the selection, whereas in this situation, it is more user-friendly
280 // to scroll at the beginning. So we override the default behavior here
281 try {
282 getSelectionController().scrollSelectionIntoView(
283 Ci.nsISelectionController.SELECTION_NORMAL,
284 Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
285 true);
286 }
287 catch(e) { }
289 // restore the current find state
290 findService.matchCase = matchCase;
291 findService.entireWord = entireWord;
292 findService.wrapFind = wrapFind;
293 findService.findBackwards = findBackwards;
294 findService.searchString = searchString;
295 findService.replaceString = replaceString;
297 findInst.matchCase = matchCase;
298 findInst.entireWord = entireWord;
299 findInst.wrapFind = wrapFind;
300 findInst.findBackwards = findBackwards;
301 findInst.searchString = searchString;
302 }
304 ////////////////////////////////////////////////////////////////////////////////
305 // special handler for markups such as MathML where reformatting the output is
306 // helpful
307 function viewPartialSourceForFragment(node, context)
308 {
309 gTargetNode = node;
310 if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE)
311 gTargetNode = gTargetNode.parentNode;
313 // walk up the tree to the top-level element (e.g., <math>, <svg>)
314 var topTag;
315 if (context == 'mathml')
316 topTag = 'math';
317 else
318 throw 'not reached';
319 var topNode = gTargetNode;
320 while (topNode && topNode.localName != topTag)
321 topNode = topNode.parentNode;
322 if (!topNode)
323 return;
325 // serialize
326 var title = gViewSourceBundle.getString("viewMathMLSourceTitle");
327 var wrapClass = gWrapLongLines ? ' class="wrap"' : '';
328 var source =
329 '<!DOCTYPE html>'
330 + '<html>'
331 + '<head><title>' + title + '</title>'
332 + '<link rel="stylesheet" type="text/css" href="' + gViewSourceCSS + '">'
333 + '<style type="text/css">'
334 + '#target { border: dashed 1px; background-color: lightyellow; }'
335 + '</style>'
336 + '</head>'
337 + '<body id="viewsource"' + wrapClass
338 + ' onload="document.title=\''+title+'\';document.getElementById(\'target\').scrollIntoView(true)">'
339 + '<pre>'
340 + getOuterMarkup(topNode, 0)
341 + '</pre></body></html>'
342 ; // end
344 // display
345 gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source));
346 }
348 ////////////////////////////////////////////////////////////////////////////////
349 function getInnerMarkup(node, indent) {
350 var str = '';
351 for (var i = 0; i < node.childNodes.length; i++) {
352 str += getOuterMarkup(node.childNodes.item(i), indent);
353 }
354 return str;
355 }
357 ////////////////////////////////////////////////////////////////////////////////
358 function getOuterMarkup(node, indent) {
359 var newline = '';
360 var padding = '';
361 var str = '';
362 if (node == gTargetNode) {
363 gStartTargetLine = gLineCount;
364 str += '</pre><pre id="target">';
365 }
367 switch (node.nodeType) {
368 case Node.ELEMENT_NODE: // Element
369 // to avoid the wide gap problem, '\n' is not emitted on the first
370 // line and the lines before & after the <pre id="target">...</pre>
371 if (gLineCount > 0 &&
372 gLineCount != gStartTargetLine &&
373 gLineCount != gEndTargetLine) {
374 newline = '\n';
375 }
376 gLineCount++;
377 if (gDebug) {
378 newline += gLineCount;
379 }
380 for (var k = 0; k < indent; k++) {
381 padding += ' ';
382 }
383 str += newline + padding
384 + '<<span class="start-tag">' + node.nodeName + '</span>';
385 for (var i = 0; i < node.attributes.length; i++) {
386 var attr = node.attributes.item(i);
387 if (!gDebug && attr.nodeName.match(/^[-_]moz/)) {
388 continue;
389 }
390 str += ' <span class="attribute-name">'
391 + attr.nodeName
392 + '</span>=<span class="attribute-value">"'
393 + unicodeTOentity(attr.nodeValue)
394 + '"</span>';
395 }
396 if (!node.hasChildNodes()) {
397 str += '/>';
398 }
399 else {
400 str += '>';
401 var oldLine = gLineCount;
402 str += getInnerMarkup(node, indent + 2);
403 if (oldLine == gLineCount) {
404 newline = '';
405 padding = '';
406 }
407 else {
408 newline = (gLineCount == gEndTargetLine) ? '' : '\n';
409 gLineCount++;
410 if (gDebug) {
411 newline += gLineCount;
412 }
413 }
414 str += newline + padding
415 + '</<span class="end-tag">' + node.nodeName + '</span>>';
416 }
417 break;
418 case Node.TEXT_NODE: // Text
419 var tmp = node.nodeValue;
420 tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
421 tmp = tmp.replace(/^ +/, "");
422 tmp = tmp.replace(/ +$/, "");
423 if (tmp.length != 0) {
424 str += '<span class="text">' + unicodeTOentity(tmp) + '</span>';
425 }
426 break;
427 default:
428 break;
429 }
431 if (node == gTargetNode) {
432 gEndTargetLine = gLineCount;
433 str += '</pre><pre>';
434 }
435 return str;
436 }
438 ////////////////////////////////////////////////////////////////////////////////
439 function unicodeTOentity(text)
440 {
441 const charTable = {
442 '&': '&<span class="entity">amp;</span>',
443 '<': '&<span class="entity">lt;</span>',
444 '>': '&<span class="entity">gt;</span>',
445 '"': '&<span class="entity">quot;</span>'
446 };
448 function charTableLookup(letter) {
449 return charTable[letter];
450 }
452 function convertEntity(letter) {
453 try {
454 var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion);
455 var entity = unichar.substring(1); // extract '&'
456 return '&<span class="entity">' + entity + '</span>';
457 } catch (ex) {
458 return letter;
459 }
460 }
462 if (!gEntityConverter) {
463 try {
464 gEntityConverter =
465 Components.classes["@mozilla.org/intl/entityconverter;1"]
466 .createInstance(Components.interfaces.nsIEntityConverter);
467 } catch(e) { }
468 }
470 const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;
472 var str = text;
474 // replace chars in our charTable
475 str = str.replace(/[<>&"]/g, charTableLookup);
477 // replace chars > 0x7f via nsIEntityConverter
478 str = str.replace(/[^\0-\u007f]/g, convertEntity);
480 return str;
481 }