summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAscrod <32915892+Ascrod@users.noreply.github.com>2018-05-07 19:45:46 -0400
committerAscrod <32915892+Ascrod@users.noreply.github.com>2018-05-15 21:06:41 -0500
commitb70d884598e1e14b99190e1e5c349553ff59849b (patch)
treeb66af009469ec85d52bf2655afdf8e08dd881abb
parent000ac0cc833c415169ed7aa8c5e3493d0d4525fd (diff)
downloadUXP-b70d884598e1e14b99190e1e5c349553ff59849b.tar
UXP-b70d884598e1e14b99190e1e5c349553ff59849b.tar.gz
UXP-b70d884598e1e14b99190e1e5c349553ff59849b.tar.lz
UXP-b70d884598e1e14b99190e1e5c349553ff59849b.tar.xz
UXP-b70d884598e1e14b99190e1e5c349553ff59849b.zip
Initial updates for Reader View.
-rw-r--r--browser/base/content/browser-sets.inc2
-rw-r--r--browser/modules/ReaderParent.jsm69
-rw-r--r--mobile/android/themes/core/jar.mn4
-rw-r--r--toolkit/components/narrate/NarrateControls.jsm134
-rw-r--r--toolkit/components/narrate/Narrator.jsm78
-rw-r--r--toolkit/components/narrate/VoiceSelect.jsm23
-rw-r--r--toolkit/components/printing/content/simplifyMode.css2
-rw-r--r--toolkit/components/reader/AboutReader.jsm314
-rw-r--r--toolkit/components/reader/JSDOMParser.js46
-rw-r--r--toolkit/components/reader/Readability.js853
-rw-r--r--toolkit/components/reader/ReaderMode.jsm312
-rw-r--r--toolkit/components/reader/ReaderWorker.js7
-rw-r--r--toolkit/components/reader/content/aboutReader.html73
-rw-r--r--toolkit/components/reader/content/aboutReader.js2
-rw-r--r--toolkit/content/browser-content.js6
-rw-r--r--toolkit/locales/en-US/chrome/global/aboutReader.properties16
-rw-r--r--toolkit/themes/shared/aboutReader.css626
-rw-r--r--toolkit/themes/shared/aboutReaderContent.css177
-rw-r--r--toolkit/themes/shared/aboutReaderControls.css388
-rw-r--r--toolkit/themes/shared/jar.inc.mn5
-rw-r--r--toolkit/themes/shared/narrate.css195
-rw-r--r--toolkit/themes/shared/narrateControls.css185
-rw-r--r--toolkit/themes/shared/reader/RM-Content-Width-Minus-42x16.svg5
-rw-r--r--toolkit/themes/shared/reader/RM-Content-Width-Plus-44x16.svg5
-rw-r--r--toolkit/themes/shared/reader/RM-Line-Height-Minus-38x14.svg5
-rw-r--r--toolkit/themes/shared/reader/RM-Line-Height-Plus-38x24.svg5
-rw-r--r--toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg16
27 files changed, 1762 insertions, 1791 deletions
diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc
index 0c753520f..d0c3d11cd 100644
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -285,7 +285,7 @@
<key id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/>
<key keycode="VK_F11" command="View:FullScreen"/>
#endif
- <key id="toggleReaderMode" key="&toggleReaderMode.key;" command="View:ReaderView" modifiers="accel,alt" disabled="true"/>
+ <key id="key_toggleReaderMode" key="&toggleReaderMode.key;" command="View:ReaderView" modifiers="accel,alt" disabled="true"/>
<key key="&reloadCmd.commandkey;" command="Browser:Reload" modifiers="accel" id="key_reload"/>
<key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
<key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/>
diff --git a/browser/modules/ReaderParent.jsm b/browser/modules/ReaderParent.jsm
index 6fcaada42..fcc0cc92b 100644
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -11,31 +11,27 @@ this.EXPORTED_SYMBOLS = [ "ReaderParent" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
var ReaderParent = {
- _readerModeInfoPanelOpen: false,
-
MESSAGES: [
"Reader:ArticleGet",
"Reader:FaviconRequest",
"Reader:UpdateReaderButton",
],
- init: function() {
+ init() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
for (let msg of this.MESSAGES) {
mm.addMessageListener(msg, this);
}
},
- receiveMessage: function(message) {
+ receiveMessage(message) {
switch (message.name) {
case "Reader:ArticleGet":
this._getArticle(message.data.url, message.target).then((article) => {
@@ -60,7 +56,7 @@ var ReaderParent = {
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
url: message.data.url,
faviconUrl: favicon.path.replace(/^favicon:/, "")
- })
+ });
},
function onRejection(reason) {
Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
@@ -80,7 +76,7 @@ var ReaderParent = {
}
},
- updateReaderButton: function(browser) {
+ updateReaderButton(browser) {
let win = browser.ownerGlobal;
if (browser != win.gBrowser.selectedBrowser) {
return;
@@ -88,7 +84,11 @@ var ReaderParent = {
let button = win.document.getElementById("reader-mode-button");
let command = win.document.getElementById("View:ReaderView");
- let key = win.document.getElementById("toggleReaderMode");
+ let key = win.document.getElementById("key_toggleReaderMode");
+ // aria-reader is not a real ARIA attribute. However, this will cause
+ // Gecko accessibility to expose the "reader" object attribute. We do this
+ // so that the reader state is easy for accessibility clients to access
+ // programmatically.
if (browser.currentURI.spec.startsWith("about:reader")) {
button.setAttribute("readeractive", true);
button.hidden = false;
@@ -98,6 +98,7 @@ var ReaderParent = {
command.setAttribute("hidden", false);
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey"));
key.setAttribute("disabled", false);
+ browser.setAttribute("aria-reader", "active");
} else {
button.removeAttribute("readeractive");
button.hidden = !browser.isArticle;
@@ -107,24 +108,15 @@ var ReaderParent = {
command.setAttribute("hidden", !browser.isArticle);
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
key.setAttribute("disabled", !browser.isArticle);
- }
-
- let currentUriHost = browser.currentURI && browser.currentURI.asciiHost;
- if (browser.isArticle &&
- !Services.prefs.getBoolPref("browser.reader.detectedFirstArticle") &&
- currentUriHost && !currentUriHost.endsWith("mozilla.org")) {
- this.showReaderModeInfoPanel(browser);
- Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", true);
- this._readerModeInfoPanelOpen = true;
- } else if (this._readerModeInfoPanelOpen) {
- if (UITour.isInfoOnTarget(win, "readerMode-urlBar")) {
- UITour.hideInfo(win);
+ if (browser.isArticle) {
+ browser.setAttribute("aria-reader", "available");
+ } else {
+ browser.removeAttribute("aria-reader");
}
- this._readerModeInfoPanelOpen = false;
}
},
- forceShowReaderIcon: function(browser) {
+ forceShowReaderIcon(browser) {
browser.isArticle = true;
this.updateReaderButton(browser);
},
@@ -136,36 +128,13 @@ var ReaderParent = {
this.toggleReaderMode(event);
},
- toggleReaderMode: function(event) {
+ toggleReaderMode(event) {
let win = event.target.ownerGlobal;
let browser = win.gBrowser.selectedBrowser;
browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
},
/**
- * Shows an info panel from the UITour for Reader Mode.
- *
- * @param browser The <browser> that the tour should be started for.
- */
- showReaderModeInfoPanel(browser) {
- let win = browser.ownerGlobal;
- let targetPromise = UITour.getTarget(win, "readerMode-urlBar");
- targetPromise.then(target => {
- let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
- let icon = "chrome://browser/skin/";
- if (win.devicePixelRatio > 1) {
- icon += "reader-tour@2x.png";
- } else {
- icon += "reader-tour.png";
- }
- UITour.showInfo(win, target,
- browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.title"),
- browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.body"),
- icon);
- });
- },
-
- /**
* Gets an article for a given URL. This method will download and parse a document.
*
* @param url The article URL.
@@ -173,8 +142,8 @@ var ReaderParent = {
* @return {Promise}
* @resolves JS object representing the article, or null if no article is found.
*/
- _getArticle: Task.async(function* (url, browser) {
- return yield ReaderMode.downloadAndParseDocument(url).catch(e => {
+ async _getArticle(url, browser) {
+ return await ReaderMode.downloadAndParseDocument(url).catch(e => {
if (e && e.newURL) {
// Pass up the error so we can navigate the browser in question to the new URL:
throw e;
@@ -182,5 +151,5 @@ var ReaderParent = {
Cu.reportError("Error downloading and parsing document: " + e);
return null;
});
- })
+ }
};
diff --git a/mobile/android/themes/core/jar.mn b/mobile/android/themes/core/jar.mn
index 248c00253..6bca48990 100644
--- a/mobile/android/themes/core/jar.mn
+++ b/mobile/android/themes/core/jar.mn
@@ -18,8 +18,6 @@ chrome.jar:
skin/aboutMemory.css (aboutMemory.css)
skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css)
skin/aboutReader.css (aboutReader.css)
- skin/aboutReaderContent.css (aboutReaderContent.css)
- skin/aboutReaderControls.css (aboutReaderControls.css)
skin/aboutSupport.css (aboutSupport.css)
skin/content.css (content.css)
skin/scrollbar.css (scrollbar-apz.css)
@@ -31,8 +29,6 @@ chrome.jar:
% override chrome://global/skin/about.css chrome://browser/skin/about.css
% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.css
% override chrome://global/skin/aboutReader.css chrome://browser/skin/aboutReader.css
-% override chrome://global/skin/aboutReaderContent.css chrome://browser/skin/aboutReaderContent.css
-% override chrome://global/skin/aboutReaderControls.css chrome://browser/skin/aboutReaderControls.css
% override chrome://global/skin/aboutSupport.css chrome://browser/skin/aboutSupport.css
% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
% override chrome://global/skin/netError.css chrome://browser/skin/netError.css
diff --git a/toolkit/components/narrate/NarrateControls.jsm b/toolkit/components/narrate/NarrateControls.jsm
index 7d8794b18..828292cc2 100644
--- a/toolkit/components/narrate/NarrateControls.jsm
+++ b/toolkit/components/narrate/NarrateControls.jsm
@@ -16,9 +16,10 @@ this.EXPORTED_SYMBOLS = ["NarrateControls"];
var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
-function NarrateControls(mm, win) {
+function NarrateControls(mm, win, languagePromise) {
this._mm = mm;
this._winRef = Cu.getWeakReference(win);
+ this._languagePromise = languagePromise;
win.addEventListener("unload", this);
@@ -37,16 +38,12 @@ function NarrateControls(mm, win) {
}
let dropdown = win.document.createElement("ul");
- dropdown.className = "dropdown";
- dropdown.id = "narrate-dropdown";
+ dropdown.className = "dropdown narrate-dropdown";
// We need inline svg here for the animation to work (bug 908634 & 1190881).
- // The style animation can't be scoped (bug 830056).
+ // eslint-disable-next-line no-unsanitized/property
dropdown.innerHTML =
- localize`<style scoped>
- @import url("chrome://global/skin/narrateControls.css");
- </style>
- <li>
- <button class="dropdown-toggle button" id="narrate-toggle"
+ localize`<li>
+ <button class="dropdown-toggle button narrate-toggle"
title="${"narrate"}" hidden>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -60,11 +57,11 @@ function NarrateControls(mm, win) {
100% { transform: scaleY(1); }
}
- #waveform > rect {
+ .waveform > rect {
fill: #808080;
}
- .speaking #waveform > rect {
+ .speaking .waveform > rect {
fill: #58bf43;
transform-box: fill-box;
transform-origin: 50% 50%;
@@ -74,15 +71,15 @@ function NarrateControls(mm, win) {
animation-timing-function: linear;
}
- #waveform > rect:nth-child(2) { animation-delay: 250ms; }
- #waveform > rect:nth-child(3) { animation-delay: 500ms; }
- #waveform > rect:nth-child(4) { animation-delay: 750ms; }
- #waveform > rect:nth-child(5) { animation-delay: 1000ms; }
- #waveform > rect:nth-child(6) { animation-delay: 1250ms; }
- #waveform > rect:nth-child(7) { animation-delay: 1500ms; }
+ .waveform > rect:nth-child(2) { animation-delay: 250ms; }
+ .waveform > rect:nth-child(3) { animation-delay: 500ms; }
+ .waveform > rect:nth-child(4) { animation-delay: 750ms; }
+ .waveform > rect:nth-child(5) { animation-delay: 1000ms; }
+ .waveform > rect:nth-child(6) { animation-delay: 1250ms; }
+ .waveform > rect:nth-child(7) { animation-delay: 1500ms; }
</style>
- <g id="waveform">
+ <g class="waveform">
<rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" />
<rect x="4" y="5" width="2" height="14" rx=".5" ry=".5" />
<rect x="7" y="8" width="2" height="8" rx=".5" ry=".5" />
@@ -95,35 +92,35 @@ function NarrateControls(mm, win) {
</button>
</li>
<li class="dropdown-popup">
- <div id="narrate-control" class="narrate-row">
- <button disabled id="narrate-skip-previous"
+ <div class="narrate-row narrate-control">
+ <button disabled class="narrate-skip-previous"
title="${"back"}"></button>
- <button id="narrate-start-stop" title="${"start"}"></button>
- <button disabled id="narrate-skip-next"
+ <button class="narrate-start-stop" title="${"start"}"></button>
+ <button disabled class="narrate-skip-next"
title="${"forward"}"></button>
</div>
- <div id="narrate-rate" class="narrate-row">
- <input id="narrate-rate-input" value="0" title="${"speed"}"
+ <div class="narrate-row narrate-rate">
+ <input class="narrate-rate-input" value="0" title="${"speed"}"
step="5" max="100" min="-100" type="range">
</div>
- <div id="narrate-voices" class="narrate-row"></div>
+ <div class="narrate-row narrate-voices"></div>
<div class="dropdown-arrow"></div>
</li>`;
- this.narrator = new Narrator(win);
+ this.narrator = new Narrator(win, languagePromise);
let branch = Services.prefs.getBranch("narrate.");
let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
this.voiceSelect = new VoiceSelect(win, selectLabel);
this.voiceSelect.element.addEventListener("change", this);
- this.voiceSelect.element.id = "voice-select";
+ this.voiceSelect.element.classList.add("voice-select");
win.speechSynthesis.addEventListener("voiceschanged", this);
- dropdown.querySelector("#narrate-voices").appendChild(
+ dropdown.querySelector(".narrate-voices").appendChild(
this.voiceSelect.element);
dropdown.addEventListener("click", this, true);
- let rateRange = dropdown.querySelector("#narrate-rate > input");
+ let rateRange = dropdown.querySelector(".narrate-rate > input");
rateRange.addEventListener("change", this);
// The rate is stored as an integer.
@@ -131,15 +128,15 @@ function NarrateControls(mm, win) {
this._setupVoices();
- let tb = win.document.getElementById("reader-toolbar");
+ let tb = win.document.querySelector(".reader-toolbar");
tb.appendChild(dropdown);
}
NarrateControls.prototype = {
- handleEvent: function(evt) {
+ handleEvent(evt) {
switch (evt.type) {
case "change":
- if (evt.target.id == "narrate-rate-input") {
+ if (evt.target.classList.contains("narrate-rate-input")) {
this._onRateInput(evt);
} else {
this._onVoiceChange();
@@ -162,8 +159,8 @@ NarrateControls.prototype = {
/**
* Returns true if synth voices are available.
*/
- _setupVoices: function() {
- return this.narrator.languagePromise.then(language => {
+ _setupVoices() {
+ return this._languagePromise.then(language => {
this.voiceSelect.clear();
let win = this._win;
let voicePrefs = this._getVoicePref();
@@ -190,7 +187,7 @@ NarrateControls.prototype = {
this.voiceSelect.addOptions(options);
}
- let narrateToggle = win.document.getElementById("narrate-toggle");
+ let narrateToggle = win.document.querySelector(".narrate-toggle");
let histogram = Services.telemetry.getKeyedHistogramById(
"NARRATE_CONTENT_BY_LANGUAGE_2");
let initial = !this._voicesInitialized;
@@ -210,7 +207,7 @@ NarrateControls.prototype = {
});
},
- _getVoicePref: function() {
+ _getVoicePref() {
let voicePref = Services.prefs.getCharPref("narrate.voice");
try {
return JSON.parse(voicePref);
@@ -219,15 +216,15 @@ NarrateControls.prototype = {
}
},
- _onRateInput: function(evt) {
+ _onRateInput(evt) {
AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
this.narrator.setRate(this._convertRate(evt.target.value));
},
- _onVoiceChange: function() {
+ _onVoiceChange() {
let voice = this.voice;
this.narrator.setVoice(voice);
- this.narrator.languagePromise.then(language => {
+ this._languagePromise.then(language => {
if (language) {
let voicePref = this._getVoicePref();
voicePref[language || "default"] = voice;
@@ -236,42 +233,39 @@ NarrateControls.prototype = {
});
},
- _onButtonClick: function(evt) {
- switch (evt.target.id) {
- case "narrate-skip-previous":
- this.narrator.skipPrevious();
- break;
- case "narrate-skip-next":
- this.narrator.skipNext();
- break;
- case "narrate-start-stop":
- if (this.narrator.speaking) {
- this.narrator.stop();
- } else {
- this._updateSpeechControls(true);
- let options = { rate: this.rate, voice: this.voice };
- this.narrator.start(options).then(() => {
- this._updateSpeechControls(false);
- }, err => {
- Cu.reportError(`Narrate failed: ${err}.`);
- this._updateSpeechControls(false);
- });
- }
- break;
+ _onButtonClick(evt) {
+ let classList = evt.target.classList;
+ if (classList.contains("narrate-skip-previous")) {
+ this.narrator.skipPrevious();
+ } else if (classList.contains("narrate-skip-next")) {
+ this.narrator.skipNext();
+ } else if (classList.contains("narrate-start-stop")) {
+ if (this.narrator.speaking) {
+ this.narrator.stop();
+ } else {
+ this._updateSpeechControls(true);
+ let options = { rate: this.rate, voice: this.voice };
+ this.narrator.start(options).then(() => {
+ this._updateSpeechControls(false);
+ }, err => {
+ Cu.reportError(`Narrate failed: ${err}.`);
+ this._updateSpeechControls(false);
+ });
+ }
}
},
- _updateSpeechControls: function(speaking) {
- let dropdown = this._doc.getElementById("narrate-dropdown");
+ _updateSpeechControls(speaking) {
+ let dropdown = this._doc.querySelector(".narrate-dropdown");
dropdown.classList.toggle("keep-open", speaking);
dropdown.classList.toggle("speaking", speaking);
- let startStopButton = this._doc.getElementById("narrate-start-stop");
+ let startStopButton = this._doc.querySelector(".narrate-start-stop");
startStopButton.title =
gStrings.GetStringFromName(speaking ? "stop" : "start");
- this._doc.getElementById("narrate-skip-previous").disabled = !speaking;
- this._doc.getElementById("narrate-skip-next").disabled = !speaking;
+ this._doc.querySelector(".narrate-skip-previous").disabled = !speaking;
+ this._doc.querySelector(".narrate-skip-next").disabled = !speaking;
if (speaking) {
TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this);
@@ -280,7 +274,7 @@ NarrateControls.prototype = {
}
},
- _createVoiceLabel: function(voice) {
+ _createVoiceLabel(voice) {
// This is a highly imperfect method of making human-readable labels
// for system voices. Because each platform has a different naming scheme
// for voices, we use a different method for each platform.
@@ -303,7 +297,7 @@ NarrateControls.prototype = {
}
},
- _getLanguageName: function(lang) {
+ _getLanguageName(lang) {
if (!this._langStrings) {
this._langStrings = Services.strings.createBundle(
"chrome://global/locale/languageNames.properties ");
@@ -317,7 +311,7 @@ NarrateControls.prototype = {
}
},
- _convertRate: function(rate) {
+ _convertRate(rate) {
// We need to convert a relative percentage value to a fraction rate value.
// eg. -100 is half the speed, 100 is twice the speed in percentage,
// 0.5 is half the speed and 2 is twice the speed in fractions.
@@ -334,7 +328,7 @@ NarrateControls.prototype = {
get rate() {
return this._convertRate(
- this._doc.getElementById("narrate-rate-input").value);
+ this._doc.querySelector(".narrate-rate-input").value);
},
get voice() {
diff --git a/toolkit/components/narrate/Narrator.jsm b/toolkit/components/narrate/Narrator.jsm
index ade06510e..ac0b2e040 100644
--- a/toolkit/components/narrate/Narrator.jsm
+++ b/toolkit/components/narrate/Narrator.jsm
@@ -8,8 +8,6 @@ const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
- "resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
@@ -24,29 +22,13 @@ const kTextStylesRules = ["font-family", "font-kerning", "font-size",
"line-height", "letter-spacing", "text-orientation",
"text-transform", "word-spacing"];
-function Narrator(win) {
+function Narrator(win, languagePromise) {
this._winRef = Cu.getWeakReference(win);
+ this._languagePromise = languagePromise;
this._inTest = Services.prefs.getBoolPref("narrate.test");
this._speechOptions = {};
this._startTime = 0;
this._stopped = false;
-
- this.languagePromise = new Promise(resolve => {
- let detect = () => {
- win.document.removeEventListener("AboutReaderContentReady", detect);
- let sampleText = this._doc.getElementById(
- "moz-reader-content").textContent.substring(0, 60 * 1024);
- LanguageDetector.detectLanguage(sampleText).then(result => {
- resolve(result.confident ? result.language : null);
- });
- };
-
- if (win.document.body.classList.contains("loaded")) {
- detect();
- } else {
- win.document.addEventListener("AboutReaderContentReady", detect);
- }
- });
}
Narrator.prototype = {
@@ -71,7 +53,7 @@ Narrator.prototype = {
// For example, paragraphs. But nested anchors and other elements
// are not interesting since their text already appears in their
// parent's textContent.
- acceptNode: function(node) {
+ acceptNode(node) {
if (this._matches.has(node.parentNode)) {
// Reject sub-trees of accepted nodes.
return nf.FILTER_REJECT;
@@ -107,7 +89,7 @@ Narrator.prototype = {
// are no other strong references, and it will be GC'ed. Instead,
// we rely on the window's lifetime and use it as a weak reference.
this._treeWalkerRef.set(this._win,
- this._doc.createTreeWalker(this._doc.getElementById("container"),
+ this._doc.createTreeWalker(this._doc.querySelector(".container"),
nf.SHOW_ELEMENT, filter, false));
}
@@ -124,7 +106,7 @@ Narrator.prototype = {
this._win.speechSynthesis.pending;
},
- _getVoice: function(voiceURI) {
+ _getVoice(voiceURI) {
if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
this._voiceMap = new Map(
this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
@@ -133,7 +115,7 @@ Narrator.prototype = {
return this._voiceMap.get(voiceURI);
},
- _isParagraphInView: function(paragraph) {
+ _isParagraphInView(paragraph) {
if (!paragraph) {
return false;
}
@@ -142,13 +124,13 @@ Narrator.prototype = {
return bb.top >= 0 && bb.top < this._win.innerHeight;
},
- _sendTestEvent: function(eventType, detail) {
+ _sendTestEvent(eventType, detail) {
let win = this._win;
win.dispatchEvent(new win.CustomEvent(eventType,
{ detail: Cu.cloneInto(detail, win.document) }));
},
- _speakInner: function() {
+ _speakInner() {
this._win.speechSynthesis.cancel();
let tw = this._treeWalker;
let paragraph = tw.currentNode;
@@ -238,18 +220,12 @@ Narrator.prototype = {
return;
}
- // Match non-whitespace. This isn't perfect, but the most universal
- // solution for now.
- let reWordBoundary = /\S+/g;
- // Match the first word from the boundary event offset.
- reWordBoundary.lastIndex = e.charIndex;
- let firstIndex = reWordBoundary.exec(paragraph.textContent);
- if (firstIndex) {
- highlighter.highlight(firstIndex.index, reWordBoundary.lastIndex);
+ if (e.charLength) {
+ highlighter.highlight(e.charIndex, e.charLength);
if (this._inTest) {
this._sendTestEvent("wordhighlight", {
- start: firstIndex.index,
- end: reWordBoundary.lastIndex
+ start: e.charIndex,
+ end: e.charIndex + e.charLength
});
}
}
@@ -259,14 +235,14 @@ Narrator.prototype = {
});
},
- start: function(speechOptions) {
+ start(speechOptions) {
this._speechOptions = {
rate: speechOptions.rate,
voice: this._getVoice(speechOptions.voice)
};
this._stopped = false;
- return this.languagePromise.then(language => {
+ return this._languagePromise.then(language => {
if (!this._speechOptions.voice) {
this._speechOptions.lang = language;
}
@@ -288,32 +264,32 @@ Narrator.prototype = {
});
},
- stop: function() {
+ stop() {
this._stopped = true;
this._win.speechSynthesis.cancel();
},
- skipNext: function() {
+ skipNext() {
this._win.speechSynthesis.cancel();
},
- skipPrevious: function() {
+ skipPrevious() {
this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
},
- setRate: function(rate) {
+ setRate(rate) {
this._speechOptions.rate = rate;
/* repeat current paragraph */
this._goBackParagraphs(1);
},
- setVoice: function(voice) {
+ setVoice(voice) {
this._speechOptions.voice = this._getVoice(voice);
/* repeat current paragraph */
this._goBackParagraphs(1);
},
- _goBackParagraphs: function(count) {
+ _goBackParagraphs(count) {
let tw = this._treeWalker;
for (let i = 0; i < count; i++) {
if (!tw.previousNode()) {
@@ -338,13 +314,13 @@ Highlighter.prototype = {
* Highlight the range within offsets relative to the container.
*
* @param {Number} startOffset the start offset
- * @param {Number} endOffset the end offset
+ * @param {Number} length the length in characters of the range
*/
- highlight: function(startOffset, endOffset) {
+ highlight(startOffset, length) {
let containerRect = this.container.getBoundingClientRect();
- let range = this._getRange(startOffset, endOffset);
+ let range = this._getRange(startOffset, startOffset + length);
let rangeRects = range.getClientRects();
- let win = this.container.ownerDocument.defaultView;
+ let win = this.container.ownerGlobal;
let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
let nodes = this._getFreshHighlightNodes(rangeRects.length);
@@ -386,7 +362,7 @@ Highlighter.prototype = {
/**
* Releases reference to container and removes all highlight nodes.
*/
- remove: function() {
+ remove() {
for (let node of this._nodes) {
node.remove();
}
@@ -400,7 +376,7 @@ Highlighter.prototype = {
*
* @param {Number} count number of nodes needed
*/
- _getFreshHighlightNodes: function(count) {
+ _getFreshHighlightNodes(count) {
let doc = this.container.ownerDocument;
let nodes = Array.from(this._nodes);
@@ -427,7 +403,7 @@ Highlighter.prototype = {
* @param {Number} startOffset the start offset
* @param {Number} endOffset the end offset
*/
- _getRange: function(startOffset, endOffset) {
+ _getRange(startOffset, endOffset) {
let doc = this.container.ownerDocument;
let i = 0;
let treeWalker = doc.createTreeWalker(
diff --git a/toolkit/components/narrate/VoiceSelect.jsm b/toolkit/components/narrate/VoiceSelect.jsm
index b283a06b3..861a21c97 100644
--- a/toolkit/components/narrate/VoiceSelect.jsm
+++ b/toolkit/components/narrate/VoiceSelect.jsm
@@ -13,6 +13,7 @@ function VoiceSelect(win, label) {
let element = win.document.createElement("div");
element.classList.add("voiceselect");
+ // eslint-disable-next-line no-unsanitized/property
element.innerHTML =
`<button class="select-toggle" aria-controls="voice-options">
<span class="label">${label}</span> <span class="current-voice"></span>
@@ -37,7 +38,7 @@ function VoiceSelect(win, label) {
}
VoiceSelect.prototype = {
- add: function(label, value) {
+ add(label, value) {
let option = this._doc.createElement("button");
option.dataset.value = value;
option.classList.add("option");
@@ -48,7 +49,7 @@ VoiceSelect.prototype = {
return option;
},
- addOptions: function(options) {
+ addOptions(options) {
let selected = null;
for (let option of options) {
if (option.selected) {
@@ -61,11 +62,11 @@ VoiceSelect.prototype = {
this._select(selected || this.options[0], true);
},
- clear: function() {
+ clear() {
this.listbox.innerHTML = "";
},
- toggleList: function(force, focus = true) {
+ toggleList(force, focus = true) {
if (this.element.classList.toggle("open", force)) {
if (focus) {
(this.selected || this.options[0]).focus();
@@ -84,7 +85,7 @@ VoiceSelect.prototype = {
}
},
- handleEvent: function(evt) {
+ handleEvent(evt) {
let target = evt.target;
switch (evt.type) {
@@ -131,7 +132,7 @@ VoiceSelect.prototype = {
}
},
- _getPagedOption: function(option, up) {
+ _getPagedOption(option, up) {
let height = elem => elem.getBoundingClientRect().height;
let listboxHeight = height(this.listbox);
@@ -148,7 +149,7 @@ VoiceSelect.prototype = {
return next;
},
- _keyPressedButton: function(evt) {
+ _keyPressedButton(evt) {
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
this.toggleList(true);
return;
@@ -178,7 +179,7 @@ VoiceSelect.prototype = {
}
},
- _keyPressedInBox: function(evt) {
+ _keyPressedInBox(evt) {
let toFocus;
let cur = this._doc.activeElement;
@@ -212,7 +213,7 @@ VoiceSelect.prototype = {
}
},
- _select: function(option, suppressEvent = false) {
+ _select(option, suppressEvent = false) {
let oldSelected = this.selected;
if (oldSelected) {
oldSelected.removeAttribute("aria-selected");
@@ -233,7 +234,7 @@ VoiceSelect.prototype = {
}
},
- _updateDropdownHeight: function(now) {
+ _updateDropdownHeight(now) {
let updateInner = () => {
let winHeight = this._win.innerHeight;
let listbox = this.listbox;
@@ -252,7 +253,7 @@ VoiceSelect.prototype = {
}
},
- _getOptionFromValue: function(value) {
+ _getOptionFromValue(value) {
return Array.from(this.options).find(o => o.dataset.value === value);
},
diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css
index 2a8706c75..d02f216dc 100644
--- a/toolkit/components/printing/content/simplifyMode.css
+++ b/toolkit/components/printing/content/simplifyMode.css
@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This file defines specific rules for print preview when using simplify mode.
- * These rules already exist on aboutReaderControls.css, however, we decoupled it
+ * These rules already exist on aboutReader.css, however, we decoupled it
* from the original file so we don't need to load a bunch of extra queries that
* will not take effect when using the simplify page checkbox. This file defines
* styling for title and author on the header element. */
diff --git a/toolkit/components/reader/AboutReader.jsm b/toolkit/components/reader/AboutReader.jsm
index 1fb9db123..ae1ff9d60 100644
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -15,8 +15,7 @@ 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");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
@@ -45,45 +44,49 @@ var AboutReader = function(mm, win, articlePromise) {
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
this._article = null;
+ this._languagePromise = new Promise(resolve => {
+ this._foundLanguage = resolve;
+ });
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._headerElementRef = Cu.getWeakReference(doc.querySelector(".reader-header"));
+ this._domainElementRef = Cu.getWeakReference(doc.querySelector(".reader-domain"));
+ this._titleElementRef = Cu.getWeakReference(doc.querySelector(".reader-title"));
+ this._readTimeElementRef = Cu.getWeakReference(doc.querySelector(".reader-estimated-time"));
+ this._creditsElementRef = Cu.getWeakReference(doc.querySelector(".reader-credits"));
+ this._contentElementRef = Cu.getWeakReference(doc.querySelector(".moz-reader-content"));
+ this._toolbarElementRef = Cu.getWeakReference(doc.querySelector(".reader-toolbar"));
+ this._messageElementRef = Cu.getWeakReference(doc.querySelector(".reader-message"));
+ this._containerElementRef = Cu.getWeakReference(doc.querySelector(".container"));
this._scrollOffset = win.pageYOffset;
- doc.addEventListener("click", this, false);
+ doc.addEventListener("click", this);
- win.addEventListener("pagehide", this, false);
- win.addEventListener("scroll", this, false);
- win.addEventListener("resize", this, false);
+ win.addEventListener("pagehide", this);
+ win.addEventListener("scroll", this);
+ win.addEventListener("resize", this);
Services.obs.addObserver(this, "inner-window-destroyed", false);
- doc.addEventListener("visibilitychange", this, false);
+ doc.addEventListener("visibilitychange", this);
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");
- }
+ // 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" };
+ return {
+ name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
+ value,
+ itemClass: value + "-button"
+ };
});
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
@@ -114,7 +117,7 @@ var AboutReader = function(mm, win, articlePromise) {
this._setupLineHeightButtons();
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
- new NarrateControls(mm, win);
+ new NarrateControls(mm, win, this._languagePromise);
}
this._loadArticle();
@@ -146,6 +149,10 @@ AboutReader.prototype = {
return this._titleElementRef.get();
},
+ get _readTimeElement() {
+ return this._readTimeElementRef.get();
+ },
+
get _creditsElement() {
return this._creditsElementRef.get();
},
@@ -162,6 +169,10 @@ AboutReader.prototype = {
return this._messageElementRef.get();
},
+ get _containerElement() {
+ return this._containerElementRef.get();
+ },
+
get _isToolbarVertical() {
if (this._toolbarVertical !== undefined) {
return this._toolbarVertical;
@@ -178,7 +189,7 @@ AboutReader.prototype = {
return _viewId;
},
- receiveMessage: function (message) {
+ receiveMessage (message) {
switch (message.name) {
// Triggered by Android user pressing BACK while the banner font-dropdown is open.
case "Reader:CloseDropdown": {
@@ -191,14 +202,14 @@ AboutReader.prototype = {
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("class", "button " + message.data.id);
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");
+ let tb = this._toolbarElement;
tb.appendChild(btn);
this._setupButton(message.data.id, button => {
this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
@@ -220,18 +231,21 @@ AboutReader.prototype = {
}
},
- handleEvent: function(aEvent) {
+ handleEvent(aEvent) {
if (!aEvent.isTrusted)
return;
switch (aEvent.type) {
case "click":
let target = aEvent.target;
- if (target.classList.contains('dropdown-toggle')) {
+ if (target.classList.contains("dropdown-toggle")) {
this._toggleDropdownClicked(aEvent);
- } else if (!target.closest('.dropdown-popup')) {
+ } else if (!target.closest(".dropdown-popup")) {
this._closeDropdowns();
}
+ if (target.tagName == "A" && !target.classList.contains("reader-domain")) {
+ this._linkClicked(aEvent);
+ }
break;
case "scroll":
this._closeDropdowns(true);
@@ -243,7 +257,7 @@ AboutReader.prototype = {
this._updateImageMargins();
if (this._isToolbarVertical) {
this._win.setTimeout(() => {
- for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
+ for (let dropdown of this._doc.querySelectorAll(".dropdown.open")) {
this._updatePopupPosition(dropdown);
}
}, 0);
@@ -271,12 +285,12 @@ AboutReader.prototype = {
}
},
- observe: function(subject, topic, data) {
+ observe(subject, topic, data) {
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
return;
}
- Services.obs.removeObserver(this, "inner-window-destroyed", false);
+ Services.obs.removeObserver(this, "inner-window-destroyed");
this._mm.removeMessageListener("Reader:CloseDropdown", this);
this._mm.removeMessageListener("Reader:AddButton", this);
@@ -284,12 +298,12 @@ AboutReader.prototype = {
this._windowUnloaded = true;
},
- _onReaderClose: function() {
+ _onReaderClose() {
ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
},
- _setFontSize: function(newFontSize) {
- let containerClasses = this._doc.getElementById("container").classList;
+ _setFontSize(newFontSize) {
+ let containerClasses = this._containerElement.classList;
if (this._fontSize > 0)
containerClasses.remove("font-size" + this._fontSize);
@@ -299,19 +313,19 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.font_size", this._fontSize);
},
- _setupFontSizeButtons: function() {
+ _setupFontSizeButtons() {
const FONT_SIZE_MIN = 1;
const FONT_SIZE_MAX = 9;
// Sample text shown in Android UI.
- let sampleText = this._doc.getElementById("font-size-sample");
+ let sampleText = this._doc.querySelector(".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");
+ let plusButton = this._doc.querySelector(".plus-button");
+ let minusButton = this._doc.querySelector(".minus-button");
function updateControls() {
if (currentSize === FONT_SIZE_MIN) {
@@ -360,8 +374,8 @@ AboutReader.prototype = {
}, true);
},
- _setContentWidth: function(newContentWidth) {
- let containerClasses = this._doc.getElementById("container").classList;
+ _setContentWidth(newContentWidth) {
+ let containerClasses = this._containerElement.classList;
if (this._contentWidth > 0)
containerClasses.remove("content-width" + this._contentWidth);
@@ -371,15 +385,15 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.content_width", this._contentWidth);
},
- _setupContentWidthButtons: function() {
+ _setupContentWidthButtons() {
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");
+ let plusButton = this._doc.querySelector(".content-width-plus-button");
+ let minusButton = this._doc.querySelector(".content-width-minus-button");
function updateControls() {
if (currentContentWidth === CONTENT_WIDTH_MIN) {
@@ -428,8 +442,8 @@ AboutReader.prototype = {
}, true);
},
- _setLineHeight: function(newLineHeight) {
- let contentClasses = this._doc.getElementById("moz-reader-content").classList;
+ _setLineHeight(newLineHeight) {
+ let contentClasses = this._contentElement.classList;
if (this._lineHeight > 0)
contentClasses.remove("line-height" + this._lineHeight);
@@ -439,15 +453,15 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.line_height", this._lineHeight);
},
- _setupLineHeightButtons: function() {
+ _setupLineHeightButtons() {
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");
+ let plusButton = this._doc.querySelector(".line-height-plus-button");
+ let minusButton = this._doc.querySelector(".line-height-minus-button");
function updateControls() {
if (currentLineHeight === LINE_HEIGHT_MIN) {
@@ -496,7 +510,7 @@ AboutReader.prototype = {
}, true);
},
- _handleDeviceLight: function(newLux) {
+ _handleDeviceLight(newLux) {
// Desired size of the this._luxValues array.
let luxValuesSize = 10;
// Add new lux value at the front of the array.
@@ -513,7 +527,7 @@ AboutReader.prototype = {
return;
}
// Holds the average of the lux values collected in this._luxValues.
- let averageLuxValue = this._totalLux/luxValuesSize;
+ let averageLuxValue = this._totalLux / luxValuesSize;
this._updateColorScheme(averageLuxValue);
// Pop the oldest value off the array.
@@ -522,7 +536,7 @@ AboutReader.prototype = {
this._totalLux -= oldLux;
},
- _handleVisibilityChange: function() {
+ _handleVisibilityChange() {
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
if (colorScheme != "auto") {
return;
@@ -533,19 +547,19 @@ AboutReader.prototype = {
},
// Setup or teardown the ambient light tracking system.
- _enableAmbientLighting: function(enable) {
+ _enableAmbientLighting(enable) {
if (enable) {
- this._win.addEventListener("devicelight", this, false);
+ this._win.addEventListener("devicelight", this);
this._luxValues = [];
this._totalLux = 0;
} else {
- this._win.removeEventListener("devicelight", this, false);
+ this._win.removeEventListener("devicelight", this);
delete this._luxValues;
delete this._totalLux;
}
},
- _updateColorScheme: function(luxValue) {
+ _updateColorScheme(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".
@@ -564,7 +578,7 @@ AboutReader.prototype = {
this._setColorScheme("light");
},
- _setColorScheme: function(newColorScheme) {
+ _setColorScheme(newColorScheme) {
// "auto" is not a real color scheme
if (this._colorScheme === newColorScheme || newColorScheme === "auto")
return;
@@ -580,14 +594,14 @@ AboutReader.prototype = {
// 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) {
+ _setColorSchemePref(colorSchemePref) {
this._enableAmbientLighting(colorSchemePref === "auto");
this._setColorScheme(colorSchemePref);
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
},
- _setFontType: function(newFontType) {
+ _setFontType(newFontType) {
if (this._fontType === newFontType)
return;
@@ -602,20 +616,38 @@ AboutReader.prototype = {
AsyncPrefs.set("reader.font_type", this._fontType);
},
- _setSystemUIVisibility: function(visible) {
+ _setSystemUIVisibility(visible) {
this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
},
- _loadArticle: Task.async(function* () {
+ _setToolbarVisibility(visible) {
+ let tb = this._toolbarElement;
+
+ if (visible) {
+ if (tb.style.opacity != "1") {
+ tb.removeAttribute("hidden");
+ tb.style.opacity = "1";
+ }
+ } else if (tb.style.opacity != "0") {
+ tb.addEventListener("transitionend", evt => {
+ if (tb.style.opacity == "0") {
+ tb.setAttribute("hidden", "");
+ }
+ }, { once: true });
+ tb.style.opacity = "0";
+ }
+ },
+
+ async _loadArticle() {
let url = this._getOriginalUrl();
this._showProgressDelayed();
let article;
if (this._articlePromise) {
- article = yield this._articlePromise;
+ article = await this._articlePromise;
} else {
try {
- article = yield this._getArticle(url);
+ article = await this._getArticle(url);
} catch (e) {
if (e && e.newURL) {
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
@@ -638,24 +670,26 @@ AboutReader.prototype = {
}
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() {
+ _getArticle(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 });
+ // });
+ //}
+ return ReaderMode.downloadAndParseDocument(url);
+ },
+
+ _requestFavicon() {
let handleFaviconReturn = (message) => {
this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
this._loadFavicon(message.data.url, message.data.faviconUrl);
@@ -665,20 +699,20 @@ AboutReader.prototype = {
this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
},
- _loadFavicon: function(url, faviconUrl) {
+ _loadFavicon(url, faviconUrl) {
if (this._article.url !== url)
return;
let doc = this._doc;
- let link = doc.createElement('link');
- link.rel = 'shortcut icon';
+ let link = doc.createElement("link");
+ link.rel = "shortcut icon";
link.href = faviconUrl;
- doc.getElementsByTagName('head')[0].appendChild(link);
+ doc.getElementsByTagName("head")[0].appendChild(link);
},
- _updateImageMargins: function() {
+ _updateImageMargins() {
let windowWidth = this._win.innerWidth;
let bodyWidth = this._doc.body.clientWidth;
@@ -691,7 +725,7 @@ AboutReader.prototype = {
}
// If the image is at least half as wide as the body, center it on desktop.
- if (img.naturalWidth >= bodyWidth/2) {
+ if (img.naturalWidth >= bodyWidth / 2) {
img.setAttribute("moz-reader-center", true);
} else {
img.removeAttribute("moz-reader-center");
@@ -713,30 +747,32 @@ AboutReader.prototype = {
},
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
- if (!article.dir)
- return;
+ if (article.dir) {
+ // Set "dir" attribute on content
+ this._contentElement.setAttribute("dir", article.dir);
+ this._headerElement.setAttribute("dir", article.dir);
+
+ // The native locale could be set differently than the article's text direction.
+ var localeDirection = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
+ this._readTimeElement.setAttribute("dir", localeDirection);
+ this._readTimeElement.style.textAlign = article.dir == "rtl" ? "right" : "left";
+ }
+ },
+
+ _formatReadTime(slowEstimate, fastEstimate) {
+ let displayStringKey = "aboutReader.estimatedReadTimeRange1";
- // 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");
+ // only show one reading estimate when they are the same value
+ if (slowEstimate == fastEstimate) {
+ displayStringKey = "aboutReader.estimatedReadTimeValue1";
}
+
+ return PluralForm.get(slowEstimate, gStrings.GetStringFromName(displayStringKey))
+ .replace("#1", fastEstimate)
+ .replace("#2", slowEstimate);
},
- _showError: function() {
+ _showError() {
this._headerElement.style.display = "none";
this._contentElement.style.display = "none";
@@ -746,11 +782,16 @@ AboutReader.prototype = {
this._doc.title = errorMessage;
+ this._doc.documentElement.dataset.isError = true;
+
this._error = true;
+
+ this._doc.dispatchEvent(
+ new this._win.CustomEvent("AboutReaderContentError", { bubbles: true, cancelable: false }));
},
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
- _stripHost: function(host) {
+ _stripHost(host) {
if (!host)
return host;
@@ -766,17 +807,18 @@ AboutReader.prototype = {
return host.substring(start);
},
- _showContent: function(article) {
+ _showContent(article) {
this._messageElement.style.display = "none";
this._article = article;
this._domainElement.href = article.url;
- let articleUri = Services.io.newURI(article.url, null, null);
+ let articleUri = Services.io.newURI(article.url);
this._domainElement.textContent = this._stripHost(articleUri.host);
this._creditsElement.textContent = article.byline;
this._titleElement.textContent = article.title;
+ this._readTimeElement.textContent = this._formatReadTime(article.readingTimeMinsSlow, article.readingTimeMinsFast);
this._doc.title = article.title;
this._headerElement.style.display = "block";
@@ -787,8 +829,8 @@ AboutReader.prototype = {
false, articleUri, this._contentElement);
this._contentElement.innerHTML = "";
this._contentElement.appendChild(contentFragment);
- this._fixLocalLinks();
this._maybeSetTextDirection(article);
+ this._foundLanguage(article.language);
this._contentElement.style.display = "block";
this._updateImageMargins();
@@ -804,13 +846,13 @@ AboutReader.prototype = {
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
},
- _hideContent: function() {
+ _hideContent() {
this._headerElement.style.display = "none";
this._contentElement.style.display = "none";
},
- _showProgressDelayed: function() {
- this._win.setTimeout(function() {
+ _showProgressDelayed() {
+ this._win.setTimeout(() => {
// 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.
@@ -823,20 +865,20 @@ AboutReader.prototype = {
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
this._messageElement.style.display = "block";
- }.bind(this), 300);
+ }, 300);
},
/**
* Returns the original article URL for this about:reader view.
*/
- _getOriginalUrl: function(win) {
+ _getOriginalUrl(win) {
let url = win ? win.location.href : this._win.location.href;
return ReaderMode.getOriginalUrl(url) || url;
},
- _setupSegmentedButton: function(id, options, initialValue, callback) {
+ _setupSegmentedButton(id, options, initialValue, callback) {
let doc = this._doc;
- let segmentedButton = doc.getElementById(id);
+ let segmentedButton = doc.getElementsByClassName(id)[0];
for (let i = 0; i < options.length; i++) {
let option = options[i];
@@ -867,10 +909,6 @@ AboutReader.prototype = {
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");
@@ -878,19 +916,19 @@ AboutReader.prototype = {
item.classList.add("selected");
callback(option.value);
- }.bind(this), true);
+ }, true);
if (option.value === initialValue)
item.classList.add("selected");
}
},
- _setupButton: function(id, callback, titleEntity, textEntity) {
+ _setupButton(id, callback, titleEntity, textEntity) {
if (titleEntity) {
this._setButtonTip(id, titleEntity);
}
- let button = this._doc.getElementById(id);
+ let button = this._doc.getElementsByClassName(id)[0];
if (textEntity) {
button.textContent = gStrings.GetStringFromName(textEntity);
}
@@ -910,17 +948,17 @@ AboutReader.prototype = {
* and dynamically as button state changes.
* @param Localizable string providing UI element usage tip.
*/
- _setButtonTip: function(id, titleEntity) {
- let button = this._doc.getElementById(id);
+ _setButtonTip(id, titleEntity) {
+ let button = this._doc.getElementsByClassName(id)[0];
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
},
- _setupStyleDropdown: function() {
- let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
+ _setupStyleDropdown() {
+ let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle");
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
},
- _updatePopupPosition: function(dropdown) {
+ _updatePopupPosition(dropdown) {
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
@@ -931,8 +969,8 @@ AboutReader.prototype = {
dropdownPopup.style.top = popupTop + "px";
},
- _toggleDropdownClicked: function(event) {
- let dropdown = event.target.closest('.dropdown');
+ _toggleDropdownClicked(event) {
+ let dropdown = event.target.closest(".dropdown");
if (!dropdown)
return;
@@ -952,7 +990,7 @@ AboutReader.prototype = {
/*
* If the ReaderView banner font-dropdown is closed, open it.
*/
- _openDropdown: function(dropdown) {
+ _openDropdown(dropdown) {
if (dropdown.classList.contains("open")) {
return;
}
@@ -969,7 +1007,7 @@ AboutReader.prototype = {
* dropdowns because the page is scrolling, allow popups to stay open with
* the keep-open class.
*/
- _closeDropdowns: function(scrolling) {
+ _closeDropdowns(scrolling) {
let selector = ".dropdown.open";
if (scrolling) {
selector += ":not(.keep-open)";
@@ -987,6 +1025,18 @@ AboutReader.prototype = {
},
/*
+ * Override link handling for same-page references so we don't exit Reader View.
+ */
+ _linkClicked(event) {
+ var originalUrl = Services.io.newURI(this._getOriginalUrl(), null, null);
+ var targetUrl = Services.io.newURI(event.target.href, null, null);
+ if (originalUrl.specIgnoringRef == targetUrl.specIgnoringRef) {
+ event.preventDefault();
+ this._goToReference(targetUrl.ref);
+ }
+ },
+
+ /*
* Scroll reader view to a reference
*/
_goToReference(ref) {
diff --git a/toolkit/components/reader/JSDOMParser.js b/toolkit/components/reader/JSDOMParser.js
index 853649775..38f59c4ea 100644
--- a/toolkit/components/reader/JSDOMParser.js
+++ b/toolkit/components/reader/JSDOMParser.js
@@ -1017,46 +1017,6 @@
}
},
- readScript: function (node) {
- while (this.currentChar < this.html.length) {
- var c = this.nextChar();
- var nextC = this.peekNext();
- if (c === "<") {
- if (nextC === "!" || nextC === "?") {
- // We're still before the ! or ? that is starting this comment:
- this.currentChar++;
- node.appendChild(this.discardNextComment());
- continue;
- }
- if (nextC === "/" && this.html.substr(this.currentChar, 8 /*"/script>".length */).toLowerCase() == "/script>") {
- // Go back before the '<' so we find the end tag.
- this.currentChar--;
- // Done with this script tag, the caller will close:
- return;
- }
- }
- // Either c wasn't a '<' or it was but we couldn't find either a comment
- // or a closing script tag, so we should just parse as text until the next one
- // comes along:
-
- var haveTextNode = node.lastChild && node.lastChild.nodeType === Node.TEXT_NODE;
- var textNode = haveTextNode ? node.lastChild : new Text();
- var n = this.html.indexOf("<", this.currentChar);
- // Decrement this to include the current character *afterwards* so we don't get stuck
- // looking for the same < all the time.
- this.currentChar--;
- if (n === -1) {
- textNode.innerHTML += this.html.substring(this.currentChar, this.html.length);
- this.currentChar = this.html.length;
- } else {
- textNode.innerHTML += this.html.substring(this.currentChar, n);
- this.currentChar = n;
- }
- if (!haveTextNode)
- node.appendChild(textNode);
- }
- },
-
discardNextComment: function() {
if (this.match("--")) {
this.discardTo("-->");
@@ -1131,11 +1091,7 @@
// If this isn't a void Element, read its child nodes
if (!closed) {
- if (localName == "script") {
- this.readScript(node);
- } else {
- this.readChildren(node);
- }
+ this.readChildren(node);
var closingTag = "</" + localName + ">";
if (!this.match(closingTag)) {
this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js
index 491461a8e..04949dc61 100644
--- a/toolkit/components/reader/Readability.js
+++ b/toolkit/components/reader/Readability.js
@@ -38,32 +38,22 @@ function Readability(uri, doc, options) {
this._uri = uri;
this._doc = doc;
- this._biggestFrame = false;
+ this._articleTitle = null;
this._articleByline = null;
this._articleDir = null;
- // Configureable options
+ // Configurable options
this._debug = !!options.debug;
this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
- this._maxPages = options.maxPages || this.DEFAULT_MAX_PAGES;
+ this._wordThreshold = options.wordThreshold || this.DEFAULT_WORD_THRESHOLD;
+ this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []);
// Start with all flags set
this._flags = this.FLAG_STRIP_UNLIKELYS |
this.FLAG_WEIGHT_CLASSES |
this.FLAG_CLEAN_CONDITIONALLY;
- // The list of pages we've parsed in this call of readability,
- // for autopaging. As a key store for easier searching.
- this._parsedPages = {};
-
- // A list of the ETag headers of pages we've parsed, in case they happen to match,
- // we'll know it's a duplicate.
- this._pageETags = {};
-
- // Make an AJAX request for each page and append it to the document.
- this._curPageNum = 1;
-
var logEl;
// Control whether log messages are sent to the console
@@ -82,12 +72,12 @@ function Readability(uri, doc, options) {
return rv + elDesc;
};
this.log = function () {
- if (typeof dump !== undefined) {
+ if (typeof dump !== "undefined") {
var msg = Array.prototype.map.call(arguments, function(x) {
return (x && x.nodeName) ? logEl(x) : x;
}).join(" ");
dump("Reader: (Readability) " + msg + "\n");
- } else if (typeof console !== undefined) {
+ } else if (typeof console !== "undefined") {
var args = ["Reader: (Readability) "].concat(arguments);
console.log.apply(console, args);
}
@@ -109,20 +99,19 @@ Readability.prototype = {
// tight the competition is among candidates.
DEFAULT_N_TOP_CANDIDATES: 5,
- // The maximum number of pages to loop through before we call
- // it quits and just show a link.
- DEFAULT_MAX_PAGES: 5,
-
// Element tags to score by default.
DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
+ // The default number of words an article must have in order to return a result
+ DEFAULT_WORD_THRESHOLD: 500,
+
// All of the regular expressions in use within readability.
// Defined up here so we don't instantiate them repeatedly in loops.
REGEXPS: {
- unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|modal|related|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
+ unlikelyCandidates: /banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
- negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|modal|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+ negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
byline: /byline|author|dateline|writtenby|p-author/i,
replaceFonts: /<(\/?)font[^>]*>/gi,
@@ -138,6 +127,13 @@ Readability.prototype = {
ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
+ PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ],
+
+ DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ],
+
+ // These are the classes that readability sets itself.
+ CLASSES_TO_PRESERVE: [ "readability-styled", "page" ],
+
/**
* Run any post-process modifications to article content as necessary.
*
@@ -147,6 +143,9 @@ Readability.prototype = {
_postProcessContent: function(articleContent) {
// Readability cannot open relative uris so we convert them to absolute uris.
this._fixRelativeUris(articleContent);
+
+ // Remove classes.
+ this._cleanClasses(articleContent);
},
/**
@@ -155,8 +154,8 @@ Readability.prototype = {
*
* If function is not passed, removes all the nodes in node list.
*
- * @param NodeList nodeList The no
- * @param Function filterFn
+ * @param NodeList nodeList The nodes to operate on
+ * @param Function filterFn the function to use as a filter
* @return void
*/
_removeNodes: function(nodeList, filterFn) {
@@ -172,6 +171,20 @@ Readability.prototype = {
},
/**
+ * Iterates over a NodeList, and calls _setNodeTag for each node.
+ *
+ * @param NodeList nodeList The nodes to operate on
+ * @param String newTagName the new tag name to use
+ * @return void
+ */
+ _replaceNodeTags: function(nodeList, newTagName) {
+ for (var i = nodeList.length - 1; i >= 0; i--) {
+ var node = nodeList[i];
+ this._setNodeTag(node, newTagName);
+ }
+ },
+
+ /**
* Iterate over a NodeList, which doesn't natively fully implement the Array
* interface.
*
@@ -180,10 +193,9 @@ Readability.prototype = {
*
* @param NodeList nodeList The NodeList.
* @param Function fn The iterate function.
- * @param Boolean backward Whether to use backward iteration.
* @return void
*/
- _forEachNode: function(nodeList, fn, backward) {
+ _forEachNode: function(nodeList, fn) {
Array.prototype.forEach.call(nodeList, fn, this);
},
@@ -228,6 +240,34 @@ Readability.prototype = {
},
/**
+ * Removes the class="" attribute from every element in the given
+ * subtree, except those that match CLASSES_TO_PRESERVE and
+ * the classesToPreserve array from the options object.
+ *
+ * @param Element
+ * @return void
+ */
+ _cleanClasses: function(node) {
+ var classesToPreserve = this._classesToPreserve;
+ var className = (node.getAttribute("class") || "")
+ .split(/\s+/)
+ .filter(function(cls) {
+ return classesToPreserve.indexOf(cls) != -1;
+ })
+ .join(" ");
+
+ if (className) {
+ node.setAttribute("class", className);
+ } else {
+ node.removeAttribute("class");
+ }
+
+ for (node = node.firstElementChild; node; node = node.nextElementSibling) {
+ this._cleanClasses(node);
+ }
+ },
+
+ /**
* Converts each <a> and <img> uri in the given element to an absolute URI,
* ignoring #ref URIs.
*
@@ -307,11 +347,20 @@ Readability.prototype = {
curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]);
} catch (e) {/* ignore exceptions setting the title. */}
- if (curTitle.match(/ [\|\-] /)) {
- curTitle = origTitle.replace(/(.*)[\|\-] .*/gi, '$1');
+ var titleHadHierarchicalSeparators = false;
+ function wordCount(str) {
+ return str.split(/\s+/).length;
+ }
+
+ // If there's a separator in the title, first remove the final part
+ if ((/ [\|\-\\\/>»] /).test(curTitle)) {
+ titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
+ curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1');
- if (curTitle.split(' ').length < 3)
- curTitle = origTitle.replace(/[^\|\-]*[\|\-](.*)/gi, '$1');
+ // If the resulting title is too short (3 words or fewer), remove
+ // the first part instead:
+ if (wordCount(curTitle) < 3)
+ curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1');
} else if (curTitle.indexOf(': ') !== -1) {
// Check if we have an heading containing this exact string, so we
// could assume it's the full title.
@@ -328,8 +377,13 @@ Readability.prototype = {
curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);
// If the title is now too short, try the first colon instead:
- if (curTitle.split(' ').length < 3)
+ if (wordCount(curTitle) < 3) {
curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
+ // But if we have too many words before the colon there's something weird
+ // with the titles and the H tags so let's just use the original title instead
+ } else if (wordCount(origTitle.substr(0, origTitle.indexOf(':'))) > 5) {
+ curTitle = origTitle;
+ }
}
} else if (curTitle.length > 150 || curTitle.length < 15) {
var hOnes = doc.getElementsByTagName('h1');
@@ -339,9 +393,16 @@ Readability.prototype = {
}
curTitle = curTitle.trim();
-
- if (curTitle.split(' ').length <= 4)
+ // If we now have 4 words or fewer as our title, and either no
+ // 'hierarchical' separators (\, /, > or ») were found in the original
+ // title or we decreased the number of words by more than 1 word, use
+ // the original title.
+ var curTitleWordCount = wordCount(curTitle);
+ if (curTitleWordCount <= 4 &&
+ (!titleHadHierarchicalSeparators ||
+ curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) {
curTitle = origTitle;
+ }
return curTitle;
},
@@ -362,9 +423,7 @@ Readability.prototype = {
this._replaceBrs(doc.body);
}
- this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) {
- this._setNodeTag(fontNode, "SPAN");
- });
+ this._replaceNodeTags(doc.getElementsByTagName("font"), "SPAN");
},
/**
@@ -464,19 +523,49 @@ Readability.prototype = {
_prepArticle: function(articleContent) {
this._cleanStyles(articleContent);
+ // Check for data tables before we continue, to avoid removing items in
+ // those tables, which will often be isolated even though they're
+ // visually linked to other content-ful elements (text, images, etc.).
+ this._markDataTables(articleContent);
+
// Clean out junk from the article content
this._cleanConditionally(articleContent, "form");
+ this._cleanConditionally(articleContent, "fieldset");
this._clean(articleContent, "object");
this._clean(articleContent, "embed");
this._clean(articleContent, "h1");
this._clean(articleContent, "footer");
- // If there is only one h2, they are probably using it as a header
- // and not a subheader, so remove it since we already have a header.
- if (articleContent.getElementsByTagName('h2').length === 1)
- this._clean(articleContent, "h2");
+ // Clean out elements have "share" in their id/class combinations from final top candidates,
+ // which means we don't remove the top candidates even they have "share".
+ this._forEachNode(articleContent.children, function(topCandidate) {
+ this._cleanMatchedNodes(topCandidate, /share/);
+ });
+
+ // If there is only one h2 and its text content substantially equals article title,
+ // they are probably using it as a header and not a subheader,
+ // so remove it since we already extract the title separately.
+ var h2 = articleContent.getElementsByTagName('h2');
+ if (h2.length === 1) {
+ var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length;
+ if (Math.abs(lengthSimilarRate) < 0.5) {
+ var titlesMatch = false;
+ if (lengthSimilarRate > 0) {
+ titlesMatch = h2[0].textContent.includes(this._articleTitle);
+ } else {
+ titlesMatch = this._articleTitle.includes(h2[0].textContent);
+ }
+ if (titlesMatch) {
+ this._clean(articleContent, "h2");
+ }
+ }
+ }
this._clean(articleContent, "iframe");
+ this._clean(articleContent, "input");
+ this._clean(articleContent, "textarea");
+ this._clean(articleContent, "select");
+ this._clean(articleContent, "button");
this._cleanHeaders(articleContent);
// Do these last as the previous stuff may have removed junk
@@ -662,9 +751,6 @@ Readability.prototype = {
var pageCacheHtml = page.innerHTML;
- // Check if any "dir" is set on the toplevel document element
- this._articleDir = doc.documentElement.getAttribute("dir");
-
while (true) {
var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS);
@@ -695,6 +781,15 @@ Readability.prototype = {
}
}
+ // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe).
+ if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" ||
+ node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" ||
+ node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") &&
+ this._isElementWithoutContent(node)) {
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) {
elementsToScore.push(node);
}
@@ -709,13 +804,14 @@ Readability.prototype = {
var newNode = node.children[0];
node.parentNode.replaceChild(newNode, node);
node = newNode;
+ elementsToScore.push(node);
} else if (!this._hasChildBlockElement(node)) {
node = this._setNodeTag(node, "P");
elementsToScore.push(node);
} else {
// EXPERIMENTAL
this._forEachNode(node.childNodes, function(childNode) {
- if (childNode.nodeType === Node.TEXT_NODE) {
+ if (childNode.nodeType === Node.TEXT_NODE && childNode.textContent.trim().length > 0) {
var p = doc.createElement('p');
p.textContent = childNode.textContent;
p.style.display = 'inline';
@@ -812,6 +908,7 @@ Readability.prototype = {
var topCandidate = topCandidates[0] || null;
var neededToCreateTopCandidate = false;
+ var parentOfTopCandidate;
// If we still have no top candidate, just use the body as a last resort.
// We also have to copy the body node so it is something we can modify.
@@ -831,6 +928,33 @@ Readability.prototype = {
this._initializeNode(topCandidate);
} else if (topCandidate) {
+ // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array
+ // and whose scores are quite closed with current `topCandidate` node.
+ var alternativeCandidateAncestors = [];
+ for (var i = 1; i < topCandidates.length; i++) {
+ if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) {
+ alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i]));
+ }
+ }
+ var MINIMUM_TOPCANDIDATES = 3;
+ if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) {
+ parentOfTopCandidate = topCandidate.parentNode;
+ while (parentOfTopCandidate.tagName !== "BODY") {
+ var listsContainingThisAncestor = 0;
+ for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) {
+ listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate));
+ }
+ if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) {
+ topCandidate = parentOfTopCandidate;
+ break;
+ }
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ }
+ }
+ if (!topCandidate.readability) {
+ this._initializeNode(topCandidate);
+ }
+
// Because of our bonus system, parents of candidates might have scores
// themselves. They get half of the node. There won't be nodes with higher
// scores than our topCandidate, but if we see the score going *up* in the first
@@ -838,11 +962,15 @@ Readability.prototype = {
// lurking in other places that we want to unify in. The sibling stuff
// below does some of that - but only if we've looked high enough up the DOM
// tree.
- var parentOfTopCandidate = topCandidate.parentNode;
+ parentOfTopCandidate = topCandidate.parentNode;
var lastScore = topCandidate.readability.contentScore;
// The scores shouldn't get too low.
var scoreThreshold = lastScore / 3;
- while (parentOfTopCandidate && parentOfTopCandidate.readability) {
+ while (parentOfTopCandidate.tagName !== "BODY") {
+ if (!parentOfTopCandidate.readability) {
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ continue;
+ }
var parentScore = parentOfTopCandidate.readability.contentScore;
if (parentScore < scoreThreshold)
break;
@@ -854,6 +982,17 @@ Readability.prototype = {
lastScore = parentOfTopCandidate.readability.contentScore;
parentOfTopCandidate = parentOfTopCandidate.parentNode;
}
+
+ // If the top candidate is the only child, use parent instead. This will help sibling
+ // joining logic when adjacent content is actually located in parent's sibling node.
+ parentOfTopCandidate = topCandidate.parentNode;
+ while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) {
+ topCandidate = parentOfTopCandidate;
+ parentOfTopCandidate = topCandidate.parentNode;
+ }
+ if (!topCandidate.readability) {
+ this._initializeNode(topCandidate);
+ }
}
// Now that we have the top candidate, look through its siblings for content
@@ -864,7 +1003,9 @@ Readability.prototype = {
articleContent.id = "readability-content";
var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2);
- var siblings = topCandidate.parentNode.children;
+ // Keep potential top candidate's parent node to try to get text direction of it later.
+ parentOfTopCandidate = topCandidate.parentNode;
+ var siblings = parentOfTopCandidate.children;
for (var s = 0, sl = siblings.length; s < sl; s++) {
var sibling = siblings[s];
@@ -927,24 +1068,22 @@ Readability.prototype = {
if (this._debug)
this.log("Article content post-prep: " + articleContent.innerHTML);
- if (this._curPageNum === 1) {
- if (neededToCreateTopCandidate) {
- // We already created a fake div thing, and there wouldn't have been any siblings left
- // for the previous loop, so there's no point trying to create a new div, and then
- // move all the children over. Just assign IDs and class names here. No need to append
- // because that already happened anyway.
- topCandidate.id = "readability-page-1";
- topCandidate.className = "page";
- } else {
- var div = doc.createElement("DIV");
- div.id = "readability-page-1";
- div.className = "page";
- var children = articleContent.childNodes;
- while (children.length) {
- div.appendChild(children[0]);
- }
- articleContent.appendChild(div);
+ if (neededToCreateTopCandidate) {
+ // We already created a fake div thing, and there wouldn't have been any siblings left
+ // for the previous loop, so there's no point trying to create a new div, and then
+ // move all the children over. Just assign IDs and class names here. No need to append
+ // because that already happened anyway.
+ topCandidate.id = "readability-page-1";
+ topCandidate.className = "page";
+ } else {
+ var div = doc.createElement("DIV");
+ div.id = "readability-page-1";
+ div.className = "page";
+ var children = articleContent.childNodes;
+ while (children.length) {
+ div.appendChild(children[0]);
}
+ articleContent.appendChild(div);
}
if (this._debug)
@@ -955,7 +1094,7 @@ Readability.prototype = {
// grabArticle with different flags set. This gives us a higher likelihood of
// finding the content, and the sieve approach gives us a higher likelihood of
// finding the -right- content.
- if (this._getInnerText(articleContent, true).length < 500) {
+ if (this._getInnerText(articleContent, true).length < this._wordThreshold) {
page.innerHTML = pageCacheHtml;
if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
@@ -968,6 +1107,18 @@ Readability.prototype = {
return null;
}
} else {
+ // Find out text direction from ancestors of final top candidate.
+ var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate));
+ this._someNode(ancestors, function(ancestor) {
+ if (!ancestor.tagName)
+ return false;
+ var articleDir = ancestor.getAttribute("dir");
+ if (articleDir) {
+ this._articleDir = articleDir;
+ return true;
+ }
+ return false;
+ });
return articleContent;
}
}
@@ -1044,12 +1195,15 @@ Readability.prototype = {
metadata.excerpt = values["twitter:description"];
}
- if ("og:title" in values) {
- // Use facebook open graph title.
- metadata.title = values["og:title"];
- } else if ("twitter:title" in values) {
- // Use twitter cards title.
- metadata.title = values["twitter:title"];
+ metadata.title = this._getArticleTitle();
+ if (!metadata.title) {
+ if ("og:title" in values) {
+ // Use facebook open graph title.
+ metadata.title = values["og:title"];
+ } else if ("twitter:title" in values) {
+ // Use twitter cards title.
+ metadata.title = values["twitter:title"];
+ }
}
return metadata;
@@ -1089,6 +1243,13 @@ Readability.prototype = {
});
},
+ _isElementWithoutContent: function(node) {
+ return node.nodeType === Node.ELEMENT_NODE &&
+ node.textContent.trim().length == 0 &&
+ (node.children.length == 0 ||
+ node.children.length == node.getElementsByTagName("br").length + node.getElementsByTagName("hr").length);
+ },
+
/**
* Determine whether element has any children block level elements.
*
@@ -1139,26 +1300,25 @@ Readability.prototype = {
* @return void
**/
_cleanStyles: function(e) {
- e = e || this._doc;
- if (!e)
+ if (!e || e.tagName.toLowerCase() === 'svg')
return;
- var cur = e.firstChild;
- // Remove any root styles, if we're able.
- if (typeof e.removeAttribute === 'function' && e.className !== 'readability-styled')
- e.removeAttribute('style');
-
- // Go until there are no more child nodes
- while (cur !== null) {
- if (cur.nodeType === cur.ELEMENT_NODE) {
- // Remove style attribute(s) :
- if (cur.className !== "readability-styled")
- cur.removeAttribute("style");
+ if (e.className !== 'readability-styled') {
+ // Remove `style` and deprecated presentational attributes
+ for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) {
+ e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]);
+ }
- this._cleanStyles(cur);
+ if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
+ e.removeAttribute('width');
+ e.removeAttribute('height');
}
+ }
- cur = cur.nextSibling;
+ var cur = e.firstElementChild;
+ while (cur !== null) {
+ this._cleanStyles(cur);
+ cur = cur.nextElementSibling;
}
},
@@ -1185,368 +1345,6 @@ Readability.prototype = {
},
/**
- * Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness.
- *
- * @author Dan Lacy
- * @return string the base url
- **/
- _findBaseUrl: function() {
- var uri = this._uri;
- var noUrlParams = uri.path.split("?")[0];
- var urlSlashes = noUrlParams.split("/").reverse();
- var cleanedSegments = [];
- var possibleType = "";
-
- for (var i = 0, slashLen = urlSlashes.length; i < slashLen; i += 1) {
- var segment = urlSlashes[i];
-
- // Split off and save anything that looks like a file type.
- if (segment.indexOf(".") !== -1) {
- possibleType = segment.split(".")[1];
-
- // If the type isn't alpha-only, it's probably not actually a file extension.
- if (!possibleType.match(/[^a-zA-Z]/))
- segment = segment.split(".")[0];
- }
-
- // EW-CMS specific segment replacement. Ugly.
- // Example: http://www.ew.com/ew/article/0,,20313460_20369436,00.html
- if (segment.indexOf(',00') !== -1)
- segment = segment.replace(',00', '');
-
- // If our first or second segment has anything looking like a page number, remove it.
- if (segment.match(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i) && ((i === 1) || (i === 0)))
- segment = segment.replace(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i, "");
-
- var del = false;
-
- // If this is purely a number, and it's the first or second segment,
- // it's probably a page number. Remove it.
- if (i < 2 && segment.match(/^\d{1,2}$/))
- del = true;
-
- // If this is the first segment and it's just "index", remove it.
- if (i === 0 && segment.toLowerCase() === "index")
- del = true;
-
- // If our first or second segment is smaller than 3 characters,
- // and the first segment was purely alphas, remove it.
- if (i < 2 && segment.length < 3 && !urlSlashes[0].match(/[a-z]/i))
- del = true;
-
- // If it's not marked for deletion, push it to cleanedSegments.
- if (!del)
- cleanedSegments.push(segment);
- }
-
- // This is our final, cleaned, base article URL.
- return uri.scheme + "://" + uri.host + cleanedSegments.reverse().join("/");
- },
-
- /**
- * Look for any paging links that may occur within the document.
- *
- * @param body
- * @return object (array)
- **/
- _findNextPageLink: function(elem) {
- var uri = this._uri;
- var possiblePages = {};
- var allLinks = elem.getElementsByTagName('a');
- var articleBaseUrl = this._findBaseUrl();
-
- // Loop through all links, looking for hints that they may be next-page links.
- // Things like having "page" in their textContent, className or id, or being a child
- // of a node with a page-y className or id.
- //
- // Also possible: levenshtein distance? longest common subsequence?
- //
- // After we do that, assign each page a score, and
- for (var i = 0, il = allLinks.length; i < il; i += 1) {
- var link = allLinks[i];
- var linkHref = allLinks[i].href.replace(/#.*$/, '').replace(/\/$/, '');
-
- // If we've already seen this page, ignore it.
- if (linkHref === "" ||
- linkHref === articleBaseUrl ||
- linkHref === uri.spec ||
- linkHref in this._parsedPages) {
- continue;
- }
-
- // If it's on a different domain, skip it.
- if (uri.host !== linkHref.split(/\/+/g)[1])
- continue;
-
- var linkText = this._getInnerText(link);
-
- // If the linkText looks like it's not the next page, skip it.
- if (linkText.match(this.REGEXPS.extraneous) || linkText.length > 25)
- continue;
-
- // If the leftovers of the URL after removing the base URL don't contain
- // any digits, it's certainly not a next page link.
- var linkHrefLeftover = linkHref.replace(articleBaseUrl, '');
- if (!linkHrefLeftover.match(/\d/))
- continue;
-
- if (!(linkHref in possiblePages)) {
- possiblePages[linkHref] = {"score": 0, "linkText": linkText, "href": linkHref};
- } else {
- possiblePages[linkHref].linkText += ' | ' + linkText;
- }
-
- var linkObj = possiblePages[linkHref];
-
- // If the articleBaseUrl isn't part of this URL, penalize this link. It could
- // still be the link, but the odds are lower.
- // Example: http://www.actionscript.org/resources/articles/745/1/JavaScript-and-VBScript-Injection-in-ActionScript-3/Page1.html
- if (linkHref.indexOf(articleBaseUrl) !== 0)
- linkObj.score -= 25;
-
- var linkData = linkText + ' ' + link.className + ' ' + link.id;
- if (linkData.match(this.REGEXPS.nextLink))
- linkObj.score += 50;
-
- if (linkData.match(/pag(e|ing|inat)/i))
- linkObj.score += 25;
-
- if (linkData.match(/(first|last)/i)) {
- // -65 is enough to negate any bonuses gotten from a > or » in the text,
- // If we already matched on "next", last is probably fine.
- // If we didn't, then it's bad. Penalize.
- if (!linkObj.linkText.match(this.REGEXPS.nextLink))
- linkObj.score -= 65;
- }
-
- if (linkData.match(this.REGEXPS.negative) || linkData.match(this.REGEXPS.extraneous))
- linkObj.score -= 50;
-
- if (linkData.match(this.REGEXPS.prevLink))
- linkObj.score -= 200;
-
- // If a parentNode contains page or paging or paginat
- var parentNode = link.parentNode;
- var positiveNodeMatch = false;
- var negativeNodeMatch = false;
-
- while (parentNode) {
- var parentNodeClassAndId = parentNode.className + ' ' + parentNode.id;
-
- if (!positiveNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(/pag(e|ing|inat)/i)) {
- positiveNodeMatch = true;
- linkObj.score += 25;
- }
-
- if (!negativeNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(this.REGEXPS.negative)) {
- // If this is just something like "footer", give it a negative.
- // If it's something like "body-and-footer", leave it be.
- if (!parentNodeClassAndId.match(this.REGEXPS.positive)) {
- linkObj.score -= 25;
- negativeNodeMatch = true;
- }
- }
-
- parentNode = parentNode.parentNode;
- }
-
- // If the URL looks like it has paging in it, add to the score.
- // Things like /page/2/, /pagenum/2, ?p=3, ?page=11, ?pagination=34
- if (linkHref.match(/p(a|g|ag)?(e|ing|ination)?(=|\/)[0-9]{1,2}/i) || linkHref.match(/(page|paging)/i))
- linkObj.score += 25;
-
- // If the URL contains negative values, give a slight decrease.
- if (linkHref.match(this.REGEXPS.extraneous))
- linkObj.score -= 15;
-
- /**
- * Minor punishment to anything that doesn't match our current URL.
- * NOTE: I'm finding this to cause more harm than good where something is exactly 50 points.
- * Dan, can you show me a counterexample where this is necessary?
- * if (linkHref.indexOf(window.location.href) !== 0) {
- * linkObj.score -= 1;
- * }
- **/
-
- // If the link text can be parsed as a number, give it a minor bonus, with a slight
- // bias towards lower numbered pages. This is so that pages that might not have 'next'
- // in their text can still get scored, and sorted properly by score.
- var linkTextAsNumber = parseInt(linkText, 10);
- if (linkTextAsNumber) {
- // Punish 1 since we're either already there, or it's probably
- // before what we want anyways.
- if (linkTextAsNumber === 1) {
- linkObj.score -= 10;
- } else {
- linkObj.score += Math.max(0, 10 - linkTextAsNumber);
- }
- }
- }
-
- // Loop thrugh all of our possible pages from above and find our top
- // candidate for the next page URL. Require at least a score of 50, which
- // is a relatively high confidence that this page is the next link.
- var topPage = null;
- for (var page in possiblePages) {
- if (possiblePages.hasOwnProperty(page)) {
- if (possiblePages[page].score >= 50 &&
- (!topPage || topPage.score < possiblePages[page].score))
- topPage = possiblePages[page];
- }
- }
-
- var nextHref = null;
- if (topPage) {
- nextHref = topPage.href.replace(/\/$/, '');
-
- this.log('NEXT PAGE IS ' + nextHref);
- this._parsedPages[nextHref] = true;
- }
- return nextHref;
- },
-
- _successfulRequest: function(request) {
- return (request.status >= 200 && request.status < 300) ||
- request.status === 304 ||
- (request.status === 0 && request.responseText);
- },
-
- _ajax: function(url, options) {
- var request = new XMLHttpRequest();
-
- function respondToReadyState(readyState) {
- if (request.readyState === 4) {
- if (this._successfulRequest(request)) {
- if (options.success)
- options.success(request);
- } else if (options.error) {
- options.error(request);
- }
- }
- }
-
- if (typeof options === 'undefined')
- options = {};
-
- request.onreadystatechange = respondToReadyState;
-
- request.open('get', url, true);
- request.setRequestHeader('Accept', 'text/html');
-
- try {
- request.send(options.postBody);
- } catch (e) {
- if (options.error)
- options.error();
- }
-
- return request;
- },
-
- _appendNextPage: function(nextPageLink) {
- var doc = this._doc;
- this._curPageNum += 1;
-
- var articlePage = doc.createElement("DIV");
- articlePage.id = 'readability-page-' + this._curPageNum;
- articlePage.className = 'page';
- articlePage.innerHTML = '<p class="page-separator" title="Page ' + this._curPageNum + '">&sect;</p>';
-
- doc.getElementById("readability-content").appendChild(articlePage);
-
- if (this._curPageNum > this._maxPages) {
- var nextPageMarkup = "<div style='text-align: center'><a href='" + nextPageLink + "'>View Next Page</a></div>";
- articlePage.innerHTML = articlePage.innerHTML + nextPageMarkup;
- return;
- }
-
- // Now that we've built the article page DOM element, get the page content
- // asynchronously and load the cleaned content into the div we created for it.
- (function(pageUrl, thisPage) {
- this._ajax(pageUrl, {
- success: function(r) {
-
- // First, check to see if we have a matching ETag in headers - if we do, this is a duplicate page.
- var eTag = r.getResponseHeader('ETag');
- if (eTag) {
- if (eTag in this._pageETags) {
- this.log("Exact duplicate page found via ETag. Aborting.");
- articlePage.style.display = 'none';
- return;
- }
- this._pageETags[eTag] = 1;
- }
-
- // TODO: this ends up doubling up page numbers on NYTimes articles. Need to generically parse those away.
- var page = doc.createElement("DIV");
-
- // Do some preprocessing to our HTML to make it ready for appending.
- // - Remove any script tags. Swap and reswap newlines with a unicode
- // character because multiline regex doesn't work in javascript.
- // - Turn any noscript tags into divs so that we can parse them. This
- // allows us to find any next page links hidden via javascript.
- // - Turn all double br's into p's - was handled by prepDocument in the original view.
- // Maybe in the future abstract out prepDocument to work for both the original document
- // and AJAX-added pages.
- var responseHtml = r.responseText.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
- responseHtml = responseHtml.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
- responseHtml = responseHtml.replace(/\uffff/g, '\n').replace(/<(\/?)noscript/gi, '<$1div');
- responseHtml = responseHtml.replace(this.REGEXPS.replaceFonts, '<$1span>');
-
- page.innerHTML = responseHtml;
- this._replaceBrs(page);
-
- // Reset all flags for the next page, as they will search through it and
- // disable as necessary at the end of grabArticle.
- this._flags = 0x1 | 0x2 | 0x4;
-
- var secondNextPageLink = this._findNextPageLink(page);
-
- // NOTE: if we end up supporting _appendNextPage(), we'll need to
- // change this call to be async
- var content = this._grabArticle(page);
-
- if (!content) {
- this.log("No content found in page to append. Aborting.");
- return;
- }
-
- // Anti-duplicate mechanism. Essentially, get the first paragraph of our new page.
- // Compare it against all of the the previous document's we've gotten. If the previous
- // document contains exactly the innerHTML of this first paragraph, it's probably a duplicate.
- var firstP = content.getElementsByTagName("P").length ? content.getElementsByTagName("P")[0] : null;
- if (firstP && firstP.innerHTML.length > 100) {
- for (var i = 1; i <= this._curPageNum; i += 1) {
- var rPage = doc.getElementById('readability-page-' + i);
- if (rPage && rPage.innerHTML.indexOf(firstP.innerHTML) !== -1) {
- this.log('Duplicate of page ' + i + ' - skipping.');
- articlePage.style.display = 'none';
- this._parsedPages[pageUrl] = true;
- return;
- }
- }
- }
-
- this._removeScripts(content);
-
- thisPage.innerHTML = thisPage.innerHTML + content.innerHTML;
-
- // After the page has rendered, post process the content. This delay is necessary because,
- // in webkit at least, offsetWidth is not set in time to determine image width. We have to
- // wait a little bit for reflow to finish before we can fix floating images.
- setTimeout((function() {
- this._postProcessContent(thisPage);
- }).bind(this), 500);
-
-
- if (secondNextPageLink)
- this._appendNextPage(secondNextPageLink);
- }
- });
- }).bind(this)(nextPageLink, articlePage);
- },
-
- /**
* Get an elements class/id weight. Uses regular expressions to tell if this
* element looks good or bad.
*
@@ -1617,16 +1415,17 @@ Readability.prototype = {
* @param HTMLElement node
* @param String tagName
* @param Number maxDepth
+ * @param Function filterFn a filter to invoke to determine whether this node 'counts'
* @return Boolean
*/
- _hasAncestorTag: function(node, tagName, maxDepth) {
+ _hasAncestorTag: function(node, tagName, maxDepth, filterFn) {
maxDepth = maxDepth || 3;
tagName = tagName.toUpperCase();
var depth = 0;
while (node.parentNode) {
- if (depth > maxDepth)
+ if (maxDepth > 0 && depth > maxDepth)
return false;
- if (node.parentNode.tagName === tagName)
+ if (node.parentNode.tagName === tagName && (!filterFn || filterFn(node.parentNode)))
return true;
node = node.parentNode;
depth++;
@@ -1635,6 +1434,93 @@ Readability.prototype = {
},
/**
+ * Return an object indicating how many rows and columns this table has.
+ */
+ _getRowAndColumnCount: function(table) {
+ var rows = 0;
+ var columns = 0;
+ var trs = table.getElementsByTagName("tr");
+ for (var i = 0; i < trs.length; i++) {
+ var rowspan = trs[i].getAttribute("rowspan") || 0;
+ if (rowspan) {
+ rowspan = parseInt(rowspan, 10);
+ }
+ rows += (rowspan || 1);
+
+ // Now look for column-related info
+ var columnsInThisRow = 0;
+ var cells = trs[i].getElementsByTagName("td");
+ for (var j = 0; j < cells.length; j++) {
+ var colspan = cells[j].getAttribute("colspan") || 0;
+ if (colspan) {
+ colspan = parseInt(colspan, 10);
+ }
+ columnsInThisRow += (colspan || 1);
+ }
+ columns = Math.max(columns, columnsInThisRow);
+ }
+ return {rows: rows, columns: columns};
+ },
+
+ /**
+ * Look for 'data' (as opposed to 'layout') tables, for which we use
+ * similar checks as
+ * https://dxr.mozilla.org/mozilla-central/rev/71224049c0b52ab190564d3ea0eab089a159a4cf/accessible/html/HTMLTableAccessible.cpp#920
+ */
+ _markDataTables: function(root) {
+ var tables = root.getElementsByTagName("table");
+ for (var i = 0; i < tables.length; i++) {
+ var table = tables[i];
+ var role = table.getAttribute("role");
+ if (role == "presentation") {
+ table._readabilityDataTable = false;
+ continue;
+ }
+ var datatable = table.getAttribute("datatable");
+ if (datatable == "0") {
+ table._readabilityDataTable = false;
+ continue;
+ }
+ var summary = table.getAttribute("summary");
+ if (summary) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ var caption = table.getElementsByTagName("caption")[0];
+ if (caption && caption.childNodes.length > 0) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ // If the table has a descendant with any of these tags, consider a data table:
+ var dataTableDescendants = ["col", "colgroup", "tfoot", "thead", "th"];
+ var descendantExists = function(tag) {
+ return !!table.getElementsByTagName(tag)[0];
+ };
+ if (dataTableDescendants.some(descendantExists)) {
+ this.log("Data table because found data-y descendant");
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ // Nested tables indicate a layout table:
+ if (table.getElementsByTagName("table")[0]) {
+ table._readabilityDataTable = false;
+ continue;
+ }
+
+ var sizeInfo = this._getRowAndColumnCount(table);
+ if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+ // Now just go by size entirely:
+ table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10;
+ }
+ },
+
+ /**
* Clean an element of all tags of type "tag" if they look fishy.
* "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
*
@@ -1652,6 +1538,15 @@ Readability.prototype = {
//
// TODO: Consider taking into account original contentScore here.
this._removeNodes(e.getElementsByTagName(tag), function(node) {
+ // First check if we're in a data table, in which case don't remove us.
+ var isDataTable = function(t) {
+ return t._readabilityDataTable;
+ };
+
+ if (this._hasAncestorTag(node, "table", -1, isDataTable)) {
+ return false;
+ }
+
var weight = this._getClassWeight(node);
var contentScore = 0;
@@ -1667,7 +1562,7 @@ Readability.prototype = {
// ominous signs, remove the element.
var p = node.getElementsByTagName("p").length;
var img = node.getElementsByTagName("img").length;
- var li = node.getElementsByTagName("li").length-100;
+ var li = node.getElementsByTagName("li").length - 100;
var input = node.getElementsByTagName("input").length;
var embedCount = 0;
@@ -1681,11 +1576,10 @@ Readability.prototype = {
var contentLength = this._getInnerText(node).length;
var haveToRemove =
- // Make an exception for elements with no p's and exactly 1 img.
- (img > p && !this._hasAncestorTag(node, "figure")) ||
+ (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) ||
(!isList && li > p) ||
(input > Math.floor(p/3)) ||
- (!isList && contentLength < 25 && (img === 0 || img > 2)) ||
+ (!isList && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) ||
(!isList && weight < 25 && linkDensity > 0.2) ||
(weight >= 25 && linkDensity > 0.5) ||
((embedCount === 1 && contentLength < 75) || embedCount > 1);
@@ -1696,6 +1590,25 @@ Readability.prototype = {
},
/**
+ * Clean out elements whose id/class combinations match specific string.
+ *
+ * @param Element
+ * @param RegExp match id/class combination.
+ * @return void
+ **/
+ _cleanMatchedNodes: function(e, regex) {
+ var endOfSearchMarkerNode = this._getNextNode(e, true);
+ var next = this._getNextNode(e);
+ while (next && next != endOfSearchMarkerNode) {
+ if (regex.test(next.className + " " + next.id)) {
+ next = this._removeAndGetNext(next);
+ } else {
+ next = this._getNextNode(next);
+ }
+ }
+ },
+
+ /**
* Clean out spurious headers from an Element. Checks things like classnames and link density.
*
* @param Element
@@ -1713,10 +1626,6 @@ Readability.prototype = {
return (this._flags & flag) > 0;
},
- _addFlag: function(flag) {
- this._flags = this._flags | flag;
- },
-
_removeFlag: function(flag) {
this._flags = this._flags & ~flag;
},
@@ -1807,20 +1716,10 @@ Readability.prototype = {
// Remove script tags from the document.
this._removeScripts(this._doc);
- // FIXME: Disabled multi-page article support for now as it
- // needs more work on infrastructure.
-
- // Make sure this document is added to the list of parsed pages first,
- // so we don't double up on the first page.
- // this._parsedPages[uri.spec.replace(/\/$/, '')] = true;
-
- // Pull out any possible next page link first.
- // var nextPageLink = this._findNextPageLink(doc.body);
-
this._prepDocument();
var metadata = this._getArticleMetadata();
- var articleTitle = metadata.title || this._getArticleTitle();
+ this._articleTitle = metadata.title;
var articleContent = this._grabArticle();
if (!articleContent)
@@ -1830,14 +1729,6 @@ Readability.prototype = {
this._postProcessContent(articleContent);
- // if (nextPageLink) {
- // // Append any additional pages after a small timeout so that people
- // // can start reading without having to wait for this to finish processing.
- // setTimeout((function() {
- // this._appendNextPage(nextPageLink);
- // }).bind(this), 500);
- // }
-
// If we haven't found an excerpt in the article's metadata, use the article's
// first paragraph as the excerpt. This is used for displaying a preview of
// the article's content.
@@ -1851,7 +1742,7 @@ Readability.prototype = {
var textContent = articleContent.textContent;
return {
uri: this._uri,
- title: articleTitle,
+ title: this._articleTitle,
byline: metadata.byline || this._articleByline,
dir: this._articleDir,
content: articleContent.innerHTML,
@@ -1861,3 +1752,7 @@ Readability.prototype = {
};
}
};
+
+if (typeof module === "object") {
+ module.exports = Readability;
+}
diff --git a/toolkit/components/reader/ReaderMode.jsm b/toolkit/components/reader/ReaderMode.jsm
index 033a02489..e9eb83154 100644
--- a/toolkit/components/reader/ReaderMode.jsm
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -8,15 +8,18 @@ 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;
+// Class names to preserve in the readerized output. We preserve these class
+// names so that rules in aboutReader.css can match them.
+const CLASSES_TO_PRESERVE = [
+ "caption",
+ "hidden",
+ "invisble",
+ "sr-only",
+ "visually-hidden",
+ "visuallyhidden",
+ "wp-caption",
+ "wp-caption-text",
+];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -24,17 +27,15 @@ 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, "EventDispatcher", "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"];
+ return scope.Readability;
});
this.ReaderMode = {
@@ -61,21 +62,13 @@ this.ReaderMode = {
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 () {
+ _getStateForParseOnLoad() {
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);
+ return isForceEnabled || isEnabled;
},
- observe: function(aMessage, aTopic, aData) {
+ observe(aMessage, aTopic, aData) {
switch (aTopic) {
case "nsPref:changed":
if (aData.startsWith("reader.parse-on-load.")) {
@@ -91,7 +84,7 @@ this.ReaderMode = {
* 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) {
+ enterReaderMode(docShell, win) {
let url = win.document.location.href;
let readerURL = "about:reader?url=" + encodeURIComponent(url);
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
@@ -112,7 +105,7 @@ this.ReaderMode = {
* 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) {
+ leaveReaderMode(docShell, win) {
let url = win.document.location.href;
let originalURL = this.getOriginalUrl(url);
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
@@ -136,14 +129,14 @@ this.ReaderMode = {
* @return The original URL for the article, or null if we did not find
* a properly formatted about:reader URL.
*/
- getOriginalUrl: function(url) {
+ getOriginalUrl(url) {
if (!url.startsWith("about:reader?")) {
return null;
}
let outerHash = "";
try {
- let uriObj = Services.io.newURI(url, null, null);
+ let uriObj = Services.io.newURI(url);
url = uriObj.specIgnoringRef;
outerHash = uriObj.ref;
} catch (ex) { /* ignore, use the raw string */ }
@@ -155,27 +148,45 @@ this.ReaderMode = {
let originalUrl = searchParams.get("url");
if (outerHash) {
try {
- let uriObj = Services.io.newURI(originalUrl, null, null);
- uriObj = Services.io.newURI('#' + outerHash, null, uriObj);
+ let uriObj = Services.io.newURI(originalUrl);
+ uriObj = Services.io.newURI("#" + outerHash, null, uriObj);
originalUrl = uriObj.spec;
} catch (ex) {}
}
return originalUrl;
},
+ getOriginalUrlObjectForDisplay(url) {
+ let originalUrl = this.getOriginalUrl(url);
+ if (originalUrl) {
+ let uriObj;
+ try {
+ uriObj = Services.uriFixup.createFixupURI(originalUrl, Services.uriFixup.FIXUP_FLAG_NONE);
+ } catch (ex) {
+ return null;
+ }
+ try {
+ return Services.uriFixup.createExposableURI(uriObj);
+ } catch (ex) {
+ return null;
+ }
+ }
+ return null;
+ },
+
/**
* 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) {
+ isProbablyReaderable(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);
+ let uri = Services.io.newURI(doc.location.href);
if (!this._shouldCheckUri(uri)) {
return false;
}
@@ -187,12 +198,12 @@ this.ReaderMode = {
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
},
- isNodeVisible: function(utils, node) {
+ isNodeVisible(utils, node) {
let bounds = utils.getBoundsWithoutFlushing(node);
return bounds.height > 0 && bounds.width > 0;
},
- getUtilsForWin: function(win) {
+ getUtilsForWin(win) {
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
},
@@ -204,16 +215,14 @@ this.ReaderMode = {
* @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)) {
+ parseDocument(doc) {
+ if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI");
return null;
}
- return yield this._readerParse(baseURI, doc);
- }),
+ return this._readerParse(doc);
+ },
/**
* Downloads and parses a document from a URL.
@@ -222,19 +231,28 @@ this.ReaderMode = {
* @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)) {
+ async downloadAndParseDocument(url) {
+ let doc = await this._downloadDocument(url);
+ if (!doc) {
+ return null;
+ }
+ if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI");
return null;
}
- return yield this._readerParse(uri, doc);
- }),
+ return await this._readerParse(doc);
+ },
- _downloadDocument: function (url) {
- let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
+ _downloadDocument(url) {
+ try {
+ if (!this._shouldCheckUri(Services.io.newURI(url))) {
+ return null;
+ }
+ } catch (ex) {
+ Cu.reportError(new Error(`Couldn't create URI from ${url} to download: ${ex}`));
+ return null;
+ }
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
@@ -243,14 +261,12 @@ this.ReaderMode = {
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;
}
@@ -261,7 +277,7 @@ this.ReaderMode = {
if (content) {
let urlIndex = content.toUpperCase().indexOf("URL=");
if (urlIndex > -1) {
- let baseURI = Services.io.newURI(url, null, null);
+ let baseURI = Services.io.newURI(url);
let newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
let newURL = newURI.spec;
let ssm = Services.scriptSecurityManager;
@@ -290,10 +306,10 @@ this.ReaderMode = {
// Convert these to real URIs to make sure the escaping (or lack
// thereof) is identical:
try {
- responseURL = Services.io.newURI(responseURL, null, null).specIgnoringRef;
+ responseURL = Services.io.newURI(responseURL).specIgnoringRef;
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
try {
- givenURL = Services.io.newURI(givenURL, null, null).specIgnoringRef;
+ givenURL = Services.io.newURI(givenURL).specIgnoringRef;
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
if (responseURL != givenURL) {
@@ -303,7 +319,6 @@ this.ReaderMode = {
return;
}
resolve(doc);
- histogram.add(DOWNLOAD_SUCCESS);
};
xhr.send();
});
@@ -318,17 +333,17 @@ this.ReaderMode = {
* @resolves JS object representing the article, or null if no article is found.
* @rejects OS.File.Error
*/
- getArticleFromCache: Task.async(function* (url) {
+ async getArticleFromCache(url) {
let path = this._toHashedPath(url);
try {
- let array = yield OS.File.read(path);
+ let array = await 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.
@@ -338,14 +353,14 @@ this.ReaderMode = {
* @resolves When the article is stored.
* @rejects OS.File.Error
*/
- storeArticleInCache: Task.async(function* (article) {
+ async storeArticleInCache(article) {
let array = new TextEncoder().encode(JSON.stringify(article));
let path = this._toHashedPath(article.url);
- yield this._ensureCacheDir();
+ await this._ensureCacheDir();
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
.then(success => {
OS.File.stat(path).then(info => {
- return Messaging.sendRequest({
+ return EventDispatcher.instance.sendRequest({
type: "Reader:AddedToCache",
url: article.url,
size: info.size,
@@ -353,7 +368,7 @@ this.ReaderMode = {
});
});
});
- }),
+ },
/**
* Removes an article from the cache given an article URI.
@@ -363,26 +378,29 @@ this.ReaderMode = {
* @resolves When the article is removed.
* @rejects OS.File.Error
*/
- removeArticleFromCache: Task.async(function* (url) {
+ async removeArticleFromCache(url) {
let path = this._toHashedPath(url);
- yield OS.File.remove(path);
- }),
+ await OS.File.remove(path);
+ },
- log: function(msg) {
+ log(msg) {
if (this.DEBUG)
dump("Reader: " + msg);
},
_blockedHosts: [
- "mail.google.com",
+ "amazon.com",
+ "basilisk-browser.org",
"github.com",
+ "mail.google.com",
+ "palemoon.org",
"pinterest.com",
"reddit.com",
"twitter.com",
"youtube.com",
],
- _shouldCheckUri: function (uri, isBaseUri = false) {
+ _shouldCheckUri(uri, isBaseUri = false) {
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
this.log("Not parsing URI scheme: " + uri.scheme);
return false;
@@ -412,59 +430,77 @@ this.ReaderMode = {
* 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");
+ async _readerParse(doc) {
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);
+ this.log("Aborting parse for " + doc.baseURIObject.spec + "; " + numTags + " elements found");
return null;
}
}
+ // Fetch this here before we send `doc` off to the worker thread, as later on the
+ // document might be nuked but we will still want the URI.
+ let {documentURI} = doc;
+
let uriParam = {
- spec: uri.spec,
- host: uri.host,
- prePath: uri.prePath,
- scheme: uri.scheme,
- pathBase: Services.io.newURI(".", null, uri).spec
+ spec: doc.baseURIObject.spec,
+ host: doc.baseURIObject.host,
+ prePath: doc.baseURIObject.prePath,
+ scheme: doc.baseURIObject.scheme,
+ pathBase: Services.io.newURI(".", null, doc.baseURIObject).spec
+ };
+
+ let langAttributes = {
+ charset: doc.characterSet,
+ lang: doc.documentElement.lang
};
let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
createInstance(Ci.nsIDOMSerializer);
let serializedDoc = serializer.serializeToString(doc);
+ let options = {
+ classesToPreserve: CLASSES_TO_PRESERVE,
+ };
+
let article = null;
try {
- article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
+ article = await ReaderWorker.post("parseDocument", [uriParam, serializedDoc, options]);
} catch (e) {
Cu.reportError("Error in ReaderWorker: " + e);
- histogram.add(PARSE_ERROR_WORKER);
}
+ // Explicitly null out doc to make it clear it might not be available from this
+ // point on.
+ doc = null;
+
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;
+ // Readability returns a URI object based on the baseURI, but we only care
+ // about the original document's URL from now on. This also avoids spoofing
+ // attempts where the baseURI doesn't match the domain of the documentURI
+ article.url = documentURI;
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);
+ await this._assignLanguage(article, langAttributes);
+ this._maybeAssignTextDirection(article);
+
+ this._assignReadTime(article);
+
return article;
- }),
+ },
get _cryptoHash() {
delete this._cryptoHash;
@@ -485,7 +521,7 @@ this.ReaderMode = {
* @param url The article URL. This should have referrers removed.
* @return The file path to the cached article.
*/
- _toHashedPath: function (url) {
+ _toHashedPath(url) {
let value = this._unicodeConverter.convertToByteArray(url);
this._cryptoHash.init(this._cryptoHash.MD5);
this._cryptoHash.update(value, value.length);
@@ -502,7 +538,7 @@ this.ReaderMode = {
* @resolves When the cache directory exists.
* @rejects OS.File.Error
*/
- _ensureCacheDir: function () {
+ _ensureCacheDir() {
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
return OS.File.exists(dir).then(exists => {
if (!exists) {
@@ -510,5 +546,107 @@ this.ReaderMode = {
}
return undefined;
});
- }
+ },
+
+ /**
+ * Sets a global language string value if possible. If langauge detection is
+ * available, use that. Otherwise, revert to a simpler mechanism using the
+ * document's lang attribute or charset.
+ *
+ * @return Promise
+ * @resolves when the language is detected
+ */
+ _assignLanguage(article, attributes) {
+ try {
+ Cu.import("resource://modules/translation/LanguageDetector.jsm");
+ return LanguageDetector.detectLanguage(article.textContent).then(result => {
+ article.language = result.confident ? result.language : null;
+ });
+ } catch(ex) {
+ return new Promise((resolve) => {
+ resolve(this._assignSimpleLanguage(attributes));
+ }).then(result => {
+ article.language = result;
+ });
+ }
+ },
+
+ _assignSimpleLanguage(attributes) {
+ var lang = attributes.lang.substring(0,2);
+ if (lang) {
+ return lang;
+ }
+
+ // If there is no lang attribute, try the charset.
+ // We can only use this for charsets that are specific to one language.
+ const charsetLang = new Map([
+ [ "us-ascii", "en" ],
+ [ "iso-8859-6", "ar" ],
+ [ "iso-8859-7", "el" ],
+ [ "iso-8859-8", "he" ],
+ [ "iso-8859-9", "tr" ],
+ [ "iso-8859-11", "th" ],
+ [ "jis_x0201", "ja" ],
+ [ "shift_jis", "ja" ],
+ [ "euc-jp", "ja" ]
+ ]);
+
+ return charsetLang.get(attributes.charset);
+ },
+
+ _maybeAssignTextDirection(article) {
+ // TODO: Remove the hardcoded language codes below once bug 1320265 is resolved.
+ if (!article.dir && ["ar", "fa", "he", "ug", "ur"].includes(article.language)) {
+ article.dir = "rtl";
+ }
+ },
+
+ /**
+ * Assigns the estimated reading time range of the article to the article object.
+ *
+ * @param article the article object to assign the reading time estimate to.
+ */
+ _assignReadTime(article) {
+ let lang = article.language || "en";
+ const readingSpeed = this._getReadingSpeedForLanguage(lang);
+ const charactersPerMinuteLow = readingSpeed.cpm - readingSpeed.variance;
+ const charactersPerMinuteHigh = readingSpeed.cpm + readingSpeed.variance;
+ const length = article.length;
+
+ article.readingTimeMinsSlow = Math.ceil(length / charactersPerMinuteLow);
+ article.readingTimeMinsFast = Math.ceil(length / charactersPerMinuteHigh);
+ },
+
+ /**
+ * Returns the reading speed of a selection of languages with likely variance.
+ *
+ * Reading speed estimated from a study done on reading speeds in various languages.
+ * study can be found here: http://iovs.arvojournals.org/article.aspx?articleid=2166061
+ *
+ * @return object with characters per minute and variance. Defaults to English
+ * if no suitable language is found in the collection.
+ */
+ _getReadingSpeedForLanguage(lang) {
+ const readingSpeed = new Map([
+ [ "en", {cpm: 987, variance: 118 } ],
+ [ "ar", {cpm: 612, variance: 88 } ],
+ [ "de", {cpm: 920, variance: 86 } ],
+ [ "es", {cpm: 1025, variance: 127 } ],
+ [ "fi", {cpm: 1078, variance: 121 } ],
+ [ "fr", {cpm: 998, variance: 126 } ],
+ [ "he", {cpm: 833, variance: 130 } ],
+ [ "it", {cpm: 950, variance: 140 } ],
+ [ "jw", {cpm: 357, variance: 56 } ],
+ [ "nl", {cpm: 978, variance: 143 } ],
+ [ "pl", {cpm: 916, variance: 126 } ],
+ [ "pt", {cpm: 913, variance: 145 } ],
+ [ "ru", {cpm: 986, variance: 175 } ],
+ [ "sk", {cpm: 885, variance: 145 } ],
+ [ "sv", {cpm: 917, variance: 156 } ],
+ [ "tr", {cpm: 1054, variance: 156 } ],
+ [ "zh", {cpm: 255, variance: 29 } ],
+ ]);
+
+ return readingSpeed.get(lang) || readingSpeed.get("en");
+ },
};
diff --git a/toolkit/components/reader/ReaderWorker.js b/toolkit/components/reader/ReaderWorker.js
index 20023d4e0..9ae589d7d 100644
--- a/toolkit/components/reader/ReaderWorker.js
+++ b/toolkit/components/reader/ReaderWorker.js
@@ -2,6 +2,8 @@
* 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/. */
+/* eslint-env mozilla/chrome-worker */
+
"use strict";
/**
@@ -40,11 +42,12 @@ var Agent = {
*
* @param {object} uri URI data for the document.
* @param {string} serializedDoc The serialized document.
+ * @param {object} options Options object to pass to Readability.
*
* @return {object} Article object returned from Readability.
*/
- parseDocument: function (uri, serializedDoc) {
+ parseDocument(uri, serializedDoc, options) {
let doc = new JSDOMParser().parse(serializedDoc);
- return new Readability(uri, doc).parse();
+ return new Readability(uri, doc, options).parse();
},
};
diff --git a/toolkit/components/reader/content/aboutReader.html b/toolkit/components/reader/content/aboutReader.html
index b9c1139f6..1aa644474 100644
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -7,63 +7,56 @@
<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>
+ <script type="text/javascript" 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="container">
+ <div class="header reader-header">
+ <a class="domain reader-domain"></a>
<div class="domain-border"></div>
- <h1 id="reader-title"></h1>
- <div id="reader-credits" class="credits"></div>
+ <h1 class="reader-title"></h1>
+ <div class="credits reader-credits"></div>
+ <div class="meta-data">
+ <div class="reader-estimated-time"></div>
+ </div>
</div>
+ <hr>
+
<div class="content">
- <style scoped>
- @import url("chrome://global/skin/aboutReaderContent.css");
- </style>
- <div id="moz-reader-content"></div>
+ <div class="moz-reader-content"></div>
</div>
<div>
- <style scoped>
- @import url("chrome://global/skin/aboutReaderControls.css");
- </style>
- <div id="reader-message"></div>
+ <div class="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">
+ <ul class="toolbar reader-toolbar">
+ <li><button class="button close-button"/></li>
+ <ul class="dropdown style-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"/>
+ <li class="dropdown-popup">
+ <div class="font-type-buttons"></div>
+ <hr>
+ <div class="font-size-buttons">
+ <button class="minus-button"/>
+ <button class="font-size-sample"/>
+ <button 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"/>
+ <hr>
+ <div class="content-width-buttons">
+ <button class="content-width-minus-button"/>
+ <button 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"/>
+ <hr>
+ <div class="line-height-buttons">
+ <button class="line-height-minus-button"/>
+ <button class="line-height-plus-button"/>
</div>
- <hr></hr>
- <div id="color-scheme-buttons"></div>
+ <hr>
+ <div class="color-scheme-buttons"></div>
<div class="dropdown-arrow"/>
</li>
</ul>
diff --git a/toolkit/components/reader/content/aboutReader.js b/toolkit/components/reader/content/aboutReader.js
index 17133e69d..6c963382e 100644
--- a/toolkit/components/reader/content/aboutReader.js
+++ b/toolkit/components/reader/content/aboutReader.js
@@ -4,6 +4,6 @@
"use strict";
-window.addEventListener("DOMContentLoaded", function () {
+window.addEventListener("DOMContentLoaded", function() {
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
});
diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js
index 1376f70a3..b392aaf88 100644
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -594,12 +594,6 @@ var Printing = {
contentElement.setAttribute("class", "content");
containerElement.appendChild(contentElement);
- // Create style element for content div and import aboutReaderContent.css
- let controlContentStyle = content.document.createElement("style");
- controlContentStyle.setAttribute("scoped", "");
- controlContentStyle.textContent = "@import url(\"chrome://global/skin/aboutReaderContent.css\");";
- contentElement.appendChild(controlContentStyle);
-
// Jam the article's content into content div
let readerContent = content.document.createElement("div");
readerContent.setAttribute("id", "moz-reader-content");
diff --git a/toolkit/locales/en-US/chrome/global/aboutReader.properties b/toolkit/locales/en-US/chrome/global/aboutReader.properties
index 8826630c2..ac004c545 100644
--- a/toolkit/locales/en-US/chrome/global/aboutReader.properties
+++ b/toolkit/locales/en-US/chrome/global/aboutReader.properties
@@ -13,6 +13,22 @@ aboutReader.colorScheme.dark=Dark
aboutReader.colorScheme.sepia=Sepia
aboutReader.colorScheme.auto=Auto
+# LOCALIZATION NOTE (aboutReader.estimatedReadTimeValue1): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of minutes it is estimated to take to read the article
+# example: `3 minutes`
+aboutReader.estimatedReadTimeValue1=#1 minute;#1 minutes
+
+#LOCALIZATION NOTE (aboutReader.estimatedReadingTimeRange1): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# When there is some uncertainty in how long the article will take to read show a range of
+# minutes it is expected to take.
+# #1 is the number of minutes it is estimated to take to read the article for a fast reader
+# #2 is the number of minutes it is estimated to take to read the article for a slow reader
+# #2 is the variable used to determine the plural form to use.
+# example: `5-8 minutes`
+aboutReader.estimatedReadTimeRange1=#1-#2 minute;#1-#2 minutes
+
# LOCALIZATION NOTE (aboutReader.fontType.serif, aboutReader.fontType.sans-serif):
# These are the styles of typeface that are options in the reader view controls.
aboutReader.fontType.serif=Serif
diff --git a/toolkit/themes/shared/aboutReader.css b/toolkit/themes/shared/aboutReader.css
index 49436de84..4dbf11f6d 100644
--- a/toolkit/themes/shared/aboutReader.css
+++ b/toolkit/themes/shared/aboutReader.css
@@ -2,23 +2,13 @@
* 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/. */
+/* Avoid adding ID selector rules in this style sheet, since they could
+ * inadvertently match elements in the article content. */
+
body {
padding: 64px 51px;
}
-@media print {
- #container {
- max-width: 100% !important;
- font-size: 14px !important;
- font-family: Georgia, "Times New Roman", serif !important;
- }
-
- body {
- padding-top: 0px;
- padding-bottom: 0px;
- }
-}
-
body.loaded {
transition: color 0.4s, background-color 0.4s;
}
@@ -56,80 +46,80 @@ body.serif .remove-button {
font-family: Georgia, "Times New Roman", serif;
}
-#container {
+.container {
max-width: 30em;
margin: 0 auto;
}
-#container.font-size1 {
+.container.font-size1 {
font-size: 12px;
}
-#container.font-size2 {
+.container.font-size2 {
font-size: 14px;
}
-#container.font-size3 {
+.container.font-size3 {
font-size: 16px;
}
-#container.font-size4 {
+.container.font-size4 {
font-size: 18px;
}
-#container.font-size5 {
+.container.font-size5 {
font-size: 20px;
}
-#container.font-size6 {
+.container.font-size6 {
font-size: 22px;
}
-#container.font-size7 {
+.container.font-size7 {
font-size: 24px;
}
-#container.font-size8 {
+.container.font-size8 {
font-size: 26px;
}
-#container.font-size9 {
+.container.font-size9 {
font-size: 28px;
}
-#container.content-width1 {
+.container.content-width1 {
max-width: 20em;
}
-#container.content-width2 {
+.container.content-width2 {
max-width: 25em;
}
-#container.content-width3 {
+.container.content-width3 {
max-width: 30em;
}
-#container.content-width4 {
+.container.content-width4 {
max-width: 35em;
}
-#container.content-width5 {
+.container.content-width5 {
max-width: 40em;
}
-#container.content-width6 {
+.container.content-width6 {
max-width: 45em;
}
-#container.content-width7 {
+.container.content-width7 {
max-width: 50em;
}
-#container.content-width8 {
+.container.content-width8 {
max-width: 55em;
}
-#container.content-width9 {
+.container.content-width9 {
max-width: 60em;
}
@@ -176,3 +166,575 @@ body:not(.loaded) .toolbar:-moz-locale-dir(ltr) {
body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
transform: translateX(100%);
}
+
+.light-button {
+ color: #333333;
+ background-color: #ffffff;
+}
+
+.dark-button {
+ color: #eeeeee;
+ background-color: #333333;
+}
+
+.sepia-button {
+ color: #5b4636;
+ background-color: #f4ecd8;
+}
+
+.sans-serif-button {
+ font-family: Helvetica, Arial, sans-serif;
+}
+
+.serif-button {
+ font-family: Georgia, "Times New Roman", serif;
+}
+
+/* Loading/error message */
+
+.reader-message {
+ margin-top: 40px;
+ display: none;
+ text-align: center;
+ width: 100%;
+ font-size: 0.9em;
+}
+
+/* Header */
+
+.header {
+ text-align: start;
+ display: none;
+}
+
+.domain {
+ font-size: 0.9em;
+ line-height: 1.48em;
+ padding-bottom: 4px;
+ font-family: Helvetica, Arial, sans-serif;
+ text-decoration: none;
+ border-bottom: 1px solid;
+ color: #0095dd;
+}
+
+.header > h1 {
+ font-size: 1.6em;
+ line-height: 1.25em;
+ width: 100%;
+ margin: 30px 0;
+ padding: 0;
+}
+
+.header > .credits {
+ font-size: 0.9em;
+ line-height: 1.48em;
+ margin: 0 0 10px 0;
+ padding: 0;
+ font-style: italic;
+}
+
+.header > .meta-data {
+ font-size: 0.65em;
+ margin: 0 0 15px 0;
+}
+
+/*======= Controls toolbar =======*/
+
+.toolbar {
+ font-family: Helvetica, Arial, sans-serif;
+ position: fixed;
+ height: 100%;
+ top: 0;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ background-color: #fbfbfb;
+ -moz-user-select: none;
+ border-right: 1px solid #b5b5b5;
+ z-index: 1;
+}
+
+.button {
+ display: block;
+ background-size: 24px 24px;
+ background-repeat: no-repeat;
+ color: #333;
+ background-color: #fbfbfb;
+ height: 40px;
+ padding: 0;
+}
+
+.toolbar .button {
+ width: 40px;
+ background-position: center;
+ margin-right: -1px;
+ border-top: 0;
+ border-left: 0;
+ border-right: 1px solid #b5b5b5;
+ border-bottom: 1px solid #c1c1c1;
+}
+
+.button[hidden] {
+ display: none;
+}
+
+.dropdown {
+ text-align: center;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.dropdown li {
+ margin: 0;
+ padding: 0;
+}
+
+/*======= Popup =======*/
+
+.dropdown-popup {
+ min-width: 300px;
+ text-align: start;
+ position: absolute;
+ left: 48px; /* offset to account for toolbar width */
+ z-index: 1000;
+ background-color: #fbfbfb;
+ visibility: hidden;
+ border-radius: 4px;
+ border: 1px solid #b5b5b5;
+ border-bottom-width: 0;
+ box-shadow: 0 1px 3px #c1c1c1;
+}
+
+.keep-open .dropdown-popup {
+ z-index: initial;
+}
+
+.dropdown-popup > hr {
+ display: none;
+}
+
+.open > .dropdown-popup {
+ visibility: visible;
+}
+
+.dropdown-arrow {
+ position: absolute;
+ top: 30px; /* offset arrow from top of popup */
+ left: -16px;
+ width: 16px;
+ height: 24px;
+ background-image: url("chrome://global/skin/reader/RM-Type-Controls-Arrow.svg");
+ display: block;
+}
+
+/*======= Font style popup =======*/
+
+.font-type-buttons,
+.font-size-buttons,
+.color-scheme-buttons,
+.content-width-buttons,
+.line-height-buttons {
+ display: flex;
+ flex-direction: row;
+}
+
+.font-type-buttons > button:first-child {
+ border-top-left-radius: 3px;
+}
+.font-type-buttons > button:last-child {
+ border-top-right-radius: 3px;
+}
+.color-scheme-buttons > button:first-child {
+ border-bottom-left-radius: 3px;
+}
+.color-scheme-buttons > button:last-child {
+ border-bottom-right-radius: 3px;
+}
+
+.font-type-buttons > button,
+.font-size-buttons > button,
+.color-scheme-buttons > button,
+.content-width-buttons > button,
+.line-height-buttons > button {
+ text-align: center;
+ border: 0;
+}
+
+.font-type-buttons > button,
+.font-size-buttons > button,
+.content-width-buttons > button,
+.line-height-buttons > button {
+ width: 50%;
+ background-color: transparent;
+ border-left: 1px solid #B5B5B5;
+ border-bottom: 1px solid #B5B5B5;
+}
+
+.color-scheme-buttons > button {
+ width: 33.33%;
+ font-size: 14px;
+}
+
+.color-scheme-buttons > .dark-button {
+ margin-top: -1px;
+ height: 61px;
+}
+
+.font-type-buttons > button:first-child,
+.font-size-buttons > button:first-child,
+.content-width-buttons > button:first-child,
+.line-height-buttons > button:first-child {
+ border-left: 0;
+}
+
+.font-type-buttons > button {
+ display: inline-block;
+ font-size: 62px;
+ height: 100px;
+}
+
+.font-size-buttons > button,
+.color-scheme-buttons > button,
+.content-width-buttons > button,
+.line-height-buttons > button {
+ height: 60px;
+}
+
+.font-type-buttons > button:active:hover,
+.font-type-buttons > button.selected,
+.color-scheme-buttons > button:active:hover,
+.color-scheme-buttons > button.selected {
+ box-shadow: inset 0 -3px 0 0 #fc6420;
+}
+
+.font-type-buttons > button:active:hover,
+.font-type-buttons > button.selected {
+ border-bottom: 1px solid #FC6420;
+}
+
+/* Make the serif button content the same size as the sans-serif button content. */
+.font-type-buttons > button > .description {
+ color: #666;
+ font-size: 12px;
+ margin-top: -5px;
+}
+
+/* Font sizes are different per-platform, so we need custom CSS to line them up. */
+%ifdef XP_MACOSX
+.font-type-buttons > .sans-serif-button > .name {
+ margin-top: 10px;
+}
+
+.font-type-buttons > .sans-serif-button > .description {
+ margin-top: -4px;
+}
+
+.font-type-buttons > .serif-button > .name {
+ font-size: 63px;
+}
+%elifdef XP_WIN
+.font-type-buttons > .sans-serif-button > .name {
+ margin-top: 2px;
+}
+
+.font-type-buttons > .sans-serif-button > .description {
+ margin-top: -4px;
+}
+
+.font-type-buttons > .serif-button > .name {
+ font-size: 63px;
+}
+%else
+.font-type-buttons > .sans-serif-button > .name {
+ margin-top: 5px;
+}
+
+.font-type-buttons > .sans-serif-button > .description {
+ margin-top: -8px;
+}
+
+.font-type-buttons > .serif-button > .name {
+ font-size: 70px;
+}
+%endif
+
+.button:hover,
+.font-size-buttons > button:hover,
+.font-type-buttons > button:hover,
+.content-width-buttons > button:hover,
+.line-height-buttons > button:hover {
+ background-color: #ebebeb;
+}
+
+.dropdown.open,
+.button:active,
+.font-size-buttons > button:active,
+.font-size-buttons > button.selected,
+.content-width-buttons > button:active,
+.content-width-buttons > button.selected,
+.line-height-buttons > button:active,
+.line-height-buttons > button.selected {
+ background-color: #dadada;
+}
+
+/* Only used on Android */
+.font-size-sample {
+ display: none;
+}
+
+.minus-button,
+.plus-button,
+.content-width-minus-button,
+.content-width-plus-button,
+.line-height-minus-button,
+.line-height-plus-button {
+ background-color: transparent;
+ border: 0;
+ background-size: 18px 18px;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+/*======= Toolbar icons =======*/
+
+.close-button {
+ background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close");
+ height: 68px;
+ background-position: center 8px;
+}
+
+.close-button:hover {
+ background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
+ background-color: #d94141;
+ border-bottom: 1px solid #d94141;
+ border-right: 1px solid #d94141;
+}
+
+.close-button:hover:active {
+ background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
+ background-color: #AE2325;
+ border-bottom: 1px solid #AE2325;
+ border-right: 1px solid #AE2325;
+}
+
+.style-button {
+ background-image: url("chrome://global/skin/reader/RM-Type-Controls-24x24.svg");
+}
+
+.minus-button {
+ background-image: url("chrome://global/skin/reader/RM-Minus-24x24.svg");
+}
+
+.plus-button {
+ background-image: url("chrome://global/skin/reader/RM-Plus-24x24.svg");
+}
+
+.content-width-minus-button {
+ background-size: 42px 16px;
+ background-image: url("chrome://global/skin/reader/RM-Content-Width-Minus-42x16.svg");
+}
+
+.content-width-plus-button {
+ background-size: 44px 16px;
+ background-image: url("chrome://global/skin/reader/RM-Content-Width-Plus-44x16.svg");
+}
+
+.line-height-minus-button {
+ background-size: 34px 14px;
+ background-image: url("chrome://global/skin/reader/RM-Line-Height-Minus-38x14.svg");
+}
+
+.line-height-plus-button {
+ background-size: 34px 24px;
+ background-image: url("chrome://global/skin/reader/RM-Line-Height-Plus-38x24.svg");
+}
+
+@media print {
+ .toolbar {
+ display: none !important;
+ }
+}
+
+/*======= Article content =======*/
+
+/* Note that any class names from the original article that we want to match on
+ * must be added to CLASSES_TO_PRESERVE in ReaderMode.jsm, so that
+ * Readability.js doesn't strip them out */
+
+.moz-reader-content {
+ display: none;
+ font-size: 1em;
+ line-height: 1.6em;
+}
+
+.moz-reader-content.line-height1 {
+ line-height: 1em;
+}
+
+.moz-reader-content.line-height2 {
+ line-height: 1.2em;
+}
+
+.moz-reader-content.line-height3 {
+ line-height: 1.4em;
+}
+
+.moz-reader-content.line-height4 {
+ line-height: 1.6em;
+}
+
+.moz-reader-content.line-height5 {
+ line-height: 1.8em;
+}
+
+.moz-reader-content.line-height6 {
+ line-height: 2.0em;
+}
+
+.moz-reader-content.line-height7 {
+ line-height: 2.2em;
+}
+
+.moz-reader-content.line-height8 {
+ line-height: 2.4em;
+}
+
+.moz-reader-content.line-height9 {
+ line-height: 2.6em;
+}
+
+@media print {
+ .moz-reader-content p,
+ .moz-reader-content code,
+ .moz-reader-content pre,
+ .moz-reader-content blockquote,
+ .moz-reader-content ul,
+ .moz-reader-content ol,
+ .moz-reader-content li,
+ .moz-reader-content figure,
+ .moz-reader-content .wp-caption {
+ margin: 0 0 10px 0 !important;
+ padding: 0 !important;
+ }
+}
+
+.moz-reader-content h1,
+.moz-reader-content h2,
+.moz-reader-content h3 {
+ font-weight: bold;
+}
+
+.moz-reader-content h1 {
+ font-size: 1.6em;
+ line-height: 1.25em;
+}
+
+.moz-reader-content h2 {
+ font-size: 1.2em;
+ line-height: 1.51em;
+}
+
+.moz-reader-content h3 {
+ font-size: 1em;
+ line-height: 1.66em;
+}
+
+.moz-reader-content a:link {
+ text-decoration: underline;
+ font-weight: normal;
+}
+
+.moz-reader-content a:link,
+.moz-reader-content a:link:hover,
+.moz-reader-content a:link:active {
+ color: #0095dd;
+}
+
+.moz-reader-content a:visited {
+ color: #c2e;
+}
+
+.moz-reader-content * {
+ max-width: 100%;
+ height: auto;
+}
+
+.moz-reader-content p,
+.moz-reader-content p,
+.moz-reader-content code,
+.moz-reader-content pre,
+.moz-reader-content blockquote,
+.moz-reader-content ul,
+.moz-reader-content ol,
+.moz-reader-content li,
+.moz-reader-content figure,
+.moz-reader-content .wp-caption {
+ margin: -10px -10px 20px -10px;
+ padding: 10px;
+ border-radius: 5px;
+}
+
+.moz-reader-content li {
+ margin-bottom: 0;
+}
+
+.moz-reader-content li > ul,
+.moz-reader-content li > ol {
+ margin-bottom: -10px;
+}
+
+.moz-reader-content p > img:only-child,
+.moz-reader-content p > a:only-child > img:only-child,
+.moz-reader-content .wp-caption img,
+.moz-reader-content figure img {
+ display: block;
+}
+
+.moz-reader-content img[moz-reader-center] {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.moz-reader-content .caption,
+.moz-reader-content .wp-caption-text
+.moz-reader-content figcaption {
+ font-size: 0.9em;
+ line-height: 1.48em;
+ font-style: italic;
+}
+
+.moz-reader-content code,
+.moz-reader-content pre {
+ white-space: pre-wrap;
+}
+
+.moz-reader-content blockquote {
+ padding: 0;
+ padding-inline-start: 16px;
+}
+
+.moz-reader-content ul,
+.moz-reader-content ol {
+ padding: 0;
+}
+
+.moz-reader-content ul {
+ padding-inline-start: 30px;
+ list-style: disc;
+}
+
+.moz-reader-content ol {
+ padding-inline-start: 30px;
+ list-style: decimal;
+}
+
+/* Hide elements with common "hidden" class names */
+.moz-reader-content .visually-hidden,
+.moz-reader-content .visuallyhidden,
+.moz-reader-content .hidden,
+.moz-reader-content .invisible,
+.moz-reader-content .sr-only {
+ display: none;
+}
diff --git a/toolkit/themes/shared/aboutReaderContent.css b/toolkit/themes/shared/aboutReaderContent.css
deleted file mode 100644
index 633391d37..000000000
--- a/toolkit/themes/shared/aboutReaderContent.css
+++ /dev/null
@@ -1,177 +0,0 @@
-/* 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/. */
-
-#moz-reader-content {
- display: none;
- font-size: 1em;
- line-height: 1.6em;
-}
-
-#moz-reader-content.line-height1 {
- line-height: 1em;
-}
-
-#moz-reader-content.line-height2 {
- line-height: 1.2em;
-}
-
-#moz-reader-content.line-height3 {
- line-height: 1.4em;
-}
-
-#moz-reader-content.line-height4 {
- line-height: 1.6em;
-}
-
-#moz-reader-content.line-height5 {
- line-height: 1.8em;
-}
-
-#moz-reader-content.line-height6 {
- line-height: 2.0em;
-}
-
-#moz-reader-content.line-height7 {
- line-height: 2.2em;
-}
-
-#moz-reader-content.line-height8 {
- line-height: 2.4em;
-}
-
-#moz-reader-content.line-height9 {
- line-height: 2.6em;
-}
-
-@media print {
- p,
- code,
- pre,
- blockquote,
- ul,
- ol,
- li,
- figure,
- .wp-caption {
- margin: 0 0 10px 0 !important;
- }
-}
-
-h1,
-h2,
-h3 {
- font-weight: bold;
-}
-
-h1 {
- font-size: 1.6em;
- line-height: 1.25em;
-}
-
-h2 {
- font-size: 1.2em;
- line-height: 1.51em;
-}
-
-h3 {
- font-size: 1em;
- line-height: 1.66em;
-}
-
-a:link {
- text-decoration: underline;
- font-weight: normal;
-}
-
-a:link,
-a:link:hover,
-a:link:active {
- color: #0095dd;
-}
-
-a:visited {
- color: #c2e;
-}
-
-* {
- max-width: 100%;
- height: auto;
-}
-
-p,
-code,
-pre,
-blockquote,
-ul,
-ol,
-li,
-figure,
-.wp-caption {
- margin: -10px -10px 20px -10px;
- padding: 10px;
- border-radius: 5px;
-}
-
-li {
- margin-bottom: 0;
-}
-
-li > ul,
-li > ol {
- margin-bottom: -10px;
-}
-
-p > img:only-child,
-p > a:only-child > img:only-child,
-.wp-caption img,
-figure img {
- display: block;
-}
-
-img[moz-reader-center] {
- margin-left: auto;
- margin-right: auto;
-}
-
-.caption,
-.wp-caption-text,
-figcaption {
- font-size: 0.9em;
- line-height: 1.48em;
- font-style: italic;
-}
-
-code,
-pre {
- white-space: pre-wrap;
-}
-
-blockquote {
- padding: 0;
- padding-inline-start: 16px;
-}
-
-ul,
-ol {
- padding: 0;
-}
-
-ul {
- padding-inline-start: 30px;
- list-style: disc;
-}
-
-ol {
- padding-inline-start: 30px;
- list-style: decimal;
-}
-
-/* Hide elements with common "hidden" class names */
-.visually-hidden,
-.visuallyhidden,
-.hidden,
-.invisible,
-.sr-only {
- display: none;
-}
diff --git a/toolkit/themes/shared/aboutReaderControls.css b/toolkit/themes/shared/aboutReaderControls.css
deleted file mode 100644
index afd65f458..000000000
--- a/toolkit/themes/shared/aboutReaderControls.css
+++ /dev/null
@@ -1,388 +0,0 @@
-/* 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/. */
-
-.light-button {
- color: #333333;
- background-color: #ffffff;
-}
-
-.dark-button {
- color: #eeeeee;
- background-color: #333333;
-}
-
-.sepia-button {
- color: #5b4636;
- background-color: #f4ecd8;
-}
-
-.sans-serif-button {
- font-family: Helvetica, Arial, sans-serif;
-}
-
-.serif-button {
- font-family: Georgia, "Times New Roman", serif;
-}
-
-/* Loading/error message */
-
-#reader-message {
- margin-top: 40px;
- display: none;
- text-align: center;
- width: 100%;
- font-size: 0.9em;
-}
-
-/* Header */
-
-.header {
- text-align: start;
- display: none;
-}
-
-.domain {
- font-size: 0.9em;
- line-height: 1.48em;
- padding-bottom: 4px;
- font-family: Helvetica, Arial, sans-serif;
- text-decoration: none;
- border-bottom: 1px solid;
- color: #0095dd;
-}
-
-.header > h1 {
- font-size: 1.6em;
- line-height: 1.25em;
- width: 100%;
- margin: 30px 0;
- padding: 0;
-}
-
-.header > .credits {
- font-size: 0.9em;
- line-height: 1.48em;
- margin: 0 0 30px 0;
- padding: 0;
- font-style: italic;
-}
-
-/*======= Controls toolbar =======*/
-
-.toolbar {
- font-family: Helvetica, Arial, sans-serif;
- position: fixed;
- height: 100%;
- top: 0;
- left: 0;
- margin: 0;
- padding: 0;
- list-style: none;
- background-color: #fbfbfb;
- -moz-user-select: none;
- border-right: 1px solid #b5b5b5;
- z-index: 1;
-}
-
-.button {
- display: block;
- background-size: 24px 24px;
- background-repeat: no-repeat;
- color: #333;
- background-color: #fbfbfb;
- height: 40px;
- padding: 0;
-}
-
-.toolbar .button {
- width: 40px;
- background-position: center;
- margin-right: -1px;
- border-top: 0;
- border-left: 0;
- border-right: 1px solid #b5b5b5;
- border-bottom: 1px solid #c1c1c1;
-}
-
-.button[hidden] {
- display: none;
-}
-
-.dropdown {
- text-align: center;
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-.dropdown li {
- margin: 0;
- padding: 0;
-}
-
-/*======= Popup =======*/
-
-.dropdown-popup {
- min-width: 300px;
- text-align: start;
- position: absolute;
- left: 48px; /* offset to account for toolbar width */
- z-index: 1000;
- background-color: #fbfbfb;
- visibility: hidden;
- border-radius: 4px;
- border: 1px solid #b5b5b5;
- border-bottom-width: 0;
- box-shadow: 0 1px 12px #666;
-}
-
-.keep-open .dropdown-popup {
- z-index: initial;
-}
-
-.dropdown-popup > hr {
- display: none;
-}
-
-.open > .dropdown-popup {
- visibility: visible;
-}
-
-.dropdown-arrow {
- position: absolute;
- top: 30px; /* offset arrow from top of popup */
- left: -16px;
- width: 24px;
- height: 24px;
- background-image: url("chrome://global/skin/reader/RM-Type-Controls-Arrow.svg");
- display: block;
-}
-
-/*======= Font style popup =======*/
-
-#font-type-buttons,
-#font-size-buttons,
-#color-scheme-buttons,
-#content-width-buttons,
-#line-height-buttons {
- display: flex;
- flex-direction: row;
-}
-
-#font-type-buttons > button:first-child {
- border-top-left-radius: 3px;
-}
-#font-type-buttons > button:last-child {
- border-top-right-radius: 3px;
-}
-#color-scheme-buttons > button:first-child {
- border-bottom-left-radius: 3px;
-}
-#color-scheme-buttons > button:last-child {
- border-bottom-right-radius: 3px;
-}
-
-#font-type-buttons > button,
-#font-size-buttons > button,
-#color-scheme-buttons > button,
-#content-width-buttons > button,
-#line-height-buttons > button {
- text-align: center;
- border: 0;
-}
-
-#font-type-buttons > button,
-#font-size-buttons > button,
-#content-width-buttons > button,
-#line-height-buttons > button {
- width: 50%;
- background-color: transparent;
- border-left: 1px solid #B5B5B5;
- border-bottom: 1px solid #B5B5B5;
-}
-
-#color-scheme-buttons > button {
- width: 33.33%;
- font-size: 14px;
-}
-
-#color-scheme-buttons > .dark-button {
- margin-top: -1px;
- height: 61px;
-}
-
-#font-type-buttons > button:first-child,
-#font-size-buttons > button:first-child,
-#content-width-buttons > button:first-child,
-#line-height-buttons > button:first-child {
- border-left: 0;
-}
-
-#font-type-buttons > button {
- display: inline-block;
- font-size: 62px;
- height: 100px;
-}
-
-#font-size-buttons > button,
-#color-scheme-buttons > button,
-#content-width-buttons > button,
-#line-height-buttons > button {
- height: 60px;
-}
-
-#font-type-buttons > button:active:hover,
-#font-type-buttons > button.selected,
-#color-scheme-buttons > button:active:hover,
-#color-scheme-buttons > button.selected {
- box-shadow: inset 0 -3px 0 0 #fc6420;
-}
-
-#font-type-buttons > button:active:hover,
-#font-type-buttons > button.selected {
- border-bottom: 1px solid #FC6420;
-}
-
-/* Make the serif button content the same size as the sans-serif button content. */
-#font-type-buttons > button > .description {
- color: #666;
- font-size: 12px;
- margin-top: -5px;
-}
-
-/* Font sizes are different per-platform, so we need custom CSS to line them up. */
-%ifdef XP_MACOSX
-#font-type-buttons > .sans-serif-button > .name {
- margin-top: 10px;
-}
-
-#font-type-buttons > .sans-serif-button > .description {
- margin-top: -4px;
-}
-
-#font-type-buttons > .serif-button > .name {
- font-size: 63px;
-}
-%elifdef XP_WIN
-#font-type-buttons > .sans-serif-button > .name {
- margin-top: 2px;
-}
-
-#font-type-buttons > .sans-serif-button > .description {
- margin-top: -4px;
-}
-
-#font-type-buttons > .serif-button > .name {
- font-size: 63px;
-}
-%else
-#font-type-buttons > .sans-serif-button > .name {
- margin-top: 5px;
-}
-
-#font-type-buttons > .sans-serif-button > .description {
- margin-top: -8px;
-}
-
-#font-type-buttons > .serif-button > .name {
- font-size: 70px;
-}
-%endif
-
-.button:hover,
-#font-size-buttons > button:hover,
-#font-type-buttons > button:hover,
-#content-width-buttons > button:hover,
-#line-height-buttons > button:hover {
- background-color: #ebebeb;
-}
-
-.dropdown.open,
-.button:active,
-#font-size-buttons > button:active,
-#font-size-buttons > button.selected,
-#content-width-buttons > button:active,
-#content-width-buttons > button.selected,
-#line-height-buttons > button:active,
-#line-height-buttons > button.selected {
- background-color: #dadada;
-}
-
-/* Only used on Android */
-#font-size-sample {
- display: none;
-}
-
-.minus-button,
-.plus-button,
-.content-width-minus-button,
-.content-width-plus-button,
-.line-height-minus-button,
-.line-height-plus-button {
- background-color: transparent;
- border: 0;
- background-size: 18px 18px;
- background-repeat: no-repeat;
- background-position: center;
-}
-
-/*======= Toolbar icons =======*/
-
-.close-button {
- background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close");
- height: 68px;
- background-position: center 8px;
-}
-
-.close-button:hover {
- background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
- background-color: #d94141;
- border-bottom: 1px solid #d94141;
- border-right: 1px solid #d94141;
-}
-
-.close-button:hover:active {
- background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
- background-color: #AE2325;
- border-bottom: 1px solid #AE2325;
- border-right: 1px solid #AE2325;
-}
-
-.style-button {
- background-image: url("chrome://global/skin/reader/RM-Type-Controls-24x24.svg");
-}
-
-.minus-button {
- background-image: url("chrome://global/skin/reader/RM-Minus-24x24.svg");
-}
-
-.plus-button {
- background-image: url("chrome://global/skin/reader/RM-Plus-24x24.svg");
-}
-
-.content-width-minus-button {
- background-size: 42px 16px;
- background-image: url("chrome://global/skin/reader/RM-Content-Width-Minus-42x16.svg");
-}
-
-.content-width-plus-button {
- background-size: 44px 16px;
- background-image: url("chrome://global/skin/reader/RM-Content-Width-Plus-44x16.svg");
-}
-
-.line-height-minus-button {
- background-size: 34px 14px;
- background-image: url("chrome://global/skin/reader/RM-Line-Height-Minus-38x14.svg");
-}
-
-.line-height-plus-button {
- background-size: 34px 24px;
- background-image: url("chrome://global/skin/reader/RM-Line-Height-Plus-38x24.svg");
-}
-
-@media print {
- .toolbar {
- display: none !important;
- }
-}
diff --git a/toolkit/themes/shared/jar.inc.mn b/toolkit/themes/shared/jar.inc.mn
index bdfca2a05..675353409 100644
--- a/toolkit/themes/shared/jar.inc.mn
+++ b/toolkit/themes/shared/jar.inc.mn
@@ -15,9 +15,7 @@ toolkit.jar:
skin/classic/global/aboutCache.css (../../shared/aboutCache.css)
skin/classic/global/aboutCacheEntry.css (../../shared/aboutCacheEntry.css)
skin/classic/global/aboutMemory.css (../../shared/aboutMemory.css)
- skin/classic/global/aboutReader.css (../../shared/aboutReader.css)
- skin/classic/global/aboutReaderContent.css (../../shared/aboutReaderContent.css)
-* skin/classic/global/aboutReaderControls.css (../../shared/aboutReaderControls.css)
+* skin/classic/global/aboutReader.css (../../shared/aboutReader.css)
skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css)
skin/classic/global/appPicker.css (../../shared/appPicker.css)
skin/classic/global/config.css (../../shared/config.css)
@@ -34,7 +32,6 @@ toolkit.jar:
skin/classic/global/icons/blocked.svg (../../shared/incontent-icons/blocked.svg)
skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css)
skin/classic/global/narrate.css (../../shared/narrate.css)
- skin/classic/global/narrateControls.css (../../shared/narrateControls.css)
skin/classic/global/narrate/arrow.svg (../../shared/narrate/arrow.svg)
skin/classic/global/narrate/back.svg (../../shared/narrate/back.svg)
skin/classic/global/narrate/fast.svg (../../shared/narrate/fast.svg)
diff --git a/toolkit/themes/shared/narrate.css b/toolkit/themes/shared/narrate.css
index 8a44e78e2..836b5d686 100644
--- a/toolkit/themes/shared/narrate.css
+++ b/toolkit/themes/shared/narrate.css
@@ -1,3 +1,6 @@
+/* Avoid adding ID selector rules in this style sheet, since they could
+ * inadvertently match elements in the article content. */
+
.narrating {
position: relative;
z-index: 1;
@@ -22,7 +25,7 @@ body.dark .narrating {
z-index: -1;
border-bottom-style: solid;
border-bottom-width: 7px;
- transition: left 0.1s ease;
+ transition: left 0.1s ease, width 0.1s ease;
}
.narrating > .narrate-word-highlight {
@@ -44,3 +47,193 @@ body.sepia .narrate-word-highlight {
body.dark .narrate-word-highlight {
border-bottom-color: #6f6f6f;
}
+
+.narrate-dropdown {
+ --border-color: #e5e5e5;
+}
+
+.narrate-toggle > svg {
+ display: block;
+ margin: 0 8px;
+}
+
+.narrate-dropdown > .dropdown-popup button {
+ background-color: transparent;
+}
+
+.narrate-dropdown > .dropdown-popup button:hover:not(:disabled) {
+ background-color: #eaeaea;
+}
+
+.narrate-row {
+ display: flex;
+ align-items: center;
+ min-height: 40px;
+ box-sizing: border-box;
+}
+
+.narrate-row:not(:first-child) {
+ border-top: 1px solid var(--border-color);
+}
+
+/* Control buttons */
+
+.narrate-control > button {
+ background-size: 24px 24px;
+ background-repeat: no-repeat;
+ background-position: center center;
+ height: 64px;
+ width: 100px;
+ border: none;
+ color: #666;
+ box-sizing: border-box;
+}
+
+.narrate-control > button:not(:first-child) {
+ border-left: 1px solid var(--border-color);
+}
+
+.narrate-skip-previous {
+ border-top-left-radius: 3px;
+ background-image: url("chrome://global/skin/narrate/back.svg#enabled");
+ -moz-context-properties: fill;
+ fill: rgb(128 128 128);
+}
+
+.narrate-skip-next {
+ border-top-right-radius: 3px;
+ background-image: url("chrome://global/skin/narrate/forward.svg#enabled");
+ -moz-context-properties: fill;
+ fill: rgb(128 128 128);
+}
+
+.narrate-skip-previous:disabled {
+ background-image: url("chrome://global/skin/narrate/back.svg#disabled");
+}
+
+.narrate-skip-next:disabled {
+ background-image: url("chrome://global/skin/narrate/forward.svg#disabled");
+}
+
+.narrate-start-stop {
+ background-image: url("chrome://global/skin/narrate/start.svg");
+}
+
+.narrate-dropdown.speaking .narrate-start-stop {
+ background-image: url("chrome://global/skin/narrate/stop.svg");
+}
+
+/* Rate control */
+
+.narrate-rate::before, .narrate-rate::after {
+ content: '';
+ width: 48px;
+ height: 40px;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 24px auto;
+}
+
+.narrate-rate::before {
+ background-image: url("chrome://global/skin/narrate/slow.svg");
+}
+
+.narrate-rate::after {
+ background-image: url("chrome://global/skin/narrate/fast.svg");
+}
+
+.narrate-rate-input {
+ margin: 0 1px;
+ flex-grow: 1;
+}
+
+.narrate-rate-input::-moz-range-track {
+ background-color: #979797;
+ height: 2px;
+}
+
+.narrate-rate-input::-moz-range-progress {
+ background-color: #2EA3FF;
+ height: 2px;
+}
+
+.narrate-rate-input::-moz-range-thumb {
+ background-color: #808080;
+ height: 16px;
+ width: 16px;
+ border-radius: 8px;
+ border-width: 0;
+}
+
+.narrate-rate-input:active::-moz-range-thumb {
+ background-color: #2EA3FF;
+}
+
+/* Voice selection */
+
+.voiceselect {
+ width: 100%;
+}
+
+.voiceselect > button.select-toggle,
+.voiceselect > .options > button.option {
+ -moz-appearance: none;
+ border: none;
+ width: 100%;
+ min-height: 40px;
+}
+
+.voiceselect.open > button.select-toggle {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.voiceselect > button.select-toggle::after {
+ content: '';
+ background-image: url("chrome://global/skin/narrate/arrow.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 12px 12px;
+ display: inline-block;
+ width: 1.5em;
+ height: 1em;
+ vertical-align: middle;
+}
+
+.voiceselect > .options > button.option:not(:first-child) {
+ border-top: 1px solid var(--border-color);
+}
+
+.voiceselect > .options > button.option {
+ box-sizing: border-box;
+}
+
+.voiceselect > .options:not(.hovering) > button.option:focus {
+ background-color: #eaeaea;
+}
+
+.voiceselect > .options:not(.hovering) > button.option:hover:not(:focus) {
+ background-color: transparent;
+}
+
+.voiceselect > .options > button.option::-moz-focus-inner {
+ outline: none;
+ border: 0;
+}
+
+.voiceselect > .options {
+ display: none;
+ overflow-y: auto;
+}
+
+.voiceselect.open > .options {
+ display: block;
+}
+
+.current-voice {
+ color: #7f7f7f;
+}
+
+.voiceselect:not(.open) > button,
+.voiceselect .option:last-child {
+ border-radius: 0 0 3px 3px;
+}
diff --git a/toolkit/themes/shared/narrateControls.css b/toolkit/themes/shared/narrateControls.css
deleted file mode 100644
index 91d02d85a..000000000
--- a/toolkit/themes/shared/narrateControls.css
+++ /dev/null
@@ -1,185 +0,0 @@
-:scope {
- --border-color: #e5e5e5;
-}
-
-#narrate-toggle > svg {
- display: block;
- margin: 0 8px;
-}
-
-.dropdown-popup button {
- background-color: transparent;
-}
-
-.dropdown-popup button:hover:not(:disabled) {
- background-color: #eaeaea;
-}
-
-.narrate-row {
- display: flex;
- align-items: center;
- min-height: 40px;
- box-sizing: border-box;
-}
-
-.narrate-row:not(:first-child) {
- border-top: 1px solid var(--border-color);
-}
-
-/* Control buttons */
-
-#narrate-control > button {
- background-size: 24px 24px;
- background-repeat: no-repeat;
- background-position: center center;
- height: 64px;
- width: 100px;
- border: none;
- color: #666;
- box-sizing: border-box;
-}
-
-#narrate-control > button:not(:first-child) {
- border-left: 1px solid var(--border-color);
-}
-
-#narrate-skip-previous {
- border-top-left-radius: 3px;
- background-image: url("chrome://global/skin/narrate/back.svg#enabled");
-}
-
-#narrate-skip-next {
- border-top-right-radius: 3px;
- background-image: url("chrome://global/skin/narrate/forward.svg#enabled");
-}
-
-#narrate-skip-previous:disabled {
- background-image: url("chrome://global/skin/narrate/back.svg#disabled");
-}
-
-#narrate-skip-next:disabled {
- background-image: url("chrome://global/skin/narrate/forward.svg#disabled");
-}
-
-#narrate-start-stop {
- background-image: url("chrome://global/skin/narrate/start.svg");
-}
-
-#narrate-dropdown.speaking #narrate-start-stop {
- background-image: url("chrome://global/skin/narrate/stop.svg");
-}
-
-/* Rate control */
-
-#narrate-rate::before, #narrate-rate::after {
- content: '';
- width: 48px;
- height: 40px;
- background-position: center;
- background-repeat: no-repeat;
- background-size: 24px auto;
-}
-
-#narrate-rate::before {
- background-image: url("chrome://global/skin/narrate/slow.svg");
-}
-
-#narrate-rate::after {
- background-image: url("chrome://global/skin/narrate/fast.svg");
-}
-
-#narrate-rate-input {
- margin: 0 1px;
- flex-grow: 1;
-}
-
-#narrate-rate-input::-moz-range-track {
- background-color: #979797;
- height: 2px;
-}
-
-#narrate-rate-input::-moz-range-progress {
- background-color: #2EA3FF;
- height: 2px;
-}
-
-#narrate-rate-input::-moz-range-thumb {
- background-color: #808080;
- height: 16px;
- width: 16px;
- border-radius: 8px;
- border-width: 0;
-}
-
-#narrate-rate-input:active::-moz-range-thumb {
- background-color: #2EA3FF;
-}
-
-/* Voice selection */
-
-.voiceselect {
- width: 100%;
-}
-
-.voiceselect > button.select-toggle,
-.voiceselect > .options > button.option {
- -moz-appearance: none;
- border: none;
- width: 100%;
- min-height: 40px;
-}
-
-.voiceselect.open > button.select-toggle {
- border-bottom: 1px solid var(--border-color);
-}
-
-.voiceselect > button.select-toggle::after {
- content: '';
- background-image: url("chrome://global/skin/narrate/arrow.svg");
- background-position: center;
- background-repeat: no-repeat;
- background-size: 12px 12px;
- display: inline-block;
- width: 1.5em;
- height: 1em;
- vertical-align: middle;
-}
-
-.voiceselect > .options > button.option:not(:first-child) {
- border-top: 1px solid var(--border-color);
-}
-
-.voiceselect > .options > button.option {
- box-sizing: border-box;
-}
-
-.voiceselect > .options:not(.hovering) > button.option:focus {
- background-color: #eaeaea;
-}
-
-.voiceselect > .options:not(.hovering) > button.option:hover:not(:focus) {
- background-color: transparent;
-}
-
-.voiceselect > .options > button.option::-moz-focus-inner {
- outline: none;
- border: 0;
-}
-
-.voiceselect > .options {
- display: none;
- overflow-y: auto;
-}
-
-.voiceselect.open > .options {
- display: block;
-}
-
-.current-voice {
- color: #7f7f7f;
-}
-
-.voiceselect:not(.open) > button,
-.option:last-child {
- border-radius: 0 0 3px 3px;
-}
diff --git a/toolkit/themes/shared/reader/RM-Content-Width-Minus-42x16.svg b/toolkit/themes/shared/reader/RM-Content-Width-Minus-42x16.svg
index 48bbb9e89..482a392fb 100644
--- a/toolkit/themes/shared/reader/RM-Content-Width-Minus-42x16.svg
+++ b/toolkit/themes/shared/reader/RM-Content-Width-Minus-42x16.svg
@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- 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/. -->
-<svg version="1.1"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
+<svg xmlns="http://www.w3.org/2000/svg"
width="42"
height="16"
viewBox="0 0 42 16"
diff --git a/toolkit/themes/shared/reader/RM-Content-Width-Plus-44x16.svg b/toolkit/themes/shared/reader/RM-Content-Width-Plus-44x16.svg
index bcdcbe8ad..da02e608f 100644
--- a/toolkit/themes/shared/reader/RM-Content-Width-Plus-44x16.svg
+++ b/toolkit/themes/shared/reader/RM-Content-Width-Plus-44x16.svg
@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- 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/. -->
-<svg version="1.1"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
+<svg xmlns="http://www.w3.org/2000/svg"
width="44"
height="16"
viewBox="0 0 44 16"
diff --git a/toolkit/themes/shared/reader/RM-Line-Height-Minus-38x14.svg b/toolkit/themes/shared/reader/RM-Line-Height-Minus-38x14.svg
index 05d52828a..59517c246 100644
--- a/toolkit/themes/shared/reader/RM-Line-Height-Minus-38x14.svg
+++ b/toolkit/themes/shared/reader/RM-Line-Height-Minus-38x14.svg
@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- 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/. -->
-<svg version="1.1"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
+<svg xmlns="http://www.w3.org/2000/svg"
width="38"
height="14"
viewBox="0 0 38 14"
diff --git a/toolkit/themes/shared/reader/RM-Line-Height-Plus-38x24.svg b/toolkit/themes/shared/reader/RM-Line-Height-Plus-38x24.svg
index 0a5ee0dcc..ba50c5a52 100644
--- a/toolkit/themes/shared/reader/RM-Line-Height-Plus-38x24.svg
+++ b/toolkit/themes/shared/reader/RM-Line-Height-Plus-38x24.svg
@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- 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/. -->
-<svg version="1.1"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
+<svg xmlns="http://www.w3.org/2000/svg"
width="38"
height="24"
viewBox="0 0 38 24"
diff --git a/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg b/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg
index 837392ff3..9c663119f 100644
--- a/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg
+++ b/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg
@@ -1,8 +1,8 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
- <polygon opacity="0.15" points="16.583,0.015 16.569,0 4.583,12 16.569,24 16.583,23.985"/>
- <polygon fill="#fbfbfb" points="16.575,1.021 16.561,1.008 5.583,12 16.577,23.008 16.591,22.994 "/>
-</svg>
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24">
+ <polygon points="16.58 0.01 16.57 0 4.58 12 16.57 24 16.58 23.98 16.58 0.01" fill="#b5b5b5"/>
+ <polyline points="16.63 1.51 6.08 12.01 16.63 22.5" fill="#fbfbfb"/>
+</svg>