home *** CD-ROM | disk | FTP | other *** search
/ PC World 2005 March / PCWorld_2005-03_cd.bin / komunikace / kmeleon / kmeleon09.exe / comm.jar / content / navigator / viewPartialSource.js < prev    next >
Text File  |  2003-12-03  |  19KB  |  500 lines

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