toolkit/components/viewsource/content/viewPartialSource.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/viewsource/content/viewPartialSource.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,481 @@
     1.4 +// -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
     1.5 +
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.11 +
    1.12 +var gDebug = 0;
    1.13 +var gLineCount = 0;
    1.14 +var gStartTargetLine = 0;
    1.15 +var gEndTargetLine = 0;
    1.16 +var gTargetNode = null;
    1.17 +
    1.18 +var gEntityConverter = null;
    1.19 +var gWrapLongLines = false;
    1.20 +const gViewSourceCSS = 'resource://gre-resources/viewsource.css';
    1.21 +const NS_XHTML = 'http://www.w3.org/1999/xhtml';
    1.22 +
    1.23 +// These are markers used to delimit the selection during processing. They
    1.24 +// are removed from the final rendering, but we pick space-like characters for
    1.25 +// safety (and futhermore, these are known to be mapped to a 0-length string
    1.26 +// in transliterate.properties). It is okay to set start=end, we use findNext()
    1.27 +// U+200B ZERO WIDTH SPACE
    1.28 +const MARK_SELECTION_START = '\u200B\u200B\u200B\u200B\u200B';
    1.29 +const MARK_SELECTION_END = '\u200B\u200B\u200B\u200B\u200B';
    1.30 +
    1.31 +function onLoadViewPartialSource()
    1.32 +{
    1.33 +  // check the view_source.wrap_long_lines pref
    1.34 +  // and set the menuitem's checked attribute accordingly
    1.35 +  gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
    1.36 +  document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines);
    1.37 +  document.getElementById("menu_highlightSyntax")
    1.38 +          .setAttribute("checked",
    1.39 +                        Services.prefs.getBoolPref("view_source.syntax_highlight"));
    1.40 +
    1.41 +  if (window.arguments[3] == 'selection')
    1.42 +    viewPartialSourceForSelection(window.arguments[2]);
    1.43 +  else
    1.44 +    viewPartialSourceForFragment(window.arguments[2], window.arguments[3]);
    1.45 +
    1.46 +  gBrowser.droppedLinkHandler = function (event, url, name) {
    1.47 +    viewSource(url)
    1.48 +    event.preventDefault();
    1.49 +  }
    1.50 +
    1.51 +  window.content.focus();
    1.52 +}
    1.53 +
    1.54 +////////////////////////////////////////////////////////////////////////////////
    1.55 +// view-source of a selection with the special effect of remapping the selection
    1.56 +// to the underlying view-source output
    1.57 +function viewPartialSourceForSelection(selection)
    1.58 +{
    1.59 +  var range = selection.getRangeAt(0);
    1.60 +  var ancestorContainer = range.commonAncestorContainer;
    1.61 +  var doc = ancestorContainer.ownerDocument;
    1.62 +
    1.63 +  var startContainer = range.startContainer;
    1.64 +  var endContainer = range.endContainer;
    1.65 +  var startOffset = range.startOffset;
    1.66 +  var endOffset = range.endOffset;
    1.67 +
    1.68 +  // let the ancestor be an element
    1.69 +  if (ancestorContainer.nodeType == Node.TEXT_NODE ||
    1.70 +      ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
    1.71 +    ancestorContainer = ancestorContainer.parentNode;
    1.72 +
    1.73 +  // for selectAll, let's use the entire document, including <html>...</html>
    1.74 +  // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
    1.75 +  try {
    1.76 +    if (ancestorContainer == doc.body)
    1.77 +      ancestorContainer = doc.documentElement;
    1.78 +  } catch (e) { }
    1.79 +
    1.80 +  // each path is a "child sequence" (a.k.a. "tumbler") that
    1.81 +  // descends from the ancestor down to the boundary point
    1.82 +  var startPath = getPath(ancestorContainer, startContainer);
    1.83 +  var endPath = getPath(ancestorContainer, endContainer);
    1.84 +
    1.85 +  // clone the fragment of interest and reset everything to be relative to it
    1.86 +  // note: it is with the clone that we operate/munge from now on.  Also note
    1.87 +  // that we clone into a data document to prevent images in the fragment from
    1.88 +  // loading and the like.  The use of importNode here, as opposed to adoptNode,
    1.89 +  // is _very_ important.
    1.90 +  // XXXbz wish there were a less hacky way to create an untrusted document here
    1.91 +  var isHTML = (doc.createElement("div").tagName == "DIV");
    1.92 +  var dataDoc = isHTML ? 
    1.93 +    ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
    1.94 +    ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
    1.95 +  ancestorContainer = dataDoc.importNode(ancestorContainer, true);
    1.96 +  startContainer = ancestorContainer;
    1.97 +  endContainer = ancestorContainer;
    1.98 +
    1.99 +  // Only bother with the selection if it can be remapped. Don't mess with
   1.100 +  // leaf elements (such as <isindex>) that secretly use anynomous content
   1.101 +  // for their display appearance.
   1.102 +  var canDrawSelection = ancestorContainer.hasChildNodes();
   1.103 +  if (canDrawSelection) {
   1.104 +    var i;
   1.105 +    for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
   1.106 +      startContainer = startContainer.childNodes.item(startPath[i]);
   1.107 +    }
   1.108 +    for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
   1.109 +      endContainer = endContainer.childNodes.item(endPath[i]);
   1.110 +    }
   1.111 +
   1.112 +    // add special markers to record the extent of the selection
   1.113 +    // note: |startOffset| and |endOffset| are interpreted either as
   1.114 +    // offsets in the text data or as child indices (see the Range spec)
   1.115 +    // (here, munging the end point first to keep the start point safe...)
   1.116 +    var tmpNode;
   1.117 +    if (endContainer.nodeType == Node.TEXT_NODE ||
   1.118 +        endContainer.nodeType == Node.CDATA_SECTION_NODE) {
   1.119 +      // do some extra tweaks to try to avoid the view-source output to look like
   1.120 +      // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
   1.121 +      // To get a neat output, the idea here is to remap the end point from:
   1.122 +      // 1. ...<tag>]...   to   ...]<tag>...
   1.123 +      // 2. ...]</tag>...  to   ...</tag>]...
   1.124 +      if ((endOffset > 0 && endOffset < endContainer.data.length) ||
   1.125 +          !endContainer.parentNode || !endContainer.parentNode.parentNode)
   1.126 +        endContainer.insertData(endOffset, MARK_SELECTION_END);
   1.127 +      else {
   1.128 +        tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
   1.129 +        endContainer = endContainer.parentNode;
   1.130 +        if (endOffset == 0)
   1.131 +          endContainer.parentNode.insertBefore(tmpNode, endContainer);
   1.132 +        else
   1.133 +          endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
   1.134 +      }
   1.135 +    }
   1.136 +    else {
   1.137 +      tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
   1.138 +      endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
   1.139 +    }
   1.140 +
   1.141 +    if (startContainer.nodeType == Node.TEXT_NODE ||
   1.142 +        startContainer.nodeType == Node.CDATA_SECTION_NODE) {
   1.143 +      // do some extra tweaks to try to avoid the view-source output to look like
   1.144 +      // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
   1.145 +      // To get a neat output, the idea here is to remap the start point from:
   1.146 +      // 1. ...<tag>[...   to   ...[<tag>...
   1.147 +      // 2. ...[</tag>...  to   ...</tag>[...
   1.148 +      if ((startOffset > 0 && startOffset < startContainer.data.length) ||
   1.149 +          !startContainer.parentNode || !startContainer.parentNode.parentNode ||
   1.150 +          startContainer != startContainer.parentNode.lastChild)
   1.151 +        startContainer.insertData(startOffset, MARK_SELECTION_START);
   1.152 +      else {
   1.153 +        tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
   1.154 +        startContainer = startContainer.parentNode;
   1.155 +        if (startOffset == 0)
   1.156 +          startContainer.parentNode.insertBefore(tmpNode, startContainer);
   1.157 +        else
   1.158 +          startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
   1.159 +      }
   1.160 +    }
   1.161 +    else {
   1.162 +      tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
   1.163 +      startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
   1.164 +    }
   1.165 +  }
   1.166 +
   1.167 +  // now extract and display the syntax highlighted source
   1.168 +  tmpNode = dataDoc.createElementNS(NS_XHTML, 'div');
   1.169 +  tmpNode.appendChild(ancestorContainer);
   1.170 +
   1.171 +  // the load is aynchronous and so we will wait until the view-source DOM is done
   1.172 +  // before drawing the selection.
   1.173 +  if (canDrawSelection) {
   1.174 +    window.document.getElementById("content").addEventListener("load", drawSelection, true);
   1.175 +  }
   1.176 +
   1.177 +  // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
   1.178 +  var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
   1.179 +  getWebNavigation().loadURIWithBase((isHTML ?
   1.180 +                                      "view-source:data:text/html;charset=utf-8," :
   1.181 +                                      "view-source:data:application/xml;charset=utf-8,")
   1.182 +                                     + encodeURIComponent(tmpNode.innerHTML),
   1.183 +                                     loadFlags, null, null, null,
   1.184 +                                     Services.io.newURI(doc.baseURI, null, null));
   1.185 +}
   1.186 +
   1.187 +////////////////////////////////////////////////////////////////////////////////
   1.188 +// helper to get a path like FIXptr, but with an array instead of the "tumbler" notation
   1.189 +// see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
   1.190 +function getPath(ancestor, node)
   1.191 +{
   1.192 +  var n = node;
   1.193 +  var p = n.parentNode;
   1.194 +  if (n == ancestor || !p)
   1.195 +    return null;
   1.196 +  var path = new Array();
   1.197 +  if (!path)
   1.198 +    return null;
   1.199 +  do {
   1.200 +    for (var i = 0; i < p.childNodes.length; i++) {
   1.201 +      if (p.childNodes.item(i) == n) {
   1.202 +        path.push(i);
   1.203 +        break;
   1.204 +      }
   1.205 +    }
   1.206 +    n = p;
   1.207 +    p = n.parentNode;
   1.208 +  } while (n != ancestor && p);
   1.209 +  return path;
   1.210 +}
   1.211 +
   1.212 +////////////////////////////////////////////////////////////////////////////////
   1.213 +// using special markers left in the serialized source, this helper makes the
   1.214 +// underlying markup of the selected fragment to automatically appear as selected
   1.215 +// on the inflated view-source DOM
   1.216 +function drawSelection()
   1.217 +{
   1.218 +  gBrowser.contentDocument.title =
   1.219 +    gViewSourceBundle.getString("viewSelectionSourceTitle");
   1.220 +
   1.221 +  // find the special selection markers that we added earlier, and
   1.222 +  // draw the selection between the two...
   1.223 +  var findService = null;
   1.224 +  try {
   1.225 +    // get the find service which stores the global find state
   1.226 +    findService = Components.classes["@mozilla.org/find/find_service;1"]
   1.227 +                            .getService(Components.interfaces.nsIFindService);
   1.228 +  } catch(e) { }
   1.229 +  if (!findService)
   1.230 +    return;
   1.231 +
   1.232 +  // cache the current global find state
   1.233 +  var matchCase     = findService.matchCase;
   1.234 +  var entireWord    = findService.entireWord;
   1.235 +  var wrapFind      = findService.wrapFind;
   1.236 +  var findBackwards = findService.findBackwards;
   1.237 +  var searchString  = findService.searchString;
   1.238 +  var replaceString = findService.replaceString;
   1.239 +
   1.240 +  // setup our find instance
   1.241 +  var findInst = gBrowser.webBrowserFind;
   1.242 +  findInst.matchCase = true;
   1.243 +  findInst.entireWord = false;
   1.244 +  findInst.wrapFind = true;
   1.245 +  findInst.findBackwards = false;
   1.246 +
   1.247 +  // ...lookup the start mark
   1.248 +  findInst.searchString = MARK_SELECTION_START;
   1.249 +  var startLength = MARK_SELECTION_START.length;
   1.250 +  findInst.findNext();
   1.251 +
   1.252 +  var selection = content.getSelection();
   1.253 +  if (!selection.rangeCount)
   1.254 +    return;
   1.255 +
   1.256 +  var range = selection.getRangeAt(0);
   1.257 +
   1.258 +  var startContainer = range.startContainer;
   1.259 +  var startOffset = range.startOffset;
   1.260 +
   1.261 +  // ...lookup the end mark
   1.262 +  findInst.searchString = MARK_SELECTION_END;
   1.263 +  var endLength = MARK_SELECTION_END.length;
   1.264 +  findInst.findNext();
   1.265 +
   1.266 +  var endContainer = selection.anchorNode;
   1.267 +  var endOffset = selection.anchorOffset;
   1.268 +
   1.269 +  // reset the selection that find has left
   1.270 +  selection.removeAllRanges();
   1.271 +
   1.272 +  // delete the special markers now...
   1.273 +  endContainer.deleteData(endOffset, endLength);
   1.274 +  startContainer.deleteData(startOffset, startLength);
   1.275 +  if (startContainer == endContainer)
   1.276 +    endOffset -= startLength; // has shrunk if on same text node...
   1.277 +  range.setEnd(endContainer, endOffset);
   1.278 +
   1.279 +  // show the selection and scroll it into view
   1.280 +  selection.addRange(range);
   1.281 +  // the default behavior of the selection is to scroll at the end of
   1.282 +  // the selection, whereas in this situation, it is more user-friendly
   1.283 +  // to scroll at the beginning. So we override the default behavior here
   1.284 +  try {
   1.285 +    getSelectionController().scrollSelectionIntoView(
   1.286 +                               Ci.nsISelectionController.SELECTION_NORMAL,
   1.287 +                               Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
   1.288 +                               true);
   1.289 +  }
   1.290 +  catch(e) { }
   1.291 +
   1.292 +  // restore the current find state
   1.293 +  findService.matchCase     = matchCase;
   1.294 +  findService.entireWord    = entireWord;
   1.295 +  findService.wrapFind      = wrapFind;
   1.296 +  findService.findBackwards = findBackwards;
   1.297 +  findService.searchString  = searchString;
   1.298 +  findService.replaceString = replaceString;
   1.299 +
   1.300 +  findInst.matchCase     = matchCase;
   1.301 +  findInst.entireWord    = entireWord;
   1.302 +  findInst.wrapFind      = wrapFind;
   1.303 +  findInst.findBackwards = findBackwards;
   1.304 +  findInst.searchString  = searchString;
   1.305 +}
   1.306 +
   1.307 +////////////////////////////////////////////////////////////////////////////////
   1.308 +// special handler for markups such as MathML where reformatting the output is
   1.309 +// helpful
   1.310 +function viewPartialSourceForFragment(node, context)
   1.311 +{
   1.312 +  gTargetNode = node;
   1.313 +  if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE)
   1.314 +    gTargetNode = gTargetNode.parentNode;
   1.315 +
   1.316 +  // walk up the tree to the top-level element (e.g., <math>, <svg>)
   1.317 +  var topTag;
   1.318 +  if (context == 'mathml')
   1.319 +    topTag = 'math';
   1.320 +  else
   1.321 +    throw 'not reached';
   1.322 +  var topNode = gTargetNode;
   1.323 +  while (topNode && topNode.localName != topTag)
   1.324 +    topNode = topNode.parentNode;
   1.325 +  if (!topNode)
   1.326 +    return;
   1.327 +
   1.328 +  // serialize
   1.329 +  var title = gViewSourceBundle.getString("viewMathMLSourceTitle");
   1.330 +  var wrapClass = gWrapLongLines ? ' class="wrap"' : '';
   1.331 +  var source =
   1.332 +    '<!DOCTYPE html>'
   1.333 +  + '<html>'
   1.334 +  + '<head><title>' + title + '</title>'
   1.335 +  + '<link rel="stylesheet" type="text/css" href="' + gViewSourceCSS + '">'
   1.336 +  + '<style type="text/css">'
   1.337 +  + '#target { border: dashed 1px; background-color: lightyellow; }'
   1.338 +  + '</style>'
   1.339 +  + '</head>'
   1.340 +  + '<body id="viewsource"' + wrapClass
   1.341 +  +        ' onload="document.title=\''+title+'\';document.getElementById(\'target\').scrollIntoView(true)">'
   1.342 +  + '<pre>'
   1.343 +  + getOuterMarkup(topNode, 0)
   1.344 +  + '</pre></body></html>'
   1.345 +  ; // end
   1.346 +
   1.347 +  // display
   1.348 +  gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source));
   1.349 +}
   1.350 +
   1.351 +////////////////////////////////////////////////////////////////////////////////
   1.352 +function getInnerMarkup(node, indent) {
   1.353 +  var str = '';
   1.354 +  for (var i = 0; i < node.childNodes.length; i++) {
   1.355 +    str += getOuterMarkup(node.childNodes.item(i), indent);
   1.356 +  }
   1.357 +  return str;
   1.358 +}
   1.359 +
   1.360 +////////////////////////////////////////////////////////////////////////////////
   1.361 +function getOuterMarkup(node, indent) {
   1.362 +  var newline = '';
   1.363 +  var padding = '';
   1.364 +  var str = '';
   1.365 +  if (node == gTargetNode) {
   1.366 +    gStartTargetLine = gLineCount;
   1.367 +    str += '</pre><pre id="target">';
   1.368 +  }
   1.369 +
   1.370 +  switch (node.nodeType) {
   1.371 +  case Node.ELEMENT_NODE: // Element
   1.372 +    // to avoid the wide gap problem, '\n' is not emitted on the first
   1.373 +    // line and the lines before & after the <pre id="target">...</pre>
   1.374 +    if (gLineCount > 0 &&
   1.375 +        gLineCount != gStartTargetLine &&
   1.376 +        gLineCount != gEndTargetLine) {
   1.377 +      newline = '\n';
   1.378 +    }
   1.379 +    gLineCount++;
   1.380 +    if (gDebug) {
   1.381 +      newline += gLineCount;
   1.382 +    }
   1.383 +    for (var k = 0; k < indent; k++) {
   1.384 +      padding += ' ';
   1.385 +    }
   1.386 +    str += newline + padding
   1.387 +        +  '&lt;<span class="start-tag">' + node.nodeName + '</span>';
   1.388 +    for (var i = 0; i < node.attributes.length; i++) {
   1.389 +      var attr = node.attributes.item(i);
   1.390 +      if (!gDebug && attr.nodeName.match(/^[-_]moz/)) {
   1.391 +        continue;
   1.392 +      }
   1.393 +      str += ' <span class="attribute-name">'
   1.394 +          +  attr.nodeName
   1.395 +          +  '</span>=<span class="attribute-value">"'
   1.396 +          +  unicodeTOentity(attr.nodeValue)
   1.397 +          +  '"</span>';
   1.398 +    }
   1.399 +    if (!node.hasChildNodes()) {
   1.400 +      str += '/&gt;';
   1.401 +    }
   1.402 +    else {
   1.403 +      str += '&gt;';
   1.404 +      var oldLine = gLineCount;
   1.405 +      str += getInnerMarkup(node, indent + 2);
   1.406 +      if (oldLine == gLineCount) {
   1.407 +        newline = '';
   1.408 +        padding = '';
   1.409 +      }
   1.410 +      else {
   1.411 +        newline = (gLineCount == gEndTargetLine) ? '' : '\n';
   1.412 +        gLineCount++;
   1.413 +        if (gDebug) {
   1.414 +          newline += gLineCount;
   1.415 +        }
   1.416 +      }
   1.417 +      str += newline + padding
   1.418 +          +  '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
   1.419 +    }
   1.420 +    break;
   1.421 +  case Node.TEXT_NODE: // Text
   1.422 +    var tmp = node.nodeValue;
   1.423 +    tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
   1.424 +    tmp = tmp.replace(/^ +/, "");
   1.425 +    tmp = tmp.replace(/ +$/, "");
   1.426 +    if (tmp.length != 0) {
   1.427 +      str += '<span class="text">' + unicodeTOentity(tmp) + '</span>';
   1.428 +    }
   1.429 +    break;
   1.430 +  default:
   1.431 +    break;
   1.432 +  }
   1.433 +
   1.434 +  if (node == gTargetNode) {
   1.435 +    gEndTargetLine = gLineCount;
   1.436 +    str += '</pre><pre>';
   1.437 +  }
   1.438 +  return str;
   1.439 +}
   1.440 +
   1.441 +////////////////////////////////////////////////////////////////////////////////
   1.442 +function unicodeTOentity(text)
   1.443 +{
   1.444 +  const charTable = {
   1.445 +    '&': '&amp;<span class="entity">amp;</span>',
   1.446 +    '<': '&amp;<span class="entity">lt;</span>',
   1.447 +    '>': '&amp;<span class="entity">gt;</span>',
   1.448 +    '"': '&amp;<span class="entity">quot;</span>'
   1.449 +  };
   1.450 +
   1.451 +  function charTableLookup(letter) {
   1.452 +    return charTable[letter];
   1.453 +  }
   1.454 +
   1.455 +  function convertEntity(letter) {
   1.456 +    try {
   1.457 +      var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion);
   1.458 +      var entity = unichar.substring(1); // extract '&'
   1.459 +      return '&amp;<span class="entity">' + entity + '</span>';
   1.460 +    } catch (ex) {
   1.461 +      return letter;
   1.462 +    }
   1.463 +  }
   1.464 +
   1.465 +  if (!gEntityConverter) {
   1.466 +    try {
   1.467 +      gEntityConverter =
   1.468 +        Components.classes["@mozilla.org/intl/entityconverter;1"]
   1.469 +                  .createInstance(Components.interfaces.nsIEntityConverter);
   1.470 +    } catch(e) { }
   1.471 +  }
   1.472 +
   1.473 +  const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;
   1.474 +
   1.475 +  var str = text;
   1.476 +
   1.477 +  // replace chars in our charTable
   1.478 +  str = str.replace(/[<>&"]/g, charTableLookup);
   1.479 +
   1.480 +  // replace chars > 0x7f via nsIEntityConverter
   1.481 +  str = str.replace(/[^\0-\u007f]/g, convertEntity);
   1.482 +
   1.483 +  return str;
   1.484 +}

mercurial