From bf6295dd94655dac213df236a45c21a0b4daa3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 13:21:01 -0500 Subject: [PATCH 001/117] url-pattern.ts = coffee-to-typescript(src/url-pattern.coffee) --- url-pattern.ts | 492 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 url-pattern.ts diff --git a/url-pattern.ts b/url-pattern.ts new file mode 100644 index 0000000..7861659 --- /dev/null +++ b/url-pattern.ts @@ -0,0 +1,492 @@ +(function (root, factory) { + // AMD + if ('function' === typeof define && define.amd != null) { + return define([], factory); + // CommonJS + } else if (typeof exports !== 'undefined' && exports !== null) { + return module.exports = factory(); + // browser globals + } else { + return root.UrlPattern = factory(); + } +})(this, function () { + + //############################################################################### + // helpers + + // source: http://stackoverflow.com/a/3561711 + let escapeForRegex = string => string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + + let concatMap = function (array, f) { + let results = []; + let i = -1; + let { length } = array; + while (++i < length) { + results = results.concat(f(array[i])); + } + return results; + }; + + let stringConcatMap = function (array, f) { + let result = ''; + let i = -1; + let { length } = array; + while (++i < length) { + result += f(array[i]); + } + return result; + }; + + // source: http://stackoverflow.com/a/16047223 + let regexGroupCount = regex => new RegExp(regex.toString() + '|').exec('').length - 1; + + let keysAndValuesToObject = function (keys, values) { + let object = {}; + let i = -1; + let { length } = keys; + while (++i < length) { + let key = keys[i]; + let value = values[i]; + if (value == null) { + continue; + } + // key already encountered + if (object[key] != null) { + // capture multiple values for same key in an array + if (!Array.isArray(object[key])) { + object[key] = [object[key]]; + } + object[key].push(value); + } else { + object[key] = value; + } + } + return object; + }; + + //############################################################################### + // parser combinators + // subset copied from + // https://github.com/snd/pcom/blob/master/src/pcom.coffee + // (where they are tested !) + // to keep this at zero dependencies and small filesize + + let P = {}; + + P.Result = function (value, rest) { + this.value = value; + this.rest = rest; + }; + + P.Tagged = function (tag, value) { + this.tag = tag; + this.value = value; + }; + + P.tag = (tag, parser) => function (input) { + let result = parser(input); + if (result == null) { + return; + } + let tagged = new P.Tagged(tag, result.value); + return new P.Result(tagged, result.rest); + }; + + P.regex = regex => + // unless regex instanceof RegExp + // throw new Error 'argument must be instanceof RegExp' + function (input) { + let matches = regex.exec(input); + if (matches == null) { + return; + } + let result = matches[0]; + return new P.Result(result, input.slice(result.length)); + }; + + P.sequence = (...parsers) => function (input) { + let i = -1; + let { length } = parsers; + let values = []; + let rest = input; + while (++i < length) { + let parser = parsers[i]; + // unless 'function' is typeof parser + // throw new Error "parser passed at index `#{i}` into `sequence` is not of type `function` but of type `#{typeof parser}`" + let result = parser(rest); + if (result == null) { + return; + } + values.push(result.value); + ({ rest } = result); + } + return new P.Result(values, rest); + }; + + P.pick = (indexes, ...parsers) => function (input) { + let result = P.sequence(...Array.from(parsers || []))(input); + if (result == null) { + return; + } + let array = result.value; + result.value = array[indexes]; + // unless Array.isArray indexes + // result.value = array[indexes] + // else + // result.value = [] + // indexes.forEach (i) -> + // result.value.push array[i] + return result; + }; + + P.string = function (string) { + let { length } = string; + // if length is 0 + // throw new Error '`string` must not be blank' + return function (input) { + if (input.slice(0, length) === string) { + return new P.Result(string, input.slice(length)); + } + }; + }; + + P.lazy = function (fn) { + let cached = null; + return function (input) { + if (cached == null) { + cached = fn(); + } + return cached(input); + }; + }; + + P.baseMany = function (parser, end, stringResult, atLeastOneResultRequired, input) { + let rest = input; + let results = stringResult ? '' : []; + while (true) { + if (end != null) { + let endResult = end(rest); + if (endResult != null) { + break; + } + } + let parserResult = parser(rest); + if (parserResult == null) { + break; + } + if (stringResult) { + results += parserResult.value; + } else { + results.push(parserResult.value); + } + ({ rest } = parserResult); + } + + if (atLeastOneResultRequired && results.length === 0) { + return; + } + + return new P.Result(results, rest); + }; + + P.many1 = parser => input => P.baseMany(parser, null, false, true, input); + + P.concatMany1Till = (parser, end) => input => P.baseMany(parser, end, true, true, input); + + P.firstChoice = (...parsers) => function (input) { + let i = -1; + let { length } = parsers; + while (++i < length) { + let parser = parsers[i]; + // unless 'function' is typeof parser + // throw new Error "parser passed at index `#{i}` into `firstChoice` is not of type `function` but of type `#{typeof parser}`" + let result = parser(input); + if (result != null) { + return result; + } + } + }; + + //############################################################################### + // url pattern parser + // copied from + // https://github.com/snd/pcom/blob/master/src/url-pattern-example.coffee + + let newParser = function (options) { + let U = {}; + + U.wildcard = P.tag('wildcard', P.string(options.wildcardChar)); + + U.optional = P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(() => U.pattern), P.string(options.optionalSegmentEndChar))); + + U.name = P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); + + U.named = P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))); + + U.escapedChar = P.pick(1, P.string(options.escapeChar), P.regex(/^./)); + + U.static = P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(() => U.escapedChar), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), U.wildcard))); + + U.token = P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)); + + U.pattern = P.many1(P.lazy(() => U.token)); + + return U; + }; + + //############################################################################### + // options + + let defaultOptions = { + escapeChar: '\\', + segmentNameStartChar: ':', + segmentValueCharset: 'a-zA-Z0-9-_~ %', + segmentNameCharset: 'a-zA-Z0-9', + optionalSegmentStartChar: '(', + optionalSegmentEndChar: ')', + wildcardChar: '*' + }; + + //############################################################################### + // functions that further process ASTs returned as `.value` in parser results + + var baseAstNodeToRegexString = function (astNode, segmentValueCharset) { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, node => baseAstNodeToRegexString(node, segmentValueCharset)); + } + + switch (astNode.tag) { + case 'wildcard': + return '(.*?)'; + case 'named': + return `([${ segmentValueCharset }]+)`; + case 'static': + return escapeForRegex(astNode.value); + case 'optional': + return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; + } + }; + + let astNodeToRegexString = function (astNode, segmentValueCharset) { + if (segmentValueCharset == null) { + ({ segmentValueCharset } = defaultOptions); + } + return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; + }; + + var astNodeToNames = function (astNode) { + if (Array.isArray(astNode)) { + return concatMap(astNode, astNodeToNames); + } + + switch (astNode.tag) { + case 'wildcard': + return ['_']; + case 'named': + return [astNode.value]; + case 'static': + return []; + case 'optional': + return astNodeToNames(astNode.value); + } + }; + + let getParam = function (params, key, nextIndexes, sideEffects) { + if (sideEffects == null) { + sideEffects = false; + } + let value = params[key]; + if (value == null) { + if (sideEffects) { + throw new Error(`no values provided for key \`${ key }\``); + } else { + return; + } + } + let index = nextIndexes[key] || 0; + let maxIndex = Array.isArray(value) ? value.length - 1 : 0; + if (index > maxIndex) { + if (sideEffects) { + throw new Error(`too few values provided for key \`${ key }\``); + } else { + return; + } + } + + let result = Array.isArray(value) ? value[index] : value; + + if (sideEffects) { + nextIndexes[key] = index + 1; + } + + return result; + }; + + var astNodeContainsSegmentsForProvidedParams = function (astNode, params, nextIndexes) { + if (Array.isArray(astNode)) { + let i = -1; + let { length } = astNode; + while (++i < length) { + if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { + return true; + } + } + return false; + } + + switch (astNode.tag) { + case 'wildcard': + return getParam(params, '_', nextIndexes, false) != null; + case 'named': + return getParam(params, astNode.value, nextIndexes, false) != null; + case 'static': + return false; + case 'optional': + return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); + } + }; + + var stringify = function (astNode, params, nextIndexes) { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, node => stringify(node, params, nextIndexes)); + } + + switch (astNode.tag) { + case 'wildcard': + return getParam(params, '_', nextIndexes, true); + case 'named': + return getParam(params, astNode.value, nextIndexes, true); + case 'static': + return astNode.value; + case 'optional': + if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { + return stringify(astNode.value, params, nextIndexes); + } else { + return ''; + } + } + }; + + //############################################################################### + // UrlPattern + + var UrlPattern = function (arg1, arg2) { + // self awareness + if (arg1 instanceof UrlPattern) { + this.isRegex = arg1.isRegex; + this.regex = arg1.regex; + this.ast = arg1.ast; + this.names = arg1.names; + return; + } + + this.isRegex = arg1 instanceof RegExp; + + if ('string' !== typeof arg1 && !this.isRegex) { + throw new TypeError('argument must be a regex or a string'); + } + + // regex + + if (this.isRegex) { + this.regex = arg1; + if (arg2 != null) { + if (!Array.isArray(arg2)) { + throw new Error('if first argument is a regex the second argument may be an array of group names but you provided something else'); + } + let groupCount = regexGroupCount(this.regex); + if (arg2.length !== groupCount) { + throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ arg2.length }`); + } + this.names = arg2; + } + return; + } + + // string pattern + + if (arg1 === '') { + throw new Error('argument must not be the empty string'); + } + let withoutWhitespace = arg1.replace(/\s+/g, ''); + if (withoutWhitespace !== arg1) { + throw new Error('argument must not contain whitespace'); + } + + let options = { + escapeChar: (arg2 != null ? arg2.escapeChar : undefined) || defaultOptions.escapeChar, + segmentNameStartChar: (arg2 != null ? arg2.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, + segmentNameCharset: (arg2 != null ? arg2.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, + segmentValueCharset: (arg2 != null ? arg2.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, + optionalSegmentStartChar: (arg2 != null ? arg2.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, + optionalSegmentEndChar: (arg2 != null ? arg2.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, + wildcardChar: (arg2 != null ? arg2.wildcardChar : undefined) || defaultOptions.wildcardChar + }; + + let parser = newParser(options); + let parsed = parser.pattern(arg1); + if (parsed == null) { + // TODO better error message + throw new Error("couldn't parse pattern"); + } + if (parsed.rest !== '') { + // TODO better error message + throw new Error("could only partially parse pattern"); + } + this.ast = parsed.value; + + this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); + this.names = astNodeToNames(this.ast); + }; + + UrlPattern.prototype.match = function (url) { + let match = this.regex.exec(url); + if (match == null) { + return null; + } + + let groups = match.slice(1); + if (this.names) { + return keysAndValuesToObject(this.names, groups); + } else { + return groups; + } + }; + + UrlPattern.prototype.stringify = function (params) { + if (params == null) { + params = {}; + } + if (this.isRegex) { + throw new Error("can't stringify patterns generated from a regex"); + } + if (params !== Object(params)) { + throw new Error("argument must be an object or undefined"); + } + return stringify(this.ast, params, {}); + }; + + //############################################################################### + // exports + + // helpers + UrlPattern.escapeForRegex = escapeForRegex; + UrlPattern.concatMap = concatMap; + UrlPattern.stringConcatMap = stringConcatMap; + UrlPattern.regexGroupCount = regexGroupCount; + UrlPattern.keysAndValuesToObject = keysAndValuesToObject; + + // parsers + UrlPattern.P = P; + UrlPattern.newParser = newParser; + UrlPattern.defaultOptions = defaultOptions; + + // ast + UrlPattern.astNodeToRegexString = astNodeToRegexString; + UrlPattern.astNodeToNames = astNodeToNames; + UrlPattern.getParam = getParam; + UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; + UrlPattern.stringify = stringify; + + return UrlPattern; +}); From 38bffe94616020ffc45a4f2605ebe042b0d76fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 13:24:34 -0500 Subject: [PATCH 002/117] get rid of module system managing code --- url-pattern.ts | 836 ++++++++++++++++++++++++------------------------- 1 file changed, 410 insertions(+), 426 deletions(-) diff --git a/url-pattern.ts b/url-pattern.ts index 7861659..cf3a50b 100644 --- a/url-pattern.ts +++ b/url-pattern.ts @@ -1,492 +1,476 @@ -(function (root, factory) { - // AMD - if ('function' === typeof define && define.amd != null) { - return define([], factory); - // CommonJS - } else if (typeof exports !== 'undefined' && exports !== null) { - return module.exports = factory(); - // browser globals - } else { - return root.UrlPattern = factory(); +//############################################################################### +// helpers + +// source: http://stackoverflow.com/a/3561711 +let escapeForRegex = string => string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + +let concatMap = function (array, f) { + let results = []; + let i = -1; + let { length } = array; + while (++i < length) { + results = results.concat(f(array[i])); } -})(this, function () { - - //############################################################################### - // helpers - - // source: http://stackoverflow.com/a/3561711 - let escapeForRegex = string => string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - - let concatMap = function (array, f) { - let results = []; - let i = -1; - let { length } = array; - while (++i < length) { - results = results.concat(f(array[i])); - } - return results; - }; - - let stringConcatMap = function (array, f) { - let result = ''; - let i = -1; - let { length } = array; - while (++i < length) { - result += f(array[i]); + return results; +}; + +let stringConcatMap = function (array, f) { + let result = ''; + let i = -1; + let { length } = array; + while (++i < length) { + result += f(array[i]); + } + return result; +}; + +// source: http://stackoverflow.com/a/16047223 +let regexGroupCount = regex => new RegExp(regex.toString() + '|').exec('').length - 1; + +let keysAndValuesToObject = function (keys, values) { + let object = {}; + let i = -1; + let { length } = keys; + while (++i < length) { + let key = keys[i]; + let value = values[i]; + if (value == null) { + continue; } - return result; - }; - - // source: http://stackoverflow.com/a/16047223 - let regexGroupCount = regex => new RegExp(regex.toString() + '|').exec('').length - 1; - - let keysAndValuesToObject = function (keys, values) { - let object = {}; - let i = -1; - let { length } = keys; - while (++i < length) { - let key = keys[i]; - let value = values[i]; - if (value == null) { - continue; - } - // key already encountered - if (object[key] != null) { - // capture multiple values for same key in an array - if (!Array.isArray(object[key])) { - object[key] = [object[key]]; - } - object[key].push(value); - } else { - object[key] = value; + // key already encountered + if (object[key] != null) { + // capture multiple values for same key in an array + if (!Array.isArray(object[key])) { + object[key] = [object[key]]; } + object[key].push(value); + } else { + object[key] = value; } - return object; - }; - - //############################################################################### - // parser combinators - // subset copied from - // https://github.com/snd/pcom/blob/master/src/pcom.coffee - // (where they are tested !) - // to keep this at zero dependencies and small filesize - - let P = {}; - - P.Result = function (value, rest) { - this.value = value; - this.rest = rest; - }; - - P.Tagged = function (tag, value) { - this.tag = tag; - this.value = value; - }; - - P.tag = (tag, parser) => function (input) { - let result = parser(input); + } + return object; +}; + +//############################################################################### +// parser combinators +// subset copied from +// https://github.com/snd/pcom/blob/master/src/pcom.coffee +// (where they are tested !) +// to keep this at zero dependencies and small filesize + +let P = {}; + +P.Result = function (value, rest) { + this.value = value; + this.rest = rest; +}; + +P.Tagged = function (tag, value) { + this.tag = tag; + this.value = value; +}; + +P.tag = (tag, parser) => function (input) { + let result = parser(input); + if (result == null) { + return; + } + let tagged = new P.Tagged(tag, result.value); + return new P.Result(tagged, result.rest); +}; + +P.regex = regex => +// unless regex instanceof RegExp +// throw new Error 'argument must be instanceof RegExp' +function (input) { + let matches = regex.exec(input); + if (matches == null) { + return; + } + let result = matches[0]; + return new P.Result(result, input.slice(result.length)); +}; + +P.sequence = (...parsers) => function (input) { + let i = -1; + let { length } = parsers; + let values = []; + let rest = input; + while (++i < length) { + let parser = parsers[i]; + // unless 'function' is typeof parser + // throw new Error "parser passed at index `#{i}` into `sequence` is not of type `function` but of type `#{typeof parser}`" + let result = parser(rest); if (result == null) { return; } - let tagged = new P.Tagged(tag, result.value); - return new P.Result(tagged, result.rest); - }; - - P.regex = regex => - // unless regex instanceof RegExp - // throw new Error 'argument must be instanceof RegExp' - function (input) { - let matches = regex.exec(input); - if (matches == null) { - return; - } - let result = matches[0]; - return new P.Result(result, input.slice(result.length)); - }; + values.push(result.value); + ({ rest } = result); + } + return new P.Result(values, rest); +}; - P.sequence = (...parsers) => function (input) { - let i = -1; - let { length } = parsers; - let values = []; - let rest = input; - while (++i < length) { - let parser = parsers[i]; - // unless 'function' is typeof parser - // throw new Error "parser passed at index `#{i}` into `sequence` is not of type `function` but of type `#{typeof parser}`" - let result = parser(rest); - if (result == null) { - return; - } - values.push(result.value); - ({ rest } = result); +P.pick = (indexes, ...parsers) => function (input) { + let result = P.sequence(...Array.from(parsers || []))(input); + if (result == null) { + return; + } + let array = result.value; + result.value = array[indexes]; + // unless Array.isArray indexes + // result.value = array[indexes] + // else + // result.value = [] + // indexes.forEach (i) -> + // result.value.push array[i] + return result; +}; + +P.string = function (string) { + let { length } = string; + // if length is 0 + // throw new Error '`string` must not be blank' + return function (input) { + if (input.slice(0, length) === string) { + return new P.Result(string, input.slice(length)); } - return new P.Result(values, rest); }; +}; - P.pick = (indexes, ...parsers) => function (input) { - let result = P.sequence(...Array.from(parsers || []))(input); - if (result == null) { - return; +P.lazy = function (fn) { + let cached = null; + return function (input) { + if (cached == null) { + cached = fn(); } - let array = result.value; - result.value = array[indexes]; - // unless Array.isArray indexes - // result.value = array[indexes] - // else - // result.value = [] - // indexes.forEach (i) -> - // result.value.push array[i] - return result; - }; - - P.string = function (string) { - let { length } = string; - // if length is 0 - // throw new Error '`string` must not be blank' - return function (input) { - if (input.slice(0, length) === string) { - return new P.Result(string, input.slice(length)); - } - }; + return cached(input); }; - - P.lazy = function (fn) { - let cached = null; - return function (input) { - if (cached == null) { - cached = fn(); - } - return cached(input); - }; - }; - - P.baseMany = function (parser, end, stringResult, atLeastOneResultRequired, input) { - let rest = input; - let results = stringResult ? '' : []; - while (true) { - if (end != null) { - let endResult = end(rest); - if (endResult != null) { - break; - } - } - let parserResult = parser(rest); - if (parserResult == null) { +}; + +P.baseMany = function (parser, end, stringResult, atLeastOneResultRequired, input) { + let rest = input; + let results = stringResult ? '' : []; + while (true) { + if (end != null) { + let endResult = end(rest); + if (endResult != null) { break; } - if (stringResult) { - results += parserResult.value; - } else { - results.push(parserResult.value); - } - ({ rest } = parserResult); } - - if (atLeastOneResultRequired && results.length === 0) { - return; + let parserResult = parser(rest); + if (parserResult == null) { + break; } + if (stringResult) { + results += parserResult.value; + } else { + results.push(parserResult.value); + } + ({ rest } = parserResult); + } - return new P.Result(results, rest); - }; + if (atLeastOneResultRequired && results.length === 0) { + return; + } - P.many1 = parser => input => P.baseMany(parser, null, false, true, input); + return new P.Result(results, rest); +}; - P.concatMany1Till = (parser, end) => input => P.baseMany(parser, end, true, true, input); +P.many1 = parser => input => P.baseMany(parser, null, false, true, input); - P.firstChoice = (...parsers) => function (input) { - let i = -1; - let { length } = parsers; - while (++i < length) { - let parser = parsers[i]; - // unless 'function' is typeof parser - // throw new Error "parser passed at index `#{i}` into `firstChoice` is not of type `function` but of type `#{typeof parser}`" - let result = parser(input); - if (result != null) { - return result; - } +P.concatMany1Till = (parser, end) => input => P.baseMany(parser, end, true, true, input); + +P.firstChoice = (...parsers) => function (input) { + let i = -1; + let { length } = parsers; + while (++i < length) { + let parser = parsers[i]; + // unless 'function' is typeof parser + // throw new Error "parser passed at index `#{i}` into `firstChoice` is not of type `function` but of type `#{typeof parser}`" + let result = parser(input); + if (result != null) { + return result; } - }; + } +}; - //############################################################################### - // url pattern parser - // copied from - // https://github.com/snd/pcom/blob/master/src/url-pattern-example.coffee +//############################################################################### +// url pattern parser +// copied from +// https://github.com/snd/pcom/blob/master/src/url-pattern-example.coffee - let newParser = function (options) { - let U = {}; +let newParser = function (options) { + let U = {}; - U.wildcard = P.tag('wildcard', P.string(options.wildcardChar)); + U.wildcard = P.tag('wildcard', P.string(options.wildcardChar)); - U.optional = P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(() => U.pattern), P.string(options.optionalSegmentEndChar))); + U.optional = P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(() => U.pattern), P.string(options.optionalSegmentEndChar))); - U.name = P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); + U.name = P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); - U.named = P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))); + U.named = P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))); - U.escapedChar = P.pick(1, P.string(options.escapeChar), P.regex(/^./)); + U.escapedChar = P.pick(1, P.string(options.escapeChar), P.regex(/^./)); - U.static = P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(() => U.escapedChar), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), U.wildcard))); + U.static = P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(() => U.escapedChar), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), U.wildcard))); - U.token = P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)); + U.token = P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)); - U.pattern = P.many1(P.lazy(() => U.token)); + U.pattern = P.many1(P.lazy(() => U.token)); - return U; - }; + return U; +}; - //############################################################################### - // options - - let defaultOptions = { - escapeChar: '\\', - segmentNameStartChar: ':', - segmentValueCharset: 'a-zA-Z0-9-_~ %', - segmentNameCharset: 'a-zA-Z0-9', - optionalSegmentStartChar: '(', - optionalSegmentEndChar: ')', - wildcardChar: '*' - }; +//############################################################################### +// options - //############################################################################### - // functions that further process ASTs returned as `.value` in parser results +let defaultOptions = { + escapeChar: '\\', + segmentNameStartChar: ':', + segmentValueCharset: 'a-zA-Z0-9-_~ %', + segmentNameCharset: 'a-zA-Z0-9', + optionalSegmentStartChar: '(', + optionalSegmentEndChar: ')', + wildcardChar: '*' +}; - var baseAstNodeToRegexString = function (astNode, segmentValueCharset) { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, node => baseAstNodeToRegexString(node, segmentValueCharset)); - } +//############################################################################### +// functions that further process ASTs returned as `.value` in parser results - switch (astNode.tag) { - case 'wildcard': - return '(.*?)'; - case 'named': - return `([${ segmentValueCharset }]+)`; - case 'static': - return escapeForRegex(astNode.value); - case 'optional': - return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; - } - }; +var baseAstNodeToRegexString = function (astNode, segmentValueCharset) { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, node => baseAstNodeToRegexString(node, segmentValueCharset)); + } - let astNodeToRegexString = function (astNode, segmentValueCharset) { - if (segmentValueCharset == null) { - ({ segmentValueCharset } = defaultOptions); - } - return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; - }; + switch (astNode.tag) { + case 'wildcard': + return '(.*?)'; + case 'named': + return `([${ segmentValueCharset }]+)`; + case 'static': + return escapeForRegex(astNode.value); + case 'optional': + return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; + } +}; - var astNodeToNames = function (astNode) { - if (Array.isArray(astNode)) { - return concatMap(astNode, astNodeToNames); - } +let astNodeToRegexString = function (astNode, segmentValueCharset) { + if (segmentValueCharset == null) { + ({ segmentValueCharset } = defaultOptions); + } + return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; +}; - switch (astNode.tag) { - case 'wildcard': - return ['_']; - case 'named': - return [astNode.value]; - case 'static': - return []; - case 'optional': - return astNodeToNames(astNode.value); - } - }; +var astNodeToNames = function (astNode) { + if (Array.isArray(astNode)) { + return concatMap(astNode, astNodeToNames); + } - let getParam = function (params, key, nextIndexes, sideEffects) { - if (sideEffects == null) { - sideEffects = false; - } - let value = params[key]; - if (value == null) { - if (sideEffects) { - throw new Error(`no values provided for key \`${ key }\``); - } else { - return; - } + switch (astNode.tag) { + case 'wildcard': + return ['_']; + case 'named': + return [astNode.value]; + case 'static': + return []; + case 'optional': + return astNodeToNames(astNode.value); + } +}; + +let getParam = function (params, key, nextIndexes, sideEffects) { + if (sideEffects == null) { + sideEffects = false; + } + let value = params[key]; + if (value == null) { + if (sideEffects) { + throw new Error(`no values provided for key \`${ key }\``); + } else { + return; } - let index = nextIndexes[key] || 0; - let maxIndex = Array.isArray(value) ? value.length - 1 : 0; - if (index > maxIndex) { - if (sideEffects) { - throw new Error(`too few values provided for key \`${ key }\``); - } else { - return; - } + } + let index = nextIndexes[key] || 0; + let maxIndex = Array.isArray(value) ? value.length - 1 : 0; + if (index > maxIndex) { + if (sideEffects) { + throw new Error(`too few values provided for key \`${ key }\``); + } else { + return; } + } - let result = Array.isArray(value) ? value[index] : value; + let result = Array.isArray(value) ? value[index] : value; - if (sideEffects) { - nextIndexes[key] = index + 1; - } + if (sideEffects) { + nextIndexes[key] = index + 1; + } - return result; - }; + return result; +}; - var astNodeContainsSegmentsForProvidedParams = function (astNode, params, nextIndexes) { - if (Array.isArray(astNode)) { - let i = -1; - let { length } = astNode; - while (++i < length) { - if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { - return true; - } +var astNodeContainsSegmentsForProvidedParams = function (astNode, params, nextIndexes) { + if (Array.isArray(astNode)) { + let i = -1; + let { length } = astNode; + while (++i < length) { + if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { + return true; } - return false; } + return false; + } - switch (astNode.tag) { - case 'wildcard': - return getParam(params, '_', nextIndexes, false) != null; - case 'named': - return getParam(params, astNode.value, nextIndexes, false) != null; - case 'static': - return false; - case 'optional': - return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); - } - }; - - var stringify = function (astNode, params, nextIndexes) { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, node => stringify(node, params, nextIndexes)); - } + switch (astNode.tag) { + case 'wildcard': + return getParam(params, '_', nextIndexes, false) != null; + case 'named': + return getParam(params, astNode.value, nextIndexes, false) != null; + case 'static': + return false; + case 'optional': + return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); + } +}; - switch (astNode.tag) { - case 'wildcard': - return getParam(params, '_', nextIndexes, true); - case 'named': - return getParam(params, astNode.value, nextIndexes, true); - case 'static': - return astNode.value; - case 'optional': - if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { - return stringify(astNode.value, params, nextIndexes); - } else { - return ''; - } - } - }; +var stringify = function (astNode, params, nextIndexes) { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, node => stringify(node, params, nextIndexes)); + } - //############################################################################### - // UrlPattern + switch (astNode.tag) { + case 'wildcard': + return getParam(params, '_', nextIndexes, true); + case 'named': + return getParam(params, astNode.value, nextIndexes, true); + case 'static': + return astNode.value; + case 'optional': + if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { + return stringify(astNode.value, params, nextIndexes); + } else { + return ''; + } + } +}; + +//############################################################################### +// UrlPattern + +var UrlPattern = function (arg1, arg2) { + // self awareness + if (arg1 instanceof UrlPattern) { + this.isRegex = arg1.isRegex; + this.regex = arg1.regex; + this.ast = arg1.ast; + this.names = arg1.names; + return; + } - var UrlPattern = function (arg1, arg2) { - // self awareness - if (arg1 instanceof UrlPattern) { - this.isRegex = arg1.isRegex; - this.regex = arg1.regex; - this.ast = arg1.ast; - this.names = arg1.names; - return; - } + this.isRegex = arg1 instanceof RegExp; - this.isRegex = arg1 instanceof RegExp; + if ('string' !== typeof arg1 && !this.isRegex) { + throw new TypeError('argument must be a regex or a string'); + } - if ('string' !== typeof arg1 && !this.isRegex) { - throw new TypeError('argument must be a regex or a string'); - } + // regex - // regex - - if (this.isRegex) { - this.regex = arg1; - if (arg2 != null) { - if (!Array.isArray(arg2)) { - throw new Error('if first argument is a regex the second argument may be an array of group names but you provided something else'); - } - let groupCount = regexGroupCount(this.regex); - if (arg2.length !== groupCount) { - throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ arg2.length }`); - } - this.names = arg2; + if (this.isRegex) { + this.regex = arg1; + if (arg2 != null) { + if (!Array.isArray(arg2)) { + throw new Error('if first argument is a regex the second argument may be an array of group names but you provided something else'); } - return; + let groupCount = regexGroupCount(this.regex); + if (arg2.length !== groupCount) { + throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ arg2.length }`); + } + this.names = arg2; } + return; + } - // string pattern + // string pattern - if (arg1 === '') { - throw new Error('argument must not be the empty string'); - } - let withoutWhitespace = arg1.replace(/\s+/g, ''); - if (withoutWhitespace !== arg1) { - throw new Error('argument must not contain whitespace'); - } - - let options = { - escapeChar: (arg2 != null ? arg2.escapeChar : undefined) || defaultOptions.escapeChar, - segmentNameStartChar: (arg2 != null ? arg2.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, - segmentNameCharset: (arg2 != null ? arg2.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, - segmentValueCharset: (arg2 != null ? arg2.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, - optionalSegmentStartChar: (arg2 != null ? arg2.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, - optionalSegmentEndChar: (arg2 != null ? arg2.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, - wildcardChar: (arg2 != null ? arg2.wildcardChar : undefined) || defaultOptions.wildcardChar - }; - - let parser = newParser(options); - let parsed = parser.pattern(arg1); - if (parsed == null) { - // TODO better error message - throw new Error("couldn't parse pattern"); - } - if (parsed.rest !== '') { - // TODO better error message - throw new Error("could only partially parse pattern"); - } - this.ast = parsed.value; + if (arg1 === '') { + throw new Error('argument must not be the empty string'); + } + let withoutWhitespace = arg1.replace(/\s+/g, ''); + if (withoutWhitespace !== arg1) { + throw new Error('argument must not contain whitespace'); + } - this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); - this.names = astNodeToNames(this.ast); + let options = { + escapeChar: (arg2 != null ? arg2.escapeChar : undefined) || defaultOptions.escapeChar, + segmentNameStartChar: (arg2 != null ? arg2.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, + segmentNameCharset: (arg2 != null ? arg2.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, + segmentValueCharset: (arg2 != null ? arg2.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, + optionalSegmentStartChar: (arg2 != null ? arg2.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, + optionalSegmentEndChar: (arg2 != null ? arg2.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, + wildcardChar: (arg2 != null ? arg2.wildcardChar : undefined) || defaultOptions.wildcardChar }; - UrlPattern.prototype.match = function (url) { - let match = this.regex.exec(url); - if (match == null) { - return null; - } + let parser = newParser(options); + let parsed = parser.pattern(arg1); + if (parsed == null) { + // TODO better error message + throw new Error("couldn't parse pattern"); + } + if (parsed.rest !== '') { + // TODO better error message + throw new Error("could only partially parse pattern"); + } + this.ast = parsed.value; - let groups = match.slice(1); - if (this.names) { - return keysAndValuesToObject(this.names, groups); - } else { - return groups; - } - }; + this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); + this.names = astNodeToNames(this.ast); +}; - UrlPattern.prototype.stringify = function (params) { - if (params == null) { - params = {}; - } - if (this.isRegex) { - throw new Error("can't stringify patterns generated from a regex"); - } - if (params !== Object(params)) { - throw new Error("argument must be an object or undefined"); - } - return stringify(this.ast, params, {}); - }; +UrlPattern.prototype.match = function (url) { + let match = this.regex.exec(url); + if (match == null) { + return null; + } - //############################################################################### - // exports - - // helpers - UrlPattern.escapeForRegex = escapeForRegex; - UrlPattern.concatMap = concatMap; - UrlPattern.stringConcatMap = stringConcatMap; - UrlPattern.regexGroupCount = regexGroupCount; - UrlPattern.keysAndValuesToObject = keysAndValuesToObject; - - // parsers - UrlPattern.P = P; - UrlPattern.newParser = newParser; - UrlPattern.defaultOptions = defaultOptions; - - // ast - UrlPattern.astNodeToRegexString = astNodeToRegexString; - UrlPattern.astNodeToNames = astNodeToNames; - UrlPattern.getParam = getParam; - UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; - UrlPattern.stringify = stringify; - - return UrlPattern; -}); + let groups = match.slice(1); + if (this.names) { + return keysAndValuesToObject(this.names, groups); + } else { + return groups; + } +}; + +UrlPattern.prototype.stringify = function (params) { + if (params == null) { + params = {}; + } + if (this.isRegex) { + throw new Error("can't stringify patterns generated from a regex"); + } + if (params !== Object(params)) { + throw new Error("argument must be an object or undefined"); + } + return stringify(this.ast, params, {}); +}; + +//############################################################################### +// exports + +// helpers +UrlPattern.escapeForRegex = escapeForRegex; +UrlPattern.concatMap = concatMap; +UrlPattern.stringConcatMap = stringConcatMap; +UrlPattern.regexGroupCount = regexGroupCount; +UrlPattern.keysAndValuesToObject = keysAndValuesToObject; + +// parsers +UrlPattern.P = P; +UrlPattern.newParser = newParser; +UrlPattern.defaultOptions = defaultOptions; + +// ast +UrlPattern.astNodeToRegexString = astNodeToRegexString; +UrlPattern.astNodeToNames = astNodeToNames; +UrlPattern.getParam = getParam; +UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; +UrlPattern.stringify = stringify; From 9f58ceeb43fc7d34084e45a283bb31e9ee5fada4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 15:48:14 -0500 Subject: [PATCH 003/117] delete src/url-pattern.coffee --- src/url-pattern.coffee | 444 ----------------------------------------- 1 file changed, 444 deletions(-) delete mode 100644 src/url-pattern.coffee diff --git a/src/url-pattern.coffee b/src/url-pattern.coffee deleted file mode 100644 index 10b6904..0000000 --- a/src/url-pattern.coffee +++ /dev/null @@ -1,444 +0,0 @@ -((root, factory) -> - # AMD - if ('function' is typeof define) and define.amd? - define([], factory) - # CommonJS - else if exports? - module.exports = factory() - # browser globals - else - root.UrlPattern = factory() -)(this, -> - -################################################################################ -# helpers - - # source: http://stackoverflow.com/a/3561711 - escapeForRegex = (string) -> - string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') - - concatMap = (array, f) -> - results = [] - i = -1 - length = array.length - while ++i < length - results = results.concat f(array[i]) - return results - - stringConcatMap = (array, f) -> - result = '' - i = -1 - length = array.length - while ++i < length - result += f(array[i]) - return result - - # source: http://stackoverflow.com/a/16047223 - regexGroupCount = (regex) -> - (new RegExp(regex.toString() + '|')).exec('').length - 1 - - keysAndValuesToObject = (keys, values) -> - object = {} - i = -1 - length = keys.length - while ++i < length - key = keys[i] - value = values[i] - unless value? - continue - # key already encountered - if object[key]? - # capture multiple values for same key in an array - unless Array.isArray object[key] - object[key] = [object[key]] - object[key].push value - else - object[key] = value - return object - -################################################################################ -# parser combinators -# subset copied from -# https://github.com/snd/pcom/blob/master/src/pcom.coffee -# (where they are tested !) -# to keep this at zero dependencies and small filesize - - P = {} - - P.Result = (value, rest) -> - this.value = value - this.rest = rest - return - - P.Tagged = (tag, value) -> - this.tag = tag - this.value = value - return - - P.tag = (tag, parser) -> - (input) -> - result = parser input - unless result? - return - tagged = new P.Tagged tag, result.value - return new P.Result tagged, result.rest - - P.regex = (regex) -> - # unless regex instanceof RegExp - # throw new Error 'argument must be instanceof RegExp' - (input) -> - matches = regex.exec input - unless matches? - return - result = matches[0] - return new P.Result result, input.slice(result.length) - - P.sequence = (parsers...) -> - (input) -> - i = -1 - length = parsers.length - values = [] - rest = input - while ++i < length - parser = parsers[i] - # unless 'function' is typeof parser - # throw new Error "parser passed at index `#{i}` into `sequence` is not of type `function` but of type `#{typeof parser}`" - result = parser rest - unless result? - return - values.push result.value - rest = result.rest - return new P.Result values, rest - - P.pick = (indexes, parsers...) -> - (input) -> - result = P.sequence(parsers...)(input) - unless result? - return - array = result.value - result.value = array[indexes] - # unless Array.isArray indexes - # result.value = array[indexes] - # else - # result.value = [] - # indexes.forEach (i) -> - # result.value.push array[i] - return result - - P.string = (string) -> - length = string.length - # if length is 0 - # throw new Error '`string` must not be blank' - (input) -> - if input.slice(0, length) is string - return new P.Result string, input.slice(length) - - P.lazy = (fn) -> - cached = null - (input) -> - unless cached? - cached = fn() - return cached input - - P.baseMany = (parser, end, stringResult, atLeastOneResultRequired, input) -> - rest = input - results = if stringResult then '' else [] - while true - if end? - endResult = end rest - if endResult? - break - parserResult = parser rest - unless parserResult? - break - if stringResult - results += parserResult.value - else - results.push parserResult.value - rest = parserResult.rest - - if atLeastOneResultRequired and results.length is 0 - return - - return new P.Result results, rest - - P.many1 = (parser) -> - (input) -> - P.baseMany parser, null, false, true, input - - P.concatMany1Till = (parser, end) -> - (input) -> - P.baseMany parser, end, true, true, input - - P.firstChoice = (parsers...) -> - (input) -> - i = -1 - length = parsers.length - while ++i < length - parser = parsers[i] - # unless 'function' is typeof parser - # throw new Error "parser passed at index `#{i}` into `firstChoice` is not of type `function` but of type `#{typeof parser}`" - result = parser input - if result? - return result - return - -################################################################################ -# url pattern parser -# copied from -# https://github.com/snd/pcom/blob/master/src/url-pattern-example.coffee - - newParser = (options) -> - U = {} - - U.wildcard = P.tag 'wildcard', P.string(options.wildcardChar) - - U.optional = P.tag( - 'optional' - P.pick(1, - P.string(options.optionalSegmentStartChar) - P.lazy(-> U.pattern) - P.string(options.optionalSegmentEndChar) - ) - ) - - U.name = P.regex new RegExp "^[#{options.segmentNameCharset}]+" - - U.named = P.tag( - 'named', - P.pick(1, - P.string(options.segmentNameStartChar) - P.lazy(-> U.name) - ) - ) - - U.escapedChar = P.pick(1, - P.string(options.escapeChar) - P.regex(/^./) - ) - - U.static = P.tag( - 'static' - P.concatMany1Till( - P.firstChoice( - P.lazy(-> U.escapedChar) - P.regex(/^./) - ) - P.firstChoice( - P.string(options.segmentNameStartChar) - P.string(options.optionalSegmentStartChar) - P.string(options.optionalSegmentEndChar) - U.wildcard - ) - ) - ) - - U.token = P.lazy -> - P.firstChoice( - U.wildcard - U.optional - U.named - U.static - ) - - U.pattern = P.many1 P.lazy(-> U.token) - - return U - -################################################################################ -# options - - defaultOptions = - escapeChar: '\\' - segmentNameStartChar: ':' - segmentValueCharset: 'a-zA-Z0-9-_~ %' - segmentNameCharset: 'a-zA-Z0-9' - optionalSegmentStartChar: '(' - optionalSegmentEndChar: ')' - wildcardChar: '*' - -################################################################################ -# functions that further process ASTs returned as `.value` in parser results - - baseAstNodeToRegexString = (astNode, segmentValueCharset) -> - if Array.isArray astNode - return stringConcatMap astNode, (node) -> - baseAstNodeToRegexString(node, segmentValueCharset) - - switch astNode.tag - when 'wildcard' then '(.*?)' - when 'named' then "([#{segmentValueCharset}]+)" - when 'static' then escapeForRegex(astNode.value) - when 'optional' - '(?:' + baseAstNodeToRegexString(astNode.value, segmentValueCharset) + ')?' - - astNodeToRegexString = (astNode, segmentValueCharset = defaultOptions.segmentValueCharset) -> - '^' + baseAstNodeToRegexString(astNode, segmentValueCharset) + '$' - - astNodeToNames = (astNode) -> - if Array.isArray astNode - return concatMap astNode, astNodeToNames - - switch astNode.tag - when 'wildcard' then ['_'] - when 'named' then [astNode.value] - when 'static' then [] - when 'optional' then astNodeToNames(astNode.value) - - getParam = (params, key, nextIndexes, sideEffects = false) -> - value = params[key] - unless value? - if sideEffects - throw new Error "no values provided for key `#{key}`" - else - return - index = nextIndexes[key] or 0 - maxIndex = if Array.isArray value then value.length - 1 else 0 - if index > maxIndex - if sideEffects - throw new Error "too few values provided for key `#{key}`" - else - return - - result = if Array.isArray value then value[index] else value - - if sideEffects - nextIndexes[key] = index + 1 - - return result - - astNodeContainsSegmentsForProvidedParams = (astNode, params, nextIndexes) -> - if Array.isArray astNode - i = -1 - length = astNode.length - while ++i < length - if astNodeContainsSegmentsForProvidedParams astNode[i], params, nextIndexes - return true - return false - - switch astNode.tag - when 'wildcard' then getParam(params, '_', nextIndexes, false)? - when 'named' then getParam(params, astNode.value, nextIndexes, false)? - when 'static' then false - when 'optional' - astNodeContainsSegmentsForProvidedParams astNode.value, params, nextIndexes - - stringify = (astNode, params, nextIndexes) -> - if Array.isArray astNode - return stringConcatMap astNode, (node) -> - stringify node, params, nextIndexes - - switch astNode.tag - when 'wildcard' then getParam params, '_', nextIndexes, true - when 'named' then getParam params, astNode.value, nextIndexes, true - when 'static' then astNode.value - when 'optional' - if astNodeContainsSegmentsForProvidedParams astNode.value, params, nextIndexes - stringify astNode.value, params, nextIndexes - else - '' - -################################################################################ -# UrlPattern - - UrlPattern = (arg1, arg2) -> - # self awareness - if arg1 instanceof UrlPattern - @isRegex = arg1.isRegex - @regex = arg1.regex - @ast = arg1.ast - @names = arg1.names - return - - @isRegex = arg1 instanceof RegExp - - unless ('string' is typeof arg1) or @isRegex - throw new TypeError 'argument must be a regex or a string' - - # regex - - if @isRegex - @regex = arg1 - if arg2? - unless Array.isArray arg2 - throw new Error 'if first argument is a regex the second argument may be an array of group names but you provided something else' - groupCount = regexGroupCount @regex - unless arg2.length is groupCount - throw new Error "regex contains #{groupCount} groups but array of group names contains #{arg2.length}" - @names = arg2 - return - - # string pattern - - if arg1 is '' - throw new Error 'argument must not be the empty string' - withoutWhitespace = arg1.replace(/\s+/g, '') - unless withoutWhitespace is arg1 - throw new Error 'argument must not contain whitespace' - - options = - escapeChar: arg2?.escapeChar or defaultOptions.escapeChar - segmentNameStartChar: arg2?.segmentNameStartChar or defaultOptions.segmentNameStartChar - segmentNameCharset: arg2?.segmentNameCharset or defaultOptions.segmentNameCharset - segmentValueCharset: arg2?.segmentValueCharset or defaultOptions.segmentValueCharset - optionalSegmentStartChar: arg2?.optionalSegmentStartChar or defaultOptions.optionalSegmentStartChar - optionalSegmentEndChar: arg2?.optionalSegmentEndChar or defaultOptions.optionalSegmentEndChar - wildcardChar: arg2?.wildcardChar or defaultOptions.wildcardChar - - parser = newParser options - parsed = parser.pattern arg1 - unless parsed? - # TODO better error message - throw new Error "couldn't parse pattern" - if parsed.rest isnt '' - # TODO better error message - throw new Error "could only partially parse pattern" - @ast = parsed.value - - @regex = new RegExp astNodeToRegexString @ast, options.segmentValueCharset - @names = astNodeToNames @ast - - return - - UrlPattern.prototype.match = (url) -> - match = @regex.exec url - unless match? - return null - - groups = match.slice(1) - if @names - keysAndValuesToObject @names, groups - else - groups - - UrlPattern.prototype.stringify = (params = {}) -> - if @isRegex - throw new Error "can't stringify patterns generated from a regex" - unless params is Object(params) - throw new Error "argument must be an object or undefined" - stringify @ast, params, {} - -################################################################################ -# exports - - # helpers - UrlPattern.escapeForRegex = escapeForRegex - UrlPattern.concatMap = concatMap - UrlPattern.stringConcatMap = stringConcatMap - UrlPattern.regexGroupCount = regexGroupCount - UrlPattern.keysAndValuesToObject = keysAndValuesToObject - - # parsers - UrlPattern.P = P - UrlPattern.newParser = newParser - UrlPattern.defaultOptions = defaultOptions - - # ast - UrlPattern.astNodeToRegexString = astNodeToRegexString - UrlPattern.astNodeToNames = astNodeToNames - UrlPattern.getParam = getParam - UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams - UrlPattern.stringify = stringify - - return UrlPattern -) From 26b44dc538ac76ca459f3fd41d490d9ced399719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 15:48:32 -0500 Subject: [PATCH 004/117] no longer need index.d.ts since we use ts for original source --- index.d.ts | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 347cdd1..0000000 --- a/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface UrlPatternOptions { - escapeChar?: string; - segmentNameStartChar?: string; - segmentValueCharset?: string; - segmentNameCharset?: string; - optionalSegmentStartChar?: string; - optionalSegmentEndChar?: string; - wildcardChar?: string; -} - -declare class UrlPattern { - constructor(pattern: string, options?: UrlPatternOptions); - constructor(pattern: RegExp, groupNames?: string[]); - - match(url: string): any; - stringify(values?: any): string; -} - -declare module UrlPattern { } - -export = UrlPattern; From 96932add27f5edf3e5b375f1144bfef15768d9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 20:33:08 -0500 Subject: [PATCH 005/117] url-pattern.ts: refactoring into correct typescript --- url-pattern.ts | 649 ++++++++++++++++++++++++++----------------------- 1 file changed, 351 insertions(+), 298 deletions(-) diff --git a/url-pattern.ts b/url-pattern.ts index cf3a50b..042018b 100644 --- a/url-pattern.ts +++ b/url-pattern.ts @@ -1,243 +1,285 @@ -//############################################################################### -// helpers +// OPTIONS + +interface UrlPatternOptions { + escapeChar?: string; + segmentNameStartChar?: string; + segmentValueCharset?: string; + segmentNameCharset?: string; + optionalSegmentStartChar?: string; + optionalSegmentEndChar?: string; + wildcardChar?: string; +} + +const defaultOptions: UrlPatternOptions = { + escapeChar: '\\', + segmentNameStartChar: ':', + segmentValueCharset: 'a-zA-Z0-9-_~ %', + segmentNameCharset: 'a-zA-Z0-9', + optionalSegmentStartChar: '(', + optionalSegmentEndChar: ')', + wildcardChar: '*' +}; -// source: http://stackoverflow.com/a/3561711 -let escapeForRegex = string => string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +// HELPERS -let concatMap = function (array, f) { - let results = []; - let i = -1; - let { length } = array; - while (++i < length) { - results = results.concat(f(array[i])); - } +// escapes a string for insertion into a regular expression +// source: http://stackoverflow.com/a/3561711 +function escapeStringForRegex(string: string) : string { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); +} + +function concatMap(array: Array, f: (T) => Array) : Array { + let results: Array = []; + array.forEach(function(value) { + results = results.concat(f(value)); + }); return results; -}; +} -let stringConcatMap = function (array, f) { +function stringConcatMap(array: Array, f: (T) => string) : string { let result = ''; - let i = -1; - let { length } = array; - while (++i < length) { - result += f(array[i]); - } + array.forEach(function(value) { + result += f(value); + }); return result; }; +// returns the number of groups in the `regex`. // source: http://stackoverflow.com/a/16047223 -let regexGroupCount = regex => new RegExp(regex.toString() + '|').exec('').length - 1; +function regexGroupCount(regex: RegExp) : number { + return new RegExp(regex.toString() + "|").exec("").length - 1; +} + +// zips an array of `keys` and an array of `values` into an object. +// `keys` and `values` must have the same length. +// if the same key appears multiple times the associated values are collected in an array. +function keysAndValuesToObject(keys: Array, values: Array) : Object { + let result = {}; + + if (keys.length !== values.length) { + throw Error("keys.length must equal values.length"); + } -let keysAndValuesToObject = function (keys, values) { - let object = {}; let i = -1; let { length } = keys; - while (++i < length) { + while (++i < keys.length) { let key = keys[i]; let value = values[i]; + if (value == null) { continue; } + // key already encountered - if (object[key] != null) { + if (result[key] != null) { // capture multiple values for same key in an array - if (!Array.isArray(object[key])) { - object[key] = [object[key]]; + if (!Array.isArray(result[key])) { + result[key] = [result[key]]; } - object[key].push(value); + result[key].push(value); } else { - object[key] = value; + result[key] = value; } } - return object; -}; - -//############################################################################### -// parser combinators -// subset copied from -// https://github.com/snd/pcom/blob/master/src/pcom.coffee -// (where they are tested !) -// to keep this at zero dependencies and small filesize - -let P = {}; - -P.Result = function (value, rest) { - this.value = value; - this.rest = rest; -}; - -P.Tagged = function (tag, value) { - this.tag = tag; - this.value = value; -}; - -P.tag = (tag, parser) => function (input) { - let result = parser(input); - if (result == null) { - return; - } - let tagged = new P.Tagged(tag, result.value); - return new P.Result(tagged, result.rest); + return result; }; -P.regex = regex => -// unless regex instanceof RegExp -// throw new Error 'argument must be instanceof RegExp' -function (input) { - let matches = regex.exec(input); - if (matches == null) { - return; - } - let result = matches[0]; - return new P.Result(result, input.slice(result.length)); -}; +// PARSER COMBINATORS -P.sequence = (...parsers) => function (input) { - let i = -1; - let { length } = parsers; - let values = []; - let rest = input; - while (++i < length) { - let parser = parsers[i]; - // unless 'function' is typeof parser - // throw new Error "parser passed at index `#{i}` into `sequence` is not of type `function` but of type `#{typeof parser}`" - let result = parser(rest); - if (result == null) { - return; - } - values.push(result.value); - ({ rest } = result); +// parse result +class Result { + // parsed value + value: Value; + // unparsed rest + readonly rest: string; + constructor(value: Value, rest: string) { + this.value = value; + this.rest = rest; } - return new P.Result(values, rest); -}; +} -P.pick = (indexes, ...parsers) => function (input) { - let result = P.sequence(...Array.from(parsers || []))(input); - if (result == null) { - return; +class Tagged { + readonly tag: string; + readonly value: Value; + constructor(tag: string, value: Value) { + this.tag = tag; + this.value = value; } - let array = result.value; - result.value = array[indexes]; - // unless Array.isArray indexes - // result.value = array[indexes] - // else - // result.value = [] - // indexes.forEach (i) -> - // result.value.push array[i] - return result; -}; +} -P.string = function (string) { - let { length } = string; - // if length is 0 - // throw new Error '`string` must not be blank' - return function (input) { - if (input.slice(0, length) === string) { - return new P.Result(string, input.slice(length)); - } - }; -}; +// a parser is a function that takes a string and returns a `Result` containing a parsed `Result.value` and the rest of the string `Result.rest` +type Parser = (string) => Result | null; -P.lazy = function (fn) { - let cached = null; - return function (input) { - if (cached == null) { - cached = fn(); +// parser combinators +let P = { + Result: Result, + Tagged: Tagged, + // transforms a `parser` into a parser that tags its `Result.value` with `tag` + tag(tag: string, parser: Parser) : Parser> { + return function(input: string): Result> | null { + let result = parser(input); + if (result == null) { + return null; + } + let tagged = new Tagged(tag, result.value); + return new Result(tagged, result.rest); } - return cached(input); - }; -}; - -P.baseMany = function (parser, end, stringResult, atLeastOneResultRequired, input) { - let rest = input; - let results = stringResult ? '' : []; - while (true) { - if (end != null) { - let endResult = end(rest); - if (endResult != null) { - break; + }, + // parser that consumes everything matched by `regex` + regex(regex: RegExp) : Parser { + return function(input: string): Result | null { + let matches = regex.exec(input); + if (matches == null) { + return null; } + let result = matches[0]; + return new Result(result, input.slice(result.length)); } - let parserResult = parser(rest); - if (parserResult == null) { - break; + }, + // takes a sequence of parsers and returns a parser that runs + // them in sequence and produces an array of their results + sequence(...parsers: Array>) : Parser> { + return function(input: string): Result> | null { + let rest = input; + let values = []; + parsers.forEach(function(parser) { + let result = parser(rest); + if (result == null) { + return null; + } + values.push(result.value); + rest = result.rest; + }); + return new Result(values, rest); } - if (stringResult) { - results += parserResult.value; - } else { + }, + // returns a parser that consumes `str` exactly + string(str: string) : Parser { + let { length } = str; + return function(input: string) : Result | null { + if (input.slice(0, length) === str) { + return new Result(str, input.slice(length)); + } + }; + }, + // takes a sequence of parser and only returns the result + // returned by the `index`th parser + pick(index, ...parsers: Array>) : Parser { + let parser = P.sequence(...parsers); + return function(input: string) : Result | null { + let result = parser(input); + if (result == null) { + return null; + } + let values = result.value; + result.value = values[index]; + return result; + } + }, + // for parsers that each depend on one another (cyclic dependencies) + // postpone lookup to when they both exist. + lazy(get_parser: () => Parser): Parser { + let cached_parser = null; + return function (input: string): Result | null { + if (cached_parser == null) { + cached_parser = get_parser(); + } + return cached_parser(input); + }; + }, + // base function for parsers that parse multiples + baseMany( + parser: Parser, + // once the `endParser` (if not null) consumes the `baseMany` parser returns. + // the result of the `endParser` is ignored + endParser: Parser | null, + isAtLeastOneResultRequired: boolean, + input: string + ) : Result> | null { + let rest = input; + let results: Array = []; + while (true) { + if (endParser != null) { + let endResult = endParser(rest); + if (endResult != null) { + break; + } + } + let parserResult = parser(rest); + if (parserResult == null) { + break; + } results.push(parserResult.value); + rest = parserResult.rest; } - ({ rest } = parserResult); - } - if (atLeastOneResultRequired && results.length === 0) { - return; - } - - return new P.Result(results, rest); -}; - -P.many1 = parser => input => P.baseMany(parser, null, false, true, input); - -P.concatMany1Till = (parser, end) => input => P.baseMany(parser, end, true, true, input); + if (isAtLeastOneResultRequired && results.length === 0) { + return null; + } -P.firstChoice = (...parsers) => function (input) { - let i = -1; - let { length } = parsers; - while (++i < length) { - let parser = parsers[i]; - // unless 'function' is typeof parser - // throw new Error "parser passed at index `#{i}` into `firstChoice` is not of type `function` but of type `#{typeof parser}`" - let result = parser(input); - if (result != null) { - return result; + return new Result(results, rest); + }, + many1(parser: Parser) : Parser> { + return function(input: string) : Result> { + const endParser = null; + const isAtLeastOneResultRequired = true; + return P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); + } + }, + concatMany1Till(parser: Parser, endParser: Parser) : Parser { + return function(input: string) : Result | null { + const isAtLeastOneResultRequired = true; + let result = P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); + if (result == null) { + return null; + } + return new Result(result.value.join(""), result.rest); + } + }, + // takes a sequence of parsers. returns the result from the first + // parser that consumes the input. + firstChoice(...parsers: Array>) : Parser { + return function(input: string) : Result | null { + parsers.forEach(function(parser) { + let result = parser(input); + if (result != null) { + return result; + } + }); + return null; } } -}; - -//############################################################################### -// url pattern parser -// copied from -// https://github.com/snd/pcom/blob/master/src/url-pattern-example.coffee - -let newParser = function (options) { - let U = {}; - - U.wildcard = P.tag('wildcard', P.string(options.wildcardChar)); - - U.optional = P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(() => U.pattern), P.string(options.optionalSegmentEndChar))); - - U.name = P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); - - U.named = P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))); +} - U.escapedChar = P.pick(1, P.string(options.escapeChar), P.regex(/^./)); +// URL PATTERN PARSER - U.static = P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(() => U.escapedChar), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), U.wildcard))); +interface UrlPatternParser { + wildcard: Parser>, + optional: Parser>, + name: Parser, + named: Parser>, + escapedChar: Parser, + pattern: Parser, +} - U.token = P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)); - - U.pattern = P.many1(P.lazy(() => U.token)); +function newUrlPatternParser(options: UrlPatternOptions) : UrlPatternParser { + let U = { + wildcard: P.tag('wildcard', P.string(options.wildcardChar)), + optional: P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(() => U.pattern), P.string(options.optionalSegmentEndChar))), + name: P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)), + named: P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))), + escapedChar: P.pick(1, P.string(options.escapeChar), P.regex(/^./)), + static: P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(() => U.escapedChar), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), P.lazy(() => U.wildcard)))), + token: P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)), + pattern: P.many1(P.lazy(() => U.token)), + } return U; }; -//############################################################################### -// options - -let defaultOptions = { - escapeChar: '\\', - segmentNameStartChar: ':', - segmentValueCharset: 'a-zA-Z0-9-_~ %', - segmentNameCharset: 'a-zA-Z0-9', - optionalSegmentStartChar: '(', - optionalSegmentEndChar: ')', - wildcardChar: '*' -}; - -//############################################################################### // functions that further process ASTs returned as `.value` in parser results -var baseAstNodeToRegexString = function (astNode, segmentValueCharset) { +function baseAstNodeToRegexString(astNode, segmentValueCharset) { if (Array.isArray(astNode)) { return stringConcatMap(astNode, node => baseAstNodeToRegexString(node, segmentValueCharset)); } @@ -248,7 +290,7 @@ var baseAstNodeToRegexString = function (astNode, segmentValueCharset) { case 'named': return `([${ segmentValueCharset }]+)`; case 'static': - return escapeForRegex(astNode.value); + return escapeStringForRegex(astNode.value); case 'optional': return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; } @@ -278,7 +320,7 @@ var astNodeToNames = function (astNode) { } }; -let getParam = function (params, key, nextIndexes, sideEffects) { +function getParam(params, key, nextIndexes, sideEffects) { if (sideEffects == null) { sideEffects = false; } @@ -333,7 +375,7 @@ var astNodeContainsSegmentsForProvidedParams = function (astNode, params, nextIn } }; -var stringify = function (astNode, params, nextIndexes) { +function stringify(astNode, params, nextIndexes) { if (Array.isArray(astNode)) { return stringConcatMap(astNode, node => stringify(node, params, nextIndexes)); } @@ -354,123 +396,134 @@ var stringify = function (astNode, params, nextIndexes) { } }; -//############################################################################### -// UrlPattern - -var UrlPattern = function (arg1, arg2) { - // self awareness - if (arg1 instanceof UrlPattern) { - this.isRegex = arg1.isRegex; - this.regex = arg1.regex; - this.ast = arg1.ast; - this.names = arg1.names; - return; - } - - this.isRegex = arg1 instanceof RegExp; +class UrlPattern { + readonly isRegex: boolean; + readonly regex: RegExp; + readonly ast: Object; + readonly names: Array; + + constructor(pattern: string, options?: UrlPatternOptions); + constructor(pattern: RegExp, groupNames?: Array); + + constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: UrlPatternOptions | Array) { + // self awareness + if (pattern instanceof UrlPattern) { + this.isRegex = pattern.isRegex; + this.regex = pattern.regex; + this.ast = pattern.ast; + this.names = pattern.names; + return; + } - if ('string' !== typeof arg1 && !this.isRegex) { - throw new TypeError('argument must be a regex or a string'); - } + this.isRegex = pattern instanceof RegExp; - // regex + if ("string" !== typeof pattern && !this.isRegex) { + throw new TypeError("first argument must be a RegExp, a string or an instance of UrlPattern"); + } - if (this.isRegex) { - this.regex = arg1; - if (arg2 != null) { - if (!Array.isArray(arg2)) { - throw new Error('if first argument is a regex the second argument may be an array of group names but you provided something else'); + // handle regex pattern and return early + if (pattern instanceof RegExp) { + this.regex = pattern; + if (optionsOrGroupNames != null) { + if (!Array.isArray(optionsOrGroupNames)) { + throw new TypeError("if first argument is a RegExp the second argument may be an Array of group names but you provided something else"); + } + let groupCount = regexGroupCount(this.regex); + if (optionsOrGroupNames.length !== groupCount) { + throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ optionsOrGroupNames.length }`); + } + this.names = optionsOrGroupNames; } - let groupCount = regexGroupCount(this.regex); - if (arg2.length !== groupCount) { - throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ arg2.length }`); - } - this.names = arg2; + return; } - return; - } - // string pattern + // everything following only concerns string patterns - if (arg1 === '') { - throw new Error('argument must not be the empty string'); - } - let withoutWhitespace = arg1.replace(/\s+/g, ''); - if (withoutWhitespace !== arg1) { - throw new Error('argument must not contain whitespace'); - } + if (pattern === '') { + throw new Error('first argument must not be the empty string'); + } + let patternWithoutWhitespace = pattern.replace(/\s+/g, ""); + if (patternWithoutWhitespace !== pattern) { + throw new Error("first argument must not contain whitespace"); + } - let options = { - escapeChar: (arg2 != null ? arg2.escapeChar : undefined) || defaultOptions.escapeChar, - segmentNameStartChar: (arg2 != null ? arg2.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, - segmentNameCharset: (arg2 != null ? arg2.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, - segmentValueCharset: (arg2 != null ? arg2.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, - optionalSegmentStartChar: (arg2 != null ? arg2.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, - optionalSegmentEndChar: (arg2 != null ? arg2.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, - wildcardChar: (arg2 != null ? arg2.wildcardChar : undefined) || defaultOptions.wildcardChar - }; - - let parser = newParser(options); - let parsed = parser.pattern(arg1); - if (parsed == null) { - // TODO better error message - throw new Error("couldn't parse pattern"); - } - if (parsed.rest !== '') { - // TODO better error message - throw new Error("could only partially parse pattern"); - } - this.ast = parsed.value; + if (Array.isArray(optionsOrGroupNames)) { + throw new Error("if first argument is a string second argument must be an options object or undefined"); + } - this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); - this.names = astNodeToNames(this.ast); -}; + let options: UrlPatternOptions = { + escapeChar: (typeof optionsOrGroupNames != null ? optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, + segmentNameStartChar: (optionsOrGroupNames != null ? optionsOrGroupNames.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, + segmentNameCharset: (optionsOrGroupNames != null ? optionsOrGroupNames.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, + segmentValueCharset: (optionsOrGroupNames != null ? optionsOrGroupNames.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, + optionalSegmentStartChar: (optionsOrGroupNames != null ? optionsOrGroupNames.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, + optionalSegmentEndChar: (optionsOrGroupNames != null ? optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, + wildcardChar: (optionsOrGroupNames != null ? optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar + }; + + let parser: UrlPatternParser = newUrlPatternParser(options); + let parsed = parser.pattern(pattern); + if (parsed == null) { + // TODO better error message + throw new Error("couldn't parse pattern"); + } + if (parsed.rest !== '') { + // TODO better error message + throw new Error("could only partially parse pattern"); + } + this.ast = parsed.value; -UrlPattern.prototype.match = function (url) { - let match = this.regex.exec(url); - if (match == null) { - return null; - } + this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); + this.names = astNodeToNames(this.ast); - let groups = match.slice(1); - if (this.names) { - return keysAndValuesToObject(this.names, groups); - } else { - return groups; } -}; -UrlPattern.prototype.stringify = function (params) { - if (params == null) { - params = {}; - } - if (this.isRegex) { - throw new Error("can't stringify patterns generated from a regex"); - } - if (params !== Object(params)) { - throw new Error("argument must be an object or undefined"); + match(url: string): Object { + let match = this.regex.exec(url); + if (match == null) { + return null; + } + + let groups = match.slice(1); + if (this.names) { + return keysAndValuesToObject(this.names, groups); + } else { + return groups; + } } - return stringify(this.ast, params, {}); -}; -//############################################################################### -// exports - -// helpers -UrlPattern.escapeForRegex = escapeForRegex; -UrlPattern.concatMap = concatMap; -UrlPattern.stringConcatMap = stringConcatMap; -UrlPattern.regexGroupCount = regexGroupCount; -UrlPattern.keysAndValuesToObject = keysAndValuesToObject; - -// parsers -UrlPattern.P = P; -UrlPattern.newParser = newParser; -UrlPattern.defaultOptions = defaultOptions; - -// ast -UrlPattern.astNodeToRegexString = astNodeToRegexString; -UrlPattern.astNodeToNames = astNodeToNames; -UrlPattern.getParam = getParam; -UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; -UrlPattern.stringify = stringify; + stringify(params?: Object): string { + if (params == null) { + params = {}; + } + if (this.isRegex) { + throw new Error("can't stringify patterns generated from a regex"); + } + if (params !== Object(params)) { + throw new Error("argument must be an object or undefined"); + } + return stringify(this.ast, params, {}); + } + + // make helpers available directly on UrlPattern + static escapeStringForRegex = escapeStringForRegex; + static concatMap = concatMap; + static stringConcatMap = stringConcatMap; + static regexGroupCount = regexGroupCount; + static keysAndValuesToObject = keysAndValuesToObject; + + // make AST helpers available directly on UrlPattern + static astNodeToRegexString = astNodeToRegexString; + static astNodeToNames = astNodeToNames; + static getParam = getParam; + static astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; + static stringify = stringify; + + // make parsers available directly on UrlPattern + static P = P; + static newUrlPatternParser = newUrlPatternParser; + static defaultOptions = defaultOptions; +} + +// export only the UrlPattern class +export = UrlPattern; From d842dee6406b0eb23af591efd7bd16c01a73f5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 20:34:00 -0500 Subject: [PATCH 006/117] url-pattern.ts -> index.ts --- url-pattern.ts => index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename url-pattern.ts => index.ts (100%) diff --git a/url-pattern.ts b/index.ts similarity index 100% rename from url-pattern.ts rename to index.ts From 85c6ef3647e73b462bd93d10172d7bb29debd003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 21:01:32 -0500 Subject: [PATCH 007/117] index.ts: consistency and comments --- index.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/index.ts b/index.ts index 042018b..a525f7f 100644 --- a/index.ts +++ b/index.ts @@ -88,9 +88,9 @@ function keysAndValuesToObject(keys: Array, values: Array) : Object { // parse result class Result { - // parsed value + /* parsed value */ value: Value; - // unparsed rest + /* unparsed rest */ readonly rest: string; constructor(value: Value, rest: string) { this.value = value; @@ -107,8 +107,10 @@ class Tagged { } } -// a parser is a function that takes a string and returns a `Result` containing a parsed `Result.value` and the rest of the string `Result.rest` -type Parser = (string) => Result | null; +/** + * a parser is a function that takes a string and returns a `Result` containing a parsed `Result.value` and the rest of the string `Result.rest` + */ +type Parser = (string: string) => Result | null; // parser combinators let P = { @@ -187,11 +189,12 @@ let P = { return cached_parser(input); }; }, - // base function for parsers that parse multiples + /* + * base function for parsers that parse multiples. + * @param endParser once the `endParser` (if not null) consumes the `baseMany` parser returns. the result of the `endParser` is ignored. + */ baseMany( parser: Parser, - // once the `endParser` (if not null) consumes the `baseMany` parser returns. - // the result of the `endParser` is ignored endParser: Parser | null, isAtLeastOneResultRequired: boolean, input: string @@ -296,14 +299,14 @@ function baseAstNodeToRegexString(astNode, segmentValueCharset) { } }; -let astNodeToRegexString = function (astNode, segmentValueCharset) { +function astNodeToRegexString(astNode, segmentValueCharset) { if (segmentValueCharset == null) { ({ segmentValueCharset } = defaultOptions); } return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; -}; +} -var astNodeToNames = function (astNode) { +function astNodeToNames(astNode) { if (Array.isArray(astNode)) { return concatMap(astNode, astNodeToNames); } @@ -318,7 +321,7 @@ var astNodeToNames = function (astNode) { case 'optional': return astNodeToNames(astNode.value); } -}; +} function getParam(params, key, nextIndexes, sideEffects) { if (sideEffects == null) { @@ -351,7 +354,7 @@ function getParam(params, key, nextIndexes, sideEffects) { return result; }; -var astNodeContainsSegmentsForProvidedParams = function (astNode, params, nextIndexes) { +function astNodeContainsSegmentsForProvidedParams(astNode, params, nextIndexes) { if (Array.isArray(astNode)) { let i = -1; let { length } = astNode; @@ -373,7 +376,7 @@ var astNodeContainsSegmentsForProvidedParams = function (astNode, params, nextIn case 'optional': return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); } -}; +} function stringify(astNode, params, nextIndexes) { if (Array.isArray(astNode)) { @@ -478,7 +481,7 @@ class UrlPattern { } - match(url: string): Object { + match(url: string): Object | null { let match = this.regex.exec(url); if (match == null) { return null; From 818f4d7fc8fc3172330c212b06a962c46133a3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 21:01:44 -0500 Subject: [PATCH 008/117] add initial tsconfig.json --- tsconfig.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4dcc4dd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true + }, + "files": [ + "index.ts" + ] +} From a4cdde685ce976c2875dfba2b883a3811616b86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 21:11:39 -0500 Subject: [PATCH 009/117] remove compiled js in lib/url-pattern.js --- lib/url-pattern.js | 436 --------------------------------------------- 1 file changed, 436 deletions(-) delete mode 100644 lib/url-pattern.js diff --git a/lib/url-pattern.js b/lib/url-pattern.js deleted file mode 100644 index 0d635c2..0000000 --- a/lib/url-pattern.js +++ /dev/null @@ -1,436 +0,0 @@ -// Generated by CoffeeScript 1.10.0 -var slice = [].slice; - -(function(root, factory) { - if (('function' === typeof define) && (define.amd != null)) { - return define([], factory); - } else if (typeof exports !== "undefined" && exports !== null) { - return module.exports = factory(); - } else { - return root.UrlPattern = factory(); - } -})(this, function() { - var P, UrlPattern, astNodeContainsSegmentsForProvidedParams, astNodeToNames, astNodeToRegexString, baseAstNodeToRegexString, concatMap, defaultOptions, escapeForRegex, getParam, keysAndValuesToObject, newParser, regexGroupCount, stringConcatMap, stringify; - escapeForRegex = function(string) { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - }; - concatMap = function(array, f) { - var i, length, results; - results = []; - i = -1; - length = array.length; - while (++i < length) { - results = results.concat(f(array[i])); - } - return results; - }; - stringConcatMap = function(array, f) { - var i, length, result; - result = ''; - i = -1; - length = array.length; - while (++i < length) { - result += f(array[i]); - } - return result; - }; - regexGroupCount = function(regex) { - return (new RegExp(regex.toString() + '|')).exec('').length - 1; - }; - keysAndValuesToObject = function(keys, values) { - var i, key, length, object, value; - object = {}; - i = -1; - length = keys.length; - while (++i < length) { - key = keys[i]; - value = values[i]; - if (value == null) { - continue; - } - if (object[key] != null) { - if (!Array.isArray(object[key])) { - object[key] = [object[key]]; - } - object[key].push(value); - } else { - object[key] = value; - } - } - return object; - }; - P = {}; - P.Result = function(value, rest) { - this.value = value; - this.rest = rest; - }; - P.Tagged = function(tag, value) { - this.tag = tag; - this.value = value; - }; - P.tag = function(tag, parser) { - return function(input) { - var result, tagged; - result = parser(input); - if (result == null) { - return; - } - tagged = new P.Tagged(tag, result.value); - return new P.Result(tagged, result.rest); - }; - }; - P.regex = function(regex) { - return function(input) { - var matches, result; - matches = regex.exec(input); - if (matches == null) { - return; - } - result = matches[0]; - return new P.Result(result, input.slice(result.length)); - }; - }; - P.sequence = function() { - var parsers; - parsers = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return function(input) { - var i, length, parser, rest, result, values; - i = -1; - length = parsers.length; - values = []; - rest = input; - while (++i < length) { - parser = parsers[i]; - result = parser(rest); - if (result == null) { - return; - } - values.push(result.value); - rest = result.rest; - } - return new P.Result(values, rest); - }; - }; - P.pick = function() { - var indexes, parsers; - indexes = arguments[0], parsers = 2 <= arguments.length ? slice.call(arguments, 1) : []; - return function(input) { - var array, result; - result = P.sequence.apply(P, parsers)(input); - if (result == null) { - return; - } - array = result.value; - result.value = array[indexes]; - return result; - }; - }; - P.string = function(string) { - var length; - length = string.length; - return function(input) { - if (input.slice(0, length) === string) { - return new P.Result(string, input.slice(length)); - } - }; - }; - P.lazy = function(fn) { - var cached; - cached = null; - return function(input) { - if (cached == null) { - cached = fn(); - } - return cached(input); - }; - }; - P.baseMany = function(parser, end, stringResult, atLeastOneResultRequired, input) { - var endResult, parserResult, rest, results; - rest = input; - results = stringResult ? '' : []; - while (true) { - if (end != null) { - endResult = end(rest); - if (endResult != null) { - break; - } - } - parserResult = parser(rest); - if (parserResult == null) { - break; - } - if (stringResult) { - results += parserResult.value; - } else { - results.push(parserResult.value); - } - rest = parserResult.rest; - } - if (atLeastOneResultRequired && results.length === 0) { - return; - } - return new P.Result(results, rest); - }; - P.many1 = function(parser) { - return function(input) { - return P.baseMany(parser, null, false, true, input); - }; - }; - P.concatMany1Till = function(parser, end) { - return function(input) { - return P.baseMany(parser, end, true, true, input); - }; - }; - P.firstChoice = function() { - var parsers; - parsers = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return function(input) { - var i, length, parser, result; - i = -1; - length = parsers.length; - while (++i < length) { - parser = parsers[i]; - result = parser(input); - if (result != null) { - return result; - } - } - }; - }; - newParser = function(options) { - var U; - U = {}; - U.wildcard = P.tag('wildcard', P.string(options.wildcardChar)); - U.optional = P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(function() { - return U.pattern; - }), P.string(options.optionalSegmentEndChar))); - U.name = P.regex(new RegExp("^[" + options.segmentNameCharset + "]+")); - U.named = P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(function() { - return U.name; - }))); - U.escapedChar = P.pick(1, P.string(options.escapeChar), P.regex(/^./)); - U["static"] = P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(function() { - return U.escapedChar; - }), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), U.wildcard))); - U.token = P.lazy(function() { - return P.firstChoice(U.wildcard, U.optional, U.named, U["static"]); - }); - U.pattern = P.many1(P.lazy(function() { - return U.token; - })); - return U; - }; - defaultOptions = { - escapeChar: '\\', - segmentNameStartChar: ':', - segmentValueCharset: 'a-zA-Z0-9-_~ %', - segmentNameCharset: 'a-zA-Z0-9', - optionalSegmentStartChar: '(', - optionalSegmentEndChar: ')', - wildcardChar: '*' - }; - baseAstNodeToRegexString = function(astNode, segmentValueCharset) { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, function(node) { - return baseAstNodeToRegexString(node, segmentValueCharset); - }); - } - switch (astNode.tag) { - case 'wildcard': - return '(.*?)'; - case 'named': - return "([" + segmentValueCharset + "]+)"; - case 'static': - return escapeForRegex(astNode.value); - case 'optional': - return '(?:' + baseAstNodeToRegexString(astNode.value, segmentValueCharset) + ')?'; - } - }; - astNodeToRegexString = function(astNode, segmentValueCharset) { - if (segmentValueCharset == null) { - segmentValueCharset = defaultOptions.segmentValueCharset; - } - return '^' + baseAstNodeToRegexString(astNode, segmentValueCharset) + '$'; - }; - astNodeToNames = function(astNode) { - if (Array.isArray(astNode)) { - return concatMap(astNode, astNodeToNames); - } - switch (astNode.tag) { - case 'wildcard': - return ['_']; - case 'named': - return [astNode.value]; - case 'static': - return []; - case 'optional': - return astNodeToNames(astNode.value); - } - }; - getParam = function(params, key, nextIndexes, sideEffects) { - var index, maxIndex, result, value; - if (sideEffects == null) { - sideEffects = false; - } - value = params[key]; - if (value == null) { - if (sideEffects) { - throw new Error("no values provided for key `" + key + "`"); - } else { - return; - } - } - index = nextIndexes[key] || 0; - maxIndex = Array.isArray(value) ? value.length - 1 : 0; - if (index > maxIndex) { - if (sideEffects) { - throw new Error("too few values provided for key `" + key + "`"); - } else { - return; - } - } - result = Array.isArray(value) ? value[index] : value; - if (sideEffects) { - nextIndexes[key] = index + 1; - } - return result; - }; - astNodeContainsSegmentsForProvidedParams = function(astNode, params, nextIndexes) { - var i, length; - if (Array.isArray(astNode)) { - i = -1; - length = astNode.length; - while (++i < length) { - if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { - return true; - } - } - return false; - } - switch (astNode.tag) { - case 'wildcard': - return getParam(params, '_', nextIndexes, false) != null; - case 'named': - return getParam(params, astNode.value, nextIndexes, false) != null; - case 'static': - return false; - case 'optional': - return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); - } - }; - stringify = function(astNode, params, nextIndexes) { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, function(node) { - return stringify(node, params, nextIndexes); - }); - } - switch (astNode.tag) { - case 'wildcard': - return getParam(params, '_', nextIndexes, true); - case 'named': - return getParam(params, astNode.value, nextIndexes, true); - case 'static': - return astNode.value; - case 'optional': - if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { - return stringify(astNode.value, params, nextIndexes); - } else { - return ''; - } - } - }; - UrlPattern = function(arg1, arg2) { - var groupCount, options, parsed, parser, withoutWhitespace; - if (arg1 instanceof UrlPattern) { - this.isRegex = arg1.isRegex; - this.regex = arg1.regex; - this.ast = arg1.ast; - this.names = arg1.names; - return; - } - this.isRegex = arg1 instanceof RegExp; - if (!(('string' === typeof arg1) || this.isRegex)) { - throw new TypeError('argument must be a regex or a string'); - } - if (this.isRegex) { - this.regex = arg1; - if (arg2 != null) { - if (!Array.isArray(arg2)) { - throw new Error('if first argument is a regex the second argument may be an array of group names but you provided something else'); - } - groupCount = regexGroupCount(this.regex); - if (arg2.length !== groupCount) { - throw new Error("regex contains " + groupCount + " groups but array of group names contains " + arg2.length); - } - this.names = arg2; - } - return; - } - if (arg1 === '') { - throw new Error('argument must not be the empty string'); - } - withoutWhitespace = arg1.replace(/\s+/g, ''); - if (withoutWhitespace !== arg1) { - throw new Error('argument must not contain whitespace'); - } - options = { - escapeChar: (arg2 != null ? arg2.escapeChar : void 0) || defaultOptions.escapeChar, - segmentNameStartChar: (arg2 != null ? arg2.segmentNameStartChar : void 0) || defaultOptions.segmentNameStartChar, - segmentNameCharset: (arg2 != null ? arg2.segmentNameCharset : void 0) || defaultOptions.segmentNameCharset, - segmentValueCharset: (arg2 != null ? arg2.segmentValueCharset : void 0) || defaultOptions.segmentValueCharset, - optionalSegmentStartChar: (arg2 != null ? arg2.optionalSegmentStartChar : void 0) || defaultOptions.optionalSegmentStartChar, - optionalSegmentEndChar: (arg2 != null ? arg2.optionalSegmentEndChar : void 0) || defaultOptions.optionalSegmentEndChar, - wildcardChar: (arg2 != null ? arg2.wildcardChar : void 0) || defaultOptions.wildcardChar - }; - parser = newParser(options); - parsed = parser.pattern(arg1); - if (parsed == null) { - throw new Error("couldn't parse pattern"); - } - if (parsed.rest !== '') { - throw new Error("could only partially parse pattern"); - } - this.ast = parsed.value; - this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); - this.names = astNodeToNames(this.ast); - }; - UrlPattern.prototype.match = function(url) { - var groups, match; - match = this.regex.exec(url); - if (match == null) { - return null; - } - groups = match.slice(1); - if (this.names) { - return keysAndValuesToObject(this.names, groups); - } else { - return groups; - } - }; - UrlPattern.prototype.stringify = function(params) { - if (params == null) { - params = {}; - } - if (this.isRegex) { - throw new Error("can't stringify patterns generated from a regex"); - } - if (params !== Object(params)) { - throw new Error("argument must be an object or undefined"); - } - return stringify(this.ast, params, {}); - }; - UrlPattern.escapeForRegex = escapeForRegex; - UrlPattern.concatMap = concatMap; - UrlPattern.stringConcatMap = stringConcatMap; - UrlPattern.regexGroupCount = regexGroupCount; - UrlPattern.keysAndValuesToObject = keysAndValuesToObject; - UrlPattern.P = P; - UrlPattern.newParser = newParser; - UrlPattern.defaultOptions = defaultOptions; - UrlPattern.astNodeToRegexString = astNodeToRegexString; - UrlPattern.astNodeToNames = astNodeToNames; - UrlPattern.getParam = getParam; - UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; - UrlPattern.stringify = stringify; - return UrlPattern; -}); From f1b33236d7cce81d6fbbf4fc5edeb5964fd9f44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 21:19:20 -0500 Subject: [PATCH 010/117] add default tslint.json --- tslint.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tslint.json diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..32fa6e5 --- /dev/null +++ b/tslint.json @@ -0,0 +1,9 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": {}, + "rulesDirectory": [] +} \ No newline at end of file From dab5f81bfb86168c6e4cecff2cff974ec1a71782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 21:45:28 -0500 Subject: [PATCH 011/117] index.ts: fix a lot of linter errors --- index.ts | 229 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 122 insertions(+), 107 deletions(-) diff --git a/index.ts b/index.ts index a525f7f..9e5772c 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ // OPTIONS -interface UrlPatternOptions { +interface IUrlPatternOptions { escapeChar?: string; segmentNameStartChar?: string; segmentValueCharset?: string; @@ -10,61 +10,63 @@ interface UrlPatternOptions { wildcardChar?: string; } -const defaultOptions: UrlPatternOptions = { - escapeChar: '\\', - segmentNameStartChar: ':', - segmentValueCharset: 'a-zA-Z0-9-_~ %', - segmentNameCharset: 'a-zA-Z0-9', - optionalSegmentStartChar: '(', - optionalSegmentEndChar: ')', - wildcardChar: '*' +const defaultOptions: IUrlPatternOptions = { + escapeChar: "\\", + optionalSegmentEndChar: ")", + optionalSegmentStartChar: "(", + segmentNameCharset: "a-zA-Z0-9", + segmentNameStartChar: ":", + segmentValueCharset: "a-zA-Z0-9-_~ %", + wildcardChar: "*", }; // HELPERS // escapes a string for insertion into a regular expression // source: http://stackoverflow.com/a/3561711 -function escapeStringForRegex(string: string) : string { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); +function escapeStringForRegex(str: string): string { + return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); } -function concatMap(array: Array, f: (T) => Array) : Array { - let results: Array = []; - array.forEach(function(value) { +function concatMap(array: T[], f: (x: T) => T[]): T[] { + let results: T[] = []; + array.forEach((value) => { results = results.concat(f(value)); }); return results; } -function stringConcatMap(array: Array, f: (T) => string) : string { - let result = ''; - array.forEach(function(value) { +function stringConcatMap(array: T[], f: (x: T) => string): string { + let result = ""; + array.forEach((value) => { result += f(value); }); return result; -}; +} -// returns the number of groups in the `regex`. -// source: http://stackoverflow.com/a/16047223 -function regexGroupCount(regex: RegExp) : number { +/* + * returns the number of groups in the `regex`. + * source: http://stackoverflow.com/a/16047223 + */ +function regexGroupCount(regex: RegExp): number { return new RegExp(regex.toString() + "|").exec("").length - 1; } // zips an array of `keys` and an array of `values` into an object. // `keys` and `values` must have the same length. // if the same key appears multiple times the associated values are collected in an array. -function keysAndValuesToObject(keys: Array, values: Array) : Object { - let result = {}; +function keysAndValuesToObject(keys: any[], values: any[]): object { + const result = {}; if (keys.length !== values.length) { throw Error("keys.length must equal values.length"); } let i = -1; - let { length } = keys; + const { length } = keys; while (++i < keys.length) { - let key = keys[i]; - let value = values[i]; + const key = keys[i]; + const value = values[i]; if (value == null) { continue; @@ -82,16 +84,16 @@ function keysAndValuesToObject(keys: Array, values: Array) : Object { } } return result; -}; +} // PARSER COMBINATORS // parse result class Result { /* parsed value */ - value: Value; + public readonly value: Value; /* unparsed rest */ - readonly rest: string; + public readonly rest: string; constructor(value: Value, rest: string) { this.value = value; this.rest = rest; @@ -99,8 +101,8 @@ class Result { } class Tagged { - readonly tag: string; - readonly value: Value; + public readonly tag: string; + public readonly value: Value; constructor(tag: string, value: Value) { this.tag = tag; this.value = value; @@ -108,44 +110,45 @@ class Tagged { } /** - * a parser is a function that takes a string and returns a `Result` containing a parsed `Result.value` and the rest of the string `Result.rest` + * a parser is a function that takes a string and returns a `Result` + * containing a parsed `Result.value` and the rest of the string `Result.rest` */ -type Parser = (string: string) => Result | null; +type Parser = (str: string) => Result | null; // parser combinators let P = { - Result: Result, - Tagged: Tagged, + Result, + Tagged, // transforms a `parser` into a parser that tags its `Result.value` with `tag` - tag(tag: string, parser: Parser) : Parser> { - return function(input: string): Result> | null { - let result = parser(input); + tag(tag: string, parser: Parser): Parser> { + return (input: string) => { + const result = parser(input); if (result == null) { return null; } - let tagged = new Tagged(tag, result.value); + const tagged = new Tagged(tag, result.value); return new Result(tagged, result.rest); - } + }; }, // parser that consumes everything matched by `regex` - regex(regex: RegExp) : Parser { - return function(input: string): Result | null { - let matches = regex.exec(input); + regex(regex: RegExp): Parser { + return (input: string) => { + const matches = regex.exec(input); if (matches == null) { return null; } - let result = matches[0]; + const result = matches[0]; return new Result(result, input.slice(result.length)); - } + }; }, // takes a sequence of parsers and returns a parser that runs // them in sequence and produces an array of their results - sequence(...parsers: Array>) : Parser> { - return function(input: string): Result> | null { + sequence(...parsers: Array>): Parser { + return (input: string) => { let rest = input; - let values = []; - parsers.forEach(function(parser) { - let result = parser(rest); + const values: any[] = []; + parsers.forEach((parser: Parser) => { + const result = parser(rest); if (result == null) { return null; } @@ -153,12 +156,12 @@ let P = { rest = result.rest; }); return new Result(values, rest); - } + }; }, // returns a parser that consumes `str` exactly - string(str: string) : Parser { - let { length } = str; - return function(input: string) : Result | null { + string(str: string): Parser { + const { length } = str; + return (input: string) => { if (input.slice(0, length) === str) { return new Result(str, input.slice(length)); } @@ -166,49 +169,49 @@ let P = { }, // takes a sequence of parser and only returns the result // returned by the `index`th parser - pick(index, ...parsers: Array>) : Parser { - let parser = P.sequence(...parsers); - return function(input: string) : Result | null { - let result = parser(input); + pick(index: number, ...parsers: Array>): Parser { + const parser = P.sequence(...parsers); + return (input: string) => { + const result = parser(input); if (result == null) { return null; } - let values = result.value; - result.value = values[index]; - return result; - } + return new Result(result.value[index], result.rest); + }; }, // for parsers that each depend on one another (cyclic dependencies) // postpone lookup to when they both exist. - lazy(get_parser: () => Parser): Parser { - let cached_parser = null; - return function (input: string): Result | null { - if (cached_parser == null) { - cached_parser = get_parser(); + lazy(getParser: () => Parser): Parser { + let cachedParser: Parser | null = null; + return (input: string) => { + if (cachedParser == null) { + cachedParser = getParser(); } - return cached_parser(input); + return cachedParser(input); }; }, /* * base function for parsers that parse multiples. - * @param endParser once the `endParser` (if not null) consumes the `baseMany` parser returns. the result of the `endParser` is ignored. + * + * @param endParser once the `endParser` (if not null) consumes + * the `baseMany` parser returns. the result of the `endParser` is ignored. */ baseMany( parser: Parser, endParser: Parser | null, isAtLeastOneResultRequired: boolean, - input: string - ) : Result> | null { + input: string, + ): Result | null { let rest = input; - let results: Array = []; + const results: T[] = []; while (true) { if (endParser != null) { - let endResult = endParser(rest); + const endResult = endParser(rest); if (endResult != null) { break; } } - let parserResult = parser(rest); + const parserResult = parser(rest); if (parserResult == null) { break; } @@ -222,67 +225,79 @@ let P = { return new Result(results, rest); }, - many1(parser: Parser) : Parser> { - return function(input: string) : Result> { - const endParser = null; + many1(parser: Parser): Parser { + return (input: string) => { + const endParser: null = null; const isAtLeastOneResultRequired = true; return P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); - } + }; }, - concatMany1Till(parser: Parser, endParser: Parser) : Parser { - return function(input: string) : Result | null { + concatMany1Till(parser: Parser, endParser: Parser): Parser { + return (input: string) => { const isAtLeastOneResultRequired = true; - let result = P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); + const result = P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); if (result == null) { return null; } return new Result(result.value.join(""), result.rest); - } + }; }, // takes a sequence of parsers. returns the result from the first // parser that consumes the input. - firstChoice(...parsers: Array>) : Parser { - return function(input: string) : Result | null { - parsers.forEach(function(parser) { - let result = parser(input); + firstChoice(...parsers: Array>): Parser { + return (input: string) => { + parsers.forEach((parser) => { + const result = parser(input); if (result != null) { return result; } }); return null; - } - } -} + }; + }, +}; // URL PATTERN PARSER -interface UrlPatternParser { - wildcard: Parser>, - optional: Parser>, - name: Parser, - named: Parser>, - escapedChar: Parser, - pattern: Parser, +interface IUrlPatternParser { + escapedChar: Parser; + name: Parser; + named: Parser>; + optional: Parser>; + pattern: Parser; + static: Parser>; + token: Parser; + wildcard: Parser>; } -function newUrlPatternParser(options: UrlPatternOptions) : UrlPatternParser { - let U = { - wildcard: P.tag('wildcard', P.string(options.wildcardChar)), - optional: P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(() => U.pattern), P.string(options.optionalSegmentEndChar))), - name: P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)), - named: P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))), +function newUrlPatternParser(options: IUrlPatternOptions): IUrlPatternParser { + const U: IUrlPatternParser = { escapedChar: P.pick(1, P.string(options.escapeChar), P.regex(/^./)), - static: P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(() => U.escapedChar), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), P.lazy(() => U.wildcard)))), - token: P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)), + name: P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)), + named: P.tag("named", P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))), + optional: P.tag("optional", P.pick(1, + P.string(options.optionalSegmentStartChar), + P.lazy(() => U.pattern), + P.string(options.optionalSegmentEndChar))), pattern: P.many1(P.lazy(() => U.token)), - } + static: P.tag("static", P.concatMany1Till(P.firstChoice( + P.lazy(() => U.escapedChar), + P.regex(/^./)), + P.firstChoice( + P.string(options.segmentNameStartChar), + P.string(options.optionalSegmentStartChar), + P.string(options.optionalSegmentEndChar), + P.lazy(() => U.wildcard)))), + token: P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)), + wildcard: P.tag("wildcard", P.string(options.wildcardChar)), + }; return U; -}; +} // functions that further process ASTs returned as `.value` in parser results -function baseAstNodeToRegexString(astNode, segmentValueCharset) { +function baseAstNodeToRegexString(astNode: Tagged, segmentValueCharset: string): string { if (Array.isArray(astNode)) { return stringConcatMap(astNode, node => baseAstNodeToRegexString(node, segmentValueCharset)); } From 10f9165aa8ae533849088bba21a5327c6104d52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 22:49:49 -0500 Subject: [PATCH 012/117] convert tests from coffee to javascript --- test/ast.coffee | 199 ------------- test/ast.js | 230 +++++++++++++++ test/errors.coffee | 107 ------- test/errors.js | 156 +++++++++++ test/helpers.coffee | 138 --------- test/helpers.js | 148 ++++++++++ test/match-fixtures.coffee | 249 ----------------- test/match-fixtures.js | 296 ++++++++++++++++++++ test/misc.coffee | 30 -- test/misc.js | 42 +++ test/parser.coffee | 426 ---------------------------- test/parser.js | 495 +++++++++++++++++++++++++++++++++ test/readme.coffee | 121 -------- test/readme.js | 159 +++++++++++ test/stringify-fixtures.coffee | 162 ----------- test/stringify-fixtures.js | 218 +++++++++++++++ 16 files changed, 1744 insertions(+), 1432 deletions(-) delete mode 100644 test/ast.coffee create mode 100644 test/ast.js delete mode 100644 test/errors.coffee create mode 100644 test/errors.js delete mode 100644 test/helpers.coffee create mode 100644 test/helpers.js delete mode 100644 test/match-fixtures.coffee create mode 100644 test/match-fixtures.js delete mode 100644 test/misc.coffee create mode 100644 test/misc.js delete mode 100644 test/parser.coffee create mode 100644 test/parser.js delete mode 100644 test/readme.coffee create mode 100644 test/readme.js delete mode 100644 test/stringify-fixtures.coffee create mode 100644 test/stringify-fixtures.js diff --git a/test/ast.coffee b/test/ast.coffee deleted file mode 100644 index 7aa6f30..0000000 --- a/test/ast.coffee +++ /dev/null @@ -1,199 +0,0 @@ -test = require 'tape' -UrlPattern = require '../lib/url-pattern' - -{ - astNodeToRegexString - astNodeToNames - getParam -} = UrlPattern - -parse = UrlPattern.newParser(UrlPattern.defaultOptions).pattern - -test 'astNodeToRegexString and astNodeToNames', (t) -> - t.test 'just static alphanumeric', (t) -> - parsed = parse 'user42' - t.equal astNodeToRegexString(parsed.value), '^user42$' - t.deepEqual astNodeToNames(parsed.value), [] - t.end() - - t.test 'just static escaped', (t) -> - parsed = parse '/api/v1/users' - t.equal astNodeToRegexString(parsed.value), '^\\/api\\/v1\\/users$' - t.deepEqual astNodeToNames(parsed.value), [] - t.end() - - t.test 'just single char variable', (t) -> - parsed = parse ':a' - t.equal astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$' - t.deepEqual astNodeToNames(parsed.value), ['a'] - t.end() - - t.test 'just variable', (t) -> - parsed = parse ':variable' - t.equal astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$' - t.deepEqual astNodeToNames(parsed.value), ['variable'] - t.end() - - t.test 'just wildcard', (t) -> - parsed = parse '*' - t.equal astNodeToRegexString(parsed.value), '^(.*?)$' - t.deepEqual astNodeToNames(parsed.value), ['_'] - t.end() - - t.test 'just optional static', (t) -> - parsed = parse '(foo)' - t.equal astNodeToRegexString(parsed.value), '^(?:foo)?$' - t.deepEqual astNodeToNames(parsed.value), [] - t.end() - - t.test 'just optional variable', (t) -> - parsed = parse '(:foo)' - t.equal astNodeToRegexString(parsed.value), '^(?:([a-zA-Z0-9-_~ %]+))?$' - t.deepEqual astNodeToNames(parsed.value), ['foo'] - t.end() - - t.test 'just optional wildcard', (t) -> - parsed = parse '(*)' - t.equal astNodeToRegexString(parsed.value), '^(?:(.*?))?$' - t.deepEqual astNodeToNames(parsed.value), ['_'] - t.end() - -test 'getParam', (t) -> - t.test 'no side effects', (t) -> - next = {} - t.equal undefined, getParam {}, 'one', next - t.deepEqual next, {} - - # value - - next = {} - t.equal 1, getParam {one: 1}, 'one', next - t.deepEqual next, {} - - next = {one: 0} - t.equal 1, getParam {one: 1}, 'one', next - t.deepEqual next, {one: 0} - - next = {one: 1} - t.equal undefined, getParam {one: 1}, 'one', next - t.deepEqual next, {one: 1} - - next = {one: 2} - t.equal undefined, getParam {one: 1}, 'one', next - t.deepEqual next, {one: 2} - - # array - - next = {} - t.equal 1, getParam {one: [1]}, 'one', next - t.deepEqual next, {} - - next = {one: 0} - t.equal 1, getParam {one: [1]}, 'one', next - t.deepEqual next, {one: 0} - - next = {one: 1} - t.equal undefined, getParam {one: [1]}, 'one', next - t.deepEqual next, {one: 1} - - next = {one: 2} - t.equal undefined, getParam {one: [1]}, 'one', next - t.deepEqual next, {one: 2} - - next = {one: 0} - t.equal 1, getParam {one: [1, 2, 3]}, 'one', next - t.deepEqual next, {one: 0} - - next = {one: 1} - t.equal 2, getParam {one: [1, 2, 3]}, 'one', next - t.deepEqual next, {one: 1} - - next = {one: 2} - t.equal 3, getParam {one: [1, 2, 3]}, 'one', next - t.deepEqual next, {one: 2} - - next = {one: 3} - t.equal undefined, getParam {one: [1, 2, 3]}, 'one', next - t.deepEqual next, {one: 3} - - t.end() - - t.test 'side effects', (t) -> - next = {} - t.equal 1, getParam {one: 1}, 'one', next, true - t.deepEqual next, {one: 1} - - next = {one: 0} - t.equal 1, getParam {one: 1}, 'one', next, true - t.deepEqual next, {one: 1} - - # array - - next = {} - t.equal 1, getParam {one: [1]}, 'one', next, true - t.deepEqual next, {one: 1} - - next = {one: 0} - t.equal 1, getParam {one: [1]}, 'one', next, true - t.deepEqual next, {one: 1} - - next = {one: 0} - t.equal 1, getParam {one: [1, 2, 3]}, 'one', next, true - t.deepEqual next, {one: 1} - - next = {one: 1} - t.equal 2, getParam {one: [1, 2, 3]}, 'one', next, true - t.deepEqual next, {one: 2} - - next = {one: 2} - t.equal 3, getParam {one: [1, 2, 3]}, 'one', next, true - t.deepEqual next, {one: 3} - - t.end() - - t.test 'side effects errors', (t) -> - t.plan 2 * 6 - - next = {} - try - getParam {}, 'one', next, true - catch e - t.equal e.message, "no values provided for key `one`" - t.deepEqual next, {} - - next = {one: 1} - try - getParam {one: 1}, 'one', next, true - catch e - t.equal e.message, "too few values provided for key `one`" - t.deepEqual next, {one: 1} - - next = {one: 2} - try - getParam {one: 2}, 'one', next, true - catch e - t.equal e.message, "too few values provided for key `one`" - t.deepEqual next, {one: 2} - - next = {one: 1} - try - getParam {one: [1]}, 'one', next, true - catch e - t.equal e.message, "too few values provided for key `one`" - t.deepEqual next, {one: 1} - - next = {one: 2} - try - getParam {one: [1]}, 'one', next, true - catch e - t.equal e.message, "too few values provided for key `one`" - t.deepEqual next, {one: 2} - - next = {one: 3} - try - getParam {one: [1, 2, 3]}, 'one', next, true - catch e - t.equal e.message, "too few values provided for key `one`" - t.deepEqual next, {one: 3} - - t.end() diff --git a/test/ast.js b/test/ast.js new file mode 100644 index 0000000..28f1e12 --- /dev/null +++ b/test/ast.js @@ -0,0 +1,230 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const UrlPattern = require('../lib/url-pattern'); + +const { + astNodeToRegexString, + astNodeToNames, + getParam +} = UrlPattern; + +const parse = UrlPattern.newParser(UrlPattern.defaultOptions).pattern; + +test('astNodeToRegexString and astNodeToNames', function(t) { + t.test('just static alphanumeric', function(t) { + const parsed = parse('user42'); + t.equal(astNodeToRegexString(parsed.value), '^user42$'); + t.deepEqual(astNodeToNames(parsed.value), []); + return t.end(); + }); + + t.test('just static escaped', function(t) { + const parsed = parse('/api/v1/users'); + t.equal(astNodeToRegexString(parsed.value), '^\\/api\\/v1\\/users$'); + t.deepEqual(astNodeToNames(parsed.value), []); + return t.end(); + }); + + t.test('just single char variable', function(t) { + const parsed = parse(':a'); + t.equal(astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$'); + t.deepEqual(astNodeToNames(parsed.value), ['a']); + return t.end(); + }); + + t.test('just variable', function(t) { + const parsed = parse(':variable'); + t.equal(astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$'); + t.deepEqual(astNodeToNames(parsed.value), ['variable']); + return t.end(); + }); + + t.test('just wildcard', function(t) { + const parsed = parse('*'); + t.equal(astNodeToRegexString(parsed.value), '^(.*?)$'); + t.deepEqual(astNodeToNames(parsed.value), ['_']); + return t.end(); + }); + + t.test('just optional static', function(t) { + const parsed = parse('(foo)'); + t.equal(astNodeToRegexString(parsed.value), '^(?:foo)?$'); + t.deepEqual(astNodeToNames(parsed.value), []); + return t.end(); + }); + + t.test('just optional variable', function(t) { + const parsed = parse('(:foo)'); + t.equal(astNodeToRegexString(parsed.value), '^(?:([a-zA-Z0-9-_~ %]+))?$'); + t.deepEqual(astNodeToNames(parsed.value), ['foo']); + return t.end(); + }); + + return t.test('just optional wildcard', function(t) { + const parsed = parse('(*)'); + t.equal(astNodeToRegexString(parsed.value), '^(?:(.*?))?$'); + t.deepEqual(astNodeToNames(parsed.value), ['_']); + return t.end(); + }); +}); + +test('getParam', function(t) { + t.test('no side effects', function(t) { + let next = {}; + t.equal(undefined, getParam({}, 'one', next)); + t.deepEqual(next, {}); + + // value + + next = {}; + t.equal(1, getParam({one: 1}, 'one', next)); + t.deepEqual(next, {}); + + next = {one: 0}; + t.equal(1, getParam({one: 1}, 'one', next)); + t.deepEqual(next, {one: 0}); + + next = {one: 1}; + t.equal(undefined, getParam({one: 1}, 'one', next)); + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + t.equal(undefined, getParam({one: 1}, 'one', next)); + t.deepEqual(next, {one: 2}); + + // array + + next = {}; + t.equal(1, getParam({one: [1]}, 'one', next)); + t.deepEqual(next, {}); + + next = {one: 0}; + t.equal(1, getParam({one: [1]}, 'one', next)); + t.deepEqual(next, {one: 0}); + + next = {one: 1}; + t.equal(undefined, getParam({one: [1]}, 'one', next)); + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + t.equal(undefined, getParam({one: [1]}, 'one', next)); + t.deepEqual(next, {one: 2}); + + next = {one: 0}; + t.equal(1, getParam({one: [1, 2, 3]}, 'one', next)); + t.deepEqual(next, {one: 0}); + + next = {one: 1}; + t.equal(2, getParam({one: [1, 2, 3]}, 'one', next)); + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + t.equal(3, getParam({one: [1, 2, 3]}, 'one', next)); + t.deepEqual(next, {one: 2}); + + next = {one: 3}; + t.equal(undefined, getParam({one: [1, 2, 3]}, 'one', next)); + t.deepEqual(next, {one: 3}); + + return t.end(); + }); + + t.test('side effects', function(t) { + let next = {}; + t.equal(1, getParam({one: 1}, 'one', next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 0}; + t.equal(1, getParam({one: 1}, 'one', next, true)); + t.deepEqual(next, {one: 1}); + + // array + + next = {}; + t.equal(1, getParam({one: [1]}, 'one', next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 0}; + t.equal(1, getParam({one: [1]}, 'one', next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 0}; + t.equal(1, getParam({one: [1, 2, 3]}, 'one', next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 1}; + t.equal(2, getParam({one: [1, 2, 3]}, 'one', next, true)); + t.deepEqual(next, {one: 2}); + + next = {one: 2}; + t.equal(3, getParam({one: [1, 2, 3]}, 'one', next, true)); + t.deepEqual(next, {one: 3}); + + return t.end(); + }); + + return t.test('side effects errors', function(t) { + let e; + t.plan(2 * 6); + + let next = {}; + try { + getParam({}, 'one', next, true); + } catch (error) { + e = error; + t.equal(e.message, "no values provided for key `one`"); + } + t.deepEqual(next, {}); + + next = {one: 1}; + try { + getParam({one: 1}, 'one', next, true); + } catch (error1) { + e = error1; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + try { + getParam({one: 2}, 'one', next, true); + } catch (error2) { + e = error2; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 2}); + + next = {one: 1}; + try { + getParam({one: [1]}, 'one', next, true); + } catch (error3) { + e = error3; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + try { + getParam({one: [1]}, 'one', next, true); + } catch (error4) { + e = error4; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 2}); + + next = {one: 3}; + try { + getParam({one: [1, 2, 3]}, 'one', next, true); + } catch (error5) { + e = error5; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 3}); + + return t.end(); + }); +}); diff --git a/test/errors.coffee b/test/errors.coffee deleted file mode 100644 index 12186de..0000000 --- a/test/errors.coffee +++ /dev/null @@ -1,107 +0,0 @@ -test = require 'tape' -UrlPattern = require '../lib/url-pattern' - -test 'invalid argument', (t) -> - UrlPattern - t.plan 5 - try - new UrlPattern() - catch e - t.equal e.message, "argument must be a regex or a string" - try - new UrlPattern(5) - catch e - t.equal e.message, "argument must be a regex or a string" - try - new UrlPattern '' - catch e - t.equal e.message, "argument must not be the empty string" - try - new UrlPattern ' ' - catch e - t.equal e.message, "argument must not contain whitespace" - try - new UrlPattern ' fo o' - catch e - t.equal e.message, "argument must not contain whitespace" - t.end() - -test 'invalid variable name in pattern', (t) -> - UrlPattern - t.plan 3 - try - new UrlPattern ':' - catch e - t.equal e.message, "couldn't parse pattern" - try - new UrlPattern ':.' - catch e - t.equal e.message, "couldn't parse pattern" - try - new UrlPattern 'foo:.' - catch e - # TODO `:` must be followed by the name of the named segment consisting of at least one character in character set `a-zA-Z0-9` at 4 - t.equal e.message, "could only partially parse pattern" - t.end() - -test 'too many closing parentheses', (t) -> - t.plan 2 - try - new UrlPattern ')' - catch e - # TODO did not plan ) at 0 - t.equal e.message, "couldn't parse pattern" - try - new UrlPattern '((foo)))bar' - catch e - # TODO did not plan ) at 7 - t.equal e.message, "could only partially parse pattern" - t.end() - -test 'unclosed parentheses', (t) -> - t.plan 2 - try - new UrlPattern '(' - catch e - # TODO unclosed parentheses at 1 - t.equal e.message, "couldn't parse pattern" - try - new UrlPattern '(((foo)bar(boo)far)' - catch e - # TODO unclosed parentheses at 19 - t.equal e.message, "couldn't parse pattern" - t.end() - -test 'regex names', (t) -> - t.plan 3 - try - new UrlPattern /x/, 5 - catch e - t.equal e.message, 'if first argument is a regex the second argument may be an array of group names but you provided something else' - try - new UrlPattern /(((foo)bar(boo))far)/, [] - catch e - t.equal e.message, "regex contains 4 groups but array of group names contains 0" - try - new UrlPattern /(((foo)bar(boo))far)/, ['a', 'b'] - catch e - t.equal e.message, "regex contains 4 groups but array of group names contains 2" - t.end() - -test 'stringify regex', (t) -> - t.plan 1 - pattern = new UrlPattern /x/ - try - pattern.stringify() - catch e - t.equal e.message, "can't stringify patterns generated from a regex" - t.end() - -test 'stringify argument', (t) -> - t.plan 1 - pattern = new UrlPattern 'foo' - try - pattern.stringify(5) - catch e - t.equal e.message, "argument must be an object or undefined" - t.end() diff --git a/test/errors.js b/test/errors.js new file mode 100644 index 0000000..98817ce --- /dev/null +++ b/test/errors.js @@ -0,0 +1,156 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const UrlPattern = require('../lib/url-pattern'); + +test('invalid argument', function(t) { + let e; + UrlPattern; + t.plan(5); + try { + new UrlPattern(); + } catch (error) { + e = error; + t.equal(e.message, "argument must be a regex or a string"); + } + try { + new UrlPattern(5); + } catch (error1) { + e = error1; + t.equal(e.message, "argument must be a regex or a string"); + } + try { + new UrlPattern(''); + } catch (error2) { + e = error2; + t.equal(e.message, "argument must not be the empty string"); + } + try { + new UrlPattern(' '); + } catch (error3) { + e = error3; + t.equal(e.message, "argument must not contain whitespace"); + } + try { + new UrlPattern(' fo o'); + } catch (error4) { + e = error4; + t.equal(e.message, "argument must not contain whitespace"); + } + return t.end(); +}); + +test('invalid variable name in pattern', function(t) { + let e; + UrlPattern; + t.plan(3); + try { + new UrlPattern(':'); + } catch (error) { + e = error; + t.equal(e.message, "couldn't parse pattern"); + } + try { + new UrlPattern(':.'); + } catch (error1) { + e = error1; + t.equal(e.message, "couldn't parse pattern"); + } + try { + new UrlPattern('foo:.'); + } catch (error2) { + // TODO `:` must be followed by the name of the named segment consisting of at least one character in character set `a-zA-Z0-9` at 4 + e = error2; + t.equal(e.message, "could only partially parse pattern"); + } + return t.end(); +}); + +test('too many closing parentheses', function(t) { + let e; + t.plan(2); + try { + new UrlPattern(')'); + } catch (error) { + // TODO did not plan ) at 0 + e = error; + t.equal(e.message, "couldn't parse pattern"); + } + try { + new UrlPattern('((foo)))bar'); + } catch (error1) { + // TODO did not plan ) at 7 + e = error1; + t.equal(e.message, "could only partially parse pattern"); + } + return t.end(); +}); + +test('unclosed parentheses', function(t) { + let e; + t.plan(2); + try { + new UrlPattern('('); + } catch (error) { + // TODO unclosed parentheses at 1 + e = error; + t.equal(e.message, "couldn't parse pattern"); + } + try { + new UrlPattern('(((foo)bar(boo)far)'); + } catch (error1) { + // TODO unclosed parentheses at 19 + e = error1; + t.equal(e.message, "couldn't parse pattern"); + } + return t.end(); +}); + +test('regex names', function(t) { + let e; + t.plan(3); + try { + new UrlPattern(/x/, 5); + } catch (error) { + e = error; + t.equal(e.message, 'if first argument is a regex the second argument may be an array of group names but you provided something else'); + } + try { + new UrlPattern(/(((foo)bar(boo))far)/, []); + } catch (error1) { + e = error1; + t.equal(e.message, "regex contains 4 groups but array of group names contains 0"); + } + try { + new UrlPattern(/(((foo)bar(boo))far)/, ['a', 'b']); + } catch (error2) { + e = error2; + t.equal(e.message, "regex contains 4 groups but array of group names contains 2"); + } + return t.end(); +}); + +test('stringify regex', function(t) { + t.plan(1); + const pattern = new UrlPattern(/x/); + try { + pattern.stringify(); + } catch (e) { + t.equal(e.message, "can't stringify patterns generated from a regex"); + } + return t.end(); +}); + +test('stringify argument', function(t) { + t.plan(1); + const pattern = new UrlPattern('foo'); + try { + pattern.stringify(5); + } catch (e) { + t.equal(e.message, "argument must be an object or undefined"); + } + return t.end(); +}); diff --git a/test/helpers.coffee b/test/helpers.coffee deleted file mode 100644 index c10e727..0000000 --- a/test/helpers.coffee +++ /dev/null @@ -1,138 +0,0 @@ -test = require 'tape' -{ - escapeForRegex - concatMap - stringConcatMap - regexGroupCount - keysAndValuesToObject -} = require '../lib/url-pattern' - -test 'escapeForRegex', (t) -> - expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]' - actual = escapeForRegex('[-\/\\^$*+?.()|[\]{}]') - t.equal expected, actual - - t.equal escapeForRegex('a$98kdjf(kdj)'), 'a\\$98kdjf\\(kdj\\)' - t.equal 'a', escapeForRegex 'a' - t.equal '!', escapeForRegex '!' - t.equal '\\.', escapeForRegex '.' - t.equal '\\/', escapeForRegex '/' - t.equal '\\-', escapeForRegex '-' - t.equal '\\-', escapeForRegex '-' - t.equal '\\[', escapeForRegex '[' - t.equal '\\]', escapeForRegex ']' - t.equal '\\(', escapeForRegex '(' - t.equal '\\)', escapeForRegex ')' - t.end() - -test 'concatMap', (t) -> - t.deepEqual [], concatMap [], -> - t.deepEqual [1], concatMap [1], (x) -> [x] - t.deepEqual [1, 1, 1, 2, 2, 2, 3, 3, 3], concatMap [1, 2, 3], (x) -> [x, x, x] - t.end() - -test 'stringConcatMap', (t) -> - t.equal '', stringConcatMap [], -> - t.equal '1', stringConcatMap [1], (x) -> x - t.equal '123', stringConcatMap [1, 2, 3], (x) -> x - t.equal '1a2a3a', stringConcatMap [1, 2, 3], (x) -> x + 'a' - t.end() - -test 'regexGroupCount', (t) -> - t.equal 0, regexGroupCount /foo/ - t.equal 1, regexGroupCount /(foo)/ - t.equal 2, regexGroupCount /((foo))/ - t.equal 2, regexGroupCount /(fo(o))/ - t.equal 2, regexGroupCount /f(o)(o)/ - t.equal 2, regexGroupCount /f(o)o()/ - t.equal 5, regexGroupCount /f(o)o()()(())/ - t.end() - -test 'keysAndValuesToObject', (t) -> - t.deepEqual( - keysAndValuesToObject( - [] - [] - ) - {} - ) - t.deepEqual( - keysAndValuesToObject( - ['one'] - [1] - ) - { - one: 1 - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two'] - [1] - ) - { - one: 1 - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two'] - [1, 2, 3] - ) - { - one: 1 - two: [2, 3] - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two'] - [1, 2, 3, null] - ) - { - one: 1 - two: [2, 3] - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two'] - [1, 2, 3, 4] - ) - { - one: 1 - two: [2, 3, 4] - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two', 'three'] - [1, 2, 3, 4, undefined] - ) - { - one: 1 - two: [2, 3, 4] - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two', 'three'] - [1, 2, 3, 4, 5] - ) - { - one: 1 - two: [2, 3, 4] - three: 5 - } - ) - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two', 'three'] - [null, 2, 3, 4, 5] - ) - { - two: [2, 3, 4] - three: 5 - } - ) - t.end() diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..16587cd --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,148 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const { + escapeForRegex, + concatMap, + stringConcatMap, + regexGroupCount, + keysAndValuesToObject +} = require('../lib/url-pattern'); + +test('escapeForRegex', function(t) { + const expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]'; + const actual = escapeForRegex('[-\/\\^$*+?.()|[\]{}]'); + t.equal(expected, actual); + + t.equal(escapeForRegex('a$98kdjf(kdj)'), 'a\\$98kdjf\\(kdj\\)'); + t.equal('a', escapeForRegex('a')); + t.equal('!', escapeForRegex('!')); + t.equal('\\.', escapeForRegex('.')); + t.equal('\\/', escapeForRegex('/')); + t.equal('\\-', escapeForRegex('-')); + t.equal('\\-', escapeForRegex('-')); + t.equal('\\[', escapeForRegex('[')); + t.equal('\\]', escapeForRegex(']')); + t.equal('\\(', escapeForRegex('(')); + t.equal('\\)', escapeForRegex(')')); + return t.end(); +}); + +test('concatMap', function(t) { + t.deepEqual([], concatMap([], function() {})); + t.deepEqual([1], concatMap([1], x => [x])); + t.deepEqual([1, 1, 1, 2, 2, 2, 3, 3, 3], concatMap([1, 2, 3], x => [x, x, x])); + return t.end(); +}); + +test('stringConcatMap', function(t) { + t.equal('', stringConcatMap([], function() {})); + t.equal('1', stringConcatMap([1], x => x)); + t.equal('123', stringConcatMap([1, 2, 3], x => x)); + t.equal('1a2a3a', stringConcatMap([1, 2, 3], x => x + 'a')); + return t.end(); +}); + +test('regexGroupCount', function(t) { + t.equal(0, regexGroupCount(/foo/)); + t.equal(1, regexGroupCount(/(foo)/)); + t.equal(2, regexGroupCount(/((foo))/)); + t.equal(2, regexGroupCount(/(fo(o))/)); + t.equal(2, regexGroupCount(/f(o)(o)/)); + t.equal(2, regexGroupCount(/f(o)o()/)); + t.equal(5, regexGroupCount(/f(o)o()()(())/)); + return t.end(); +}); + +test('keysAndValuesToObject', function(t) { + t.deepEqual( + keysAndValuesToObject( + [], + [] + ), + {} + ); + t.deepEqual( + keysAndValuesToObject( + ['one'], + [1] + ), + { + one: 1 + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two'], + [1] + ), + { + one: 1 + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two', 'two'], + [1, 2, 3] + ), + { + one: 1, + two: [2, 3] + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two', 'two', 'two'], + [1, 2, 3, null] + ), + { + one: 1, + two: [2, 3] + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two', 'two', 'two'], + [1, 2, 3, 4] + ), + { + one: 1, + two: [2, 3, 4] + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two', 'two', 'two', 'three'], + [1, 2, 3, 4, undefined] + ), + { + one: 1, + two: [2, 3, 4] + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two', 'two', 'two', 'three'], + [1, 2, 3, 4, 5] + ), + { + one: 1, + two: [2, 3, 4], + three: 5 + } + ); + t.deepEqual( + keysAndValuesToObject( + ['one', 'two', 'two', 'two', 'three'], + [null, 2, 3, 4, 5] + ), + { + two: [2, 3, 4], + three: 5 + } + ); + return t.end(); +}); diff --git a/test/match-fixtures.coffee b/test/match-fixtures.coffee deleted file mode 100644 index ca4d5ec..0000000 --- a/test/match-fixtures.coffee +++ /dev/null @@ -1,249 +0,0 @@ -test = require 'tape' -UrlPattern = require '../lib/url-pattern' - -test 'match', (t) -> - pattern = new UrlPattern '/foo' - t.deepEqual pattern.match('/foo'), {} - - pattern = new UrlPattern '.foo' - t.deepEqual pattern.match('.foo'), {} - - pattern = new UrlPattern '/foo' - t.equals pattern.match('/foobar'), null - - pattern = new UrlPattern '.foo' - t.equals pattern.match('.foobar'), null - - pattern = new UrlPattern '/foo' - t.equals pattern.match('/bar/foo'), null - - pattern = new UrlPattern '.foo' - t.equals pattern.match('.bar.foo'), null - - pattern = new UrlPattern /foo/ - t.deepEqual pattern.match('foo'), [] - - pattern = new UrlPattern /\/foo\/(.*)/ - t.deepEqual pattern.match('/foo/bar'), ['bar'] - - pattern = new UrlPattern /\/foo\/(.*)/ - t.deepEqual pattern.match('/foo/'), [''] - - pattern = new UrlPattern '/user/:userId/task/:taskId' - t.deepEqual pattern.match('/user/10/task/52'), - userId: '10' - taskId: '52' - - pattern = new UrlPattern '.user.:userId.task.:taskId' - t.deepEqual pattern.match('.user.10.task.52'), - userId: '10' - taskId: '52' - - pattern = new UrlPattern '*/user/:userId' - t.deepEqual pattern.match('/school/10/user/10'), - _: '/school/10', - userId: '10' - - pattern = new UrlPattern '*-user-:userId' - t.deepEqual pattern.match('-school-10-user-10'), - _: '-school-10' - userId: '10' - - pattern = new UrlPattern '/admin*' - t.deepEqual pattern.match('/admin/school/10/user/10'), - _: '/school/10/user/10' - - pattern = new UrlPattern '#admin*' - t.deepEqual pattern.match('#admin#school#10#user#10'), - _: '#school#10#user#10' - - pattern = new UrlPattern '/admin/*/user/:userId' - t.deepEqual pattern.match('/admin/school/10/user/10'), - _: 'school/10', - userId: '10' - - pattern = new UrlPattern '$admin$*$user$:userId' - t.deepEqual pattern.match('$admin$school$10$user$10'), - _: 'school$10' - userId: '10' - - pattern = new UrlPattern '/admin/*/user/*/tail' - t.deepEqual pattern.match('/admin/school/10/user/10/12/tail'), - _: ['school/10', '10/12'] - - pattern = new UrlPattern '$admin$*$user$*$tail' - t.deepEqual pattern.match('$admin$school$10$user$10$12$tail'), - _: ['school$10', '10$12'] - - pattern = new UrlPattern '/admin/*/user/:id/*/tail' - t.deepEqual pattern.match('/admin/school/10/user/10/12/13/tail'), - _: ['school/10', '12/13'] - id: '10' - - pattern = new UrlPattern '^admin^*^user^:id^*^tail' - t.deepEqual pattern.match('^admin^school^10^user^10^12^13^tail'), - _: ['school^10', '12^13'] - id: '10' - - pattern = new UrlPattern '/*/admin(/:path)' - t.deepEqual pattern.match('/admin/admin/admin'), - _: 'admin' - path: 'admin' - - pattern = new UrlPattern '(/)' - t.deepEqual pattern.match(''), {} - t.deepEqual pattern.match('/'), {} - - pattern = new UrlPattern '/admin(/foo)/bar' - t.deepEqual pattern.match('/admin/foo/bar'), {} - t.deepEqual pattern.match('/admin/bar'), {} - - pattern = new UrlPattern '/admin(/:foo)/bar' - t.deepEqual pattern.match('/admin/baz/bar'), - foo: 'baz' - t.deepEqual pattern.match('/admin/bar'), {} - - pattern = new UrlPattern '/admin/(*/)foo' - t.deepEqual pattern.match('/admin/foo'), {} - t.deepEqual pattern.match('/admin/baz/bar/biff/foo'), - _: 'baz/bar/biff' - - pattern = new UrlPattern '/v:major.:minor/*' - t.deepEqual pattern.match('/v1.2/resource/'), - _: 'resource/' - major: '1' - minor: '2' - - pattern = new UrlPattern '/v:v.:v/*' - t.deepEqual pattern.match('/v1.2/resource/'), - _: 'resource/' - v: ['1', '2'] - - pattern = new UrlPattern '/:foo_bar' - t.equal pattern.match('/_bar'), null - t.deepEqual pattern.match('/a_bar'), - foo: 'a' - t.deepEqual pattern.match('/a__bar'), - foo: 'a_' - t.deepEqual pattern.match('/a-b-c-d__bar'), - foo: 'a-b-c-d_' - t.deepEqual pattern.match('/a b%c-d__bar'), - foo: 'a b%c-d_' - - pattern = new UrlPattern '((((a)b)c)d)' - t.deepEqual pattern.match(''), {} - t.equal pattern.match('a'), null - t.equal pattern.match('ab'), null - t.equal pattern.match('abc'), null - t.deepEqual pattern.match('abcd'), {} - t.deepEqual pattern.match('bcd'), {} - t.deepEqual pattern.match('cd'), {} - t.deepEqual pattern.match('d'), {} - - pattern = new UrlPattern '/user/:range' - t.deepEqual pattern.match('/user/10-20'), - range: '10-20' - - pattern = new UrlPattern '/user/:range' - t.deepEqual pattern.match('/user/10_20'), - range: '10_20' - - pattern = new UrlPattern '/user/:range' - t.deepEqual pattern.match('/user/10 20'), - range: '10 20' - - pattern = new UrlPattern '/user/:range' - t.deepEqual pattern.match('/user/10%20'), - range: '10%20' - - pattern = new UrlPattern '/vvv:version/*' - t.equal null, pattern.match('/vvv/resource') - t.deepEqual pattern.match('/vvv1/resource'), - _: 'resource' - version: '1' - t.equal null, pattern.match('/vvv1.1/resource') - - pattern = new UrlPattern '/api/users/:id', - segmentValueCharset: 'a-zA-Z0-9-_~ %.@' - t.deepEqual pattern.match('/api/users/someuser@example.com'), - id: 'someuser@example.com' - - pattern = new UrlPattern '/api/users?username=:username', - segmentValueCharset: 'a-zA-Z0-9-_~ %.@' - t.deepEqual pattern.match('/api/users?username=someone@example.com'), - username: 'someone@example.com' - - pattern = new UrlPattern '/api/users?param1=:param1¶m2=:param2' - t.deepEqual pattern.match('/api/users?param1=foo¶m2=bar'), - param1: 'foo' - param2: 'bar' - - pattern = new UrlPattern ':scheme\\://:host(\\::port)', - segmentValueCharset: 'a-zA-Z0-9-_~ %.' - t.deepEqual pattern.match('ftp://ftp.example.com'), - scheme: 'ftp' - host: 'ftp.example.com' - t.deepEqual pattern.match('ftp://ftp.example.com:8080'), - scheme: 'ftp' - host: 'ftp.example.com' - port: '8080' - t.deepEqual pattern.match('https://example.com:80'), - scheme: 'https' - host: 'example.com' - port: '80' - - pattern = new UrlPattern ':scheme\\://:host(\\::port)(/api(/:resource(/:id)))', - segmentValueCharset: 'a-zA-Z0-9-_~ %.@' - t.deepEqual pattern.match('https://sss.www.localhost.com'), - scheme: 'https' - host: 'sss.www.localhost.com' - t.deepEqual pattern.match('https://sss.www.localhost.com:8080'), - scheme: 'https' - host: 'sss.www.localhost.com' - port: '8080' - t.deepEqual pattern.match('https://sss.www.localhost.com/api'), - scheme: 'https' - host: 'sss.www.localhost.com' - t.deepEqual pattern.match('https://sss.www.localhost.com/api/security'), - scheme: 'https' - host: 'sss.www.localhost.com' - resource: 'security' - t.deepEqual pattern.match('https://sss.www.localhost.com/api/security/bob@example.com'), - scheme: 'https' - host: 'sss.www.localhost.com' - resource: 'security' - id: 'bob@example.com' - - regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ - pattern = new UrlPattern regex - t.equal null, pattern.match('10.10.10.10') - t.equal null, pattern.match('ip/10.10.10.10') - t.equal null, pattern.match('/ip/10.10.10.') - t.equal null, pattern.match('/ip/10.') - t.equal null, pattern.match('/ip/') - t.deepEqual pattern.match('/ip/10.10.10.10'), ['10', '10', '10', '10'] - t.deepEqual pattern.match('/ip/127.0.0.1'), ['127', '0', '0', '1'] - - regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/ - pattern = new UrlPattern regex - t.equal null, pattern.match('10.10.10.10') - t.equal null, pattern.match('ip/10.10.10.10') - t.equal null, pattern.match('/ip/10.10.10.') - t.equal null, pattern.match('/ip/10.') - t.equal null, pattern.match('/ip/') - t.deepEqual pattern.match('/ip/10.10.10.10'), ['10.10.10.10'] - t.deepEqual pattern.match('/ip/127.0.0.1'), ['127.0.0.1'] - - regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/ - pattern = new UrlPattern regex, ['ip'] - t.equal null, pattern.match('10.10.10.10') - t.equal null, pattern.match('ip/10.10.10.10') - t.equal null, pattern.match('/ip/10.10.10.') - t.equal null, pattern.match('/ip/10.') - t.equal null, pattern.match('/ip/') - t.deepEqual pattern.match('/ip/10.10.10.10'), - ip: '10.10.10.10' - t.deepEqual pattern.match('/ip/127.0.0.1'), - ip: '127.0.0.1' - - t.end() diff --git a/test/match-fixtures.js b/test/match-fixtures.js new file mode 100644 index 0000000..c579683 --- /dev/null +++ b/test/match-fixtures.js @@ -0,0 +1,296 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const UrlPattern = require('../lib/url-pattern'); + +test('match', function(t) { + let pattern = new UrlPattern('/foo'); + t.deepEqual(pattern.match('/foo'), {}); + + pattern = new UrlPattern('.foo'); + t.deepEqual(pattern.match('.foo'), {}); + + pattern = new UrlPattern('/foo'); + t.equals(pattern.match('/foobar'), null); + + pattern = new UrlPattern('.foo'); + t.equals(pattern.match('.foobar'), null); + + pattern = new UrlPattern('/foo'); + t.equals(pattern.match('/bar/foo'), null); + + pattern = new UrlPattern('.foo'); + t.equals(pattern.match('.bar.foo'), null); + + pattern = new UrlPattern(/foo/); + t.deepEqual(pattern.match('foo'), []); + + pattern = new UrlPattern(/\/foo\/(.*)/); + t.deepEqual(pattern.match('/foo/bar'), ['bar']); + + pattern = new UrlPattern(/\/foo\/(.*)/); + t.deepEqual(pattern.match('/foo/'), ['']); + + pattern = new UrlPattern('/user/:userId/task/:taskId'); + t.deepEqual(pattern.match('/user/10/task/52'), { + userId: '10', + taskId: '52' + } + ); + + pattern = new UrlPattern('.user.:userId.task.:taskId'); + t.deepEqual(pattern.match('.user.10.task.52'), { + userId: '10', + taskId: '52' + } + ); + + pattern = new UrlPattern('*/user/:userId'); + t.deepEqual(pattern.match('/school/10/user/10'), { + _: '/school/10', + userId: '10' + } + ); + + pattern = new UrlPattern('*-user-:userId'); + t.deepEqual(pattern.match('-school-10-user-10'), { + _: '-school-10', + userId: '10' + } + ); + + pattern = new UrlPattern('/admin*'); + t.deepEqual(pattern.match('/admin/school/10/user/10'), + {_: '/school/10/user/10'}); + + pattern = new UrlPattern('#admin*'); + t.deepEqual(pattern.match('#admin#school#10#user#10'), + {_: '#school#10#user#10'}); + + pattern = new UrlPattern('/admin/*/user/:userId'); + t.deepEqual(pattern.match('/admin/school/10/user/10'), { + _: 'school/10', + userId: '10' + } + ); + + pattern = new UrlPattern('$admin$*$user$:userId'); + t.deepEqual(pattern.match('$admin$school$10$user$10'), { + _: 'school$10', + userId: '10' + } + ); + + pattern = new UrlPattern('/admin/*/user/*/tail'); + t.deepEqual(pattern.match('/admin/school/10/user/10/12/tail'), + {_: ['school/10', '10/12']}); + + pattern = new UrlPattern('$admin$*$user$*$tail'); + t.deepEqual(pattern.match('$admin$school$10$user$10$12$tail'), + {_: ['school$10', '10$12']}); + + pattern = new UrlPattern('/admin/*/user/:id/*/tail'); + t.deepEqual(pattern.match('/admin/school/10/user/10/12/13/tail'), { + _: ['school/10', '12/13'], + id: '10' + } + ); + + pattern = new UrlPattern('^admin^*^user^:id^*^tail'); + t.deepEqual(pattern.match('^admin^school^10^user^10^12^13^tail'), { + _: ['school^10', '12^13'], + id: '10' + } + ); + + pattern = new UrlPattern('/*/admin(/:path)'); + t.deepEqual(pattern.match('/admin/admin/admin'), { + _: 'admin', + path: 'admin' + } + ); + + pattern = new UrlPattern('(/)'); + t.deepEqual(pattern.match(''), {}); + t.deepEqual(pattern.match('/'), {}); + + pattern = new UrlPattern('/admin(/foo)/bar'); + t.deepEqual(pattern.match('/admin/foo/bar'), {}); + t.deepEqual(pattern.match('/admin/bar'), {}); + + pattern = new UrlPattern('/admin(/:foo)/bar'); + t.deepEqual(pattern.match('/admin/baz/bar'), + {foo: 'baz'}); + t.deepEqual(pattern.match('/admin/bar'), {}); + + pattern = new UrlPattern('/admin/(*/)foo'); + t.deepEqual(pattern.match('/admin/foo'), {}); + t.deepEqual(pattern.match('/admin/baz/bar/biff/foo'), + {_: 'baz/bar/biff'}); + + pattern = new UrlPattern('/v:major.:minor/*'); + t.deepEqual(pattern.match('/v1.2/resource/'), { + _: 'resource/', + major: '1', + minor: '2' + } + ); + + pattern = new UrlPattern('/v:v.:v/*'); + t.deepEqual(pattern.match('/v1.2/resource/'), { + _: 'resource/', + v: ['1', '2'] + }); + + pattern = new UrlPattern('/:foo_bar'); + t.equal(pattern.match('/_bar'), null); + t.deepEqual(pattern.match('/a_bar'), + {foo: 'a'}); + t.deepEqual(pattern.match('/a__bar'), + {foo: 'a_'}); + t.deepEqual(pattern.match('/a-b-c-d__bar'), + {foo: 'a-b-c-d_'}); + t.deepEqual(pattern.match('/a b%c-d__bar'), + {foo: 'a b%c-d_'}); + + pattern = new UrlPattern('((((a)b)c)d)'); + t.deepEqual(pattern.match(''), {}); + t.equal(pattern.match('a'), null); + t.equal(pattern.match('ab'), null); + t.equal(pattern.match('abc'), null); + t.deepEqual(pattern.match('abcd'), {}); + t.deepEqual(pattern.match('bcd'), {}); + t.deepEqual(pattern.match('cd'), {}); + t.deepEqual(pattern.match('d'), {}); + + pattern = new UrlPattern('/user/:range'); + t.deepEqual(pattern.match('/user/10-20'), + {range: '10-20'}); + + pattern = new UrlPattern('/user/:range'); + t.deepEqual(pattern.match('/user/10_20'), + {range: '10_20'}); + + pattern = new UrlPattern('/user/:range'); + t.deepEqual(pattern.match('/user/10 20'), + {range: '10 20'}); + + pattern = new UrlPattern('/user/:range'); + t.deepEqual(pattern.match('/user/10%20'), + {range: '10%20'}); + + pattern = new UrlPattern('/vvv:version/*'); + t.equal(null, pattern.match('/vvv/resource')); + t.deepEqual(pattern.match('/vvv1/resource'), { + _: 'resource', + version: '1' + } + ); + t.equal(null, pattern.match('/vvv1.1/resource')); + + pattern = new UrlPattern('/api/users/:id', + {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); + t.deepEqual(pattern.match('/api/users/someuser@example.com'), + {id: 'someuser@example.com'}); + + pattern = new UrlPattern('/api/users?username=:username', + {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); + t.deepEqual(pattern.match('/api/users?username=someone@example.com'), + {username: 'someone@example.com'}); + + pattern = new UrlPattern('/api/users?param1=:param1¶m2=:param2'); + t.deepEqual(pattern.match('/api/users?param1=foo¶m2=bar'), { + param1: 'foo', + param2: 'bar' + } + ); + + pattern = new UrlPattern(':scheme\\://:host(\\::port)', + {segmentValueCharset: 'a-zA-Z0-9-_~ %.'}); + t.deepEqual(pattern.match('ftp://ftp.example.com'), { + scheme: 'ftp', + host: 'ftp.example.com' + } + ); + t.deepEqual(pattern.match('ftp://ftp.example.com:8080'), { + scheme: 'ftp', + host: 'ftp.example.com', + port: '8080' + } + ); + t.deepEqual(pattern.match('https://example.com:80'), { + scheme: 'https', + host: 'example.com', + port: '80' + } + ); + + pattern = new UrlPattern(':scheme\\://:host(\\::port)(/api(/:resource(/:id)))', + {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); + t.deepEqual(pattern.match('https://sss.www.localhost.com'), { + scheme: 'https', + host: 'sss.www.localhost.com' + } + ); + t.deepEqual(pattern.match('https://sss.www.localhost.com:8080'), { + scheme: 'https', + host: 'sss.www.localhost.com', + port: '8080' + } + ); + t.deepEqual(pattern.match('https://sss.www.localhost.com/api'), { + scheme: 'https', + host: 'sss.www.localhost.com' + } + ); + t.deepEqual(pattern.match('https://sss.www.localhost.com/api/security'), { + scheme: 'https', + host: 'sss.www.localhost.com', + resource: 'security' + } + ); + t.deepEqual(pattern.match('https://sss.www.localhost.com/api/security/bob@example.com'), { + scheme: 'https', + host: 'sss.www.localhost.com', + resource: 'security', + id: 'bob@example.com' + } + ); + + let regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + pattern = new UrlPattern(regex); + t.equal(null, pattern.match('10.10.10.10')); + t.equal(null, pattern.match('ip/10.10.10.10')); + t.equal(null, pattern.match('/ip/10.10.10.')); + t.equal(null, pattern.match('/ip/10.')); + t.equal(null, pattern.match('/ip/')); + t.deepEqual(pattern.match('/ip/10.10.10.10'), ['10', '10', '10', '10']); + t.deepEqual(pattern.match('/ip/127.0.0.1'), ['127', '0', '0', '1']); + + regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; + pattern = new UrlPattern(regex); + t.equal(null, pattern.match('10.10.10.10')); + t.equal(null, pattern.match('ip/10.10.10.10')); + t.equal(null, pattern.match('/ip/10.10.10.')); + t.equal(null, pattern.match('/ip/10.')); + t.equal(null, pattern.match('/ip/')); + t.deepEqual(pattern.match('/ip/10.10.10.10'), ['10.10.10.10']); + t.deepEqual(pattern.match('/ip/127.0.0.1'), ['127.0.0.1']); + + regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; + pattern = new UrlPattern(regex, ['ip']); + t.equal(null, pattern.match('10.10.10.10')); + t.equal(null, pattern.match('ip/10.10.10.10')); + t.equal(null, pattern.match('/ip/10.10.10.')); + t.equal(null, pattern.match('/ip/10.')); + t.equal(null, pattern.match('/ip/')); + t.deepEqual(pattern.match('/ip/10.10.10.10'), + {ip: '10.10.10.10'}); + t.deepEqual(pattern.match('/ip/127.0.0.1'), + {ip: '127.0.0.1'}); + + return t.end(); +}); diff --git a/test/misc.coffee b/test/misc.coffee deleted file mode 100644 index 14550ac..0000000 --- a/test/misc.coffee +++ /dev/null @@ -1,30 +0,0 @@ -test = require 'tape' -UrlPattern = require '../lib/url-pattern' - -test 'instance of UrlPattern is handled correctly as constructor argument', (t) -> - pattern = new UrlPattern '/user/:userId/task/:taskId' - copy = new UrlPattern pattern - t.deepEqual copy.match('/user/10/task/52'), - userId: '10' - taskId: '52' - t.end() - -test 'match full stops in segment values', (t) -> - options = - segmentValueCharset: 'a-zA-Z0-9-_ %.' - pattern = new UrlPattern '/api/v1/user/:id/', options - t.deepEqual pattern.match('/api/v1/user/test.name/'), - id: 'test.name' - t.end() - -test 'regex group names', (t) -> - pattern = new UrlPattern /^\/api\/([a-zA-Z0-9-_~ %]+)(?:\/(\d+))?$/, ['resource', 'id'] - t.deepEqual pattern.match('/api/users'), - resource: 'users' - t.equal pattern.match('/apiii/users'), null - t.deepEqual pattern.match('/api/users/foo'), null - t.deepEqual pattern.match('/api/users/10'), - resource: 'users' - id: '10' - t.deepEqual pattern.match('/api/projects/10/'), null - t.end() diff --git a/test/misc.js b/test/misc.js new file mode 100644 index 0000000..f395734 --- /dev/null +++ b/test/misc.js @@ -0,0 +1,42 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const UrlPattern = require('../lib/url-pattern'); + +test('instance of UrlPattern is handled correctly as constructor argument', function(t) { + const pattern = new UrlPattern('/user/:userId/task/:taskId'); + const copy = new UrlPattern(pattern); + t.deepEqual(copy.match('/user/10/task/52'), { + userId: '10', + taskId: '52' + } + ); + return t.end(); +}); + +test('match full stops in segment values', function(t) { + const options = + {segmentValueCharset: 'a-zA-Z0-9-_ %.'}; + const pattern = new UrlPattern('/api/v1/user/:id/', options); + t.deepEqual(pattern.match('/api/v1/user/test.name/'), + {id: 'test.name'}); + return t.end(); +}); + +test('regex group names', function(t) { + const pattern = new UrlPattern(/^\/api\/([a-zA-Z0-9-_~ %]+)(?:\/(\d+))?$/, ['resource', 'id']); + t.deepEqual(pattern.match('/api/users'), + {resource: 'users'}); + t.equal(pattern.match('/apiii/users'), null); + t.deepEqual(pattern.match('/api/users/foo'), null); + t.deepEqual(pattern.match('/api/users/10'), { + resource: 'users', + id: '10' + } + ); + t.deepEqual(pattern.match('/api/projects/10/'), null); + return t.end(); +}); diff --git a/test/parser.coffee b/test/parser.coffee deleted file mode 100644 index e7d5c74..0000000 --- a/test/parser.coffee +++ /dev/null @@ -1,426 +0,0 @@ -# taken from -# https://github.com/snd/pcom/blob/master/t/url-pattern-example.coffee - -test = require 'tape' - -UrlPattern = require '../lib/url-pattern' -U = UrlPattern.newParser(UrlPattern.defaultOptions) -parse = U.pattern - -test 'wildcard', (t) -> - t.deepEqual U.wildcard('*'), - value: - tag: 'wildcard' - value: '*' - rest: '' - t.deepEqual U.wildcard('*/'), - value: - tag: 'wildcard' - value: '*' - rest: '/' - t.equal U.wildcard(' *'), undefined - t.equal U.wildcard('()'), undefined - t.equal U.wildcard('foo(100)'), undefined - t.equal U.wildcard('(100foo)'), undefined - t.equal U.wildcard('(foo100)'), undefined - t.equal U.wildcard('(foobar)'), undefined - t.equal U.wildcard('foobar'), undefined - t.equal U.wildcard('_aa'), undefined - t.equal U.wildcard('$foobar'), undefined - t.equal U.wildcard('$'), undefined - t.equal U.wildcard(''), undefined - t.end() - -test 'named', (t) -> - t.deepEqual U.named(':a'), - value: - tag: 'named' - value: 'a' - rest: '' - t.deepEqual U.named(':ab96c'), - value: - tag: 'named' - value: 'ab96c' - rest: '' - t.deepEqual U.named(':ab96c.'), - value: - tag: 'named' - value: 'ab96c' - rest: '.' - t.deepEqual U.named(':96c-:ab'), - value: - tag: 'named' - value: '96c' - rest: '-:ab' - t.equal U.named(':'), undefined - t.equal U.named(''), undefined - t.equal U.named('a'), undefined - t.equal U.named('abc'), undefined - t.end() - -test 'static', (t) -> - t.deepEqual U.static('a'), - value: - tag: 'static' - value: 'a' - rest: '' - t.deepEqual U.static('abc:d'), - value: - tag: 'static' - value: 'abc' - rest: ':d' - t.equal U.static(':ab96c'), undefined - t.equal U.static(':'), undefined - t.equal U.static('('), undefined - t.equal U.static(')'), undefined - t.equal U.static('*'), undefined - t.equal U.static(''), undefined - t.end() - - -test 'fixtures', (t) -> - t.equal parse(''), undefined - t.equal parse('('), undefined - t.equal parse(')'), undefined - t.equal parse('()'), undefined - t.equal parse(':'), undefined - t.equal parse('((foo)'), undefined - t.equal parse('(((foo)bar(boo)far)'), undefined - - t.deepEqual parse('(foo))'), - rest: ')' - value: [ - {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} - ] - - t.deepEqual parse('((foo)))bar'), - rest: ')bar' - value: [ - { - tag: 'optional' - value: [ - {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} - ] - } - ] - - - t.deepEqual parse('foo:*'), - rest: ':*' - value: [ - {tag: 'static', value: 'foo'} - ] - - t.deepEqual parse(':foo:bar'), - rest: '' - value: [ - {tag: 'named', value: 'foo'} - {tag: 'named', value: 'bar'} - ] - - t.deepEqual parse('a'), - rest: '' - value: [ - {tag: 'static', value: 'a'} - ] - t.deepEqual parse('user42'), - rest: '' - value: [ - {tag: 'static', value: 'user42'} - ] - t.deepEqual parse(':a'), - rest: '' - value: [ - {tag: 'named', value: 'a'} - ] - t.deepEqual parse('*'), - rest: '' - value: [ - {tag: 'wildcard', value: '*'} - ] - t.deepEqual parse('(foo)'), - rest: '' - value: [ - {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} - ] - t.deepEqual parse('(:foo)'), - rest: '' - value: [ - {tag: 'optional', value: [{tag: 'named', value: 'foo'}]} - ] - t.deepEqual parse('(*)'), - rest: '' - value: [ - {tag: 'optional', value: [{tag: 'wildcard', value: '*'}]} - ] - - - t.deepEqual parse('/api/users/:id'), - rest: '' - value: [ - {tag: 'static', value: '/api/users/'} - {tag: 'named', value: 'id'} - ] - t.deepEqual parse('/v:major(.:minor)/*'), - rest: '' - value: [ - {tag: 'static', value: '/v'} - {tag: 'named', value: 'major'} - { - tag: 'optional' - value: [ - {tag: 'static', value: '.'} - {tag: 'named', value: 'minor'} - ] - } - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - ] - t.deepEqual parse('(http(s)\\://)(:subdomain.):domain.:tld(/*)'), - rest: '' - value: [ - { - tag: 'optional' - value: [ - {tag: 'static', value: 'http'} - { - tag: 'optional' - value: [ - {tag: 'static', value: 's'} - ] - } - {tag: 'static', value: '://'} - ] - } - { - tag: 'optional' - value: [ - {tag: 'named', value: 'subdomain'} - {tag: 'static', value: '.'} - ] - } - {tag: 'named', value: 'domain'} - {tag: 'static', value: '.'} - {tag: 'named', value: 'tld'} - { - tag: 'optional' - value: [ - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - ] - } - ] - t.deepEqual parse('/api/users/:ids/posts/:ids'), - rest: '' - value: [ - {tag: 'static', value: '/api/users/'} - {tag: 'named', value: 'ids'} - {tag: 'static', value: '/posts/'} - {tag: 'named', value: 'ids'} - ] - - t.deepEqual parse('/user/:userId/task/:taskId'), - rest: '' - value: [ - {tag: 'static', value: '/user/'} - {tag: 'named', value: 'userId'} - {tag: 'static', value: '/task/'} - {tag: 'named', value: 'taskId'} - ] - - t.deepEqual parse('.user.:userId.task.:taskId'), - rest: '' - value: [ - {tag: 'static', value: '.user.'} - {tag: 'named', value: 'userId'} - {tag: 'static', value: '.task.'} - {tag: 'named', value: 'taskId'} - ] - - t.deepEqual parse('*/user/:userId'), - rest: '' - value: [ - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/user/'} - {tag: 'named', value: 'userId'} - ] - - t.deepEqual parse('*-user-:userId'), - rest: '' - value: [ - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '-user-'} - {tag: 'named', value: 'userId'} - ] - - t.deepEqual parse('/admin*'), - rest: '' - value: [ - {tag: 'static', value: '/admin'} - {tag: 'wildcard', value: '*'} - ] - - t.deepEqual parse('#admin*'), - rest: '' - value: [ - {tag: 'static', value: '#admin'} - {tag: 'wildcard', value: '*'} - ] - - t.deepEqual parse('/admin/*/user/:userId'), - rest: '' - value: [ - {tag: 'static', value: '/admin/'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/user/'} - {tag: 'named', value: 'userId'} - ] - - t.deepEqual parse('$admin$*$user$:userId'), - rest: '' - value: [ - {tag: 'static', value: '$admin$'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '$user$'} - {tag: 'named', value: 'userId'} - ] - - t.deepEqual parse('/admin/*/user/*/tail'), - rest: '' - value: [ - {tag: 'static', value: '/admin/'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/user/'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/tail'} - ] - - t.deepEqual parse('/admin/*/user/:id/*/tail'), - rest: '' - value: [ - {tag: 'static', value: '/admin/'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/user/'} - {tag: 'named', value: 'id'} - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/tail'} - ] - - t.deepEqual parse('^admin^*^user^:id^*^tail'), - rest: '' - value: [ - {tag: 'static', value: '^admin^'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '^user^'} - {tag: 'named', value: 'id'} - {tag: 'static', value: '^'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '^tail'} - ] - - t.deepEqual parse('/*/admin(/:path)'), - rest: '' - value: [ - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/admin'} - {tag: 'optional', value: [ - {tag: 'static', value: '/'} - {tag: 'named', value: 'path'} - ]} - ] - - t.deepEqual parse('/'), - rest: '' - value: [ - {tag: 'static', value: '/'} - ] - - t.deepEqual parse('(/)'), - rest: '' - value: [ - {tag: 'optional', value: [ - {tag: 'static', value: '/'} - ]} - ] - - t.deepEqual parse('/admin(/:foo)/bar'), - rest: '' - value: [ - {tag: 'static', value: '/admin'} - {tag: 'optional', value: [ - {tag: 'static', value: '/'} - {tag: 'named', value: 'foo'} - ]} - {tag: 'static', value: '/bar'} - ] - - t.deepEqual parse('/admin(*/)foo'), - rest: '' - value: [ - {tag: 'static', value: '/admin'} - {tag: 'optional', value: [ - {tag: 'wildcard', value: '*'} - {tag: 'static', value: '/'} - ]} - {tag: 'static', value: 'foo'} - ] - - t.deepEqual parse('/v:major.:minor/*'), - rest: '' - value: [ - {tag: 'static', value: '/v'} - {tag: 'named', value: 'major'} - {tag: 'static', value: '.'} - {tag: 'named', value: 'minor'} - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - ] - - t.deepEqual parse('/v:v.:v/*'), - rest: '' - value: [ - {tag: 'static', value: '/v'} - {tag: 'named', value: 'v'} - {tag: 'static', value: '.'} - {tag: 'named', value: 'v'} - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - ] - - t.deepEqual parse('/:foo_bar'), - rest: '' - value: [ - {tag: 'static', value: '/'} - {tag: 'named', value: 'foo'} - {tag: 'static', value: '_bar'} - ] - - t.deepEqual parse('((((a)b)c)d)'), - rest: '' - value: [ - {tag: 'optional', value: [ - {tag: 'optional', value: [ - {tag: 'optional', value: [ - {tag: 'optional', value: [ - {tag: 'static', value: 'a'} - ]} - {tag: 'static', value: 'b'} - ]} - {tag: 'static', value: 'c'} - ]} - {tag: 'static', value: 'd'} - ]} - ] - - t.deepEqual parse('/vvv:version/*'), - rest: '' - value: [ - {tag: 'static', value: '/vvv'} - {tag: 'named', value: 'version'} - {tag: 'static', value: '/'} - {tag: 'wildcard', value: '*'} - ] - - t.end() diff --git a/test/parser.js b/test/parser.js new file mode 100644 index 0000000..1b61a6f --- /dev/null +++ b/test/parser.js @@ -0,0 +1,495 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// taken from +// https://github.com/snd/pcom/blob/master/t/url-pattern-example.coffee + +const test = require('tape'); + +const UrlPattern = require('../lib/url-pattern'); +const U = UrlPattern.newParser(UrlPattern.defaultOptions); +const parse = U.pattern; + +test('wildcard', function(t) { + t.deepEqual(U.wildcard('*'), { + value: { + tag: 'wildcard', + value: '*' + }, + rest: '' + } + ); + t.deepEqual(U.wildcard('*/'), { + value: { + tag: 'wildcard', + value: '*' + }, + rest: '/' + } + ); + t.equal(U.wildcard(' *'), undefined); + t.equal(U.wildcard('()'), undefined); + t.equal(U.wildcard('foo(100)'), undefined); + t.equal(U.wildcard('(100foo)'), undefined); + t.equal(U.wildcard('(foo100)'), undefined); + t.equal(U.wildcard('(foobar)'), undefined); + t.equal(U.wildcard('foobar'), undefined); + t.equal(U.wildcard('_aa'), undefined); + t.equal(U.wildcard('$foobar'), undefined); + t.equal(U.wildcard('$'), undefined); + t.equal(U.wildcard(''), undefined); + return t.end(); +}); + +test('named', function(t) { + t.deepEqual(U.named(':a'), { + value: { + tag: 'named', + value: 'a' + }, + rest: '' + } + ); + t.deepEqual(U.named(':ab96c'), { + value: { + tag: 'named', + value: 'ab96c' + }, + rest: '' + } + ); + t.deepEqual(U.named(':ab96c.'), { + value: { + tag: 'named', + value: 'ab96c' + }, + rest: '.' + } + ); + t.deepEqual(U.named(':96c-:ab'), { + value: { + tag: 'named', + value: '96c' + }, + rest: '-:ab' + } + ); + t.equal(U.named(':'), undefined); + t.equal(U.named(''), undefined); + t.equal(U.named('a'), undefined); + t.equal(U.named('abc'), undefined); + return t.end(); +}); + +test('static', function(t) { + t.deepEqual(U.static('a'), { + value: { + tag: 'static', + value: 'a' + }, + rest: '' + } + ); + t.deepEqual(U.static('abc:d'), { + value: { + tag: 'static', + value: 'abc' + }, + rest: ':d' + } + ); + t.equal(U.static(':ab96c'), undefined); + t.equal(U.static(':'), undefined); + t.equal(U.static('('), undefined); + t.equal(U.static(')'), undefined); + t.equal(U.static('*'), undefined); + t.equal(U.static(''), undefined); + return t.end(); +}); + + +test('fixtures', function(t) { + t.equal(parse(''), undefined); + t.equal(parse('('), undefined); + t.equal(parse(')'), undefined); + t.equal(parse('()'), undefined); + t.equal(parse(':'), undefined); + t.equal(parse('((foo)'), undefined); + t.equal(parse('(((foo)bar(boo)far)'), undefined); + + t.deepEqual(parse('(foo))'), { + rest: ')', + value: [ + {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} + ] + }); + + t.deepEqual(parse('((foo)))bar'), { + rest: ')bar', + value: [ + { + tag: 'optional', + value: [ + {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} + ] + } + ] + }); + + + t.deepEqual(parse('foo:*'), { + rest: ':*', + value: [ + {tag: 'static', value: 'foo'} + ] + }); + + t.deepEqual(parse(':foo:bar'), { + rest: '', + value: [ + {tag: 'named', value: 'foo'}, + {tag: 'named', value: 'bar'} + ] + }); + + t.deepEqual(parse('a'), { + rest: '', + value: [ + {tag: 'static', value: 'a'} + ] + }); + t.deepEqual(parse('user42'), { + rest: '', + value: [ + {tag: 'static', value: 'user42'} + ] + }); + t.deepEqual(parse(':a'), { + rest: '', + value: [ + {tag: 'named', value: 'a'} + ] + }); + t.deepEqual(parse('*'), { + rest: '', + value: [ + {tag: 'wildcard', value: '*'} + ] + }); + t.deepEqual(parse('(foo)'), { + rest: '', + value: [ + {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} + ] + }); + t.deepEqual(parse('(:foo)'), { + rest: '', + value: [ + {tag: 'optional', value: [{tag: 'named', value: 'foo'}]} + ] + }); + t.deepEqual(parse('(*)'), { + rest: '', + value: [ + {tag: 'optional', value: [{tag: 'wildcard', value: '*'}]} + ] + }); + + + t.deepEqual(parse('/api/users/:id'), { + rest: '', + value: [ + {tag: 'static', value: '/api/users/'}, + {tag: 'named', value: 'id'} + ] + }); + t.deepEqual(parse('/v:major(.:minor)/*'), { + rest: '', + value: [ + {tag: 'static', value: '/v'}, + {tag: 'named', value: 'major'}, + { + tag: 'optional', + value: [ + {tag: 'static', value: '.'}, + {tag: 'named', value: 'minor'} + ] + }, + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'} + ] + }); + t.deepEqual(parse('(http(s)\\://)(:subdomain.):domain.:tld(/*)'), { + rest: '', + value: [ + { + tag: 'optional', + value: [ + {tag: 'static', value: 'http'}, + { + tag: 'optional', + value: [ + {tag: 'static', value: 's'} + ] + }, + {tag: 'static', value: '://'} + ] + }, + { + tag: 'optional', + value: [ + {tag: 'named', value: 'subdomain'}, + {tag: 'static', value: '.'} + ] + }, + {tag: 'named', value: 'domain'}, + {tag: 'static', value: '.'}, + {tag: 'named', value: 'tld'}, + { + tag: 'optional', + value: [ + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'} + ] + } + ] + }); + t.deepEqual(parse('/api/users/:ids/posts/:ids'), { + rest: '', + value: [ + {tag: 'static', value: '/api/users/'}, + {tag: 'named', value: 'ids'}, + {tag: 'static', value: '/posts/'}, + {tag: 'named', value: 'ids'} + ] + }); + + t.deepEqual(parse('/user/:userId/task/:taskId'), { + rest: '', + value: [ + {tag: 'static', value: '/user/'}, + {tag: 'named', value: 'userId'}, + {tag: 'static', value: '/task/'}, + {tag: 'named', value: 'taskId'} + ] + }); + + t.deepEqual(parse('.user.:userId.task.:taskId'), { + rest: '', + value: [ + {tag: 'static', value: '.user.'}, + {tag: 'named', value: 'userId'}, + {tag: 'static', value: '.task.'}, + {tag: 'named', value: 'taskId'} + ] + }); + + t.deepEqual(parse('*/user/:userId'), { + rest: '', + value: [ + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/user/'}, + {tag: 'named', value: 'userId'} + ] + }); + + t.deepEqual(parse('*-user-:userId'), { + rest: '', + value: [ + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '-user-'}, + {tag: 'named', value: 'userId'} + ] + }); + + t.deepEqual(parse('/admin*'), { + rest: '', + value: [ + {tag: 'static', value: '/admin'}, + {tag: 'wildcard', value: '*'} + ] + }); + + t.deepEqual(parse('#admin*'), { + rest: '', + value: [ + {tag: 'static', value: '#admin'}, + {tag: 'wildcard', value: '*'} + ] + }); + + t.deepEqual(parse('/admin/*/user/:userId'), { + rest: '', + value: [ + {tag: 'static', value: '/admin/'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/user/'}, + {tag: 'named', value: 'userId'} + ] + }); + + t.deepEqual(parse('$admin$*$user$:userId'), { + rest: '', + value: [ + {tag: 'static', value: '$admin$'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '$user$'}, + {tag: 'named', value: 'userId'} + ] + }); + + t.deepEqual(parse('/admin/*/user/*/tail'), { + rest: '', + value: [ + {tag: 'static', value: '/admin/'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/user/'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/tail'} + ] + }); + + t.deepEqual(parse('/admin/*/user/:id/*/tail'), { + rest: '', + value: [ + {tag: 'static', value: '/admin/'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/user/'}, + {tag: 'named', value: 'id'}, + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/tail'} + ] + }); + + t.deepEqual(parse('^admin^*^user^:id^*^tail'), { + rest: '', + value: [ + {tag: 'static', value: '^admin^'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '^user^'}, + {tag: 'named', value: 'id'}, + {tag: 'static', value: '^'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '^tail'} + ] + }); + + t.deepEqual(parse('/*/admin(/:path)'), { + rest: '', + value: [ + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/admin'}, + {tag: 'optional', value: [ + {tag: 'static', value: '/'}, + {tag: 'named', value: 'path'} + ]} + ] + }); + + t.deepEqual(parse('/'), { + rest: '', + value: [ + {tag: 'static', value: '/'} + ] + }); + + t.deepEqual(parse('(/)'), { + rest: '', + value: [ + {tag: 'optional', value: [ + {tag: 'static', value: '/'} + ]} + ] + }); + + t.deepEqual(parse('/admin(/:foo)/bar'), { + rest: '', + value: [ + {tag: 'static', value: '/admin'}, + {tag: 'optional', value: [ + {tag: 'static', value: '/'}, + {tag: 'named', value: 'foo'} + ]}, + {tag: 'static', value: '/bar'} + ] + }); + + t.deepEqual(parse('/admin(*/)foo'), { + rest: '', + value: [ + {tag: 'static', value: '/admin'}, + {tag: 'optional', value: [ + {tag: 'wildcard', value: '*'}, + {tag: 'static', value: '/'} + ]}, + {tag: 'static', value: 'foo'} + ] + }); + + t.deepEqual(parse('/v:major.:minor/*'), { + rest: '', + value: [ + {tag: 'static', value: '/v'}, + {tag: 'named', value: 'major'}, + {tag: 'static', value: '.'}, + {tag: 'named', value: 'minor'}, + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'} + ] + }); + + t.deepEqual(parse('/v:v.:v/*'), { + rest: '', + value: [ + {tag: 'static', value: '/v'}, + {tag: 'named', value: 'v'}, + {tag: 'static', value: '.'}, + {tag: 'named', value: 'v'}, + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'} + ] + }); + + t.deepEqual(parse('/:foo_bar'), { + rest: '', + value: [ + {tag: 'static', value: '/'}, + {tag: 'named', value: 'foo'}, + {tag: 'static', value: '_bar'} + ] + }); + + t.deepEqual(parse('((((a)b)c)d)'), { + rest: '', + value: [ + {tag: 'optional', value: [ + {tag: 'optional', value: [ + {tag: 'optional', value: [ + {tag: 'optional', value: [ + {tag: 'static', value: 'a'} + ]}, + {tag: 'static', value: 'b'} + ]}, + {tag: 'static', value: 'c'} + ]}, + {tag: 'static', value: 'd'} + ]} + ] + }); + + t.deepEqual(parse('/vvv:version/*'), { + rest: '', + value: [ + {tag: 'static', value: '/vvv'}, + {tag: 'named', value: 'version'}, + {tag: 'static', value: '/'}, + {tag: 'wildcard', value: '*'} + ] + }); + + return t.end(); +}); diff --git a/test/readme.coffee b/test/readme.coffee deleted file mode 100644 index e3861c1..0000000 --- a/test/readme.coffee +++ /dev/null @@ -1,121 +0,0 @@ -test = require 'tape' -UrlPattern = require '../lib/url-pattern' - -test 'simple', (t) -> - pattern = new UrlPattern('/api/users/:id') - t.deepEqual pattern.match('/api/users/10'), {id: '10'} - t.equal pattern.match('/api/products/5'), null - t.end() - -test 'api versioning', (t) -> - pattern = new UrlPattern('/v:major(.:minor)/*') - t.deepEqual pattern.match('/v1.2/'), {major: '1', minor: '2', _: ''} - t.deepEqual pattern.match('/v2/users'), {major: '2', _: 'users'} - t.equal pattern.match('/v/'), null - t.end() - -test 'domain', (t) -> - pattern = new UrlPattern('(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)') - t.deepEqual pattern.match('google.de'), - domain: 'google' - tld: 'de' - t.deepEqual pattern.match('https://www.google.com'), - subdomain: 'www' - domain: 'google' - tld: 'com' - t.deepEqual pattern.match('http://mail.google.com/mail'), - subdomain: 'mail' - domain: 'google' - tld: 'com' - _: 'mail' - t.deepEqual pattern.match('http://mail.google.com:80/mail'), - subdomain: 'mail' - domain: 'google' - tld: 'com' - port: '80' - _: 'mail' - t.equal pattern.match('google'), null - - t.deepEqual pattern.match('www.google.com'), - subdomain: 'www' - domain: 'google' - tld: 'com' - t.equal pattern.match('httpp://mail.google.com/mail'), null - t.deepEqual pattern.match('google.de/search'), - domain: 'google' - tld: 'de' - _: 'search' - - t.end() - -test 'named segment occurs more than once', (t) -> - pattern = new UrlPattern('/api/users/:ids/posts/:ids') - t.deepEqual pattern.match('/api/users/10/posts/5'), {ids: ['10', '5']} - t.end() - -test 'regex', (t) -> - pattern = new UrlPattern(/^\/api\/(.*)$/) - t.deepEqual pattern.match('/api/users'), ['users'] - t.equal pattern.match('/apiii/users'), null - t.end() - -test 'regex group names', (t) -> - pattern = new UrlPattern(/^\/api\/([^\/]+)(?:\/(\d+))?$/, ['resource', 'id']) - t.deepEqual pattern.match('/api/users'), - resource: 'users' - t.equal pattern.match('/api/users/'), null - t.deepEqual pattern.match('/api/users/5'), - resource: 'users' - id: '5' - t.equal pattern.match('/api/users/foo'), null - t.end() - -test 'stringify', (t) -> - pattern = new UrlPattern('/api/users/:id') - t.equal '/api/users/10', pattern.stringify(id: 10) - - pattern = new UrlPattern('/api/users(/:id)') - t.equal '/api/users', pattern.stringify() - t.equal '/api/users/10', pattern.stringify(id: 10) - - t.end() - -test 'customization', (t) -> - options = - escapeChar: '!' - segmentNameStartChar: '$' - segmentNameCharset: 'a-zA-Z0-9_-' - segmentValueCharset: 'a-zA-Z0-9' - optionalSegmentStartChar: '[' - optionalSegmentEndChar: ']' - wildcardChar: '?' - - pattern = new UrlPattern( - '[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]' - options - ) - - t.deepEqual pattern.match('google.de'), - domain: 'google' - 'toplevel-domain': 'de' - t.deepEqual pattern.match('http://mail.google.com/mail'), - sub_domain: 'mail' - domain: 'google' - 'toplevel-domain': 'com' - _: 'mail' - t.equal pattern.match('http://mail.this-should-not-match.com/mail'), null - t.equal pattern.match('google'), null - t.deepEqual pattern.match('www.google.com'), - sub_domain: 'www' - domain: 'google' - 'toplevel-domain': 'com' - t.deepEqual pattern.match('https://www.google.com'), - sub_domain: 'www' - domain: 'google' - 'toplevel-domain': 'com' - t.equal pattern.match('httpp://mail.google.com/mail'), null - t.deepEqual pattern.match('google.de/search'), - domain: 'google' - 'toplevel-domain': 'de' - _: 'search' - t.end() diff --git a/test/readme.js b/test/readme.js new file mode 100644 index 0000000..e976ef5 --- /dev/null +++ b/test/readme.js @@ -0,0 +1,159 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const UrlPattern = require('../lib/url-pattern'); + +test('simple', function(t) { + const pattern = new UrlPattern('/api/users/:id'); + t.deepEqual(pattern.match('/api/users/10'), {id: '10'}); + t.equal(pattern.match('/api/products/5'), null); + return t.end(); +}); + +test('api versioning', function(t) { + const pattern = new UrlPattern('/v:major(.:minor)/*'); + t.deepEqual(pattern.match('/v1.2/'), {major: '1', minor: '2', _: ''}); + t.deepEqual(pattern.match('/v2/users'), {major: '2', _: 'users'}); + t.equal(pattern.match('/v/'), null); + return t.end(); +}); + +test('domain', function(t) { + const pattern = new UrlPattern('(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)'); + t.deepEqual(pattern.match('google.de'), { + domain: 'google', + tld: 'de' + } + ); + t.deepEqual(pattern.match('https://www.google.com'), { + subdomain: 'www', + domain: 'google', + tld: 'com' + } + ); + t.deepEqual(pattern.match('http://mail.google.com/mail'), { + subdomain: 'mail', + domain: 'google', + tld: 'com', + _: 'mail' + } + ); + t.deepEqual(pattern.match('http://mail.google.com:80/mail'), { + subdomain: 'mail', + domain: 'google', + tld: 'com', + port: '80', + _: 'mail' + } + ); + t.equal(pattern.match('google'), null); + + t.deepEqual(pattern.match('www.google.com'), { + subdomain: 'www', + domain: 'google', + tld: 'com' + } + ); + t.equal(pattern.match('httpp://mail.google.com/mail'), null); + t.deepEqual(pattern.match('google.de/search'), { + domain: 'google', + tld: 'de', + _: 'search' + } + ); + + return t.end(); +}); + +test('named segment occurs more than once', function(t) { + const pattern = new UrlPattern('/api/users/:ids/posts/:ids'); + t.deepEqual(pattern.match('/api/users/10/posts/5'), {ids: ['10', '5']}); + return t.end(); +}); + +test('regex', function(t) { + const pattern = new UrlPattern(/^\/api\/(.*)$/); + t.deepEqual(pattern.match('/api/users'), ['users']); + t.equal(pattern.match('/apiii/users'), null); + return t.end(); +}); + +test('regex group names', function(t) { + const pattern = new UrlPattern(/^\/api\/([^\/]+)(?:\/(\d+))?$/, ['resource', 'id']); + t.deepEqual(pattern.match('/api/users'), + {resource: 'users'}); + t.equal(pattern.match('/api/users/'), null); + t.deepEqual(pattern.match('/api/users/5'), { + resource: 'users', + id: '5' + } + ); + t.equal(pattern.match('/api/users/foo'), null); + return t.end(); +}); + +test('stringify', function(t) { + let pattern = new UrlPattern('/api/users/:id'); + t.equal('/api/users/10', pattern.stringify({id: 10})); + + pattern = new UrlPattern('/api/users(/:id)'); + t.equal('/api/users', pattern.stringify()); + t.equal('/api/users/10', pattern.stringify({id: 10})); + + return t.end(); +}); + +test('customization', function(t) { + const options = { + escapeChar: '!', + segmentNameStartChar: '$', + segmentNameCharset: 'a-zA-Z0-9_-', + segmentValueCharset: 'a-zA-Z0-9', + optionalSegmentStartChar: '[', + optionalSegmentEndChar: ']', + wildcardChar: '?' + }; + + const pattern = new UrlPattern( + '[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]', + options + ); + + t.deepEqual(pattern.match('google.de'), { + domain: 'google', + 'toplevel-domain': 'de' + } + ); + t.deepEqual(pattern.match('http://mail.google.com/mail'), { + sub_domain: 'mail', + domain: 'google', + 'toplevel-domain': 'com', + _: 'mail' + } + ); + t.equal(pattern.match('http://mail.this-should-not-match.com/mail'), null); + t.equal(pattern.match('google'), null); + t.deepEqual(pattern.match('www.google.com'), { + sub_domain: 'www', + domain: 'google', + 'toplevel-domain': 'com' + } + ); + t.deepEqual(pattern.match('https://www.google.com'), { + sub_domain: 'www', + domain: 'google', + 'toplevel-domain': 'com' + } + ); + t.equal(pattern.match('httpp://mail.google.com/mail'), null); + t.deepEqual(pattern.match('google.de/search'), { + domain: 'google', + 'toplevel-domain': 'de', + _: 'search' + } + ); + return t.end(); +}); diff --git a/test/stringify-fixtures.coffee b/test/stringify-fixtures.coffee deleted file mode 100644 index cd69bfe..0000000 --- a/test/stringify-fixtures.coffee +++ /dev/null @@ -1,162 +0,0 @@ -test = require 'tape' -UrlPattern = require '../lib/url-pattern' - -test 'stringify', (t) -> - pattern = new UrlPattern '/foo' - t.equal '/foo', pattern.stringify() - - pattern = new UrlPattern '/user/:userId/task/:taskId' - t.equal '/user/10/task/52', pattern.stringify - userId: '10' - taskId: '52' - - pattern = new UrlPattern '/user/:userId/task/:taskId' - t.equal '/user/10/task/52', pattern.stringify - userId: '10' - taskId: '52' - ignored: 'ignored' - - pattern = new UrlPattern '.user.:userId.task.:taskId' - t.equal '.user.10.task.52', pattern.stringify - userId: '10' - taskId: '52' - - pattern = new UrlPattern '*/user/:userId' - t.equal '/school/10/user/10', pattern.stringify - _: '/school/10', - userId: '10' - - pattern = new UrlPattern '*-user-:userId' - t.equal '-school-10-user-10', pattern.stringify - _: '-school-10' - userId: '10' - - pattern = new UrlPattern '/admin*' - t.equal '/admin/school/10/user/10', pattern.stringify - _: '/school/10/user/10' - - pattern = new UrlPattern '/admin/*/user/*/tail' - t.equal '/admin/school/10/user/10/12/tail', pattern.stringify - _: ['school/10', '10/12'] - - pattern = new UrlPattern '/admin/*/user/:id/*/tail' - t.equal '/admin/school/10/user/10/12/13/tail', pattern.stringify - _: ['school/10', '12/13'] - id: '10' - - pattern = new UrlPattern '/*/admin(/:path)' - t.equal '/foo/admin/baz', pattern.stringify - _: 'foo' - path: 'baz' - t.equal '/foo/admin', pattern.stringify - _: 'foo' - - pattern = new UrlPattern '(/)' - t.equal '', pattern.stringify() - - pattern = new UrlPattern '/admin(/foo)/bar' - t.equal '/admin/bar', pattern.stringify() - - pattern = new UrlPattern '/admin(/:foo)/bar' - t.equal '/admin/bar', pattern.stringify() - t.equal '/admin/baz/bar', pattern.stringify - foo: 'baz' - - pattern = new UrlPattern '/admin/(*/)foo' - t.equal '/admin/foo', pattern.stringify() - t.equal '/admin/baz/bar/biff/foo', pattern.stringify - _: 'baz/bar/biff' - - pattern = new UrlPattern '/v:major.:minor/*' - t.equal '/v1.2/resource/', pattern.stringify - _: 'resource/' - major: '1' - minor: '2' - - pattern = new UrlPattern '/v:v.:v/*' - t.equal '/v1.2/resource/', pattern.stringify - _: 'resource/' - v: ['1', '2'] - - pattern = new UrlPattern '/:foo_bar' - t.equal '/a_bar', pattern.stringify - foo: 'a' - t.equal '/a__bar', pattern.stringify - foo: 'a_' - t.equal '/a-b-c-d__bar', pattern.stringify - foo: 'a-b-c-d_' - t.equal '/a b%c-d__bar', pattern.stringify - foo: 'a b%c-d_' - - pattern = new UrlPattern '((((a)b)c)d)' - t.equal '', pattern.stringify() - - pattern = new UrlPattern '(:a-)1-:b(-2-:c-3-:d(-4-*-:a))' - t.equal '1-B', pattern.stringify - b: 'B' - t.equal 'A-1-B', pattern.stringify - a: 'A' - b: 'B' - t.equal 'A-1-B', pattern.stringify - a: 'A' - b: 'B' - t.equal 'A-1-B-2-C-3-D', pattern.stringify - a: 'A' - b: 'B' - c: 'C' - d: 'D' - t.equal 'A-1-B-2-C-3-D-4-E-F', pattern.stringify - a: ['A', 'F'] - b: 'B' - c: 'C' - d: 'D' - _: 'E' - - pattern = new UrlPattern '/user/:range' - t.equal '/user/10-20', pattern.stringify - range: '10-20' - - t.end() - -test 'stringify errors', (t) -> - t.plan 5 - - pattern = new UrlPattern '(:a-)1-:b(-2-:c-3-:d(-4-*-:a))' - - try - pattern.stringify() - catch e - t.equal e.message, "no values provided for key `b`" - try - pattern.stringify - a: 'A' - b: 'B' - c: 'C' - catch e - t.equal e.message, "no values provided for key `d`" - try - pattern.stringify - a: 'A' - b: 'B' - d: 'D' - catch e - t.equal e.message, "no values provided for key `c`" - try - pattern.stringify - a: 'A' - b: 'B' - c: 'C' - d: 'D' - _: 'E' - catch e - t.equal e.message, "too few values provided for key `a`" - try - pattern.stringify - a: ['A', 'F'] - b: 'B' - c: 'C' - d: 'D' - catch e - t.equal e.message, "no values provided for key `_`" - - t.end() diff --git a/test/stringify-fixtures.js b/test/stringify-fixtures.js new file mode 100644 index 0000000..4e055dd --- /dev/null +++ b/test/stringify-fixtures.js @@ -0,0 +1,218 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const test = require('tape'); +const UrlPattern = require('../lib/url-pattern'); + +test('stringify', function(t) { + let pattern = new UrlPattern('/foo'); + t.equal('/foo', pattern.stringify()); + + pattern = new UrlPattern('/user/:userId/task/:taskId'); + t.equal('/user/10/task/52', pattern.stringify({ + userId: '10', + taskId: '52' + }) + ); + + pattern = new UrlPattern('/user/:userId/task/:taskId'); + t.equal('/user/10/task/52', pattern.stringify({ + userId: '10', + taskId: '52', + ignored: 'ignored' + }) + ); + + pattern = new UrlPattern('.user.:userId.task.:taskId'); + t.equal('.user.10.task.52', pattern.stringify({ + userId: '10', + taskId: '52' + }) + ); + + pattern = new UrlPattern('*/user/:userId'); + t.equal('/school/10/user/10', pattern.stringify({ + _: '/school/10', + userId: '10' + }) + ); + + pattern = new UrlPattern('*-user-:userId'); + t.equal('-school-10-user-10', pattern.stringify({ + _: '-school-10', + userId: '10' + }) + ); + + pattern = new UrlPattern('/admin*'); + t.equal('/admin/school/10/user/10', pattern.stringify({ + _: '/school/10/user/10'}) + ); + + pattern = new UrlPattern('/admin/*/user/*/tail'); + t.equal('/admin/school/10/user/10/12/tail', pattern.stringify({ + _: ['school/10', '10/12']})); + + pattern = new UrlPattern('/admin/*/user/:id/*/tail'); + t.equal('/admin/school/10/user/10/12/13/tail', pattern.stringify({ + _: ['school/10', '12/13'], + id: '10' + }) + ); + + pattern = new UrlPattern('/*/admin(/:path)'); + t.equal('/foo/admin/baz', pattern.stringify({ + _: 'foo', + path: 'baz' + }) + ); + t.equal('/foo/admin', pattern.stringify({ + _: 'foo'}) + ); + + pattern = new UrlPattern('(/)'); + t.equal('', pattern.stringify()); + + pattern = new UrlPattern('/admin(/foo)/bar'); + t.equal('/admin/bar', pattern.stringify()); + + pattern = new UrlPattern('/admin(/:foo)/bar'); + t.equal('/admin/bar', pattern.stringify()); + t.equal('/admin/baz/bar', pattern.stringify({ + foo: 'baz'}) + ); + + pattern = new UrlPattern('/admin/(*/)foo'); + t.equal('/admin/foo', pattern.stringify()); + t.equal('/admin/baz/bar/biff/foo', pattern.stringify({ + _: 'baz/bar/biff'}) + ); + + pattern = new UrlPattern('/v:major.:minor/*'); + t.equal('/v1.2/resource/', pattern.stringify({ + _: 'resource/', + major: '1', + minor: '2' + }) + ); + + pattern = new UrlPattern('/v:v.:v/*'); + t.equal('/v1.2/resource/', pattern.stringify({ + _: 'resource/', + v: ['1', '2']})); + + pattern = new UrlPattern('/:foo_bar'); + t.equal('/a_bar', pattern.stringify({ + foo: 'a'}) + ); + t.equal('/a__bar', pattern.stringify({ + foo: 'a_'}) + ); + t.equal('/a-b-c-d__bar', pattern.stringify({ + foo: 'a-b-c-d_'}) + ); + t.equal('/a b%c-d__bar', pattern.stringify({ + foo: 'a b%c-d_'}) + ); + + pattern = new UrlPattern('((((a)b)c)d)'); + t.equal('', pattern.stringify()); + + pattern = new UrlPattern('(:a-)1-:b(-2-:c-3-:d(-4-*-:a))'); + t.equal('1-B', pattern.stringify({ + b: 'B'}) + ); + t.equal('A-1-B', pattern.stringify({ + a: 'A', + b: 'B' + }) + ); + t.equal('A-1-B', pattern.stringify({ + a: 'A', + b: 'B' + }) + ); + t.equal('A-1-B-2-C-3-D', pattern.stringify({ + a: 'A', + b: 'B', + c: 'C', + d: 'D' + }) + ); + t.equal('A-1-B-2-C-3-D-4-E-F', pattern.stringify({ + a: ['A', 'F'], + b: 'B', + c: 'C', + d: 'D', + _: 'E' + }) + ); + + pattern = new UrlPattern('/user/:range'); + t.equal('/user/10-20', pattern.stringify({ + range: '10-20'}) + ); + + return t.end(); +}); + +test('stringify errors', function(t) { + let e; + t.plan(5); + + const pattern = new UrlPattern('(:a-)1-:b(-2-:c-3-:d(-4-*-:a))'); + + try { + pattern.stringify(); + } catch (error) { + e = error; + t.equal(e.message, "no values provided for key `b`"); + } + try { + pattern.stringify({ + a: 'A', + b: 'B', + c: 'C' + }); + } catch (error1) { + e = error1; + t.equal(e.message, "no values provided for key `d`"); + } + try { + pattern.stringify({ + a: 'A', + b: 'B', + d: 'D' + }); + } catch (error2) { + e = error2; + t.equal(e.message, "no values provided for key `c`"); + } + try { + pattern.stringify({ + a: 'A', + b: 'B', + c: 'C', + d: 'D', + _: 'E' + }); + } catch (error3) { + e = error3; + t.equal(e.message, "too few values provided for key `a`"); + } + try { + pattern.stringify({ + a: ['A', 'F'], + b: 'B', + c: 'C', + d: 'D' + }); + } catch (error4) { + e = error4; + t.equal(e.message, "no values provided for key `_`"); + } + + return t.end(); +}); From d06e1d146d6507bddb1368532a2a9b6dda3fb83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:07:17 -0500 Subject: [PATCH 013/117] index.ts: export things, fix errors, fix parser returns --- index.ts | 145 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/index.ts b/index.ts index 9e5772c..85fe16a 100644 --- a/index.ts +++ b/index.ts @@ -10,7 +10,7 @@ interface IUrlPatternOptions { wildcardChar?: string; } -const defaultOptions: IUrlPatternOptions = { +export const defaultOptions: IUrlPatternOptions = { escapeChar: "\\", optionalSegmentEndChar: ")", optionalSegmentStartChar: "(", @@ -24,11 +24,11 @@ const defaultOptions: IUrlPatternOptions = { // escapes a string for insertion into a regular expression // source: http://stackoverflow.com/a/3561711 -function escapeStringForRegex(str: string): string { +export function escapeStringForRegex(str: string): string { return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); } -function concatMap(array: T[], f: (x: T) => T[]): T[] { +export function concatMap(array: T[], f: (x: T) => T[]): T[] { let results: T[] = []; array.forEach((value) => { results = results.concat(f(value)); @@ -36,7 +36,7 @@ function concatMap(array: T[], f: (x: T) => T[]): T[] { return results; } -function stringConcatMap(array: T[], f: (x: T) => string): string { +export function stringConcatMap(array: T[], f: (x: T) => string): string { let result = ""; array.forEach((value) => { result += f(value); @@ -48,15 +48,15 @@ function stringConcatMap(array: T[], f: (x: T) => string): string { * returns the number of groups in the `regex`. * source: http://stackoverflow.com/a/16047223 */ -function regexGroupCount(regex: RegExp): number { +export function regexGroupCount(regex: RegExp): number { return new RegExp(regex.toString() + "|").exec("").length - 1; } // zips an array of `keys` and an array of `values` into an object. // `keys` and `values` must have the same length. // if the same key appears multiple times the associated values are collected in an array. -function keysAndValuesToObject(keys: any[], values: any[]): object { - const result = {}; +export function keysAndValuesToObject(keys: any[], values: any[]): object { + const result: object = {}; if (keys.length !== values.length) { throw Error("keys.length must equal values.length"); @@ -113,7 +113,7 @@ class Tagged { * a parser is a function that takes a string and returns a `Result` * containing a parsed `Result.value` and the rest of the string `Result.rest` */ -type Parser = (str: string) => Result | null; +type Parser = (str: string) => Result | undefined; // parser combinators let P = { @@ -124,7 +124,7 @@ let P = { return (input: string) => { const result = parser(input); if (result == null) { - return null; + return; } const tagged = new Tagged(tag, result.value); return new Result(tagged, result.rest); @@ -135,7 +135,7 @@ let P = { return (input: string) => { const matches = regex.exec(input); if (matches == null) { - return null; + return; } const result = matches[0]; return new Result(result, input.slice(result.length)); @@ -150,7 +150,7 @@ let P = { parsers.forEach((parser: Parser) => { const result = parser(rest); if (result == null) { - return null; + return; } values.push(result.value); rest = result.rest; @@ -174,7 +174,7 @@ let P = { return (input: string) => { const result = parser(input); if (result == null) { - return null; + return; } return new Result(result.value[index], result.rest); }; @@ -201,7 +201,7 @@ let P = { endParser: Parser | null, isAtLeastOneResultRequired: boolean, input: string, - ): Result | null { + ): Result | undefined { let rest = input; const results: T[] = []; while (true) { @@ -220,7 +220,7 @@ let P = { } if (isAtLeastOneResultRequired && results.length === 0) { - return null; + return; } return new Result(results, rest); @@ -237,7 +237,7 @@ let P = { const isAtLeastOneResultRequired = true; const result = P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); if (result == null) { - return null; + return; } return new Result(result.value.join(""), result.rest); }; @@ -246,13 +246,13 @@ let P = { // parser that consumes the input. firstChoice(...parsers: Array>): Parser { return (input: string) => { - parsers.forEach((parser) => { + for (const parser of parsers) { const result = parser(input); if (result != null) { return result; } - }); - return null; + } + return; }; }, }; @@ -270,7 +270,7 @@ interface IUrlPatternParser { wildcard: Parser>; } -function newUrlPatternParser(options: IUrlPatternOptions): IUrlPatternParser { +export function newUrlPatternParser(options: IUrlPatternOptions): IUrlPatternParser { const U: IUrlPatternParser = { escapedChar: P.pick(1, P.string(options.escapeChar), P.regex(/^./)), name: P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)), @@ -297,52 +297,53 @@ function newUrlPatternParser(options: IUrlPatternOptions): IUrlPatternParser { // functions that further process ASTs returned as `.value` in parser results -function baseAstNodeToRegexString(astNode: Tagged, segmentValueCharset: string): string { +function baseAstNodeToRegexString(astNode: Tagged, segmentValueCharset: string): string { if (Array.isArray(astNode)) { - return stringConcatMap(astNode, node => baseAstNodeToRegexString(node, segmentValueCharset)); + return stringConcatMap(astNode, (node) => baseAstNodeToRegexString(node, segmentValueCharset)); } switch (astNode.tag) { - case 'wildcard': - return '(.*?)'; - case 'named': + case "wildcard": + return "(.*?)"; + case "named": return `([${ segmentValueCharset }]+)`; - case 'static': + case "static": return escapeStringForRegex(astNode.value); - case 'optional': + case "optional": return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; } -}; +} -function astNodeToRegexString(astNode, segmentValueCharset) { +function astNodeToRegexString(astNode: Tagged, segmentValueCharset?: string) { if (segmentValueCharset == null) { ({ segmentValueCharset } = defaultOptions); } return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; } -function astNodeToNames(astNode) { +function astNodeToNames(astNode: Tagged | Array>): string[] { if (Array.isArray(astNode)) { return concatMap(astNode, astNodeToNames); } switch (astNode.tag) { - case 'wildcard': - return ['_']; - case 'named': + case "wildcard": + return ["_"]; + case "named": return [astNode.value]; - case 'static': + case "static": return []; - case 'optional': + case "optional": return astNodeToNames(astNode.value); } } -function getParam(params, key, nextIndexes, sideEffects) { +// TODO better name +export function getParam(params, key, nextIndexes, sideEffects) { if (sideEffects == null) { sideEffects = false; } - let value = params[key]; + const value = params[key]; if (value == null) { if (sideEffects) { throw new Error(`no values provided for key \`${ key }\``); @@ -372,7 +373,7 @@ function getParam(params, key, nextIndexes, sideEffects) { function astNodeContainsSegmentsForProvidedParams(astNode, params, nextIndexes) { if (Array.isArray(astNode)) { let i = -1; - let { length } = astNode; + const { length } = astNode; while (++i < length) { if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { return true; @@ -382,48 +383,48 @@ function astNodeContainsSegmentsForProvidedParams(astNode, params, nextIndexes) } switch (astNode.tag) { - case 'wildcard': - return getParam(params, '_', nextIndexes, false) != null; - case 'named': + case "wildcard": + return getParam(params, "_", nextIndexes, false) != null; + case "named": return getParam(params, astNode.value, nextIndexes, false) != null; - case 'static': + case "static": return false; - case 'optional': + case "optional": return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); } } -function stringify(astNode, params, nextIndexes) { +function stringify(astNode: Tagged, params, nextIndexes): string { if (Array.isArray(astNode)) { return stringConcatMap(astNode, node => stringify(node, params, nextIndexes)); } switch (astNode.tag) { - case 'wildcard': - return getParam(params, '_', nextIndexes, true); - case 'named': + case "wildcard": + return getParam(params, "_", nextIndexes, true); + case "named": return getParam(params, astNode.value, nextIndexes, true); - case 'static': + case "static": return astNode.value; - case 'optional': + case "optional": if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { return stringify(astNode.value, params, nextIndexes); } else { - return ''; + return "" } } -}; +} -class UrlPattern { - readonly isRegex: boolean; - readonly regex: RegExp; - readonly ast: Object; - readonly names: Array; +export class UrlPattern { + public readonly isRegex: boolean; + public readonly regex: RegExp; + public readonly ast: Tagged; + public readonly names: string[]; - constructor(pattern: string, options?: UrlPatternOptions); - constructor(pattern: RegExp, groupNames?: Array); + constructor(pattern: string, options?: IUrlPatternOptions); + constructor(pattern: RegExp, groupNames?: string[]); - constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: UrlPatternOptions | Array) { + constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: IUrlPatternOptions | string[]) { // self awareness if (pattern instanceof UrlPattern) { this.isRegex = pattern.isRegex; @@ -444,9 +445,10 @@ class UrlPattern { this.regex = pattern; if (optionsOrGroupNames != null) { if (!Array.isArray(optionsOrGroupNames)) { - throw new TypeError("if first argument is a RegExp the second argument may be an Array of group names but you provided something else"); + throw new TypeError( + "if first argument is a RegExp the second argument may be an Array of group names but you provided something else"); } - let groupCount = regexGroupCount(this.regex); + const groupCount = regexGroupCount(this.regex); if (optionsOrGroupNames.length !== groupCount) { throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ optionsOrGroupNames.length }`); } @@ -457,10 +459,10 @@ class UrlPattern { // everything following only concerns string patterns - if (pattern === '') { - throw new Error('first argument must not be the empty string'); + if (pattern === "") { + throw new Error("first argument must not be the empty string"); } - let patternWithoutWhitespace = pattern.replace(/\s+/g, ""); + const patternWithoutWhitespace = pattern.replace(/\s+/g, ""); if (patternWithoutWhitespace !== pattern) { throw new Error("first argument must not contain whitespace"); } @@ -479,13 +481,13 @@ class UrlPattern { wildcardChar: (optionsOrGroupNames != null ? optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar }; - let parser: UrlPatternParser = newUrlPatternParser(options); - let parsed = parser.pattern(pattern); + const parser: IUrlPatternParser = newUrlPatternParser(options); + const parsed = parser.pattern(pattern); if (parsed == null) { // TODO better error message throw new Error("couldn't parse pattern"); } - if (parsed.rest !== '') { + if (parsed.rest !== "") { // TODO better error message throw new Error("could only partially parse pattern"); } @@ -493,16 +495,15 @@ class UrlPattern { this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); this.names = astNodeToNames(this.ast); - } - match(url: string): Object | null { - let match = this.regex.exec(url); + public match(url: string): object | undefined { + const match = this.regex.exec(url); if (match == null) { - return null; + return; } - let groups = match.slice(1); + const groups = match.slice(1); if (this.names) { return keysAndValuesToObject(this.names, groups); } else { @@ -510,7 +511,7 @@ class UrlPattern { } } - stringify(params?: Object): string { + public stringify(params?: object): string { if (params == null) { params = {}; } From d67db52d5c4bc484282399bea8425ee469bb614e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:10:54 -0500 Subject: [PATCH 014/117] index.ts: decrease line size --- index.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index 85fe16a..b4c9144 100644 --- a/index.ts +++ b/index.ts @@ -471,14 +471,21 @@ export class UrlPattern { throw new Error("if first argument is a string second argument must be an options object or undefined"); } - let options: UrlPatternOptions = { - escapeChar: (typeof optionsOrGroupNames != null ? optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, - segmentNameStartChar: (optionsOrGroupNames != null ? optionsOrGroupNames.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, - segmentNameCharset: (optionsOrGroupNames != null ? optionsOrGroupNames.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, - segmentValueCharset: (optionsOrGroupNames != null ? optionsOrGroupNames.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, - optionalSegmentStartChar: (optionsOrGroupNames != null ? optionsOrGroupNames.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, - optionalSegmentEndChar: (optionsOrGroupNames != null ? optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, - wildcardChar: (optionsOrGroupNames != null ? optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar + const options: IUrlPatternOptions = { + escapeChar: (typeof optionsOrGroupNames != null ? + optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, + optionalSegmentEndChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, + optionalSegmentStartChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, + segmentNameCharset: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, + segmentNameStartChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, + segmentValueCharset: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, + wildcardChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar, }; const parser: IUrlPatternParser = newUrlPatternParser(options); From 39b935016c1e22f82ebf45ead7370e431fc9e8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:11:08 -0500 Subject: [PATCH 015/117] index.ts: dont put everything on UrlPattern and export that --- index.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/index.ts b/index.ts index b4c9144..99f86a7 100644 --- a/index.ts +++ b/index.ts @@ -530,26 +530,4 @@ export class UrlPattern { } return stringify(this.ast, params, {}); } - - // make helpers available directly on UrlPattern - static escapeStringForRegex = escapeStringForRegex; - static concatMap = concatMap; - static stringConcatMap = stringConcatMap; - static regexGroupCount = regexGroupCount; - static keysAndValuesToObject = keysAndValuesToObject; - - // make AST helpers available directly on UrlPattern - static astNodeToRegexString = astNodeToRegexString; - static astNodeToNames = astNodeToNames; - static getParam = getParam; - static astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams; - static stringify = stringify; - - // make parsers available directly on UrlPattern - static P = P; - static newUrlPatternParser = newUrlPatternParser; - static defaultOptions = defaultOptions; } - -// export only the UrlPattern class -export = UrlPattern; From c1ba51853905bf242290952efe7d1d96df610449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:18:58 -0500 Subject: [PATCH 016/117] index.ts: get rid of forEach --- index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 99f86a7..f0a0a94 100644 --- a/index.ts +++ b/index.ts @@ -30,17 +30,17 @@ export function escapeStringForRegex(str: string): string { export function concatMap(array: T[], f: (x: T) => T[]): T[] { let results: T[] = []; - array.forEach((value) => { + for (const value of array) { results = results.concat(f(value)); - }); + } return results; } export function stringConcatMap(array: T[], f: (x: T) => string): string { let result = ""; - array.forEach((value) => { + for (const value of array) { result += f(value); - }); + } return result; } @@ -147,14 +147,14 @@ let P = { return (input: string) => { let rest = input; const values: any[] = []; - parsers.forEach((parser: Parser) => { + for (const parser of parsers) { const result = parser(rest); if (result == null) { return; } values.push(result.value); rest = result.rest; - }); + } return new Result(values, rest); }; }, From e4e45ce3435c3ac97d6e6b121e6dd6e425f6d574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:20:16 -0500 Subject: [PATCH 017/117] make test/helpers.js pass --- test/helpers.js | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/test/helpers.js b/test/helpers.js index 16587cd..d5a9b39 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -5,29 +5,29 @@ */ const test = require('tape'); const { - escapeForRegex, + escapeStringForRegex, concatMap, stringConcatMap, regexGroupCount, keysAndValuesToObject -} = require('../lib/url-pattern'); +} = require('../index.js'); -test('escapeForRegex', function(t) { +test('escapeStringForRegex', function(t) { const expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]'; - const actual = escapeForRegex('[-\/\\^$*+?.()|[\]{}]'); + const actual = escapeStringForRegex('[-\/\\^$*+?.()|[\]{}]'); t.equal(expected, actual); - t.equal(escapeForRegex('a$98kdjf(kdj)'), 'a\\$98kdjf\\(kdj\\)'); - t.equal('a', escapeForRegex('a')); - t.equal('!', escapeForRegex('!')); - t.equal('\\.', escapeForRegex('.')); - t.equal('\\/', escapeForRegex('/')); - t.equal('\\-', escapeForRegex('-')); - t.equal('\\-', escapeForRegex('-')); - t.equal('\\[', escapeForRegex('[')); - t.equal('\\]', escapeForRegex(']')); - t.equal('\\(', escapeForRegex('(')); - t.equal('\\)', escapeForRegex(')')); + t.equal(escapeStringForRegex('a$98kdjf(kdj)'), 'a\\$98kdjf\\(kdj\\)'); + t.equal('a', escapeStringForRegex('a')); + t.equal('!', escapeStringForRegex('!')); + t.equal('\\.', escapeStringForRegex('.')); + t.equal('\\/', escapeStringForRegex('/')); + t.equal('\\-', escapeStringForRegex('-')); + t.equal('\\-', escapeStringForRegex('-')); + t.equal('\\[', escapeStringForRegex('[')); + t.equal('\\]', escapeStringForRegex(']')); + t.equal('\\(', escapeStringForRegex('(')); + t.equal('\\)', escapeStringForRegex(')')); return t.end(); }); @@ -74,15 +74,6 @@ test('keysAndValuesToObject', function(t) { one: 1 } ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two'], - [1] - ), - { - one: 1 - } - ); t.deepEqual( keysAndValuesToObject( ['one', 'two', 'two'], From ebe45cf10eceb143a306e631634f7f695707e8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:20:35 -0500 Subject: [PATCH 018/117] make test/parser.js pass --- test/parser.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/parser.js b/test/parser.js index 1b61a6f..4aa9bd5 100644 --- a/test/parser.js +++ b/test/parser.js @@ -8,8 +8,11 @@ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); -const U = UrlPattern.newParser(UrlPattern.defaultOptions); +const { + newUrlPatternParser, + defaultOptions, +} = require('../index.js'); +const U = newUrlPatternParser(defaultOptions); const parse = U.pattern; test('wildcard', function(t) { From 69253a250073d08151c2744a62e38f1ca1a7e703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:22:12 -0500 Subject: [PATCH 019/117] make test/ast.js pass --- index.ts | 4 ++-- test/ast.js | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index f0a0a94..9a30868 100644 --- a/index.ts +++ b/index.ts @@ -314,14 +314,14 @@ function baseAstNodeToRegexString(astNode: Tagged, segmentValueCharset: str } } -function astNodeToRegexString(astNode: Tagged, segmentValueCharset?: string) { +export function astNodeToRegexString(astNode: Tagged, segmentValueCharset?: string) { if (segmentValueCharset == null) { ({ segmentValueCharset } = defaultOptions); } return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; } -function astNodeToNames(astNode: Tagged | Array>): string[] { +export function astNodeToNames(astNode: Tagged | Array>): string[] { if (Array.isArray(astNode)) { return concatMap(astNode, astNodeToNames); } diff --git a/test/ast.js b/test/ast.js index 28f1e12..71097a5 100644 --- a/test/ast.js +++ b/test/ast.js @@ -4,15 +4,16 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); - const { + UrlPattern, + newUrlPatternParser, + defaultOptions, + getParam, astNodeToRegexString, - astNodeToNames, - getParam -} = UrlPattern; + astNodeToNames +} = require('../index.js'); -const parse = UrlPattern.newParser(UrlPattern.defaultOptions).pattern; +const parse = newUrlPatternParser(defaultOptions).pattern; test('astNodeToRegexString and astNodeToNames', function(t) { t.test('just static alphanumeric', function(t) { From b619202a461176617926fe7583445265da7e61c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:23:14 -0500 Subject: [PATCH 020/117] tslint.json: allow 5 classes per file --- tslint.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tslint.json b/tslint.json index 32fa6e5..9740cb3 100644 --- a/tslint.json +++ b/tslint.json @@ -4,6 +4,8 @@ "tslint:recommended" ], "jsRules": {}, - "rules": {}, + "rules": { + "max-classes-per-file": [true, 5] + }, "rulesDirectory": [] -} \ No newline at end of file +} From e60d6a9a3ec03ab57bee7be10b1d4ee470be45ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:26:32 -0500 Subject: [PATCH 021/117] index.ts: fix escapeChar options assignment --- index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 9a30868..5e0f4e1 100644 --- a/index.ts +++ b/index.ts @@ -472,7 +472,7 @@ export class UrlPattern { } const options: IUrlPatternOptions = { - escapeChar: (typeof optionsOrGroupNames != null ? + escapeChar: (optionsOrGroupNames != null ? optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, optionalSegmentEndChar: (optionsOrGroupNames != null ? optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, From 2fdac95963f401e0ae1670c8f50a1b2f418f406a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:27:28 -0500 Subject: [PATCH 022/117] make test/readme.js pass --- test/readme.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/readme.js b/test/readme.js index e976ef5..d2745b4 100644 --- a/test/readme.js +++ b/test/readme.js @@ -4,7 +4,9 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); +const { + UrlPattern +} = require('../index.js'); test('simple', function(t) { const pattern = new UrlPattern('/api/users/:id'); From d1b7354853aa77ad2bc8a556e9b7cfebebe91e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:32:10 -0500 Subject: [PATCH 023/117] make test/errors.js pass --- test/errors.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/errors.js b/test/errors.js index 98817ce..f01d733 100644 --- a/test/errors.js +++ b/test/errors.js @@ -4,7 +4,9 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); +const { + UrlPattern +} = require('../index.js'); test('invalid argument', function(t) { let e; @@ -14,31 +16,31 @@ test('invalid argument', function(t) { new UrlPattern(); } catch (error) { e = error; - t.equal(e.message, "argument must be a regex or a string"); + t.equal(e.message, "first argument must be a RegExp, a string or an instance of UrlPattern"); } try { new UrlPattern(5); } catch (error1) { e = error1; - t.equal(e.message, "argument must be a regex or a string"); + t.equal(e.message, "first argument must be a RegExp, a string or an instance of UrlPattern"); } try { new UrlPattern(''); } catch (error2) { e = error2; - t.equal(e.message, "argument must not be the empty string"); + t.equal(e.message, "first argument must not be the empty string"); } try { new UrlPattern(' '); } catch (error3) { e = error3; - t.equal(e.message, "argument must not contain whitespace"); + t.equal(e.message, "first argument must not contain whitespace"); } try { new UrlPattern(' fo o'); } catch (error4) { e = error4; - t.equal(e.message, "argument must not contain whitespace"); + t.equal(e.message, "first argument must not contain whitespace"); } return t.end(); }); @@ -116,7 +118,7 @@ test('regex names', function(t) { new UrlPattern(/x/, 5); } catch (error) { e = error; - t.equal(e.message, 'if first argument is a regex the second argument may be an array of group names but you provided something else'); + t.equal(e.message, 'if first argument is a RegExp the second argument may be an Array of group names but you provided something else'); } try { new UrlPattern(/(((foo)bar(boo))far)/, []); From d470b0cd3cbb4d642c8e8bcf2758666daf929674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:36:33 -0500 Subject: [PATCH 024/117] make test/match-fixtures.js pass --- test/match-fixtures.js | 54 ++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/test/match-fixtures.js b/test/match-fixtures.js index c579683..2986554 100644 --- a/test/match-fixtures.js +++ b/test/match-fixtures.js @@ -4,7 +4,9 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); +const { + UrlPattern +} = require('../index.js'); test('match', function(t) { let pattern = new UrlPattern('/foo'); @@ -14,16 +16,16 @@ test('match', function(t) { t.deepEqual(pattern.match('.foo'), {}); pattern = new UrlPattern('/foo'); - t.equals(pattern.match('/foobar'), null); + t.equals(pattern.match('/foobar'), undefined); pattern = new UrlPattern('.foo'); - t.equals(pattern.match('.foobar'), null); + t.equals(pattern.match('.foobar'), undefined); pattern = new UrlPattern('/foo'); - t.equals(pattern.match('/bar/foo'), null); + t.equals(pattern.match('/bar/foo'), undefined); pattern = new UrlPattern('.foo'); - t.equals(pattern.match('.bar.foo'), null); + t.equals(pattern.match('.bar.foo'), undefined); pattern = new UrlPattern(/foo/); t.deepEqual(pattern.match('foo'), []); @@ -146,7 +148,7 @@ test('match', function(t) { }); pattern = new UrlPattern('/:foo_bar'); - t.equal(pattern.match('/_bar'), null); + t.equal(pattern.match('/_bar'), undefined); t.deepEqual(pattern.match('/a_bar'), {foo: 'a'}); t.deepEqual(pattern.match('/a__bar'), @@ -158,9 +160,9 @@ test('match', function(t) { pattern = new UrlPattern('((((a)b)c)d)'); t.deepEqual(pattern.match(''), {}); - t.equal(pattern.match('a'), null); - t.equal(pattern.match('ab'), null); - t.equal(pattern.match('abc'), null); + t.equal(pattern.match('a'), undefined); + t.equal(pattern.match('ab'), undefined); + t.equal(pattern.match('abc'), undefined); t.deepEqual(pattern.match('abcd'), {}); t.deepEqual(pattern.match('bcd'), {}); t.deepEqual(pattern.match('cd'), {}); @@ -183,13 +185,13 @@ test('match', function(t) { {range: '10%20'}); pattern = new UrlPattern('/vvv:version/*'); - t.equal(null, pattern.match('/vvv/resource')); + t.equal(undefined, pattern.match('/vvv/resource')); t.deepEqual(pattern.match('/vvv1/resource'), { _: 'resource', version: '1' } ); - t.equal(null, pattern.match('/vvv1.1/resource')); + t.equal(undefined, pattern.match('/vvv1.1/resource')); pattern = new UrlPattern('/api/users/:id', {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); @@ -262,31 +264,31 @@ test('match', function(t) { let regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; pattern = new UrlPattern(regex); - t.equal(null, pattern.match('10.10.10.10')); - t.equal(null, pattern.match('ip/10.10.10.10')); - t.equal(null, pattern.match('/ip/10.10.10.')); - t.equal(null, pattern.match('/ip/10.')); - t.equal(null, pattern.match('/ip/')); + t.equal(undefined, pattern.match('10.10.10.10')); + t.equal(undefined, pattern.match('ip/10.10.10.10')); + t.equal(undefined, pattern.match('/ip/10.10.10.')); + t.equal(undefined, pattern.match('/ip/10.')); + t.equal(undefined, pattern.match('/ip/')); t.deepEqual(pattern.match('/ip/10.10.10.10'), ['10', '10', '10', '10']); t.deepEqual(pattern.match('/ip/127.0.0.1'), ['127', '0', '0', '1']); regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; pattern = new UrlPattern(regex); - t.equal(null, pattern.match('10.10.10.10')); - t.equal(null, pattern.match('ip/10.10.10.10')); - t.equal(null, pattern.match('/ip/10.10.10.')); - t.equal(null, pattern.match('/ip/10.')); - t.equal(null, pattern.match('/ip/')); + t.equal(undefined, pattern.match('10.10.10.10')); + t.equal(undefined, pattern.match('ip/10.10.10.10')); + t.equal(undefined, pattern.match('/ip/10.10.10.')); + t.equal(undefined, pattern.match('/ip/10.')); + t.equal(undefined, pattern.match('/ip/')); t.deepEqual(pattern.match('/ip/10.10.10.10'), ['10.10.10.10']); t.deepEqual(pattern.match('/ip/127.0.0.1'), ['127.0.0.1']); regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; pattern = new UrlPattern(regex, ['ip']); - t.equal(null, pattern.match('10.10.10.10')); - t.equal(null, pattern.match('ip/10.10.10.10')); - t.equal(null, pattern.match('/ip/10.10.10.')); - t.equal(null, pattern.match('/ip/10.')); - t.equal(null, pattern.match('/ip/')); + t.equal(undefined, pattern.match('10.10.10.10')); + t.equal(undefined, pattern.match('ip/10.10.10.10')); + t.equal(undefined, pattern.match('/ip/10.10.10.')); + t.equal(undefined, pattern.match('/ip/10.')); + t.equal(undefined, pattern.match('/ip/')); t.deepEqual(pattern.match('/ip/10.10.10.10'), {ip: '10.10.10.10'}); t.deepEqual(pattern.match('/ip/127.0.0.1'), From 2fb3327168ba4ec07cab348d78f44bc750bf06c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:37:00 -0500 Subject: [PATCH 025/117] changelog for 0.11 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ce5cd..ac817b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,3 +82,10 @@ non breaking non breaking messages on errors thrown on invalid patterns have changed slightly. + +#### 0.11 + +- UrlPattern now uses typescript instead of coffeescript +- renamed `UrlPattern.newParser` to `UrlPattern.newUrlPatternParser` +- renamed `UrlPattern.escapeStringForRegex` to `UrlPattern.escapeStringForRegex` +- `UrlPattern.match` now returns `undefined` (previously `null`) if there's no match From d70210b2806dae5d6f146b3900f4fd95c9273074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:38:52 -0500 Subject: [PATCH 026/117] make test/misc.js pass --- test/misc.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/test/misc.js b/test/misc.js index f395734..93cf13b 100644 --- a/test/misc.js +++ b/test/misc.js @@ -4,17 +4,19 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); +const { + UrlPattern +} = require('../index.js'); test('instance of UrlPattern is handled correctly as constructor argument', function(t) { - const pattern = new UrlPattern('/user/:userId/task/:taskId'); - const copy = new UrlPattern(pattern); - t.deepEqual(copy.match('/user/10/task/52'), { - userId: '10', - taskId: '52' - } - ); - return t.end(); + const pattern = new UrlPattern('/user/:userId/task/:taskId'); + const copy = new UrlPattern(pattern); + t.deepEqual(copy.match('/user/10/task/52'), { + userId: '10', + taskId: '52' + } + ); + t.end(); }); test('match full stops in segment values', function(t) { @@ -23,20 +25,20 @@ test('match full stops in segment values', function(t) { const pattern = new UrlPattern('/api/v1/user/:id/', options); t.deepEqual(pattern.match('/api/v1/user/test.name/'), {id: 'test.name'}); - return t.end(); + t.end(); }); test('regex group names', function(t) { const pattern = new UrlPattern(/^\/api\/([a-zA-Z0-9-_~ %]+)(?:\/(\d+))?$/, ['resource', 'id']); t.deepEqual(pattern.match('/api/users'), {resource: 'users'}); - t.equal(pattern.match('/apiii/users'), null); - t.deepEqual(pattern.match('/api/users/foo'), null); + t.equal(pattern.match('/apiii/users'), undefined); + t.deepEqual(pattern.match('/api/users/foo'), undefined); t.deepEqual(pattern.match('/api/users/10'), { resource: 'users', id: '10' } ); - t.deepEqual(pattern.match('/api/projects/10/'), null); - return t.end(); + t.deepEqual(pattern.match('/api/projects/10/'), undefined); + t.end(); }); From 26b58a60d2c75323273b65c4b568b81c96b1f2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:40:57 -0500 Subject: [PATCH 027/117] make test/readme.js pass --- test/readme.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/readme.js b/test/readme.js index d2745b4..3504a7a 100644 --- a/test/readme.js +++ b/test/readme.js @@ -11,7 +11,7 @@ const { test('simple', function(t) { const pattern = new UrlPattern('/api/users/:id'); t.deepEqual(pattern.match('/api/users/10'), {id: '10'}); - t.equal(pattern.match('/api/products/5'), null); + t.equal(pattern.match('/api/products/5'), undefined); return t.end(); }); @@ -19,7 +19,7 @@ test('api versioning', function(t) { const pattern = new UrlPattern('/v:major(.:minor)/*'); t.deepEqual(pattern.match('/v1.2/'), {major: '1', minor: '2', _: ''}); t.deepEqual(pattern.match('/v2/users'), {major: '2', _: 'users'}); - t.equal(pattern.match('/v/'), null); + t.equal(pattern.match('/v/'), undefined); return t.end(); }); @@ -51,7 +51,7 @@ test('domain', function(t) { _: 'mail' } ); - t.equal(pattern.match('google'), null); + t.equal(pattern.match('google'), undefined); t.deepEqual(pattern.match('www.google.com'), { subdomain: 'www', @@ -59,7 +59,7 @@ test('domain', function(t) { tld: 'com' } ); - t.equal(pattern.match('httpp://mail.google.com/mail'), null); + t.equal(pattern.match('httpp://mail.google.com/mail'), undefined); t.deepEqual(pattern.match('google.de/search'), { domain: 'google', tld: 'de', @@ -79,7 +79,7 @@ test('named segment occurs more than once', function(t) { test('regex', function(t) { const pattern = new UrlPattern(/^\/api\/(.*)$/); t.deepEqual(pattern.match('/api/users'), ['users']); - t.equal(pattern.match('/apiii/users'), null); + t.equal(pattern.match('/apiii/users'), undefined); return t.end(); }); @@ -87,13 +87,13 @@ test('regex group names', function(t) { const pattern = new UrlPattern(/^\/api\/([^\/]+)(?:\/(\d+))?$/, ['resource', 'id']); t.deepEqual(pattern.match('/api/users'), {resource: 'users'}); - t.equal(pattern.match('/api/users/'), null); + t.equal(pattern.match('/api/users/'), undefined); t.deepEqual(pattern.match('/api/users/5'), { resource: 'users', id: '5' } ); - t.equal(pattern.match('/api/users/foo'), null); + t.equal(pattern.match('/api/users/foo'), undefined); return t.end(); }); @@ -136,8 +136,8 @@ test('customization', function(t) { _: 'mail' } ); - t.equal(pattern.match('http://mail.this-should-not-match.com/mail'), null); - t.equal(pattern.match('google'), null); + t.equal(pattern.match('http://mail.this-should-not-match.com/mail'), undefined); + t.equal(pattern.match('google'), undefined); t.deepEqual(pattern.match('www.google.com'), { sub_domain: 'www', domain: 'google', @@ -150,7 +150,7 @@ test('customization', function(t) { 'toplevel-domain': 'com' } ); - t.equal(pattern.match('httpp://mail.google.com/mail'), null); + t.equal(pattern.match('httpp://mail.google.com/mail'), undefined); t.deepEqual(pattern.match('google.de/search'), { domain: 'google', 'toplevel-domain': 'de', From 7507eaaec8b52f768a8157b5aa65d6fbecb08f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:41:28 -0500 Subject: [PATCH 028/117] make test/stringify-fixtures.js pass --- test/stringify-fixtures.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/stringify-fixtures.js b/test/stringify-fixtures.js index 4e055dd..90e791c 100644 --- a/test/stringify-fixtures.js +++ b/test/stringify-fixtures.js @@ -4,7 +4,9 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const test = require('tape'); -const UrlPattern = require('../lib/url-pattern'); +const { + UrlPattern +} = require('../index.js'); test('stringify', function(t) { let pattern = new UrlPattern('/foo'); From 8325b315425246a55c5a272e3b41e9c1e60ce1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sat, 27 Apr 2019 23:55:26 -0500 Subject: [PATCH 029/117] update tsconfig.json to make it more strict --- tsconfig.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 4dcc4dd..9736a60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,15 @@ { "compilerOptions": { "module": "commonjs", - "noImplicitAny": true, - "removeComments": false, + "target": "ES3", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": true, "preserveConstEnums": true, - "sourceMap": true + "sourceMap": true, + "allowUnreachableCode": false, + "declaration": true }, "files": [ "index.ts" From 32357c7bdd8ecb991c0025bee61a8d19a220a251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 28 Apr 2019 00:00:32 -0500 Subject: [PATCH 030/117] index.ts: linter and compiler fixes --- index.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index 5e0f4e1..4adf5d8 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ // OPTIONS -interface IUrlPatternOptions { +interface IUrlPatternOptionsInput { escapeChar?: string; segmentNameStartChar?: string; segmentValueCharset?: string; @@ -10,6 +10,16 @@ interface IUrlPatternOptions { wildcardChar?: string; } +interface IUrlPatternOptions { + escapeChar: string; + segmentNameStartChar: string; + segmentValueCharset: string; + segmentNameCharset: string; + optionalSegmentStartChar: string; + optionalSegmentEndChar: string; + wildcardChar: string; +} + export const defaultOptions: IUrlPatternOptions = { escapeChar: "\\", optionalSegmentEndChar: ")", @@ -63,7 +73,6 @@ export function keysAndValuesToObject(keys: any[], values: any[]): object { } let i = -1; - const { length } = keys; while (++i < keys.length) { const key = keys[i]; const value = values[i]; @@ -116,7 +125,7 @@ class Tagged { type Parser = (str: string) => Result | undefined; // parser combinators -let P = { +const P = { Result, Tagged, // transforms a `parser` into a parser that tags its `Result.value` with `tag` @@ -418,13 +427,13 @@ function stringify(astNode: Tagged, params, nextIndexes): string { export class UrlPattern { public readonly isRegex: boolean; public readonly regex: RegExp; - public readonly ast: Tagged; + public readonly ast?: Tagged; public readonly names: string[]; constructor(pattern: string, options?: IUrlPatternOptions); constructor(pattern: RegExp, groupNames?: string[]); - constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: IUrlPatternOptions | string[]) { + constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: IUrlPatternOptionsInput | string[]) { // self awareness if (pattern instanceof UrlPattern) { this.isRegex = pattern.isRegex; From eacad7450af3a3c6dd63cbaa2aebf49ed432181b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 29 Apr 2019 23:57:56 -0500 Subject: [PATCH 031/117] cleanup tests --- test/ast.js | 46 ++++++++++++++++++-------------------- test/errors.js | 26 +++++++++------------ test/helpers.js | 22 ++++++++---------- test/match-fixtures.js | 14 ++++-------- test/misc.js | 12 +++------- test/parser.js | 30 ++++++++++++------------- test/readme.js | 28 +++++++++-------------- test/stringify-fixtures.js | 16 +++++-------- 8 files changed, 78 insertions(+), 116 deletions(-) diff --git a/test/ast.js b/test/ast.js index 71097a5..acbf8f4 100644 --- a/test/ast.js +++ b/test/ast.js @@ -1,75 +1,73 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { - UrlPattern, +import test from "tape"; + +import { newUrlPatternParser, - defaultOptions, getParam, astNodeToRegexString, astNodeToNames -} = require('../index.js'); +} from "../dist/parser.js"; + +import { + defaultOptions, +} from "../dist/options.js"; -const parse = newUrlPatternParser(defaultOptions).pattern; +const parse = newUrlPatternParser(defaultOptions); test('astNodeToRegexString and astNodeToNames', function(t) { t.test('just static alphanumeric', function(t) { const parsed = parse('user42'); t.equal(astNodeToRegexString(parsed.value), '^user42$'); t.deepEqual(astNodeToNames(parsed.value), []); - return t.end(); + t.end(); }); t.test('just static escaped', function(t) { const parsed = parse('/api/v1/users'); t.equal(astNodeToRegexString(parsed.value), '^\\/api\\/v1\\/users$'); t.deepEqual(astNodeToNames(parsed.value), []); - return t.end(); + t.end(); }); t.test('just single char variable', function(t) { const parsed = parse(':a'); t.equal(astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$'); t.deepEqual(astNodeToNames(parsed.value), ['a']); - return t.end(); + t.end(); }); t.test('just variable', function(t) { const parsed = parse(':variable'); t.equal(astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$'); t.deepEqual(astNodeToNames(parsed.value), ['variable']); - return t.end(); + t.end(); }); t.test('just wildcard', function(t) { const parsed = parse('*'); t.equal(astNodeToRegexString(parsed.value), '^(.*?)$'); t.deepEqual(astNodeToNames(parsed.value), ['_']); - return t.end(); + t.end(); }); t.test('just optional static', function(t) { const parsed = parse('(foo)'); t.equal(astNodeToRegexString(parsed.value), '^(?:foo)?$'); t.deepEqual(astNodeToNames(parsed.value), []); - return t.end(); + t.end(); }); t.test('just optional variable', function(t) { const parsed = parse('(:foo)'); t.equal(astNodeToRegexString(parsed.value), '^(?:([a-zA-Z0-9-_~ %]+))?$'); t.deepEqual(astNodeToNames(parsed.value), ['foo']); - return t.end(); + t.end(); }); - return t.test('just optional wildcard', function(t) { + t.test('just optional wildcard', function(t) { const parsed = parse('(*)'); t.equal(astNodeToRegexString(parsed.value), '^(?:(.*?))?$'); t.deepEqual(astNodeToNames(parsed.value), ['_']); - return t.end(); + t.end(); }); }); @@ -131,7 +129,7 @@ test('getParam', function(t) { t.equal(undefined, getParam({one: [1, 2, 3]}, 'one', next)); t.deepEqual(next, {one: 3}); - return t.end(); + t.end(); }); t.test('side effects', function(t) { @@ -165,10 +163,10 @@ test('getParam', function(t) { t.equal(3, getParam({one: [1, 2, 3]}, 'one', next, true)); t.deepEqual(next, {one: 3}); - return t.end(); + t.end(); }); - return t.test('side effects errors', function(t) { + t.test('side effects errors', function(t) { let e; t.plan(2 * 6); @@ -226,6 +224,6 @@ test('getParam', function(t) { } t.deepEqual(next, {one: 3}); - return t.end(); + t.end(); }); }); diff --git a/test/errors.js b/test/errors.js index f01d733..665127f 100644 --- a/test/errors.js +++ b/test/errors.js @@ -1,12 +1,6 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { - UrlPattern -} = require('../index.js'); +import test from "tape"; + +import UrlPattern from "../dist/url-pattern.js"; test('invalid argument', function(t) { let e; @@ -42,7 +36,7 @@ test('invalid argument', function(t) { e = error4; t.equal(e.message, "first argument must not contain whitespace"); } - return t.end(); + t.end(); }); test('invalid variable name in pattern', function(t) { @@ -68,7 +62,7 @@ test('invalid variable name in pattern', function(t) { e = error2; t.equal(e.message, "could only partially parse pattern"); } - return t.end(); + t.end(); }); test('too many closing parentheses', function(t) { @@ -88,7 +82,7 @@ test('too many closing parentheses', function(t) { e = error1; t.equal(e.message, "could only partially parse pattern"); } - return t.end(); + t.end(); }); test('unclosed parentheses', function(t) { @@ -108,7 +102,7 @@ test('unclosed parentheses', function(t) { e = error1; t.equal(e.message, "couldn't parse pattern"); } - return t.end(); + t.end(); }); test('regex names', function(t) { @@ -132,7 +126,7 @@ test('regex names', function(t) { e = error2; t.equal(e.message, "regex contains 4 groups but array of group names contains 2"); } - return t.end(); + t.end(); }); test('stringify regex', function(t) { @@ -143,7 +137,7 @@ test('stringify regex', function(t) { } catch (e) { t.equal(e.message, "can't stringify patterns generated from a regex"); } - return t.end(); + t.end(); }); test('stringify argument', function(t) { @@ -154,5 +148,5 @@ test('stringify argument', function(t) { } catch (e) { t.equal(e.message, "argument must be an object or undefined"); } - return t.end(); + t.end(); }); diff --git a/test/helpers.js b/test/helpers.js index d5a9b39..f2e4c3b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,16 +1,12 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { +import test from "tape"; + +import { escapeStringForRegex, concatMap, stringConcatMap, regexGroupCount, keysAndValuesToObject -} = require('../index.js'); +} from "../dist/helpers.js"; test('escapeStringForRegex', function(t) { const expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]'; @@ -28,14 +24,14 @@ test('escapeStringForRegex', function(t) { t.equal('\\]', escapeStringForRegex(']')); t.equal('\\(', escapeStringForRegex('(')); t.equal('\\)', escapeStringForRegex(')')); - return t.end(); + t.end(); }); test('concatMap', function(t) { t.deepEqual([], concatMap([], function() {})); t.deepEqual([1], concatMap([1], x => [x])); t.deepEqual([1, 1, 1, 2, 2, 2, 3, 3, 3], concatMap([1, 2, 3], x => [x, x, x])); - return t.end(); + t.end(); }); test('stringConcatMap', function(t) { @@ -43,7 +39,7 @@ test('stringConcatMap', function(t) { t.equal('1', stringConcatMap([1], x => x)); t.equal('123', stringConcatMap([1, 2, 3], x => x)); t.equal('1a2a3a', stringConcatMap([1, 2, 3], x => x + 'a')); - return t.end(); + t.end(); }); test('regexGroupCount', function(t) { @@ -54,7 +50,7 @@ test('regexGroupCount', function(t) { t.equal(2, regexGroupCount(/f(o)(o)/)); t.equal(2, regexGroupCount(/f(o)o()/)); t.equal(5, regexGroupCount(/f(o)o()()(())/)); - return t.end(); + t.end(); }); test('keysAndValuesToObject', function(t) { @@ -135,5 +131,5 @@ test('keysAndValuesToObject', function(t) { three: 5 } ); - return t.end(); + t.end(); }); diff --git a/test/match-fixtures.js b/test/match-fixtures.js index 2986554..9dffa7a 100644 --- a/test/match-fixtures.js +++ b/test/match-fixtures.js @@ -1,12 +1,6 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { - UrlPattern -} = require('../index.js'); +import test from "tape"; + +import UrlPattern from "../dist/url-pattern.js"; test('match', function(t) { let pattern = new UrlPattern('/foo'); @@ -294,5 +288,5 @@ test('match', function(t) { t.deepEqual(pattern.match('/ip/127.0.0.1'), {ip: '127.0.0.1'}); - return t.end(); + t.end(); }); diff --git a/test/misc.js b/test/misc.js index 93cf13b..eebb54d 100644 --- a/test/misc.js +++ b/test/misc.js @@ -1,12 +1,6 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { - UrlPattern -} = require('../index.js'); +import test from "tape"; + +import UrlPattern from "../dist/url-pattern.js"; test('instance of UrlPattern is handled correctly as constructor argument', function(t) { const pattern = new UrlPattern('/user/:userId/task/:taskId'); diff --git a/test/parser.js b/test/parser.js index 4aa9bd5..d5b4b6b 100644 --- a/test/parser.js +++ b/test/parser.js @@ -1,19 +1,17 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -// taken from -// https://github.com/snd/pcom/blob/master/t/url-pattern-example.coffee - const test = require('tape'); -const { +import { newUrlPatternParser, + getParam, + astNodeToRegexString, + astNodeToNames +} from "../dist/parser.js"; + +import { defaultOptions, -} = require('../index.js'); -const U = newUrlPatternParser(defaultOptions); -const parse = U.pattern; +} from "../dist/options.js"; + +const parse = newUrlPatternParser(defaultOptions); test('wildcard', function(t) { t.deepEqual(U.wildcard('*'), { @@ -43,7 +41,7 @@ test('wildcard', function(t) { t.equal(U.wildcard('$foobar'), undefined); t.equal(U.wildcard('$'), undefined); t.equal(U.wildcard(''), undefined); - return t.end(); + t.end(); }); test('named', function(t) { @@ -83,7 +81,7 @@ test('named', function(t) { t.equal(U.named(''), undefined); t.equal(U.named('a'), undefined); t.equal(U.named('abc'), undefined); - return t.end(); + t.end(); }); test('static', function(t) { @@ -109,7 +107,7 @@ test('static', function(t) { t.equal(U.static(')'), undefined); t.equal(U.static('*'), undefined); t.equal(U.static(''), undefined); - return t.end(); + t.end(); }); @@ -494,5 +492,5 @@ test('fixtures', function(t) { ] }); - return t.end(); + t.end(); }); diff --git a/test/readme.js b/test/readme.js index 3504a7a..94d8a77 100644 --- a/test/readme.js +++ b/test/readme.js @@ -1,18 +1,12 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { - UrlPattern -} = require('../index.js'); +import test from "tape"; + +import UrlPattern from "../dist/url-pattern.js"; test('simple', function(t) { const pattern = new UrlPattern('/api/users/:id'); t.deepEqual(pattern.match('/api/users/10'), {id: '10'}); t.equal(pattern.match('/api/products/5'), undefined); - return t.end(); + t.end(); }); test('api versioning', function(t) { @@ -20,7 +14,7 @@ test('api versioning', function(t) { t.deepEqual(pattern.match('/v1.2/'), {major: '1', minor: '2', _: ''}); t.deepEqual(pattern.match('/v2/users'), {major: '2', _: 'users'}); t.equal(pattern.match('/v/'), undefined); - return t.end(); + t.end(); }); test('domain', function(t) { @@ -67,20 +61,20 @@ test('domain', function(t) { } ); - return t.end(); + t.end(); }); test('named segment occurs more than once', function(t) { const pattern = new UrlPattern('/api/users/:ids/posts/:ids'); t.deepEqual(pattern.match('/api/users/10/posts/5'), {ids: ['10', '5']}); - return t.end(); + t.end(); }); test('regex', function(t) { const pattern = new UrlPattern(/^\/api\/(.*)$/); t.deepEqual(pattern.match('/api/users'), ['users']); t.equal(pattern.match('/apiii/users'), undefined); - return t.end(); + t.end(); }); test('regex group names', function(t) { @@ -94,7 +88,7 @@ test('regex group names', function(t) { } ); t.equal(pattern.match('/api/users/foo'), undefined); - return t.end(); + t.end(); }); test('stringify', function(t) { @@ -105,7 +99,7 @@ test('stringify', function(t) { t.equal('/api/users', pattern.stringify()); t.equal('/api/users/10', pattern.stringify({id: 10})); - return t.end(); + t.end(); }); test('customization', function(t) { @@ -157,5 +151,5 @@ test('customization', function(t) { _: 'search' } ); - return t.end(); + t.end(); }); diff --git a/test/stringify-fixtures.js b/test/stringify-fixtures.js index 90e791c..d1d8484 100644 --- a/test/stringify-fixtures.js +++ b/test/stringify-fixtures.js @@ -1,12 +1,6 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const test = require('tape'); -const { - UrlPattern -} = require('../index.js'); +import test from "tape"; + +import UrlPattern from "../dist/url-pattern.js"; test('stringify', function(t) { let pattern = new UrlPattern('/foo'); @@ -157,7 +151,7 @@ test('stringify', function(t) { range: '10-20'}) ); - return t.end(); + t.end(); }); test('stringify errors', function(t) { @@ -216,5 +210,5 @@ test('stringify errors', function(t) { t.equal(e.message, "no values provided for key `_`"); } - return t.end(); + t.end(); }); From da792be1f9818465be02c2fb803a58f06f049d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 29 Apr 2019 23:58:06 -0500 Subject: [PATCH 032/117] gitignore dist dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c789a61..ee640d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/** test/url-pattern.js **.DS_Store npm-debug.log +dist From fea0e8b70acc96cd8267a307b1bc187621371bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 29 Apr 2019 23:58:54 -0500 Subject: [PATCH 033/117] split source into multiple files and refactor some more --- index.ts | 542 --------------------------------------- src/helpers.ts | 82 ++++++ src/options.ts | 31 +++ src/parser.ts | 224 ++++++++++++++++ src/parsercombinators.ts | 195 ++++++++++++++ src/url-pattern.ts | 147 +++++++++++ 6 files changed, 679 insertions(+), 542 deletions(-) delete mode 100644 index.ts create mode 100644 src/helpers.ts create mode 100644 src/options.ts create mode 100644 src/parser.ts create mode 100644 src/parsercombinators.ts create mode 100644 src/url-pattern.ts diff --git a/index.ts b/index.ts deleted file mode 100644 index 4adf5d8..0000000 --- a/index.ts +++ /dev/null @@ -1,542 +0,0 @@ -// OPTIONS - -interface IUrlPatternOptionsInput { - escapeChar?: string; - segmentNameStartChar?: string; - segmentValueCharset?: string; - segmentNameCharset?: string; - optionalSegmentStartChar?: string; - optionalSegmentEndChar?: string; - wildcardChar?: string; -} - -interface IUrlPatternOptions { - escapeChar: string; - segmentNameStartChar: string; - segmentValueCharset: string; - segmentNameCharset: string; - optionalSegmentStartChar: string; - optionalSegmentEndChar: string; - wildcardChar: string; -} - -export const defaultOptions: IUrlPatternOptions = { - escapeChar: "\\", - optionalSegmentEndChar: ")", - optionalSegmentStartChar: "(", - segmentNameCharset: "a-zA-Z0-9", - segmentNameStartChar: ":", - segmentValueCharset: "a-zA-Z0-9-_~ %", - wildcardChar: "*", -}; - -// HELPERS - -// escapes a string for insertion into a regular expression -// source: http://stackoverflow.com/a/3561711 -export function escapeStringForRegex(str: string): string { - return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); -} - -export function concatMap(array: T[], f: (x: T) => T[]): T[] { - let results: T[] = []; - for (const value of array) { - results = results.concat(f(value)); - } - return results; -} - -export function stringConcatMap(array: T[], f: (x: T) => string): string { - let result = ""; - for (const value of array) { - result += f(value); - } - return result; -} - -/* - * returns the number of groups in the `regex`. - * source: http://stackoverflow.com/a/16047223 - */ -export function regexGroupCount(regex: RegExp): number { - return new RegExp(regex.toString() + "|").exec("").length - 1; -} - -// zips an array of `keys` and an array of `values` into an object. -// `keys` and `values` must have the same length. -// if the same key appears multiple times the associated values are collected in an array. -export function keysAndValuesToObject(keys: any[], values: any[]): object { - const result: object = {}; - - if (keys.length !== values.length) { - throw Error("keys.length must equal values.length"); - } - - let i = -1; - while (++i < keys.length) { - const key = keys[i]; - const value = values[i]; - - if (value == null) { - continue; - } - - // key already encountered - if (result[key] != null) { - // capture multiple values for same key in an array - if (!Array.isArray(result[key])) { - result[key] = [result[key]]; - } - result[key].push(value); - } else { - result[key] = value; - } - } - return result; -} - -// PARSER COMBINATORS - -// parse result -class Result { - /* parsed value */ - public readonly value: Value; - /* unparsed rest */ - public readonly rest: string; - constructor(value: Value, rest: string) { - this.value = value; - this.rest = rest; - } -} - -class Tagged { - public readonly tag: string; - public readonly value: Value; - constructor(tag: string, value: Value) { - this.tag = tag; - this.value = value; - } -} - -/** - * a parser is a function that takes a string and returns a `Result` - * containing a parsed `Result.value` and the rest of the string `Result.rest` - */ -type Parser = (str: string) => Result | undefined; - -// parser combinators -const P = { - Result, - Tagged, - // transforms a `parser` into a parser that tags its `Result.value` with `tag` - tag(tag: string, parser: Parser): Parser> { - return (input: string) => { - const result = parser(input); - if (result == null) { - return; - } - const tagged = new Tagged(tag, result.value); - return new Result(tagged, result.rest); - }; - }, - // parser that consumes everything matched by `regex` - regex(regex: RegExp): Parser { - return (input: string) => { - const matches = regex.exec(input); - if (matches == null) { - return; - } - const result = matches[0]; - return new Result(result, input.slice(result.length)); - }; - }, - // takes a sequence of parsers and returns a parser that runs - // them in sequence and produces an array of their results - sequence(...parsers: Array>): Parser { - return (input: string) => { - let rest = input; - const values: any[] = []; - for (const parser of parsers) { - const result = parser(rest); - if (result == null) { - return; - } - values.push(result.value); - rest = result.rest; - } - return new Result(values, rest); - }; - }, - // returns a parser that consumes `str` exactly - string(str: string): Parser { - const { length } = str; - return (input: string) => { - if (input.slice(0, length) === str) { - return new Result(str, input.slice(length)); - } - }; - }, - // takes a sequence of parser and only returns the result - // returned by the `index`th parser - pick(index: number, ...parsers: Array>): Parser { - const parser = P.sequence(...parsers); - return (input: string) => { - const result = parser(input); - if (result == null) { - return; - } - return new Result(result.value[index], result.rest); - }; - }, - // for parsers that each depend on one another (cyclic dependencies) - // postpone lookup to when they both exist. - lazy(getParser: () => Parser): Parser { - let cachedParser: Parser | null = null; - return (input: string) => { - if (cachedParser == null) { - cachedParser = getParser(); - } - return cachedParser(input); - }; - }, - /* - * base function for parsers that parse multiples. - * - * @param endParser once the `endParser` (if not null) consumes - * the `baseMany` parser returns. the result of the `endParser` is ignored. - */ - baseMany( - parser: Parser, - endParser: Parser | null, - isAtLeastOneResultRequired: boolean, - input: string, - ): Result | undefined { - let rest = input; - const results: T[] = []; - while (true) { - if (endParser != null) { - const endResult = endParser(rest); - if (endResult != null) { - break; - } - } - const parserResult = parser(rest); - if (parserResult == null) { - break; - } - results.push(parserResult.value); - rest = parserResult.rest; - } - - if (isAtLeastOneResultRequired && results.length === 0) { - return; - } - - return new Result(results, rest); - }, - many1(parser: Parser): Parser { - return (input: string) => { - const endParser: null = null; - const isAtLeastOneResultRequired = true; - return P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); - }; - }, - concatMany1Till(parser: Parser, endParser: Parser): Parser { - return (input: string) => { - const isAtLeastOneResultRequired = true; - const result = P.baseMany(parser, endParser, isAtLeastOneResultRequired, input); - if (result == null) { - return; - } - return new Result(result.value.join(""), result.rest); - }; - }, - // takes a sequence of parsers. returns the result from the first - // parser that consumes the input. - firstChoice(...parsers: Array>): Parser { - return (input: string) => { - for (const parser of parsers) { - const result = parser(input); - if (result != null) { - return result; - } - } - return; - }; - }, -}; - -// URL PATTERN PARSER - -interface IUrlPatternParser { - escapedChar: Parser; - name: Parser; - named: Parser>; - optional: Parser>; - pattern: Parser; - static: Parser>; - token: Parser; - wildcard: Parser>; -} - -export function newUrlPatternParser(options: IUrlPatternOptions): IUrlPatternParser { - const U: IUrlPatternParser = { - escapedChar: P.pick(1, P.string(options.escapeChar), P.regex(/^./)), - name: P.regex(new RegExp(`^[${ options.segmentNameCharset }]+`)), - named: P.tag("named", P.pick(1, P.string(options.segmentNameStartChar), P.lazy(() => U.name))), - optional: P.tag("optional", P.pick(1, - P.string(options.optionalSegmentStartChar), - P.lazy(() => U.pattern), - P.string(options.optionalSegmentEndChar))), - pattern: P.many1(P.lazy(() => U.token)), - static: P.tag("static", P.concatMany1Till(P.firstChoice( - P.lazy(() => U.escapedChar), - P.regex(/^./)), - P.firstChoice( - P.string(options.segmentNameStartChar), - P.string(options.optionalSegmentStartChar), - P.string(options.optionalSegmentEndChar), - P.lazy(() => U.wildcard)))), - token: P.lazy(() => P.firstChoice(U.wildcard, U.optional, U.named, U.static)), - wildcard: P.tag("wildcard", P.string(options.wildcardChar)), - }; - - return U; -} - -// functions that further process ASTs returned as `.value` in parser results - -function baseAstNodeToRegexString(astNode: Tagged, segmentValueCharset: string): string { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, (node) => baseAstNodeToRegexString(node, segmentValueCharset)); - } - - switch (astNode.tag) { - case "wildcard": - return "(.*?)"; - case "named": - return `([${ segmentValueCharset }]+)`; - case "static": - return escapeStringForRegex(astNode.value); - case "optional": - return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; - } -} - -export function astNodeToRegexString(astNode: Tagged, segmentValueCharset?: string) { - if (segmentValueCharset == null) { - ({ segmentValueCharset } = defaultOptions); - } - return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; -} - -export function astNodeToNames(astNode: Tagged | Array>): string[] { - if (Array.isArray(astNode)) { - return concatMap(astNode, astNodeToNames); - } - - switch (astNode.tag) { - case "wildcard": - return ["_"]; - case "named": - return [astNode.value]; - case "static": - return []; - case "optional": - return astNodeToNames(astNode.value); - } -} - -// TODO better name -export function getParam(params, key, nextIndexes, sideEffects) { - if (sideEffects == null) { - sideEffects = false; - } - const value = params[key]; - if (value == null) { - if (sideEffects) { - throw new Error(`no values provided for key \`${ key }\``); - } else { - return; - } - } - let index = nextIndexes[key] || 0; - let maxIndex = Array.isArray(value) ? value.length - 1 : 0; - if (index > maxIndex) { - if (sideEffects) { - throw new Error(`too few values provided for key \`${ key }\``); - } else { - return; - } - } - - let result = Array.isArray(value) ? value[index] : value; - - if (sideEffects) { - nextIndexes[key] = index + 1; - } - - return result; -}; - -function astNodeContainsSegmentsForProvidedParams(astNode, params, nextIndexes) { - if (Array.isArray(astNode)) { - let i = -1; - const { length } = astNode; - while (++i < length) { - if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { - return true; - } - } - return false; - } - - switch (astNode.tag) { - case "wildcard": - return getParam(params, "_", nextIndexes, false) != null; - case "named": - return getParam(params, astNode.value, nextIndexes, false) != null; - case "static": - return false; - case "optional": - return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); - } -} - -function stringify(astNode: Tagged, params, nextIndexes): string { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, node => stringify(node, params, nextIndexes)); - } - - switch (astNode.tag) { - case "wildcard": - return getParam(params, "_", nextIndexes, true); - case "named": - return getParam(params, astNode.value, nextIndexes, true); - case "static": - return astNode.value; - case "optional": - if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { - return stringify(astNode.value, params, nextIndexes); - } else { - return "" - } - } -} - -export class UrlPattern { - public readonly isRegex: boolean; - public readonly regex: RegExp; - public readonly ast?: Tagged; - public readonly names: string[]; - - constructor(pattern: string, options?: IUrlPatternOptions); - constructor(pattern: RegExp, groupNames?: string[]); - - constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: IUrlPatternOptionsInput | string[]) { - // self awareness - if (pattern instanceof UrlPattern) { - this.isRegex = pattern.isRegex; - this.regex = pattern.regex; - this.ast = pattern.ast; - this.names = pattern.names; - return; - } - - this.isRegex = pattern instanceof RegExp; - - if ("string" !== typeof pattern && !this.isRegex) { - throw new TypeError("first argument must be a RegExp, a string or an instance of UrlPattern"); - } - - // handle regex pattern and return early - if (pattern instanceof RegExp) { - this.regex = pattern; - if (optionsOrGroupNames != null) { - if (!Array.isArray(optionsOrGroupNames)) { - throw new TypeError( - "if first argument is a RegExp the second argument may be an Array of group names but you provided something else"); - } - const groupCount = regexGroupCount(this.regex); - if (optionsOrGroupNames.length !== groupCount) { - throw new Error(`regex contains ${ groupCount } groups but array of group names contains ${ optionsOrGroupNames.length }`); - } - this.names = optionsOrGroupNames; - } - return; - } - - // everything following only concerns string patterns - - if (pattern === "") { - throw new Error("first argument must not be the empty string"); - } - const patternWithoutWhitespace = pattern.replace(/\s+/g, ""); - if (patternWithoutWhitespace !== pattern) { - throw new Error("first argument must not contain whitespace"); - } - - if (Array.isArray(optionsOrGroupNames)) { - throw new Error("if first argument is a string second argument must be an options object or undefined"); - } - - const options: IUrlPatternOptions = { - escapeChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, - optionalSegmentEndChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, - optionalSegmentStartChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, - segmentNameCharset: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, - segmentNameStartChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, - segmentValueCharset: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, - wildcardChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar, - }; - - const parser: IUrlPatternParser = newUrlPatternParser(options); - const parsed = parser.pattern(pattern); - if (parsed == null) { - // TODO better error message - throw new Error("couldn't parse pattern"); - } - if (parsed.rest !== "") { - // TODO better error message - throw new Error("could only partially parse pattern"); - } - this.ast = parsed.value; - - this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset)); - this.names = astNodeToNames(this.ast); - } - - public match(url: string): object | undefined { - const match = this.regex.exec(url); - if (match == null) { - return; - } - - const groups = match.slice(1); - if (this.names) { - return keysAndValuesToObject(this.names, groups); - } else { - return groups; - } - } - - public stringify(params?: object): string { - if (params == null) { - params = {}; - } - if (this.isRegex) { - throw new Error("can't stringify patterns generated from a regex"); - } - if (params !== Object(params)) { - throw new Error("argument must be an object or undefined"); - } - return stringify(this.ast, params, {}); - } -} diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..f52c348 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,82 @@ +/* + * escapes a string for insertion into a regular expression + * source: http://stackoverflow.com/a/3561711 + */ +export function escapeStringForRegex(str: string): string { + return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); +} + +/* + * like `Array.prototype.map` except that the function `f` + * returns an array and `concatMap` returns the concatenation + * of all arrays returned by `f` + */ +export function concatMap(array: T[], f: (x: T) => T[]): T[] { + let results: T[] = []; + for (const value of array) { + results = results.concat(f(value)); + } + return results; +} + +/* + * like `Array.prototype.map` except that the function `f` + * returns a string and `stringConcatMap` returns the concatenation + * of all strings returned by `f` + */ +export function stringConcatMap(array: T[], f: (x: T) => string): string { + let result = ""; + for (const value of array) { + result += f(value); + } + return result; +} + +/* + * returns the number of groups in the `regex`. + * source: http://stackoverflow.com/a/16047223 + */ +export function regexGroupCount(regex: RegExp): number { + const testingRegex = new RegExp(regex.toString() + "|"); + const matches = testingRegex.exec(""); + if (matches == null) { + throw new Error("no matches"); + } + return matches.length - 1; +} + +/* + * zips an array of `keys` and an array of `values` into an object + * so `keys[i]` is associated with `values[i]` for every i. + * `keys` and `values` must have the same length. + * if the same key appears multiple times the associated values are collected in an array. + */ +export function keysAndValuesToObject(keys: string[], values: any[]): object { + const result: { [index: string]: any } = {}; + + if (keys.length !== values.length) { + throw Error("keys.length must equal values.length"); + } + + let i = -1; + while (++i < keys.length) { + const key = keys[i]; + const value = values[i]; + + if (value == null) { + continue; + } + + // key already encountered + if (result[key] != null) { + // capture multiple values for same key in an array + if (!Array.isArray(result[key])) { + result[key] = [result[key]]; + } + result[key].push(value); + } else { + result[key] = value; + } + } + return result; +} diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..d8c9a1e --- /dev/null +++ b/src/options.ts @@ -0,0 +1,31 @@ +export interface IUserInputOptions { + escapeChar?: string; + segmentNameStartChar?: string; + segmentNameEndChar?: string; + segmentValueCharset?: string; + segmentNameCharset?: string; + optionalSegmentStartChar?: string; + optionalSegmentEndChar?: string; + wildcardChar?: string; +} + +export interface IOptions { + escapeChar: string; + segmentNameStartChar: string; + segmentNameEndChar?: string; + segmentValueCharset: string; + segmentNameCharset: string; + optionalSegmentStartChar: string; + optionalSegmentEndChar: string; + wildcardChar: string; +} + +export const defaultOptions: IOptions = { + escapeChar: "\\", + optionalSegmentEndChar: ")", + optionalSegmentStartChar: "(", + segmentNameCharset: "a-zA-Z0-9", + segmentNameStartChar: ":", + segmentValueCharset: "a-zA-Z0-9-_~ %", + wildcardChar: "*", +}; diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..e67df36 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,224 @@ +/* + * the url pattern parser + */ + +import { + Ast, + concatMany1Till, + firstChoice, + lazy, + many1, + newAst, + Parser, + pick, + regex, + string, +} from "./parsercombinators"; + +import { + concatMap, + escapeStringForRegex, + stringConcatMap, +} from "./helpers"; + +import { + defaultOptions, + IOptions, +} from "./options"; + +/* + * + */ +export function newUrlPatternParser(options: IOptions): Parser> { + const parseEscapedChar = pick(1, string(options.escapeChar), regex(/^./)); + + const parseSegmentName = regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); + + let parseNamedSegment = newAst("namedSegment", pick(1, + string(options.segmentNameStartChar), + parseSegmentName)); + if (options.segmentNameEndChar != null) { + parseNamedSegment = newAst("namedSegment", pick(1, + string(options.segmentNameStartChar), + parseSegmentName, + string(options.segmentNameEndChar))); + } + + const parseWildcard = newAst("wildcard", string(options.wildcardChar)); + + let pattern: Parser = (input: string) => { + throw new Error(` + this is just a temporary placeholder + to make a circular dependency work. + that this got called is a bug + `); + }; + + const parseOptionalSegment = newAst("optionalSegment", pick(1, + string(options.optionalSegmentStartChar), + lazy(() => pattern), + string(options.optionalSegmentEndChar))); + + const parseStatic = newAst("static", concatMany1Till(firstChoice( + parseEscapedChar, + regex(/^./)), + firstChoice( + string(options.segmentNameStartChar), + string(options.optionalSegmentStartChar), + string(options.optionalSegmentEndChar), + lazy(() => parseWildcard)))); + + const token = firstChoice( + parseWildcard, + parseOptionalSegment, + parseNamedSegment, + parseStatic); + + pattern = many1(token); + + return pattern; +} + +// functions that further process ASTs returned as `.value` in parser results + +function baseAstNodeToRegexString(astNode: Ast, segmentValueCharset: string): string { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, (node) => baseAstNodeToRegexString(node, segmentValueCharset)); + } + + switch (astNode.tag) { + case "wildcard": + return "(.*?)"; + case "namedSegment": + return `([${ segmentValueCharset }]+)`; + case "static": + return escapeStringForRegex(astNode.value); + case "optionalSegment": + return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} + +export function astNodeToRegexString(astNode: Ast, segmentValueCharset?: string) { + if (segmentValueCharset == null) { + ({ segmentValueCharset } = defaultOptions); + } + return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; +} + +export function astNodeToNames(astNode: Ast | Array>): string[] { + if (Array.isArray(astNode)) { + return concatMap(astNode, astNodeToNames); + } + + switch (astNode.tag) { + case "wildcard": + return ["_"]; + case "namedSegment": + return [astNode.value]; + case "static": + return []; + case "optionalSegment": + return astNodeToNames(astNode.value); + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} + +// TODO better name +export function getParam( + params: { [index: string]: any }, + key: string, + nextIndexes: { [index: string]: number }, + hasSideEffects: boolean, +) { + if (hasSideEffects == null) { + hasSideEffects = false; + } + const value = params[key]; + if (value == null) { + if (hasSideEffects) { + throw new Error(`no values provided for key \`${ key }\``); + } else { + return; + } + } + const index = nextIndexes[key] || 0; + const maxIndex = Array.isArray(value) ? value.length - 1 : 0; + if (index > maxIndex) { + if (hasSideEffects) { + throw new Error(`too few values provided for key \`${ key }\``); + } else { + return; + } + } + + const result = Array.isArray(value) ? value[index] : value; + + if (hasSideEffects) { + nextIndexes[key] = index + 1; + } + + return result; +} + +function astNodeContainsSegmentsForProvidedParams( + astNode: Ast, + params: { [index: string]: any }, + nextIndexes: { [index: string]: number }, +): boolean { + if (Array.isArray(astNode)) { + let i = -1; + const { length } = astNode; + while (++i < length) { + if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { + return true; + } + } + return false; + } + + switch (astNode.tag) { + case "wildcard": + return getParam(params, "_", nextIndexes, false) != null; + case "namedSegment": + return getParam(params, astNode.value, nextIndexes, false) != null; + case "static": + return false; + case "optionalSegment": + return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} + +/* + * stringify a url pattern AST + */ +export function stringify( + astNode: Ast | Array>, + params: { [index: string]: any }, + nextIndexes: { [index: string]: number }, +): string { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, (node) => stringify(node, params, nextIndexes)); + } + + switch (astNode.tag) { + case "wildcard": + return getParam(params, "_", nextIndexes, true); + case "namedSegment": + return getParam(params, astNode.value, nextIndexes, true); + case "static": + return astNode.value; + case "optionalSegment": + if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { + return stringify(astNode.value, params, nextIndexes); + } else { + return ""; + } + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} diff --git a/src/parsercombinators.ts b/src/parsercombinators.ts new file mode 100644 index 0000000..205ed05 --- /dev/null +++ b/src/parsercombinators.ts @@ -0,0 +1,195 @@ +/* + * generic parser combinators used to build the url pattern parser (module `parser`) + */ + +/* + * parse result + */ +export class Result { + /* parsed value */ + public readonly value: Value; + /* unparsed rest */ + public readonly rest: string; + constructor(value: Value, rest: string) { + this.value = value; + this.rest = rest; + } +} + +/** + * node in the AST (abstract syntax tree) + */ +export class Ast { + public readonly tag: string; + public readonly value: Value; + constructor(tag: string, value: Value) { + this.tag = tag; + this.value = value; + } +} + +/* + * a parser is a function that takes a string and returns a `Result` + * containing a parsed `Result.value` and the rest of the string `Result.rest` + */ +export type Parser = (str: string) => Result | undefined; + +/* + * transforms a `parser` into a parser that returns an Ast node + */ +export function newAst(tag: string, parser: Parser): Parser> { + return (input: string) => { + const result = parser(input); + if (result == null) { + return; + } + const ast = new Ast(tag, result.value); + return new Result(ast, result.rest); + }; +} + +/* + * parser that consumes everything matched by `regex` + */ +export function regex(regexp: RegExp): Parser { + return (input: string) => { + const matches = regexp.exec(input); + if (matches == null) { + return; + } + const result = matches[0]; + return new Result(result, input.slice(result.length)); + }; +} + +/* + * takes a sequence of parsers and returns a parser that runs + * them in sequence and produces an array of their results + */ +export function sequence(...parsers: Array>): Parser { + return (input: string) => { + let rest = input; + const values: any[] = []; + for (const parser of parsers) { + const result = parser(rest); + if (result == null) { + return; + } + values.push(result.value); + rest = result.rest; + } + return new Result(values, rest); + }; +} + +/* + * returns a parser that consumes `str` exactly + */ +export function string(str: string): Parser { + const { length } = str; + return (input: string) => { + if (input.slice(0, length) === str) { + return new Result(str, input.slice(length)); + } + }; +} + +/* + * takes a sequence of parser and only returns the result + * returned by the `index`th parser + */ +export function pick(index: number, ...parsers: Array>): Parser { + const parser = sequence(...parsers); + return (input: string) => { + const result = parser(input); + if (result == null) { + return; + } + return new Result(result.value[index], result.rest); + }; +} + +/* + * for parsers that each depend on one another (cyclic dependencies) + * postpone lookup to when they both exist. + */ +export function lazy(getParser: () => Parser): Parser { + let cachedParser: Parser | null = null; + return (input: string) => { + if (cachedParser == null) { + cachedParser = getParser(); + } + return cachedParser(input); + }; +} + +/* + * base function for parsers that parse multiples. + * + * @param endParser once the `endParser` (if not null) consumes + * the `baseMany` parser returns. the result of the `endParser` is ignored. + */ +export function baseMany( + parser: Parser, + endParser: Parser | null, + isAtLeastOneResultRequired: boolean, + input: string, +): Result | undefined { + let rest = input; + const results: T[] = []; + while (true) { + if (endParser != null) { + const endResult = endParser(rest); + if (endResult != null) { + break; + } + } + const parserResult = parser(rest); + if (parserResult == null) { + break; + } + results.push(parserResult.value); + rest = parserResult.rest; + } + + if (isAtLeastOneResultRequired && results.length === 0) { + return; + } + + return new Result(results, rest); +} + +export function many1(parser: Parser): Parser { + return (input: string) => { + const endParser: null = null; + const isAtLeastOneResultRequired = true; + return baseMany(parser, endParser, isAtLeastOneResultRequired, input); + }; +} + +export function concatMany1Till(parser: Parser, endParser: Parser): Parser { + return (input: string) => { + const isAtLeastOneResultRequired = true; + const result = baseMany(parser, endParser, isAtLeastOneResultRequired, input); + if (result == null) { + return; + } + return new Result(result.value.join(""), result.rest); + }; +} + +/* + * takes a sequence of parsers. returns the result from the first + * parser that consumes the input. + */ +export function firstChoice(...parsers: Array>): Parser { + return (input: string) => { + for (const parser of parsers) { + const result = parser(input); + if (result != null) { + return result; + } + } + return; + }; +} diff --git a/src/url-pattern.ts b/src/url-pattern.ts new file mode 100644 index 0000000..b751bad --- /dev/null +++ b/src/url-pattern.ts @@ -0,0 +1,147 @@ +import { + keysAndValuesToObject, + regexGroupCount, +} from "./helpers"; + +import { + Ast, +} from "./parsercombinators"; + +import { + defaultOptions, + IOptions, + IUserInputOptions, +} from "./options"; + +import { + astNodeToNames, + astNodeToRegexString, + newUrlPatternParser, + stringify, +} from "./parser"; + +export default class UrlPattern { + public readonly isRegex: boolean; + public readonly regex: RegExp; + public readonly ast?: Ast; + public readonly names?: string[]; + + constructor(pattern: string, options?: IUserInputOptions); + constructor(pattern: RegExp, groupNames?: string[]); + + constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: IUserInputOptions | string[]) { + // self awareness + if (pattern instanceof UrlPattern) { + this.isRegex = pattern.isRegex; + this.regex = pattern.regex; + this.ast = pattern.ast; + this.names = pattern.names; + return; + } + + this.isRegex = pattern instanceof RegExp; + + if ("string" !== typeof pattern && !this.isRegex) { + throw new TypeError("first argument must be a RegExp, a string or an instance of UrlPattern"); + } + + // handle regex pattern and return early + if (pattern instanceof RegExp) { + this.regex = pattern; + if (optionsOrGroupNames != null) { + if (!Array.isArray(optionsOrGroupNames)) { + throw new TypeError(` + if first argument is a RegExp the second argument + may be an Array of group names + but you provided something else + `); + } + const groupCount = regexGroupCount(this.regex); + if (optionsOrGroupNames.length !== groupCount) { + throw new Error(` + regex contains ${ groupCount } groups + but array of group names contains ${ optionsOrGroupNames.length } + `); + } + this.names = optionsOrGroupNames; + } + return; + } + + // everything following only concerns string patterns + + if (pattern === "") { + throw new Error("first argument must not be the empty string"); + } + const patternWithoutWhitespace = pattern.replace(/\s+/g, ""); + if (patternWithoutWhitespace !== pattern) { + throw new Error("first argument must not contain whitespace"); + } + + if (Array.isArray(optionsOrGroupNames)) { + throw new Error("if first argument is a string second argument must be an options object or undefined"); + } + + const options: IOptions = { + escapeChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, + optionalSegmentEndChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, + optionalSegmentStartChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, + segmentNameCharset: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, + segmentNameEndChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentNameEndChar : undefined), + segmentNameStartChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, + segmentValueCharset: (optionsOrGroupNames != null ? + optionsOrGroupNames.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, + wildcardChar: (optionsOrGroupNames != null ? + optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar, + }; + + const parser = newUrlPatternParser(options); + const parsed = parser(pattern); + if (parsed == null) { + // TODO better error message + throw new Error("couldn't parse pattern"); + } + if (parsed.rest !== "") { + // TODO better error message + throw new Error("could only partially parse pattern"); + } + const ast = parsed.value; + this.ast = ast; + + this.regex = new RegExp(astNodeToRegexString(ast, options.segmentValueCharset)); + this.names = astNodeToNames(ast); + } + + public match(url: string): object | undefined { + const match = this.regex.exec(url); + if (match == null) { + return; + } + + const groups = match.slice(1); + if (this.names != null) { + return keysAndValuesToObject(this.names, groups); + } else { + return groups; + } + } + + public stringify(params?: object): string { + if (params == null) { + params = {}; + } + if (this.ast == null) { + throw new Error("can't stringify patterns generated from a regex"); + } + if (params !== Object(params)) { + throw new Error("argument must be an object or undefined"); + } + return stringify(this.ast, params, {}); + } +} From 441a3c51071a04f9945dd1a73f92dc76c9cdc820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 00:03:21 -0500 Subject: [PATCH 034/117] tsconfig: generate ES6 and compile src into dist --- tsconfig.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 9736a60..91b0bab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { "compilerOptions": { - "module": "commonjs", - "target": "ES3", + "module": "ES6", + "target": "ES6", "strict": true, "noUnusedLocals": true, - "noUnusedParameters": true, "removeComments": true, "preserveConstEnums": true, "sourceMap": true, "allowUnreachableCode": false, - "declaration": true + "declaration": true, + "outDir": "dist" }, - "files": [ - "index.ts" + "include": [ + "src/*.ts" ] } From aee34109eebbb5a1f74501280a9d76bd720b44bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 00:06:53 -0500 Subject: [PATCH 035/117] fix next version in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac817b5..06d02ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,7 +83,7 @@ non breaking messages on errors thrown on invalid patterns have changed slightly. -#### 0.11 +#### 2.0 - UrlPattern now uses typescript instead of coffeescript - renamed `UrlPattern.newParser` to `UrlPattern.newUrlPatternParser` From 8002edc0781c034eb4c104bcf218fa273bdd0223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 00:16:24 -0500 Subject: [PATCH 036/117] add _ to default segmentNameCharset closes #33 --- src/options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.ts b/src/options.ts index d8c9a1e..3d29460 100644 --- a/src/options.ts +++ b/src/options.ts @@ -24,7 +24,7 @@ export const defaultOptions: IOptions = { escapeChar: "\\", optionalSegmentEndChar: ")", optionalSegmentStartChar: "(", - segmentNameCharset: "a-zA-Z0-9", + segmentNameCharset: "a-zA-Z0-9_", segmentNameStartChar: ":", segmentValueCharset: "a-zA-Z0-9-_~ %", wildcardChar: "*", From 7c1f8025518b2b1e016206bbecf0875a3f4472d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 00:17:50 -0500 Subject: [PATCH 037/117] package.json: update dependencies and work on scripts --- package.json | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7cb6ee7..8fa2aea 100644 --- a/package.json +++ b/package.json @@ -72,20 +72,17 @@ }, "dependencies": {}, "devDependencies": { + "esm": "^3.2.22", "codecov.io": "0.1.6", - "coffee-script": "1.10.0", - "coffeeify": "2.0.1", - "coffeetape": "1.0.1", - "istanbul": "0.4.1", - "tape": "4.2.2", - "zuul": "3.8.0" + "tape": "4.10.1", + "zuul": "3.12.0" }, "main": "lib/url-pattern", "scripts": { - "compile": "coffee --bare --compile --output lib src", + "compile": "tsc", + "docs": "typedoc --out doc --module UrlPattern index.ts", "prepublish": "npm run compile", - "pretest": "npm run compile", - "test": "coffeetape test/*", + "test": "tape test/*", "test-with-coverage": "istanbul cover coffeetape test/* && cat ./coverage/coverage.json | ./node_modules/codecov.io/bin/codecov.io.js", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" From 130e0a7d6d43f2619f8922571de5640560379e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 12:39:04 -0500 Subject: [PATCH 038/117] fix type for helpers.concatMap --- src/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index f52c348..9becde9 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -11,8 +11,8 @@ export function escapeStringForRegex(str: string): string { * returns an array and `concatMap` returns the concatenation * of all arrays returned by `f` */ -export function concatMap(array: T[], f: (x: T) => T[]): T[] { - let results: T[] = []; +export function concatMap(array: T[], f: (x: T) => U[]): U[] { + let results: U[] = []; for (const value of array) { results = results.concat(f(value)); } From d34cd6afa8d9ed045b5d8ba1dc860999e3d9614d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 12:40:50 -0500 Subject: [PATCH 039/117] gitignore package-lock.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ee640d9..94725da 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ test/url-pattern.js **.DS_Store npm-debug.log dist +package-lock.json From 2861b95f886d1af4aa6bac073ce72021db0325f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 12:52:33 -0500 Subject: [PATCH 040/117] remove tests for wildcard which are overkill --- test/parser.js | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/test/parser.js b/test/parser.js index d5b4b6b..e2207d7 100644 --- a/test/parser.js +++ b/test/parser.js @@ -13,37 +13,6 @@ import { const parse = newUrlPatternParser(defaultOptions); -test('wildcard', function(t) { - t.deepEqual(U.wildcard('*'), { - value: { - tag: 'wildcard', - value: '*' - }, - rest: '' - } - ); - t.deepEqual(U.wildcard('*/'), { - value: { - tag: 'wildcard', - value: '*' - }, - rest: '/' - } - ); - t.equal(U.wildcard(' *'), undefined); - t.equal(U.wildcard('()'), undefined); - t.equal(U.wildcard('foo(100)'), undefined); - t.equal(U.wildcard('(100foo)'), undefined); - t.equal(U.wildcard('(foo100)'), undefined); - t.equal(U.wildcard('(foobar)'), undefined); - t.equal(U.wildcard('foobar'), undefined); - t.equal(U.wildcard('_aa'), undefined); - t.equal(U.wildcard('$foobar'), undefined); - t.equal(U.wildcard('$'), undefined); - t.equal(U.wildcard(''), undefined); - t.end(); -}); - test('named', function(t) { t.deepEqual(U.named(':a'), { value: { From 080c6eb1ace93a6f5a031c7c877e928a0dc47565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 13:34:54 -0500 Subject: [PATCH 041/117] extend and refactor parser. all tests pass. npm test works closes #33 closes #49 closes #45 --- package.json | 3 +- src/parser.ts | 104 +++++++++----- src/url-pattern.ts | 20 +-- test/match-fixtures.js | 11 +- test/parser.js | 271 +++++++++++++++++++------------------ test/stringify-fixtures.js | 8 +- 6 files changed, 230 insertions(+), 187 deletions(-) diff --git a/package.json b/package.json index 8fa2aea..5a5dae3 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,8 @@ "compile": "tsc", "docs": "typedoc --out doc --module UrlPattern index.ts", "prepublish": "npm run compile", - "test": "tape test/*", + "pretest": "npm run compile", + "test": "tape -r esm test/*", "test-with-coverage": "istanbul cover coffeetape test/* && cat ./coverage/coverage.json | ./node_modules/codecov.io/bin/codecov.io.js", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" diff --git a/src/parser.ts b/src/parser.ts index e67df36..4f5a1a8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -26,27 +26,72 @@ import { IOptions, } from "./options"; +export function newEscapedCharParser(options: IOptions): Parser> { + return pick(1, string(options.escapeChar), regex(/^./)); +} + +export function newWildcardParser(options: IOptions): Parser> { + return newAst("wildcard", string(options.wildcardChar)); +} + /* - * + * parses just the segment name in a named segment */ -export function newUrlPatternParser(options: IOptions): Parser> { - const parseEscapedChar = pick(1, string(options.escapeChar), regex(/^./)); - - const parseSegmentName = regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); +export function newSegmentNameParser(options: IOptions): Parser { + return regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); +} - let parseNamedSegment = newAst("namedSegment", pick(1, - string(options.segmentNameStartChar), - parseSegmentName)); - if (options.segmentNameEndChar != null) { - parseNamedSegment = newAst("namedSegment", pick(1, +export function newNamedSegmentParser(options: IOptions): Parser> { + const parseSegmentName = newSegmentNameParser(options); + if (options.segmentNameEndChar == null) { + return newAst("namedSegment", pick(1, + string(options.segmentNameStartChar), + parseSegmentName)); + } else { + return newAst("namedSegment", pick(1, string(options.segmentNameStartChar), parseSegmentName, string(options.segmentNameEndChar))); } +} + +export function newNamedWildcardParser(options: IOptions): Parser> { + if (options.segmentNameEndChar == null) { + return newAst("namedWildcard", pick(2, + string(options.wildcardChar), + string(options.segmentNameStartChar), + newSegmentNameParser(options), + )); + } else { + return newAst("namedWildcard", pick(2, + string(options.wildcardChar), + string(options.segmentNameStartChar), + newSegmentNameParser(options), + string(options.segmentNameEndChar), + )); + } +} - const parseWildcard = newAst("wildcard", string(options.wildcardChar)); +export function newStaticContentParser(options: IOptions): Parser> { + return newAst("staticContent", concatMany1Till(firstChoice( + newEscapedCharParser(options), + regex(/^./)), + // parse any normal or escaped char until the following matches: + firstChoice( + string(options.segmentNameStartChar), + string(options.optionalSegmentStartChar), + string(options.optionalSegmentEndChar), + newWildcardParser(options), + newNamedWildcardParser(options), + ), + )); +} - let pattern: Parser = (input: string) => { +/* + * + */ +export function newUrlPatternParser(options: IOptions): Parser> { + let parsePattern: Parser = (input: string) => { throw new Error(` this is just a temporary placeholder to make a circular dependency work. @@ -56,27 +101,20 @@ export function newUrlPatternParser(options: IOptions): Parser> { const parseOptionalSegment = newAst("optionalSegment", pick(1, string(options.optionalSegmentStartChar), - lazy(() => pattern), + lazy(() => parsePattern), string(options.optionalSegmentEndChar))); - const parseStatic = newAst("static", concatMany1Till(firstChoice( - parseEscapedChar, - regex(/^./)), - firstChoice( - string(options.segmentNameStartChar), - string(options.optionalSegmentStartChar), - string(options.optionalSegmentEndChar), - lazy(() => parseWildcard)))); - - const token = firstChoice( - parseWildcard, + const parseToken = firstChoice( + newNamedWildcardParser(options), + newWildcardParser(options), parseOptionalSegment, - parseNamedSegment, - parseStatic); + newNamedSegmentParser(options), + newStaticContentParser(options), + ); - pattern = many1(token); + parsePattern = many1(parseToken); - return pattern; + return parsePattern; } // functions that further process ASTs returned as `.value` in parser results @@ -91,7 +129,7 @@ function baseAstNodeToRegexString(astNode: Ast, segmentValueCharset: string return "(.*?)"; case "namedSegment": return `([${ segmentValueCharset }]+)`; - case "static": + case "staticContent": return escapeStringForRegex(astNode.value); case "optionalSegment": return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; @@ -117,7 +155,7 @@ export function astNodeToNames(astNode: Ast | Array>): string[] { return ["_"]; case "namedSegment": return [astNode.value]; - case "static": + case "staticContent": return []; case "optionalSegment": return astNodeToNames(astNode.value); @@ -184,7 +222,7 @@ function astNodeContainsSegmentsForProvidedParams( return getParam(params, "_", nextIndexes, false) != null; case "namedSegment": return getParam(params, astNode.value, nextIndexes, false) != null; - case "static": + case "staticContent": return false; case "optionalSegment": return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); @@ -199,7 +237,7 @@ function astNodeContainsSegmentsForProvidedParams( export function stringify( astNode: Ast | Array>, params: { [index: string]: any }, - nextIndexes: { [index: string]: number }, + nextIndexes: { [index: string]: number } = {}, ): string { if (Array.isArray(astNode)) { return stringConcatMap(astNode, (node) => stringify(node, params, nextIndexes)); @@ -210,7 +248,7 @@ export function stringify( return getParam(params, "_", nextIndexes, true); case "namedSegment": return getParam(params, astNode.value, nextIndexes, true); - case "static": + case "staticContent": return astNode.value; case "optionalSegment": if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { diff --git a/src/url-pattern.ts b/src/url-pattern.ts index b751bad..fb26c4b 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -50,18 +50,18 @@ export default class UrlPattern { this.regex = pattern; if (optionsOrGroupNames != null) { if (!Array.isArray(optionsOrGroupNames)) { - throw new TypeError(` - if first argument is a RegExp the second argument - may be an Array of group names - but you provided something else - `); + throw new TypeError([ + "if first argument is a RegExp the second argument", + "may be an Array of group names", + "but you provided something else", + ].join(" ")); } const groupCount = regexGroupCount(this.regex); if (optionsOrGroupNames.length !== groupCount) { - throw new Error(` - regex contains ${ groupCount } groups - but array of group names contains ${ optionsOrGroupNames.length } - `); + throw new Error([ + `regex contains ${ groupCount } groups`, + `but array of group names contains ${ optionsOrGroupNames.length }`, + ].join(" ")); } this.names = optionsOrGroupNames; } @@ -142,6 +142,6 @@ export default class UrlPattern { if (params !== Object(params)) { throw new Error("argument must be an object or undefined"); } - return stringify(this.ast, params, {}); + return stringify(this.ast, params); } } diff --git a/test/match-fixtures.js b/test/match-fixtures.js index 9dffa7a..2acce49 100644 --- a/test/match-fixtures.js +++ b/test/match-fixtures.js @@ -142,15 +142,16 @@ test('match', function(t) { }); pattern = new UrlPattern('/:foo_bar'); - t.equal(pattern.match('/_bar'), undefined); + t.deepEqual(pattern.match('/_bar'), + {foo_bar: '_bar'}); t.deepEqual(pattern.match('/a_bar'), - {foo: 'a'}); + {foo_bar: 'a_bar'}); t.deepEqual(pattern.match('/a__bar'), - {foo: 'a_'}); + {foo_bar: 'a__bar'}); t.deepEqual(pattern.match('/a-b-c-d__bar'), - {foo: 'a-b-c-d_'}); + {foo_bar: 'a-b-c-d__bar'}); t.deepEqual(pattern.match('/a b%c-d__bar'), - {foo: 'a b%c-d_'}); + {foo_bar: 'a b%c-d__bar'}); pattern = new UrlPattern('((((a)b)c)d)'); t.deepEqual(pattern.match(''), {}); diff --git a/test/parser.js b/test/parser.js index e2207d7..71b7537 100644 --- a/test/parser.js +++ b/test/parser.js @@ -2,6 +2,8 @@ const test = require('tape'); import { newUrlPatternParser, + newNamedSegmentParser, + newStaticContentParser, getParam, astNodeToRegexString, astNodeToNames @@ -12,70 +14,72 @@ import { } from "../dist/options.js"; const parse = newUrlPatternParser(defaultOptions); +const parseNamedSegment = newNamedSegmentParser(defaultOptions); +const parseStaticContent= newStaticContentParser(defaultOptions); -test('named', function(t) { - t.deepEqual(U.named(':a'), { +test('namedSegment', function(t) { + t.deepEqual(parseNamedSegment(':a'), { value: { - tag: 'named', + tag: 'namedSegment', value: 'a' }, rest: '' } ); - t.deepEqual(U.named(':ab96c'), { + t.deepEqual(parseNamedSegment(':ab96c'), { value: { - tag: 'named', + tag: 'namedSegment', value: 'ab96c' }, rest: '' } ); - t.deepEqual(U.named(':ab96c.'), { + t.deepEqual(parseNamedSegment(':ab96c.'), { value: { - tag: 'named', + tag: 'namedSegment', value: 'ab96c' }, rest: '.' } ); - t.deepEqual(U.named(':96c-:ab'), { + t.deepEqual(parseNamedSegment(':96c-:ab'), { value: { - tag: 'named', + tag: 'namedSegment', value: '96c' }, rest: '-:ab' } ); - t.equal(U.named(':'), undefined); - t.equal(U.named(''), undefined); - t.equal(U.named('a'), undefined); - t.equal(U.named('abc'), undefined); + t.equal(parseNamedSegment(':'), undefined); + t.equal(parseNamedSegment(''), undefined); + t.equal(parseNamedSegment('a'), undefined); + t.equal(parseNamedSegment('abc'), undefined); t.end(); }); test('static', function(t) { - t.deepEqual(U.static('a'), { + t.deepEqual(parseStaticContent('a'), { value: { - tag: 'static', + tag: 'staticContent', value: 'a' }, rest: '' } ); - t.deepEqual(U.static('abc:d'), { + t.deepEqual(parseStaticContent('abc:d'), { value: { - tag: 'static', + tag: 'staticContent', value: 'abc' }, rest: ':d' } ); - t.equal(U.static(':ab96c'), undefined); - t.equal(U.static(':'), undefined); - t.equal(U.static('('), undefined); - t.equal(U.static(')'), undefined); - t.equal(U.static('*'), undefined); - t.equal(U.static(''), undefined); + t.equal(parseStaticContent(':ab96c'), undefined); + t.equal(parseStaticContent(':'), undefined); + t.equal(parseStaticContent('('), undefined); + t.equal(parseStaticContent(')'), undefined); + t.equal(parseStaticContent('*'), undefined); + t.equal(parseStaticContent(''), undefined); t.end(); }); @@ -92,7 +96,7 @@ test('fixtures', function(t) { t.deepEqual(parse('(foo))'), { rest: ')', value: [ - {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} + {tag: 'optionalSegment', value: [{tag: 'staticContent', value: 'foo'}]} ] }); @@ -100,9 +104,9 @@ test('fixtures', function(t) { rest: ')bar', value: [ { - tag: 'optional', + tag: 'optionalSegment', value: [ - {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} + {tag: 'optionalSegment', value: [{tag: 'staticContent', value: 'foo'}]} ] } ] @@ -112,34 +116,34 @@ test('fixtures', function(t) { t.deepEqual(parse('foo:*'), { rest: ':*', value: [ - {tag: 'static', value: 'foo'} + {tag: 'staticContent', value: 'foo'} ] }); t.deepEqual(parse(':foo:bar'), { rest: '', value: [ - {tag: 'named', value: 'foo'}, - {tag: 'named', value: 'bar'} + {tag: 'namedSegment', value: 'foo'}, + {tag: 'namedSegment', value: 'bar'} ] }); t.deepEqual(parse('a'), { rest: '', value: [ - {tag: 'static', value: 'a'} + {tag: 'staticContent', value: 'a'} ] }); t.deepEqual(parse('user42'), { rest: '', value: [ - {tag: 'static', value: 'user42'} + {tag: 'staticContent', value: 'user42'} ] }); t.deepEqual(parse(':a'), { rest: '', value: [ - {tag: 'named', value: 'a'} + {tag: 'namedSegment', value: 'a'} ] }); t.deepEqual(parse('*'), { @@ -151,19 +155,19 @@ test('fixtures', function(t) { t.deepEqual(parse('(foo)'), { rest: '', value: [ - {tag: 'optional', value: [{tag: 'static', value: 'foo'}]} + {tag: 'optionalSegment', value: [{tag: 'staticContent', value: 'foo'}]} ] }); t.deepEqual(parse('(:foo)'), { rest: '', value: [ - {tag: 'optional', value: [{tag: 'named', value: 'foo'}]} + {tag: 'optionalSegment', value: [{tag: 'namedSegment', value: 'foo'}]} ] }); t.deepEqual(parse('(*)'), { rest: '', value: [ - {tag: 'optional', value: [{tag: 'wildcard', value: '*'}]} + {tag: 'optionalSegment', value: [{tag: 'wildcard', value: '*'}]} ] }); @@ -171,23 +175,23 @@ test('fixtures', function(t) { t.deepEqual(parse('/api/users/:id'), { rest: '', value: [ - {tag: 'static', value: '/api/users/'}, - {tag: 'named', value: 'id'} + {tag: 'staticContent', value: '/api/users/'}, + {tag: 'namedSegment', value: 'id'} ] }); t.deepEqual(parse('/v:major(.:minor)/*'), { rest: '', value: [ - {tag: 'static', value: '/v'}, - {tag: 'named', value: 'major'}, + {tag: 'staticContent', value: '/v'}, + {tag: 'namedSegment', value: 'major'}, { - tag: 'optional', + tag: 'optionalSegment', value: [ - {tag: 'static', value: '.'}, - {tag: 'named', value: 'minor'} + {tag: 'staticContent', value: '.'}, + {tag: 'namedSegment', value: 'minor'} ] }, - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'} ] }); @@ -195,32 +199,32 @@ test('fixtures', function(t) { rest: '', value: [ { - tag: 'optional', + tag: 'optionalSegment', value: [ - {tag: 'static', value: 'http'}, + {tag: 'staticContent', value: 'http'}, { - tag: 'optional', + tag: 'optionalSegment', value: [ - {tag: 'static', value: 's'} + {tag: 'staticContent', value: 's'} ] }, - {tag: 'static', value: '://'} + {tag: 'staticContent', value: '://'} ] }, { - tag: 'optional', + tag: 'optionalSegment', value: [ - {tag: 'named', value: 'subdomain'}, - {tag: 'static', value: '.'} + {tag: 'namedSegment', value: 'subdomain'}, + {tag: 'staticContent', value: '.'} ] }, - {tag: 'named', value: 'domain'}, - {tag: 'static', value: '.'}, - {tag: 'named', value: 'tld'}, + {tag: 'namedSegment', value: 'domain'}, + {tag: 'staticContent', value: '.'}, + {tag: 'namedSegment', value: 'tld'}, { - tag: 'optional', + tag: 'optionalSegment', value: [ - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'} ] } @@ -229,30 +233,30 @@ test('fixtures', function(t) { t.deepEqual(parse('/api/users/:ids/posts/:ids'), { rest: '', value: [ - {tag: 'static', value: '/api/users/'}, - {tag: 'named', value: 'ids'}, - {tag: 'static', value: '/posts/'}, - {tag: 'named', value: 'ids'} + {tag: 'staticContent', value: '/api/users/'}, + {tag: 'namedSegment', value: 'ids'}, + {tag: 'staticContent', value: '/posts/'}, + {tag: 'namedSegment', value: 'ids'} ] }); t.deepEqual(parse('/user/:userId/task/:taskId'), { rest: '', value: [ - {tag: 'static', value: '/user/'}, - {tag: 'named', value: 'userId'}, - {tag: 'static', value: '/task/'}, - {tag: 'named', value: 'taskId'} + {tag: 'staticContent', value: '/user/'}, + {tag: 'namedSegment', value: 'userId'}, + {tag: 'staticContent', value: '/task/'}, + {tag: 'namedSegment', value: 'taskId'} ] }); t.deepEqual(parse('.user.:userId.task.:taskId'), { rest: '', value: [ - {tag: 'static', value: '.user.'}, - {tag: 'named', value: 'userId'}, - {tag: 'static', value: '.task.'}, - {tag: 'named', value: 'taskId'} + {tag: 'staticContent', value: '.user.'}, + {tag: 'namedSegment', value: 'userId'}, + {tag: 'staticContent', value: '.task.'}, + {tag: 'namedSegment', value: 'taskId'} ] }); @@ -260,8 +264,8 @@ test('fixtures', function(t) { rest: '', value: [ {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/user/'}, - {tag: 'named', value: 'userId'} + {tag: 'staticContent', value: '/user/'}, + {tag: 'namedSegment', value: 'userId'} ] }); @@ -269,15 +273,15 @@ test('fixtures', function(t) { rest: '', value: [ {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '-user-'}, - {tag: 'named', value: 'userId'} + {tag: 'staticContent', value: '-user-'}, + {tag: 'namedSegment', value: 'userId'} ] }); t.deepEqual(parse('/admin*'), { rest: '', value: [ - {tag: 'static', value: '/admin'}, + {tag: 'staticContent', value: '/admin'}, {tag: 'wildcard', value: '*'} ] }); @@ -285,7 +289,7 @@ test('fixtures', function(t) { t.deepEqual(parse('#admin*'), { rest: '', value: [ - {tag: 'static', value: '#admin'}, + {tag: 'staticContent', value: '#admin'}, {tag: 'wildcard', value: '*'} ] }); @@ -293,69 +297,69 @@ test('fixtures', function(t) { t.deepEqual(parse('/admin/*/user/:userId'), { rest: '', value: [ - {tag: 'static', value: '/admin/'}, + {tag: 'staticContent', value: '/admin/'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/user/'}, - {tag: 'named', value: 'userId'} + {tag: 'staticContent', value: '/user/'}, + {tag: 'namedSegment', value: 'userId'} ] }); t.deepEqual(parse('$admin$*$user$:userId'), { rest: '', value: [ - {tag: 'static', value: '$admin$'}, + {tag: 'staticContent', value: '$admin$'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '$user$'}, - {tag: 'named', value: 'userId'} + {tag: 'staticContent', value: '$user$'}, + {tag: 'namedSegment', value: 'userId'} ] }); t.deepEqual(parse('/admin/*/user/*/tail'), { rest: '', value: [ - {tag: 'static', value: '/admin/'}, + {tag: 'staticContent', value: '/admin/'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/user/'}, + {tag: 'staticContent', value: '/user/'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/tail'} + {tag: 'staticContent', value: '/tail'} ] }); t.deepEqual(parse('/admin/*/user/:id/*/tail'), { rest: '', value: [ - {tag: 'static', value: '/admin/'}, + {tag: 'staticContent', value: '/admin/'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/user/'}, - {tag: 'named', value: 'id'}, - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/user/'}, + {tag: 'namedSegment', value: 'id'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/tail'} + {tag: 'staticContent', value: '/tail'} ] }); t.deepEqual(parse('^admin^*^user^:id^*^tail'), { rest: '', value: [ - {tag: 'static', value: '^admin^'}, + {tag: 'staticContent', value: '^admin^'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '^user^'}, - {tag: 'named', value: 'id'}, - {tag: 'static', value: '^'}, + {tag: 'staticContent', value: '^user^'}, + {tag: 'namedSegment', value: 'id'}, + {tag: 'staticContent', value: '^'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '^tail'} + {tag: 'staticContent', value: '^tail'} ] }); t.deepEqual(parse('/*/admin(/:path)'), { rest: '', value: [ - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/admin'}, - {tag: 'optional', value: [ - {tag: 'static', value: '/'}, - {tag: 'named', value: 'path'} + {tag: 'staticContent', value: '/admin'}, + {tag: 'optionalSegment', value: [ + {tag: 'staticContent', value: '/'}, + {tag: 'namedSegment', value: 'path'} ]} ] }); @@ -363,15 +367,15 @@ test('fixtures', function(t) { t.deepEqual(parse('/'), { rest: '', value: [ - {tag: 'static', value: '/'} + {tag: 'staticContent', value: '/'} ] }); t.deepEqual(parse('(/)'), { rest: '', value: [ - {tag: 'optional', value: [ - {tag: 'static', value: '/'} + {tag: 'optionalSegment', value: [ + {tag: 'staticContent', value: '/'} ]} ] }); @@ -379,35 +383,35 @@ test('fixtures', function(t) { t.deepEqual(parse('/admin(/:foo)/bar'), { rest: '', value: [ - {tag: 'static', value: '/admin'}, - {tag: 'optional', value: [ - {tag: 'static', value: '/'}, - {tag: 'named', value: 'foo'} + {tag: 'staticContent', value: '/admin'}, + {tag: 'optionalSegment', value: [ + {tag: 'staticContent', value: '/'}, + {tag: 'namedSegment', value: 'foo'} ]}, - {tag: 'static', value: '/bar'} + {tag: 'staticContent', value: '/bar'} ] }); t.deepEqual(parse('/admin(*/)foo'), { rest: '', value: [ - {tag: 'static', value: '/admin'}, - {tag: 'optional', value: [ + {tag: 'staticContent', value: '/admin'}, + {tag: 'optionalSegment', value: [ {tag: 'wildcard', value: '*'}, - {tag: 'static', value: '/'} + {tag: 'staticContent', value: '/'} ]}, - {tag: 'static', value: 'foo'} + {tag: 'staticContent', value: 'foo'} ] }); t.deepEqual(parse('/v:major.:minor/*'), { rest: '', value: [ - {tag: 'static', value: '/v'}, - {tag: 'named', value: 'major'}, - {tag: 'static', value: '.'}, - {tag: 'named', value: 'minor'}, - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/v'}, + {tag: 'namedSegment', value: 'major'}, + {tag: 'staticContent', value: '.'}, + {tag: 'namedSegment', value: 'minor'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'} ] }); @@ -415,11 +419,11 @@ test('fixtures', function(t) { t.deepEqual(parse('/v:v.:v/*'), { rest: '', value: [ - {tag: 'static', value: '/v'}, - {tag: 'named', value: 'v'}, - {tag: 'static', value: '.'}, - {tag: 'named', value: 'v'}, - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/v'}, + {tag: 'namedSegment', value: 'v'}, + {tag: 'staticContent', value: '.'}, + {tag: 'namedSegment', value: 'v'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'} ] }); @@ -427,26 +431,25 @@ test('fixtures', function(t) { t.deepEqual(parse('/:foo_bar'), { rest: '', value: [ - {tag: 'static', value: '/'}, - {tag: 'named', value: 'foo'}, - {tag: 'static', value: '_bar'} + {tag: 'staticContent', value: '/'}, + {tag: 'namedSegment', value: 'foo_bar'}, ] }); t.deepEqual(parse('((((a)b)c)d)'), { rest: '', value: [ - {tag: 'optional', value: [ - {tag: 'optional', value: [ - {tag: 'optional', value: [ - {tag: 'optional', value: [ - {tag: 'static', value: 'a'} + {tag: 'optionalSegment', value: [ + {tag: 'optionalSegment', value: [ + {tag: 'optionalSegment', value: [ + {tag: 'optionalSegment', value: [ + {tag: 'staticContent', value: 'a'} ]}, - {tag: 'static', value: 'b'} + {tag: 'staticContent', value: 'b'} ]}, - {tag: 'static', value: 'c'} + {tag: 'staticContent', value: 'c'} ]}, - {tag: 'static', value: 'd'} + {tag: 'staticContent', value: 'd'} ]} ] }); @@ -454,9 +457,9 @@ test('fixtures', function(t) { t.deepEqual(parse('/vvv:version/*'), { rest: '', value: [ - {tag: 'static', value: '/vvv'}, - {tag: 'named', value: 'version'}, - {tag: 'static', value: '/'}, + {tag: 'staticContent', value: '/vvv'}, + {tag: 'namedSegment', value: 'version'}, + {tag: 'staticContent', value: '/'}, {tag: 'wildcard', value: '*'} ] }); diff --git a/test/stringify-fixtures.js b/test/stringify-fixtures.js index d1d8484..7bf5109 100644 --- a/test/stringify-fixtures.js +++ b/test/stringify-fixtures.js @@ -101,16 +101,16 @@ test('stringify', function(t) { pattern = new UrlPattern('/:foo_bar'); t.equal('/a_bar', pattern.stringify({ - foo: 'a'}) + foo_bar: 'a_bar'}) ); t.equal('/a__bar', pattern.stringify({ - foo: 'a_'}) + foo_bar: 'a__bar'}) ); t.equal('/a-b-c-d__bar', pattern.stringify({ - foo: 'a-b-c-d_'}) + foo_bar: 'a-b-c-d__bar'}) ); t.equal('/a b%c-d__bar', pattern.stringify({ - foo: 'a b%c-d_'}) + foo_bar: 'a b%c-d__bar'}) ); pattern = new UrlPattern('((((a)b)c)d)'); From c0eadbb97e2b9f542b0ce6ff43bf46402241d653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 13:44:59 -0500 Subject: [PATCH 042/117] package.json: add tslint --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a5dae3..6fcbd19 100644 --- a/package.json +++ b/package.json @@ -75,13 +75,15 @@ "esm": "^3.2.22", "codecov.io": "0.1.6", "tape": "4.10.1", - "zuul": "3.12.0" + "zuul": "3.12.0", + "tslint": "^5.16.0" }, "main": "lib/url-pattern", "scripts": { "compile": "tsc", "docs": "typedoc --out doc --module UrlPattern index.ts", "prepublish": "npm run compile", + "lint": "tslint --project .", "pretest": "npm run compile", "test": "tape -r esm test/*", "test-with-coverage": "istanbul cover coffeetape test/* && cat ./coverage/coverage.json | ./node_modules/codecov.io/bin/codecov.io.js", From ab6d4de6bbb5b1bfc06282c740a58d955c9d7efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 13:46:43 -0500 Subject: [PATCH 043/117] package.json: add typescript --- package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6fcbd19..640f84d 100644 --- a/package.json +++ b/package.json @@ -72,11 +72,12 @@ }, "dependencies": {}, "devDependencies": { + "typescript": "^3.4.5", + "tslint": "^5.16.0", "esm": "^3.2.22", - "codecov.io": "0.1.6", - "tape": "4.10.1", - "zuul": "3.12.0", - "tslint": "^5.16.0" + "codecov.io": "^0.1.6", + "tape": "^4.10.1", + "zuul": "^3.12.0" }, "main": "lib/url-pattern", "scripts": { From 32ee1591892f03ebaf6bc1f4797a213ffa7a08cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 13:50:55 -0500 Subject: [PATCH 044/117] package.json: fix doc command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 640f84d..d05f592 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "main": "lib/url-pattern", "scripts": { "compile": "tsc", - "docs": "typedoc --out doc --module UrlPattern index.ts", + "doc": "typedoc --out doc .", "prepublish": "npm run compile", "lint": "tslint --project .", "pretest": "npm run compile", From 356477a91ae1a30d32e250fb18509c87839922a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 13:53:30 -0500 Subject: [PATCH 045/117] fix docstrings for helpers --- src/helpers.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 9becde9..0d0cf5b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,4 +1,4 @@ -/* +/** * escapes a string for insertion into a regular expression * source: http://stackoverflow.com/a/3561711 */ @@ -6,7 +6,7 @@ export function escapeStringForRegex(str: string): string { return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); } -/* +/** * like `Array.prototype.map` except that the function `f` * returns an array and `concatMap` returns the concatenation * of all arrays returned by `f` @@ -19,7 +19,7 @@ export function concatMap(array: T[], f: (x: T) => U[]): U[] { return results; } -/* +/** * like `Array.prototype.map` except that the function `f` * returns a string and `stringConcatMap` returns the concatenation * of all strings returned by `f` @@ -32,7 +32,7 @@ export function stringConcatMap(array: T[], f: (x: T) => string): string { return result; } -/* +/** * returns the number of groups in the `regex`. * source: http://stackoverflow.com/a/16047223 */ @@ -45,7 +45,7 @@ export function regexGroupCount(regex: RegExp): number { return matches.length - 1; } -/* +/** * zips an array of `keys` and an array of `values` into an object * so `keys[i]` is associated with `values[i]` for every i. * `keys` and `values` must have the same length. From 01d87c16f6b36299ba739cb169f1acaf47f03dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 13:53:37 -0500 Subject: [PATCH 046/117] better type variable names for concatMap --- src/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 0d0cf5b..9f404fc 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -11,8 +11,8 @@ export function escapeStringForRegex(str: string): string { * returns an array and `concatMap` returns the concatenation * of all arrays returned by `f` */ -export function concatMap(array: T[], f: (x: T) => U[]): U[] { - let results: U[] = []; +export function concatMap(array: Input[], f: (x: Input) => Output[]): Output[] { + let results: Output[] = []; for (const value of array) { results = results.concat(f(value)); } From 50fa301bdde9638e3f504c6de67ce85ee79166b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 14:02:31 -0500 Subject: [PATCH 047/117] package.json: remove engines --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index d05f592..b1d2248 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,6 @@ "url": "git://github.com/snd/url-pattern.git" }, "license": "MIT", - "engines": { - "node": ">=0.12.0" - }, "dependencies": {}, "devDependencies": { "typescript": "^3.4.5", From 7f80b5f39c9804ff158aa20aa8b11418e661feb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 14:02:42 -0500 Subject: [PATCH 048/117] .travis.yml: try to get minimal CI working --- .travis.yml | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index dc308e7..7a2693c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ language: node_js node_js: - - "0.12" - - "iojs-3" - - "4" - - "5" + - "12.1", + - "10.15", script: npm run $NPM_COMMAND sudo: false env: @@ -14,24 +12,24 @@ env: - secure: "idJFmSy6EyMNO9UoxUx0wG83G/w8H1Sh1fG5lWodAdV01/Ft0j3KQo/zelENBx7zMWf+iqdWOhL4rBLIIkaajHbmvkMYDzhFXK4GIZmd1HnV4MZCunipscMsEbtQU+uTY/I3fersnIz74aTuj3SKlFW4jVNgvc8fawijBtTbuhU=" matrix: - NPM_COMMAND=test - - NPM_COMMAND=test-with-coverage - - NPM_COMMAND=test-in-browsers + # - NPM_COMMAND=test-with-coverage + # - NPM_COMMAND=test-in-browsers matrix: exclude: # don't test in browsers more than once (already done with node 5) - - node_js: "0.12" - env: NPM_COMMAND=test-in-browsers - - node_js: "iojs-3" - env: NPM_COMMAND=test-in-browsers - - node_js: "4" - env: NPM_COMMAND=test-in-browsers + # - node_js: "0.12" + # env: NPM_COMMAND=test-in-browsers + # - node_js: "iojs-3" + # env: NPM_COMMAND=test-in-browsers + # - node_js: "4" + # env: NPM_COMMAND=test-in-browsers # don't collect code coverage more than once (already done with node 5) - - node_js: "0.12" - env: NPM_COMMAND=test-with-coverage - - node_js: "iojs-3" - env: NPM_COMMAND=test-with-coverage - - node_js: "4" - env: NPM_COMMAND=test-with-coverage + # - node_js: "0.12" + # env: NPM_COMMAND=test-with-coverage + # - node_js: "iojs-3" + # env: NPM_COMMAND=test-with-coverage + # - node_js: "4" + # env: NPM_COMMAND=test-with-coverage # already tested with coverage (with node 5). no need to test again without - - node_js: "5" - env: NPM_COMMAND=test + # - node_js: "5" + # env: NPM_COMMAND=test From 20d84ed9ae52047040c13420613f486e39854cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 14:05:04 -0500 Subject: [PATCH 049/117] .travis.yml: fixes --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a2693c..8f597b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js node_js: - - "12.1", - - "10.15", + - "12.1" + - "10.15" script: npm run $NPM_COMMAND sudo: false env: @@ -14,8 +14,8 @@ env: - NPM_COMMAND=test # - NPM_COMMAND=test-with-coverage # - NPM_COMMAND=test-in-browsers -matrix: - exclude: +# matrix: +# exclude: # don't test in browsers more than once (already done with node 5) # - node_js: "0.12" # env: NPM_COMMAND=test-in-browsers From 75a5a05f1f5be1d6ac1bff79209f77326e32449c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 30 Apr 2019 23:55:41 -0500 Subject: [PATCH 050/117] update readme --- README.md | 137 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 8cd7832..6fb619b 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,25 @@ turn strings into data or data into strings.** [make pattern:](#make-pattern-from-string) ``` javascript -var pattern = new UrlPattern('/api/users(/:id)'); +> const pattern = new UrlPattern("/api/users(/:id)"); ``` [match pattern against string and extract values:](#match-pattern-against-string) ``` javascript -pattern.match('/api/users/10'); // {id: '10'} -pattern.match('/api/users'); // {} -pattern.match('/api/products/5'); // null +> pattern.match("/api/users/10"); +{id: "10"} + +> pattern.match("/api/users"); +{} + +> pattern.match("/api/products/5"); +null ``` [generate string from pattern and values:](#stringify-patterns) ``` javascript -pattern.stringify() // '/api/users' -pattern.stringify({id: 20}) // '/api/users/20' +> pattern.stringify() // "/api/users" +pattern.stringify({id: 20}) // "/api/users/20" ``` - continuously tested in Node.js (0.12, 4.2.3 and 5.3) and all relevant browsers: @@ -39,7 +44,7 @@ pattern.stringify({id: 20}) // '/api/users/20' code coverage - widely used [![Downloads per Month](https://img.shields.io/npm/dm/url-pattern.svg?style=flat)](https://www.npmjs.org/package/url-pattern) - supports CommonJS, [AMD](http://requirejs.org/docs/whyamd.html) and browser globals - - `require('url-pattern')` + - `require("url-pattern")` - use [lib/url-pattern.js](lib/url-pattern.js) in the browser - sets the global variable `UrlPattern` when neither CommonJS nor [AMD](http://requirejs.org/docs/whyamd.html) are available. - very fast matching as each pattern is compiled into a regex exactly once @@ -51,8 +56,6 @@ pattern.stringify({id: 20}) // '/api/users/20' - pattern parser implemented using simple, combosable, testable [parser combinators](https://en.wikipedia.org/wiki/Parser_combinator) - [typescript typings](index.d.ts) -[check out **passage** if you are looking for simple composable routing that builds on top of url-pattern](https://github.com/snd/passage) - ``` npm install url-pattern ``` @@ -62,44 +65,44 @@ bower install url-pattern ``` ```javascript -> var UrlPattern = require('url-pattern'); +const UrlPattern = require("url-pattern"); ``` ``` javascript -> var pattern = new UrlPattern('/v:major(.:minor)/*'); +> const pattern = new UrlPattern("/v:major(.:minor)/*"); -> pattern.match('/v1.2/'); -{major: '1', minor: '2', _: ''} +> pattern.match("/v1.2/"); +{major: "1", minor: "2", _: ""} -> pattern.match('/v2/users'); -{major: '2', _: 'users'} +> pattern.match("/v2/users"); +{major: "2", _: "users"} -> pattern.match('/v/'); +> pattern.match("/v/"); null ``` ``` javascript -> var pattern = new UrlPattern('(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)') +> var pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)") -> pattern.match('google.de'); -{domain: 'google', tld: 'de'} +> pattern.match("google.de"); +{domain: "google", tld: "de"} -> pattern.match('https://www.google.com'); -{subdomain: 'www', domain: 'google', tld: 'com'} +> pattern.match("https://www.google.com"); +{subdomain: "www", domain: "google", tld: "com"} -> pattern.match('http://mail.google.com/mail'); -{subdomain: 'mail', domain: 'google', tld: 'com', _: 'mail'} +> pattern.match("http://mail.google.com/mail"); +{subdomain: "mail", domain: "google", tld: "com", _: "mail"} -> pattern.match('http://mail.google.com:80/mail'); -{subdomain: 'mail', domain: 'google', tld: 'com', port: '80', _: 'mail'} +> pattern.match("http://mail.google.com:80/mail"); +{subdomain: "mail", domain: "google", tld: "com", port: "80", _: "mail"} -> pattern.match('google'); +> pattern.match("google"); null ``` ## make pattern from string ```javascript -> var pattern = new UrlPattern('/api/users/:id'); +> var pattern = new UrlPattern("/api/users/:id"); ``` a `pattern` is immutable after construction. @@ -111,14 +114,14 @@ that makes it easier to reason about. match returns the extracted segments: ```javascript -> pattern.match('/api/users/10'); -{id: '10'} +> pattern.match("/api/users/10"); +{id: "10"} ``` or `null` if there was no match: ``` javascript -> pattern.match('/api/products/5'); +> pattern.match("/api/products/5"); null ``` @@ -141,9 +144,9 @@ if a named segment **name** occurs more than once in the pattern string, then the multiple results are stored in an array on the returned object: ```javascript -> var pattern = new UrlPattern('/api/users/:ids/posts/:ids'); -> pattern.match('/api/users/10/posts/5'); -{ids: ['10', '5']} +> var pattern = new UrlPattern("/api/users/:ids/posts/:ids"); +> pattern.match("/api/users/10/posts/5"); +{ids: ["10", "5"]} ``` ## optional segments, wildcards and escaping @@ -152,7 +155,7 @@ to make part of a pattern optional just wrap it in `(` and `)`: ```javascript > var pattern = new UrlPattern( - '(http(s)\\://)(:subdomain.):domain.:tld(/*)' + "(http(s)\\://)(:subdomain.):domain.:tld(/*)" ); ``` @@ -163,21 +166,21 @@ url-pattern. optional named segments are stored in the corresponding property only if they are present in the source string: ```javascript -> pattern.match('google.de'); -{domain: 'google', tld: 'de'} +> pattern.match("google.de"); +{domain: "google", tld: "de"} ``` ```javascript -> pattern.match('https://www.google.com'); -{subdomain: 'www', domain: 'google', tld: 'com'} +> pattern.match("https://www.google.com"); +{subdomain: "www", domain: "google", tld: "com"} ``` `*` in patterns are wildcards and match anything. wildcard matches are collected in the `_` property: ```javascript -> pattern.match('http://mail.google.com/mail'); -{subdomain: 'mail', domain: 'google', tld: 'com', _: 'mail'} +> pattern.match("http://mail.google.com/mail"); +{subdomain: "mail", domain: "google", tld: "com", _: "mail"} ``` if there is only one wildcard then `_` contains the matching string. @@ -194,10 +197,10 @@ otherwise `_` contains an array of matching strings. if the pattern was created from a regex an array of the captured groups is returned on a match: ```javascript -> pattern.match('/api/users'); -['users'] +> pattern.match("/api/users"); +["users"] -> pattern.match('/apiii/test'); +> pattern.match("/apiii/test"); null ``` @@ -208,39 +211,39 @@ returns objects on match with each key mapped to a captured value: ```javascript > var pattern = new UrlPattern( /^\/api\/([^\/]+)(?:\/(\d+))?$/, - ['resource', 'id'] + ["resource", "id"] ); -> pattern.match('/api/users'); -{resource: 'users'} +> pattern.match("/api/users"); +{resource: "users"} -> pattern.match('/api/users/5'); -{resource: 'users', id: '5'} +> pattern.match("/api/users/5"); +{resource: "users", id: "5"} -> pattern.match('/api/users/foo'); +> pattern.match("/api/users/foo"); null ``` ## stringify patterns ```javascript -> var pattern = new UrlPattern('/api/users/:id'); +> var pattern = new UrlPattern("/api/users/:id"); > pattern.stringify({id: 10}) -'/api/users/10' +"/api/users/10" ``` optional segments are only included in the output if they contain named segments and/or wildcards and values for those are provided: ```javascript -> var pattern = new UrlPattern('/api/users(/:id)'); +> var pattern = new UrlPattern("/api/users(/:id)"); > pattern.stringify() -'/api/users' +"/api/users" > pattern.stringify({id: 10}) -'/api/users/10' +"/api/users/10" ``` wildcards (key = `_`), deeply nested optional groups and multiple value arrays should stringify as expected. @@ -265,47 +268,47 @@ finally we can completely change pattern-parsing and regex-compilation to suit o let's change the char used for escaping (default `\\`): ```javascript -> options.escapeChar = '!'; +> options.escapeChar = "!"; ``` let's change the char used to start a named segment (default `:`): ```javascript -> options.segmentNameStartChar = '$'; +> options.segmentNameStartChar = "$"; ``` let's change the set of chars allowed in named segment names (default `a-zA-Z0-9`) to also include `_` and `-`: ```javascript -> options.segmentNameCharset = 'a-zA-Z0-9_-'; +> options.segmentNameCharset = "a-zA-Z0-9_-"; ``` let's change the set of chars allowed in named segment values (default `a-zA-Z0-9-_~ %`) to not allow non-alphanumeric chars: ```javascript -> options.segmentValueCharset = 'a-zA-Z0-9'; +> options.segmentValueCharset = "a-zA-Z0-9"; ``` let's change the chars used to surround an optional segment (default `(` and `)`): ```javascript -> options.optionalSegmentStartChar = '['; -> options.optionalSegmentEndChar = ']'; +> options.optionalSegmentStartChar = "["; +> options.optionalSegmentEndChar = "]"; ``` let's change the char used to denote a wildcard (default `*`): ```javascript -> options.wildcardChar = '?'; +> options.wildcardChar = "?"; ``` pass options as the second argument to the constructor: ```javascript > var pattern = new UrlPattern( - '[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]', + "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]", options ); ``` @@ -313,12 +316,12 @@ pass options as the second argument to the constructor: then match: ```javascript -> pattern.match('http://mail.google.com/mail'); +> pattern.match("http://mail.google.com/mail"); { - sub_domain: 'mail', - domain: 'google', - 'toplevel-domain': 'com', - _: 'mail' + sub_domain: "mail", + domain: "google", + "toplevel-domain": "com", + _: "mail" } ``` From 5a01fc5a8304738138a0646e7f54823c679b0a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 5 May 2019 22:31:21 -0500 Subject: [PATCH 051/117] lint on travis --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8f597b1..1365707 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,9 @@ language: node_js node_js: - "12.1" - "10.15" -script: npm run $NPM_COMMAND +script: + - npm run lint + - npm run $NPM_COMMAND sudo: false env: global: From 167c476e4f195c4dfb69392a76108e64e35c9f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 5 May 2019 22:31:41 -0500 Subject: [PATCH 052/117] better docs and names for parsers --- src/parser.ts | 77 ++++++++-------- src/parsercombinators.ts | 183 +++++++++++++++++++++------------------ 2 files changed, 137 insertions(+), 123 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 4f5a1a8..1594f5a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -4,15 +4,15 @@ import { Ast, - concatMany1Till, - firstChoice, - lazy, - many1, newAst, + newAtLeastOneParser, + newConcatAtLeastOneUntilParser, + newEitherParser, + newLazyParser, + newPickNthParser, + newRegexParser, + newStringParser, Parser, - pick, - regex, - string, } from "./parsercombinators"; import { @@ -27,63 +27,64 @@ import { } from "./options"; export function newEscapedCharParser(options: IOptions): Parser> { - return pick(1, string(options.escapeChar), regex(/^./)); + return newPickNthParser(1, newStringParser(options.escapeChar), newRegexParser(/^./)); } export function newWildcardParser(options: IOptions): Parser> { - return newAst("wildcard", string(options.wildcardChar)); + return newAst("wildcard", newStringParser(options.wildcardChar)); } /* * parses just the segment name in a named segment */ export function newSegmentNameParser(options: IOptions): Parser { - return regex(new RegExp(`^[${ options.segmentNameCharset }]+`)); + return newRegexParser(new RegExp(`^[${ options.segmentNameCharset }]+`)); } export function newNamedSegmentParser(options: IOptions): Parser> { const parseSegmentName = newSegmentNameParser(options); if (options.segmentNameEndChar == null) { - return newAst("namedSegment", pick(1, - string(options.segmentNameStartChar), + return newAst("namedSegment", newPickNthParser(1, + newStringParser(options.segmentNameStartChar), parseSegmentName)); } else { - return newAst("namedSegment", pick(1, - string(options.segmentNameStartChar), + return newAst("namedSegment", newPickNthParser(1, + newStringParser(options.segmentNameStartChar), parseSegmentName, - string(options.segmentNameEndChar))); + newStringParser(options.segmentNameEndChar))); } } export function newNamedWildcardParser(options: IOptions): Parser> { if (options.segmentNameEndChar == null) { - return newAst("namedWildcard", pick(2, - string(options.wildcardChar), - string(options.segmentNameStartChar), + return newAst("namedWildcard", newPickNthParser(2, + newStringParser(options.wildcardChar), + newStringParser(options.segmentNameStartChar), newSegmentNameParser(options), )); } else { - return newAst("namedWildcard", pick(2, - string(options.wildcardChar), - string(options.segmentNameStartChar), + return newAst("namedWildcard", newPickNthParser(2, + newStringParser(options.wildcardChar), + newStringParser(options.segmentNameStartChar), newSegmentNameParser(options), - string(options.segmentNameEndChar), + newStringParser(options.segmentNameEndChar), )); } } export function newStaticContentParser(options: IOptions): Parser> { - return newAst("staticContent", concatMany1Till(firstChoice( + const parseUntil = newEitherParser( + newStringParser(options.segmentNameStartChar), + newStringParser(options.optionalSegmentStartChar), + newStringParser(options.optionalSegmentEndChar), + newWildcardParser(options), + newNamedWildcardParser(options), + ); + return newAst("staticContent", newConcatAtLeastOneUntilParser(newEitherParser( newEscapedCharParser(options), - regex(/^./)), + newRegexParser(/^./)), // parse any normal or escaped char until the following matches: - firstChoice( - string(options.segmentNameStartChar), - string(options.optionalSegmentStartChar), - string(options.optionalSegmentEndChar), - newWildcardParser(options), - newNamedWildcardParser(options), - ), + parseUntil, )); } @@ -95,16 +96,16 @@ export function newUrlPatternParser(options: IOptions): Parser> { throw new Error(` this is just a temporary placeholder to make a circular dependency work. - that this got called is a bug + if you see this error it's a bug. `); }; - const parseOptionalSegment = newAst("optionalSegment", pick(1, - string(options.optionalSegmentStartChar), - lazy(() => parsePattern), - string(options.optionalSegmentEndChar))); + const parseOptionalSegment = newAst("optionalSegment", newPickNthParser(1, + newStringParser(options.optionalSegmentStartChar), + newLazyParser(() => parsePattern), + newStringParser(options.optionalSegmentEndChar))); - const parseToken = firstChoice( + const parseToken = newEitherParser( newNamedWildcardParser(options), newWildcardParser(options), parseOptionalSegment, @@ -112,7 +113,7 @@ export function newUrlPatternParser(options: IOptions): Parser> { newStaticContentParser(options), ); - parsePattern = many1(parseToken); + parsePattern = newAtLeastOneParser(parseToken); return parsePattern; } diff --git a/src/parsercombinators.ts b/src/parsercombinators.ts index 205ed05..f1095b9 100644 --- a/src/parsercombinators.ts +++ b/src/parsercombinators.ts @@ -1,8 +1,8 @@ -/* +/** * generic parser combinators used to build the url pattern parser (module `parser`) */ -/* +/** * parse result */ export class Result { @@ -16,6 +16,38 @@ export class Result { } } +/** + * a parser is a function that takes a string and returns a `Result` + * containing a parsed `Result.value` and the rest of the string `Result.rest` + */ +export type Parser = (str: string) => Result | undefined; + +/* + * returns a parser that consumes `str` exactly + */ +export function newStringParser(str: string): Parser { + const { length } = str; + return (input: string) => { + if (input.slice(0, length) === str) { + return new Result(str, input.slice(length)); + } + }; +} + +/** + * returns a parser that consumes everything matched by `regexp` + */ +export function newRegexParser(regexp: RegExp): Parser { + return (input: string) => { + const matches = regexp.exec(input); + if (matches == null) { + return; + } + const result = matches[0]; + return new Result(result, input.slice(result.length)); + }; +} + /** * node in the AST (abstract syntax tree) */ @@ -28,13 +60,7 @@ export class Ast { } } -/* - * a parser is a function that takes a string and returns a `Result` - * containing a parsed `Result.value` and the rest of the string `Result.rest` - */ -export type Parser = (str: string) => Result | undefined; - -/* +/** * transforms a `parser` into a parser that returns an Ast node */ export function newAst(tag: string, parser: Parser): Parser> { @@ -49,24 +75,11 @@ export function newAst(tag: string, parser: Parser): Parser> { } /* - * parser that consumes everything matched by `regex` - */ -export function regex(regexp: RegExp): Parser { - return (input: string) => { - const matches = regexp.exec(input); - if (matches == null) { - return; - } - const result = matches[0]; - return new Result(result, input.slice(result.length)); - }; -} - -/* - * takes a sequence of parsers and returns a parser that runs - * them in sequence and produces an array of their results + * takes many `parsers`. + * returns a new parser that runs + * all `parsers` in sequence and returns an array of their results */ -export function sequence(...parsers: Array>): Parser { +export function newSequenceParser(...parsers: Array>): Parser { return (input: string) => { let rest = input; const values: any[] = []; @@ -83,23 +96,13 @@ export function sequence(...parsers: Array>): Parser { } /* - * returns a parser that consumes `str` exactly - */ -export function string(str: string): Parser { - const { length } = str; - return (input: string) => { - if (input.slice(0, length) === str) { - return new Result(str, input.slice(length)); - } - }; -} - -/* + * takes an `index` and many `parsers` + * * takes a sequence of parser and only returns the result * returned by the `index`th parser */ -export function pick(index: number, ...parsers: Array>): Parser { - const parser = sequence(...parsers); +export function newPickNthParser(index: number, ...parsers: Array>): Parser { + const parser = newSequenceParser(...parsers); return (input: string) => { const result = parser(input); if (result == null) { @@ -113,7 +116,7 @@ export function pick(index: number, ...parsers: Array>): Parser * for parsers that each depend on one another (cyclic dependencies) * postpone lookup to when they both exist. */ -export function lazy(getParser: () => Parser): Parser { +export function newLazyParser(getParser: () => Parser): Parser { let cachedParser: Parser | null = null; return (input: string) => { if (cachedParser == null) { @@ -123,66 +126,76 @@ export function lazy(getParser: () => Parser): Parser { }; } -/* - * base function for parsers that parse multiples. - * - * @param endParser once the `endParser` (if not null) consumes - * the `baseMany` parser returns. the result of the `endParser` is ignored. +/** + * takes a `parser` and returns a parser that parses + * many occurences of the parser + * returns the results collected in an array. */ -export function baseMany( - parser: Parser, - endParser: Parser | null, - isAtLeastOneResultRequired: boolean, - input: string, -): Result | undefined { - let rest = input; - const results: T[] = []; - while (true) { - if (endParser != null) { - const endResult = endParser(rest); - if (endResult != null) { +export function newAtLeastOneParser(parser: Parser): Parser { + return (input: string) => { + let rest = input; + const results: T[] = []; + while (true) { + const parserResult = parser(rest); + if (parserResult == null) { break; } + results.push(parserResult.value); + rest = parserResult.rest; } - const parserResult = parser(rest); - if (parserResult == null) { - break; - } - results.push(parserResult.value); - rest = parserResult.rest; - } - - if (isAtLeastOneResultRequired && results.length === 0) { - return; - } - return new Result(results, rest); -} + if (results.length === 0) { + return; + } -export function many1(parser: Parser): Parser { - return (input: string) => { - const endParser: null = null; - const isAtLeastOneResultRequired = true; - return baseMany(parser, endParser, isAtLeastOneResultRequired, input); + return new Result(results, rest); }; } -export function concatMany1Till(parser: Parser, endParser: Parser): Parser { +/** + * takes a `parser` returning strings. + * returns a parser that parses + * at least one occurence of `parser` and concatenates the results. + * stops parsing whenever `endParser` matches and ignores the `endParser` result. + */ +export function newConcatAtLeastOneUntilParser(parser: Parser, endParser: Parser): Parser { return (input: string) => { - const isAtLeastOneResultRequired = true; - const result = baseMany(parser, endParser, isAtLeastOneResultRequired, input); - if (result == null) { + let hasAtLeastOneMatch = false; + let rest = input; + let result = ""; + + while (true) { + if (endParser != null) { + if (endParser(rest) != null) { + break; + } + } + + const parserResult = parser(rest); + if (parserResult == null) { + break; + } + + hasAtLeastOneMatch = true; + result += parserResult.value; + rest = parserResult.rest; + } + + if (!hasAtLeastOneMatch) { return; } - return new Result(result.value.join(""), result.rest); + + return new Result(result, rest); }; } -/* - * takes a sequence of parsers. returns the result from the first - * parser that consumes the input. +/** + * takes many `parsers`. + * returns a new parser that tries all `parsers` in order + * and stops and returns as soon as a parser returns a non-null result. */ -export function firstChoice(...parsers: Array>): Parser { +// TODO any +export function newEitherParser(...parsers: Array>): Parser { return (input: string) => { for (const parser of parsers) { const result = parser(input); From c2ab44501e77bf0fea7a78a794a4189a1e5e91b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:09:45 -0500 Subject: [PATCH 053/117] run npm audit on travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1365707..b7a3567 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ node_js: - "12.1" - "10.15" script: + - npm audit - npm run lint - npm run $NPM_COMMAND sudo: false From b6a053ea68de56ba9f36562caa87c8bb752181d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:10:21 -0500 Subject: [PATCH 054/117] make ts-node tape and nyc interact to produce coverage --- package.json | 30 +++++++++++++++++++++--------- tsconfig.json | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b1d2248..e123369 100644 --- a/package.json +++ b/package.json @@ -69,12 +69,12 @@ "license": "MIT", "dependencies": {}, "devDependencies": { - "typescript": "^3.4.5", - "tslint": "^5.16.0", - "esm": "^3.2.22", - "codecov.io": "^0.1.6", + "@types/tape": "^4.2.33", + "nyc": "^14.1.0", "tape": "^4.10.1", - "zuul": "^3.12.0" + "ts-node": "^8.1.0", + "tslint": "^5.16.0", + "typescript": "^3.4.5" }, "main": "lib/url-pattern", "scripts": { @@ -82,11 +82,23 @@ "doc": "typedoc --out doc .", "prepublish": "npm run compile", "lint": "tslint --project .", - "pretest": "npm run compile", - "test": "tape -r esm test/*", - "test-with-coverage": "istanbul cover coffeetape test/* && cat ./coverage/coverage.json | ./node_modules/codecov.io/bin/codecov.io.js", + "test": "tape -r ts-node/register test/*.ts", + "coverage": "nyc npm test", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" }, - "typings": "index.d.ts" + "typings": "index.d.ts", + "nyc": { + "include": [ + "src/*.ts" + ], + "extension": [ + ".ts" + ], + "reporter": [ + "json", + "html" + ], + "all": true + } } diff --git a/tsconfig.json b/tsconfig.json index 91b0bab..21870b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "ES6", + "module": "commonjs", "target": "ES6", "strict": true, "noUnusedLocals": true, From 0469a016eed892614c4401e2b892b4d93f1b59d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:16:10 -0500 Subject: [PATCH 055/117] mv test/helpers.{js,ts} --- test/helpers.js | 135 ------------------------------------------------ test/helpers.ts | 135 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 135 deletions(-) delete mode 100644 test/helpers.js create mode 100644 test/helpers.ts diff --git a/test/helpers.js b/test/helpers.js deleted file mode 100644 index f2e4c3b..0000000 --- a/test/helpers.js +++ /dev/null @@ -1,135 +0,0 @@ -import test from "tape"; - -import { - escapeStringForRegex, - concatMap, - stringConcatMap, - regexGroupCount, - keysAndValuesToObject -} from "../dist/helpers.js"; - -test('escapeStringForRegex', function(t) { - const expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]'; - const actual = escapeStringForRegex('[-\/\\^$*+?.()|[\]{}]'); - t.equal(expected, actual); - - t.equal(escapeStringForRegex('a$98kdjf(kdj)'), 'a\\$98kdjf\\(kdj\\)'); - t.equal('a', escapeStringForRegex('a')); - t.equal('!', escapeStringForRegex('!')); - t.equal('\\.', escapeStringForRegex('.')); - t.equal('\\/', escapeStringForRegex('/')); - t.equal('\\-', escapeStringForRegex('-')); - t.equal('\\-', escapeStringForRegex('-')); - t.equal('\\[', escapeStringForRegex('[')); - t.equal('\\]', escapeStringForRegex(']')); - t.equal('\\(', escapeStringForRegex('(')); - t.equal('\\)', escapeStringForRegex(')')); - t.end(); -}); - -test('concatMap', function(t) { - t.deepEqual([], concatMap([], function() {})); - t.deepEqual([1], concatMap([1], x => [x])); - t.deepEqual([1, 1, 1, 2, 2, 2, 3, 3, 3], concatMap([1, 2, 3], x => [x, x, x])); - t.end(); -}); - -test('stringConcatMap', function(t) { - t.equal('', stringConcatMap([], function() {})); - t.equal('1', stringConcatMap([1], x => x)); - t.equal('123', stringConcatMap([1, 2, 3], x => x)); - t.equal('1a2a3a', stringConcatMap([1, 2, 3], x => x + 'a')); - t.end(); -}); - -test('regexGroupCount', function(t) { - t.equal(0, regexGroupCount(/foo/)); - t.equal(1, regexGroupCount(/(foo)/)); - t.equal(2, regexGroupCount(/((foo))/)); - t.equal(2, regexGroupCount(/(fo(o))/)); - t.equal(2, regexGroupCount(/f(o)(o)/)); - t.equal(2, regexGroupCount(/f(o)o()/)); - t.equal(5, regexGroupCount(/f(o)o()()(())/)); - t.end(); -}); - -test('keysAndValuesToObject', function(t) { - t.deepEqual( - keysAndValuesToObject( - [], - [] - ), - {} - ); - t.deepEqual( - keysAndValuesToObject( - ['one'], - [1] - ), - { - one: 1 - } - ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two'], - [1, 2, 3] - ), - { - one: 1, - two: [2, 3] - } - ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two'], - [1, 2, 3, null] - ), - { - one: 1, - two: [2, 3] - } - ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two'], - [1, 2, 3, 4] - ), - { - one: 1, - two: [2, 3, 4] - } - ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two', 'three'], - [1, 2, 3, 4, undefined] - ), - { - one: 1, - two: [2, 3, 4] - } - ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two', 'three'], - [1, 2, 3, 4, 5] - ), - { - one: 1, - two: [2, 3, 4], - three: 5 - } - ); - t.deepEqual( - keysAndValuesToObject( - ['one', 'two', 'two', 'two', 'three'], - [null, 2, 3, 4, 5] - ), - { - two: [2, 3, 4], - three: 5 - } - ); - t.end(); -}); diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 0000000..8653dc9 --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,135 @@ +import * as tape from "tape"; + +import { + concatMap, + escapeStringForRegex, + keysAndValuesToObject, + regexGroupCount, + stringConcatMap, +} from "../src/helpers"; + +tape("escapeStringForRegex", (t: tape.Test) => { + const expected = "\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]"; + const actual = escapeStringForRegex("[-\/\\^$*+?.()|[\]{}]"); + t.equal(expected, actual); + + t.equal(escapeStringForRegex("a$98kdjf(kdj)"), "a\\$98kdjf\\(kdj\\)"); + t.equal("a", escapeStringForRegex("a")); + t.equal("!", escapeStringForRegex("!")); + t.equal("\\.", escapeStringForRegex(".")); + t.equal("\\/", escapeStringForRegex("/")); + t.equal("\\-", escapeStringForRegex("-")); + t.equal("\\-", escapeStringForRegex("-")); + t.equal("\\[", escapeStringForRegex("[")); + t.equal("\\]", escapeStringForRegex("]")); + t.equal("\\(", escapeStringForRegex("(")); + t.equal("\\)", escapeStringForRegex(")")); + t.end(); +}); + +tape("concatMap", (t: tape.Test) => { + t.deepEqual([], concatMap([], () => [])); + t.deepEqual([1], concatMap([1], (x) => [x])); + t.deepEqual([1, 1, 1, 2, 2, 2, 3, 3, 3], concatMap([1, 2, 3], (x) => [x, x, x])); + t.end(); +}); + +tape("stringConcatMap", (t: tape.Test) => { + t.equal("", stringConcatMap([], () => "")); + t.equal("1", stringConcatMap([1], (x) => x.toString())); + t.equal("123", stringConcatMap([1, 2, 3], (x) => x.toString())); + t.equal("1a2a3a", stringConcatMap([1, 2, 3], (x) => x + "a")); + t.end(); +}); + +tape("regexGroupCount", (t: tape.Test) => { + t.equal(0, regexGroupCount(/foo/)); + t.equal(1, regexGroupCount(/(foo)/)); + t.equal(2, regexGroupCount(/((foo))/)); + t.equal(2, regexGroupCount(/(fo(o))/)); + t.equal(2, regexGroupCount(/f(o)(o)/)); + t.equal(2, regexGroupCount(/f(o)o()/)); + t.equal(5, regexGroupCount(/f(o)o()()(())/)); + t.end(); +}); + +tape("keysAndValuesToObject", (t: tape.Test) => { + t.deepEqual( + keysAndValuesToObject( + [], + [], + ), + {}, + ); + t.deepEqual( + keysAndValuesToObject( + ["one"], + [1], + ), + { + one: 1, + }, + ); + t.deepEqual( + keysAndValuesToObject( + ["one", "two", "two"], + [1, 2, 3], + ), + { + one: 1, + two: [2, 3], + }, + ); + t.deepEqual( + keysAndValuesToObject( + ["one", "two", "two", "two"], + [1, 2, 3, null], + ), + { + one: 1, + two: [2, 3], + }, + ); + t.deepEqual( + keysAndValuesToObject( + ["one", "two", "two", "two"], + [1, 2, 3, 4], + ), + { + one: 1, + two: [2, 3, 4], + }, + ); + t.deepEqual( + keysAndValuesToObject( + ["one", "two", "two", "two", "three"], + [1, 2, 3, 4, undefined], + ), + { + one: 1, + two: [2, 3, 4], + }, + ); + t.deepEqual( + keysAndValuesToObject( + ["one", "two", "two", "two", "three"], + [1, 2, 3, 4, 5], + ), + { + one: 1, + three: 5, + two: [2, 3, 4], + }, + ); + t.deepEqual( + keysAndValuesToObject( + ["one", "two", "two", "two", "three"], + [null, 2, 3, 4, 5], + ), + { + three: 5, + two: [2, 3, 4], + }, + ); + t.end(); +}); From 11c687762a30b221a48c553121ea8472b5514e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:16:31 -0500 Subject: [PATCH 056/117] run converage on travis --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b7a3567..b76d827 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,11 @@ node_js: - "10.15" script: - npm audit + - npm compile - npm run lint - - npm run $NPM_COMMAND + - npm test + - npm run coverage + # - npm run $NPM_COMMAND sudo: false env: global: From d7758500ad04ec4110d76d24a838e9c52292376d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:32:09 -0500 Subject: [PATCH 057/117] fix travis script --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b76d827..c07cc0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ node_js: - "10.15" script: - npm audit - - npm compile + - npm run compile - npm run lint - npm test - npm run coverage From 691beec4dc248eb2ce7919518780d87e894be488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:32:27 -0500 Subject: [PATCH 058/117] mv test/readme.{js,ts} --- test/readme.js | 155 ------------------------------------------------- test/readme.ts | 155 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 155 deletions(-) delete mode 100644 test/readme.js create mode 100644 test/readme.ts diff --git a/test/readme.js b/test/readme.js deleted file mode 100644 index 94d8a77..0000000 --- a/test/readme.js +++ /dev/null @@ -1,155 +0,0 @@ -import test from "tape"; - -import UrlPattern from "../dist/url-pattern.js"; - -test('simple', function(t) { - const pattern = new UrlPattern('/api/users/:id'); - t.deepEqual(pattern.match('/api/users/10'), {id: '10'}); - t.equal(pattern.match('/api/products/5'), undefined); - t.end(); -}); - -test('api versioning', function(t) { - const pattern = new UrlPattern('/v:major(.:minor)/*'); - t.deepEqual(pattern.match('/v1.2/'), {major: '1', minor: '2', _: ''}); - t.deepEqual(pattern.match('/v2/users'), {major: '2', _: 'users'}); - t.equal(pattern.match('/v/'), undefined); - t.end(); -}); - -test('domain', function(t) { - const pattern = new UrlPattern('(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)'); - t.deepEqual(pattern.match('google.de'), { - domain: 'google', - tld: 'de' - } - ); - t.deepEqual(pattern.match('https://www.google.com'), { - subdomain: 'www', - domain: 'google', - tld: 'com' - } - ); - t.deepEqual(pattern.match('http://mail.google.com/mail'), { - subdomain: 'mail', - domain: 'google', - tld: 'com', - _: 'mail' - } - ); - t.deepEqual(pattern.match('http://mail.google.com:80/mail'), { - subdomain: 'mail', - domain: 'google', - tld: 'com', - port: '80', - _: 'mail' - } - ); - t.equal(pattern.match('google'), undefined); - - t.deepEqual(pattern.match('www.google.com'), { - subdomain: 'www', - domain: 'google', - tld: 'com' - } - ); - t.equal(pattern.match('httpp://mail.google.com/mail'), undefined); - t.deepEqual(pattern.match('google.de/search'), { - domain: 'google', - tld: 'de', - _: 'search' - } - ); - - t.end(); -}); - -test('named segment occurs more than once', function(t) { - const pattern = new UrlPattern('/api/users/:ids/posts/:ids'); - t.deepEqual(pattern.match('/api/users/10/posts/5'), {ids: ['10', '5']}); - t.end(); -}); - -test('regex', function(t) { - const pattern = new UrlPattern(/^\/api\/(.*)$/); - t.deepEqual(pattern.match('/api/users'), ['users']); - t.equal(pattern.match('/apiii/users'), undefined); - t.end(); -}); - -test('regex group names', function(t) { - const pattern = new UrlPattern(/^\/api\/([^\/]+)(?:\/(\d+))?$/, ['resource', 'id']); - t.deepEqual(pattern.match('/api/users'), - {resource: 'users'}); - t.equal(pattern.match('/api/users/'), undefined); - t.deepEqual(pattern.match('/api/users/5'), { - resource: 'users', - id: '5' - } - ); - t.equal(pattern.match('/api/users/foo'), undefined); - t.end(); -}); - -test('stringify', function(t) { - let pattern = new UrlPattern('/api/users/:id'); - t.equal('/api/users/10', pattern.stringify({id: 10})); - - pattern = new UrlPattern('/api/users(/:id)'); - t.equal('/api/users', pattern.stringify()); - t.equal('/api/users/10', pattern.stringify({id: 10})); - - t.end(); -}); - -test('customization', function(t) { - const options = { - escapeChar: '!', - segmentNameStartChar: '$', - segmentNameCharset: 'a-zA-Z0-9_-', - segmentValueCharset: 'a-zA-Z0-9', - optionalSegmentStartChar: '[', - optionalSegmentEndChar: ']', - wildcardChar: '?' - }; - - const pattern = new UrlPattern( - '[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]', - options - ); - - t.deepEqual(pattern.match('google.de'), { - domain: 'google', - 'toplevel-domain': 'de' - } - ); - t.deepEqual(pattern.match('http://mail.google.com/mail'), { - sub_domain: 'mail', - domain: 'google', - 'toplevel-domain': 'com', - _: 'mail' - } - ); - t.equal(pattern.match('http://mail.this-should-not-match.com/mail'), undefined); - t.equal(pattern.match('google'), undefined); - t.deepEqual(pattern.match('www.google.com'), { - sub_domain: 'www', - domain: 'google', - 'toplevel-domain': 'com' - } - ); - t.deepEqual(pattern.match('https://www.google.com'), { - sub_domain: 'www', - domain: 'google', - 'toplevel-domain': 'com' - } - ); - t.equal(pattern.match('httpp://mail.google.com/mail'), undefined); - t.deepEqual(pattern.match('google.de/search'), { - domain: 'google', - 'toplevel-domain': 'de', - _: 'search' - } - ); - t.end(); -}); diff --git a/test/readme.ts b/test/readme.ts new file mode 100644 index 0000000..b1f1f00 --- /dev/null +++ b/test/readme.ts @@ -0,0 +1,155 @@ +import * as tape from "tape"; + +import UrlPattern from "../src/url-pattern"; + +tape("simple", (t: tape.Test) => { + const pattern = new UrlPattern("/api/users/:id"); + t.deepEqual(pattern.match("/api/users/10"), {id: "10"}); + t.equal(pattern.match("/api/products/5"), undefined); + t.end(); +}); + +tape("api versioning", (t: tape.Test) => { + const pattern = new UrlPattern("/v:major(.:minor)/*"); + t.deepEqual(pattern.match("/v1.2/"), {major: "1", minor: "2", _: ""}); + t.deepEqual(pattern.match("/v2/users"), {major: "2", _: "users"}); + t.equal(pattern.match("/v/"), undefined); + t.end(); +}); + +tape("domain", (t: tape.Test) => { + const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)"); + t.deepEqual(pattern.match("google.de"), { + domain: "google", + tld: "de", + }, + ); + t.deepEqual(pattern.match("https://www.google.com"), { + domain: "google", + subdomain: "www", + tld: "com", + }, + ); + t.deepEqual(pattern.match("http://mail.google.com/mail"), { + _: "mail", + domain: "google", + subdomain: "mail", + tld: "com", + }, + ); + t.deepEqual(pattern.match("http://mail.google.com:80/mail"), { + _: "mail", + domain: "google", + port: "80", + subdomain: "mail", + tld: "com", + }, + ); + t.equal(pattern.match("google"), undefined); + + t.deepEqual(pattern.match("www.google.com"), { + domain: "google", + subdomain: "www", + tld: "com", + }, + ); + t.equal(pattern.match("httpp://mail.google.com/mail"), undefined); + t.deepEqual(pattern.match("google.de/search"), { + _: "search", + domain: "google", + tld: "de", + }, + ); + + t.end(); +}); + +tape("named segment occurs more than once", (t: tape.Test) => { + const pattern = new UrlPattern("/api/users/:ids/posts/:ids"); + t.deepEqual(pattern.match("/api/users/10/posts/5"), {ids: ["10", "5"]}); + t.end(); +}); + +tape("regex", (t: tape.Test) => { + const pattern = new UrlPattern(/^\/api\/(.*)$/); + t.deepEqual(pattern.match("/api/users"), ["users"]); + t.equal(pattern.match("/apiii/users"), undefined); + t.end(); +}); + +tape("regex group names", (t: tape.Test) => { + const pattern = new UrlPattern(/^\/api\/([^\/]+)(?:\/(\d+))?$/, ["resource", "id"]); + t.deepEqual(pattern.match("/api/users"), + {resource: "users"}); + t.equal(pattern.match("/api/users/"), undefined); + t.deepEqual(pattern.match("/api/users/5"), { + id: "5", + resource: "users", + }, + ); + t.equal(pattern.match("/api/users/foo"), undefined); + t.end(); +}); + +tape("stringify", (t: tape.Test) => { + let pattern = new UrlPattern("/api/users/:id"); + t.equal("/api/users/10", pattern.stringify({id: 10})); + + pattern = new UrlPattern("/api/users(/:id)"); + t.equal("/api/users", pattern.stringify()); + t.equal("/api/users/10", pattern.stringify({id: 10})); + + t.end(); +}); + +tape("customization", (t: tape.Test) => { + const options = { + escapeChar: "!", + optionalSegmentEndChar: "]", + optionalSegmentStartChar: "[", + segmentNameCharset: "a-zA-Z0-9_-", + segmentNameStartChar: "$", + segmentValueCharset: "a-zA-Z0-9", + wildcardChar: "?", + }; + + const pattern = new UrlPattern( + "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]", + options, + ); + + t.deepEqual(pattern.match("google.de"), { + "domain": "google", + "toplevel-domain": "de", + }, + ); + t.deepEqual(pattern.match("http://mail.google.com/mail"), { + "_": "mail", + "domain": "google", + "sub_domain": "mail", + "toplevel-domain": "com", + }, + ); + t.equal(pattern.match("http://mail.this-should-not-match.com/mail"), undefined); + t.equal(pattern.match("google"), undefined); + t.deepEqual(pattern.match("www.google.com"), { + "domain": "google", + "sub_domain": "www", + "toplevel-domain": "com", + }, + ); + t.deepEqual(pattern.match("https://www.google.com"), { + "domain": "google", + "sub_domain": "www", + "toplevel-domain": "com", + }, + ); + t.equal(pattern.match("httpp://mail.google.com/mail"), undefined); + t.deepEqual(pattern.match("google.de/search"), { + "_": "search", + "domain": "google", + "toplevel-domain": "de", + }, + ); + t.end(); +}); From ece00b211580bd7f91da3d2d149a17faadc546ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:46:57 -0500 Subject: [PATCH 059/117] mv test/misc.{js,ts} --- test/misc.js | 38 -------------------------------------- test/misc.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 38 deletions(-) delete mode 100644 test/misc.js create mode 100644 test/misc.ts diff --git a/test/misc.js b/test/misc.js deleted file mode 100644 index eebb54d..0000000 --- a/test/misc.js +++ /dev/null @@ -1,38 +0,0 @@ -import test from "tape"; - -import UrlPattern from "../dist/url-pattern.js"; - -test('instance of UrlPattern is handled correctly as constructor argument', function(t) { - const pattern = new UrlPattern('/user/:userId/task/:taskId'); - const copy = new UrlPattern(pattern); - t.deepEqual(copy.match('/user/10/task/52'), { - userId: '10', - taskId: '52' - } - ); - t.end(); -}); - -test('match full stops in segment values', function(t) { - const options = - {segmentValueCharset: 'a-zA-Z0-9-_ %.'}; - const pattern = new UrlPattern('/api/v1/user/:id/', options); - t.deepEqual(pattern.match('/api/v1/user/test.name/'), - {id: 'test.name'}); - t.end(); -}); - -test('regex group names', function(t) { - const pattern = new UrlPattern(/^\/api\/([a-zA-Z0-9-_~ %]+)(?:\/(\d+))?$/, ['resource', 'id']); - t.deepEqual(pattern.match('/api/users'), - {resource: 'users'}); - t.equal(pattern.match('/apiii/users'), undefined); - t.deepEqual(pattern.match('/api/users/foo'), undefined); - t.deepEqual(pattern.match('/api/users/10'), { - resource: 'users', - id: '10' - } - ); - t.deepEqual(pattern.match('/api/projects/10/'), undefined); - t.end(); -}); diff --git a/test/misc.ts b/test/misc.ts new file mode 100644 index 0000000..9c2a8aa --- /dev/null +++ b/test/misc.ts @@ -0,0 +1,39 @@ +import * as tape from "tape"; + +import UrlPattern from "../src/url-pattern"; + +tape("instance of UrlPattern is handled correctly as constructor argument", (t: tape.Test) => { + const pattern = new UrlPattern("/user/:userId/task/:taskId"); + const copy = new UrlPattern(pattern); + t.deepEqual(copy.match("/user/10/task/52"), { + taskId: "52", + userId: "10", + }, + ); + t.end(); +}); + +tape("match full stops in segment values", (t: tape.Test) => { + const options = { + segmentValueCharset: "a-zA-Z0-9-_ %.", + }; + const pattern = new UrlPattern("/api/v1/user/:id/", options); + t.deepEqual(pattern.match("/api/v1/user/test.name/"), + {id: "test.name"}); + t.end(); +}); + +tape("regex group names", (t: tape.Test) => { + const pattern = new UrlPattern(/^\/api\/([a-zA-Z0-9-_~ %]+)(?:\/(\d+))?$/, ["resource", "id"]); + t.deepEqual(pattern.match("/api/users"), + {resource: "users"}); + t.equal(pattern.match("/apiii/users"), undefined); + t.deepEqual(pattern.match("/api/users/foo"), undefined); + t.deepEqual(pattern.match("/api/users/10"), { + id: "10", + resource: "users", + }, + ); + t.deepEqual(pattern.match("/api/projects/10/"), undefined); + t.end(); +}); From 163473c4a8266249fb969e2bc83216e7eddff5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 00:47:11 -0500 Subject: [PATCH 060/117] add missing constructor type to UrlPattern --- src/url-pattern.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/url-pattern.ts b/src/url-pattern.ts index fb26c4b..58d3147 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -28,6 +28,7 @@ export default class UrlPattern { constructor(pattern: string, options?: IUserInputOptions); constructor(pattern: RegExp, groupNames?: string[]); + constructor(pattern: UrlPattern); constructor(pattern: string | RegExp | UrlPattern, optionsOrGroupNames?: IUserInputOptions | string[]) { // self awareness From 18e395ac1b63308edbf5dc232d2386821d4ebc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 09:39:17 -0500 Subject: [PATCH 061/117] mv test/errors.{js,ts} --- test/errors.js | 152 ------------------------------------------------- test/errors.ts | 132 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 152 deletions(-) delete mode 100644 test/errors.js create mode 100644 test/errors.ts diff --git a/test/errors.js b/test/errors.js deleted file mode 100644 index 665127f..0000000 --- a/test/errors.js +++ /dev/null @@ -1,152 +0,0 @@ -import test from "tape"; - -import UrlPattern from "../dist/url-pattern.js"; - -test('invalid argument', function(t) { - let e; - UrlPattern; - t.plan(5); - try { - new UrlPattern(); - } catch (error) { - e = error; - t.equal(e.message, "first argument must be a RegExp, a string or an instance of UrlPattern"); - } - try { - new UrlPattern(5); - } catch (error1) { - e = error1; - t.equal(e.message, "first argument must be a RegExp, a string or an instance of UrlPattern"); - } - try { - new UrlPattern(''); - } catch (error2) { - e = error2; - t.equal(e.message, "first argument must not be the empty string"); - } - try { - new UrlPattern(' '); - } catch (error3) { - e = error3; - t.equal(e.message, "first argument must not contain whitespace"); - } - try { - new UrlPattern(' fo o'); - } catch (error4) { - e = error4; - t.equal(e.message, "first argument must not contain whitespace"); - } - t.end(); -}); - -test('invalid variable name in pattern', function(t) { - let e; - UrlPattern; - t.plan(3); - try { - new UrlPattern(':'); - } catch (error) { - e = error; - t.equal(e.message, "couldn't parse pattern"); - } - try { - new UrlPattern(':.'); - } catch (error1) { - e = error1; - t.equal(e.message, "couldn't parse pattern"); - } - try { - new UrlPattern('foo:.'); - } catch (error2) { - // TODO `:` must be followed by the name of the named segment consisting of at least one character in character set `a-zA-Z0-9` at 4 - e = error2; - t.equal(e.message, "could only partially parse pattern"); - } - t.end(); -}); - -test('too many closing parentheses', function(t) { - let e; - t.plan(2); - try { - new UrlPattern(')'); - } catch (error) { - // TODO did not plan ) at 0 - e = error; - t.equal(e.message, "couldn't parse pattern"); - } - try { - new UrlPattern('((foo)))bar'); - } catch (error1) { - // TODO did not plan ) at 7 - e = error1; - t.equal(e.message, "could only partially parse pattern"); - } - t.end(); -}); - -test('unclosed parentheses', function(t) { - let e; - t.plan(2); - try { - new UrlPattern('('); - } catch (error) { - // TODO unclosed parentheses at 1 - e = error; - t.equal(e.message, "couldn't parse pattern"); - } - try { - new UrlPattern('(((foo)bar(boo)far)'); - } catch (error1) { - // TODO unclosed parentheses at 19 - e = error1; - t.equal(e.message, "couldn't parse pattern"); - } - t.end(); -}); - -test('regex names', function(t) { - let e; - t.plan(3); - try { - new UrlPattern(/x/, 5); - } catch (error) { - e = error; - t.equal(e.message, 'if first argument is a RegExp the second argument may be an Array of group names but you provided something else'); - } - try { - new UrlPattern(/(((foo)bar(boo))far)/, []); - } catch (error1) { - e = error1; - t.equal(e.message, "regex contains 4 groups but array of group names contains 0"); - } - try { - new UrlPattern(/(((foo)bar(boo))far)/, ['a', 'b']); - } catch (error2) { - e = error2; - t.equal(e.message, "regex contains 4 groups but array of group names contains 2"); - } - t.end(); -}); - -test('stringify regex', function(t) { - t.plan(1); - const pattern = new UrlPattern(/x/); - try { - pattern.stringify(); - } catch (e) { - t.equal(e.message, "can't stringify patterns generated from a regex"); - } - t.end(); -}); - -test('stringify argument', function(t) { - t.plan(1); - const pattern = new UrlPattern('foo'); - try { - pattern.stringify(5); - } catch (e) { - t.equal(e.message, "argument must be an object or undefined"); - } - t.end(); -}); diff --git a/test/errors.ts b/test/errors.ts new file mode 100644 index 0000000..f8798b2 --- /dev/null +++ b/test/errors.ts @@ -0,0 +1,132 @@ +/* tslint:disable:no-unused-expression */ +import * as tape from "tape"; + +import UrlPattern from "../src/url-pattern"; + +const UntypedUrlPattern: any = UrlPattern; + +tape("invalid argument", (t: tape.Test) => { + t.plan(5); + + try { + new UntypedUrlPattern(); + } catch (error) { + t.equal(error.message, "first argument must be a RegExp, a string or an instance of UrlPattern"); + } + try { + new UntypedUrlPattern(5); + } catch (error) { + t.equal(error.message, "first argument must be a RegExp, a string or an instance of UrlPattern"); + } + try { + new UrlPattern(""); + } catch (error) { + t.equal(error.message, "first argument must not be the empty string"); + } + try { + new UrlPattern(" "); + } catch (error) { + t.equal(error.message, "first argument must not contain whitespace"); + } + try { + new UrlPattern(" fo o"); + } catch (error) { + t.equal(error.message, "first argument must not contain whitespace"); + } + t.end(); +}); + +tape("invalid variable name in pattern", (t: tape.Test) => { + t.plan(3); + try { + new UrlPattern(":"); + } catch (error) { + t.equal(error.message, "couldn't parse pattern"); + } + try { + new UrlPattern(":."); + } catch (error) { + t.equal(error.message, "couldn't parse pattern"); + } + try { + new UrlPattern("foo:."); + } catch (error) { + t.equal(error.message, "could only partially parse pattern"); + } + t.end(); +}); + +tape("too many closing parentheses", (t: tape.Test) => { + t.plan(2); + try { + new UrlPattern(")"); + } catch (error) { + t.equal(error.message, "couldn't parse pattern"); + } + try { + new UrlPattern("((foo)))bar"); + } catch (error) { + t.equal(error.message, "could only partially parse pattern"); + } + t.end(); +}); + +tape("unclosed parentheses", (t: tape.Test) => { + t.plan(2); + try { + new UrlPattern("("); + } catch (error) { + t.equal(error.message, "couldn't parse pattern"); + } + try { + new UrlPattern("(((foo)bar(boo)far)"); + } catch (error) { + t.equal(error.message, "couldn't parse pattern"); + } + t.end(); +}); + +tape("regex names", (t: tape.Test) => { + t.plan(3); + try { + new UntypedUrlPattern(/x/, 5); + } catch (error) { + t.equal(error.message, [ + "if first argument is a RegExp the second argument may be an Array", + "of group names but you provided something else", + ].join(" ")); + } + try { + new UrlPattern(/(((foo)bar(boo))far)/, []); + } catch (error) { + t.equal(error.message, "regex contains 4 groups but array of group names contains 0"); + } + try { + new UrlPattern(/(((foo)bar(boo))far)/, ["a", "b"]); + } catch (error) { + t.equal(error.message, "regex contains 4 groups but array of group names contains 2"); + } + t.end(); +}); + +tape("stringify regex", (t: tape.Test) => { + t.plan(1); + const pattern = new UrlPattern(/x/); + try { + pattern.stringify(); + } catch (error) { + t.equal(error.message, "can't stringify patterns generated from a regex"); + } + t.end(); +}); + +tape("stringify argument", (t: tape.Test) => { + t.plan(1); + const pattern = new UntypedUrlPattern("foo"); + try { + pattern.stringify(5); + } catch (error) { + t.equal(error.message, "argument must be an object or undefined"); + } + t.end(); +}); From 62e8e49f1102ae24401c36e3b7e623f44f0b74b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 09:50:59 -0500 Subject: [PATCH 062/117] fix typings for parser.getParam --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 1594f5a..fd3323c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -170,7 +170,7 @@ export function getParam( params: { [index: string]: any }, key: string, nextIndexes: { [index: string]: number }, - hasSideEffects: boolean, + hasSideEffects: boolean = false, ) { if (hasSideEffects == null) { hasSideEffects = false; From 557285d42c152de01ab01fd3975c5ea9d9a91956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 09:51:23 -0500 Subject: [PATCH 063/117] mv test/ast.{js,ts} --- test/ast.js | 229 --------------------------------------------------- test/ast.ts | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 229 deletions(-) delete mode 100644 test/ast.js create mode 100644 test/ast.ts diff --git a/test/ast.js b/test/ast.js deleted file mode 100644 index acbf8f4..0000000 --- a/test/ast.js +++ /dev/null @@ -1,229 +0,0 @@ -import test from "tape"; - -import { - newUrlPatternParser, - getParam, - astNodeToRegexString, - astNodeToNames -} from "../dist/parser.js"; - -import { - defaultOptions, -} from "../dist/options.js"; - -const parse = newUrlPatternParser(defaultOptions); - -test('astNodeToRegexString and astNodeToNames', function(t) { - t.test('just static alphanumeric', function(t) { - const parsed = parse('user42'); - t.equal(astNodeToRegexString(parsed.value), '^user42$'); - t.deepEqual(astNodeToNames(parsed.value), []); - t.end(); - }); - - t.test('just static escaped', function(t) { - const parsed = parse('/api/v1/users'); - t.equal(astNodeToRegexString(parsed.value), '^\\/api\\/v1\\/users$'); - t.deepEqual(astNodeToNames(parsed.value), []); - t.end(); - }); - - t.test('just single char variable', function(t) { - const parsed = parse(':a'); - t.equal(astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$'); - t.deepEqual(astNodeToNames(parsed.value), ['a']); - t.end(); - }); - - t.test('just variable', function(t) { - const parsed = parse(':variable'); - t.equal(astNodeToRegexString(parsed.value), '^([a-zA-Z0-9-_~ %]+)$'); - t.deepEqual(astNodeToNames(parsed.value), ['variable']); - t.end(); - }); - - t.test('just wildcard', function(t) { - const parsed = parse('*'); - t.equal(astNodeToRegexString(parsed.value), '^(.*?)$'); - t.deepEqual(astNodeToNames(parsed.value), ['_']); - t.end(); - }); - - t.test('just optional static', function(t) { - const parsed = parse('(foo)'); - t.equal(astNodeToRegexString(parsed.value), '^(?:foo)?$'); - t.deepEqual(astNodeToNames(parsed.value), []); - t.end(); - }); - - t.test('just optional variable', function(t) { - const parsed = parse('(:foo)'); - t.equal(astNodeToRegexString(parsed.value), '^(?:([a-zA-Z0-9-_~ %]+))?$'); - t.deepEqual(astNodeToNames(parsed.value), ['foo']); - t.end(); - }); - - t.test('just optional wildcard', function(t) { - const parsed = parse('(*)'); - t.equal(astNodeToRegexString(parsed.value), '^(?:(.*?))?$'); - t.deepEqual(astNodeToNames(parsed.value), ['_']); - t.end(); - }); -}); - -test('getParam', function(t) { - t.test('no side effects', function(t) { - let next = {}; - t.equal(undefined, getParam({}, 'one', next)); - t.deepEqual(next, {}); - - // value - - next = {}; - t.equal(1, getParam({one: 1}, 'one', next)); - t.deepEqual(next, {}); - - next = {one: 0}; - t.equal(1, getParam({one: 1}, 'one', next)); - t.deepEqual(next, {one: 0}); - - next = {one: 1}; - t.equal(undefined, getParam({one: 1}, 'one', next)); - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - t.equal(undefined, getParam({one: 1}, 'one', next)); - t.deepEqual(next, {one: 2}); - - // array - - next = {}; - t.equal(1, getParam({one: [1]}, 'one', next)); - t.deepEqual(next, {}); - - next = {one: 0}; - t.equal(1, getParam({one: [1]}, 'one', next)); - t.deepEqual(next, {one: 0}); - - next = {one: 1}; - t.equal(undefined, getParam({one: [1]}, 'one', next)); - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - t.equal(undefined, getParam({one: [1]}, 'one', next)); - t.deepEqual(next, {one: 2}); - - next = {one: 0}; - t.equal(1, getParam({one: [1, 2, 3]}, 'one', next)); - t.deepEqual(next, {one: 0}); - - next = {one: 1}; - t.equal(2, getParam({one: [1, 2, 3]}, 'one', next)); - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - t.equal(3, getParam({one: [1, 2, 3]}, 'one', next)); - t.deepEqual(next, {one: 2}); - - next = {one: 3}; - t.equal(undefined, getParam({one: [1, 2, 3]}, 'one', next)); - t.deepEqual(next, {one: 3}); - - t.end(); - }); - - t.test('side effects', function(t) { - let next = {}; - t.equal(1, getParam({one: 1}, 'one', next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 0}; - t.equal(1, getParam({one: 1}, 'one', next, true)); - t.deepEqual(next, {one: 1}); - - // array - - next = {}; - t.equal(1, getParam({one: [1]}, 'one', next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 0}; - t.equal(1, getParam({one: [1]}, 'one', next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 0}; - t.equal(1, getParam({one: [1, 2, 3]}, 'one', next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 1}; - t.equal(2, getParam({one: [1, 2, 3]}, 'one', next, true)); - t.deepEqual(next, {one: 2}); - - next = {one: 2}; - t.equal(3, getParam({one: [1, 2, 3]}, 'one', next, true)); - t.deepEqual(next, {one: 3}); - - t.end(); - }); - - t.test('side effects errors', function(t) { - let e; - t.plan(2 * 6); - - let next = {}; - try { - getParam({}, 'one', next, true); - } catch (error) { - e = error; - t.equal(e.message, "no values provided for key `one`"); - } - t.deepEqual(next, {}); - - next = {one: 1}; - try { - getParam({one: 1}, 'one', next, true); - } catch (error1) { - e = error1; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - try { - getParam({one: 2}, 'one', next, true); - } catch (error2) { - e = error2; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 2}); - - next = {one: 1}; - try { - getParam({one: [1]}, 'one', next, true); - } catch (error3) { - e = error3; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - try { - getParam({one: [1]}, 'one', next, true); - } catch (error4) { - e = error4; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 2}); - - next = {one: 3}; - try { - getParam({one: [1, 2, 3]}, 'one', next, true); - } catch (error5) { - e = error5; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 3}); - - t.end(); - }); -}); diff --git a/test/ast.ts b/test/ast.ts new file mode 100644 index 0000000..62e4f76 --- /dev/null +++ b/test/ast.ts @@ -0,0 +1,230 @@ +/* tslint:disable:no-shadowed-variable */ +import * as tape from "tape"; + +import { + astNodeToNames, + astNodeToRegexString, + getParam, + newUrlPatternParser, +} from "../src/parser"; + +import { + defaultOptions, +} from "../src/options"; + +const parse: any = newUrlPatternParser(defaultOptions); + +tape("astNodeToRegexString and astNodeToNames", (t: tape.Test) => { + t.test("just static alphanumeric", (t: tape.Test) => { + const parsed = parse("user42"); + t.equal(astNodeToRegexString(parsed.value), "^user42$"); + t.deepEqual(astNodeToNames(parsed.value), []); + t.end(); + }); + + t.test("just static escaped", (t: tape.Test) => { + const parsed = parse("/api/v1/users"); + t.equal(astNodeToRegexString(parsed.value), "^\\/api\\/v1\\/users$"); + t.deepEqual(astNodeToNames(parsed.value), []); + t.end(); + }); + + t.test("just single char variable", (t: tape.Test) => { + const parsed = parse(":a"); + t.equal(astNodeToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); + t.deepEqual(astNodeToNames(parsed.value), ["a"]); + t.end(); + }); + + t.test("just variable", (t: tape.Test) => { + const parsed = parse(":variable"); + t.equal(astNodeToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); + t.deepEqual(astNodeToNames(parsed.value), ["variable"]); + t.end(); + }); + + t.test("just wildcard", (t: tape.Test) => { + const parsed = parse("*"); + t.equal(astNodeToRegexString(parsed.value), "^(.*?)$"); + t.deepEqual(astNodeToNames(parsed.value), ["_"]); + t.end(); + }); + + t.test("just optional static", (t: tape.Test) => { + const parsed = parse("(foo)"); + t.equal(astNodeToRegexString(parsed.value), "^(?:foo)?$"); + t.deepEqual(astNodeToNames(parsed.value), []); + t.end(); + }); + + t.test("just optional variable", (t: tape.Test) => { + const parsed = parse("(:foo)"); + t.equal(astNodeToRegexString(parsed.value), "^(?:([a-zA-Z0-9-_~ %]+))?$"); + t.deepEqual(astNodeToNames(parsed.value), ["foo"]); + t.end(); + }); + + t.test("just optional wildcard", (t: tape.Test) => { + const parsed = parse("(*)"); + t.equal(astNodeToRegexString(parsed.value), "^(?:(.*?))?$"); + t.deepEqual(astNodeToNames(parsed.value), ["_"]); + t.end(); + }); +}); + +tape("getParam", (t: tape.Test) => { + t.test("no side effects", (t: tape.Test) => { + let next = {}; + t.equal(undefined, getParam({}, "one", next)); + t.deepEqual(next, {}); + + // value + + next = {}; + t.equal(1, getParam({one: 1}, "one", next)); + t.deepEqual(next, {}); + + next = {one: 0}; + t.equal(1, getParam({one: 1}, "one", next)); + t.deepEqual(next, {one: 0}); + + next = {one: 1}; + t.equal(undefined, getParam({one: 1}, "one", next)); + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + t.equal(undefined, getParam({one: 1}, "one", next)); + t.deepEqual(next, {one: 2}); + + // array + + next = {}; + t.equal(1, getParam({one: [1]}, "one", next)); + t.deepEqual(next, {}); + + next = {one: 0}; + t.equal(1, getParam({one: [1]}, "one", next)); + t.deepEqual(next, {one: 0}); + + next = {one: 1}; + t.equal(undefined, getParam({one: [1]}, "one", next)); + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + t.equal(undefined, getParam({one: [1]}, "one", next)); + t.deepEqual(next, {one: 2}); + + next = {one: 0}; + t.equal(1, getParam({one: [1, 2, 3]}, "one", next)); + t.deepEqual(next, {one: 0}); + + next = {one: 1}; + t.equal(2, getParam({one: [1, 2, 3]}, "one", next)); + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + t.equal(3, getParam({one: [1, 2, 3]}, "one", next)); + t.deepEqual(next, {one: 2}); + + next = {one: 3}; + t.equal(undefined, getParam({one: [1, 2, 3]}, "one", next)); + t.deepEqual(next, {one: 3}); + + t.end(); + }); + + t.test("side effects", (t: tape.Test) => { + let next = {}; + t.equal(1, getParam({one: 1}, "one", next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 0}; + t.equal(1, getParam({one: 1}, "one", next, true)); + t.deepEqual(next, {one: 1}); + + // array + + next = {}; + t.equal(1, getParam({one: [1]}, "one", next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 0}; + t.equal(1, getParam({one: [1]}, "one", next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 0}; + t.equal(1, getParam({one: [1, 2, 3]}, "one", next, true)); + t.deepEqual(next, {one: 1}); + + next = {one: 1}; + t.equal(2, getParam({one: [1, 2, 3]}, "one", next, true)); + t.deepEqual(next, {one: 2}); + + next = {one: 2}; + t.equal(3, getParam({one: [1, 2, 3]}, "one", next, true)); + t.deepEqual(next, {one: 3}); + + t.end(); + }); + + t.test("side effects errors", (t: tape.Test) => { + let e; + t.plan(2 * 6); + + let next = {}; + try { + getParam({}, "one", next, true); + } catch (error) { + e = error; + t.equal(e.message, "no values provided for key `one`"); + } + t.deepEqual(next, {}); + + next = {one: 1}; + try { + getParam({one: 1}, "one", next, true); + } catch (error1) { + e = error1; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + try { + getParam({one: 2}, "one", next, true); + } catch (error2) { + e = error2; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 2}); + + next = {one: 1}; + try { + getParam({one: [1]}, "one", next, true); + } catch (error3) { + e = error3; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 1}); + + next = {one: 2}; + try { + getParam({one: [1]}, "one", next, true); + } catch (error4) { + e = error4; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 2}); + + next = {one: 3}; + try { + getParam({one: [1, 2, 3]}, "one", next, true); + } catch (error5) { + e = error5; + t.equal(e.message, "too few values provided for key `one`"); + } + t.deepEqual(next, {one: 3}); + + t.end(); + }); +}); From c375341fe8629173ec74d6ec4875952e51cf95b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 09:59:35 -0500 Subject: [PATCH 064/117] mv test/parser.{js,ts} --- test/parser.js | 468 ------------------------------------------------- test/parser.ts | 462 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 462 insertions(+), 468 deletions(-) delete mode 100644 test/parser.js create mode 100644 test/parser.ts diff --git a/test/parser.js b/test/parser.js deleted file mode 100644 index 71b7537..0000000 --- a/test/parser.js +++ /dev/null @@ -1,468 +0,0 @@ -const test = require('tape'); - -import { - newUrlPatternParser, - newNamedSegmentParser, - newStaticContentParser, - getParam, - astNodeToRegexString, - astNodeToNames -} from "../dist/parser.js"; - -import { - defaultOptions, -} from "../dist/options.js"; - -const parse = newUrlPatternParser(defaultOptions); -const parseNamedSegment = newNamedSegmentParser(defaultOptions); -const parseStaticContent= newStaticContentParser(defaultOptions); - -test('namedSegment', function(t) { - t.deepEqual(parseNamedSegment(':a'), { - value: { - tag: 'namedSegment', - value: 'a' - }, - rest: '' - } - ); - t.deepEqual(parseNamedSegment(':ab96c'), { - value: { - tag: 'namedSegment', - value: 'ab96c' - }, - rest: '' - } - ); - t.deepEqual(parseNamedSegment(':ab96c.'), { - value: { - tag: 'namedSegment', - value: 'ab96c' - }, - rest: '.' - } - ); - t.deepEqual(parseNamedSegment(':96c-:ab'), { - value: { - tag: 'namedSegment', - value: '96c' - }, - rest: '-:ab' - } - ); - t.equal(parseNamedSegment(':'), undefined); - t.equal(parseNamedSegment(''), undefined); - t.equal(parseNamedSegment('a'), undefined); - t.equal(parseNamedSegment('abc'), undefined); - t.end(); -}); - -test('static', function(t) { - t.deepEqual(parseStaticContent('a'), { - value: { - tag: 'staticContent', - value: 'a' - }, - rest: '' - } - ); - t.deepEqual(parseStaticContent('abc:d'), { - value: { - tag: 'staticContent', - value: 'abc' - }, - rest: ':d' - } - ); - t.equal(parseStaticContent(':ab96c'), undefined); - t.equal(parseStaticContent(':'), undefined); - t.equal(parseStaticContent('('), undefined); - t.equal(parseStaticContent(')'), undefined); - t.equal(parseStaticContent('*'), undefined); - t.equal(parseStaticContent(''), undefined); - t.end(); -}); - - -test('fixtures', function(t) { - t.equal(parse(''), undefined); - t.equal(parse('('), undefined); - t.equal(parse(')'), undefined); - t.equal(parse('()'), undefined); - t.equal(parse(':'), undefined); - t.equal(parse('((foo)'), undefined); - t.equal(parse('(((foo)bar(boo)far)'), undefined); - - t.deepEqual(parse('(foo))'), { - rest: ')', - value: [ - {tag: 'optionalSegment', value: [{tag: 'staticContent', value: 'foo'}]} - ] - }); - - t.deepEqual(parse('((foo)))bar'), { - rest: ')bar', - value: [ - { - tag: 'optionalSegment', - value: [ - {tag: 'optionalSegment', value: [{tag: 'staticContent', value: 'foo'}]} - ] - } - ] - }); - - - t.deepEqual(parse('foo:*'), { - rest: ':*', - value: [ - {tag: 'staticContent', value: 'foo'} - ] - }); - - t.deepEqual(parse(':foo:bar'), { - rest: '', - value: [ - {tag: 'namedSegment', value: 'foo'}, - {tag: 'namedSegment', value: 'bar'} - ] - }); - - t.deepEqual(parse('a'), { - rest: '', - value: [ - {tag: 'staticContent', value: 'a'} - ] - }); - t.deepEqual(parse('user42'), { - rest: '', - value: [ - {tag: 'staticContent', value: 'user42'} - ] - }); - t.deepEqual(parse(':a'), { - rest: '', - value: [ - {tag: 'namedSegment', value: 'a'} - ] - }); - t.deepEqual(parse('*'), { - rest: '', - value: [ - {tag: 'wildcard', value: '*'} - ] - }); - t.deepEqual(parse('(foo)'), { - rest: '', - value: [ - {tag: 'optionalSegment', value: [{tag: 'staticContent', value: 'foo'}]} - ] - }); - t.deepEqual(parse('(:foo)'), { - rest: '', - value: [ - {tag: 'optionalSegment', value: [{tag: 'namedSegment', value: 'foo'}]} - ] - }); - t.deepEqual(parse('(*)'), { - rest: '', - value: [ - {tag: 'optionalSegment', value: [{tag: 'wildcard', value: '*'}]} - ] - }); - - - t.deepEqual(parse('/api/users/:id'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/api/users/'}, - {tag: 'namedSegment', value: 'id'} - ] - }); - t.deepEqual(parse('/v:major(.:minor)/*'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/v'}, - {tag: 'namedSegment', value: 'major'}, - { - tag: 'optionalSegment', - value: [ - {tag: 'staticContent', value: '.'}, - {tag: 'namedSegment', value: 'minor'} - ] - }, - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'} - ] - }); - t.deepEqual(parse('(http(s)\\://)(:subdomain.):domain.:tld(/*)'), { - rest: '', - value: [ - { - tag: 'optionalSegment', - value: [ - {tag: 'staticContent', value: 'http'}, - { - tag: 'optionalSegment', - value: [ - {tag: 'staticContent', value: 's'} - ] - }, - {tag: 'staticContent', value: '://'} - ] - }, - { - tag: 'optionalSegment', - value: [ - {tag: 'namedSegment', value: 'subdomain'}, - {tag: 'staticContent', value: '.'} - ] - }, - {tag: 'namedSegment', value: 'domain'}, - {tag: 'staticContent', value: '.'}, - {tag: 'namedSegment', value: 'tld'}, - { - tag: 'optionalSegment', - value: [ - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'} - ] - } - ] - }); - t.deepEqual(parse('/api/users/:ids/posts/:ids'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/api/users/'}, - {tag: 'namedSegment', value: 'ids'}, - {tag: 'staticContent', value: '/posts/'}, - {tag: 'namedSegment', value: 'ids'} - ] - }); - - t.deepEqual(parse('/user/:userId/task/:taskId'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/user/'}, - {tag: 'namedSegment', value: 'userId'}, - {tag: 'staticContent', value: '/task/'}, - {tag: 'namedSegment', value: 'taskId'} - ] - }); - - t.deepEqual(parse('.user.:userId.task.:taskId'), { - rest: '', - value: [ - {tag: 'staticContent', value: '.user.'}, - {tag: 'namedSegment', value: 'userId'}, - {tag: 'staticContent', value: '.task.'}, - {tag: 'namedSegment', value: 'taskId'} - ] - }); - - t.deepEqual(parse('*/user/:userId'), { - rest: '', - value: [ - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/user/'}, - {tag: 'namedSegment', value: 'userId'} - ] - }); - - t.deepEqual(parse('*-user-:userId'), { - rest: '', - value: [ - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '-user-'}, - {tag: 'namedSegment', value: 'userId'} - ] - }); - - t.deepEqual(parse('/admin*'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/admin'}, - {tag: 'wildcard', value: '*'} - ] - }); - - t.deepEqual(parse('#admin*'), { - rest: '', - value: [ - {tag: 'staticContent', value: '#admin'}, - {tag: 'wildcard', value: '*'} - ] - }); - - t.deepEqual(parse('/admin/*/user/:userId'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/admin/'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/user/'}, - {tag: 'namedSegment', value: 'userId'} - ] - }); - - t.deepEqual(parse('$admin$*$user$:userId'), { - rest: '', - value: [ - {tag: 'staticContent', value: '$admin$'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '$user$'}, - {tag: 'namedSegment', value: 'userId'} - ] - }); - - t.deepEqual(parse('/admin/*/user/*/tail'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/admin/'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/user/'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/tail'} - ] - }); - - t.deepEqual(parse('/admin/*/user/:id/*/tail'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/admin/'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/user/'}, - {tag: 'namedSegment', value: 'id'}, - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/tail'} - ] - }); - - t.deepEqual(parse('^admin^*^user^:id^*^tail'), { - rest: '', - value: [ - {tag: 'staticContent', value: '^admin^'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '^user^'}, - {tag: 'namedSegment', value: 'id'}, - {tag: 'staticContent', value: '^'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '^tail'} - ] - }); - - t.deepEqual(parse('/*/admin(/:path)'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/admin'}, - {tag: 'optionalSegment', value: [ - {tag: 'staticContent', value: '/'}, - {tag: 'namedSegment', value: 'path'} - ]} - ] - }); - - t.deepEqual(parse('/'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/'} - ] - }); - - t.deepEqual(parse('(/)'), { - rest: '', - value: [ - {tag: 'optionalSegment', value: [ - {tag: 'staticContent', value: '/'} - ]} - ] - }); - - t.deepEqual(parse('/admin(/:foo)/bar'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/admin'}, - {tag: 'optionalSegment', value: [ - {tag: 'staticContent', value: '/'}, - {tag: 'namedSegment', value: 'foo'} - ]}, - {tag: 'staticContent', value: '/bar'} - ] - }); - - t.deepEqual(parse('/admin(*/)foo'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/admin'}, - {tag: 'optionalSegment', value: [ - {tag: 'wildcard', value: '*'}, - {tag: 'staticContent', value: '/'} - ]}, - {tag: 'staticContent', value: 'foo'} - ] - }); - - t.deepEqual(parse('/v:major.:minor/*'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/v'}, - {tag: 'namedSegment', value: 'major'}, - {tag: 'staticContent', value: '.'}, - {tag: 'namedSegment', value: 'minor'}, - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'} - ] - }); - - t.deepEqual(parse('/v:v.:v/*'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/v'}, - {tag: 'namedSegment', value: 'v'}, - {tag: 'staticContent', value: '.'}, - {tag: 'namedSegment', value: 'v'}, - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'} - ] - }); - - t.deepEqual(parse('/:foo_bar'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/'}, - {tag: 'namedSegment', value: 'foo_bar'}, - ] - }); - - t.deepEqual(parse('((((a)b)c)d)'), { - rest: '', - value: [ - {tag: 'optionalSegment', value: [ - {tag: 'optionalSegment', value: [ - {tag: 'optionalSegment', value: [ - {tag: 'optionalSegment', value: [ - {tag: 'staticContent', value: 'a'} - ]}, - {tag: 'staticContent', value: 'b'} - ]}, - {tag: 'staticContent', value: 'c'} - ]}, - {tag: 'staticContent', value: 'd'} - ]} - ] - }); - - t.deepEqual(parse('/vvv:version/*'), { - rest: '', - value: [ - {tag: 'staticContent', value: '/vvv'}, - {tag: 'namedSegment', value: 'version'}, - {tag: 'staticContent', value: '/'}, - {tag: 'wildcard', value: '*'} - ] - }); - - t.end(); -}); diff --git a/test/parser.ts b/test/parser.ts new file mode 100644 index 0000000..b954efd --- /dev/null +++ b/test/parser.ts @@ -0,0 +1,462 @@ +import * as tape from "tape"; + +import { + newNamedSegmentParser, + newStaticContentParser, + newUrlPatternParser, +} from "../src/parser"; + +import { + defaultOptions, +} from "../src/options"; + +const parse = newUrlPatternParser(defaultOptions); +const parseNamedSegment = newNamedSegmentParser(defaultOptions); +const parseStaticContent = newStaticContentParser(defaultOptions); + +tape("namedSegment", (t: tape.Test) => { + t.deepEqual(parseNamedSegment(":a"), { + rest: "", + value: { + tag: "namedSegment", + value: "a", + }, + }, + ); + t.deepEqual(parseNamedSegment(":ab96c"), { + rest: "", + value: { + tag: "namedSegment", + value: "ab96c", + }, + }, + ); + t.deepEqual(parseNamedSegment(":ab96c."), { + rest: ".", + value: { + tag: "namedSegment", + value: "ab96c", + }, + }, + ); + t.deepEqual(parseNamedSegment(":96c-:ab"), { + rest: "-:ab", + value: { + tag: "namedSegment", + value: "96c", + }, + }, + ); + t.equal(parseNamedSegment(":"), undefined); + t.equal(parseNamedSegment(""), undefined); + t.equal(parseNamedSegment("a"), undefined); + t.equal(parseNamedSegment("abc"), undefined); + t.end(); +}); + +tape("static", (t: tape.Test) => { + t.deepEqual(parseStaticContent("a"), { + rest: "", + value: { + tag: "staticContent", + value: "a", + }, + }, + ); + t.deepEqual(parseStaticContent("abc:d"), { + rest: ":d", + value: { + tag: "staticContent", + value: "abc", + }, + }, + ); + t.equal(parseStaticContent(":ab96c"), undefined); + t.equal(parseStaticContent(":"), undefined); + t.equal(parseStaticContent("("), undefined); + t.equal(parseStaticContent(")"), undefined); + t.equal(parseStaticContent("*"), undefined); + t.equal(parseStaticContent(""), undefined); + t.end(); +}); + +tape("fixtures", (t: tape.Test) => { + t.equal(parse(""), undefined); + t.equal(parse("("), undefined); + t.equal(parse(")"), undefined); + t.equal(parse("()"), undefined); + t.equal(parse(":"), undefined); + t.equal(parse("((foo)"), undefined); + t.equal(parse("(((foo)bar(boo)far)"), undefined); + + t.deepEqual(parse("(foo))"), { + rest: ")", + value: [ + {tag: "optionalSegment", value: [{tag: "staticContent", value: "foo"}]}, + ], + }); + + t.deepEqual(parse("((foo)))bar"), { + rest: ")bar", + value: [ + { + tag: "optionalSegment", + value: [ + {tag: "optionalSegment", value: [{tag: "staticContent", value: "foo"}]}, + ], + }, + ], + }); + + t.deepEqual(parse("foo:*"), { + rest: ":*", + value: [ + {tag: "staticContent", value: "foo"}, + ], + }); + + t.deepEqual(parse(":foo:bar"), { + rest: "", + value: [ + {tag: "namedSegment", value: "foo"}, + {tag: "namedSegment", value: "bar"}, + ], + }); + + t.deepEqual(parse("a"), { + rest: "", + value: [ + {tag: "staticContent", value: "a"}, + ], + }); + t.deepEqual(parse("user42"), { + rest: "", + value: [ + {tag: "staticContent", value: "user42"}, + ], + }); + t.deepEqual(parse(":a"), { + rest: "", + value: [ + {tag: "namedSegment", value: "a"}, + ], + }); + t.deepEqual(parse("*"), { + rest: "", + value: [ + {tag: "wildcard", value: "*"}, + ], + }); + t.deepEqual(parse("(foo)"), { + rest: "", + value: [ + {tag: "optionalSegment", value: [{tag: "staticContent", value: "foo"}]}, + ], + }); + t.deepEqual(parse("(:foo)"), { + rest: "", + value: [ + {tag: "optionalSegment", value: [{tag: "namedSegment", value: "foo"}]}, + ], + }); + t.deepEqual(parse("(*)"), { + rest: "", + value: [ + {tag: "optionalSegment", value: [{tag: "wildcard", value: "*"}]}, + ], + }); + + t.deepEqual(parse("/api/users/:id"), { + rest: "", + value: [ + {tag: "staticContent", value: "/api/users/"}, + {tag: "namedSegment", value: "id"}, + ], + }); + t.deepEqual(parse("/v:major(.:minor)/*"), { + rest: "", + value: [ + {tag: "staticContent", value: "/v"}, + {tag: "namedSegment", value: "major"}, + { + tag: "optionalSegment", + value: [ + {tag: "staticContent", value: "."}, + {tag: "namedSegment", value: "minor"}, + ], + }, + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + ], + }); + t.deepEqual(parse("(http(s)\\://)(:subdomain.):domain.:tld(/*)"), { + rest: "", + value: [ + { + tag: "optionalSegment", + value: [ + {tag: "staticContent", value: "http"}, + { + tag: "optionalSegment", + value: [ + {tag: "staticContent", value: "s"}, + ], + }, + {tag: "staticContent", value: "://"}, + ], + }, + { + tag: "optionalSegment", + value: [ + {tag: "namedSegment", value: "subdomain"}, + {tag: "staticContent", value: "."}, + ], + }, + {tag: "namedSegment", value: "domain"}, + {tag: "staticContent", value: "."}, + {tag: "namedSegment", value: "tld"}, + { + tag: "optionalSegment", + value: [ + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + ], + }, + ], + }); + t.deepEqual(parse("/api/users/:ids/posts/:ids"), { + rest: "", + value: [ + {tag: "staticContent", value: "/api/users/"}, + {tag: "namedSegment", value: "ids"}, + {tag: "staticContent", value: "/posts/"}, + {tag: "namedSegment", value: "ids"}, + ], + }); + + t.deepEqual(parse("/user/:userId/task/:taskId"), { + rest: "", + value: [ + {tag: "staticContent", value: "/user/"}, + {tag: "namedSegment", value: "userId"}, + {tag: "staticContent", value: "/task/"}, + {tag: "namedSegment", value: "taskId"}, + ], + }); + + t.deepEqual(parse(".user.:userId.task.:taskId"), { + rest: "", + value: [ + {tag: "staticContent", value: ".user."}, + {tag: "namedSegment", value: "userId"}, + {tag: "staticContent", value: ".task."}, + {tag: "namedSegment", value: "taskId"}, + ], + }); + + t.deepEqual(parse("*/user/:userId"), { + rest: "", + value: [ + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/user/"}, + {tag: "namedSegment", value: "userId"}, + ], + }); + + t.deepEqual(parse("*-user-:userId"), { + rest: "", + value: [ + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "-user-"}, + {tag: "namedSegment", value: "userId"}, + ], + }); + + t.deepEqual(parse("/admin*"), { + rest: "", + value: [ + {tag: "staticContent", value: "/admin"}, + {tag: "wildcard", value: "*"}, + ], + }); + + t.deepEqual(parse("#admin*"), { + rest: "", + value: [ + {tag: "staticContent", value: "#admin"}, + {tag: "wildcard", value: "*"}, + ], + }); + + t.deepEqual(parse("/admin/*/user/:userId"), { + rest: "", + value: [ + {tag: "staticContent", value: "/admin/"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/user/"}, + {tag: "namedSegment", value: "userId"}, + ], + }); + + t.deepEqual(parse("$admin$*$user$:userId"), { + rest: "", + value: [ + {tag: "staticContent", value: "$admin$"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "$user$"}, + {tag: "namedSegment", value: "userId"}, + ], + }); + + t.deepEqual(parse("/admin/*/user/*/tail"), { + rest: "", + value: [ + {tag: "staticContent", value: "/admin/"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/user/"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/tail"}, + ], + }); + + t.deepEqual(parse("/admin/*/user/:id/*/tail"), { + rest: "", + value: [ + {tag: "staticContent", value: "/admin/"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/user/"}, + {tag: "namedSegment", value: "id"}, + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/tail"}, + ], + }); + + t.deepEqual(parse("^admin^*^user^:id^*^tail"), { + rest: "", + value: [ + {tag: "staticContent", value: "^admin^"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "^user^"}, + {tag: "namedSegment", value: "id"}, + {tag: "staticContent", value: "^"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "^tail"}, + ], + }); + + t.deepEqual(parse("/*/admin(/:path)"), { + rest: "", + value: [ + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/admin"}, + {tag: "optionalSegment", value: [ + {tag: "staticContent", value: "/"}, + {tag: "namedSegment", value: "path"}, + ]}, + ], + }); + + t.deepEqual(parse("/"), { + rest: "", + value: [ + {tag: "staticContent", value: "/"}, + ], + }); + + t.deepEqual(parse("(/)"), { + rest: "", + value: [ + {tag: "optionalSegment", value: [ + {tag: "staticContent", value: "/"}, + ]}, + ], + }); + + t.deepEqual(parse("/admin(/:foo)/bar"), { + rest: "", + value: [ + {tag: "staticContent", value: "/admin"}, + {tag: "optionalSegment", value: [ + {tag: "staticContent", value: "/"}, + {tag: "namedSegment", value: "foo"}, + ]}, + {tag: "staticContent", value: "/bar"}, + ], + }); + + t.deepEqual(parse("/admin(*/)foo"), { + rest: "", + value: [ + {tag: "staticContent", value: "/admin"}, + {tag: "optionalSegment", value: [ + {tag: "wildcard", value: "*"}, + {tag: "staticContent", value: "/"}, + ]}, + {tag: "staticContent", value: "foo"}, + ], + }); + + t.deepEqual(parse("/v:major.:minor/*"), { + rest: "", + value: [ + {tag: "staticContent", value: "/v"}, + {tag: "namedSegment", value: "major"}, + {tag: "staticContent", value: "."}, + {tag: "namedSegment", value: "minor"}, + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + ], + }); + + t.deepEqual(parse("/v:v.:v/*"), { + rest: "", + value: [ + {tag: "staticContent", value: "/v"}, + {tag: "namedSegment", value: "v"}, + {tag: "staticContent", value: "."}, + {tag: "namedSegment", value: "v"}, + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + ], + }); + + t.deepEqual(parse("/:foo_bar"), { + rest: "", + value: [ + {tag: "staticContent", value: "/"}, + {tag: "namedSegment", value: "foo_bar"}, + ], + }); + + t.deepEqual(parse("((((a)b)c)d)"), { + rest: "", + value: [ + {tag: "optionalSegment", value: [ + {tag: "optionalSegment", value: [ + {tag: "optionalSegment", value: [ + {tag: "optionalSegment", value: [ + {tag: "staticContent", value: "a"}, + ]}, + {tag: "staticContent", value: "b"}, + ]}, + {tag: "staticContent", value: "c"}, + ]}, + {tag: "staticContent", value: "d"}, + ]}, + ], + }); + + t.deepEqual(parse("/vvv:version/*"), { + rest: "", + value: [ + {tag: "staticContent", value: "/vvv"}, + {tag: "namedSegment", value: "version"}, + {tag: "staticContent", value: "/"}, + {tag: "wildcard", value: "*"}, + ], + }); + + t.end(); +}); From 26a277590a3504550ac9ea9e34133772b792adfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 12:04:38 -0500 Subject: [PATCH 065/117] mv test/match-fixtures.{js,ts} --- test/match-fixtures.js | 293 ---------------------------------------- test/match-fixtures.ts | 294 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 293 deletions(-) delete mode 100644 test/match-fixtures.js create mode 100644 test/match-fixtures.ts diff --git a/test/match-fixtures.js b/test/match-fixtures.js deleted file mode 100644 index 2acce49..0000000 --- a/test/match-fixtures.js +++ /dev/null @@ -1,293 +0,0 @@ -import test from "tape"; - -import UrlPattern from "../dist/url-pattern.js"; - -test('match', function(t) { - let pattern = new UrlPattern('/foo'); - t.deepEqual(pattern.match('/foo'), {}); - - pattern = new UrlPattern('.foo'); - t.deepEqual(pattern.match('.foo'), {}); - - pattern = new UrlPattern('/foo'); - t.equals(pattern.match('/foobar'), undefined); - - pattern = new UrlPattern('.foo'); - t.equals(pattern.match('.foobar'), undefined); - - pattern = new UrlPattern('/foo'); - t.equals(pattern.match('/bar/foo'), undefined); - - pattern = new UrlPattern('.foo'); - t.equals(pattern.match('.bar.foo'), undefined); - - pattern = new UrlPattern(/foo/); - t.deepEqual(pattern.match('foo'), []); - - pattern = new UrlPattern(/\/foo\/(.*)/); - t.deepEqual(pattern.match('/foo/bar'), ['bar']); - - pattern = new UrlPattern(/\/foo\/(.*)/); - t.deepEqual(pattern.match('/foo/'), ['']); - - pattern = new UrlPattern('/user/:userId/task/:taskId'); - t.deepEqual(pattern.match('/user/10/task/52'), { - userId: '10', - taskId: '52' - } - ); - - pattern = new UrlPattern('.user.:userId.task.:taskId'); - t.deepEqual(pattern.match('.user.10.task.52'), { - userId: '10', - taskId: '52' - } - ); - - pattern = new UrlPattern('*/user/:userId'); - t.deepEqual(pattern.match('/school/10/user/10'), { - _: '/school/10', - userId: '10' - } - ); - - pattern = new UrlPattern('*-user-:userId'); - t.deepEqual(pattern.match('-school-10-user-10'), { - _: '-school-10', - userId: '10' - } - ); - - pattern = new UrlPattern('/admin*'); - t.deepEqual(pattern.match('/admin/school/10/user/10'), - {_: '/school/10/user/10'}); - - pattern = new UrlPattern('#admin*'); - t.deepEqual(pattern.match('#admin#school#10#user#10'), - {_: '#school#10#user#10'}); - - pattern = new UrlPattern('/admin/*/user/:userId'); - t.deepEqual(pattern.match('/admin/school/10/user/10'), { - _: 'school/10', - userId: '10' - } - ); - - pattern = new UrlPattern('$admin$*$user$:userId'); - t.deepEqual(pattern.match('$admin$school$10$user$10'), { - _: 'school$10', - userId: '10' - } - ); - - pattern = new UrlPattern('/admin/*/user/*/tail'); - t.deepEqual(pattern.match('/admin/school/10/user/10/12/tail'), - {_: ['school/10', '10/12']}); - - pattern = new UrlPattern('$admin$*$user$*$tail'); - t.deepEqual(pattern.match('$admin$school$10$user$10$12$tail'), - {_: ['school$10', '10$12']}); - - pattern = new UrlPattern('/admin/*/user/:id/*/tail'); - t.deepEqual(pattern.match('/admin/school/10/user/10/12/13/tail'), { - _: ['school/10', '12/13'], - id: '10' - } - ); - - pattern = new UrlPattern('^admin^*^user^:id^*^tail'); - t.deepEqual(pattern.match('^admin^school^10^user^10^12^13^tail'), { - _: ['school^10', '12^13'], - id: '10' - } - ); - - pattern = new UrlPattern('/*/admin(/:path)'); - t.deepEqual(pattern.match('/admin/admin/admin'), { - _: 'admin', - path: 'admin' - } - ); - - pattern = new UrlPattern('(/)'); - t.deepEqual(pattern.match(''), {}); - t.deepEqual(pattern.match('/'), {}); - - pattern = new UrlPattern('/admin(/foo)/bar'); - t.deepEqual(pattern.match('/admin/foo/bar'), {}); - t.deepEqual(pattern.match('/admin/bar'), {}); - - pattern = new UrlPattern('/admin(/:foo)/bar'); - t.deepEqual(pattern.match('/admin/baz/bar'), - {foo: 'baz'}); - t.deepEqual(pattern.match('/admin/bar'), {}); - - pattern = new UrlPattern('/admin/(*/)foo'); - t.deepEqual(pattern.match('/admin/foo'), {}); - t.deepEqual(pattern.match('/admin/baz/bar/biff/foo'), - {_: 'baz/bar/biff'}); - - pattern = new UrlPattern('/v:major.:minor/*'); - t.deepEqual(pattern.match('/v1.2/resource/'), { - _: 'resource/', - major: '1', - minor: '2' - } - ); - - pattern = new UrlPattern('/v:v.:v/*'); - t.deepEqual(pattern.match('/v1.2/resource/'), { - _: 'resource/', - v: ['1', '2'] - }); - - pattern = new UrlPattern('/:foo_bar'); - t.deepEqual(pattern.match('/_bar'), - {foo_bar: '_bar'}); - t.deepEqual(pattern.match('/a_bar'), - {foo_bar: 'a_bar'}); - t.deepEqual(pattern.match('/a__bar'), - {foo_bar: 'a__bar'}); - t.deepEqual(pattern.match('/a-b-c-d__bar'), - {foo_bar: 'a-b-c-d__bar'}); - t.deepEqual(pattern.match('/a b%c-d__bar'), - {foo_bar: 'a b%c-d__bar'}); - - pattern = new UrlPattern('((((a)b)c)d)'); - t.deepEqual(pattern.match(''), {}); - t.equal(pattern.match('a'), undefined); - t.equal(pattern.match('ab'), undefined); - t.equal(pattern.match('abc'), undefined); - t.deepEqual(pattern.match('abcd'), {}); - t.deepEqual(pattern.match('bcd'), {}); - t.deepEqual(pattern.match('cd'), {}); - t.deepEqual(pattern.match('d'), {}); - - pattern = new UrlPattern('/user/:range'); - t.deepEqual(pattern.match('/user/10-20'), - {range: '10-20'}); - - pattern = new UrlPattern('/user/:range'); - t.deepEqual(pattern.match('/user/10_20'), - {range: '10_20'}); - - pattern = new UrlPattern('/user/:range'); - t.deepEqual(pattern.match('/user/10 20'), - {range: '10 20'}); - - pattern = new UrlPattern('/user/:range'); - t.deepEqual(pattern.match('/user/10%20'), - {range: '10%20'}); - - pattern = new UrlPattern('/vvv:version/*'); - t.equal(undefined, pattern.match('/vvv/resource')); - t.deepEqual(pattern.match('/vvv1/resource'), { - _: 'resource', - version: '1' - } - ); - t.equal(undefined, pattern.match('/vvv1.1/resource')); - - pattern = new UrlPattern('/api/users/:id', - {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); - t.deepEqual(pattern.match('/api/users/someuser@example.com'), - {id: 'someuser@example.com'}); - - pattern = new UrlPattern('/api/users?username=:username', - {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); - t.deepEqual(pattern.match('/api/users?username=someone@example.com'), - {username: 'someone@example.com'}); - - pattern = new UrlPattern('/api/users?param1=:param1¶m2=:param2'); - t.deepEqual(pattern.match('/api/users?param1=foo¶m2=bar'), { - param1: 'foo', - param2: 'bar' - } - ); - - pattern = new UrlPattern(':scheme\\://:host(\\::port)', - {segmentValueCharset: 'a-zA-Z0-9-_~ %.'}); - t.deepEqual(pattern.match('ftp://ftp.example.com'), { - scheme: 'ftp', - host: 'ftp.example.com' - } - ); - t.deepEqual(pattern.match('ftp://ftp.example.com:8080'), { - scheme: 'ftp', - host: 'ftp.example.com', - port: '8080' - } - ); - t.deepEqual(pattern.match('https://example.com:80'), { - scheme: 'https', - host: 'example.com', - port: '80' - } - ); - - pattern = new UrlPattern(':scheme\\://:host(\\::port)(/api(/:resource(/:id)))', - {segmentValueCharset: 'a-zA-Z0-9-_~ %.@'}); - t.deepEqual(pattern.match('https://sss.www.localhost.com'), { - scheme: 'https', - host: 'sss.www.localhost.com' - } - ); - t.deepEqual(pattern.match('https://sss.www.localhost.com:8080'), { - scheme: 'https', - host: 'sss.www.localhost.com', - port: '8080' - } - ); - t.deepEqual(pattern.match('https://sss.www.localhost.com/api'), { - scheme: 'https', - host: 'sss.www.localhost.com' - } - ); - t.deepEqual(pattern.match('https://sss.www.localhost.com/api/security'), { - scheme: 'https', - host: 'sss.www.localhost.com', - resource: 'security' - } - ); - t.deepEqual(pattern.match('https://sss.www.localhost.com/api/security/bob@example.com'), { - scheme: 'https', - host: 'sss.www.localhost.com', - resource: 'security', - id: 'bob@example.com' - } - ); - - let regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - pattern = new UrlPattern(regex); - t.equal(undefined, pattern.match('10.10.10.10')); - t.equal(undefined, pattern.match('ip/10.10.10.10')); - t.equal(undefined, pattern.match('/ip/10.10.10.')); - t.equal(undefined, pattern.match('/ip/10.')); - t.equal(undefined, pattern.match('/ip/')); - t.deepEqual(pattern.match('/ip/10.10.10.10'), ['10', '10', '10', '10']); - t.deepEqual(pattern.match('/ip/127.0.0.1'), ['127', '0', '0', '1']); - - regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; - pattern = new UrlPattern(regex); - t.equal(undefined, pattern.match('10.10.10.10')); - t.equal(undefined, pattern.match('ip/10.10.10.10')); - t.equal(undefined, pattern.match('/ip/10.10.10.')); - t.equal(undefined, pattern.match('/ip/10.')); - t.equal(undefined, pattern.match('/ip/')); - t.deepEqual(pattern.match('/ip/10.10.10.10'), ['10.10.10.10']); - t.deepEqual(pattern.match('/ip/127.0.0.1'), ['127.0.0.1']); - - regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; - pattern = new UrlPattern(regex, ['ip']); - t.equal(undefined, pattern.match('10.10.10.10')); - t.equal(undefined, pattern.match('ip/10.10.10.10')); - t.equal(undefined, pattern.match('/ip/10.10.10.')); - t.equal(undefined, pattern.match('/ip/10.')); - t.equal(undefined, pattern.match('/ip/')); - t.deepEqual(pattern.match('/ip/10.10.10.10'), - {ip: '10.10.10.10'}); - t.deepEqual(pattern.match('/ip/127.0.0.1'), - {ip: '127.0.0.1'}); - - t.end(); -}); diff --git a/test/match-fixtures.ts b/test/match-fixtures.ts new file mode 100644 index 0000000..ba41d43 --- /dev/null +++ b/test/match-fixtures.ts @@ -0,0 +1,294 @@ +/* tslint:disable:max-line-length */ +import * as tape from "tape"; + +import UrlPattern from "../src/url-pattern"; + +tape("match", (t: tape.Test) => { + let pattern = new UrlPattern("/foo"); + t.deepEqual(pattern.match("/foo"), {}); + + pattern = new UrlPattern(".foo"); + t.deepEqual(pattern.match(".foo"), {}); + + pattern = new UrlPattern("/foo"); + t.equals(pattern.match("/foobar"), undefined); + + pattern = new UrlPattern(".foo"); + t.equals(pattern.match(".foobar"), undefined); + + pattern = new UrlPattern("/foo"); + t.equals(pattern.match("/bar/foo"), undefined); + + pattern = new UrlPattern(".foo"); + t.equals(pattern.match(".bar.foo"), undefined); + + pattern = new UrlPattern(/foo/); + t.deepEqual(pattern.match("foo"), []); + + pattern = new UrlPattern(/\/foo\/(.*)/); + t.deepEqual(pattern.match("/foo/bar"), ["bar"]); + + pattern = new UrlPattern(/\/foo\/(.*)/); + t.deepEqual(pattern.match("/foo/"), [""]); + + pattern = new UrlPattern("/user/:userId/task/:taskId"); + t.deepEqual(pattern.match("/user/10/task/52"), { + taskId: "52", + userId: "10", + }, + ); + + pattern = new UrlPattern(".user.:userId.task.:taskId"); + t.deepEqual(pattern.match(".user.10.task.52"), { + taskId: "52", + userId: "10", + }, + ); + + pattern = new UrlPattern("*/user/:userId"); + t.deepEqual(pattern.match("/school/10/user/10"), { + _: "/school/10", + userId: "10", + }, + ); + + pattern = new UrlPattern("*-user-:userId"); + t.deepEqual(pattern.match("-school-10-user-10"), { + _: "-school-10", + userId: "10", + }, + ); + + pattern = new UrlPattern("/admin*"); + t.deepEqual(pattern.match("/admin/school/10/user/10"), + {_: "/school/10/user/10"}); + + pattern = new UrlPattern("#admin*"); + t.deepEqual(pattern.match("#admin#school#10#user#10"), + {_: "#school#10#user#10"}); + + pattern = new UrlPattern("/admin/*/user/:userId"); + t.deepEqual(pattern.match("/admin/school/10/user/10"), { + _: "school/10", + userId: "10", + }, + ); + + pattern = new UrlPattern("$admin$*$user$:userId"); + t.deepEqual(pattern.match("$admin$school$10$user$10"), { + _: "school$10", + userId: "10", + }, + ); + + pattern = new UrlPattern("/admin/*/user/*/tail"); + t.deepEqual(pattern.match("/admin/school/10/user/10/12/tail"), + {_: ["school/10", "10/12"]}); + + pattern = new UrlPattern("$admin$*$user$*$tail"); + t.deepEqual(pattern.match("$admin$school$10$user$10$12$tail"), + {_: ["school$10", "10$12"]}); + + pattern = new UrlPattern("/admin/*/user/:id/*/tail"); + t.deepEqual(pattern.match("/admin/school/10/user/10/12/13/tail"), { + _: ["school/10", "12/13"], + id: "10", + }, + ); + + pattern = new UrlPattern("^admin^*^user^:id^*^tail"); + t.deepEqual(pattern.match("^admin^school^10^user^10^12^13^tail"), { + _: ["school^10", "12^13"], + id: "10", + }, + ); + + pattern = new UrlPattern("/*/admin(/:path)"); + t.deepEqual(pattern.match("/admin/admin/admin"), { + _: "admin", + path: "admin", + }, + ); + + pattern = new UrlPattern("(/)"); + t.deepEqual(pattern.match(""), {}); + t.deepEqual(pattern.match("/"), {}); + + pattern = new UrlPattern("/admin(/foo)/bar"); + t.deepEqual(pattern.match("/admin/foo/bar"), {}); + t.deepEqual(pattern.match("/admin/bar"), {}); + + pattern = new UrlPattern("/admin(/:foo)/bar"); + t.deepEqual(pattern.match("/admin/baz/bar"), + {foo: "baz"}); + t.deepEqual(pattern.match("/admin/bar"), {}); + + pattern = new UrlPattern("/admin/(*/)foo"); + t.deepEqual(pattern.match("/admin/foo"), {}); + t.deepEqual(pattern.match("/admin/baz/bar/biff/foo"), + {_: "baz/bar/biff"}); + + pattern = new UrlPattern("/v:major.:minor/*"); + t.deepEqual(pattern.match("/v1.2/resource/"), { + _: "resource/", + major: "1", + minor: "2", + }, + ); + + pattern = new UrlPattern("/v:v.:v/*"); + t.deepEqual(pattern.match("/v1.2/resource/"), { + _: "resource/", + v: ["1", "2"], + }); + + pattern = new UrlPattern("/:foo_bar"); + t.deepEqual(pattern.match("/_bar"), + {foo_bar: "_bar"}); + t.deepEqual(pattern.match("/a_bar"), + {foo_bar: "a_bar"}); + t.deepEqual(pattern.match("/a__bar"), + {foo_bar: "a__bar"}); + t.deepEqual(pattern.match("/a-b-c-d__bar"), + {foo_bar: "a-b-c-d__bar"}); + t.deepEqual(pattern.match("/a b%c-d__bar"), + {foo_bar: "a b%c-d__bar"}); + + pattern = new UrlPattern("((((a)b)c)d)"); + t.deepEqual(pattern.match(""), {}); + t.equal(pattern.match("a"), undefined); + t.equal(pattern.match("ab"), undefined); + t.equal(pattern.match("abc"), undefined); + t.deepEqual(pattern.match("abcd"), {}); + t.deepEqual(pattern.match("bcd"), {}); + t.deepEqual(pattern.match("cd"), {}); + t.deepEqual(pattern.match("d"), {}); + + pattern = new UrlPattern("/user/:range"); + t.deepEqual(pattern.match("/user/10-20"), + {range: "10-20"}); + + pattern = new UrlPattern("/user/:range"); + t.deepEqual(pattern.match("/user/10_20"), + {range: "10_20"}); + + pattern = new UrlPattern("/user/:range"); + t.deepEqual(pattern.match("/user/10 20"), + {range: "10 20"}); + + pattern = new UrlPattern("/user/:range"); + t.deepEqual(pattern.match("/user/10%20"), + {range: "10%20"}); + + pattern = new UrlPattern("/vvv:version/*"); + t.equal(undefined, pattern.match("/vvv/resource")); + t.deepEqual(pattern.match("/vvv1/resource"), { + _: "resource", + version: "1", + }, + ); + t.equal(undefined, pattern.match("/vvv1.1/resource")); + + pattern = new UrlPattern("/api/users/:id", + {segmentValueCharset: "a-zA-Z0-9-_~ %.@"}); + t.deepEqual(pattern.match("/api/users/someuser@example.com"), + {id: "someuser@example.com"}); + + pattern = new UrlPattern("/api/users?username=:username", + {segmentValueCharset: "a-zA-Z0-9-_~ %.@"}); + t.deepEqual(pattern.match("/api/users?username=someone@example.com"), + {username: "someone@example.com"}); + + pattern = new UrlPattern("/api/users?param1=:param1¶m2=:param2"); + t.deepEqual(pattern.match("/api/users?param1=foo¶m2=bar"), { + param1: "foo", + param2: "bar", + }, + ); + + pattern = new UrlPattern(":scheme\\://:host(\\::port)", + {segmentValueCharset: "a-zA-Z0-9-_~ %."}); + t.deepEqual(pattern.match("ftp://ftp.example.com"), { + host: "ftp.example.com", + scheme: "ftp", + }, + ); + t.deepEqual(pattern.match("ftp://ftp.example.com:8080"), { + host: "ftp.example.com", + port: "8080", + scheme: "ftp", + }, + ); + t.deepEqual(pattern.match("https://example.com:80"), { + host: "example.com", + port: "80", + scheme: "https", + }, + ); + + pattern = new UrlPattern(":scheme\\://:host(\\::port)(/api(/:resource(/:id)))", + {segmentValueCharset: "a-zA-Z0-9-_~ %.@"}); + t.deepEqual(pattern.match("https://sss.www.localhost.com"), { + host: "sss.www.localhost.com", + scheme: "https", + }, + ); + t.deepEqual(pattern.match("https://sss.www.localhost.com:8080"), { + host: "sss.www.localhost.com", + port: "8080", + scheme: "https", + }, + ); + t.deepEqual(pattern.match("https://sss.www.localhost.com/api"), { + host: "sss.www.localhost.com", + scheme: "https", + }, + ); + t.deepEqual(pattern.match("https://sss.www.localhost.com/api/security"), { + host: "sss.www.localhost.com", + resource: "security", + scheme: "https", + }, + ); + t.deepEqual(pattern.match("https://sss.www.localhost.com/api/security/bob@example.com"), { + host: "sss.www.localhost.com", + id: "bob@example.com", + resource: "security", + scheme: "https", + }, + ); + + let regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + pattern = new UrlPattern(regex); + t.equal(undefined, pattern.match("10.10.10.10")); + t.equal(undefined, pattern.match("ip/10.10.10.10")); + t.equal(undefined, pattern.match("/ip/10.10.10.")); + t.equal(undefined, pattern.match("/ip/10.")); + t.equal(undefined, pattern.match("/ip/")); + t.deepEqual(pattern.match("/ip/10.10.10.10"), ["10", "10", "10", "10"]); + t.deepEqual(pattern.match("/ip/127.0.0.1"), ["127", "0", "0", "1"]); + + regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; + pattern = new UrlPattern(regex); + t.equal(undefined, pattern.match("10.10.10.10")); + t.equal(undefined, pattern.match("ip/10.10.10.10")); + t.equal(undefined, pattern.match("/ip/10.10.10.")); + t.equal(undefined, pattern.match("/ip/10.")); + t.equal(undefined, pattern.match("/ip/")); + t.deepEqual(pattern.match("/ip/10.10.10.10"), ["10.10.10.10"]); + t.deepEqual(pattern.match("/ip/127.0.0.1"), ["127.0.0.1"]); + + regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; + pattern = new UrlPattern(regex, ["ip"]); + t.equal(undefined, pattern.match("10.10.10.10")); + t.equal(undefined, pattern.match("ip/10.10.10.10")); + t.equal(undefined, pattern.match("/ip/10.10.10.")); + t.equal(undefined, pattern.match("/ip/10.")); + t.equal(undefined, pattern.match("/ip/")); + t.deepEqual(pattern.match("/ip/10.10.10.10"), + {ip: "10.10.10.10"}); + t.deepEqual(pattern.match("/ip/127.0.0.1"), + {ip: "127.0.0.1"}); + + t.end(); +}); From 39d72eb2e409bc275a12d0e50e30b482aa2cad5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 12:08:37 -0500 Subject: [PATCH 066/117] test/stringify-fixtures.{js,ts} --- test/stringify-fixtures.js | 214 ------------------------------------- test/stringify-fixtures.ts | 183 +++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 214 deletions(-) delete mode 100644 test/stringify-fixtures.js create mode 100644 test/stringify-fixtures.ts diff --git a/test/stringify-fixtures.js b/test/stringify-fixtures.js deleted file mode 100644 index 7bf5109..0000000 --- a/test/stringify-fixtures.js +++ /dev/null @@ -1,214 +0,0 @@ -import test from "tape"; - -import UrlPattern from "../dist/url-pattern.js"; - -test('stringify', function(t) { - let pattern = new UrlPattern('/foo'); - t.equal('/foo', pattern.stringify()); - - pattern = new UrlPattern('/user/:userId/task/:taskId'); - t.equal('/user/10/task/52', pattern.stringify({ - userId: '10', - taskId: '52' - }) - ); - - pattern = new UrlPattern('/user/:userId/task/:taskId'); - t.equal('/user/10/task/52', pattern.stringify({ - userId: '10', - taskId: '52', - ignored: 'ignored' - }) - ); - - pattern = new UrlPattern('.user.:userId.task.:taskId'); - t.equal('.user.10.task.52', pattern.stringify({ - userId: '10', - taskId: '52' - }) - ); - - pattern = new UrlPattern('*/user/:userId'); - t.equal('/school/10/user/10', pattern.stringify({ - _: '/school/10', - userId: '10' - }) - ); - - pattern = new UrlPattern('*-user-:userId'); - t.equal('-school-10-user-10', pattern.stringify({ - _: '-school-10', - userId: '10' - }) - ); - - pattern = new UrlPattern('/admin*'); - t.equal('/admin/school/10/user/10', pattern.stringify({ - _: '/school/10/user/10'}) - ); - - pattern = new UrlPattern('/admin/*/user/*/tail'); - t.equal('/admin/school/10/user/10/12/tail', pattern.stringify({ - _: ['school/10', '10/12']})); - - pattern = new UrlPattern('/admin/*/user/:id/*/tail'); - t.equal('/admin/school/10/user/10/12/13/tail', pattern.stringify({ - _: ['school/10', '12/13'], - id: '10' - }) - ); - - pattern = new UrlPattern('/*/admin(/:path)'); - t.equal('/foo/admin/baz', pattern.stringify({ - _: 'foo', - path: 'baz' - }) - ); - t.equal('/foo/admin', pattern.stringify({ - _: 'foo'}) - ); - - pattern = new UrlPattern('(/)'); - t.equal('', pattern.stringify()); - - pattern = new UrlPattern('/admin(/foo)/bar'); - t.equal('/admin/bar', pattern.stringify()); - - pattern = new UrlPattern('/admin(/:foo)/bar'); - t.equal('/admin/bar', pattern.stringify()); - t.equal('/admin/baz/bar', pattern.stringify({ - foo: 'baz'}) - ); - - pattern = new UrlPattern('/admin/(*/)foo'); - t.equal('/admin/foo', pattern.stringify()); - t.equal('/admin/baz/bar/biff/foo', pattern.stringify({ - _: 'baz/bar/biff'}) - ); - - pattern = new UrlPattern('/v:major.:minor/*'); - t.equal('/v1.2/resource/', pattern.stringify({ - _: 'resource/', - major: '1', - minor: '2' - }) - ); - - pattern = new UrlPattern('/v:v.:v/*'); - t.equal('/v1.2/resource/', pattern.stringify({ - _: 'resource/', - v: ['1', '2']})); - - pattern = new UrlPattern('/:foo_bar'); - t.equal('/a_bar', pattern.stringify({ - foo_bar: 'a_bar'}) - ); - t.equal('/a__bar', pattern.stringify({ - foo_bar: 'a__bar'}) - ); - t.equal('/a-b-c-d__bar', pattern.stringify({ - foo_bar: 'a-b-c-d__bar'}) - ); - t.equal('/a b%c-d__bar', pattern.stringify({ - foo_bar: 'a b%c-d__bar'}) - ); - - pattern = new UrlPattern('((((a)b)c)d)'); - t.equal('', pattern.stringify()); - - pattern = new UrlPattern('(:a-)1-:b(-2-:c-3-:d(-4-*-:a))'); - t.equal('1-B', pattern.stringify({ - b: 'B'}) - ); - t.equal('A-1-B', pattern.stringify({ - a: 'A', - b: 'B' - }) - ); - t.equal('A-1-B', pattern.stringify({ - a: 'A', - b: 'B' - }) - ); - t.equal('A-1-B-2-C-3-D', pattern.stringify({ - a: 'A', - b: 'B', - c: 'C', - d: 'D' - }) - ); - t.equal('A-1-B-2-C-3-D-4-E-F', pattern.stringify({ - a: ['A', 'F'], - b: 'B', - c: 'C', - d: 'D', - _: 'E' - }) - ); - - pattern = new UrlPattern('/user/:range'); - t.equal('/user/10-20', pattern.stringify({ - range: '10-20'}) - ); - - t.end(); -}); - -test('stringify errors', function(t) { - let e; - t.plan(5); - - const pattern = new UrlPattern('(:a-)1-:b(-2-:c-3-:d(-4-*-:a))'); - - try { - pattern.stringify(); - } catch (error) { - e = error; - t.equal(e.message, "no values provided for key `b`"); - } - try { - pattern.stringify({ - a: 'A', - b: 'B', - c: 'C' - }); - } catch (error1) { - e = error1; - t.equal(e.message, "no values provided for key `d`"); - } - try { - pattern.stringify({ - a: 'A', - b: 'B', - d: 'D' - }); - } catch (error2) { - e = error2; - t.equal(e.message, "no values provided for key `c`"); - } - try { - pattern.stringify({ - a: 'A', - b: 'B', - c: 'C', - d: 'D', - _: 'E' - }); - } catch (error3) { - e = error3; - t.equal(e.message, "too few values provided for key `a`"); - } - try { - pattern.stringify({ - a: ['A', 'F'], - b: 'B', - c: 'C', - d: 'D' - }); - } catch (error4) { - e = error4; - t.equal(e.message, "no values provided for key `_`"); - } - - t.end(); -}); diff --git a/test/stringify-fixtures.ts b/test/stringify-fixtures.ts new file mode 100644 index 0000000..1190103 --- /dev/null +++ b/test/stringify-fixtures.ts @@ -0,0 +1,183 @@ +import * as tape from "tape"; + +import UrlPattern from "../src/url-pattern"; + +tape("stringify", (t: tape.Test) => { + let pattern = new UrlPattern("/foo"); + t.equal("/foo", pattern.stringify()); + + pattern = new UrlPattern("/user/:userId/task/:taskId"); + t.equal("/user/10/task/52", pattern.stringify({ + taskId: "52", + userId: "10", + })); + + pattern = new UrlPattern("/user/:userId/task/:taskId"); + t.equal("/user/10/task/52", pattern.stringify({ + ignored: "ignored", + taskId: "52", + userId: "10", + })); + + pattern = new UrlPattern(".user.:userId.task.:taskId"); + t.equal(".user.10.task.52", pattern.stringify({ + taskId: "52", + userId: "10", + })); + + pattern = new UrlPattern("*/user/:userId"); + t.equal("/school/10/user/10", pattern.stringify({ + _: "/school/10", + userId: "10", + })); + + pattern = new UrlPattern("*-user-:userId"); + t.equal("-school-10-user-10", pattern.stringify({ + _: "-school-10", + userId: "10", + })); + + pattern = new UrlPattern("/admin*"); + t.equal("/admin/school/10/user/10", pattern.stringify({ + _: "/school/10/user/10"})); + + pattern = new UrlPattern("/admin/*/user/*/tail"); + t.equal("/admin/school/10/user/10/12/tail", pattern.stringify({ + _: ["school/10", "10/12"]})); + + pattern = new UrlPattern("/admin/*/user/:id/*/tail"); + t.equal("/admin/school/10/user/10/12/13/tail", pattern.stringify({ + _: ["school/10", "12/13"], + id: "10", + })); + + pattern = new UrlPattern("/*/admin(/:path)"); + t.equal("/foo/admin/baz", pattern.stringify({ + _: "foo", + path: "baz", + })); + t.equal("/foo/admin", pattern.stringify({ _: "foo" })); + + pattern = new UrlPattern("(/)"); + t.equal("", pattern.stringify()); + + pattern = new UrlPattern("/admin(/foo)/bar"); + t.equal("/admin/bar", pattern.stringify()); + + pattern = new UrlPattern("/admin(/:foo)/bar"); + t.equal("/admin/bar", pattern.stringify()); + t.equal("/admin/baz/bar", pattern.stringify({ foo: "baz" })); + + pattern = new UrlPattern("/admin/(*/)foo"); + t.equal("/admin/foo", pattern.stringify()); + t.equal("/admin/baz/bar/biff/foo", pattern.stringify({ _: "baz/bar/biff" })); + + pattern = new UrlPattern("/v:major.:minor/*"); + t.equal("/v1.2/resource/", pattern.stringify({ + _: "resource/", + major: "1", + minor: "2", + })); + + pattern = new UrlPattern("/v:v.:v/*"); + t.equal("/v1.2/resource/", pattern.stringify({ + _: "resource/", + v: ["1", "2"]})); + + pattern = new UrlPattern("/:foo_bar"); + t.equal("/a_bar", pattern.stringify({ foo_bar: "a_bar" })); + t.equal("/a__bar", pattern.stringify({ foo_bar: "a__bar" })); + t.equal("/a-b-c-d__bar", pattern.stringify({ foo_bar: "a-b-c-d__bar" })); + t.equal("/a b%c-d__bar", pattern.stringify({ foo_bar: "a b%c-d__bar" })); + + pattern = new UrlPattern("((((a)b)c)d)"); + t.equal("", pattern.stringify()); + + pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:a))"); + t.equal("1-B", pattern.stringify({ b: "B" })); + t.equal("A-1-B", pattern.stringify({ + a: "A", + b: "B", + })); + t.equal("A-1-B", pattern.stringify({ + a: "A", + b: "B", + })); + t.equal("A-1-B-2-C-3-D", pattern.stringify({ + a: "A", + b: "B", + c: "C", + d: "D", + })); + t.equal("A-1-B-2-C-3-D-4-E-F", pattern.stringify({ + _: "E", + a: ["A", "F"], + b: "B", + c: "C", + d: "D", + })); + + pattern = new UrlPattern("/user/:range"); + t.equal("/user/10-20", pattern.stringify({ range: "10-20" })); + + t.end(); +}); + +tape("stringify errors", (t: tape.Test) => { + let e; + t.plan(5); + + const pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:a))"); + + try { + pattern.stringify(); + } catch (error) { + e = error; + t.equal(e.message, "no values provided for key `b`"); + } + try { + pattern.stringify({ + a: "A", + b: "B", + c: "C", + }); + } catch (error1) { + e = error1; + t.equal(e.message, "no values provided for key `d`"); + } + try { + pattern.stringify({ + a: "A", + b: "B", + d: "D", + }); + } catch (error2) { + e = error2; + t.equal(e.message, "no values provided for key `c`"); + } + try { + pattern.stringify({ + _: "E", + a: "A", + b: "B", + c: "C", + d: "D", + }); + } catch (error3) { + e = error3; + t.equal(e.message, "too few values provided for key `a`"); + } + try { + pattern.stringify({ + a: ["A", "F"], + b: "B", + c: "C", + d: "D", + }); + } catch (error4) { + e = error4; + t.equal(e.message, "no values provided for key `_`"); + } + + t.end(); +}); From 4b12c66f42fd9c9c5d751aff27ee892fb192961a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 6 May 2019 14:43:31 -0500 Subject: [PATCH 067/117] some work on readme --- README.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6fb619b..6d4982b 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ turn strings into data or data into strings.** > This is a great little library -- thanks! > [michael](https://github.com/snd/url-pattern/pull/7) -[make pattern:](#make-pattern-from-string) +[make a pattern:](#make-pattern-from-string) ``` javascript > const pattern = new UrlPattern("/api/users(/:id)"); ``` -[match pattern against string and extract values:](#match-pattern-against-string) +[match a pattern against a string and extract values:](#match-pattern-against-string) ``` javascript > pattern.match("/api/users/10"); {id: "10"} @@ -29,10 +29,24 @@ turn strings into data or data into strings.** null ``` -[generate string from pattern and values:](#stringify-patterns) +[generate a string from a pattern and values:](#stringify-patterns) ``` javascript -> pattern.stringify() // "/api/users" -pattern.stringify({id: 20}) // "/api/users/20" +> pattern.stringify() +"/api/users" + +> pattern.stringify({id: 20}) +"/api/users/20" +``` + +don't like the syntax? [customize it:](#customize-the-pattern-syntax) +```javascript +> const pattern = new UrlPattern("/api/users/{id}", { + segmentNameEndChar: "}", + segmentNameStartChar: "{", +} + +> pattern.match("/api/users/5") +{id: "10"} ``` - continuously tested in Node.js (0.12, 4.2.3 and 5.3) and all relevant browsers: From 9c9f9f86d4cb4ad7f0f23ebe9aef473a02e44418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:28:33 -0500 Subject: [PATCH 068/117] much better error message on partial parse --- src/url-pattern.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 58d3147..b3eb863 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -105,12 +105,16 @@ export default class UrlPattern { const parser = newUrlPatternParser(options); const parsed = parser(pattern); if (parsed == null) { - // TODO better error message throw new Error("couldn't parse pattern"); } - if (parsed.rest !== "") { - // TODO better error message - throw new Error("could only partially parse pattern"); + if (parsed.rest.length !== 0) { + const failureIndex = pattern.length - parsed.rest.length; + throw new Error([ + `could only partially parse pattern.`, + `failure at character ${ failureIndex + 1} in pattern:`, + pattern, + " ".repeat(failureIndex) + "^ parsing failed here", + ].join("\n")); } const ast = parsed.value; this.ast = ast; From 60cfd38d269839b90f35501c7049e6438df851c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:29:04 -0500 Subject: [PATCH 069/117] package.json: add ts-node command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e123369..649ef30 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "prepublish": "npm run compile", "lint": "tslint --project .", "test": "tape -r ts-node/register test/*.ts", + "ts-node": "ts-node", "coverage": "nyc npm test", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" From 7c0d663aeb8d5ece359ad71d3bc78c11afe6bdd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:29:43 -0500 Subject: [PATCH 070/117] add some tests for parsercombinators --- test/parsercombinators.ts | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/parsercombinators.ts diff --git a/test/parsercombinators.ts b/test/parsercombinators.ts new file mode 100644 index 0000000..defca2d --- /dev/null +++ b/test/parsercombinators.ts @@ -0,0 +1,40 @@ +import * as tape from "tape"; + +import { + newRegexParser, + newStringParser, +} from "../src/parsercombinators"; + +tape("newStringParser", (t: tape.Test) => { + const parse = newStringParser("foo"); + t.deepEqual(parse("foo"), { + rest: "", + value: "foo", + }); + t.deepEqual(parse("foobar"), { + rest: "bar", + value: "foo", + }); + t.equal(parse("bar"), undefined); + t.equal(parse(""), undefined); + t.end(); +}); + +tape("newRegexParser", (t: tape.Test) => { + const parse = newRegexParser(/^[a-zA-Z0-9]+/); + t.deepEqual(parse("foobar"), { + rest: "", + value: "foobar", + }); + t.equal(parse("_aa"), undefined); + t.deepEqual(parse("a"), { + rest: "", + value: "a", + }); + t.deepEqual(parse("foo90$bar"), { + rest: "$bar", + value: "foo90", + }); + t.equal(parse(""), undefined); + t.end(); +}); From 603b51002bc0ddaa52283971b1e534a14d80d7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:32:06 -0500 Subject: [PATCH 071/117] README.md: wording and minor changes --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d4982b..64349c5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ null "/api/users/20" ``` -don't like the syntax? [customize it:](#customize-the-pattern-syntax) +prefer a different syntax? [customize it:](#customize-the-pattern-syntax) ```javascript > const pattern = new UrlPattern("/api/users/{id}", { segmentNameEndChar: "}", @@ -79,7 +79,7 @@ bower install url-pattern ``` ```javascript -const UrlPattern = require("url-pattern"); +> const UrlPattern = require("url-pattern"); ``` ``` javascript @@ -94,6 +94,7 @@ const UrlPattern = require("url-pattern"); > pattern.match("/v/"); null ``` + ``` javascript > var pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)") From 581bc1496259d18fbcd19aa816077eecc245303a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:32:35 -0500 Subject: [PATCH 072/117] make tests for errors pass again --- test/errors.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/errors.ts b/test/errors.ts index f8798b2..1ce8388 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -51,7 +51,12 @@ tape("invalid variable name in pattern", (t: tape.Test) => { try { new UrlPattern("foo:."); } catch (error) { - t.equal(error.message, "could only partially parse pattern"); + t.equal(error.message, [ + "could only partially parse pattern.", + "failure at character 4 in pattern:", + "foo:.", + " ^ parsing failed here", + ].join("\n")); } t.end(); }); @@ -66,7 +71,12 @@ tape("too many closing parentheses", (t: tape.Test) => { try { new UrlPattern("((foo)))bar"); } catch (error) { - t.equal(error.message, "could only partially parse pattern"); + t.equal(error.message, [ + "could only partially parse pattern.", + "failure at character 8 in pattern:", + "((foo)))bar", + " ^ parsing failed here", + ].join("\n")); } t.end(); }); From 7931231c67f968aa40a9a4b3e1c83ec279f64e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:32:59 -0500 Subject: [PATCH 073/117] add some more match fixtures --- test/match-fixtures.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/match-fixtures.ts b/test/match-fixtures.ts index ba41d43..1cf8855 100644 --- a/test/match-fixtures.ts +++ b/test/match-fixtures.ts @@ -290,5 +290,21 @@ tape("match", (t: tape.Test) => { t.deepEqual(pattern.match("/ip/127.0.0.1"), {ip: "127.0.0.1"}); + pattern = new UrlPattern("https\\://translate.google.com/translate?sl=auto&tl=:targetLanguage&u=:url", { + segmentValueCharset: "a-zA-Z0-9-_~ %.", + }); + t.deepEqual(pattern.match("https://translate.google.com/translate?sl=auto&tl=es&u=yahoo.com"), { + targetLanguage: "es", + url: "yahoo.com", + }); + + pattern = new UrlPattern("https\\://translate.google.com/translate?sl=auto&tl=:target_language&u=:url", { + segmentValueCharset: "a-zA-Z0-9-_~ %.", + }); + t.deepEqual(pattern.match("https://translate.google.com/translate?sl=auto&tl=es&u=yahoo.com"), { + target_language: "es", + url: "yahoo.com", + }); + t.end(); }); From f074f80a8763ebbbb7f267bfae411573d0f1ef97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:41:45 -0500 Subject: [PATCH 074/117] add comments to tests --- test/match-fixtures.ts | 4 +++- test/readme.ts | 2 ++ test/stringify-fixtures.ts | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/match-fixtures.ts b/test/match-fixtures.ts index 1cf8855..c93eb2b 100644 --- a/test/match-fixtures.ts +++ b/test/match-fixtures.ts @@ -1,4 +1,6 @@ -/* tslint:disable:max-line-length */ +// tests to ensure that there are no regressions in matching functionality +// +// tslint:disable:max-line-length import * as tape from "tape"; import UrlPattern from "../src/url-pattern"; diff --git a/test/readme.ts b/test/readme.ts index b1f1f00..177b503 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -1,3 +1,5 @@ +// tests for all the examples in the readme + import * as tape from "tape"; import UrlPattern from "../src/url-pattern"; diff --git a/test/stringify-fixtures.ts b/test/stringify-fixtures.ts index 1190103..c745d0c 100644 --- a/test/stringify-fixtures.ts +++ b/test/stringify-fixtures.ts @@ -1,3 +1,5 @@ +// tests to ensure that there are no regressions in stringify functionality + import * as tape from "tape"; import UrlPattern from "../src/url-pattern"; From 515a654a171d84d1d666d0355cf6d47dc00d981a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 14:49:08 -0500 Subject: [PATCH 075/117] sync up readme and tests for readme a bit --- README.md | 16 ++++++++-------- test/readme.ts | 26 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 64349c5..0348ed3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ turn strings into data or data into strings.** {} > pattern.match("/api/products/5"); -null +undefined ``` [generate a string from a pattern and values:](#stringify-patterns) @@ -46,7 +46,7 @@ prefer a different syntax? [customize it:](#customize-the-pattern-syntax) } > pattern.match("/api/users/5") -{id: "10"} +{id: "5"} ``` - continuously tested in Node.js (0.12, 4.2.3 and 5.3) and all relevant browsers: @@ -92,7 +92,7 @@ bower install url-pattern {major: "2", _: "users"} > pattern.match("/v/"); -null +undefined ``` ``` javascript @@ -111,7 +111,7 @@ null {subdomain: "mail", domain: "google", tld: "com", port: "80", _: "mail"} > pattern.match("google"); -null +undefined ``` ## make pattern from string @@ -133,11 +133,11 @@ match returns the extracted segments: {id: "10"} ``` -or `null` if there was no match: +or `undefined` if there was no match: ``` javascript > pattern.match("/api/products/5"); -null +undefined ``` patterns are compiled into regexes which makes `.match()` superfast. @@ -216,7 +216,7 @@ if the pattern was created from a regex an array of the captured groups is retur ["users"] > pattern.match("/apiii/test"); -null +undefined ``` when making a pattern from a regex @@ -236,7 +236,7 @@ returns objects on match with each key mapped to a captured value: {resource: "users", id: "5"} > pattern.match("/api/users/foo"); -null +undefined ``` ## stringify patterns diff --git a/test/readme.ts b/test/readme.ts index 177b503..3c6b031 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -4,13 +4,37 @@ import * as tape from "tape"; import UrlPattern from "../src/url-pattern"; -tape("simple", (t: tape.Test) => { +tape("match a pattern against a string and extract values", (t: tape.Test) => { const pattern = new UrlPattern("/api/users/:id"); t.deepEqual(pattern.match("/api/users/10"), {id: "10"}); t.equal(pattern.match("/api/products/5"), undefined); t.end(); }); +tape("generate a string from a pattern and values", (t: tape.Test) => { + const pattern = new UrlPattern("/api/users/:id"); + t.equal(pattern.stringify(), "/api/users"); + t.equal(pattern.stringify({id: 20}), "/api/users/20"); + t.end(); +}); + +tape("prefer a different syntax. customize it", (t: tape.Test) => { + const options = { + segmentNameEndChar: "}", + segmentNameStartChar: "{", + }; + + const pattern = new UrlPattern( + "/api/users/{id}", + options, + ); + + t.deepEqual(pattern.match("/users/5"), { + id: "5", + }); + t.end(); +}); + tape("api versioning", (t: tape.Test) => { const pattern = new UrlPattern("/v:major(.:minor)/*"); t.deepEqual(pattern.match("/v1.2/"), {major: "1", minor: "2", _: ""}); From ac5ea66e8c3225a53166354e23d18fc96c5599cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 15:49:27 -0500 Subject: [PATCH 076/117] readme: var to const/let and fix paths --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0348ed3..72c6689 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ undefined ``` ``` javascript -> var pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)") +> const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)") > pattern.match("google.de"); {domain: "google", tld: "de"} @@ -117,7 +117,7 @@ undefined ## make pattern from string ```javascript -> var pattern = new UrlPattern("/api/users/:id"); +> const pattern = new UrlPattern("/api/users/:id"); ``` a `pattern` is immutable after construction. @@ -159,7 +159,7 @@ if a named segment **name** occurs more than once in the pattern string, then the multiple results are stored in an array on the returned object: ```javascript -> var pattern = new UrlPattern("/api/users/:ids/posts/:ids"); +> const pattern = new UrlPattern("/api/users/:ids/posts/:ids"); > pattern.match("/api/users/10/posts/5"); {ids: ["10", "5"]} ``` @@ -169,7 +169,7 @@ then the multiple results are stored in an array on the returned object: to make part of a pattern optional just wrap it in `(` and `)`: ```javascript -> var pattern = new UrlPattern( +> const pattern = new UrlPattern( "(http(s)\\://)(:subdomain.):domain.:tld(/*)" ); ``` @@ -206,7 +206,7 @@ otherwise `_` contains an array of matching strings. ## make pattern from regex ```javascript -> var pattern = new UrlPattern(/^\/api\/(.*)$/); +> const pattern = new UrlPattern(/^\/api\/(.*)$/); ``` if the pattern was created from a regex an array of the captured groups is returned on a match: @@ -224,7 +224,7 @@ you can pass an array of keys as the second argument. returns objects on match with each key mapped to a captured value: ```javascript -> var pattern = new UrlPattern( +> const pattern = new UrlPattern( /^\/api\/([^\/]+)(?:\/(\d+))?$/, ["resource", "id"] ); @@ -242,7 +242,7 @@ undefined ## stringify patterns ```javascript -> var pattern = new UrlPattern("/api/users/:id"); +> const pattern = new UrlPattern("/api/users/:id"); > pattern.stringify({id: 10}) "/api/users/10" @@ -252,7 +252,7 @@ optional segments are only included in the output if they contain named segments and/or wildcards and values for those are provided: ```javascript -> var pattern = new UrlPattern("/api/users(/:id)"); +> const pattern = new UrlPattern("/api/users(/:id)"); > pattern.stringify() "/api/users" @@ -270,14 +270,14 @@ params and not all of them are provided. *one provided value for an optional segment makes all values in that optional segment required.* -[look at the tests for additional examples of `.stringify`](test/stringify-fixtures.coffee) +[look at the tests for additional examples of `.stringify`](test/stringify-fixtures.ts) ## customize the pattern syntax finally we can completely change pattern-parsing and regex-compilation to suit our needs: ```javascript -> var options = {}; +> let options = {}; ``` let's change the char used for escaping (default `\\`): @@ -322,7 +322,7 @@ let's change the char used to denote a wildcard (default `*`): pass options as the second argument to the constructor: ```javascript -> var pattern = new UrlPattern( +> const pattern = new UrlPattern( "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]", options ); From e14c06459832d7d1e382dfede70b32dd9bdbb68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 15:49:44 -0500 Subject: [PATCH 077/117] readme: document named wildcards --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72c6689..0aa39e0 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,15 @@ wildcard matches are collected in the `_` property: if there is only one wildcard then `_` contains the matching string. otherwise `_` contains an array of matching strings. -[look at the tests for additional examples of `.match`](test/match-fixtures.coffee) +wildcards can be named like this: + +```javascript +> const pattern = new UrlPattern('/search/*:term'); +> pattern.match('/search/fruit'); +{term: 'fruit'} +``` + +[look at the tests for additional examples of `.match`](test/match-fixtures.ts) ## make pattern from regex From 5ae12ce62ea377ecaa2866b846c20c75295e6724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 15:51:04 -0500 Subject: [PATCH 078/117] parser: more support for named wildcards --- src/parser.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parser.ts b/src/parser.ts index fd3323c..5a56884 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -128,6 +128,8 @@ function baseAstNodeToRegexString(astNode: Ast, segmentValueCharset: string switch (astNode.tag) { case "wildcard": return "(.*?)"; + case "namedWildcard": + return "(.*?)"; case "namedSegment": return `([${ segmentValueCharset }]+)`; case "staticContent": @@ -154,6 +156,8 @@ export function astNodeToNames(astNode: Ast | Array>): string[] { switch (astNode.tag) { case "wildcard": return ["_"]; + case "namedWildcard": + return [astNode.value]; case "namedSegment": return [astNode.value]; case "staticContent": From d976ac82989c4ef8544b2c38fda8a931c745d050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 15:51:21 -0500 Subject: [PATCH 079/117] ast tests for named wildcards --- test/ast.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/ast.ts b/test/ast.ts index 62e4f76..9c71e30 100644 --- a/test/ast.ts +++ b/test/ast.ts @@ -50,6 +50,13 @@ tape("astNodeToRegexString and astNodeToNames", (t: tape.Test) => { t.end(); }); + t.test("just named wildcard", (t: tape.Test) => { + const parsed = parse("*:variable"); + t.equal(astNodeToRegexString(parsed.value), "^(.*?)$"); + t.deepEqual(astNodeToNames(parsed.value), ["variable"]); + t.end(); + }); + t.test("just optional static", (t: tape.Test) => { const parsed = parse("(foo)"); t.equal(astNodeToRegexString(parsed.value), "^(?:foo)?$"); @@ -70,6 +77,13 @@ tape("astNodeToRegexString and astNodeToNames", (t: tape.Test) => { t.deepEqual(astNodeToNames(parsed.value), ["_"]); t.end(); }); + + t.test("just optional named wildcard", (t: tape.Test) => { + const parsed = parse("(*:variable)"); + t.equal(astNodeToRegexString(parsed.value), "^(?:(.*?))?$"); + t.deepEqual(astNodeToNames(parsed.value), ["variable"]); + t.end(); + }); }); tape("getParam", (t: tape.Test) => { From 4d7ffa729bb61fcc54b7567b82cd7a02dce0dd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 15:51:34 -0500 Subject: [PATCH 080/117] make readme tests pass --- test/readme.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/readme.ts b/test/readme.ts index 3c6b031..97a8666 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -5,14 +5,15 @@ import * as tape from "tape"; import UrlPattern from "../src/url-pattern"; tape("match a pattern against a string and extract values", (t: tape.Test) => { - const pattern = new UrlPattern("/api/users/:id"); + const pattern = new UrlPattern("/api/users(/:id)"); t.deepEqual(pattern.match("/api/users/10"), {id: "10"}); + t.deepEqual(pattern.match("/api/users"), {}); t.equal(pattern.match("/api/products/5"), undefined); t.end(); }); tape("generate a string from a pattern and values", (t: tape.Test) => { - const pattern = new UrlPattern("/api/users/:id"); + const pattern = new UrlPattern("/api/users(/:id)"); t.equal(pattern.stringify(), "/api/users"); t.equal(pattern.stringify({id: 20}), "/api/users/20"); t.end(); @@ -29,7 +30,7 @@ tape("prefer a different syntax. customize it", (t: tape.Test) => { options, ); - t.deepEqual(pattern.match("/users/5"), { + t.deepEqual(pattern.match("/api/users/5"), { id: "5", }); t.end(); From 78865f214cfb5b07331ad9918aeb8cfa9feb64f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 17:40:40 -0500 Subject: [PATCH 081/117] add test for error increasing coverage --- test/errors.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/errors.ts b/test/errors.ts index 1ce8388..c37bfdb 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -6,7 +6,7 @@ import UrlPattern from "../src/url-pattern"; const UntypedUrlPattern: any = UrlPattern; tape("invalid argument", (t: tape.Test) => { - t.plan(5); + t.plan(6); try { new UntypedUrlPattern(); @@ -33,6 +33,12 @@ tape("invalid argument", (t: tape.Test) => { } catch (error) { t.equal(error.message, "first argument must not contain whitespace"); } + try { + const str: any = "foo"; + new UrlPattern(str, []); + } catch (error) { + t.equal(error.message, "if first argument is a string second argument must be an options object or undefined"); + } t.end(); }); From c818b1043b5943524996305c85f2b44c04bf998e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Thu, 9 May 2019 18:10:30 -0500 Subject: [PATCH 082/117] src/parsercombinators.ts -> src/parser-combinators.ts --- src/{parsercombinators.ts => parser-combinators.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{parsercombinators.ts => parser-combinators.ts} (100%) diff --git a/src/parsercombinators.ts b/src/parser-combinators.ts similarity index 100% rename from src/parsercombinators.ts rename to src/parser-combinators.ts From ed72dc0c5422250bd8a71ef2044b8320e4f7ac49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Fri, 10 May 2019 19:10:01 -0500 Subject: [PATCH 083/117] renames --- src/ast-helpers.ts | 185 ++++++++++++++++++ src/parser.ts | 157 +-------------- src/url-pattern.ts | 15 +- test/ast.ts | 51 ++--- ...ercombinators.ts => parser-combinators.ts} | 2 +- 5 files changed, 223 insertions(+), 187 deletions(-) create mode 100644 src/ast-helpers.ts rename test/{parsercombinators.ts => parser-combinators.ts} (95%) diff --git a/src/ast-helpers.ts b/src/ast-helpers.ts new file mode 100644 index 0000000..bcf2e43 --- /dev/null +++ b/src/ast-helpers.ts @@ -0,0 +1,185 @@ +/** + * functions that work on ASTs returned from the url-pattern parser + * within the `parser` module. + */ + +import { + Ast, +} from "./parser-combinators"; + +import { + concatMap, + escapeStringForRegex, + stringConcatMap, +} from "./helpers"; + +import { + defaultOptions, +} from "./options"; + +/** + * converts an `astNode` within the AST of a parsed url-pattern into + * a string representing the regex that matches the url-pattern. + */ +function astToRegexString(astNode: Ast, segmentValueCharset: string): string { + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, (node) => astToRegexString(node, segmentValueCharset)); + } + + switch (astNode.tag) { + case "wildcard": + return "(.*?)"; + case "namedWildcard": + return "(.*?)"; + case "namedSegment": + return `([${ segmentValueCharset }]+)`; + case "staticContent": + return escapeStringForRegex(astNode.value); + case "optionalSegment": + return `(?:${ astToRegexString(astNode.value, segmentValueCharset) })?`; + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} + +/** + * converts the root `astNode` of a parsed url-pattern into + * a string representing the regex that matches the url-pattern. + */ +export function astRootToRegexString(astNode: Ast, segmentValueCharset?: string) { + if (segmentValueCharset == null) { + ({ segmentValueCharset } = defaultOptions); + } + return `^${ astToRegexString(astNode, segmentValueCharset) }$`; +} + +/** + * returns the names of any named segments and wildcards contained + * in the url-pattern represented by the given `astNode` in order. + */ +export function astToNames(astNode: Ast | Array>): string[] { + if (Array.isArray(astNode)) { + return concatMap(astNode, astToNames); + } + + switch (astNode.tag) { + case "wildcard": + return ["_"]; + case "namedWildcard": + return [astNode.value]; + case "namedSegment": + return [astNode.value]; + case "staticContent": + return []; + case "optionalSegment": + return astToNames(astNode.value); + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} + +/** + * since a param + * nextIndexes contains a mapping from param key to the + * next index + * `hasSideEffects` is a boolean that controls whether + */ +export function getParam( + params: { [index: string]: any }, + key: string, + nextIndexes: { [index: string]: number }, + hasSideEffects: boolean = false, +) { + const value = params[key]; + if (value == null) { + if (hasSideEffects) { + throw new Error(`no values provided for key \`${ key }\``); + } else { + return; + } + } + const index = nextIndexes[key] || 0; + const maxIndex = Array.isArray(value) ? value.length - 1 : 0; + if (index > maxIndex) { + if (hasSideEffects) { + throw new Error(`too few values provided for key \`${ key }\``); + } else { + return; + } + } + + const result = Array.isArray(value) ? value[index] : value; + + if (hasSideEffects) { + nextIndexes[key] = index + 1; + } + + return result; +} + +/** + * returns whether the given `astNode` contains + */ +function astNodeContainsAnySegmentsForParams( + astNode: Ast, + params: { [index: string]: any }, + nextIndexes: { [index: string]: number }, +): boolean { + if (Array.isArray(astNode)) { + let i = -1; + const { length } = astNode; + while (++i < length) { + if (astNodeContainsAnySegmentsForParams(astNode[i], params, nextIndexes)) { + return true; + } + } + return false; + } + + // TODO namedWildcard + switch (astNode.tag) { + case "wildcard": + return getParam(params, "_", nextIndexes, false) != null; + case "namedSegment": + return getParam(params, astNode.value, nextIndexes, false) != null; + case "staticContent": + return false; + case "optionalSegment": + return astNodeContainsAnySegmentsForParams(astNode.value, params, nextIndexes); + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} + +/** + * stringify an url-pattern AST + */ +export function stringify( + astNode: Ast | Array>, + params: { [index: string]: any }, + nextIndexes: { [index: string]: number } = {}, +): string { + // stringify an array by concatenating the result of stringifying its elements + if (Array.isArray(astNode)) { + return stringConcatMap(astNode, (node) => stringify(node, params, nextIndexes)); + } + + switch (astNode.tag) { + case "wildcard": + return getParam(params, "_", nextIndexes, true); + case "namedWildcard": + return getParam(params, astNode.value, nextIndexes, true); + case "namedSegment": + return getParam(params, astNode.value, nextIndexes, true); + case "staticContent": + return astNode.value; + case "optionalSegment": + if (astNodeContainsAnySegmentsForParams(astNode.value, params, nextIndexes)) { + return stringify(astNode.value, params, nextIndexes); + } else { + return ""; + } + default: + throw new Error(`unknown tag \`${ astNode.tag }\``); + } +} diff --git a/src/parser.ts b/src/parser.ts index 5a56884..afab6db 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -13,16 +13,9 @@ import { newRegexParser, newStringParser, Parser, -} from "./parsercombinators"; +} from "./parser-combinators"; import { - concatMap, - escapeStringForRegex, - stringConcatMap, -} from "./helpers"; - -import { - defaultOptions, IOptions, } from "./options"; @@ -117,151 +110,3 @@ export function newUrlPatternParser(options: IOptions): Parser> { return parsePattern; } - -// functions that further process ASTs returned as `.value` in parser results - -function baseAstNodeToRegexString(astNode: Ast, segmentValueCharset: string): string { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, (node) => baseAstNodeToRegexString(node, segmentValueCharset)); - } - - switch (astNode.tag) { - case "wildcard": - return "(.*?)"; - case "namedWildcard": - return "(.*?)"; - case "namedSegment": - return `([${ segmentValueCharset }]+)`; - case "staticContent": - return escapeStringForRegex(astNode.value); - case "optionalSegment": - return `(?:${ baseAstNodeToRegexString(astNode.value, segmentValueCharset) })?`; - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } -} - -export function astNodeToRegexString(astNode: Ast, segmentValueCharset?: string) { - if (segmentValueCharset == null) { - ({ segmentValueCharset } = defaultOptions); - } - return `^${ baseAstNodeToRegexString(astNode, segmentValueCharset) }$`; -} - -export function astNodeToNames(astNode: Ast | Array>): string[] { - if (Array.isArray(astNode)) { - return concatMap(astNode, astNodeToNames); - } - - switch (astNode.tag) { - case "wildcard": - return ["_"]; - case "namedWildcard": - return [astNode.value]; - case "namedSegment": - return [astNode.value]; - case "staticContent": - return []; - case "optionalSegment": - return astNodeToNames(astNode.value); - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } -} - -// TODO better name -export function getParam( - params: { [index: string]: any }, - key: string, - nextIndexes: { [index: string]: number }, - hasSideEffects: boolean = false, -) { - if (hasSideEffects == null) { - hasSideEffects = false; - } - const value = params[key]; - if (value == null) { - if (hasSideEffects) { - throw new Error(`no values provided for key \`${ key }\``); - } else { - return; - } - } - const index = nextIndexes[key] || 0; - const maxIndex = Array.isArray(value) ? value.length - 1 : 0; - if (index > maxIndex) { - if (hasSideEffects) { - throw new Error(`too few values provided for key \`${ key }\``); - } else { - return; - } - } - - const result = Array.isArray(value) ? value[index] : value; - - if (hasSideEffects) { - nextIndexes[key] = index + 1; - } - - return result; -} - -function astNodeContainsSegmentsForProvidedParams( - astNode: Ast, - params: { [index: string]: any }, - nextIndexes: { [index: string]: number }, -): boolean { - if (Array.isArray(astNode)) { - let i = -1; - const { length } = astNode; - while (++i < length) { - if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) { - return true; - } - } - return false; - } - - switch (astNode.tag) { - case "wildcard": - return getParam(params, "_", nextIndexes, false) != null; - case "namedSegment": - return getParam(params, astNode.value, nextIndexes, false) != null; - case "staticContent": - return false; - case "optionalSegment": - return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes); - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } -} - -/* - * stringify a url pattern AST - */ -export function stringify( - astNode: Ast | Array>, - params: { [index: string]: any }, - nextIndexes: { [index: string]: number } = {}, -): string { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, (node) => stringify(node, params, nextIndexes)); - } - - switch (astNode.tag) { - case "wildcard": - return getParam(params, "_", nextIndexes, true); - case "namedSegment": - return getParam(params, astNode.value, nextIndexes, true); - case "staticContent": - return astNode.value; - case "optionalSegment": - if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) { - return stringify(astNode.value, params, nextIndexes); - } else { - return ""; - } - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } -} diff --git a/src/url-pattern.ts b/src/url-pattern.ts index b3eb863..53894de 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -5,7 +5,7 @@ import { import { Ast, -} from "./parsercombinators"; +} from "./parser-combinators"; import { defaultOptions, @@ -14,12 +14,15 @@ import { } from "./options"; import { - astNodeToNames, - astNodeToRegexString, newUrlPatternParser, - stringify, } from "./parser"; +import { + astRootToRegexString, + astToNames, + stringify, +} from "./ast-helpers"; + export default class UrlPattern { public readonly isRegex: boolean; public readonly regex: RegExp; @@ -119,8 +122,8 @@ export default class UrlPattern { const ast = parsed.value; this.ast = ast; - this.regex = new RegExp(astNodeToRegexString(ast, options.segmentValueCharset)); - this.names = astNodeToNames(ast); + this.regex = new RegExp(astRootToRegexString(ast, options.segmentValueCharset)); + this.names = astToNames(ast); } public match(url: string): object | undefined { diff --git a/test/ast.ts b/test/ast.ts index 9c71e30..43e781a 100644 --- a/test/ast.ts +++ b/test/ast.ts @@ -2,86 +2,89 @@ import * as tape from "tape"; import { - astNodeToNames, - astNodeToRegexString, - getParam, newUrlPatternParser, } from "../src/parser"; +import { + astRootToRegexString, + astToNames, + getParam, +} from "../src/ast-helpers"; + import { defaultOptions, } from "../src/options"; const parse: any = newUrlPatternParser(defaultOptions); -tape("astNodeToRegexString and astNodeToNames", (t: tape.Test) => { +tape("astRootToRegexString and astToNames", (t: tape.Test) => { t.test("just static alphanumeric", (t: tape.Test) => { const parsed = parse("user42"); - t.equal(astNodeToRegexString(parsed.value), "^user42$"); - t.deepEqual(astNodeToNames(parsed.value), []); + t.equal(astRootToRegexString(parsed.value), "^user42$"); + t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just static escaped", (t: tape.Test) => { const parsed = parse("/api/v1/users"); - t.equal(astNodeToRegexString(parsed.value), "^\\/api\\/v1\\/users$"); - t.deepEqual(astNodeToNames(parsed.value), []); + t.equal(astRootToRegexString(parsed.value), "^\\/api\\/v1\\/users$"); + t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just single char variable", (t: tape.Test) => { const parsed = parse(":a"); - t.equal(astNodeToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); - t.deepEqual(astNodeToNames(parsed.value), ["a"]); + t.equal(astRootToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); + t.deepEqual(astToNames(parsed.value), ["a"]); t.end(); }); t.test("just variable", (t: tape.Test) => { const parsed = parse(":variable"); - t.equal(astNodeToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); - t.deepEqual(astNodeToNames(parsed.value), ["variable"]); + t.equal(astRootToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); + t.deepEqual(astToNames(parsed.value), ["variable"]); t.end(); }); t.test("just wildcard", (t: tape.Test) => { const parsed = parse("*"); - t.equal(astNodeToRegexString(parsed.value), "^(.*?)$"); - t.deepEqual(astNodeToNames(parsed.value), ["_"]); + t.equal(astRootToRegexString(parsed.value), "^(.*?)$"); + t.deepEqual(astToNames(parsed.value), ["_"]); t.end(); }); t.test("just named wildcard", (t: tape.Test) => { const parsed = parse("*:variable"); - t.equal(astNodeToRegexString(parsed.value), "^(.*?)$"); - t.deepEqual(astNodeToNames(parsed.value), ["variable"]); + t.equal(astRootToRegexString(parsed.value), "^(.*?)$"); + t.deepEqual(astToNames(parsed.value), ["variable"]); t.end(); }); t.test("just optional static", (t: tape.Test) => { const parsed = parse("(foo)"); - t.equal(astNodeToRegexString(parsed.value), "^(?:foo)?$"); - t.deepEqual(astNodeToNames(parsed.value), []); + t.equal(astRootToRegexString(parsed.value), "^(?:foo)?$"); + t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just optional variable", (t: tape.Test) => { const parsed = parse("(:foo)"); - t.equal(astNodeToRegexString(parsed.value), "^(?:([a-zA-Z0-9-_~ %]+))?$"); - t.deepEqual(astNodeToNames(parsed.value), ["foo"]); + t.equal(astRootToRegexString(parsed.value), "^(?:([a-zA-Z0-9-_~ %]+))?$"); + t.deepEqual(astToNames(parsed.value), ["foo"]); t.end(); }); t.test("just optional wildcard", (t: tape.Test) => { const parsed = parse("(*)"); - t.equal(astNodeToRegexString(parsed.value), "^(?:(.*?))?$"); - t.deepEqual(astNodeToNames(parsed.value), ["_"]); + t.equal(astRootToRegexString(parsed.value), "^(?:(.*?))?$"); + t.deepEqual(astToNames(parsed.value), ["_"]); t.end(); }); t.test("just optional named wildcard", (t: tape.Test) => { const parsed = parse("(*:variable)"); - t.equal(astNodeToRegexString(parsed.value), "^(?:(.*?))?$"); - t.deepEqual(astNodeToNames(parsed.value), ["variable"]); + t.equal(astRootToRegexString(parsed.value), "^(?:(.*?))?$"); + t.deepEqual(astToNames(parsed.value), ["variable"]); t.end(); }); }); diff --git a/test/parsercombinators.ts b/test/parser-combinators.ts similarity index 95% rename from test/parsercombinators.ts rename to test/parser-combinators.ts index defca2d..03e32b5 100644 --- a/test/parsercombinators.ts +++ b/test/parser-combinators.ts @@ -3,7 +3,7 @@ import * as tape from "tape"; import { newRegexParser, newStringParser, -} from "../src/parsercombinators"; +} from "../src/parser-combinators"; tape("newStringParser", (t: tape.Test) => { const parse = newStringParser("foo"); From 497c67f070aa77f9a666672668ef4f7135e19e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 20:01:11 -0500 Subject: [PATCH 084/117] anonymous wildcards aren't captured. names must be unique. refactoring --- README.md | 42 +++---- src/ast-helpers.ts | 244 ++++++++++++++++--------------------- src/helpers.ts | 29 +---- src/parser.ts | 2 +- src/url-pattern.ts | 3 +- test/ast.ts | 185 +++------------------------- test/helpers.ts | 17 --- test/match-fixtures.ts | 178 +++++++++++++++++---------- test/parser.ts | 55 +++++---- test/readme.ts | 56 +++------ test/stringify-fixtures.ts | 153 +++++++++++++---------- 11 files changed, 396 insertions(+), 568 deletions(-) diff --git a/README.md b/README.md index 0aa39e0..a6a9d86 100644 --- a/README.md +++ b/README.md @@ -86,17 +86,17 @@ bower install url-pattern > const pattern = new UrlPattern("/v:major(.:minor)/*"); > pattern.match("/v1.2/"); -{major: "1", minor: "2", _: ""} +{major: "1", minor: "2"} > pattern.match("/v2/users"); -{major: "2", _: "users"} +{major: "2"} > pattern.match("/v/"); undefined ``` ``` javascript -> const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)") +> const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*:path)") > pattern.match("google.de"); {domain: "google", tld: "de"} @@ -105,10 +105,10 @@ undefined {subdomain: "www", domain: "google", tld: "com"} > pattern.match("http://mail.google.com/mail"); -{subdomain: "mail", domain: "google", tld: "com", _: "mail"} +{subdomain: "mail", domain: "google", tld: "com", path: "mail"} -> pattern.match("http://mail.google.com:80/mail"); -{subdomain: "mail", domain: "google", tld: "com", port: "80", _: "mail"} +> pattern.match("http://mail.google.com:80/mail/inbox"); +{subdomain: "mail", domain: "google", tld: "com", port: "80", path: "mail/inbox"} > pattern.match("google"); undefined @@ -155,14 +155,7 @@ a named segment match stops at `/`, `.`, ... but not at `_`, `-`, ` `, `%`... [you can change these character sets. click here to see how.](#customize-the-pattern-syntax) -if a named segment **name** occurs more than once in the pattern string, -then the multiple results are stored in an array on the returned object: - -```javascript -> const pattern = new UrlPattern("/api/users/:ids/posts/:ids"); -> pattern.match("/api/users/10/posts/5"); -{ids: ["10", "5"]} -``` +a named segment name can only occur once in the pattern string. ## optional segments, wildcards and escaping @@ -170,7 +163,7 @@ to make part of a pattern optional just wrap it in `(` and `)`: ```javascript > const pattern = new UrlPattern( - "(http(s)\\://)(:subdomain.):domain.:tld(/*)" + "(http(s)\\://)(:subdomain.):domain.:tld(/*:path)" ); ``` @@ -190,18 +183,15 @@ optional named segments are stored in the corresponding property only if they ar {subdomain: "www", domain: "google", tld: "com"} ``` -`*` in patterns are wildcards and match anything. -wildcard matches are collected in the `_` property: +`:*{name}` in patterns are named wildcards and match anything. ```javascript > pattern.match("http://mail.google.com/mail"); -{subdomain: "mail", domain: "google", tld: "com", _: "mail"} +{subdomain: "mail", domain: "google", tld: "com", path: "mail"} ``` -if there is only one wildcard then `_` contains the matching string. -otherwise `_` contains an array of matching strings. - wildcards can be named like this: +unnamed wildcards are not collected. ```javascript > const pattern = new UrlPattern('/search/*:term'); @@ -269,8 +259,9 @@ and/or wildcards and values for those are provided: "/api/users/10" ``` -wildcards (key = `_`), deeply nested optional groups and multiple value arrays should stringify as expected. +named wildcards and deeply nested optional groups should stringify as expected. +TODO an error is thrown if a value that is not in an optional group is not provided. an error is thrown if an optional segment contains multiple @@ -278,6 +269,9 @@ params and not all of them are provided. *one provided value for an optional segment makes all values in that optional segment required.* +TODO +anonymous wildcards are ignored. + [look at the tests for additional examples of `.stringify`](test/stringify-fixtures.ts) ## customize the pattern syntax @@ -331,7 +325,7 @@ pass options as the second argument to the constructor: ```javascript > const pattern = new UrlPattern( - "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]", + "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?$path]", options ); ``` @@ -344,7 +338,7 @@ then match: sub_domain: "mail", domain: "google", "toplevel-domain": "com", - _: "mail" + path: "mail" } ``` diff --git a/src/ast-helpers.ts b/src/ast-helpers.ts index bcf2e43..b6b88d7 100644 --- a/src/ast-helpers.ts +++ b/src/ast-helpers.ts @@ -8,178 +8,150 @@ import { } from "./parser-combinators"; import { - concatMap, escapeStringForRegex, - stringConcatMap, } from "./helpers"; -import { - defaultOptions, -} from "./options"; - /** - * converts an `astNode` within the AST of a parsed url-pattern into - * a string representing the regex that matches the url-pattern. + * converts an array of AST nodes `nodes` representing a parsed url-pattern into + * a string representing the regex which matches that url-pattern. */ -function astToRegexString(astNode: Ast, segmentValueCharset: string): string { - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, (node) => astToRegexString(node, segmentValueCharset)); +function astToRegexString(nodes: Array>, segmentValueCharset: string): string { + let result = ""; + + for (const node of nodes) { + switch (node.tag) { + case "wildcard": + // ? = lazy + result += ".*?"; + continue; + case "namedWildcard": + // ? = lazy + result += "(.*?)"; + continue; + case "namedSegment": + result += `([${ segmentValueCharset }]+)`; + continue; + case "staticContent": + result += escapeStringForRegex(node.value); + continue; + case "optionalSegment": + result += `(?:${ astToRegexString(node.value, segmentValueCharset) })?`; + continue; + default: + throw new Error(`unknown tag \`${ node.tag }\``); + } } - switch (astNode.tag) { - case "wildcard": - return "(.*?)"; - case "namedWildcard": - return "(.*?)"; - case "namedSegment": - return `([${ segmentValueCharset }]+)`; - case "staticContent": - return escapeStringForRegex(astNode.value); - case "optionalSegment": - return `(?:${ astToRegexString(astNode.value, segmentValueCharset) })?`; - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } + return result; } /** * converts the root `astNode` of a parsed url-pattern into * a string representing the regex that matches the url-pattern. */ -export function astRootToRegexString(astNode: Ast, segmentValueCharset?: string) { - if (segmentValueCharset == null) { - ({ segmentValueCharset } = defaultOptions); - } - return `^${ astToRegexString(astNode, segmentValueCharset) }$`; -} - -/** - * returns the names of any named segments and wildcards contained - * in the url-pattern represented by the given `astNode` in order. - */ -export function astToNames(astNode: Ast | Array>): string[] { - if (Array.isArray(astNode)) { - return concatMap(astNode, astToNames); - } - - switch (astNode.tag) { - case "wildcard": - return ["_"]; - case "namedWildcard": - return [astNode.value]; - case "namedSegment": - return [astNode.value]; - case "staticContent": - return []; - case "optionalSegment": - return astToNames(astNode.value); - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } +export function astRootToRegexString(nodes: Array>, segmentValueCharset: string) { + return `^${ astToRegexString(nodes, segmentValueCharset) }$`; } /** - * since a param - * nextIndexes contains a mapping from param key to the - * next index - * `hasSideEffects` is a boolean that controls whether + * returns the names of any named segments and named wildcards contained + * in the url-pattern represented by the given AST `nodes` in order. */ -export function getParam( - params: { [index: string]: any }, - key: string, - nextIndexes: { [index: string]: number }, - hasSideEffects: boolean = false, -) { - const value = params[key]; - if (value == null) { - if (hasSideEffects) { - throw new Error(`no values provided for key \`${ key }\``); - } else { - return; - } - } - const index = nextIndexes[key] || 0; - const maxIndex = Array.isArray(value) ? value.length - 1 : 0; - if (index > maxIndex) { - if (hasSideEffects) { - throw new Error(`too few values provided for key \`${ key }\``); - } else { - return; +export function astToNames(nodes: Array>): string[] { + const result: string[] = []; + + for (const node of nodes) { + switch (node.tag) { + case "wildcard": + case "staticContent": + continue; + case "namedWildcard": + case "namedSegment": + result.push(node.value); + continue; + case "optionalSegment": + // recurse into the optional segment + // optional segments values are always arrays + result.push(...astToNames(node.value)); + continue; + default: + throw new Error(`unknown tag \`${ node.tag }\``); } } - const result = Array.isArray(value) ? value[index] : value; - - if (hasSideEffects) { - nextIndexes[key] = index + 1; - } - return result; } /** * returns whether the given `astNode` contains + * any segments that + * based on this information optional segments are included or not. */ -function astNodeContainsAnySegmentsForParams( - astNode: Ast, +function astContainsAnySegmentsForParams( + nodes: Array>, params: { [index: string]: any }, - nextIndexes: { [index: string]: number }, ): boolean { - if (Array.isArray(astNode)) { - let i = -1; - const { length } = astNode; - while (++i < length) { - if (astNodeContainsAnySegmentsForParams(astNode[i], params, nextIndexes)) { - return true; - } + for (const node of nodes) { + switch (node.tag) { + case "staticContent": + case "wildcard": + continue; + case "namedWildcard": + case "namedSegment": + if (params[node.value] != null) { + return true; + } + continue; + case "optionalSegment": + if (astContainsAnySegmentsForParams(node.value, params)) { + return false; + } + continue; + default: + throw new Error(`unknown tag \`${ node.tag }\``); } - return false; - } - - // TODO namedWildcard - switch (astNode.tag) { - case "wildcard": - return getParam(params, "_", nextIndexes, false) != null; - case "namedSegment": - return getParam(params, astNode.value, nextIndexes, false) != null; - case "staticContent": - return false; - case "optionalSegment": - return astNodeContainsAnySegmentsForParams(astNode.value, params, nextIndexes); - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); } + return false; } /** - * stringify an url-pattern AST + * turn an url-pattern AST and a mapping of `namesToValues` */ export function stringify( - astNode: Ast | Array>, - params: { [index: string]: any }, - nextIndexes: { [index: string]: number } = {}, + nodes: Array>, + namesToValues: { [index: string]: any }, ): string { - // stringify an array by concatenating the result of stringifying its elements - if (Array.isArray(astNode)) { - return stringConcatMap(astNode, (node) => stringify(node, params, nextIndexes)); + let result = ""; + + for (const node of nodes) { + switch (node.tag) { + case "wildcard": + continue; + case "namedWildcard": + case "namedSegment": + const value = namesToValues[node.value]; + if (value == null) { + throw new Error(`no value provided for name \`${ node.value }\``); + } + result += value; + continue; + case "staticContent": + result += node.value; + continue; + case "optionalSegment": + // only add optional segments if values are present. + // optional segments are only included if values are provided + // for all names (of named segments) within the optional segment + if (astContainsAnySegmentsForParams(node.value, namesToValues)) { + // recurse into the optional segment + // optional segments values are always arrays + result += stringify(node.value, namesToValues); + } + continue; + default: + throw new Error(`unknown tag \`${ node.tag }\``); + } } - switch (astNode.tag) { - case "wildcard": - return getParam(params, "_", nextIndexes, true); - case "namedWildcard": - return getParam(params, astNode.value, nextIndexes, true); - case "namedSegment": - return getParam(params, astNode.value, nextIndexes, true); - case "staticContent": - return astNode.value; - case "optionalSegment": - if (astNodeContainsAnySegmentsForParams(astNode.value, params, nextIndexes)) { - return stringify(astNode.value, params, nextIndexes); - } else { - return ""; - } - default: - throw new Error(`unknown tag \`${ astNode.tag }\``); - } + return result; } diff --git a/src/helpers.ts b/src/helpers.ts index 9f404fc..59c7013 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -6,32 +6,6 @@ export function escapeStringForRegex(str: string): string { return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); } -/** - * like `Array.prototype.map` except that the function `f` - * returns an array and `concatMap` returns the concatenation - * of all arrays returned by `f` - */ -export function concatMap(array: Input[], f: (x: Input) => Output[]): Output[] { - let results: Output[] = []; - for (const value of array) { - results = results.concat(f(value)); - } - return results; -} - -/** - * like `Array.prototype.map` except that the function `f` - * returns a string and `stringConcatMap` returns the concatenation - * of all strings returned by `f` - */ -export function stringConcatMap(array: T[], f: (x: T) => string): string { - let result = ""; - for (const value of array) { - result += f(value); - } - return result; -} - /** * returns the number of groups in the `regex`. * source: http://stackoverflow.com/a/16047223 @@ -58,8 +32,7 @@ export function keysAndValuesToObject(keys: string[], values: any[]): object { throw Error("keys.length must equal values.length"); } - let i = -1; - while (++i < keys.length) { + for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = values[i]; diff --git a/src/parser.ts b/src/parser.ts index afab6db..335e212 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -84,7 +84,7 @@ export function newStaticContentParser(options: IOptions): Parser> { /* * */ -export function newUrlPatternParser(options: IOptions): Parser> { +export function newUrlPatternParser(options: IOptions): Parser>> { let parsePattern: Parser = (input: string) => { throw new Error(` this is just a temporary placeholder diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 53894de..dc34139 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -26,7 +26,7 @@ import { export default class UrlPattern { public readonly isRegex: boolean; public readonly regex: RegExp; - public readonly ast?: Ast; + public readonly ast?: Array>; public readonly names?: string[]; constructor(pattern: string, options?: IUserInputOptions); @@ -124,6 +124,7 @@ export default class UrlPattern { this.regex = new RegExp(astRootToRegexString(ast, options.segmentValueCharset)); this.names = astToNames(ast); + // TODO don't allow duplicate names } public match(url: string): object | undefined { diff --git a/test/ast.ts b/test/ast.ts index 43e781a..a6e39a4 100644 --- a/test/ast.ts +++ b/test/ast.ts @@ -8,7 +8,6 @@ import { import { astRootToRegexString, astToNames, - getParam, } from "../src/ast-helpers"; import { @@ -17,231 +16,77 @@ import { const parse: any = newUrlPatternParser(defaultOptions); +// test both functions in one go as they are related +// and extract data from the same input tape("astRootToRegexString and astToNames", (t: tape.Test) => { t.test("just static alphanumeric", (t: tape.Test) => { const parsed = parse("user42"); - t.equal(astRootToRegexString(parsed.value), "^user42$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), + "^user42$"); t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just static escaped", (t: tape.Test) => { const parsed = parse("/api/v1/users"); - t.equal(astRootToRegexString(parsed.value), "^\\/api\\/v1\\/users$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^\\/api\\/v1\\/users$"); t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just single char variable", (t: tape.Test) => { const parsed = parse(":a"); - t.equal(astRootToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^([a-zA-Z0-9-_~ %]+)$"); t.deepEqual(astToNames(parsed.value), ["a"]); t.end(); }); t.test("just variable", (t: tape.Test) => { const parsed = parse(":variable"); - t.equal(astRootToRegexString(parsed.value), "^([a-zA-Z0-9-_~ %]+)$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^([a-zA-Z0-9-_~ %]+)$"); t.deepEqual(astToNames(parsed.value), ["variable"]); t.end(); }); t.test("just wildcard", (t: tape.Test) => { const parsed = parse("*"); - t.equal(astRootToRegexString(parsed.value), "^(.*?)$"); - t.deepEqual(astToNames(parsed.value), ["_"]); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^.*?$"); + t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just named wildcard", (t: tape.Test) => { const parsed = parse("*:variable"); - t.equal(astRootToRegexString(parsed.value), "^(.*?)$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^(.*?)$"); t.deepEqual(astToNames(parsed.value), ["variable"]); t.end(); }); t.test("just optional static", (t: tape.Test) => { const parsed = parse("(foo)"); - t.equal(astRootToRegexString(parsed.value), "^(?:foo)?$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^(?:foo)?$"); t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just optional variable", (t: tape.Test) => { const parsed = parse("(:foo)"); - t.equal(astRootToRegexString(parsed.value), "^(?:([a-zA-Z0-9-_~ %]+))?$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^(?:([a-zA-Z0-9-_~ %]+))?$"); t.deepEqual(astToNames(parsed.value), ["foo"]); t.end(); }); t.test("just optional wildcard", (t: tape.Test) => { const parsed = parse("(*)"); - t.equal(astRootToRegexString(parsed.value), "^(?:(.*?))?$"); - t.deepEqual(astToNames(parsed.value), ["_"]); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^(?:.*?)?$"); + t.deepEqual(astToNames(parsed.value), []); t.end(); }); t.test("just optional named wildcard", (t: tape.Test) => { const parsed = parse("(*:variable)"); - t.equal(astRootToRegexString(parsed.value), "^(?:(.*?))?$"); + t.equal(astRootToRegexString(parsed.value, defaultOptions.segmentValueCharset), "^(?:(.*?))?$"); t.deepEqual(astToNames(parsed.value), ["variable"]); t.end(); }); }); - -tape("getParam", (t: tape.Test) => { - t.test("no side effects", (t: tape.Test) => { - let next = {}; - t.equal(undefined, getParam({}, "one", next)); - t.deepEqual(next, {}); - - // value - - next = {}; - t.equal(1, getParam({one: 1}, "one", next)); - t.deepEqual(next, {}); - - next = {one: 0}; - t.equal(1, getParam({one: 1}, "one", next)); - t.deepEqual(next, {one: 0}); - - next = {one: 1}; - t.equal(undefined, getParam({one: 1}, "one", next)); - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - t.equal(undefined, getParam({one: 1}, "one", next)); - t.deepEqual(next, {one: 2}); - - // array - - next = {}; - t.equal(1, getParam({one: [1]}, "one", next)); - t.deepEqual(next, {}); - - next = {one: 0}; - t.equal(1, getParam({one: [1]}, "one", next)); - t.deepEqual(next, {one: 0}); - - next = {one: 1}; - t.equal(undefined, getParam({one: [1]}, "one", next)); - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - t.equal(undefined, getParam({one: [1]}, "one", next)); - t.deepEqual(next, {one: 2}); - - next = {one: 0}; - t.equal(1, getParam({one: [1, 2, 3]}, "one", next)); - t.deepEqual(next, {one: 0}); - - next = {one: 1}; - t.equal(2, getParam({one: [1, 2, 3]}, "one", next)); - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - t.equal(3, getParam({one: [1, 2, 3]}, "one", next)); - t.deepEqual(next, {one: 2}); - - next = {one: 3}; - t.equal(undefined, getParam({one: [1, 2, 3]}, "one", next)); - t.deepEqual(next, {one: 3}); - - t.end(); - }); - - t.test("side effects", (t: tape.Test) => { - let next = {}; - t.equal(1, getParam({one: 1}, "one", next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 0}; - t.equal(1, getParam({one: 1}, "one", next, true)); - t.deepEqual(next, {one: 1}); - - // array - - next = {}; - t.equal(1, getParam({one: [1]}, "one", next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 0}; - t.equal(1, getParam({one: [1]}, "one", next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 0}; - t.equal(1, getParam({one: [1, 2, 3]}, "one", next, true)); - t.deepEqual(next, {one: 1}); - - next = {one: 1}; - t.equal(2, getParam({one: [1, 2, 3]}, "one", next, true)); - t.deepEqual(next, {one: 2}); - - next = {one: 2}; - t.equal(3, getParam({one: [1, 2, 3]}, "one", next, true)); - t.deepEqual(next, {one: 3}); - - t.end(); - }); - - t.test("side effects errors", (t: tape.Test) => { - let e; - t.plan(2 * 6); - - let next = {}; - try { - getParam({}, "one", next, true); - } catch (error) { - e = error; - t.equal(e.message, "no values provided for key `one`"); - } - t.deepEqual(next, {}); - - next = {one: 1}; - try { - getParam({one: 1}, "one", next, true); - } catch (error1) { - e = error1; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - try { - getParam({one: 2}, "one", next, true); - } catch (error2) { - e = error2; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 2}); - - next = {one: 1}; - try { - getParam({one: [1]}, "one", next, true); - } catch (error3) { - e = error3; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 1}); - - next = {one: 2}; - try { - getParam({one: [1]}, "one", next, true); - } catch (error4) { - e = error4; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 2}); - - next = {one: 3}; - try { - getParam({one: [1, 2, 3]}, "one", next, true); - } catch (error5) { - e = error5; - t.equal(e.message, "too few values provided for key `one`"); - } - t.deepEqual(next, {one: 3}); - - t.end(); - }); -}); diff --git a/test/helpers.ts b/test/helpers.ts index 8653dc9..9803f46 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,11 +1,9 @@ import * as tape from "tape"; import { - concatMap, escapeStringForRegex, keysAndValuesToObject, regexGroupCount, - stringConcatMap, } from "../src/helpers"; tape("escapeStringForRegex", (t: tape.Test) => { @@ -27,21 +25,6 @@ tape("escapeStringForRegex", (t: tape.Test) => { t.end(); }); -tape("concatMap", (t: tape.Test) => { - t.deepEqual([], concatMap([], () => [])); - t.deepEqual([1], concatMap([1], (x) => [x])); - t.deepEqual([1, 1, 1, 2, 2, 2, 3, 3, 3], concatMap([1, 2, 3], (x) => [x, x, x])); - t.end(); -}); - -tape("stringConcatMap", (t: tape.Test) => { - t.equal("", stringConcatMap([], () => "")); - t.equal("1", stringConcatMap([1], (x) => x.toString())); - t.equal("123", stringConcatMap([1, 2, 3], (x) => x.toString())); - t.equal("1a2a3a", stringConcatMap([1, 2, 3], (x) => x + "a")); - t.end(); -}); - tape("regexGroupCount", (t: tape.Test) => { t.equal(0, regexGroupCount(/foo/)); t.equal(1, regexGroupCount(/(foo)/)); diff --git a/test/match-fixtures.ts b/test/match-fixtures.ts index c93eb2b..c69ac38 100644 --- a/test/match-fixtures.ts +++ b/test/match-fixtures.ts @@ -37,80 +37,114 @@ tape("match", (t: tape.Test) => { t.deepEqual(pattern.match("/user/10/task/52"), { taskId: "52", userId: "10", - }, - ); + }); pattern = new UrlPattern(".user.:userId.task.:taskId"); t.deepEqual(pattern.match(".user.10.task.52"), { taskId: "52", userId: "10", - }, - ); + }); pattern = new UrlPattern("*/user/:userId"); t.deepEqual(pattern.match("/school/10/user/10"), { - _: "/school/10", userId: "10", - }, - ); + }); + + pattern = new UrlPattern("*:prefix/user/:userId"); + t.deepEqual(pattern.match("/school/10/user/10"), { + prefix: "/school/10", + userId: "10", + }); pattern = new UrlPattern("*-user-:userId"); t.deepEqual(pattern.match("-school-10-user-10"), { - _: "-school-10", userId: "10", - }, - ); + }); + + pattern = new UrlPattern("*:prefix-user-:userId"); + t.deepEqual(pattern.match("-school-10-user-10"), { + prefix: "-school-10", + userId: "10", + }); pattern = new UrlPattern("/admin*"); - t.deepEqual(pattern.match("/admin/school/10/user/10"), - {_: "/school/10/user/10"}); + t.deepEqual(pattern.match("/admin/school/10/user/10"), {}); + + pattern = new UrlPattern("/admin*:suffix"); + t.deepEqual(pattern.match("/admin/school/10/user/10"), { + suffix: "/school/10/user/10", + }); + t.deepEqual(pattern.match("/admin"), { + suffix: "", + }); + + pattern = new UrlPattern("/admin(*:suffix)"); + t.deepEqual(pattern.match("/admin/school/10/user/10"), { + suffix: "/school/10/user/10", + }); + t.deepEqual(pattern.match("/admin"), {}); pattern = new UrlPattern("#admin*"); - t.deepEqual(pattern.match("#admin#school#10#user#10"), - {_: "#school#10#user#10"}); + t.deepEqual(pattern.match("#admin#school#10#user#10"), {}); + + pattern = new UrlPattern("#admin*:suffix"); + t.deepEqual(pattern.match("#admin#school#10#user#10"), { + suffix: "#school#10#user#10", + }); pattern = new UrlPattern("/admin/*/user/:userId"); t.deepEqual(pattern.match("/admin/school/10/user/10"), { - _: "school/10", userId: "10", - }, - ); + }); + + pattern = new UrlPattern("/admin/*:infix/user/:userId"); + t.deepEqual(pattern.match("/admin/school/10/user/10"), { + infix: "school/10", + userId: "10", + }); pattern = new UrlPattern("$admin$*$user$:userId"); t.deepEqual(pattern.match("$admin$school$10$user$10"), { - _: "school$10", userId: "10", - }, - ); + }); + + pattern = new UrlPattern("$admin$*:infix$user$:userId"); + t.deepEqual(pattern.match("$admin$school$10$user$10"), { + infix: "school$10", + userId: "10", + }); pattern = new UrlPattern("/admin/*/user/*/tail"); - t.deepEqual(pattern.match("/admin/school/10/user/10/12/tail"), - {_: ["school/10", "10/12"]}); + t.deepEqual(pattern.match("/admin/school/10/user/10/12/tail"), {}); - pattern = new UrlPattern("$admin$*$user$*$tail"); - t.deepEqual(pattern.match("$admin$school$10$user$10$12$tail"), - {_: ["school$10", "10$12"]}); + pattern = new UrlPattern("/admin/*:infix1/user/*:infix2/tail"); + t.deepEqual(pattern.match("/admin/school/10/user/10/12/tail"), { + infix1: "school/10", + infix2: "10/12", + }); pattern = new UrlPattern("/admin/*/user/:id/*/tail"); t.deepEqual(pattern.match("/admin/school/10/user/10/12/13/tail"), { - _: ["school/10", "12/13"], id: "10", - }, - ); + }); - pattern = new UrlPattern("^admin^*^user^:id^*^tail"); - t.deepEqual(pattern.match("^admin^school^10^user^10^12^13^tail"), { - _: ["school^10", "12^13"], + pattern = new UrlPattern("/admin/*:infix1/user/:id/*:infix2/tail"); + t.deepEqual(pattern.match("/admin/school/10/user/10/12/13/tail"), { id: "10", - }, - ); + infix1: "school/10", + infix2: "12/13", + }); pattern = new UrlPattern("/*/admin(/:path)"); t.deepEqual(pattern.match("/admin/admin/admin"), { - _: "admin", path: "admin", - }, - ); + }); + + pattern = new UrlPattern("/*:infix/admin(/:path)"); + t.deepEqual(pattern.match("/admin/admin/admin"), { + infix: "admin", + path: "admin", + }); pattern = new UrlPattern("(/)"); t.deepEqual(pattern.match(""), {}); @@ -127,21 +161,38 @@ tape("match", (t: tape.Test) => { pattern = new UrlPattern("/admin/(*/)foo"); t.deepEqual(pattern.match("/admin/foo"), {}); - t.deepEqual(pattern.match("/admin/baz/bar/biff/foo"), - {_: "baz/bar/biff"}); + t.deepEqual(pattern.match("/admin/baz/bar/biff/foo"), {}); + + pattern = new UrlPattern("/admin/(*:infix/)foo"); + t.deepEqual(pattern.match("/admin/foo"), {}); + t.deepEqual(pattern.match("/admin/baz/bar/biff/foo"), { + infix: "baz/bar/biff", + }); pattern = new UrlPattern("/v:major.:minor/*"); t.deepEqual(pattern.match("/v1.2/resource/"), { - _: "resource/", major: "1", minor: "2", - }, - ); + }); + + pattern = new UrlPattern("/v:major.:minor/*:suffix"); + t.deepEqual(pattern.match("/v1.2/resource/"), { + major: "1", + minor: "2", + suffix: "resource/", + }); + + pattern = new UrlPattern("/v:minor.:major/*"); + t.deepEqual(pattern.match("/v1.2/resource/"), { + major: "2", + minor: "1", + }); - pattern = new UrlPattern("/v:v.:v/*"); + pattern = new UrlPattern("/v:minor.:major/*:suffix"); t.deepEqual(pattern.match("/v1.2/resource/"), { - _: "resource/", - v: ["1", "2"], + major: "2", + minor: "1", + suffix: "resource/", }); pattern = new UrlPattern("/:foo_bar"); @@ -185,10 +236,16 @@ tape("match", (t: tape.Test) => { pattern = new UrlPattern("/vvv:version/*"); t.equal(undefined, pattern.match("/vvv/resource")); t.deepEqual(pattern.match("/vvv1/resource"), { - _: "resource", version: "1", - }, - ); + }); + t.equal(undefined, pattern.match("/vvv1.1/resource")); + + pattern = new UrlPattern("/vvv:version/*:suffix"); + t.equal(undefined, pattern.match("/vvv/resource")); + t.deepEqual(pattern.match("/vvv1/resource"), { + suffix: "resource", + version: "1", + }); t.equal(undefined, pattern.match("/vvv1.1/resource")); pattern = new UrlPattern("/api/users/:id", @@ -205,60 +262,51 @@ tape("match", (t: tape.Test) => { t.deepEqual(pattern.match("/api/users?param1=foo¶m2=bar"), { param1: "foo", param2: "bar", - }, - ); + }); pattern = new UrlPattern(":scheme\\://:host(\\::port)", {segmentValueCharset: "a-zA-Z0-9-_~ %."}); t.deepEqual(pattern.match("ftp://ftp.example.com"), { host: "ftp.example.com", scheme: "ftp", - }, - ); + }); t.deepEqual(pattern.match("ftp://ftp.example.com:8080"), { host: "ftp.example.com", port: "8080", scheme: "ftp", - }, - ); + }); t.deepEqual(pattern.match("https://example.com:80"), { host: "example.com", port: "80", scheme: "https", - }, - ); + }); pattern = new UrlPattern(":scheme\\://:host(\\::port)(/api(/:resource(/:id)))", {segmentValueCharset: "a-zA-Z0-9-_~ %.@"}); t.deepEqual(pattern.match("https://sss.www.localhost.com"), { host: "sss.www.localhost.com", scheme: "https", - }, - ); + }); t.deepEqual(pattern.match("https://sss.www.localhost.com:8080"), { host: "sss.www.localhost.com", port: "8080", scheme: "https", - }, - ); + }); t.deepEqual(pattern.match("https://sss.www.localhost.com/api"), { host: "sss.www.localhost.com", scheme: "https", - }, - ); + }); t.deepEqual(pattern.match("https://sss.www.localhost.com/api/security"), { host: "sss.www.localhost.com", resource: "security", scheme: "https", - }, - ); + }); t.deepEqual(pattern.match("https://sss.www.localhost.com/api/security/bob@example.com"), { host: "sss.www.localhost.com", id: "bob@example.com", resource: "security", scheme: "https", - }, - ); + }); let regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; pattern = new UrlPattern(regex); diff --git a/test/parser.ts b/test/parser.ts index b954efd..5dc7f15 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -2,6 +2,7 @@ import * as tape from "tape"; import { newNamedSegmentParser, + newNamedWildcardParser, newStaticContentParser, newUrlPatternParser, } from "../src/parser"; @@ -13,6 +14,7 @@ import { const parse = newUrlPatternParser(defaultOptions); const parseNamedSegment = newNamedSegmentParser(defaultOptions); const parseStaticContent = newStaticContentParser(defaultOptions); +const parseNamedWildcard = newNamedWildcardParser(defaultOptions); tape("namedSegment", (t: tape.Test) => { t.deepEqual(parseNamedSegment(":a"), { @@ -55,29 +57,40 @@ tape("namedSegment", (t: tape.Test) => { }); tape("static", (t: tape.Test) => { - t.deepEqual(parseStaticContent("a"), { - rest: "", - value: { - tag: "staticContent", - value: "a", - }, + t.deepEqual(parseStaticContent("a"), { + rest: "", + value: { + tag: "staticContent", + value: "a", }, - ); - t.deepEqual(parseStaticContent("abc:d"), { - rest: ":d", - value: { - tag: "staticContent", - value: "abc", - }, + }, + ); + t.deepEqual(parseStaticContent("abc:d"), { + rest: ":d", + value: { + tag: "staticContent", + value: "abc", }, - ); - t.equal(parseStaticContent(":ab96c"), undefined); - t.equal(parseStaticContent(":"), undefined); - t.equal(parseStaticContent("("), undefined); - t.equal(parseStaticContent(")"), undefined); - t.equal(parseStaticContent("*"), undefined); - t.equal(parseStaticContent(""), undefined); - t.end(); + }, + ); + t.equal(parseStaticContent(":ab96c"), undefined); + t.equal(parseStaticContent(":"), undefined); + t.equal(parseStaticContent("("), undefined); + t.equal(parseStaticContent(")"), undefined); + t.equal(parseStaticContent("*"), undefined); + t.equal(parseStaticContent(""), undefined); + t.end(); +}); + +tape("namedWildcard", (t: tape.Test) => { + t.deepEqual(parseNamedWildcard("*:a"), { + rest: "", + value: { + tag: "namedWildcard", + value: "a", + }, + }); + t.end(); }); tape("fixtures", (t: tape.Test) => { diff --git a/test/readme.ts b/test/readme.ts index 97a8666..59d022d 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -36,68 +36,44 @@ tape("prefer a different syntax. customize it", (t: tape.Test) => { t.end(); }); -tape("api versioning", (t: tape.Test) => { +tape("api versioning example", (t: tape.Test) => { const pattern = new UrlPattern("/v:major(.:minor)/*"); - t.deepEqual(pattern.match("/v1.2/"), {major: "1", minor: "2", _: ""}); - t.deepEqual(pattern.match("/v2/users"), {major: "2", _: "users"}); + t.deepEqual(pattern.match("/v1.2/"), {major: "1", minor: "2"}); + t.deepEqual(pattern.match("/v2/users"), {major: "2"}); t.equal(pattern.match("/v/"), undefined); t.end(); }); -tape("domain", (t: tape.Test) => { - const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*)"); +tape("domain example", (t: tape.Test) => { + const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*:path)"); t.deepEqual(pattern.match("google.de"), { domain: "google", tld: "de", - }, - ); + }); t.deepEqual(pattern.match("https://www.google.com"), { domain: "google", subdomain: "www", tld: "com", - }, - ); + }); t.deepEqual(pattern.match("http://mail.google.com/mail"), { - _: "mail", domain: "google", + path: "mail", subdomain: "mail", tld: "com", - }, - ); - t.deepEqual(pattern.match("http://mail.google.com:80/mail"), { - _: "mail", + }); + t.deepEqual(pattern.match("http://mail.google.com:80/mail/inbox"), { domain: "google", + path: "mail/inbox", port: "80", subdomain: "mail", tld: "com", - }, - ); + }); t.equal(pattern.match("google"), undefined); - t.deepEqual(pattern.match("www.google.com"), { - domain: "google", - subdomain: "www", - tld: "com", - }, - ); - t.equal(pattern.match("httpp://mail.google.com/mail"), undefined); - t.deepEqual(pattern.match("google.de/search"), { - _: "search", - domain: "google", - tld: "de", - }, - ); - - t.end(); -}); - -tape("named segment occurs more than once", (t: tape.Test) => { - const pattern = new UrlPattern("/api/users/:ids/posts/:ids"); - t.deepEqual(pattern.match("/api/users/10/posts/5"), {ids: ["10", "5"]}); t.end(); }); -tape("regex", (t: tape.Test) => { +tape("regex example", (t: tape.Test) => { const pattern = new UrlPattern(/^\/api\/(.*)$/); t.deepEqual(pattern.match("/api/users"), ["users"]); t.equal(pattern.match("/apiii/users"), undefined); @@ -141,7 +117,7 @@ tape("customization", (t: tape.Test) => { }; const pattern = new UrlPattern( - "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]", + "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?$path]", options, ); @@ -151,8 +127,8 @@ tape("customization", (t: tape.Test) => { }, ); t.deepEqual(pattern.match("http://mail.google.com/mail"), { - "_": "mail", "domain": "google", + "path": "mail", "sub_domain": "mail", "toplevel-domain": "com", }, @@ -173,8 +149,8 @@ tape("customization", (t: tape.Test) => { ); t.equal(pattern.match("httpp://mail.google.com/mail"), undefined); t.deepEqual(pattern.match("google.de/search"), { - "_": "search", "domain": "google", + "path": "search", "toplevel-domain": "de", }, ); diff --git a/test/stringify-fixtures.ts b/test/stringify-fixtures.ts index c745d0c..013390b 100644 --- a/test/stringify-fixtures.ts +++ b/test/stringify-fixtures.ts @@ -28,37 +28,68 @@ tape("stringify", (t: tape.Test) => { })); pattern = new UrlPattern("*/user/:userId"); + t.equal("/user/10", pattern.stringify({ + userId: "10", + })); + + pattern = new UrlPattern("*:prefix/user/:userId"); t.equal("/school/10/user/10", pattern.stringify({ - _: "/school/10", + prefix: "/school/10", userId: "10", })); pattern = new UrlPattern("*-user-:userId"); + t.equal("-user-10", pattern.stringify({ + userId: "10", + })); + + pattern = new UrlPattern("*:prefix-user-:userId"); t.equal("-school-10-user-10", pattern.stringify({ - _: "-school-10", + prefix: "-school-10", userId: "10", })); pattern = new UrlPattern("/admin*"); + t.equal("/admin", pattern.stringify({})); + + pattern = new UrlPattern("/admin*:suffix"); t.equal("/admin/school/10/user/10", pattern.stringify({ - _: "/school/10/user/10"})); + suffix: "/school/10/user/10", + })); pattern = new UrlPattern("/admin/*/user/*/tail"); + t.equal("/admin//user//tail", pattern.stringify({})); + + pattern = new UrlPattern("/admin/*:infix1/user/*:infix2/tail"); t.equal("/admin/school/10/user/10/12/tail", pattern.stringify({ - _: ["school/10", "10/12"]})); + infix1: "school/10", + infix2: "10/12", + })); pattern = new UrlPattern("/admin/*/user/:id/*/tail"); + t.equal("/admin//user/10//tail", pattern.stringify({ + id: "10", + })); + + pattern = new UrlPattern("/admin/*:infix1/user/:id/*:infix2/tail"); t.equal("/admin/school/10/user/10/12/13/tail", pattern.stringify({ - _: ["school/10", "12/13"], id: "10", + infix1: "school/10", + infix2: "12/13", })); pattern = new UrlPattern("/*/admin(/:path)"); + t.equal("//admin/baz", pattern.stringify({ + path: "baz", + })); + t.equal("//admin", pattern.stringify({})); + + pattern = new UrlPattern("/*:infix/admin(/:path)"); t.equal("/foo/admin/baz", pattern.stringify({ - _: "foo", + infix: "foo", path: "baz", })); - t.equal("/foo/admin", pattern.stringify({ _: "foo" })); + t.equal("/foo/admin", pattern.stringify({ infix: "foo" })); pattern = new UrlPattern("(/)"); t.equal("", pattern.stringify()); @@ -70,21 +101,36 @@ tape("stringify", (t: tape.Test) => { t.equal("/admin/bar", pattern.stringify()); t.equal("/admin/baz/bar", pattern.stringify({ foo: "baz" })); - pattern = new UrlPattern("/admin/(*/)foo"); - t.equal("/admin/foo", pattern.stringify()); - t.equal("/admin/baz/bar/biff/foo", pattern.stringify({ _: "baz/bar/biff" })); +// pattern = new UrlPattern("/admin/(*/)foo"); +// t.equal("/admin/foo", pattern.stringify()); +// t.equal("/admin/baz/bar/biff/foo", pattern.stringify({ _: "baz/bar/biff" })); +// +// pattern = new UrlPattern("/v:major.:minor/*"); +// t.equal("/v1.2/resource/", pattern.stringify({ +// _: "resource/", +// major: "1", +// minor: "2", +// })); pattern = new UrlPattern("/v:major.:minor/*"); - t.equal("/v1.2/resource/", pattern.stringify({ - _: "resource/", + t.equal("/v1.2/", pattern.stringify({ major: "1", minor: "2", })); - pattern = new UrlPattern("/v:v.:v/*"); + pattern = new UrlPattern("/v:major.:minor/*:suffix"); + t.equal("/v1.2/", pattern.stringify({ + major: "1", + minor: "2", + suffix: "", + })); + + pattern = new UrlPattern("/v:major.:minor/*:suffix"); t.equal("/v1.2/resource/", pattern.stringify({ - _: "resource/", - v: ["1", "2"]})); + major: "1", + minor: "2", + suffix: "resource/", + })); pattern = new UrlPattern("/:foo_bar"); t.equal("/a_bar", pattern.stringify({ foo_bar: "a_bar" })); @@ -95,29 +141,29 @@ tape("stringify", (t: tape.Test) => { pattern = new UrlPattern("((((a)b)c)d)"); t.equal("", pattern.stringify()); - pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:a))"); - t.equal("1-B", pattern.stringify({ b: "B" })); - t.equal("A-1-B", pattern.stringify({ - a: "A", - b: "B", - })); - t.equal("A-1-B", pattern.stringify({ - a: "A", - b: "B", - })); - t.equal("A-1-B-2-C-3-D", pattern.stringify({ - a: "A", - b: "B", - c: "C", - d: "D", - })); - t.equal("A-1-B-2-C-3-D-4-E-F", pattern.stringify({ - _: "E", - a: ["A", "F"], - b: "B", - c: "C", - d: "D", - })); +// pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:a))"); +// t.equal("1-B", pattern.stringify({ b: "B" })); +// t.equal("A-1-B", pattern.stringify({ +// a: "A", +// b: "B", +// })); +// t.equal("A-1-B", pattern.stringify({ +// a: "A", +// b: "B", +// })); +// t.equal("A-1-B-2-C-3-D", pattern.stringify({ +// a: "A", +// b: "B", +// c: "C", +// d: "D", +// })); +// t.equal("A-1-B-2-C-3-D-4-E-F", pattern.stringify({ +// _: "E", +// a: ["A", "F"], +// b: "B", +// c: "C", +// d: "D", +// })); pattern = new UrlPattern("/user/:range"); t.equal("/user/10-20", pattern.stringify({ range: "10-20" })); @@ -127,7 +173,7 @@ tape("stringify", (t: tape.Test) => { tape("stringify errors", (t: tape.Test) => { let e; - t.plan(5); + t.plan(3); const pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:a))"); @@ -135,7 +181,7 @@ tape("stringify errors", (t: tape.Test) => { pattern.stringify(); } catch (error) { e = error; - t.equal(e.message, "no values provided for key `b`"); + t.equal(e.message, "no value provided for name `b`"); } try { pattern.stringify({ @@ -145,7 +191,7 @@ tape("stringify errors", (t: tape.Test) => { }); } catch (error1) { e = error1; - t.equal(e.message, "no values provided for key `d`"); + t.equal(e.message, "no value provided for name `d`"); } try { pattern.stringify({ @@ -155,30 +201,7 @@ tape("stringify errors", (t: tape.Test) => { }); } catch (error2) { e = error2; - t.equal(e.message, "no values provided for key `c`"); - } - try { - pattern.stringify({ - _: "E", - a: "A", - b: "B", - c: "C", - d: "D", - }); - } catch (error3) { - e = error3; - t.equal(e.message, "too few values provided for key `a`"); - } - try { - pattern.stringify({ - a: ["A", "F"], - b: "B", - c: "C", - d: "D", - }); - } catch (error4) { - e = error4; - t.equal(e.message, "no values provided for key `_`"); + t.equal(e.message, "no value provided for name `c`"); } t.end(); From 819870d4b4ca53c47a1a0416dffe59d8fdf92fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 20:03:15 -0500 Subject: [PATCH 085/117] test/ast.ts -> test/ast-helpers.ts --- test/{ast.ts => ast-helpers.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{ast.ts => ast-helpers.ts} (100%) diff --git a/test/ast.ts b/test/ast-helpers.ts similarity index 100% rename from test/ast.ts rename to test/ast-helpers.ts From f01e6fdeddcd1990b565e37505c090c56b52d944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 20:03:28 -0500 Subject: [PATCH 086/117] package.json: rename command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 649ef30..d2b5bf3 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "prepublish": "npm run compile", "lint": "tslint --project .", "test": "tape -r ts-node/register test/*.ts", - "ts-node": "ts-node", + "node": "ts-node", "coverage": "nyc npm test", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" From 9e9990e57554de8c0ea90c91bae6ddbe6264765e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 20:20:28 -0500 Subject: [PATCH 087/117] code formatting --- test/readme.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/test/readme.ts b/test/readme.ts index 59d022d..39b9655 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -124,35 +124,30 @@ tape("customization", (t: tape.Test) => { t.deepEqual(pattern.match("google.de"), { "domain": "google", "toplevel-domain": "de", - }, - ); + }); t.deepEqual(pattern.match("http://mail.google.com/mail"), { "domain": "google", "path": "mail", "sub_domain": "mail", "toplevel-domain": "com", - }, - ); + }); t.equal(pattern.match("http://mail.this-should-not-match.com/mail"), undefined); t.equal(pattern.match("google"), undefined); t.deepEqual(pattern.match("www.google.com"), { "domain": "google", "sub_domain": "www", "toplevel-domain": "com", - }, - ); + }); t.deepEqual(pattern.match("https://www.google.com"), { "domain": "google", "sub_domain": "www", "toplevel-domain": "com", - }, - ); + }); t.equal(pattern.match("httpp://mail.google.com/mail"), undefined); t.deepEqual(pattern.match("google.de/search"), { "domain": "google", "path": "search", "toplevel-domain": "de", - }, - ); + }); t.end(); }); From ae8001c3d889aad1e24fe253240572244bf29217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 21:20:34 -0500 Subject: [PATCH 088/117] error on duplicate segment names. simplify --- src/helpers.ts | 39 +++++------------ src/url-pattern.ts | 19 ++++++-- test/helpers.ts | 88 ++++---------------------------------- test/stringify-fixtures.ts | 2 +- 4 files changed, 36 insertions(+), 112 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 59c7013..6f66a46 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -20,36 +20,19 @@ export function regexGroupCount(regex: RegExp): number { } /** - * zips an array of `keys` and an array of `values` into an object - * so `keys[i]` is associated with `values[i]` for every i. - * `keys` and `values` must have the same length. - * if the same key appears multiple times the associated values are collected in an array. + * returns the index of the first duplicate element in `elements` + * or -1 if there are no duplicates. */ -export function keysAndValuesToObject(keys: string[], values: any[]): object { - const result: { [index: string]: any } = {}; +export function indexOfDuplicateElement(elements: T[]): number { + const knownElements: Set = new Set(); - if (keys.length !== values.length) { - throw Error("keys.length must equal values.length"); - } - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - - if (value == null) { - continue; - } - - // key already encountered - if (result[key] != null) { - // capture multiple values for same key in an array - if (!Array.isArray(result[key])) { - result[key] = [result[key]]; - } - result[key].push(value); - } else { - result[key] = value; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + if (knownElements.has(element)) { + return i; } + knownElements.add(element); } - return result; + + return -1; } diff --git a/src/url-pattern.ts b/src/url-pattern.ts index dc34139..d0f65f9 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -1,5 +1,5 @@ import { - keysAndValuesToObject, + indexOfDuplicateElement, regexGroupCount, } from "./helpers"; @@ -124,10 +124,15 @@ export default class UrlPattern { this.regex = new RegExp(astRootToRegexString(ast, options.segmentValueCharset)); this.names = astToNames(ast); - // TODO don't allow duplicate names + const index = indexOfDuplicateElement(this.names); + if (index !== -1) { + throw new Error( + `duplicate name "${ this.names[index] } in pattern. names must be unique`, + ); + } } - public match(url: string): object | undefined { + public match(url: string): { [index: string]: string } | string[] | undefined { const match = this.regex.exec(url); if (match == null) { return; @@ -135,7 +140,13 @@ export default class UrlPattern { const groups = match.slice(1); if (this.names != null) { - return keysAndValuesToObject(this.names, groups); + const result: { [index: string]: string } = {}; + for (let i = 0; i < this.names.length; i++) { + if (groups[i] != null) { + result[this.names[i]] = groups[i]; + } + } + return result; } else { return groups; } diff --git a/test/helpers.ts b/test/helpers.ts index 9803f46..678e914 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -2,7 +2,7 @@ import * as tape from "tape"; import { escapeStringForRegex, - keysAndValuesToObject, + indexOfDuplicateElement, regexGroupCount, } from "../src/helpers"; @@ -36,83 +36,13 @@ tape("regexGroupCount", (t: tape.Test) => { t.end(); }); -tape("keysAndValuesToObject", (t: tape.Test) => { - t.deepEqual( - keysAndValuesToObject( - [], - [], - ), - {}, - ); - t.deepEqual( - keysAndValuesToObject( - ["one"], - [1], - ), - { - one: 1, - }, - ); - t.deepEqual( - keysAndValuesToObject( - ["one", "two", "two"], - [1, 2, 3], - ), - { - one: 1, - two: [2, 3], - }, - ); - t.deepEqual( - keysAndValuesToObject( - ["one", "two", "two", "two"], - [1, 2, 3, null], - ), - { - one: 1, - two: [2, 3], - }, - ); - t.deepEqual( - keysAndValuesToObject( - ["one", "two", "two", "two"], - [1, 2, 3, 4], - ), - { - one: 1, - two: [2, 3, 4], - }, - ); - t.deepEqual( - keysAndValuesToObject( - ["one", "two", "two", "two", "three"], - [1, 2, 3, 4, undefined], - ), - { - one: 1, - two: [2, 3, 4], - }, - ); - t.deepEqual( - keysAndValuesToObject( - ["one", "two", "two", "two", "three"], - [1, 2, 3, 4, 5], - ), - { - one: 1, - three: 5, - two: [2, 3, 4], - }, - ); - t.deepEqual( - keysAndValuesToObject( - ["one", "two", "two", "two", "three"], - [null, 2, 3, 4, 5], - ), - { - three: 5, - two: [2, 3, 4], - }, - ); +tape("indexOfDuplicateElement", (t: tape.Test) => { + t.equal(-1, indexOfDuplicateElement([])); + t.equal(-1, indexOfDuplicateElement([1, 2, 3, 4, 5])); + t.equal(1, indexOfDuplicateElement([1, 1, 3, 4, 5])); + t.equal(2, indexOfDuplicateElement([1, 2, 1, 4, 5])); + t.equal(3, indexOfDuplicateElement([1, 2, 3, 2, 5])); + t.equal(-1, indexOfDuplicateElement(["a", "b", "c"])); + t.equal(2, indexOfDuplicateElement(["a", "b", "a"])); t.end(); }); diff --git a/test/stringify-fixtures.ts b/test/stringify-fixtures.ts index 013390b..d01252a 100644 --- a/test/stringify-fixtures.ts +++ b/test/stringify-fixtures.ts @@ -175,7 +175,7 @@ tape("stringify errors", (t: tape.Test) => { let e; t.plan(3); - const pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:a))"); + const pattern = new UrlPattern("(:a-)1-:b(-2-:c-3-:d(-4-*-:e))"); try { pattern.stringify(); From f258c817805ac9a840d8fb3c0f0d0b8a95f3f42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 21:45:00 -0500 Subject: [PATCH 089/117] document regexGroupCount and increase code coverage --- src/helpers.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 6f66a46..903ef46 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -11,11 +11,13 @@ export function escapeStringForRegex(str: string): string { * source: http://stackoverflow.com/a/16047223 */ export function regexGroupCount(regex: RegExp): number { + // add a "|" to the end of the regex meaning logical OR. const testingRegex = new RegExp(regex.toString() + "|"); - const matches = testingRegex.exec(""); - if (matches == null) { - throw new Error("no matches"); - } + // executing the regex on an empty string matches the empty right side of the "|" (OR). + const matches: any = testingRegex.exec(""); + // `matches` is never null here as the regex always matches. + // the matches array contains an element for every group in the `regex`. + // thus we detect the number of groups in the regex. return matches.length - 1; } From fe9375ea5d0d55aa2c4cc88cf7418ef94da18ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 21:45:29 -0500 Subject: [PATCH 090/117] make npm run coverage output reproducable --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d2b5bf3..71479a2 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "lint": "tslint --project .", "test": "tape -r ts-node/register test/*.ts", "node": "ts-node", - "coverage": "nyc npm test", + "coverage": "rm -r .nyc_output && rm -r coverage && nyc npm test", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" }, From ede87eb435c8bf5a8d3b4aff78e6b29c0bd8219e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 21:46:01 -0500 Subject: [PATCH 091/117] ignore coverage artifacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 94725da..5def1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ test/url-pattern.js npm-debug.log dist package-lock.json +.nyc_output +coverage From ac175dd57bbc743705a933de353e7b684742f756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 21:49:24 -0500 Subject: [PATCH 092/117] test for another error --- src/url-pattern.ts | 2 +- test/errors.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/url-pattern.ts b/src/url-pattern.ts index d0f65f9..5703b84 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -127,7 +127,7 @@ export default class UrlPattern { const index = indexOfDuplicateElement(this.names); if (index !== -1) { throw new Error( - `duplicate name "${ this.names[index] } in pattern. names must be unique`, + `duplicate name "${ this.names[index] }" in pattern. names must be unique`, ); } } diff --git a/test/errors.ts b/test/errors.ts index c37bfdb..eb1b496 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -67,6 +67,16 @@ tape("invalid variable name in pattern", (t: tape.Test) => { t.end(); }); +tape("duplicate variable name in pattern", (t: tape.Test) => { + t.plan(1); + try { + new UrlPattern(":a/:a"); + } catch (error) { + t.equal(error.message, "duplicate name \"a\" in pattern. names must be unique"); + } + t.end(); +}); + tape("too many closing parentheses", (t: tape.Test) => { t.plan(2); try { From b3f2b616b12ab0feee1f7463620b4913d5004502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 22:06:28 -0500 Subject: [PATCH 093/117] better variable name --- src/url-pattern.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 5703b84..3c237fe 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -140,13 +140,13 @@ export default class UrlPattern { const groups = match.slice(1); if (this.names != null) { - const result: { [index: string]: string } = {}; + const mergedNamesAndGroups: { [index: string]: string } = {}; for (let i = 0; i < this.names.length; i++) { if (groups[i] != null) { - result[this.names[i]] = groups[i]; + mergedNamesAndGroups[this.names[i]] = groups[i]; } } - return result; + return mergedNamesAndGroups; } else { return groups; } From 34961a094189d6c2632eb22f0947c10e83e816d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 22:20:12 -0500 Subject: [PATCH 094/117] fix commands in package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 71479a2..12a6e0f 100644 --- a/package.json +++ b/package.json @@ -79,12 +79,12 @@ "main": "lib/url-pattern", "scripts": { "compile": "tsc", - "doc": "typedoc --out doc .", + "doc": "typedoc --out doc", "prepublish": "npm run compile", "lint": "tslint --project .", "test": "tape -r ts-node/register test/*.ts", "node": "ts-node", - "coverage": "rm -r .nyc_output && rm -r coverage && nyc npm test", + "coverage": "rm -r .nyc_output || true && rm -r coverage || true && nyc npm test", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" }, From dc53d4537ee69febcf368f8268119d67849f7226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 22:21:07 -0500 Subject: [PATCH 095/117] improve docstring --- src/ast-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast-helpers.ts b/src/ast-helpers.ts index b6b88d7..96314aa 100644 --- a/src/ast-helpers.ts +++ b/src/ast-helpers.ts @@ -115,7 +115,7 @@ function astContainsAnySegmentsForParams( } /** - * turn an url-pattern AST and a mapping of `namesToValues` + * turn an url-pattern AST and a mapping of `namesToValues` into a string */ export function stringify( nodes: Array>, From 253d3dd75f0df454efa59050cf8e962661b14e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 22:23:06 -0500 Subject: [PATCH 096/117] improve docstrings --- src/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 903ef46..85854e0 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,5 @@ /** - * escapes a string for insertion into a regular expression + * escapes a string for insertion into a regular expression. * source: http://stackoverflow.com/a/3561711 */ export function escapeStringForRegex(str: string): string { @@ -23,7 +23,7 @@ export function regexGroupCount(regex: RegExp): number { /** * returns the index of the first duplicate element in `elements` - * or -1 if there are no duplicates. + * or `-1` if there are no duplicates. */ export function indexOfDuplicateElement(elements: T[]): number { const knownElements: Set = new Set(); From 6b495323baf23bd4d386dc330c84ca5011db0e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Sun, 12 May 2019 22:36:17 -0500 Subject: [PATCH 097/117] simplify code by using Object.assign --- src/url-pattern.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 3c237fe..04dca2b 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -9,7 +9,6 @@ import { import { defaultOptions, - IOptions, IUserInputOptions, } from "./options"; @@ -86,24 +85,7 @@ export default class UrlPattern { throw new Error("if first argument is a string second argument must be an options object or undefined"); } - const options: IOptions = { - escapeChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.escapeChar : undefined) || defaultOptions.escapeChar, - optionalSegmentEndChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.optionalSegmentEndChar : undefined) || defaultOptions.optionalSegmentEndChar, - optionalSegmentStartChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.optionalSegmentStartChar : undefined) || defaultOptions.optionalSegmentStartChar, - segmentNameCharset: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentNameCharset : undefined) || defaultOptions.segmentNameCharset, - segmentNameEndChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentNameEndChar : undefined), - segmentNameStartChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentNameStartChar : undefined) || defaultOptions.segmentNameStartChar, - segmentValueCharset: (optionsOrGroupNames != null ? - optionsOrGroupNames.segmentValueCharset : undefined) || defaultOptions.segmentValueCharset, - wildcardChar: (optionsOrGroupNames != null ? - optionsOrGroupNames.wildcardChar : undefined) || defaultOptions.wildcardChar, - }; + const options = Object.assign({}, defaultOptions, optionsOrGroupNames); const parser = newUrlPatternParser(options); const parsed = parser(pattern); From 4408b70ab5a867498cdde9f9933ffe96087150e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 00:03:27 -0500 Subject: [PATCH 098/117] make second names argument mandatory for regexes --- README.md | 10 +++----- src/url-pattern.ts | 53 +++++++++++++++++++++--------------------- test/errors.ts | 9 ++++--- test/match-fixtures.ts | 32 ++++--------------------- test/readme.ts | 4 ++-- 5 files changed, 40 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index a6a9d86..1b71569 100644 --- a/README.md +++ b/README.md @@ -204,21 +204,17 @@ unnamed wildcards are not collected. ## make pattern from regex ```javascript -> const pattern = new UrlPattern(/^\/api\/(.*)$/); -``` - -if the pattern was created from a regex an array of the captured groups is returned on a match: +> const pattern = new UrlPattern(/^\/api\/(.*)$/, ["path"]); -```javascript > pattern.match("/api/users"); -["users"] +{path: "users"} > pattern.match("/apiii/test"); undefined ``` when making a pattern from a regex -you can pass an array of keys as the second argument. +you have to pass an array of keys as the second argument. returns objects on match with each key mapped to a captured value: ```javascript diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 04dca2b..a457b31 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -26,7 +26,7 @@ export default class UrlPattern { public readonly isRegex: boolean; public readonly regex: RegExp; public readonly ast?: Array>; - public readonly names?: string[]; + public readonly names: string[]; constructor(pattern: string, options?: IUserInputOptions); constructor(pattern: RegExp, groupNames?: string[]); @@ -51,22 +51,25 @@ export default class UrlPattern { // handle regex pattern and return early if (pattern instanceof RegExp) { this.regex = pattern; - if (optionsOrGroupNames != null) { - if (!Array.isArray(optionsOrGroupNames)) { - throw new TypeError([ - "if first argument is a RegExp the second argument", - "may be an Array of group names", - "but you provided something else", - ].join(" ")); - } - const groupCount = regexGroupCount(this.regex); - if (optionsOrGroupNames.length !== groupCount) { - throw new Error([ - `regex contains ${ groupCount } groups`, - `but array of group names contains ${ optionsOrGroupNames.length }`, - ].join(" ")); - } - this.names = optionsOrGroupNames; + if (optionsOrGroupNames == null || !Array.isArray(optionsOrGroupNames)) { + throw new TypeError([ + "if first argument is a RegExp the second argument", + "must be an Array of group names", + ].join(" ")); + } + const groupCount = regexGroupCount(this.regex); + if (optionsOrGroupNames.length !== groupCount) { + throw new Error([ + `regex contains ${ groupCount } groups`, + `but array of group names contains ${ optionsOrGroupNames.length }`, + ].join(" ")); + } + this.names = optionsOrGroupNames; + const regexNameIndex = indexOfDuplicateElement(this.names); + if (regexNameIndex !== -1) { + throw new Error( + `duplicate name "${ this.names[regexNameIndex] }" in pattern. names must be unique`, + ); } return; } @@ -114,24 +117,20 @@ export default class UrlPattern { } } - public match(url: string): { [index: string]: string } | string[] | undefined { + public match(url: string): { [index: string]: string } | undefined { const match = this.regex.exec(url); if (match == null) { return; } const groups = match.slice(1); - if (this.names != null) { - const mergedNamesAndGroups: { [index: string]: string } = {}; - for (let i = 0; i < this.names.length; i++) { - if (groups[i] != null) { - mergedNamesAndGroups[this.names[i]] = groups[i]; - } + const mergedNamesAndGroups: { [index: string]: string } = {}; + for (let i = 0; i < this.names.length; i++) { + if (groups[i] != null) { + mergedNamesAndGroups[this.names[i]] = groups[i]; } - return mergedNamesAndGroups; - } else { - return groups; } + return mergedNamesAndGroups; } public stringify(params?: object): string { diff --git a/test/errors.ts b/test/errors.ts index eb1b496..c28e990 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -117,10 +117,9 @@ tape("regex names", (t: tape.Test) => { try { new UntypedUrlPattern(/x/, 5); } catch (error) { - t.equal(error.message, [ - "if first argument is a RegExp the second argument may be an Array", - "of group names but you provided something else", - ].join(" ")); + t.equal(error.message, + "if first argument is a RegExp the second argument must be an Array of group names", + ); } try { new UrlPattern(/(((foo)bar(boo))far)/, []); @@ -137,7 +136,7 @@ tape("regex names", (t: tape.Test) => { tape("stringify regex", (t: tape.Test) => { t.plan(1); - const pattern = new UrlPattern(/x/); + const pattern = new UrlPattern(/x/, []); try { pattern.stringify(); } catch (error) { diff --git a/test/match-fixtures.ts b/test/match-fixtures.ts index c69ac38..58f6ad8 100644 --- a/test/match-fixtures.ts +++ b/test/match-fixtures.ts @@ -24,14 +24,12 @@ tape("match", (t: tape.Test) => { pattern = new UrlPattern(".foo"); t.equals(pattern.match(".bar.foo"), undefined); - pattern = new UrlPattern(/foo/); + pattern = new UrlPattern(/foo/, []); t.deepEqual(pattern.match("foo"), []); - pattern = new UrlPattern(/\/foo\/(.*)/); - t.deepEqual(pattern.match("/foo/bar"), ["bar"]); - - pattern = new UrlPattern(/\/foo\/(.*)/); - t.deepEqual(pattern.match("/foo/"), [""]); + pattern = new UrlPattern(/\/foo\/(.*)/, ["path"]); + t.deepEqual(pattern.match("/foo/bar"), {path: "bar"}); + t.deepEqual(pattern.match("/foo/"), {path: ""}); pattern = new UrlPattern("/user/:userId/task/:taskId"); t.deepEqual(pattern.match("/user/10/task/52"), { @@ -308,27 +306,7 @@ tape("match", (t: tape.Test) => { scheme: "https", }); - let regex = /\/ip\/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - pattern = new UrlPattern(regex); - t.equal(undefined, pattern.match("10.10.10.10")); - t.equal(undefined, pattern.match("ip/10.10.10.10")); - t.equal(undefined, pattern.match("/ip/10.10.10.")); - t.equal(undefined, pattern.match("/ip/10.")); - t.equal(undefined, pattern.match("/ip/")); - t.deepEqual(pattern.match("/ip/10.10.10.10"), ["10", "10", "10", "10"]); - t.deepEqual(pattern.match("/ip/127.0.0.1"), ["127", "0", "0", "1"]); - - regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; - pattern = new UrlPattern(regex); - t.equal(undefined, pattern.match("10.10.10.10")); - t.equal(undefined, pattern.match("ip/10.10.10.10")); - t.equal(undefined, pattern.match("/ip/10.10.10.")); - t.equal(undefined, pattern.match("/ip/10.")); - t.equal(undefined, pattern.match("/ip/")); - t.deepEqual(pattern.match("/ip/10.10.10.10"), ["10.10.10.10"]); - t.deepEqual(pattern.match("/ip/127.0.0.1"), ["127.0.0.1"]); - - regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; + const regex = /\/ip\/((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/; pattern = new UrlPattern(regex, ["ip"]); t.equal(undefined, pattern.match("10.10.10.10")); t.equal(undefined, pattern.match("ip/10.10.10.10")); diff --git a/test/readme.ts b/test/readme.ts index 39b9655..f28891e 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -74,8 +74,8 @@ tape("domain example", (t: tape.Test) => { }); tape("regex example", (t: tape.Test) => { - const pattern = new UrlPattern(/^\/api\/(.*)$/); - t.deepEqual(pattern.match("/api/users"), ["users"]); + const pattern = new UrlPattern(/^\/api\/(.*)$/, ["path"]); + t.deepEqual(pattern.match("/api/users"), {path: "users"}); t.equal(pattern.match("/apiii/users"), undefined); t.end(); }); From 1fcfb049222f7b91e2a8e30956a880a95076a96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 12:53:04 -0500 Subject: [PATCH 099/117] add explicit .ts extensions to imports typescript should not enforce this hopefully https://github.com/Microsoft/TypeScript/issues/27481 will get addressed --- src/ast-helpers.ts | 6 ++++-- src/parser.ts | 6 ++++-- src/url-pattern.ts | 15 ++++++++++----- test/ast-helpers.ts | 9 ++++++--- test/errors.ts | 3 ++- test/helpers.ts | 3 ++- test/match-fixtures.ts | 3 ++- test/misc.ts | 3 ++- test/parser-combinators.ts | 3 ++- test/parser.ts | 6 ++++-- test/readme.ts | 3 ++- test/stringify-fixtures.ts | 3 ++- 12 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/ast-helpers.ts b/src/ast-helpers.ts index 96314aa..f98d6b2 100644 --- a/src/ast-helpers.ts +++ b/src/ast-helpers.ts @@ -5,11 +5,13 @@ import { Ast, -} from "./parser-combinators"; +// @ts-ignore +} from "./parser-combinators.ts"; import { escapeStringForRegex, -} from "./helpers"; +// @ts-ignore +} from "./helpers.ts"; /** * converts an array of AST nodes `nodes` representing a parsed url-pattern into diff --git a/src/parser.ts b/src/parser.ts index 335e212..91cdfa5 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -13,11 +13,13 @@ import { newRegexParser, newStringParser, Parser, -} from "./parser-combinators"; +// @ts-ignore +} from "./parser-combinators.ts"; import { IOptions, -} from "./options"; +// @ts-ignore +} from "./options.ts"; export function newEscapedCharParser(options: IOptions): Parser> { return newPickNthParser(1, newStringParser(options.escapeChar), newRegexParser(/^./)); diff --git a/src/url-pattern.ts b/src/url-pattern.ts index a457b31..2a1512a 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -1,26 +1,31 @@ import { indexOfDuplicateElement, regexGroupCount, -} from "./helpers"; +// @ts-ignore +} from "./helpers.ts"; import { Ast, -} from "./parser-combinators"; +// @ts-ignore +} from "./parser-combinators.ts"; import { defaultOptions, IUserInputOptions, -} from "./options"; +// @ts-ignore +} from "./options.ts"; import { newUrlPatternParser, -} from "./parser"; +// @ts-ignore +} from "./parser.ts"; import { astRootToRegexString, astToNames, stringify, -} from "./ast-helpers"; +// @ts-ignore +} from "./ast-helpers.ts"; export default class UrlPattern { public readonly isRegex: boolean; diff --git a/test/ast-helpers.ts b/test/ast-helpers.ts index a6e39a4..d510f8e 100644 --- a/test/ast-helpers.ts +++ b/test/ast-helpers.ts @@ -3,16 +3,19 @@ import * as tape from "tape"; import { newUrlPatternParser, -} from "../src/parser"; +// @ts-ignore +} from "../src/parser.ts"; import { astRootToRegexString, astToNames, -} from "../src/ast-helpers"; +// @ts-ignore +} from "../src/ast-helpers.ts"; import { defaultOptions, -} from "../src/options"; +// @ts-ignore +} from "../src/options.ts"; const parse: any = newUrlPatternParser(defaultOptions); diff --git a/test/errors.ts b/test/errors.ts index c28e990..e7f198d 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -1,7 +1,8 @@ /* tslint:disable:no-unused-expression */ import * as tape from "tape"; -import UrlPattern from "../src/url-pattern"; +// @ts-ignore +import UrlPattern from "../src/url-pattern.ts"; const UntypedUrlPattern: any = UrlPattern; diff --git a/test/helpers.ts b/test/helpers.ts index 678e914..74609c2 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -4,7 +4,8 @@ import { escapeStringForRegex, indexOfDuplicateElement, regexGroupCount, -} from "../src/helpers"; +// @ts-ignore +} from "../src/helpers.ts"; tape("escapeStringForRegex", (t: tape.Test) => { const expected = "\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}\\]"; diff --git a/test/match-fixtures.ts b/test/match-fixtures.ts index 58f6ad8..8478810 100644 --- a/test/match-fixtures.ts +++ b/test/match-fixtures.ts @@ -3,7 +3,8 @@ // tslint:disable:max-line-length import * as tape from "tape"; -import UrlPattern from "../src/url-pattern"; +// @ts-ignore +import UrlPattern from "../src/url-pattern.ts"; tape("match", (t: tape.Test) => { let pattern = new UrlPattern("/foo"); diff --git a/test/misc.ts b/test/misc.ts index 9c2a8aa..54b4105 100644 --- a/test/misc.ts +++ b/test/misc.ts @@ -1,6 +1,7 @@ import * as tape from "tape"; -import UrlPattern from "../src/url-pattern"; +// @ts-ignore +import UrlPattern from "../src/url-pattern.ts"; tape("instance of UrlPattern is handled correctly as constructor argument", (t: tape.Test) => { const pattern = new UrlPattern("/user/:userId/task/:taskId"); diff --git a/test/parser-combinators.ts b/test/parser-combinators.ts index 03e32b5..ad7082d 100644 --- a/test/parser-combinators.ts +++ b/test/parser-combinators.ts @@ -3,7 +3,8 @@ import * as tape from "tape"; import { newRegexParser, newStringParser, -} from "../src/parser-combinators"; +// @ts-ignore +} from "../src/parser-combinators.ts"; tape("newStringParser", (t: tape.Test) => { const parse = newStringParser("foo"); diff --git a/test/parser.ts b/test/parser.ts index 5dc7f15..23359ab 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -5,11 +5,13 @@ import { newNamedWildcardParser, newStaticContentParser, newUrlPatternParser, -} from "../src/parser"; +// @ts-ignore +} from "../src/parser.ts"; import { defaultOptions, -} from "../src/options"; +// @ts-ignore +} from "../src/options.ts"; const parse = newUrlPatternParser(defaultOptions); const parseNamedSegment = newNamedSegmentParser(defaultOptions); diff --git a/test/readme.ts b/test/readme.ts index f28891e..654bde2 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -2,7 +2,8 @@ import * as tape from "tape"; -import UrlPattern from "../src/url-pattern"; +// @ts-ignore +import UrlPattern from "../src/url-pattern.ts"; tape("match a pattern against a string and extract values", (t: tape.Test) => { const pattern = new UrlPattern("/api/users(/:id)"); diff --git a/test/stringify-fixtures.ts b/test/stringify-fixtures.ts index d01252a..e794a73 100644 --- a/test/stringify-fixtures.ts +++ b/test/stringify-fixtures.ts @@ -2,7 +2,8 @@ import * as tape from "tape"; -import UrlPattern from "../src/url-pattern"; +// @ts-ignore +import UrlPattern from "../src/url-pattern.ts"; tape("stringify", (t: tape.Test) => { let pattern = new UrlPattern("/foo"); From 5ceb48260768a49199870d8541d30327a0bae82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 14:22:22 -0500 Subject: [PATCH 100/117] .travis.yml: update node version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c07cc0c..03e1bb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "12.1" + - "12" - "10.15" script: - npm audit From 84a26e75bc006d89a2b41e2a751f464b75beb435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 14:22:49 -0500 Subject: [PATCH 101/117] plenty of work on readme --- README.md | 162 +++++++++++++++++++++++++++++------------------------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 1b71569..c852452 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ turn strings into data or data into strings.** > [michael](https://github.com/snd/url-pattern/pull/7) [make a pattern:](#make-pattern-from-string) -``` javascript +```typescript > const pattern = new UrlPattern("/api/users(/:id)"); ``` [match a pattern against a string and extract values:](#match-pattern-against-string) -``` javascript +```typescript > pattern.match("/api/users/10"); {id: "10"} @@ -30,7 +30,7 @@ undefined ``` [generate a string from a pattern and values:](#stringify-patterns) -``` javascript +```typescript > pattern.stringify() "/api/users" @@ -39,7 +39,7 @@ undefined ``` prefer a different syntax? [customize it:](#customize-the-pattern-syntax) -```javascript +```typescript > const pattern = new UrlPattern("/api/users/{id}", { segmentNameEndChar: "}", segmentNameStartChar: "{", @@ -49,53 +49,22 @@ prefer a different syntax? [customize it:](#customize-the-pattern-syntax) {id: "5"} ``` -- continuously tested in Node.js (0.12, 4.2.3 and 5.3) and all relevant browsers: - [![Sauce Test Status](https://saucelabs.com/browser-matrix/urlpattern.svg)](https://saucelabs.com/u/urlpattern) -- [tiny single file with just under 500 lines of simple, readable, maintainable code](src/url-pattern.coffee) +- continuously tested in Node.js (10.15 (LTS), 12) and all relevant browsers: +- [tiny source of around 500 lines of simple, readable, maintainable typescript](src/) - [huge test suite](test) passing [![Build Status](https://travis-ci.org/snd/url-pattern.svg?branch=master)](https://travis-ci.org/snd/url-pattern/branches) with [![codecov.io](http://codecov.io/github/snd/url-pattern/coverage.svg?branch=master)](http://codecov.io/github/snd/url-pattern?branch=master) code coverage - widely used [![Downloads per Month](https://img.shields.io/npm/dm/url-pattern.svg?style=flat)](https://www.npmjs.org/package/url-pattern) -- supports CommonJS, [AMD](http://requirejs.org/docs/whyamd.html) and browser globals - - `require("url-pattern")` - - use [lib/url-pattern.js](lib/url-pattern.js) in the browser - - sets the global variable `UrlPattern` when neither CommonJS nor [AMD](http://requirejs.org/docs/whyamd.html) are available. -- very fast matching as each pattern is compiled into a regex exactly once +- very fast matching as each pattern is compiled into a regex - zero dependencies - [customizable](#customize-the-pattern-syntax) - [frequently asked questions](#frequently-asked-questions) -- npm package: `npm install url-pattern` -- bower package: `bower install url-pattern` -- pattern parser implemented using simple, combosable, testable [parser combinators](https://en.wikipedia.org/wiki/Parser_combinator) -- [typescript typings](index.d.ts) +- pattern parser implemented using simple, composable, testable [parser combinators](https://en.wikipedia.org/wiki/Parser_combinator) -``` -npm install url-pattern -``` - -``` -bower install url-pattern -``` - -```javascript -> const UrlPattern = require("url-pattern"); -``` +## a more complex example showing the power of url-pattern -``` javascript -> const pattern = new UrlPattern("/v:major(.:minor)/*"); - -> pattern.match("/v1.2/"); -{major: "1", minor: "2"} - -> pattern.match("/v2/users"); -{major: "2"} - -> pattern.match("/v/"); -undefined -``` - -``` javascript +``` typescript > const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*:path)") > pattern.match("google.de"); @@ -114,9 +83,44 @@ undefined undefined ``` -## make pattern from string +## install -```javascript +``` +npm install url-pattern +``` +and +```typescript +> import UrlPattern from "url-pattern"; +``` +or +```typescript +> const UrlPattern = require("url-pattern"); +``` + +## url-pattern works with [deno](https://deno.land/): + +**bleeding edge** master: +```typescript +import UrlPattern from "https://raw.githubusercontent.com/snd/url-pattern/2.0.0/src/url-pattern.ts"; +``` + +**stable** latest release: +```typescript +import UrlPattern from "https://raw.githubusercontent.com/snd/url-pattern/master/src/url-pattern.ts"; +``` + +you can also use the parser combinators url-pattern is build on: +```typescript +import UrlPattern from "https://raw.githubusercontent.com/snd/url-pattern/master/src/parser-combinators.ts"; +``` + +TODO see the documentation here + +## reference + +### make pattern from string + +```typescript > const pattern = new UrlPattern("/api/users/:id"); ``` @@ -124,25 +128,25 @@ a `pattern` is immutable after construction. none of its methods changes its state. that makes it easier to reason about. -## match pattern against string +### match pattern against string match returns the extracted segments: -```javascript +```typescript > pattern.match("/api/users/10"); {id: "10"} ``` or `undefined` if there was no match: -``` javascript +```typescript > pattern.match("/api/products/5"); undefined ``` patterns are compiled into regexes which makes `.match()` superfast. -## named segments +### named segments `:id` (in the example above) is a named segment: @@ -157,11 +161,11 @@ a named segment match stops at `/`, `.`, ... but not at `_`, `-`, ` `, `%`... a named segment name can only occur once in the pattern string. -## optional segments, wildcards and escaping +### optional segments, wildcards and escaping to make part of a pattern optional just wrap it in `(` and `)`: -```javascript +```typescript > const pattern = new UrlPattern( "(http(s)\\://)(:subdomain.):domain.:tld(/*:path)" ); @@ -173,19 +177,19 @@ url-pattern. optional named segments are stored in the corresponding property only if they are present in the source string: -```javascript +```typescript > pattern.match("google.de"); {domain: "google", tld: "de"} ``` -```javascript +```typescript > pattern.match("https://www.google.com"); {subdomain: "www", domain: "google", tld: "com"} ``` `:*{name}` in patterns are named wildcards and match anything. -```javascript +```typescript > pattern.match("http://mail.google.com/mail"); {subdomain: "mail", domain: "google", tld: "com", path: "mail"} ``` @@ -193,7 +197,7 @@ optional named segments are stored in the corresponding property only if they ar wildcards can be named like this: unnamed wildcards are not collected. -```javascript +```typescript > const pattern = new UrlPattern('/search/*:term'); > pattern.match('/search/fruit'); {term: 'fruit'} @@ -201,9 +205,9 @@ unnamed wildcards are not collected. [look at the tests for additional examples of `.match`](test/match-fixtures.ts) -## make pattern from regex +### make pattern from regex -```javascript +```typescript > const pattern = new UrlPattern(/^\/api\/(.*)$/, ["path"]); > pattern.match("/api/users"); @@ -217,7 +221,7 @@ when making a pattern from a regex you have to pass an array of keys as the second argument. returns objects on match with each key mapped to a captured value: -```javascript +```typescript > const pattern = new UrlPattern( /^\/api\/([^\/]+)(?:\/(\d+))?$/, ["resource", "id"] @@ -233,9 +237,9 @@ returns objects on match with each key mapped to a captured value: undefined ``` -## stringify patterns +### stringify patterns -```javascript +```typescript > const pattern = new UrlPattern("/api/users/:id"); > pattern.stringify({id: 10}) @@ -245,7 +249,7 @@ undefined optional segments are only included in the output if they contain named segments and/or wildcards and values for those are provided: -```javascript +```typescript > const pattern = new UrlPattern("/api/users(/:id)"); > pattern.stringify() @@ -270,65 +274,71 @@ anonymous wildcards are ignored. [look at the tests for additional examples of `.stringify`](test/stringify-fixtures.ts) -## customize the pattern syntax +### customize the pattern syntax finally we can completely change pattern-parsing and regex-compilation to suit our needs: -```javascript +```typescript > let options = {}; ``` let's change the char used for escaping (default `\\`): -```javascript +```typescript > options.escapeChar = "!"; ``` let's change the char used to start a named segment (default `:`): -```javascript -> options.segmentNameStartChar = "$"; +```typescript +> options.segmentNameStartChar = "{"; +``` + +let's add a char required at the end of a named segment (default nothing): + +```typescript +> options.segmentNameEndChar = "}"; ``` -let's change the set of chars allowed in named segment names (default `a-zA-Z0-9`) -to also include `_` and `-`: +let's change the set of chars allowed in named segment names (default `a-zA-Z0-9_`) +to also include `-`: -```javascript +```typescript > options.segmentNameCharset = "a-zA-Z0-9_-"; ``` let's change the set of chars allowed in named segment values (default `a-zA-Z0-9-_~ %`) to not allow non-alphanumeric chars: -```javascript +```typescript > options.segmentValueCharset = "a-zA-Z0-9"; ``` let's change the chars used to surround an optional segment (default `(` and `)`): -```javascript -> options.optionalSegmentStartChar = "["; -> options.optionalSegmentEndChar = "]"; +```typescript +> options.optionalSegmentStartChar = "<"; +> options.optionalSegmentEndChar = ">"; ``` let's change the char used to denote a wildcard (default `*`): -```javascript -> options.wildcardChar = "?"; +```typescript +> options.wildcardChar = "#"; ``` pass options as the second argument to the constructor: -```javascript +```typescript > const pattern = new UrlPattern( - "[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?$path]", + "!://><{sub_domain}.>{domain}.{toplevel-domain}", options ); ``` then match: -```javascript +```typescript > pattern.match("http://mail.google.com/mail"); { sub_domain: "mail", From b854cb1fd47f76b9a6709852d9dfbef027c7bd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 14:53:03 -0500 Subject: [PATCH 102/117] more work on readme --- README.md | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index c852452..a1d2cdd 100644 --- a/README.md +++ b/README.md @@ -49,18 +49,18 @@ prefer a different syntax? [customize it:](#customize-the-pattern-syntax) {id: "5"} ``` +- very fast matching as each pattern is compiled into a regex +- [tiny source of around 500 lines of simple, readable typescript](src/) +- widely used [![Downloads per Month](https://img.shields.io/npm/dm/url-pattern.svg?style=flat)](https://www.npmjs.org/package/url-pattern) +- zero dependencies +- [parser](src/parser.ts) implemented using simple, precise, reusable [parser combinators](src/parsercombinators.ts) - continuously tested in Node.js (10.15 (LTS), 12) and all relevant browsers: -- [tiny source of around 500 lines of simple, readable, maintainable typescript](src/) - [huge test suite](test) passing [![Build Status](https://travis-ci.org/snd/url-pattern.svg?branch=master)](https://travis-ci.org/snd/url-pattern/branches) with [![codecov.io](http://codecov.io/github/snd/url-pattern/coverage.svg?branch=master)](http://codecov.io/github/snd/url-pattern?branch=master) code coverage -- widely used [![Downloads per Month](https://img.shields.io/npm/dm/url-pattern.svg?style=flat)](https://www.npmjs.org/package/url-pattern) -- very fast matching as each pattern is compiled into a regex -- zero dependencies - [customizable](#customize-the-pattern-syntax) - [frequently asked questions](#frequently-asked-questions) -- pattern parser implemented using simple, composable, testable [parser combinators](https://en.wikipedia.org/wiki/Parser_combinator) ## a more complex example showing the power of url-pattern @@ -97,25 +97,18 @@ or > const UrlPattern = require("url-pattern"); ``` -## url-pattern works with [deno](https://deno.land/): +## works with [deno](https://deno.land/): -**bleeding edge** master: +**stable** latest release: ```typescript import UrlPattern from "https://raw.githubusercontent.com/snd/url-pattern/2.0.0/src/url-pattern.ts"; ``` -**stable** latest release: +**bleeding edge** master: ```typescript import UrlPattern from "https://raw.githubusercontent.com/snd/url-pattern/master/src/url-pattern.ts"; ``` -you can also use the parser combinators url-pattern is build on: -```typescript -import UrlPattern from "https://raw.githubusercontent.com/snd/url-pattern/master/src/parser-combinators.ts"; -``` - -TODO see the documentation here - ## reference ### make pattern from string @@ -124,7 +117,7 @@ TODO see the documentation here > const pattern = new UrlPattern("/api/users/:id"); ``` -a `pattern` is immutable after construction. +a `UrlPattern` is immutable after construction. none of its methods changes its state. that makes it easier to reason about. @@ -151,15 +144,15 @@ patterns are compiled into regexes which makes `.match()` superfast. `:id` (in the example above) is a named segment: a named segment starts with `:` followed by the **name**. -the **name** must be at least one character in the regex character set `a-zA-Z0-9`. +the **name** must be at least one character in the regex character set `a-zA-Z0-9_`. when matching, a named segment consumes all characters in the regex character set -`a-zA-Z0-9-_~ %`. +`a-zA-Z0-9-_~ %`. a named segment match stops at `/`, `.`, ... but not at `_`, `-`, ` `, `%`... [you can change these character sets. click here to see how.](#customize-the-pattern-syntax) -a named segment name can only occur once in the pattern string. +names must be unique. a **name** may not appear twice in a pattern. ### optional segments, wildcards and escaping @@ -187,14 +180,17 @@ optional named segments are stored in the corresponding property only if they ar {subdomain: "www", domain: "google", tld: "com"} ``` -`:*{name}` in patterns are named wildcards and match anything. +`:*path` in the pattern above is a named wildcard with the name `path`. +named wildcards match anything. that makes them different from named segments which +only match characters inside the `options.segmentNameCharset` (default: `a-zA-Z0-9_-`). ```typescript -> pattern.match("http://mail.google.com/mail"); -{subdomain: "mail", domain: "google", tld: "com", path: "mail"} +> pattern.match("http://mail.google.com/mail/inbox"); +{subdomain: "mail", domain: "google", tld: "com", path: "mail/inbox"} ``` -wildcards can be named like this: +there are also + unnamed wildcards are not collected. ```typescript From 12e20f638e9a259647430712f6b8ac6305c69349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:34:00 -0500 Subject: [PATCH 103/117] add initial version of deno test --- .travis.yml | 11 +++++++++-- deno-test.ts | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 deno-test.ts diff --git a/.travis.yml b/.travis.yml index 03e1bb6..a463f55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: node_js node_js: - "12" - "10.15" +before_install: + - curl -fsSL https://deno.land/x/install/install.sh | sh script: - npm audit - npm run compile @@ -16,8 +18,13 @@ env: - secure: "Js6Pr7dJfvAKY5JuuuEJSrDvoBCrnjTjISMCPBmH0CkGwtGR7J2mCvaLcXQ9RO3zrSasdj8Rb6gBmrIgk1fQbp/NpwQVwMUPH4J+dhbwTHIrrIHVtxt6q8cPx43RJqjE6qN+G1MA/Y4IVbgAzjJPnzu6A6v7E/FzSFbpNilv2i4=" # SAUCE_ACCESS_KEY - secure: "idJFmSy6EyMNO9UoxUx0wG83G/w8H1Sh1fG5lWodAdV01/Ft0j3KQo/zelENBx7zMWf+iqdWOhL4rBLIIkaajHbmvkMYDzhFXK4GIZmd1HnV4MZCunipscMsEbtQU+uTY/I3fersnIz74aTuj3SKlFW4jVNgvc8fawijBtTbuhU=" - matrix: - - NPM_COMMAND=test +matrix: + include: + name: "deno" + script: + - deno run deno-test.ts + # matrix: + # - NPM_COMMAND=test # - NPM_COMMAND=test-with-coverage # - NPM_COMMAND=test-in-browsers # matrix: diff --git a/deno-test.ts b/deno-test.ts new file mode 100644 index 0000000..d6f2e3e --- /dev/null +++ b/deno-test.ts @@ -0,0 +1,7 @@ +import UrlPattern from "./src/url-pattern.ts"; +import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + +const pattern = new UrlPattern("/api/users/:id"); + +assertEquals(pattern.match("/api/users/5"), {id: "5"}); +assertEquals(pattern.stringify({id: 10}), "/api/users/10"); From 9907600b908636985ef062f4f0f5928ab7096688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:34:13 -0500 Subject: [PATCH 104/117] minor changes to readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1d2cdd..0797106 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,10 @@ prefer a different syntax? [customize it:](#customize-the-pattern-syntax) - [parser](src/parser.ts) implemented using simple, precise, reusable [parser combinators](src/parsercombinators.ts) - continuously tested in Node.js (10.15 (LTS), 12) and all relevant browsers: - [huge test suite](test) - passing [![Build Status](https://travis-ci.org/snd/url-pattern.svg?branch=master)](https://travis-ci.org/snd/url-pattern/branches) + [![Build Status](https://travis-ci.org/snd/url-pattern.svg?branch=master)](https://travis-ci.org/snd/url-pattern/branches) with [![codecov.io](http://codecov.io/github/snd/url-pattern/coverage.svg?branch=master)](http://codecov.io/github/snd/url-pattern?branch=master) code coverage -- [customizable](#customize-the-pattern-syntax) +- [customizable pattern syntax](#customize-the-pattern-syntax) - [frequently asked questions](#frequently-asked-questions) ## a more complex example showing the power of url-pattern From af755b9654c72b95052985a8829d734c3c5c9fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:34:26 -0500 Subject: [PATCH 105/117] sync up readme test with readme --- test/readme.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/readme.ts b/test/readme.ts index 654bde2..649346c 100644 --- a/test/readme.ts +++ b/test/readme.ts @@ -37,14 +37,6 @@ tape("prefer a different syntax. customize it", (t: tape.Test) => { t.end(); }); -tape("api versioning example", (t: tape.Test) => { - const pattern = new UrlPattern("/v:major(.:minor)/*"); - t.deepEqual(pattern.match("/v1.2/"), {major: "1", minor: "2"}); - t.deepEqual(pattern.match("/v2/users"), {major: "2"}); - t.equal(pattern.match("/v/"), undefined); - t.end(); -}); - tape("domain example", (t: tape.Test) => { const pattern = new UrlPattern("(http(s)\\://)(:subdomain.):domain.:tld(\\::port)(/*:path)"); t.deepEqual(pattern.match("google.de"), { @@ -56,12 +48,6 @@ tape("domain example", (t: tape.Test) => { subdomain: "www", tld: "com", }); - t.deepEqual(pattern.match("http://mail.google.com/mail"), { - domain: "google", - path: "mail", - subdomain: "mail", - tld: "com", - }); t.deepEqual(pattern.match("http://mail.google.com:80/mail/inbox"), { domain: "google", path: "mail/inbox", From 5dbd17cef762eeb9517a173126839bacb783744a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:39:01 -0500 Subject: [PATCH 106/117] try to fix deno test on travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a463f55..4f6c17e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: node_js node_js: - "12" - "10.15" -before_install: - - curl -fsSL https://deno.land/x/install/install.sh | sh script: - npm audit - npm run compile @@ -22,6 +20,8 @@ matrix: include: name: "deno" script: + - curl -fsSL https://deno.land/x/install/install.sh | sh + - export PATH="$HOME/.deno/bin:$PATH" - deno run deno-test.ts # matrix: # - NPM_COMMAND=test From 2294e59832f37472b16c9a4adebe1bd5d06f0f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:48:20 -0500 Subject: [PATCH 107/117] simplify .travis.yml --- .travis.yml | 46 ++++++++++------------------------------------ 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f6c17e..513987c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,3 @@ -language: node_js -node_js: - - "12" - - "10.15" -script: - - npm audit - - npm run compile - - npm run lint - - npm test - - npm run coverage - # - npm run $NPM_COMMAND -sudo: false env: global: # SAUCE_USERNAME @@ -18,31 +6,17 @@ env: - secure: "idJFmSy6EyMNO9UoxUx0wG83G/w8H1Sh1fG5lWodAdV01/Ft0j3KQo/zelENBx7zMWf+iqdWOhL4rBLIIkaajHbmvkMYDzhFXK4GIZmd1HnV4MZCunipscMsEbtQU+uTY/I3fersnIz74aTuj3SKlFW4jVNgvc8fawijBtTbuhU=" matrix: include: - name: "deno" + - node_js: "12" + script: + - npm audit + - npm run compile + - npm run lint + - npm run coverage + - node_js: "10.15" + script: + - npm test + - name: "deno" script: - curl -fsSL https://deno.land/x/install/install.sh | sh - export PATH="$HOME/.deno/bin:$PATH" - deno run deno-test.ts - # matrix: - # - NPM_COMMAND=test - # - NPM_COMMAND=test-with-coverage - # - NPM_COMMAND=test-in-browsers -# matrix: -# exclude: - # don't test in browsers more than once (already done with node 5) - # - node_js: "0.12" - # env: NPM_COMMAND=test-in-browsers - # - node_js: "iojs-3" - # env: NPM_COMMAND=test-in-browsers - # - node_js: "4" - # env: NPM_COMMAND=test-in-browsers - # don't collect code coverage more than once (already done with node 5) - # - node_js: "0.12" - # env: NPM_COMMAND=test-with-coverage - # - node_js: "iojs-3" - # env: NPM_COMMAND=test-with-coverage - # - node_js: "4" - # env: NPM_COMMAND=test-with-coverage - # already tested with coverage (with node 5). no need to test again without - # - node_js: "5" - # env: NPM_COMMAND=test From 18587826dcf94df14e6cf7538d1e6c7f1b59a3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:51:09 -0500 Subject: [PATCH 108/117] fix travis build --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 513987c..a05a820 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,15 @@ env: - secure: "idJFmSy6EyMNO9UoxUx0wG83G/w8H1Sh1fG5lWodAdV01/Ft0j3KQo/zelENBx7zMWf+iqdWOhL4rBLIIkaajHbmvkMYDzhFXK4GIZmd1HnV4MZCunipscMsEbtQU+uTY/I3fersnIz74aTuj3SKlFW4jVNgvc8fawijBtTbuhU=" matrix: include: - - node_js: "12" + - language: node_js + node_js: "12" script: - npm audit - npm run compile - npm run lint - npm run coverage - - node_js: "10.15" + - language: node_js + node_js: "10.15" script: - npm test - name: "deno" From 406e441e676544c357bf51d8795198ea6ba75f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:54:02 -0500 Subject: [PATCH 109/117] improve travis.yml --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a05a820..b770919 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,8 @@ matrix: node_js: "10.15" script: - npm test - - name: "deno" + - language: "generic" + name: "deno" script: - curl -fsSL https://deno.land/x/install/install.sh | sh - export PATH="$HOME/.deno/bin:$PATH" From d1b2f46ef2d23e332dea243f327bf816066757d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Mon, 13 May 2019 18:56:20 -0500 Subject: [PATCH 110/117] travis: language generic -> minimal --- .travis.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index b770919..333c944 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,3 @@ -env: - global: - # SAUCE_USERNAME - - secure: "Js6Pr7dJfvAKY5JuuuEJSrDvoBCrnjTjISMCPBmH0CkGwtGR7J2mCvaLcXQ9RO3zrSasdj8Rb6gBmrIgk1fQbp/NpwQVwMUPH4J+dhbwTHIrrIHVtxt6q8cPx43RJqjE6qN+G1MA/Y4IVbgAzjJPnzu6A6v7E/FzSFbpNilv2i4=" - # SAUCE_ACCESS_KEY - - secure: "idJFmSy6EyMNO9UoxUx0wG83G/w8H1Sh1fG5lWodAdV01/Ft0j3KQo/zelENBx7zMWf+iqdWOhL4rBLIIkaajHbmvkMYDzhFXK4GIZmd1HnV4MZCunipscMsEbtQU+uTY/I3fersnIz74aTuj3SKlFW4jVNgvc8fawijBtTbuhU=" matrix: include: - language: node_js @@ -17,9 +11,15 @@ matrix: node_js: "10.15" script: - npm test - - language: "generic" + - language: minimal name: "deno" script: - curl -fsSL https://deno.land/x/install/install.sh | sh - export PATH="$HOME/.deno/bin:$PATH" - deno run deno-test.ts +env: + global: + # SAUCE_USERNAME + - secure: "Js6Pr7dJfvAKY5JuuuEJSrDvoBCrnjTjISMCPBmH0CkGwtGR7J2mCvaLcXQ9RO3zrSasdj8Rb6gBmrIgk1fQbp/NpwQVwMUPH4J+dhbwTHIrrIHVtxt6q8cPx43RJqjE6qN+G1MA/Y4IVbgAzjJPnzu6A6v7E/FzSFbpNilv2i4=" + # SAUCE_ACCESS_KEY + - secure: "idJFmSy6EyMNO9UoxUx0wG83G/w8H1Sh1fG5lWodAdV01/Ft0j3KQo/zelENBx7zMWf+iqdWOhL4rBLIIkaajHbmvkMYDzhFXK4GIZmd1HnV4MZCunipscMsEbtQU+uTY/I3fersnIz74aTuj3SKlFW4jVNgvc8fawijBtTbuhU=" From a384d52133c9356a95f88c0c13bbd2b251089a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 14 May 2019 14:19:49 -0500 Subject: [PATCH 111/117] initial work on creating a bundle with parcel --- .travis.yml | 2 ++ README.md | 2 +- package.json | 2 ++ parcel-bundle-test.js | 10 ++++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 parcel-bundle-test.js diff --git a/.travis.yml b/.travis.yml index 333c944..78dfd38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ matrix: - npm run compile - npm run lint - npm run coverage + - npm run bundle + - node parcel-bundle-test.js - language: node_js node_js: "10.15" script: diff --git a/README.md b/README.md index 0797106..33d1005 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ and ``` or ```typescript -> const UrlPattern = require("url-pattern"); +> const UrlPattern = require("url-pattern").default; ``` ## works with [deno](https://deno.land/): diff --git a/package.json b/package.json index 12a6e0f..82a2abd 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "devDependencies": { "@types/tape": "^4.2.33", "nyc": "^14.1.0", + "parcel-bundler": "^1.12.3", "tape": "^4.10.1", "ts-node": "^8.1.0", "tslint": "^5.16.0", @@ -85,6 +86,7 @@ "test": "tape -r ts-node/register test/*.ts", "node": "ts-node", "coverage": "rm -r .nyc_output || true && rm -r coverage || true && nyc npm test", + "bundle": "parcel build src/url-pattern.ts", "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" }, diff --git a/parcel-bundle-test.js b/parcel-bundle-test.js new file mode 100644 index 0000000..425fcec --- /dev/null +++ b/parcel-bundle-test.js @@ -0,0 +1,10 @@ +// tests that the bundle generated by parcel via `npm run bundle` works + +const assert = require("assert"); +const UrlPattern = require("./dist/url-pattern.js").default; + +const pattern = new UrlPattern("/api/users/:id"); + +assert.deepEqual(pattern.match("/api/users/5"), {id: "5"}); +assert.deepEqual(pattern.stringify({id: 10}), "/api/users/10"); +console.log("OK"); From 725a19f6a87cd4ed53643e1ba87a0e94bcdfcb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 14 May 2019 14:20:45 -0500 Subject: [PATCH 112/117] remove colon in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33d1005..192c61c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ prefer a different syntax? [customize it:](#customize-the-pattern-syntax) - widely used [![Downloads per Month](https://img.shields.io/npm/dm/url-pattern.svg?style=flat)](https://www.npmjs.org/package/url-pattern) - zero dependencies - [parser](src/parser.ts) implemented using simple, precise, reusable [parser combinators](src/parsercombinators.ts) -- continuously tested in Node.js (10.15 (LTS), 12) and all relevant browsers: +- continuously tested in Node.js (10.15 (LTS), 12) and all relevant browsers - [huge test suite](test) [![Build Status](https://travis-ci.org/snd/url-pattern.svg?branch=master)](https://travis-ci.org/snd/url-pattern/branches) with [![codecov.io](http://codecov.io/github/snd/url-pattern/coverage.svg?branch=master)](http://codecov.io/github/snd/url-pattern?branch=master) From 76a4b36d00a7271605d6ffae1e909fed0c0f2990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Tue, 14 May 2019 14:20:54 -0500 Subject: [PATCH 113/117] add final "OK" to deno-test --- deno-test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/deno-test.ts b/deno-test.ts index d6f2e3e..cca8a52 100644 --- a/deno-test.ts +++ b/deno-test.ts @@ -5,3 +5,4 @@ const pattern = new UrlPattern("/api/users/:id"); assertEquals(pattern.match("/api/users/5"), {id: "5"}); assertEquals(pattern.stringify({id: 10}), "/api/users/10"); +console.log("OK"); From 715c6afd7f7c5c4775c2839f4d8300aaa3419607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Wed, 15 May 2019 15:20:45 -0500 Subject: [PATCH 114/117] improve file links in package.json --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 82a2abd..f32eee7 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,10 @@ "tslint": "^5.16.0", "typescript": "^3.4.5" }, - "main": "lib/url-pattern", + "main": "dist/url-pattern.js", + "browser": "dist/url-pattern.js", + "jsdelivr": "dist/url-pattern.js", + "types": "dist/url-pattern.d.ts", "scripts": { "compile": "tsc", "doc": "typedoc --out doc", @@ -90,7 +93,7 @@ "test-in-browsers": "zuul test/*", "test-zuul-local": "zuul --local 8080 test/*" }, - "typings": "index.d.ts", + "sideEffects": false, "nyc": { "include": [ "src/*.ts" From d76a23e7b43924303a99384a190fe3536dc1868e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Wed, 15 May 2019 15:21:45 -0500 Subject: [PATCH 115/117] bump version to 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f32eee7..03ae55e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "url-pattern", - "version": "1.0.3", + "version": "2.0.0", "description": "easier than regex string matching patterns for urls and other strings. turn strings into data or data into strings.", "keywords": [ "url", From 8fcd6a5a0f9098e49ffdf56abb93b66ec2123fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kru=CC=88ger?= Date: Wed, 15 May 2019 15:29:27 -0500 Subject: [PATCH 116/117] reorder package.json and add karma for testing --- package.json | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 03ae55e..37d829f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,11 @@ "processing" ], "homepage": "http://github.com/snd/url-pattern", + "bugs": { + "url": "http://github.com/snd/url-pattern/issues", + "email": "kruemaxi@gmail.com" + }, + "license": "MIT", "author": { "name": "Maximilian Krüger", "email": "kruemaxi@gmail.com", @@ -58,29 +63,18 @@ "url": "https://github.com/caasi" } ], - "bugs": { - "url": "http://github.com/snd/url-pattern/issues", - "email": "kruemaxi@gmail.com" - }, - "repository": { - "type": "git", - "url": "git://github.com/snd/url-pattern.git" - }, - "license": "MIT", - "dependencies": {}, - "devDependencies": { - "@types/tape": "^4.2.33", - "nyc": "^14.1.0", - "parcel-bundler": "^1.12.3", - "tape": "^4.10.1", - "ts-node": "^8.1.0", - "tslint": "^5.16.0", - "typescript": "^3.4.5" - }, + "files": [ + "src", + "dist" + ], "main": "dist/url-pattern.js", "browser": "dist/url-pattern.js", "jsdelivr": "dist/url-pattern.js", "types": "dist/url-pattern.d.ts", + "repository": { + "type": "git", + "url": "git://github.com/snd/url-pattern.git" + }, "scripts": { "compile": "tsc", "doc": "typedoc --out doc", @@ -90,9 +84,22 @@ "node": "ts-node", "coverage": "rm -r .nyc_output || true && rm -r coverage || true && nyc npm test", "bundle": "parcel build src/url-pattern.ts", - "test-in-browsers": "zuul test/*", + "test-in-browsers": "karma start test/*", "test-zuul-local": "zuul --local 8080 test/*" }, + "dependencies": {}, + "devDependencies": { + "@types/tape": "^4.2.33", + "karma": "^4.1.0", + "karma-sauce-launcher": "^2.0.2", + "karma-typescript": "^4.0.0", + "nyc": "^14.1.0", + "parcel-bundler": "^1.12.3", + "tape": "^4.10.1", + "ts-node": "^8.1.0", + "tslint": "^5.16.0", + "typescript": "^3.4.5" + }, "sideEffects": false, "nyc": { "include": [ From a3887a3e36c32f700883c837ff1061399556b7e7 Mon Sep 17 00:00:00 2001 From: Maximilian Krueger Date: Mon, 25 Nov 2019 15:53:02 -0600 Subject: [PATCH 117/117] add test for error increasing code coverage --- src/url-pattern.ts | 2 +- test/errors.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/url-pattern.ts b/src/url-pattern.ts index 2a1512a..c47f84d 100644 --- a/src/url-pattern.ts +++ b/src/url-pattern.ts @@ -73,7 +73,7 @@ export default class UrlPattern { const regexNameIndex = indexOfDuplicateElement(this.names); if (regexNameIndex !== -1) { throw new Error( - `duplicate name "${ this.names[regexNameIndex] }" in pattern. names must be unique`, + `duplicate group name "${ this.names[regexNameIndex] }". group names must be unique`, ); } return; diff --git a/test/errors.ts b/test/errors.ts index e7f198d..b4ee30a 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -114,7 +114,7 @@ tape("unclosed parentheses", (t: tape.Test) => { }); tape("regex names", (t: tape.Test) => { - t.plan(3); + t.plan(4); try { new UntypedUrlPattern(/x/, 5); } catch (error) { @@ -132,6 +132,11 @@ tape("regex names", (t: tape.Test) => { } catch (error) { t.equal(error.message, "regex contains 4 groups but array of group names contains 2"); } + try { + new UrlPattern(/(\d).(\d).(\d)/, ["a", "b", "a"]); + } catch (error) { + t.equal(error.message, "duplicate group name \"a\". group names must be unique"); + } t.end(); });