2016-06-23 09:47:54 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2016-06-23 09:47:54 -07:00
|
|
|
*
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
|
2016-06-08 11:13:41 -07:00
|
|
|
import {Type} from '@angular/core';
|
2020-04-13 16:40:21 -07:00
|
|
|
import {Observable, Observer, of} from 'rxjs';
|
2016-06-08 11:13:41 -07:00
|
|
|
|
2017-04-11 08:34:58 -07:00
|
|
|
import {Data, ResolveData, Route, Routes} from './config';
|
2020-04-13 16:40:21 -07:00
|
|
|
import {ActivatedRouteSnapshot, inheritedParamsDataResolve, ParamsInheritanceStrategy, RouterStateSnapshot} from './router_state';
|
|
|
|
|
import {defaultUrlMatcher, PRIMARY_OUTLET} from './shared';
|
2020-11-26 09:26:57 -08:00
|
|
|
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
|
2017-03-26 17:15:10 +03:00
|
|
|
import {forEach, last} from './utils/collection';
|
2020-11-19 16:17:12 +01:00
|
|
|
import {getOutlet} from './utils/config';
|
2016-06-08 11:13:41 -07:00
|
|
|
import {TreeNode} from './utils/tree';
|
2016-05-23 16:14:23 -07:00
|
|
|
|
2016-08-04 18:56:22 -07:00
|
|
|
class NoMatch {}
|
2016-06-06 10:15:23 -07:00
|
|
|
|
2020-11-26 09:26:57 -08:00
|
|
|
function newObservableError(e: unknown): Observable<RouterStateSnapshot> {
|
|
|
|
|
// TODO(atscott): This pattern is used throughout the router code and can be `throwError` instead.
|
|
|
|
|
return new Observable<RouterStateSnapshot>((obs: Observer<RouterStateSnapshot>) => obs.error(e));
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-10 18:21:28 -07:00
|
|
|
export function recognize(
|
2020-04-13 16:40:21 -07:00
|
|
|
rootComponentType: Type<any>|null, config: Routes, urlTree: UrlTree, url: string,
|
2018-02-23 10:24:51 +01:00
|
|
|
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly',
|
2020-04-13 16:40:21 -07:00
|
|
|
relativeLinkResolution: 'legacy'|'corrected' = 'legacy'): Observable<RouterStateSnapshot> {
|
2020-11-26 09:26:57 -08:00
|
|
|
try {
|
|
|
|
|
const result = new Recognizer(
|
|
|
|
|
rootComponentType, config, urlTree, url, paramsInheritanceStrategy,
|
|
|
|
|
relativeLinkResolution)
|
|
|
|
|
.recognize();
|
|
|
|
|
if (result === null) {
|
|
|
|
|
return newObservableError(new NoMatch());
|
|
|
|
|
} else {
|
|
|
|
|
return of(result);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Catch the potential error from recognize due to duplicate outlet matches and return as an
|
|
|
|
|
// `Observable` error instead.
|
|
|
|
|
return newObservableError(e);
|
|
|
|
|
}
|
2016-05-23 16:14:23 -07:00
|
|
|
}
|
|
|
|
|
|
2020-11-26 09:26:57 -08:00
|
|
|
export class Recognizer {
|
2016-08-02 15:31:56 -07:00
|
|
|
constructor(
|
2017-04-17 11:13:13 -07:00
|
|
|
private rootComponentType: Type<any>|null, private config: Routes, private urlTree: UrlTree,
|
2018-02-23 10:24:51 +01:00
|
|
|
private url: string, private paramsInheritanceStrategy: ParamsInheritanceStrategy,
|
|
|
|
|
private relativeLinkResolution: 'legacy'|'corrected') {}
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2020-11-26 09:26:57 -08:00
|
|
|
recognize(): RouterStateSnapshot|null {
|
|
|
|
|
const rootSegmentGroup =
|
|
|
|
|
split(this.urlTree.root, [], [], this.config, this.relativeLinkResolution).segmentGroup;
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2020-11-26 09:26:57 -08:00
|
|
|
const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET);
|
|
|
|
|
if (children === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2016-08-02 15:31:56 -07:00
|
|
|
|
2020-11-26 09:26:57 -08:00
|
|
|
const root = new ActivatedRouteSnapshot(
|
|
|
|
|
[], Object.freeze({}), Object.freeze({...this.urlTree.queryParams}), this.urlTree.fragment!,
|
|
|
|
|
{}, PRIMARY_OUTLET, this.rootComponentType, null, this.urlTree.root, -1, {});
|
2016-06-02 11:30:38 -07:00
|
|
|
|
2020-11-26 09:26:57 -08:00
|
|
|
const rootNode = new TreeNode<ActivatedRouteSnapshot>(root, children);
|
|
|
|
|
const routeState = new RouterStateSnapshot(this.url, rootNode);
|
|
|
|
|
this.inheritParamsAndData(routeState._root);
|
|
|
|
|
return routeState;
|
2016-06-06 10:15:23 -07:00
|
|
|
}
|
2016-05-23 16:14:23 -07:00
|
|
|
|
2017-05-03 11:17:27 +02:00
|
|
|
inheritParamsAndData(routeNode: TreeNode<ActivatedRouteSnapshot>): void {
|
2016-10-25 14:33:18 -07:00
|
|
|
const route = routeNode.value;
|
2016-06-19 14:44:20 -07:00
|
|
|
|
2017-11-28 16:57:10 -08:00
|
|
|
const i = inheritedParamsDataResolve(route, this.paramsInheritanceStrategy);
|
2016-10-25 14:33:18 -07:00
|
|
|
route.params = Object.freeze(i.params);
|
|
|
|
|
route.data = Object.freeze(i.data);
|
|
|
|
|
|
2017-05-03 11:17:27 +02:00
|
|
|
routeNode.children.forEach(n => this.inheritParamsAndData(n));
|
2016-10-25 14:33:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processSegmentGroup(config: Route[], segmentGroup: UrlSegmentGroup, outlet: string):
|
2020-11-25 17:24:58 -08:00
|
|
|
TreeNode<ActivatedRouteSnapshot>[]|null {
|
2016-08-02 15:31:56 -07:00
|
|
|
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
|
2016-10-25 14:33:18 -07:00
|
|
|
return this.processChildren(config, segmentGroup);
|
2016-08-02 15:31:56 -07:00
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
|
|
|
|
|
return this.processSegment(config, segmentGroup, segmentGroup.segments, outlet);
|
2016-08-02 15:31:56 -07:00
|
|
|
}
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2020-11-25 17:27:48 -08:00
|
|
|
/**
|
|
|
|
|
* Matches every child outlet in the `segmentGroup` to a `Route` in the config. Returns `null` if
|
|
|
|
|
* we cannot find a match for _any_ of the children.
|
|
|
|
|
*
|
|
|
|
|
* @param config - The `Routes` to match against
|
|
|
|
|
* @param segmentGroup - The `UrlSegmentGroup` whose children need to be matched against the
|
|
|
|
|
* config.
|
|
|
|
|
*/
|
2016-10-25 14:33:18 -07:00
|
|
|
processChildren(config: Route[], segmentGroup: UrlSegmentGroup):
|
2020-11-25 17:24:58 -08:00
|
|
|
TreeNode<ActivatedRouteSnapshot>[]|null {
|
2020-11-25 17:19:43 -08:00
|
|
|
const children: Array<TreeNode<ActivatedRouteSnapshot>> = [];
|
|
|
|
|
for (const childOutlet of Object.keys(segmentGroup.children)) {
|
|
|
|
|
const child = segmentGroup.children[childOutlet];
|
2020-11-25 17:27:48 -08:00
|
|
|
// Sort the config so that routes with outlets that match the one being activated appear
|
|
|
|
|
// first, followed by routes for other outlets, which might match if they have an empty path.
|
|
|
|
|
const sortedConfig = config.filter(r => getOutlet(r) === childOutlet);
|
|
|
|
|
sortedConfig.push(...config.filter(r => getOutlet(r) !== childOutlet));
|
|
|
|
|
const outletChildren = this.processSegmentGroup(sortedConfig, child, childOutlet);
|
2020-11-25 17:24:58 -08:00
|
|
|
if (outletChildren === null) {
|
2020-11-25 17:27:48 -08:00
|
|
|
// Configs must match all segment children so because we did not find a match for this
|
|
|
|
|
// outlet, return `null`.
|
2020-11-25 17:24:58 -08:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
children.push(...outletChildren);
|
2020-11-25 17:19:43 -08:00
|
|
|
}
|
2020-11-25 17:27:48 -08:00
|
|
|
// Because we may have matched two outlets to the same empty path segment, we can have multiple
|
|
|
|
|
// activated results for the same outlet. We should merge the children of these results so the
|
|
|
|
|
// final return value is only one `TreeNode` per outlet.
|
|
|
|
|
const mergedChildren = mergeEmptyPathMatches(children);
|
|
|
|
|
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
|
|
|
// This should really never happen - we are only taking the first match for each outlet and
|
|
|
|
|
// merge the empty path matches.
|
|
|
|
|
checkOutletNameUniqueness(mergedChildren);
|
|
|
|
|
}
|
|
|
|
|
sortActivatedRouteSnapshots(mergedChildren);
|
|
|
|
|
return mergedChildren;
|
2016-08-02 15:31:56 -07:00
|
|
|
}
|
2016-06-27 14:00:07 -07:00
|
|
|
|
2016-08-02 15:31:56 -07:00
|
|
|
processSegment(
|
2016-12-02 05:25:53 +08:00
|
|
|
config: Route[], segmentGroup: UrlSegmentGroup, segments: UrlSegment[],
|
2020-11-25 17:24:58 -08:00
|
|
|
outlet: string): TreeNode<ActivatedRouteSnapshot>[]|null {
|
2016-11-12 14:08:58 +01:00
|
|
|
for (const r of config) {
|
2020-11-25 17:24:58 -08:00
|
|
|
const children = this.processSegmentAgainstRoute(r, segmentGroup, segments, outlet);
|
|
|
|
|
if (children !== null) {
|
|
|
|
|
return children;
|
2016-08-02 15:31:56 -07:00
|
|
|
}
|
|
|
|
|
}
|
2016-11-08 13:36:59 -08:00
|
|
|
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
|
2020-11-25 17:24:58 -08:00
|
|
|
return null;
|
2016-11-08 13:36:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
|
|
|
|
|
boolean {
|
|
|
|
|
return segments.length === 0 && !segmentGroup.children[outlet];
|
2016-05-23 16:14:23 -07:00
|
|
|
}
|
2016-06-06 10:15:23 -07:00
|
|
|
|
2016-08-02 15:31:56 -07:00
|
|
|
processSegmentAgainstRoute(
|
2016-12-02 05:25:53 +08:00
|
|
|
route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[],
|
2020-11-25 17:24:58 -08:00
|
|
|
outlet: string): TreeNode<ActivatedRouteSnapshot>[]|null {
|
2020-11-25 17:27:48 -08:00
|
|
|
if (!isImmediateMatch(route, rawSegment, segments, outlet)) return null;
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2017-07-14 14:10:37 -07:00
|
|
|
let snapshot: ActivatedRouteSnapshot;
|
|
|
|
|
let consumedSegments: UrlSegment[] = [];
|
|
|
|
|
let rawSlicedSegments: UrlSegment[] = [];
|
|
|
|
|
|
2016-08-02 15:31:56 -07:00
|
|
|
if (route.path === '**') {
|
2020-04-13 16:40:21 -07:00
|
|
|
const params = segments.length > 0 ? last(segments)!.parameters : {};
|
2017-07-14 14:10:37 -07:00
|
|
|
snapshot = new ActivatedRouteSnapshot(
|
2020-04-13 16:40:21 -07:00
|
|
|
segments, params, Object.freeze({...this.urlTree.queryParams}), this.urlTree.fragment!,
|
2020-11-25 17:27:48 -08:00
|
|
|
getData(route), getOutlet(route), route.component!, route,
|
|
|
|
|
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + segments.length,
|
|
|
|
|
getResolve(route));
|
2017-07-14 14:10:37 -07:00
|
|
|
} else {
|
2020-11-25 17:24:58 -08:00
|
|
|
const result: MatchResult|null = match(rawSegment, route, segments);
|
|
|
|
|
if (result === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2017-07-14 14:10:37 -07:00
|
|
|
consumedSegments = result.consumedSegments;
|
|
|
|
|
rawSlicedSegments = segments.slice(result.lastChild);
|
|
|
|
|
|
|
|
|
|
snapshot = new ActivatedRouteSnapshot(
|
2018-03-08 12:16:01 -08:00
|
|
|
consumedSegments, result.parameters, Object.freeze({...this.urlTree.queryParams}),
|
2020-11-25 17:27:48 -08:00
|
|
|
this.urlTree.fragment!, getData(route), getOutlet(route), route.component!, route,
|
2017-07-14 14:10:37 -07:00
|
|
|
getSourceSegmentGroup(rawSegment),
|
|
|
|
|
getPathIndexShift(rawSegment) + consumedSegments.length, getResolve(route));
|
2016-08-02 15:31:56 -07:00
|
|
|
}
|
|
|
|
|
|
2017-07-14 14:10:37 -07:00
|
|
|
const childConfig: Route[] = getChildConfig(route);
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2018-02-23 10:24:51 +01:00
|
|
|
const {segmentGroup, slicedSegments} = split(
|
|
|
|
|
rawSegment, consumedSegments, rawSlicedSegments, childConfig, this.relativeLinkResolution);
|
2016-07-08 14:23:23 -07:00
|
|
|
|
2016-08-02 15:31:56 -07:00
|
|
|
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
|
2016-10-25 14:33:18 -07:00
|
|
|
const children = this.processChildren(childConfig, segmentGroup);
|
2020-11-25 17:24:58 -08:00
|
|
|
if (children === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2016-08-02 15:31:56 -07:00
|
|
|
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
|
2017-03-29 09:44:04 -07:00
|
|
|
}
|
2016-08-02 15:31:56 -07:00
|
|
|
|
2017-03-29 09:44:04 -07:00
|
|
|
if (childConfig.length === 0 && slicedSegments.length === 0) {
|
2016-08-02 15:31:56 -07:00
|
|
|
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
|
|
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
|
2020-11-25 17:27:48 -08:00
|
|
|
const matchedOnOutlet = getOutlet(route) === outlet;
|
|
|
|
|
// If we matched a config due to empty path match on a different outlet, we need to continue
|
|
|
|
|
// passing the current outlet for the segment rather than switch to PRIMARY.
|
|
|
|
|
// Note that we switch to primary when we have a match because outlet configs look like this:
|
|
|
|
|
// {path: 'a', outlet: 'a', children: [
|
|
|
|
|
// {path: 'b', component: B},
|
|
|
|
|
// {path: 'c', component: C},
|
|
|
|
|
// ]}
|
|
|
|
|
// Notice that the children of the named outlet are configured with the primary outlet
|
|
|
|
|
const children = this.processSegment(
|
|
|
|
|
childConfig, segmentGroup, slicedSegments, matchedOnOutlet ? PRIMARY_OUTLET : outlet);
|
2020-11-25 17:24:58 -08:00
|
|
|
if (children === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
|
2016-05-23 16:14:23 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-02 15:31:56 -07:00
|
|
|
function sortActivatedRouteSnapshots(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
|
|
|
|
|
nodes.sort((a, b) => {
|
|
|
|
|
if (a.value.outlet === PRIMARY_OUTLET) return -1;
|
|
|
|
|
if (b.value.outlet === PRIMARY_OUTLET) return 1;
|
|
|
|
|
return a.value.outlet.localeCompare(b.value.outlet);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-11 08:34:58 -07:00
|
|
|
function getChildConfig(route: Route): Route[] {
|
2016-07-06 11:02:16 -07:00
|
|
|
if (route.children) {
|
|
|
|
|
return route.children;
|
|
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
|
|
|
|
|
if (route.loadChildren) {
|
2020-04-13 16:40:21 -07:00
|
|
|
return route._loadedConfig!.routes;
|
2017-03-29 09:44:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [];
|
2016-07-06 11:02:16 -07:00
|
|
|
}
|
|
|
|
|
|
2017-07-14 14:10:37 -07:00
|
|
|
interface MatchResult {
|
|
|
|
|
consumedSegments: UrlSegment[];
|
|
|
|
|
lastChild: number;
|
|
|
|
|
parameters: any;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-25 17:24:58 -08:00
|
|
|
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): MatchResult|
|
|
|
|
|
null {
|
2016-06-16 14:14:09 -07:00
|
|
|
if (route.path === '') {
|
2016-08-16 13:40:28 -07:00
|
|
|
if (route.pathMatch === 'full' && (segmentGroup.hasChildren() || segments.length > 0)) {
|
2020-11-25 17:24:58 -08:00
|
|
|
return null;
|
2016-06-14 14:55:59 -07:00
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
|
|
|
|
|
return {consumedSegments: [], lastChild: 0, parameters: {}};
|
2016-06-14 14:55:59 -07:00
|
|
|
}
|
2016-05-23 16:14:23 -07:00
|
|
|
|
2016-11-09 15:25:47 -08:00
|
|
|
const matcher = route.matcher || defaultUrlMatcher;
|
|
|
|
|
const res = matcher(segments, segmentGroup, route);
|
2020-11-25 17:24:58 -08:00
|
|
|
if (!res) return null;
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2016-11-09 15:25:47 -08:00
|
|
|
const posParams: {[n: string]: string} = {};
|
2020-04-13 16:40:21 -07:00
|
|
|
forEach(res.posParams!, (v: UrlSegment, k: string) => {
|
|
|
|
|
posParams[k] = v.path;
|
|
|
|
|
});
|
2017-03-01 21:37:28 +01:00
|
|
|
const parameters = res.consumed.length > 0 ?
|
|
|
|
|
{...posParams, ...res.consumed[res.consumed.length - 1].parameters} :
|
|
|
|
|
posParams;
|
2016-06-14 14:55:59 -07:00
|
|
|
|
2016-11-09 15:25:47 -08:00
|
|
|
return {consumedSegments: res.consumed, lastChild: res.consumed.length, parameters};
|
2016-05-23 16:14:23 -07:00
|
|
|
}
|
|
|
|
|
|
2020-11-25 17:27:48 -08:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
* empty path route config and the results need to then be merged.
|
|
|
|
|
*/
|
|
|
|
|
function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
|
|
|
|
|
Array<TreeNode<ActivatedRouteSnapshot>> {
|
|
|
|
|
const result: Array<TreeNode<ActivatedRouteSnapshot>> = [];
|
|
|
|
|
|
|
|
|
|
function hasEmptyConfig(node: TreeNode<ActivatedRouteSnapshot>) {
|
|
|
|
|
const config = node.value.routeConfig;
|
|
|
|
|
return config && config.path === '' && config.redirectTo === undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const node of nodes) {
|
|
|
|
|
if (!hasEmptyConfig(node)) {
|
|
|
|
|
result.push(node);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const duplicateEmptyPathNode =
|
|
|
|
|
result.find(resultNode => node.value.routeConfig === resultNode.value.routeConfig);
|
|
|
|
|
if (duplicateEmptyPathNode !== undefined) {
|
|
|
|
|
duplicateEmptyPathNode.children.push(...node.children);
|
|
|
|
|
} else {
|
|
|
|
|
result.push(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-14 14:55:59 -07:00
|
|
|
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
|
2016-06-15 16:45:19 -07:00
|
|
|
const names: {[k: string]: ActivatedRouteSnapshot} = {};
|
2016-06-14 14:55:59 -07:00
|
|
|
nodes.forEach(n => {
|
2016-11-12 14:08:58 +01:00
|
|
|
const routeWithSameOutletName = names[n.value.outlet];
|
2016-06-14 14:55:59 -07:00
|
|
|
if (routeWithSameOutletName) {
|
2016-06-15 16:45:19 -07:00
|
|
|
const p = routeWithSameOutletName.url.map(s => s.toString()).join('/');
|
2016-06-14 14:55:59 -07:00
|
|
|
const c = n.value.url.map(s => s.toString()).join('/');
|
|
|
|
|
throw new Error(`Two segments cannot have the same outlet name: '${p}' and '${c}'.`);
|
|
|
|
|
}
|
|
|
|
|
names[n.value.outlet] = n.value;
|
|
|
|
|
});
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-07-25 12:15:07 -07:00
|
|
|
function getSourceSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup {
|
|
|
|
|
let s = segmentGroup;
|
2016-06-24 11:17:17 -07:00
|
|
|
while (s._sourceSegment) {
|
|
|
|
|
s = s._sourceSegment;
|
|
|
|
|
}
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-25 12:15:07 -07:00
|
|
|
function getPathIndexShift(segmentGroup: UrlSegmentGroup): number {
|
|
|
|
|
let s = segmentGroup;
|
|
|
|
|
let res = (s._segmentIndexShift ? s._segmentIndexShift : 0);
|
2016-06-24 11:17:17 -07:00
|
|
|
while (s._sourceSegment) {
|
|
|
|
|
s = s._sourceSegment;
|
2016-07-25 12:15:07 -07:00
|
|
|
res += (s._segmentIndexShift ? s._segmentIndexShift : 0);
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
2016-07-18 14:53:13 -07:00
|
|
|
return res - 1;
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function split(
|
2016-07-25 12:15:07 -07:00
|
|
|
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
2020-04-13 16:40:21 -07:00
|
|
|
config: Route[], relativeLinkResolution: 'legacy'|'corrected') {
|
2016-07-25 12:15:07 -07:00
|
|
|
if (slicedSegments.length > 0 &&
|
|
|
|
|
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) {
|
|
|
|
|
const s = new UrlSegmentGroup(
|
2020-04-13 16:40:21 -07:00
|
|
|
consumedSegments,
|
|
|
|
|
createChildrenForEmptyPaths(
|
|
|
|
|
segmentGroup, consumedSegments, config,
|
|
|
|
|
new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
|
2016-07-25 12:15:07 -07:00
|
|
|
s._sourceSegment = segmentGroup;
|
|
|
|
|
s._segmentIndexShift = consumedSegments.length;
|
|
|
|
|
return {segmentGroup: s, slicedSegments: []};
|
2017-03-29 09:44:04 -07:00
|
|
|
}
|
2016-07-25 12:15:07 -07:00
|
|
|
|
2017-03-29 09:44:04 -07:00
|
|
|
if (slicedSegments.length === 0 &&
|
2016-07-25 12:15:07 -07:00
|
|
|
containsEmptyPathMatches(segmentGroup, slicedSegments, config)) {
|
|
|
|
|
const s = new UrlSegmentGroup(
|
2020-04-13 16:40:21 -07:00
|
|
|
segmentGroup.segments,
|
|
|
|
|
addEmptyPathsToChildrenIfNeeded(
|
|
|
|
|
segmentGroup, consumedSegments, slicedSegments, config, segmentGroup.children,
|
|
|
|
|
relativeLinkResolution));
|
2016-07-25 12:15:07 -07:00
|
|
|
s._sourceSegment = segmentGroup;
|
|
|
|
|
s._segmentIndexShift = consumedSegments.length;
|
|
|
|
|
return {segmentGroup: s, slicedSegments};
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
2017-03-29 09:44:04 -07:00
|
|
|
|
|
|
|
|
const s = new UrlSegmentGroup(segmentGroup.segments, segmentGroup.children);
|
|
|
|
|
s._sourceSegment = segmentGroup;
|
|
|
|
|
s._segmentIndexShift = consumedSegments.length;
|
|
|
|
|
return {segmentGroup: s, slicedSegments};
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addEmptyPathsToChildrenIfNeeded(
|
2018-02-23 10:24:51 +01:00
|
|
|
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
|
|
|
|
routes: Route[], children: {[name: string]: UrlSegmentGroup},
|
2020-04-13 16:40:21 -07:00
|
|
|
relativeLinkResolution: 'legacy'|'corrected'): {[name: string]: UrlSegmentGroup} {
|
2016-07-25 12:15:07 -07:00
|
|
|
const res: {[name: string]: UrlSegmentGroup} = {};
|
2016-11-12 14:08:58 +01:00
|
|
|
for (const r of routes) {
|
2016-07-25 12:15:07 -07:00
|
|
|
if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
|
|
|
|
|
const s = new UrlSegmentGroup([], {});
|
|
|
|
|
s._sourceSegment = segmentGroup;
|
2018-02-23 10:24:51 +01:00
|
|
|
if (relativeLinkResolution === 'legacy') {
|
|
|
|
|
s._segmentIndexShift = segmentGroup.segments.length;
|
|
|
|
|
} else {
|
|
|
|
|
s._segmentIndexShift = consumedSegments.length;
|
|
|
|
|
}
|
2016-06-24 11:17:17 -07:00
|
|
|
res[getOutlet(r)] = s;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-03-26 17:15:10 +03:00
|
|
|
return {...children, ...res};
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createChildrenForEmptyPaths(
|
2016-07-25 12:15:07 -07:00
|
|
|
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], routes: Route[],
|
|
|
|
|
primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
|
|
|
|
|
const res: {[name: string]: UrlSegmentGroup} = {};
|
2016-06-24 11:17:17 -07:00
|
|
|
res[PRIMARY_OUTLET] = primarySegment;
|
2016-07-25 12:15:07 -07:00
|
|
|
primarySegment._sourceSegment = segmentGroup;
|
|
|
|
|
primarySegment._segmentIndexShift = consumedSegments.length;
|
2016-06-24 11:17:17 -07:00
|
|
|
|
2016-11-12 14:08:58 +01:00
|
|
|
for (const r of routes) {
|
2016-07-22 18:32:26 -07:00
|
|
|
if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) {
|
2016-07-25 12:15:07 -07:00
|
|
|
const s = new UrlSegmentGroup([], {});
|
|
|
|
|
s._sourceSegment = segmentGroup;
|
|
|
|
|
s._segmentIndexShift = consumedSegments.length;
|
2016-06-24 11:17:17 -07:00
|
|
|
res[getOutlet(r)] = s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function containsEmptyPathMatchesWithNamedOutlets(
|
2016-07-25 12:15:07 -07:00
|
|
|
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
2017-03-29 09:44:04 -07:00
|
|
|
return routes.some(
|
|
|
|
|
r => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET);
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function containsEmptyPathMatches(
|
2016-07-25 12:15:07 -07:00
|
|
|
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
2017-03-29 09:44:04 -07:00
|
|
|
return routes.some(r => emptyPathMatch(segmentGroup, slicedSegments, r));
|
2016-06-24 11:17:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-07-25 12:15:07 -07:00
|
|
|
function emptyPathMatch(
|
|
|
|
|
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
|
2017-03-29 09:44:04 -07:00
|
|
|
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') {
|
2016-06-27 20:10:36 -07:00
|
|
|
return false;
|
2017-03-29 09:44:04 -07:00
|
|
|
}
|
|
|
|
|
|
2016-06-24 11:17:17 -07:00
|
|
|
return r.path === '' && r.redirectTo === undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-27 14:00:07 -07:00
|
|
|
function getData(route: Route): Data {
|
2017-03-29 09:44:04 -07:00
|
|
|
return route.data || {};
|
2016-06-27 14:00:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getResolve(route: Route): ResolveData {
|
2017-03-29 09:44:04 -07:00
|
|
|
return route.resolve || {};
|
2016-08-10 18:21:28 -07:00
|
|
|
}
|
2020-11-25 17:27:48 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
}
|