| Index: lib/elemHide.js | 
| =================================================================== | 
| --- a/lib/elemHide.js | 
| +++ b/lib/elemHide.js | 
| @@ -20,177 +20,195 @@ | 
| /** | 
| * @fileOverview Element hiding implementation. | 
| */ | 
|  | 
| const {ElemHideExceptions} = require("./elemHideExceptions"); | 
| const {FilterNotifier} = require("./filterNotifier"); | 
|  | 
| /** | 
| - * Lookup table, active flag, by filter by domain. | 
| - * (Only contains filters that aren't unconditionally matched for all domains.) | 
| - * @type {Map.<string,Map.<Filter,boolean>>} | 
| - */ | 
| -let filtersByDomain = new Map(); | 
| - | 
| -/** | 
| - * Lookup table, filter by selector. (Only used for selectors that are | 
| - * unconditionally matched for all domains.) | 
| - * @type {Map.<string,Filter>} | 
| - */ | 
| -let filterBySelector = new Map(); | 
| - | 
| -/** | 
| - * This array caches the keys of filterBySelector table (selectors | 
| - * which unconditionally apply on all domains). It will be null if the | 
| - * cache needs to be rebuilt. | 
| - * @type {?string[]} | 
| - */ | 
| -let unconditionalSelectors = null; | 
| - | 
| -/** | 
| * Map to be used instead when a filter has a blank domains property. | 
| * @type {Map.<string,boolean>} | 
| * @const | 
| */ | 
| let defaultDomains = new Map([["", true]]); | 
|  | 
| /** | 
| - * Set containing known element hiding filters | 
| - * @type {Set.<ElemHideFilter>} | 
| + * Template for element hiding and element hiding emulation filter containers | 
| + * @class | 
| */ | 
| -let knownFilters = new Set(); | 
| - | 
| -/** | 
| - * Adds a filter to the lookup table of filters by domain. | 
| - * @param {Filter} filter | 
| - */ | 
| -function addToFiltersByDomain(filter) | 
| +function ElemHideTemplate() | 
| { | 
| -  let domains = filter.domains || defaultDomains; | 
| -  for (let [domain, isIncluded] of domains) | 
| -  { | 
| -    // There's no need to note that a filter is generically disabled. | 
| -    if (!isIncluded && domain == "") | 
| -      continue; | 
| +  this._filtersByDomain = new Map(); | 
| +  this._filterBySelector = new Map(); | 
| +  this._unconditionalSelectors = null; | 
| +  this._knownFilters = new Set(); | 
|  | 
| -    let filters = filtersByDomain.get(domain); | 
| -    if (!filters) | 
| -      filtersByDomain.set(domain, filters = new Map()); | 
| -    filters.set(filter, isIncluded); | 
| -  } | 
| +  ElemHideExceptions.on("added", this._onExceptionAdded.bind(this)); | 
| } | 
|  | 
| -/** | 
| - * Returns a list of selectors that apply on each website unconditionally. | 
| - * @returns {string[]} | 
| - */ | 
| -function getUnconditionalSelectors() | 
| -{ | 
| -  if (!unconditionalSelectors) | 
| -    unconditionalSelectors = [...filterBySelector.keys()]; | 
| +ElemHideTemplate.prototype = { | 
| +  /** | 
| +   * Lookup table, active flag, by filter by domain. | 
| +   * (Only contains filters that aren't unconditionally matched for all | 
| +   * domains.) | 
| +   * @type {Map.<string,Map.<Filter,boolean>>} | 
| +   */ | 
| +  _filtersByDomain: null, | 
| + | 
| +  /** | 
| +   * Lookup table, filter by selector. (Only used for selectors that are | 
| +   * unconditionally matched for all domains.) | 
| +   * @type {Map.<string,Filter>} | 
| +   */ | 
| +  _filterBySelector: null, | 
|  | 
| -  return unconditionalSelectors; | 
| -} | 
| +  /** | 
| +   * This array caches the keys of _filterBySelector table (selectors | 
| +   * which unconditionally apply on all domains). It will be null if the | 
| +   * cache needs to be rebuilt. | 
| +   * @type {?string[]} | 
| +   */ | 
| +  _unconditionalSelectors: null, | 
|  | 
| -ElemHideExceptions.on("added", ({selector}) => | 
| -{ | 
| -  // If this is the first exception for a previously unconditionally applied | 
| -  // element hiding selector we need to take care to update the lookups. | 
| -  let unconditionalFilterForSelector = filterBySelector.get(selector); | 
| -  if (unconditionalFilterForSelector) | 
| +  /** | 
| +   * Set containing known element hiding filters | 
| +   * @type {Set.<ElemHideFilter>} | 
| +   */ | 
| +  _knownFilters: null, | 
| + | 
| +  /** | 
| +   * Adds a filter to the lookup table of filters by domain. | 
| +   * @param {Filter} filter | 
| +   */ | 
| +  _addToFiltersByDomain(filter) | 
| { | 
| -    addToFiltersByDomain(unconditionalFilterForSelector); | 
| -    filterBySelector.delete(selector); | 
| -    unconditionalSelectors = null; | 
| -  } | 
| +    let domains = filter.domains || defaultDomains; | 
| +    for (let [domain, isIncluded] of domains) | 
| +    { | 
| +      // There's no need to note that a filter is generically disabled. | 
| +      if (!isIncluded && domain == "") | 
| +        continue; | 
|  | 
| -  FilterNotifier.emit("elemhideupdate"); | 
| -}); | 
| +      let filters = this._filtersByDomain.get(domain); | 
| +      if (!filters) | 
| +        this._filtersByDomain.set(domain, filters = new Map()); | 
| +      filters.set(filter, isIncluded); | 
| +    } | 
| +  }, | 
| + | 
| +  /** | 
| +   * Returns a list of selectors that apply on each website unconditionally. | 
| +   * @returns {string[]} | 
| +   */ | 
| +  _getUnconditionalSelectors() | 
| +  { | 
| +    if (!this._unconditionalSelectors) | 
| +      this._unconditionalSelectors = [...this._filterBySelector.keys()]; | 
|  | 
| -ElemHideExceptions.on("removed", () => | 
| -{ | 
| -  FilterNotifier.emit("elemhideupdate"); | 
| -}); | 
| +    return this._unconditionalSelectors; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Handles the event when a new element hiding exception has been added | 
| +   * @param {ElemHideException} exception | 
| +   */ | 
| +  _onExceptionAdded(exception) | 
| +  { | 
| +    let {selector} = exception; | 
|  | 
| -/** | 
| - * Container for element hiding filters | 
| - * @class | 
| - */ | 
| -exports.ElemHide = { | 
| +    // If this is the first exception for a previously unconditionally applied | 
| +    // element hiding selector we need to take care to update the lookups. | 
| +    let unconditionalFilterForSelector = this._filterBySelector.get(selector); | 
| +    if (unconditionalFilterForSelector) | 
| +    { | 
| +      this._addToFiltersByDomain(unconditionalFilterForSelector); | 
| +      this._filterBySelector.delete(selector); | 
| +      this._unconditionalSelectors = null; | 
| +    } | 
| +  }, | 
| + | 
| /** | 
| * Removes all known filters | 
| */ | 
| clear() | 
| { | 
| -    for (let collection of [filtersByDomain, filterBySelector, | 
| -                            knownFilters]) | 
| +    for (let collection of [this._filtersByDomain, this._filterBySelector, | 
| +                            this._knownFilters]) | 
| { | 
| collection.clear(); | 
| } | 
| -    unconditionalSelectors = null; | 
| +    this._unconditionalSelectors = null; | 
| FilterNotifier.emit("elemhideupdate"); | 
| }, | 
|  | 
| /** | 
| * Add a new element hiding filter | 
| * @param {ElemHideFilter} filter | 
| */ | 
| add(filter) | 
| { | 
| -    if (knownFilters.has(filter)) | 
| +    if (this._knownFilters.has(filter)) | 
| return; | 
|  | 
| if (!(filter.domains || ElemHideExceptions.hasExceptions(filter.selector))) | 
| { | 
| // The new filter's selector is unconditionally applied to all domains | 
| -      filterBySelector.set(filter.selector, filter); | 
| -      unconditionalSelectors = null; | 
| +      this._filterBySelector.set(filter.selector, filter); | 
| +      this._unconditionalSelectors = null; | 
| } | 
| else | 
| { | 
| // The new filter's selector only applies to some domains | 
| -      addToFiltersByDomain(filter); | 
| +      this._addToFiltersByDomain(filter); | 
| } | 
|  | 
| -    knownFilters.add(filter); | 
| +    this._knownFilters.add(filter); | 
| FilterNotifier.emit("elemhideupdate"); | 
| }, | 
|  | 
| /** | 
| * Removes an element hiding filter | 
| * @param {ElemHideFilter} filter | 
| */ | 
| remove(filter) | 
| { | 
| -    if (!knownFilters.has(filter)) | 
| +    if (!this._knownFilters.has(filter)) | 
| return; | 
|  | 
| // Unconditially applied element hiding filters | 
| -    if (filterBySelector.get(filter.selector) == filter) | 
| +    if (this._filterBySelector.get(filter.selector) == filter) | 
| { | 
| -      filterBySelector.delete(filter.selector); | 
| -      unconditionalSelectors = null; | 
| +      this._filterBySelector.delete(filter.selector); | 
| +      this._unconditionalSelectors = null; | 
| } | 
| // Conditionally applied element hiding filters | 
| else | 
| { | 
| let domains = filter.domains || defaultDomains; | 
| for (let domain of domains.keys()) | 
| { | 
| -        let filters = filtersByDomain.get(domain); | 
| +        let filters = this._filtersByDomain.get(domain); | 
| if (filters) | 
| filters.delete(filter); | 
| } | 
| } | 
|  | 
| -    knownFilters.delete(filter); | 
| +    this._knownFilters.delete(filter); | 
| FilterNotifier.emit("elemhideupdate"); | 
| -  }, | 
| +  } | 
| +}; | 
| + | 
| +exports.ElemHideTemplate = ElemHideTemplate; | 
|  | 
| +/** | 
| + * Container for element hiding filters | 
| + * @class | 
| + */ | 
| +exports.ElemHide = Object.assign(new ElemHideTemplate(), { | 
| /** | 
| * Determines from the current filter list which selectors should be applied | 
| * on a particular host name. | 
| * @param {string} domain | 
| * @param {boolean} [specificOnly] true if generic filters should not apply. | 
| * @returns {string[]} List of selectors. | 
| */ | 
| getSelectorsForDomain(domain, specificOnly = false) | 
| @@ -202,17 +220,17 @@ | 
|  | 
| // This code is a performance hot-spot, which is why we've made certain | 
| // micro-optimisations. Please be careful before making changes. | 
| while (true) | 
| { | 
| if (specificOnly && currentDomain == "") | 
| break; | 
|  | 
| -      let filters = filtersByDomain.get(currentDomain); | 
| +      let filters = this._filtersByDomain.get(currentDomain); | 
| if (filters) | 
| { | 
| for (let [filter, isIncluded] of filters) | 
| { | 
| if (!isIncluded) | 
| { | 
| excluded.add(filter); | 
| } | 
| @@ -227,13 +245,13 @@ | 
| if (currentDomain == "") | 
| break; | 
|  | 
| let nextDot = currentDomain.indexOf("."); | 
| currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1); | 
| } | 
|  | 
| if (!specificOnly) | 
| -      selectors = getUnconditionalSelectors().concat(selectors); | 
| +      selectors = this._getUnconditionalSelectors().concat(selectors); | 
|  | 
| return selectors; | 
| } | 
| -}; | 
| +}); | 
|  |