 Issue 29728690:
  Issue 6504, 6458, 6446 - Update selectors based on DOM mutations  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore/
    
  
    Issue 29728690:
  Issue 6504, 6458, 6446 - Update selectors based on DOM mutations  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore/| Index: lib/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/lib/content/elemHideEmulation.js | 
| +++ b/lib/content/elemHideEmulation.js | 
| @@ -469,23 +469,177 @@ | 
| return patterns.some(pattern => pattern.maybeDependsOnAttributes); | 
| } | 
| function shouldObserveCharacterData(patterns) | 
| { | 
| return patterns.some(pattern => pattern.dependsOnCharacterData); | 
| } | 
| +function CSSSelectorManager(root) | 
| +{ | 
| + this._root = root; | 
| + | 
| + this.selectors = []; | 
| + this.filters = []; | 
| + | 
| + this._lookup = new Set(); | 
| + | 
| + this._extended = false; | 
| +} | 
| + | 
| +CSSSelectorManager.prototype = { | 
| + add(selectors, filters) | 
| + { | 
| + for (let i = 0; i < selectors.length; i++) | 
| + { | 
| + this.selectors.push(selectors[i]); | 
| + this.filters.push(filters[i]); | 
| + | 
| + this._lookup.add(selectors[i]); | 
| + | 
| + if (!this._extended && selectors[i].startsWith(":root > ")) | 
| + this._extended = true; | 
| + } | 
| + }, | 
| + | 
| + has(selector) | 
| + { | 
| + return this._lookup.has(selector); | 
| + }, | 
| + | 
| + setAt(index, selector) | 
| + { | 
| + this._lookup.delete(this.selectors[index]); | 
| + this._lookup.add(selector); | 
| + | 
| + this.selectors[index] = selector; | 
| + }, | 
| + | 
| + deleteAt(index) | 
| + { | 
| + this._lookup.delete(this.selectors[index]); | 
| + | 
| + this.selectors.splice(index, 1); | 
| + this.filters.splice(index, 1); | 
| + }, | 
| + | 
| + update(mutations) | 
| + { | 
| + // If there are no extended CSS selectors (i.e. :-abp-*), there's nothing | 
| + // to update. | 
| + if (!this._extended || this.selectors.length == 0) | 
| 
Manish Jethani
2018/03/21 15:35:26
This is useful. In EasyList the number of sites wi
 | 
| + return false; | 
| + | 
| + let changed = false; | 
| + | 
| + for (let mutation of mutations) | 
| + { | 
| + if (this._updateForMutation(mutation)) | 
| + changed = true; | 
| + } | 
| + | 
| + return changed; | 
| + }, | 
| + | 
| + _updateForMutation(mutation) | 
| + { | 
| + let {target} = mutation; | 
| + | 
| + // Existing selectors aren't affected by non-childList mutations. | 
| + if (mutation.type != "childList" || !this._root.contains(target)) | 
| + return false; | 
| + | 
| + // Find the mutation index, i.e. the index in the parent element where the | 
| + // mutation has occurred. | 
| + let index = 0; | 
| + let ref = mutation.previousSibling; | 
| + if (ref) | 
| + { | 
| + if (!(ref instanceof Element)) | 
| + ref = ref.previousElementSibling; | 
| + | 
| + if (ref) | 
| + index = positionInParent(ref); | 
| + } | 
| + | 
| + // Count the number of elements added and removed. Both can be non-zero in | 
| + // some cases, e.g. when Element.innerHTML is set on the parent element to | 
| + // overwrite all the existing child nodes. | 
| + let numAddedElements = 0; | 
| + let numRemovedElements = 0; | 
| + | 
| + for (let node of mutation.addedNodes) | 
| + { | 
| + if (node instanceof Element) | 
| + numAddedElements++; | 
| + } | 
| + | 
| + for (let node of mutation.removedNodes) | 
| + { | 
| + if (node instanceof Element) | 
| + numRemovedElements++; | 
| + } | 
| + | 
| + let changed = false; | 
| + | 
| + // Go through all the selectors and find any that match the mutation target. | 
| + let targetSelector = makeSelector(target) + " > "; | 
| + for (let i = 0; i < this.selectors.length; i++) | 
| + { | 
| + let selector = this.selectors[i]; | 
| + if (selector.startsWith(targetSelector)) | 
| + { | 
| + // If there's a match, extract the one-based child index. | 
| + let subSelector = selector.substring(targetSelector.length); | 
| + let match = /^[^:]*:nth-child\((\d+)\)/.exec(subSelector); | 
| + let nthIndex = match ? +match[1] : 0; | 
| + | 
| + // If the child being targeted by the selector is before the mutation | 
| + // index (i.e. the one-based child index is not affected), do nothing. | 
| + if (nthIndex <= index) | 
| + continue; | 
| + | 
| + nthIndex -= numRemovedElements; | 
| + | 
| + // After adjusting for the number of removed elements, if the one-based | 
| + // child index appears to be lower than the mutation index, it means the | 
| + // child got removed. In that case we must delete the selector. | 
| + if (nthIndex <= index) | 
| + { | 
| + this.deleteAt(i--); | 
| + changed = true; | 
| + continue; | 
| + } | 
| + | 
| + nthIndex += numAddedElements; | 
| + | 
| + // If the one-based child index has changed, we must update the | 
| + // selector. | 
| + if (nthIndex != +match[1]) | 
| + { | 
| + this.setAt(i, targetSelector + | 
| + subSelector.replace(match[1], nthIndex)); | 
| + changed = true; | 
| + } | 
| + } | 
| + } | 
| + | 
| + return changed; | 
| + } | 
| +}; | 
| + | 
| function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) | 
| { | 
| this.document = document; | 
| this.addSelectorsFunc = addSelectorsFunc; | 
| this.hideElemsFunc = hideElemsFunc; | 
| this.observer = new MutationObserver(this.observe.bind(this)); | 
| this.useInlineStyles = true; | 
| + this.css = new CSSSelectorManager(this.document); | 
| } | 
| ElemHideEmulation.prototype = { | 
| isSameOrigin(stylesheet) | 
| { | 
| try | 
| { | 
| return new URL(stylesheet.href).origin == this.document.location.origin; | 
| @@ -574,16 +728,19 @@ | 
| * then and all rules, including the ones not dependent on the DOM. | 
| * @param {function} [done] | 
| * Callback to call when done. | 
| */ | 
| _addSelectors(stylesheets, mutations, done) | 
| { | 
| let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); | 
| + if (patterns.length == 0) | 
| + return; | 
| + | 
| let selectors = []; | 
| let selectorFilters = []; | 
| let elements = []; | 
| let elementFilters = []; | 
| let cssStyles = []; | 
| @@ -628,19 +785,24 @@ | 
| { | 
| let cycleStart = performance.now(); | 
| if (!pattern) | 
| { | 
| if (!patterns.length) | 
| { | 
| if (selectors.length > 0) | 
| - this.addSelectorsFunc(selectors, selectorFilters); | 
| + { | 
| + this.css.add(selectors, selectorFilters); | 
| + this.addSelectorsFunc(this.css.selectors, this.css.filters); | 
| + } | 
| + | 
| if (elements.length > 0) | 
| this.hideElemsFunc(elements, elementFilters); | 
| + | 
| if (typeof done == "function") | 
| done(); | 
| return; | 
| } | 
| pattern = patterns.shift(); | 
| generator = evaluate(pattern.selectors, 0, "", | 
| @@ -648,18 +810,21 @@ | 
| } | 
| for (let selector of generator) | 
| { | 
| if (selector != null) | 
| { | 
| if (!this.useInlineStyles || | 
| pattern.isSelectorHidingOnlyPattern()) | 
| { | 
| - selectors.push(selector); | 
| - selectorFilters.push(pattern.text); | 
| + if (!this.css.has(selector)) | 
| + { | 
| + selectors.push(selector); | 
| + selectorFilters.push(pattern.text); | 
| + } | 
| } | 
| else | 
| { | 
| for (let element of this.document.querySelectorAll(selector)) | 
| { | 
| elements.push(element); | 
| elementFilters.push(pattern.text); | 
| } | 
| @@ -779,16 +944,19 @@ | 
| { | 
| let stylesheet = event.target.sheet; | 
| if (stylesheet) | 
| this.queueFiltering([stylesheet]); | 
| }, | 
| observe(mutations) | 
| { | 
| + if (this.css.update(mutations)) | 
| + this.addSelectorsFunc(this.css.selectors, this.css.filters); | 
| 
hub
2018/03/23 15:54:25
My concern here is that the intent of queueFilteri
 
Manish Jethani
2018/03/23 16:55:33
The thing is that I'm trying to avoid running full
 | 
| + | 
| this.queueFiltering(null, mutations); | 
| }, | 
| apply(patterns) | 
| { | 
| this.patterns = []; | 
| for (let pattern of patterns) | 
| { |