├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src └── index.js └── test ├── fixtures ├── .gitkeep ├── chrome-examples.js └── firefox-examples.js ├── index.js ├── integration.js ├── mocha.opts ├── regression.js ├── setup.js └── unit.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-1" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | 5 | "ecmaFeatures": { 6 | "modules": true 7 | }, 8 | 9 | "rules": { 10 | "prefer-template": [0], 11 | "comma-dangle": [0], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | src 4 | test 5 | 6 | .babelrc 7 | .eslintrc 8 | .gitignore 9 | .travis.yml 10 | CONTRIBUTING.md 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.2' 4 | - '5.5' 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## PRs and Contributions 4 | * Tests must pass. 5 | * Follow existing coding style. 6 | * If you fix a bug or add a feature, add a test. 7 | 8 | ## Issues 9 | Things that will help get your question issue looked at: 10 | * Full and runnable JS code. 11 | * Clear description of the problem or unexpected behavior. 12 | * Clear description of the expected result. 13 | * Steps you have taken to debug it yourself. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 Nicholas Clawson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # url-match-patterns [![Build Status](https://travis-ci.org/nickclaw/url-match-patterns.svg?branch=master)](https://travis-ci.org/nickclaw/url-match-patterns) 2 | 3 | A node module for testing URLs against match patterns, as defined by 4 | [Google](https://developer.chrome.com/extensions/match_patterns) and 5 | [Mozilla](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns). 6 | 7 | ### Example 8 | 9 | ```js 10 | import match from 'url-match-patterns'; 11 | 12 | // check that a url matches 13 | match('*://*/', 'https://www.example.com'); // true 14 | 15 | // or build a reusable function 16 | const matches = match('*://*/'); 17 | matches('https://www.example.com'); // true 18 | 19 | ``` 20 | 21 | ### API 22 | 23 | ##### `match(pattern, url) -> Boolean` 24 | Compares a pattern against a URL. Returns `true` if the pattern matches 25 | or `false` if it doesn't. Will throw an `AssertionError` if your pattern 26 | is invalid. 27 | 28 | ##### `match(pattern) -> Function` 29 | Create a reusable match function for a pattern. This will be faster since 30 | it only has to initialize once. Will throw an `AssertionError` if your pattern 31 | is invalid. 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-match-patterns", 3 | "version": "0.2.0", 4 | "description": "A module for testing URLs against Chrome and Firefox match patterns.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "rm -rf lib && babel src --out-dir lib", 8 | "lint": "eslint src", 9 | "pretest": "npm run lint", 10 | "test": "mocha test", 11 | "prepublish": "in-publish && npm run test && npm run build || not-in-publish", 12 | "publish:major": "npm version major && npm publish", 13 | "publish:minor": "npm version minor && npm publish", 14 | "publish:patch": "npm version patch && npm publish", 15 | "postpublish": "git push origin master --tags" 16 | }, 17 | "repository": "https://github.com/nickclaw/url-match-patterns", 18 | "bugs": { 19 | "url": "http://github.com/nickclaw/url-match-patterns/issues" 20 | }, 21 | "keywords": [ 22 | "match", 23 | "patterns", 24 | "chrome", 25 | "firefox", 26 | "extension", 27 | "WebExtensions", 28 | "url" 29 | ], 30 | "author": "Nicholas Clawson (nickclaw.com)", 31 | "license": "MIT", 32 | "dependencies": { 33 | "lodash": "^4.3.0" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.4.5", 37 | "babel-eslint": "^7.1.1", 38 | "babel-preset-es2015": "^6.3.13", 39 | "babel-preset-stage-1": "^6.3.13", 40 | "babel-register": "^6.4.3", 41 | "chai": "^3.4.1", 42 | "eslint": "^3.16.1", 43 | "eslint-config-airbnb": "^14.1.0", 44 | "eslint-plugin-import": "^2.2.0", 45 | "eslint-plugin-jsx-a11y": "^4.0.0", 46 | "eslint-plugin-react": "^6.10.0", 47 | "in-publish": "^2.0.0", 48 | "mocha": "^3.2.0", 49 | "sinon": "^1.17.2", 50 | "sinon-chai": "^2.8.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import startsWith from 'lodash/startsWith'; 3 | 4 | const ALL_SCHEMES = {}; 5 | 6 | function getParts(pattern) { 7 | if (pattern === '') { 8 | return { 9 | scheme: ALL_SCHEMES, 10 | host: '*', 11 | path: '*', 12 | }; 13 | } 14 | 15 | const matchScheme = '(\\*|http|https|file|ftp)'; 16 | const matchHost = '(\\*|(?:\\*\\.)?(?:[^/*]+))?'; 17 | const matchPath = '(.*)?'; 18 | const regex = new RegExp( 19 | '^' 20 | + matchScheme 21 | + '://' 22 | + matchHost 23 | + '(/)' 24 | + matchPath 25 | + '$' 26 | ); 27 | 28 | const result = regex.exec(pattern); 29 | assert(result, 'Invalid pattern'); 30 | 31 | return { 32 | scheme: result[1], 33 | host: result[2], 34 | path: result[4], 35 | }; 36 | } 37 | 38 | function createMatcher(pattern) { 39 | const parts = getParts(pattern); 40 | let str = '^'; 41 | 42 | // check scheme 43 | if (parts.scheme === ALL_SCHEMES) { 44 | str += '(http|https|ftp|file)'; 45 | } else if (parts.scheme === '*') { 46 | str += '(http|https)'; 47 | } else { 48 | str += parts.scheme; 49 | } 50 | 51 | str += '://'; 52 | 53 | // check host 54 | if (parts.host === '*') { 55 | str += '.*'; 56 | } else if (startsWith(parts.host, '*.')) { 57 | str += '.*'; 58 | str += '\\.?'; 59 | str += parts.host.substr(2).replace(/\./g, '\\.'); 60 | } else if (parts.host) { 61 | str += parts.host; 62 | } 63 | 64 | // check path 65 | if (!parts.path) { 66 | str += '/?'; 67 | } else if (parts.path) { 68 | str += '/'; 69 | str += parts.path 70 | .replace(/[?.+^${}()|[\]\\]/g, '\\$&') 71 | .replace(/\*/g, '.*'); 72 | } 73 | 74 | str += '$'; 75 | 76 | const regex = new RegExp(str); 77 | return function matchUrl(url) { 78 | return regex.test(url); 79 | }; 80 | } 81 | 82 | export default function match(pattern, optionalUrl) { 83 | const matcher = createMatcher(pattern); 84 | 85 | if (arguments.length === 2) { 86 | return matcher(optionalUrl); 87 | } 88 | 89 | return matcher; 90 | } 91 | -------------------------------------------------------------------------------- /test/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickclaw/url-match-patterns/6348d84c3b91952607919bb6fa1e2f0c2ab05a23/test/fixtures/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/chrome-examples.js: -------------------------------------------------------------------------------- 1 | // examples taken from https://developer.chrome.com/extensions/match_patterns 2 | 3 | exports.invalidPatterns = [ 4 | 5 | ]; 6 | 7 | exports.patterns = [ 8 | { 9 | pattern: 'http://*/*', 10 | accept: [ 11 | 'http://www.google.com/', 12 | 'http://example.org/foo/bar.html', 13 | ], 14 | reject: [], 15 | }, 16 | { 17 | pattern: 'http://*/foo*', 18 | accept: [ 19 | 'http://example.com/foo/bar.html', 20 | 'http://www.google.com/foo', 21 | ], 22 | reject: [], 23 | }, 24 | { 25 | pattern: 'https://*.google.com/foo*bar', 26 | accept: [ 27 | // the example here was wrong 28 | // 'http://www.google.com/foo/baz/bar', 29 | // 'http://docs.google.com/foobar', 30 | 'https://www.google.com/foo/baz/bar', 31 | 'https://docs.google.com/foobar', 32 | ], 33 | reject: [], 34 | }, 35 | { 36 | pattern: 'http://example.org/foo/bar.html', 37 | accept: [ 38 | 'http://example.org/foo/bar.html', 39 | ], 40 | reject: [], 41 | }, 42 | { 43 | pattern: 'file:///foo*', 44 | accept: [ 45 | 'file:///foo/bar.html', 46 | 'file:///foo', 47 | ], 48 | reject: [], 49 | }, 50 | { 51 | pattern: 'http://127.0.0.1/*', 52 | accept: [ 53 | 'http://127.0.0.1/', 54 | 'http://127.0.0.1/foo/bar.html', 55 | ], 56 | reject: [], 57 | }, 58 | { 59 | pattern: '*://mail.google.com/*', 60 | accept: [ 61 | 'http://mail.google.com/foo/baz/bar', 62 | 'https://mail.google.com/foobar', 63 | ], 64 | reject: [], 65 | }, 66 | { 67 | pattern: '', 68 | accept: [ 69 | 'http://example.org/foo/bar.html', 70 | 'file:///bar/baz.html', 71 | ], 72 | reject: [], 73 | }, 74 | ]; 75 | -------------------------------------------------------------------------------- /test/fixtures/firefox-examples.js: -------------------------------------------------------------------------------- 1 | // from https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns 2 | 3 | exports.invalidPatterns = [ 4 | { 5 | pattern: 'resource://path/', 6 | reason: /Invalid/, 7 | }, 8 | { 9 | pattern: 'https://mozilla.org', 10 | reason: /Invalid/, 11 | }, 12 | { 13 | pattern: 'https://mozilla.*.org/', 14 | reason: /Invalid/, 15 | }, 16 | { 17 | pattern: 'https://*zilla.org/', 18 | reason: /Invalid/, 19 | }, 20 | { 21 | pattern: 'http*://mozilla.org/', 22 | reason: /Invalid/, 23 | }, 24 | { 25 | pattern: 'file://*', 26 | reason: /Invalid/, 27 | }, 28 | ]; 29 | 30 | exports.patterns = [ 31 | { 32 | pattern: '', 33 | accept: [ 34 | 'http://example.org/', 35 | 'ftp://files.somewhere.org/', 36 | 'https://a.org/some/path/', 37 | ], 38 | reject: [ 39 | 'resource://a/b/c/', 40 | ], 41 | }, 42 | { 43 | pattern: '*://*.mozilla.org/*', 44 | accept: [ 45 | 'http://mozilla.org/', 46 | 'https://mozilla.org/', 47 | 'http://a.mozilla.org/', 48 | 'http://a.b.mozilla.org/', 49 | 'https://b.mozilla.org/path/', 50 | ], 51 | reject: [ 52 | 'ftp://mozilla.org/', 53 | 'http://mozilla.com/', 54 | 'http://firefox.org/', 55 | ], 56 | }, 57 | { 58 | pattern: '*://mozilla.org/', 59 | accept: [ 60 | 'http://mozilla.org/', 61 | 'https://mozilla.org/', 62 | ], 63 | reject: [ 64 | 'ftp://mozilla.org/', 65 | 'http://a.mozilla.org/', 66 | 'http://mozilla.org/a', 67 | ], 68 | }, 69 | { 70 | pattern: 'ftp://mozilla.org/', 71 | accept: [ 72 | 'ftp://mozilla.org', 73 | ], 74 | reject: [ 75 | 'http://mozilla.org/', 76 | 'ftp://sub.mozilla.org/', 77 | 'ftp://mozilla.org/path', 78 | ], 79 | }, 80 | { 81 | pattern: 'https://*/path', 82 | accept: [ 83 | 'https://mozilla.org/path', 84 | 'https://a.mozilla.org/path', 85 | 'https://something.com/path', 86 | ], 87 | reject: [ 88 | 'http://mozilla.org/path', 89 | 'https://mozilla.org/path/', 90 | 'https://mozilla.org/a', 91 | 'https://mozilla.org/', 92 | ], 93 | }, 94 | { 95 | pattern: 'https://*/path/', 96 | accept: [ 97 | 'https://mozilla.org/path/', 98 | 'https://a.mozilla.org/path/', 99 | 'https://something.com/path/', 100 | ], 101 | reject: [ 102 | 'http://mozilla.org/path/', 103 | 'https://mozilla.org/path', 104 | 'https://mozilla.org/a', 105 | 'https://mozilla.org/', 106 | ], 107 | }, 108 | { 109 | pattern: 'https://mozilla.org/*', 110 | accept: [ 111 | 'https://mozilla.org/', 112 | 'https://mozilla.org/path', 113 | 'https://mozilla.org/another', 114 | 'https://mozilla.org/path/to/doc', 115 | ], 116 | reject: [ 117 | 'http://mozilla.org/path', 118 | 'https://mozilla.com/path', 119 | ], 120 | }, 121 | { 122 | pattern: 'https://mozilla.org/a/b/c/', 123 | accept: [ 124 | 'https://mozilla.org/a/b/c/', 125 | ], 126 | reject: [ 127 | 128 | ], 129 | }, 130 | { 131 | pattern: 'https://mozilla.org/*/b/*/', 132 | accept: [ 133 | 'https://mozilla.org/a/b/c/', 134 | 'https://mozilla.org/d/b/f/', 135 | 'https://mozilla.org/a/b/c/d/', 136 | ], 137 | reject: [ 138 | 'https://mozilla.org/b/*/', 139 | 'https://mozilla.org/a/b/', 140 | ], 141 | }, 142 | { 143 | pattern: 'file:///blah/*', 144 | accept: [ 145 | 'file:///blah/', 146 | 'file:///blah/bleh', 147 | ], 148 | reject: [ 149 | 'file:///bleh/', 150 | ], 151 | } 152 | ]; 153 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | describe('url-match-patterns', () => { 3 | require('./unit'); 4 | require('./integration'); 5 | require('./regression'); 6 | }); 7 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | import match from '../src'; 2 | import chromeExamples from './fixtures/chrome-examples'; 3 | import firefoxExamples from './fixtures/firefox-examples'; 4 | 5 | describe('integration tests', () => { 6 | 7 | describe('from chromium examples', () => { 8 | chromeExamples.invalidPatterns.forEach(item => 9 | it(`should reject the pattern "${item.pattern}"`, () => { 10 | expect(() => match(item.pattern)).to.throw(item.reason); 11 | }) 12 | ); 13 | 14 | chromeExamples.patterns.forEach(item => 15 | describe(`for the pattern "${item.pattern}"`, () => { 16 | item.accept.forEach(url => 17 | it(`it should accept ${url}`, () => { 18 | expect(match(item.pattern, url)).to.be.true; 19 | }) 20 | ); 21 | 22 | item.reject.forEach(url => 23 | it(`it should reject ${url}`, () => { 24 | expect(match(item.pattern, url)).to.be.false; 25 | }) 26 | ); 27 | }) 28 | ); 29 | }); 30 | 31 | describe('from mozilla examples', () => { 32 | firefoxExamples.invalidPatterns.forEach(item => 33 | it(`should reject the pattern "${item.pattern}"`, () => { 34 | expect(() => match(item.pattern)).to.throw(item.reason); 35 | }) 36 | ); 37 | 38 | firefoxExamples.patterns.forEach(item => 39 | describe(`for the pattern "${item.pattern}"`, () => { 40 | item.accept.forEach(url => 41 | it(`it should accept ${url}`, () => { 42 | expect(match(item.pattern, url)).to.be.true; 43 | }) 44 | ); 45 | 46 | item.reject.forEach(url => 47 | it(`it should reject ${url}`, () => { 48 | expect(match(item.pattern, url)).to.be.false; 49 | }) 50 | ); 51 | }) 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-register 2 | --require test/setup 3 | --check-leaks 4 | --throw-deprecation 5 | -------------------------------------------------------------------------------- /test/regression.js: -------------------------------------------------------------------------------- 1 | import match from '../src'; 2 | 3 | describe('regresssion tests', () => { 4 | 5 | describe('path glob escaping', () => { 6 | it('should properly escape "?"', () => { 7 | const result = match('*://*/fo?o', 'http://example.com/fo'); 8 | expect(result).to.equal(false); 9 | }); 10 | 11 | it('should properly escape "."', () => { 12 | const result = match('*://*/f.o', 'http://example.com/foo'); 13 | expect(result).to.equal(false); 14 | }); 15 | 16 | it('should properly escape "+"', () => { 17 | const result = match('*://*/fo+', 'http://example.com/foo'); 18 | expect(result).to.equal(false); 19 | }); 20 | 21 | it('should properly escape "^"', () => { 22 | const result = match('*://*/fo^', 'http://example.com/fo^'); 23 | expect(result).to.equal(true); 24 | }); 25 | 26 | it('should properly escape "$"', () => { 27 | const result = match('*://*/fo$', 'http://example.com/fo$'); 28 | expect(result).to.equal(true); 29 | }); 30 | 31 | it('should properly escape "{" and "}"', () => { 32 | const result = match('*://*/fo{1,2}', 'http://example.com/foo'); 33 | expect(result).to.equal(false); 34 | }); 35 | 36 | it('should properly escape "("', () => { 37 | const result = match('*://*/fo(', 'http://example.com/fo('); 38 | expect(result).to.equal(true); 39 | }); 40 | 41 | it('should properly escape ")"', () => { 42 | const result = match('*://*/fo)', 'http://example.com/fo)'); 43 | expect(result).to.equal(true); 44 | }); 45 | 46 | it('should properly escape "|"', () => { 47 | const result = match('*://*/fo|a)', 'http://example.com/fo|a)'); 48 | expect(result).to.equal(true); 49 | }); 50 | 51 | it('should properly escape "[" and "]"', () => { 52 | const result = match('*://*/[fo])', 'http://example.com/[fo])'); 53 | expect(result).to.equal(true); 54 | }); 55 | 56 | it.skip('should properly escape "\\"', () => { 57 | const result = match('*://*/\.', 'http://example.com/\.'); 58 | expect(result).to.equal(true); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import sinonChai from "sinon-chai"; 4 | 5 | chai.should(); 6 | chai.use(sinonChai); 7 | global.expect = chai.expect; 8 | global.sinon = sinon; 9 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | import match from '../src'; 2 | 3 | describe('unit tests', () => { 4 | 5 | it('should compare a pattern to a url', () => { 6 | const result = match('', 'https://example.com'); 7 | expect(result).to.equal.true; 8 | }); 9 | 10 | it('should return a matching function if no url is provided', () => { 11 | const result = match(''); 12 | expect(result).to.be.instanceOf(Function); 13 | expect(result('https://example.com')).to.equal.true; 14 | }); 15 | }); 16 | --------------------------------------------------------------------------------