Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: lib/content/elemHideEmulation.js

Issue 29712655: Issue 6437 - Skip styles-only patterns on DOM mutations (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Patch Set: Merge patch #29716641 Created March 8, 2018, 9:02 p.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: lib/content/elemHideEmulation.js
===================================================================
--- a/lib/content/elemHideEmulation.js
+++ b/lib/content/elemHideEmulation.js
@@ -18,16 +18,24 @@
"use strict";
const {textToRegExp, filterToRegExp, splitSelector} = require("../common");
let MIN_INVOCATION_INTERVAL = 3000;
const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
+function getCachedPropertyValue(object, name, defaultValueFunc = () => {})
+{
+ let value = object[name];
+ if (value === undefined)
hub 2018/03/14 13:04:03 there is the argument of value === undefined vs ty
Manish Jethani 2018/03/14 14:54:53 Oh yeah, for the sake of consistency though I've m
+ Object.defineProperty(object, name, {value: value = defaultValueFunc()});
+ return value;
+}
+
/** Return position of node from parent.
* @param {Node} node the node to find the position of.
* @return {number} One-based index like for :nth-child(), or 0 on error.
*/
function positionInParent(node)
{
let {children} = node.parentNode;
for (let i = 0; i < children.length; i++)
@@ -239,16 +247,17 @@
function HasSelector(selectors)
{
this._innerSelectors = selectors;
}
HasSelector.prototype = {
requiresHiding: true,
+ dependsOnDOM: true,
get dependsOnStyles()
{
return this._innerSelectors.some(selector => selector.dependsOnStyles);
},
get dependsOnCharacterData()
{
@@ -301,16 +310,17 @@
function ContainsSelector(textContent)
{
this._regexp = makeRegExpParameter(textContent);
}
ContainsSelector.prototype = {
requiresHiding: true,
+ dependsOnDOM: true,
dependsOnCharacterData: true,
*getSelectors(prefix, subtree, styles)
{
for (let element of this.getElements(prefix, subtree, styles))
yield [makeSelector(element, ""), subtree];
},
@@ -372,47 +382,104 @@
*getSelectors(prefix, subtree, styles)
{
for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
yield [selector, subtree];
}
};
-function isSelectorHidingOnlyPattern(pattern)
+function Pattern(selectors, text)
{
- return pattern.selectors.some(s => s.preferHideWithSelector) &&
- !pattern.selectors.some(s => s.requiresHiding);
+ this.selectors = selectors;
+ this.text = text;
+}
+
+Pattern.prototype = {
+ isSelectorHidingOnlyPattern()
+ {
+ return getCachedPropertyValue(
+ this, "_selectorHidingOnlyPattern",
+ () => this.selectors.some(selector => selector.preferHideWithSelector) &&
+ !this.selectors.some(selector => selector.requiresHiding)
+ );
+ },
+
+ get dependsOnStyles()
+ {
+ return getCachedPropertyValue(
+ this, "_dependsOnStyles",
+ () => this.selectors.some(selector => selector.dependsOnStyles)
+ );
+ },
+
+ get dependsOnDOM()
+ {
+ return getCachedPropertyValue(
+ this, "_dependsOnDOM",
+ () => this.selectors.some(selector => selector.dependsOnDOM)
+ );
+ },
+
+ get dependsOnStylesAndDOM()
+ {
+ return getCachedPropertyValue(
+ this, "_dependsOnStylesAndDOM",
+ () => this.selectors.some(selector => selector.dependsOnStyles &&
+ selector.dependsOnDOM)
+ );
+ },
+
+ get maybeDependsOnAttributes()
+ {
+ // Observe changes to attributes if either there's a plain selector that
+ // looks like an ID selector, class selector, or attribute selector in one
+ // of the patterns (e.g. "a[href='https://example.com/']")
+ // or there's a properties selector nested inside a has selector
+ // (e.g. "div:-abp-has(:-abp-properties(color: blue))")
+ return getCachedPropertyValue(
+ this, "_maybeDependsOnAttributes",
+ () => this.selectors.some(
+ selector => selector.maybeDependsOnAttributes ||
+ (selector instanceof HasSelector &&
+ selector.dependsOnStyles)
+ )
+ );
+ },
+
+ get dependsOnCharacterData()
+ {
+ // Observe changes to character data only if there's a contains selector in
+ // one of the patterns.
+ return getCachedPropertyValue(
+ this, "_dependsOnCharacterData",
+ () => this.selectors.some(selector => selector.dependsOnCharacterData)
+ );
+ }
+};
+
+function filterPatterns(patterns, {stylesheets, mutations})
+{
+ if (!stylesheets && !mutations)
+ return patterns.slice();
+
+ return patterns.filter(
+ pattern => (stylesheets && pattern.dependsOnStyles) ||
+ (mutations && pattern.dependsOnDOM)
+ );
}
function shouldObserveAttributes(patterns)
{
- // Observe changes to attributes if either there's a plain selector that
- // looks like an ID selector, class selector, or attribute selector in one of
- // the patterns (e.g. "a[href='https://example.com/']")
- // or there's a properties selector nested inside a has selector
- // (e.g. "div:-abp-has(:-abp-properties(color: blue))")
- return patterns.some(
- pattern => pattern.selectors.some(
- selector => selector.maybeDependsOnAttributes ||
- (selector instanceof HasSelector &&
- selector.dependsOnStyles)
- )
- );
+ return patterns.some(pattern => pattern.maybeDependsOnAttributes);
}
function shouldObserveCharacterData(patterns)
{
- // Observe changes to character data only if there's a contains selector in
- // one of the patterns.
- return patterns.some(
- pattern => pattern.selectors.some(
- selector => selector.dependsOnCharacterData
- )
- );
+ return patterns.some(pattern => pattern.dependsOnCharacterData);
}
function ElemHideEmulation(addSelectorsFunc, hideElemsFunc)
{
this.document = document;
this.addSelectorsFunc = addSelectorsFunc;
this.hideElemsFunc = hideElemsFunc;
this.observer = new MutationObserver(this.observe.bind(this));
@@ -497,34 +564,51 @@
/**
* Processes the current document and applies all rules to it.
* @param {CSSStyleSheet[]} [stylesheets]
* The list of new stylesheets that have been added to the document and
* made reprocessing necessary. This parameter shouldn't be passed in for
* the initial processing, all of document's stylesheets will be considered
* then and all rules, including the ones not dependent on styles.
+ * @param {MutationRecord[]} [mutations]
+ * The list of DOM mutations that have been applied to the document and
+ * made reprocessing necessary. This parameter shouldn't be passed in for
+ * the initial processing, the entire document will be considered
+ * then and all rules, including the ones not dependent on the DOM.
* @param {function} [done]
* Callback to call when done.
*/
- _addSelectors(stylesheets, done)
+ _addSelectors(stylesheets, mutations, done)
{
+ let patterns = filterPatterns(this.patterns, {stylesheets, mutations});
+
let selectors = [];
let selectorFilters = [];
let elements = [];
let elementFilters = [];
let cssStyles = [];
- let stylesheetOnlyChange = !!stylesheets;
- if (!stylesheets)
+ // If neither any style sheets nor any DOM mutations have been specified,
+ // do full processing.
+ if (!stylesheets && !mutations)
stylesheets = this.document.styleSheets;
- for (let stylesheet of stylesheets)
+ // If there are any DOM mutations and any of the patterns depends on both
+ // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the
+ // rules in every style sheet in the document, because we need to run
+ // querySelectorAll afterwards. On the other hand, if we only have patterns
+ // that depend on either styles or DOM both not both
+ // (e.g. -abp-properties or -abp-contains), we can skip this part.
+ if (mutations && patterns.some(pattern => pattern.dependsOnStylesAndDOM))
+ stylesheets = this.document.styleSheets;
+
+ for (let stylesheet of stylesheets || [])
{
// Explicitly ignore third-party stylesheets to ensure consistent behavior
// between Firefox and Chrome.
if (!this.isSameOrigin(stylesheet))
continue;
let rules = stylesheet.cssRules;
if (!rules)
@@ -534,51 +618,46 @@
{
if (rule.type != rule.STYLE_RULE)
continue;
cssStyles.push(stringifyStyle(rule));
}
}
- let patterns = this.patterns.slice();
let pattern = null;
let generator = null;
let processPatterns = () =>
{
let cycleStart = performance.now();
if (!pattern)
{
if (!patterns.length)
{
- this.addSelectorsFunc(selectors, selectorFilters);
- this.hideElemsFunc(elements, elementFilters);
+ if (selectors.length > 0)
Manish Jethani 2018/03/08 21:08:15 We need to do this because of an existing bug, whi
Manish Jethani 2018/03/08 21:28:34 Sorry, I just checked, there is no bug. In any cas
+ this.addSelectorsFunc(selectors, selectorFilters);
+ if (elements.length > 0)
+ this.hideElemsFunc(elements, elementFilters);
if (typeof done == "function")
done();
return;
}
pattern = patterns.shift();
- if (stylesheetOnlyChange &&
- !pattern.selectors.some(selector => selector.dependsOnStyles))
- {
- pattern = null;
- return processPatterns();
- }
generator = evaluate(pattern.selectors, 0, "",
this.document, cssStyles);
}
for (let selector of generator)
{
if (selector != null)
{
- if (isSelectorHidingOnlyPattern(pattern))
+ if (pattern.isSelectorHidingOnlyPattern())
{
selectors.push(selector);
selectorFilters.push(pattern.text);
}
else
{
for (let element of this.document.querySelectorAll(selector))
{
@@ -615,95 +694,113 @@
_filteringInProgress: false,
_lastInvocation: -MIN_INVOCATION_INTERVAL,
_scheduledProcessing: null,
/**
* Re-run filtering either immediately or queued.
* @param {CSSStyleSheet[]} [stylesheets]
* new stylesheets to be processed. This parameter should be omitted
- * for DOM modification (full reprocessing required).
+ * for full reprocessing.
+ * @param {MutationRecord[]} [mutations]
+ * new DOM mutations to be processed. This parameter should be omitted
+ * for full reprocessing.
*/
- queueFiltering(stylesheets)
+ queueFiltering(stylesheets, mutations)
{
let completion = () =>
{
this._lastInvocation = performance.now();
this._filteringInProgress = false;
if (this._scheduledProcessing)
{
- let newStylesheets = this._scheduledProcessing.stylesheets;
+ let params = Object.assign({}, this._scheduledProcessing);
this._scheduledProcessing = null;
- this.queueFiltering(newStylesheets);
+ this.queueFiltering(params.stylesheets, params.mutations);
}
};
if (this._scheduledProcessing)
{
- if (!stylesheets)
- this._scheduledProcessing.stylesheets = null;
- else if (this._scheduledProcessing.stylesheets)
- this._scheduledProcessing.stylesheets.push(...stylesheets);
+ if (!stylesheets && !mutations)
+ {
+ this._scheduledProcessing = {};
+ }
+ else
+ {
+ if (stylesheets)
+ {
+ if (!this._scheduledProcessing.stylesheets)
+ this._scheduledProcessing.stylesheets = [];
+ this._scheduledProcessing.stylesheets.push(...stylesheets);
+ }
+ if (mutations)
+ {
+ if (!this._scheduledProcessing.mutations)
+ this._scheduledProcessing.mutations = [];
+ this._scheduledProcessing.mutations.push(...mutations);
+ }
+ }
}
else if (this._filteringInProgress)
{
- this._scheduledProcessing = {stylesheets};
+ this._scheduledProcessing = {stylesheets, mutations};
}
else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
{
- this._scheduledProcessing = {stylesheets};
+ this._scheduledProcessing = {stylesheets, mutations};
setTimeout(() =>
{
- let newStylesheets = this._scheduledProcessing.stylesheets;
+ let params = Object.assign({}, this._scheduledProcessing);
this._filteringInProgress = true;
this._scheduledProcessing = null;
- this._addSelectors(newStylesheets, completion);
+ this._addSelectors(params.stylesheets, params.mutations, completion);
},
MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation));
}
else if (this.document.readyState == "loading")
{
- this._scheduledProcessing = {stylesheets};
+ this._scheduledProcessing = {stylesheets, mutations};
let handler = () =>
{
document.removeEventListener("DOMContentLoaded", handler);
- let newStylesheets = this._scheduledProcessing.stylesheets;
+ let params = Object.assign({}, this._scheduledProcessing);
this._filteringInProgress = true;
this._scheduledProcessing = null;
- this._addSelectors(newStylesheets, completion);
+ this._addSelectors(params.stylesheets, params.mutations, completion);
};
document.addEventListener("DOMContentLoaded", handler);
}
else
{
this._filteringInProgress = true;
- this._addSelectors(stylesheets, completion);
+ this._addSelectors(stylesheets, mutations, completion);
}
},
onLoad(event)
{
let stylesheet = event.target.sheet;
if (stylesheet)
this.queueFiltering([stylesheet]);
},
observe(mutations)
{
- this.queueFiltering();
+ this.queueFiltering(null, mutations);
},
apply(patterns)
{
this.patterns = [];
for (let pattern of patterns)
{
let selectors = this.parseSelector(pattern.selector);
if (selectors != null && selectors.length > 0)
- this.patterns.push({selectors, text: pattern.text});
+ this.patterns.push(new Pattern(selectors, pattern.text));
}
if (this.patterns.length > 0)
{
this.queueFiltering();
this.observer.observe(
this.document,
{
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld