refactor(router): Extract common functions for config matching (#40029)

The `applyRedirects` and `recognize` algorithms have the same overall goal:
match a `UrlTree` with the application's `Routes` config. There are a
few key functions in these algorithms which can be shared rather than
duplicated between the two. This also makes it easier to see how the two
are similar and where they diverge.

PR Close #40029
This commit is contained in:
Andrew Scott 2020-12-03 09:53:50 -08:00 committed by Joey Perrott
parent 3966bcc5d9
commit 5842467134
4 changed files with 252 additions and 284 deletions

View File

@ -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<UrlTree> {
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<UrlTree> {
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<UrlTree> => {
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<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
@ -256,8 +254,8 @@ class ApplyRedirects {
match(segmentGroup, route, segments);
if (!matched) return noMatch(segmentGroup);
const newTree = this.applyRedirectCommands(
consumedSegments, route.redirectTo!, <any>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: <any[]>[],
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);
}

View File

@ -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<ActivatedRouteSnapshot>[]|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;
}
}

View File

@ -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;

View File

@ -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];
}