michael@0: // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
michael@0:
michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: Components.utils.import("resource://gre/modules/Services.jsm");
michael@0:
michael@0: var gDebug = 0;
michael@0: var gLineCount = 0;
michael@0: var gStartTargetLine = 0;
michael@0: var gEndTargetLine = 0;
michael@0: var gTargetNode = null;
michael@0:
michael@0: var gEntityConverter = null;
michael@0: var gWrapLongLines = false;
michael@0: const gViewSourceCSS = 'resource://gre-resources/viewsource.css';
michael@0: const NS_XHTML = 'http://www.w3.org/1999/xhtml';
michael@0:
michael@0: // These are markers used to delimit the selection during processing. They
michael@0: // are removed from the final rendering, but we pick space-like characters for
michael@0: // safety (and futhermore, these are known to be mapped to a 0-length string
michael@0: // in transliterate.properties). It is okay to set start=end, we use findNext()
michael@0: // U+200B ZERO WIDTH SPACE
michael@0: const MARK_SELECTION_START = '\u200B\u200B\u200B\u200B\u200B';
michael@0: const MARK_SELECTION_END = '\u200B\u200B\u200B\u200B\u200B';
michael@0:
michael@0: function onLoadViewPartialSource()
michael@0: {
michael@0: // check the view_source.wrap_long_lines pref
michael@0: // and set the menuitem's checked attribute accordingly
michael@0: gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
michael@0: document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines);
michael@0: document.getElementById("menu_highlightSyntax")
michael@0: .setAttribute("checked",
michael@0: Services.prefs.getBoolPref("view_source.syntax_highlight"));
michael@0:
michael@0: if (window.arguments[3] == 'selection')
michael@0: viewPartialSourceForSelection(window.arguments[2]);
michael@0: else
michael@0: viewPartialSourceForFragment(window.arguments[2], window.arguments[3]);
michael@0:
michael@0: gBrowser.droppedLinkHandler = function (event, url, name) {
michael@0: viewSource(url)
michael@0: event.preventDefault();
michael@0: }
michael@0:
michael@0: window.content.focus();
michael@0: }
michael@0:
michael@0: ////////////////////////////////////////////////////////////////////////////////
michael@0: // view-source of a selection with the special effect of remapping the selection
michael@0: // to the underlying view-source output
michael@0: function viewPartialSourceForSelection(selection)
michael@0: {
michael@0: var range = selection.getRangeAt(0);
michael@0: var ancestorContainer = range.commonAncestorContainer;
michael@0: var doc = ancestorContainer.ownerDocument;
michael@0:
michael@0: var startContainer = range.startContainer;
michael@0: var endContainer = range.endContainer;
michael@0: var startOffset = range.startOffset;
michael@0: var endOffset = range.endOffset;
michael@0:
michael@0: // let the ancestor be an element
michael@0: if (ancestorContainer.nodeType == Node.TEXT_NODE ||
michael@0: ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
michael@0: ancestorContainer = ancestorContainer.parentNode;
michael@0:
michael@0: // for selectAll, let's use the entire document, including ...
michael@0: // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
michael@0: try {
michael@0: if (ancestorContainer == doc.body)
michael@0: ancestorContainer = doc.documentElement;
michael@0: } catch (e) { }
michael@0:
michael@0: // each path is a "child sequence" (a.k.a. "tumbler") that
michael@0: // descends from the ancestor down to the boundary point
michael@0: var startPath = getPath(ancestorContainer, startContainer);
michael@0: var endPath = getPath(ancestorContainer, endContainer);
michael@0:
michael@0: // clone the fragment of interest and reset everything to be relative to it
michael@0: // note: it is with the clone that we operate/munge from now on. Also note
michael@0: // that we clone into a data document to prevent images in the fragment from
michael@0: // loading and the like. The use of importNode here, as opposed to adoptNode,
michael@0: // is _very_ important.
michael@0: // XXXbz wish there were a less hacky way to create an untrusted document here
michael@0: var isHTML = (doc.createElement("div").tagName == "DIV");
michael@0: var dataDoc = isHTML ?
michael@0: ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
michael@0: ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
michael@0: ancestorContainer = dataDoc.importNode(ancestorContainer, true);
michael@0: startContainer = ancestorContainer;
michael@0: endContainer = ancestorContainer;
michael@0:
michael@0: // Only bother with the selection if it can be remapped. Don't mess with
michael@0: // leaf elements (such as ) that secretly use anynomous content
michael@0: // for their display appearance.
michael@0: var canDrawSelection = ancestorContainer.hasChildNodes();
michael@0: if (canDrawSelection) {
michael@0: var i;
michael@0: for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
michael@0: startContainer = startContainer.childNodes.item(startPath[i]);
michael@0: }
michael@0: for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
michael@0: endContainer = endContainer.childNodes.item(endPath[i]);
michael@0: }
michael@0:
michael@0: // add special markers to record the extent of the selection
michael@0: // note: |startOffset| and |endOffset| are interpreted either as
michael@0: // offsets in the text data or as child indices (see the Range spec)
michael@0: // (here, munging the end point first to keep the start point safe...)
michael@0: var tmpNode;
michael@0: if (endContainer.nodeType == Node.TEXT_NODE ||
michael@0: endContainer.nodeType == Node.CDATA_SECTION_NODE) {
michael@0: // do some extra tweaks to try to avoid the view-source output to look like
michael@0: // ...]... or ...]... (where ']' marks the end of the selection).
michael@0: // To get a neat output, the idea here is to remap the end point from:
michael@0: // 1. ...]... to ...]...
michael@0: // 2. ...]... to ...]...
michael@0: if ((endOffset > 0 && endOffset < endContainer.data.length) ||
michael@0: !endContainer.parentNode || !endContainer.parentNode.parentNode)
michael@0: endContainer.insertData(endOffset, MARK_SELECTION_END);
michael@0: else {
michael@0: tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
michael@0: endContainer = endContainer.parentNode;
michael@0: if (endOffset == 0)
michael@0: endContainer.parentNode.insertBefore(tmpNode, endContainer);
michael@0: else
michael@0: endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
michael@0: }
michael@0: }
michael@0: else {
michael@0: tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
michael@0: endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
michael@0: }
michael@0:
michael@0: if (startContainer.nodeType == Node.TEXT_NODE ||
michael@0: startContainer.nodeType == Node.CDATA_SECTION_NODE) {
michael@0: // do some extra tweaks to try to avoid the view-source output to look like
michael@0: // ...[... or ...[... (where '[' marks the start of the selection).
michael@0: // To get a neat output, the idea here is to remap the start point from:
michael@0: // 1. ...[... to ...[...
michael@0: // 2. ...[... to ...[...
michael@0: if ((startOffset > 0 && startOffset < startContainer.data.length) ||
michael@0: !startContainer.parentNode || !startContainer.parentNode.parentNode ||
michael@0: startContainer != startContainer.parentNode.lastChild)
michael@0: startContainer.insertData(startOffset, MARK_SELECTION_START);
michael@0: else {
michael@0: tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
michael@0: startContainer = startContainer.parentNode;
michael@0: if (startOffset == 0)
michael@0: startContainer.parentNode.insertBefore(tmpNode, startContainer);
michael@0: else
michael@0: startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
michael@0: }
michael@0: }
michael@0: else {
michael@0: tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
michael@0: startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
michael@0: }
michael@0: }
michael@0:
michael@0: // now extract and display the syntax highlighted source
michael@0: tmpNode = dataDoc.createElementNS(NS_XHTML, 'div');
michael@0: tmpNode.appendChild(ancestorContainer);
michael@0:
michael@0: // the load is aynchronous and so we will wait until the view-source DOM is done
michael@0: // before drawing the selection.
michael@0: if (canDrawSelection) {
michael@0: window.document.getElementById("content").addEventListener("load", drawSelection, true);
michael@0: }
michael@0:
michael@0: // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
michael@0: var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
michael@0: getWebNavigation().loadURIWithBase((isHTML ?
michael@0: "view-source:data:text/html;charset=utf-8," :
michael@0: "view-source:data:application/xml;charset=utf-8,")
michael@0: + encodeURIComponent(tmpNode.innerHTML),
michael@0: loadFlags, null, null, null,
michael@0: Services.io.newURI(doc.baseURI, null, null));
michael@0: }
michael@0:
michael@0: ////////////////////////////////////////////////////////////////////////////////
michael@0: // helper to get a path like FIXptr, but with an array instead of the "tumbler" notation
michael@0: // see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
michael@0: function getPath(ancestor, node)
michael@0: {
michael@0: var n = node;
michael@0: var p = n.parentNode;
michael@0: if (n == ancestor || !p)
michael@0: return null;
michael@0: var path = new Array();
michael@0: if (!path)
michael@0: return null;
michael@0: do {
michael@0: for (var i = 0; i < p.childNodes.length; i++) {
michael@0: if (p.childNodes.item(i) == n) {
michael@0: path.push(i);
michael@0: break;
michael@0: }
michael@0: }
michael@0: n = p;
michael@0: p = n.parentNode;
michael@0: } while (n != ancestor && p);
michael@0: return path;
michael@0: }
michael@0:
michael@0: ////////////////////////////////////////////////////////////////////////////////
michael@0: // using special markers left in the serialized source, this helper makes the
michael@0: // underlying markup of the selected fragment to automatically appear as selected
michael@0: // on the inflated view-source DOM
michael@0: function drawSelection()
michael@0: {
michael@0: gBrowser.contentDocument.title =
michael@0: gViewSourceBundle.getString("viewSelectionSourceTitle");
michael@0:
michael@0: // find the special selection markers that we added earlier, and
michael@0: // draw the selection between the two...
michael@0: var findService = null;
michael@0: try {
michael@0: // get the find service which stores the global find state
michael@0: findService = Components.classes["@mozilla.org/find/find_service;1"]
michael@0: .getService(Components.interfaces.nsIFindService);
michael@0: } catch(e) { }
michael@0: if (!findService)
michael@0: return;
michael@0:
michael@0: // cache the current global find state
michael@0: var matchCase = findService.matchCase;
michael@0: var entireWord = findService.entireWord;
michael@0: var wrapFind = findService.wrapFind;
michael@0: var findBackwards = findService.findBackwards;
michael@0: var searchString = findService.searchString;
michael@0: var replaceString = findService.replaceString;
michael@0:
michael@0: // setup our find instance
michael@0: var findInst = gBrowser.webBrowserFind;
michael@0: findInst.matchCase = true;
michael@0: findInst.entireWord = false;
michael@0: findInst.wrapFind = true;
michael@0: findInst.findBackwards = false;
michael@0:
michael@0: // ...lookup the start mark
michael@0: findInst.searchString = MARK_SELECTION_START;
michael@0: var startLength = MARK_SELECTION_START.length;
michael@0: findInst.findNext();
michael@0:
michael@0: var selection = content.getSelection();
michael@0: if (!selection.rangeCount)
michael@0: return;
michael@0:
michael@0: var range = selection.getRangeAt(0);
michael@0:
michael@0: var startContainer = range.startContainer;
michael@0: var startOffset = range.startOffset;
michael@0:
michael@0: // ...lookup the end mark
michael@0: findInst.searchString = MARK_SELECTION_END;
michael@0: var endLength = MARK_SELECTION_END.length;
michael@0: findInst.findNext();
michael@0:
michael@0: var endContainer = selection.anchorNode;
michael@0: var endOffset = selection.anchorOffset;
michael@0:
michael@0: // reset the selection that find has left
michael@0: selection.removeAllRanges();
michael@0:
michael@0: // delete the special markers now...
michael@0: endContainer.deleteData(endOffset, endLength);
michael@0: startContainer.deleteData(startOffset, startLength);
michael@0: if (startContainer == endContainer)
michael@0: endOffset -= startLength; // has shrunk if on same text node...
michael@0: range.setEnd(endContainer, endOffset);
michael@0:
michael@0: // show the selection and scroll it into view
michael@0: selection.addRange(range);
michael@0: // the default behavior of the selection is to scroll at the end of
michael@0: // the selection, whereas in this situation, it is more user-friendly
michael@0: // to scroll at the beginning. So we override the default behavior here
michael@0: try {
michael@0: getSelectionController().scrollSelectionIntoView(
michael@0: Ci.nsISelectionController.SELECTION_NORMAL,
michael@0: Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
michael@0: true);
michael@0: }
michael@0: catch(e) { }
michael@0:
michael@0: // restore the current find state
michael@0: findService.matchCase = matchCase;
michael@0: findService.entireWord = entireWord;
michael@0: findService.wrapFind = wrapFind;
michael@0: findService.findBackwards = findBackwards;
michael@0: findService.searchString = searchString;
michael@0: findService.replaceString = replaceString;
michael@0:
michael@0: findInst.matchCase = matchCase;
michael@0: findInst.entireWord = entireWord;
michael@0: findInst.wrapFind = wrapFind;
michael@0: findInst.findBackwards = findBackwards;
michael@0: findInst.searchString = searchString;
michael@0: }
michael@0:
michael@0: ////////////////////////////////////////////////////////////////////////////////
michael@0: // special handler for markups such as MathML where reformatting the output is
michael@0: // helpful
michael@0: function viewPartialSourceForFragment(node, context)
michael@0: {
michael@0: gTargetNode = node;
michael@0: if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE)
michael@0: gTargetNode = gTargetNode.parentNode;
michael@0:
michael@0: // walk up the tree to the top-level element (e.g.,