Merge inbound to mozilla-central r=merge a=merge

This commit is contained in:
shindli 2017-11-12 23:48:36 +02:00
commit 0d157e6e2e
15 changed files with 184 additions and 584 deletions

View File

@ -7326,28 +7326,13 @@ var gIdentityHandler = {
this._identityPopup.hidePopup();
},
toggleSubView(name, anchor) {
let view = this._identityPopupMultiView;
let id = `identity-popup-${name}View`;
let subView = document.getElementById(id);
showSecuritySubView() {
this._identityPopupMultiView.showSubView("identity-popup-securityView",
this._popupExpander);
// Listen for the subview showing or hiding to change
// the tooltip on the expander button.
subView.addEventListener("ViewShowing", this);
subView.addEventListener("ViewHiding", this);
if (view.showingSubView) {
view.showMainView();
} else {
view.showSubView(id, anchor);
}
// If an element is focused that's not the anchor, clear the focus.
// Elements of hidden views have -moz-user-focus:ignore but setting that
// per CSS selector doesn't blur a focused element in those hidden views.
if (Services.focus.focusedElement != anchor) {
Services.focus.clearFocus(window);
}
Services.focus.clearFocus(window);
},
disableMixedContentProtection() {
@ -7680,7 +7665,7 @@ var gIdentityHandler = {
this._identityPopupInsecureLoginFormsLearnMore
.setAttribute("href", baseURL + "insecure-password");
// The expander switches its tooltip when toggled, change it to the default.
// This is in the properties file because the expander used to switch its tooltip.
this._popupExpander.tooltipText = gNavigatorBundle.getString("identity.showDetails.tooltip");
// Determine connection security information.
@ -7910,16 +7895,6 @@ var gIdentityHandler = {
},
handleEvent(event) {
// If the subview is shown or hidden, change the tooltip on the expander button.
if (event.type == "ViewShowing") {
this._popupExpander.tooltipText = gNavigatorBundle.getString("identity.hideDetails.tooltip");
return;
}
if (event.type == "ViewHiding") {
this._popupExpander.tooltipText = gNavigatorBundle.getString("identity.showDetails.tooltip");
return;
}
let elem = document.activeElement;
let position = elem.compareDocumentPosition(this._identityPopup);

View File

@ -12,28 +12,24 @@
add_task(async function() {
await BrowserTestUtils.openNewForegroundTab(gBrowser);
await loadBadCertPage("https://expired.example.com");
checkControlPanelIcons();
let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
.getService(Ci.nsICertOverrideService);
certOverrideService.clearValidityOverride("expired.example.com", -1);
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Check for the correct icons in the identity box and control center.
function checkControlPanelIcons() {
let { gIdentityHandler } = gBrowser.ownerGlobal;
gIdentityHandler._identityBox.click();
let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
document.getElementById("identity-popup-security-expander").click();
await promiseViewShown;
is_element_visible(document.getElementById("connection-icon"), "Should see connection icon");
let connectionIconImage = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("connection-icon"))
.getPropertyValue("list-style-image");
let securityViewBG = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-securityView"))
.getComputedStyle(document.getElementById("identity-popup-securityView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
let securityContentBG = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-security-content"))
.getComputedStyle(document.getElementById("identity-popup-mainView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
is(connectionIconImage,
"url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg\")",
@ -46,5 +42,9 @@ function checkControlPanelIcons() {
"Using expected icon image in the Control Center subview");
gIdentityHandler._identityPopup.hidden = true;
}
let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
.getService(Ci.nsICertOverrideService);
certOverrideService.clearValidityOverride("expired.example.com", -1);
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

View File

@ -51,7 +51,8 @@ const EXPECTED_APPMENU_SUBVIEW_REFLOWS = [
* correct. Unfortunately this requires 2 sync reflows.
*
* If we add more views where this is necessary, we may need to duplicate
* these expected reflows further.
* these expected reflows further. Bug 1392340 is on file to remove the
* reflows completely when opening subviews.
*/
{
stack: [
@ -59,7 +60,16 @@ const EXPECTED_APPMENU_SUBVIEW_REFLOWS = [
"hideAllViewsExcept@resource:///modules/PanelMultiView.jsm",
],
times: 2, // This number should only ever go down - never up.
times: 1, // This number should only ever go down - never up.
},
{
stack: [
"descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
"_transitionViews@resource:///modules/PanelMultiView.jsm",
],
times: 3, // This number should only ever go down - never up.
},
/**

View File

@ -103,11 +103,13 @@ function nextTest() {
gPopupHidden = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
gIdentityHandler._identityBox.click();
info("Waiting for the Control Center to be shown");
popupShown.then(() => {
popupShown.then(async () => {
ok(!is_hidden(gIdentityHandler._identityPopup), "Control Center is visible");
// Show the subview, which is an easy way in automation to reproduce
// Bug 1207542, where the CC wouldn't close on navigation.
let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
gBrowser.ownerDocument.querySelector("#identity-popup-security-expander").click();
await promiseViewShown;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.location);
});
}

View File

@ -38,6 +38,15 @@ add_task(async function test_simple() {
let { gIdentityHandler } = gBrowser.ownerGlobal;
gIdentityHandler._identityBox.click();
// Messages should be visible when the scheme is HTTP, and invisible when
// the scheme is HTTPS.
is(Array.every(document.getElementById("identity-popup-mainView")
.querySelectorAll("[when-loginforms=insecure]"),
element => !is_hidden(element)),
expectWarning,
"The relevant messages should be visible or hidden in the main view.");
let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
document.getElementById("identity-popup-security-expander").click();
await promiseViewShown;
@ -48,10 +57,12 @@ add_task(async function test_simple() {
.getComputedStyle(document.getElementById("connection-icon"))
.getPropertyValue("list-style-image");
let securityViewBG = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-securityView"))
.getComputedStyle(document.getElementById("identity-popup-securityView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
let securityContentBG = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-security-content"))
.getComputedStyle(document.getElementById("identity-popup-mainView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
is(connectionIconImage,
"url(\"chrome://browser/skin/connection-mixed-active-loaded.svg\")",
@ -62,17 +73,19 @@ add_task(async function test_simple() {
is(securityContentBG,
"url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
"Using expected icon image in the Control Center subview");
is(Array.filter(document.querySelectorAll("[observes=identity-popup-insecure-login-forms-learn-more]"),
is(Array.filter(document.getElementById("identity-popup-securityView")
.querySelectorAll("[observes=identity-popup-insecure-login-forms-learn-more]"),
element => !is_hidden(element)).length, 1,
"The 'Learn more' link should be visible once.");
}
// Messages should be visible when the scheme is HTTP, and invisible when
// the scheme is HTTPS.
is(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
is(Array.every(document.getElementById("identity-popup-securityView")
.querySelectorAll("[when-loginforms=insecure]"),
element => !is_hidden(element)),
expectWarning,
"The relevant messages should be visible or hidden.");
"The relevant messages should be visible or hidden in the security view.");
if (gIdentityHandler._identityPopup.state != "closed") {
let hideEvent = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
@ -134,6 +147,12 @@ add_task(async function test_ignoring_window_opener() {
// Open the identity popup.
let { gIdentityHandler } = gBrowser.ownerGlobal;
gIdentityHandler._identityBox.click();
ok(Array.every(document.getElementById("identity-popup-mainView")
.querySelectorAll("[when-loginforms=insecure]"),
element => is_hidden(element)),
"All messages should be hidden in the main view.");
let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
document.getElementById("identity-popup-security-expander").click();
await promiseViewShown;
@ -146,10 +165,12 @@ add_task(async function test_ignoring_window_opener() {
.getComputedStyle(document.getElementById("connection-icon"))
.getPropertyValue("list-style-image");
let securityViewBG = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-securityView"))
.getComputedStyle(document.getElementById("identity-popup-securityView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
let securityContentBG = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-security-content"))
.getComputedStyle(document.getElementById("identity-popup-mainView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
is(connectionIconImage,
"url(\"chrome://browser/skin/connection-secure.svg\")",
@ -161,9 +182,10 @@ add_task(async function test_ignoring_window_opener() {
"url(\"chrome://browser/skin/controlcenter/connection.svg\")",
"Using expected icon image in the Control Center subview");
ok(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
ok(Array.every(document.getElementById("identity-popup-securityView")
.querySelectorAll("[when-loginforms=insecure]"),
element => is_hidden(element)),
"All messages should be hidden.");
"All messages should be hidden in the security view.");
if (gIdentityHandler._identityPopup.state != "closed") {
info("hiding popup");

View File

@ -218,11 +218,14 @@ async function assertMixedContentBlockingState(tabbrowser, states = {}) {
// Make sure the correct icon is visible in the Control Center.
// This logic is controlled with CSS, so this helps prevent regressions there.
let securityView = doc.getElementById("identity-popup-securityView");
let securityViewBG = tabbrowser.ownerGlobal.getComputedStyle(securityView).
getPropertyValue("background-image");
let securityContentBG = tabbrowser.ownerGlobal.getComputedStyle(securityView).
getPropertyValue("background-image");
let securityViewBG = tabbrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-securityView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
let securityContentBG = tabbrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-popup-mainView")
.getElementsByClassName("identity-popup-security-content")[0])
.getPropertyValue("background-image");
if (stateInsecure) {
is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/conn-not-secure.svg\")",
@ -262,7 +265,8 @@ async function assertMixedContentBlockingState(tabbrowser, states = {}) {
let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
doc.getElementById("identity-popup-security-expander").click();
await promiseViewShown;
is(Array.filter(doc.querySelectorAll("[observes=identity-popup-mcb-learn-more]"),
is(Array.filter(doc.getElementById("identity-popup-securityView")
.querySelectorAll("[observes=identity-popup-mcb-learn-more]"),
element => !is_hidden(element)).length, 1,
"The 'Learn more' link should be visible once.");
}

View File

@ -5,6 +5,7 @@
<panel id="identity-popup"
type="arrow"
hidden="true"
photon="true"
role="alertdialog"
onpopupshown="gIdentityHandler.onPopupShown(event);"
onpopuphidden="gIdentityHandler.onPopupHidden(event);"
@ -15,13 +16,13 @@
<broadcaster id="identity-popup-insecure-login-forms-learn-more" class="text-link plain" value="&identity.learnMore;"/>
</broadcasterset>
<panelmultiview id="identity-popup-multiView"
mainViewId="identity-popup-mainView">
<panelview id="identity-popup-mainView" flex="1"
<photonpanelmultiview id="identity-popup-multiView"
mainViewId="identity-popup-mainView">
<panelview id="identity-popup-mainView"
descriptionheightworkaround="true">
<!-- Security Section -->
<hbox id="identity-popup-security" class="identity-popup-section">
<vbox id="identity-popup-security-content" flex="1">
<vbox class="identity-popup-security-content" flex="1">
<label class="plain">
<label class="identity-popup-headline identity-popup-host"></label>
<label class="identity-popup-headline identity-popup-hostless" crop="end"/>
@ -48,7 +49,7 @@
<button id="identity-popup-security-expander"
class="identity-popup-expander"
when-connection="not-secure secure secure-ev secure-cert-user-overridden"
oncommand="gIdentityHandler.toggleSubView('security', this)"/>
oncommand="gIdentityHandler.showSecuritySubView();"/>
</hbox>
<!-- Tracking Protection Section -->
@ -98,8 +99,9 @@
<!-- Security SubView -->
<panelview id="identity-popup-securityView"
title="&identity.securityView.label;"
descriptionheightworkaround="true">
<vbox id="identity-popup-securityView-header">
<vbox class="identity-popup-security-content">
<label class="plain">
<label class="identity-popup-headline identity-popup-host"></label>
<label class="identity-popup-headline identity-popup-hostless" crop="end"/>
@ -182,5 +184,5 @@
</vbox>
</panelview>
</panelmultiview>
</photonpanelmultiview>
</panel>

View File

@ -682,6 +682,10 @@ this.PanelMultiView = class {
this._offscreenViewStack.appendChild(viewNode);
viewNode.setAttribute("in-transition", true);
// Now that the subview is visible, we can check the height of the
// description elements it contains.
this.descriptionHeightWorkaround(viewNode);
viewRect = await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
return this._dwu.getBoundsWithoutFlushing(viewNode);
});

View File

@ -72,7 +72,7 @@
closemenu="none"
tabindex="0"
tooltip="&backCmd.label;"
onclick="document.getBindingParent(this).panelMultiView.goBack(); this.blur()"/>
oncommand="document.getBindingParent(this).panelMultiView.goBack(); this.blur()"/>
<xul:label xbl:inherits="value=title"/>
</xul:box>
<children/>

View File

@ -732,6 +732,10 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY editBookmark.done.label "Done">
<!ENTITY editBookmark.removeBookmark.accessKey "R">
<!-- LOCALIZATION NOTE (identity.securityView.label)
This is the header of the security subview in the Site Identity panel. -->
<!ENTITY identity.securityView.label "Site Security">
<!ENTITY identity.connectionSecure "Secure Connection">
<!ENTITY identity.connectionNotSecure "Connection is Not Secure">
<!ENTITY identity.connectionFile "This page is stored on your computer.">

View File

@ -505,7 +505,6 @@ identity.icon.tooltip=Show site information
identity.extension.label=Extension (%S)
identity.extension.tooltip=Loaded by extension: %S
identity.showDetails.tooltip=Show connection details
identity.hideDetails.tooltip=Hide connection details
trackingProtection.intro.title=How Tracking Protection works
# LOCALIZATION NOTE (trackingProtection.intro.description2):

View File

@ -3,8 +3,8 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>Amazon.com.nl</ShortName>
<Description>Amazon.com.nl Search</Description>
<ShortName>Amazon.nl</ShortName>
<Description>Amazon.nl Search</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16">resource://search-plugins/images/amazon.ico</Image>
<Url type="text/html" method="GET" template="https://www.amazon.nl/exec/obidos/external-search/" rel="searchform">

View File

@ -55,18 +55,6 @@
display: none !important;
}
#identity-popup,
#identity-popup:not([panelopen]) .panel-viewstack[viewtype="main"]:not([transitioning]) #identity-popup-mainView {
/* Tiny hack to ensure the panel shrinks back to its original
size after closing a subview that is bigger than the main view. */
max-height: 0;
}
.panel-mainview[panelid=identity-popup][viewtype=subview] > #identity-popup-mainView menulist,
.panel-mainview[panelid=identity-popup][viewtype=subview] > #identity-popup-mainView button:not([panel-multiview-anchor]) {
-moz-user-focus: ignore;
}
#identity-popup > .panel-arrowcontainer > .panel-arrowcontent {
padding: 0;
/* Set default fill for icons in the identity popup.
@ -75,8 +63,9 @@
fill-opacity: .6;
}
.panel-mainview[panelid=identity-popup] {
#identity-popup-mainView {
min-width: 30em;
max-width: 30em;
}
#identity-popup-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="main"] > .panel-subviews {
@ -97,26 +86,19 @@
border-top: 1px solid var(--panel-separator-color);
}
#identity-popup-securityView,
#identity-popup-security-content,
.identity-popup-security-content,
#identity-popup-permissions-content,
#tracking-protection-content {
background-repeat: no-repeat;
background-position: 1em 1em;
background-size: 24px auto;
}
#identity-popup-security-content,
#identity-popup-permissions-content,
#tracking-protection-content {
padding: 0.5em 0 1em;
/* .identity-popup-host depends on this width */
padding-inline-start: calc(2em + 24px);
padding-inline-end: 1em;
}
#identity-popup-securityView:-moz-locale-dir(rtl),
#identity-popup-security-content:-moz-locale-dir(rtl),
.identity-popup-security-content:-moz-locale-dir(rtl),
#identity-popup-permissions-content:-moz-locale-dir(rtl),
#tracking-protection-content:-moz-locale-dir(rtl) {
background-position: calc(100% - 1em) 1em;
@ -138,29 +120,17 @@
color: inherit;
}
.identity-popup-expander[panel-multiview-anchor] {
transition: background-color 250ms ease-in;
background-color: Highlight;
background-image: url("chrome://browser/skin/arrow-left.svg");
color: HighlightText;
}
.identity-popup-expander[panel-multiview-anchor]:-moz-locale-dir(rtl),
.identity-popup-expander:not([panel-multiview-anchor]):-moz-locale-dir(ltr) {
.identity-popup-expander:-moz-locale-dir(ltr) {
transform: scaleX(-1);
}
.identity-popup-expander > .button-box {
padding: 0;
}
.identity-popup-expander:not([panel-multiview-anchor]) > .button-box {
border-right: 1px solid var(--panel-separator-color);
padding: 0;
}
.identity-popup-expander:hover {
background-color: var(--arrowpanel-dimmed);
background-image: url("chrome://browser/skin/arrow-left.svg");
}
.identity-popup-expander:hover:active {
@ -172,9 +142,8 @@
.identity-popup-permission-label,
.identity-popup-permission-state-label,
#identity-popup-security-content > description,
.identity-popup-security-content > description,
#identity-popup-security-descriptions > description,
#identity-popup-securityView-header > description,
#identity-popup-securityView-body > description,
#identity-popup-permissions-content > description,
#tracking-protection-content > description {
@ -194,7 +163,7 @@
.identity-popup-host {
word-wrap: break-word;
/* 1em + 2em + 24px is #identity-popup-security-content padding
/* 1em + 2em + 24px is .identity-popup-security-content padding
* 30em is .panel-mainview width */
max-width: calc(30rem - 3rem - 24px - @identityPopupExpanderWidth@);
}
@ -220,50 +189,38 @@
.identity-popup-connection-not-secure {
color: #d74345;
}
#identity-popup-securityView {
overflow: hidden;
}
#identity-popup-securityView,
#identity-popup-security-content {
.identity-popup-security-content {
background-image: url(chrome://browser/skin/controlcenter/conn-not-secure.svg);
}
#identity-popup[connection=chrome] #identity-popup-securityView,
#identity-popup[connection=chrome] #identity-popup-security-content {
#identity-popup[connection=chrome] .identity-popup-security-content {
background-image: url(chrome://branding/content/icon48.png);
}
#identity-popup[connection^=secure] #identity-popup-securityView,
#identity-popup[connection^=secure] #identity-popup-security-content {
#identity-popup[connection^=secure] .identity-popup-security-content {
background-image: url(chrome://browser/skin/controlcenter/connection.svg);
-moz-context-properties: fill;
fill: #12BC00;
}
/* Use [isbroken] to make sure we don't show a lock on an http page. See Bug 1192162. */
#identity-popup[ciphers=weak] #identity-popup-securityView,
#identity-popup[ciphers=weak] #identity-popup-security-content,
#identity-popup[mixedcontent~=passive-loaded][isbroken] #identity-popup-securityView,
#identity-popup[mixedcontent~=passive-loaded][isbroken] #identity-popup-security-content {
#identity-popup[ciphers=weak] .identity-popup-security-content,
#identity-popup[mixedcontent~=passive-loaded][isbroken] .identity-popup-security-content {
background-image: url(chrome://browser/skin/controlcenter/connection.svg);
-moz-context-properties: fill, fill-opacity;
}
#identity-popup[connection=secure-cert-user-overridden] #identity-popup-securityView,
#identity-popup[connection=secure-cert-user-overridden] #identity-popup-security-content {
#identity-popup[connection=secure-cert-user-overridden] .identity-popup-security-content {
background-image: url(chrome://browser/skin/connection-mixed-passive-loaded.svg);
-moz-context-properties: fill, fill-opacity;
}
#identity-popup[loginforms=insecure] #identity-popup-securityView,
#identity-popup[loginforms=insecure] #identity-popup-security-content,
#identity-popup[mixedcontent~=active-loaded][isbroken] #identity-popup-securityView,
#identity-popup[mixedcontent~=active-loaded][isbroken] #identity-popup-security-content {
#identity-popup[loginforms=insecure] .identity-popup-security-content,
#identity-popup[mixedcontent~=active-loaded][isbroken] .identity-popup-security-content {
background-image: url(chrome://browser/skin/controlcenter/mcb-disabled.svg);
-moz-context-properties: fill, fill-opacity;
}
#identity-popup[connection=extension] #identity-popup-securityView,
#identity-popup[connection=extension] #identity-popup-security-content {
#identity-popup[connection=extension] .identity-popup-security-content {
background-image: url(chrome://browser/skin/controlcenter/extension.svg);
-moz-context-properties: fill;
fill: #60bf4c;
@ -274,19 +231,10 @@
color: Graytext;
}
#identity-popup-securityView-header,
#identity-popup-securityView-body {
margin-inline-start: calc(2em + 24px);
margin-inline-end: 1em;
}
#identity-popup-securityView-header {
margin-top: 0.5em;
border-bottom: 1px solid var(--panel-separator-color);
padding-bottom: 1em;
}
#identity-popup-securityView-body {
border-top: 1px solid var(--panel-separator-color);
padding-inline-end: 1em;
}

View File

@ -104,14 +104,13 @@ void* get_proc_address_from_glcontext(void* glcontext_ptr, const char* procname)
void
gecko_profiler_register_thread(const char* name)
{
char stackTop;
profiler_register_thread(name, &stackTop);
PROFILER_REGISTER_THREAD(name);
}
void
gecko_profiler_unregister_thread()
{
profiler_unregister_thread();
PROFILER_UNREGISTER_THREAD();
}
namespace mozilla {

View File

@ -38,7 +38,6 @@ function Readability(uri, doc, options) {
this._uri = uri;
this._doc = doc;
this._biggestFrame = false;
this._articleTitle = null;
this._articleByline = null;
this._articleDir = null;
@ -47,24 +46,13 @@ function Readability(uri, doc, options) {
this._debug = !!options.debug;
this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
this._maxPages = options.maxPages || this.DEFAULT_MAX_PAGES;
this._wordThreshold = options.wordThreshold || this.DEFAULT_WORD_THRESHOLD;
// Start with all flags set
this._flags = this.FLAG_STRIP_UNLIKELYS |
this.FLAG_WEIGHT_CLASSES |
this.FLAG_CLEAN_CONDITIONALLY;
// The list of pages we've parsed in this call of readability,
// for autopaging. As a key store for easier searching.
this._parsedPages = {};
// A list of the ETag headers of pages we've parsed, in case they happen to match,
// we'll know it's a duplicate.
this._pageETags = {};
// Make an AJAX request for each page and append it to the document.
this._curPageNum = 1;
var logEl;
// Control whether log messages are sent to the console
@ -110,13 +98,12 @@ Readability.prototype = {
// tight the competition is among candidates.
DEFAULT_N_TOP_CANDIDATES: 5,
// The maximum number of pages to loop through before we call
// it quits and just show a link.
DEFAULT_MAX_PAGES: 5,
// Element tags to score by default.
DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
// The default number of words an article must have in order to return a result
DEFAULT_WORD_THRESHOLD: 500,
// All of the regular expressions in use within readability.
// Defined up here so we don't instantiate them repeatedly in loops.
REGEXPS: {
@ -139,6 +126,10 @@ Readability.prototype = {
ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ],
DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ],
/**
* Run any post-process modifications to article content as necessary.
*
@ -321,11 +312,20 @@ Readability.prototype = {
curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]);
} catch (e) {/* ignore exceptions setting the title. */}
if (curTitle.match(/ [\|\-] /)) {
curTitle = origTitle.replace(/(.*)[\|\-] .*/gi, '$1');
var titleHadHierarchicalSeparators = false;
function wordCount(str) {
return str.split(/\s+/).length;
}
if (curTitle.split(' ').length < 3)
curTitle = origTitle.replace(/[^\|\-]*[\|\-](.*)/gi, '$1');
// If there's a separator in the title, first remove the final part
if ((/ [\|\-\\\/>»] /).test(curTitle)) {
titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1');
// If the resulting title is too short (3 words or fewer), remove
// the first part instead:
if (wordCount(curTitle) < 3)
curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1');
} else if (curTitle.indexOf(': ') !== -1) {
// Check if we have an heading containing this exact string, so we
// could assume it's the full title.
@ -342,7 +342,7 @@ Readability.prototype = {
curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);
// If the title is now too short, try the first colon instead:
if (curTitle.split(' ').length < 3)
if (wordCount(curTitle) < 3)
curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
}
} else if (curTitle.length > 150 || curTitle.length < 15) {
@ -353,9 +353,16 @@ Readability.prototype = {
}
curTitle = curTitle.trim();
if (curTitle.split(' ').length <= 4)
// If we now have 4 words or fewer as our title, and either no
// 'hierarchical' separators (\, /, > or ») were found in the original
// title or we decreased the number of words by more than 1 word, use
// the original title.
var curTitleWordCount = wordCount(curTitle);
if (curTitleWordCount <= 4 &&
(!titleHadHierarchicalSeparators ||
curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) {
curTitle = origTitle;
}
return curTitle;
},
@ -501,10 +508,16 @@ Readability.prototype = {
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 &&
(lengthSimilarRate > 0 ? h2[0].textContent.includes(this._articleTitle) :
this._articleTitle.includes(h2[0].textContent))) {
this._clean(articleContent, "h2");
if (Math.abs(lengthSimilarRate) < 0.5) {
var titlesMatch = false;
if (lengthSimilarRate > 0) {
titlesMatch = h2[0].textContent.includes(this._articleTitle);
} else {
titlesMatch = this._articleTitle.includes(h2[0].textContent);
}
if (titlesMatch) {
this._clean(articleContent, "h2");
}
}
}
@ -1014,24 +1027,22 @@ Readability.prototype = {
if (this._debug)
this.log("Article content post-prep: " + articleContent.innerHTML);
if (this._curPageNum === 1) {
if (neededToCreateTopCandidate) {
// We already created a fake div thing, and there wouldn't have been any siblings left
// for the previous loop, so there's no point trying to create a new div, and then
// move all the children over. Just assign IDs and class names here. No need to append
// because that already happened anyway.
topCandidate.id = "readability-page-1";
topCandidate.className = "page";
} else {
var div = doc.createElement("DIV");
div.id = "readability-page-1";
div.className = "page";
var children = articleContent.childNodes;
while (children.length) {
div.appendChild(children[0]);
}
articleContent.appendChild(div);
if (neededToCreateTopCandidate) {
// We already created a fake div thing, and there wouldn't have been any siblings left
// for the previous loop, so there's no point trying to create a new div, and then
// move all the children over. Just assign IDs and class names here. No need to append
// because that already happened anyway.
topCandidate.id = "readability-page-1";
topCandidate.className = "page";
} else {
var div = doc.createElement("DIV");
div.id = "readability-page-1";
div.className = "page";
var children = articleContent.childNodes;
while (children.length) {
div.appendChild(children[0]);
}
articleContent.appendChild(div);
}
if (this._debug)
@ -1042,7 +1053,7 @@ Readability.prototype = {
// grabArticle with different flags set. This gives us a higher likelihood of
// finding the content, and the sieve approach gives us a higher likelihood of
// finding the -right- content.
if (this._getInnerText(articleContent, true).length < 500) {
if (this._getInnerText(articleContent, true).length < this._wordThreshold) {
page.innerHTML = pageCacheHtml;
if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
@ -1248,26 +1259,25 @@ Readability.prototype = {
* @return void
**/
_cleanStyles: function(e) {
e = e || this._doc;
if (!e)
if (!e || e.tagName.toLowerCase() === 'svg')
return;
var cur = e.firstChild;
// Remove any root styles, if we're able.
if (typeof e.removeAttribute === 'function' && e.className !== 'readability-styled')
e.removeAttribute('style');
// Go until there are no more child nodes
while (cur !== null) {
if (cur.nodeType === cur.ELEMENT_NODE) {
// Remove style attribute(s) :
if (cur.className !== "readability-styled")
cur.removeAttribute("style");
this._cleanStyles(cur);
if (e.className !== 'readability-styled') {
// Remove `style` and deprecated presentational attributes
for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) {
e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]);
}
cur = cur.nextSibling;
if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
e.removeAttribute('width');
e.removeAttribute('height');
}
}
var cur = e.firstElementChild;
while (cur !== null) {
this._cleanStyles(cur);
cur = cur.nextElementSibling;
}
},
@ -1293,363 +1303,6 @@ Readability.prototype = {
return linkLength / textLength;
},
/**
* Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness.
*
* @author Dan Lacy
* @return string the base url
**/
_findBaseUrl: function() {
var uri = this._uri;
var noUrlParams = uri.pathQueryRef.split("?")[0];
var urlSlashes = noUrlParams.split("/").reverse();
var cleanedSegments = [];
var possibleType = "";
for (var i = 0, slashLen = urlSlashes.length; i < slashLen; i += 1) {
var segment = urlSlashes[i];
// Split off and save anything that looks like a file type.
if (segment.indexOf(".") !== -1) {
possibleType = segment.split(".")[1];
// If the type isn't alpha-only, it's probably not actually a file extension.
if (!possibleType.match(/[^a-zA-Z]/))
segment = segment.split(".")[0];
}
// If our first or second segment has anything looking like a page number, remove it.
if (segment.match(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i) && ((i === 1) || (i === 0)))
segment = segment.replace(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i, "");
var del = false;
// If this is purely a number, and it's the first or second segment,
// it's probably a page number. Remove it.
if (i < 2 && segment.match(/^\d{1,2}$/))
del = true;
// If this is the first segment and it's just "index", remove it.
if (i === 0 && segment.toLowerCase() === "index")
del = true;
// If our first or second segment is smaller than 3 characters,
// and the first segment was purely alphas, remove it.
if (i < 2 && segment.length < 3 && !urlSlashes[0].match(/[a-z]/i))
del = true;
// If it's not marked for deletion, push it to cleanedSegments.
if (!del)
cleanedSegments.push(segment);
}
// This is our final, cleaned, base article URL.
return uri.scheme + "://" + uri.host + cleanedSegments.reverse().join("/");
},
/**
* Look for any paging links that may occur within the document.
*
* @param body
* @return object (array)
**/
_findNextPageLink: function(elem) {
var uri = this._uri;
var possiblePages = {};
var allLinks = elem.getElementsByTagName('a');
var articleBaseUrl = this._findBaseUrl();
// Loop through all links, looking for hints that they may be next-page links.
// Things like having "page" in their textContent, className or id, or being a child
// of a node with a page-y className or id.
//
// Also possible: levenshtein distance? longest common subsequence?
//
// After we do that, assign each page a score, and
for (var i = 0, il = allLinks.length; i < il; i += 1) {
var link = allLinks[i];
var linkHref = allLinks[i].href.replace(/#.*$/, '').replace(/\/$/, '');
// If we've already seen this page, ignore it.
if (linkHref === "" ||
linkHref === articleBaseUrl ||
linkHref === uri.spec ||
linkHref in this._parsedPages) {
continue;
}
// If it's on a different domain, skip it.
if (uri.host !== linkHref.split(/\/+/g)[1])
continue;
var linkText = this._getInnerText(link);
// If the linkText looks like it's not the next page, skip it.
if (linkText.match(this.REGEXPS.extraneous) || linkText.length > 25)
continue;
// If the leftovers of the URL after removing the base URL don't contain
// any digits, it's certainly not a next page link.
var linkHrefLeftover = linkHref.replace(articleBaseUrl, '');
if (!linkHrefLeftover.match(/\d/))
continue;
if (!(linkHref in possiblePages)) {
possiblePages[linkHref] = {"score": 0, "linkText": linkText, "href": linkHref};
} else {
possiblePages[linkHref].linkText += ' | ' + linkText;
}
var linkObj = possiblePages[linkHref];
// If the articleBaseUrl isn't part of this URL, penalize this link. It could
// still be the link, but the odds are lower.
// Example: http://www.actionscript.org/resources/articles/745/1/JavaScript-and-VBScript-Injection-in-ActionScript-3/Page1.html
if (linkHref.indexOf(articleBaseUrl) !== 0)
linkObj.score -= 25;
var linkData = linkText + ' ' + link.className + ' ' + link.id;
if (linkData.match(this.REGEXPS.nextLink))
linkObj.score += 50;
if (linkData.match(/pag(e|ing|inat)/i))
linkObj.score += 25;
if (linkData.match(/(first|last)/i)) {
// -65 is enough to negate any bonuses gotten from a > or » in the text,
// If we already matched on "next", last is probably fine.
// If we didn't, then it's bad. Penalize.
if (!linkObj.linkText.match(this.REGEXPS.nextLink))
linkObj.score -= 65;
}
if (linkData.match(this.REGEXPS.negative) || linkData.match(this.REGEXPS.extraneous))
linkObj.score -= 50;
if (linkData.match(this.REGEXPS.prevLink))
linkObj.score -= 200;
// If a parentNode contains page or paging or paginat
var parentNode = link.parentNode;
var positiveNodeMatch = false;
var negativeNodeMatch = false;
while (parentNode) {
var parentNodeClassAndId = parentNode.className + ' ' + parentNode.id;
if (!positiveNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(/pag(e|ing|inat)/i)) {
positiveNodeMatch = true;
linkObj.score += 25;
}
if (!negativeNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(this.REGEXPS.negative)) {
// If this is just something like "footer", give it a negative.
// If it's something like "body-and-footer", leave it be.
if (!parentNodeClassAndId.match(this.REGEXPS.positive)) {
linkObj.score -= 25;
negativeNodeMatch = true;
}
}
parentNode = parentNode.parentNode;
}
// If the URL looks like it has paging in it, add to the score.
// Things like /page/2/, /pagenum/2, ?p=3, ?page=11, ?pagination=34
if (linkHref.match(/p(a|g|ag)?(e|ing|ination)?(=|\/)[0-9]{1,2}/i) || linkHref.match(/(page|paging)/i))
linkObj.score += 25;
// If the URL contains negative values, give a slight decrease.
if (linkHref.match(this.REGEXPS.extraneous))
linkObj.score -= 15;
/**
* Minor punishment to anything that doesn't match our current URL.
* NOTE: I'm finding this to cause more harm than good where something is exactly 50 points.
* Dan, can you show me a counterexample where this is necessary?
* if (linkHref.indexOf(window.location.href) !== 0) {
* linkObj.score -= 1;
* }
**/
// If the link text can be parsed as a number, give it a minor bonus, with a slight
// bias towards lower numbered pages. This is so that pages that might not have 'next'
// in their text can still get scored, and sorted properly by score.
var linkTextAsNumber = parseInt(linkText, 10);
if (linkTextAsNumber) {
// Punish 1 since we're either already there, or it's probably
// before what we want anyways.
if (linkTextAsNumber === 1) {
linkObj.score -= 10;
} else {
linkObj.score += Math.max(0, 10 - linkTextAsNumber);
}
}
}
// Loop thrugh all of our possible pages from above and find our top
// candidate for the next page URL. Require at least a score of 50, which
// is a relatively high confidence that this page is the next link.
var topPage = null;
for (var page in possiblePages) {
if (possiblePages.hasOwnProperty(page)) {
if (possiblePages[page].score >= 50 &&
(!topPage || topPage.score < possiblePages[page].score))
topPage = possiblePages[page];
}
}
var nextHref = null;
if (topPage) {
nextHref = topPage.href.replace(/\/$/, '');
this.log('NEXT PAGE IS ' + nextHref);
this._parsedPages[nextHref] = true;
}
return nextHref;
},
_successfulRequest: function(request) {
return (request.status >= 200 && request.status < 300) ||
request.status === 304 ||
(request.status === 0 && request.responseText);
},
_ajax: function(url, options) {
var request = new XMLHttpRequest();
function respondToReadyState(readyState) {
if (request.readyState === 4) {
if (this._successfulRequest(request)) {
if (options.success)
options.success(request);
} else if (options.error) {
options.error(request);
}
}
}
if (typeof options === 'undefined')
options = {};
request.onreadystatechange = respondToReadyState;
request.open('get', url, true);
request.setRequestHeader('Accept', 'text/html');
try {
request.send(options.postBody);
} catch (e) {
if (options.error)
options.error();
}
return request;
},
_appendNextPage: function(nextPageLink) {
var doc = this._doc;
this._curPageNum += 1;
var articlePage = doc.createElement("DIV");
articlePage.id = 'readability-page-' + this._curPageNum;
articlePage.className = 'page';
articlePage.innerHTML = '<p class="page-separator" title="Page ' + this._curPageNum + '">&sect;</p>';
doc.getElementById("readability-content").appendChild(articlePage);
if (this._curPageNum > this._maxPages) {
var nextPageMarkup = "<div style='text-align: center'><a href='" + nextPageLink + "'>View Next Page</a></div>";
articlePage.innerHTML = articlePage.innerHTML + nextPageMarkup;
return;
}
// Now that we've built the article page DOM element, get the page content
// asynchronously and load the cleaned content into the div we created for it.
((pageUrl, thisPage) => {
this._ajax(pageUrl, {
success: function(r) {
// First, check to see if we have a matching ETag in headers - if we do, this is a duplicate page.
var eTag = r.getResponseHeader('ETag');
if (eTag) {
if (eTag in this._pageETags) {
this.log("Exact duplicate page found via ETag. Aborting.");
articlePage.style.display = 'none';
return;
}
this._pageETags[eTag] = 1;
}
// TODO: this ends up doubling up page numbers on NYTimes articles. Need to generically parse those away.
var page = doc.createElement("DIV");
// Do some preprocessing to our HTML to make it ready for appending.
// - Remove any script tags. Swap and reswap newlines with a unicode
// character because multiline regex doesn't work in javascript.
// - Turn any noscript tags into divs so that we can parse them. This
// allows us to find any next page links hidden via javascript.
// - Turn all double br's into p's - was handled by prepDocument in the original view.
// Maybe in the future abstract out prepDocument to work for both the original document
// and AJAX-added pages.
var responseHtml = r.responseText.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
responseHtml = responseHtml.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
responseHtml = responseHtml.replace(/\uffff/g, '\n').replace(/<(\/?)noscript/gi, '<$1div');
responseHtml = responseHtml.replace(this.REGEXPS.replaceFonts, '<$1span>');
page.innerHTML = responseHtml;
this._replaceBrs(page);
// Reset all flags for the next page, as they will search through it and
// disable as necessary at the end of grabArticle.
this._flags = 0x1 | 0x2 | 0x4;
var secondNextPageLink = this._findNextPageLink(page);
// NOTE: if we end up supporting _appendNextPage(), we'll need to
// change this call to be async
var content = this._grabArticle(page);
if (!content) {
this.log("No content found in page to append. Aborting.");
return;
}
// Anti-duplicate mechanism. Essentially, get the first paragraph of our new page.
// Compare it against all of the the previous document's we've gotten. If the previous
// document contains exactly the innerHTML of this first paragraph, it's probably a duplicate.
var firstP = content.getElementsByTagName("P").length ? content.getElementsByTagName("P")[0] : null;
if (firstP && firstP.innerHTML.length > 100) {
for (var i = 1; i <= this._curPageNum; i += 1) {
var rPage = doc.getElementById('readability-page-' + i);
if (rPage && rPage.innerHTML.indexOf(firstP.innerHTML) !== -1) {
this.log('Duplicate of page ' + i + ' - skipping.');
articlePage.style.display = 'none';
this._parsedPages[pageUrl] = true;
return;
}
}
}
this._removeScripts(content);
thisPage.innerHTML = thisPage.innerHTML + content.innerHTML;
// After the page has rendered, post process the content. This delay is necessary because,
// in webkit at least, offsetWidth is not set in time to determine image width. We have to
// wait a little bit for reflow to finish before we can fix floating images.
setTimeout(() => {
this._postProcessContent(thisPage);
}, 500);
if (secondNextPageLink)
this._appendNextPage(secondNextPageLink);
}
});
})(nextPageLink, articlePage);
},
/**
* Get an elements class/id weight. Uses regular expressions to tell if this
* element looks good or bad.
@ -1932,10 +1585,6 @@ Readability.prototype = {
return (this._flags & flag) > 0;
},
_addFlag: function(flag) {
this._flags = this._flags | flag;
},
_removeFlag: function(flag) {
this._flags = this._flags & ~flag;
},
@ -2026,16 +1675,6 @@ Readability.prototype = {
// Remove script tags from the document.
this._removeScripts(this._doc);
// FIXME: Disabled multi-page article support for now as it
// needs more work on infrastructure.
// Make sure this document is added to the list of parsed pages first,
// so we don't double up on the first page.
// this._parsedPages[uri.spec.replace(/\/$/, '')] = true;
// Pull out any possible next page link first.
// var nextPageLink = this._findNextPageLink(doc.body);
this._prepDocument();
var metadata = this._getArticleMetadata();
@ -2049,14 +1688,6 @@ Readability.prototype = {
this._postProcessContent(articleContent);
// if (nextPageLink) {
// // Append any additional pages after a small timeout so that people
// // can start reading without having to wait for this to finish processing.
// setTimeout((function() {
// this._appendNextPage(nextPageLink);
// }).bind(this), 500);
// }
// If we haven't found an excerpt in the article's metadata, use the article's
// first paragraph as the excerpt. This is used for displaying a preview of
// the article's content.