diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 923f4b86..a005f783 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -1,11 +1,12 @@ /** * @coreapi * @module url - */ /** for typedoc */ + */ +/** for typedoc */ import { - map, defaults, inherit, identity, unnest, tail, find, Obj, pairs, allTrueR, unnestR, arrayTuples + map, defaults, inherit, identity, unnest, tail, find, Obj, pairs, allTrueR, unnestR, arrayTuples } from "../common/common"; -import { prop, propEq, pattern, eq, is, val } from "../common/hof"; +import { prop, propEq } from "../common/hof"; import { isArray, isString, isDefined } from "../common/predicates"; import { Param, DefType } from "../params/param"; import { ParamTypes } from "../params/paramTypes"; @@ -35,11 +36,16 @@ function quoteRegExp(string: any, param?: any) { const memoizeTo = (obj: Obj, prop: string, fn: Function) => obj[prop] = obj[prop] || fn(); +/** @hidden */ +const splitOnSlash = splitOnDelim('/'); + /** @hidden */ interface UrlMatcherCache { - path: UrlMatcher[]; - parent: UrlMatcher; - pattern: RegExp; + segments?: any[]; + weights?: number[]; + path?: UrlMatcher[]; + parent?: UrlMatcher; + pattern?: RegExp; } /** @@ -98,7 +104,7 @@ export class UrlMatcher { static nameValidator: RegExp = /^\w+([-.]+\w+)*(?:\[\])?$/; /** @hidden */ - private _cache: UrlMatcherCache = { path: [this], parent: null, pattern: null }; + private _cache: UrlMatcherCache = { path: [this] }; /** @hidden */ private _children: UrlMatcher[] = []; /** @hidden */ @@ -474,8 +480,6 @@ export class UrlMatcher { * The comparison function sorts static segments before dynamic ones. */ static compare(a: UrlMatcher, b: UrlMatcher): number { - const splitOnSlash = splitOnDelim('/'); - /** * Turn a UrlMatcher and all its parent matchers into an array * of slash literals '/', string literals, and Param objects @@ -484,27 +488,38 @@ export class UrlMatcher { * var matcher = $umf.compile("/foo").append($umf.compile("/:param")).append($umf.compile("/")).append($umf.compile("tail")); * var result = segments(matcher); // [ '/', 'foo', '/', Param, '/', 'tail' ] * + * Caches the result as `matcher._cache.segments` */ const segments = (matcher: UrlMatcher) => - matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams) - .reduce(unnestR, []) - .reduce(joinNeighborsR, []) - .map(x => isString(x) ? splitOnSlash(x) : x) - .reduce(unnestR, []); - - let aSegments = segments(a), bSegments = segments(b); - // console.table( { aSegments, bSegments }); - - // Sort slashes first, then static strings, the Params - const weight = pattern([ - [eq("/"), val(1)], - [isString, val(2)], - [is(Param), val(3)] - ]); - let pairs = arrayTuples(aSegments.map(weight), bSegments.map(weight)); - // console.table(pairs); - - return pairs.reduce((cmp, weightPair) => cmp !== 0 ? cmp : weightPair[0] - weightPair[1], 0); + matcher._cache.segments = matcher._cache.segments || + matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams) + .reduce(unnestR, []) + .reduce(joinNeighborsR, []) + .map(x => isString(x) ? splitOnSlash(x) : x) + .reduce(unnestR, []); + + /** + * Gets the sort weight for each segment of a UrlMatcher + * + * Caches the result as `matcher._cache.weights` + */ + const weights = (matcher: UrlMatcher) => + matcher._cache.weights = matcher._cache.weights || + segments(matcher).map(segment => { + // Sort slashes first, then static strings, the Params + if (segment === '/') return 1; + if (isString(segment)) return 2; + if (segment instanceof Param) return 3; + }); + + let cmp, i, pairs = arrayTuples(weights(a), weights(b)); + + for (i = 0; i < pairs.length; i++) { + cmp = pairs[i][0] - pairs[i][1]; + if (cmp !== 0) return cmp; + } + + return 0; } } diff --git a/src/url/urlRouter.ts b/src/url/urlRouter.ts index 24e46d22..29b2374f 100644 --- a/src/url/urlRouter.ts +++ b/src/url/urlRouter.ts @@ -26,8 +26,6 @@ function appendBasePath(url: string, isHtml5: boolean, absolute: boolean, baseHr /** @hidden */ const getMatcher = prop("urlMatcher"); - - /** * Default rule priority sorting function. * @@ -71,6 +69,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable { /** @hidden */ private _otherwiseFn: UrlRule; /** @hidden */ interceptDeferred = false; /** @hidden */ private _id = 0; + /** @hidden */ private _sorted = false; /** @hidden */ constructor(router: UIRouter) { @@ -89,6 +88,11 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable { /** @inheritdoc */ sort(compareFn?: (a: UrlRule, b: UrlRule) => number) { this._rules.sort(this._sortFn = compareFn || this._sortFn); + this._sorted = true; + } + + private ensureSorted() { + this._sorted || this.sort(); } /** @@ -97,6 +101,8 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable { * @returns {MatchResult} */ match(url: UrlParts): MatchResult { + this.ensureSorted(); + url = extend({path: '', search: {}, hash: '' }, url); let rules = this.rules(); if (this._otherwiseFn) rules.push(this._otherwiseFn); @@ -247,19 +253,23 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable { if (!UrlRuleFactory.isUrlRule(rule)) throw new Error("invalid rule"); rule.$id = this._id++; rule.priority = rule.priority || 0; + this._rules.push(rule); - this.sort(); + this._sorted = false; + return () => this.removeRule(rule); } /** @inheritdoc */ removeRule(rule): void { removeFrom(this._rules, rule); - this.sort(); } /** @inheritdoc */ - rules(): UrlRule[] { return this._rules.slice(); } + rules(): UrlRule[] { + this.ensureSorted(); + return this._rules.slice(); + } /** @inheritdoc */ otherwise(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef) { @@ -269,7 +279,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable { let handlerFn: UrlRuleHandlerFn = isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler); this._otherwiseFn = this.urlRuleFactory.create(val(true), handlerFn); - this.sort(); + this._sorted = false; }; /** @inheritdoc */