Index: lib/content/elemHideEmulation.js |
=================================================================== |
--- a/lib/content/elemHideEmulation.js |
+++ b/lib/content/elemHideEmulation.js |
@@ -14,17 +14,18 @@ |
* You should have received a copy of the GNU General Public License |
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
*/ |
"use strict"; |
const {filterToRegExp, splitSelector} = require("common"); |
-const MIN_INVOCATION_INTERVAL = 3000; |
+let MIN_INVOCATION_INTERVAL = 3000; |
+const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; |
const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
/** 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) |
{ |
@@ -32,16 +33,18 @@ |
for (let i = 0; i < children.length; i++) |
if (children[i] == node) |
return i + 1; |
return 0; |
} |
function makeSelector(node, selector) |
{ |
+ if (node == null) |
+ return null; |
if (!node.parentElement) |
{ |
let newSelector = ":root"; |
if (selector) |
newSelector += " > " + selector; |
return newSelector; |
} |
let idx = positionInParent(node); |
@@ -123,17 +126,26 @@ |
{ |
if (index >= chain.length) |
{ |
yield prefix; |
return; |
} |
for (let [selector, element] of |
chain[index].getSelectors(prefix, subtree, styles)) |
- yield* evaluate(chain, index + 1, selector, element, styles); |
+ { |
+ if (selector == null) |
+ yield null; |
+ else |
+ yield* evaluate(chain, index + 1, selector, element, styles); |
+ } |
+ // Just in case the getSelectors() generator above had to run some heavy |
+ // document.querySelectorAll() call which didn't produce any results, make |
+ // sure there is at least one point where execution can pause. |
+ yield null; |
} |
function PlainSelector(selector) |
{ |
this._selector = selector; |
} |
PlainSelector.prototype = { |
@@ -183,28 +195,34 @@ |
let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
prefix + "*" : prefix; |
let elements = subtree.querySelectorAll(actualPrefix); |
for (let element of elements) |
{ |
let iter = evaluate(this._innerSelectors, 0, "", element, styles); |
for (let selector of iter) |
{ |
+ if (selector == null) |
+ { |
+ yield null; |
+ continue; |
+ } |
if (relativeSelectorRegexp.test(selector)) |
selector = ":scope" + selector; |
try |
{ |
if (element.querySelector(selector)) |
yield element; |
} |
catch (e) |
{ |
// :scope isn't supported on Edge, ignore error caused by it. |
} |
} |
+ yield null; |
} |
} |
}; |
function ContainsSelector(textContent) |
{ |
this._text = textContent; |
} |
@@ -218,19 +236,24 @@ |
yield [makeSelector(element, ""), subtree]; |
}, |
*getElements(prefix, subtree, stylesheet) |
{ |
let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
prefix + "*" : prefix; |
let elements = subtree.querySelectorAll(actualPrefix); |
+ |
for (let element of elements) |
+ { |
if (element.textContent.includes(this._text)) |
yield element; |
+ else |
+ yield null; |
+ } |
} |
}; |
function PropsSelector(propertyExpression) |
{ |
let regexpString; |
if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
propertyExpression[propertyExpression.length - 1] == "/") |
@@ -268,23 +291,30 @@ |
*getSelectors(prefix, subtree, styles) |
{ |
for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
yield [selector, subtree]; |
} |
}; |
+function isSelectorHidingOnlyPattern(pattern) |
+{ |
+ return pattern.selectors.some(s => s.preferHideWithSelector) && |
+ !pattern.selectors.some(s => s.requiresHiding); |
+} |
+ |
function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, |
hideElemsFunc) |
{ |
this.window = window; |
this.getFiltersFunc = getFiltersFunc; |
this.addSelectorsFunc = addSelectorsFunc; |
this.hideElemsFunc = hideElemsFunc; |
+ this.observer = new window.MutationObserver(this.observe.bind(this)); |
} |
ElemHideEmulation.prototype = { |
isSameOrigin(stylesheet) |
{ |
try |
{ |
return new URL(stylesheet.href).origin == this.window.location.origin; |
@@ -357,30 +387,28 @@ |
new SyntaxError("Failed to parse Adblock Plus " + |
`selector ${selector}, can't ` + |
"have a lonely :-abp-contains().")); |
return null; |
} |
return selectors; |
}, |
- _lastInvocation: 0, |
- |
/** |
* 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 {function} [done] |
+ * Callback to call when done. |
*/ |
- addSelectors(stylesheets) |
+ _addSelectors(stylesheets, done) |
{ |
- this._lastInvocation = Date.now(); |
- |
let selectors = []; |
let selectorFilters = []; |
let elements = []; |
let elementFilters = []; |
let cssStyles = []; |
@@ -407,72 +435,156 @@ |
if (rule.type != rule.STYLE_RULE) |
continue; |
cssStyles.push(stringifyStyle(rule)); |
} |
} |
let {document} = this.window; |
- for (let pattern of this.patterns) |
+ |
+ let patterns = this.patterns.slice(); |
+ let pattern = null; |
+ let generator = null; |
+ |
+ let processPatterns = () => |
{ |
- if (stylesheetOnlyChange && |
- !pattern.selectors.some(selector => selector.dependsOnStyles)) |
- { |
- continue; |
- } |
+ let cycleStart = this.window.performance.now(); |
- for (let selector of evaluate(pattern.selectors, |
- 0, "", document, cssStyles)) |
+ if (!pattern) |
{ |
- if (pattern.selectors.some(s => s.preferHideWithSelector) && |
- !pattern.selectors.some(s => s.requiresHiding)) |
+ if (!patterns.length) |
{ |
- selectors.push(selector); |
- selectorFilters.push(pattern.text); |
+ this.addSelectorsFunc(selectors, selectorFilters); |
+ 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(); |
} |
- else |
+ generator = evaluate(pattern.selectors, 0, "", document, cssStyles); |
+ } |
+ for (let selector of generator) |
+ { |
+ if (selector != null) |
{ |
- for (let element of document.querySelectorAll(selector)) |
+ if (isSelectorHidingOnlyPattern(pattern)) |
{ |
- elements.push(element); |
- elementFilters.push(pattern.text); |
+ selectors.push(selector); |
+ selectorFilters.push(pattern.text); |
+ } |
+ else |
+ { |
+ for (let element of document.querySelectorAll(selector)) |
+ { |
+ elements.push(element); |
+ elementFilters.push(pattern.text); |
+ } |
} |
} |
+ if (this.window.performance.now() - |
+ cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) |
+ { |
+ this.window.setTimeout(processPatterns, 0); |
+ return; |
+ } |
} |
- } |
+ pattern = null; |
+ return processPatterns(); |
+ }; |
- this.addSelectorsFunc(selectors, selectorFilters); |
- this.hideElemsFunc(elements, elementFilters); |
+ processPatterns(); |
+ }, |
+ |
+ // This property is only used in the tests |
+ // to shorten the invocation interval |
+ get MIN_INVOCATION_INTERVAL() |
+ { |
+ return MIN_INVOCATION_INTERVAL; |
+ }, |
+ |
+ set MIN_INVOCATION_INTERVAL(interval) |
+ { |
+ MIN_INVOCATION_INTERVAL = interval; |
}, |
- _stylesheetQueue: null, |
+ _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). |
+ */ |
+ queueFiltering(stylesheets) |
+ { |
+ let completion = () => |
+ { |
+ this._lastInvocation = this.window.performance.now(); |
+ this._filteringInProgress = false; |
+ if (this._scheduledProcessing) |
+ { |
+ let newStylesheets = this._scheduledProcessing.stylesheets; |
+ this._scheduledProcessing = null; |
+ this.queueFiltering(newStylesheets); |
+ } |
+ }; |
+ |
+ if (this._scheduledProcessing) |
+ { |
+ if (!stylesheets) |
+ this._scheduledProcessing.stylesheets = null; |
+ else if (this._scheduledProcessing.stylesheets) |
+ this._scheduledProcessing.stylesheets.push(...stylesheets); |
+ } |
+ else if (this._filteringInProgress) |
+ { |
+ this._scheduledProcessing = {stylesheets}; |
+ } |
+ else if (this.window.performance.now() - |
+ this._lastInvocation < MIN_INVOCATION_INTERVAL) |
+ { |
+ this._scheduledProcessing = {stylesheets}; |
+ this.window.setTimeout(() => |
+ { |
+ let newStylesheets = this._scheduledProcessing.stylesheets; |
+ this._filteringInProgress = true; |
+ this._scheduledProcessing = null; |
+ this._addSelectors(newStylesheets, completion); |
+ }, |
+ MIN_INVOCATION_INTERVAL - |
+ (this.window.performance.now() - this._lastInvocation)); |
+ } |
+ else |
+ { |
+ this._filteringInProgress = true; |
+ this._addSelectors(stylesheets, completion); |
+ } |
+ }, |
onLoad(event) |
{ |
let stylesheet = event.target.sheet; |
if (stylesheet) |
- { |
- if (!this._stylesheetQueue && |
- Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
- { |
- this._stylesheetQueue = []; |
- this.window.setTimeout(() => |
- { |
- let stylesheets = this._stylesheetQueue; |
- this._stylesheetQueue = null; |
- this.addSelectors(stylesheets); |
- }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); |
- } |
+ this.queueFiltering([stylesheet]); |
+ }, |
- if (this._stylesheetQueue) |
- this._stylesheetQueue.push(stylesheet); |
- else |
- this.addSelectors([stylesheet]); |
- } |
+ observe(mutations) |
+ { |
+ this.queueFiltering(); |
}, |
apply() |
{ |
this.getFiltersFunc(patterns => |
{ |
this.patterns = []; |
for (let pattern of patterns) |
@@ -480,16 +592,25 @@ |
let selectors = this.parseSelector(pattern.selector); |
if (selectors != null && selectors.length > 0) |
this.patterns.push({selectors, text: pattern.text}); |
} |
if (this.patterns.length > 0) |
{ |
let {document} = this.window; |
- this.addSelectors(); |
+ this.queueFiltering(); |
+ this.observer.observe( |
+ document, |
+ { |
+ childList: true, |
+ attributes: true, |
+ characterData: true, |
+ subtree: true |
+ } |
+ ); |
document.addEventListener("load", this.onLoad.bind(this), true); |
} |
}); |
} |
}; |
exports.ElemHideEmulation = ElemHideEmulation; |