mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 09:18:42 +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
199
toolkit/components/reader/.eslintrc.js
Normal file
199
toolkit/components/reader/.eslintrc.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"rules": {
|
||||
// Braces only needed for multi-line arrow function blocks
|
||||
// "arrow-body-style": ["error", "as-needed"],
|
||||
|
||||
// Require spacing around =>
|
||||
// "arrow-spacing": "error",
|
||||
|
||||
// Always require spacing around a single line block
|
||||
// "block-spacing": "warn",
|
||||
|
||||
// No newline before open brace for a block
|
||||
"brace-style": "error",
|
||||
|
||||
// No space before always a space after a comma
|
||||
"comma-spacing": ["error", {"before": false, "after": true}],
|
||||
|
||||
// Commas at the end of the line not the start
|
||||
// "comma-style": "error",
|
||||
|
||||
// Don't require spaces around computed properties
|
||||
// "computed-property-spacing": ["error", "never"],
|
||||
|
||||
// Functions must always return something or nothing
|
||||
"consistent-return": "error",
|
||||
|
||||
// Require braces around blocks that start a new line
|
||||
// Note that this rule is likely to be overridden on a per-directory basis
|
||||
// very frequently.
|
||||
// "curly": ["error", "multi-line"],
|
||||
|
||||
// Always require a trailing EOL
|
||||
"eol-last": "error",
|
||||
|
||||
// Require function* name()
|
||||
// "generator-star-spacing": ["error", {"before": false, "after": true}],
|
||||
|
||||
// Two space indent
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
|
||||
// Space after colon not before in property declarations
|
||||
"key-spacing": ["error", { "beforeColon": false, "afterColon": true, "mode": "minimum" }],
|
||||
|
||||
// Unix linebreaks
|
||||
"linebreak-style": ["error", "unix"],
|
||||
|
||||
// Always require parenthesis for new calls
|
||||
"new-parens": "error",
|
||||
|
||||
// Use [] instead of Array()
|
||||
// "no-array-constructor": "error",
|
||||
|
||||
// No duplicate arguments in function declarations
|
||||
"no-dupe-args": "error",
|
||||
|
||||
// No duplicate keys in object declarations
|
||||
"no-dupe-keys": "error",
|
||||
|
||||
// No duplicate cases in switch statements
|
||||
"no-duplicate-case": "error",
|
||||
|
||||
// No labels
|
||||
"no-labels": "error",
|
||||
|
||||
// If an if block ends with a return no need for an else block
|
||||
"no-else-return": "error",
|
||||
|
||||
// No empty statements
|
||||
"no-empty": "error",
|
||||
|
||||
// No empty character classes in regex
|
||||
"no-empty-character-class": "error",
|
||||
|
||||
// Disallow empty destructuring
|
||||
"no-empty-pattern": "error",
|
||||
|
||||
// No assiging to exception variable
|
||||
// "no-ex-assign": "error",
|
||||
|
||||
// No using !! where casting to boolean is already happening
|
||||
// "no-extra-boolean-cast": "error",
|
||||
|
||||
// No double semicolon
|
||||
"no-extra-semi": "error",
|
||||
|
||||
// No overwriting defined functions
|
||||
"no-func-assign": "error",
|
||||
|
||||
// Declarations in Program or Function Body
|
||||
"no-inner-declarations": "error",
|
||||
|
||||
// No invalid regular expresions
|
||||
"no-invalid-regexp": "error",
|
||||
|
||||
// No odd whitespace characters
|
||||
"no-irregular-whitespace": "error",
|
||||
|
||||
// No single if block inside an else block
|
||||
"no-lonely-if": "error",
|
||||
|
||||
// No mixing spaces and tabs in indent
|
||||
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
||||
|
||||
// No unnecessary spacing
|
||||
"no-multi-spaces": ["error", { exceptions: { "AssignmentExpression": true, "VariableDeclarator": true, "ArrayExpression": true, "ObjectExpression": true } }],
|
||||
|
||||
// No reassigning native JS objects
|
||||
"no-native-reassign": "error",
|
||||
|
||||
// No (!foo in bar)
|
||||
"no-negated-in-lhs": "error",
|
||||
|
||||
// Nested ternary statements are confusing
|
||||
"no-nested-ternary": "error",
|
||||
|
||||
// Use {} instead of new Object()
|
||||
// "no-new-object": "error",
|
||||
|
||||
// No Math() or JSON()
|
||||
"no-obj-calls": "error",
|
||||
|
||||
// No octal literals
|
||||
"no-octal": "error",
|
||||
|
||||
// No redeclaring variables
|
||||
"no-redeclare": "error",
|
||||
|
||||
// No unnecessary comparisons
|
||||
"no-self-compare": "error",
|
||||
|
||||
// No declaring variables from an outer scope
|
||||
"no-shadow": "error",
|
||||
|
||||
// No declaring variables that hide things like arguments
|
||||
"no-shadow-restricted-names": "error",
|
||||
|
||||
// No spaces between function name and parentheses
|
||||
"no-spaced-func": "error",
|
||||
|
||||
// No trailing whitespace
|
||||
"no-trailing-spaces": "error",
|
||||
|
||||
// No using undeclared variables
|
||||
// "no-undef": "error",
|
||||
|
||||
// Error on newline where a semicolon is needed
|
||||
"no-unexpected-multiline": "error",
|
||||
|
||||
// No unreachable statements
|
||||
"no-unreachable": "error",
|
||||
|
||||
// No expressions where a statement is expected
|
||||
// "no-unused-expressions": "error",
|
||||
|
||||
// No declaring variables that are never used
|
||||
"no-unused-vars": ["error", {"vars": "all", "args": "none"}],
|
||||
|
||||
// No using variables before defined
|
||||
// "no-use-before-define": ["error", "nofunc"],
|
||||
|
||||
// No using with
|
||||
"no-with": "error",
|
||||
|
||||
// Always require semicolon at end of statement
|
||||
"semi": ["error", "always"],
|
||||
|
||||
// Require space after keywords
|
||||
"keyword-spacing": "error",
|
||||
|
||||
// Require space before blocks
|
||||
"space-before-blocks": "error",
|
||||
|
||||
// Never use spaces before function parentheses
|
||||
// "space-before-function-paren": ["error", { "anonymous": "always", "named": "never" }],
|
||||
|
||||
// Require spaces before finally, catch, etc.
|
||||
// "space-before-keywords": ["error", "always"],
|
||||
|
||||
// No space padding in parentheses
|
||||
// "space-in-parens": ["error", "never"],
|
||||
|
||||
// Require spaces around operators
|
||||
// "space-infix-ops": "error",
|
||||
|
||||
// Require spaces after return, throw and case
|
||||
// "space-return-throw-case": "error",
|
||||
|
||||
// ++ and -- should not need spacing
|
||||
// "space-unary-ops": ["error", { "words": true, "nonwords": false }],
|
||||
|
||||
// No comparisons to NaN
|
||||
"use-isnan": "error",
|
||||
|
||||
// Only check typeof against valid results
|
||||
"valid-typeof": "error",
|
||||
},
|
||||
}
|
||||
997
toolkit/components/reader/AboutReader.jsm
Normal file
997
toolkit/components/reader/AboutReader.jsm
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "AboutReader" ];
|
||||
|
||||
Cu.import("resource://gre/modules/ReaderMode.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
|
||||
|
||||
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
|
||||
|
||||
var AboutReader = function(mm, win, articlePromise) {
|
||||
let url = this._getOriginalUrl(win);
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
let errorMsg = "Only http:// and https:// URLs can be loaded in about:reader.";
|
||||
if (Services.prefs.getBoolPref("reader.errors.includeURLs"))
|
||||
errorMsg += " Tried to load: " + url + ".";
|
||||
Cu.reportError(errorMsg);
|
||||
win.location.href = "about:blank";
|
||||
return;
|
||||
}
|
||||
|
||||
let doc = win.document;
|
||||
|
||||
this._mm = mm;
|
||||
this._mm.addMessageListener("Reader:CloseDropdown", this);
|
||||
this._mm.addMessageListener("Reader:AddButton", this);
|
||||
this._mm.addMessageListener("Reader:RemoveButton", this);
|
||||
this._mm.addMessageListener("Reader:GetStoredArticleData", this);
|
||||
|
||||
this._docRef = Cu.getWeakReference(doc);
|
||||
this._winRef = Cu.getWeakReference(win);
|
||||
this._innerWindowId = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
|
||||
|
||||
this._article = null;
|
||||
|
||||
if (articlePromise) {
|
||||
this._articlePromise = articlePromise;
|
||||
}
|
||||
|
||||
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("moz-reader-content"));
|
||||
this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
|
||||
this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
|
||||
|
||||
this._scrollOffset = win.pageYOffset;
|
||||
|
||||
doc.addEventListener("click", this, false);
|
||||
|
||||
win.addEventListener("pagehide", this, false);
|
||||
win.addEventListener("scroll", this, false);
|
||||
win.addEventListener("resize", this, false);
|
||||
|
||||
Services.obs.addObserver(this, "inner-window-destroyed", false);
|
||||
|
||||
doc.addEventListener("visibilitychange", this, false);
|
||||
|
||||
this._setupStyleDropdown();
|
||||
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
|
||||
|
||||
const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
|
||||
if (gIsFirefoxDesktop) {
|
||||
// we're ready for any external setup, send a signal for that.
|
||||
this._mm.sendAsyncMessage("Reader:OnSetup");
|
||||
}
|
||||
|
||||
let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
|
||||
let colorSchemeOptions = colorSchemeValues.map((value) => {
|
||||
return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
|
||||
value: value,
|
||||
itemClass: value + "-button" };
|
||||
});
|
||||
|
||||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||||
this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this));
|
||||
this._setColorSchemePref(colorScheme);
|
||||
|
||||
let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample");
|
||||
let fontTypeOptions = [
|
||||
{ name: fontTypeSample,
|
||||
description: gStrings.GetStringFromName("aboutReader.fontType.sans-serif"),
|
||||
value: "sans-serif",
|
||||
itemClass: "sans-serif-button"
|
||||
},
|
||||
{ name: fontTypeSample,
|
||||
description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
|
||||
value: "serif",
|
||||
itemClass: "serif-button" },
|
||||
];
|
||||
|
||||
let fontType = Services.prefs.getCharPref("reader.font_type");
|
||||
this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
|
||||
this._setFontType(fontType);
|
||||
|
||||
this._setupFontSizeButtons();
|
||||
|
||||
this._setupContentWidthButtons();
|
||||
|
||||
this._setupLineHeightButtons();
|
||||
|
||||
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
|
||||
new NarrateControls(mm, win);
|
||||
}
|
||||
|
||||
this._loadArticle();
|
||||
};
|
||||
|
||||
AboutReader.prototype = {
|
||||
_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();
|
||||
},
|
||||
|
||||
get _messageElement() {
|
||||
return this._messageElementRef.get();
|
||||
},
|
||||
|
||||
get _isToolbarVertical() {
|
||||
if (this._toolbarVertical !== undefined) {
|
||||
return this._toolbarVertical;
|
||||
}
|
||||
return this._toolbarVertical = Services.prefs.getBoolPref("reader.toolbar.vertical");
|
||||
},
|
||||
|
||||
// Provides unique view Id.
|
||||
get viewId() {
|
||||
let _viewId = Cc["@mozilla.org/uuid-generator;1"].
|
||||
getService(Ci.nsIUUIDGenerator).generateUUID().toString();
|
||||
Object.defineProperty(this, "viewId", { value: _viewId });
|
||||
|
||||
return _viewId;
|
||||
},
|
||||
|
||||
receiveMessage: function (message) {
|
||||
switch (message.name) {
|
||||
// Triggered by Android user pressing BACK while the banner font-dropdown is open.
|
||||
case "Reader:CloseDropdown": {
|
||||
// Just close it.
|
||||
this._closeDropdowns();
|
||||
break;
|
||||
}
|
||||
|
||||
case "Reader:AddButton": {
|
||||
if (message.data.id && message.data.image &&
|
||||
!this._doc.getElementById(message.data.id)) {
|
||||
let btn = this._doc.createElement("button");
|
||||
btn.setAttribute("class", "button");
|
||||
btn.setAttribute("style", "background-image: url('" + message.data.image + "')");
|
||||
btn.setAttribute("id", message.data.id);
|
||||
if (message.data.title)
|
||||
btn.setAttribute("title", message.data.title);
|
||||
if (message.data.text)
|
||||
btn.textContent = message.data.text;
|
||||
let tb = this._doc.getElementById("reader-toolbar");
|
||||
tb.appendChild(btn);
|
||||
this._setupButton(message.data.id, button => {
|
||||
this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Reader:RemoveButton": {
|
||||
if (message.data.id) {
|
||||
let btn = this._doc.getElementById(message.data.id);
|
||||
if (btn)
|
||||
btn.remove();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Reader:GetStoredArticleData": {
|
||||
this._mm.sendAsyncMessage("Reader:StoredArticleData", { article: this._article });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
if (!aEvent.isTrusted)
|
||||
return;
|
||||
|
||||
switch (aEvent.type) {
|
||||
case "click":
|
||||
let target = aEvent.target;
|
||||
if (target.classList.contains('dropdown-toggle')) {
|
||||
this._toggleDropdownClicked(aEvent);
|
||||
} else if (!target.closest('.dropdown-popup')) {
|
||||
this._closeDropdowns();
|
||||
}
|
||||
break;
|
||||
case "scroll":
|
||||
this._closeDropdowns(true);
|
||||
let isScrollingUp = this._scrollOffset > aEvent.pageY;
|
||||
this._setSystemUIVisibility(isScrollingUp);
|
||||
this._scrollOffset = aEvent.pageY;
|
||||
break;
|
||||
case "resize":
|
||||
this._updateImageMargins();
|
||||
if (this._isToolbarVertical) {
|
||||
this._win.setTimeout(() => {
|
||||
for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
|
||||
this._updatePopupPosition(dropdown);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case "devicelight":
|
||||
this._handleDeviceLight(aEvent.value);
|
||||
break;
|
||||
|
||||
case "visibilitychange":
|
||||
this._handleVisibilityChange();
|
||||
break;
|
||||
|
||||
case "pagehide":
|
||||
// Close the Banners Font-dropdown, cleanup Android BackPressListener.
|
||||
this._closeDropdowns();
|
||||
|
||||
this._mm.removeMessageListener("Reader:CloseDropdown", this);
|
||||
this._mm.removeMessageListener("Reader:AddButton", this);
|
||||
this._mm.removeMessageListener("Reader:RemoveButton", this);
|
||||
this._mm.removeMessageListener("Reader:GetStoredArticleData", this);
|
||||
this._windowUnloaded = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
Services.obs.removeObserver(this, "inner-window-destroyed", false);
|
||||
|
||||
this._mm.removeMessageListener("Reader:CloseDropdown", this);
|
||||
this._mm.removeMessageListener("Reader:AddButton", this);
|
||||
this._mm.removeMessageListener("Reader:RemoveButton", this);
|
||||
this._windowUnloaded = true;
|
||||
},
|
||||
|
||||
_onReaderClose: function() {
|
||||
ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
|
||||
},
|
||||
|
||||
_setFontSize: function(newFontSize) {
|
||||
let containerClasses = this._doc.getElementById("container").classList;
|
||||
|
||||
if (this._fontSize > 0)
|
||||
containerClasses.remove("font-size" + this._fontSize);
|
||||
|
||||
this._fontSize = newFontSize;
|
||||
containerClasses.add("font-size" + this._fontSize);
|
||||
return AsyncPrefs.set("reader.font_size", this._fontSize);
|
||||
},
|
||||
|
||||
_setupFontSizeButtons: function() {
|
||||
const FONT_SIZE_MIN = 1;
|
||||
const FONT_SIZE_MAX = 9;
|
||||
|
||||
// Sample text shown in Android UI.
|
||||
let sampleText = this._doc.getElementById("font-size-sample");
|
||||
sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
|
||||
|
||||
let currentSize = Services.prefs.getIntPref("reader.font_size");
|
||||
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
|
||||
|
||||
let plusButton = this._doc.getElementById("font-size-plus");
|
||||
let minusButton = this._doc.getElementById("font-size-minus");
|
||||
|
||||
function updateControls() {
|
||||
if (currentSize === FONT_SIZE_MIN) {
|
||||
minusButton.setAttribute("disabled", true);
|
||||
} else {
|
||||
minusButton.removeAttribute("disabled");
|
||||
}
|
||||
if (currentSize === FONT_SIZE_MAX) {
|
||||
plusButton.setAttribute("disabled", true);
|
||||
} else {
|
||||
plusButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
updateControls();
|
||||
this._setFontSize(currentSize);
|
||||
|
||||
plusButton.addEventListener("click", (event) => {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
if (currentSize >= FONT_SIZE_MAX) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentSize++;
|
||||
updateControls();
|
||||
this._setFontSize(currentSize);
|
||||
}, true);
|
||||
|
||||
minusButton.addEventListener("click", (event) => {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
if (currentSize <= FONT_SIZE_MIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentSize--;
|
||||
updateControls();
|
||||
this._setFontSize(currentSize);
|
||||
}, true);
|
||||
},
|
||||
|
||||
_setContentWidth: function(newContentWidth) {
|
||||
let containerClasses = this._doc.getElementById("container").classList;
|
||||
|
||||
if (this._contentWidth > 0)
|
||||
containerClasses.remove("content-width" + this._contentWidth);
|
||||
|
||||
this._contentWidth = newContentWidth;
|
||||
containerClasses.add("content-width" + this._contentWidth);
|
||||
return AsyncPrefs.set("reader.content_width", this._contentWidth);
|
||||
},
|
||||
|
||||
_setupContentWidthButtons: function() {
|
||||
const CONTENT_WIDTH_MIN = 1;
|
||||
const CONTENT_WIDTH_MAX = 9;
|
||||
|
||||
let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
|
||||
currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
|
||||
|
||||
let plusButton = this._doc.getElementById("content-width-plus");
|
||||
let minusButton = this._doc.getElementById("content-width-minus");
|
||||
|
||||
function updateControls() {
|
||||
if (currentContentWidth === CONTENT_WIDTH_MIN) {
|
||||
minusButton.setAttribute("disabled", true);
|
||||
} else {
|
||||
minusButton.removeAttribute("disabled");
|
||||
}
|
||||
if (currentContentWidth === CONTENT_WIDTH_MAX) {
|
||||
plusButton.setAttribute("disabled", true);
|
||||
} else {
|
||||
plusButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
updateControls();
|
||||
this._setContentWidth(currentContentWidth);
|
||||
|
||||
plusButton.addEventListener("click", (event) => {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
if (currentContentWidth >= CONTENT_WIDTH_MAX) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentContentWidth++;
|
||||
updateControls();
|
||||
this._setContentWidth(currentContentWidth);
|
||||
}, true);
|
||||
|
||||
minusButton.addEventListener("click", (event) => {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
if (currentContentWidth <= CONTENT_WIDTH_MIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentContentWidth--;
|
||||
updateControls();
|
||||
this._setContentWidth(currentContentWidth);
|
||||
}, true);
|
||||
},
|
||||
|
||||
_setLineHeight: function(newLineHeight) {
|
||||
let contentClasses = this._doc.getElementById("moz-reader-content").classList;
|
||||
|
||||
if (this._lineHeight > 0)
|
||||
contentClasses.remove("line-height" + this._lineHeight);
|
||||
|
||||
this._lineHeight = newLineHeight;
|
||||
contentClasses.add("line-height" + this._lineHeight);
|
||||
return AsyncPrefs.set("reader.line_height", this._lineHeight);
|
||||
},
|
||||
|
||||
_setupLineHeightButtons: function() {
|
||||
const LINE_HEIGHT_MIN = 1;
|
||||
const LINE_HEIGHT_MAX = 9;
|
||||
|
||||
let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
|
||||
currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
|
||||
|
||||
let plusButton = this._doc.getElementById("line-height-plus");
|
||||
let minusButton = this._doc.getElementById("line-height-minus");
|
||||
|
||||
function updateControls() {
|
||||
if (currentLineHeight === LINE_HEIGHT_MIN) {
|
||||
minusButton.setAttribute("disabled", true);
|
||||
} else {
|
||||
minusButton.removeAttribute("disabled");
|
||||
}
|
||||
if (currentLineHeight === LINE_HEIGHT_MAX) {
|
||||
plusButton.setAttribute("disabled", true);
|
||||
} else {
|
||||
plusButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
updateControls();
|
||||
this._setLineHeight(currentLineHeight);
|
||||
|
||||
plusButton.addEventListener("click", (event) => {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
if (currentLineHeight >= LINE_HEIGHT_MAX) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentLineHeight++;
|
||||
updateControls();
|
||||
this._setLineHeight(currentLineHeight);
|
||||
}, true);
|
||||
|
||||
minusButton.addEventListener("click", (event) => {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
if (currentLineHeight <= LINE_HEIGHT_MIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentLineHeight--;
|
||||
updateControls();
|
||||
this._setLineHeight(currentLineHeight);
|
||||
}, true);
|
||||
},
|
||||
|
||||
_handleDeviceLight: function(newLux) {
|
||||
// Desired size of the this._luxValues array.
|
||||
let luxValuesSize = 10;
|
||||
// Add new lux value at the front of the array.
|
||||
this._luxValues.unshift(newLux);
|
||||
// Add new lux value to this._totalLux for averaging later.
|
||||
this._totalLux += newLux;
|
||||
|
||||
// Don't update when length of array is less than luxValuesSize except when it is 1.
|
||||
if (this._luxValues.length < luxValuesSize) {
|
||||
// Use the first lux value to set the color scheme until our array equals luxValuesSize.
|
||||
if (this._luxValues.length == 1) {
|
||||
this._updateColorScheme(newLux);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Holds the average of the lux values collected in this._luxValues.
|
||||
let averageLuxValue = this._totalLux/luxValuesSize;
|
||||
|
||||
this._updateColorScheme(averageLuxValue);
|
||||
// Pop the oldest value off the array.
|
||||
let oldLux = this._luxValues.pop();
|
||||
// Subtract oldLux since it has been discarded from the array.
|
||||
this._totalLux -= oldLux;
|
||||
},
|
||||
|
||||
_handleVisibilityChange: function() {
|
||||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||||
if (colorScheme != "auto") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Turn off the ambient light sensor if the page is hidden
|
||||
this._enableAmbientLighting(!this._doc.hidden);
|
||||
},
|
||||
|
||||
// Setup or teardown the ambient light tracking system.
|
||||
_enableAmbientLighting: function(enable) {
|
||||
if (enable) {
|
||||
this._win.addEventListener("devicelight", this, false);
|
||||
this._luxValues = [];
|
||||
this._totalLux = 0;
|
||||
} else {
|
||||
this._win.removeEventListener("devicelight", this, false);
|
||||
delete this._luxValues;
|
||||
delete this._totalLux;
|
||||
}
|
||||
},
|
||||
|
||||
_updateColorScheme: function(luxValue) {
|
||||
// Upper bound value for "dark" color scheme beyond which it changes to "light".
|
||||
let upperBoundDark = 50;
|
||||
// Lower bound value for "light" color scheme beyond which it changes to "dark".
|
||||
let lowerBoundLight = 10;
|
||||
// Threshold for color scheme change.
|
||||
let colorChangeThreshold = 20;
|
||||
|
||||
// Ignore changes that are within a certain threshold of previous lux values.
|
||||
if ((this._colorScheme === "dark" && luxValue < upperBoundDark) ||
|
||||
(this._colorScheme === "light" && luxValue > lowerBoundLight))
|
||||
return;
|
||||
|
||||
if (luxValue < colorChangeThreshold)
|
||||
this._setColorScheme("dark");
|
||||
else
|
||||
this._setColorScheme("light");
|
||||
},
|
||||
|
||||
_setColorScheme: function(newColorScheme) {
|
||||
// "auto" is not a real color scheme
|
||||
if (this._colorScheme === newColorScheme || newColorScheme === "auto")
|
||||
return;
|
||||
|
||||
let bodyClasses = this._doc.body.classList;
|
||||
|
||||
if (this._colorScheme)
|
||||
bodyClasses.remove(this._colorScheme);
|
||||
|
||||
this._colorScheme = newColorScheme;
|
||||
bodyClasses.add(this._colorScheme);
|
||||
},
|
||||
|
||||
// Pref values include "dark", "light", and "auto", which automatically switches
|
||||
// between light and dark color schemes based on the ambient light level.
|
||||
_setColorSchemePref: function(colorSchemePref) {
|
||||
this._enableAmbientLighting(colorSchemePref === "auto");
|
||||
this._setColorScheme(colorSchemePref);
|
||||
|
||||
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
|
||||
},
|
||||
|
||||
_setFontType: function(newFontType) {
|
||||
if (this._fontType === newFontType)
|
||||
return;
|
||||
|
||||
let bodyClasses = this._doc.body.classList;
|
||||
|
||||
if (this._fontType)
|
||||
bodyClasses.remove(this._fontType);
|
||||
|
||||
this._fontType = newFontType;
|
||||
bodyClasses.add(this._fontType);
|
||||
|
||||
AsyncPrefs.set("reader.font_type", this._fontType);
|
||||
},
|
||||
|
||||
_setSystemUIVisibility: function(visible) {
|
||||
this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
|
||||
},
|
||||
|
||||
_loadArticle: Task.async(function* () {
|
||||
let url = this._getOriginalUrl();
|
||||
this._showProgressDelayed();
|
||||
|
||||
let article;
|
||||
if (this._articlePromise) {
|
||||
article = yield this._articlePromise;
|
||||
} else {
|
||||
try {
|
||||
article = yield this._getArticle(url);
|
||||
} catch (e) {
|
||||
if (e && e.newURL) {
|
||||
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
|
||||
this._win.location.replace(readerURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._windowUnloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the loading message with an error message if there's a failure.
|
||||
// Users are supposed to navigate away by themselves (because we cannot
|
||||
// remove ourselves from session history.)
|
||||
if (!article) {
|
||||
this._showError();
|
||||
return;
|
||||
}
|
||||
|
||||
this._showContent(article);
|
||||
}),
|
||||
|
||||
_getArticle: function(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let listener = (message) => {
|
||||
this._mm.removeMessageListener("Reader:ArticleData", listener);
|
||||
if (message.data.newURL) {
|
||||
reject({ newURL: message.data.newURL });
|
||||
return;
|
||||
}
|
||||
resolve(message.data.article);
|
||||
};
|
||||
this._mm.addMessageListener("Reader:ArticleData", listener);
|
||||
this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
|
||||
});
|
||||
},
|
||||
|
||||
_requestFavicon: function() {
|
||||
let handleFaviconReturn = (message) => {
|
||||
this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
|
||||
this._loadFavicon(message.data.url, message.data.faviconUrl);
|
||||
};
|
||||
|
||||
this._mm.addMessageListener("Reader:FaviconReturn", handleFaviconReturn);
|
||||
this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
|
||||
},
|
||||
|
||||
_loadFavicon: function(url, faviconUrl) {
|
||||
if (this._article.url !== url)
|
||||
return;
|
||||
|
||||
let doc = this._doc;
|
||||
|
||||
let link = doc.createElement('link');
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = faviconUrl;
|
||||
|
||||
doc.getElementsByTagName('head')[0].appendChild(link);
|
||||
},
|
||||
|
||||
_updateImageMargins: function() {
|
||||
let windowWidth = this._win.innerWidth;
|
||||
let bodyWidth = this._doc.body.clientWidth;
|
||||
|
||||
let setImageMargins = function(img) {
|
||||
// If the image is at least as wide as the window, make it fill edge-to-edge on mobile.
|
||||
if (img.naturalWidth >= windowWidth) {
|
||||
img.setAttribute("moz-reader-full-width", true);
|
||||
} else {
|
||||
img.removeAttribute("moz-reader-full-width");
|
||||
}
|
||||
|
||||
// If the image is at least half as wide as the body, center it on desktop.
|
||||
if (img.naturalWidth >= bodyWidth/2) {
|
||||
img.setAttribute("moz-reader-center", true);
|
||||
} else {
|
||||
img.removeAttribute("moz-reader-center");
|
||||
}
|
||||
};
|
||||
|
||||
let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR);
|
||||
for (let i = imgs.length; --i >= 0;) {
|
||||
let img = imgs[i];
|
||||
|
||||
if (img.naturalWidth > 0) {
|
||||
setImageMargins(img);
|
||||
} else {
|
||||
img.onload = function() {
|
||||
setImageMargins(img);
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
|
||||
if (!article.dir)
|
||||
return;
|
||||
|
||||
// Set "dir" attribute on content
|
||||
this._contentElement.setAttribute("dir", article.dir);
|
||||
this._headerElement.setAttribute("dir", article.dir);
|
||||
},
|
||||
|
||||
_fixLocalLinks() {
|
||||
// We need to do this because preprocessing the content through nsIParserUtils
|
||||
// gives back a DOM with a <base> element. That influences how these URLs get
|
||||
// resolved, making them no longer match the document URI (which is
|
||||
// about:reader?url=...). To fix this, make all the hash URIs absolute. This
|
||||
// is hacky, but the alternative of removing the base element has potential
|
||||
// security implications if Readability has not successfully made all the URLs
|
||||
// absolute, so we pick just fixing these in-document links explicitly.
|
||||
let localLinks = this._contentElement.querySelectorAll("a[href^='#']");
|
||||
for (let localLink of localLinks) {
|
||||
// Have to get the attribute because .href provides an absolute URI.
|
||||
localLink.href = this._doc.documentURI + localLink.getAttribute("href");
|
||||
}
|
||||
},
|
||||
|
||||
_showError: function() {
|
||||
this._headerElement.style.display = "none";
|
||||
this._contentElement.style.display = "none";
|
||||
|
||||
let errorMessage = gStrings.GetStringFromName("aboutReader.loadError");
|
||||
this._messageElement.textContent = errorMessage;
|
||||
this._messageElement.style.display = "block";
|
||||
|
||||
this._doc.title = errorMessage;
|
||||
|
||||
this._error = true;
|
||||
},
|
||||
|
||||
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
|
||||
_stripHost: function(host) {
|
||||
if (!host)
|
||||
return host;
|
||||
|
||||
let start = 0;
|
||||
|
||||
if (host.startsWith("www."))
|
||||
start = 4;
|
||||
else if (host.startsWith("m."))
|
||||
start = 2;
|
||||
else if (host.startsWith("mobile."))
|
||||
start = 7;
|
||||
|
||||
return host.substring(start);
|
||||
},
|
||||
|
||||
_showContent: function(article) {
|
||||
this._messageElement.style.display = "none";
|
||||
|
||||
this._article = article;
|
||||
|
||||
this._domainElement.href = article.url;
|
||||
let articleUri = Services.io.newURI(article.url, null, null);
|
||||
this._domainElement.textContent = this._stripHost(articleUri.host);
|
||||
this._creditsElement.textContent = article.byline;
|
||||
|
||||
this._titleElement.textContent = article.title;
|
||||
this._doc.title = article.title;
|
||||
|
||||
this._headerElement.style.display = "block";
|
||||
|
||||
let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
|
||||
let contentFragment = parserUtils.parseFragment(article.content,
|
||||
Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle,
|
||||
false, articleUri, this._contentElement);
|
||||
this._contentElement.innerHTML = "";
|
||||
this._contentElement.appendChild(contentFragment);
|
||||
this._fixLocalLinks();
|
||||
this._maybeSetTextDirection(article);
|
||||
|
||||
this._contentElement.style.display = "block";
|
||||
this._updateImageMargins();
|
||||
|
||||
this._requestFavicon();
|
||||
this._doc.body.classList.add("loaded");
|
||||
|
||||
this._goToReference(articleUri.ref);
|
||||
|
||||
Services.obs.notifyObservers(this._win, "AboutReader:Ready", "");
|
||||
|
||||
this._doc.dispatchEvent(
|
||||
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
|
||||
},
|
||||
|
||||
_hideContent: function() {
|
||||
this._headerElement.style.display = "none";
|
||||
this._contentElement.style.display = "none";
|
||||
},
|
||||
|
||||
_showProgressDelayed: function() {
|
||||
this._win.setTimeout(function() {
|
||||
// No need to show progress if the article has been loaded,
|
||||
// if the window has been unloaded, or if there was an error
|
||||
// trying to load the article.
|
||||
if (this._article || this._windowUnloaded || this._error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._headerElement.style.display = "none";
|
||||
this._contentElement.style.display = "none";
|
||||
|
||||
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
|
||||
this._messageElement.style.display = "block";
|
||||
}.bind(this), 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the original article URL for this about:reader view.
|
||||
*/
|
||||
_getOriginalUrl: function(win) {
|
||||
let url = win ? win.location.href : this._win.location.href;
|
||||
return ReaderMode.getOriginalUrl(url) || url;
|
||||
},
|
||||
|
||||
_setupSegmentedButton: function(id, options, initialValue, callback) {
|
||||
let doc = this._doc;
|
||||
let segmentedButton = doc.getElementById(id);
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
let option = options[i];
|
||||
|
||||
let item = doc.createElement("button");
|
||||
|
||||
// Put the name in a div so that Android can hide it.
|
||||
let div = doc.createElement("div");
|
||||
div.textContent = option.name;
|
||||
div.classList.add("name");
|
||||
item.appendChild(div);
|
||||
|
||||
if (option.itemClass !== undefined)
|
||||
item.classList.add(option.itemClass);
|
||||
|
||||
if (option.description !== undefined) {
|
||||
let description = doc.createElement("div");
|
||||
description.textContent = option.description;
|
||||
description.classList.add("description");
|
||||
item.appendChild(description);
|
||||
}
|
||||
|
||||
segmentedButton.appendChild(item);
|
||||
|
||||
item.addEventListener("click", function(aEvent) {
|
||||
if (!aEvent.isTrusted)
|
||||
return;
|
||||
|
||||
aEvent.stopPropagation();
|
||||
|
||||
// Just pass the ID of the button as an extra and hope the ID doesn't change
|
||||
// unless the context changes
|
||||
UITelemetry.addEvent("action.1", "button", null, id);
|
||||
|
||||
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(id, callback, titleEntity, textEntity) {
|
||||
if (titleEntity) {
|
||||
this._setButtonTip(id, titleEntity);
|
||||
}
|
||||
|
||||
let button = this._doc.getElementById(id);
|
||||
if (textEntity) {
|
||||
button.textContent = gStrings.GetStringFromName(textEntity);
|
||||
}
|
||||
button.removeAttribute("hidden");
|
||||
button.addEventListener("click", function(aEvent) {
|
||||
if (!aEvent.isTrusted)
|
||||
return;
|
||||
|
||||
aEvent.stopPropagation();
|
||||
let btn = aEvent.target;
|
||||
callback(btn);
|
||||
}, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a toolTip for a button. Performed at initial button setup
|
||||
* and dynamically as button state changes.
|
||||
* @param Localizable string providing UI element usage tip.
|
||||
*/
|
||||
_setButtonTip: function(id, titleEntity) {
|
||||
let button = this._doc.getElementById(id);
|
||||
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
|
||||
},
|
||||
|
||||
_setupStyleDropdown: function() {
|
||||
let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
|
||||
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
|
||||
},
|
||||
|
||||
_updatePopupPosition: function(dropdown) {
|
||||
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
|
||||
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
|
||||
|
||||
let toggleHeight = dropdownToggle.offsetHeight;
|
||||
let toggleTop = dropdownToggle.offsetTop;
|
||||
let popupTop = toggleTop - toggleHeight / 2;
|
||||
|
||||
dropdownPopup.style.top = popupTop + "px";
|
||||
},
|
||||
|
||||
_toggleDropdownClicked: function(event) {
|
||||
let dropdown = event.target.closest('.dropdown');
|
||||
|
||||
if (!dropdown)
|
||||
return;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
if (dropdown.classList.contains("open")) {
|
||||
this._closeDropdowns();
|
||||
} else {
|
||||
this._openDropdown(dropdown);
|
||||
if (this._isToolbarVertical) {
|
||||
this._updatePopupPosition(dropdown);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* If the ReaderView banner font-dropdown is closed, open it.
|
||||
*/
|
||||
_openDropdown: function(dropdown) {
|
||||
if (dropdown.classList.contains("open")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._closeDropdowns();
|
||||
|
||||
// Trigger BackPressListener initialization in Android.
|
||||
dropdown.classList.add("open");
|
||||
this._mm.sendAsyncMessage("Reader:DropdownOpened", this.viewId);
|
||||
},
|
||||
|
||||
/*
|
||||
* If the ReaderView has open dropdowns, close them. If we are closing the
|
||||
* dropdowns because the page is scrolling, allow popups to stay open with
|
||||
* the keep-open class.
|
||||
*/
|
||||
_closeDropdowns: function(scrolling) {
|
||||
let selector = ".dropdown.open";
|
||||
if (scrolling) {
|
||||
selector += ":not(.keep-open)";
|
||||
}
|
||||
|
||||
let openDropdowns = this._doc.querySelectorAll(selector);
|
||||
for (let dropdown of openDropdowns) {
|
||||
dropdown.classList.remove("open");
|
||||
}
|
||||
|
||||
// Trigger BackPressListener cleanup in Android.
|
||||
if (openDropdowns.length) {
|
||||
this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Scroll reader view to a reference
|
||||
*/
|
||||
_goToReference(ref) {
|
||||
if (ref) {
|
||||
this._win.location.hash = ref;
|
||||
}
|
||||
}
|
||||
};
|
||||
1195
toolkit/components/reader/JSDOMParser.js
Normal file
1195
toolkit/components/reader/JSDOMParser.js
Normal file
File diff suppressed because it is too large
Load diff
1863
toolkit/components/reader/Readability.js
Normal file
1863
toolkit/components/reader/Readability.js
Normal file
File diff suppressed because it is too large
Load diff
514
toolkit/components/reader/ReaderMode.jsm
Normal file
514
toolkit/components/reader/ReaderMode.jsm
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["ReaderMode"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
// Constants for telemetry.
|
||||
const DOWNLOAD_SUCCESS = 0;
|
||||
const DOWNLOAD_ERROR_XHR = 1;
|
||||
const DOWNLOAD_ERROR_NO_DOC = 2;
|
||||
|
||||
const PARSE_SUCCESS = 0;
|
||||
const PARSE_ERROR_TOO_MANY_ELEMENTS = 1;
|
||||
const PARSE_ERROR_WORKER = 2;
|
||||
const PARSE_ERROR_NO_ARTICLE = 3;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Cu.importGlobalProperties(["XMLHttpRequest"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "Readability", function() {
|
||||
let scope = {};
|
||||
scope.dump = this.dump;
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
|
||||
return scope["Readability"];
|
||||
});
|
||||
|
||||
this.ReaderMode = {
|
||||
// Version of the cache schema.
|
||||
CACHE_VERSION: 1,
|
||||
|
||||
DEBUG: 0,
|
||||
|
||||
// Don't try to parse the page if it has too many elements (for memory and
|
||||
// performance reasons)
|
||||
get maxElemsToParse() {
|
||||
delete this.parseNodeLimit;
|
||||
|
||||
Services.prefs.addObserver("reader.parse-node-limit", this, false);
|
||||
return this.parseNodeLimit = Services.prefs.getIntPref("reader.parse-node-limit");
|
||||
},
|
||||
|
||||
get isEnabledForParseOnLoad() {
|
||||
delete this.isEnabledForParseOnLoad;
|
||||
|
||||
// Listen for future pref changes.
|
||||
Services.prefs.addObserver("reader.parse-on-load.", this, false);
|
||||
|
||||
return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
|
||||
},
|
||||
|
||||
get isOnLowMemoryPlatform() {
|
||||
let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
|
||||
delete this.isOnLowMemoryPlatform;
|
||||
return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
|
||||
},
|
||||
|
||||
_getStateForParseOnLoad: function () {
|
||||
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
|
||||
let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
|
||||
// For low-memory devices, don't allow reader mode since it takes up a lot of memory.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
|
||||
return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
|
||||
},
|
||||
|
||||
observe: function(aMessage, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "nsPref:changed":
|
||||
if (aData.startsWith("reader.parse-on-load.")) {
|
||||
this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
|
||||
} else if (aData === "reader.parse-node-limit") {
|
||||
this.parseNodeLimit = Services.prefs.getIntPref(aData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enter the reader mode by going forward one step in history if applicable,
|
||||
* if not, append the about:reader page in the history instead.
|
||||
*/
|
||||
enterReaderMode: function(docShell, win) {
|
||||
let url = win.document.location.href;
|
||||
let readerURL = "about:reader?url=" + encodeURIComponent(url);
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let sh = webNav.sessionHistory;
|
||||
if (webNav.canGoForward) {
|
||||
let forwardEntry = sh.getEntryAtIndex(sh.index + 1, false);
|
||||
let forwardURL = forwardEntry.URI.spec;
|
||||
if (forwardURL && (forwardURL == readerURL || !readerURL)) {
|
||||
webNav.goForward();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
win.document.location = readerURL;
|
||||
},
|
||||
|
||||
/**
|
||||
* Exit the reader mode by going back one step in history if applicable,
|
||||
* if not, append the original page in the history instead.
|
||||
*/
|
||||
leaveReaderMode: function(docShell, win) {
|
||||
let url = win.document.location.href;
|
||||
let originalURL = this.getOriginalUrl(url);
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let sh = webNav.sessionHistory;
|
||||
if (webNav.canGoBack) {
|
||||
let prevEntry = sh.getEntryAtIndex(sh.index - 1, false);
|
||||
let prevURL = prevEntry.URI.spec;
|
||||
if (prevURL && (prevURL == originalURL || !originalURL)) {
|
||||
webNav.goBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
win.document.location = originalURL;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns original URL from an about:reader URL.
|
||||
*
|
||||
* @param url An about:reader URL.
|
||||
* @return The original URL for the article, or null if we did not find
|
||||
* a properly formatted about:reader URL.
|
||||
*/
|
||||
getOriginalUrl: function(url) {
|
||||
if (!url.startsWith("about:reader?")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let outerHash = "";
|
||||
try {
|
||||
let uriObj = Services.io.newURI(url, null, null);
|
||||
url = uriObj.specIgnoringRef;
|
||||
outerHash = uriObj.ref;
|
||||
} catch (ex) { /* ignore, use the raw string */ }
|
||||
|
||||
let searchParams = new URLSearchParams(url.substring("about:reader?".length));
|
||||
if (!searchParams.has("url")) {
|
||||
return null;
|
||||
}
|
||||
let originalUrl = searchParams.get("url");
|
||||
if (outerHash) {
|
||||
try {
|
||||
let uriObj = Services.io.newURI(originalUrl, null, null);
|
||||
uriObj = Services.io.newURI('#' + outerHash, null, uriObj);
|
||||
originalUrl = uriObj.spec;
|
||||
} catch (ex) {}
|
||||
}
|
||||
return originalUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decides whether or not a document is reader-able without parsing the whole thing.
|
||||
*
|
||||
* @param doc A document to parse.
|
||||
* @return boolean Whether or not we should show the reader mode button.
|
||||
*/
|
||||
isProbablyReaderable: function(doc) {
|
||||
// Only care about 'real' HTML documents:
|
||||
if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let uri = Services.io.newURI(doc.location.href, null, null);
|
||||
if (!this._shouldCheckUri(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let utils = this.getUtilsForWin(doc.defaultView);
|
||||
// We pass in a helper function to determine if a node is visible, because
|
||||
// it uses gecko APIs that the engine-agnostic readability code can't rely
|
||||
// upon.
|
||||
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
|
||||
},
|
||||
|
||||
isNodeVisible: function(utils, node) {
|
||||
let bounds = utils.getBoundsWithoutFlushing(node);
|
||||
return bounds.height > 0 && bounds.width > 0;
|
||||
},
|
||||
|
||||
getUtilsForWin: function(win) {
|
||||
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an article from a loaded browser's document. This method will not attempt
|
||||
* to parse certain URIs (e.g. about: URIs).
|
||||
*
|
||||
* @param doc A document to parse.
|
||||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
parseDocument: Task.async(function* (doc) {
|
||||
let documentURI = Services.io.newURI(doc.documentURI, null, null);
|
||||
let baseURI = Services.io.newURI(doc.baseURI, null, null);
|
||||
if (!this._shouldCheckUri(documentURI) || !this._shouldCheckUri(baseURI, true)) {
|
||||
this.log("Reader mode disabled for URI");
|
||||
return null;
|
||||
}
|
||||
|
||||
return yield this._readerParse(baseURI, doc);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Downloads and parses a document from a URL.
|
||||
*
|
||||
* @param url URL to download and parse.
|
||||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
downloadAndParseDocument: Task.async(function* (url) {
|
||||
let doc = yield this._downloadDocument(url);
|
||||
let uri = Services.io.newURI(doc.baseURI, null, null);
|
||||
if (!this._shouldCheckUri(uri, true)) {
|
||||
this.log("Reader mode disabled for URI");
|
||||
return null;
|
||||
}
|
||||
|
||||
return yield this._readerParse(uri, doc);
|
||||
}),
|
||||
|
||||
_downloadDocument: function (url) {
|
||||
let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.onerror = evt => reject(evt.error);
|
||||
xhr.responseType = "document";
|
||||
xhr.onload = evt => {
|
||||
if (xhr.status !== 200) {
|
||||
reject("Reader mode XHR failed with status: " + xhr.status);
|
||||
histogram.add(DOWNLOAD_ERROR_XHR);
|
||||
return;
|
||||
}
|
||||
|
||||
let doc = xhr.responseXML;
|
||||
if (!doc) {
|
||||
reject("Reader mode XHR didn't return a document");
|
||||
histogram.add(DOWNLOAD_ERROR_NO_DOC);
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually follow a meta refresh tag if one exists.
|
||||
let meta = doc.querySelector("meta[http-equiv=refresh]");
|
||||
if (meta) {
|
||||
let content = meta.getAttribute("content");
|
||||
if (content) {
|
||||
let urlIndex = content.toUpperCase().indexOf("URL=");
|
||||
if (urlIndex > -1) {
|
||||
let baseURI = Services.io.newURI(url, null, null);
|
||||
let newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
|
||||
let newURL = newURI.spec;
|
||||
let ssm = Services.scriptSecurityManager;
|
||||
let flags = ssm.LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT |
|
||||
ssm.DISALLOW_INHERIT_PRINCIPAL;
|
||||
try {
|
||||
ssm.checkLoadURIStrWithPrincipal(doc.nodePrincipal, newURL, flags);
|
||||
} catch (ex) {
|
||||
let errorMsg = "Reader mode disallowed meta refresh (reason: " + ex + ").";
|
||||
|
||||
if (Services.prefs.getBoolPref("reader.errors.includeURLs"))
|
||||
errorMsg += " Refresh target URI: '" + newURL + "'.";
|
||||
reject(errorMsg);
|
||||
return;
|
||||
}
|
||||
// Otherwise, pass an object indicating our new URL:
|
||||
if (!baseURI.equalsExceptRef(newURI)) {
|
||||
reject({newURL});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let responseURL = xhr.responseURL;
|
||||
let givenURL = url;
|
||||
// Convert these to real URIs to make sure the escaping (or lack
|
||||
// thereof) is identical:
|
||||
try {
|
||||
responseURL = Services.io.newURI(responseURL, null, null).specIgnoringRef;
|
||||
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
|
||||
try {
|
||||
givenURL = Services.io.newURI(givenURL, null, null).specIgnoringRef;
|
||||
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
|
||||
|
||||
if (responseURL != givenURL) {
|
||||
// We were redirected without a meta refresh tag.
|
||||
// Force redirect to the correct place:
|
||||
reject({newURL: xhr.responseURL});
|
||||
return;
|
||||
}
|
||||
resolve(doc);
|
||||
histogram.add(DOWNLOAD_SUCCESS);
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves an article from the cache given an article URI.
|
||||
*
|
||||
* @param url The article URL.
|
||||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
getArticleFromCache: Task.async(function* (url) {
|
||||
let path = this._toHashedPath(url);
|
||||
try {
|
||||
let array = yield OS.File.read(path);
|
||||
return JSON.parse(new TextDecoder().decode(array));
|
||||
} catch (e) {
|
||||
if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
|
||||
throw e;
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stores an article in the cache.
|
||||
*
|
||||
* @param article JS object representing article.
|
||||
* @return {Promise}
|
||||
* @resolves When the article is stored.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
storeArticleInCache: Task.async(function* (article) {
|
||||
let array = new TextEncoder().encode(JSON.stringify(article));
|
||||
let path = this._toHashedPath(article.url);
|
||||
yield this._ensureCacheDir();
|
||||
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
|
||||
.then(success => {
|
||||
OS.File.stat(path).then(info => {
|
||||
return Messaging.sendRequest({
|
||||
type: "Reader:AddedToCache",
|
||||
url: article.url,
|
||||
size: info.size,
|
||||
path: path,
|
||||
});
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Removes an article from the cache given an article URI.
|
||||
*
|
||||
* @param url The article URL.
|
||||
* @return {Promise}
|
||||
* @resolves When the article is removed.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
removeArticleFromCache: Task.async(function* (url) {
|
||||
let path = this._toHashedPath(url);
|
||||
yield OS.File.remove(path);
|
||||
}),
|
||||
|
||||
log: function(msg) {
|
||||
if (this.DEBUG)
|
||||
dump("Reader: " + msg);
|
||||
},
|
||||
|
||||
_blockedHosts: [
|
||||
"mail.google.com",
|
||||
"github.com",
|
||||
"pinterest.com",
|
||||
"reddit.com",
|
||||
"twitter.com",
|
||||
"youtube.com",
|
||||
],
|
||||
|
||||
_shouldCheckUri: function (uri, isBaseUri = false) {
|
||||
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
|
||||
this.log("Not parsing URI scheme: " + uri.scheme);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
uri.QueryInterface(Ci.nsIURL);
|
||||
} catch (ex) {
|
||||
// If this doesn't work, presumably the URL is not well-formed or something
|
||||
return false;
|
||||
}
|
||||
// Sadly, some high-profile pages have false positives, so bail early for those:
|
||||
let asciiHost = uri.asciiHost;
|
||||
if (!isBaseUri && this._blockedHosts.some(blockedHost => asciiHost.endsWith(blockedHost))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isBaseUri && (!uri.filePath || uri.filePath == "/")) {
|
||||
this.log("Not parsing home page: " + uri.spec);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempts to parse a document into an article. Heavy lifting happens
|
||||
* in readerWorker.js.
|
||||
*
|
||||
* @param uri The base URI of the article.
|
||||
* @param doc The document to parse.
|
||||
* @return {Promise}
|
||||
* @resolves JS object representing the article, or null if no article is found.
|
||||
*/
|
||||
_readerParse: Task.async(function* (uri, doc) {
|
||||
let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
|
||||
if (this.parseNodeLimit) {
|
||||
let numTags = doc.getElementsByTagName("*").length;
|
||||
if (numTags > this.parseNodeLimit) {
|
||||
this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
|
||||
histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let uriParam = {
|
||||
spec: uri.spec,
|
||||
host: uri.host,
|
||||
prePath: uri.prePath,
|
||||
scheme: uri.scheme,
|
||||
pathBase: Services.io.newURI(".", null, uri).spec
|
||||
};
|
||||
|
||||
let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
|
||||
createInstance(Ci.nsIDOMSerializer);
|
||||
let serializedDoc = serializer.serializeToString(doc);
|
||||
|
||||
let article = null;
|
||||
try {
|
||||
article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
|
||||
} catch (e) {
|
||||
Cu.reportError("Error in ReaderWorker: " + e);
|
||||
histogram.add(PARSE_ERROR_WORKER);
|
||||
}
|
||||
|
||||
if (!article) {
|
||||
this.log("Worker did not return an article");
|
||||
histogram.add(PARSE_ERROR_NO_ARTICLE);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Readability returns a URI object, but we only care about the URL.
|
||||
article.url = article.uri.spec;
|
||||
delete article.uri;
|
||||
|
||||
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
|
||||
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
|
||||
.convertToPlainText(article.title, flags, 0);
|
||||
|
||||
histogram.add(PARSE_SUCCESS);
|
||||
return article;
|
||||
}),
|
||||
|
||||
get _cryptoHash() {
|
||||
delete this._cryptoHash;
|
||||
return this._cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
|
||||
},
|
||||
|
||||
get _unicodeConverter() {
|
||||
delete this._unicodeConverter;
|
||||
this._unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
this._unicodeConverter.charset = "utf8";
|
||||
return this._unicodeConverter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate the hashed path for a stripped article URL.
|
||||
*
|
||||
* @param url The article URL. This should have referrers removed.
|
||||
* @return The file path to the cached article.
|
||||
*/
|
||||
_toHashedPath: function (url) {
|
||||
let value = this._unicodeConverter.convertToByteArray(url);
|
||||
this._cryptoHash.init(this._cryptoHash.MD5);
|
||||
this._cryptoHash.update(value, value.length);
|
||||
|
||||
let hash = CommonUtils.encodeBase32(this._cryptoHash.finish(false));
|
||||
let fileName = hash.substring(0, hash.indexOf("=")) + ".json";
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, "readercache", fileName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists.
|
||||
*
|
||||
* @return Promise
|
||||
* @resolves When the cache directory exists.
|
||||
* @rejects OS.File.Error
|
||||
*/
|
||||
_ensureCacheDir: function () {
|
||||
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
|
||||
return OS.File.exists(dir).then(exists => {
|
||||
if (!exists) {
|
||||
return OS.File.makeDir(dir);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
};
|
||||
50
toolkit/components/reader/ReaderWorker.js
Normal file
50
toolkit/components/reader/ReaderWorker.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A worker dedicated to handle parsing documents for reader view.
|
||||
*/
|
||||
|
||||
importScripts("resource://gre/modules/workers/require.js",
|
||||
"resource://gre/modules/reader/JSDOMParser.js",
|
||||
"resource://gre/modules/reader/Readability.js");
|
||||
|
||||
var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
var worker = new PromiseWorker.AbstractWorker();
|
||||
worker.dispatch = function(method, args = []) {
|
||||
return Agent[method](...args);
|
||||
};
|
||||
worker.postMessage = function(result, ...transfers) {
|
||||
self.postMessage(result, ...transfers);
|
||||
};
|
||||
worker.close = function() {
|
||||
self.close();
|
||||
};
|
||||
worker.log = function(...args) {
|
||||
if (DEBUG) {
|
||||
dump("ReaderWorker: " + args.join(" ") + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener("message", msg => worker.handleMessage(msg));
|
||||
|
||||
var Agent = {
|
||||
/**
|
||||
* Parses structured article data from a document.
|
||||
*
|
||||
* @param {object} uri URI data for the document.
|
||||
* @param {string} serializedDoc The serialized document.
|
||||
*
|
||||
* @return {object} Article object returned from Readability.
|
||||
*/
|
||||
parseDocument: function (uri, serializedDoc) {
|
||||
let doc = new JSDOMParser().parse(serializedDoc);
|
||||
return new Readability(uri, doc).parse();
|
||||
},
|
||||
};
|
||||
17
toolkit/components/reader/ReaderWorker.jsm
Normal file
17
toolkit/components/reader/ReaderWorker.jsm
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Interface to a dedicated thread handling readability parsing.
|
||||
*/
|
||||
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["ReaderWorker"];
|
||||
|
||||
this.ReaderWorker = new BasePromiseWorker("resource://gre/modules/reader/ReaderWorker.js");
|
||||
74
toolkit/components/reader/content/aboutReader.html
Normal file
74
toolkit/components/reader/content/aboutReader.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
|
||||
<meta name="viewport" content="width=device-width; user-scalable=0" />
|
||||
|
||||
<link rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css"/>
|
||||
|
||||
<script type="text/javascript;version=1.8" src="chrome://global/content/reader/aboutReader.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container" class="container">
|
||||
<div id="reader-header" class="header">
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
||||
</style>
|
||||
<a id="reader-domain" class="domain"></a>
|
||||
<div class="domain-border"></div>
|
||||
<h1 id="reader-title"></h1>
|
||||
<div id="reader-credits" class="credits"></div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderContent.css");
|
||||
</style>
|
||||
<div id="moz-reader-content"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
||||
</style>
|
||||
<div id="reader-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="reader-toolbar" class="toolbar">
|
||||
<style scoped>
|
||||
@import url("chrome://global/skin/aboutReaderControls.css");
|
||||
</style>
|
||||
<li><button id="close-button" class="button close-button"/></li>
|
||||
<ul id="style-dropdown" class="dropdown">
|
||||
<li><button class="dropdown-toggle button style-button"/></li>
|
||||
<li id="reader-popup" class="dropdown-popup">
|
||||
<div id="font-type-buttons"></div>
|
||||
<hr></hr>
|
||||
<div id="font-size-buttons">
|
||||
<button id="font-size-minus" class="minus-button"/>
|
||||
<button id="font-size-sample"/>
|
||||
<button id="font-size-plus" class="plus-button"/>
|
||||
</div>
|
||||
<hr></hr>
|
||||
<div id="content-width-buttons">
|
||||
<button id="content-width-minus" class="content-width-minus-button"/>
|
||||
<button id="content-width-plus" class="content-width-plus-button"/>
|
||||
</div>
|
||||
<hr></hr>
|
||||
<div id="line-height-buttons">
|
||||
<button id="line-height-minus" class="line-height-minus-button"/>
|
||||
<button id="line-height-plus" class="line-height-plus-button"/>
|
||||
</div>
|
||||
<hr></hr>
|
||||
<div id="color-scheme-buttons"></div>
|
||||
<div class="dropdown-arrow"/>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
9
toolkit/components/reader/content/aboutReader.js
Normal file
9
toolkit/components/reader/content/aboutReader.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
|
||||
});
|
||||
7
toolkit/components/reader/jar.mn
Normal file
7
toolkit/components/reader/jar.mn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# 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/.
|
||||
|
||||
toolkit.jar:
|
||||
content/global/reader/aboutReader.html (content/aboutReader.html)
|
||||
content/global/reader/aboutReader.js (content/aboutReader.js)
|
||||
26
toolkit/components/reader/moz.build
Normal file
26
toolkit/components/reader/moz.build
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'AboutReader.jsm',
|
||||
'ReaderMode.jsm'
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.reader = [
|
||||
'JSDOMParser.js',
|
||||
'Readability.js',
|
||||
'ReaderWorker.js',
|
||||
'ReaderWorker.jsm'
|
||||
]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += [
|
||||
'test/browser.ini'
|
||||
]
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Toolkit', 'Reader Mode')
|
||||
15
toolkit/components/reader/test/browser.ini
Normal file
15
toolkit/components/reader/test/browser.ini
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[DEFAULT]
|
||||
support-files = head.js
|
||||
[browser_readerMode.js]
|
||||
support-files =
|
||||
readerModeArticle.html
|
||||
readerModeArticleHiddenNodes.html
|
||||
[browser_readerMode_hidden_nodes.js]
|
||||
support-files =
|
||||
readerModeArticleHiddenNodes.html
|
||||
[browser_readerMode_with_anchor.js]
|
||||
support-files =
|
||||
readerModeArticle.html
|
||||
[browser_bug1124271_readerModePinnedTab.js]
|
||||
support-files =
|
||||
readerModeArticle.html
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test that the reader mode button won't open in a new tab when clicked from a pinned tab
|
||||
|
||||
const PREF = "reader.parse-on-load.enabled";
|
||||
|
||||
const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
|
||||
|
||||
var readerButton = document.getElementById("reader-mode-button");
|
||||
|
||||
add_task(function* () {
|
||||
registerCleanupFunction(function() {
|
||||
Services.prefs.clearUserPref(PREF);
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
// Enable the reader mode button.
|
||||
Services.prefs.setBoolPref(PREF, true);
|
||||
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
gBrowser.pinTab(tab);
|
||||
|
||||
let initialTabsCount = gBrowser.tabs.length;
|
||||
|
||||
// Point tab to a test page that is reader-able.
|
||||
let url = TEST_PATH + "readerModeArticle.html";
|
||||
yield promiseTabLoadEvent(tab, url);
|
||||
yield promiseWaitForCondition(() => !readerButton.hidden);
|
||||
|
||||
readerButton.click();
|
||||
yield promiseTabLoadEvent(tab);
|
||||
|
||||
// Ensure no new tabs are opened when exiting reader mode in a pinned tab
|
||||
is(gBrowser.tabs.length, initialTabsCount, "No additional tabs were opened.");
|
||||
|
||||
let pageShownPromise = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
readerButton.click();
|
||||
yield pageShownPromise;
|
||||
// Ensure no new tabs are opened when exiting reader mode in a pinned tab
|
||||
is(gBrowser.tabs.length, initialTabsCount, "No additional tabs were opened.");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
220
toolkit/components/reader/test/browser_readerMode.js
Normal file
220
toolkit/components/reader/test/browser_readerMode.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Test that the reader mode button appears and works properly on
|
||||
* reader-able content.
|
||||
*/
|
||||
const TEST_PREFS = [
|
||||
["reader.parse-on-load.enabled", true],
|
||||
];
|
||||
|
||||
const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
|
||||
|
||||
var readerButton = document.getElementById("reader-mode-button");
|
||||
|
||||
add_task(function* test_reader_button() {
|
||||
registerCleanupFunction(function() {
|
||||
// Reset test prefs.
|
||||
TEST_PREFS.forEach(([name, value]) => {
|
||||
Services.prefs.clearUserPref(name);
|
||||
});
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
// Set required test prefs.
|
||||
TEST_PREFS.forEach(([name, value]) => {
|
||||
Services.prefs.setBoolPref(name, value);
|
||||
});
|
||||
Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", false);
|
||||
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
|
||||
ok(!UITour.isInfoOnTarget(window, "readerMode-urlBar"),
|
||||
"Info panel shouldn't appear without the reader mode button");
|
||||
ok(!Services.prefs.getBoolPref("browser.reader.detectedFirstArticle"),
|
||||
"Shouldn't have detected the first article");
|
||||
|
||||
// We're going to show the reader mode intro popup, make sure we wait for it:
|
||||
let tourPopupShownPromise =
|
||||
BrowserTestUtils.waitForEvent(document.getElementById("UITourTooltip"), "popupshown");
|
||||
// Point tab to a test page that is reader-able.
|
||||
let url = TEST_PATH + "readerModeArticle.html";
|
||||
yield promiseTabLoadEvent(tab, url);
|
||||
yield promiseWaitForCondition(() => !readerButton.hidden);
|
||||
yield tourPopupShownPromise;
|
||||
is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
|
||||
ok(UITour.isInfoOnTarget(window, "readerMode-urlBar"),
|
||||
"Info panel should be anchored at the reader mode button");
|
||||
ok(Services.prefs.getBoolPref("browser.reader.detectedFirstArticle"),
|
||||
"Should have detected the first article");
|
||||
|
||||
// Switch page into reader mode.
|
||||
readerButton.click();
|
||||
yield promiseTabLoadEvent(tab);
|
||||
ok(!UITour.isInfoOnTarget(window, "readerMode-urlBar"), "Info panel should have closed");
|
||||
|
||||
let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
|
||||
ok(readerUrl.startsWith("about:reader"), "about:reader loaded after clicking reader mode button");
|
||||
is_element_visible(readerButton, "Reader mode button is present on about:reader");
|
||||
|
||||
is(gURLBar.value, readerUrl, "gURLBar value is about:reader URL");
|
||||
is(gURLBar.textValue, url.substring("http://".length), "gURLBar is displaying original article URL");
|
||||
|
||||
// Check selected value for URL bar
|
||||
yield new Promise((resolve, reject) => {
|
||||
waitForClipboard(url, function () {
|
||||
gURLBar.focus();
|
||||
gURLBar.select();
|
||||
goDoCommand("cmd_copy");
|
||||
}, resolve, reject);
|
||||
});
|
||||
|
||||
info("Got correct URL when copying");
|
||||
|
||||
// Switch page back out of reader mode.
|
||||
let promisePageShow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
readerButton.click();
|
||||
yield promisePageShow;
|
||||
is(gBrowser.selectedBrowser.currentURI.spec, url,
|
||||
"Back to the original page after clicking active reader mode button");
|
||||
ok(gBrowser.selectedBrowser.canGoForward,
|
||||
"Moved one step back in the session history.");
|
||||
|
||||
// Load a new tab that is NOT reader-able.
|
||||
let newTab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
yield promiseTabLoadEvent(newTab, "about:robots");
|
||||
yield promiseWaitForCondition(() => readerButton.hidden);
|
||||
is_element_hidden(readerButton, "Reader mode button is not present on a non-reader-able page");
|
||||
|
||||
// Switch back to the original tab to make sure reader mode button is still visible.
|
||||
gBrowser.removeCurrentTab();
|
||||
yield promiseWaitForCondition(() => !readerButton.hidden);
|
||||
is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
|
||||
});
|
||||
|
||||
add_task(function* test_getOriginalUrl() {
|
||||
let { ReaderMode } = Cu.import("resource://gre/modules/ReaderMode.jsm", {});
|
||||
let url = "http://foo.com/article.html";
|
||||
|
||||
is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(url)), url, "Found original URL from encoded URL");
|
||||
is(ReaderMode.getOriginalUrl("about:reader?foobar"), null, "Did not find original URL from malformed reader URL");
|
||||
is(ReaderMode.getOriginalUrl(url), null, "Did not find original URL from non-reader URL");
|
||||
|
||||
let badUrl = "http://foo.com/?;$%^^";
|
||||
is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(badUrl)), badUrl, "Found original URL from encoded malformed URL");
|
||||
is(ReaderMode.getOriginalUrl("about:reader?url=" + badUrl), badUrl, "Found original URL from non-encoded malformed URL");
|
||||
});
|
||||
|
||||
add_task(function* test_reader_view_element_attribute_transform() {
|
||||
registerCleanupFunction(function() {
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
function observeAttribute(element, attribute, triggerFn, checkFn) {
|
||||
return new Promise(resolve => {
|
||||
let observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach( mu => {
|
||||
if (element.getAttribute(attribute) !== mu.oldValue) {
|
||||
checkFn();
|
||||
resolve();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(element, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
attributeFilter: [attribute]
|
||||
});
|
||||
|
||||
triggerFn();
|
||||
});
|
||||
}
|
||||
|
||||
let command = document.getElementById("View:ReaderView");
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
|
||||
is(command.hidden, true, "Command element should have the hidden attribute");
|
||||
|
||||
info("Navigate a reader-able page");
|
||||
let waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
yield observeAttribute(command, "hidden",
|
||||
() => {
|
||||
let url = TEST_PATH + "readerModeArticle.html";
|
||||
tab.linkedBrowser.loadURI(url);
|
||||
},
|
||||
() => {
|
||||
is(command.hidden, false, "Command's hidden attribute should be false on a reader-able page");
|
||||
}
|
||||
);
|
||||
yield waitForPageshow;
|
||||
|
||||
info("Navigate a non-reader-able page");
|
||||
waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
yield observeAttribute(command, "hidden",
|
||||
() => {
|
||||
let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
|
||||
tab.linkedBrowser.loadURI(url);
|
||||
},
|
||||
() => {
|
||||
is(command.hidden, true, "Command's hidden attribute should be true on a non-reader-able page");
|
||||
}
|
||||
);
|
||||
yield waitForPageshow;
|
||||
|
||||
info("Navigate a reader-able page");
|
||||
waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
yield observeAttribute(command, "hidden",
|
||||
() => {
|
||||
let url = TEST_PATH + "readerModeArticle.html";
|
||||
tab.linkedBrowser.loadURI(url);
|
||||
},
|
||||
() => {
|
||||
is(command.hidden, false, "Command's hidden attribute should be false on a reader-able page");
|
||||
}
|
||||
);
|
||||
yield waitForPageshow;
|
||||
|
||||
info("Enter Reader Mode");
|
||||
waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
yield observeAttribute(readerButton, "readeractive",
|
||||
() => {
|
||||
readerButton.click();
|
||||
},
|
||||
() => {
|
||||
is(readerButton.getAttribute("readeractive"), "true", "readerButton's readeractive attribute should be true when entering reader mode");
|
||||
}
|
||||
);
|
||||
yield waitForPageshow;
|
||||
|
||||
info("Exit Reader Mode");
|
||||
waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
yield observeAttribute(readerButton, "readeractive",
|
||||
() => {
|
||||
readerButton.click();
|
||||
},
|
||||
() => {
|
||||
is(readerButton.getAttribute("readeractive"), "", "readerButton's readeractive attribute should be empty when reader mode is exited");
|
||||
}
|
||||
);
|
||||
yield waitForPageshow;
|
||||
|
||||
info("Navigate a non-reader-able page");
|
||||
waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
|
||||
yield observeAttribute(command, "hidden",
|
||||
() => {
|
||||
let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
|
||||
tab.linkedBrowser.loadURI(url);
|
||||
},
|
||||
() => {
|
||||
is(command.hidden, true, "Command's hidden attribute should be true on a non-reader-able page");
|
||||
}
|
||||
);
|
||||
yield waitForPageshow;
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Test that the reader mode button appears and works properly on
|
||||
* reader-able content.
|
||||
*/
|
||||
const TEST_PREFS = [
|
||||
["reader.parse-on-load.enabled", true],
|
||||
["browser.reader.detectedFirstArticle", false],
|
||||
];
|
||||
|
||||
const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
|
||||
|
||||
var readerButton = document.getElementById("reader-mode-button");
|
||||
|
||||
add_task(function* test_reader_button() {
|
||||
registerCleanupFunction(function() {
|
||||
// Reset test prefs.
|
||||
TEST_PREFS.forEach(([name, value]) => {
|
||||
Services.prefs.clearUserPref(name);
|
||||
});
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
// Set required test prefs.
|
||||
TEST_PREFS.forEach(([name, value]) => {
|
||||
Services.prefs.setBoolPref(name, value);
|
||||
});
|
||||
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
|
||||
// Point tab to a test page that is not reader-able due to hidden nodes.
|
||||
let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
|
||||
let paintPromise = ContentTask.spawn(tab.linkedBrowser, "", function() {
|
||||
return new Promise(resolve => {
|
||||
addEventListener("DOMContentLoaded", function onDCL() {
|
||||
removeEventListener("DOMContentLoaded", onDCL);
|
||||
addEventListener("MozAfterPaint", function onPaint() {
|
||||
removeEventListener("MozAfterPaint", onPaint);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
tab.linkedBrowser.loadURI(url);
|
||||
yield paintPromise;
|
||||
|
||||
is_element_hidden(readerButton, "Reader mode button is still not present on tab with unreadable content.");
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
|
||||
|
||||
add_task(function* () {
|
||||
yield BrowserTestUtils.withNewTab(TEST_PATH + "readerModeArticle.html#foo", function* (browser) {
|
||||
let pageShownPromise = BrowserTestUtils.waitForContentEvent(browser, "AboutReaderContentReady");
|
||||
let readerButton = document.getElementById("reader-mode-button");
|
||||
readerButton.click();
|
||||
yield pageShownPromise;
|
||||
yield ContentTask.spawn(browser, null, function* () {
|
||||
// Check if offset != 0
|
||||
ok(content.document.getElementById("foo") !== null, "foo element should be in document");
|
||||
ok(content.pageYOffset != 0, "pageYOffset should be > 0");
|
||||
});
|
||||
});
|
||||
});
|
||||
126
toolkit/components/reader/test/head.js
Normal file
126
toolkit/components/reader/test/head.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
|
||||
/* exported promiseTabLoadEvent, promiseWaitForCondition, is_element_visible, is_element_hidden */
|
||||
|
||||
/**
|
||||
* Waits for a load (or custom) event to finish in a given tab. If provided
|
||||
* load an uri into the tab.
|
||||
*
|
||||
* @param tab
|
||||
* The tab to load into.
|
||||
* @param [optional] url
|
||||
* The url to load, or the current url.
|
||||
* @return {Promise} resolved when the event is handled.
|
||||
* @resolves to the received event
|
||||
* @rejects if a valid load event is not received within a meaningful interval
|
||||
*/
|
||||
function promiseTabLoadEvent(tab, url) {
|
||||
let deferred = Promise.defer();
|
||||
info("Wait tab event: load");
|
||||
|
||||
function handle(loadedUrl) {
|
||||
if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
|
||||
info(`Skipping spurious load event for ${loadedUrl}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
info("Tab event received: load");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create two promises: one resolved from the content process when the page
|
||||
// loads and one that is rejected if we take too long to load the url.
|
||||
let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
deferred.reject(new Error("Timed out while waiting for a 'load' event"));
|
||||
}, 30000);
|
||||
|
||||
loaded.then(() => {
|
||||
clearTimeout(timeout);
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
if (url)
|
||||
BrowserTestUtils.loadURI(tab.linkedBrowser, url);
|
||||
|
||||
// Promise.all rejects if either promise rejects (i.e. if we time out) and
|
||||
// if our loaded promise resolves before the timeout, then we resolve the
|
||||
// timeout promise as well, causing the all promise to resolve.
|
||||
return Promise.all([deferred.promise, loaded]);
|
||||
}
|
||||
|
||||
function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
|
||||
retryTimes = typeof retryTimes !== 'undefined' ? retryTimes : 30;
|
||||
var tries = 0;
|
||||
var interval = setInterval(function() {
|
||||
if (tries >= retryTimes) {
|
||||
ok(false, errorMsg);
|
||||
moveOn();
|
||||
}
|
||||
var conditionPassed;
|
||||
try {
|
||||
conditionPassed = condition();
|
||||
} catch (e) {
|
||||
ok(false, e + "\n" + e.stack);
|
||||
conditionPassed = false;
|
||||
}
|
||||
if (conditionPassed) {
|
||||
moveOn();
|
||||
}
|
||||
tries++;
|
||||
}, 100);
|
||||
var moveOn = function() {
|
||||
clearInterval(interval);
|
||||
nextTest();
|
||||
};
|
||||
}
|
||||
|
||||
function promiseWaitForCondition(aConditionFn) {
|
||||
let deferred = Promise.defer();
|
||||
waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function is_element_visible(element, msg) {
|
||||
isnot(element, null, "Element should not be null, when checking visibility");
|
||||
ok(is_visible(element), msg || "Element should be visible");
|
||||
|
||||
}
|
||||
function is_element_hidden(element, msg) {
|
||||
isnot(element, null, "Element should not be null, when checking visibility");
|
||||
ok(is_hidden(element), msg || "Element should be hidden");
|
||||
}
|
||||
|
||||
function is_visible(element) {
|
||||
var style = element.ownerGlobal.getComputedStyle(element);
|
||||
if (style.display == "none")
|
||||
return false;
|
||||
if (style.visibility != "visible")
|
||||
return false;
|
||||
if (style.display == "-moz-popup" && element.state != "open")
|
||||
return false;
|
||||
|
||||
// Hiding a parent element will hide all its children
|
||||
if (element.parentNode != element.ownerDocument)
|
||||
return is_visible(element.parentNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function is_hidden(element) {
|
||||
var style = element.ownerGlobal.getComputedStyle(element);
|
||||
if (style.display == "none")
|
||||
return true;
|
||||
if (style.visibility != "visible")
|
||||
return true;
|
||||
if (style.display == "-moz-popup")
|
||||
return ["hiding", "closed"].indexOf(element.state) != -1;
|
||||
|
||||
// Hiding a parent element will hide all its children
|
||||
if (element.parentNode != element.ownerDocument)
|
||||
return is_hidden(element.parentNode);
|
||||
|
||||
return false;
|
||||
}
|
||||
25
toolkit/components/reader/test/readerModeArticle.html
Normal file
25
toolkit/components/reader/test/readerModeArticle.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Article title</title>
|
||||
<meta name="description" content="This is the article description." />
|
||||
</head>
|
||||
<body>
|
||||
<header>Site header</header>
|
||||
<div>
|
||||
<h1>Article title</h1>
|
||||
<h2 class="author">by Jane Doe</h2>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<div id="foo">by John Doe</div>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Article title</title>
|
||||
<meta name="description" content="This is the article description." />
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
p { display: none }
|
||||
</style>
|
||||
<header>Site header</header>
|
||||
<div>
|
||||
<h1>Article title</h1>
|
||||
<h2 class="author">by Jane Doe</h2>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue