webext-patterns
Version:
Tool to convert the patterns and globs of your WebExtension manifest to regex
120 lines (119 loc) • 4.8 kB
JavaScript
// Copied from https://github.com/mozilla/gecko-dev/blob/5836a062726f715fda621338a17b51aff30d0a8c/toolkit/components/extensions/schemas/manifest.json#L729-L741
export const patternValidationRegex = /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/;
const isFirefox = globalThis.navigator?.userAgent.includes('Firefox/');
export const allStarsRegex = isFirefox
? /^(https?|wss?):[/][/][^/]+([/].*)?$/
: /^https?:[/][/][^/]+([/].*)?$/;
export const allUrlsRegex = /^(https?|file|ftp):[/]+/;
export function assertValidPattern(matchPattern) {
if (!isValidPattern(matchPattern)) {
throw new Error(matchPattern + ' is an invalid pattern. See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns for more info.');
}
}
export function isValidPattern(matchPattern) {
return matchPattern === '<all_urls>' || patternValidationRegex.test(matchPattern);
}
export function testPatterns(url, patterns) {
if (patterns.includes('<all_urls>') && allUrlsRegex.test(url)) {
return true;
}
if (patterns.includes('*://*/*') && allStarsRegex.test(url)) {
return true;
}
for (const pattern of patterns) {
if (patternToRegex(pattern).test(url)) {
return true;
}
}
return false;
}
export function getMatchingPatterns(url, patterns) {
return patterns.filter(pattern => testPatterns(url, [pattern]));
}
function getRawPatternRegex(matchPattern) {
assertValidPattern(matchPattern);
// Host undefined for file:///
let [, protocol, host = '', pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/);
protocol = protocol
.replace('*', isFirefox ? '(https?|wss?)' : 'https?') // Protocol wildcard
.replaceAll(/[/]/g, '[/]'); // Escape slashes
if (host === '*') {
host = '[^/]+';
}
host &&= host
.replace(/^[*][.]/, '([^/]+.)*') // Initial wildcard
.replaceAll(/[.]/g, '[.]') // Escape dots
.replace(/[*]$/, '[^.]+'); // Last wildcard
pathname = pathname
.replaceAll(/[/]/g, '[/]') // Escape slashes
.replaceAll(/[.]/g, '[.]') // Escape dots
.replaceAll(/[*]/g, '.*'); // Any wildcard
return '^' + protocol + host + '(' + pathname + ')?$';
}
export function patternToRegex(...matchPatterns) {
// No pattern, match nothing https://stackoverflow.com/q/14115522/288906
if (matchPatterns.length === 0) {
return /$./;
}
if (matchPatterns.includes('<all_urls>')) {
return allUrlsRegex;
}
if (matchPatterns.includes('*://*/*')) {
return allStarsRegex;
}
return new RegExp(matchPatterns.map(x => getRawPatternRegex(x)).join('|'));
}
// The parens are required by .split() to preserve the symbols
const globSymbols = /([?*]+)/;
function splitReplace(part, index) {
if (part === '') {
// Shortcut for speed
return '';
}
if (index % 2 === 0) {
// Raw text, escape it
// eslint-disable-next-line no-use-extend-native/no-use-extend-native -- TODO: Drop after https://github.com/dustinspecker/eslint-plugin-no-use-extend-native/issues/157
return RegExp.escape(part);
}
// Else: Symbol
if (part.includes('*')) { // Can be more than one and it swallows surrounding question marks
return '.*';
}
return [...part].map(() => isFirefox ? '.' : '.?').join('');
}
function getRawGlobRegex(glob) {
const regexString = glob
.split(globSymbols)
// eslint-disable-next-line unicorn/no-array-callback-reference -- tis ok 🤫
.map(splitReplace)
.join('');
// Drop "start with anything" and "end with anything" sequences because they're the default for regex
return ('^' + regexString + '$')
.replace(/^[.][*]/, '')
.replace(/[.][*]$/, '')
.replace(/^[$]$/, '.+'); // Catch `*` and `*`
}
export function globToRegex(...globs) {
// No glob, match anything; `include_globs: []` is the default
if (globs.length === 0) {
return /.*/;
}
return new RegExp(globs.map(x => getRawGlobRegex(x)).join('|'));
}
export function removeRedundantPatterns(matchPatterns) {
if (matchPatterns.includes('<all_urls>')) {
return ['<all_urls>'];
}
if (matchPatterns.includes('*://*/*')) {
return ['*://*/*'];
}
// Cover identical patterns
const uniquePatterns = [...new Set(matchPatterns)];
return uniquePatterns.filter(possibleSubset =>
// Keep if there are no matches
!uniquePatterns.some(possibleSuperset =>
// Don't compare to self
possibleSubset !== possibleSuperset
// Drop if it's a subset
&& patternToRegex(possibleSuperset).test(possibleSubset)));
}