@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 ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2024–2025 Hong Minhee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ <!-- deno-fmt-ignore-file -->
2
+
3
+ @fedify/uri-template: RFC 6570 URI Template implementation
4
+ ===========================================================
5
+
6
+ [![JSR][JSR badge]][JSR]
7
+ [![npm][npm badge]][npm]
8
+
9
+ This package provides [RFC 6570] fully compliant URI template expansion and
10
+ pattern matching library. Supports symmetric matching where
11
+ `expand(match(url))` and `match(expand(vars))` behave predictably.
12
+
13
+ [JSR]: https://jsr.io/@fedify/uri-template
14
+ [JSR badge]: https://jsr.io/badges/@fedify/uri-template
15
+ [npm]: https://www.npmjs.com/package/@fedify/uri-template
16
+ [npm badge]: https://img.shields.io/npm/v/@fedify/uri-template?logo=npm
17
+ [RFC 6570]: https://datatracker.ietf.org/doc/html/rfc6570
18
+
19
+
20
+ Features
21
+ --------
22
+
23
+ - **Full RFC 6570 Level 4 support**: Handles all operators and modifiers
24
+ (explode `*`, prefix `:n`)
25
+ - **Symmetric pattern matching**:
26
+ - `opaque`: byte-for-byte exact round-trips
27
+ - `cooked`: human-readable decoded values
28
+ - `lossless`: preserves both raw and decoded forms
29
+ - **Strict percent-encoding validation**: Prevents malformed sequences
30
+ (`%GZ`, etc.)
31
+ - **Deterministic expansion**: Correctly handles undefined/empty values per
32
+ RFC rules
33
+
34
+
35
+ Installation
36
+ ------------
37
+
38
+ ~~~~ sh
39
+ deno add jsr:@fedify/uri-template # Deno
40
+ npm add @fedify/uri-template # npm
41
+ pnpm add @fedify/uri-template # pnpm
42
+ yarn add @fedify/uri-template # Yarn
43
+ bun add @fedify/uri-template # Bun
44
+ ~~~~
45
+
46
+
47
+ Usage
48
+ -----
49
+
50
+ ~~~~ typescript
51
+ import { compile } from "@fedify/uri-template";
52
+
53
+ const tmpl = compile("/repos{/owner,repo}{?q,lang}");
54
+
55
+ // Expansion
56
+ const url = tmpl.expand({ owner: "foo", repo: "hello/world", q: "a b" });
57
+ // => "/repos/foo/hello%2Fworld?q=a%20b"
58
+
59
+ // Matching
60
+ const result = tmpl.match("/repos/foo/hello%2Fworld?q=a%20b", {
61
+ encoding: "cooked"
62
+ });
63
+ // => { owner: "foo", repo: "hello/world", q: "a b" }
64
+ ~~~~
65
+
66
+ **Matching options:**
67
+
68
+ - `encoding`: `"opaque"` (default, preserves raw) | `"cooked"` (decoded) |
69
+ `"lossless"` (both)
70
+ - `strict`: `true` (default, strict) | `false` (lenient parsing)
71
+
72
+
73
+ Documentation
74
+ -------------
75
+
76
+ For detailed implementation details, see [*specification.md*].
77
+
78
+ [*specification.md*]: ./docs/specification.md
@@ -0,0 +1,30 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+
25
+ Object.defineProperty(exports, '__toESM', {
26
+ enumerable: true,
27
+ get: function () {
28
+ return __toESM;
29
+ }
30
+ });
package/dist/ast.d.cts ADDED
@@ -0,0 +1,39 @@
1
+ //#region src/ast.d.ts
2
+ /**
3
+ * RFC 6570 operator set. Keep in sync with {@link OperatorSpec} in spec.ts.
4
+ * The operator controls separators, first-char prefix, and reserved char handling.
5
+ */
6
+ type Operator = "" | "+" | "#" | "." | "/" | ";" | "?" | "&";
7
+ /**
8
+ * A variable specification inside an expression
9
+ * - `explode` (`*`) and `prefix` (`:n`) modifies are Level 4 features.
10
+ * - Parser guarantees: if `prefix` is present, it is a positive integer.
11
+ */
12
+ interface VarSpec {
13
+ name: string;
14
+ explode: boolean;
15
+ prefix?: number;
16
+ }
17
+ type Node = Literal | Expression;
18
+ interface Literal {
19
+ kind: "literal";
20
+ value: string;
21
+ start?: number;
22
+ end?: number;
23
+ }
24
+ interface Expression {
25
+ kind: "expression";
26
+ op: Operator;
27
+ vars: VarSpec[];
28
+ start?: number;
29
+ end?: number;
30
+ }
31
+ /**
32
+ * Template AST root. Expansion and matching both consume this structure.
33
+ * @note Literals are kept as-is to avoid re-encoding surprises.
34
+ */
35
+ interface TemplateAst {
36
+ nodes: Node[];
37
+ }
38
+ //#endregion
39
+ export { Expression, Literal, Node, Operator, TemplateAst, VarSpec };
package/dist/ast.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ //#region src/ast.d.ts
2
+ /**
3
+ * RFC 6570 operator set. Keep in sync with {@link OperatorSpec} in spec.ts.
4
+ * The operator controls separators, first-char prefix, and reserved char handling.
5
+ */
6
+ type Operator = "" | "+" | "#" | "." | "/" | ";" | "?" | "&";
7
+ /**
8
+ * A variable specification inside an expression
9
+ * - `explode` (`*`) and `prefix` (`:n`) modifies are Level 4 features.
10
+ * - Parser guarantees: if `prefix` is present, it is a positive integer.
11
+ */
12
+ interface VarSpec {
13
+ name: string;
14
+ explode: boolean;
15
+ prefix?: number;
16
+ }
17
+ type Node = Literal | Expression;
18
+ interface Literal {
19
+ kind: "literal";
20
+ value: string;
21
+ start?: number;
22
+ end?: number;
23
+ }
24
+ interface Expression {
25
+ kind: "expression";
26
+ op: Operator;
27
+ vars: VarSpec[];
28
+ start?: number;
29
+ end?: number;
30
+ }
31
+ /**
32
+ * Template AST root. Expansion and matching both consume this structure.
33
+ * @note Literals are kept as-is to avoid re-encoding surprises.
34
+ */
35
+ interface TemplateAst {
36
+ nodes: Node[];
37
+ }
38
+ //#endregion
39
+ export { Expression, Literal, Node, Operator, TemplateAst, VarSpec };
@@ -0,0 +1,30 @@
1
+ const require_expand = require('./expand.cjs');
2
+ const require_match = require('./match.cjs');
3
+ const require_parser = require('./parser.cjs');
4
+
5
+ //#region src/compile.ts
6
+ /**
7
+ * Compile a template string once.
8
+ * Returns a handle with:
9
+ * - `ast()` -> the parsed AST (for diagnostics/introspection)
10
+ * - `expand()` -> RFC 6570 expansion (L1–L4)
11
+ * - `match()` -> symmetric pattern matching
12
+ *
13
+ * Rationale:
14
+ * Compilation isolates parsing cost and allows future VM/bytecode backends
15
+ * to optimize hot routes without changing the API.
16
+ */
17
+ function compile(template) {
18
+ const ast = require_parser.parse(template);
19
+ return {
20
+ ast: () => ast,
21
+ expand: (vars) => require_expand.expand(ast, vars),
22
+ match: (url, mo) => {
23
+ const result = require_match.match(ast, url, mo);
24
+ return result ? { vars: result.vars } : null;
25
+ }
26
+ };
27
+ }
28
+
29
+ //#endregion
30
+ exports.compile = compile;
@@ -0,0 +1,80 @@
1
+ import { TemplateAst } from "./ast.cjs";
2
+ import { Vars } from "./expand.cjs";
3
+ import { EncodingPolicy, MatchOptions } from "./match.cjs";
4
+
5
+ //#region src/compile.d.ts
6
+
7
+ /**
8
+ * Options that control how a compiled template behaves during matching.
9
+ *
10
+ * ### encoding
11
+ * Determines how percent-encoded sequences are handled.
12
+ *
13
+ * ### strict
14
+ * If true (default), malformed percent triplets (e.g. "%GZ" or lone "%")
15
+ * cause matching to fail immediately.
16
+ * Disabling strict mode may allow more lenient parsing but can lead to ambiguity.
17
+ */
18
+ interface CompileOptions {
19
+ encoding?: EncodingPolicy;
20
+ strict?: boolean;
21
+ }
22
+ /**
23
+ * A compiled URI template that can efficiently expand and match URLs.
24
+ *
25
+ * @typeParam V - The type of variables expected by this template
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const t = compile("{+path}/here");
30
+ * const url = t.expand({ path: "/foo/bar" }); // "/foo/bar/here"
31
+ * const match = t.match("/foo/bar/here"); // { vars: { path: "/foo/bar" } }
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const t = compile("/repos{/owner,repo}{?q,lang}");
37
+ * const url = t.expand({ owner: "alice", repo: "hello/world", q: "a b", lang: "en" });
38
+ * // "/repos/alice/hello%2Fworld?q=a%20b&lang=en"
39
+ * ```
40
+ */
41
+ interface CompiledTemplate<V = Record<string, unknown>> {
42
+ /**
43
+ * Get the parsed AST of this template.
44
+ * Useful for diagnostics and introspection.
45
+ *
46
+ * @returns The parsed template AST
47
+ */
48
+ ast(): TemplateAst;
49
+ /**
50
+ * Expand the template with the given variables according to RFC 6570.
51
+ *
52
+ * @param vars - Variables to substitute into the template
53
+ * @returns The expanded URL string
54
+ */
55
+ expand(vars: V & Vars): string;
56
+ /**
57
+ * Match a URL against this template and extract variables.
58
+ *
59
+ * @param url - The URL to match against the template
60
+ * @param opts - Optional matching options
61
+ * @returns An object with extracted variables if matched, null otherwise
62
+ */
63
+ match(url: string, opts?: MatchOptions): null | {
64
+ vars: V;
65
+ };
66
+ }
67
+ /**
68
+ * Compile a template string once.
69
+ * Returns a handle with:
70
+ * - `ast()` -> the parsed AST (for diagnostics/introspection)
71
+ * - `expand()` -> RFC 6570 expansion (L1–L4)
72
+ * - `match()` -> symmetric pattern matching
73
+ *
74
+ * Rationale:
75
+ * Compilation isolates parsing cost and allows future VM/bytecode backends
76
+ * to optimize hot routes without changing the API.
77
+ */
78
+ declare function compile<V = Record<string, unknown>>(template: string): CompiledTemplate<V>;
79
+ //#endregion
80
+ export { CompileOptions, CompiledTemplate, compile };
@@ -0,0 +1,80 @@
1
+ import { TemplateAst } from "./ast.js";
2
+ import { Vars } from "./expand.js";
3
+ import { EncodingPolicy, MatchOptions } from "./match.js";
4
+
5
+ //#region src/compile.d.ts
6
+
7
+ /**
8
+ * Options that control how a compiled template behaves during matching.
9
+ *
10
+ * ### encoding
11
+ * Determines how percent-encoded sequences are handled.
12
+ *
13
+ * ### strict
14
+ * If true (default), malformed percent triplets (e.g. "%GZ" or lone "%")
15
+ * cause matching to fail immediately.
16
+ * Disabling strict mode may allow more lenient parsing but can lead to ambiguity.
17
+ */
18
+ interface CompileOptions {
19
+ encoding?: EncodingPolicy;
20
+ strict?: boolean;
21
+ }
22
+ /**
23
+ * A compiled URI template that can efficiently expand and match URLs.
24
+ *
25
+ * @typeParam V - The type of variables expected by this template
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const t = compile("{+path}/here");
30
+ * const url = t.expand({ path: "/foo/bar" }); // "/foo/bar/here"
31
+ * const match = t.match("/foo/bar/here"); // { vars: { path: "/foo/bar" } }
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const t = compile("/repos{/owner,repo}{?q,lang}");
37
+ * const url = t.expand({ owner: "alice", repo: "hello/world", q: "a b", lang: "en" });
38
+ * // "/repos/alice/hello%2Fworld?q=a%20b&lang=en"
39
+ * ```
40
+ */
41
+ interface CompiledTemplate<V = Record<string, unknown>> {
42
+ /**
43
+ * Get the parsed AST of this template.
44
+ * Useful for diagnostics and introspection.
45
+ *
46
+ * @returns The parsed template AST
47
+ */
48
+ ast(): TemplateAst;
49
+ /**
50
+ * Expand the template with the given variables according to RFC 6570.
51
+ *
52
+ * @param vars - Variables to substitute into the template
53
+ * @returns The expanded URL string
54
+ */
55
+ expand(vars: V & Vars): string;
56
+ /**
57
+ * Match a URL against this template and extract variables.
58
+ *
59
+ * @param url - The URL to match against the template
60
+ * @param opts - Optional matching options
61
+ * @returns An object with extracted variables if matched, null otherwise
62
+ */
63
+ match(url: string, opts?: MatchOptions): null | {
64
+ vars: V;
65
+ };
66
+ }
67
+ /**
68
+ * Compile a template string once.
69
+ * Returns a handle with:
70
+ * - `ast()` -> the parsed AST (for diagnostics/introspection)
71
+ * - `expand()` -> RFC 6570 expansion (L1–L4)
72
+ * - `match()` -> symmetric pattern matching
73
+ *
74
+ * Rationale:
75
+ * Compilation isolates parsing cost and allows future VM/bytecode backends
76
+ * to optimize hot routes without changing the API.
77
+ */
78
+ declare function compile<V = Record<string, unknown>>(template: string): CompiledTemplate<V>;
79
+ //#endregion
80
+ export { CompileOptions, CompiledTemplate, compile };
@@ -0,0 +1,30 @@
1
+ import { expand } from "./expand.js";
2
+ import { match } from "./match.js";
3
+ import { parse } from "./parser.js";
4
+
5
+ //#region src/compile.ts
6
+ /**
7
+ * Compile a template string once.
8
+ * Returns a handle with:
9
+ * - `ast()` -> the parsed AST (for diagnostics/introspection)
10
+ * - `expand()` -> RFC 6570 expansion (L1–L4)
11
+ * - `match()` -> symmetric pattern matching
12
+ *
13
+ * Rationale:
14
+ * Compilation isolates parsing cost and allows future VM/bytecode backends
15
+ * to optimize hot routes without changing the API.
16
+ */
17
+ function compile(template) {
18
+ const ast = parse(template);
19
+ return {
20
+ ast: () => ast,
21
+ expand: (vars) => expand(ast, vars),
22
+ match: (url, mo) => {
23
+ const result = match(ast, url, mo);
24
+ return result ? { vars: result.vars } : null;
25
+ }
26
+ };
27
+ }
28
+
29
+ //#endregion
30
+ export { compile };
package/dist/error.cjs ADDED
@@ -0,0 +1,29 @@
1
+
2
+ //#region src/error.ts
3
+ /**
4
+ * Error thrown when parsing an invalid RFC 6570 URI template.
5
+ */
6
+ var ParseError = class ParseError extends Error {
7
+ /**
8
+ * The error name, always "RFC6570ParseError".
9
+ */
10
+ name = "RFC6570ParseError";
11
+ /**
12
+ * The index in the template string where the error occurred.
13
+ */
14
+ index;
15
+ /**
16
+ * Create a new ParseError.
17
+ *
18
+ * @param message - The error message
19
+ * @param index - The index in the template string where the error occurred
20
+ */
21
+ constructor(message, index) {
22
+ super(message);
23
+ this.index = index;
24
+ Object.setPrototypeOf(this, ParseError.prototype);
25
+ }
26
+ };
27
+
28
+ //#endregion
29
+ exports.ParseError = ParseError;
package/dist/error.js ADDED
@@ -0,0 +1,28 @@
1
+ //#region src/error.ts
2
+ /**
3
+ * Error thrown when parsing an invalid RFC 6570 URI template.
4
+ */
5
+ var ParseError = class ParseError extends Error {
6
+ /**
7
+ * The error name, always "RFC6570ParseError".
8
+ */
9
+ name = "RFC6570ParseError";
10
+ /**
11
+ * The index in the template string where the error occurred.
12
+ */
13
+ index;
14
+ /**
15
+ * Create a new ParseError.
16
+ *
17
+ * @param message - The error message
18
+ * @param index - The index in the template string where the error occurred
19
+ */
20
+ constructor(message, index) {
21
+ super(message);
22
+ this.index = index;
23
+ Object.setPrototypeOf(this, ParseError.prototype);
24
+ }
25
+ };
26
+
27
+ //#endregion
28
+ export { ParseError };
@@ -0,0 +1,94 @@
1
+ const require_spec = require('./spec.cjs');
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 = require_spec.OP[op];
9
+ const enc = (str) => require_spec.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 = require_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
+ exports.expand = expand;
@@ -0,0 +1,72 @@
1
+ import { TemplateAst } from "./ast.cjs";
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 };