@fedify/uri-template 2.0.0-pr.475.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +78 -0
- package/dist/_virtual/rolldown_runtime.cjs +30 -0
- package/dist/ast.d.cts +39 -0
- package/dist/ast.d.ts +39 -0
- package/dist/compile.cjs +30 -0
- package/dist/compile.d.cts +80 -0
- package/dist/compile.d.ts +80 -0
- package/dist/compile.js +30 -0
- package/dist/error.cjs +29 -0
- package/dist/error.js +28 -0
- package/dist/expand.cjs +94 -0
- package/dist/expand.d.cts +72 -0
- package/dist/expand.d.ts +72 -0
- package/dist/expand.js +94 -0
- package/dist/index.cjs +9 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/match.cjs +180 -0
- package/dist/match.d.cts +92 -0
- package/dist/match.d.ts +92 -0
- package/dist/match.js +180 -0
- package/dist/match.spec.cjs +109 -0
- package/dist/match.spec.d.cts +1 -0
- package/dist/match.spec.d.ts +1 -0
- package/dist/match.spec.js +108 -0
- package/dist/parser.cjs +102 -0
- package/dist/parser.d.cts +35 -0
- package/dist/parser.d.ts +35 -0
- package/dist/parser.js +102 -0
- package/dist/rfc6570.spec.cjs +419 -0
- package/dist/rfc6570.spec.d.cts +1 -0
- package/dist/rfc6570.spec.d.ts +1 -0
- package/dist/rfc6570.spec.js +418 -0
- package/dist/spec.cjs +112 -0
- package/dist/spec.js +108 -0
- package/package.json +60 -0
package/dist/expand.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TemplateAst } from "./ast.js";
|
|
2
|
+
|
|
3
|
+
//#region src/expand.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A scalar value that can be used in template expansion.
|
|
7
|
+
*/
|
|
8
|
+
type Scalar = string | number | boolean;
|
|
9
|
+
/**
|
|
10
|
+
* A list of scalar values that can be used in template expansion.
|
|
11
|
+
*/
|
|
12
|
+
type List = (Scalar | undefined)[];
|
|
13
|
+
/**
|
|
14
|
+
* A map-like object with scalar values.
|
|
15
|
+
*/
|
|
16
|
+
type MapLike = Record<string, Scalar | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Valid value types for template variables.
|
|
19
|
+
*/
|
|
20
|
+
type VarsValue = Scalar | List | MapLike | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A collection of variables for template expansion.
|
|
23
|
+
* Maps variable names to their values.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const vars: Vars = {
|
|
28
|
+
* var: "value",
|
|
29
|
+
* list: ["red", "green", "blue"],
|
|
30
|
+
* keys: { semi: ";", dot: ".", comma: "," }
|
|
31
|
+
* };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
type Vars = Record<string, VarsValue>;
|
|
35
|
+
/**
|
|
36
|
+
* Expand a parsed template with variables according to RFC 6570 (Level 1-4).
|
|
37
|
+
*
|
|
38
|
+
* @param ast - The parsed template AST to expand
|
|
39
|
+
* @param vars - Variables to substitute into the template
|
|
40
|
+
* @returns The expanded URL string
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* - Idempotent percent encoding (existing `%XX` kept)
|
|
44
|
+
* - Operator-specific empty/undefined rules:
|
|
45
|
+
* - ";" empty -> `nameOnly` (";x")
|
|
46
|
+
* - "?" "&" empty -> "key=" ("?x=")
|
|
47
|
+
* - `undefined` -> `omit` (all operators)
|
|
48
|
+
* - Label "." emits the dot even if empty ("X{.y}" with y="" -> "X.")
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* import { parse } from "./parser.ts";
|
|
53
|
+
* import { expand } from "./expand.ts";
|
|
54
|
+
*
|
|
55
|
+
* const ast = parse("{+x,hello,y}");
|
|
56
|
+
* const url = expand(ast, { x: "1024", hello: "Hello World!", y: "768" });
|
|
57
|
+
* // Returns: "1024,Hello%20World!,768"
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { parse } from "./parser.ts";
|
|
63
|
+
* import { expand } from "./expand.ts";
|
|
64
|
+
*
|
|
65
|
+
* const ast = parse("{+path}/here");
|
|
66
|
+
* const url = expand(ast, { path: "/foo/bar" });
|
|
67
|
+
* // Returns: "/foo/bar/here"
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
declare function expand(ast: TemplateAst, vars: Vars): string;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { Vars, expand };
|
package/dist/expand.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { OP, encodeComponentIdempotent } from "./spec.js";
|
|
2
|
+
|
|
3
|
+
//#region src/expand.ts
|
|
4
|
+
function emitNamed(spec, name, raw) {
|
|
5
|
+
return spec.named ? raw === "" && spec.ifEmpty === "empty" ? `${name}${spec.kvSep}` : `${name}${spec.kvSep}${raw}` : raw;
|
|
6
|
+
}
|
|
7
|
+
function expandVar(op, v, value) {
|
|
8
|
+
const spec = OP[op];
|
|
9
|
+
const enc = (str) => encodeComponentIdempotent(str, spec.allowReserved, spec.reservedSet);
|
|
10
|
+
if (value === void 0 || value === null) return [];
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
const items = value.filter((x) => x !== void 0).map((x) => enc(String(x)));
|
|
13
|
+
if (items.length === 0) {
|
|
14
|
+
if (spec.first === ".") return [""];
|
|
15
|
+
if (spec.named && spec.ifEmpty === "nameOnly") return [v.name];
|
|
16
|
+
if (spec.named && spec.ifEmpty === "empty") return [`${v.name}${spec.kvSep}`];
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
if (v.explode) return items.map((it) => emitNamed(spec, v.name, it));
|
|
20
|
+
return [emitNamed(spec, v.name, items.join(","))];
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "object" && value && !Array.isArray(value)) {
|
|
23
|
+
const entries = Object.entries(value).filter(([, vv]) => vv !== void 0);
|
|
24
|
+
if (entries.length === 0) {
|
|
25
|
+
if (spec.named && spec.ifEmpty === "nameOnly") return [v.name];
|
|
26
|
+
if (spec.named && spec.ifEmpty === "empty") return [`${v.name}${spec.kvSep}`];
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (v.explode) return entries.map(([k, vv]) => `${enc(k)}${spec.kvSep}${enc(String(vv))}`);
|
|
30
|
+
const joined = entries.map(([k, vv]) => `${enc(k)},${enc(String(vv))}`).join(",");
|
|
31
|
+
return [emitNamed(spec, v.name, joined)];
|
|
32
|
+
}
|
|
33
|
+
let s = String(value);
|
|
34
|
+
if (v.prefix !== void 0) s = s.slice(0, v.prefix);
|
|
35
|
+
const e = enc(s);
|
|
36
|
+
if (e.length === 0) {
|
|
37
|
+
if (spec.first === ".") return [""];
|
|
38
|
+
if (spec.named && spec.ifEmpty === "nameOnly") return [v.name];
|
|
39
|
+
if (spec.named && spec.ifEmpty === "empty") return [`${v.name}${spec.kvSep}`];
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return [emitNamed(spec, v.name, e)];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Expand a parsed template with variables according to RFC 6570 (Level 1-4).
|
|
46
|
+
*
|
|
47
|
+
* @param ast - The parsed template AST to expand
|
|
48
|
+
* @param vars - Variables to substitute into the template
|
|
49
|
+
* @returns The expanded URL string
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* - Idempotent percent encoding (existing `%XX` kept)
|
|
53
|
+
* - Operator-specific empty/undefined rules:
|
|
54
|
+
* - ";" empty -> `nameOnly` (";x")
|
|
55
|
+
* - "?" "&" empty -> "key=" ("?x=")
|
|
56
|
+
* - `undefined` -> `omit` (all operators)
|
|
57
|
+
* - Label "." emits the dot even if empty ("X{.y}" with y="" -> "X.")
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { parse } from "./parser.ts";
|
|
62
|
+
* import { expand } from "./expand.ts";
|
|
63
|
+
*
|
|
64
|
+
* const ast = parse("{+x,hello,y}");
|
|
65
|
+
* const url = expand(ast, { x: "1024", hello: "Hello World!", y: "768" });
|
|
66
|
+
* // Returns: "1024,Hello%20World!,768"
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* import { parse } from "./parser.ts";
|
|
72
|
+
* import { expand } from "./expand.ts";
|
|
73
|
+
*
|
|
74
|
+
* const ast = parse("{+path}/here");
|
|
75
|
+
* const url = expand(ast, { path: "/foo/bar" });
|
|
76
|
+
* // Returns: "/foo/bar/here"
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
function expand(ast, vars) {
|
|
80
|
+
let out = "";
|
|
81
|
+
for (const node of ast.nodes) if (node.kind === "literal") out += node.value;
|
|
82
|
+
else {
|
|
83
|
+
const spec = OP[node.op];
|
|
84
|
+
const pieces = [];
|
|
85
|
+
for (const v of node.vars) pieces.push(...expandVar(node.op, v, vars[v.name]));
|
|
86
|
+
if (pieces.length === 0) continue;
|
|
87
|
+
if (spec.first) out += spec.first;
|
|
88
|
+
out += pieces.join(spec.itemSep);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { expand };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const require_expand = require('./expand.cjs');
|
|
2
|
+
const require_match = require('./match.cjs');
|
|
3
|
+
const require_parser = require('./parser.cjs');
|
|
4
|
+
const require_compile = require('./compile.cjs');
|
|
5
|
+
|
|
6
|
+
exports.compile = require_compile.compile;
|
|
7
|
+
exports.expand = require_expand.expand;
|
|
8
|
+
exports.match = require_match.match;
|
|
9
|
+
exports.parse = require_parser.parse;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Expression, Literal, Node, Operator, TemplateAst, VarSpec } from "./ast.cjs";
|
|
2
|
+
import { Vars, expand } from "./expand.cjs";
|
|
3
|
+
import { EncodingPolicy, MatchOptions, match } from "./match.cjs";
|
|
4
|
+
import { CompileOptions, CompiledTemplate, compile } from "./compile.cjs";
|
|
5
|
+
import { parse } from "./parser.cjs";
|
|
6
|
+
export { CompileOptions, CompiledTemplate, EncodingPolicy, Expression, Literal, MatchOptions, Node, Operator, TemplateAst, VarSpec, Vars, compile, expand, match, parse };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Expression, Literal, Node, Operator, TemplateAst, VarSpec } from "./ast.js";
|
|
2
|
+
import { Vars, expand } from "./expand.js";
|
|
3
|
+
import { EncodingPolicy, MatchOptions, match } from "./match.js";
|
|
4
|
+
import { CompileOptions, CompiledTemplate, compile } from "./compile.js";
|
|
5
|
+
import { parse } from "./parser.js";
|
|
6
|
+
export { CompileOptions, CompiledTemplate, EncodingPolicy, Expression, Literal, MatchOptions, Node, Operator, TemplateAst, VarSpec, Vars, compile, expand, match, parse };
|
package/dist/index.js
ADDED
package/dist/match.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const require_spec = require('./spec.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/match.ts
|
|
4
|
+
function decodeAccordingToPolicy(s, policy) {
|
|
5
|
+
if (policy === "opaque") return s;
|
|
6
|
+
if (policy === "cooked") return require_spec.strictPercentDecode(s);
|
|
7
|
+
return {
|
|
8
|
+
raw: s,
|
|
9
|
+
decoded: require_spec.strictPercentDecode(s)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* consume a percent triplet or a single char; used to advance safely
|
|
14
|
+
*
|
|
15
|
+
* Percent triplet handling policies:
|
|
16
|
+
* - When advancing, treat "%XX" as a single atom to avoid slicing inside a byte.
|
|
17
|
+
* - In strict mode, a bare '%' or bad hex after '%' fails the match.
|
|
18
|
+
*/
|
|
19
|
+
function advanceOne(url, i) {
|
|
20
|
+
if (i < url.length && url[i] === "%" && require_spec.looksLikePctTriplet(url, i)) return i + 3;
|
|
21
|
+
return i + 1;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Match a URL string against a compiled template and extract variables.
|
|
25
|
+
*
|
|
26
|
+
* @param ast - The parsed template AST to match against
|
|
27
|
+
* @param url - The URL string to match
|
|
28
|
+
* @param opts - Optional matching options
|
|
29
|
+
* @returns An object with extracted variables if matched, null otherwise
|
|
30
|
+
*
|
|
31
|
+
* Encoding policies:
|
|
32
|
+
* - "opaque" -> return raw percent-encoded slices (byte-for-byte round-trip)
|
|
33
|
+
* - "cooked" -> decode %XX exactly once
|
|
34
|
+
* - "lossless"-> { raw, decoded } pair
|
|
35
|
+
*
|
|
36
|
+
* Strictness:
|
|
37
|
+
* - strict=true rejects bad percent triplets (e.g. "%GZ"), preventing
|
|
38
|
+
* ambiguous or lossy normalization early.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { parse } from "./parser.ts";
|
|
43
|
+
* import { match } from "./match.ts";
|
|
44
|
+
*
|
|
45
|
+
* const ast = parse("/repos{/owner,repo}{?q,lang}");
|
|
46
|
+
* const result = match(ast, "/repos/alice/hello%2Fworld?q=a%20b&lang=en", { encoding: "opaque" });
|
|
47
|
+
* // Returns: { vars: { owner: "alice", repo: "hello%2Fworld", q: "a%20b", lang: "en" } }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* import { parse } from "./parser.ts";
|
|
53
|
+
* import { match } from "./match.ts";
|
|
54
|
+
*
|
|
55
|
+
* const ast = parse("/files{/path}");
|
|
56
|
+
* const result = match(ast, "/files/a%2Fb", { encoding: "cooked" });
|
|
57
|
+
* // Returns: { vars: { path: "a/b" } }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
function match(ast, url, opts) {
|
|
61
|
+
const policy = opts?.encoding ?? "opaque";
|
|
62
|
+
const strict = opts?.strict ?? true;
|
|
63
|
+
let i = 0;
|
|
64
|
+
const varsOut = {};
|
|
65
|
+
const readLiteral = (lit) => {
|
|
66
|
+
if (url.slice(i, i + lit.length) !== lit) return false;
|
|
67
|
+
i += lit.length;
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
const nextIs = (s) => url.slice(i, i + s.length) === s;
|
|
71
|
+
for (const node of ast.nodes) {
|
|
72
|
+
if (node.kind === "literal") {
|
|
73
|
+
if (!readLiteral(node.value)) return null;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const spec = require_spec.OP[node.op];
|
|
77
|
+
if (spec.first) {
|
|
78
|
+
if (!readLiteral(spec.first)) return null;
|
|
79
|
+
}
|
|
80
|
+
const takeUntil = (stopPred) => {
|
|
81
|
+
const start = i;
|
|
82
|
+
while (i < url.length && !stopPred(i)) {
|
|
83
|
+
if (strict && url[i] === "%" && !require_spec.looksLikePctTriplet(url, i)) return "";
|
|
84
|
+
i = advanceOne(url, i);
|
|
85
|
+
}
|
|
86
|
+
return url.slice(start, i);
|
|
87
|
+
};
|
|
88
|
+
const emitScalar = (v, raw) => {
|
|
89
|
+
varsOut[v.name] = decodeAccordingToPolicy(raw, policy);
|
|
90
|
+
};
|
|
91
|
+
const splitItems = (raw) => {
|
|
92
|
+
if (raw.length === 0) return [""];
|
|
93
|
+
const sep = spec.itemSep;
|
|
94
|
+
const out = [];
|
|
95
|
+
let start = 0;
|
|
96
|
+
for (let j = 0; j <= raw.length;) {
|
|
97
|
+
if (j === raw.length || raw.slice(j, j + sep.length) === sep) {
|
|
98
|
+
out.push(raw.slice(start, j));
|
|
99
|
+
j += sep.length;
|
|
100
|
+
start = j;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
j = advanceOne(raw, j);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
};
|
|
107
|
+
const captureOneVar = (v, last, nextLiteral) => {
|
|
108
|
+
const stopPred = (j) => {
|
|
109
|
+
if (nextLiteral && url.slice(j, j + nextLiteral.length) === nextLiteral) return true;
|
|
110
|
+
if (!last && spec.itemSep && url.slice(j, j + spec.itemSep.length) === spec.itemSep) return true;
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
const raw = takeUntil(stopPred);
|
|
114
|
+
if (raw === "" && strict && i < url.length && url[i] !== spec.itemSep && nextLiteral === null) return false;
|
|
115
|
+
if (v.explode) {
|
|
116
|
+
const parts = splitItems(raw);
|
|
117
|
+
if (spec.named) varsOut[v.name] = parseExplodeNamedParts(v, parts, spec, policy);
|
|
118
|
+
else varsOut[v.name] = parts.map((p) => decodeAccordingToPolicy(p, policy));
|
|
119
|
+
} else if (spec.named) {
|
|
120
|
+
const [ok, val] = captureNamedNonExplode(v, raw, spec, policy);
|
|
121
|
+
if (!ok) return false;
|
|
122
|
+
varsOut[v.name] = val;
|
|
123
|
+
} else emitScalar(v, raw);
|
|
124
|
+
if (!last && spec.itemSep && nextIs(spec.itemSep)) i += spec.itemSep.length;
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
for (let idx = 0; idx < node.vars.length; idx++) {
|
|
128
|
+
const v = node.vars[idx];
|
|
129
|
+
const last = idx === node.vars.length - 1;
|
|
130
|
+
let nextLiteral = null;
|
|
131
|
+
const nodeIndex = ast.nodes.indexOf(node);
|
|
132
|
+
for (let k = nodeIndex + 1; k < ast.nodes.length; k++) {
|
|
133
|
+
const n = ast.nodes[k];
|
|
134
|
+
if (n.kind === "literal" && n.value.length > 0) {
|
|
135
|
+
nextLiteral = n.value;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
if (n.kind === "expression" && require_spec.OP[n.op].first) {
|
|
139
|
+
nextLiteral = require_spec.OP[n.op].first;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!captureOneVar(v, last, nextLiteral)) return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { vars: varsOut };
|
|
147
|
+
}
|
|
148
|
+
function captureNamedNonExplode(v, raw, spec, policy) {
|
|
149
|
+
const eq = raw.indexOf(spec.kvSep);
|
|
150
|
+
if (eq === -1) {
|
|
151
|
+
if (spec.first === ";") {
|
|
152
|
+
if (raw === v.name) return [true, decodeAccordingToPolicy("", policy)];
|
|
153
|
+
}
|
|
154
|
+
return [false, null];
|
|
155
|
+
}
|
|
156
|
+
const lhs = raw.slice(0, eq), rhs = raw.slice(eq + spec.kvSep.length);
|
|
157
|
+
if (lhs !== v.name) return [false, null];
|
|
158
|
+
return [true, decodeAccordingToPolicy(rhs, policy)];
|
|
159
|
+
}
|
|
160
|
+
function parseExplodeNamedParts(v, parts, spec, policy) {
|
|
161
|
+
const allVarName = parts.every((p) => p.startsWith(v.name + spec.kvSep));
|
|
162
|
+
if (allVarName) return parts.map((p) => {
|
|
163
|
+
const rhs = p.slice((v.name + spec.kvSep).length);
|
|
164
|
+
return decodeAccordingToPolicy(rhs, policy);
|
|
165
|
+
});
|
|
166
|
+
const obj = {};
|
|
167
|
+
for (const p of parts) {
|
|
168
|
+
const i = p.indexOf(spec.kvSep);
|
|
169
|
+
if (i < 0) {
|
|
170
|
+
obj[decodeAccordingToPolicy(p, policy)] = "";
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const k = p.slice(0, i), val = p.slice(i + spec.kvSep.length);
|
|
174
|
+
obj[decodeAccordingToPolicy(k, policy)] = decodeAccordingToPolicy(val, policy);
|
|
175
|
+
}
|
|
176
|
+
return obj;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
exports.match = match;
|
package/dist/match.d.cts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { TemplateAst } from "./ast.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/match.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encoding policy for variable values when matching URLs.
|
|
7
|
+
*
|
|
8
|
+
* Three modes are supported:
|
|
9
|
+
* - `"opaque"` (default):
|
|
10
|
+
* - Treats `%XX` as opaque atoms.
|
|
11
|
+
* - No decoding is applied; raw bytes are preserved.
|
|
12
|
+
* - Guarantees byte-for-byte symmetry:
|
|
13
|
+
* `expand(match(url)) === url`.
|
|
14
|
+
* - Background: added to solve `uri-template-router#11` where
|
|
15
|
+
* sequences like "%30#" lost information.
|
|
16
|
+
*
|
|
17
|
+
* - `"cooked"`:
|
|
18
|
+
* - Decodes valid `%XX` sequences exactly once.
|
|
19
|
+
* - More convenient for application logic where you want
|
|
20
|
+
* human-readable values (`"a%20b"` → `"a b"`).
|
|
21
|
+
* - Guarantees semantic symmetry:
|
|
22
|
+
* `match(expand(vars)) === vars`.
|
|
23
|
+
* - Note: Name chosen to contrast with `"opaque"`: think of
|
|
24
|
+
* “cooked strings” (escaped string literals) in programming languages.
|
|
25
|
+
*
|
|
26
|
+
* - `"lossless"`:
|
|
27
|
+
* - Returns both raw and decoded forms:
|
|
28
|
+
* `{ raw: "a%2Fb", decoded: "a/b" }`.
|
|
29
|
+
* - Useful if you need to display the original URL while
|
|
30
|
+
* still working with decoded values.
|
|
31
|
+
*/
|
|
32
|
+
type EncodingPolicy = "opaque" | "cooked" | "lossless";
|
|
33
|
+
/**
|
|
34
|
+
* Options for matching URLs against templates.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* These options control how percent-encoded sequences are handled during matching.
|
|
38
|
+
*/
|
|
39
|
+
interface MatchOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The encoding policy for variable values (default: "opaque").
|
|
42
|
+
*/
|
|
43
|
+
encoding?: EncodingPolicy;
|
|
44
|
+
/**
|
|
45
|
+
* If true (default), malformed percent triplets cause matching to fail.
|
|
46
|
+
* If false, allows more lenient parsing but may lead to ambiguity.
|
|
47
|
+
*/
|
|
48
|
+
strict?: boolean;
|
|
49
|
+
}
|
|
50
|
+
type VarsOut = Record<string, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* Match a URL string against a compiled template and extract variables.
|
|
53
|
+
*
|
|
54
|
+
* @param ast - The parsed template AST to match against
|
|
55
|
+
* @param url - The URL string to match
|
|
56
|
+
* @param opts - Optional matching options
|
|
57
|
+
* @returns An object with extracted variables if matched, null otherwise
|
|
58
|
+
*
|
|
59
|
+
* Encoding policies:
|
|
60
|
+
* - "opaque" -> return raw percent-encoded slices (byte-for-byte round-trip)
|
|
61
|
+
* - "cooked" -> decode %XX exactly once
|
|
62
|
+
* - "lossless"-> { raw, decoded } pair
|
|
63
|
+
*
|
|
64
|
+
* Strictness:
|
|
65
|
+
* - strict=true rejects bad percent triplets (e.g. "%GZ"), preventing
|
|
66
|
+
* ambiguous or lossy normalization early.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* import { parse } from "./parser.ts";
|
|
71
|
+
* import { match } from "./match.ts";
|
|
72
|
+
*
|
|
73
|
+
* const ast = parse("/repos{/owner,repo}{?q,lang}");
|
|
74
|
+
* const result = match(ast, "/repos/alice/hello%2Fworld?q=a%20b&lang=en", { encoding: "opaque" });
|
|
75
|
+
* // Returns: { vars: { owner: "alice", repo: "hello%2Fworld", q: "a%20b", lang: "en" } }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* import { parse } from "./parser.ts";
|
|
81
|
+
* import { match } from "./match.ts";
|
|
82
|
+
*
|
|
83
|
+
* const ast = parse("/files{/path}");
|
|
84
|
+
* const result = match(ast, "/files/a%2Fb", { encoding: "cooked" });
|
|
85
|
+
* // Returns: { vars: { path: "a/b" } }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function match(ast: TemplateAst, url: string, opts?: MatchOptions): null | {
|
|
89
|
+
vars: VarsOut;
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { EncodingPolicy, MatchOptions, match };
|
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { TemplateAst } from "./ast.js";
|
|
2
|
+
|
|
3
|
+
//#region src/match.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encoding policy for variable values when matching URLs.
|
|
7
|
+
*
|
|
8
|
+
* Three modes are supported:
|
|
9
|
+
* - `"opaque"` (default):
|
|
10
|
+
* - Treats `%XX` as opaque atoms.
|
|
11
|
+
* - No decoding is applied; raw bytes are preserved.
|
|
12
|
+
* - Guarantees byte-for-byte symmetry:
|
|
13
|
+
* `expand(match(url)) === url`.
|
|
14
|
+
* - Background: added to solve `uri-template-router#11` where
|
|
15
|
+
* sequences like "%30#" lost information.
|
|
16
|
+
*
|
|
17
|
+
* - `"cooked"`:
|
|
18
|
+
* - Decodes valid `%XX` sequences exactly once.
|
|
19
|
+
* - More convenient for application logic where you want
|
|
20
|
+
* human-readable values (`"a%20b"` → `"a b"`).
|
|
21
|
+
* - Guarantees semantic symmetry:
|
|
22
|
+
* `match(expand(vars)) === vars`.
|
|
23
|
+
* - Note: Name chosen to contrast with `"opaque"`: think of
|
|
24
|
+
* “cooked strings” (escaped string literals) in programming languages.
|
|
25
|
+
*
|
|
26
|
+
* - `"lossless"`:
|
|
27
|
+
* - Returns both raw and decoded forms:
|
|
28
|
+
* `{ raw: "a%2Fb", decoded: "a/b" }`.
|
|
29
|
+
* - Useful if you need to display the original URL while
|
|
30
|
+
* still working with decoded values.
|
|
31
|
+
*/
|
|
32
|
+
type EncodingPolicy = "opaque" | "cooked" | "lossless";
|
|
33
|
+
/**
|
|
34
|
+
* Options for matching URLs against templates.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* These options control how percent-encoded sequences are handled during matching.
|
|
38
|
+
*/
|
|
39
|
+
interface MatchOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The encoding policy for variable values (default: "opaque").
|
|
42
|
+
*/
|
|
43
|
+
encoding?: EncodingPolicy;
|
|
44
|
+
/**
|
|
45
|
+
* If true (default), malformed percent triplets cause matching to fail.
|
|
46
|
+
* If false, allows more lenient parsing but may lead to ambiguity.
|
|
47
|
+
*/
|
|
48
|
+
strict?: boolean;
|
|
49
|
+
}
|
|
50
|
+
type VarsOut = Record<string, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* Match a URL string against a compiled template and extract variables.
|
|
53
|
+
*
|
|
54
|
+
* @param ast - The parsed template AST to match against
|
|
55
|
+
* @param url - The URL string to match
|
|
56
|
+
* @param opts - Optional matching options
|
|
57
|
+
* @returns An object with extracted variables if matched, null otherwise
|
|
58
|
+
*
|
|
59
|
+
* Encoding policies:
|
|
60
|
+
* - "opaque" -> return raw percent-encoded slices (byte-for-byte round-trip)
|
|
61
|
+
* - "cooked" -> decode %XX exactly once
|
|
62
|
+
* - "lossless"-> { raw, decoded } pair
|
|
63
|
+
*
|
|
64
|
+
* Strictness:
|
|
65
|
+
* - strict=true rejects bad percent triplets (e.g. "%GZ"), preventing
|
|
66
|
+
* ambiguous or lossy normalization early.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* import { parse } from "./parser.ts";
|
|
71
|
+
* import { match } from "./match.ts";
|
|
72
|
+
*
|
|
73
|
+
* const ast = parse("/repos{/owner,repo}{?q,lang}");
|
|
74
|
+
* const result = match(ast, "/repos/alice/hello%2Fworld?q=a%20b&lang=en", { encoding: "opaque" });
|
|
75
|
+
* // Returns: { vars: { owner: "alice", repo: "hello%2Fworld", q: "a%20b", lang: "en" } }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* import { parse } from "./parser.ts";
|
|
81
|
+
* import { match } from "./match.ts";
|
|
82
|
+
*
|
|
83
|
+
* const ast = parse("/files{/path}");
|
|
84
|
+
* const result = match(ast, "/files/a%2Fb", { encoding: "cooked" });
|
|
85
|
+
* // Returns: { vars: { path: "a/b" } }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function match(ast: TemplateAst, url: string, opts?: MatchOptions): null | {
|
|
89
|
+
vars: VarsOut;
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { EncodingPolicy, MatchOptions, match };
|