UNPKG

@aurelia/route-recognizer

Version:

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![CircleCI](https://circleci.com/

664 lines (636 loc) • 19.6 kB
import { isArray as t } from "@aurelia/kernel"; function isIHandler(e) { return e != null && typeof e === "object" && t(e.path); } class Parameter { constructor(t, e, s, n) { this.name = t; this.isOptional = e; this.isStar = s; this.pattern = n; } satisfiesPattern(t) { if (this.pattern === null) return true; this.pattern.lastIndex = 0; return this.pattern.test(t); } } class ConfigurableRoute { constructor(t, e, s) { this.path = t; this.caseSensitive = e; this.handler = s; } } class Endpoint { get residualEndpoint() { return this.t; } set residualEndpoint(t) { if (this.t !== null) throw new Error("Residual endpoint is already set"); this.t = t; } constructor(t, e) { this.route = t; this.params = e; this.t = null; } equalsOrResidual(t) { return t != null && this === t || this.t === t; } } class RecognizedRoute { constructor(t, s, n) { this.endpoint = t; this.i = null; const i = Object.create(null); for (const t in n) { const e = n[t]; i[t] = e != null ? decodeURIComponent(e) : e; } this.params = Object.freeze(i); const r = this.params[e]; if ((r?.length ?? 0) > 0 && s.endsWith(r)) { s = s.slice(0, -r.length); } s = s.startsWith("/") ? s.slice(1) : s; s = s.endsWith("/") ? s.slice(0, -1) : s; this.path = s; } u() { let t = this.i; if (t != null) return t; t = this.path; if (t.length !== 0) return this.i = t; const e = this.endpoint.route.handler; t = isIHandler(e) ? e.path.find(t => t.length > 0) ?? null : null; if (t !== null) return this.i = t; throw new Error(`No non-empty path found`); } } class Candidate { constructor(t, e, s, n) { this.chars = t; this.states = e; this.skippedStates = s; this.result = n; this.childRoutes = []; this.recognizedResult = null; this.isConstrained = false; this.satisfiesConstraints = null; this.head = e[e.length - 1]; this.endpoint = this.head?.endpoint; } advance(t) { const {chars: e, states: s, skippedStates: n, result: i} = this; let r = null; let l = 0; const o = s[s.length - 1]; function $process(u, h) { if (u.isMatch(t)) { if (++l === 1) { r = u; } else { i.add(new Candidate(e.concat(t), s.concat(u), h === null ? n : n.concat(h), i)); } } if (o.segment === null && u.isOptional && u.nextStates !== null) { if (u.nextStates.length > 1) { throw createError(`${u.nextStates.length} nextStates`); } const t = u.nextStates[0]; if (!t.isSeparator) { throw createError(`Not a separator`); } if (t.nextStates !== null) { for (const e of t.nextStates) { $process(e, u); } } } } if (o.isDynamic) { $process(o, null); } if (o.nextStates !== null) { for (const t of o.nextStates) { $process(t, null); } } if (r !== null) { s.push(this.head = r); e.push(t); this.isConstrained = this.isConstrained || r.isDynamic && r.segment.isConstrained; if (r.endpoint !== null) { this.endpoint = r.endpoint; } } if (l === 0) { i.remove(this); } } h() { function collectSkippedStates(t, e) { const s = e.nextStates; if (s !== null) { if (s.length === 1 && s[0].segment === null) { collectSkippedStates(t, s[0]); } else { for (const e of s) { if (e.isOptional && e.endpoint !== null) { t.push(e); if (e.nextStates !== null) { for (const s of e.nextStates) { collectSkippedStates(t, s); } } break; } } } } } collectSkippedStates(this.skippedStates, this.head); if (!this.isConstrained) return true; this.R(); return this.satisfiesConstraints; } R() { let t = this.recognizedResult; if (t != null) return t; const {states: e, chars: s} = this; this.satisfiesConstraints = true; const n = []; let i = null; for (let t = e.length - 1, r = 0; t >= r; --t) { const r = e[t]; const l = r.endpoint !== null && (i === null || i.isDifferentEndpoint(r.endpoint) && i.isFulfilled()); if (l) { if (i !== null) { n.unshift(i.toRecognizedRoute()); } i = new EndpointRequirement(r); } if (i === null) continue; this.satisfiesConstraints = this.satisfiesConstraints && i.consume(r, s[t], e[t - 1]); } if (i !== null && i.isDifferentRecognizedRoute(n[0])) { n.unshift(i.toRecognizedRoute()); } if (n.length > 1 && n[0].path === "") n.shift(); if (this.satisfiesConstraints) { this.recognizedResult = t = [ n, this.head ]; } return t; } compareTo(t) { const e = this.states; const s = t.states; for (let t = 0, n = 0, i = Math.max(e.length, s.length); t < i; ++t) { let i = e[t]; if (i === void 0) { return 1; } let r = s[n]; if (r === void 0) { return -1; } let l = i.segment; let o = r.segment; if (l === null) { if (o === null) { ++n; continue; } if ((i = e[++t]) === void 0) { return 1; } l = i.segment; } else if (o === null) { if ((r = s[++n]) === void 0) { return -1; } o = r.segment; } if (l.kind < o.kind) { return 1; } if (l.kind > o.kind) { return -1; } ++n; } const n = this.skippedStates; const i = t.skippedStates; const r = n.length; const l = i.length; if (r < l) { return 1; } if (r > l) { return -1; } for (let t = 0; t < r; ++t) { const e = n[t]; const s = i[t]; if (e.length < s.length) { return 1; } if (e.length > s.length) { return -1; } } return 0; } } class EndpointRequirement { constructor(t) { this.$ = Object.create(null); this.C = []; this.P = ""; if (t.endpoint == null) throw new Error("Invalid state; endpoint is required."); const e = this.j = t.endpoint; for (const t of e.params) { this.$[t.name] = [ void 0, !t.isOptional, false ]; } for (const t of e.route.path.split("/").filter(t => isNotEmpty(t) && !t.startsWith(":") && !t.startsWith("*"))) { this.C.push([ t, "", false ]); } } consume(t, e, s) { this.P = e + this.P; if (t.isDynamic) { const n = t.segment; const i = n.name; const r = this.$[i]; if (r[0] === void 0) { r[0] = e; if (r[1]) r[2] = true; } else { r[0] = e + r[0]; } const l = t.isConstrained && !Object.is(s?.segment, n); if (!l) return true; return t.satisfiesConstraint(r[0]); } if (this.C.length > 0 && e !== "" && e !== "/") { const s = this.C.findLast(t => !t[2]); if (s == null) throw createError(`Unexpected state`); s[1] = e + s[1]; s[2] = t.segment.caseSensitive ? s[0] === s[1] : s[0].toLowerCase() === s[1].toLowerCase(); } return true; } isFulfilled() { for (const [, t, e] of Object.values(this.$)) { if (t && !e) return false; } return this.C.length === 0 || this.C[0][2]; } toRecognizedRoute() { const t = Object.create(null); for (const e in this.$) { t[e] = this.$[e][0]; } return new RecognizedRoute(this.j, this.P, t); } isDifferentRecognizedRoute(t) { return this.isDifferentEndpoint(t?.endpoint); } isDifferentEndpoint(t) { return t == null || this.j.route.handler !== t.route.handler; } } function hasEndpoint(t) { return t.head.endpoint !== null; } function compareChains(t, e) { return t.compareTo(e); } class RecognizeResult { get isEmpty() { return this.candidates.length === 0; } constructor(t) { this.candidates = []; this.candidates = [ new Candidate([ "" ], [ t ], [], this) ]; } getSolution() { const t = this.candidates.filter(t => hasEndpoint(t) && t.h()); if (t.length === 0) { return null; } t.sort(compareChains); return t[0]; } add(t) { this.candidates.push(t); } remove(t) { this.candidates.splice(this.candidates.indexOf(t), 1); } advance(t) { const e = this.candidates.slice(); for (const s of e) { s.advance(t); } } } const e = "$$residue"; const s = /^:(?<name>[^?\s{}]+)(?:\{\{(?<constraint>.+)\}\})?(?<optional>\?)?$/g; class RouteRecognizer { constructor() { this.rootState = new State(null, null, ""); this.cache = new Map; this.endpointLookup = new Map; } add(t, s = false, n = null) { let i; let r; if (t instanceof Array) { for (const l of t) { r = this.$add(l, false, n); i = r.params; if (!s || (i[i.length - 1]?.isStar ?? false)) continue; r.residualEndpoint = this.$add({ ...l, path: `${l.path}/*${e}` }, true, n); } } else { r = this.$add(t, false, n); i = r.params; if (s && !(i[i.length - 1]?.isStar ?? false)) { r.residualEndpoint = this.$add({ ...t, path: `${t.path}/*${e}` }, true, n); } } this.cache.clear(); } $add(t, n, i) { const r = i === null ? t.path : `${i}/${t.path}`; const l = this.endpointLookup; if (l.has(r)) throw createError(`Cannot add duplicate path '${r}'.`); const o = new ConfigurableRoute(t.path, t.caseSensitive === true, t.handler); const u = r === "" ? [ "" ] : r.split("/").filter(isNotEmpty); const h = []; let c = this.rootState; const a = i === null ? 0 : i.split("/").filter(isNotEmpty).length; const f = u.length; for (let t = 0; t < f; ++t) { const i = u[t]; c = c.append(null, "/"); switch (i.charAt(0)) { case ":": { s.lastIndex = 0; const n = s.exec(i); const {name: r, optional: l} = n?.groups ?? {}; const o = l === "?"; if (r === e) throw new Error(`Invalid parameter name; usage of the reserved parameter name '${e}' is used.`); const u = n?.groups?.constraint; const f = u != null ? new RegExp(u) : null; if (t >= a) { h.push(new Parameter(r, o, false, f)); } c = new DynamicSegment(r, o, f).appendTo(c); break; } case "*": { const s = i.slice(1); let r; if (s === e) { if (!n) throw new Error(`Invalid parameter name; usage of the reserved parameter name '${e}' is used.`); r = 1; } else { r = 2; } if (t >= a) { h.push(new Parameter(s, true, true, null)); } c = new StarSegment(s, r).appendTo(c); break; } default: { c = new StaticSegment(i, o.caseSensitive).appendTo(c); break; } } } const d = new Endpoint(o, h); c.setEndpoint(d); l.set(r, d); return d; } recognize(t, e = null) { const s = this.cache; let n; if (e == null) { n = s.get(t); if (n !== void 0) return n?.[0] ?? null; s.set(t, n = this.$recognize(t, this.rootState)); return n?.[0] ?? null; } const i = e.map(t => t.u()).join("/"); const r = combinePaths(i, t); n = s.get(r); if (n !== void 0) return n === null ? null : n[0].slice(e.length); let l = s.get(i); if (l === null) return null; if (l === void 0) { l = this.$recognize(i, this.rootState); if (l === null) return null; } const o = this.$recognize(t, l[1]); if (o === null) return null; let u = o[0]; if (u.length > 1 && u[0].path === "") u = u.slice(1); s.set(r, [ e.concat(u), o[1] ]); return u; function combinePaths(t, e) { if (t === "") return e; if (e === "") return t; if (t.endsWith("/")) return e.startsWith("/") ? t + e.slice(1) : t + e; return e.startsWith("/") ? t + e : `${t}/${e}`; } } $recognize(t, e) { if (!t.startsWith("/")) { t = `/${t}`; } if (t.length > 1 && t.endsWith("/")) { t = t.slice(0, -1); } const s = new RecognizeResult(e); for (let e = 0, n = t.length; e < n; ++e) { const n = t.charAt(e); s.advance(n); if (s.isEmpty) { return null; } } const n = s.getSolution(); if (n === null) { return null; } return n.R(); } getEndpoint(t) { return this.endpointLookup.get(t) ?? null; } } class State { constructor(t, e, s) { this.prevState = t; this.segment = e; this.value = s; this.nextStates = null; this.endpoint = null; this.isConstrained = false; switch (e?.kind) { case 3: this.length = t.length + 1; this.isSeparator = false; this.isDynamic = true; this.isOptional = e.optional; this.isConstrained = e.isConstrained; break; case 2: case 1: this.length = t.length + 1; this.isSeparator = false; this.isDynamic = true; this.isOptional = false; break; case 4: this.length = t.length + 1; this.isSeparator = false; this.isDynamic = false; this.isOptional = false; break; case undefined: this.length = t === null ? 0 : t.length; this.isSeparator = true; this.isDynamic = false; this.isOptional = false; break; } } append(t, e) { let s; let n = this.nextStates; if (n === null) { s = void 0; n = this.nextStates = []; } else if (t === null) { s = n.find(t => t.value === e); } else { s = n.find(e => e.segment?.equals(t)); } if (s === void 0) { n.push(s = new State(this, t, e)); } return s; } setEndpoint(t) { if (this.endpoint !== null) { throw createError(`Cannot add ambiguous route. The pattern '${t.route.path}' clashes with '${this.endpoint.route.path}'`); } this.endpoint = t; if (this.isOptional) { this.prevState.setEndpoint(t); if (this.prevState.isSeparator && this.prevState.prevState !== null) { this.prevState.prevState.setEndpoint(t); } } } isMatch(t) { const e = this.segment; switch (e?.kind) { case 3: return !this.value.includes(t); case 2: case 1: return true; case 4: case undefined: return this.value.includes(t); } } satisfiesConstraint(t) { return this.isConstrained ? this.segment.satisfiesPattern(t) : true; } } function isNotEmpty(t) { return t.length > 0; } class StaticSegment { get kind() { return 4; } constructor(t, e) { this.value = t; this.caseSensitive = e; } appendTo(t) { const {value: e, value: {length: s}} = this; if (this.caseSensitive) { for (let n = 0; n < s; ++n) { t = t.append(this, e.charAt(n)); } } else { for (let n = 0; n < s; ++n) { const s = e.charAt(n); t = t.append(this, s.toUpperCase() + s.toLowerCase()); } } return t; } equals(t) { return t.kind === 4 && t.caseSensitive === this.caseSensitive && t.value === this.value; } } class DynamicSegment { get kind() { return 3; } constructor(t, e, s) { this.name = t; this.optional = e; this.pattern = s; if (s === void 0) throw new Error(`Pattern is undefined`); this.isConstrained = s !== null; } appendTo(t) { t = t.append(this, "/"); return t; } equals(t) { return t.kind === 3 && t.optional === this.optional && t.name === this.name; } satisfiesPattern(t) { if (this.pattern === null) return true; this.pattern.lastIndex = 0; return this.pattern.test(t); } } class StarSegment { constructor(t, e) { this.name = t; this.kind = e; } appendTo(t) { t = t.append(this, ""); return t; } equals(t) { return (t.kind === 2 || t.kind === 1) && t.name === this.name; } } const createError = t => new Error(t); export { ConfigurableRoute, Endpoint, Parameter, e as RESIDUE, RecognizedRoute, RouteRecognizer }; //# sourceMappingURL=index.mjs.map