| Index: lib/elemHide.js | 
| =================================================================== | 
| --- a/lib/elemHide.js | 
| +++ b/lib/elemHide.js | 
| @@ -58,29 +58,62 @@ | 
| /** | 
| * The default style sheet that applies on all domains. This is based on the | 
| * value of <code>{@link unconditionalSelectors}</code>. | 
| * @type {?string} | 
| */ | 
| let defaultStyleSheet = null; | 
| /** | 
| + * The common style sheet that applies on all unknown domains. This is a | 
| + * concatenation of the default style sheet and an additional style sheet based | 
| + * on selectors from all generic filters that are not in the | 
| + * <code>{@link unconditionalSelectors}</code> list. | 
| + * @type {?string} | 
| + */ | 
| +let commonStyleSheet = 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>} | 
| */ | 
| let knownFilters = new Set(); | 
| /** | 
| + * All domains known to occur in exceptions | 
| + * @type {Set.<string>} | 
| + */ | 
| +let knownExceptionDomains = new Set(); | 
| + | 
| +/** | 
| + * Returns the suffix of the given domain that is known. If no suffix is known, | 
| + * an empty string is returned. | 
| + * @param {?string} domain | 
| + * @returns {string} | 
| + */ | 
| +function getKnownSuffix(domain) | 
| +{ | 
| + while (domain && !filtersByDomain.has(domain) && | 
| + !knownExceptionDomains.has(domain)) | 
| + { | 
| + let index = domain.indexOf("."); | 
| + domain = index == -1 ? "" : domain.substring(index + 1); | 
| + } | 
| + | 
| + return domain; | 
| +} | 
| + | 
| +/** | 
| * 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) | 
| { | 
| @@ -107,27 +140,27 @@ | 
| return unconditionalSelectors; | 
| } | 
| /** | 
| * Returns the list of selectors that apply on a given domain from the subset | 
| * of filters that do not apply unconditionally on all domains. | 
| * | 
| * @param {string} domain The domain. | 
| - * @param {boolean} [specificOnly=false] Whether selectors from generic filters | 
| - * should be included. | 
| + * @param {boolean} specificOnly Whether selectors from generic filters should | 
| + * be included. | 
| * | 
| * @returns {Array.<string>} The list of selectors. | 
| */ | 
| -function getConditionalSelectorsForDomain(domain, specificOnly = false) | 
| +function getConditionalSelectors(domain, specificOnly) | 
| { | 
| let selectors = []; | 
| let excluded = new Set(); | 
| - let currentDomain = domain ? domain.replace(/\.+$/, "").toLowerCase() : ""; | 
| + let currentDomain = domain; | 
| // 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; | 
| @@ -169,18 +202,47 @@ | 
| function getDefaultStyleSheet() | 
| { | 
| if (!defaultStyleSheet) | 
| defaultStyleSheet = createStyleSheet(getUnconditionalSelectors()); | 
| return defaultStyleSheet; | 
| } | 
| -ElemHideExceptions.on("added", ({selector}) => | 
| +/** | 
| + * Returns the common style sheet that applies on all unknown domains. | 
| + * @returns {string} | 
| + */ | 
| +function getCommonStyleSheet() | 
| { | 
| + if (!commonStyleSheet) | 
| + { | 
| + commonStyleSheet = getDefaultStyleSheet() + | 
| + createStyleSheet(getConditionalSelectors("", false)); | 
| + } | 
| + | 
| + return commonStyleSheet; | 
| +} | 
| + | 
| +ElemHideExceptions.on("added", ({domains, selector}) => | 
| +{ | 
| + commonStyleSheet = null; | 
| + | 
| + if (domains) | 
| + { | 
| + for (let domain of domains.keys()) | 
| + { | 
| + // Note: Once an exception domain is known it never becomes unknown, even | 
| + // when all the exceptions containing that domain are removed. This is a | 
| + // best-case optimization. | 
| + if (domain != "") | 
| + knownExceptionDomains.add(domain); | 
| + } | 
| + } | 
| + | 
| // 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); | 
| unconditionalSelectors = null; | 
| @@ -193,34 +255,41 @@ | 
| * @class | 
| */ | 
| exports.ElemHide = { | 
| /** | 
| * Removes all known filters | 
| */ | 
| clear() | 
| { | 
| - for (let collection of [filtersByDomain, filterBySelector, knownFilters]) | 
| + commonStyleSheet = null; | 
| + | 
| + for (let collection of [filtersByDomain, filterBySelector, knownFilters, | 
| + knownExceptionDomains]) | 
| + { | 
| collection.clear(); | 
| + } | 
| unconditionalSelectors = null; | 
| defaultStyleSheet = null; | 
| filterNotifier.emit("elemhideupdate"); | 
| }, | 
| /** | 
| * Add a new element hiding filter | 
| * @param {ElemHideFilter} filter | 
| */ | 
| add(filter) | 
| { | 
| if (knownFilters.has(filter)) | 
| return; | 
| + commonStyleSheet = null; | 
| + | 
| let {selector} = filter; | 
| if (!(filter.domains || ElemHideExceptions.hasExceptions(selector))) | 
| { | 
| // The new filter's selector is unconditionally applied to all domains | 
| filterBySelector.set(selector, filter); | 
| unconditionalSelectors = null; | 
| defaultStyleSheet = null; | 
| @@ -239,16 +308,18 @@ | 
| * Removes an element hiding filter | 
| * @param {ElemHideFilter} filter | 
| */ | 
| remove(filter) | 
| { | 
| if (!knownFilters.has(filter)) | 
| return; | 
| + commonStyleSheet = null; | 
| + | 
| let {selector} = filter; | 
| // Unconditially applied element hiding filters | 
| if (filterBySelector.get(selector) == filter) | 
| { | 
| filterBySelector.delete(selector); | 
| unconditionalSelectors = null; | 
| defaultStyleSheet = null; | 
| @@ -288,22 +359,39 @@ | 
| * @param {boolean} [specificOnly=false] Whether selectors from generic | 
| * filters should be included. | 
| * | 
| * @returns {ElemHideStyleSheet} An object containing the CSS code and the | 
| * list of selectors. | 
| */ | 
| generateStyleSheetForDomain(domain, specificOnly = false) | 
| { | 
| - let selectors = getConditionalSelectorsForDomain(domain, specificOnly); | 
| - let code = specificOnly ? createStyleSheet(selectors) : | 
| - (getDefaultStyleSheet() + createStyleSheet(selectors)); | 
| + let code = null; | 
| + let selectors = null; | 
| + | 
| + if (domain[domain.length - 1] == ".") | 
| + domain = domain.replace(/\.+$/, ""); | 
| + | 
| + domain = domain.toLowerCase(); | 
| - if (!specificOnly) | 
| + if (specificOnly) | 
| + { | 
| + selectors = getConditionalSelectors(domain, true); | 
| + code = createStyleSheet(selectors); | 
| + } | 
| + else | 
| + { | 
| + let knownSuffix = getKnownSuffix(domain); | 
| + | 
| + selectors = getConditionalSelectors(knownSuffix, false); | 
| + code = knownSuffix == "" ? getCommonStyleSheet() : | 
| + (getDefaultStyleSheet() + createStyleSheet(selectors)); | 
| + | 
| selectors = getUnconditionalSelectors().concat(selectors); | 
| + } | 
| return {code, selectors}; | 
| } | 
| }; | 
| /** | 
| * Splits a list of selectors into groups determined by the value of | 
| * <code>{@link selectorGroupSize}</code>. |