Initial updates for Reader View.

This commit is contained in:
Ascrod 2018-05-07 19:45:46 -04:00 committed by Roy Tam
commit 1c494e80a6
27 changed files with 1757 additions and 1786 deletions

View file

@ -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"/>

View file

@ -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,35 +128,12 @@ 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.
*
@ -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;
});
})
}
};

View file

@ -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

View file

@ -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() {

View file

@ -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(

View file

@ -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);
},

View file

@ -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. */

View file

@ -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);
// 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");
// 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";
}
},
_showError: function() {
_formatReadTime(slowEstimate, fastEstimate) {
let displayStringKey = "aboutReader.estimatedReadTimeRange1";
// 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() {
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)";
@ -986,6 +1024,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
*/

View file

@ -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));

File diff suppressed because it is too large Load diff

View file

@ -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");
},
};

View file

@ -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();
},
};

View file

@ -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>

View file

@ -4,6 +4,6 @@
"use strict";
window.addEventListener("DOMContentLoaded", function () {
window.addEventListener("DOMContentLoaded", function() {
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
});

View file

@ -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");

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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)

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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"

Before

Width:  |  Height:  |  Size: 905 B

After

Width:  |  Height:  |  Size: 739 B

Before After
Before After

View file

@ -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"

Before

Width:  |  Height:  |  Size: 904 B

After

Width:  |  Height:  |  Size: 738 B

Before After
Before After

View file

@ -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"

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 514 B

Before After
Before After

View file

@ -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"

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 515 B

Before After
Before After

View file

@ -2,7 +2,7 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<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 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>

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 486 B

Before After
Before After