diff --git a/packages/router/src/apply_redirects.ts b/packages/router/src/apply_redirects.ts index f984a93c07..a6b76085b2 100644 --- a/packages/router/src/apply_redirects.ts +++ b/packages/router/src/apply_redirects.ts @@ -14,10 +14,11 @@ import {LoadedRouterConfig, Route, Routes} from './config'; import {CanLoadFn} from './interfaces'; import {prioritizedGuardValue} from './operators/prioritized_guard_value'; import {RouterConfigLoader} from './router_config_loader'; -import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared'; +import {navigationCancelingError, Params, PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; import {forEach, waitForMap, wrapIntoObservable} from './utils/collection'; import {getOutlet, groupRoutesByOutlet} from './utils/config'; +import {match, noLeftoversInUrl, split} from './utils/config_matching'; import {isCanLoad, isFunction, isUrlTree} from './utils/type_guards'; class NoMatch { @@ -79,9 +80,10 @@ class ApplyRedirects { apply(): Observable { const expanded$ = this.expandSegmentGroup(this.ngModule, this.config, this.urlTree.root, PRIMARY_OUTLET); - const urlTrees$ = expanded$.pipe( - map((rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree( - rootSegmentGroup, this.urlTree.queryParams, this.urlTree.fragment!))); + const urlTrees$ = expanded$.pipe(map((rootSegmentGroup: UrlSegmentGroup) => { + return this.createUrlTree( + squashSegmentGroup(rootSegmentGroup), this.urlTree.queryParams, this.urlTree.fragment!); + })); return urlTrees$.pipe(catchError((e: any) => { if (e instanceof AbsoluteRedirect) { // after an absolute redirect we do not apply any more redirects! @@ -101,9 +103,10 @@ class ApplyRedirects { private match(tree: UrlTree): Observable { const expanded$ = this.expandSegmentGroup(this.ngModule, this.config, tree.root, PRIMARY_OUTLET); - const mapped$ = expanded$.pipe( - map((rootSegmentGroup: UrlSegmentGroup) => - this.createUrlTree(rootSegmentGroup, tree.queryParams, tree.fragment!))); + const mapped$ = expanded$.pipe(map((rootSegmentGroup: UrlSegmentGroup) => { + return this.createUrlTree( + squashSegmentGroup(rootSegmentGroup), tree.queryParams, tree.fragment!); + })); return mapped$.pipe(catchError((e: any): Observable => { if (e instanceof NoMatch) { throw this.noMatchError(e); @@ -172,7 +175,7 @@ class ApplyRedirects { first((s: UrlSegmentGroup|null): s is UrlSegmentGroup => s !== null), catchError(e => { if (e instanceof EmptyError || e.name === 'EmptyError') { - if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) { + if (noLeftoversInUrl(segmentGroup, segments, outlet)) { return of(new UrlSegmentGroup([], {})); } throw new NoMatch(segmentGroup); @@ -197,11 +200,6 @@ class ApplyRedirects { ); } - private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string): - boolean { - return segments.length === 0 && !segmentGroup.children[outlet]; - } - private expandSegmentAgainstRoute( ngModule: NgModuleRef, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route, paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable { @@ -256,8 +254,8 @@ class ApplyRedirects { match(segmentGroup, route, segments); if (!matched) return noMatch(segmentGroup); - const newTree = this.applyRedirectCommands( - consumedSegments, route.redirectTo!, positionalParamSegments); + const newTree = + this.applyRedirectCommands(consumedSegments, route.redirectTo!, positionalParamSegments); if (route.redirectTo!.startsWith('/')) { return absoluteRedirect(newTree); } @@ -296,6 +294,14 @@ class ApplyRedirects { const {segmentGroup, slicedSegments} = split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig); + // TODO(atscott): clearing the source segment and segment index shift is only necessary to + // prevent failures in tests which assert exact object matches. The `split` is now shared + // between applyRedirects and recognize and only the `recognize` step needs these properties. + // Before the implementations were merged, the applyRedirects would not assign them. + // We should be able to remove this logic as a "breaking change" but should do some more + // investigation into the failures first. + segmentGroup._sourceSegment = undefined; + segmentGroup._segmentIndexShift = undefined; if (slicedSegments.length === 0 && segmentGroup.hasChildren()) { const expanded$ = this.expandChildren(childModule, childConfig, segmentGroup); @@ -467,64 +473,15 @@ class ApplyRedirects { } } -function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): { - matched: boolean, - consumedSegments: UrlSegment[], - lastChild: number, - positionalParamSegments: {[k: string]: UrlSegment} -} { - if (route.path === '') { - if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) { - return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}}; - } - - return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}}; - } - - const matcher = route.matcher || defaultUrlMatcher; - const res = matcher(segments, segmentGroup, route); - - if (!res) { - return { - matched: false, - consumedSegments: [], - lastChild: 0, - positionalParamSegments: {}, - }; - } - - return { - matched: true, - consumedSegments: res.consumed!, - lastChild: res.consumed.length!, - positionalParamSegments: res.posParams!, - }; -} - -function split( - segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], - config: Route[]) { - if (slicedSegments.length > 0 && - containsEmptyPathRedirectsWithNamedOutlets(segmentGroup, slicedSegments, config)) { - const s = new UrlSegmentGroup( - consumedSegments, - createChildrenForEmptySegments( - config, new UrlSegmentGroup(slicedSegments, segmentGroup.children))); - return {segmentGroup: mergeTrivialChildren(s), slicedSegments: []}; - } - - if (slicedSegments.length === 0 && - containsEmptyPathRedirects(segmentGroup, slicedSegments, config)) { - const s = new UrlSegmentGroup( - segmentGroup.segments, - addEmptySegmentsToChildrenIfNeeded( - segmentGroup, slicedSegments, config, segmentGroup.children)); - return {segmentGroup: mergeTrivialChildren(s), slicedSegments}; - } - - return {segmentGroup, slicedSegments}; -} +/** + * When possible, merges the primary outlet child into the parent `UrlSegmentGroup`. + * + * When a segment group has only one child which is a primary outlet, merges that child into the + * parent. That is, the child segment group's segments are merged into the `s` and the child's + * children become the children of `s`. Think of this like a 'squash', merging the child segment + * group into the parent. + */ function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup { if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) { const c = s.children[PRIMARY_OUTLET]; @@ -534,46 +491,20 @@ function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup { return s; } -function addEmptySegmentsToChildrenIfNeeded( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[], - children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} { - const res: {[name: string]: UrlSegmentGroup} = {}; - for (const r of routes) { - if (isEmptyPathRedirect(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) { - res[getOutlet(r)] = new UrlSegmentGroup([], {}); +/** + * Recursively merges primary segment children into their parents and also drops empty children + * (those which have no segments and no children themselves). The latter prevents serializing a + * group into something like `/a(aux:)`, where `aux` is an empty child segment. + */ +function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup { + const newChildren = {} as any; + for (const [childOutlet, child] of Object.entries(segmentGroup.children)) { + const childCandidate = squashSegmentGroup(child); + // don't add empty children + if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) { + newChildren[childOutlet] = childCandidate; } } - return {...children, ...res}; -} - -function createChildrenForEmptySegments( - routes: Route[], primarySegmentGroup: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} { - const res: {[name: string]: UrlSegmentGroup} = {}; - res[PRIMARY_OUTLET] = primarySegmentGroup; - for (const r of routes) { - if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) { - res[getOutlet(r)] = new UrlSegmentGroup([], {}); - } - } - return res; -} - -function containsEmptyPathRedirectsWithNamedOutlets( - segmentGroup: UrlSegmentGroup, segments: UrlSegment[], routes: Route[]): boolean { - return routes.some( - r => isEmptyPathRedirect(segmentGroup, segments, r) && getOutlet(r) !== PRIMARY_OUTLET); -} - -function containsEmptyPathRedirects( - segmentGroup: UrlSegmentGroup, segments: UrlSegment[], routes: Route[]): boolean { - return routes.some(r => isEmptyPathRedirect(segmentGroup, segments, r)); -} - -function isEmptyPathRedirect( - segmentGroup: UrlSegmentGroup, segments: UrlSegment[], r: Route): boolean { - if ((segmentGroup.hasChildren() || segments.length > 0) && r.pathMatch === 'full') { - return false; - } - - return r.path === '' && r.redirectTo !== undefined; + const s = new UrlSegmentGroup(segmentGroup.segments, newChildren); + return mergeTrivialChildren(s); } diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index 8cbbe39136..d55513beb2 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -11,10 +11,11 @@ import {Observable, Observer, of} from 'rxjs'; import {Data, ResolveData, Route, Routes} from './config'; import {ActivatedRouteSnapshot, inheritedParamsDataResolve, ParamsInheritanceStrategy, RouterStateSnapshot} from './router_state'; -import {defaultUrlMatcher, PRIMARY_OUTLET} from './shared'; +import {PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree'; -import {forEach, last} from './utils/collection'; +import {last} from './utils/collection'; import {getOutlet} from './utils/config'; +import {isImmediateMatch, match, noLeftoversInUrl, split} from './utils/config_matching'; import {TreeNode} from './utils/tree'; class NoMatch {} @@ -53,7 +54,10 @@ export class Recognizer { recognize(): RouterStateSnapshot|null { const rootSegmentGroup = - split(this.urlTree.root, [], [], this.config, this.relativeLinkResolution).segmentGroup; + split( + this.urlTree.root, [], [], this.config.filter(c => c.redirectTo === undefined), + this.relativeLinkResolution) + .segmentGroup; const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET); if (children === null) { @@ -136,22 +140,17 @@ export class Recognizer { return children; } } - if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) { + if (noLeftoversInUrl(segmentGroup, segments, outlet)) { return []; } return null; } - private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string): - boolean { - return segments.length === 0 && !segmentGroup.children[outlet]; - } - processSegmentAgainstRoute( route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[], outlet: string): TreeNode[]|null { - if (!isImmediateMatch(route, rawSegment, segments, outlet)) return null; + if (route.redirectTo || !isImmediateMatch(route, rawSegment, segments, outlet)) return null; let snapshot: ActivatedRouteSnapshot; let consumedSegments: UrlSegment[] = []; @@ -165,8 +164,8 @@ export class Recognizer { getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + segments.length, getResolve(route)); } else { - const result: MatchResult|null = match(rawSegment, route, segments); - if (result === null) { + const result = match(rawSegment, route, segments); + if (!result.matched) { return null; } consumedSegments = result.consumedSegments; @@ -182,7 +181,8 @@ export class Recognizer { const childConfig: Route[] = getChildConfig(route); const {segmentGroup, slicedSegments} = split( - rawSegment, consumedSegments, rawSlicedSegments, childConfig, this.relativeLinkResolution); + rawSegment, consumedSegments, rawSlicedSegments, + childConfig.filter(c => c.redirectTo === undefined), this.relativeLinkResolution); if (slicedSegments.length === 0 && segmentGroup.hasChildren()) { const children = this.processChildren(childConfig, segmentGroup); @@ -234,37 +234,6 @@ function getChildConfig(route: Route): Route[] { return []; } -interface MatchResult { - consumedSegments: UrlSegment[]; - lastChild: number; - parameters: any; -} - -function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): MatchResult| - null { - if (route.path === '') { - if (route.pathMatch === 'full' && (segmentGroup.hasChildren() || segments.length > 0)) { - return null; - } - - return {consumedSegments: [], lastChild: 0, parameters: {}}; - } - - const matcher = route.matcher || defaultUrlMatcher; - const res = matcher(segments, segmentGroup, route); - if (!res) return null; - - const posParams: {[n: string]: string} = {}; - forEach(res.posParams!, (v: UrlSegment, k: string) => { - posParams[k] = v.path; - }); - const parameters = res.consumed.length > 0 ? - {...posParams, ...res.consumed[res.consumed.length - 1].parameters} : - posParams; - - return {consumedSegments: res.consumed, lastChild: res.consumed.length, parameters}; -} - /** * Finds `TreeNode`s with matching empty path route configs and merges them into `TreeNode` with the * children from each duplicate. This is necessary because different outlets can match a single @@ -327,98 +296,6 @@ function getPathIndexShift(segmentGroup: UrlSegmentGroup): number { return res - 1; } -function split( - segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], - config: Route[], relativeLinkResolution: 'legacy'|'corrected') { - if (slicedSegments.length > 0 && - containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) { - const s = new UrlSegmentGroup( - consumedSegments, - createChildrenForEmptyPaths( - segmentGroup, consumedSegments, config, - new UrlSegmentGroup(slicedSegments, segmentGroup.children))); - s._sourceSegment = segmentGroup; - s._segmentIndexShift = consumedSegments.length; - return {segmentGroup: s, slicedSegments: []}; - } - - if (slicedSegments.length === 0 && - containsEmptyPathMatches(segmentGroup, slicedSegments, config)) { - const s = new UrlSegmentGroup( - segmentGroup.segments, - addEmptyPathsToChildrenIfNeeded( - segmentGroup, consumedSegments, slicedSegments, config, segmentGroup.children, - relativeLinkResolution)); - s._sourceSegment = segmentGroup; - s._segmentIndexShift = consumedSegments.length; - return {segmentGroup: s, slicedSegments}; - } - - const s = new UrlSegmentGroup(segmentGroup.segments, segmentGroup.children); - s._sourceSegment = segmentGroup; - s._segmentIndexShift = consumedSegments.length; - return {segmentGroup: s, slicedSegments}; -} - -function addEmptyPathsToChildrenIfNeeded( - segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], - routes: Route[], children: {[name: string]: UrlSegmentGroup}, - relativeLinkResolution: 'legacy'|'corrected'): {[name: string]: UrlSegmentGroup} { - const res: {[name: string]: UrlSegmentGroup} = {}; - for (const r of routes) { - if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) { - const s = new UrlSegmentGroup([], {}); - s._sourceSegment = segmentGroup; - if (relativeLinkResolution === 'legacy') { - s._segmentIndexShift = segmentGroup.segments.length; - } else { - s._segmentIndexShift = consumedSegments.length; - } - res[getOutlet(r)] = s; - } - } - return {...children, ...res}; -} - -function createChildrenForEmptyPaths( - segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], routes: Route[], - primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} { - const res: {[name: string]: UrlSegmentGroup} = {}; - res[PRIMARY_OUTLET] = primarySegment; - primarySegment._sourceSegment = segmentGroup; - primarySegment._segmentIndexShift = consumedSegments.length; - - for (const r of routes) { - if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) { - const s = new UrlSegmentGroup([], {}); - s._sourceSegment = segmentGroup; - s._segmentIndexShift = consumedSegments.length; - res[getOutlet(r)] = s; - } - } - return res; -} - -function containsEmptyPathMatchesWithNamedOutlets( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean { - return routes.some( - r => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET); -} - -function containsEmptyPathMatches( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean { - return routes.some(r => emptyPathMatch(segmentGroup, slicedSegments, r)); -} - -function emptyPathMatch( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean { - if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') { - return false; - } - - return r.path === '' && r.redirectTo === undefined; -} - function getData(route: Route): Data { return route.data || {}; } @@ -426,35 +303,3 @@ function getData(route: Route): Data { function getResolve(route: Route): ResolveData { return route.resolve || {}; } - -/** - * Determines if `route` is a path match for the `rawSegment`, `segments`, and `outlet` without - * verifying that its children are a full match for the remainder of the `rawSegment` children as - * well. - */ -function isImmediateMatch( - route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean { - if (route.redirectTo) { - return false; - } - // We allow matches to empty paths when the outlets differ so we can match a url like `/(b:b)` to - // a config like - // * `{path: '', children: [{path: 'b', outlet: 'b'}]}` - // or even - // * `{path: '', outlet: 'a', children: [{path: 'b', outlet: 'b'}]` - // - // The exception here is when the segment outlet is for the primary outlet. This would - // result in a match inside the named outlet because all children there are written as primary - // outlets. So we need to prevent child named outlet matches in a url like `/b` in a config like - // * `{path: '', outlet: 'x' children: [{path: 'b'}]}` - // This should only match if the url is `/(x:b)`. - if (getOutlet(route) !== outlet && - (outlet === PRIMARY_OUTLET || !emptyPathMatch(rawSegment, segments, route))) { - return false; - } - if (route.path === '**') { - return true; - } else { - return match(rawSegment, route, segments) !== null; - } -} diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index c1cee55f97..66b90d61ba 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -140,11 +140,9 @@ export class UrlTree { */ export class UrlSegmentGroup { /** @internal */ - // TODO(issue/24571): remove '!'. - _sourceSegment!: UrlSegmentGroup; + _sourceSegment?: UrlSegmentGroup; /** @internal */ - // TODO(issue/24571): remove '!'. - _segmentIndexShift!: number; + _segmentIndexShift?: number; /** The parent node in the url tree */ parent: UrlSegmentGroup|null = null; diff --git a/packages/router/src/utils/config_matching.ts b/packages/router/src/utils/config_matching.ts new file mode 100644 index 0000000000..b52f2ea344 --- /dev/null +++ b/packages/router/src/utils/config_matching.ts @@ -0,0 +1,194 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Route} from '../config'; +import {defaultUrlMatcher, PRIMARY_OUTLET} from '../shared'; +import {UrlSegment, UrlSegmentGroup} from '../url_tree'; + +import {forEach} from './collection'; +import {getOutlet} from './config'; + +export interface MatchResult { + matched: boolean; + consumedSegments: UrlSegment[]; + lastChild: number; + parameters: {[k: string]: string}; + positionalParamSegments: {[k: string]: UrlSegment}; +} + +const noMatch = { + matched: false, + consumedSegments: [], + lastChild: 0, + parameters: {}, + positionalParamSegments: {} +}; + +export function match( + segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): MatchResult { + if (route.path === '') { + if (route.pathMatch === 'full' && (segmentGroup.hasChildren() || segments.length > 0)) { + return {...noMatch}; + } + + return { + matched: true, + consumedSegments: [], + lastChild: 0, + parameters: {}, + positionalParamSegments: {} + }; + } + + const matcher = route.matcher || defaultUrlMatcher; + const res = matcher(segments, segmentGroup, route); + if (!res) return {...noMatch}; + + const posParams: {[n: string]: string} = {}; + forEach(res.posParams!, (v: UrlSegment, k: string) => { + posParams[k] = v.path; + }); + const parameters = res.consumed.length > 0 ? + {...posParams, ...res.consumed[res.consumed.length - 1].parameters} : + posParams; + + return { + matched: true, + consumedSegments: res.consumed, + lastChild: res.consumed.length, + // TODO(atscott): investigate combining parameters and positionalParamSegments + parameters, + positionalParamSegments: res.posParams ?? {} + }; +} + +export function split( + segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], + config: Route[], relativeLinkResolution: 'legacy'|'corrected' = 'corrected') { + if (slicedSegments.length > 0 && + containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) { + const s = new UrlSegmentGroup( + consumedSegments, + createChildrenForEmptyPaths( + segmentGroup, consumedSegments, config, + new UrlSegmentGroup(slicedSegments, segmentGroup.children))); + s._sourceSegment = segmentGroup; + s._segmentIndexShift = consumedSegments.length; + return {segmentGroup: s, slicedSegments: []}; + } + + if (slicedSegments.length === 0 && + containsEmptyPathMatches(segmentGroup, slicedSegments, config)) { + const s = new UrlSegmentGroup( + segmentGroup.segments, + addEmptyPathsToChildrenIfNeeded( + segmentGroup, consumedSegments, slicedSegments, config, segmentGroup.children, + relativeLinkResolution)); + s._sourceSegment = segmentGroup; + s._segmentIndexShift = consumedSegments.length; + return {segmentGroup: s, slicedSegments}; + } + + const s = new UrlSegmentGroup(segmentGroup.segments, segmentGroup.children); + s._sourceSegment = segmentGroup; + s._segmentIndexShift = consumedSegments.length; + return {segmentGroup: s, slicedSegments}; +} + +function addEmptyPathsToChildrenIfNeeded( + segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], + routes: Route[], children: {[name: string]: UrlSegmentGroup}, + relativeLinkResolution: 'legacy'|'corrected'): {[name: string]: UrlSegmentGroup} { + const res: {[name: string]: UrlSegmentGroup} = {}; + for (const r of routes) { + if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) { + const s = new UrlSegmentGroup([], {}); + s._sourceSegment = segmentGroup; + if (relativeLinkResolution === 'legacy') { + s._segmentIndexShift = segmentGroup.segments.length; + } else { + s._segmentIndexShift = consumedSegments.length; + } + res[getOutlet(r)] = s; + } + } + return {...children, ...res}; +} + +function createChildrenForEmptyPaths( + segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], routes: Route[], + primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} { + const res: {[name: string]: UrlSegmentGroup} = {}; + res[PRIMARY_OUTLET] = primarySegment; + primarySegment._sourceSegment = segmentGroup; + primarySegment._segmentIndexShift = consumedSegments.length; + + for (const r of routes) { + if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) { + const s = new UrlSegmentGroup([], {}); + s._sourceSegment = segmentGroup; + s._segmentIndexShift = consumedSegments.length; + res[getOutlet(r)] = s; + } + } + return res; +} + +function containsEmptyPathMatchesWithNamedOutlets( + segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean { + return routes.some( + r => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET); +} + +function containsEmptyPathMatches( + segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean { + return routes.some(r => emptyPathMatch(segmentGroup, slicedSegments, r)); +} + +function emptyPathMatch( + segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean { + if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') { + return false; + } + + return r.path === ''; +} + +/** + * Determines if `route` is a path match for the `rawSegment`, `segments`, and `outlet` without + * verifying that its children are a full match for the remainder of the `rawSegment` children as + * well. + */ +export function isImmediateMatch( + route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean { + // We allow matches to empty paths when the outlets differ so we can match a url like `/(b:b)` to + // a config like + // * `{path: '', children: [{path: 'b', outlet: 'b'}]}` + // or even + // * `{path: '', outlet: 'a', children: [{path: 'b', outlet: 'b'}]` + // + // The exception here is when the segment outlet is for the primary outlet. This would + // result in a match inside the named outlet because all children there are written as primary + // outlets. So we need to prevent child named outlet matches in a url like `/b` in a config like + // * `{path: '', outlet: 'x' children: [{path: 'b'}]}` + // This should only match if the url is `/(x:b)`. + if (getOutlet(route) !== outlet && + (outlet === PRIMARY_OUTLET || !emptyPathMatch(rawSegment, segments, route))) { + return false; + } + if (route.path === '**') { + return true; + } else { + return match(rawSegment, route, segments).matched; + } +} + +export function noLeftoversInUrl( + segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean { + return segments.length === 0 && !segmentGroup.children[outlet]; +}