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>} |
*/ |
@@ -56,38 +56,204 @@ |
/** |
* Set containing known element hiding and exception filters |
* @type {Set.<ElemHideBase>} |
*/ |
let knownFilters = new Set(); |
/** |
* Lookup table, lists of element hiding exceptions by selector |
- * @type {Map.<string,Filter>} |
+ * @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; |
+ |
+/** |
+ * Domains that are known not to be specifically excluded from any generic |
+ * filters |
+ * @type {Set.<string>} |
+ */ |
+let genericFriendlyDomains = new Set(); |
+ |
+/** |
* 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); |
+ } |
+ } |
+} |
+ |
+/** |
+ * Checks whether a filter applies on a domain |
+ * @param {Filter} filter |
+ * @param {string} [domain] |
+ * @param {Set.<Filter>} excludeSet |
+ * @returns {boolean} |
+ */ |
+function doesFilterApply(filter, domain, excludeSet) |
+{ |
+ return (excludeSet.size == 0 || !excludeSet.has(filter)) && |
+ !ElemHide.getException(filter, domain); |
+} |
+ |
+/** |
+ * Returns a list of domain-specific filters matching a domain |
+ * @param {string} [domain] |
+ * @returns {Array.<{domain: string, filters: ?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({domain, filters}); |
+ |
+ let nextDot = domain.indexOf("."); |
+ domain = nextDot == -1 ? null : domain.substring(nextDot + 1); |
+ } |
+ |
+ return filtersList; |
+} |
+ |
+/** |
+ * Returns a list of selectors that apply on a domain |
+ * @param {string} [domain] |
+ * @param {boolean} specificOnly |
+ * @returns {string[]} |
+ */ |
+function getConditionalSelectorsForDomain(domain, specificOnly) |
+{ |
+ let specificFilters = getSpecificFiltersForDomain(domain); |
- let filters = filtersByDomain.get(domain); |
- if (!filters) |
- filtersByDomain.set(domain, filters = new Map()); |
- filters.set(filter, isIncluded); |
+ // 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 ? [] : getConditionalGenericSelectors(); |
+ |
+ let specificSelectors = []; |
+ |
+ let excludeSet = new Set(); |
+ |
+ // 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 < specificFilters.length; i++) |
+ { |
+ for (let [filter, isIncluded] of specificFilters[i].filters || []) |
+ { |
+ if (!isIncluded) |
+ excludeSet.add(filter); |
+ else if (doesFilterApply(filter, domain, excludeSet)) |
+ specificSelectors.push(filter.selector); |
+ } |
} |
+ |
+ if (specificOnly) |
+ return specificSelectors; |
+ |
+ // We use the longest subdomain of this domain found in our data structures |
+ // as the key to check if the domain is "generic friendly." For example, |
+ // given foo.example.com, there may be an entry for example.com in our data |
+ // structures (e.g. "example.com###foo"), so we use that subdomain as the |
+ // key. This way we make only one entry and it works for all subdomains of |
+ // example.com, except those that have specific entries |
+ // (e.g. "~bar.example.com##.no-bar"). |
+ let domainKey = specificFilters[0].domain; |
+ |
+ if (genericFriendlyDomains.has(domainKey)) |
+ return specificSelectors.concat(getConditionalGenericSelectors()); |
+ |
+ let genericFilters = filtersByDomain.get(""); |
+ if (!genericFilters) |
+ return specificSelectors; |
+ |
+ let genericSelectors = []; |
+ |
+ for (let filter of genericFilters.keys()) |
+ { |
+ if (doesFilterApply(filter, domain, excludeSet)) |
+ genericSelectors.push(filter.selector); |
+ } |
+ |
+ // If the number of conditional generic selectors that apply on this domain |
+ // is the same as the total number of conditional generic selectors, the |
+ // domain is "generic friendly" (i.e. all generic filters apply, except those |
+ // with generic exceptions). In that case, we mark it is as such for faster |
+ // lookups. |
+ if (genericSelectors.length == (conditionalGenericSelectors || {}).length) |
+ genericFriendlyDomains.add(domainKey); |
+ |
+ return specificSelectors.concat(genericSelectors); |
+} |
+ |
+/** |
+ * 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 +269,60 @@ |
*/ |
let ElemHide = exports.ElemHide = { |
/** |
* Removes all known filters |
*/ |
clear() |
{ |
for (let collection of [filtersByDomain, filterBySelector, |
- knownFilters, exceptions]) |
+ knownFilters, exceptions, |
+ genericExceptions, genericFriendlyDomains]) |
{ |
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; |
+ genericFriendlyDomains.clear(); |
+ |
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 +349,39 @@ |
* Removes an element hiding filter |
* @param {ElemHideBase} filter |
*/ |
remove(filter) |
{ |
if (!knownFilters.has(filter)) |
return; |
+ conditionalGenericSelectors = null; |
+ genericFriendlyDomains.clear(); |
+ |
// 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 |
@@ -253,51 +453,19 @@ |
* @param {number} [criteria] |
* One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or |
* ElemHide.SPECIFIC_ONLY. |
* @returns {string[]} |
* List of selectors. |
*/ |
getSelectorsForDomain(domain, criteria = ElemHide.ALL_MATCHING) |
{ |
- let selectors = []; |
- |
- let specificOnly = (criteria >= ElemHide.SPECIFIC_ONLY); |
- let excluded = new Set(); |
- let currentDomain = domain ? domain.toUpperCase() : ""; |
- |
- // 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); |
- 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); |
- } |
+ let specificOnly = criteria >= ElemHide.SPECIFIC_ONLY; |
+ let selectors = getConditionalSelectorsForDomain(domain, specificOnly); |
if (criteria < ElemHide.NO_UNCONDITIONAL) |
selectors = getUnconditionalSelectors().concat(selectors); |
+ else if (criteria == ElemHide.NO_UNCONDITIONAL) |
+ selectors = selectors.slice(); |
return selectors; |
} |
}; |