Index: chrome/content/elemHideEmulation.js |
=================================================================== |
--- a/chrome/content/elemHideEmulation.js |
+++ b/chrome/content/elemHideEmulation.js |
@@ -152,26 +152,27 @@ |
} |
styles.sort(); |
return { |
style: styles.join(" "), |
subSelectors: splitSelector(rule.selectorText) |
}; |
} |
-function* evaluate(chain, index, prefix, subtree, styles) |
+function* evaluate(chain, index, prefix, subtree, styles, map) |
{ |
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); |
+ chain[index].getSelectors(prefix, subtree, styles, |
+ chain.slice(index), map)) |
+ yield* evaluate(chain, index + 1, selector, element, styles, map); |
} |
function PlainSelector(selector) |
{ |
this._selector = selector; |
} |
PlainSelector.prototype = { |
@@ -199,36 +200,43 @@ |
HasSelector.prototype = { |
requiresHiding: true, |
get dependsOnStyles() |
{ |
return this._innerSelectors.some(selector => selector.dependsOnStyles); |
}, |
- *getSelectors(prefix, subtree, styles) |
+ *getSelectors(prefix, subtree, styles, chain, map) |
{ |
- for (let element of this.getElements(prefix, subtree, styles)) |
+ for (let element of this.getElements(prefix, subtree, styles, chain, map)) |
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 {Array} chain the chain of selectors including this. |
+ * @param {WeakMap} map of the elements and chain for re-evaluation. |
*/ |
- *getElements(prefix, subtree, styles) |
+ *getElements(prefix, subtree, styles, chain, map) |
{ |
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); |
+ let e = map.get(element); |
+ if (e == undefined) |
+ map.set(element, [chain]); |
+ else |
+ e.push(chain); |
+ let iter = evaluate(this._innerSelectors, 0, "", element, styles, map); |
for (let selector of iter) |
{ |
if (relativeSelector.test(selector)) |
selector = ":scope" + selector; |
if (element.querySelector(selector)) |
yield element; |
} |
} |
@@ -238,23 +246,24 @@ |
function ContainsSelector(textContent) |
{ |
this._text = textContent; |
} |
ContainsSelector.prototype = { |
requiresHiding: true, |
- *getSelectors(prefix, subtree, stylesheet) |
+ *getSelectors(prefix, subtree, stylesheet, chain, map) |
{ |
- for (let element of this.getElements(prefix, subtree, stylesheet)) |
+ for (let element of this.getElements(prefix, subtree, stylesheet, |
+ chain, map)) |
yield [makeSelector(element, ""), subtree]; |
}, |
- *getElements(prefix, subtree, stylesheet) |
+ *getElements(prefix, subtree, stylesheet, chain, map) |
{ |
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; |
} |
@@ -305,23 +314,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; |
@@ -398,39 +414,43 @@ |
new SyntaxError("Failed to parse Adblock Plus " + |
`selector ${selector}, can't ` + |
"have a lonely :-abp-contains().")); |
return null; |
} |
return selectors; |
}, |
+ _observerMap: new WeakMap(), |
+ |
_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 {boolean} [domUpdate] |
+ * Indicate this is a DOM update. |
*/ |
- addSelectors(stylesheets) |
+ addSelectors(stylesheets, domUpdate) |
{ |
this._lastInvocation = Date.now(); |
let selectors = []; |
let selectorFilters = []; |
let elements = []; |
let elementFilters = []; |
let cssStyles = []; |
- let stylesheetOnlyChange = !!stylesheets; |
+ let stylesheetOnlyChange = !!stylesheets && !domUpdate; |
if (!stylesheets) |
stylesheets = this.window.document.styleSheets; |
// Chrome < 51 doesn't have an iterable StyleSheetList |
// https://issues.adblockplus.org/ticket/5381 |
for (let i = 0; i < stylesheets.length; i++) |
{ |
let stylesheet = stylesheets[i]; |
@@ -456,21 +476,20 @@ |
for (let pattern of this.patterns) |
{ |
if (stylesheetOnlyChange && |
!pattern.selectors.some(selector => selector.dependsOnStyles)) |
{ |
continue; |
} |
- for (let selector of evaluate(pattern.selectors, |
- 0, "", document, cssStyles)) |
+ for (let selector of evaluate(pattern.selectors, 0, "", document, |
+ cssStyles, this._observerMap)) |
{ |
- if (pattern.selectors.some(s => s.preferHideWithSelector) && |
- !pattern.selectors.some(s => s.requiresHiding)) |
+ if (isSelectorHidingOnlyPattern(pattern)) |
{ |
selectors.push(selector); |
selectorFilters.push(pattern.text); |
} |
else |
{ |
for (let element of document.querySelectorAll(selector)) |
{ |
@@ -482,38 +501,96 @@ |
} |
this.addSelectorsFunc(selectors, selectorFilters); |
this.hideElemsFunc(elements, elementFilters); |
}, |
_stylesheetQueue: null, |
+ /** Filtering reason |
+ * @typedef {Object} FilteringReason |
+ * @property {boolean} dom Indicate the DOM changed (tree or attributes) |
+ * @property {CSSStyleSheet[]} [stylesheets] |
+ * Indicate the stylesheets that needs refresh |
+ * @property {WeakSet} subtrees The subtrees affected we were watching. |
+ */ |
+ |
+ /** Re-run filtering either immediately or queued. |
+ * @param {FilteringReason} reason why the filtering must be queued. |
+ */ |
+ queueFiltering(reason) |
+ { |
+ if (!this._stylesheetQueue && |
+ (Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL || |
+ reason.dom)) |
+ { |
+ this._stylesheetQueue = []; |
+ this.window.setTimeout(() => |
+ { |
+ let stylesheets = this._stylesheetQueue; |
+ this._stylesheetQueue = null; |
+ let domUpdate = reason.dom; |
hub
2017/08/03 16:28:18
I realise this should have been moved up, out of t
|
+ this.addSelectors(stylesheets, domUpdate); |
hub
2017/08/02 04:10:55
Here we don't use reason.subtree.
Actually I'm no
|
+ }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); |
+ } |
+ if (reason.stylesheets) |
+ { |
+ if (this._stylesheetQueue) |
+ this._stylesheetQueue.push(...reason.stylesheets); |
+ else |
+ this.addSelectors(reason.stylesheets); |
+ } |
+ }, |
+ |
onLoad(event) |
{ |
let stylesheet = event.target.sheet; |
if (stylesheet) |
+ this.queueFiltering({stylesheets: [stylesheet]}); |
+ }, |
+ |
+ observe(mutations) |
+ { |
+ let reason = {}; |
+ reason.dom = true; |
+ let stylesheets = []; |
+ for (let mutation of mutations) |
{ |
- if (!this._stylesheetQueue && |
- Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
+ if (mutation.type == "childList") |
{ |
- this._stylesheetQueue = []; |
- this.window.setTimeout(() => |
+ for (let added of mutation.addedNodes) |
+ { |
+ if (added.nodeType == Node.ELEMENT_NODE && |
+ (added.tagName == "STYLE" || added.tagName == "style") && |
+ added.styesheet) |
+ stylesheets.push(added.stylesheet); |
hub
2017/08/02 04:10:54
If we have a new style element, then we likely hav
|
+ } |
+ for (let removed of mutation.removedNodes) |
{ |
- let stylesheets = this._stylesheetQueue; |
- this._stylesheetQueue = null; |
- this.addSelectors(stylesheets); |
- }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); |
+ this._observerMap.delete(removed); |
+ } |
} |
- |
- if (this._stylesheetQueue) |
- this._stylesheetQueue.push(stylesheet); |
- else |
- this.addSelectors([stylesheet]); |
+ let currentNode = mutation.target; |
+ while (currentNode) |
+ { |
+ let e = this._observerMap.has(currentNode); |
+ if (e) |
+ { |
+ if (!(reason.subtrees instanceof Set)) |
+ reason.subtrees = new Set(); |
+ reason.subtrees.add(currentNode); |
+ break; |
+ } |
+ currentNode = currentNode.parentNode; |
+ } |
} |
+ if (stylesheets.length > 0) |
+ reason.stylesheets = stylesheets; |
+ this.queueFiltering(reason); |
}, |
apply() |
{ |
this.getFiltersFunc(patterns => |
{ |
this.patterns = []; |
for (let pattern of patterns) |
@@ -522,13 +599,22 @@ |
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.observer.observe( |
+ document, |
+ { |
+ childList: true, |
+ attributes: true, |
+ subtree: true, |
+ attributeFilter: ["class", "id"] |
hub
2017/08/02 04:10:54
check for the obvious class and id attributes that
|
+ } |
+ ); |
document.addEventListener("load", this.onLoad.bind(this), true); |
} |
}); |
} |
}; |