toolkit/components/viewsource/content/viewPartialSource.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial