UNPKG

webext-patterns

Version:

Tool to convert the patterns and globs of your WebExtension manifest to regex

120 lines (119 loc) 4.8 kB
// 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))); }