Bug 750712 - Add about:reader special url for reader mode pages (r=mfinkle)

This commit is contained in:
Lucas Rocha 2012-06-11 15:59:50 +01:00
parent 7d13aab294
commit 2e309072a5
11 changed files with 1770 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=480; initial-scale=.6667; user-scalable=0" />
<link rel="icon" type="image/png" href="chrome://branding/content/favicon32.png" />
<link rel="stylesheet" href="chrome://browser/skin/aboutReader.css" type="text/css"/>
</head>
<body onload="AboutReader.init();" onunload="AboutReader.uninit();">
<div id="reader-header" class="header">
</div>
<div id="reader-content" class="content">
</div>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutReader.js">
</script>
</body>
</html>

View File

@ -0,0 +1,120 @@
/* 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("Reader: " + s);
}
let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutReader.properties");
let AboutReader = {
init: function Reader_init() {
dump("Init()");
dump("Feching header and content notes from about:reader");
this._titleElement = document.getElementById("reader-header");
this._contentElement = document.getElementById("reader-content");
dump("Decoding query arguments");
let queryArgs = this._decodeQueryString(window.location.href);
let url = queryArgs.url;
if (url) {
dump("Fetching page with URL: " + url);
this._loadFromURL(url);
} else {
var tabId = queryArgs.tabId;
if (tabId) {
dump("Loading from tab with ID: " + tabId);
this._loadFromTab(tabId);
}
}
},
uninit: function Reader_uninit() {
this._hideContent();
delete this._titleElement;
delete this._contentElement;
},
_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) {
this._showProgress();
gChromeWin.Reader.parseDocumentFromTab(tabId, function(article) {
if (article)
this._showContent(article);
else
this._showError(gStrings.GetStringFromName("aboutReader.loadError"));
}.bind(this));
},
_showError: function Reader_showError(error) {
this._titleElement.style.display = "none";
this._contentElement.innerHTML = error;
this._contentElement.style.display = "block";
document.title = error;
},
_showContent: function Reader_showContent(article) {
this._titleElement.innerHTML = article.title;
this._titleElement.style.display = "block";
this._contentElement.innerHTML = article.content;
this._contentElement.style.display = "block";
document.title = article.title;
},
_hideContent: function Reader_hideContent() {
this._titleElement.style.display = "none";
this._contentElement.style.display = "none";
},
_showProgress: function Reader_showProgress() {
this._titleElement.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;
}
}

View File

@ -30,6 +30,7 @@ XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function() {
// Lazily-loaded browser scripts:
[
["SelectHelper", "chrome://browser/content/SelectHelper.js"],
["Readability", "chrome://browser/content/Readability.js"],
].forEach(function (aScript) {
let [name, script] = aScript;
XPCOMUtils.defineLazyGetter(window, name, function() {
@ -206,6 +207,7 @@ var BrowserApp = {
ActivityObserver.init();
WebappsUI.init();
RemoteDebugger.init();
Reader.init();
#ifdef ACCESSIBILITY
AccessFu.attach(window);
#endif
@ -407,6 +409,7 @@ var BrowserApp = {
SearchEngines.uninit();
WebappsUI.uninit();
RemoteDebugger.uninit();
Reader.uninit();
},
// This function returns false during periods where the browser displayed document is
@ -5338,3 +5341,288 @@ var RemoteDebugger = {
dump("Remote debugger stopped");
}
}
let Reader = {
// Version of the cache database schema
DB_VERSION: 1,
DEBUG: 1,
init: function Reader_init() {
this.log("Init()");
this._requests = {};
},
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);
}
},
parseDocumentFromTab: function(tabId, callback) {
try {
this.log("parseDocumentFromTab: " + tabId);
let tab = BrowserApp.getTabForId(tabId);
let url = tab.browser.contentWindow.location.href;
// 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;
}
// We need to clone the document before parsing because readability
// changes the document object in several ways to find the article
// in it.
let doc = tab.browser.contentWindow.document.cloneNode(true);
let uri = Services.io.newURI(url, null, null);
let readability = new Readability(uri, doc);
let article = readability.parse();
if (!article) {
this.log("Failed to parse page");
callback(null);
return;
}
// Append URL to the article data
article.url = url;
callback(article);
}.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: " + event.target.result);
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));
},
uninit: function Reader_uninit() {
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);
},
_runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) {
delete this._requests[request.url];
request.callbacks.forEach(function(callback) {
callback(result);
});
},
_dowloadDocument: 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;
this.log("Done loading: " + doc);
if (doc.location.href == "about:blank" || doc.defaultView.frameElement) {
callback(null);
// Request has finished with error, remove browser element
browser.parentNode.removeChild(browser);
return;
}
callback(doc);
// Request has finished, remove browser element
browser.parentNode.removeChild(browser);
}.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._dowloadDocument(url, function(doc) {
this.log("Finished loading page: " + doc);
// Delete reference to the browser element as we're
// now done with this request.
delete request.browser;
if (!doc) {
this.log("Error loading page");
this._runCallbacksAndFinish(request, null);
}
this.log("Parsing response with Readability");
let uri = Services.io.newURI(url, null, null);
let readability = new Readability(uri, doc);
let article = readability.parse();
if (!article) {
this.log("Failed to parse page");
this._runCallbacksAndFinish(request, null);
return;
}
this.log("Parsing has been successful");
// Append URL to the article data
article.url = url;
this._runCallbacksAndFinish(request, article);
}.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;
}
let request = window.mozIndexedDB.open("about:reader", this.DB_VERSION);
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);
}
};

View File

@ -14,6 +14,9 @@ chrome.jar:
content/aboutCertError.xhtml (content/aboutCertError.xhtml)
content/aboutDownloads.xhtml (content/aboutDownloads.xhtml)
content/aboutDownloads.js (content/aboutDownloads.js)
content/aboutReader.html (content/aboutReader.html)
content/aboutReader.js (content/aboutReader.js)
content/Readability.js (content/Readability.js)
content/aboutHome.xhtml (content/aboutHome.xhtml)
* content/aboutRights.xhtml (content/aboutRights.xhtml)
* content/aboutApps.xhtml (content/aboutApps.xhtml)

View File

@ -58,6 +58,10 @@ let modules = {
downloads: {
uri: "chrome://browser/content/aboutDownloads.xhtml",
privileged: true
},
reader: {
uri: "chrome://browser/content/aboutReader.html",
privileged: true
}
}

View File

@ -9,6 +9,7 @@ contract @mozilla.org/network/protocol/about;1?what=certerror {322ba47e-7047-4f7
contract @mozilla.org/network/protocol/about;1?what=home {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=apps {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=downloads {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=reader {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#ifdef MOZ_SAFE_BROWSING
contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif

View File

@ -0,0 +1,6 @@
# 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/.
aboutReader.loading=Loading...
aboutReader.loadError=Failed to load article from page

View File

@ -14,6 +14,7 @@
locale/@AB_CD@/browser/aboutCertError.dtd (%chrome/aboutCertError.dtd)
locale/@AB_CD@/browser/aboutDownloads.dtd (%chrome/aboutDownloads.dtd)
locale/@AB_CD@/browser/aboutDownloads.properties (%chrome/aboutDownloads.properties)
locale/@AB_CD@/browser/aboutReader.properties (%chrome/aboutReader.properties)
locale/@AB_CD@/browser/browser.properties (%chrome/browser.properties)
locale/@AB_CD@/browser/config.dtd (%chrome/config.dtd)
locale/@AB_CD@/browser/config.properties (%chrome/config.properties)

View File

@ -0,0 +1,37 @@
/* 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/. */
%filter substitution
%include defines.inc
html {
font-family: "Droid Sans",helvetica,arial,clean,sans-serif;
font-size: 24px;
background: #FFFFFF;
-moz-text-size-adjust: none;
}
body {
margin: 20px;
}
.header {
width: 100%;
color: black;
text-align: center;
font-size: 30px;
font-weight: bold;
padding-top: 30px;
padding-bottom: 30px;
border-bottom: 4px solid;
-moz-border-bottom-colors: #ff9100 #f27900;
display: none;
}
.content {
font-family: "Droid Serif",serif;
padding-top: 30px;
padding-bottom: 30px;
display: none;
}

View File

@ -11,6 +11,7 @@ chrome.jar:
skin/aboutAddons.css (aboutAddons.css)
skin/aboutApps.css (aboutApps.css)
* skin/aboutDownloads.css (aboutDownloads.css)
* skin/aboutReader.css (aboutReader.css)
* skin/browser.css (browser.css)
* skin/content.css (content.css)
skin/config.css (config.css)