Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/uriTemplate.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
export interface IUriTemplateVariable {
7
readonly explodable: boolean;
8
readonly name: string;
9
readonly optional: boolean;
10
readonly prefixLength?: number;
11
readonly repeatable: boolean;
12
}
13
14
interface IUriTemplateComponent {
15
readonly expression: string;
16
readonly operator: string;
17
readonly variables: readonly IUriTemplateVariable[];
18
}
19
20
/**
21
* Represents an RFC 6570 URI Template.
22
*/
23
export class UriTemplate {
24
/**
25
* The parsed template components (expressions).
26
*/
27
public readonly components: ReadonlyArray<IUriTemplateComponent | string>;
28
29
private constructor(
30
public readonly template: string,
31
components: ReadonlyArray<IUriTemplateComponent | string>
32
) {
33
this.template = template;
34
this.components = components;
35
}
36
37
/**
38
* Parses a URI template string into a UriTemplate instance.
39
*/
40
public static parse(template: string): UriTemplate {
41
const components: Array<IUriTemplateComponent | string> = [];
42
const regex = /\{([^{}]+)\}/g;
43
let match: RegExpExecArray | null;
44
let lastPos = 0;
45
while ((match = regex.exec(template))) {
46
const [expression, inner] = match;
47
components.push(template.slice(lastPos, match.index));
48
lastPos = match.index + expression.length;
49
50
// Handle escaped braces: treat '{{' and '}}' as literals, not expressions
51
if (template[match.index - 1] === '{' || template[lastPos] === '}') {
52
components.push(inner);
53
continue;
54
}
55
56
let operator = '';
57
let rest = inner;
58
if (rest.length > 0 && UriTemplate._isOperator(rest[0])) {
59
operator = rest[0];
60
rest = rest.slice(1);
61
}
62
const variables = rest.split(',').map((v): IUriTemplateVariable => {
63
let name = v;
64
let explodable = false;
65
let repeatable = false;
66
let prefixLength: number | undefined = undefined;
67
let optional = false;
68
if (name.endsWith('*')) {
69
explodable = true;
70
repeatable = true;
71
name = name.slice(0, -1);
72
}
73
const prefixMatch = name.match(/^(.*?):(\d+)$/);
74
if (prefixMatch) {
75
name = prefixMatch[1];
76
prefixLength = parseInt(prefixMatch[2], 10);
77
}
78
if (name.endsWith('?')) {
79
optional = true;
80
name = name.slice(0, -1);
81
}
82
return { explodable, name, optional, prefixLength, repeatable };
83
});
84
components.push({ expression, operator, variables });
85
}
86
components.push(template.slice(lastPos));
87
88
return new UriTemplate(template, components);
89
}
90
91
private static _operators = ['+', '#', '.', '/', ';', '?', '&'] as const;
92
private static _isOperator(ch: string): boolean {
93
return (UriTemplate._operators as readonly string[]).includes(ch);
94
}
95
96
/**
97
* Resolves the template with the given variables.
98
*/
99
public resolve(variables: Record<string, unknown>): string {
100
let result = '';
101
for (const comp of this.components) {
102
if (typeof comp === 'string') {
103
result += comp;
104
} else {
105
result += this._expand(comp, variables);
106
}
107
}
108
return result;
109
}
110
111
private _expand(comp: IUriTemplateComponent, variables: Record<string, unknown>): string {
112
const op = comp.operator;
113
const varSpecs = comp.variables;
114
if (varSpecs.length === 0) {
115
return comp.expression;
116
}
117
const vals: string[] = [];
118
const isNamed = op === ';' || op === '?' || op === '&';
119
const isReserved = op === '+' || op === '#';
120
const isFragment = op === '#';
121
const isLabel = op === '.';
122
const isPath = op === '/';
123
const isForm = op === '?';
124
const isFormCont = op === '&';
125
const isParam = op === ';';
126
127
let prefix = '';
128
if (op === '+') { prefix = ''; }
129
else if (op === '#') { prefix = '#'; }
130
else if (op === '.') { prefix = '.'; }
131
else if (op === '/') { prefix = ''; }
132
else if (op === ';') { prefix = ';'; }
133
else if (op === '?') { prefix = '?'; }
134
else if (op === '&') { prefix = '&'; }
135
136
for (const v of varSpecs) {
137
const value = variables[v.name];
138
const defined = Object.prototype.hasOwnProperty.call(variables, v.name);
139
if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) {
140
if (isParam) {
141
if (defined && (value === null || value === undefined)) {
142
vals.push(v.name);
143
}
144
continue;
145
}
146
if (isForm || isFormCont) {
147
if (defined) {
148
vals.push(UriTemplate._formPair(v.name, '', isNamed));
149
}
150
continue;
151
}
152
continue;
153
}
154
if (typeof value === 'object' && !Array.isArray(value)) {
155
if (v.explodable) {
156
const pairs: string[] = [];
157
for (const k in value) {
158
if (Object.prototype.hasOwnProperty.call(value, k)) {
159
const thisVal = String((value as any)[k]);
160
if (isParam) {
161
pairs.push(k + '=' + thisVal);
162
} else if (isForm || isFormCont) {
163
pairs.push(k + '=' + thisVal);
164
} else if (isLabel) {
165
pairs.push(k + '=' + thisVal);
166
} else if (isPath) {
167
pairs.push('/' + k + '=' + UriTemplate._encode(thisVal, isReserved));
168
} else {
169
pairs.push(k + '=' + UriTemplate._encode(thisVal, isReserved));
170
}
171
}
172
}
173
if (isLabel) {
174
vals.push(pairs.join('.'));
175
} else if (isPath) {
176
vals.push(pairs.join(''));
177
} else if (isParam) {
178
vals.push(pairs.join(';'));
179
} else if (isForm || isFormCont) {
180
vals.push(pairs.join('&'));
181
} else {
182
vals.push(pairs.join(','));
183
}
184
} else {
185
// Not explodable: join as k1,v1,k2,v2,... and assign to variable name
186
const pairs: string[] = [];
187
for (const k in value) {
188
if (Object.prototype.hasOwnProperty.call(value, k)) {
189
pairs.push(k);
190
pairs.push(String((value as any)[k]));
191
}
192
}
193
// For label, param, form, join as keys=semi,;,dot,.,comma,, (no encoding of , or ;)
194
const joined = pairs.join(',');
195
if (isLabel) {
196
vals.push(joined);
197
} else if (isParam || isForm || isFormCont) {
198
vals.push(v.name + '=' + joined);
199
} else {
200
vals.push(joined);
201
}
202
}
203
continue;
204
}
205
if (Array.isArray(value)) {
206
if (v.explodable) {
207
if (isLabel) {
208
vals.push(value.join('.'));
209
} else if (isPath) {
210
vals.push(value.map(x => '/' + UriTemplate._encode(x, isReserved)).join(''));
211
} else if (isParam) {
212
vals.push(value.map(x => v.name + '=' + String(x)).join(';'));
213
} else if (isForm || isFormCont) {
214
vals.push(value.map(x => v.name + '=' + String(x)).join('&'));
215
} else {
216
vals.push(value.map(x => UriTemplate._encode(x, isReserved)).join(','));
217
}
218
} else {
219
if (isLabel) {
220
vals.push(value.join(','));
221
} else if (isParam) {
222
vals.push(v.name + '=' + value.join(','));
223
} else if (isForm || isFormCont) {
224
vals.push(v.name + '=' + value.join(','));
225
} else {
226
vals.push(value.map(x => UriTemplate._encode(x, isReserved)).join(','));
227
}
228
}
229
continue;
230
}
231
let str = String(value);
232
if (v.prefixLength !== undefined) {
233
str = str.substring(0, v.prefixLength);
234
}
235
// For simple expansion, encode ! as well (not reserved)
236
// Only + and # are reserved
237
const enc = UriTemplate._encode(str, op === '+' || op === '#');
238
if (isParam) {
239
vals.push(v.name + '=' + enc);
240
} else if (isForm || isFormCont) {
241
vals.push(v.name + '=' + enc);
242
} else if (isLabel) {
243
vals.push(enc);
244
} else if (isPath) {
245
vals.push('/' + enc);
246
} else {
247
vals.push(enc);
248
}
249
}
250
251
let joined = '';
252
if (isLabel) {
253
// Remove trailing dot for missing values
254
const filtered = vals.filter(v => v !== '');
255
joined = filtered.length ? prefix + filtered.join('.') : '';
256
} else if (isPath) {
257
// Remove empty segments for undefined/null
258
const filtered = vals.filter(v => v !== '');
259
joined = filtered.length ? filtered.join('') : '';
260
if (joined && !joined.startsWith('/')) {
261
joined = '/' + joined;
262
}
263
} else if (isParam) {
264
// For param, if value is empty string, just append ;name
265
joined = vals.length ? prefix + vals.map(v => v.replace(/=\s*$/, '')).join(';') : '';
266
} else if (isForm) {
267
joined = vals.length ? prefix + vals.join('&') : '';
268
} else if (isFormCont) {
269
joined = vals.length ? prefix + vals.join('&') : '';
270
} else if (isFragment) {
271
joined = prefix + vals.join(',');
272
} else if (isReserved) {
273
joined = vals.join(',');
274
} else {
275
joined = vals.join(',');
276
}
277
return joined;
278
}
279
280
private static _encode(str: string, reserved: boolean): string {
281
return reserved ? encodeURI(str) : pctEncode(str);
282
}
283
284
private static _formPair(k: string, v: unknown, named: boolean): string {
285
return named ? k + '=' + encodeURIComponent(String(v)) : encodeURIComponent(String(v));
286
}
287
}
288
289
function pctEncode(str: string): string {
290
let out = '';
291
for (let i = 0; i < str.length; i++) {
292
const chr = str.charCodeAt(i);
293
if (
294
// alphanum ranges:
295
(chr >= 0x30 && chr <= 0x39 || chr >= 0x41 && chr <= 0x5a || chr >= 0x61 && chr <= 0x7a) ||
296
// unreserved characters:
297
(chr === 0x2d || chr === 0x2e || chr === 0x5f || chr === 0x7e)
298
) {
299
out += str[i];
300
} else {
301
out += '%' + chr.toString(16).toUpperCase();
302
}
303
}
304
return out;
305
}
306
307