@@ -7,6 +7,9 @@ import {prop, propEq } from "../common/hof";
7
7
import { isArray , isString } from "../common/predicates" ;
8
8
import { Param , paramTypes } from "../params/module" ;
9
9
import { isDefined } from "../common/predicates" ;
10
+ import { DefType } from "../params/param" ;
11
+ import { unnestR } from "../common/common" ;
12
+ import { arrayTuples } from "../common/common" ;
10
13
11
14
interface params {
12
15
$$validates : ( params : string ) => Array < string > ;
@@ -364,54 +367,93 @@ export class UrlMatcher {
364
367
* @returns {string } the formatted URL (path and optionally search part).
365
368
*/
366
369
format ( values = { } ) {
367
- let segments : string [ ] = this . _segments ,
368
- result : string = segments [ 0 ] ,
369
- search : boolean = false ,
370
- params : Param [ ] = this . parameters ( { inherit : false } ) ,
371
- parent : UrlMatcher = tail ( this . _cache . path ) ;
372
-
373
370
if ( ! this . validates ( values ) ) return null ;
374
371
375
- function encodeDashes ( str ) { // Replace dashes with encoded "\-"
376
- return encodeURIComponent ( str ) . replace ( / - / g, c => `%5C%${ c . charCodeAt ( 0 ) . toString ( 16 ) . toUpperCase ( ) } ` ) ;
377
- }
372
+ // Build the full path of UrlMatchers (including all parent UrlMatchers)
373
+ let urlMatchers = this . _cache . path . slice ( ) . concat ( this ) ;
374
+
375
+ // Extract all the static segments and Params into an ordered array
376
+ let pathSegmentsAndParams : Array < string | Param > =
377
+ urlMatchers . map ( UrlMatcher . pathSegmentsAndParams ) . reduce ( unnestR , [ ] ) ;
378
378
379
- // TODO: rewrite as reduce over params with result as initial
380
- params . map ( ( param : Param , i ) => {
381
- let isPathParam = i < segments . length - 1 ;
382
- var isFinalPathParam = i + 2 === segments . length ;
379
+ // Extract the query params into a separate array
380
+ let queryParams : Array < Param > =
381
+ urlMatchers . map ( UrlMatcher . queryParams ) . reduce ( unnestR , [ ] ) ;
382
+
383
+ /**
384
+ * Given a Param,
385
+ * Applies the parameter value, then returns details about it
386
+ */
387
+ function getDetails ( param : Param ) : ParamDetails {
388
+ // Normalize to typed value
383
389
let value = param . value ( values [ param . id ] ) ;
384
390
let isDefaultValue = param . isDefaultValue ( value ) ;
391
+ // Check if we're in squash mode for the parameter
385
392
let squash = isDefaultValue ? param . squash : false ;
393
+ // Allow the Parameter's Type to encode the value
386
394
let encoded = param . type . encode ( value ) ;
387
395
388
- if ( ! isPathParam ) {
389
- if ( encoded == null || ( isDefaultValue && squash !== false ) ) return ;
390
- if ( ! isArray ( encoded ) ) encoded = [ < string > encoded ] ;
391
- if ( encoded . length === 0 ) return ;
396
+ return { param, value, isDefaultValue, squash, encoded } ;
397
+ }
392
398
393
- encoded = map ( < string [ ] > encoded , encodeURIComponent ) . join ( `&${ param . id } =` ) ;
394
- result += ( search ? '&' : '?' ) + ( `${ param . id } =${ encoded } ` ) ;
395
- search = true ;
396
- return ;
397
- }
399
+ // Build up the path-portion from the list of static segments and parameters
400
+ let pathString = pathSegmentsAndParams . reduce ( ( acc : string , x : string | Param ) => {
401
+ // The element is a static segment (a raw string); just append it
402
+ if ( isString ( x ) ) return acc + x ;
403
+
404
+ // Otherwise, it's a Param. Fetch details about the parameter value
405
+ let { squash, encoded, param} = getDetails ( < Param > x ) ;
406
+
407
+ // If squash is === true, try to remove a slash from the path
408
+ if ( squash === true ) return ( acc . match ( / \/ $ / ) ) ? acc . slice ( 0 , - 1 ) : acc ;
409
+ // If squash is a string, use the string for the param value
410
+ if ( isString ( squash ) ) return acc + squash ;
411
+ if ( squash !== false ) return acc ; // ?
412
+ if ( encoded == null ) return acc ;
413
+ // If this parameter value is an array, encode the value using encodeDashes
414
+ if ( isArray ( encoded ) ) return acc + map ( < string [ ] > encoded , UrlMatcher . encodeDashes ) . join ( "-" ) ;
415
+ // If the parameter type is "raw", then do not encodeURIComponent
416
+ if ( param . type . raw ) return acc + encoded ;
417
+ // Encode the value
418
+ return acc + encodeURIComponent ( < string > encoded ) ;
419
+ } , "" ) ;
420
+
421
+ // Build the query string by
422
+ let queryString = queryParams . map ( ( param : Param ) => {
423
+ let { squash, encoded, isDefaultValue} = getDetails ( param ) ;
424
+ if ( encoded == null || ( isDefaultValue && squash !== false ) ) return ;
425
+ if ( ! isArray ( encoded ) ) encoded = [ < string > encoded ] ;
426
+ if ( encoded . length === 0 ) return ;
427
+ if ( ! param . type . raw ) encoded = map ( < string [ ] > encoded , encodeURIComponent ) ;
428
+
429
+ return encoded . map ( val => `${ param . id } =${ val } ` ) ;
430
+ } ) . filter ( identity ) . reduce ( unnestR , [ ] ) . join ( "&" ) ;
431
+
432
+ // Concat the pathstring with the queryString (if exists) and the hasString (if exists)
433
+ return pathString + ( queryString ? `?${ queryString } ` : "" ) + ( values [ "#" ] ? "#" + values [ "#" ] : "" ) ;
434
+ }
398
435
399
- result += ( ( segment , result ) => {
400
- if ( squash === true ) return segment . match ( result . match ( / \/ $ / ) ? / \/ ? ( .* ) / : / ( .* ) / ) [ 1 ] ;
401
- if ( isString ( squash ) ) return squash + segment ;
402
- if ( squash !== false ) return "" ;
403
- if ( encoded == null ) return segment ;
404
- if ( isArray ( encoded ) ) return map ( < string [ ] > encoded , encodeDashes ) . join ( "-" ) + segment ;
405
- if ( param . type . raw ) return encoded + segment ;
406
- return encodeURIComponent ( < string > encoded ) + segment ;
407
- } ) ( segments [ i + 1 ] , result ) ;
408
-
409
- if ( isFinalPathParam && squash === true && result . slice ( - 1 ) === '/' ) result = result . slice ( 0 , - 1 ) ;
410
- } ) ;
436
+ static encodeDashes ( str ) { // Replace dashes with encoded "\-"
437
+ return encodeURIComponent ( str ) . replace ( / - / g, c => `%5C%${ c . charCodeAt ( 0 ) . toString ( 16 ) . toUpperCase ( ) } ` ) ;
438
+ }
411
439
412
- if ( values [ "#" ] ) result += "#" + values [ "#" ] ;
440
+ /** Given a matcher, return an array with the matcher's path segments and path params, in order */
441
+ static pathSegmentsAndParams ( matcher : UrlMatcher ) {
442
+ let staticSegments = matcher . _segments ;
443
+ let pathParams = matcher . _params . filter ( p => p . location === DefType . PATH ) ;
444
+ return arrayTuples ( staticSegments , pathParams . concat ( undefined ) ) . reduce ( unnestR , [ ] ) . filter ( x => x !== "" && isDefined ( x ) ) ;
445
+ }
413
446
414
- let processedParams = [ '#' ] . concat ( params . map ( prop ( 'id' ) ) ) ;
415
- return ( parent && parent . format ( omit ( values , processedParams ) ) || '' ) + result ;
447
+ /** Given a matcher, return an array with the matcher's query params */
448
+ static queryParams ( matcher : UrlMatcher ) : Param [ ] {
449
+ return matcher . _params . filter ( p => p . location === DefType . SEARCH ) ;
416
450
}
417
451
}
452
+
453
+ interface ParamDetails {
454
+ param : Param ;
455
+ value : any ;
456
+ isDefaultValue : boolean ;
457
+ squash : ( boolean | string ) ;
458
+ encoded : ( string | string [ ] ) ;
459
+ }
0 commit comments