| 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); | 
| } | 
| }); | 
| } | 
| }; |