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 id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/>
<key keycode="VK_F11" command="View:FullScreen"/> <key keycode="VK_F11" command="View:FullScreen"/>
#endif #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:Reload" modifiers="accel" id="key_reload"/>
<key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/> <key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
<key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/> <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/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.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, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.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"); const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
var ReaderParent = { var ReaderParent = {
_readerModeInfoPanelOpen: false,
MESSAGES: [ MESSAGES: [
"Reader:ArticleGet", "Reader:ArticleGet",
"Reader:FaviconRequest", "Reader:FaviconRequest",
"Reader:UpdateReaderButton", "Reader:UpdateReaderButton",
], ],
init: function() { init() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
for (let msg of this.MESSAGES) { for (let msg of this.MESSAGES) {
mm.addMessageListener(msg, this); mm.addMessageListener(msg, this);
} }
}, },
receiveMessage: function(message) { receiveMessage(message) {
switch (message.name) { switch (message.name) {
case "Reader:ArticleGet": case "Reader:ArticleGet":
this._getArticle(message.data.url, message.target).then((article) => { this._getArticle(message.data.url, message.target).then((article) => {
@ -60,7 +56,7 @@ var ReaderParent = {
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", { message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
url: message.data.url, url: message.data.url,
faviconUrl: favicon.path.replace(/^favicon:/, "") faviconUrl: favicon.path.replace(/^favicon:/, "")
}) });
}, },
function onRejection(reason) { function onRejection(reason) {
Cu.reportError("Error requesting favicon URL for about:reader content: " + 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; let win = browser.ownerGlobal;
if (browser != win.gBrowser.selectedBrowser) { if (browser != win.gBrowser.selectedBrowser) {
return; return;
@ -88,7 +84,11 @@ var ReaderParent = {
let button = win.document.getElementById("reader-mode-button"); let button = win.document.getElementById("reader-mode-button");
let command = win.document.getElementById("View:ReaderView"); 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")) { if (browser.currentURI.spec.startsWith("about:reader")) {
button.setAttribute("readeractive", true); button.setAttribute("readeractive", true);
button.hidden = false; button.hidden = false;
@ -98,6 +98,7 @@ var ReaderParent = {
command.setAttribute("hidden", false); command.setAttribute("hidden", false);
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey")); command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey"));
key.setAttribute("disabled", false); key.setAttribute("disabled", false);
browser.setAttribute("aria-reader", "active");
} else { } else {
button.removeAttribute("readeractive"); button.removeAttribute("readeractive");
button.hidden = !browser.isArticle; button.hidden = !browser.isArticle;
@ -107,24 +108,15 @@ var ReaderParent = {
command.setAttribute("hidden", !browser.isArticle); command.setAttribute("hidden", !browser.isArticle);
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey")); command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
key.setAttribute("disabled", !browser.isArticle); key.setAttribute("disabled", !browser.isArticle);
} if (browser.isArticle) {
browser.setAttribute("aria-reader", "available");
let currentUriHost = browser.currentURI && browser.currentURI.asciiHost; } else {
if (browser.isArticle && browser.removeAttribute("aria-reader");
!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);
} }
this._readerModeInfoPanelOpen = false;
} }
}, },
forceShowReaderIcon: function(browser) { forceShowReaderIcon(browser) {
browser.isArticle = true; browser.isArticle = true;
this.updateReaderButton(browser); this.updateReaderButton(browser);
}, },
@ -136,35 +128,12 @@ var ReaderParent = {
this.toggleReaderMode(event); this.toggleReaderMode(event);
}, },
toggleReaderMode: function(event) { toggleReaderMode(event) {
let win = event.target.ownerGlobal; let win = event.target.ownerGlobal;
let browser = win.gBrowser.selectedBrowser; let browser = win.gBrowser.selectedBrowser;
browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode"); 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. * Gets an article for a given URL. This method will download and parse a document.
* *
@ -173,8 +142,8 @@ var ReaderParent = {
* @return {Promise} * @return {Promise}
* @resolves JS object representing the article, or null if no article is found. * @resolves JS object representing the article, or null if no article is found.
*/ */
_getArticle: Task.async(function* (url, browser) { async _getArticle(url, browser) {
return yield ReaderMode.downloadAndParseDocument(url).catch(e => { return await ReaderMode.downloadAndParseDocument(url).catch(e => {
if (e && e.newURL) { if (e && e.newURL) {
// Pass up the error so we can navigate the browser in question to the new URL: // Pass up the error so we can navigate the browser in question to the new URL:
throw e; throw e;
@ -182,5 +151,5 @@ var ReaderParent = {
Cu.reportError("Error downloading and parsing document: " + e); Cu.reportError("Error downloading and parsing document: " + e);
return null; return null;
}); });
}) }
}; };

View file

@ -18,8 +18,6 @@ chrome.jar:
skin/aboutMemory.css (aboutMemory.css) skin/aboutMemory.css (aboutMemory.css)
skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css) skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css)
skin/aboutReader.css (aboutReader.css) skin/aboutReader.css (aboutReader.css)
skin/aboutReaderContent.css (aboutReaderContent.css)
skin/aboutReaderControls.css (aboutReaderControls.css)
skin/aboutSupport.css (aboutSupport.css) skin/aboutSupport.css (aboutSupport.css)
skin/content.css (content.css) skin/content.css (content.css)
skin/scrollbar.css (scrollbar-apz.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/about.css chrome://browser/skin/about.css
% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.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/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/aboutSupport.css chrome://browser/skin/aboutSupport.css
% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css % override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
% override chrome://global/skin/netError.css chrome://browser/skin/netError.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"); var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
function NarrateControls(mm, win) { function NarrateControls(mm, win, languagePromise) {
this._mm = mm; this._mm = mm;
this._winRef = Cu.getWeakReference(win); this._winRef = Cu.getWeakReference(win);
this._languagePromise = languagePromise;
win.addEventListener("unload", this); win.addEventListener("unload", this);
@ -37,16 +38,12 @@ function NarrateControls(mm, win) {
} }
let dropdown = win.document.createElement("ul"); let dropdown = win.document.createElement("ul");
dropdown.className = "dropdown"; dropdown.className = "dropdown narrate-dropdown";
dropdown.id = "narrate-dropdown";
// We need inline svg here for the animation to work (bug 908634 & 1190881). // 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 = dropdown.innerHTML =
localize`<style scoped> localize`<li>
@import url("chrome://global/skin/narrateControls.css"); <button class="dropdown-toggle button narrate-toggle"
</style>
<li>
<button class="dropdown-toggle button" id="narrate-toggle"
title="${"narrate"}" hidden> title="${"narrate"}" hidden>
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
@ -60,11 +57,11 @@ function NarrateControls(mm, win) {
100% { transform: scaleY(1); } 100% { transform: scaleY(1); }
} }
#waveform > rect { .waveform > rect {
fill: #808080; fill: #808080;
} }
.speaking #waveform > rect { .speaking .waveform > rect {
fill: #58bf43; fill: #58bf43;
transform-box: fill-box; transform-box: fill-box;
transform-origin: 50% 50%; transform-origin: 50% 50%;
@ -74,15 +71,15 @@ function NarrateControls(mm, win) {
animation-timing-function: linear; animation-timing-function: linear;
} }
#waveform > rect:nth-child(2) { animation-delay: 250ms; } .waveform > rect:nth-child(2) { animation-delay: 250ms; }
#waveform > rect:nth-child(3) { animation-delay: 500ms; } .waveform > rect:nth-child(3) { animation-delay: 500ms; }
#waveform > rect:nth-child(4) { animation-delay: 750ms; } .waveform > rect:nth-child(4) { animation-delay: 750ms; }
#waveform > rect:nth-child(5) { animation-delay: 1000ms; } .waveform > rect:nth-child(5) { animation-delay: 1000ms; }
#waveform > rect:nth-child(6) { animation-delay: 1250ms; } .waveform > rect:nth-child(6) { animation-delay: 1250ms; }
#waveform > rect:nth-child(7) { animation-delay: 1500ms; } .waveform > rect:nth-child(7) { animation-delay: 1500ms; }
</style> </style>
<g id="waveform"> <g class="waveform">
<rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" /> <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="4" y="5" width="2" height="14" rx=".5" ry=".5" />
<rect x="7" y="8" width="2" height="8" 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> </button>
</li> </li>
<li class="dropdown-popup"> <li class="dropdown-popup">
<div id="narrate-control" class="narrate-row"> <div class="narrate-row narrate-control">
<button disabled id="narrate-skip-previous" <button disabled class="narrate-skip-previous"
title="${"back"}"></button> title="${"back"}"></button>
<button id="narrate-start-stop" title="${"start"}"></button> <button class="narrate-start-stop" title="${"start"}"></button>
<button disabled id="narrate-skip-next" <button disabled class="narrate-skip-next"
title="${"forward"}"></button> title="${"forward"}"></button>
</div> </div>
<div id="narrate-rate" class="narrate-row"> <div class="narrate-row narrate-rate">
<input id="narrate-rate-input" value="0" title="${"speed"}" <input class="narrate-rate-input" value="0" title="${"speed"}"
step="5" max="100" min="-100" type="range"> step="5" max="100" min="-100" type="range">
</div> </div>
<div id="narrate-voices" class="narrate-row"></div> <div class="narrate-row narrate-voices"></div>
<div class="dropdown-arrow"></div> <div class="dropdown-arrow"></div>
</li>`; </li>`;
this.narrator = new Narrator(win); this.narrator = new Narrator(win, languagePromise);
let branch = Services.prefs.getBranch("narrate."); let branch = Services.prefs.getBranch("narrate.");
let selectLabel = gStrings.GetStringFromName("selectvoicelabel"); let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
this.voiceSelect = new VoiceSelect(win, selectLabel); this.voiceSelect = new VoiceSelect(win, selectLabel);
this.voiceSelect.element.addEventListener("change", this); this.voiceSelect.element.addEventListener("change", this);
this.voiceSelect.element.id = "voice-select"; this.voiceSelect.element.classList.add("voice-select");
win.speechSynthesis.addEventListener("voiceschanged", this); win.speechSynthesis.addEventListener("voiceschanged", this);
dropdown.querySelector("#narrate-voices").appendChild( dropdown.querySelector(".narrate-voices").appendChild(
this.voiceSelect.element); this.voiceSelect.element);
dropdown.addEventListener("click", this, true); dropdown.addEventListener("click", this, true);
let rateRange = dropdown.querySelector("#narrate-rate > input"); let rateRange = dropdown.querySelector(".narrate-rate > input");
rateRange.addEventListener("change", this); rateRange.addEventListener("change", this);
// The rate is stored as an integer. // The rate is stored as an integer.
@ -131,15 +128,15 @@ function NarrateControls(mm, win) {
this._setupVoices(); this._setupVoices();
let tb = win.document.getElementById("reader-toolbar"); let tb = win.document.querySelector(".reader-toolbar");
tb.appendChild(dropdown); tb.appendChild(dropdown);
} }
NarrateControls.prototype = { NarrateControls.prototype = {
handleEvent: function(evt) { handleEvent(evt) {
switch (evt.type) { switch (evt.type) {
case "change": case "change":
if (evt.target.id == "narrate-rate-input") { if (evt.target.classList.contains("narrate-rate-input")) {
this._onRateInput(evt); this._onRateInput(evt);
} else { } else {
this._onVoiceChange(); this._onVoiceChange();
@ -162,8 +159,8 @@ NarrateControls.prototype = {
/** /**
* Returns true if synth voices are available. * Returns true if synth voices are available.
*/ */
_setupVoices: function() { _setupVoices() {
return this.narrator.languagePromise.then(language => { return this._languagePromise.then(language => {
this.voiceSelect.clear(); this.voiceSelect.clear();
let win = this._win; let win = this._win;
let voicePrefs = this._getVoicePref(); let voicePrefs = this._getVoicePref();
@ -190,7 +187,7 @@ NarrateControls.prototype = {
this.voiceSelect.addOptions(options); this.voiceSelect.addOptions(options);
} }
let narrateToggle = win.document.getElementById("narrate-toggle"); let narrateToggle = win.document.querySelector(".narrate-toggle");
let histogram = Services.telemetry.getKeyedHistogramById( let histogram = Services.telemetry.getKeyedHistogramById(
"NARRATE_CONTENT_BY_LANGUAGE_2"); "NARRATE_CONTENT_BY_LANGUAGE_2");
let initial = !this._voicesInitialized; let initial = !this._voicesInitialized;
@ -210,7 +207,7 @@ NarrateControls.prototype = {
}); });
}, },
_getVoicePref: function() { _getVoicePref() {
let voicePref = Services.prefs.getCharPref("narrate.voice"); let voicePref = Services.prefs.getCharPref("narrate.voice");
try { try {
return JSON.parse(voicePref); return JSON.parse(voicePref);
@ -219,15 +216,15 @@ NarrateControls.prototype = {
} }
}, },
_onRateInput: function(evt) { _onRateInput(evt) {
AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10)); AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
this.narrator.setRate(this._convertRate(evt.target.value)); this.narrator.setRate(this._convertRate(evt.target.value));
}, },
_onVoiceChange: function() { _onVoiceChange() {
let voice = this.voice; let voice = this.voice;
this.narrator.setVoice(voice); this.narrator.setVoice(voice);
this.narrator.languagePromise.then(language => { this._languagePromise.then(language => {
if (language) { if (language) {
let voicePref = this._getVoicePref(); let voicePref = this._getVoicePref();
voicePref[language || "default"] = voice; voicePref[language || "default"] = voice;
@ -236,42 +233,39 @@ NarrateControls.prototype = {
}); });
}, },
_onButtonClick: function(evt) { _onButtonClick(evt) {
switch (evt.target.id) { let classList = evt.target.classList;
case "narrate-skip-previous": if (classList.contains("narrate-skip-previous")) {
this.narrator.skipPrevious(); this.narrator.skipPrevious();
break; } else if (classList.contains("narrate-skip-next")) {
case "narrate-skip-next": this.narrator.skipNext();
this.narrator.skipNext(); } else if (classList.contains("narrate-start-stop")) {
break; if (this.narrator.speaking) {
case "narrate-start-stop": this.narrator.stop();
if (this.narrator.speaking) { } else {
this.narrator.stop(); this._updateSpeechControls(true);
} else { let options = { rate: this.rate, voice: this.voice };
this._updateSpeechControls(true); this.narrator.start(options).then(() => {
let options = { rate: this.rate, voice: this.voice }; this._updateSpeechControls(false);
this.narrator.start(options).then(() => { }, err => {
this._updateSpeechControls(false); Cu.reportError(`Narrate failed: ${err}.`);
}, err => { this._updateSpeechControls(false);
Cu.reportError(`Narrate failed: ${err}.`); });
this._updateSpeechControls(false); }
});
}
break;
} }
}, },
_updateSpeechControls: function(speaking) { _updateSpeechControls(speaking) {
let dropdown = this._doc.getElementById("narrate-dropdown"); let dropdown = this._doc.querySelector(".narrate-dropdown");
dropdown.classList.toggle("keep-open", speaking); dropdown.classList.toggle("keep-open", speaking);
dropdown.classList.toggle("speaking", speaking); dropdown.classList.toggle("speaking", speaking);
let startStopButton = this._doc.getElementById("narrate-start-stop"); let startStopButton = this._doc.querySelector(".narrate-start-stop");
startStopButton.title = startStopButton.title =
gStrings.GetStringFromName(speaking ? "stop" : "start"); gStrings.GetStringFromName(speaking ? "stop" : "start");
this._doc.getElementById("narrate-skip-previous").disabled = !speaking; this._doc.querySelector(".narrate-skip-previous").disabled = !speaking;
this._doc.getElementById("narrate-skip-next").disabled = !speaking; this._doc.querySelector(".narrate-skip-next").disabled = !speaking;
if (speaking) { if (speaking) {
TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this); 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 // This is a highly imperfect method of making human-readable labels
// for system voices. Because each platform has a different naming scheme // for system voices. Because each platform has a different naming scheme
// for voices, we use a different method for each platform. // for voices, we use a different method for each platform.
@ -303,7 +297,7 @@ NarrateControls.prototype = {
} }
}, },
_getLanguageName: function(lang) { _getLanguageName(lang) {
if (!this._langStrings) { if (!this._langStrings) {
this._langStrings = Services.strings.createBundle( this._langStrings = Services.strings.createBundle(
"chrome://global/locale/languageNames.properties "); "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. // 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, // 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. // 0.5 is half the speed and 2 is twice the speed in fractions.
@ -334,7 +328,7 @@ NarrateControls.prototype = {
get rate() { get rate() {
return this._convertRate( return this._convertRate(
this._doc.getElementById("narrate-rate-input").value); this._doc.querySelector(".narrate-rate-input").value);
}, },
get voice() { get voice() {

View file

@ -8,8 +8,6 @@ const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
"resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm"); "resource://gre/modules/Services.jsm");
@ -24,29 +22,13 @@ const kTextStylesRules = ["font-family", "font-kerning", "font-size",
"line-height", "letter-spacing", "text-orientation", "line-height", "letter-spacing", "text-orientation",
"text-transform", "word-spacing"]; "text-transform", "word-spacing"];
function Narrator(win) { function Narrator(win, languagePromise) {
this._winRef = Cu.getWeakReference(win); this._winRef = Cu.getWeakReference(win);
this._languagePromise = languagePromise;
this._inTest = Services.prefs.getBoolPref("narrate.test"); this._inTest = Services.prefs.getBoolPref("narrate.test");
this._speechOptions = {}; this._speechOptions = {};
this._startTime = 0; this._startTime = 0;
this._stopped = false; 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 = { Narrator.prototype = {
@ -71,7 +53,7 @@ Narrator.prototype = {
// For example, paragraphs. But nested anchors and other elements // For example, paragraphs. But nested anchors and other elements
// are not interesting since their text already appears in their // are not interesting since their text already appears in their
// parent's textContent. // parent's textContent.
acceptNode: function(node) { acceptNode(node) {
if (this._matches.has(node.parentNode)) { if (this._matches.has(node.parentNode)) {
// Reject sub-trees of accepted nodes. // Reject sub-trees of accepted nodes.
return nf.FILTER_REJECT; return nf.FILTER_REJECT;
@ -107,7 +89,7 @@ Narrator.prototype = {
// are no other strong references, and it will be GC'ed. Instead, // 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. // we rely on the window's lifetime and use it as a weak reference.
this._treeWalkerRef.set(this._win, this._treeWalkerRef.set(this._win,
this._doc.createTreeWalker(this._doc.getElementById("container"), this._doc.createTreeWalker(this._doc.querySelector(".container"),
nf.SHOW_ELEMENT, filter, false)); nf.SHOW_ELEMENT, filter, false));
} }
@ -124,7 +106,7 @@ Narrator.prototype = {
this._win.speechSynthesis.pending; this._win.speechSynthesis.pending;
}, },
_getVoice: function(voiceURI) { _getVoice(voiceURI) {
if (!this._voiceMap || !this._voiceMap.has(voiceURI)) { if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
this._voiceMap = new Map( this._voiceMap = new Map(
this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v])); this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
@ -133,7 +115,7 @@ Narrator.prototype = {
return this._voiceMap.get(voiceURI); return this._voiceMap.get(voiceURI);
}, },
_isParagraphInView: function(paragraph) { _isParagraphInView(paragraph) {
if (!paragraph) { if (!paragraph) {
return false; return false;
} }
@ -142,13 +124,13 @@ Narrator.prototype = {
return bb.top >= 0 && bb.top < this._win.innerHeight; return bb.top >= 0 && bb.top < this._win.innerHeight;
}, },
_sendTestEvent: function(eventType, detail) { _sendTestEvent(eventType, detail) {
let win = this._win; let win = this._win;
win.dispatchEvent(new win.CustomEvent(eventType, win.dispatchEvent(new win.CustomEvent(eventType,
{ detail: Cu.cloneInto(detail, win.document) })); { detail: Cu.cloneInto(detail, win.document) }));
}, },
_speakInner: function() { _speakInner() {
this._win.speechSynthesis.cancel(); this._win.speechSynthesis.cancel();
let tw = this._treeWalker; let tw = this._treeWalker;
let paragraph = tw.currentNode; let paragraph = tw.currentNode;
@ -238,18 +220,12 @@ Narrator.prototype = {
return; return;
} }
// Match non-whitespace. This isn't perfect, but the most universal if (e.charLength) {
// solution for now. highlighter.highlight(e.charIndex, e.charLength);
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 (this._inTest) { if (this._inTest) {
this._sendTestEvent("wordhighlight", { this._sendTestEvent("wordhighlight", {
start: firstIndex.index, start: e.charIndex,
end: reWordBoundary.lastIndex end: e.charIndex + e.charLength
}); });
} }
} }
@ -259,14 +235,14 @@ Narrator.prototype = {
}); });
}, },
start: function(speechOptions) { start(speechOptions) {
this._speechOptions = { this._speechOptions = {
rate: speechOptions.rate, rate: speechOptions.rate,
voice: this._getVoice(speechOptions.voice) voice: this._getVoice(speechOptions.voice)
}; };
this._stopped = false; this._stopped = false;
return this.languagePromise.then(language => { return this._languagePromise.then(language => {
if (!this._speechOptions.voice) { if (!this._speechOptions.voice) {
this._speechOptions.lang = language; this._speechOptions.lang = language;
} }
@ -288,32 +264,32 @@ Narrator.prototype = {
}); });
}, },
stop: function() { stop() {
this._stopped = true; this._stopped = true;
this._win.speechSynthesis.cancel(); this._win.speechSynthesis.cancel();
}, },
skipNext: function() { skipNext() {
this._win.speechSynthesis.cancel(); this._win.speechSynthesis.cancel();
}, },
skipPrevious: function() { skipPrevious() {
this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1); this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
}, },
setRate: function(rate) { setRate(rate) {
this._speechOptions.rate = rate; this._speechOptions.rate = rate;
/* repeat current paragraph */ /* repeat current paragraph */
this._goBackParagraphs(1); this._goBackParagraphs(1);
}, },
setVoice: function(voice) { setVoice(voice) {
this._speechOptions.voice = this._getVoice(voice); this._speechOptions.voice = this._getVoice(voice);
/* repeat current paragraph */ /* repeat current paragraph */
this._goBackParagraphs(1); this._goBackParagraphs(1);
}, },
_goBackParagraphs: function(count) { _goBackParagraphs(count) {
let tw = this._treeWalker; let tw = this._treeWalker;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
if (!tw.previousNode()) { if (!tw.previousNode()) {
@ -338,13 +314,13 @@ Highlighter.prototype = {
* Highlight the range within offsets relative to the container. * Highlight the range within offsets relative to the container.
* *
* @param {Number} startOffset the start offset * @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 containerRect = this.container.getBoundingClientRect();
let range = this._getRange(startOffset, endOffset); let range = this._getRange(startOffset, startOffset + length);
let rangeRects = range.getClientRects(); let rangeRects = range.getClientRects();
let win = this.container.ownerDocument.defaultView; let win = this.container.ownerGlobal;
let computedStyle = win.getComputedStyle(range.endContainer.parentNode); let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
let nodes = this._getFreshHighlightNodes(rangeRects.length); let nodes = this._getFreshHighlightNodes(rangeRects.length);
@ -386,7 +362,7 @@ Highlighter.prototype = {
/** /**
* Releases reference to container and removes all highlight nodes. * Releases reference to container and removes all highlight nodes.
*/ */
remove: function() { remove() {
for (let node of this._nodes) { for (let node of this._nodes) {
node.remove(); node.remove();
} }
@ -400,7 +376,7 @@ Highlighter.prototype = {
* *
* @param {Number} count number of nodes needed * @param {Number} count number of nodes needed
*/ */
_getFreshHighlightNodes: function(count) { _getFreshHighlightNodes(count) {
let doc = this.container.ownerDocument; let doc = this.container.ownerDocument;
let nodes = Array.from(this._nodes); let nodes = Array.from(this._nodes);
@ -427,7 +403,7 @@ Highlighter.prototype = {
* @param {Number} startOffset the start offset * @param {Number} startOffset the start offset
* @param {Number} endOffset the end offset * @param {Number} endOffset the end offset
*/ */
_getRange: function(startOffset, endOffset) { _getRange(startOffset, endOffset) {
let doc = this.container.ownerDocument; let doc = this.container.ownerDocument;
let i = 0; let i = 0;
let treeWalker = doc.createTreeWalker( let treeWalker = doc.createTreeWalker(

View file

@ -13,6 +13,7 @@ function VoiceSelect(win, label) {
let element = win.document.createElement("div"); let element = win.document.createElement("div");
element.classList.add("voiceselect"); element.classList.add("voiceselect");
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = element.innerHTML =
`<button class="select-toggle" aria-controls="voice-options"> `<button class="select-toggle" aria-controls="voice-options">
<span class="label">${label}</span> <span class="current-voice"></span> <span class="label">${label}</span> <span class="current-voice"></span>
@ -37,7 +38,7 @@ function VoiceSelect(win, label) {
} }
VoiceSelect.prototype = { VoiceSelect.prototype = {
add: function(label, value) { add(label, value) {
let option = this._doc.createElement("button"); let option = this._doc.createElement("button");
option.dataset.value = value; option.dataset.value = value;
option.classList.add("option"); option.classList.add("option");
@ -48,7 +49,7 @@ VoiceSelect.prototype = {
return option; return option;
}, },
addOptions: function(options) { addOptions(options) {
let selected = null; let selected = null;
for (let option of options) { for (let option of options) {
if (option.selected) { if (option.selected) {
@ -61,11 +62,11 @@ VoiceSelect.prototype = {
this._select(selected || this.options[0], true); this._select(selected || this.options[0], true);
}, },
clear: function() { clear() {
this.listbox.innerHTML = ""; this.listbox.innerHTML = "";
}, },
toggleList: function(force, focus = true) { toggleList(force, focus = true) {
if (this.element.classList.toggle("open", force)) { if (this.element.classList.toggle("open", force)) {
if (focus) { if (focus) {
(this.selected || this.options[0]).focus(); (this.selected || this.options[0]).focus();
@ -84,7 +85,7 @@ VoiceSelect.prototype = {
} }
}, },
handleEvent: function(evt) { handleEvent(evt) {
let target = evt.target; let target = evt.target;
switch (evt.type) { switch (evt.type) {
@ -131,7 +132,7 @@ VoiceSelect.prototype = {
} }
}, },
_getPagedOption: function(option, up) { _getPagedOption(option, up) {
let height = elem => elem.getBoundingClientRect().height; let height = elem => elem.getBoundingClientRect().height;
let listboxHeight = height(this.listbox); let listboxHeight = height(this.listbox);
@ -148,7 +149,7 @@ VoiceSelect.prototype = {
return next; return next;
}, },
_keyPressedButton: function(evt) { _keyPressedButton(evt) {
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) { if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
this.toggleList(true); this.toggleList(true);
return; return;
@ -178,7 +179,7 @@ VoiceSelect.prototype = {
} }
}, },
_keyPressedInBox: function(evt) { _keyPressedInBox(evt) {
let toFocus; let toFocus;
let cur = this._doc.activeElement; let cur = this._doc.activeElement;
@ -212,7 +213,7 @@ VoiceSelect.prototype = {
} }
}, },
_select: function(option, suppressEvent = false) { _select(option, suppressEvent = false) {
let oldSelected = this.selected; let oldSelected = this.selected;
if (oldSelected) { if (oldSelected) {
oldSelected.removeAttribute("aria-selected"); oldSelected.removeAttribute("aria-selected");
@ -233,7 +234,7 @@ VoiceSelect.prototype = {
} }
}, },
_updateDropdownHeight: function(now) { _updateDropdownHeight(now) {
let updateInner = () => { let updateInner = () => {
let winHeight = this._win.innerHeight; let winHeight = this._win.innerHeight;
let listbox = this.listbox; 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); 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This file defines specific rules for print preview when using simplify mode. /* 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 * 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 * will not take effect when using the simplify page checkbox. This file defines
* styling for title and author on the header element. */ * 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, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties"); var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
@ -45,45 +44,49 @@ var AboutReader = function(mm, win, articlePromise) {
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
this._article = null; this._article = null;
this._languagePromise = new Promise(resolve => {
this._foundLanguage = resolve;
});
if (articlePromise) { if (articlePromise) {
this._articlePromise = articlePromise; this._articlePromise = articlePromise;
} }
this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header")); this._headerElementRef = Cu.getWeakReference(doc.querySelector(".reader-header"));
this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain")); this._domainElementRef = Cu.getWeakReference(doc.querySelector(".reader-domain"));
this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title")); this._titleElementRef = Cu.getWeakReference(doc.querySelector(".reader-title"));
this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits")); this._readTimeElementRef = Cu.getWeakReference(doc.querySelector(".reader-estimated-time"));
this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content")); this._creditsElementRef = Cu.getWeakReference(doc.querySelector(".reader-credits"));
this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar")); this._contentElementRef = Cu.getWeakReference(doc.querySelector(".moz-reader-content"));
this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message")); 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; this._scrollOffset = win.pageYOffset;
doc.addEventListener("click", this, false); doc.addEventListener("click", this);
win.addEventListener("pagehide", this, false); win.addEventListener("pagehide", this);
win.addEventListener("scroll", this, false); win.addEventListener("scroll", this);
win.addEventListener("resize", this, false); win.addEventListener("resize", this);
Services.obs.addObserver(this, "inner-window-destroyed", false); Services.obs.addObserver(this, "inner-window-destroyed", false);
doc.addEventListener("visibilitychange", this, false); doc.addEventListener("visibilitychange", this);
this._setupStyleDropdown(); this._setupStyleDropdown();
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close"); this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; // we're ready for any external setup, send a signal for that.
if (gIsFirefoxDesktop) { 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 colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
let colorSchemeOptions = colorSchemeValues.map((value) => { let colorSchemeOptions = colorSchemeValues.map((value) => {
return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value), return {
value: value, name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
itemClass: value + "-button" }; value,
itemClass: value + "-button"
};
}); });
let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
@ -114,7 +117,7 @@ var AboutReader = function(mm, win, articlePromise) {
this._setupLineHeightButtons(); this._setupLineHeightButtons();
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) { if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
new NarrateControls(mm, win); new NarrateControls(mm, win, this._languagePromise);
} }
this._loadArticle(); this._loadArticle();
@ -146,6 +149,10 @@ AboutReader.prototype = {
return this._titleElementRef.get(); return this._titleElementRef.get();
}, },
get _readTimeElement() {
return this._readTimeElementRef.get();
},
get _creditsElement() { get _creditsElement() {
return this._creditsElementRef.get(); return this._creditsElementRef.get();
}, },
@ -162,6 +169,10 @@ AboutReader.prototype = {
return this._messageElementRef.get(); return this._messageElementRef.get();
}, },
get _containerElement() {
return this._containerElementRef.get();
},
get _isToolbarVertical() { get _isToolbarVertical() {
if (this._toolbarVertical !== undefined) { if (this._toolbarVertical !== undefined) {
return this._toolbarVertical; return this._toolbarVertical;
@ -178,7 +189,7 @@ AboutReader.prototype = {
return _viewId; return _viewId;
}, },
receiveMessage: function (message) { receiveMessage (message) {
switch (message.name) { switch (message.name) {
// Triggered by Android user pressing BACK while the banner font-dropdown is open. // Triggered by Android user pressing BACK while the banner font-dropdown is open.
case "Reader:CloseDropdown": { case "Reader:CloseDropdown": {
@ -191,14 +202,14 @@ AboutReader.prototype = {
if (message.data.id && message.data.image && if (message.data.id && message.data.image &&
!this._doc.getElementById(message.data.id)) { !this._doc.getElementById(message.data.id)) {
let btn = this._doc.createElement("button"); 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("style", "background-image: url('" + message.data.image + "')");
btn.setAttribute("id", message.data.id); btn.setAttribute("id", message.data.id);
if (message.data.title) if (message.data.title)
btn.setAttribute("title", message.data.title); btn.setAttribute("title", message.data.title);
if (message.data.text) if (message.data.text)
btn.textContent = message.data.text; btn.textContent = message.data.text;
let tb = this._doc.getElementById("reader-toolbar"); let tb = this._toolbarElement;
tb.appendChild(btn); tb.appendChild(btn);
this._setupButton(message.data.id, button => { this._setupButton(message.data.id, button => {
this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article }); 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) if (!aEvent.isTrusted)
return; return;
switch (aEvent.type) { switch (aEvent.type) {
case "click": case "click":
let target = aEvent.target; let target = aEvent.target;
if (target.classList.contains('dropdown-toggle')) { if (target.classList.contains("dropdown-toggle")) {
this._toggleDropdownClicked(aEvent); this._toggleDropdownClicked(aEvent);
} else if (!target.closest('.dropdown-popup')) { } else if (!target.closest(".dropdown-popup")) {
this._closeDropdowns(); this._closeDropdowns();
} }
if (target.tagName == "A" && !target.classList.contains("reader-domain")) {
this._linkClicked(aEvent);
}
break; break;
case "scroll": case "scroll":
this._closeDropdowns(true); this._closeDropdowns(true);
@ -243,7 +257,7 @@ AboutReader.prototype = {
this._updateImageMargins(); this._updateImageMargins();
if (this._isToolbarVertical) { if (this._isToolbarVertical) {
this._win.setTimeout(() => { this._win.setTimeout(() => {
for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) { for (let dropdown of this._doc.querySelectorAll(".dropdown.open")) {
this._updatePopupPosition(dropdown); this._updatePopupPosition(dropdown);
} }
}, 0); }, 0);
@ -271,12 +285,12 @@ AboutReader.prototype = {
} }
}, },
observe: function(subject, topic, data) { observe(subject, topic, data) {
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) { if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
return; 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:CloseDropdown", this);
this._mm.removeMessageListener("Reader:AddButton", this); this._mm.removeMessageListener("Reader:AddButton", this);
@ -284,12 +298,12 @@ AboutReader.prototype = {
this._windowUnloaded = true; this._windowUnloaded = true;
}, },
_onReaderClose: function() { _onReaderClose() {
ReaderMode.leaveReaderMode(this._mm.docShell, this._win); ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
}, },
_setFontSize: function(newFontSize) { _setFontSize(newFontSize) {
let containerClasses = this._doc.getElementById("container").classList; let containerClasses = this._containerElement.classList;
if (this._fontSize > 0) if (this._fontSize > 0)
containerClasses.remove("font-size" + this._fontSize); containerClasses.remove("font-size" + this._fontSize);
@ -299,19 +313,19 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.font_size", this._fontSize); return AsyncPrefs.set("reader.font_size", this._fontSize);
}, },
_setupFontSizeButtons: function() { _setupFontSizeButtons() {
const FONT_SIZE_MIN = 1; const FONT_SIZE_MIN = 1;
const FONT_SIZE_MAX = 9; const FONT_SIZE_MAX = 9;
// Sample text shown in Android UI. // 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"); sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
let currentSize = Services.prefs.getIntPref("reader.font_size"); let currentSize = Services.prefs.getIntPref("reader.font_size");
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize)); currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
let plusButton = this._doc.getElementById("font-size-plus"); let plusButton = this._doc.querySelector(".plus-button");
let minusButton = this._doc.getElementById("font-size-minus"); let minusButton = this._doc.querySelector(".minus-button");
function updateControls() { function updateControls() {
if (currentSize === FONT_SIZE_MIN) { if (currentSize === FONT_SIZE_MIN) {
@ -360,8 +374,8 @@ AboutReader.prototype = {
}, true); }, true);
}, },
_setContentWidth: function(newContentWidth) { _setContentWidth(newContentWidth) {
let containerClasses = this._doc.getElementById("container").classList; let containerClasses = this._containerElement.classList;
if (this._contentWidth > 0) if (this._contentWidth > 0)
containerClasses.remove("content-width" + this._contentWidth); containerClasses.remove("content-width" + this._contentWidth);
@ -371,15 +385,15 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.content_width", this._contentWidth); return AsyncPrefs.set("reader.content_width", this._contentWidth);
}, },
_setupContentWidthButtons: function() { _setupContentWidthButtons() {
const CONTENT_WIDTH_MIN = 1; const CONTENT_WIDTH_MIN = 1;
const CONTENT_WIDTH_MAX = 9; const CONTENT_WIDTH_MAX = 9;
let currentContentWidth = Services.prefs.getIntPref("reader.content_width"); let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth)); currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
let plusButton = this._doc.getElementById("content-width-plus"); let plusButton = this._doc.querySelector(".content-width-plus-button");
let minusButton = this._doc.getElementById("content-width-minus"); let minusButton = this._doc.querySelector(".content-width-minus-button");
function updateControls() { function updateControls() {
if (currentContentWidth === CONTENT_WIDTH_MIN) { if (currentContentWidth === CONTENT_WIDTH_MIN) {
@ -428,8 +442,8 @@ AboutReader.prototype = {
}, true); }, true);
}, },
_setLineHeight: function(newLineHeight) { _setLineHeight(newLineHeight) {
let contentClasses = this._doc.getElementById("moz-reader-content").classList; let contentClasses = this._contentElement.classList;
if (this._lineHeight > 0) if (this._lineHeight > 0)
contentClasses.remove("line-height" + this._lineHeight); contentClasses.remove("line-height" + this._lineHeight);
@ -439,15 +453,15 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.line_height", this._lineHeight); return AsyncPrefs.set("reader.line_height", this._lineHeight);
}, },
_setupLineHeightButtons: function() { _setupLineHeightButtons() {
const LINE_HEIGHT_MIN = 1; const LINE_HEIGHT_MIN = 1;
const LINE_HEIGHT_MAX = 9; const LINE_HEIGHT_MAX = 9;
let currentLineHeight = Services.prefs.getIntPref("reader.line_height"); let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight)); currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
let plusButton = this._doc.getElementById("line-height-plus"); let plusButton = this._doc.querySelector(".line-height-plus-button");
let minusButton = this._doc.getElementById("line-height-minus"); let minusButton = this._doc.querySelector(".line-height-minus-button");
function updateControls() { function updateControls() {
if (currentLineHeight === LINE_HEIGHT_MIN) { if (currentLineHeight === LINE_HEIGHT_MIN) {
@ -496,7 +510,7 @@ AboutReader.prototype = {
}, true); }, true);
}, },
_handleDeviceLight: function(newLux) { _handleDeviceLight(newLux) {
// Desired size of the this._luxValues array. // Desired size of the this._luxValues array.
let luxValuesSize = 10; let luxValuesSize = 10;
// Add new lux value at the front of the array. // Add new lux value at the front of the array.
@ -513,7 +527,7 @@ AboutReader.prototype = {
return; return;
} }
// Holds the average of the lux values collected in this._luxValues. // Holds the average of the lux values collected in this._luxValues.
let averageLuxValue = this._totalLux/luxValuesSize; let averageLuxValue = this._totalLux / luxValuesSize;
this._updateColorScheme(averageLuxValue); this._updateColorScheme(averageLuxValue);
// Pop the oldest value off the array. // Pop the oldest value off the array.
@ -522,7 +536,7 @@ AboutReader.prototype = {
this._totalLux -= oldLux; this._totalLux -= oldLux;
}, },
_handleVisibilityChange: function() { _handleVisibilityChange() {
let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
if (colorScheme != "auto") { if (colorScheme != "auto") {
return; return;
@ -533,19 +547,19 @@ AboutReader.prototype = {
}, },
// Setup or teardown the ambient light tracking system. // Setup or teardown the ambient light tracking system.
_enableAmbientLighting: function(enable) { _enableAmbientLighting(enable) {
if (enable) { if (enable) {
this._win.addEventListener("devicelight", this, false); this._win.addEventListener("devicelight", this);
this._luxValues = []; this._luxValues = [];
this._totalLux = 0; this._totalLux = 0;
} else { } else {
this._win.removeEventListener("devicelight", this, false); this._win.removeEventListener("devicelight", this);
delete this._luxValues; delete this._luxValues;
delete this._totalLux; delete this._totalLux;
} }
}, },
_updateColorScheme: function(luxValue) { _updateColorScheme(luxValue) {
// Upper bound value for "dark" color scheme beyond which it changes to "light". // Upper bound value for "dark" color scheme beyond which it changes to "light".
let upperBoundDark = 50; let upperBoundDark = 50;
// Lower bound value for "light" color scheme beyond which it changes to "dark". // Lower bound value for "light" color scheme beyond which it changes to "dark".
@ -564,7 +578,7 @@ AboutReader.prototype = {
this._setColorScheme("light"); this._setColorScheme("light");
}, },
_setColorScheme: function(newColorScheme) { _setColorScheme(newColorScheme) {
// "auto" is not a real color scheme // "auto" is not a real color scheme
if (this._colorScheme === newColorScheme || newColorScheme === "auto") if (this._colorScheme === newColorScheme || newColorScheme === "auto")
return; return;
@ -580,14 +594,14 @@ AboutReader.prototype = {
// Pref values include "dark", "light", and "auto", which automatically switches // Pref values include "dark", "light", and "auto", which automatically switches
// between light and dark color schemes based on the ambient light level. // between light and dark color schemes based on the ambient light level.
_setColorSchemePref: function(colorSchemePref) { _setColorSchemePref(colorSchemePref) {
this._enableAmbientLighting(colorSchemePref === "auto"); this._enableAmbientLighting(colorSchemePref === "auto");
this._setColorScheme(colorSchemePref); this._setColorScheme(colorSchemePref);
AsyncPrefs.set("reader.color_scheme", colorSchemePref); AsyncPrefs.set("reader.color_scheme", colorSchemePref);
}, },
_setFontType: function(newFontType) { _setFontType(newFontType) {
if (this._fontType === newFontType) if (this._fontType === newFontType)
return; return;
@ -602,20 +616,38 @@ AboutReader.prototype = {
AsyncPrefs.set("reader.font_type", this._fontType); AsyncPrefs.set("reader.font_type", this._fontType);
}, },
_setSystemUIVisibility: function(visible) { _setSystemUIVisibility(visible) {
this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: 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(); let url = this._getOriginalUrl();
this._showProgressDelayed(); this._showProgressDelayed();
let article; let article;
if (this._articlePromise) { if (this._articlePromise) {
article = yield this._articlePromise; article = await this._articlePromise;
} else { } else {
try { try {
article = yield this._getArticle(url); article = await this._getArticle(url);
} catch (e) { } catch (e) {
if (e && e.newURL) { if (e && e.newURL) {
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL); let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
@ -638,24 +670,26 @@ AboutReader.prototype = {
} }
this._showContent(article); 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) => { let handleFaviconReturn = (message) => {
this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn); this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
this._loadFavicon(message.data.url, message.data.faviconUrl); this._loadFavicon(message.data.url, message.data.faviconUrl);
@ -665,20 +699,20 @@ AboutReader.prototype = {
this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url }); this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
}, },
_loadFavicon: function(url, faviconUrl) { _loadFavicon(url, faviconUrl) {
if (this._article.url !== url) if (this._article.url !== url)
return; return;
let doc = this._doc; let doc = this._doc;
let link = doc.createElement('link'); let link = doc.createElement("link");
link.rel = 'shortcut icon'; link.rel = "shortcut icon";
link.href = faviconUrl; link.href = faviconUrl;
doc.getElementsByTagName('head')[0].appendChild(link); doc.getElementsByTagName("head")[0].appendChild(link);
}, },
_updateImageMargins: function() { _updateImageMargins() {
let windowWidth = this._win.innerWidth; let windowWidth = this._win.innerWidth;
let bodyWidth = this._doc.body.clientWidth; 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 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); img.setAttribute("moz-reader-center", true);
} else { } else {
img.removeAttribute("moz-reader-center"); img.removeAttribute("moz-reader-center");
@ -713,30 +747,32 @@ AboutReader.prototype = {
}, },
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) { _maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
if (!article.dir) if (article.dir) {
return; // Set "dir" attribute on content
this._contentElement.setAttribute("dir", article.dir);
this._headerElement.setAttribute("dir", article.dir);
// Set "dir" attribute on content // The native locale could be set differently than the article's text direction.
this._contentElement.setAttribute("dir", article.dir); var localeDirection = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
this._headerElement.setAttribute("dir", article.dir); this._readTimeElement.setAttribute("dir", localeDirection);
}, this._readTimeElement.style.textAlign = article.dir == "rtl" ? "right" : "left";
_fixLocalLinks() {
// We need to do this because preprocessing the content through nsIParserUtils
// gives back a DOM with a <base> element. That influences how these URLs get
// resolved, making them no longer match the document URI (which is
// about:reader?url=...). To fix this, make all the hash URIs absolute. This
// is hacky, but the alternative of removing the base element has potential
// security implications if Readability has not successfully made all the URLs
// absolute, so we pick just fixing these in-document links explicitly.
let localLinks = this._contentElement.querySelectorAll("a[href^='#']");
for (let localLink of localLinks) {
// Have to get the attribute because .href provides an absolute URI.
localLink.href = this._doc.documentURI + localLink.getAttribute("href");
} }
}, },
_showError: function() { _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._headerElement.style.display = "none";
this._contentElement.style.display = "none"; this._contentElement.style.display = "none";
@ -746,11 +782,16 @@ AboutReader.prototype = {
this._doc.title = errorMessage; this._doc.title = errorMessage;
this._doc.documentElement.dataset.isError = true;
this._error = 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. // This function is the JS version of Java's StringUtils.stripCommonSubdomains.
_stripHost: function(host) { _stripHost(host) {
if (!host) if (!host)
return host; return host;
@ -766,17 +807,18 @@ AboutReader.prototype = {
return host.substring(start); return host.substring(start);
}, },
_showContent: function(article) { _showContent(article) {
this._messageElement.style.display = "none"; this._messageElement.style.display = "none";
this._article = article; this._article = article;
this._domainElement.href = article.url; 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._domainElement.textContent = this._stripHost(articleUri.host);
this._creditsElement.textContent = article.byline; this._creditsElement.textContent = article.byline;
this._titleElement.textContent = article.title; this._titleElement.textContent = article.title;
this._readTimeElement.textContent = this._formatReadTime(article.readingTimeMinsSlow, article.readingTimeMinsFast);
this._doc.title = article.title; this._doc.title = article.title;
this._headerElement.style.display = "block"; this._headerElement.style.display = "block";
@ -787,8 +829,8 @@ AboutReader.prototype = {
false, articleUri, this._contentElement); false, articleUri, this._contentElement);
this._contentElement.innerHTML = ""; this._contentElement.innerHTML = "";
this._contentElement.appendChild(contentFragment); this._contentElement.appendChild(contentFragment);
this._fixLocalLinks();
this._maybeSetTextDirection(article); this._maybeSetTextDirection(article);
this._foundLanguage(article.language);
this._contentElement.style.display = "block"; this._contentElement.style.display = "block";
this._updateImageMargins(); this._updateImageMargins();
@ -804,13 +846,13 @@ AboutReader.prototype = {
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false })); new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
}, },
_hideContent: function() { _hideContent() {
this._headerElement.style.display = "none"; this._headerElement.style.display = "none";
this._contentElement.style.display = "none"; this._contentElement.style.display = "none";
}, },
_showProgressDelayed: function() { _showProgressDelayed() {
this._win.setTimeout(function() { this._win.setTimeout(() => {
// No need to show progress if the article has been loaded, // No need to show progress if the article has been loaded,
// if the window has been unloaded, or if there was an error // if the window has been unloaded, or if there was an error
// trying to load the article. // trying to load the article.
@ -823,20 +865,20 @@ AboutReader.prototype = {
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2"); this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
this._messageElement.style.display = "block"; this._messageElement.style.display = "block";
}.bind(this), 300); }, 300);
}, },
/** /**
* Returns the original article URL for this about:reader view. * 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; let url = win ? win.location.href : this._win.location.href;
return ReaderMode.getOriginalUrl(url) || url; return ReaderMode.getOriginalUrl(url) || url;
}, },
_setupSegmentedButton: function(id, options, initialValue, callback) { _setupSegmentedButton(id, options, initialValue, callback) {
let doc = this._doc; let doc = this._doc;
let segmentedButton = doc.getElementById(id); let segmentedButton = doc.getElementsByClassName(id)[0];
for (let i = 0; i < options.length; i++) { for (let i = 0; i < options.length; i++) {
let option = options[i]; let option = options[i];
@ -867,10 +909,6 @@ AboutReader.prototype = {
aEvent.stopPropagation(); 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; let items = segmentedButton.children;
for (let j = items.length - 1; j >= 0; j--) { for (let j = items.length - 1; j >= 0; j--) {
items[j].classList.remove("selected"); items[j].classList.remove("selected");
@ -878,19 +916,19 @@ AboutReader.prototype = {
item.classList.add("selected"); item.classList.add("selected");
callback(option.value); callback(option.value);
}.bind(this), true); }, true);
if (option.value === initialValue) if (option.value === initialValue)
item.classList.add("selected"); item.classList.add("selected");
} }
}, },
_setupButton: function(id, callback, titleEntity, textEntity) { _setupButton(id, callback, titleEntity, textEntity) {
if (titleEntity) { if (titleEntity) {
this._setButtonTip(id, titleEntity); this._setButtonTip(id, titleEntity);
} }
let button = this._doc.getElementById(id); let button = this._doc.getElementsByClassName(id)[0];
if (textEntity) { if (textEntity) {
button.textContent = gStrings.GetStringFromName(textEntity); button.textContent = gStrings.GetStringFromName(textEntity);
} }
@ -910,17 +948,17 @@ AboutReader.prototype = {
* and dynamically as button state changes. * and dynamically as button state changes.
* @param Localizable string providing UI element usage tip. * @param Localizable string providing UI element usage tip.
*/ */
_setButtonTip: function(id, titleEntity) { _setButtonTip(id, titleEntity) {
let button = this._doc.getElementById(id); let button = this._doc.getElementsByClassName(id)[0];
button.setAttribute("title", gStrings.GetStringFromName(titleEntity)); button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
}, },
_setupStyleDropdown: function() { _setupStyleDropdown() {
let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle"); let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle");
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls")); dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
}, },
_updatePopupPosition: function(dropdown) { _updatePopupPosition(dropdown) {
let dropdownToggle = dropdown.querySelector(".dropdown-toggle"); let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
let dropdownPopup = dropdown.querySelector(".dropdown-popup"); let dropdownPopup = dropdown.querySelector(".dropdown-popup");
@ -931,8 +969,8 @@ AboutReader.prototype = {
dropdownPopup.style.top = popupTop + "px"; dropdownPopup.style.top = popupTop + "px";
}, },
_toggleDropdownClicked: function(event) { _toggleDropdownClicked(event) {
let dropdown = event.target.closest('.dropdown'); let dropdown = event.target.closest(".dropdown");
if (!dropdown) if (!dropdown)
return; return;
@ -952,7 +990,7 @@ AboutReader.prototype = {
/* /*
* If the ReaderView banner font-dropdown is closed, open it. * If the ReaderView banner font-dropdown is closed, open it.
*/ */
_openDropdown: function(dropdown) { _openDropdown(dropdown) {
if (dropdown.classList.contains("open")) { if (dropdown.classList.contains("open")) {
return; return;
} }
@ -969,7 +1007,7 @@ AboutReader.prototype = {
* dropdowns because the page is scrolling, allow popups to stay open with * dropdowns because the page is scrolling, allow popups to stay open with
* the keep-open class. * the keep-open class.
*/ */
_closeDropdowns: function(scrolling) { _closeDropdowns(scrolling) {
let selector = ".dropdown.open"; let selector = ".dropdown.open";
if (scrolling) { if (scrolling) {
selector += ":not(.keep-open)"; 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 * 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() { discardNextComment: function() {
if (this.match("--")) { if (this.match("--")) {
this.discardTo("-->"); this.discardTo("-->");
@ -1131,11 +1091,7 @@
// If this isn't a void Element, read its child nodes // If this isn't a void Element, read its child nodes
if (!closed) { if (!closed) {
if (localName == "script") { this.readChildren(node);
this.readScript(node);
} else {
this.readChildren(node);
}
var closingTag = "</" + localName + ">"; var closingTag = "</" + localName + ">";
if (!this.match(closingTag)) { if (!this.match(closingTag)) {
this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length)); 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; const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
// Constants for telemetry. // Class names to preserve in the readerized output. We preserve these class
const DOWNLOAD_SUCCESS = 0; // names so that rules in aboutReader.css can match them.
const DOWNLOAD_ERROR_XHR = 1; const CLASSES_TO_PRESERVE = [
const DOWNLOAD_ERROR_NO_DOC = 2; "caption",
"hidden",
const PARSE_SUCCESS = 0; "invisble",
const PARSE_ERROR_TOO_MANY_ELEMENTS = 1; "sr-only",
const PARSE_ERROR_WORKER = 2; "visually-hidden",
const PARSE_ERROR_NO_ARTICLE = 3; "visuallyhidden",
"wp-caption",
"wp-caption-text",
];
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -24,17 +27,15 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["XMLHttpRequest"]); Cu.importGlobalProperties(["XMLHttpRequest"]);
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js"); 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, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.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() { XPCOMUtils.defineLazyGetter(this, "Readability", function() {
let scope = {}; let scope = {};
scope.dump = this.dump; scope.dump = this.dump;
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope); Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
return scope["Readability"]; return scope.Readability;
}); });
this.ReaderMode = { this.ReaderMode = {
@ -61,21 +62,13 @@ this.ReaderMode = {
return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad(); return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
}, },
get isOnLowMemoryPlatform() { _getStateForParseOnLoad() {
let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
delete this.isOnLowMemoryPlatform;
return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
},
_getStateForParseOnLoad: function () {
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled"); let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-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. return isForceEnabled || isEnabled;
// See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
}, },
observe: function(aMessage, aTopic, aData) { observe(aMessage, aTopic, aData) {
switch (aTopic) { switch (aTopic) {
case "nsPref:changed": case "nsPref:changed":
if (aData.startsWith("reader.parse-on-load.")) { 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, * Enter the reader mode by going forward one step in history if applicable,
* if not, append the about:reader page in the history instead. * 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 url = win.document.location.href;
let readerURL = "about:reader?url=" + encodeURIComponent(url); let readerURL = "about:reader?url=" + encodeURIComponent(url);
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); 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, * Exit the reader mode by going back one step in history if applicable,
* if not, append the original page in the history instead. * if not, append the original page in the history instead.
*/ */
leaveReaderMode: function(docShell, win) { leaveReaderMode(docShell, win) {
let url = win.document.location.href; let url = win.document.location.href;
let originalURL = this.getOriginalUrl(url); let originalURL = this.getOriginalUrl(url);
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); 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 * @return The original URL for the article, or null if we did not find
* a properly formatted about:reader URL. * a properly formatted about:reader URL.
*/ */
getOriginalUrl: function(url) { getOriginalUrl(url) {
if (!url.startsWith("about:reader?")) { if (!url.startsWith("about:reader?")) {
return null; return null;
} }
let outerHash = ""; let outerHash = "";
try { try {
let uriObj = Services.io.newURI(url, null, null); let uriObj = Services.io.newURI(url);
url = uriObj.specIgnoringRef; url = uriObj.specIgnoringRef;
outerHash = uriObj.ref; outerHash = uriObj.ref;
} catch (ex) { /* ignore, use the raw string */ } } catch (ex) { /* ignore, use the raw string */ }
@ -155,27 +148,45 @@ this.ReaderMode = {
let originalUrl = searchParams.get("url"); let originalUrl = searchParams.get("url");
if (outerHash) { if (outerHash) {
try { try {
let uriObj = Services.io.newURI(originalUrl, null, null); let uriObj = Services.io.newURI(originalUrl);
uriObj = Services.io.newURI('#' + outerHash, null, uriObj); uriObj = Services.io.newURI("#" + outerHash, null, uriObj);
originalUrl = uriObj.spec; originalUrl = uriObj.spec;
} catch (ex) {} } catch (ex) {}
} }
return originalUrl; 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. * Decides whether or not a document is reader-able without parsing the whole thing.
* *
* @param doc A document to parse. * @param doc A document to parse.
* @return boolean Whether or not we should show the reader mode button. * @return boolean Whether or not we should show the reader mode button.
*/ */
isProbablyReaderable: function(doc) { isProbablyReaderable(doc) {
// Only care about 'real' HTML documents: // Only care about 'real' HTML documents:
if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) { if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
return false; return false;
} }
let uri = Services.io.newURI(doc.location.href, null, null); let uri = Services.io.newURI(doc.location.href);
if (!this._shouldCheckUri(uri)) { if (!this._shouldCheckUri(uri)) {
return false; return false;
} }
@ -187,12 +198,12 @@ this.ReaderMode = {
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils)); return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
}, },
isNodeVisible: function(utils, node) { isNodeVisible(utils, node) {
let bounds = utils.getBoundsWithoutFlushing(node); let bounds = utils.getBoundsWithoutFlushing(node);
return bounds.height > 0 && bounds.width > 0; return bounds.height > 0 && bounds.width > 0;
}, },
getUtilsForWin: function(win) { getUtilsForWin(win) {
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
}, },
@ -204,16 +215,14 @@ this.ReaderMode = {
* @return {Promise} * @return {Promise}
* @resolves JS object representing the article, or null if no article is found. * @resolves JS object representing the article, or null if no article is found.
*/ */
parseDocument: Task.async(function* (doc) { parseDocument(doc) {
let documentURI = Services.io.newURI(doc.documentURI, null, null); if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
let baseURI = Services.io.newURI(doc.baseURI, null, null);
if (!this._shouldCheckUri(documentURI) || !this._shouldCheckUri(baseURI, true)) {
this.log("Reader mode disabled for URI"); this.log("Reader mode disabled for URI");
return null; return null;
} }
return yield this._readerParse(baseURI, doc); return this._readerParse(doc);
}), },
/** /**
* Downloads and parses a document from a URL. * Downloads and parses a document from a URL.
@ -222,19 +231,28 @@ this.ReaderMode = {
* @return {Promise} * @return {Promise}
* @resolves JS object representing the article, or null if no article is found. * @resolves JS object representing the article, or null if no article is found.
*/ */
downloadAndParseDocument: Task.async(function* (url) { async downloadAndParseDocument(url) {
let doc = yield this._downloadDocument(url); let doc = await this._downloadDocument(url);
let uri = Services.io.newURI(doc.baseURI, null, null); if (!doc) {
if (!this._shouldCheckUri(uri, true)) { return null;
}
if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI"); this.log("Reader mode disabled for URI");
return null; return null;
} }
return yield this._readerParse(uri, doc); return await this._readerParse(doc);
}), },
_downloadDocument: function (url) { _downloadDocument(url) {
let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT"); 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) => { return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open("GET", url, true); xhr.open("GET", url, true);
@ -243,14 +261,12 @@ this.ReaderMode = {
xhr.onload = evt => { xhr.onload = evt => {
if (xhr.status !== 200) { if (xhr.status !== 200) {
reject("Reader mode XHR failed with status: " + xhr.status); reject("Reader mode XHR failed with status: " + xhr.status);
histogram.add(DOWNLOAD_ERROR_XHR);
return; return;
} }
let doc = xhr.responseXML; let doc = xhr.responseXML;
if (!doc) { if (!doc) {
reject("Reader mode XHR didn't return a document"); reject("Reader mode XHR didn't return a document");
histogram.add(DOWNLOAD_ERROR_NO_DOC);
return; return;
} }
@ -261,7 +277,7 @@ this.ReaderMode = {
if (content) { if (content) {
let urlIndex = content.toUpperCase().indexOf("URL="); let urlIndex = content.toUpperCase().indexOf("URL=");
if (urlIndex > -1) { 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 newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
let newURL = newURI.spec; let newURL = newURI.spec;
let ssm = Services.scriptSecurityManager; let ssm = Services.scriptSecurityManager;
@ -290,10 +306,10 @@ this.ReaderMode = {
// Convert these to real URIs to make sure the escaping (or lack // Convert these to real URIs to make sure the escaping (or lack
// thereof) is identical: // thereof) is identical:
try { 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 */ } } catch (ex) { /* Ignore errors - we'll use what we had before */ }
try { 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 */ } } catch (ex) { /* Ignore errors - we'll use what we had before */ }
if (responseURL != givenURL) { if (responseURL != givenURL) {
@ -303,7 +319,6 @@ this.ReaderMode = {
return; return;
} }
resolve(doc); resolve(doc);
histogram.add(DOWNLOAD_SUCCESS);
}; };
xhr.send(); xhr.send();
}); });
@ -318,17 +333,17 @@ this.ReaderMode = {
* @resolves JS object representing the article, or null if no article is found. * @resolves JS object representing the article, or null if no article is found.
* @rejects OS.File.Error * @rejects OS.File.Error
*/ */
getArticleFromCache: Task.async(function* (url) { async getArticleFromCache(url) {
let path = this._toHashedPath(url); let path = this._toHashedPath(url);
try { try {
let array = yield OS.File.read(path); let array = await OS.File.read(path);
return JSON.parse(new TextDecoder().decode(array)); return JSON.parse(new TextDecoder().decode(array));
} catch (e) { } catch (e) {
if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile) if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
throw e; throw e;
return null; return null;
} }
}), },
/** /**
* Stores an article in the cache. * Stores an article in the cache.
@ -338,14 +353,14 @@ this.ReaderMode = {
* @resolves When the article is stored. * @resolves When the article is stored.
* @rejects OS.File.Error * @rejects OS.File.Error
*/ */
storeArticleInCache: Task.async(function* (article) { async storeArticleInCache(article) {
let array = new TextEncoder().encode(JSON.stringify(article)); let array = new TextEncoder().encode(JSON.stringify(article));
let path = this._toHashedPath(article.url); let path = this._toHashedPath(article.url);
yield this._ensureCacheDir(); await this._ensureCacheDir();
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" }) return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
.then(success => { .then(success => {
OS.File.stat(path).then(info => { OS.File.stat(path).then(info => {
return Messaging.sendRequest({ return EventDispatcher.instance.sendRequest({
type: "Reader:AddedToCache", type: "Reader:AddedToCache",
url: article.url, url: article.url,
size: info.size, size: info.size,
@ -353,7 +368,7 @@ this.ReaderMode = {
}); });
}); });
}); });
}), },
/** /**
* Removes an article from the cache given an article URI. * Removes an article from the cache given an article URI.
@ -363,26 +378,29 @@ this.ReaderMode = {
* @resolves When the article is removed. * @resolves When the article is removed.
* @rejects OS.File.Error * @rejects OS.File.Error
*/ */
removeArticleFromCache: Task.async(function* (url) { async removeArticleFromCache(url) {
let path = this._toHashedPath(url); let path = this._toHashedPath(url);
yield OS.File.remove(path); await OS.File.remove(path);
}), },
log: function(msg) { log(msg) {
if (this.DEBUG) if (this.DEBUG)
dump("Reader: " + msg); dump("Reader: " + msg);
}, },
_blockedHosts: [ _blockedHosts: [
"mail.google.com", "amazon.com",
"basilisk-browser.org",
"github.com", "github.com",
"mail.google.com",
"palemoon.org",
"pinterest.com", "pinterest.com",
"reddit.com", "reddit.com",
"twitter.com", "twitter.com",
"youtube.com", "youtube.com",
], ],
_shouldCheckUri: function (uri, isBaseUri = false) { _shouldCheckUri(uri, isBaseUri = false) {
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) { if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
this.log("Not parsing URI scheme: " + uri.scheme); this.log("Not parsing URI scheme: " + uri.scheme);
return false; return false;
@ -412,59 +430,77 @@ this.ReaderMode = {
* Attempts to parse a document into an article. Heavy lifting happens * Attempts to parse a document into an article. Heavy lifting happens
* in readerWorker.js. * in readerWorker.js.
* *
* @param uri The base URI of the article.
* @param doc The document to parse. * @param doc The document to parse.
* @return {Promise} * @return {Promise}
* @resolves JS object representing the article, or null if no article is found. * @resolves JS object representing the article, or null if no article is found.
*/ */
_readerParse: Task.async(function* (uri, doc) { async _readerParse(doc) {
let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
if (this.parseNodeLimit) { if (this.parseNodeLimit) {
let numTags = doc.getElementsByTagName("*").length; let numTags = doc.getElementsByTagName("*").length;
if (numTags > this.parseNodeLimit) { if (numTags > this.parseNodeLimit) {
this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found"); this.log("Aborting parse for " + doc.baseURIObject.spec + "; " + numTags + " elements found");
histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
return null; 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 = { let uriParam = {
spec: uri.spec, spec: doc.baseURIObject.spec,
host: uri.host, host: doc.baseURIObject.host,
prePath: uri.prePath, prePath: doc.baseURIObject.prePath,
scheme: uri.scheme, scheme: doc.baseURIObject.scheme,
pathBase: Services.io.newURI(".", null, uri).spec 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"]. let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
createInstance(Ci.nsIDOMSerializer); createInstance(Ci.nsIDOMSerializer);
let serializedDoc = serializer.serializeToString(doc); let serializedDoc = serializer.serializeToString(doc);
let options = {
classesToPreserve: CLASSES_TO_PRESERVE,
};
let article = null; let article = null;
try { try {
article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]); article = await ReaderWorker.post("parseDocument", [uriParam, serializedDoc, options]);
} catch (e) { } catch (e) {
Cu.reportError("Error in ReaderWorker: " + 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) { if (!article) {
this.log("Worker did not return an article"); this.log("Worker did not return an article");
histogram.add(PARSE_ERROR_NO_ARTICLE);
return null; return null;
} }
// Readability returns a URI object, but we only care about the URL. // Readability returns a URI object based on the baseURI, but we only care
article.url = article.uri.spec; // 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; delete article.uri;
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks; let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils) article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
.convertToPlainText(article.title, flags, 0); .convertToPlainText(article.title, flags, 0);
histogram.add(PARSE_SUCCESS); await this._assignLanguage(article, langAttributes);
this._maybeAssignTextDirection(article);
this._assignReadTime(article);
return article; return article;
}), },
get _cryptoHash() { get _cryptoHash() {
delete this._cryptoHash; delete this._cryptoHash;
@ -485,7 +521,7 @@ this.ReaderMode = {
* @param url The article URL. This should have referrers removed. * @param url The article URL. This should have referrers removed.
* @return The file path to the cached article. * @return The file path to the cached article.
*/ */
_toHashedPath: function (url) { _toHashedPath(url) {
let value = this._unicodeConverter.convertToByteArray(url); let value = this._unicodeConverter.convertToByteArray(url);
this._cryptoHash.init(this._cryptoHash.MD5); this._cryptoHash.init(this._cryptoHash.MD5);
this._cryptoHash.update(value, value.length); this._cryptoHash.update(value, value.length);
@ -502,7 +538,7 @@ this.ReaderMode = {
* @resolves When the cache directory exists. * @resolves When the cache directory exists.
* @rejects OS.File.Error * @rejects OS.File.Error
*/ */
_ensureCacheDir: function () { _ensureCacheDir() {
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache"); let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
return OS.File.exists(dir).then(exists => { return OS.File.exists(dir).then(exists => {
if (!exists) { if (!exists) {
@ -510,5 +546,107 @@ this.ReaderMode = {
} }
return undefined; 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, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env mozilla/chrome-worker */
"use strict"; "use strict";
/** /**
@ -40,11 +42,12 @@ var Agent = {
* *
* @param {object} uri URI data for the document. * @param {object} uri URI data for the document.
* @param {string} serializedDoc The serialized document. * @param {string} serializedDoc The serialized document.
* @param {object} options Options object to pass to Readability.
* *
* @return {object} Article object returned from Readability. * @return {object} Article object returned from Readability.
*/ */
parseDocument: function (uri, serializedDoc) { parseDocument(uri, serializedDoc, options) {
let doc = new JSDOMParser().parse(serializedDoc); 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"/> <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> </head>
<body> <body>
<div id="container" class="container"> <div class="container">
<div id="reader-header" class="header"> <div class="header reader-header">
<style scoped> <a class="domain reader-domain"></a>
@import url("chrome://global/skin/aboutReaderControls.css");
</style>
<a id="reader-domain" class="domain"></a>
<div class="domain-border"></div> <div class="domain-border"></div>
<h1 id="reader-title"></h1> <h1 class="reader-title"></h1>
<div id="reader-credits" class="credits"></div> <div class="credits reader-credits"></div>
<div class="meta-data">
<div class="reader-estimated-time"></div>
</div>
</div> </div>
<hr>
<div class="content"> <div class="content">
<style scoped> <div class="moz-reader-content"></div>
@import url("chrome://global/skin/aboutReaderContent.css");
</style>
<div id="moz-reader-content"></div>
</div> </div>
<div> <div>
<style scoped> <div class="reader-message"></div>
@import url("chrome://global/skin/aboutReaderControls.css");
</style>
<div id="reader-message"></div>
</div> </div>
</div> </div>
<ul id="reader-toolbar" class="toolbar"> <ul class="toolbar reader-toolbar">
<style scoped> <li><button class="button close-button"/></li>
@import url("chrome://global/skin/aboutReaderControls.css"); <ul class="dropdown style-dropdown">
</style>
<li><button id="close-button" class="button close-button"/></li>
<ul id="style-dropdown" class="dropdown">
<li><button class="dropdown-toggle button style-button"/></li> <li><button class="dropdown-toggle button style-button"/></li>
<li id="reader-popup" class="dropdown-popup"> <li class="dropdown-popup">
<div id="font-type-buttons"></div> <div class="font-type-buttons"></div>
<hr></hr> <hr>
<div id="font-size-buttons"> <div class="font-size-buttons">
<button id="font-size-minus" class="minus-button"/> <button class="minus-button"/>
<button id="font-size-sample"/> <button class="font-size-sample"/>
<button id="font-size-plus" class="plus-button"/> <button class="plus-button"/>
</div> </div>
<hr></hr> <hr>
<div id="content-width-buttons"> <div class="content-width-buttons">
<button id="content-width-minus" class="content-width-minus-button"/> <button class="content-width-minus-button"/>
<button id="content-width-plus" class="content-width-plus-button"/> <button class="content-width-plus-button"/>
</div> </div>
<hr></hr> <hr>
<div id="line-height-buttons"> <div class="line-height-buttons">
<button id="line-height-minus" class="line-height-minus-button"/> <button class="line-height-minus-button"/>
<button id="line-height-plus" class="line-height-plus-button"/> <button class="line-height-plus-button"/>
</div> </div>
<hr></hr> <hr>
<div id="color-scheme-buttons"></div> <div class="color-scheme-buttons"></div>
<div class="dropdown-arrow"/> <div class="dropdown-arrow"/>
</li> </li>
</ul> </ul>

View file

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

View file

@ -594,12 +594,6 @@ var Printing = {
contentElement.setAttribute("class", "content"); contentElement.setAttribute("class", "content");
containerElement.appendChild(contentElement); 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 // Jam the article's content into content div
let readerContent = content.document.createElement("div"); let readerContent = content.document.createElement("div");
readerContent.setAttribute("id", "moz-reader-content"); readerContent.setAttribute("id", "moz-reader-content");

View file

@ -13,6 +13,22 @@ aboutReader.colorScheme.dark=Dark
aboutReader.colorScheme.sepia=Sepia aboutReader.colorScheme.sepia=Sepia
aboutReader.colorScheme.auto=Auto 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): # LOCALIZATION NOTE (aboutReader.fontType.serif, aboutReader.fontType.sans-serif):
# These are the styles of typeface that are options in the reader view controls. # These are the styles of typeface that are options in the reader view controls.
aboutReader.fontType.serif=Serif 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, * 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/. */ * 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 { body {
padding: 64px 51px; 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 { body.loaded {
transition: color 0.4s, background-color 0.4s; transition: color 0.4s, background-color 0.4s;
} }
@ -56,80 +46,80 @@ body.serif .remove-button {
font-family: Georgia, "Times New Roman", serif; font-family: Georgia, "Times New Roman", serif;
} }
#container { .container {
max-width: 30em; max-width: 30em;
margin: 0 auto; margin: 0 auto;
} }
#container.font-size1 { .container.font-size1 {
font-size: 12px; font-size: 12px;
} }
#container.font-size2 { .container.font-size2 {
font-size: 14px; font-size: 14px;
} }
#container.font-size3 { .container.font-size3 {
font-size: 16px; font-size: 16px;
} }
#container.font-size4 { .container.font-size4 {
font-size: 18px; font-size: 18px;
} }
#container.font-size5 { .container.font-size5 {
font-size: 20px; font-size: 20px;
} }
#container.font-size6 { .container.font-size6 {
font-size: 22px; font-size: 22px;
} }
#container.font-size7 { .container.font-size7 {
font-size: 24px; font-size: 24px;
} }
#container.font-size8 { .container.font-size8 {
font-size: 26px; font-size: 26px;
} }
#container.font-size9 { .container.font-size9 {
font-size: 28px; font-size: 28px;
} }
#container.content-width1 { .container.content-width1 {
max-width: 20em; max-width: 20em;
} }
#container.content-width2 { .container.content-width2 {
max-width: 25em; max-width: 25em;
} }
#container.content-width3 { .container.content-width3 {
max-width: 30em; max-width: 30em;
} }
#container.content-width4 { .container.content-width4 {
max-width: 35em; max-width: 35em;
} }
#container.content-width5 { .container.content-width5 {
max-width: 40em; max-width: 40em;
} }
#container.content-width6 { .container.content-width6 {
max-width: 45em; max-width: 45em;
} }
#container.content-width7 { .container.content-width7 {
max-width: 50em; max-width: 50em;
} }
#container.content-width8 { .container.content-width8 {
max-width: 55em; max-width: 55em;
} }
#container.content-width9 { .container.content-width9 {
max-width: 60em; max-width: 60em;
} }
@ -176,3 +166,575 @@ body:not(.loaded) .toolbar:-moz-locale-dir(ltr) {
body:not(.loaded) .toolbar:-moz-locale-dir(rtl) { body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
transform: translateX(100%); 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/aboutCache.css (../../shared/aboutCache.css)
skin/classic/global/aboutCacheEntry.css (../../shared/aboutCacheEntry.css) skin/classic/global/aboutCacheEntry.css (../../shared/aboutCacheEntry.css)
skin/classic/global/aboutMemory.css (../../shared/aboutMemory.css) skin/classic/global/aboutMemory.css (../../shared/aboutMemory.css)
skin/classic/global/aboutReader.css (../../shared/aboutReader.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/aboutSupport.css (../../shared/aboutSupport.css) skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css)
skin/classic/global/appPicker.css (../../shared/appPicker.css) skin/classic/global/appPicker.css (../../shared/appPicker.css)
skin/classic/global/config.css (../../shared/config.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/icons/blocked.svg (../../shared/incontent-icons/blocked.svg)
skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css) skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css)
skin/classic/global/narrate.css (../../shared/narrate.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/arrow.svg (../../shared/narrate/arrow.svg)
skin/classic/global/narrate/back.svg (../../shared/narrate/back.svg) skin/classic/global/narrate/back.svg (../../shared/narrate/back.svg)
skin/classic/global/narrate/fast.svg (../../shared/narrate/fast.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 { .narrating {
position: relative; position: relative;
z-index: 1; z-index: 1;
@ -22,7 +25,7 @@ body.dark .narrating {
z-index: -1; z-index: -1;
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-width: 7px; border-bottom-width: 7px;
transition: left 0.1s ease; transition: left 0.1s ease, width 0.1s ease;
} }
.narrating > .narrate-word-highlight { .narrating > .narrate-word-highlight {
@ -44,3 +47,193 @@ body.sepia .narrate-word-highlight {
body.dark .narrate-word-highlight { body.dark .narrate-word-highlight {
border-bottom-color: #6f6f6f; 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"?> <?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 <!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg version="1.1" <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="42" width="42"
height="16" height="16"
viewBox="0 0 42 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"?> <?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 <!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg version="1.1" <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="44" width="44"
height="16" height="16"
viewBox="0 0 44 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"?> <?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 <!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg version="1.1" <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="38" width="38"
height="14" height="14"
viewBox="0 0 38 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"?> <?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 <!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg version="1.1" <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="38" width="38"
height="24" height="24"
viewBox="0 0 38 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 <!-- 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 - 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/. --> - 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"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24">
<polygon opacity="0.15" points="16.583,0.015 16.569,0 4.583,12 16.569,24 16.583,23.985"/> <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"/>
<polygon fill="#fbfbfb" points="16.575,1.021 16.561,1.008 5.583,12 16.577,23.008 16.591,22.994 "/> <polyline points="16.63 1.51 6.08 12.01 16.63 22.5" fill="#fbfbfb"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 486 B

Before After
Before After