@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/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 };
|
package/dist/compile.cjs
ADDED
|
@@ -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 };
|
package/dist/compile.js
ADDED
|
@@ -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 };
|
package/dist/expand.cjs
ADDED
|
@@ -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 };
|