mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 00:08:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
978
toolkit/components/viewsource/content/viewSource-content.js
Normal file
978
toolkit/components/viewsource/content/viewSource-content.js
Normal file
|
|
@ -0,0 +1,978 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
|
||||
"resource://gre/modules/BrowserUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
|
||||
"resource://gre/modules/DeferredTask.jsm");
|
||||
|
||||
const NS_XHTML = "http://www.w3.org/1999/xhtml";
|
||||
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";
|
||||
|
||||
var global = this;
|
||||
|
||||
/**
|
||||
* ViewSourceContent should be loaded in the <xul:browser> of the
|
||||
* view source window, and initialized as soon as it has loaded.
|
||||
*/
|
||||
var ViewSourceContent = {
|
||||
/**
|
||||
* We'll act as an nsISelectionListener as well so that we can send
|
||||
* updates to the view source window's status bar.
|
||||
*/
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISelectionListener]),
|
||||
|
||||
/**
|
||||
* These are the messages that ViewSourceContent is prepared to listen
|
||||
* for. If you need ViewSourceContent to handle more messages, add them
|
||||
* here.
|
||||
*/
|
||||
messages: [
|
||||
"ViewSource:LoadSource",
|
||||
"ViewSource:LoadSourceDeprecated",
|
||||
"ViewSource:LoadSourceWithSelection",
|
||||
"ViewSource:GoToLine",
|
||||
"ViewSource:ToggleWrapping",
|
||||
"ViewSource:ToggleSyntaxHighlighting",
|
||||
"ViewSource:SetCharacterSet",
|
||||
],
|
||||
|
||||
/**
|
||||
* When showing selection source, chrome will construct a page fragment to
|
||||
* show, and then instruct content to draw a selection after load. This is
|
||||
* set true when there is a pending request to draw selection.
|
||||
*/
|
||||
needsDrawSelection: false,
|
||||
|
||||
/**
|
||||
* ViewSourceContent is attached as an nsISelectionListener on pageshow,
|
||||
* and removed on pagehide. When the initial about:blank is transitioned
|
||||
* away from, a pagehide is fired without us having attached ourselves
|
||||
* first. We use this boolean to keep track of whether or not we're
|
||||
* attached, so we don't attempt to remove our listener when it's not
|
||||
* yet there (which throws).
|
||||
*/
|
||||
selectionListenerAttached: false,
|
||||
|
||||
get isViewSource() {
|
||||
let uri = content.document.documentURI;
|
||||
return uri.startsWith("view-source:") ||
|
||||
(uri.startsWith("data:") && uri.includes("MathML"));
|
||||
},
|
||||
|
||||
get isAboutBlank() {
|
||||
let uri = content.document.documentURI;
|
||||
return uri == "about:blank";
|
||||
},
|
||||
|
||||
/**
|
||||
* This should be called as soon as this frame script has loaded.
|
||||
*/
|
||||
init() {
|
||||
this.messages.forEach((msgName) => {
|
||||
addMessageListener(msgName, this);
|
||||
});
|
||||
|
||||
addEventListener("pagehide", this, true);
|
||||
addEventListener("pageshow", this, true);
|
||||
addEventListener("click", this);
|
||||
addEventListener("unload", this);
|
||||
Services.els.addSystemEventListener(global, "contextmenu", this, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* This should be called when the frame script is being unloaded,
|
||||
* and the browser is tearing down.
|
||||
*/
|
||||
uninit() {
|
||||
this.messages.forEach((msgName) => {
|
||||
removeMessageListener(msgName, this);
|
||||
});
|
||||
|
||||
removeEventListener("pagehide", this, true);
|
||||
removeEventListener("pageshow", this, true);
|
||||
removeEventListener("click", this);
|
||||
removeEventListener("unload", this);
|
||||
|
||||
Services.els.removeSystemEventListener(global, "contextmenu", this, false);
|
||||
|
||||
// Cancel any pending toolbar updates.
|
||||
if (this.updateStatusTask) {
|
||||
this.updateStatusTask.disarm();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Anything added to the messages array will get handled here, and should
|
||||
* get dispatched to a specific function for the message name.
|
||||
*/
|
||||
receiveMessage(msg) {
|
||||
if (!this.isViewSource && !this.isAboutBlank) {
|
||||
return;
|
||||
}
|
||||
let data = msg.data;
|
||||
let objects = msg.objects;
|
||||
switch (msg.name) {
|
||||
case "ViewSource:LoadSource":
|
||||
this.viewSource(data.URL, data.outerWindowID, data.lineNumber,
|
||||
data.shouldWrap);
|
||||
break;
|
||||
case "ViewSource:LoadSourceDeprecated":
|
||||
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;
|
||||
case "ViewSource:ToggleWrapping":
|
||||
this.toggleWrapping();
|
||||
break;
|
||||
case "ViewSource:ToggleSyntaxHighlighting":
|
||||
this.toggleSyntaxHighlighting();
|
||||
break;
|
||||
case "ViewSource:SetCharacterSet":
|
||||
this.setCharacterSet(data.charset, data.doPageLoad);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Any events should get handled here, and should get dispatched to
|
||||
* a specific function for the event type.
|
||||
*/
|
||||
handleEvent(event) {
|
||||
if (!this.isViewSource) {
|
||||
return;
|
||||
}
|
||||
switch (event.type) {
|
||||
case "pagehide":
|
||||
this.onPageHide(event);
|
||||
break;
|
||||
case "pageshow":
|
||||
this.onPageShow(event);
|
||||
break;
|
||||
case "click":
|
||||
this.onClick(event);
|
||||
break;
|
||||
case "unload":
|
||||
this.uninit();
|
||||
break;
|
||||
case "contextmenu":
|
||||
this.onContextMenu(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A getter for the view source string bundle.
|
||||
*/
|
||||
get bundle() {
|
||||
delete this.bundle;
|
||||
this.bundle = Services.strings.createBundle(BUNDLE_URL);
|
||||
return this.bundle;
|
||||
},
|
||||
|
||||
/**
|
||||
* A shortcut to the nsISelectionController for the content.
|
||||
*/
|
||||
get selectionController() {
|
||||
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsISelectionDisplay)
|
||||
.QueryInterface(Ci.nsISelectionController);
|
||||
},
|
||||
|
||||
/**
|
||||
* A shortcut to the nsIWebBrowserFind for the content.
|
||||
*/
|
||||
get webBrowserFind() {
|
||||
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebBrowserFind);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the parent sends a message to view some source code.
|
||||
*
|
||||
* @param URL (required)
|
||||
* The URL string of the source to be shown.
|
||||
* @param outerWindowID (optional)
|
||||
* The outerWindowID of the content window that has hosted
|
||||
* the document, in case we want to retrieve it from the network
|
||||
* cache.
|
||||
* @param lineNumber (optional)
|
||||
* The line number to focus as soon as the source has finished
|
||||
* loading.
|
||||
*/
|
||||
viewSource(URL, outerWindowID, lineNumber) {
|
||||
let pageDescriptor, forcedCharSet;
|
||||
|
||||
if (outerWindowID) {
|
||||
let contentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
|
||||
let requestor = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
|
||||
|
||||
try {
|
||||
let otherWebNav = requestor.getInterface(Ci.nsIWebNavigation);
|
||||
pageDescriptor = otherWebNav.QueryInterface(Ci.nsIWebPageDescriptor)
|
||||
.currentDescriptor;
|
||||
} catch (e) {
|
||||
// We couldn't get the page descriptor, so we'll probably end up re-retrieving
|
||||
// this document off of the network.
|
||||
}
|
||||
|
||||
let utils = requestor.getInterface(Ci.nsIDOMWindowUtils);
|
||||
let doc = contentWindow.document;
|
||||
forcedCharSet = utils.docCharsetIsForced ? doc.characterSet
|
||||
: null;
|
||||
}
|
||||
|
||||
this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the parent is using the deprecated API for viewSource.xul.
|
||||
* This function will throw if it's called on a remote browser.
|
||||
*
|
||||
* @param URL (required)
|
||||
* The URL string of the source to be shown.
|
||||
* @param pageDescriptor (optional)
|
||||
* The currentDescriptor off of an nsIWebPageDescriptor, in the
|
||||
* event that the caller wants to try to load the source out of
|
||||
* the network cache.
|
||||
* @param lineNumber (optional)
|
||||
* The line number to focus as soon as the source has finished
|
||||
* loading.
|
||||
* @param forcedCharSet (optional)
|
||||
* The document character set to use instead of the default one.
|
||||
*/
|
||||
viewSourceDeprecated(URL, pageDescriptor, lineNumber, forcedCharSet) {
|
||||
// This should not be called if this frame script is running
|
||||
// in a content process!
|
||||
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
||||
throw new Error("ViewSource deprecated API should not be used with " +
|
||||
"remote browsers.");
|
||||
}
|
||||
|
||||
this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
|
||||
},
|
||||
|
||||
/**
|
||||
* Common utility function used by both the current and deprecated APIs
|
||||
* for loading source.
|
||||
*
|
||||
* @param URL (required)
|
||||
* The URL string of the source to be shown.
|
||||
* @param pageDescriptor (optional)
|
||||
* The currentDescriptor off of an nsIWebPageDescriptor, in the
|
||||
* event that the caller wants to try to load the source out of
|
||||
* the network cache.
|
||||
* @param lineNumber (optional)
|
||||
* The line number to focus as soon as the source has finished
|
||||
* loading.
|
||||
* @param forcedCharSet (optional)
|
||||
* The document character set to use instead of the default one.
|
||||
*/
|
||||
loadSource(URL, pageDescriptor, lineNumber, forcedCharSet) {
|
||||
const viewSrcURL = "view-source:" + URL;
|
||||
|
||||
if (forcedCharSet) {
|
||||
try {
|
||||
docShell.charset = forcedCharSet;
|
||||
} catch (e) { /* invalid charset */ }
|
||||
}
|
||||
|
||||
if (lineNumber && lineNumber > 0) {
|
||||
let doneLoading = (event) => {
|
||||
// Ignore possible initial load of about:blank
|
||||
if (this.isAboutBlank ||
|
||||
!content.document.body) {
|
||||
return;
|
||||
}
|
||||
this.goToLine(lineNumber);
|
||||
removeEventListener("pageshow", doneLoading);
|
||||
};
|
||||
|
||||
addEventListener("pageshow", doneLoading);
|
||||
}
|
||||
|
||||
if (!pageDescriptor) {
|
||||
this.loadSourceFromURL(viewSrcURL);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
|
||||
pageLoader.loadPage(pageDescriptor,
|
||||
Ci.nsIWebPageDescriptor.DISPLAY_AS_SOURCE);
|
||||
} catch (e) {
|
||||
// We were not able to load the source from the network cache.
|
||||
this.loadSourceFromURL(viewSrcURL);
|
||||
return;
|
||||
}
|
||||
|
||||
let shEntrySource = pageDescriptor.QueryInterface(Ci.nsISHEntry);
|
||||
let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]
|
||||
.createInstance(Ci.nsISHEntry);
|
||||
shEntry.setURI(BrowserUtils.makeURI(viewSrcURL, null, null));
|
||||
shEntry.setTitle(viewSrcURL);
|
||||
shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
|
||||
shEntry.cacheKey = shEntrySource.cacheKey;
|
||||
docShell.QueryInterface(Ci.nsIWebNavigation)
|
||||
.sessionHistory
|
||||
.QueryInterface(Ci.nsISHistoryInternal)
|
||||
.addEntry(shEntry, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load some URL in the browser.
|
||||
*
|
||||
* @param URL
|
||||
* The URL string to load.
|
||||
*/
|
||||
loadSourceFromURL(URL) {
|
||||
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.loadURI(URL, loadFlags, null, null, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* This handler is for click events from:
|
||||
* * error page content, which can show up if the user attempts to view the
|
||||
* source of an attack page.
|
||||
* * in-page context menu actions
|
||||
*/
|
||||
onClick(event) {
|
||||
let target = event.originalTarget;
|
||||
// Check for content menu actions
|
||||
if (target.id) {
|
||||
this.contextMenuItems.forEach(itemSpec => {
|
||||
if (itemSpec.id !== target.id) {
|
||||
return;
|
||||
}
|
||||
itemSpec.handler.call(this, event);
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// Don't trust synthetic events
|
||||
if (!event.isTrusted || event.target.localName != "button")
|
||||
return;
|
||||
|
||||
let errorDoc = target.ownerDocument;
|
||||
|
||||
if (/^about:blocked/.test(errorDoc.documentURI)) {
|
||||
// The event came from a button on a malware/phishing block page
|
||||
|
||||
if (target == errorDoc.getElementById("getMeOutButton")) {
|
||||
// Instead of loading some safe page, just close the window
|
||||
sendAsyncMessage("ViewSource:Close");
|
||||
} else if (target == errorDoc.getElementById("reportButton")) {
|
||||
// This is the "Why is this site blocked" button. We redirect
|
||||
// to the generic page describing phishing/malware protection.
|
||||
let URL = Services.urlFormatter.formatURLPref("app.support.baseURL");
|
||||
sendAsyncMessage("ViewSource:OpenURL", { URL })
|
||||
} else if (target == errorDoc.getElementById("ignoreWarningButton")) {
|
||||
// Allow users to override and continue through to the site
|
||||
docShell.QueryInterface(Ci.nsIWebNavigation)
|
||||
.loadURIWithOptions(content.location.href,
|
||||
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
|
||||
null, Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
|
||||
null, null, null);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for the pageshow event.
|
||||
*
|
||||
* @param event
|
||||
* The pageshow event being handled.
|
||||
*/
|
||||
onPageShow(event) {
|
||||
let selection = content.getSelection();
|
||||
if (selection) {
|
||||
selection.QueryInterface(Ci.nsISelectionPrivate)
|
||||
.addSelectionListener(this);
|
||||
this.selectionListenerAttached = true;
|
||||
}
|
||||
content.focus();
|
||||
|
||||
// If we need to draw the selection, wait until an actual view source page
|
||||
// has loaded, instead of about:blank.
|
||||
if (this.needsDrawSelection &&
|
||||
content.document.documentURI.startsWith("view-source:")) {
|
||||
this.needsDrawSelection = false;
|
||||
this.drawSelection();
|
||||
}
|
||||
|
||||
if (content.document.body) {
|
||||
this.injectContextMenu();
|
||||
}
|
||||
|
||||
sendAsyncMessage("ViewSource:SourceLoaded");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for the pagehide event.
|
||||
*
|
||||
* @param event
|
||||
* The pagehide event being handled.
|
||||
*/
|
||||
onPageHide(event) {
|
||||
// The initial about:blank will fire pagehide before we
|
||||
// ever set a selectionListener, so we have a boolean around
|
||||
// to keep track of when the listener is attached.
|
||||
if (this.selectionListenerAttached) {
|
||||
content.getSelection()
|
||||
.QueryInterface(Ci.nsISelectionPrivate)
|
||||
.removeSelectionListener(this);
|
||||
this.selectionListenerAttached = false;
|
||||
}
|
||||
sendAsyncMessage("ViewSource:SourceUnloaded");
|
||||
},
|
||||
|
||||
onContextMenu(event) {
|
||||
let addonInfo = {};
|
||||
let subject = {
|
||||
event: event,
|
||||
addonInfo: addonInfo,
|
||||
};
|
||||
|
||||
subject.wrappedJSObject = subject;
|
||||
Services.obs.notifyObservers(subject, "content-contextmenu", null);
|
||||
|
||||
let node = event.target;
|
||||
|
||||
let result = {
|
||||
isEmail: false,
|
||||
isLink: false,
|
||||
href: "",
|
||||
// We have to pass these in the event that we're running in
|
||||
// a remote browser, so that ViewSourceChrome knows where to
|
||||
// open the context menu.
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
};
|
||||
|
||||
if (node && node.localName == "a") {
|
||||
result.isLink = node.href.startsWith("view-source:");
|
||||
result.isEmail = node.href.startsWith("mailto:");
|
||||
result.href = node.href.substring(node.href.indexOf(":") + 1);
|
||||
}
|
||||
|
||||
sendSyncMessage("ViewSource:ContextMenuOpening", result);
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempts to go to a particular line in the source code being
|
||||
* shown. If it succeeds in finding the line, it will fire a
|
||||
* "ViewSource:GoToLine:Success" message, passing up an object
|
||||
* with the lineNumber we just went to. If it cannot find the line,
|
||||
* it will fire a "ViewSource:GoToLine:Failed" message.
|
||||
*
|
||||
* @param lineNumber
|
||||
* The line number to attempt to go to.
|
||||
*/
|
||||
goToLine(lineNumber) {
|
||||
let body = content.document.body;
|
||||
|
||||
// The source document is made up of a number of pre elements with
|
||||
// id attributes in the format <pre id="line123">, meaning that
|
||||
// the first line in the pre element is number 123.
|
||||
// Do binary search to find the pre element containing the line.
|
||||
// However, in the plain text case, we have only one pre without an
|
||||
// attribute, so assume it begins on line 1.
|
||||
let pre;
|
||||
for (let lbound = 0, ubound = body.childNodes.length; ; ) {
|
||||
let middle = (lbound + ubound) >> 1;
|
||||
pre = body.childNodes[middle];
|
||||
|
||||
let firstLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
|
||||
|
||||
if (lbound == ubound - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (lineNumber >= firstLine) {
|
||||
lbound = middle;
|
||||
} else {
|
||||
ubound = middle;
|
||||
}
|
||||
}
|
||||
|
||||
let result = {};
|
||||
let found = this.findLocation(pre, lineNumber, null, -1, false, result);
|
||||
|
||||
if (!found) {
|
||||
sendAsyncMessage("ViewSource:GoToLine:Failed");
|
||||
return;
|
||||
}
|
||||
|
||||
let selection = content.getSelection();
|
||||
selection.removeAllRanges();
|
||||
|
||||
// In our case, the range's startOffset is after "\n" on the previous line.
|
||||
// Tune the selection at the beginning of the next line and do some tweaking
|
||||
// to position the focusNode and the caret at the beginning of the line.
|
||||
selection.QueryInterface(Ci.nsISelectionPrivate)
|
||||
.interlinePosition = true;
|
||||
|
||||
selection.addRange(result.range);
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
selection.collapseToEnd();
|
||||
|
||||
let offset = result.range.startOffset;
|
||||
let node = result.range.startContainer;
|
||||
if (offset < node.data.length) {
|
||||
// The same text node spans across the "\n", just focus where we were.
|
||||
selection.extend(node, offset);
|
||||
}
|
||||
else {
|
||||
// There is another tag just after the "\n", hook there. We need
|
||||
// to focus a safe point because there are edgy cases such as
|
||||
// <span>...\n</span><span>...</span> vs.
|
||||
// <span>...\n<span>...</span></span><span>...</span>
|
||||
node = node.nextSibling ? node.nextSibling : node.parentNode.nextSibling;
|
||||
selection.extend(node, 0);
|
||||
}
|
||||
}
|
||||
|
||||
let selCon = this.selectionController;
|
||||
selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
|
||||
selCon.setCaretVisibilityDuringSelection(true);
|
||||
|
||||
// Scroll the beginning of the line into view.
|
||||
selCon.scrollSelectionIntoView(
|
||||
Ci.nsISelectionController.SELECTION_NORMAL,
|
||||
Ci.nsISelectionController.SELECTION_FOCUS_REGION,
|
||||
true);
|
||||
|
||||
sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber });
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Some old code from the original view source implementation. Original
|
||||
* documentation follows:
|
||||
*
|
||||
* "Loops through the text lines in the pre element. The arguments are either
|
||||
* (pre, line) or (node, offset, interlinePosition). result is an out
|
||||
* argument. If (pre, line) are specified (and node == null), result.range is
|
||||
* a range spanning the specified line. If the (node, offset,
|
||||
* interlinePosition) are specified, result.line and result.col are the line
|
||||
* and column number of the specified offset in the specified node relative to
|
||||
* the whole file."
|
||||
*/
|
||||
findLocation(pre, lineNumber, node, offset, interlinePosition, result) {
|
||||
if (node && !pre) {
|
||||
// Look upwards to find the current pre element.
|
||||
for (pre = node;
|
||||
pre.nodeName != "PRE";
|
||||
pre = pre.parentNode);
|
||||
}
|
||||
|
||||
// The source document is made up of a number of pre elements with
|
||||
// id attributes in the format <pre id="line123">, meaning that
|
||||
// the first line in the pre element is number 123.
|
||||
// However, in the plain text case, there is only one <pre> without an id,
|
||||
// so assume line 1.
|
||||
let curLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
|
||||
|
||||
// Walk through each of the text nodes and count newlines.
|
||||
let treewalker = content.document
|
||||
.createTreeWalker(pre, Ci.nsIDOMNodeFilter.SHOW_TEXT, null);
|
||||
|
||||
// The column number of the first character in the current text node.
|
||||
let firstCol = 1;
|
||||
|
||||
let found = false;
|
||||
for (let textNode = treewalker.firstChild();
|
||||
textNode && !found;
|
||||
textNode = treewalker.nextNode()) {
|
||||
|
||||
// \r is not a valid character in the DOM, so we only check for \n.
|
||||
let lineArray = textNode.data.split(/\n/);
|
||||
let lastLineInNode = curLine + lineArray.length - 1;
|
||||
|
||||
// Check if we can skip the text node without further inspection.
|
||||
if (node ? (textNode != node) : (lastLineInNode < lineNumber)) {
|
||||
if (lineArray.length > 1) {
|
||||
firstCol = 1;
|
||||
}
|
||||
firstCol += lineArray[lineArray.length - 1].length;
|
||||
curLine = lastLineInNode;
|
||||
continue;
|
||||
}
|
||||
|
||||
// curPos is the offset within the current text node of the first
|
||||
// character in the current line.
|
||||
for (var i = 0, curPos = 0;
|
||||
i < lineArray.length;
|
||||
curPos += lineArray[i++].length + 1) {
|
||||
|
||||
if (i > 0) {
|
||||
curLine++;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
if (offset >= curPos && offset <= curPos + lineArray[i].length) {
|
||||
// If we are right after the \n of a line and interlinePosition is
|
||||
// false, the caret looks as if it were at the end of the previous
|
||||
// line, so we display that line and column instead.
|
||||
|
||||
if (i > 0 && offset == curPos && !interlinePosition) {
|
||||
result.line = curLine - 1;
|
||||
var prevPos = curPos - lineArray[i - 1].length;
|
||||
result.col = (i == 1 ? firstCol : 1) + offset - prevPos;
|
||||
} else {
|
||||
result.line = curLine;
|
||||
result.col = (i == 0 ? firstCol : 1) + offset - curPos;
|
||||
}
|
||||
found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (curLine == lineNumber && !("range" in result)) {
|
||||
result.range = content.document.createRange();
|
||||
result.range.setStart(textNode, curPos);
|
||||
|
||||
// This will always be overridden later, except when we look for
|
||||
// the very last line in the file (this is the only line that does
|
||||
// not end with \n).
|
||||
result.range.setEndAfter(pre.lastChild);
|
||||
|
||||
} else if (curLine == lineNumber + 1) {
|
||||
result.range.setEnd(textNode, curPos - 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found || ("range" in result);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the "wrap" class on the document body, which sets whether
|
||||
* or not long lines are wrapped. Notifies parent to update the pref.
|
||||
*/
|
||||
toggleWrapping() {
|
||||
let body = content.document.body;
|
||||
let state = body.classList.toggle("wrap");
|
||||
sendAsyncMessage("ViewSource:StoreWrapping", { state });
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the "highlight" class on the document body, which sets whether
|
||||
* or not syntax highlighting is displayed. Notifies parent to update the
|
||||
* pref.
|
||||
*/
|
||||
toggleSyntaxHighlighting() {
|
||||
let body = content.document.body;
|
||||
let state = body.classList.toggle("highlight");
|
||||
sendAsyncMessage("ViewSource:StoreSyntaxHighlighting", { state });
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the parent has changed the character set to view the
|
||||
* source with.
|
||||
*
|
||||
* @param charset
|
||||
* The character set to use.
|
||||
* @param doPageLoad
|
||||
* Whether or not we should reload the page ourselves with the
|
||||
* nsIWebPageDescriptor. Part of a workaround for bug 136322.
|
||||
*/
|
||||
setCharacterSet(charset, doPageLoad) {
|
||||
docShell.charset = charset;
|
||||
if (doPageLoad) {
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reloads the content.
|
||||
*/
|
||||
reload() {
|
||||
let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
|
||||
try {
|
||||
pageLoader.loadPage(pageLoader.currentDescriptor,
|
||||
Ci.nsIWebPageDescriptor.DISPLAY_NORMAL);
|
||||
} catch (e) {
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A reference to a DeferredTask that is armed every time the
|
||||
* selection changes.
|
||||
*/
|
||||
updateStatusTask: null,
|
||||
|
||||
/**
|
||||
* Called once the DeferredTask fires. Sends a message up to the
|
||||
* parent to update the status bar text.
|
||||
*/
|
||||
updateStatus() {
|
||||
let selection = content.getSelection();
|
||||
|
||||
if (!selection.focusNode) {
|
||||
sendAsyncMessage("ViewSource:UpdateStatus", { label: "" });
|
||||
return;
|
||||
}
|
||||
if (selection.focusNode.nodeType != Ci.nsIDOMNode.TEXT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selCon = this.selectionController;
|
||||
selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
|
||||
selCon.setCaretVisibilityDuringSelection(true);
|
||||
|
||||
let interlinePosition = selection.QueryInterface(Ci.nsISelectionPrivate)
|
||||
.interlinePosition;
|
||||
|
||||
let result = {};
|
||||
this.findLocation(null, -1,
|
||||
selection.focusNode, selection.focusOffset, interlinePosition, result);
|
||||
|
||||
let label = this.bundle.formatStringFromName("statusBarLineCol",
|
||||
[result.line, result.col], 2);
|
||||
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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets called every time the selection is changed. Coalesces frequent
|
||||
* changes, and calls updateStatus after 100ms of no selection change
|
||||
* activity.
|
||||
*/
|
||||
notifySelectionChanged(doc, sel, reason) {
|
||||
if (!this.updateStatusTask) {
|
||||
this.updateStatusTask = new DeferredTask(() => {
|
||||
this.updateStatus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
this.updateStatusTask.arm();
|
||||
},
|
||||
|
||||
/**
|
||||
* Using special markers left in the serialized source, this helper makes the
|
||||
* underlying markup of the selected fragment to automatically appear as
|
||||
* selected on the inflated view-source DOM.
|
||||
*/
|
||||
drawSelection() {
|
||||
content.document.title =
|
||||
this.bundle.GetStringFromName("viewSelectionSourceTitle");
|
||||
|
||||
// find the special selection markers that we added earlier, and
|
||||
// draw the selection between the two...
|
||||
var findService = null;
|
||||
try {
|
||||
// get the find service which stores the global find state
|
||||
findService = Cc["@mozilla.org/find/find_service;1"]
|
||||
.getService(Ci.nsIFindService);
|
||||
} catch (e) { }
|
||||
if (!findService)
|
||||
return;
|
||||
|
||||
// cache the current global find state
|
||||
var matchCase = findService.matchCase;
|
||||
var entireWord = findService.entireWord;
|
||||
var wrapFind = findService.wrapFind;
|
||||
var findBackwards = findService.findBackwards;
|
||||
var searchString = findService.searchString;
|
||||
var replaceString = findService.replaceString;
|
||||
|
||||
// setup our find instance
|
||||
var findInst = this.webBrowserFind;
|
||||
findInst.matchCase = true;
|
||||
findInst.entireWord = false;
|
||||
findInst.wrapFind = true;
|
||||
findInst.findBackwards = false;
|
||||
|
||||
// ...lookup the start mark
|
||||
findInst.searchString = MARK_SELECTION_START;
|
||||
var startLength = MARK_SELECTION_START.length;
|
||||
findInst.findNext();
|
||||
|
||||
var selection = content.getSelection();
|
||||
if (!selection.rangeCount)
|
||||
return;
|
||||
|
||||
var range = selection.getRangeAt(0);
|
||||
|
||||
var startContainer = range.startContainer;
|
||||
var startOffset = range.startOffset;
|
||||
|
||||
// ...lookup the end mark
|
||||
findInst.searchString = MARK_SELECTION_END;
|
||||
var endLength = MARK_SELECTION_END.length;
|
||||
findInst.findNext();
|
||||
|
||||
var endContainer = selection.anchorNode;
|
||||
var endOffset = selection.anchorOffset;
|
||||
|
||||
// reset the selection that find has left
|
||||
selection.removeAllRanges();
|
||||
|
||||
// delete the special markers now...
|
||||
endContainer.deleteData(endOffset, endLength);
|
||||
startContainer.deleteData(startOffset, startLength);
|
||||
if (startContainer == endContainer)
|
||||
endOffset -= startLength; // has shrunk if on same text node...
|
||||
range.setEnd(endContainer, endOffset);
|
||||
|
||||
// show the selection and scroll it into view
|
||||
selection.addRange(range);
|
||||
// the default behavior of the selection is to scroll at the end of
|
||||
// the selection, whereas in this situation, it is more user-friendly
|
||||
// to scroll at the beginning. So we override the default behavior here
|
||||
try {
|
||||
this.selectionController.scrollSelectionIntoView(
|
||||
Ci.nsISelectionController.SELECTION_NORMAL,
|
||||
Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
|
||||
true);
|
||||
}
|
||||
catch (e) { }
|
||||
|
||||
// restore the current find state
|
||||
findService.matchCase = matchCase;
|
||||
findService.entireWord = entireWord;
|
||||
findService.wrapFind = wrapFind;
|
||||
findService.findBackwards = findBackwards;
|
||||
findService.searchString = searchString;
|
||||
findService.replaceString = replaceString;
|
||||
|
||||
findInst.matchCase = matchCase;
|
||||
findInst.entireWord = entireWord;
|
||||
findInst.wrapFind = wrapFind;
|
||||
findInst.findBackwards = findBackwards;
|
||||
findInst.searchString = searchString;
|
||||
},
|
||||
|
||||
/**
|
||||
* In-page context menu items that are injected after page load.
|
||||
*/
|
||||
contextMenuItems: [
|
||||
{
|
||||
id: "goToLine",
|
||||
accesskey: true,
|
||||
handler() {
|
||||
sendAsyncMessage("ViewSource:PromptAndGoToLine");
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "wrapLongLines",
|
||||
get checked() {
|
||||
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
|
||||
},
|
||||
handler() {
|
||||
this.toggleWrapping();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "highlightSyntax",
|
||||
get checked() {
|
||||
return Services.prefs.getBoolPref("view_source.syntax_highlight");
|
||||
},
|
||||
handler() {
|
||||
this.toggleSyntaxHighlighting();
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* Add context menu items for view source specific actions.
|
||||
*/
|
||||
injectContextMenu() {
|
||||
let doc = content.document;
|
||||
|
||||
let menu = doc.createElementNS(NS_XHTML, "menu");
|
||||
menu.setAttribute("type", "context");
|
||||
menu.setAttribute("id", "actions");
|
||||
doc.body.appendChild(menu);
|
||||
doc.body.setAttribute("contextmenu", "actions");
|
||||
|
||||
this.contextMenuItems.forEach(itemSpec => {
|
||||
let item = doc.createElementNS(NS_XHTML, "menuitem");
|
||||
item.setAttribute("id", itemSpec.id);
|
||||
let labelName = `context_${itemSpec.id}_label`;
|
||||
let label = this.bundle.GetStringFromName(labelName);
|
||||
item.setAttribute("label", label);
|
||||
if ("checked" in itemSpec) {
|
||||
item.setAttribute("type", "checkbox");
|
||||
}
|
||||
if (itemSpec.accesskey) {
|
||||
let accesskeyName = `context_${itemSpec.id}_accesskey`;
|
||||
item.setAttribute("accesskey",
|
||||
this.bundle.GetStringFromName(accesskeyName))
|
||||
}
|
||||
menu.appendChild(item);
|
||||
});
|
||||
|
||||
this.updateContextMenu();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update state of checkbox-style context menu items.
|
||||
*/
|
||||
updateContextMenu() {
|
||||
let doc = content.document;
|
||||
this.contextMenuItems.forEach(itemSpec => {
|
||||
if (!("checked" in itemSpec)) {
|
||||
return;
|
||||
}
|
||||
let item = doc.getElementById(itemSpec.id);
|
||||
if (itemSpec.checked) {
|
||||
item.setAttribute("checked", true);
|
||||
} else {
|
||||
item.removeAttribute("checked");
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
ViewSourceContent.init();
|
||||
Loading…
Add table
Add a link
Reference in a new issue