1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/viewsource/content/viewPartialSource.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,481 @@ 1.4 +// -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 + 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.11 + 1.12 +var gDebug = 0; 1.13 +var gLineCount = 0; 1.14 +var gStartTargetLine = 0; 1.15 +var gEndTargetLine = 0; 1.16 +var gTargetNode = null; 1.17 + 1.18 +var gEntityConverter = null; 1.19 +var gWrapLongLines = false; 1.20 +const gViewSourceCSS = 'resource://gre-resources/viewsource.css'; 1.21 +const NS_XHTML = 'http://www.w3.org/1999/xhtml'; 1.22 + 1.23 +// These are markers used to delimit the selection during processing. They 1.24 +// are removed from the final rendering, but we pick space-like characters for 1.25 +// safety (and futhermore, these are known to be mapped to a 0-length string 1.26 +// in transliterate.properties). It is okay to set start=end, we use findNext() 1.27 +// U+200B ZERO WIDTH SPACE 1.28 +const MARK_SELECTION_START = '\u200B\u200B\u200B\u200B\u200B'; 1.29 +const MARK_SELECTION_END = '\u200B\u200B\u200B\u200B\u200B'; 1.30 + 1.31 +function onLoadViewPartialSource() 1.32 +{ 1.33 + // check the view_source.wrap_long_lines pref 1.34 + // and set the menuitem's checked attribute accordingly 1.35 + gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines"); 1.36 + document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines); 1.37 + document.getElementById("menu_highlightSyntax") 1.38 + .setAttribute("checked", 1.39 + Services.prefs.getBoolPref("view_source.syntax_highlight")); 1.40 + 1.41 + if (window.arguments[3] == 'selection') 1.42 + viewPartialSourceForSelection(window.arguments[2]); 1.43 + else 1.44 + viewPartialSourceForFragment(window.arguments[2], window.arguments[3]); 1.45 + 1.46 + gBrowser.droppedLinkHandler = function (event, url, name) { 1.47 + viewSource(url) 1.48 + event.preventDefault(); 1.49 + } 1.50 + 1.51 + window.content.focus(); 1.52 +} 1.53 + 1.54 +//////////////////////////////////////////////////////////////////////////////// 1.55 +// view-source of a selection with the special effect of remapping the selection 1.56 +// to the underlying view-source output 1.57 +function viewPartialSourceForSelection(selection) 1.58 +{ 1.59 + var range = selection.getRangeAt(0); 1.60 + var ancestorContainer = range.commonAncestorContainer; 1.61 + var doc = ancestorContainer.ownerDocument; 1.62 + 1.63 + var startContainer = range.startContainer; 1.64 + var endContainer = range.endContainer; 1.65 + var startOffset = range.startOffset; 1.66 + var endOffset = range.endOffset; 1.67 + 1.68 + // let the ancestor be an element 1.69 + if (ancestorContainer.nodeType == Node.TEXT_NODE || 1.70 + ancestorContainer.nodeType == Node.CDATA_SECTION_NODE) 1.71 + ancestorContainer = ancestorContainer.parentNode; 1.72 + 1.73 + // for selectAll, let's use the entire document, including <html>...</html> 1.74 + // @see nsDocumentViewer::SelectAll() for how selectAll is implemented 1.75 + try { 1.76 + if (ancestorContainer == doc.body) 1.77 + ancestorContainer = doc.documentElement; 1.78 + } catch (e) { } 1.79 + 1.80 + // each path is a "child sequence" (a.k.a. "tumbler") that 1.81 + // descends from the ancestor down to the boundary point 1.82 + var startPath = getPath(ancestorContainer, startContainer); 1.83 + var endPath = getPath(ancestorContainer, endContainer); 1.84 + 1.85 + // clone the fragment of interest and reset everything to be relative to it 1.86 + // note: it is with the clone that we operate/munge from now on. Also note 1.87 + // that we clone into a data document to prevent images in the fragment from 1.88 + // loading and the like. The use of importNode here, as opposed to adoptNode, 1.89 + // is _very_ important. 1.90 + // XXXbz wish there were a less hacky way to create an untrusted document here 1.91 + var isHTML = (doc.createElement("div").tagName == "DIV"); 1.92 + var dataDoc = isHTML ? 1.93 + ancestorContainer.ownerDocument.implementation.createHTMLDocument("") : 1.94 + ancestorContainer.ownerDocument.implementation.createDocument("", "", null); 1.95 + ancestorContainer = dataDoc.importNode(ancestorContainer, true); 1.96 + startContainer = ancestorContainer; 1.97 + endContainer = ancestorContainer; 1.98 + 1.99 + // Only bother with the selection if it can be remapped. Don't mess with 1.100 + // leaf elements (such as <isindex>) that secretly use anynomous content 1.101 + // for their display appearance. 1.102 + var canDrawSelection = ancestorContainer.hasChildNodes(); 1.103 + if (canDrawSelection) { 1.104 + var i; 1.105 + for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) { 1.106 + startContainer = startContainer.childNodes.item(startPath[i]); 1.107 + } 1.108 + for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) { 1.109 + endContainer = endContainer.childNodes.item(endPath[i]); 1.110 + } 1.111 + 1.112 + // add special markers to record the extent of the selection 1.113 + // note: |startOffset| and |endOffset| are interpreted either as 1.114 + // offsets in the text data or as child indices (see the Range spec) 1.115 + // (here, munging the end point first to keep the start point safe...) 1.116 + var tmpNode; 1.117 + if (endContainer.nodeType == Node.TEXT_NODE || 1.118 + endContainer.nodeType == Node.CDATA_SECTION_NODE) { 1.119 + // do some extra tweaks to try to avoid the view-source output to look like 1.120 + // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection). 1.121 + // To get a neat output, the idea here is to remap the end point from: 1.122 + // 1. ...<tag>]... to ...]<tag>... 1.123 + // 2. ...]</tag>... to ...</tag>]... 1.124 + if ((endOffset > 0 && endOffset < endContainer.data.length) || 1.125 + !endContainer.parentNode || !endContainer.parentNode.parentNode) 1.126 + endContainer.insertData(endOffset, MARK_SELECTION_END); 1.127 + else { 1.128 + tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); 1.129 + endContainer = endContainer.parentNode; 1.130 + if (endOffset == 0) 1.131 + endContainer.parentNode.insertBefore(tmpNode, endContainer); 1.132 + else 1.133 + endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling); 1.134 + } 1.135 + } 1.136 + else { 1.137 + tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); 1.138 + endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset)); 1.139 + } 1.140 + 1.141 + if (startContainer.nodeType == Node.TEXT_NODE || 1.142 + startContainer.nodeType == Node.CDATA_SECTION_NODE) { 1.143 + // do some extra tweaks to try to avoid the view-source output to look like 1.144 + // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection). 1.145 + // To get a neat output, the idea here is to remap the start point from: 1.146 + // 1. ...<tag>[... to ...[<tag>... 1.147 + // 2. ...[</tag>... to ...</tag>[... 1.148 + if ((startOffset > 0 && startOffset < startContainer.data.length) || 1.149 + !startContainer.parentNode || !startContainer.parentNode.parentNode || 1.150 + startContainer != startContainer.parentNode.lastChild) 1.151 + startContainer.insertData(startOffset, MARK_SELECTION_START); 1.152 + else { 1.153 + tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); 1.154 + startContainer = startContainer.parentNode; 1.155 + if (startOffset == 0) 1.156 + startContainer.parentNode.insertBefore(tmpNode, startContainer); 1.157 + else 1.158 + startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling); 1.159 + } 1.160 + } 1.161 + else { 1.162 + tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); 1.163 + startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset)); 1.164 + } 1.165 + } 1.166 + 1.167 + // now extract and display the syntax highlighted source 1.168 + tmpNode = dataDoc.createElementNS(NS_XHTML, 'div'); 1.169 + tmpNode.appendChild(ancestorContainer); 1.170 + 1.171 + // the load is aynchronous and so we will wait until the view-source DOM is done 1.172 + // before drawing the selection. 1.173 + if (canDrawSelection) { 1.174 + window.document.getElementById("content").addEventListener("load", drawSelection, true); 1.175 + } 1.176 + 1.177 + // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl) 1.178 + var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE; 1.179 + getWebNavigation().loadURIWithBase((isHTML ? 1.180 + "view-source:data:text/html;charset=utf-8," : 1.181 + "view-source:data:application/xml;charset=utf-8,") 1.182 + + encodeURIComponent(tmpNode.innerHTML), 1.183 + loadFlags, null, null, null, 1.184 + Services.io.newURI(doc.baseURI, null, null)); 1.185 +} 1.186 + 1.187 +//////////////////////////////////////////////////////////////////////////////// 1.188 +// helper to get a path like FIXptr, but with an array instead of the "tumbler" notation 1.189 +// see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm 1.190 +function getPath(ancestor, node) 1.191 +{ 1.192 + var n = node; 1.193 + var p = n.parentNode; 1.194 + if (n == ancestor || !p) 1.195 + return null; 1.196 + var path = new Array(); 1.197 + if (!path) 1.198 + return null; 1.199 + do { 1.200 + for (var i = 0; i < p.childNodes.length; i++) { 1.201 + if (p.childNodes.item(i) == n) { 1.202 + path.push(i); 1.203 + break; 1.204 + } 1.205 + } 1.206 + n = p; 1.207 + p = n.parentNode; 1.208 + } while (n != ancestor && p); 1.209 + return path; 1.210 +} 1.211 + 1.212 +//////////////////////////////////////////////////////////////////////////////// 1.213 +// using special markers left in the serialized source, this helper makes the 1.214 +// underlying markup of the selected fragment to automatically appear as selected 1.215 +// on the inflated view-source DOM 1.216 +function drawSelection() 1.217 +{ 1.218 + gBrowser.contentDocument.title = 1.219 + gViewSourceBundle.getString("viewSelectionSourceTitle"); 1.220 + 1.221 + // find the special selection markers that we added earlier, and 1.222 + // draw the selection between the two... 1.223 + var findService = null; 1.224 + try { 1.225 + // get the find service which stores the global find state 1.226 + findService = Components.classes["@mozilla.org/find/find_service;1"] 1.227 + .getService(Components.interfaces.nsIFindService); 1.228 + } catch(e) { } 1.229 + if (!findService) 1.230 + return; 1.231 + 1.232 + // cache the current global find state 1.233 + var matchCase = findService.matchCase; 1.234 + var entireWord = findService.entireWord; 1.235 + var wrapFind = findService.wrapFind; 1.236 + var findBackwards = findService.findBackwards; 1.237 + var searchString = findService.searchString; 1.238 + var replaceString = findService.replaceString; 1.239 + 1.240 + // setup our find instance 1.241 + var findInst = gBrowser.webBrowserFind; 1.242 + findInst.matchCase = true; 1.243 + findInst.entireWord = false; 1.244 + findInst.wrapFind = true; 1.245 + findInst.findBackwards = false; 1.246 + 1.247 + // ...lookup the start mark 1.248 + findInst.searchString = MARK_SELECTION_START; 1.249 + var startLength = MARK_SELECTION_START.length; 1.250 + findInst.findNext(); 1.251 + 1.252 + var selection = content.getSelection(); 1.253 + if (!selection.rangeCount) 1.254 + return; 1.255 + 1.256 + var range = selection.getRangeAt(0); 1.257 + 1.258 + var startContainer = range.startContainer; 1.259 + var startOffset = range.startOffset; 1.260 + 1.261 + // ...lookup the end mark 1.262 + findInst.searchString = MARK_SELECTION_END; 1.263 + var endLength = MARK_SELECTION_END.length; 1.264 + findInst.findNext(); 1.265 + 1.266 + var endContainer = selection.anchorNode; 1.267 + var endOffset = selection.anchorOffset; 1.268 + 1.269 + // reset the selection that find has left 1.270 + selection.removeAllRanges(); 1.271 + 1.272 + // delete the special markers now... 1.273 + endContainer.deleteData(endOffset, endLength); 1.274 + startContainer.deleteData(startOffset, startLength); 1.275 + if (startContainer == endContainer) 1.276 + endOffset -= startLength; // has shrunk if on same text node... 1.277 + range.setEnd(endContainer, endOffset); 1.278 + 1.279 + // show the selection and scroll it into view 1.280 + selection.addRange(range); 1.281 + // the default behavior of the selection is to scroll at the end of 1.282 + // the selection, whereas in this situation, it is more user-friendly 1.283 + // to scroll at the beginning. So we override the default behavior here 1.284 + try { 1.285 + getSelectionController().scrollSelectionIntoView( 1.286 + Ci.nsISelectionController.SELECTION_NORMAL, 1.287 + Ci.nsISelectionController.SELECTION_ANCHOR_REGION, 1.288 + true); 1.289 + } 1.290 + catch(e) { } 1.291 + 1.292 + // restore the current find state 1.293 + findService.matchCase = matchCase; 1.294 + findService.entireWord = entireWord; 1.295 + findService.wrapFind = wrapFind; 1.296 + findService.findBackwards = findBackwards; 1.297 + findService.searchString = searchString; 1.298 + findService.replaceString = replaceString; 1.299 + 1.300 + findInst.matchCase = matchCase; 1.301 + findInst.entireWord = entireWord; 1.302 + findInst.wrapFind = wrapFind; 1.303 + findInst.findBackwards = findBackwards; 1.304 + findInst.searchString = searchString; 1.305 +} 1.306 + 1.307 +//////////////////////////////////////////////////////////////////////////////// 1.308 +// special handler for markups such as MathML where reformatting the output is 1.309 +// helpful 1.310 +function viewPartialSourceForFragment(node, context) 1.311 +{ 1.312 + gTargetNode = node; 1.313 + if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE) 1.314 + gTargetNode = gTargetNode.parentNode; 1.315 + 1.316 + // walk up the tree to the top-level element (e.g., <math>, <svg>) 1.317 + var topTag; 1.318 + if (context == 'mathml') 1.319 + topTag = 'math'; 1.320 + else 1.321 + throw 'not reached'; 1.322 + var topNode = gTargetNode; 1.323 + while (topNode && topNode.localName != topTag) 1.324 + topNode = topNode.parentNode; 1.325 + if (!topNode) 1.326 + return; 1.327 + 1.328 + // serialize 1.329 + var title = gViewSourceBundle.getString("viewMathMLSourceTitle"); 1.330 + var wrapClass = gWrapLongLines ? ' class="wrap"' : ''; 1.331 + var source = 1.332 + '<!DOCTYPE html>' 1.333 + + '<html>' 1.334 + + '<head><title>' + title + '</title>' 1.335 + + '<link rel="stylesheet" type="text/css" href="' + gViewSourceCSS + '">' 1.336 + + '<style type="text/css">' 1.337 + + '#target { border: dashed 1px; background-color: lightyellow; }' 1.338 + + '</style>' 1.339 + + '</head>' 1.340 + + '<body id="viewsource"' + wrapClass 1.341 + + ' onload="document.title=\''+title+'\';document.getElementById(\'target\').scrollIntoView(true)">' 1.342 + + '<pre>' 1.343 + + getOuterMarkup(topNode, 0) 1.344 + + '</pre></body></html>' 1.345 + ; // end 1.346 + 1.347 + // display 1.348 + gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source)); 1.349 +} 1.350 + 1.351 +//////////////////////////////////////////////////////////////////////////////// 1.352 +function getInnerMarkup(node, indent) { 1.353 + var str = ''; 1.354 + for (var i = 0; i < node.childNodes.length; i++) { 1.355 + str += getOuterMarkup(node.childNodes.item(i), indent); 1.356 + } 1.357 + return str; 1.358 +} 1.359 + 1.360 +//////////////////////////////////////////////////////////////////////////////// 1.361 +function getOuterMarkup(node, indent) { 1.362 + var newline = ''; 1.363 + var padding = ''; 1.364 + var str = ''; 1.365 + if (node == gTargetNode) { 1.366 + gStartTargetLine = gLineCount; 1.367 + str += '</pre><pre id="target">'; 1.368 + } 1.369 + 1.370 + switch (node.nodeType) { 1.371 + case Node.ELEMENT_NODE: // Element 1.372 + // to avoid the wide gap problem, '\n' is not emitted on the first 1.373 + // line and the lines before & after the <pre id="target">...</pre> 1.374 + if (gLineCount > 0 && 1.375 + gLineCount != gStartTargetLine && 1.376 + gLineCount != gEndTargetLine) { 1.377 + newline = '\n'; 1.378 + } 1.379 + gLineCount++; 1.380 + if (gDebug) { 1.381 + newline += gLineCount; 1.382 + } 1.383 + for (var k = 0; k < indent; k++) { 1.384 + padding += ' '; 1.385 + } 1.386 + str += newline + padding 1.387 + + '<<span class="start-tag">' + node.nodeName + '</span>'; 1.388 + for (var i = 0; i < node.attributes.length; i++) { 1.389 + var attr = node.attributes.item(i); 1.390 + if (!gDebug && attr.nodeName.match(/^[-_]moz/)) { 1.391 + continue; 1.392 + } 1.393 + str += ' <span class="attribute-name">' 1.394 + + attr.nodeName 1.395 + + '</span>=<span class="attribute-value">"' 1.396 + + unicodeTOentity(attr.nodeValue) 1.397 + + '"</span>'; 1.398 + } 1.399 + if (!node.hasChildNodes()) { 1.400 + str += '/>'; 1.401 + } 1.402 + else { 1.403 + str += '>'; 1.404 + var oldLine = gLineCount; 1.405 + str += getInnerMarkup(node, indent + 2); 1.406 + if (oldLine == gLineCount) { 1.407 + newline = ''; 1.408 + padding = ''; 1.409 + } 1.410 + else { 1.411 + newline = (gLineCount == gEndTargetLine) ? '' : '\n'; 1.412 + gLineCount++; 1.413 + if (gDebug) { 1.414 + newline += gLineCount; 1.415 + } 1.416 + } 1.417 + str += newline + padding 1.418 + + '</<span class="end-tag">' + node.nodeName + '</span>>'; 1.419 + } 1.420 + break; 1.421 + case Node.TEXT_NODE: // Text 1.422 + var tmp = node.nodeValue; 1.423 + tmp = tmp.replace(/(\n|\r|\t)+/g, " "); 1.424 + tmp = tmp.replace(/^ +/, ""); 1.425 + tmp = tmp.replace(/ +$/, ""); 1.426 + if (tmp.length != 0) { 1.427 + str += '<span class="text">' + unicodeTOentity(tmp) + '</span>'; 1.428 + } 1.429 + break; 1.430 + default: 1.431 + break; 1.432 + } 1.433 + 1.434 + if (node == gTargetNode) { 1.435 + gEndTargetLine = gLineCount; 1.436 + str += '</pre><pre>'; 1.437 + } 1.438 + return str; 1.439 +} 1.440 + 1.441 +//////////////////////////////////////////////////////////////////////////////// 1.442 +function unicodeTOentity(text) 1.443 +{ 1.444 + const charTable = { 1.445 + '&': '&<span class="entity">amp;</span>', 1.446 + '<': '&<span class="entity">lt;</span>', 1.447 + '>': '&<span class="entity">gt;</span>', 1.448 + '"': '&<span class="entity">quot;</span>' 1.449 + }; 1.450 + 1.451 + function charTableLookup(letter) { 1.452 + return charTable[letter]; 1.453 + } 1.454 + 1.455 + function convertEntity(letter) { 1.456 + try { 1.457 + var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion); 1.458 + var entity = unichar.substring(1); // extract '&' 1.459 + return '&<span class="entity">' + entity + '</span>'; 1.460 + } catch (ex) { 1.461 + return letter; 1.462 + } 1.463 + } 1.464 + 1.465 + if (!gEntityConverter) { 1.466 + try { 1.467 + gEntityConverter = 1.468 + Components.classes["@mozilla.org/intl/entityconverter;1"] 1.469 + .createInstance(Components.interfaces.nsIEntityConverter); 1.470 + } catch(e) { } 1.471 + } 1.472 + 1.473 + const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C; 1.474 + 1.475 + var str = text; 1.476 + 1.477 + // replace chars in our charTable 1.478 + str = str.replace(/[<>&"]/g, charTableLookup); 1.479 + 1.480 + // replace chars > 0x7f via nsIEntityConverter 1.481 + str = str.replace(/[^\0-\u007f]/g, convertEntity); 1.482 + 1.483 + return str; 1.484 +}