| Index: lib/matcher.js |
| =================================================================== |
| --- a/lib/matcher.js |
| +++ b/lib/matcher.js |
| @@ -20,172 +20,165 @@ |
| /** |
| * @fileOverview Matcher class implementing matching addresses against |
| * a list of filters. |
| */ |
| const {WhitelistFilter} = require("./filterClasses"); |
| /** |
| + * Regular expression for matching a keyword in a filter. |
| + * @type {RegExp} |
| + */ |
| +const keywordRegExp = /[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/; |
| + |
| +/** |
| + * Regular expression for matching all keywords in a filter. |
| + * @type {RegExp} |
| + */ |
| +const allKeywordsRegExp = new RegExp(keywordRegExp, "g"); |
| + |
| +/** |
| + * Checks whether a particular filter is slow. |
| + * @param {RegExpFilter} filter |
| + * @returns {boolean} |
| + */ |
| +function isSlowFilter(filter) |
| +{ |
| + return !filter.pattern || !keywordRegExp.test(filter.pattern); |
| +} |
| + |
| +exports.isSlowFilter = isSlowFilter; |
| + |
| +/** |
| * Blacklist/whitelist filter matching |
| */ |
| class Matcher |
| { |
| constructor() |
| { |
| /** |
| * Lookup table for filters by their associated keyword |
| - * @type {Map.<string,(Filter|Filter[])>} |
| + * @type {Map.<string,(Filter|Set.<Filter>)>} |
| */ |
| this.filterByKeyword = new Map(); |
| - |
| - /** |
| - * Lookup table for keywords by the filter |
| - * @type {Map.<Filter,string>} |
| - */ |
| - this.keywordByFilter = new Map(); |
| } |
| /** |
| * Removes all known filters |
| */ |
| clear() |
| { |
| this.filterByKeyword.clear(); |
| - this.keywordByFilter.clear(); |
| } |
| /** |
| * Adds a filter to the matcher |
| * @param {RegExpFilter} filter |
| */ |
| add(filter) |
| { |
| - if (this.keywordByFilter.has(filter)) |
| - return; |
| - |
| // Look for a suitable keyword |
| let keyword = this.findKeyword(filter); |
| - let oldEntry = this.filterByKeyword.get(keyword); |
| - if (typeof oldEntry == "undefined") |
| + let set = this.filterByKeyword.get(keyword); |
| + if (typeof set == "undefined") |
| + { |
| this.filterByKeyword.set(keyword, filter); |
| - else if (oldEntry.length == 1) |
| - this.filterByKeyword.set(keyword, [oldEntry, filter]); |
| + } |
| + else if (set.size == 1) |
| + { |
| + if (filter != set) |
| + this.filterByKeyword.set(keyword, new Set([set, filter])); |
| + } |
| else |
| - oldEntry.push(filter); |
| - this.keywordByFilter.set(filter, keyword); |
| + { |
| + set.add(filter); |
| + } |
| } |
| /** |
| * Removes a filter from the matcher |
| * @param {RegExpFilter} filter |
| */ |
| remove(filter) |
| { |
| - let keyword = this.keywordByFilter.get(filter); |
| - if (typeof keyword == "undefined") |
| + let keyword = this.findKeyword(filter); |
| + let set = this.filterByKeyword.get(keyword); |
| + if (typeof set == "undefined") |
| return; |
| - let list = this.filterByKeyword.get(keyword); |
| - if (list.length <= 1) |
| - this.filterByKeyword.delete(keyword); |
| + if (set.size == 1) |
| + { |
| + if (filter == set) |
| + this.filterByKeyword.delete(keyword); |
| + } |
| else |
| { |
| - let index = list.indexOf(filter); |
| - if (index >= 0) |
| - { |
| - list.splice(index, 1); |
| - if (list.length == 1) |
| - this.filterByKeyword.set(keyword, list[0]); |
| - } |
| + set.delete(filter); |
| + |
| + if (set.size == 1) |
| + this.filterByKeyword.set(keyword, [...set][0]); |
| } |
| - |
| - this.keywordByFilter.delete(filter); |
| } |
| /** |
| * Chooses a keyword to be associated with the filter |
| * @param {Filter} filter |
| * @returns {string} keyword or an empty string if no keyword could be found |
| */ |
| findKeyword(filter) |
| { |
| let result = ""; |
| let {pattern} = filter; |
| if (pattern == null) |
| return result; |
| - let candidates = pattern.toLowerCase().match( |
| - /[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g |
| - ); |
| + let candidates = pattern.toLowerCase().match(allKeywordsRegExp); |
| if (!candidates) |
| return result; |
| let hash = this.filterByKeyword; |
| let resultCount = 0xFFFFFF; |
| let resultLength = 0; |
| for (let i = 0, l = candidates.length; i < l; i++) |
| { |
| let candidate = candidates[i].substr(1); |
| let filters = hash.get(candidate); |
| - let count = typeof filters != "undefined" ? filters.length : 0; |
| + let count = typeof filters != "undefined" ? filters.size : 0; |
| if (count < resultCount || |
| (count == resultCount && candidate.length > resultLength)) |
| { |
| result = candidate; |
| resultCount = count; |
| resultLength = candidate.length; |
| } |
| } |
| return result; |
| } |
| /** |
| - * Checks whether a particular filter is being matched against. |
| - * @param {RegExpFilter} filter |
| - * @returns {boolean} |
| - */ |
| - hasFilter(filter) |
| - { |
| - return this.keywordByFilter.has(filter); |
| - } |
| - |
| - /** |
| - * Returns the keyword used for a filter, <code>null</code> |
| - * for unknown filters. |
| - * @param {RegExpFilter} filter |
| - * @returns {?string} |
| - */ |
| - getKeywordForFilter(filter) |
| - { |
| - let keyword = this.keywordByFilter.get(filter); |
| - return typeof keyword != "undefined" ? keyword : null; |
| - } |
| - |
| - /** |
| * Checks whether the entries for a particular keyword match a URL |
| * @param {string} keyword |
| * @param {string} location |
| * @param {number} typeMask |
| * @param {string} [docDomain] |
| * @param {boolean} [thirdParty] |
| * @param {string} [sitekey] |
| * @param {boolean} [specificOnly] |
| * @returns {?Filter} |
| */ |
| _checkEntryMatch(keyword, location, typeMask, docDomain, thirdParty, sitekey, |
| specificOnly) |
| { |
| - let list = this.filterByKeyword.get(keyword); |
| - if (typeof list == "undefined") |
| + let set = this.filterByKeyword.get(keyword); |
| + if (typeof set == "undefined") |
| return null; |
| - for (let i = 0; i < list.length; i++) |
| + |
| + for (let filter of set) |
| { |
| - let filter = list[i]; |
| - |
| if (specificOnly && filter.isGeneric() && |
| !(filter instanceof WhitelistFilter)) |
| continue; |
| if (filter.matches(location, typeMask, docDomain, thirdParty, sitekey)) |
| return filter; |
| } |
| return null; |
| @@ -308,56 +301,16 @@ |
| findKeyword(filter) |
| { |
| if (filter instanceof WhitelistFilter) |
| return this.whitelist.findKeyword(filter); |
| return this.blacklist.findKeyword(filter); |
| } |
| /** |
| - * @see Matcher#hasFilter |
| - * @param {Filter} filter |
| - * @returns {boolean} |
| - */ |
| - hasFilter(filter) |
| - { |
| - if (filter instanceof WhitelistFilter) |
| - return this.whitelist.hasFilter(filter); |
| - return this.blacklist.hasFilter(filter); |
| - } |
| - |
| - /** |
| - * @see Matcher#getKeywordForFilter |
| - * @param {Filter} filter |
| - * @returns {string} keyword |
| - */ |
| - getKeywordForFilter(filter) |
| - { |
| - if (filter instanceof WhitelistFilter) |
| - return this.whitelist.getKeywordForFilter(filter); |
| - return this.blacklist.getKeywordForFilter(filter); |
| - } |
| - |
| - /** |
| - * Checks whether a particular filter is slow |
| - * @param {RegExpFilter} filter |
| - * @returns {boolean} |
| - */ |
| - isSlowFilter(filter) |
| - { |
| - let matcher = ( |
| - filter instanceof WhitelistFilter ? this.whitelist : this.blacklist |
| - ); |
| - let keyword = matcher.getKeywordForFilter(filter); |
| - if (keyword != null) |
| - return !keyword; |
| - return !matcher.findKeyword(filter); |
| - } |
| - |
| - /** |
| * Optimized filter matching testing both whitelist and blacklist matchers |
| * simultaneously. For parameters see |
| {@link Matcher#matchesAny Matcher.matchesAny()}. |
| * @see Matcher#matchesAny |
| * @inheritdoc |
| */ |
| matchesAnyInternal(location, typeMask, docDomain, thirdParty, sitekey, |
| specificOnly) |