# HG changeset patch # Parent 541ccce39563cf1e660747e1bcbede359a6cccaf # User Eklavya Mirani (eklavyamirani@hotmail.com) Bug 558882: Fixed entries in winstripe/jar.mn for reader mode files under aero section. Reader mode is now working on Windows. * * * Bug 793920: Moved Readability.js and JSDOMparser.js to reader/toolkit * * * Bug 795973:Kevin added a preference * * * Bug 795981:Made fixes to previous patch. * * * Bug test: Reader Mode is working. diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -16,16 +16,18 @@ # #ifdef XP_UNIX #ifndef XP_MACOSX #define UNIX_BUT_NOT_MAC #endif #endif + + pref("browser.chromeURL","chrome://browser/content/"); pref("browser.hiddenWindowChromeURL", "chrome://browser/content/hiddenWindow.xul"); // Enables some extra Extension System Logging (can reduce performance) pref("extensions.logging.enabled", false); // Disables strict compatibility, making addons compatible-by-default. pref("extensions.strictCompatibility", false); @@ -1187,8 +1189,15 @@ pref("social.manifest.facebook", "{\"ori // built-in social functionality. pref("social.activation.whitelist", "https://www.facebook.com"); pref("social.sidebar.open", true); pref("social.sidebar.unload_timeout_ms", 10000); pref("social.active", false); pref("social.toast-notifications.enabled", true); pref("dom.identity.enabled", false); + +//Reader settings +pref("reader.margin_size", 5); +pref("reader.font_size", 12); +pref("reader.color_scheme", "light"); +pref("reader.has_used_toolbar", false); +pref("reader.enabled", false); \ No newline at end of file diff --git a/browser/base/content/aboutReader.html b/browser/base/content/aboutReader.html new file mode 100644 --- /dev/null +++ b/browser/base/content/aboutReader.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + +
+
+

+
+
+ +
+
+ +
+ +
+ + + + diff --git a/browser/base/content/aboutReader.js b/browser/base/content/aboutReader.js new file mode 100644 --- /dev/null +++ b/browser/base/content/aboutReader.js @@ -0,0 +1,561 @@ +/* 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/. */ + +let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm") +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(window, "gChromeWin", function () + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow)); + +function dump(s) { + Services.console.logStringMessage("AboutReader: " + s); +} + +let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutReader.properties"); + +let AboutReader = function(doc, win) { + this._docRef = Cu.getWeakReference(doc); + this._winRef = Cu.getWeakReference(win); + + this._article = null; + + dump("Feching toolbar, header and content notes from about:reader"); + this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header")); + this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain")); + + this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title")); + + this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits")); + this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content")); + this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar")); + + this._toolbarEnabled = true; + + this._scrollOffset = win.pageYOffset; + + win.addEventListener("click", this, false); + + win.addEventListener("scroll", this, false); + win.addEventListener("popstate", this, false); + win.addEventListener("resize", this, false); + + this._setupAllDropdowns(); + + let colorSchemeOptions = [ + { name: gStrings.GetStringFromName("aboutReader.colorSchemeLight"), + value: "light"}, + { name: gStrings.GetStringFromName("aboutReader.colorSchemeDark"), + value: "dark"} + ]; + + let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); + this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorScheme.bind(this)); + this._setColorScheme(colorScheme); + + let fontTitle = gStrings.GetStringFromName("aboutReader.textTitle"); + this._setupStepControl("font-size-control", fontTitle, this._onFontSizeChange.bind(this)); + this._fontSize = 0; + this._setFontSize(Services.prefs.getIntPref("reader.font_size")); + + let marginTitle = gStrings.GetStringFromName("aboutReader.marginTitle"); + this._setupStepControl("margin-size-control", marginTitle, this._onMarginSizeChange.bind(this)); + this._marginSize = 0; + this._setMarginSize(Services.prefs.getIntPref("reader.margin_size")); + + dump("Decoding query arguments"); + let queryArgs = this._decodeQueryString(win.location.href); + + let url = queryArgs.url; + let tabId = 1; + if (tabId) { + dump("Loading from tab with ID: " + tabId + ", URL: " + url); + this._loadFromTab(tabId, url); + } else { + dump("Fetching page with URL: " + url); + this._loadFromURL(url); + } +} + +AboutReader.prototype = { + _STEP_INCREMENT: 0, + _STEP_DECREMENT: 1, + + _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " + + ".content p > a:only-child > img:only-child, " + + ".content .wp-caption img, " + + ".content figure img", + + get _doc() { + return this._docRef.get(); + }, + + get _win() { + return this._winRef.get(); + }, + + get _headerElement() { + return this._headerElementRef.get(); + }, + + get _domainElement() { + return this._domainElementRef.get(); + }, + + get _titleElement() { + return this._titleElementRef.get(); + }, + + get _creditsElement() { + return this._creditsElementRef.get(); + }, + + get _contentElement() { + return this._contentElementRef.get(); + }, + + get _toolbarElement() { + return this._toolbarElementRef.get(); + }, + + handleEvent: function Reader_handleEvent(aEvent) { + if (!aEvent.isTrusted) + return; + + switch (aEvent.type) { + case "click": + this._scrolled = false; + if (!this._scrolled) + this._toggleToolbarVisibility(); + break; + case "scroll": + if (!this._scrolled) { + this._scrolled = true; + this._setToolbarVisibility(false); + } + break; + case "popstate": + if (!aEvent.state) + this._closeAllDropdowns(); + break; + case "resize": + this._updateImageMargins(); + break; + } + }, + + _onMarginSizeChange: function Reader_onMarginSizeChange(operation) { + if (operation == this._STEP_INCREMENT) + this._setMarginSize(this._marginSize + 5); + else + this._setMarginSize(this._marginSize - 5); + }, + + _setMarginSize: function Reader_setMarginSize(newMarginSize) { + if (this._marginSize === newMarginSize) + return; + + let doc = this._doc; + + this._marginSize = Math.max(5, Math.min(25, newMarginSize)); + doc.body.style.marginLeft = this._marginSize + "%"; + doc.body.style.marginRight = this._marginSize + "%"; + + this._updateImageMargins(); + + Services.prefs.setIntPref("reader.margin_size", this._marginSize); + }, + + _onFontSizeChange: function Reader_onFontSizeChange(operation) { + if (operation == this._STEP_INCREMENT) + this._setFontSize(this._fontSize + 1); + else + this._setFontSize(this._fontSize - 1); + }, + + _setFontSize: function Reader_setFontSize(newFontSize) { + if (this._fontSize === newFontSize) + return; + + let bodyClasses = this._doc.body.classList; + + if (this._fontSize > 0) + bodyClasses.remove("font-size" + this._fontSize); + + this._fontSize = Math.max(1, Math.min(7, newFontSize)); + bodyClasses.add("font-size" + this._fontSize); + + Services.prefs.setIntPref("reader.font_size", this._fontSize); + }, + + _setColorScheme: function Reader_setColorScheme(newColorScheme) { + if (this._colorScheme === newColorScheme) + return; + + let bodyClasses = this._doc.body.classList; + + if (this._colorScheme) + bodyClasses.remove(this._colorScheme); + + this._colorScheme = newColorScheme; + bodyClasses.add(this._colorScheme); + + Services.prefs.setCharPref("reader.color_scheme", this._colorScheme); + }, + + _getToolbarVisibility: function Reader_getToolbarVisibility() { + return !this._toolbarElement.classList.contains("toolbar-hidden"); + }, + + _setToolbarVisibility: function Reader_setToolbarVisibility(visible) { + let win = this._win; + if (win.history.state) + win.history.back(); + + if (!this._toolbarEnabled) + return; + + if (this._getToolbarVisibility() === visible) + return; + + this._toolbarElement.classList.toggle("toolbar-hidden"); + + if (!visible && !this._hasUsedToolbar) { + this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar"); + if (!this._hasUsedToolbar) { + Services.prefs.setBoolPref("reader.has_used_toolbar", true); + this._hasUsedToolbar = true; + } + } + }, + + _toggleToolbarVisibility: function Reader_toggleToolbarVisibility(visible) { + this._setToolbarVisibility(!this._getToolbarVisibility()); + }, + + _loadFromURL: function Reader_loadFromURL(url) { + this._showProgress(); + gChromeWin.Reader.parseDocumentFromURL(url, function(article) { + + if (article) { + this._showContent(article); + } + else { + this._showError(gStrings.GetStringFromName("aboutReader.loadError")); + } + }.bind(this)); + }, + + _loadFromTab: function Reader_loadFromTab(tabId, url) { + this._showProgress(); + + gChromeWin.Reader.getArticleForTab(tabId, url, function(article) { + if (article) + this._showContent(article); + else + this._showError(gStrings.GetStringFromName("aboutReader.loadError")); + }.bind(this)); + }, + + + + _updateImageMargins: function Reader_updateImageMargins() { + let windowWidth = this._win.innerWidth; + let contentWidth = this._contentElement.offsetWidth; + let maxWidthStyle = windowWidth + "px !important"; + + let setImageMargins = function(img) { + if (!img._originalWidth) + img._originalWidth = img.offsetWidth; + + let imgWidth = img._originalWidth; + + // If the image is taking more than half of the screen, just make + // it fill edge-to-edge. + if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) + imgWidth = windowWidth; + + let sideMargin = Math.max((contentWidth - windowWidth) / 2, + (contentWidth - imgWidth) / 2); + + let imageStyle = sideMargin + "px !important"; + let widthStyle = imgWidth + "px !important"; + + let cssText = "max-width: " + maxWidthStyle + ";" + + "width: " + widthStyle + ";" + + "margin-left: " + imageStyle + ";" + + "margin-right: " + imageStyle + ";"; + + img.style.cssText = cssText; + } + + let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR); + for (let i = imgs.length; --i >= 0;) { + let img = imgs[i]; + + if (img.width > 0) { + setImageMargins(img); + } else { + img.onload = function() { + setImageMargins(img); + } + } + } + }, + + _showError: function Reader_showError(error) { + this._headerElement.style.display = "none"; + this._contentElement.innerHTML = error; + this._contentElement.style.display = "block"; + + this._doc.title = error; + }, + + _showContent: function Reader_showContent(article) { + this._article = article; + + let domain = Services.io.newURI(article.url, null, null).host; + this._domainElement.innerHTML = domain; + + this._creditsElement.innerHTML = article.byline; + + this._titleElement.innerHTML = article.title; + this._doc.title = article.title; + + this._headerElement.style.display = "block"; + + this._contentElement.innerHTML = article.content; + this._updateImageMargins(); + + this._contentElement.style.display = "block"; + + + this._toolbarEnabled = true; + this._setToolbarVisibility(true); + }, + + _hideContent: function Reader_hideContent() { + this._headerElement.style.display = "none"; + this._contentElement.style.display = "none"; + }, + + _showProgress: function Reader_showProgress() { + this._headerElement.style.display = "none"; + this._contentElement.innerHTML = gStrings.GetStringFromName("aboutReader.loading"); + this._contentElement.style.display = "block"; + }, + + _decodeQueryString: function Reader_decodeQueryString(url) { + let result = {}; + let query = url.split("?")[1]; + if (query) { + let pairs = query.split("&"); + for (let i = 0; i < pairs.length; i++) { + let [name, value] = pairs[i].split("="); + result[name] = decodeURIComponent(value); + } + } + + return result; + }, + + _setupStepControl: function Reader_setupStepControl(id, name, callback) { + let doc = this._doc; + let stepControl = doc.getElementById(id); + + while (stepControl.hasChildNodes()) { + stepControl.removeChild(stepControl.lastChild); + } + + let title = this._doc.createElement("h1"); + title.innerHTML = name; + stepControl.appendChild(title); + + let plusButton = doc.createElement("div"); + plusButton.className = "button plus-button"; + stepControl.appendChild(plusButton); + + let minusButton = doc.createElement("div"); + minusButton.className = "button minus-button"; + stepControl.appendChild(minusButton); + + plusButton.addEventListener("click", function(aEvent) { + if (!aEvent.isTrusted) + return; + + aEvent.stopPropagation(); + callback(this._STEP_INCREMENT); + }.bind(this), true); + + minusButton.addEventListener("click", function(aEvent) { + if (!aEvent.isTrusted) + return; + + aEvent.stopPropagation(); + callback(this._STEP_DECREMENT); + }.bind(this), true); + }, + + _setupSegmentedButton: function Reader_setupSegmentedButton(id, options, initialValue, callback) { + let doc = this._doc; + let segmentedButton = doc.getElementById(id); + + if (segmentedButton != null) { + while (segmentedButton.hasChildNodes()) { + if (segmentedButton.lastChild.tagName == "LI") { + segmentedButton.removeChild(segmentedButton.lastChild); + } + } + } + for (let i = 0; i < options.length; i++) { + let option = options[i]; + + let item = doc.createElement("li"); + let link = doc.createElement("a"); + link.innerHTML = option.name; + item.appendChild(link); + + segmentedButton.appendChild(item); + + link.addEventListener("click", function(aEvent) { + if (!aEvent.isTrusted) + return; + + aEvent.stopPropagation(); + + let items = segmentedButton.children; + for (let j = items.length - 1; j >= 0; j--) { + items[j].classList.remove("selected"); + } + + item.classList.add("selected"); + callback(option.value); + }.bind(this), true); + + if (option.value === initialValue) + item.classList.add("selected"); + } + }, + + _setupButton: function Reader_setupButton(id, callback) { + let button = this._doc.getElementById(id); + + button.addEventListener("click", function(aEvent) { + if (!aEvent.isTrusted) + return; + + aEvent.stopPropagation(); + callback(); + }, true); + }, + + _setupAllDropdowns: function Reader_setupAllDropdowns() { + let doc = this._doc; + let win = this._win; + + let dropdowns = doc.getElementsByClassName("dropdown"); + + for (let i = dropdowns.length - 1; i >= 0; i--) { + let dropdown = dropdowns[i]; + + let dropdownToggle = dropdown.getElementsByClassName("dropdown-toggle")[0]; + let dropdownPopup = dropdown.getElementsByClassName("dropdown-popup")[0]; + + if (!dropdownToggle || !dropdownPopup) + continue; + + for (var j = 0; j < dropdownPopup.childNodes.length; j++) { + if (dropdownPopup.childNodes[j].className == "dropdown-arrow") { + dropdownPopup.removeChild(dropdownPopup.childNodes[j]); + } + } + + let dropdownArrow = doc.createElement("div"); + dropdownArrow.className = "dropdown-arrow"; + dropdownPopup.appendChild(dropdownArrow); + + let updatePopupPosition = function() { + let popupWidth = dropdownPopup.offsetWidth + 30; + let arrowWidth = dropdownArrow.offsetWidth; + let toggleWidth = dropdownToggle.offsetWidth; + let toggleLeft = dropdownToggle.offsetLeft; + + let popupShift = (toggleWidth - popupWidth) / 2; + let popupLeft = Math.max(0, Math.min(win.innerWidth - popupWidth, toggleLeft + popupShift)); + dropdownPopup.style.left = popupLeft + "px"; + + let arrowShift = (toggleWidth - arrowWidth) / 2; + let arrowLeft = toggleLeft - popupLeft + arrowShift; + dropdownArrow.style.left = arrowLeft + "px"; + }; + + win.addEventListener("resize", function(aEvent) { + if (!aEvent.isTrusted) + return; + + updatePopupPosition(); + }, true); + + dropdownToggle.addEventListener("click", function(aEvent) { + if (!aEvent.isTrusted) + return; + + aEvent.stopPropagation(); + + let dropdownClasses = dropdown.classList; + + if (dropdownClasses.contains("open")) { + win.history.back(); + } else { + updatePopupPosition(); + if (!this._closeAllDropdowns()) + this._pushDropdownState(); + + dropdownClasses.add("open"); + } + }.bind(this), true); + } + }, + + _pushDropdownState: function Reader_pushDropdownState() { + // FIXME: We're getting a NS_ERROR_UNEXPECTED error when we try + // to do win.history.pushState() here (see bug 682296). This is + // a workaround that allows us to push history state on the target + // content document. + + let doc = this._doc; + let body = doc.body; + + if (this._pushStateScript) + body.removeChild(this._pushStateScript); + + for (var j = 0; j < body.childNodes.length; j++) { + if (body.childNodes[j].type == "text/javascript") { + body.removeChild(body.childNodes[j]); + } + } + + this._pushStateScript = doc.createElement('script'); + this._pushStateScript.type = "text/javascript"; + this._pushStateScript.innerHTML = 'history.pushState({ dropdown: 1 }, document.title);'; + + body.appendChild(this._pushStateScript); + }, + + _closeAllDropdowns : function Reader_closeAllDropdowns() { + let dropdowns = this._doc.querySelectorAll(".dropdown.open"); + for (let i = dropdowns.length - 1; i >= 0; i--) { + dropdowns[i].classList.remove("open"); + } + + return (dropdowns.length > 0) + } +}; diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -38,17 +38,24 @@ var gEditUIVisible = true; delete window[name]; return window[name] = element; }); window.__defineSetter__(name, function (val) { delete window[name]; return window[name] = val; }); }); - + +let readerName = "AboutReader", readerScript = "chrome://browser/content/aboutReader.js"; +XPCOMUtils.defineLazyGetter(window, readerName, function() { + let sandbox = {}; + Services.scriptloader.loadSubScript(readerScript, sandbox); + return sandbox[readerName]; +}); + // Smart getter for the findbar. If you don't wish to force the creation of // the findbar, check gFindBarInitialized first. var gFindBarInitialized = false; XPCOMUtils.defineLazyGetter(window, "gFindBar", function() { let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; let findbar = document.createElementNS(XULNS, "findbar"); findbar.id = "FindToolbar"; @@ -149,17 +156,18 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); let gInitialPages = [ "about:blank", "about:newtab", "about:home", "about:privatebrowsing", - "about:sessionrestore" + "about:sessionrestore", + "about:reader" ]; #include browser-addons.js #include browser-feeds.js #include browser-fullScreen.js #include browser-fullZoom.js #include browser-places.js #include browser-plugins.js @@ -206,24 +214,27 @@ XPCOMUtils.defineLazyGetter(this, "PageM return new tmp.PageMenu(); }); /** * We can avoid adding multiple load event listeners and save some time by adding * one listener that calls all real handlers. */ function pageShowEventHandlers(event) { - charsetLoadListener(); - XULBrowserWindow.asyncUpdateUI(); - - // The PluginClickToPlay events are not fired when navigating using the - // BF cache. |event.persisted| is true when the page is loaded from the - // BF cache, so this code reshows the notification if necessary. - if (event.persisted) - gPluginHandler.reshowClickToPlayNotification(); + charsetLoadListener(); + XULBrowserWindow.asyncUpdateUI(); + + // The PluginClickToPlay events are not fired when navigating using the + // BF cache. |event.persisted| is true when the page is loaded from the + // BF cache, so this code reshows the notification if necessary. + if (event.persisted) { + gPluginHandler.reshowClickToPlayNotification(); + } + + ReaderMode.PageShowHandler(); } function UpdateBackForwardCommands(aWebNavigation) { var backBroadcaster = document.getElementById("Browser:Back"); var forwardBroadcaster = document.getElementById("Browser:Forward"); // Avoid setting attributes on broadcasters if the value hasn't changed! // Remember, guys, setting attributes on elements is expensive! They @@ -1175,16 +1186,18 @@ var gBrowserInit = { goSetCommandEnabled("cmd_newNavigatorTab", false); } #ifdef MENUBAR_CAN_AUTOHIDE updateAppButtonDisplay(); #endif // Misc. inits. + ReaderMode.init(); + Reader.init(); CombinedStopReload.init(); allTabs.readPref(); TabsOnTop.init(); BookmarksMenuButton.init(); TabsInTitlebar.init(); gPrivateBrowsingUI.init(); retrieveToolbarIconsizesFromTheme(); @@ -1261,24 +1274,26 @@ var gBrowserInit = { Services.obs.addObserver(gFormSubmitObserver, "invalidformsubmit", false); BrowserOffline.init(); OfflineApps.init(); IndexedDBPromptHelper.init(); gFormSubmitObserver.init(); SocialUI.init(); AddonManager.addAddonListener(AddonsMgrListener); - + gBrowser.addEventListener("pageshow", function(event) { - // Filter out events that are not about the document load we are interested in - if (event.target == content.document) - setTimeout(pageShowEventHandlers, 0, event); - }, true); - - // Ensure login manager is up and running. +      // Filter out events that are not about the document load we are interested in +      if (event.target == content.document) +        setTimeout(pageShowEventHandlers, 0, event); +    }, true); + + gBrowser.tabContainer.addEventListener("TabSelect", function(evt) { setTimeout(ReaderMode.TabSwitchHandler, 0, evt); }, true); + + // Ensure login manager is up and running. Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); if (mustLoadSidebar) { let sidebar = document.getElementById("sidebar"); let sidebarBox = document.getElementById("sidebar-box"); sidebar.setAttribute("src", sidebarBox.getAttribute("src")); } @@ -5652,16 +5667,83 @@ var stylesheetFillPopup = gPageStyleMenu function stylesheetSwitchAll(contentWindow, title) { gPageStyleMenu.switchStyleSheet(title, contentWindow); } function setStyleDisabled(disabled) { if (disabled) gPageStyleMenu.disableStyle(); } +var savedArticle = null; + +var ReaderMode = { + + //////////////////////////// + // ReaderMode Public Methods + init: function () + { + //Reader Button disable/enable + document.getElementById("reader-button").hidden = true;//!gPrefService.getBoolPref("reader.enabled"); + }, + +  //Reader Mode On-Click + BrowserOpenReaderMode: function(aEvent) { + document.getElementById("reader-button").hidden = true;                             + openUILink("about:reader?url=" + gBrowser.currentURI.specIgnoringRef, {target:gBrowser.selectedTab}); + ReaderMode.selectedReaderTab.addEventListener("DOMContentLoaded", ReaderMode.handleDomContentLoaded, true); + }, + + PageShowHandler: function() { + ReaderMode.checkReaderMode(); + ReaderMode.readerButton.hidden = (savedArticle == null); + }, + + TabSwitchHandler: function(event) { + ReaderMode.checkReaderMode(); + ReaderMode.readerButton.hidden = (savedArticle == null); + }, + + handleDomContentLoaded: function(event) { + if(/^about:reader/.test(ReaderMode.selectedReaderTab.currentURI.specIgnoringRef)) { + new AboutReader(ReaderMode.selectedReaderTab.contentDocument, ReaderMode.selectedReaderTab.contentWindow); +      ReaderMode.selectedReaderTab.removeEventListener("DOMContentLoaded", ReaderMode.handleDomContentLoaded, true);   + }  + }, + + checkReaderMode: function() { + savedArticle = null; + if (gPrefService.getBoolPref("reader.enabled")) { + + Reader.parseDocumentFromTab(gBrowser.selectedTab, function (article) { +     // Do nothing if there's no article or the page in this tab has +     // changed +      let tabURL = gBrowser.currentURI.specIgnoringRef; +      if (article == null || (article.url != tabURL)) { +        // Don't clear the article for about:reader pages since we want to +        // use the article from the previous page + if (tabURL.startsWith("about:reader")) +          savedArticle = null; +                               +        return; +      }                +      savedArticle = article; + ReaderMode.readerButton.hidden = (savedArticle == null); + }.bind(this)); + } + }, + + get readerButton () { + return document.getElementById("reader-button"); + }, + + get selectedReaderTab () { + return gBrowser.getBrowserForTab(gBrowser.selectedTab); + }, + +}; var BrowserOffline = { _inited: false, ///////////////////////////////////////////////////////////////////////////// // BrowserOffline Public Methods init: function () { @@ -7652,8 +7734,436 @@ var MousePosTracker = { function focusNextFrame(event) { let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); let dir = event.shiftKey ? fm.MOVEFOCUS_BACKWARDDOC : fm.MOVEFOCUS_FORWARDDOC; let element = fm.moveFocus(window, null, dir, fm.FLAG_BYKEY); if (element.ownerDocument == document) focusAndSelectUrlBar(); } + + +let Reader = { + // Version of the cache database schema +DB_VERSION: 1, + +DEBUG: 1, + + // Don't try to parse the page if it has too many elements (for memory and + // performance reasons) +MAX_ELEMS_TO_PARSE: 3000, + +init: function Reader_init() { + this.log("Init()"); + this._requests = {}; + + Services.obs.addObserver(this, "Reader:Add", false); + Services.obs.addObserver(this, "Reader:Remove", false); +}, + +observe: function(aMessage, aTopic, aData) { + switch(aTopic) { + case "Reader:Add": { + //let tab = gBrowser.selectedTab; + let currentURI = gBrowser.currentURI; + let url = currentURI.spec; + + let sendResult = function(success, title) { + this.log("Reader:Add success=" + success + ", url=" + url + ", title=" + title); + }.bind(this); + + this.getArticleForTab(aData, currentURI.specIgnoringRef, function (article) { + if (!article) { + sendResult(false, ""); + return; + } + + this.storeArticleInCache(article, function(success) { + sendResult(success, article.title); + }); + }.bind(this)); + break; + } + + case "Reader:Remove": { + this.removeArticleFromCache(aData, function(success) { + this.log("Reader:Remove success=" + success + ", url=" + aData); + }.bind(this)); + break; + } + } +}, + +parseDocumentFromURL: function Reader_parseDocumentFromURL(url, callback) { + // If there's an on-going request for the same URL, simply append one + // more callback to it to be called when the request is done. + if (url in this._requests) { + let request = this._requests[url]; + request.callbacks.push(callback); + return; + } + + let request = { url: url, callbacks: [callback] }; + this._requests[url] = request; + + try { + this.log("parseDocumentFromURL: " + url); + + // First, try to find a cached parsed article in the DB + this.getArticleFromCache(url, function(article) { + if (article) { + //this.log("Page found in cache, return article immediately"); + this._runCallbacksAndFinish(request, article); + return; + } + + if (!this._requests) { + this.log("Reader has been destroyed, abort"); + return; + } + + // Article hasn't been found in the cache DB, we need to + // download the page and parse the article out of it. + this._downloadAndParseDocument(url, request); + }.bind(this)); + } catch (e) { + this.log("Error parsing document from URL: " + e); + this._runCallbacksAndFinish(request, null); + } +}, + +getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) { + var article; + try { + article = savedArticle; + } + catch (e) {} + if (article && article.url == url) { + this.log("Saved article found in tab"); + callback(article); + } else { + this.parseDocumentFromURL(url, callback); + } +}, + +parseDocumentFromTab: function(tabId, callback) { + try { + this.log("parseDocumentFromTab: " + tabId); + + //let tab = gBrowser.getTabForId(tabId); + let url = gBrowser.contentWindow.location.href; + let uri = Services.io.newURI(url, null, null); + + if (!this._shouldCheckUri(uri)) { + callback(null); + return; + } + + // First, try to find a cached parsed article in the DB + this.getArticleFromCache(url, function(article) { + if (article) { + this.log("Page found in cache, return article immediately"); + callback(article); + return; + } + + let doc = gBrowser.contentWindow.document; + this._readerParse(uri, doc, function (article) { + if (!article) { + this.log("Failed to parse page"); + callback(null); + return; + } + callback(article); + }.bind(this)); + }.bind(this)); + } catch (e) { + this.log("Error parsing document from tab: " + e); + callback(null); + } +}, + +getArticleFromCache: function Reader_getArticleFromCache(url, callback) { + this._getCacheDB(function(cacheDB) { + if (!cacheDB) { + callback(false); + return; + } + let transaction = cacheDB.transaction(cacheDB.objectStoreNames); + let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); + + let request = articles.get(url); + + request.onerror = function(event) { + this.log("Error getting article from the cache DB: " + url); + callback(null); + }.bind(this); + + request.onsuccess = function(event) { + this.log("Got article from the cache DB! "); + callback(event.target.result); + }.bind(this); + }.bind(this)); +}, + +storeArticleInCache: function Reader_storeArticleInCache(article, callback) { + this._getCacheDB(function(cacheDB) { + if (!cacheDB) { + callback(false); + return; + } + + let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite"); + let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); + + let request = articles.add(article); + + request.onerror = function(event) { + this.log("Error storing article in the cache DB: " + article.url); + callback(false); + }.bind(this); + + request.onsuccess = function(event) { + //this.log("Stored article in the cache DB: " + article.url); + callback(true); + }.bind(this); + }.bind(this)); +}, + +removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) { + this._getCacheDB(function(cacheDB) { + if (!cacheDB) { + callback(false); + return; + } + + let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite"); + let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); + + let request = articles.delete(url); + + request.onerror = function(event) { + this.log("Error removing article from the cache DB: " + url); + callback(false); + }.bind(this); + + request.onsuccess = function(event) { + this.log("Removed article from the cache DB: " + url); + callback(true); + }.bind(this); + }.bind(this)); +}, + +uninit: function Reader_uninit() { + Services.obs.removeObserver(this, "Reader:Add", false); + Services.obs.removeObserver(this, "Reader:Remove", false); + + let requests = this._requests; + for (let url in requests) { + let request = requests[url]; + if (request.browser) { + let browser = request.browser; + browser.parentNode.removeChild(browser); + } + } + delete this._requests; + + if (this._cacheDB) { + this._cacheDB.close(); + delete this._cacheDB; + } +}, + +log: function(msg) { + if (this.DEBUG) + dump("Reader: " + msg); +}, + +_shouldCheckUri: function Reader_shouldCheckUri(uri) { + if ((uri.prePath + "/") === uri.spec) { + //this.log("Not parsing home page: " + uri.spec); + return false; + } + + if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) { + //this.log("Not parsing URI scheme: " + uri.scheme); + return false; + } + + return true; +}, + +_readerParse: function Reader_readerParse(uri, doc, callback) { + let numTags = doc.getElementsByTagName("*").length; + if (numTags > this.MAX_ELEMS_TO_PARSE) { + //this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found"); + callback(null); + return; + } + + let worker = new ChromeWorker("resource://gre/modules/readerWorker.jsm"); + worker.onmessage = function (evt) { + let article = evt.data; + + // Append URL to the article data. specIgnoringRef will ignore any hash + // in the URL. + if (article) + article.url = uri.specIgnoringRef; + + callback(article); + }; + + try { + worker.postMessage({ + uri: { + spec: uri.spec, + host: uri.host, + prePath: uri.prePath, + scheme: uri.scheme, + pathBase: Services.io.newURI(".", null, uri).spec + }, + doc: new XMLSerializer().serializeToString(doc) + }); + } catch (e) { + this.log("Reader: could not build Readability arguments: " + e); + callback(null); + } +}, + +_runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) { + delete this._requests[request.url]; + + request.callbacks.forEach(function(callback) { + callback(result); + }); +}, + +_downloadDocument: function Reader_downloadDocument(url, callback) { + // We want to parse those arbitrary pages safely, outside the privileged + // context of chrome. We create a hidden browser element to fetch the + // loaded page's document object then discard the browser element. + + let browser = document.createElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("collapsed", "true"); + + document.documentElement.appendChild(browser); + browser.stop(); + + browser.webNavigation.allowAuth = false; + browser.webNavigation.allowImages = false; + browser.webNavigation.allowJavascript = false; + browser.webNavigation.allowMetaRedirects = true; + browser.webNavigation.allowPlugins = false; + + browser.addEventListener("DOMContentLoaded", function (event) { + let doc = event.originalTarget; + + // ignore on frames and other documents + if (doc != browser.contentDocument) + return; + + this.log("Done loading: " + doc); + if (doc.location.href == "about:blank") { + callback(null); + + // Request has finished with error, remove browser element + browser.parentNode.removeChild(browser); + return; + } + + callback(doc); + }.bind(this)); + + browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null); + + return browser; +}, + +_downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) { + try { + this.log("Needs to fetch page, creating request: " + url); + + request.browser = this._downloadDocument(url, function(doc) { + this.log("Finished loading page: " + doc); + + if (!doc) { + this.log("Error loading page"); + this._runCallbacksAndFinish(request, null); + return; + } + + this.log("Parsing response with Readability"); + + let uri = Services.io.newURI(url, null, null); + this._readerParse(uri, doc, function (article) { + // Delete reference to the browser element as we've finished parsing. + let browser = request.browser; + if (browser) { + browser.parentNode.removeChild(browser); + delete request.browser; + } + + if (!article) { + this.log("Failed to parse page"); + this._runCallbacksAndFinish(request, null); + return; + } + + this.log("Parsing has been successful"); + + this._runCallbacksAndFinish(request, article); + }.bind(this)); + }.bind(this)); + } catch (e) { + this.log("Error downloading and parsing document: " + e); + this._runCallbacksAndFinish(request, null); + } +}, + +_getCacheDB: function Reader_getCacheDB(callback) { + if (this._cacheDB) { + callback(this._cacheDB); + return; + } + + // Throws a silent error, and does not call the onerror callback. request object itself becomes null. + let request = window.indexedDB.open("about:reader", this.DB_VERSION); + + if (!request){ + this.log("Error connecting to the cache DB\n"); + this._cacheDB = null; + callback(null); + return; + } + + request.onerror = function(event) { + //this.log("Error connecting to the cache DB"); + this._cacheDB = null; + callback(null); + + + }.bind(this); + + request.onsuccess = function(event) { + //this.log("Successfully connected to the cache DB"); + this._cacheDB = event.target.result; + callback(this._cacheDB); + }.bind(this); + + request.onupgradeneeded = function(event) { + this.log("Database schema upgrade from " + + event.oldVersion + " to " + event.newVersion); + + let cacheDB = event.target.result; + + // Create the articles object store + this.log("Creating articles object store"); + cacheDB.createObjectStore("articles", { keyPath: "url" }); + + this.log("Database upgrade done: " + this.DB_VERSION); + }.bind(this); +} +}; + + + diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -601,17 +601,19 @@ tooltiptext="&pageReportIcon.tooltip;" onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>