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., , ) michael@0: var topTag; michael@0: if (context == 'mathml') michael@0: topTag = 'math'; michael@0: else michael@0: throw 'not reached'; michael@0: var topNode = gTargetNode; michael@0: while (topNode && topNode.localName != topTag) michael@0: topNode = topNode.parentNode; michael@0: if (!topNode) michael@0: return; michael@0: michael@0: // serialize michael@0: var title = gViewSourceBundle.getString("viewMathMLSourceTitle"); michael@0: var wrapClass = gWrapLongLines ? ' class="wrap"' : ''; michael@0: var source = michael@0: '' michael@0: + '' michael@0: + '' + title + '' michael@0: + '' michael@0: + '' michael@0: + '' michael@0: + '' michael@0: + '
'
michael@0:   + getOuterMarkup(topNode, 0)
michael@0:   + '
' michael@0: ; // end michael@0: michael@0: // display michael@0: gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source)); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: function getInnerMarkup(node, indent) { michael@0: var str = ''; michael@0: for (var i = 0; i < node.childNodes.length; i++) { michael@0: str += getOuterMarkup(node.childNodes.item(i), indent); michael@0: } michael@0: return str; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: function getOuterMarkup(node, indent) { michael@0: var newline = ''; michael@0: var padding = ''; michael@0: var str = ''; michael@0: if (node == gTargetNode) { michael@0: gStartTargetLine = gLineCount; michael@0: str += '
';
michael@0:   }
michael@0: 
michael@0:   switch (node.nodeType) {
michael@0:   case Node.ELEMENT_NODE: // Element
michael@0:     // to avoid the wide gap problem, '\n' is not emitted on the first
michael@0:     // line and the lines before & after the 
...
michael@0: if (gLineCount > 0 && michael@0: gLineCount != gStartTargetLine && michael@0: gLineCount != gEndTargetLine) { michael@0: newline = '\n'; michael@0: } michael@0: gLineCount++; michael@0: if (gDebug) { michael@0: newline += gLineCount; michael@0: } michael@0: for (var k = 0; k < indent; k++) { michael@0: padding += ' '; michael@0: } michael@0: str += newline + padding michael@0: + '<' + node.nodeName + ''; michael@0: for (var i = 0; i < node.attributes.length; i++) { michael@0: var attr = node.attributes.item(i); michael@0: if (!gDebug && attr.nodeName.match(/^[-_]moz/)) { michael@0: continue; michael@0: } michael@0: str += ' ' michael@0: + attr.nodeName michael@0: + '="' michael@0: + unicodeTOentity(attr.nodeValue) michael@0: + '"'; michael@0: } michael@0: if (!node.hasChildNodes()) { michael@0: str += '/>'; michael@0: } michael@0: else { michael@0: str += '>'; michael@0: var oldLine = gLineCount; michael@0: str += getInnerMarkup(node, indent + 2); michael@0: if (oldLine == gLineCount) { michael@0: newline = ''; michael@0: padding = ''; michael@0: } michael@0: else { michael@0: newline = (gLineCount == gEndTargetLine) ? '' : '\n'; michael@0: gLineCount++; michael@0: if (gDebug) { michael@0: newline += gLineCount; michael@0: } michael@0: } michael@0: str += newline + padding michael@0: + '</' + node.nodeName + '>'; michael@0: } michael@0: break; michael@0: case Node.TEXT_NODE: // Text michael@0: var tmp = node.nodeValue; michael@0: tmp = tmp.replace(/(\n|\r|\t)+/g, " "); michael@0: tmp = tmp.replace(/^ +/, ""); michael@0: tmp = tmp.replace(/ +$/, ""); michael@0: if (tmp.length != 0) { michael@0: str += '' + unicodeTOentity(tmp) + ''; michael@0: } michael@0: break; michael@0: default: michael@0: break; michael@0: } michael@0: michael@0: if (node == gTargetNode) { michael@0: gEndTargetLine = gLineCount; michael@0: str += '
';
michael@0:   }
michael@0:   return str;
michael@0: }
michael@0: 
michael@0: ////////////////////////////////////////////////////////////////////////////////
michael@0: function unicodeTOentity(text)
michael@0: {
michael@0:   const charTable = {
michael@0:     '&': '&amp;',
michael@0:     '<': '&lt;',
michael@0:     '>': '&gt;',
michael@0:     '"': '&quot;'
michael@0:   };
michael@0: 
michael@0:   function charTableLookup(letter) {
michael@0:     return charTable[letter];
michael@0:   }
michael@0: 
michael@0:   function convertEntity(letter) {
michael@0:     try {
michael@0:       var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion);
michael@0:       var entity = unichar.substring(1); // extract '&'
michael@0:       return '&' + entity + '';
michael@0:     } catch (ex) {
michael@0:       return letter;
michael@0:     }
michael@0:   }
michael@0: 
michael@0:   if (!gEntityConverter) {
michael@0:     try {
michael@0:       gEntityConverter =
michael@0:         Components.classes["@mozilla.org/intl/entityconverter;1"]
michael@0:                   .createInstance(Components.interfaces.nsIEntityConverter);
michael@0:     } catch(e) { }
michael@0:   }
michael@0: 
michael@0:   const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;
michael@0: 
michael@0:   var str = text;
michael@0: 
michael@0:   // replace chars in our charTable
michael@0:   str = str.replace(/[<>&"]/g, charTableLookup);
michael@0: 
michael@0:   // replace chars > 0x7f via nsIEntityConverter
michael@0:   str = str.replace(/[^\0-\u007f]/g, convertEntity);
michael@0: 
michael@0:   return str;
michael@0: }