Index: lib/elemHide.js
===================================================================
--- a/lib/elemHide.js
+++ b/lib/elemHide.js
@@ -51,16 +51,23 @@
* 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;
/**
+ * The default style sheet that applies on all domains. This is based on the
+ * value of {@link unconditionalSelectors}
.
+ * @type {?string}
+ */
+let defaultStyleSheet = null;
+
+/**
* Map to be used instead when a filter has a blank domains property.
* @type {Map.}
* @const
*/
let defaultDomains = new Map([["", true]]);
/**
* Set containing known element hiding filters
@@ -95,26 +102,94 @@
function getUnconditionalSelectors()
{
if (!unconditionalSelectors)
unconditionalSelectors = [...filterBySelector.keys()];
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.
+ *
+ * @returns {Array.} The list of selectors.
+ */
+function getConditionalSelectorsForDomain(domain, specificOnly = false)
+{
+ let selectors = [];
+
+ let excluded = new Set();
+ let currentDomain = domain ? domain.replace(/\.+$/, "").toLowerCase() : "";
+
+ // 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
+ {
+ let {selector} = filter;
+ if ((excluded.size == 0 || !excluded.has(filter)) &&
+ !ElemHideExceptions.getException(selector, domain))
+ {
+ selectors.push(selector);
+ }
+ }
+ }
+ }
+
+ if (currentDomain == "")
+ break;
+
+ let nextDot = currentDomain.indexOf(".");
+ currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1);
+ }
+
+ return selectors;
+}
+
+/**
+ * Returns the default style sheet that applies on all domains.
+ * @returns {string}
+ */
+function getDefaultStyleSheet()
+{
+ if (!defaultStyleSheet)
+ defaultStyleSheet = createStyleSheet(getUnconditionalSelectors());
+
+ return defaultStyleSheet;
+}
+
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)
{
addToFiltersByDomain(unconditionalFilterForSelector);
filterBySelector.delete(selector);
unconditionalSelectors = null;
+ defaultStyleSheet = null;
}
});
/**
* Container for element hiding filters
* @class
*/
exports.ElemHide = {
@@ -122,16 +197,18 @@
* Removes all known filters
*/
clear()
{
for (let collection of [filtersByDomain, filterBySelector, knownFilters])
collection.clear();
unconditionalSelectors = null;
+ defaultStyleSheet = null;
+
filterNotifier.emit("elemhideupdate");
},
/**
* Add a new element hiding filter
* @param {ElemHideFilter} filter
*/
add(filter)
@@ -141,16 +218,17 @@
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;
}
else
{
// The new filter's selector only applies to some domains
addToFiltersByDomain(filter);
}
knownFilters.add(filter);
@@ -168,16 +246,17 @@
let {selector} = filter;
// Unconditially applied element hiding filters
if (filterBySelector.get(selector) == filter)
{
filterBySelector.delete(selector);
unconditionalSelectors = null;
+ defaultStyleSheet = null;
}
// Conditionally applied element hiding filters
else
{
let domains = filter.domains || defaultDomains;
for (let domain of domains.keys())
{
let filters = filtersByDomain.get(domain);
@@ -191,68 +270,42 @@
}
}
knownFilters.delete(filter);
filterNotifier.emit("elemhideupdate");
},
/**
- * 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.
+ * @typedef {object} ElemHideStyleSheet
+ * @property {string} code CSS code.
+ * @property {Array.} selectors List of selectors.
*/
- getSelectorsForDomain(domain, specificOnly = false)
- {
- let selectors = [];
-
- let excluded = new Set();
- let currentDomain = domain ? domain.replace(/\.+$/, "").toLowerCase() : "";
-
- // 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
- {
- let {selector} = filter;
- if ((excluded.size == 0 || !excluded.has(filter)) &&
- !ElemHideExceptions.getException(selector, domain))
- {
- selectors.push(selector);
- }
- }
- }
- }
-
- if (currentDomain == "")
- break;
-
- let nextDot = currentDomain.indexOf(".");
- currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1);
- }
+ /**
+ * Generates a style sheet for a given domain based on the current set of
+ * filters.
+ *
+ * @param {string} domain The domain.
+ * @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));
if (!specificOnly)
selectors = getUnconditionalSelectors().concat(selectors);
- return selectors;
+ return {code, selectors};
}
};
/**
* Splits a list of selectors into groups determined by the value of
* {@link selectorGroupSize}
.
*
* @param {Array.} selectors
Index: test/elemHide.js
===================================================================
--- a/test/elemHide.js
+++ b/test/elemHide.js
@@ -40,35 +40,42 @@
{Filter} = sandboxedRequire("../lib/filterClasses")
);
callback();
};
function normalizeSelectors(selectors)
{
- // getSelectorsForDomain is currently allowed to return duplicate selectors
- // for performance reasons, so we need to remove duplicates here.
- return selectors.sort().filter((selector, index, sortedSelectors) =>
+ // generateStyleSheetForDomain is currently allowed to return duplicate
+ // selectors for performance reasons, so we need to remove duplicates here.
+ return selectors.slice().sort().filter((selector, index, sortedSelectors) =>
{
return index == 0 || selector != sortedSelectors[index - 1];
});
}
function testResult(test, domain, expectedSelectors, specificOnly)
{
let normalizedExpectedSelectors = normalizeSelectors(expectedSelectors);
- test.deepEqual(
- normalizeSelectors(ElemHide.getSelectorsForDomain(domain, specificOnly)),
- normalizedExpectedSelectors
- );
+ let {code, selectors} =
+ ElemHide.generateStyleSheetForDomain(domain, specificOnly);
+
+ test.deepEqual(normalizeSelectors(selectors), normalizedExpectedSelectors);
+
+ // Make sure each expected selector is in the actual CSS code.
+ for (let selector of normalizedExpectedSelectors)
+ {
+ test.ok(code.includes(selector + ", ") ||
+ code.includes(selector + " {display: none !important;}\n"));
+ }
}
-exports.testGetSelectorsForDomain = function(test)
+exports.testGenerateStyleSheetForDomain = function(test)
{
let addFilter = filterText => ElemHide.add(Filter.fromText(filterText));
let removeFilter = filterText => ElemHide.remove(Filter.fromText(filterText));
let addException =
filterText => ElemHideExceptions.add(Filter.fromText(filterText));
let removeException =
filterText => ElemHideExceptions.remove(Filter.fromText(filterText));