| Index: lib/content/elemHideEmulation.js |
| =================================================================== |
| --- a/lib/content/elemHideEmulation.js |
| +++ b/lib/content/elemHideEmulation.js |
| @@ -239,16 +239,17 @@ |
| function HasSelector(selectors) |
| { |
| this._innerSelectors = selectors; |
| } |
| HasSelector.prototype = { |
| requiresHiding: true, |
| + dependsOnDOM: true, |
| get dependsOnStyles() |
| { |
| return this._innerSelectors.some(selector => selector.dependsOnStyles); |
| }, |
| get dependsOnCharacterData() |
| { |
| @@ -301,16 +302,17 @@ |
| function ContainsSelector(textContent) |
| { |
| this._regexp = makeRegExpParameter(textContent); |
| } |
| ContainsSelector.prototype = { |
| requiresHiding: true, |
| + dependsOnDOM: true, |
| dependsOnCharacterData: true, |
| *getSelectors(prefix, subtree, styles) |
| { |
| for (let element of this.getElements(prefix, subtree, styles)) |
| yield [makeSelector(element, ""), subtree]; |
| }, |
| @@ -378,16 +380,42 @@ |
| }; |
| function isSelectorHidingOnlyPattern(pattern) |
| { |
| return pattern.selectors.some(s => s.preferHideWithSelector) && |
| !pattern.selectors.some(s => s.requiresHiding); |
| } |
| +function patternDependsOnStyles(pattern) |
| +{ |
| + return pattern.selectors.some(s => s.dependsOnStyles); |
| +} |
| + |
| +function patternDependsOnDOM(pattern) |
| +{ |
| + return pattern.selectors.some(s => s.dependsOnDOM); |
| +} |
| + |
| +function patternDependsOnStylesAndDOM(pattern) |
| +{ |
| + return pattern.selectors.some(s => s.dependsOnStyles && s.dependsOnDOM); |
| +} |
| + |
| +function filterPatterns(patterns, {stylesheets, mutations}) |
| +{ |
| + if (stylesheets && !mutations) |
| + return patterns.filter(patternDependsOnStyles); |
| + |
| + if (!stylesheets && mutations) |
| + return patterns.filter(patternDependsOnDOM); |
| + |
| + return patterns.slice(); |
| +} |
| + |
| function shouldObserveAttributes(patterns) |
| { |
| // Observe changes to attributes if either there's a plain selector that |
| // looks like an ID selector, class selector, or attribute selector in one of |
| // the patterns (e.g. "a[href='https://example.com/']") |
| // or there's a properties selector nested inside a has selector |
| // (e.g. "div:-abp-has(:-abp-properties(color: blue))") |
| return patterns.some( |
| @@ -497,34 +525,51 @@ |
| /** |
| * 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 {MutationRecord[]} [mutations] |
| + * The list of DOM mutations that have been applied to the document and |
| + * 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, done) |
| + _addSelectors(stylesheets, mutations, done) |
| { |
| + let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); |
| + |
| let selectors = []; |
| let selectorFilters = []; |
| let elements = []; |
| let elementFilters = []; |
| let cssStyles = []; |
| - let stylesheetOnlyChange = !!stylesheets; |
| - if (!stylesheets) |
| + // If neither any style sheets nor any DOM mutations have been specified, |
| + // do full processing. |
| + if (!stylesheets && !mutations) |
| stylesheets = this.document.styleSheets; |
| - for (let stylesheet of stylesheets) |
| + // If there are any DOM mutations and any of the patterns depends on both |
| + // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the |
| + // rules in every style sheet in the document, because we need to run |
| + // querySelectorAll afterwards. On the other hand, if we only have patterns |
| + // that depend on either styles or DOM both not both |
| + // (e.g. -abp-properties or -abp-contains), we can skip this part. |
| + if (mutations && patterns.some(patternDependsOnStylesAndDOM)) |
| + stylesheets = this.document.styleSheets; |
| + |
| + for (let stylesheet of stylesheets || []) |
| { |
| // Explicitly ignore third-party stylesheets to ensure consistent behavior |
| // between Firefox and Chrome. |
| if (!this.isSameOrigin(stylesheet)) |
| continue; |
| let rules = stylesheet.cssRules; |
| if (!rules) |
| @@ -534,17 +579,16 @@ |
| { |
| if (rule.type != rule.STYLE_RULE) |
| continue; |
| cssStyles.push(stringifyStyle(rule)); |
| } |
| } |
| - let patterns = this.patterns.slice(); |
| let pattern = null; |
| let generator = null; |
| let processPatterns = () => |
| { |
| let cycleStart = performance.now(); |
| if (!pattern) |
| @@ -555,22 +599,16 @@ |
| this.hideElemsFunc(elements, elementFilters); |
| if (typeof done == "function") |
| done(); |
| return; |
| } |
| pattern = patterns.shift(); |
| - if (stylesheetOnlyChange && |
| - !pattern.selectors.some(selector => selector.dependsOnStyles)) |
| - { |
| - pattern = null; |
| - return processPatterns(); |
| - } |
| generator = evaluate(pattern.selectors, 0, "", |
| this.document, cssStyles); |
| } |
| for (let selector of generator) |
| { |
| if (selector != null) |
| { |
| if (isSelectorHidingOnlyPattern(pattern)) |
| @@ -615,85 +653,103 @@ |
| _filteringInProgress: false, |
| _lastInvocation: -MIN_INVOCATION_INTERVAL, |
| _scheduledProcessing: null, |
| /** |
| * Re-run filtering either immediately or queued. |
| * @param {CSSStyleSheet[]} [stylesheets] |
| * new stylesheets to be processed. This parameter should be omitted |
| - * for DOM modification (full reprocessing required). |
| + * for full reprocessing. |
| + * @param {MutationRecord[]} [mutations] |
| + * new DOM mutations to be processed. This parameter should be omitted |
| + * for full reprocessing. |
| */ |
| - queueFiltering(stylesheets) |
| + queueFiltering(stylesheets, mutations) |
| { |
| let completion = () => |
| { |
| this._lastInvocation = performance.now(); |
| this._filteringInProgress = false; |
| if (this._scheduledProcessing) |
| { |
| - let newStylesheets = this._scheduledProcessing.stylesheets; |
| + let {stylesheets, mutations} = this._scheduledProcessing; |
| this._scheduledProcessing = null; |
| - this.queueFiltering(newStylesheets); |
| + this.queueFiltering(stylesheets, mutations); |
| } |
| }; |
| if (this._scheduledProcessing) |
| { |
| - if (!stylesheets) |
| - this._scheduledProcessing.stylesheets = null; |
| - else if (this._scheduledProcessing.stylesheets) |
| - this._scheduledProcessing.stylesheets.push(...stylesheets); |
| + if (!stylesheets && !mutations) |
| + { |
| + this._scheduledProcessing = {}; |
| + } |
| + else |
| + { |
| + if (stylesheets) |
| + { |
| + if (!this._scheduledProcessing.stylesheets) |
| + this._scheduledProcessing.stylesheets = []; |
| + this._scheduledProcessing.stylesheets.push(...stylesheets); |
| + } |
| + if (mutations) |
| + { |
| + if (!this._scheduledProcessing.mutations) |
| + this._scheduledProcessing.mutations = []; |
| + this._scheduledProcessing.mutations.push(...mutations); |
| + } |
| + } |
| } |
| else if (this._filteringInProgress) |
| { |
| - this._scheduledProcessing = {stylesheets}; |
| + this._scheduledProcessing = {stylesheets, mutations}; |
| } |
| else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
| { |
| - this._scheduledProcessing = {stylesheets}; |
| + this._scheduledProcessing = {stylesheets, mutations}; |
| setTimeout(() => |
| { |
| - let newStylesheets = this._scheduledProcessing.stylesheets; |
| + let {stylesheets, mutations} = this._scheduledProcessing; |
| this._filteringInProgress = true; |
| this._scheduledProcessing = null; |
| - this._addSelectors(newStylesheets, completion); |
| + this._addSelectors(stylesheets, mutations, completion); |
| }, |
| MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation)); |
| } |
| else if (this.document.readyState == "loading") |
| { |
| - this._scheduledProcessing = {stylesheets}; |
| + this._scheduledProcessing = {stylesheets, mutations}; |
| let handler = () => |
| { |
| document.removeEventListener("DOMContentLoaded", handler); |
| - let newStylesheets = this._scheduledProcessing.stylesheets; |
| + let {stylesheets, mutations} = this._scheduledProcessing; |
| this._filteringInProgress = true; |
| this._scheduledProcessing = null; |
| - this._addSelectors(newStylesheets, completion); |
| + this._addSelectors(stylesheets, mutations, completion); |
| }; |
| document.addEventListener("DOMContentLoaded", handler); |
| } |
| else |
| { |
| this._filteringInProgress = true; |
| - this._addSelectors(stylesheets, completion); |
| + this._addSelectors(stylesheets, mutations, completion); |
| } |
| }, |
| onLoad(event) |
| { |
| let stylesheet = event.target.sheet; |
| if (stylesheet) |
| this.queueFiltering([stylesheet]); |
| }, |
| observe(mutations) |
| { |
| - this.queueFiltering(); |
| + this.queueFiltering(null, mutations); |
| }, |
| apply(patterns) |
| { |
| this.patterns = []; |
| for (let pattern of patterns) |
| { |
| let selectors = this.parseSelector(pattern.selector); |