// -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/Services.jsm");
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-resources/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
gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines);
document.getElementById("menu_highlightSyntax")
.setAttribute("checked",
Services.prefs.getBoolPref("view_source.syntax_highlight"));
if (window.arguments[3] == 'selection')
viewPartialSourceForSelection(window.arguments[2]);
else
viewPartialSourceForFragment(window.arguments[2], window.arguments[3]);
gBrowser.droppedLinkHandler = function (event, url, name) {
viewSource(url)
event.preventDefault();
}
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 nsDocumentViewer::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. Also note
// that we clone into a data document to prevent images in the fragment from
// loading and the like. The use of importNode here, as opposed to adoptNode,
// is _very_ important.
// XXXbz wish there were a less hacky way to create an untrusted document here
var isHTML = (doc.createElement("div").tagName == "DIV");
var dataDoc = isHTML ?
ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
ancestorContainer = dataDoc.importNode(ancestorContainer, 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 = dataDoc.createTextNode(MARK_SELECTION_END);
endContainer = endContainer.parentNode;
if (endOffset == 0)
endContainer.parentNode.insertBefore(tmpNode, endContainer);
else
endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
}
}
else {
tmpNode = dataDoc.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 = dataDoc.createTextNode(MARK_SELECTION_START);
startContainer = startContainer.parentNode;
if (startOffset == 0)
startContainer.parentNode.insertBefore(tmpNode, startContainer);
else
startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
}
}
else {
tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
}
}
// now extract and display the syntax highlighted source
tmpNode = dataDoc.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("content").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_NONE;
getWebNavigation().loadURI((isHTML ?
"view-source:data:text/html;charset=utf-8," :
"view-source:data:application/xml;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()
{
gBrowser.contentDocument.title =
gViewSourceBundle.getString("viewSelectionSourceTitle");
// 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 = gBrowser.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 selection = content.getSelection();
if (!selection.rangeCount)
return;
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 {
getSelectionController().scrollSelectionIntoView(
Ci.nsISelectionController.SELECTION_NORMAL,
Ci.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.,