@lando/platformsh
Version:
A Lando plugin that provides a tight integration with Platform.sh.
445 lines (375 loc) • 12.8 kB
text/coffeescript
((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
= arg1.isRegex
= arg1.regex
= arg1.ast
= arg1.names
return
= arg1 instanceof RegExp
unless ('string' is typeof arg1) or
throw new TypeError 'argument must be a regex or a string'
# regex
if
= 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
unless arg2.length is groupCount
throw new Error "regex contains #{groupCount} groups but array of group names contains #{arg2.length}"
= 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"
= parsed.value
= new RegExp astNodeToRegexString , options.segmentValueCharset
= astNodeToNames
return
UrlPattern.prototype.match = (url) ->
match = .exec url
unless match?
return null
groups = match.slice(1)
if
keysAndValuesToObject , groups
else
groups
UrlPattern.prototype.stringify = (params = {}) ->
if
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 , 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
)