Bug 1134585, remove cpow usage from view selection source, r=mconley

This commit is contained in:
Neil Deakin 2015-08-10 09:42:51 -04:00
parent 37caec30be
commit 6c2b4c2577
13 changed files with 584 additions and 570 deletions

View File

@ -1005,27 +1005,8 @@ nsContextMenu.prototype = {
// View Partial Source
viewPartialSource: function(aContext) {
var focusedWindow = document.commandDispatcher.focusedWindow;
if (focusedWindow == window)
focusedWindow = gBrowser.selectedBrowser.contentWindowAsCPOW;
var docCharset = null;
if (focusedWindow)
docCharset = "charset=" + focusedWindow.document.characterSet;
// "View Selection Source" and others such as "View MathML Source"
// are mutually exclusive, with the precedence given to the selection
// when there is one
var reference = null;
if (aContext == "selection")
reference = focusedWindow.getSelection();
else if (aContext == "mathml")
reference = this.target;
else
throw "not reached";
let inTab = Services.prefs.getBoolPref("view_source.tab");
if (inTab) {
let inWindow = !Services.prefs.getBoolPref("view_source.tab");
let openSelectionFn = inWindow ? null : function() {
let tabBrowser = gBrowser;
// In the case of sidebars and chat windows, gBrowser is defined but null,
// because no #content element exists. For these cases, we need to find
@ -1040,22 +1021,11 @@ nsContextMenu.prototype = {
relatedToCurrent: true,
inBackground: false
});
let viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
if (aContext == "selection") {
top.gViewSourceUtils
.viewSourceFromSelectionInBrowser(reference, viewSourceBrowser);
} else {
top.gViewSourceUtils
.viewSourceFromFragmentInBrowser(reference, aContext,
viewSourceBrowser);
}
} else {
// unused (and play nice for fragments generated via XSLT too)
var docUrl = null;
window.openDialog("chrome://global/content/viewPartialSource.xul",
"_blank", "scrollbars,resizable,chrome,dialog=no",
docUrl, docCharset, reference, aContext);
return tabBrowser.getBrowserForTab(tab);
}
let target = aContext == "mathml" ? this.target : null;
top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target, openSelectionFn);
},
// Open new "view source" window with the frame's URL.

View File

@ -161,7 +161,7 @@ this.BrowserTestUtils = {
* @param {tabbrowser} tabbrowser
* The tabbrowser to look for the next new tab in.
* @param {string} url
* A string URL to look for in the new tab.
* A string URL to look for in the new tab. If null, allows any non-blank URL.
*
* @return {Promise}
* @resolves With the {xul:tab} when a tab is opened and its location changes to the given URL.
@ -174,7 +174,8 @@ this.BrowserTestUtils = {
let progressListener = {
onLocationChange(aBrowser) {
if (aBrowser != openEvent.target.linkedBrowser ||
aBrowser.currentURI.spec != url) {
(url && aBrowser.currentURI.spec != url) ||
(!url && aBrowser.currentURI.spec == "about:blank")) {
return;
}

View File

@ -13,18 +13,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
const NS_XHTML = "http://www.w3.org/1999/xhtml";
const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css";
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
// These are markers used to delimit the selection during processing. They
// are removed from the final rendering.
// We use noncharacter Unicode codepoints to minimize the risk of clashing
// with anything that might legitimately be present in the document.
// U+FDD0..FDEF <noncharacters>
const MARK_SELECTION_START = "\uFDD0";
const MARK_SELECTION_END = "\uFDEF";
const FRAME_SCRIPT = "chrome://global/content/viewSource-content.js";
this.EXPORTED_SYMBOLS = ["ViewSourceBrowser"];
@ -158,20 +148,6 @@ ViewSourceBrowser.prototype = {
this.browser.messageManager.sendAsyncMessage(...args);
},
/**
* Getter for the nsIWebNavigation of the view source browser.
*/
get webNav() {
return this.browser.webNavigation;
},
/**
* Getter for whether long lines should be wrapped.
*/
get wrapLongLines() {
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
},
/**
* A getter for the view source string bundle.
*/
@ -223,6 +199,19 @@ ViewSourceBrowser.prototype = {
{ URL, outerWindowID, lineNumber });
},
/**
* Loads a view source selection showing the given view-source url and
* highlight the selection.
*
* @param uri view-source uri to show
* @param drawSelection true to highlight the selection
* @param baseURI base URI of the original document
*/
loadViewSourceFromSelection(URL, drawSelection, baseURI) {
this.sendAsyncMessage("ViewSource:LoadSourceWithSelection",
{ URL, drawSelection, baseURI });
},
/**
* Updates the "remote" attribute of the view source browser. This
* will remove the browser from the DOM, and then re-add it in the
@ -243,351 +232,6 @@ ViewSourceBrowser.prototype = {
}
},
/**
* Load the view source browser from a selection in some document.
*
* @param selection
* A Selection object for the content of interest.
*/
loadViewSourceFromSelection(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
var Node = doc.defaultView.Node;
if (ancestorContainer.nodeType == Node.TEXT_NODE ||
ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
ancestorContainer = ancestorContainer.parentNode;
// for selectAll, let's use the entire document, including <html>...</html>
// @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 = this._getPath(ancestorContainer, startContainer);
var endPath = this._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 <isindex>) that secretly use anynomous content
// for their display appearance.
var canDrawSelection = ancestorContainer.hasChildNodes();
var tmpNode;
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...)
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
// ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
// To get a neat output, the idea here is to remap the end point from:
// 1. ...<tag>]... to ...]<tag>...
// 2. ...]</tag>... to ...</tag>]...
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
// ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
// To get a neat output, the idea here is to remap the start point from:
// 1. ...<tag>[... to ...[<tag>...
// 2. ...[</tag>... to ...</tag>[...
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);
// Tell content to draw a selection after the load below
if (canDrawSelection) {
this.sendAsyncMessage("ViewSource:ScheduleDrawSelection");
}
// 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;
var referrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
this.webNav.loadURIWithOptions((isHTML ?
"view-source:data:text/html;charset=utf-8," :
"view-source:data:application/xml;charset=utf-8,")
+ encodeURIComponent(tmpNode.innerHTML),
loadFlags,
null, referrerPolicy, // referrer
null, null, // postData, headers
Services.io.newURI(doc.baseURI, null, null));
},
/**
* A 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
*/
_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;
},
/**
* Load the view source browser from a fragment of some document, as in
* markups such as MathML where reformatting the output is helpful.
*
* @param aNode
* Some element within the fragment of interest.
* @param aContext
* A string denoting the type of fragment. Currently, "mathml" is the
* only accepted value.
*/
loadViewSourceFromFragment(node, context) {
var Node = node.ownerDocument.defaultView.Node;
this._lineCount = 0;
this._startTargetLine = 0;
this._endTargetLine = 0;
this._targetNode = node;
if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE)
this._targetNode = this._targetNode.parentNode;
// walk up the tree to the top-level element (e.g., <math>, <svg>)
var topTag;
if (context == "mathml")
topTag = "math";
else
throw "not reached";
var topNode = this._targetNode;
while (topNode && topNode.localName != topTag)
topNode = topNode.parentNode;
if (!topNode)
return;
// serialize
var title = this.bundle.GetStringFromName("viewMathMLSourceTitle");
var wrapClass = this.wrapLongLines ? ' class="wrap"' : '';
var source =
'<!DOCTYPE html>'
+ '<html>'
+ '<head><title>' + title + '</title>'
+ '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">'
+ '<style type="text/css">'
+ '#target { border: dashed 1px; background-color: lightyellow; }'
+ '</style>'
+ '</head>'
+ '<body id="viewsource"' + wrapClass
+ ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">'
+ '<pre>'
+ this._getOuterMarkup(topNode, 0)
+ '</pre></body></html>'
; // end
// display
this.browser.loadURI("data:text/html;charset=utf-8," +
encodeURIComponent(source));
},
_getInnerMarkup(node, indent) {
var str = '';
for (var i = 0; i < node.childNodes.length; i++) {
str += this._getOuterMarkup(node.childNodes.item(i), indent);
}
return str;
},
_getOuterMarkup(node, indent) {
var Node = node.ownerDocument.defaultView.Node;
var newline = "";
var padding = "";
var str = "";
if (node == this._targetNode) {
this._startTargetLine = this._lineCount;
str += '</pre><pre id="target">';
}
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 <pre id="target">...</pre>
if (this._lineCount > 0 &&
this._lineCount != this._startTargetLine &&
this._lineCount != this._endTargetLine) {
newline = "\n";
}
this._lineCount++;
for (var k = 0; k < indent; k++) {
padding += " ";
}
str += newline + padding
+ '&lt;<span class="start-tag">' + node.nodeName + '</span>';
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes.item(i);
if (attr.nodeName.match(/^[-_]moz/)) {
continue;
}
str += ' <span class="attribute-name">'
+ attr.nodeName
+ '</span>=<span class="attribute-value">"'
+ this._unicodeToEntity(attr.nodeValue)
+ '"</span>';
}
if (!node.hasChildNodes()) {
str += "/&gt;";
}
else {
str += "&gt;";
var oldLine = this._lineCount;
str += this._getInnerMarkup(node, indent + 2);
if (oldLine == this._lineCount) {
newline = "";
padding = "";
}
else {
newline = (this._lineCount == this._endTargetLine) ? "" : "\n";
this._lineCount++;
}
str += newline + padding
+ '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
}
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 += '<span class="text">' + this._unicodeToEntity(tmp) + '</span>';
}
break;
default:
break;
}
if (node == this._targetNode) {
this._endTargetLine = this._lineCount;
str += '</pre><pre>';
}
return str;
},
_unicodeToEntity(text) {
const charTable = {
'&': '&amp;<span class="entity">amp;</span>',
'<': '&amp;<span class="entity">lt;</span>',
'>': '&amp;<span class="entity">gt;</span>',
'"': '&amp;<span class="entity">quot;</span>'
};
function charTableLookup(letter) {
return charTable[letter];
}
function convertEntity(letter) {
try {
var unichar = this._entityConverter
.ConvertToEntity(letter, entityVersion);
var entity = unichar.substring(1); // extract '&'
return '&amp;<span class="entity">' + entity + '</span>';
} catch (ex) {
return letter;
}
}
if (!this._entityConverter) {
try {
this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"]
.createInstance(Ci.nsIEntityConverter);
} catch(e) { }
}
const entityVersion = Ci.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;
},
/**
* Opens the "Go to line" prompt for a user to hop to a particular line
* of the source code they're viewing. This will keep prompting until the

View File

@ -16,10 +16,7 @@ function onLoadViewPartialSource() {
.setAttribute("checked",
Services.prefs.getBoolPref("view_source.syntax_highlight"));
if (window.arguments[3] == 'selection')
viewSourceChrome.loadViewSourceFromSelection(window.arguments[2]);
else
viewSourceChrome.loadViewSourceFromFragment(window.arguments[2], window.arguments[3]);
let args = window.arguments;
viewSourceChrome.loadViewSourceFromSelection(args.URI, args.drawSelection, args.baseURI);
window.content.focus();
}

View File

@ -44,11 +44,11 @@ let ViewSourceContent = {
messages: [
"ViewSource:LoadSource",
"ViewSource:LoadSourceDeprecated",
"ViewSource:LoadSourceWithSelection",
"ViewSource:GoToLine",
"ViewSource:ToggleWrapping",
"ViewSource:ToggleSyntaxHighlighting",
"ViewSource:SetCharacterSet",
"ViewSource:ScheduleDrawSelection",
],
/**
@ -135,6 +135,9 @@ let ViewSourceContent = {
this.viewSourceDeprecated(data.URL, objects.pageDescriptor, data.lineNumber,
data.forcedCharSet);
break;
case "ViewSource:LoadSourceWithSelection":
this.viewSourceWithSelection(data.URL, data.drawSelection, data.baseURI);
break;
case "ViewSource:GoToLine":
this.goToLine(data.lineNumber);
break;
@ -147,9 +150,6 @@ let ViewSourceContent = {
case "ViewSource:SetCharacterSet":
this.setCharacterSet(data.charset, data.doPageLoad);
break;
case "ViewSource:ScheduleDrawSelection":
this.scheduleDrawSelection();
break;
}
},
@ -759,6 +759,28 @@ let ViewSourceContent = {
sendAsyncMessage("ViewSource:UpdateStatus", { label });
},
/**
* Loads a view source selection showing the given view-source url and
* highlight the selection.
*
* @param uri view-source uri to show
* @param drawSelection true to highlight the selection
* @param baseURI base URI of the original document
*/
viewSourceWithSelection(uri, drawSelection, baseURI)
{
this.needsDrawSelection = drawSelection;
// all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURIWithOptions(uri, loadFlags,
null, referrerPolicy, // referrer
null, null, // postData, headers
Services.io.newURI(baseURI, null, null));
},
/**
* nsISelectionListener
*/
@ -778,15 +800,6 @@ let ViewSourceContent = {
this.updateStatusTask.arm();
},
/**
* Chrome has requested that we draw a selection once the content loads.
* We set a flag, and wait for the load event, where drawSelection() will be
* called to do the real work.
*/
scheduleDrawSelection() {
this.needsDrawSelection = true;
},
/**
* Using special markers left in the serialized source, this helper makes the
* underlying markup of the selected fragment to automatically appear as

View File

@ -305,7 +305,11 @@ ViewSourceChrome.prototype = {
// We're using the modern API, which allows us to view the
// source of documents from out of process browsers.
let args = window.arguments[0];
this.loadViewSource(args);
// viewPartialSource.js will take care of loading the content in partial mode.
if (!args.partial) {
this.loadViewSource(args);
}
},
/**
@ -323,12 +327,6 @@ ViewSourceChrome.prototype = {
// arg[3] - Line number to go to.
// arg[4] - Whether charset was forced by the user
if (aArguments[3] == "selection" ||
aArguments[3] == "mathml") {
// viewPartialSource.js will take care of loading the content.
return;
}
if (aArguments[2]) {
let pageDescriptor = aArguments[2];
if (Cu.isCrossProcessWrapper(pageDescriptor)) {

View File

@ -95,33 +95,42 @@ var gViewSourceUtils = {
* <browser>. This allows for non-window display methods, such as a tab from
* Firefox.
*
* @param aSelection
* A Selection object for the content of interest.
* @param aViewSourceInBrowser
* The browser to display the view source in.
* The browser containing the page to view the source of.
* @param aTarget
* Set to the target node for MathML. Null for other types of elements.
* @param aGetBrowserFn
* If set, a function that will return a browser to open the source in.
* If null, or this function returns null, opens the source in a new window.
*/
viewSourceFromSelectionInBrowser: function(aSelection, aViewSourceInBrowser) {
let viewSourceBrowser = new ViewSourceBrowser(aViewSourceInBrowser);
viewSourceBrowser.loadViewSourceFromSelection(aSelection);
},
viewPartialSourceInBrowser: function(aViewSourceInBrowser, aTarget, aGetBrowserFn) {
let mm = aViewSourceInBrowser.messageManager;
mm.addMessageListener("ViewSource:GetSelectionDone", function gotSelection(message) {
mm.removeMessageListener("ViewSource:GetSelectionDone", gotSelection);
/**
* Displays view source for a MathML fragment from some document in the
* provided <browser>. This allows for non-window display methods, such as a
* tab from Firefox.
*
* @param aNode
* Some element within the fragment of interest.
* @param aContext
* A string denoting the type of fragment. Currently, "mathml" is the
* only accepted value.
* @param aViewSourceInBrowser
* The browser to display the view source in.
*/
viewSourceFromFragmentInBrowser: function(aNode, aContext,
aViewSourceInBrowser) {
let viewSourceBrowser = new ViewSourceBrowser(aViewSourceInBrowser);
viewSourceBrowser.loadViewSourceFromFragment(aNode, aContext);
if (!message.data)
return;
let browserToOpenIn = aGetBrowserFn ? aGetBrowserFn() : null;
if (browserToOpenIn) {
let viewSourceBrowser = new ViewSourceBrowser(browserToOpenIn);
viewSourceBrowser.loadViewSourceFromSelection(message.data.uri, message.data.drawSelection,
message.data.baseURI);
}
else {
let docUrl = null;
window.openDialog("chrome://global/content/viewPartialSource.xul",
"_blank", "scrollbars,resizable,chrome,dialog=no",
{
URI: message.data.uri,
drawSelection: message.data.drawSelection,
baseURI: message.data.baseURI,
partial: true,
});
}
});
mm.sendAsyncMessage("ViewSource:GetSelection", { }, { target: aTarget });
},
// Opens the interval view source viewer

View File

@ -6,7 +6,6 @@ support-files = head.js
[browser_bug699356.js]
[browser_bug713810.js]
[browser_contextmenu.js]
skip-if = e10s # occasional timeouts with dead CPOW in console
[browser_gotoline.js]
[browser_viewsourceprefs.js]
skip-if = e10s # Bug ?????? - obscure failure (Syntax highlighting off - Got true, expected false)

View File

@ -1,14 +1,12 @@
const source = "http://example.com/browser/toolkit/components/viewsource/test/browser/file_bug464222.html";
function test() {
waitForExplicitFinish();
testSelection();
}
add_task(function *() {
let viewSourceTab = yield* openDocumentSelect(source, "a");
function testSelection() {
openDocumentSelect(source, "a", function(aWindow) {
let aTags = aWindow.gBrowser.contentDocument.querySelectorAll("a[href]");
is(aTags[0].href, "view-source:" + source, "Relative links broken?");
closeViewSourceWindow(aWindow, finish);
let href = yield ContentTask.spawn(viewSourceTab.linkedBrowser, { }, function* () {
return content.document.querySelectorAll("a[href]")[0].href;
});
}
is(href, "view-source:" + source, "Relative links broken?");
gBrowser.removeTab(viewSourceTab);
});

View File

@ -2,27 +2,22 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
var source = '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>This is a paragraph.</p></body></html>';
const source = '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>This is a paragraph.</p></body></html>';
function test() {
waitForExplicitFinish();
testHTML();
}
function testHTML() {
openDocumentSelect("data:text/html," + source, "p", function(aWindow) {
is(aWindow.gBrowser.contentDocument.body.textContent,
"<p>This is a paragraph.</p>",
"Correct source for text/html");
closeViewSourceWindow(aWindow, testXHTML);
add_task(function *() {
let viewSourceTab = yield* openDocumentSelect("data:text/html," + source, "p");
let text = yield ContentTask.spawn(viewSourceTab.linkedBrowser, { }, function* () {
return content.document.body.textContent;
});
}
is(text, "<p>This is a paragraph.</p>", "Correct source for text/html");
gBrowser.removeTab(viewSourceTab);
function testXHTML() {
openDocumentSelect("data:application/xhtml+xml," + source, "p", function(aWindow) {
is(aWindow.gBrowser.contentDocument.body.textContent,
'<p xmlns="http://www.w3.org/1999/xhtml">This is a paragraph.</p>',
"Correct source for application/xhtml+xml");
closeViewSourceWindow(aWindow, finish);
viewSourceTab = yield* openDocumentSelect("data:application/xhtml+xml," + source, "p");
text = yield ContentTask.spawn(viewSourceTab.linkedBrowser, { }, function* () {
return content.document.body.textContent;
});
}
is(text, '<p xmlns="http://www.w3.org/1999/xhtml">This is a paragraph.</p>',
"Correct source for application/xhtml+xml");
gBrowser.removeTab(viewSourceTab);
});

View File

@ -6,78 +6,85 @@ let source = "data:text/html,text<link%20href='http://example.com/'%20/>more%20t
let gViewSourceWindow, gContextMenu, gCopyLinkMenuItem, gCopyEmailMenuItem;
let expectedData = [];
let currentTest = 0;
let partialTestRunning = false;
function test() {
waitForExplicitFinish();
openViewSourceWindow(source, onViewSourceWindowOpen);
}
add_task(function *() {
let newWindow = yield loadViewSourceWindow(source);
yield SimpleTest.promiseFocus(newWindow);
function onViewSourceWindowOpen(aWindow) {
yield* onViewSourceWindowOpen(newWindow, false);
let contextMenu = gViewSourceWindow.document.getElementById("viewSourceContextMenu");
for (let test of expectedData) {
yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
}
yield new Promise(resolve => {
closeViewSourceWindow(newWindow, resolve);
});
expectedData = [];
let newTab = yield openDocumentSelect(source, "body");
yield* onViewSourceWindowOpen(window, true);
contextMenu = document.getElementById("contentAreaContextMenu");
// Prepend view-source to this one as it opens in a tab.
expectedData[0][3] = "view-source:" + expectedData[0][3];
for (let test of expectedData) {
yield* checkMenuItems(contextMenu, true, test[0], test[1], test[2], test[3]);
}
gBrowser.removeTab(newTab);
});
function* onViewSourceWindowOpen(aWindow, aIsTab) {
gViewSourceWindow = aWindow;
gContextMenu = aWindow.document.getElementById("viewSourceContextMenu");
gCopyLinkMenuItem = aWindow.document.getElementById("context-copyLink");
gCopyEmailMenuItem = aWindow.document.getElementById("context-copyEmail");
gCopyLinkMenuItem = aWindow.document.getElementById(aIsTab ? "context-copylink" : "context-copyLink");
gCopyEmailMenuItem = aWindow.document.getElementById(aIsTab ? "context-copyemail" : "context-copyEmail");
let aTags = aWindow.gBrowser.contentDocument.querySelectorAll("a[href]");
is(aTags[0].href, "view-source:http://example.com/", "Link has correct href");
is(aTags[1].href, "mailto:abc@def.ghi", "Link has correct href");
let spanTag = aWindow.gBrowser.contentDocument.querySelector("span");
let browser = aIsTab ? gBrowser.selectedBrowser : gViewSourceWindow.gBrowser;
let items = yield ContentTask.spawn(browser, { }, function* (arg) {
let tags = content.document.querySelectorAll("a[href]");
return [tags[0].href, tags[1].href];
});
expectedData.push([aTags[0], true, false, "http://example.com/"]);
expectedData.push([aTags[1], false, true, "abc@def.ghi"]);
expectedData.push([spanTag, false, false, null]);
is(items[0], "view-source:http://example.com/", "Link has correct href");
is(items[1], "mailto:abc@def.ghi", "Link has correct href");
waitForFocus(runNextTest, aWindow);
expectedData.push(["a[href]", true, false, "http://example.com/"]);
expectedData.push(["a[href^=mailto]", false, true, "abc@def.ghi"]);
expectedData.push(["span", false, false, null]);
}
function runNextTest() {
if (currentTest == expectedData.length) {
closeViewSourceWindow(gViewSourceWindow, function() {
if (partialTestRunning) {
finish();
return;
}
partialTestRunning = true;
currentTest = 0;
expectedData = [];
openDocumentSelect(source, "body", onViewSourceWindowOpen);
});
return;
}
let test = expectedData[currentTest++];
checkMenuItems(test[0], test[1], test[2], test[3]);
}
function checkMenuItems(contextMenu, isTab, selector, copyLinkExpected, copyEmailExpected, expectedClipboardContent) {
function checkMenuItems(popupNode, copyLinkExpected, copyEmailExpected, expectedClipboardContent) {
popupNode.scrollIntoView();
let browser = isTab ? gBrowser.selectedBrowser : gViewSourceWindow.gBrowser;
yield ContentTask.spawn(browser, { selector: selector }, function* (arg) {
content.document.querySelector(arg.selector).scrollIntoView();
});
let cachedEvent = null;
let mouseFn = function(event) {
cachedEvent = event;
};
gViewSourceWindow.gBrowser.contentWindow.addEventListener("mousedown", mouseFn, false);
EventUtils.synthesizeMouseAtCenter(popupNode, { type: "contextmenu", button: 2 }, gViewSourceWindow.gBrowser.contentWindow);
gViewSourceWindow.gBrowser.contentWindow.removeEventListener("mousedown", mouseFn, false);
gContextMenu.openPopup(popupNode, "after_start", 0, 0, false, false, cachedEvent);
let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
yield BrowserTestUtils.synthesizeMouseAtCenter(selector,
{ type: "contextmenu", button: 2}, browser);
yield popupShownPromise;
is(gCopyLinkMenuItem.hidden, !copyLinkExpected, "Copy link menuitem is " + (copyLinkExpected ? "not hidden" : "hidden"));
is(gCopyEmailMenuItem.hidden, !copyEmailExpected, "Copy email menuitem is " + (copyEmailExpected ? "not hidden" : "hidden"));
if (!copyLinkExpected && !copyEmailExpected) {
runNextTest();
return;
if (copyLinkExpected || copyEmailExpected) {
yield new Promise((resolve, reject) => {
waitForClipboard(expectedClipboardContent, function() {
if (copyLinkExpected)
gCopyLinkMenuItem.click();
else
gCopyEmailMenuItem.click();
}, resolve, reject);
});
}
waitForClipboard(expectedClipboardContent, function() {
if (copyLinkExpected)
gCopyLinkMenuItem.doCommand();
else
gCopyEmailMenuItem.doCommand();
gContextMenu.hidePopup();
}, runNextTest, runNextTest);
let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
contextMenu.hidePopup();
yield popupHiddenPromise;
}

View File

@ -40,17 +40,29 @@ function testViewSourceWindow(aURI, aTestCallback, aCloseCallback) {
});
}
function openViewPartialSourceWindow(aReference, aCallback) {
let viewSourceWindow = openDialog("chrome://global/content/viewPartialSource.xul",
null, null, null, null, aReference, "selection");
viewSourceWindow.addEventListener("pageshow", function pageShowHandler(event) {
// Wait for the inner window to load, not viewSourceWindow.
if (/^view-source:/.test(event.target.location)) {
info("View source window opened: " + event.target.location);
viewSourceWindow.removeEventListener("pageshow", pageShowHandler, false);
aCallback(viewSourceWindow);
}
}, false);
/**
* Opens a view source tab for a selection (View Selection Source) within the
* currently selected browser in gBrowser.
*
* @param aCSSSelector - used to specify a node within the selection to
* view the source of. It is expected that this node is
* within an existing selection.
* @returns the new tab which shows the source.
*/
function* openViewPartialSourceWindow(aCSSSelector) {
var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
yield BrowserTestUtils.synthesizeMouseAtCenter(aCSSSelector,
{ type: "contextmenu", button: 2}, gBrowser.selectedBrowser);
yield popupShownPromise;
let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
EventUtils.synthesizeMouseAtCenter(document.getElementById("context-viewpartialsource-selection"), {});
yield popupHiddenPromise;
return (yield newTabPromise);
}
registerCleanupFunction(function() {
@ -60,26 +72,38 @@ registerCleanupFunction(function() {
windows.getNext().close();
});
function openDocument(aURI, aCallback) {
let tab = gBrowser.addTab(aURI);
let browser = tab.linkedBrowser;
browser.addEventListener("DOMContentLoaded", function pageLoadedListener() {
browser.removeEventListener("DOMContentLoaded", pageLoadedListener, false);
aCallback(tab);
}, false);
/**
* Open a new document in a new tab, select part of it, and view the source of
* that selection. The document is not closed afterwards.
*
* @param aURI - url to load
* @param aCSSSelector - used to specify a node to select. All of this node's
* children will be selected.
* @returns the new tab which shows the source.
*/
function* openDocumentSelect(aURI, aCSSSelector) {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, aURI);
registerCleanupFunction(function() {
gBrowser.removeTab(tab);
});
}
function openDocumentSelect(aURI, aCSSSelector, aCallback) {
openDocument(aURI, function(aTab) {
let element = aTab.linkedBrowser.contentDocument.querySelector(aCSSSelector);
let selection = aTab.linkedBrowser.contentWindow.getSelection();
selection.selectAllChildren(element);
openViewPartialSourceWindow(selection, aCallback);
yield ContentTask.spawn(gBrowser.selectedBrowser, { selector: aCSSSelector }, function* (arg) {
let element = content.document.querySelector(arg.selector);
content.getSelection().selectAllChildren(element);
});
let newtab = yield openViewPartialSourceWindow(aCSSSelector);
// Wait until the source has been loaded.
yield new Promise(resolve => {
let mm = newtab.linkedBrowser.messageManager;
mm.addMessageListener("ViewSource:SourceLoaded", function selectionDrawn() {
mm.removeMessageListener("ViewSource:SourceLoaded", selectionDrawn);
setTimeout(resolve, 0);
});
});
return newtab;
}
function waitForPrefChange(pref) {

View File

@ -724,3 +724,362 @@ let AudioPlaybackListener = {
},
};
AudioPlaybackListener.init();
let ViewSelectionSource = {
init: function () {
addMessageListener("ViewSource:GetSelection", this);
},
receiveMessage: function(message) {
if (message.name == "ViewSource:GetSelection") {
let selectionDetails;
try {
selectionDetails = message.objects.target ? this.getMathMLSelection(message.objects.target)
: this.getSelection();
} finally {
sendAsyncMessage("ViewSource:GetSelectionDone", selectionDetails);
}
}
},
/**
* A 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
*/
getPath: function(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;
},
getSelection: function () {
// These are markers used to delimit the selection during processing. They
// are removed from the final rendering.
// We use noncharacter Unicode codepoints to minimize the risk of clashing
// with anything that might legitimately be present in the document.
// U+FDD0..FDEF <noncharacters>
const MARK_SELECTION_START = "\uFDD0";
const MARK_SELECTION_END = "\uFDEF";
var focusedWindow = Services.focus.focusedWindow || content;
var selection = focusedWindow.getSelection();
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
var Node = doc.defaultView.Node;
if (ancestorContainer.nodeType == Node.TEXT_NODE ||
ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
ancestorContainer = ancestorContainer.parentNode;
// for selectAll, let's use the entire document, including <html>...</html>
// @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 = this.getPath(ancestorContainer, startContainer);
var endPath = this.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 <isindex>) that secretly use anynomous content
// for their display appearance.
var canDrawSelection = ancestorContainer.hasChildNodes();
var tmpNode;
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...)
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
// ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
// To get a neat output, the idea here is to remap the end point from:
// 1. ...<tag>]... to ...]<tag>...
// 2. ...]</tag>... to ...</tag>]...
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
// ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
// To get a neat output, the idea here is to remap the start point from:
// 1. ...<tag>[... to ...[<tag>...
// 2. ...[</tag>... to ...</tag>[...
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("http://www.w3.org/1999/xhtml", "div");
tmpNode.appendChild(ancestorContainer);
return { uri: (isHTML ? "view-source:data:text/html;charset=utf-8," :
"view-source:data:application/xml;charset=utf-8,")
+ encodeURIComponent(tmpNode.innerHTML),
drawSelection: canDrawSelection,
baseURI: doc.baseURI };
},
/**
* Reformat the source of a MathML node to highlight the node that was targetted.
*
* @param node
* Some element within the fragment of interest.
*/
getMathMLSelection: function(node) {
var Node = node.ownerDocument.defaultView.Node;
this._lineCount = 0;
this._startTargetLine = 0;
this._endTargetLine = 0;
this._targetNode = node;
if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE)
this._targetNode = this._targetNode.parentNode;
// walk up the tree to the top-level element (e.g., <math>, <svg>)
var topTag = "math";
var topNode = this._targetNode;
while (topNode && topNode.localName != topTag) {
topNode = topNode.parentNode;
}
if (!topNode)
return;
// serialize
const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css";
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
let bundle = Services.strings.createBundle(BUNDLE_URL);
var title = bundle.GetStringFromName("viewMathMLSourceTitle");
var wrapClass = this.wrapLongLines ? ' class="wrap"' : '';
var source =
'<!DOCTYPE html>'
+ '<html>'
+ '<head><title>' + title + '</title>'
+ '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">'
+ '<style type="text/css">'
+ '#target { border: dashed 1px; background-color: lightyellow; }'
+ '</style>'
+ '</head>'
+ '<body id="viewsource"' + wrapClass
+ ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">'
+ '<pre>'
+ this.getOuterMarkup(topNode, 0)
+ '</pre></body></html>'
; // end
return { uri: "data:text/html;charset=utf-8," + encodeURIComponent(source),
drawSelection: false, baseURI: node.ownerDocument.baseURI };
},
get wrapLongLines() {
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
},
getInnerMarkup: function(node, indent) {
var str = '';
for (var i = 0; i < node.childNodes.length; i++) {
str += this.getOuterMarkup(node.childNodes.item(i), indent);
}
return str;
},
getOuterMarkup: function(node, indent) {
var Node = node.ownerDocument.defaultView.Node;
var newline = "";
var padding = "";
var str = "";
if (node == this._targetNode) {
this._startTargetLine = this._lineCount;
str += '</pre><pre id="target">';
}
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 <pre id="target">...</pre>
if (this._lineCount > 0 &&
this._lineCount != this._startTargetLine &&
this._lineCount != this._endTargetLine) {
newline = "\n";
}
this._lineCount++;
for (var k = 0; k < indent; k++) {
padding += " ";
}
str += newline + padding
+ '&lt;<span class="start-tag">' + node.nodeName + '</span>';
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes.item(i);
if (attr.nodeName.match(/^[-_]moz/)) {
continue;
}
str += ' <span class="attribute-name">'
+ attr.nodeName
+ '</span>=<span class="attribute-value">"'
+ this.unicodeToEntity(attr.nodeValue)
+ '"</span>';
}
if (!node.hasChildNodes()) {
str += "/&gt;";
}
else {
str += "&gt;";
var oldLine = this._lineCount;
str += this.getInnerMarkup(node, indent + 2);
if (oldLine == this._lineCount) {
newline = "";
padding = "";
}
else {
newline = (this._lineCount == this._endTargetLine) ? "" : "\n";
this._lineCount++;
}
str += newline + padding
+ '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
}
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 += '<span class="text">' + this.unicodeToEntity(tmp) + '</span>';
}
break;
default:
break;
}
if (node == this._targetNode) {
this._endTargetLine = this._lineCount;
str += '</pre><pre>';
}
return str;
},
unicodeToEntity: function(text) {
const charTable = {
'&': '&amp;<span class="entity">amp;</span>',
'<': '&amp;<span class="entity">lt;</span>',
'>': '&amp;<span class="entity">gt;</span>',
'"': '&amp;<span class="entity">quot;</span>'
};
function charTableLookup(letter) {
return charTable[letter];
}
function convertEntity(letter) {
try {
var unichar = this._entityConverter
.ConvertToEntity(letter, entityVersion);
var entity = unichar.substring(1); // extract '&'
return '&amp;<span class="entity">' + entity + '</span>';
} catch (ex) {
return letter;
}
}
if (!this._entityConverter) {
try {
this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"]
.createInstance(Ci.nsIEntityConverter);
} catch(e) { }
}
const entityVersion = Ci.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;
}
};
ViewSelectionSource.init();