@@ -71,23 +71,22 @@ function UrlMatcher(pattern, config) {
71
71
// '{' name ':' regexp '}'
72
72
// The regular expression is somewhat complicated due to the need to allow curly braces
73
73
// inside the regular expression. The placeholder regexp breaks down as follows:
74
- // ([:*])(\w+) classic placeholder ($1 / $2)
75
- // ([:]?)([\w-]+) classic search placeholder (supports snake-case-params) ($1 / $2)
76
- // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp/type ... ($4)
77
- // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either
78
- // [^{}\\]+ - anything other than curly braces or backslash
79
- // \\. - a backslash escape
80
- // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
81
- var placeholder = / ( [: * ] ) ( \w + ) | \{ ( \w + ) (?: \: ( (?: [ ^ { } \\ ] + | \\ .| \{ (?: [ ^ { } \\ ] + | \\ .) * \} ) + ) ) ? \} / g,
82
- searchPlaceholder = / ( [: ] ? ) ( [ \w - ] + ) | \{ ( \w + ) (?: \: ( (?: [ ^ { } \\ ] + | \\ .| \{ (?: [ ^ { } \\ ] + | \\ .) * \} ) + ) ) ? \} / g,
74
+ // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case)
75
+ // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case
76
+ // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either
77
+ // [^{}\\]+ - anything other than curly braces or backslash
78
+ // \\. - a backslash escape
79
+ // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
80
+ var placeholder = / ( [: * ] ) ( [ \w \[ \] ] + ) | \{ ( [ \w \[ \] ] + ) (?: \: ( (?: [ ^ { } \\ ] + | \\ .| \{ (?: [ ^ { } \\ ] + | \\ .) * \} ) + ) ) ? \} / g,
81
+ searchPlaceholder = / ( [: ] ? ) ( [ \w \[ \] - ] + ) | \{ ( [ \w \[ \] - ] + ) (?: \: ( (?: [ ^ { } \\ ] + | \\ .| \{ (?: [ ^ { } \\ ] + | \\ .) * \} ) + ) ) ? \} / g,
83
82
compiled = '^' , last = 0 , m ,
84
83
segments = this . segments = [ ] ,
85
84
params = this . params = new $$UMFP . ParamSet ( ) ;
86
85
87
- function addParameter ( id , type , config ) {
88
- if ( ! / ^ \w + ( - + \w + ) * $ / . test ( id ) ) throw new Error ( "Invalid parameter name '" + id + "' in pattern '" + pattern + "'" ) ;
86
+ function addParameter ( id , type , config , isSearch ) {
87
+ if ( ! / ^ \w + ( - + \w + ) * (?: \[ \] ) ? $ / . test ( id ) ) throw new Error ( "Invalid parameter name '" + id + "' in pattern '" + pattern + "'" ) ;
89
88
if ( params [ id ] ) throw new Error ( "Duplicate parameter name '" + id + "' in pattern '" + pattern + "'" ) ;
90
- params [ id ] = new $$UMFP . Param ( id , type , config ) ;
89
+ params [ id ] = new $$UMFP . Param ( id , type , config , isSearch ) ;
91
90
return params [ id ] ;
92
91
}
93
92
@@ -107,13 +106,12 @@ function UrlMatcher(pattern, config) {
107
106
// Split into static segments separated by path parameter placeholders.
108
107
// The number of segments is always 1 more than the number of parameters.
109
108
function matchDetails ( m , isSearch ) {
110
- var id , regexp , segment , type , cfg ;
111
- id = m [ 2 ] || m [ 3 ] ; // IE[78] returns '' for unmatched groups instead of null
112
- segment = pattern . substring ( last , m . index ) ;
113
- regexp = isSearch ? m [ 4 ] : m [ 4 ] || ( m [ 1 ] == '*' ? '.*' : null ) ;
114
- type = $$UMFP . type ( regexp || "string" ) || inherit ( $$UMFP . type ( "string" ) , { pattern : new RegExp ( regexp ) } ) ;
115
- type = isSearch ? type . $asSearchType ( ) : type ;
116
- cfg = config . params [ id ] ;
109
+ var id , regexp , segment , type , cfg , arrayMode ;
110
+ id = m [ 2 ] || m [ 3 ] ; // IE[78] returns '' for unmatched groups instead of null
111
+ cfg = config . params [ id ] ;
112
+ segment = pattern . substring ( last , m . index ) ;
113
+ regexp = isSearch ? m [ 4 ] : m [ 4 ] || ( m [ 1 ] == '*' ? '.*' : null ) ;
114
+ type = $$UMFP . type ( regexp || "string" ) || inherit ( $$UMFP . type ( "string" ) , { pattern : new RegExp ( regexp ) } ) ;
117
115
return {
118
116
id : id , regexp : regexp , segment : segment , type : type , cfg : cfg
119
117
} ;
@@ -124,7 +122,7 @@ function UrlMatcher(pattern, config) {
124
122
p = matchDetails ( m , false ) ;
125
123
if ( p . segment . indexOf ( '?' ) >= 0 ) break ; // we're into the search part
126
124
127
- param = addParameter ( p . id , p . type , p . cfg ) ;
125
+ param = addParameter ( p . id , p . type , p . cfg , false ) ;
128
126
compiled += quoteRegExp ( p . segment , param . type . pattern . source , param . squash ) ;
129
127
segments . push ( p . segment ) ;
130
128
last = placeholder . lastIndex ;
@@ -143,7 +141,7 @@ function UrlMatcher(pattern, config) {
143
141
last = 0 ;
144
142
while ( ( m = searchPlaceholder . exec ( search ) ) ) {
145
143
p = matchDetails ( m , true ) ;
146
- param = addParameter ( p . id , p . type , p . cfg ) ;
144
+ param = addParameter ( p . id , p . type , p . cfg , true ) ;
147
145
last = placeholder . lastIndex ;
148
146
// check if ?&
149
147
}
@@ -228,9 +226,19 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
228
226
229
227
if ( nPath !== m . length - 1 ) throw new Error ( "Unbalanced capture group in route '" + this . source + "'" ) ;
230
228
229
+ function decodePathArray ( string ) {
230
+ function reverseString ( str ) { return str . split ( "" ) . reverse ( ) . join ( "" ) ; }
231
+ function unquoteDashes ( str ) { return str . replace ( / \\ - / , "-" ) ; }
232
+ return reverseString ( string ) . split ( / - (? ! \\ ) / ) . map ( reverseString ) . map ( unquoteDashes ) . reverse ( ) ;
233
+ }
234
+
231
235
for ( i = 0 ; i < nPath ; i ++ ) {
232
236
paramName = paramNames [ i ] ;
233
- values [ paramName ] = this . params [ paramName ] . value ( m [ i + 1 ] ) ;
237
+ var param = this . params [ paramName ] ;
238
+ // if the param is optional, convert an empty string to `undefined`
239
+ var paramVal = m [ i + 1 ] === "" ? param . emptyString : m [ i + 1 ] ;
240
+ if ( paramVal && param . array === true ) paramVal = decodePathArray ( paramVal ) ;
241
+ values [ paramName ] = param . value ( paramVal ) ;
234
242
}
235
243
for ( /**/ ; i < nTotal ; i ++ ) {
236
244
paramName = paramNames [ i ] ;
@@ -298,6 +306,10 @@ UrlMatcher.prototype.format = function (values) {
298
306
299
307
var i , search = false , nPath = segments . length - 1 , nTotal = params . length , result = segments [ 0 ] ;
300
308
309
+ function encodeDashes ( str ) { // Replace dashes with encoded "\-"
310
+ return encodeURIComponent ( str ) . replace ( / - / g, function ( c ) { return '%5C%' + c . charCodeAt ( 0 ) . toString ( 16 ) . toUpperCase ( ) ; } ) ;
311
+ }
312
+
301
313
for ( i = 0 ; i < nTotal ; i ++ ) {
302
314
var isPathParam = i < nPath ;
303
315
var name = params [ i ] , param = paramset [ name ] , value = param . value ( values [ name ] ) ;
@@ -308,7 +320,13 @@ UrlMatcher.prototype.format = function (values) {
308
320
if ( isPathParam ) {
309
321
var nextSegment = segments [ i + 1 ] ;
310
322
if ( squash === "nosquash" ) {
311
- if ( encoded != null ) result += encodeURIComponent ( encoded ) ;
323
+ if ( encoded != null ) {
324
+ if ( isArray ( encoded ) ) {
325
+ result += encoded . map ( encodeDashes ) . join ( "-" ) ;
326
+ } else {
327
+ result += encodeURIComponent ( encoded ) ;
328
+ }
329
+ }
312
330
result += nextSegment ;
313
331
} else if ( squash === "value" ) {
314
332
result += nextSegment ;
@@ -443,49 +461,45 @@ Type.prototype.pattern = /.*/;
443
461
Type . prototype . toString = function ( ) { return "{Type:" + this . name + "}" ; } ;
444
462
445
463
/*
446
- * Wraps an existing custom Type as a search-query aware type which adds multi-value support .
464
+ * Wraps an existing custom Type as an array of Type, depending on 'mode' .
447
465
* e.g.:
448
- * - urlmatcher pattern "/path?{queryParam:int}"
466
+ * - urlmatcher pattern "/path?{queryParam[] :int}"
449
467
* - url: "/path?queryParam=1&queryParam=2
450
468
* - $stateParams.queryParam will be [1, 2]
469
+ * if `mode` is "auto", then
470
+ * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1
471
+ * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2]
451
472
*/
452
- Type . prototype . $asSearchType = function ( ) {
453
- return new SearchType ( this ) ;
454
-
455
- function SearchType ( type ) {
456
- var self = this ;
457
- if ( type . $$autoSearchArray === false ) return type ;
458
-
459
- function allTruthy ( array ) { // TODO: use reduce fn
460
- var result = true ;
461
- forEach ( array , function ( val ) { result = result && ! ! val ; } ) ;
462
- return result ;
463
- }
464
-
465
- function map ( array , callback ) { // TODO: move to common.js in 1.0
466
- var result = [ ] ;
467
- forEach ( array , function ( val ) { result . push ( callback ( val ) ) ; } ) ;
468
- return result ;
473
+ Type . prototype . $asArray = function ( mode , isSearch ) {
474
+ if ( ! mode ) return this ;
475
+ if ( mode === "auto" && ! isSearch ) throw new Error ( "'auto' array mode is for query parameters only" ) ;
476
+ return new ArrayType ( this , mode ) ;
477
+
478
+ function ArrayType ( type , mode ) {
479
+ function bindTo ( thisObj , callback ) {
480
+ return function ( ) {
481
+ return callback . apply ( thisObj , arguments ) ;
482
+ } ;
469
483
}
470
484
471
- function autoHandleArray ( callback , reducefn ) {
472
- return function ( val ) {
473
- if ( isArray ( val ) ) {
474
- var result = map ( val , callback ) ;
475
- return reducefn ? reducefn ( result ) : result ;
476
- } else {
477
- return callback ( val ) ;
478
- }
485
+ function arrayHandler ( callback , reducefn ) {
486
+ // Wraps type functions to operate on each value of an array
487
+ return function handleArray ( val ) {
488
+ if ( ! isArray ( val ) ) val = [ val ] ;
489
+ var result = val . map ( callback ) ;
490
+ if ( reducefn )
491
+ return result . reduce ( reducefn , true ) ;
492
+ return ( result && result . length == 1 && mode === "auto" ) ? result [ 0 ] : result ;
479
493
} ;
480
494
}
481
495
482
- function bindTo ( thisObj , callback ) { return function ( ) { return callback . apply ( thisObj , arguments ) ; } ; }
483
-
484
- this . encode = autoHandleArray ( bindTo ( this , type . encode ) ) ;
485
- this . decode = autoHandleArray ( bindTo ( this , type . decode ) ) ;
486
- this . equals = autoHandleArray ( bindTo ( this , type . equals ) , allTruthy ) ;
487
- this . is = autoHandleArray ( bindTo ( this , type . is ) , allTruthy ) ;
496
+ function alltruthy ( val , memo ) { return val && memo ; }
497
+ this . encode = arrayHandler ( bindTo ( this , type . encode ) ) ;
498
+ this . decode = arrayHandler ( bindTo ( this , type . decode ) ) ;
499
+ this . equals = arrayHandler ( bindTo ( this , type . equals ) , alltruthy ) ;
500
+ this . is = arrayHandler ( bindTo ( this , type . is ) , alltruthy ) ;
488
501
this . pattern = type . pattern ;
502
+ this . $arrayMode = mode ;
489
503
}
490
504
} ;
491
505
@@ -797,17 +811,21 @@ function $UrlMatcherFactory() {
797
811
return this ;
798
812
} ] ;
799
813
800
- this . Param = function Param ( id , type , config ) {
814
+ this . Param = function Param ( id , type , config , isSearch ) {
801
815
var self = this ;
802
816
var defaultValueConfig = getDefaultValueConfig ( config ) ;
803
817
config = config || { } ;
804
818
type = getType ( config , type ) ;
819
+ var arrayMode = getArrayMode ( ) ;
820
+ type = arrayMode ? type . $asArray ( arrayMode , isSearch ) : type ;
805
821
var isOptional = defaultValueConfig . value !== undefined ;
806
822
var squash = getSquashPolicy ( config , isOptional ) ;
823
+ var emptyString = getEmptyStringValue ( config , arrayMode , isOptional ) ;
807
824
808
825
function getDefaultValueConfig ( config ) {
809
826
var keys = isObject ( config ) ? objectKeys ( config ) : [ ] ;
810
- var isShorthand = keys . indexOf ( "value" ) === - 1 && keys . indexOf ( "type" ) === - 1 && keys . indexOf ( "squash" ) === - 1 ;
827
+ var isShorthand = keys . indexOf ( "value" ) === - 1 && keys . indexOf ( "type" ) === - 1 &&
828
+ keys . indexOf ( "squash" ) === - 1 && keys . indexOf ( "array" ) === - 1 ;
811
829
var configValue = isShorthand ? config : config . value ;
812
830
return {
813
831
fn : isInjectable ( configValue ) ? configValue : function ( ) { return configValue ; } ,
@@ -822,6 +840,13 @@ function $UrlMatcherFactory() {
822
840
return config . type instanceof Type ? config . type : new Type ( config . type ) ;
823
841
}
824
842
843
+ // array config: param name (param[]) overrides default settings. explicit config overrides param name.
844
+ function getArrayMode ( ) {
845
+ var arrayDefaults = { array : isSearch ? "auto" : false } ;
846
+ var arrayParamNomenclature = id . match ( / \[ \] $ / ) ? { array : true } : { } ;
847
+ return extend ( arrayDefaults , arrayParamNomenclature , config ) . array ;
848
+ }
849
+
825
850
/**
826
851
* returns "nosquash", "value", "slash" to indicate the "default parameter url squash policy".
827
852
* undefined aliases to urlMatcherFactory default. `false` aliases to "nosquash". `true` aliases to "slash".
@@ -835,6 +860,15 @@ function $UrlMatcherFactory() {
835
860
throw new Error ( "Invalid squash policy: '" + squash + "'. Valid policies: 'nosquash' (false), 'value', 'slash' (true)" ) ;
836
861
}
837
862
863
+ /**
864
+ * Returns "" or undefined, or whatever is defined in the param's config.emptyString.
865
+ * If the parameter was matched in a URL, but was matched as an empty string, this value will be used instead.
866
+ */
867
+ function getEmptyStringValue ( config , arrayMode , isOptional ) {
868
+ var defaultPolicy = { emptyString : ( isOptional || arrayMode ? undefined : "" ) } ;
869
+ return extend ( defaultPolicy , config ) . emptyString ;
870
+ }
871
+
838
872
/**
839
873
* [Internal] Get the default value of a parameter, which may be an injectable function.
840
874
*/
@@ -856,10 +890,12 @@ function $UrlMatcherFactory() {
856
890
extend ( this , {
857
891
id : id ,
858
892
type : type ,
893
+ array : arrayMode ,
859
894
config : config ,
860
895
squash : squash ,
861
- dynamic : undefined ,
896
+ emptyString : emptyString ,
862
897
isOptional : isOptional ,
898
+ dynamic : undefined ,
863
899
value : $value ,
864
900
toString : toString
865
901
} ) ;
0 commit comments