Index: lib/content/elemHideEmulation.js |
=================================================================== |
--- a/lib/content/elemHideEmulation.js |
+++ b/lib/content/elemHideEmulation.js |
@@ -19,16 +19,34 @@ |
const {textToRegExp, filterToRegExp, splitSelector} = require("../common"); |
const {indexOf} = require("../coreUtils"); |
let MIN_INVOCATION_INTERVAL = 3000; |
const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; |
const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
+let debugInfo = null; |
+ |
+function setDebugMode() |
+{ |
+ debugInfo = { |
+ lastProcessedElements: new Set() |
+ }; |
+} |
+ |
+exports.setDebugMode = setDebugMode; |
+ |
+function getDebugInfo() |
+{ |
+ return debugInfo; |
+} |
+ |
+exports.getDebugInfo = getDebugInfo; |
+ |
function getCachedPropertyValue(object, name, defaultValueFunc = () => {}) |
{ |
let value = object[name]; |
if (typeof value == "undefined") |
Object.defineProperty(object, name, {value: value = defaultValueFunc()}); |
return value; |
} |
@@ -194,53 +212,55 @@ |
return new RegExp(pattern, flags); |
} |
catch (e) |
{ |
} |
return null; |
} |
-function* evaluate(chain, index, prefix, subtree, styles) |
+function* evaluate(chain, index, prefix, subtree, styles, targets) |
{ |
if (index >= chain.length) |
{ |
yield prefix; |
return; |
} |
for (let [selector, element] of |
- chain[index].getSelectors(prefix, subtree, styles)) |
+ chain[index].getSelectors(prefix, subtree, styles, targets)) |
{ |
if (selector == null) |
yield null; |
else |
- yield* evaluate(chain, index + 1, selector, element, styles); |
+ yield* evaluate(chain, index + 1, selector, element, styles, targets); |
} |
// 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; |
this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector); |
this.dependsOnDOM = this.maybeDependsOnAttributes; |
+ this.maybeContainsSiblingCombinators = /[~+]/.test(selector); |
} |
PlainSelector.prototype = { |
/** |
* Generator function returning a pair of selector |
* string and subtree. |
* @param {string} prefix the prefix for the selector. |
* @param {Node} subtree the subtree we work on. |
* @param {StringifiedStyle[]} styles the stringified style objects. |
+ * @param {Node[]} [targets] the nodes we are interested in. |
*/ |
- *getSelectors(prefix, subtree, styles) |
+ *getSelectors(prefix, subtree, styles, targets) |
{ |
yield [prefix + this._selector, subtree]; |
} |
}; |
const incompletePrefixRegexp = /[\s>+~]$/; |
function HasSelector(selectors) |
@@ -265,67 +285,81 @@ |
get maybeDependsOnAttributes() |
{ |
return this._innerSelectors.some( |
selector => selector.maybeDependsOnAttributes |
); |
}, |
- *getSelectors(prefix, subtree, styles) |
+ *getSelectors(prefix, subtree, styles, targets) |
{ |
- for (let element of this.getElements(prefix, subtree, styles)) |
+ for (let element of this.getElements(prefix, subtree, styles, targets)) |
yield [makeSelector(element), element]; |
}, |
/** |
* Generator function returning selected elements. |
* @param {string} prefix the prefix for the selector. |
* @param {Node} subtree the subtree we work on. |
* @param {StringifiedStyle[]} styles the stringified style objects. |
+ * @param {Node[]} [targets] the nodes we are interested in. |
*/ |
- *getElements(prefix, subtree, styles) |
+ *getElements(prefix, subtree, styles, targets) |
{ |
let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
prefix + "*" : prefix; |
let elements = scopedQuerySelectorAll(subtree, actualPrefix); |
if (elements) |
{ |
for (let element of elements) |
{ |
- let iter = evaluate(this._innerSelectors, 0, "", element, styles); |
+ // If the element is neither an ancestor nor a descendant of one of the |
+ // targets, we can skip it. |
+ if (targets && !targets.some(target => element.contains(target) || |
+ target.contains(element))) |
+ { |
+ yield null; |
+ continue; |
+ } |
+ |
+ let iter = evaluate(this._innerSelectors, 0, "", element, styles, |
+ targets); |
for (let selector of iter) |
{ |
if (selector == null) |
yield null; |
else if (scopedQuerySelector(element, selector)) |
yield element; |
} |
yield null; |
+ |
+ if (debugInfo) |
+ debugInfo.lastProcessedElements.add(element); |
} |
} |
} |
}; |
function ContainsSelector(textContent) |
{ |
this._regexp = makeRegExpParameter(textContent); |
} |
ContainsSelector.prototype = { |
dependsOnDOM: true, |
dependsOnCharacterData: true, |
- *getSelectors(prefix, subtree, styles) |
+ *getSelectors(prefix, subtree, styles, targets) |
{ |
- for (let element of this.getElements(prefix, subtree, styles)) |
+ for (let element of this.getElements(prefix, subtree, styles, targets)) |
yield [makeSelector(element), subtree]; |
}, |
- *getElements(prefix, subtree, styles) |
+ *getElements(prefix, subtree, styles, targets) |
{ |
let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
prefix + "*" : prefix; |
let elements = scopedQuerySelectorAll(subtree, actualPrefix); |
if (elements) |
{ |
@@ -338,20 +372,30 @@ |
if (lastRoot && lastRoot.contains(element)) |
{ |
yield null; |
continue; |
} |
lastRoot = element; |
+ if (targets && !targets.some(target => element.contains(target) || |
+ target.contains(element))) |
+ { |
+ yield null; |
+ continue; |
+ } |
+ |
if (this._regexp && this._regexp.test(element.textContent)) |
yield element; |
else |
yield null; |
+ |
+ if (debugInfo) |
+ debugInfo.lastProcessedElements.add(element); |
} |
} |
} |
}; |
function PropsSelector(propertyExpression) |
{ |
let regexpString; |
@@ -383,17 +427,17 @@ |
} |
let idx = subSelector.lastIndexOf("::"); |
if (idx != -1) |
subSelector = subSelector.substr(0, idx); |
yield prefix + subSelector; |
} |
}, |
- *getSelectors(prefix, subtree, styles) |
+ *getSelectors(prefix, subtree, styles, targets) |
{ |
for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
yield [selector, subtree]; |
} |
}; |
function Pattern(selectors, text) |
{ |
@@ -449,16 +493,25 @@ |
// 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) |
); |
}, |
+ get maybeContainsSiblingCombinators() |
+ { |
+ return getCachedPropertyValue( |
+ this, "_maybeContainsSiblingCombinators", |
+ () => this.selectors.some(selector => |
+ selector.maybeContainsSiblingCombinators) |
+ ); |
+ }, |
+ |
matchesMutationTypes(mutationTypes) |
{ |
let mutationTypeMatchMap = getCachedPropertyValue( |
this, "_mutationTypeMatchMap", |
() => new Map([ |
// All types of DOM-dependent patterns are affected by mutations of |
// type "childList". |
["childList", true], |
@@ -489,16 +542,41 @@ |
// "childList". |
if (types.size == 3) |
break; |
} |
return types; |
} |
+function extractMutationTargets(mutations) |
+{ |
+ if (!mutations) |
+ return null; |
+ |
+ let targets = new Set(); |
+ |
+ for (let mutation of mutations) |
+ { |
+ if (mutation.type == "childList") |
+ { |
+ // When new nodes are added, we're interested in the added nodes rather |
+ // than the parent. |
+ for (let node of mutation.addedNodes) |
+ targets.add(node); |
+ } |
+ else |
+ { |
+ targets.add(mutation.target); |
+ } |
+ } |
+ |
+ return [...targets]; |
+} |
+ |
function filterPatterns(patterns, {stylesheets, mutations}) |
{ |
if (!stylesheets && !mutations) |
return patterns.slice(); |
let mutationTypes = mutations ? extractMutationTypes(mutations) : null; |
return patterns.filter( |
@@ -616,16 +694,19 @@ |
* 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, mutations, done) |
{ |
+ if (debugInfo) |
+ debugInfo.lastProcessedElements.clear(); |
+ |
let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); |
let selectors = []; |
let selectorFilters = []; |
let elements = []; |
let elementFilters = []; |
@@ -673,16 +754,18 @@ |
{ |
if (rule.type != rule.STYLE_RULE) |
continue; |
cssStyles.push(stringifyStyle(rule)); |
} |
} |
+ let targets = extractMutationTargets(mutations); |
+ |
let pattern = null; |
let generator = null; |
let processPatterns = () => |
{ |
let cycleStart = performance.now(); |
if (!pattern) |
@@ -695,18 +778,32 @@ |
this.hideElemsFunc(elements, elementFilters); |
if (typeof done == "function") |
done(); |
return; |
} |
pattern = patterns.shift(); |
+ let evaluationTargets = targets; |
+ |
+ // If the pattern appears to contain any sibling combinators, we can't |
+ // easily optimize based on the mutation targets. Since this is a |
+ // special case, skip the optimization. By setting it to null here we |
+ // make sure we process the entire DOM. |
+ if (pattern.maybeContainsSiblingCombinators) |
+ evaluationTargets = null; |
+ |
+ // Ignore mutation targets when using style sheets, because we may have |
+ // to update all the CSS selectors. |
+ if (!this.useInlineStyles) |
+ evaluationTargets = null; |
+ |
generator = evaluate(pattern.selectors, 0, "", |
- this.document, cssStyles); |
+ this.document, cssStyles, evaluationTargets); |
} |
for (let selector of generator) |
{ |
if (selector != null) |
{ |
if (!this.useInlineStyles) |
{ |
selectors.push(selector); |
@@ -836,16 +933,34 @@ |
{ |
let stylesheet = event.target.sheet; |
if (stylesheet) |
this.queueFiltering([stylesheet]); |
}, |
observe(mutations) |
{ |
+ if (debugInfo) |
+ { |
+ // In debug mode, filter out any mutations likely done by us |
+ // (i.e. style="display: none !important"). This makes it easier to |
+ // observe how the code responds to DOM mutations. |
+ // |
+ // Note: This also creates the potential for "heisenbugs", but as long as |
+ // this is the only instance it should be manageable. |
+ mutations = mutations.filter( |
+ ({type, attributeName, target: {style: newValue}, oldValue}) => |
+ !(type == "attributes" && attributeName == "style" && |
+ newValue.display == "none" && oldValue.display != "none") |
+ ); |
+ |
+ if (mutations.length == 0) |
+ return; |
+ } |
+ |
this.queueFiltering(null, mutations); |
}, |
apply(patterns) |
{ |
this.patterns = []; |
for (let pattern of patterns) |
{ |