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