| Index: lib/content/elemHideEmulation.js |
| =================================================================== |
| --- a/lib/content/elemHideEmulation.js |
| +++ b/lib/content/elemHideEmulation.js |
| @@ -18,16 +18,24 @@ |
| "use strict"; |
| const {textToRegExp, filterToRegExp, splitSelector} = require("../common"); |
| let MIN_INVOCATION_INTERVAL = 3000; |
| const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; |
| const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
| +function getCachedPropertyValue(object, name, defaultValueFunc = () => {}) |
| +{ |
| + let value = object[name]; |
| + if (value === undefined) |
|
hub
2018/03/14 13:04:03
there is the argument of value === undefined vs ty
Manish Jethani
2018/03/14 14:54:53
Oh yeah, for the sake of consistency though I've m
|
| + Object.defineProperty(object, name, {value: value = defaultValueFunc()}); |
| + return value; |
| +} |
| + |
| /** Return position of node from parent. |
| * @param {Node} node the node to find the position of. |
| * @return {number} One-based index like for :nth-child(), or 0 on error. |
| */ |
| function positionInParent(node) |
| { |
| let {children} = node.parentNode; |
| for (let i = 0; i < children.length; i++) |
| @@ -239,16 +247,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 +310,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]; |
| }, |
| @@ -372,47 +382,104 @@ |
| *getSelectors(prefix, subtree, styles) |
| { |
| for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
| yield [selector, subtree]; |
| } |
| }; |
| -function isSelectorHidingOnlyPattern(pattern) |
| +function Pattern(selectors, text) |
| { |
| - return pattern.selectors.some(s => s.preferHideWithSelector) && |
| - !pattern.selectors.some(s => s.requiresHiding); |
| + this.selectors = selectors; |
| + this.text = text; |
| +} |
| + |
| +Pattern.prototype = { |
| + isSelectorHidingOnlyPattern() |
| + { |
| + return getCachedPropertyValue( |
| + this, "_selectorHidingOnlyPattern", |
| + () => this.selectors.some(selector => selector.preferHideWithSelector) && |
| + !this.selectors.some(selector => selector.requiresHiding) |
| + ); |
| + }, |
| + |
| + get dependsOnStyles() |
| + { |
| + return getCachedPropertyValue( |
| + this, "_dependsOnStyles", |
| + () => this.selectors.some(selector => selector.dependsOnStyles) |
| + ); |
| + }, |
| + |
| + get dependsOnDOM() |
| + { |
| + return getCachedPropertyValue( |
| + this, "_dependsOnDOM", |
| + () => this.selectors.some(selector => selector.dependsOnDOM) |
| + ); |
| + }, |
| + |
| + get dependsOnStylesAndDOM() |
| + { |
| + return getCachedPropertyValue( |
| + this, "_dependsOnStylesAndDOM", |
| + () => this.selectors.some(selector => selector.dependsOnStyles && |
| + selector.dependsOnDOM) |
| + ); |
| + }, |
| + |
| + get maybeDependsOnAttributes() |
| + { |
| + // 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 getCachedPropertyValue( |
| + this, "_maybeDependsOnAttributes", |
| + () => this.selectors.some( |
| + selector => selector.maybeDependsOnAttributes || |
| + (selector instanceof HasSelector && |
| + selector.dependsOnStyles) |
| + ) |
| + ); |
| + }, |
| + |
| + get dependsOnCharacterData() |
| + { |
| + // Observe changes to character data only if there's a contains selector in |
| + // one of the patterns. |
| + return getCachedPropertyValue( |
| + this, "_dependsOnCharacterData", |
| + () => this.selectors.some(selector => selector.dependsOnCharacterData) |
| + ); |
| + } |
| +}; |
| + |
| +function filterPatterns(patterns, {stylesheets, mutations}) |
| +{ |
| + if (!stylesheets && !mutations) |
| + return patterns.slice(); |
| + |
| + return patterns.filter( |
| + pattern => (stylesheets && pattern.dependsOnStyles) || |
| + (mutations && pattern.dependsOnDOM) |
| + ); |
| } |
| 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( |
| - pattern => pattern.selectors.some( |
| - selector => selector.maybeDependsOnAttributes || |
| - (selector instanceof HasSelector && |
| - selector.dependsOnStyles) |
| - ) |
| - ); |
| + return patterns.some(pattern => pattern.maybeDependsOnAttributes); |
| } |
| function shouldObserveCharacterData(patterns) |
| { |
| - // Observe changes to character data only if there's a contains selector in |
| - // one of the patterns. |
| - return patterns.some( |
| - pattern => pattern.selectors.some( |
| - selector => selector.dependsOnCharacterData |
| - ) |
| - ); |
| + return patterns.some(pattern => pattern.dependsOnCharacterData); |
| } |
| function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) |
| { |
| this.document = document; |
| this.addSelectorsFunc = addSelectorsFunc; |
| this.hideElemsFunc = hideElemsFunc; |
| this.observer = new MutationObserver(this.observe.bind(this)); |
| @@ -497,34 +564,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(pattern => pattern.dependsOnStylesAndDOM)) |
| + 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,51 +618,46 @@ |
| { |
| 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) |
| { |
| if (!patterns.length) |
| { |
| - this.addSelectorsFunc(selectors, selectorFilters); |
| - this.hideElemsFunc(elements, elementFilters); |
| + if (selectors.length > 0) |
|
Manish Jethani
2018/03/08 21:08:15
We need to do this because of an existing bug, whi
Manish Jethani
2018/03/08 21:28:34
Sorry, I just checked, there is no bug. In any cas
|
| + this.addSelectorsFunc(selectors, selectorFilters); |
| + if (elements.length > 0) |
| + 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)) |
| + if (pattern.isSelectorHidingOnlyPattern()) |
| { |
| selectors.push(selector); |
| selectorFilters.push(pattern.text); |
| } |
| else |
| { |
| for (let element of this.document.querySelectorAll(selector)) |
| { |
| @@ -615,95 +694,113 @@ |
| _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 params = Object.assign({}, this._scheduledProcessing); |
| this._scheduledProcessing = null; |
| - this.queueFiltering(newStylesheets); |
| + this.queueFiltering(params.stylesheets, params.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 params = Object.assign({}, this._scheduledProcessing); |
| this._filteringInProgress = true; |
| this._scheduledProcessing = null; |
| - this._addSelectors(newStylesheets, completion); |
| + this._addSelectors(params.stylesheets, params.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 params = Object.assign({}, this._scheduledProcessing); |
| this._filteringInProgress = true; |
| this._scheduledProcessing = null; |
| - this._addSelectors(newStylesheets, completion); |
| + this._addSelectors(params.stylesheets, params.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); |
| if (selectors != null && selectors.length > 0) |
| - this.patterns.push({selectors, text: pattern.text}); |
| + this.patterns.push(new Pattern(selectors, pattern.text)); |
| } |
| if (this.patterns.length > 0) |
| { |
| this.queueFiltering(); |
| this.observer.observe( |
| this.document, |
| { |