diff options
Diffstat (limited to 'toolkit/components/viewsource/content/viewSource-content.js')
-rw-r--r-- | toolkit/components/viewsource/content/viewSource-content.js | 978 |
1 files changed, 978 insertions, 0 deletions
diff --git a/toolkit/components/viewsource/content/viewSource-content.js b/toolkit/components/viewsource/content/viewSource-content.js new file mode 100644 index 000000000..4efa1e952 --- /dev/null +++ b/toolkit/components/viewsource/content/viewSource-content.js @@ -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(); |