/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org view-source front-end. * * The Initial Developer of the Original Code is mozilla.org. * Portions created by the Initial Developer are Copyright (C) 2002 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Roger B. Sidje (Original Author) * Steve Swanson * Doron Rosenberg * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var gDebug = 0; var gLineCount = 0; var gStartTargetLine = 0; var gEndTargetLine = 0; var gTargetNode = null; var gEntityConverter = null; var gWrapLongLines = false; const gViewSourceCSS = 'resource://gre/res/viewsource.css'; const NS_XHTML = 'http://www.w3.org/1999/xhtml'; // These are markers used to delimit the selection during processing. They // are removed from the final rendering, but we pick space-like characters for // safety (and futhermore, these are known to be mapped to a 0-length string // in transliterate.properties). It is okay to set start=end, we use findNext() // U+200B ZERO WIDTH SPACE const MARK_SELECTION_START = '\u200B\u200B\u200B\u200B\u200B'; const MARK_SELECTION_END = '\u200B\u200B\u200B\u200B\u200B'; function onLoadViewPartialSource() { // check the view_source.wrap_long_lines pref and set the menuitem's checked attribute accordingly if (gPrefs) { try { var wraplonglinesPrefValue = gPrefs.getBoolPref('view_source.wrap_long_lines'); if (wraplonglinesPrefValue) { document.getElementById('menu_wrapLongLines').setAttribute('checked', 'true'); gWrapLongLines = true; } } catch (e) { } try { document.getElementById("menu_highlightSyntax").setAttribute("checked", gPrefs.getBoolPref("view_source.syntax_highlight")); } catch (e) { } } else { document.getElementById("menu_highlightSyntax").setAttribute("hidden", "true"); } // disable menu items that don't work since the selection is munged and // the editor doesn't work for MathML document.getElementById('cmd_savePage').setAttribute('disabled', 'true'); document.getElementById('cmd_editPage').setAttribute('disabled', 'true'); if (window.arguments[3] == 'selection') viewPartialSourceForSelection(window.arguments[2]); else viewPartialSourceForFragment(window.arguments[2], window.arguments[3]); window.content.focus(); } //////////////////////////////////////////////////////////////////////////////// // view-source of a selection with the special effect of remapping the selection // to the underlying view-source output function viewPartialSourceForSelection(selection) { var range = selection.getRangeAt(0); var ancestorContainer = range.commonAncestorContainer; var doc = ancestorContainer.ownerDocument; var startContainer = range.startContainer; var endContainer = range.endContainer; var startOffset = range.startOffset; var endOffset = range.endOffset; // let the ancestor be an element if (ancestorContainer.nodeType == Node.TEXT_NODE || ancestorContainer.nodeType == Node.CDATA_SECTION_NODE) ancestorContainer = ancestorContainer.parentNode; // for selectAll, let's use the entire document, including ... // @see DocumentViewerImpl::SelectAll() for how selectAll is implemented try { if (ancestorContainer == doc.body) ancestorContainer = doc.documentElement; } catch (e) { } // each path is a "child sequence" (a.k.a. "tumbler") that // descends from the ancestor down to the boundary point var startPath = getPath(ancestorContainer, startContainer); var endPath = getPath(ancestorContainer, endContainer); // clone the fragment of interest and reset everything to be relative to it // note: it is with the clone that we operate/munge from now on ancestorContainer = ancestorContainer.cloneNode(true); startContainer = ancestorContainer; endContainer = ancestorContainer; // Only bother with the selection if it can be remapped. Don't mess with // leaf elements (such as ) that secretly use anynomous content // for their display appearance. var canDrawSelection = ancestorContainer.hasChildNodes(); if (canDrawSelection) { var i; for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) { startContainer = startContainer.childNodes.item(startPath[i]); } for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) { endContainer = endContainer.childNodes.item(endPath[i]); } // add special markers to record the extent of the selection // note: |startOffset| and |endOffset| are interpreted either as // offsets in the text data or as child indices (see the Range spec) // (here, munging the end point first to keep the start point safe...) var tmpNode; if (endContainer.nodeType == Node.TEXT_NODE || endContainer.nodeType == Node.CDATA_SECTION_NODE) { // do some extra tweaks to try to avoid the view-source output to look like // ...]... or ...]... (where ']' marks the end of the selection). // To get a neat output, the idea here is to remap the end point from: // 1. ...]... to ...]... // 2. ...]... to ...]... if ((endOffset > 0 && endOffset < endContainer.data.length) || !endContainer.parentNode || !endContainer.parentNode.parentNode) endContainer.insertData(endOffset, MARK_SELECTION_END); else { tmpNode = doc.createTextNode(MARK_SELECTION_END); endContainer = endContainer.parentNode; if (endOffset == 0) endContainer.parentNode.insertBefore(tmpNode, endContainer); else endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling); } } else { tmpNode = doc.createTextNode(MARK_SELECTION_END); endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset)); } if (startContainer.nodeType == Node.TEXT_NODE || startContainer.nodeType == Node.CDATA_SECTION_NODE) { // do some extra tweaks to try to avoid the view-source output to look like // ...[... or ...[... (where '[' marks the start of the selection). // To get a neat output, the idea here is to remap the start point from: // 1. ...[... to ...[... // 2. ...[... to ...[... if ((startOffset > 0 && startOffset < startContainer.data.length) || !startContainer.parentNode || !startContainer.parentNode.parentNode || startContainer != startContainer.parentNode.lastChild) startContainer.insertData(startOffset, MARK_SELECTION_START); else { tmpNode = doc.createTextNode(MARK_SELECTION_START); startContainer = startContainer.parentNode; if (startOffset == 0) startContainer.parentNode.insertBefore(tmpNode, startContainer); else startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling); } } else { tmpNode = doc.createTextNode(MARK_SELECTION_START); startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset)); } } // now extract and display the syntax highlighted source tmpNode = doc.createElementNS(NS_XHTML, 'div'); tmpNode.appendChild(ancestorContainer); // the load is aynchronous and so we will wait until the view-source DOM is done // before drawing the selection. if (canDrawSelection) { window.document.getElementById("appcontent").addEventListener("load", drawSelection, true); } // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl) var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; getBrowser().webNavigation .loadURI("view-source:data:text/html;charset=utf-8," + encodeURIComponent(tmpNode.innerHTML), loadFlags, null, null, null); } //////////////////////////////////////////////////////////////////////////////// // helper to get a path like FIXptr, but with an array instead of the "tumbler" notation // see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm function getPath(ancestor, node) { var n = node; var p = n.parentNode; if (n == ancestor || !p) return null; var path = new Array(); if (!path) return null; do { for (var i = 0; i < p.childNodes.length; i++) { if (p.childNodes.item(i) == n) { path.push(i); break; } } n = p; p = n.parentNode; } while (n != ancestor && p); return path; } //////////////////////////////////////////////////////////////////////////////// // using special markers left in the serialized source, this helper makes the // underlying markup of the selected fragment to automatically appear as selected // on the inflated view-source DOM function drawSelection() { // find the special selection markers that we added earlier, and // draw the selection between the two... var findService = null; try { // get the find service which stores the global find state findService = Components.classes["@mozilla.org/find/find_service;1"] .getService(Components.interfaces.nsIFindService); } catch(e) { } if (!findService) return; // cache the current global find state var matchCase = findService.matchCase; var entireWord = findService.entireWord; var wrapFind = findService.wrapFind; var findBackwards = findService.findBackwards; var searchString = findService.searchString; var replaceString = findService.replaceString; // setup our find instance var findInst = getBrowser().webBrowserFind; findInst.matchCase = true; findInst.entireWord = false; findInst.wrapFind = true; findInst.findBackwards = false; // ...lookup the start mark findInst.searchString = MARK_SELECTION_START; var startLength = MARK_SELECTION_START.length; findInst.findNext(); var contentWindow = getBrowser().contentDocument.defaultView; var selection = contentWindow.getSelection(); var range = selection.getRangeAt(0); var startContainer = range.startContainer; var startOffset = range.startOffset; // ...lookup the end mark findInst.searchString = MARK_SELECTION_END; var endLength = MARK_SELECTION_END.length; findInst.findNext(); var endContainer = selection.anchorNode; var endOffset = selection.anchorOffset; // reset the selection that find has left selection.removeAllRanges(); // delete the special markers now... endContainer.deleteData(endOffset, endLength); startContainer.deleteData(startOffset, startLength); if (startContainer == endContainer) endOffset -= startLength; // has shrunk if on same text node... range.setEnd(endContainer, endOffset); // show the selection and scroll it into view selection.addRange(range); // the default behavior of the selection is to scroll at the end of // the selection, whereas in this situation, it is more user-friendly // to scroll at the beginning. So we override the default behavior here try { getBrowser().docShell .QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsISelectionDisplay) .QueryInterface(Components.interfaces.nsISelectionController) .scrollSelectionIntoView(Components.interfaces.nsISelectionController.SELECTION_NORMAL, Components.interfaces.nsISelectionController.SELECTION_ANCHOR_REGION, true); } catch(e) { } // restore the current find state findService.matchCase = matchCase; findService.entireWord = entireWord; findService.wrapFind = wrapFind; findService.findBackwards = findBackwards; findService.searchString = searchString; findService.replaceString = replaceString; findInst.matchCase = matchCase; findInst.entireWord = entireWord; findInst.wrapFind = wrapFind; findInst.findBackwards = findBackwards; findInst.searchString = searchString; } //////////////////////////////////////////////////////////////////////////////// // special handler for markups such as MathML where reformatting the output is // helpful function viewPartialSourceForFragment(node, context) { gTargetNode = node; if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE) gTargetNode = gTargetNode.parentNode; // walk up the tree to the top-level element (e.g., , ) var topTag; if (context == 'mathml') topTag = 'math'; else throw 'not reached'; var topNode = gTargetNode; while (topNode && topNode.localName != topTag) topNode = topNode.parentNode; if (!topNode) return; // serialize (note: the main window overrides the title set here) var wrapClass = gWrapLongLines ? ' class="wrap"' : ''; var source = '' + 'Mozilla' + '' + '' + '' + '' + '
'
  + getOuterMarkup(topNode, 0)
  + '
' ; // end // display var doc = getBrowser().contentDocument; doc.open("text/html", "replace"); doc.write(source); doc.close(); } //////////////////////////////////////////////////////////////////////////////// function getInnerMarkup(node, indent) { var str = ''; for (var i = 0; i < node.childNodes.length; i++) { str += getOuterMarkup(node.childNodes.item(i), indent); } return str; } //////////////////////////////////////////////////////////////////////////////// function getOuterMarkup(node, indent) { var newline = ''; var padding = ''; var str = ''; if (node == gTargetNode) { gStartTargetLine = gLineCount; str += '
';
  }

  switch (node.nodeType) {
  case Node.ELEMENT_NODE: // Element
    // to avoid the wide gap problem, '\n' is not emitted on the first
    // line and the lines before & after the 
...
if (gLineCount > 0 && gLineCount != gStartTargetLine && gLineCount != gEndTargetLine) { newline = '\n'; } gLineCount++; if (gDebug) { newline += gLineCount; } for (var k = 0; k < indent; k++) { padding += ' '; } str += newline + padding + '<' + node.nodeName + ''; for (var i = 0; i < node.attributes.length; i++) { var attr = node.attributes.item(i); if (!gDebug && attr.nodeName.match(/^[-_]moz/)) { continue; } str += ' ' + attr.nodeName + '="' + unicodeTOentity(attr.nodeValue) + '"'; } if (!node.hasChildNodes()) { str += '/>'; } else { str += '>'; var oldLine = gLineCount; str += getInnerMarkup(node, indent + 2); if (oldLine == gLineCount) { newline = ''; padding = ''; } else { newline = (gLineCount == gEndTargetLine) ? '' : '\n'; gLineCount++; if (gDebug) { newline += gLineCount; } } str += newline + padding + '</' + node.nodeName + '>'; } break; case Node.TEXT_NODE: // Text var tmp = node.nodeValue; tmp = tmp.replace(/(\n|\r|\t)+/g, " "); tmp = tmp.replace(/^ +/, ""); tmp = tmp.replace(/ +$/, ""); if (tmp.length != 0) { str += '' + unicodeTOentity(tmp) + ''; } break; default: break; } if (node == gTargetNode) { gEndTargetLine = gLineCount; str += '
';
  }
  return str;
}

////////////////////////////////////////////////////////////////////////////////
function unicodeTOentity(text)
{
  const charTable = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;'
  };

  function charTableLookup(letter) {
    return charTable[letter];
  }

  function convertEntity(letter) {
    try {
      var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion);
      var entity = unichar.substring(1); // extract '&'
      return '&' + entity + '';
    } catch (ex) {
      return letter;
    }
  }

  if (!gEntityConverter) {
    try {
      gEntityConverter =
        Components.classes["@mozilla.org/intl/entityconverter;1"]
                  .createInstance(Components.interfaces.nsIEntityConverter);
    } catch(e) { }
  }

  const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;

  var str = text;

  // replace chars in our charTable
  str = str.replace(/[<>&"]/g, charTableLookup);

  // replace chars > 0x7f via nsIEntityConverter
  str = str.replace(/[^\0-\u007f]/g, convertEntity);

  return str;
}