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

     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         +  '&lt;<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 += '/&gt;';
   398     }
   399     else {
   400       str += '&gt;';
   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           +  '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
   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     '&': '&amp;<span class="entity">amp;</span>',
   443     '<': '&amp;<span class="entity">lt;</span>',
   444     '>': '&amp;<span class="entity">gt;</span>',
   445     '"': '&amp;<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 '&amp;<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 }

mercurial