Skip to content

Commit e7aa173

Browse files
committed
Convert to TypeScript
1 parent c922360 commit e7aa173

16 files changed

+528
-313
lines changed

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
lib
1+
lib/grammar.js
2+
dist/*
23
node_modules
3-
index.js
44
npm-debug.log

.npmignore

-2
This file was deleted.

Makefile

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
.PHONY: all test publish clean
22

3-
all: index.js ./lib/classes.js ./lib/encoders.js
3+
all: lib/grammar.js dist/index.js dist/classes.js
44

5-
index.js: index.pegjs
6-
@node_modules/.bin/pegjs index.pegjs
5+
dist/%.js: lib/%.ts
6+
@npx tsc
77

8-
lib/%.js: src/%.coffee
9-
@mkdir -p lib
10-
@node_modules/.bin/coffee -p -c $< > $@
8+
lib/grammar.js: lib/grammar.pegjs
9+
@npx pegjs lib/grammar.pegjs ./lib/grammar.js
1110

1211
test: all
13-
@coffee ./test/index.coffee
12+
@node ./test/index.js
1413

1514
publish: test
1615
@npm publish
1716

1817
clean:
19-
@rm -r lib
20-
@rm index.js
18+
@rm -fr dist

index.d.ts

-34
This file was deleted.

lib/classes.ts

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import pctEncode from "pct-encode";
2+
3+
const encoders = {
4+
U: pctEncode(/[^\w~.-]/g),
5+
"U+R": pctEncode(/[^\w.~:\/\?#\[\]@!\$&'()*+,;=%-]|%(?!\d\d)/g),
6+
};
7+
8+
export interface Param {
9+
name: string;
10+
cut?: number;
11+
explode?: "*";
12+
extended?: string;
13+
}
14+
15+
export type Operator = "/" | ";" | "." | "?" | "&" | "+" | "#" | "";
16+
export type Variables = Record<string, unknown>;
17+
18+
export class Template {
19+
public prefix: string;
20+
public expressions: SimpleExpression[];
21+
22+
constructor(pieces: (string | SimpleExpression)[]) {
23+
/*
24+
:param pieces: An array of strings and expressions in the order they appear in the template.
25+
*/
26+
27+
this.expressions = [];
28+
this.prefix =
29+
"string" === typeof pieces[0] ? (pieces.shift() as string) : "";
30+
pieces.forEach((p, i) => {
31+
switch (typeof p) {
32+
case "object":
33+
return (this.expressions[i++] = p);
34+
case "string":
35+
return (this.expressions[i - 1].suffix = p);
36+
}
37+
});
38+
}
39+
40+
expand(vars: Variables) {
41+
return (
42+
this.prefix +
43+
this.expressions
44+
.map(function (expr) {
45+
return expr.expand(vars);
46+
})
47+
.join("")
48+
);
49+
}
50+
51+
toString() {
52+
return this.prefix + this.expressions.join("");
53+
}
54+
55+
toJSON() {
56+
return this.toString();
57+
}
58+
}
59+
60+
export function expression(
61+
op: Operator,
62+
params: Param[] = []
63+
): SimpleExpression {
64+
switch (op) {
65+
case "":
66+
return new SimpleExpression(params);
67+
case "+":
68+
return new ReservedExpression(params);
69+
case "#":
70+
return new FragmentExpression(params);
71+
case ".":
72+
return new LabelExpression(params);
73+
case "/":
74+
return new PathSegmentExpression(params);
75+
case ";":
76+
return new PathParamExpression(params);
77+
case "?":
78+
return new FormStartExpression(params);
79+
case "&":
80+
return new FormContinuationExpression(params);
81+
}
82+
}
83+
84+
class SimpleExpression {
85+
params: Param[];
86+
encode: (s: string) => string = encoders.U;
87+
suffix: string = "";
88+
first = "";
89+
sep = ",";
90+
named = false;
91+
empty = "";
92+
93+
constructor(variables: Param[]) {
94+
this.params = variables;
95+
}
96+
97+
expand(vars: Variables) {
98+
const defined = definedPairs(this.params, vars);
99+
const expanded = defined
100+
.map(([param, value]) => this.expandValue(param, value))
101+
.join(this.sep);
102+
if (expanded) {
103+
return this.first + expanded + this.suffix;
104+
} else {
105+
if (this.empty && defined.length) {
106+
return this.empty + this.suffix;
107+
} else {
108+
return this.suffix;
109+
}
110+
}
111+
}
112+
113+
/*
114+
Return the expanded string form of `pair`.
115+
*/
116+
expandValue(param: Param, value: unknown): string {
117+
if (param.explode) {
118+
if (Array.isArray(value)) {
119+
return this.explodeArray(param, value);
120+
} else if (typeof value === "object") {
121+
return this.explodeObject(param, value as object);
122+
} else {
123+
return this.stringifySingle(param, value);
124+
}
125+
} else {
126+
return this.stringifySingle(param, value);
127+
}
128+
}
129+
130+
/*
131+
Encode a single value as a string
132+
*/
133+
stringifySingle(param: Param, value: unknown) {
134+
if (Array.isArray(value)) {
135+
if (param.cut) {
136+
throw new Error(
137+
"Prefixed Values do not support lists. Check " + param.name
138+
);
139+
}
140+
return value.map(this.encode).join(",");
141+
} else if (typeof value === "object") {
142+
if (value == null) {
143+
return "";
144+
}
145+
if (param.cut) {
146+
throw new Error(
147+
"Prefixed Values do not support maps. Check " + param.name
148+
);
149+
}
150+
return Object.entries(value)
151+
.map((entry) => entry.map(this.encode).join(","))
152+
.join(",");
153+
} else {
154+
let s = (value as string).toString();
155+
if (param.cut) {
156+
s = s.substring(0, param.cut);
157+
}
158+
return this.encode(s);
159+
}
160+
}
161+
162+
explodeArray(_param: Param, array: string[]) {
163+
return array.map(this.encode).join(this.sep);
164+
}
165+
166+
explodeObject(_param: Param, object: object) {
167+
const pairs: string[] = [];
168+
Object.entries(object).forEach(([k, v]) => {
169+
k = this.encode(k);
170+
if (Array.isArray(v)) {
171+
v.forEach((item) => {
172+
pairs.push(`${k}=${this.encode(item)}`);
173+
});
174+
} else {
175+
pairs.push(`${k}=${this.encode(v)}`);
176+
}
177+
});
178+
return pairs.join(this.sep);
179+
}
180+
181+
toString() {
182+
const params = this.params.map((p) => `${p.name}${p.explode}`).join(",");
183+
return "{" + this.first + params + "}" + this.suffix;
184+
}
185+
186+
toJSON() {
187+
return this.toString();
188+
}
189+
}
190+
191+
class NamedExpression extends SimpleExpression {
192+
override stringifySingle(param: Param, value: unknown) {
193+
value = super.stringifySingle(param, value);
194+
value = value ? "=" + value : this.empty;
195+
return "" + param.name + value;
196+
}
197+
198+
override explodeArray(param: Param, array: string[]) {
199+
var _this = this;
200+
return array
201+
.map(function (v) {
202+
return "" + param.name + "=" + _this.encode(v);
203+
})
204+
.join(this.sep);
205+
}
206+
}
207+
208+
class ReservedExpression extends SimpleExpression {
209+
override encode = encoders["U+R"];
210+
211+
override toString() {
212+
return "{+" + super.toString().substring(1);
213+
}
214+
}
215+
216+
class FragmentExpression extends SimpleExpression {
217+
override first = "#";
218+
override empty = "#";
219+
override encode = encoders["U+R"];
220+
}
221+
222+
class LabelExpression extends SimpleExpression {
223+
override first = ".";
224+
override sep = ".";
225+
override empty = ".";
226+
}
227+
228+
class PathSegmentExpression extends SimpleExpression {
229+
override first = "/";
230+
override sep = "/";
231+
}
232+
233+
class PathParamExpression extends NamedExpression {
234+
override first = ";";
235+
override sep = ";";
236+
}
237+
238+
class FormStartExpression extends NamedExpression {
239+
override first = "?";
240+
override sep = "&";
241+
override empty = "=";
242+
}
243+
244+
class FormContinuationExpression extends FormStartExpression {
245+
override first = "&";
246+
}
247+
248+
export type {
249+
SimpleExpression,
250+
NamedExpression,
251+
ReservedExpression,
252+
FragmentExpression,
253+
LabelExpression,
254+
PathSegmentExpression,
255+
PathParamExpression,
256+
FormStartExpression,
257+
FormContinuationExpression,
258+
};
259+
260+
/* Return an array of `[param, value]` arrays where `value` is a defined, non-empty value from`vars` */
261+
function definedPairs(params: Param[], vars: Variables): [Param, unknown][] {
262+
return params
263+
.map((p) => [p, vars[p.name]] as [Param, unknown])
264+
.filter(([_p, v]) => {
265+
switch (typeof v) {
266+
case "undefined":
267+
return false;
268+
case "object":
269+
if (v == null) {
270+
return false;
271+
}
272+
if (Array.isArray(v)) {
273+
return v.length > 0;
274+
}
275+
return Object.values(v).some((vv) => vv != null);
276+
default:
277+
return true;
278+
}
279+
});
280+
}

index.pegjs lib/grammar.pegjs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
var cls = require('./lib/classes')
2+
var cls = require('./classes')
33
var Template = cls.Template
44
var expression = cls.expression
55
}
@@ -10,7 +10,7 @@ expression
1010
= '{' op:op params:paramList '}' { return expression(op, params) }
1111

1212
op
13-
= [/;:.?&+#] / ''
13+
= [/;.?&+#] / ''
1414

1515
pathExpression
1616
= "{/"

lib/index.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as grammar from "./grammar.js";
2+
3+
export type StartRule =
4+
| "uriTemplate"
5+
| "expression"
6+
| "op"
7+
| "pathExpression"
8+
| "paramList"
9+
| "param"
10+
| "cut"
11+
| "listMarker"
12+
| "substr"
13+
| "nonexpression"
14+
| "extension";
15+
16+
import type { Template } from "./classes";
17+
18+
export function parse(input: string, startRule?: StartRule): Template {
19+
return grammar.parse(input, startRule);
20+
}
21+
22+
export type {
23+
Template,
24+
SimpleExpression as TemplateExpression,
25+
Param as TemplateExpressionParam,
26+
Variables as Var,
27+
} from "./classes";

0 commit comments

Comments
 (0)