summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--application/basilisk/base/content/tab-content.js6
-rw-r--r--toolkit/components/reader/AboutReader.jsm38
-rw-r--r--toolkit/components/reader/JSDOMParser.js8
-rw-r--r--toolkit/components/reader/Readability-readerable.js104
-rw-r--r--toolkit/components/reader/Readability.js289
-rw-r--r--toolkit/components/reader/ReaderMode.jsm130
-rw-r--r--toolkit/components/reader/Readerable.js96
-rw-r--r--toolkit/components/reader/Readerable.jsm10
-rw-r--r--toolkit/components/reader/moz.build4
-rw-r--r--toolkit/locales/en-US/chrome/global/aboutReader.properties11
-rw-r--r--toolkit/themes/shared/aboutReader.css49
11 files changed, 415 insertions, 330 deletions
diff --git a/application/basilisk/base/content/tab-content.js b/application/basilisk/base/content/tab-content.js
index 35ef8ceb2..fec13eba7 100644
--- a/application/basilisk/base/content/tab-content.js
+++ b/application/basilisk/base/content/tab-content.js
@@ -20,6 +20,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
"resource://gre/modules/AboutReader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
"resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Readerable",
+ "resource://gre/modules/Readerable.jsm");
XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() {
let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery;
// Register targets
@@ -336,7 +338,7 @@ var AboutReaderListener = {
* painted is not going to work.
*/
updateReaderButton: function(forceNonArticle) {
- if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
+ if (!Readerable.isEnabledForParseOnLoad || this.isAboutReader ||
!content || !(content.document instanceof content.HTMLDocument) ||
content.document.mozSyntheticDocument) {
return;
@@ -375,7 +377,7 @@ var AboutReaderListener = {
this.cancelPotentialPendingReadabilityCheck();
// Only send updates when there are articles; there's no point updating with
// |false| all the time.
- if (ReaderMode.isProbablyReaderable(content.document)) {
+ if (Readerable.isProbablyReaderable(content.document)) {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
} else if (forceNonArticle) {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
diff --git a/toolkit/components/reader/AboutReader.jsm b/toolkit/components/reader/AboutReader.jsm
index 6ec959eba..9d9362a0c 100644
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -58,6 +58,7 @@ var AboutReader = function(win, articlePromise) {
this._scrollOffset = win.pageYOffset;
+ doc.addEventListener("mousedown", this);
doc.addEventListener("click", this);
win.addEventListener("pagehide", this);
@@ -119,6 +120,25 @@ var AboutReader = function(win, articlePromise) {
}
this._loadArticle();
+
+ let dropdown = this._toolbarElement;
+
+ let elemL10nMap = {
+ ".minus-button": "minus",
+ ".plus-button": "plus",
+ ".content-width-minus-button": "contentwidthminus",
+ ".content-width-plus-button": "contentwidthplus",
+ ".line-height-minus-button": "lineheightminus",
+ ".line-height-plus-button": "lineheightplus",
+ ".light-button": "colorschemelight",
+ ".dark-button": "colorschemedark",
+ ".sepia-button": "colorschemesepia",
+ };
+
+ for (let [selector, stringID] of Object.entries(elemL10nMap)) {
+ dropdown.querySelector(selector).setAttribute("title",
+ gStrings.GetStringFromName("aboutReader.toolbar." + stringID));
+ }
};
AboutReader.prototype = {
@@ -191,13 +211,16 @@ AboutReader.prototype = {
if (!aEvent.isTrusted)
return;
+ let target = aEvent.target;
switch (aEvent.type) {
+ case "mousedown":
+ if (!target.closest(".dropdown-popup")) {
+ this._closeDropdowns();
+ }
+ break;
case "click":
- let target = aEvent.target;
if (target.classList.contains("dropdown-toggle")) {
this._toggleDropdownClicked(aEvent);
- } else if (!target.closest(".dropdown-popup")) {
- this._closeDropdowns();
}
break;
case "scroll":
@@ -276,13 +299,10 @@ AboutReader.prototype = {
},
_setFontSize(newFontSize) {
- let containerClasses = this._containerElement.classList;
-
- if (this._fontSize > 0)
- containerClasses.remove("font-size" + this._fontSize);
-
this._fontSize = newFontSize;
- containerClasses.add("font-size" + this._fontSize);
+ let size = (10 + 2 * this._fontSize) + "px";
+
+ this._containerElement.style.setProperty("--font-size", size);
return AsyncPrefs.set("reader.font_size", this._fontSize);
},
diff --git a/toolkit/components/reader/JSDOMParser.js b/toolkit/components/reader/JSDOMParser.js
index debdb08eb..ab2f503e1 100644
--- a/toolkit/components/reader/JSDOMParser.js
+++ b/toolkit/components/reader/JSDOMParser.js
@@ -691,7 +691,7 @@
// the attribute value will be HTML escaped.
var val = attr.value;
var quote = (val.indexOf('"') === -1 ? '"' : "'");
- arr.push(" " + attr.name + '=' + quote + val + quote);
+ arr.push(" " + attr.name + "=" + quote + val + quote);
}
if (child.localName in voidElems && !child.childNodes.length) {
@@ -970,7 +970,7 @@
strBuf.push(c);
c = this.nextChar();
}
- var tag = strBuf.join('');
+ var tag = strBuf.join("");
if (!tag)
return false;
@@ -981,7 +981,9 @@
while (c !== "/" && c !== ">") {
if (c === undefined)
return false;
- while (whitespace.indexOf(this.html[this.currentChar++]) != -1);
+ while (whitespace.indexOf(this.html[this.currentChar++]) != -1) {
+ // Advance cursor to first non-whitespace char.
+ }
this.currentChar--;
c = this.nextChar();
if (c !== "/" && c !== ">") {
diff --git a/toolkit/components/reader/Readability-readerable.js b/toolkit/components/reader/Readability-readerable.js
new file mode 100644
index 000000000..d0e1b8164
--- /dev/null
+++ b/toolkit/components/reader/Readability-readerable.js
@@ -0,0 +1,104 @@
+/* eslint-env es6:false */
+/* globals exports */
+/*
+ * DO NOT MODIFY THIS FILE DIRECTLY!
+ *
+ * This is a shared library that is maintained in an external repo:
+ * https://github.com/mozilla/readability
+ */
+
+/*
+ * Copyright (c) 2010 Arc90 Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This code is heavily based on Arc90's readability.js (1.7.1) script
+ * available at: http://code.google.com/p/arc90labs-readability
+ */
+
+var REGEXPS = {
+ // NOTE: These two regular expressions are duplicated in
+ // Readability.js. Please keep both copies in sync.
+ unlikelyCandidates: /-ad-|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
+ okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
+};
+
+function isNodeVisible(node) {
+ // Have to null-check node.style to deal with SVG and MathML nodes.
+ return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden");
+}
+
+/**
+ * Decides whether or not the document is reader-able without parsing the whole thing.
+ *
+ * @return boolean Whether or not we suspect Readability.parse() will suceeed at returning an article object.
+ */
+function isProbablyReaderable(doc, isVisible) {
+ if (!isVisible) {
+ isVisible = isNodeVisible;
+ }
+
+ var nodes = doc.querySelectorAll("p, pre");
+
+ // Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
+ // Some articles' DOM structures might look like
+ // <div>
+ // Sentences<br>
+ // <br>
+ // Sentences<br>
+ // </div>
+ var brNodes = doc.querySelectorAll("div > br");
+ if (brNodes.length) {
+ var set = new Set(nodes);
+ [].forEach.call(brNodes, function(node) {
+ set.add(node.parentNode);
+ });
+ nodes = Array.from(set);
+ }
+
+ var score = 0;
+ // This is a little cheeky, we use the accumulator 'score' to decide what to return from
+ // this callback:
+ return [].some.call(nodes, function(node) {
+ if (!isVisible(node))
+ return false;
+
+ var matchString = node.className + " " + node.id;
+ if (REGEXPS.unlikelyCandidates.test(matchString) &&
+ !REGEXPS.okMaybeItsACandidate.test(matchString)) {
+ return false;
+ }
+
+ if (node.matches("li p")) {
+ return false;
+ }
+
+ var textContentLength = node.textContent.trim().length;
+ if (textContentLength < 140) {
+ return false;
+ }
+
+ score += Math.sqrt(textContentLength - 140);
+
+ if (score > 20) {
+ return true;
+ }
+ return false;
+ });
+}
+
+if (typeof exports === "object") {
+ exports.isProbablyReaderable = isProbablyReaderable;
+}
diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js
index c2bba0cd3..69fb53f86 100644
--- a/toolkit/components/reader/Readability.js
+++ b/toolkit/components/reader/Readability.js
@@ -46,6 +46,7 @@ function Readability(doc, options) {
this._articleTitle = null;
this._articleByline = null;
this._articleDir = null;
+ this._articleSiteName = null;
this._attempts = [];
// Configurable options
@@ -118,15 +119,18 @@ Readability.prototype = {
// All of the regular expressions in use within readability.
// Defined up here so we don't instantiate them repeatedly in loops.
REGEXPS: {
+ // NOTE: These two regular expressions are duplicated in
+ // Readability-readerable.js. Please keep both copies in sync.
unlikelyCandidates: /-ad-|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
+
positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
byline: /byline|author|dateline|writtenby|p-author/i,
replaceFonts: /<(\/?)font[^>]*>/gi,
normalize: /\s{2,}/g,
- videos: /\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
+ videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
prevLink: /(prev|earl|old|new|<|«)/i,
whitespace: /^\s*$/,
@@ -267,7 +271,7 @@ Readability.prototype = {
_getAllNodesWithTag: function(node, tagNames) {
if (node.querySelectorAll) {
- return node.querySelectorAll(tagNames.join(','));
+ return node.querySelectorAll(tagNames.join(","));
}
return [].concat.apply([], tagNames.map(function(tag) {
var collection = node.getElementsByTagName(tag);
@@ -327,7 +331,7 @@ Readability.prototype = {
return uri;
}
- var links = articleContent.getElementsByTagName("a");
+ var links = this._getAllNodesWithTag(articleContent, ["a"]);
this._forEachNode(links, function(link) {
var href = link.getAttribute("href");
if (href) {
@@ -342,7 +346,7 @@ Readability.prototype = {
}
});
- var imgs = articleContent.getElementsByTagName("img");
+ var imgs = this._getAllNodesWithTag(articleContent, ["img"]);
this._forEachNode(imgs, function(img) {
var src = img.getAttribute("src");
if (src) {
@@ -366,7 +370,7 @@ Readability.prototype = {
// If they had an element with id "title" in their HTML
if (typeof curTitle !== "string")
- curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]);
+ curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]);
} catch (e) {/* ignore exceptions setting the title. */}
var titleHadHierarchicalSeparators = false;
@@ -377,18 +381,18 @@ Readability.prototype = {
// If there's a separator in the title, first remove the final part
if ((/ [\|\-\\\/>»] /).test(curTitle)) {
titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
- curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1');
+ curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1");
// If the resulting title is too short (3 words or fewer), remove
// the first part instead:
if (wordCount(curTitle) < 3)
- curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1');
- } else if (curTitle.indexOf(': ') !== -1) {
+ curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, "$1");
+ } else if (curTitle.indexOf(": ") !== -1) {
// Check if we have an heading containing this exact string, so we
// could assume it's the full title.
var headings = this._concatNodeLists(
- doc.getElementsByTagName('h1'),
- doc.getElementsByTagName('h2')
+ doc.getElementsByTagName("h1"),
+ doc.getElementsByTagName("h2")
);
var trimmedTitle = curTitle.trim();
var match = this._someNode(headings, function(heading) {
@@ -397,25 +401,25 @@ Readability.prototype = {
// If we don't, let's extract the title out of the original title string.
if (!match) {
- curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);
+ curTitle = origTitle.substring(origTitle.lastIndexOf(":") + 1);
// If the title is now too short, try the first colon instead:
if (wordCount(curTitle) < 3) {
- curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
+ curTitle = origTitle.substring(origTitle.indexOf(":") + 1);
// But if we have too many words before the colon there's something weird
// with the titles and the H tags so let's just use the original title instead
- } else if (wordCount(origTitle.substr(0, origTitle.indexOf(':'))) > 5) {
+ } else if (wordCount(origTitle.substr(0, origTitle.indexOf(":"))) > 5) {
curTitle = origTitle;
}
}
} else if (curTitle.length > 150 || curTitle.length < 15) {
- var hOnes = doc.getElementsByTagName('h1');
+ var hOnes = doc.getElementsByTagName("h1");
if (hOnes.length === 1)
curTitle = this._getInnerText(hOnes[0]);
}
- curTitle = curTitle.trim();
+ curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " ");
// If we now have 4 words or fewer as our title, and either no
// 'hierarchical' separators (\, /, > or ») were found in the original
// title or we decreased the number of words by more than 1 word, use
@@ -505,7 +509,8 @@ Readability.prototype = {
break;
}
- if (!this._isPhrasingContent(next)) break;
+ if (!this._isPhrasingContent(next))
+ break;
// Otherwise, make this node a child of the new <p>.
var sibling = next.nextSibling;
@@ -513,9 +518,12 @@ Readability.prototype = {
next = sibling;
}
- while (p.lastChild && this._isWhitespace(p.lastChild)) p.removeChild(p.lastChild);
+ while (p.lastChild && this._isWhitespace(p.lastChild)) {
+ p.removeChild(p.lastChild);
+ }
- if (p.parentNode.tagName === "P") this._setNodeTag(p.parentNode, "DIV");
+ if (p.parentNode.tagName === "P")
+ this._setNodeTag(p.parentNode, "DIV");
}
});
},
@@ -576,7 +584,7 @@ Readability.prototype = {
// If there is only one h2 and its text content substantially equals article title,
// they are probably using it as a header and not a subheader,
// so remove it since we already extract the title separately.
- var h2 = articleContent.getElementsByTagName('h2');
+ var h2 = articleContent.getElementsByTagName("h2");
if (h2.length === 1) {
var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length;
if (Math.abs(lengthSimilarRate) < 0.5) {
@@ -606,12 +614,12 @@ Readability.prototype = {
this._cleanConditionally(articleContent, "div");
// Remove extra paragraphs
- this._removeNodes(articleContent.getElementsByTagName('p'), function (paragraph) {
- var imgCount = paragraph.getElementsByTagName('img').length;
- var embedCount = paragraph.getElementsByTagName('embed').length;
- var objectCount = paragraph.getElementsByTagName('object').length;
+ this._removeNodes(articleContent.getElementsByTagName("p"), function (paragraph) {
+ var imgCount = paragraph.getElementsByTagName("img").length;
+ var embedCount = paragraph.getElementsByTagName("embed").length;
+ var objectCount = paragraph.getElementsByTagName("object").length;
// At this point, nasty iframes have been removed, only remain embedded video ones.
- var iframeCount = paragraph.getElementsByTagName('iframe').length;
+ var iframeCount = paragraph.getElementsByTagName("iframe").length;
var totalCount = imgCount + embedCount + objectCount + iframeCount;
return totalCount === 0 && !this._getInnerText(paragraph, false);
@@ -648,34 +656,34 @@ Readability.prototype = {
node.readability = {"contentScore": 0};
switch (node.tagName) {
- case 'DIV':
+ case "DIV":
node.readability.contentScore += 5;
break;
- case 'PRE':
- case 'TD':
- case 'BLOCKQUOTE':
+ case "PRE":
+ case "TD":
+ case "BLOCKQUOTE":
node.readability.contentScore += 3;
break;
- case 'ADDRESS':
- case 'OL':
- case 'UL':
- case 'DL':
- case 'DD':
- case 'DT':
- case 'LI':
- case 'FORM':
+ case "ADDRESS":
+ case "OL":
+ case "UL":
+ case "DL":
+ case "DD":
+ case "DT":
+ case "LI":
+ case "FORM":
node.readability.contentScore -= 3;
break;
- case 'H1':
- case 'H2':
- case 'H3':
- case 'H4':
- case 'H5':
- case 'H6':
- case 'TH':
+ case "H1":
+ case "H2":
+ case "H3":
+ case "H4":
+ case "H5":
+ case "H6":
+ case "TH":
node.readability.contentScore -= 5;
break;
}
@@ -824,12 +832,14 @@ Readability.prototype = {
if (p !== null) {
p.appendChild(childNode);
} else if (!this._isWhitespace(childNode)) {
- p = doc.createElement('p');
+ p = doc.createElement("p");
node.replaceChild(p, childNode);
p.appendChild(childNode);
}
} else if (p !== null) {
- while (p.lastChild && this._isWhitespace(p.lastChild)) p.removeChild(p.lastChild);
+ while (p.lastChild && this._isWhitespace(p.lastChild)) {
+ p.removeChild(p.lastChild);
+ }
p = null;
}
childNode = nextSibling;
@@ -860,7 +870,7 @@ Readability.prototype = {
**/
var candidates = [];
this._forEachNode(elementsToScore, function(elementToScore) {
- if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === 'undefined')
+ if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined")
return;
// If this paragraph is less than 25 characters, don't even count it.
@@ -879,17 +889,17 @@ Readability.prototype = {
contentScore += 1;
// Add points for any commas within this paragraph.
- contentScore += innerText.split(',').length;
+ contentScore += innerText.split(",").length;
// For every 100 characters in this paragraph, add another point. Up to 3 points.
contentScore += Math.min(Math.floor(innerText.length / 100), 3);
// Initialize and score ancestors.
this._forEachNode(ancestors, function(ancestor, level) {
- if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === 'undefined')
+ if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined")
return;
- if (typeof(ancestor.readability) === 'undefined') {
+ if (typeof(ancestor.readability) === "undefined") {
this._initializeNode(ancestor);
candidates.push(ancestor);
}
@@ -920,7 +930,7 @@ Readability.prototype = {
var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate));
candidate.readability.contentScore = candidateScore;
- this.log('Candidate:', candidate, "with score " + candidateScore);
+ this.log("Candidate:", candidate, "with score " + candidateScore);
for (var t = 0; t < this._nbTopCandidates; t++) {
var aTopCandidate = topCandidates[t];
@@ -1039,8 +1049,8 @@ Readability.prototype = {
var sibling = siblings[s];
var append = false;
- this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : '');
- this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : 'Unknown');
+ this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : "");
+ this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown");
if (sibling === topCandidate) {
append = true;
@@ -1074,7 +1084,7 @@ Readability.prototype = {
if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) {
// We have a node that isn't a common block level element, like a form or td tag.
// Turn it into a div so it doesn't get filtered out later by accident.
- this.log("Altering sibling:", sibling, 'to div.');
+ this.log("Altering sibling:", sibling, "to div.");
sibling = this._setNodeTag(sibling, "DIV");
}
@@ -1142,7 +1152,7 @@ Readability.prototype = {
this._attempts.push({articleContent: articleContent, textLength: textLength});
// No luck after removing flags, just return the longest text we found during the different loops
this._attempts.sort(function (a, b) {
- return a.textLength < b.textLength;
+ return b.textLength - a.textLength;
});
// But first check if we actually have something
@@ -1182,7 +1192,7 @@ Readability.prototype = {
* @return Boolean - whether the input string is a byline.
*/
_isValidByline: function(byline) {
- if (typeof byline == 'string' || byline instanceof String) {
+ if (typeof byline == "string" || byline instanceof String) {
byline = byline.trim();
return (byline.length > 0) && (byline.length < 100);
}
@@ -1199,62 +1209,73 @@ Readability.prototype = {
var values = {};
var metaElements = this._doc.getElementsByTagName("meta");
- // Match "description", or Twitter's "twitter:description" (Cards)
- // in name attribute.
- var namePattern = /^\s*((twitter)\s*:\s*)?(description|title)\s*$/gi;
+ // property is a space-separated list of values
+ var propertyPattern = /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi;
- // Match Facebook's Open Graph title & description properties.
- var propertyPattern = /^\s*og\s*:\s*(description|title)\s*$/gi;
+ // name is a single value
+ var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;
// Find description tags.
this._forEachNode(metaElements, function(element) {
var elementName = element.getAttribute("name");
var elementProperty = element.getAttribute("property");
+ var content = element.getAttribute("content");
+ var matches = null;
+ var name = null;
- if ([elementName, elementProperty].indexOf("author") !== -1) {
- metadata.byline = element.getAttribute("content");
- return;
+ if (elementProperty) {
+ matches = elementProperty.match(propertyPattern);
+ if (matches) {
+ for (var i = matches.length - 1; i >= 0; i--) {
+ // Convert to lowercase, and remove any whitespace
+ // so we can match below.
+ name = matches[i].toLowerCase().replace(/\s/g, "");
+ // multiple authors
+ values[name] = content.trim();
+ }
+ }
}
-
- var name = null;
- if (namePattern.test(elementName)) {
+ if (!matches && elementName && namePattern.test(elementName)) {
name = elementName;
- } else if (propertyPattern.test(elementProperty)) {
- name = elementProperty;
- }
-
- if (name) {
- var content = element.getAttribute("content");
if (content) {
- // Convert to lowercase and remove any whitespace
- // so we can match below.
- name = name.toLowerCase().replace(/\s/g, '');
+ // Convert to lowercase, remove any whitespace, and convert dots
+ // to colons so we can match below.
+ name = name.toLowerCase().replace(/\s/g, "").replace(/\./g, ":");
values[name] = content.trim();
}
}
});
- if ("description" in values) {
- metadata.excerpt = values["description"];
- } else if ("og:description" in values) {
- // Use facebook open graph description.
- metadata.excerpt = values["og:description"];
- } else if ("twitter:description" in values) {
- // Use twitter cards description.
- metadata.excerpt = values["twitter:description"];
- }
+ // get title
+ metadata.title = values["dc:title"] ||
+ values["dcterm:title"] ||
+ values["og:title"] ||
+ values["weibo:article:title"] ||
+ values["weibo:webpage:title"] ||
+ values["title"] ||
+ values["twitter:title"];
- metadata.title = this._getArticleTitle();
if (!metadata.title) {
- if ("og:title" in values) {
- // Use facebook open graph title.
- metadata.title = values["og:title"];
- } else if ("twitter:title" in values) {
- // Use twitter cards title.
- metadata.title = values["twitter:title"];
- }
+ metadata.title = this._getArticleTitle();
}
+ // get author
+ metadata.byline = values["dc:creator"] ||
+ values["dcterm:creator"] ||
+ values["author"];
+
+ // get description
+ metadata.excerpt = values["dc:description"] ||
+ values["dcterm:description"] ||
+ values["og:description"] ||
+ values["weibo:article:description"] ||
+ values["weibo:webpage:description"] ||
+ values["description"] ||
+ values["twitter:description"];
+
+ // get site name
+ metadata.siteName = values["og:site_name"];
+
return metadata;
},
@@ -1264,12 +1285,12 @@ Readability.prototype = {
* @param Element
**/
_removeScripts: function(doc) {
- this._removeNodes(doc.getElementsByTagName('script'), function(scriptNode) {
+ this._removeNodes(doc.getElementsByTagName("script"), function(scriptNode) {
scriptNode.nodeValue = "";
- scriptNode.removeAttribute('src');
+ scriptNode.removeAttribute("src");
return true;
});
- this._removeNodes(doc.getElementsByTagName('noscript'));
+ this._removeNodes(doc.getElementsByTagName("noscript"));
},
/**
@@ -1336,7 +1357,7 @@ Readability.prototype = {
* @return string
**/
_getInnerText: function(e, normalizeSpaces) {
- normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
+ normalizeSpaces = (typeof normalizeSpaces === "undefined") ? true : normalizeSpaces;
var textContent = e.textContent.trim();
if (normalizeSpaces) {
@@ -1365,7 +1386,7 @@ Readability.prototype = {
* @return void
**/
_cleanStyles: function(e) {
- if (!e || e.tagName.toLowerCase() === 'svg')
+ if (!e || e.tagName.toLowerCase() === "svg")
return;
// Remove `style` and deprecated presentational attributes
@@ -1374,8 +1395,8 @@ Readability.prototype = {
}
if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
- e.removeAttribute('width');
- e.removeAttribute('height');
+ e.removeAttribute("width");
+ e.removeAttribute("height");
}
var cur = e.firstElementChild;
@@ -1421,7 +1442,7 @@ Readability.prototype = {
var weight = 0;
// Look for a special classname
- if (typeof(e.className) === 'string' && e.className !== '') {
+ if (typeof(e.className) === "string" && e.className !== "") {
if (this.REGEXPS.negative.test(e.className))
weight -= 25;
@@ -1430,7 +1451,7 @@ Readability.prototype = {
}
// Look for a special ID
- if (typeof(e.id) === 'string' && e.id !== '') {
+ if (typeof(e.id) === "string" && e.id !== "") {
if (this.REGEXPS.negative.test(e.id))
weight -= 25;
@@ -1619,7 +1640,7 @@ Readability.prototype = {
return true;
}
- if (this._getCharCount(node, ',') < 10) {
+ if (this._getCharCount(node, ",") < 10) {
// If there are not very many commas, and the number of
// non-paragraph elements is more than paragraphs or other
// ominous signs, remove the element.
@@ -1679,7 +1700,7 @@ Readability.prototype = {
**/
_cleanHeaders: function(e) {
for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) {
- this._removeNodes(e.getElementsByTagName('h' + headerIndex), function (header) {
+ this._removeNodes(e.getElementsByTagName("h" + headerIndex), function (header) {
return this._getClassWeight(header) < 0;
});
}
@@ -1694,66 +1715,7 @@ Readability.prototype = {
},
_isProbablyVisible: function(node) {
- return node.style.display != "none" && !node.hasAttribute("hidden");
- },
-
- /**
- * Decides whether or not the document is reader-able without parsing the whole thing.
- *
- * @return boolean Whether or not we suspect parse() will suceeed at returning an article object.
- */
- isProbablyReaderable: function(helperIsVisible) {
- var nodes = this._getAllNodesWithTag(this._doc, ["p", "pre"]);
-
- // Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
- // Some articles' DOM structures might look like
- // <div>
- // Sentences<br>
- // <br>
- // Sentences<br>
- // </div>
- var brNodes = this._getAllNodesWithTag(this._doc, ["div > br"]);
- if (brNodes.length) {
- var set = new Set();
- [].forEach.call(brNodes, function(node) {
- set.add(node.parentNode);
- });
- nodes = [].concat.apply(Array.from(set), nodes);
- }
-
- if (!helperIsVisible) {
- helperIsVisible = this._isProbablyVisible;
- }
-
- var score = 0;
- // This is a little cheeky, we use the accumulator 'score' to decide what to return from
- // this callback:
- return this._someNode(nodes, function(node) {
- if (helperIsVisible && !helperIsVisible(node))
- return false;
- var matchString = node.className + " " + node.id;
-
- if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
- !this.REGEXPS.okMaybeItsACandidate.test(matchString)) {
- return false;
- }
-
- if (node.matches && node.matches("li p")) {
- return false;
- }
-
- var textContentLength = node.textContent.trim().length;
- if (textContentLength < 140) {
- return false;
- }
-
- score += Math.sqrt(textContentLength - 140);
-
- if (score > 20) {
- return true;
- }
- return false;
- });
+ return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden");
},
/**
@@ -1812,6 +1774,7 @@ Readability.prototype = {
textContent: textContent,
length: textContent.length,
excerpt: metadata.excerpt,
+ siteName: metadata.siteName || this._articleSiteName
};
}
};
diff --git a/toolkit/components/reader/ReaderMode.jsm b/toolkit/components/reader/ReaderMode.jsm
index 218e12d60..5ba898aec 100644
--- a/toolkit/components/reader/ReaderMode.jsm
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -12,6 +12,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
// names so that rules in aboutReader.css can match them.
const CLASSES_TO_PRESERVE = [
"caption",
+ "emoji",
"hidden",
"invisble",
"sr-only",
@@ -19,6 +20,7 @@ const CLASSES_TO_PRESERVE = [
"visuallyhidden",
"wp-caption",
"wp-caption-text",
+ "wp-smiley",
];
Cu.import("resource://gre/modules/Services.jsm");
@@ -30,13 +32,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-comm
XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "Readability", function() {
- let scope = {};
- scope.dump = this.dump;
- Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
- return scope.Readability;
-});
+XPCOMUtils.defineLazyModuleGetter(this, "Readerable", "resource://gre/modules/Readerable.jsm");
this.ReaderMode = {
// Version of the cache schema.
@@ -44,42 +40,6 @@ this.ReaderMode = {
DEBUG: 0,
- // Don't try to parse the page if it has too many elements (for memory and
- // performance reasons)
- get maxElemsToParse() {
- delete this.parseNodeLimit;
-
- Services.prefs.addObserver("reader.parse-node-limit", this, false);
- return this.parseNodeLimit = Services.prefs.getIntPref("reader.parse-node-limit");
- },
-
- get isEnabledForParseOnLoad() {
- delete this.isEnabledForParseOnLoad;
-
- // Listen for future pref changes.
- Services.prefs.addObserver("reader.parse-on-load.", this, false);
-
- return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
- },
-
- _getStateForParseOnLoad() {
- let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
- let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
- return isForceEnabled || isEnabled;
- },
-
- observe(aMessage, aTopic, aData) {
- switch (aTopic) {
- case "nsPref:changed":
- if (aData.startsWith("reader.parse-on-load.")) {
- this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
- } else if (aData === "reader.parse-node-limit") {
- this.parseNodeLimit = Services.prefs.getIntPref(aData);
- }
- break;
- }
- },
-
/**
* Enter the reader mode by going forward one step in history if applicable,
* if not, append the about:reader page in the history instead.
@@ -175,39 +135,6 @@ this.ReaderMode = {
},
/**
- * Decides whether or not a document is reader-able without parsing the whole thing.
- *
- * @param doc A document to parse.
- * @return boolean Whether or not we should show the reader mode button.
- */
- isProbablyReaderable(doc) {
- // Only care about 'real' HTML documents:
- if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
- return false;
- }
-
- let uri = Services.io.newURI(doc.location.href);
- if (!this._shouldCheckUri(uri)) {
- return false;
- }
-
- let utils = this.getUtilsForWin(doc.defaultView);
- // We pass in a helper function to determine if a node is visible, because
- // it uses gecko APIs that the engine-agnostic readability code can't rely
- // upon.
- return new Readability(doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
- },
-
- isNodeVisible(utils, node) {
- let bounds = utils.getBoundsWithoutFlushing(node);
- return bounds.height > 0 && bounds.width > 0;
- },
-
- getUtilsForWin(win) {
- return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
- },
-
- /**
* Gets an article from a loaded browser's document. This method will not attempt
* to parse certain URIs (e.g. about: URIs).
*
@@ -216,7 +143,8 @@ this.ReaderMode = {
* @resolves JS object representing the article, or null if no article is found.
*/
parseDocument(doc) {
- if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
+ if (!Readerable.shouldCheckUri(doc.documentURIObject) ||
+ !Readerable.shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI");
return null;
}
@@ -236,7 +164,8 @@ this.ReaderMode = {
if (!doc) {
return null;
}
- if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
+ if (!Readerable.shouldCheckUri(doc.documentURIObject) ||
+ !Readerable.shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI");
return null;
}
@@ -246,7 +175,7 @@ this.ReaderMode = {
_downloadDocument(url) {
try {
- if (!this._shouldCheckUri(Services.io.newURI(url))) {
+ if (!Readerable.shouldCheckUri(Services.io.newURI(url))) {
return null;
}
} catch (ex) {
@@ -388,44 +317,6 @@ this.ReaderMode = {
dump("Reader: " + msg);
},
- _blockedHosts: [
- "amazon.com",
- "basilisk-browser.org",
- "github.com",
- "mail.google.com",
- "palemoon.org",
- "pinterest.com",
- "reddit.com",
- "twitter.com",
- "youtube.com",
- ],
-
- _shouldCheckUri(uri, isBaseUri = false) {
- if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
- this.log("Not parsing URI scheme: " + uri.scheme);
- return false;
- }
-
- try {
- uri.QueryInterface(Ci.nsIURL);
- } catch (ex) {
- // If this doesn't work, presumably the URL is not well-formed or something
- return false;
- }
- // Sadly, some high-profile pages have false positives, so bail early for those:
- let asciiHost = uri.asciiHost;
- if (!isBaseUri && this._blockedHosts.some(blockedHost => asciiHost.endsWith(blockedHost))) {
- return false;
- }
-
- if (!isBaseUri && (!uri.filePath || uri.filePath == "/")) {
- this.log("Not parsing home page: " + uri.spec);
- return false;
- }
-
- return true;
- },
-
/**
* Attempts to parse a document into an article. Heavy lifting happens
* in readerWorker.js.
@@ -650,3 +541,8 @@ this.ReaderMode = {
return readingSpeed.get(lang) || readingSpeed.get("en");
},
};
+
+// Don't try to parse the page if it has too many elements (for memory and
+// performance reasons)
+XPCOMUtils.defineLazyPreferenceGetter(
+ ReaderMode, "parseNodeLimit", "reader.parse-node-limit", 0);
diff --git a/toolkit/components/reader/Readerable.js b/toolkit/components/reader/Readerable.js
new file mode 100644
index 000000000..cee8adc08
--- /dev/null
+++ b/toolkit/components/reader/Readerable.js
@@ -0,0 +1,96 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// This file and Readability-readerable.js are merged together into
+// Readerable.jsm.
+
+/* exported Readerable */
+/* import-globals-from Readability-readerable.js */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function isNodeVisible(node) {
+ return node.clientHeight > 0 && node.clientWidth > 0;
+}
+
+var Readerable = {
+ DEBUG: 0,
+
+ get isEnabledForParseOnLoad() {
+ return this.isEnabled || this.isForceEnabled;
+ },
+
+ log(msg) {
+ if (this.DEBUG)
+ dump("Reader: " + msg);
+ },
+
+ /**
+ * Decides whether or not a document is reader-able without parsing the whole thing.
+ *
+ * @param doc A document to parse.
+ * @return boolean Whether or not we should show the reader mode button.
+ */
+ isProbablyReaderable(doc) {
+ // Only care about 'real' HTML documents:
+ if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
+ return false;
+ }
+
+ let uri = Services.io.newURI(doc.location.href);
+ if (!this.shouldCheckUri(uri)) {
+ return false;
+ }
+
+ return isProbablyReaderable(doc, isNodeVisible);
+ },
+
+ _blockedHosts: [
+ "amazon.com",
+ "basilisk-browser.org",
+ "github.com",
+ "mail.google.com",
+ "palemoon.org",
+ "pinterest.com",
+ "reddit.com",
+ "twitter.com",
+ "youtube.com",
+ ],
+
+ shouldCheckUri(uri, isBaseUri = false) {
+ if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
+ this.log("Not parsing URI scheme: " + uri.scheme);
+ return false;
+ }
+
+ try {
+ uri.QueryInterface(Ci.nsIURL);
+ } catch (ex) {
+ // If this doesn't work, presumably the URL is not well-formed or something
+ return false;
+ }
+ // Sadly, some high-profile pages have false positives, so bail early for those:
+ let asciiHost = uri.asciiHost;
+ if (!isBaseUri && this._blockedHosts.some(blockedHost => asciiHost.endsWith(blockedHost))) {
+ return false;
+ }
+
+ if (!isBaseUri && (!uri.filePath || uri.filePath == "/")) {
+ this.log("Not parsing home page: " + uri.spec);
+ return false;
+ }
+
+ return true;
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ Readerable, "isEnabled", "reader.parse-on-load.enabled", true);
+XPCOMUtils.defineLazyPreferenceGetter(
+ Readerable, "isForceEnabled", "reader.parse-on-load.force-enabled", false);
diff --git a/toolkit/components/reader/Readerable.jsm b/toolkit/components/reader/Readerable.jsm
new file mode 100644
index 000000000..2268487e4
--- /dev/null
+++ b/toolkit/components/reader/Readerable.jsm
@@ -0,0 +1,10 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Readerable"];
+
+#include Readability-readerable.js
+#include Readerable.js
diff --git a/toolkit/components/reader/moz.build b/toolkit/components/reader/moz.build
index 6863d6542..d49bda14f 100644
--- a/toolkit/components/reader/moz.build
+++ b/toolkit/components/reader/moz.build
@@ -11,6 +11,10 @@ EXTRA_JS_MODULES += [
'ReaderMode.jsm'
]
+EXTRA_PP_JS_MODULES += [
+ 'Readerable.jsm'
+]
+
EXTRA_JS_MODULES.reader = [
'JSDOMParser.js',
'Readability.js',
diff --git a/toolkit/locales/en-US/chrome/global/aboutReader.properties b/toolkit/locales/en-US/chrome/global/aboutReader.properties
index ac004c545..10d145686 100644
--- a/toolkit/locales/en-US/chrome/global/aboutReader.properties
+++ b/toolkit/locales/en-US/chrome/global/aboutReader.properties
@@ -46,3 +46,14 @@ readerView.enter=Enter Reader View
readerView.enter.accesskey=R
readerView.close=Close Reader View
readerView.close.accesskey=R
+
+# These are used as tooltips in Type Control
+aboutReader.toolbar.minus = Decrease Font Size
+aboutReader.toolbar.plus = Increase Font Size
+aboutReader.toolbar.contentwidthminus = Decrease Content Width
+aboutReader.toolbar.contentwidthplus = Increase Content Width
+aboutReader.toolbar.lineheightminus = Decrease Line Height
+aboutReader.toolbar.lineheightplus = Increase Line Height
+aboutReader.toolbar.colorschemelight = Color Scheme Light
+aboutReader.toolbar.colorschemedark = Color Scheme Dark
+aboutReader.toolbar.colorschemesepia = Color Scheme Sepia
diff --git a/toolkit/themes/shared/aboutReader.css b/toolkit/themes/shared/aboutReader.css
index 4dbf11f6d..ff8f27565 100644
--- a/toolkit/themes/shared/aboutReader.css
+++ b/toolkit/themes/shared/aboutReader.css
@@ -47,44 +47,10 @@ body.serif .remove-button {
}
.container {
+ --font-size: 12;
max-width: 30em;
margin: 0 auto;
-}
-
-.container.font-size1 {
- font-size: 12px;
-}
-
-.container.font-size2 {
- font-size: 14px;
-}
-
-.container.font-size3 {
- font-size: 16px;
-}
-
-.container.font-size4 {
- font-size: 18px;
-}
-
-.container.font-size5 {
- font-size: 20px;
-}
-
-.container.font-size6 {
- font-size: 22px;
-}
-
-.container.font-size7 {
- font-size: 24px;
-}
-
-.container.font-size8 {
- font-size: 26px;
-}
-
-.container.font-size9 {
- font-size: 28px;
+ font-size: var(--font-size);
}
.container.content-width1 {
@@ -738,3 +704,14 @@ body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
.moz-reader-content .sr-only {
display: none;
}
+
+/* Enforce wordpress and similar emoji/smileys aren't sized to be full-width */
+.moz-reader-content img.wp-smiley,
+.moz-reader-content img.emoji {
+ display: inline-block;
+ border-width: 0;
+ /* height: auto is implied from `.moz-reader-content *` rule. */
+ width: 1em;
+ margin: 0 .07em;
+ padding: 0;
+}