| Index: lib/content/elemHideEmulation.js |
| =================================================================== |
| --- a/lib/content/elemHideEmulation.js |
| +++ b/lib/content/elemHideEmulation.js |
| @@ -14,17 +14,18 @@ |
| * You should have received a copy of the GNU General Public License |
| * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| "use strict"; |
| const {filterToRegExp, splitSelector} = require("common"); |
| -const MIN_INVOCATION_INTERVAL = 3000; |
| +let MIN_INVOCATION_INTERVAL = 3000; |
| +const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; |
| const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
| /** 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) |
| { |
| @@ -32,16 +33,18 @@ |
| for (let i = 0; i < children.length; i++) |
| if (children[i] == node) |
| return i + 1; |
| return 0; |
| } |
| function makeSelector(node, selector) |
| { |
| + if (node == null) |
| + return null; |
| if (!node.parentElement) |
| { |
| let newSelector = ":root"; |
| if (selector) |
| newSelector += " > " + selector; |
| return newSelector; |
| } |
| let idx = positionInParent(node); |
| @@ -123,17 +126,26 @@ |
| { |
| 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); |
| + { |
| + if (selector == null) |
| + yield null; |
| + else |
| + yield* evaluate(chain, index + 1, selector, element, styles); |
| + } |
| + // Just in case the getSelectors() generator above had to run some heavy |
| + // document.querySelectorAll() call which didn't produce any results, make |
| + // sure there is at least one point where execution can pause. |
| + yield null; |
| } |
| function PlainSelector(selector) |
| { |
| this._selector = selector; |
| } |
| PlainSelector.prototype = { |
| @@ -183,28 +195,34 @@ |
| 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); |
| for (let selector of iter) |
| { |
| + if (selector == null) |
| + { |
| + yield null; |
| + continue; |
| + } |
| if (relativeSelectorRegexp.test(selector)) |
| selector = ":scope" + selector; |
| try |
| { |
| if (element.querySelector(selector)) |
| yield element; |
| } |
| catch (e) |
| { |
| // :scope isn't supported on Edge, ignore error caused by it. |
| } |
| } |
| + yield null; |
| } |
| } |
| }; |
| function ContainsSelector(textContent) |
| { |
| this._text = textContent; |
| } |
| @@ -218,19 +236,24 @@ |
| yield [makeSelector(element, ""), subtree]; |
| }, |
| *getElements(prefix, subtree, stylesheet) |
| { |
| 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; |
| + else |
| + yield null; |
| + } |
| } |
| }; |
| function PropsSelector(propertyExpression) |
| { |
| let regexpString; |
| if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
| propertyExpression[propertyExpression.length - 1] == "/") |
| @@ -268,23 +291,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; |
| @@ -357,30 +387,28 @@ |
| new SyntaxError("Failed to parse Adblock Plus " + |
| `selector ${selector}, can't ` + |
| "have a lonely :-abp-contains().")); |
| return null; |
| } |
| return selectors; |
| }, |
| - _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 {function} [done] |
| + * Callback to call when done. |
| */ |
| - addSelectors(stylesheets) |
| + _addSelectors(stylesheets, done) |
| { |
| - this._lastInvocation = Date.now(); |
| - |
| let selectors = []; |
| let selectorFilters = []; |
| let elements = []; |
| let elementFilters = []; |
| let cssStyles = []; |
| @@ -407,72 +435,156 @@ |
| if (rule.type != rule.STYLE_RULE) |
| continue; |
| cssStyles.push(stringifyStyle(rule)); |
| } |
| } |
| let {document} = this.window; |
| - for (let pattern of this.patterns) |
| + |
| + let patterns = this.patterns.slice(); |
| + let pattern = null; |
| + let generator = null; |
| + |
| + let processPatterns = () => |
| { |
| - if (stylesheetOnlyChange && |
| - !pattern.selectors.some(selector => selector.dependsOnStyles)) |
| - { |
| - continue; |
| - } |
| + let cycleStart = this.window.performance.now(); |
| - for (let selector of evaluate(pattern.selectors, |
| - 0, "", document, cssStyles)) |
| + if (!pattern) |
| { |
| - if (pattern.selectors.some(s => s.preferHideWithSelector) && |
| - !pattern.selectors.some(s => s.requiresHiding)) |
| + if (!patterns.length) |
| { |
| - selectors.push(selector); |
| - selectorFilters.push(pattern.text); |
| + this.addSelectorsFunc(selectors, selectorFilters); |
| + 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(); |
| } |
| - else |
| + generator = evaluate(pattern.selectors, 0, "", document, cssStyles); |
| + } |
| + for (let selector of generator) |
| + { |
| + if (selector != null) |
| { |
| - for (let element of document.querySelectorAll(selector)) |
| + if (isSelectorHidingOnlyPattern(pattern)) |
| { |
| - elements.push(element); |
| - elementFilters.push(pattern.text); |
| + selectors.push(selector); |
| + selectorFilters.push(pattern.text); |
| + } |
| + else |
| + { |
| + for (let element of document.querySelectorAll(selector)) |
| + { |
| + elements.push(element); |
| + elementFilters.push(pattern.text); |
| + } |
| } |
| } |
| + if (this.window.performance.now() - |
| + cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) |
| + { |
| + this.window.setTimeout(processPatterns, 0); |
| + return; |
| + } |
| } |
| - } |
| + pattern = null; |
| + return processPatterns(); |
| + }; |
| - this.addSelectorsFunc(selectors, selectorFilters); |
| - this.hideElemsFunc(elements, elementFilters); |
| + processPatterns(); |
| + }, |
| + |
| + // This property is only used in the tests |
| + // to shorten the invocation interval |
| + get MIN_INVOCATION_INTERVAL() |
| + { |
| + return MIN_INVOCATION_INTERVAL; |
| + }, |
| + |
| + set MIN_INVOCATION_INTERVAL(interval) |
| + { |
| + MIN_INVOCATION_INTERVAL = interval; |
| }, |
| - _stylesheetQueue: null, |
| + _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). |
| + */ |
| + queueFiltering(stylesheets) |
| + { |
| + let completion = () => |
| + { |
| + this._lastInvocation = this.window.performance.now(); |
| + this._filteringInProgress = false; |
| + if (this._scheduledProcessing) |
| + { |
| + let newStylesheets = this._scheduledProcessing.stylesheets; |
| + this._scheduledProcessing = null; |
| + this.queueFiltering(newStylesheets); |
| + } |
| + }; |
| + |
| + if (this._scheduledProcessing) |
| + { |
| + if (!stylesheets) |
| + this._scheduledProcessing.stylesheets = null; |
| + else if (this._scheduledProcessing.stylesheets) |
| + this._scheduledProcessing.stylesheets.push(...stylesheets); |
| + } |
| + else if (this._filteringInProgress) |
| + { |
| + this._scheduledProcessing = {stylesheets}; |
| + } |
| + else if (this.window.performance.now() - |
| + this._lastInvocation < MIN_INVOCATION_INTERVAL) |
| + { |
| + this._scheduledProcessing = {stylesheets}; |
| + this.window.setTimeout(() => |
| + { |
| + let newStylesheets = this._scheduledProcessing.stylesheets; |
| + this._filteringInProgress = true; |
| + this._scheduledProcessing = null; |
| + this._addSelectors(newStylesheets, completion); |
| + }, |
| + MIN_INVOCATION_INTERVAL - |
| + (this.window.performance.now() - this._lastInvocation)); |
| + } |
| + else |
| + { |
| + this._filteringInProgress = true; |
| + this._addSelectors(stylesheets, completion); |
| + } |
| + }, |
| onLoad(event) |
| { |
| let stylesheet = event.target.sheet; |
| if (stylesheet) |
| - { |
| - if (!this._stylesheetQueue && |
| - Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
| - { |
| - this._stylesheetQueue = []; |
| - this.window.setTimeout(() => |
| - { |
| - let stylesheets = this._stylesheetQueue; |
| - this._stylesheetQueue = null; |
| - this.addSelectors(stylesheets); |
| - }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); |
| - } |
| + this.queueFiltering([stylesheet]); |
| + }, |
| - if (this._stylesheetQueue) |
| - this._stylesheetQueue.push(stylesheet); |
| - else |
| - this.addSelectors([stylesheet]); |
| - } |
| + observe(mutations) |
| + { |
| + this.queueFiltering(); |
| }, |
| apply() |
| { |
| this.getFiltersFunc(patterns => |
| { |
| this.patterns = []; |
| for (let pattern of patterns) |
| @@ -480,16 +592,25 @@ |
| let selectors = this.parseSelector(pattern.selector); |
| 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.queueFiltering(); |
| + this.observer.observe( |
| + document, |
| + { |
| + childList: true, |
| + attributes: true, |
| + characterData: true, |
| + subtree: true |
| + } |
| + ); |
| document.addEventListener("load", this.onLoad.bind(this), true); |
| } |
| }); |
| } |
| }; |
| exports.ElemHideEmulation = ElemHideEmulation; |