├── Makefile ├── .gitignore ├── index.js ├── lib ├── fragments.js ├── pattern.js └── querystring.js ├── .nycrc ├── parse.js ├── karma.conf.js ├── format.js ├── types.d.ts ├── karma.conf.ci.js ├── extra └── index.js ├── package.json ├── test ├── test-benchmark.js ├── test-querystring.js ├── test-format.js ├── test-extra-urlite.js └── test-parse.js ├── .kube-ci └── ci.yaml └── README.md /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | npm test 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parse: require('./parse'), 3 | format: require('./format') 4 | } 5 | -------------------------------------------------------------------------------- /lib/fragments.js: -------------------------------------------------------------------------------- 1 | module.exports = ['protocol', 'auth', 'hostname', 'port', 'pathname', 'search', 'hash'] 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "lines": 100, 4 | "statements": 100, 5 | "functions": 100, 6 | "branches": 100 7 | } 8 | -------------------------------------------------------------------------------- /lib/pattern.js: -------------------------------------------------------------------------------- 1 | module.exports = /([^:/?#]+:)?(?:(?:\/\/)(?:([^/?#]*:[^@/]+)@)?([^/:?#]+)(?:(?::)(\d+))?)?(\/?[^?#]*)?(\?[^#]*)?(#[^\s]*)?/ 2 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | var pattern = require('./lib/pattern') 2 | var fragments = require('./lib/fragments') 3 | module.exports = function parse (url) { 4 | var parts = {} 5 | parts.href = url 6 | var matches = url.match(pattern) 7 | var l = fragments.length 8 | while (l--) { parts[fragments[l]] = matches[l + 1] } 9 | parts.path = parts.search ? (parts.pathname ? parts.pathname + parts.search : parts.search) : parts.pathname 10 | return parts 11 | } 12 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | frameworks: ['mocha'], 4 | files: ['test/test*'], 5 | preprocessors: { '/**/*.js': ['webpack', 'sourcemap'] }, 6 | webpackMiddleware: { 7 | stats: 'errors-only', 8 | logLevel: 'error' 9 | }, 10 | webpack: { 11 | mode: 'development', 12 | watch: true, 13 | devtool: 'inline-source-map' 14 | }, 15 | browsers: ['Chrome'] 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /format.js: -------------------------------------------------------------------------------- 1 | var fragments = require('./lib/fragments') 2 | module.exports = function format (obj) { 3 | var result = '' 4 | var i = fragments.length 5 | while (i--) { 6 | var fragment = fragments[i] 7 | var part = obj[fragment] 8 | if (part) { 9 | if (fragment === 'protocol') { 10 | part += '//' 11 | } else if (fragment === 'auth') { 12 | part += '@' 13 | } else if (fragment === 'port') { 14 | part = ':' + part 15 | } 16 | result = part + result 17 | } 18 | } 19 | if (obj.href && obj.href.indexOf('//') === 0) result = '//' + result 20 | return result 21 | } 22 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "urlite" { 2 | export type URLDescriptor = { 3 | protocol?: string 4 | auth?: string 5 | hostname?: string 6 | port?: string 7 | path?: string 8 | pathname?: string 9 | search?: string 10 | hash?: string 11 | href?: string 12 | } 13 | export function parse(path: string): URLDescriptor 14 | export function format(obj: URLDescriptor): string 15 | } 16 | 17 | declare module "urlite/extra" { 18 | export type URLDescriptorExtra = { 19 | protocol?: string 20 | auth?: { user?: string; password?: string } 21 | hostname?: string 22 | port?: string 23 | path?: string 24 | pathname?: string 25 | search: Record 26 | hash: Record 27 | href?: string 28 | } 29 | 30 | export function parse(path: string): URLDescriptorExtra 31 | export function format(obj: URLDescriptorExtra): string 32 | } 33 | -------------------------------------------------------------------------------- /karma.conf.ci.js: -------------------------------------------------------------------------------- 1 | const karmaConfig = require('./karma.conf') 2 | const customLaunchers = { 3 | ChromeCustom: { 4 | base: 'ChromeHeadless', 5 | flags: ['--no-sandbox'] 6 | }, 7 | sl_firefox: { 8 | base: 'SauceLabs', 9 | browserName: 'firefox', 10 | version: 'latest' 11 | }, 12 | sl_ios_safari_10: { 13 | base: 'SauceLabs', 14 | browserName: 'safari', 15 | platform: 'OS X 10.11', 16 | version: '10.0' 17 | }, 18 | sl_ie_11: { 19 | base: 'SauceLabs', 20 | browserName: 'internet explorer', 21 | platform: 'Windows 8.1', 22 | version: '11' 23 | }, 24 | sl_android: { 25 | base: 'SauceLabs', 26 | browserName: 'Browser', 27 | platform: 'Android', 28 | version: '6.0', 29 | deviceName: 'Android Emulator' 30 | } 31 | } 32 | 33 | module.exports = function (config) { 34 | karmaConfig({ 35 | set: function setter (baseConfig) { 36 | config.set(sauceConfig(baseConfig)) 37 | } 38 | }) 39 | } 40 | 41 | function sauceConfig (baseConfig) { 42 | return Object.assign({}, baseConfig, { 43 | sauceLabs: { testName: 'urlite' }, 44 | customLaunchers: customLaunchers, 45 | browsers: Object.keys(customLaunchers), 46 | reporters: ['progress', 'saucelabs'], 47 | coverageReporter: null 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /extra/index.js: -------------------------------------------------------------------------------- 1 | var urlite = require('../index') 2 | var qs = require('../lib/querystring')('?') 3 | var hs = require('../lib/querystring')('#') 4 | var fragments = require('../lib/fragments') 5 | 6 | function parse (url) { 7 | var parsed = urlite.parse(url) 8 | if (parsed.auth) parsed.auth = decodeAuth(parsed.auth) 9 | parsed.search = parsed.search ? qs.parse(parsed.search) : {} 10 | parsed.hash = parsed.hash ? hs.parse(parsed.hash) : {} 11 | return parsed 12 | } 13 | 14 | function format (parsed) { 15 | var encoded = {} 16 | var i = fragments.length 17 | while (i--) { 18 | encoded[fragments[i]] = parsed[fragments[i]] 19 | } 20 | if (typeof encoded.auth !== 'string') encoded.auth = encoded.auth && encodeAuth(encoded.auth) 21 | if (typeof encoded.search !== 'string') encoded.search = encoded.search && qs.format(encoded.search) 22 | if (typeof encoded.hash !== 'string') encoded.hash = encoded.hash && hs.format(encoded.hash) 23 | return urlite.format(encoded) 24 | } 25 | 26 | module.exports = { 27 | parse: parse, 28 | format: format 29 | } 30 | 31 | function decodeAuth (auth) { 32 | var split = auth.split(':') 33 | return { 34 | user: split[0], 35 | password: split[1] 36 | } 37 | } 38 | 39 | function encodeAuth (auth) { 40 | return auth.user + ':' + auth.password 41 | } 42 | -------------------------------------------------------------------------------- /lib/querystring.js: -------------------------------------------------------------------------------- 1 | module.exports = function querystring (identifier) { 2 | function format (obj) { 3 | var result = '' 4 | for (var key in obj) { 5 | if (has(obj, key)) { 6 | var val = obj[key] 7 | if (isArray(val)) { 8 | var l = val.length 9 | for (var i = 0; i < l; i++) { result += '&' + key + '=' + val[i] } 10 | } else if ((val && val !== true) || val === 0) { 11 | result += '&' + key + '=' + val 12 | } else if (val) { 13 | result += '&' + key 14 | } 15 | } 16 | } 17 | return encodeURI(result.replace('&', identifier)) 18 | } 19 | 20 | function parse (qs) { 21 | var obj = {} 22 | var params = (qs || '').replace(new RegExp('\\' + identifier), '').split(/&|&/).map(function (part) { 23 | try { 24 | return decodeURI(part) 25 | } catch (e) { 26 | return part 27 | } 28 | }) 29 | var l = params.length 30 | for (var i = 0; i < l; i++) { 31 | if (params[i]) { 32 | var index = params[i].indexOf('=') 33 | if (index === -1) index = params[i].length 34 | var key = params[i].substring(0, index) 35 | var val = params[i].substring(index + 1) 36 | if (has(obj, key)) { 37 | if (!isArray(obj[key])) obj[key] = [obj[key]] 38 | obj[key].push(val) 39 | } else { 40 | obj[key] = val || true 41 | } 42 | } 43 | } 44 | return obj 45 | } 46 | 47 | function has (obj, key) { 48 | return obj.hasOwnProperty(key) 49 | } 50 | 51 | function isArray (thing) { 52 | return Object.prototype.toString.call(thing) === '[object Array]' 53 | } 54 | 55 | return { 56 | parse: parse, 57 | format: format 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "urlite", 3 | "version": "3.1.0", 4 | "description": "A very small, fast, dependency free url parser and formatter for nodejs and the web", 5 | "main": "index.js", 6 | "types": "types.d.ts", 7 | "directories": { 8 | "lib": "lib", 9 | "test": "test" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "benchmark": "^2.1.4", 14 | "expect.js": "^0.3.1", 15 | "fast-url-parser": "^1.1.3", 16 | "karma": "^6.3.19", 17 | "karma-chrome-launcher": "^3.1.0", 18 | "karma-mocha": "^2.0.1", 19 | "karma-sauce-launcher": "^2.0.2", 20 | "karma-sourcemap-loader": "^0.3.8", 21 | "karma-webpack": "^4.0.2", 22 | "min-url": "^1.4.0", 23 | "mocha": "^8.4.0", 24 | "nyc": "^15.1.0", 25 | "standard": "^11.0.1", 26 | "url": "^0.11.0", 27 | "url-parse": "^1.5.10", 28 | "url-parse-as-address": "^1.0.0", 29 | "urlparser": "^0.3.9", 30 | "webpack": "^4.44.1" 31 | }, 32 | "scripts": { 33 | "test": "mocha", 34 | "test-browser": "karma start --single-run", 35 | "coverage": "nyc --reporter=lcov --reporter=text-summary mocha", 36 | "test-ci": "karma start karma.conf.ci.js --single-run", 37 | "lint": "standard" 38 | }, 39 | "author": "Alan Clarke (qubit.com)", 40 | "license": "UNLICENSED", 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/QubitProducts/urlite.git" 44 | }, 45 | "keywords": [ 46 | "url", 47 | "uri", 48 | "url-parse", 49 | "url-parser", 50 | "uri-parse", 51 | "uri-parser", 52 | "querystring", 53 | "qs" 54 | ], 55 | "bugs": { 56 | "url": "https://github.com/QubitProducts/urlite/issues" 57 | }, 58 | "homepage": "https://github.com/QubitProducts/urlite#readme" 59 | } 60 | -------------------------------------------------------------------------------- /test/test-benchmark.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | var Benchmark = require('benchmark') 3 | var suite = new Benchmark.Suite() 4 | var inputs = ['https://user:pass@example.com/'] 5 | var parse = require('../parse') 6 | var url = require('url') 7 | var fastUrlParser = require('fast-url-parser') 8 | var asAddress = require('url-parse-as-address') 9 | var minUrl = require('min-url') 10 | var urlParse = require('url-parse') 11 | var urlparser = require('urlparser') 12 | var expect = require('expect.js') 13 | 14 | describe.skip('benchmarks', function () { 15 | this.timeout(0) 16 | it('should be one of the fastest', function (done) { 17 | suite 18 | .add('require("url").parse', function () { 19 | inputs.forEach(url.parse) 20 | }) 21 | .add('require("fast-url-parser").parse', function () { 22 | inputs.forEach(fastUrlParser.parse) 23 | }) 24 | .add('require("url-parse-as-address")', function () { 25 | inputs.forEach(asAddress) 26 | }) 27 | .add('require("min-url").parse', function () { 28 | inputs.forEach(minUrl.parse) 29 | }) 30 | .add('require("url-parse")', function () { 31 | inputs.forEach(urlParse) 32 | }) 33 | .add('require("urlparser").parse', function () { 34 | inputs.forEach(urlparser.parse) 35 | }) 36 | .add('require("urlite").parse', function () { 37 | inputs.forEach(parse) 38 | }) 39 | .on('cycle', function (event) { 40 | console.log(String(event.target)) 41 | }) 42 | .on('complete', function () { 43 | try { 44 | expect(this.filter('fastest').pluck('name')).to.contain('require("urlite").parse') 45 | done() 46 | } catch (e) { 47 | done(e) 48 | } 49 | }) 50 | .run({ 51 | 'async': false 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/test-querystring.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | var expect = require('expect.js') 3 | var qs = require('../lib/querystring')('?') 4 | describe('querystring', function () { 5 | describe('parse', function () { 6 | it('should parse a querystring', function () { 7 | expect(qs.parse('?a=b&b=c')).to.eql({ a: 'b', b: 'c' }) 8 | }) 9 | it('should handle html encoded entities', function () { 10 | expect(qs.parse('?a=b&b=c')).to.eql({ a: 'b', b: 'c' }) 11 | }) 12 | it('should handle arrays', function () { 13 | expect(qs.parse('?a=b&a=c&&a=d')).to.eql({ a: ['b', 'c', 'd'] }) 14 | }) 15 | it('should handle empty querystring values', function () { 16 | expect(qs.parse('?&&&')).to.eql({}) 17 | }) 18 | it('should handle empty querystring values', function () { 19 | expect(qs.parse('?a&b&c')).to.eql({ a: true, b: true, c: true }) 20 | }) 21 | it('should handle empty querystring keys', function () { 22 | expect(qs.parse('?=')).to.eql({ '': true }) 23 | }) 24 | it('should handle undefined querystring', function () { 25 | expect(qs.parse()).to.eql({}) 26 | }) 27 | it('should handle badly encoded parts', function () { 28 | expect(qs.parse('foo=%C1%A4%BB%F3+%C3%B3%B8%AE+%B5%C7%BE%FA%BD%C0%B4%CF%B4%D9&bar=boz')).to.eql({ 29 | bar: 'boz', 30 | foo: '%C1%A4%BB%F3+%C3%B3%B8%AE+%B5%C7%BE%FA%BD%C0%B4%CF%B4%D9' 31 | }) 32 | }) 33 | }) 34 | 35 | describe('format', function () { 36 | it('should format a querystring', function () { 37 | expect(qs.format({ a: 'b', b: 'c' })).to.eql('?a=b&b=c') 38 | }) 39 | it('should handle arrays', function () { 40 | expect(qs.format({ a: ['b', 'c'] })).to.eql('?a=b&a=c') 41 | }) 42 | it('should handle bools', function () { 43 | expect(qs.format({ a: true, b: false, c: 4 })).to.eql('?a&c=4') 44 | }) 45 | it('should handle empty objects', function () { 46 | expect(qs.format({})).to.eql('') 47 | }) 48 | it('should guard against things attached to the prototype', function () { 49 | var F = function () {} 50 | F.prototype.a = 'b' 51 | var f = new F() 52 | expect(qs.format(f)).to.eql('') 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /.kube-ci/ci.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Workflow 3 | metadata: 4 | annotations: 5 | kube-ci.qutics.com/cacheScope: project 6 | kube-ci.qutics.com/cacheSize: 20Gi 7 | creationTimestamp: null 8 | spec: 9 | arguments: {} 10 | entrypoint: run 11 | templates: 12 | - inputs: {} 13 | metadata: {} 14 | name: main 15 | outputs: {} 16 | steps: 17 | - - arguments: {} 18 | name: run 19 | template: run 20 | when: '{{workflow.parameters.branch}} != "gh-pages"' 21 | - container: 22 | args: 23 | - sh 24 | - -c 25 | - | 26 | set -x 27 | set -e 28 | 29 | cp /.ci-secrets/npmrc $HOME/.npmrc 30 | export XDG_CACHE_HOME=/cache/.cache 31 | export CYPRESS_CACHE_FOLDER=/cache/.cache 32 | npm set cache /cache/npm 33 | PATH=$PATH:$(pwd)/node_modules/.bin 34 | 35 | npm i 36 | npm run lint 37 | npm run coverage 38 | npm run test-ci 39 | command: 40 | - docker-entrypoint.sh 41 | env: 42 | - name: SAUCE_USERNAME 43 | valueFrom: 44 | secretKeyRef: 45 | key: SAUCE_USERNAME 46 | name: ci-secrets 47 | - name: SAUCE_ACCESS_KEY 48 | valueFrom: 49 | secretKeyRef: 50 | key: SAUCE_ACCESS_KEY 51 | name: ci-secrets 52 | image: eu.gcr.io/qubit-registry/tools/node12chrome:latest 53 | name: "" 54 | resources: {} 55 | volumeMounts: 56 | - mountPath: /.ci-secrets 57 | name: secrets 58 | - mountPath: /cache 59 | name: build-cache 60 | workingDir: /src 61 | inputs: 62 | artifacts: 63 | - git: 64 | repo: '{{workflow.parameters.repo}}' 65 | revision: '{{workflow.parameters.revision}}' 66 | sshPrivateKeySecret: 67 | key: ssh-private-key 68 | name: ci-secrets 69 | name: code 70 | path: /src 71 | metadata: {} 72 | name: run 73 | outputs: {} 74 | volumes: 75 | - name: secrets 76 | secret: 77 | secretName: ci-secrets 78 | - name: build-cache 79 | persistentVolumeClaim: 80 | claimName: '{{workflow.parameters.cacheVolumeClaimName}}' 81 | status: 82 | finishedAt: null 83 | startedAt: null 84 | -------------------------------------------------------------------------------- /test/test-format.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | var expect = require('expect.js') 3 | var format = require('../format') 4 | 5 | describe('format', function () { 6 | it('should handle an empty string', function () { 7 | var url = '' 8 | expect(format({ 9 | auth: undefined, 10 | protocol: undefined, 11 | port: undefined, 12 | hostname: undefined, 13 | pathname: undefined, 14 | search: undefined, 15 | hash: undefined, 16 | href: undefined 17 | })).to.eql(url) 18 | }) 19 | 20 | it('should parse a full url', function () { 21 | var url = 'proto://domain.com:3000/some/pathname?query=string#fragment' 22 | expect(format({ 23 | auth: undefined, 24 | protocol: 'proto:', 25 | port: '3000', 26 | hostname: 'domain.com', 27 | pathname: '/some/pathname', 28 | search: '?query=string', 29 | hash: '#fragment', 30 | href: url 31 | })).to.eql(url) 32 | }) 33 | 34 | it('should handle auth', function () { 35 | var url = 'proto://user:password@domain.com:3000/some/pathname?query=string#fragment' 36 | expect(format({ 37 | auth: 'user:password', 38 | protocol: 'proto:', 39 | port: '3000', 40 | hostname: 'domain.com', 41 | pathname: '/some/pathname', 42 | search: '?query=string', 43 | hash: '#fragment', 44 | href: url 45 | })).to.eql(url) 46 | }) 47 | 48 | it('should parse a relative url', function () { 49 | var url = '/some/pathname?query=string#fragment' 50 | expect(format({ 51 | auth: undefined, 52 | protocol: undefined, 53 | port: undefined, 54 | hostname: undefined, 55 | pathname: '/some/pathname', 56 | search: '?query=string', 57 | hash: '#fragment', 58 | href: url 59 | })).to.eql(url) 60 | }) 61 | 62 | it('should handle case where there is no querystring', function () { 63 | var url = '/some/pathname#fragment' 64 | expect(format({ 65 | auth: undefined, 66 | protocol: undefined, 67 | port: undefined, 68 | hostname: undefined, 69 | pathname: '/some/pathname', 70 | search: undefined, 71 | hash: '#fragment', 72 | href: url 73 | })).to.eql(url) 74 | }) 75 | 76 | it('should preserve relative protocol', function () { 77 | var url = '//example.com/hello' 78 | expect(format({ 79 | auth: undefined, 80 | protocol: undefined, 81 | port: undefined, 82 | hostname: 'example.com', 83 | pathname: '/hello', 84 | search: undefined, 85 | hash: undefined, 86 | href: url 87 | })).to.eql(url) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/test-extra-urlite.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | var expect = require('expect.js') 3 | var rawUrlite = require('../') 4 | var urlite = require('../extra') 5 | 6 | describe('querystring urlite', function () { 7 | it('should parse extras', function () { 8 | var url = 'proto://user:password@domain.com:3000/some/pathname?a=b#fragment' 9 | var parsed = { 10 | auth: { user: 'user', password: 'password' }, 11 | protocol: 'proto:', 12 | port: '3000', 13 | hostname: 'domain.com', 14 | pathname: '/some/pathname', 15 | path: '/some/pathname?a=b', 16 | search: { 17 | a: 'b' 18 | }, 19 | hash: { 20 | fragment: true 21 | }, 22 | href: url 23 | } 24 | expect(urlite.parse(url)).to.eql(parsed) 25 | expect(urlite.format(parsed)).to.eql(url) 26 | }) 27 | 28 | it('should handle no query', function () { 29 | var url = 'proto://user:password@domain.com:3000/some/pathname#fragment' 30 | var parsed = { 31 | auth: { user: 'user', password: 'password' }, 32 | protocol: 'proto:', 33 | port: '3000', 34 | hostname: 'domain.com', 35 | pathname: '/some/pathname', 36 | path: '/some/pathname', 37 | search: {}, 38 | hash: { 39 | fragment: true 40 | }, 41 | href: url 42 | } 43 | expect(urlite.parse(url)).to.eql(parsed) 44 | expect(urlite.format(parsed)).to.eql(url) 45 | }) 46 | 47 | it('should handle no auth', function () { 48 | var url = 'proto://domain.com:3000/some/pathname#fragment' 49 | var parsed = { 50 | auth: undefined, 51 | protocol: 'proto:', 52 | port: '3000', 53 | hostname: 'domain.com', 54 | pathname: '/some/pathname', 55 | path: '/some/pathname', 56 | search: {}, 57 | hash: { 58 | fragment: true 59 | }, 60 | href: url 61 | } 62 | expect(urlite.parse(url)).to.eql(parsed) 63 | expect(urlite.format(parsed)).to.eql(url) 64 | }) 65 | 66 | it('should handle empty querystring params', function () { 67 | var url = 'proto://user:password@domain.com:3000/some/pathname?blah&blah2=&boop' 68 | var parsed = { 69 | auth: { user: 'user', password: 'password' }, 70 | protocol: 'proto:', 71 | port: '3000', 72 | hostname: 'domain.com', 73 | pathname: '/some/pathname', 74 | path: '/some/pathname?blah&blah2=&boop', 75 | search: { 76 | blah: true, 77 | blah2: true, 78 | boop: true 79 | }, 80 | hash: {}, 81 | href: url 82 | } 83 | expect(urlite.parse(url)).to.eql(parsed) 84 | }) 85 | 86 | it('should handle empty querystring values', function () { 87 | var url = 'proto://user:password@domain.com:3000/some/pathname?=&' 88 | var parsed = { 89 | auth: { user: 'user', password: 'password' }, 90 | protocol: 'proto:', 91 | port: '3000', 92 | hostname: 'domain.com', 93 | pathname: '/some/pathname', 94 | path: '/some/pathname?=&', 95 | search: { 96 | '': true 97 | }, 98 | hash: {}, 99 | href: url 100 | } 101 | expect(urlite.parse(url)).to.eql(parsed) 102 | }) 103 | 104 | it('should handle formated search, auth and hash', function () { 105 | var url = 'proto://user:password@domain.com:3000/some/pathname?a=b#fragment' 106 | expect(urlite.format(rawUrlite.parse(url))).to.eql(url) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![urlite](https://cloud.githubusercontent.com/assets/640611/11125144/30a12ab0-8960-11e5-91ba-dfb682572a6c.png) 2 | 3 | A very small, fast, dependency free url parser and formatter for nodejs and the web 4 | 5 | - fast 6 | - few lines of code 7 | - 100% test coverage 8 | 9 | ## why is it so small and fast? 10 | It extracts all url fragments in a single step using one massive regex 11 | 12 | ## usage 13 | ```js 14 | npm install --save urlite 15 | 16 | var url = require('urlite') 17 | 18 | url.parse('http://user:pass@blah.com:3000/path?query=string#fragment') 19 | 20 | { 21 | auth: 'user:pass', 22 | hash: '#fragment', 23 | hostname: 'blah.com', 24 | href: 'http://user:pass@blah.com:3000/path?query=string#fragment', 25 | path: '/path?query=string', 26 | pathname: '/path', 27 | port: '3000', 28 | protocol: 'http:', 29 | search: '?query=string' 30 | } 31 | 32 | var href = window.location.href 33 | url.format(url.parse(href)) === href 34 | ``` 35 | 36 | ## Urlite extra 37 | An extended version of urlite is available at `urlite/extra`. This includes helpful features such as querystring, hash and auth parsing: 38 | 39 | ```js 40 | // version of urlite with additional extras like querystring and auth parsing 41 | var url = require('urlite/extra') 42 | var parsed = url.parse('http://user:pass@blah.com:3000/path?a=b#c=d') 43 | parsed.search // -> { a: "b" } 44 | parsed.search.a = 'c' 45 | parsed.hash // -> { c: "d" } 46 | parsed.hash.c = 'e' 47 | parsed.auth // -> { user: 'user', password: 'password' } 48 | url.format(parsed) // -> 'http://user:pass@blah.com:3000/path?a=c#c=e' 49 | ``` 50 | 51 | # comparison 52 | ``` 53 | File size: 54 | 55 | NAME SIZE SIZE (minified) 56 | urlite 3.02 kB 0.957 kB 57 | urlparser 5.82 kB 1.57 kB 58 | url-parse 12 kB 2.89 kB 59 | url 46.5 kB 11.8 kB 60 | min-url 25.6 kB 12.6 kB 61 | fast-url-parser 55.2 kB 15 kB 62 | url-parse-as-address 78.7 kB 22.7 kB 63 | ``` 64 | 65 | ``` 66 | Performance: 67 | 68 | require("urlite").parse 2,210,417 ops/sec ±0.90% (95 runs sampled) 69 | require("fast-url-parser").parse 2,047,302 ops/sec ±0.89% (95 runs sampled) 70 | require("urlparser").parse 631,561 ops/secs ±0.87% (92 runs sampled) 71 | require("min-url").parse 343,680 ops/sec ±1.16% (93 runs sampled) 72 | require("url-parse") 334,385 ops/sec ±1.03% (97 runs sampled) 73 | require("url").parse 140,836 ops/sec ±1.26% (94 runs sampled) 74 | require("url-parse-as-address") 135,691 ops/sec ±0.94% (95 runs sampled) 75 | ``` 76 | 77 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 78 | 79 | 80 | 81 | ## Want to work on this for your day job? 82 | 83 | This project was created by the Engineering team at Qubit. As we use open source libraries, we make our projects public where possible. 84 | 85 | We’re currently looking to grow our team, so if you’re a JavaScript engineer and keen on ES2016 React+Redux applications and Node micro services, why not get in touch? Work with like minded engineers in an environment that has fantastic perks, including an annual ski trip, yoga, a competitive foosball league, and copious amounts of yogurt. 86 | 87 | Find more details on our Engineering site. Don’t have an up to date CV? Just link us your Github profile! Better yet, send us a pull request that improves this project.` 88 | Contact GitHub API Training Shop Blog About 89 | -------------------------------------------------------------------------------- /test/test-parse.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | var expect = require('expect.js') 3 | var parse = require('../parse') 4 | 5 | describe('parse', function () { 6 | it('should handle an empty string', function () { 7 | expect(parse('')).to.eql({ 8 | auth: undefined, 9 | protocol: undefined, 10 | port: undefined, 11 | hostname: undefined, 12 | pathname: undefined, 13 | path: undefined, 14 | search: undefined, 15 | hash: undefined, 16 | href: '' 17 | }) 18 | }) 19 | 20 | it('should parse a full url', function () { 21 | var url = 'proto://domain.com:3000/some/pathname?query=string#fragment' 22 | expect(parse(url)).to.eql({ 23 | auth: undefined, 24 | protocol: 'proto:', 25 | port: '3000', 26 | hostname: 'domain.com', 27 | pathname: '/some/pathname', 28 | path: '/some/pathname?query=string', 29 | search: '?query=string', 30 | hash: '#fragment', 31 | href: url 32 | }) 33 | }) 34 | 35 | it('should handle auth', function () { 36 | var url = 'proto://user:password@domain.com:3000/some/pathname?query=string#fragment' 37 | expect(parse(url)).to.eql({ 38 | auth: 'user:password', 39 | hash: '#fragment', 40 | hostname: 'domain.com', 41 | href: url, 42 | path: '/some/pathname?query=string', 43 | pathname: '/some/pathname', 44 | port: '3000', 45 | protocol: 'proto:', 46 | search: '?query=string' 47 | }) 48 | }) 49 | 50 | it('should parse a relative url', function () { 51 | var url = '/some/pathname?query=string#fragment' 52 | expect(parse(url)).to.eql({ 53 | auth: undefined, 54 | protocol: undefined, 55 | port: undefined, 56 | hostname: undefined, 57 | pathname: '/some/pathname', 58 | path: '/some/pathname?query=string', 59 | search: '?query=string', 60 | hash: '#fragment', 61 | href: url 62 | }) 63 | }) 64 | 65 | it('should handle case where there is no querystring', function () { 66 | var url = '/some/pathname#fragment' 67 | expect(parse(url)).to.eql({ 68 | auth: undefined, 69 | protocol: undefined, 70 | port: undefined, 71 | hostname: undefined, 72 | pathname: '/some/pathname', 73 | path: '/some/pathname', 74 | search: undefined, 75 | hash: '#fragment', 76 | href: url 77 | }) 78 | }) 79 | 80 | it('should handle case where there is no path', function () { 81 | var url = 'http://dev---www-sitting--duck-com.poxy.com:666?_q_portal_protocol=http' 82 | expect(parse(url)).to.eql({ 83 | href: url, 84 | hash: undefined, 85 | search: '?_q_portal_protocol=http', 86 | pathname: undefined, 87 | port: '666', 88 | hostname: 'dev---www-sitting--duck-com.poxy.com', 89 | auth: undefined, 90 | protocol: 'http:', 91 | path: '?_q_portal_protocol=http' 92 | }) 93 | }) 94 | 95 | it('should handle javascript protocol', function () { 96 | var url = 'javascript:alert("node is awesome");' 97 | expect(parse(url)).to.eql({ 98 | auth: undefined, 99 | protocol: 'javascript:', 100 | port: undefined, 101 | hostname: undefined, 102 | pathname: 'alert("node is awesome");', 103 | path: 'alert("node is awesome");', 104 | search: undefined, 105 | hash: undefined, 106 | href: url 107 | }) 108 | }) 109 | 110 | it('should handle @ signs in the path', function () { 111 | var url = 'http://localhost:4400/@qubit/layer@2.20.47/lib/layer_editor.js' 112 | expect(parse(url)).to.eql({ 113 | href: 'http://localhost:4400/@qubit/layer@2.20.47/lib/layer_editor.js', 114 | hash: undefined, 115 | search: undefined, 116 | pathname: '/@qubit/layer@2.20.47/lib/layer_editor.js', 117 | port: '4400', 118 | hostname: 'localhost', 119 | auth: undefined, 120 | protocol: 'http:', 121 | path: '/@qubit/layer@2.20.47/lib/layer_editor.js' 122 | }) 123 | }) 124 | 125 | it('should handle other cases taken from node code & RFC 3986', function () { 126 | var cases = [{ 127 | url: 'http://nodejs.org/docs/latest/api/url.html#url_url_format_urlobj', 128 | result: { 129 | protocol: 'http:', 130 | auth: undefined, 131 | port: undefined, 132 | hostname: 'nodejs.org', 133 | hash: '#url_url_format_urlobj', 134 | search: undefined, 135 | pathname: '/docs/latest/api/url.html', 136 | path: '/docs/latest/api/url.html', 137 | href: 'http://nodejs.org/docs/latest/api/url.html#url_url_format_urlobj' 138 | } 139 | }, { 140 | url: 'http://blog.nodejs.org/', 141 | result: { 142 | protocol: 'http:', 143 | auth: undefined, 144 | port: undefined, 145 | hostname: 'blog.nodejs.org', 146 | hash: undefined, 147 | search: undefined, 148 | pathname: '/', 149 | path: '/', 150 | href: 'http://blog.nodejs.org/' 151 | } 152 | }, { 153 | url: 'https://encrypted.google.com/search?q=url&q=site:npmjs.org&hl=en', 154 | result: { 155 | protocol: 'https:', 156 | auth: undefined, 157 | port: undefined, 158 | hostname: 'encrypted.google.com', 159 | hash: undefined, 160 | search: '?q=url&q=site:npmjs.org&hl=en', 161 | pathname: '/search', 162 | path: '/search?q=url&q=site:npmjs.org&hl=en', 163 | href: 'https://encrypted.google.com/search?q=url&q=site:npmjs.org&hl=en' 164 | } 165 | }, { 166 | url: 'some.ran/dom/url.thing?oh=yes#whoo', 167 | result: { 168 | protocol: undefined, 169 | auth: undefined, 170 | port: undefined, 171 | hostname: undefined, 172 | hash: '#whoo', 173 | search: '?oh=yes', 174 | pathname: 'some.ran/dom/url.thing', 175 | path: 'some.ran/dom/url.thing?oh=yes', 176 | href: 'some.ran/dom/url.thing?oh=yes#whoo' 177 | } 178 | }, { 179 | url: 'https://user:pass@example.com/', 180 | result: { 181 | protocol: 'https:', 182 | auth: 'user:pass', 183 | port: undefined, 184 | hostname: 'example.com', 185 | hash: undefined, 186 | search: undefined, 187 | pathname: '/', 188 | path: '/', 189 | href: 'https://user:pass@example.com/' 190 | } 191 | }, { 192 | url: '/wiki/Help:IPA', 193 | result: { 194 | protocol: undefined, 195 | auth: undefined, 196 | port: undefined, 197 | hostname: undefined, 198 | hash: undefined, 199 | search: undefined, 200 | pathname: '/wiki/Help:IPA', 201 | path: '/wiki/Help:IPA', 202 | href: '/wiki/Help:IPA' 203 | } 204 | }, { 205 | url: 'http://:pass@example.org:123/some/directory/file.html?query=string#fragment', 206 | result: { 207 | protocol: 'http:', 208 | auth: ':pass', 209 | port: '123', 210 | hostname: 'example.org', 211 | hash: '#fragment', 212 | search: '?query=string', 213 | pathname: '/some/directory/file.html', 214 | path: '/some/directory/file.html?query=string', 215 | href: 'http://:pass@example.org:123/some/directory/file.html?query=string#fragment' 216 | } 217 | }] 218 | for (var i = 0; i < cases.length; i++) { 219 | expect(parse(cases[i].url)).to.eql(cases[i].result) 220 | } 221 | }) 222 | }) 223 | --------------------------------------------------------------------------------