| Index: lib/elemHide.js | 
| =================================================================== | 
| --- a/lib/elemHide.js | 
| +++ b/lib/elemHide.js | 
| @@ -22,17 +22,17 @@ | 
| */ | 
|  | 
| const {ElemHideException} = require("./filterClasses"); | 
| 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>>} | 
| + * @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>} | 
| */ | 
| @@ -61,33 +61,145 @@ | 
|  | 
| /** | 
| * Lookup table, lists of element hiding exceptions by selector | 
| * @type {Map.<string,Filter[]>} | 
| */ | 
| let exceptions = new Map(); | 
|  | 
| /** | 
| + * Lookup table, lists of generic element hiding exceptions by selector | 
| + * @type {Map.<string,Filter[]>} | 
| + */ | 
| +let genericExceptions = new Map(); | 
| + | 
| +/** | 
| + * List of selectors that apply on any unknown domain | 
| + * @type {?string[]} | 
| + */ | 
| +let conditionalGenericSelectors = null; | 
| + | 
| +/** | 
| * Adds a filter to the lookup table of filters by domain. | 
| * @param {Filter} filter | 
| */ | 
| function addToFiltersByDomain(filter) | 
| { | 
| let domains = filter.domains || defaultDomains; | 
| -  for (let [domain, isIncluded] of domains) | 
| +  if (filter instanceof ElemHideException) | 
| +  { | 
| +    for (let domain of domains.keys()) | 
| +    { | 
| +      // Add an entry for each domain, but without any filters. This makes | 
| +      // the domain "known" and helps us avoid certain optimizations that | 
| +      // would otherwise yield incorrect results. | 
| +      if (domain != "" && !filtersByDomain.has(domain)) | 
| +        filtersByDomain.set(domain, null); | 
| +    } | 
| +  } | 
| +  else | 
| { | 
| -    // There's no need to note that a filter is generically disabled. | 
| -    if (!isIncluded && domain == "") | 
| -      continue; | 
| +    for (let [domain, isIncluded] of domains) | 
| +    { | 
| +      // There's no need to note that a filter is generically disabled. | 
| +      if (!isIncluded && domain == "") | 
| +        continue; | 
| + | 
| +      let filters = filtersByDomain.get(domain); | 
| +      if (!filters) | 
| +        filtersByDomain.set(domain, filters = new Map()); | 
| +      filters.set(filter, isIncluded); | 
| +    } | 
| +  } | 
| +} | 
| + | 
| +/** | 
| + * Returns a list of domain-specific filters matching a domain | 
| + * @param {string} [domain] | 
| + * @returns {Array.<?Map.<Filter,boolean>>} | 
| + */ | 
| +function getSpecificFiltersForDomain(domain) | 
| +{ | 
| +  let filtersList = []; | 
| + | 
| +  if (domain) | 
| +    domain = domain.toUpperCase(); | 
| + | 
| +  while (domain) | 
| +  { | 
| +    let filters = filtersByDomain.get(domain); | 
| +    if (typeof filters != "undefined") | 
| +      filtersList.push(filters); | 
| + | 
| +    let nextDot = domain.indexOf("."); | 
| +    domain = nextDot == -1 ? null : domain.substring(nextDot + 1); | 
| +  } | 
| + | 
| +  return filtersList; | 
| +} | 
|  | 
| -    let filters = filtersByDomain.get(domain); | 
| -    if (!filters) | 
| -      filtersByDomain.set(domain, filters = new Map()); | 
| -    filters.set(filter, isIncluded); | 
| +/** | 
| + * Returns a list of selectors that apply on a domain from a given list of | 
| + * filters | 
| + * @param {string} [domain] | 
| + * @param {Array.<?Map.<Filter,boolean>>} filtersList | 
| + * @param {Set.<Filter>} excludeSet | 
| + * @returns {string[]} | 
| + */ | 
| +function matchSelectors(domain, filtersList, excludeSet) | 
| +{ | 
| +  let matches = []; | 
| + | 
| +  // This code is a performance hot-spot, which is why we've made certain | 
| +  // micro-optimisations. Please be careful before making changes. | 
| +  for (let i = 0; i < filtersList.length; i++) | 
| +  { | 
| +    let filters = filtersList[i]; | 
| +    if (filters) | 
| +    { | 
| +      for (let [filter, isIncluded] of filters) | 
| +      { | 
| +        if (!isIncluded) | 
| +        { | 
| +          excludeSet.add(filter); | 
| +        } | 
| +        else if ((excludeSet.size == 0 || !excludeSet.has(filter)) && | 
| +                  !exports.ElemHide.getException(filter, domain)) | 
| +        { | 
| +          matches.push(filter.selector); | 
| +        } | 
| +      } | 
| +    } | 
| } | 
| + | 
| +  return matches; | 
| +} | 
| + | 
| +/** | 
| + * Returns a list of selectors that apply on any unknown domain | 
| + * @returns {string[]} | 
| + */ | 
| +function getConditionalGenericSelectors() | 
| +{ | 
| +  if (conditionalGenericSelectors) | 
| +    return conditionalGenericSelectors; | 
| + | 
| +  conditionalGenericSelectors = []; | 
| + | 
| +  let filters = filtersByDomain.get(""); | 
| +  if (!filters) | 
| +    return conditionalGenericSelectors; | 
| + | 
| +  for (let {selector} of filters.keys()) | 
| +  { | 
| +    if (genericExceptions.size == 0 || !genericExceptions.has(selector)) | 
| +      conditionalGenericSelectors.push(selector); | 
| +  } | 
| + | 
| +  return conditionalGenericSelectors; | 
| } | 
|  | 
| /** | 
| * Returns a list of selectors that apply on each website unconditionally. | 
| * @returns {string[]} | 
| */ | 
| function getUnconditionalSelectors() | 
| { | 
| @@ -103,42 +215,59 @@ | 
| */ | 
| exports.ElemHide = { | 
| /** | 
| * Removes all known filters | 
| */ | 
| clear() | 
| { | 
| for (let collection of [filtersByDomain, filterBySelector, | 
| -                            knownFilters, exceptions]) | 
| +                            knownFilters, exceptions, | 
| +                            genericExceptions]) | 
| { | 
| collection.clear(); | 
| } | 
| unconditionalSelectors = null; | 
| +    conditionalGenericSelectors = null; | 
| FilterNotifier.emit("elemhideupdate"); | 
| }, | 
|  | 
| /** | 
| * Add a new element hiding filter | 
| * @param {ElemHideBase} filter | 
| */ | 
| add(filter) | 
| { | 
| if (knownFilters.has(filter)) | 
| return; | 
|  | 
| +    conditionalGenericSelectors = null; | 
| + | 
| if (filter instanceof ElemHideException) | 
| { | 
| -      let {selector} = filter; | 
| +      let {selector, domains} = filter; | 
| + | 
| let list = exceptions.get(selector); | 
| if (list) | 
| list.push(filter); | 
| else | 
| exceptions.set(selector, [filter]); | 
|  | 
| +      if (domains) | 
| +        addToFiltersByDomain(filter); | 
| + | 
| +      if (filter.isGeneric()) | 
| +      { | 
| +        list = genericExceptions.get(selector); | 
| +        if (list) | 
| +          list.push(filter); | 
| +        else | 
| +          genericExceptions.set(selector, [filter]); | 
| +      } | 
| + | 
| // 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) | 
| { | 
| addToFiltersByDomain(unconditionalFilterForSelector); | 
| filterBySelector.delete(selector); | 
| @@ -165,23 +294,38 @@ | 
| * Removes an element hiding filter | 
| * @param {ElemHideBase} filter | 
| */ | 
| remove(filter) | 
| { | 
| if (!knownFilters.has(filter)) | 
| return; | 
|  | 
| +    conditionalGenericSelectors = null; | 
| + | 
| // Whitelisting filters | 
| if (filter instanceof ElemHideException) | 
| { | 
| let list = exceptions.get(filter.selector); | 
| let index = list.indexOf(filter); | 
| if (index >= 0) | 
| list.splice(index, 1); | 
| + | 
| +      if (filter.isGeneric()) | 
| +      { | 
| +        list = genericExceptions.get(filter.selector); | 
| +        index = list.indexOf(filter); | 
| +        if (index >= 0) | 
| +          list.splice(index, 1); | 
| + | 
| +        // It's important to delete the entry here so the selector no longer | 
| +        // appears to have any generic exceptions. | 
| +        if (list.length == 0) | 
| +          genericExceptions.delete(filter.selector); | 
| +      } | 
| } | 
| // Unconditially applied element hiding filters | 
| else if (filterBySelector.get(filter.selector) == filter) | 
| { | 
| filterBySelector.delete(filter.selector); | 
| unconditionalSelectors = null; | 
| } | 
| // Conditionally applied element hiding filters | 
| @@ -226,50 +370,32 @@ | 
| * 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) | 
| { | 
| -    let selectors = []; | 
| +    let specificFilters = getSpecificFiltersForDomain(domain); | 
| + | 
| +    // If there are no specific filters (nor any specific exceptions), we can | 
| +    // just return the selectors from all the generic filters modulo any | 
| +    // generic exceptions. | 
| +    if (specificFilters.length == 0) | 
| +    { | 
| +      return specificOnly ? [] : | 
| +               getUnconditionalSelectors() | 
| +               .concat(getConditionalGenericSelectors()); | 
| +    } | 
|  | 
| let excluded = new Set(); | 
| -    let currentDomain = domain ? domain.toUpperCase() : ""; | 
| +    let selectors = matchSelectors(domain, specificFilters, excluded); | 
|  | 
| -    // 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; | 
| +    if (specificOnly) | 
| +      return selectors; | 
|  | 
| -      let filters = filtersByDomain.get(currentDomain); | 
| -      if (filters) | 
| -      { | 
| -        for (let [filter, isIncluded] of filters) | 
| -        { | 
| -          if (!isIncluded) | 
| -          { | 
| -            excluded.add(filter); | 
| -          } | 
| -          else if ((excluded.size == 0 || !excluded.has(filter)) && | 
| -                   !this.getException(filter, domain)) | 
| -          { | 
| -            selectors.push(filter.selector); | 
| -          } | 
| -        } | 
| -      } | 
| - | 
| -      if (currentDomain == "") | 
| -        break; | 
| - | 
| -      let nextDot = currentDomain.indexOf("."); | 
| -      currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1); | 
| -    } | 
| - | 
| -    if (!specificOnly) | 
| -      selectors = getUnconditionalSelectors().concat(selectors); | 
| - | 
| -    return selectors; | 
| +    return getUnconditionalSelectors() | 
| +           .concat(selectors, | 
| +                   matchSelectors(domain, [filtersByDomain.get("")], | 
| +                                  excluded)); | 
| } | 
| }; | 
|  |