url-templates 1.0.0 → 1.0.2
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/#/tests/0/0.json +5 -0
- package/#/tests/0/1.json +5 -0
- package/#/tests/0/10.json +5 -0
- package/#/tests/0/11.json +5 -0
- package/#/tests/0/12.json +5 -0
- package/#/tests/0/13.json +5 -0
- package/#/tests/0/14.json +5 -0
- package/#/tests/0/15.json +5 -0
- package/#/tests/0/16.json +5 -0
- package/#/tests/0/17.json +5 -0
- package/#/tests/0/18.json +5 -0
- package/#/tests/0/19.json +5 -0
- package/#/tests/0/2.json +5 -0
- package/#/tests/0/20.json +5 -0
- package/#/tests/0/21.json +5 -0
- package/#/tests/0/22.json +5 -0
- package/#/tests/0/23.json +5 -0
- package/#/tests/0/24.json +5 -0
- package/#/tests/0/25.json +5 -0
- package/#/tests/0/3.json +5 -0
- package/#/tests/0/4.json +5 -0
- package/#/tests/0/5.json +5 -0
- package/#/tests/0/6.json +5 -0
- package/#/tests/0/7.json +5 -0
- package/#/tests/0/8.json +5 -0
- package/#/tests/0/9.json +5 -0
- package/#/tests/0/schema.json +4 -0
- package/index.js +22 -11
- package/package.json +1 -1
- package/readme.md +30 -3
- package/#/tests/0/.gitkeep +0 -0
package/#/tests/0/0.json
ADDED
package/#/tests/0/1.json
ADDED
package/#/tests/0/2.json
ADDED
package/#/tests/0/3.json
ADDED
package/#/tests/0/4.json
ADDED
package/#/tests/0/5.json
ADDED
package/#/tests/0/6.json
ADDED
package/#/tests/0/7.json
ADDED
package/#/tests/0/8.json
ADDED
package/#/tests/0/9.json
ADDED
package/index.js
CHANGED
|
@@ -5,7 +5,11 @@ const operators = new Set(['+', '#', '.', '/', '?', '&', ';']);
|
|
|
5
5
|
const separator = (operator) => (operator === '' || operator === '+' || operator == '#' ? ',' : operator === '?' ? '&' : operator);
|
|
6
6
|
const isDefined = (value) => value !== undefined && value !== null;
|
|
7
7
|
const encodeUnreserved = (string) => encodeURIComponent(string).replace(/[!'()*]/g, (char) => '%' + char.charCodeAt(0).toString(16).toUpperCase());
|
|
8
|
-
const encodeReserved = (string) =>
|
|
8
|
+
const encodeReserved = (string) =>
|
|
9
|
+
string
|
|
10
|
+
.split(/(%[0-9A-Fa-f]{2})/g)
|
|
11
|
+
.map((part, i) => (i % 2 ? part : encodeURI(part).replace(/%5B/g, '[').replace(/%5D/g, ']')))
|
|
12
|
+
.join('');
|
|
9
13
|
const encodeValue = (operator, value, key) => {
|
|
10
14
|
value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeUnreserved(value);
|
|
11
15
|
return key ? encodeReserved(key) + '=' + value : value;
|
|
@@ -14,28 +18,32 @@ const ast = [];
|
|
|
14
18
|
// url-template validator
|
|
15
19
|
function isUrlTemplate(template, inspect) {
|
|
16
20
|
if (typeof template !== 'string') throw new TypeError('uri-template must be a string.');
|
|
17
|
-
if (!/^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%{}]*$/.test(template)) throw new
|
|
21
|
+
if (!/^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%{}]*$/.test(template)) throw new SyntaxError('invalid character(s) in uri-template.');
|
|
18
22
|
for (let i = 0; i < template.length; ) {
|
|
19
23
|
const start = template.indexOf('{', i);
|
|
20
|
-
|
|
21
|
-
if (start === -1) {
|
|
24
|
+
const nextClose = template.indexOf('}', i);
|
|
25
|
+
if (nextClose !== -1 && (start === -1 || nextClose < start)) throw new SyntaxError(`at index ${nextClose}: unstarted expression in uri-template.`);
|
|
26
|
+
if (start === -1) {
|
|
27
|
+
inspect && ast.push(template.slice(i));
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
22
30
|
if (inspect && start > i) ast.push(template.slice(i, start));
|
|
23
31
|
const end = template.indexOf('}', start + 1);
|
|
24
|
-
|
|
32
|
+
const nestedStart = template.indexOf('{', start + 1);
|
|
33
|
+
if (end === -1 || (nestedStart !== -1 && nestedStart < end)) throw new SyntaxError(`at index ${start}: unterminated expression in uri-template.`);
|
|
25
34
|
let expression = template.slice(start + 1, end);
|
|
26
|
-
if (expression.length === 0) throw new
|
|
35
|
+
if (expression.length === 0) throw new SyntaxError(`at index ${start + 1}: empty expression.`);
|
|
27
36
|
const first = expression[0];
|
|
28
37
|
const operator = operators.has(first) ? ((expression = expression.slice(1)), first) : '';
|
|
29
|
-
if (expression.length === 0) throw new
|
|
38
|
+
if (expression.length === 0) throw new SyntaxError(`at index ${start + 2}: expression missing variable names.`);
|
|
30
39
|
const varspecs = expression.split(',').map((key) => {
|
|
31
40
|
const colon = key.indexOf(':');
|
|
32
41
|
const limit = colon !== -1 ? Number(key.slice(colon + 1)) : null;
|
|
33
|
-
if (isDefined(limit) && (limit % 1 !== 0 || isNaN(limit) || limit < 1 || limit > 9999)) throw new
|
|
42
|
+
if (isDefined(limit) && (limit % 1 !== 0 || isNaN(limit) || limit < 1 || limit > 9999)) throw new SyntaxError(`at index ${start}: invalid limit modifier.`);
|
|
34
43
|
if (limit) key = key.slice(0, colon);
|
|
35
44
|
const explode = key.endsWith('*');
|
|
36
|
-
if (explode && limit) throw new Error('invalid modifier having both explode and limit.');
|
|
37
45
|
if (explode) key = key.slice(0, -1);
|
|
38
|
-
if (!/^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*$/.test(key)) throw new
|
|
46
|
+
if (!/^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*$/.test(key)) throw new SyntaxError(`at index ${operator ? start + 2 : start + 1}: invalid variable name in uri-template.`);
|
|
39
47
|
return Object.assign({ key }, limit ? { limit } : explode ? { explode } : {});
|
|
40
48
|
});
|
|
41
49
|
if (inspect) ast.push({ [operator]: varspecs });
|
|
@@ -52,17 +60,19 @@ function parseTemplate(template, validate) {
|
|
|
52
60
|
if (first + expression) {
|
|
53
61
|
const values = [];
|
|
54
62
|
const operator = operators.has(first) ? first : ((expression = first + expression), '');
|
|
63
|
+
let defined = expression.split(/,/g).length;
|
|
55
64
|
expression.split(/,/g).forEach((variable) => {
|
|
56
65
|
const match = /(?<key>[^:\*]*)(?::(?<length>\d+)|(?<explode>\*))?/.exec(variable).groups;
|
|
57
66
|
const key = match.key;
|
|
58
67
|
const value = vars[key];
|
|
68
|
+
if (!isDefined(value)) defined--;
|
|
59
69
|
if (isDefined(value) && value !== '') {
|
|
60
70
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
61
71
|
let string = value.toString();
|
|
62
72
|
if (match.length) string = string.substring(0, parseInt(match.length));
|
|
63
73
|
values.push(encodeValue(operator, string, keyOperators.has(operator) ? key : ''));
|
|
64
74
|
} else {
|
|
65
|
-
if (validate && match.length) throw new
|
|
75
|
+
if (validate && match.length) throw new SyntaxError('invalid limit modifier on objects.');
|
|
66
76
|
if (match.explode) {
|
|
67
77
|
if (Array.isArray(value)) {
|
|
68
78
|
value.filter(isDefined).forEach((item) => values.push(encodeValue(operator, item, keyOperators.has(operator) ? key : '')));
|
|
@@ -97,6 +107,7 @@ function parseTemplate(template, validate) {
|
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
});
|
|
110
|
+
if (!defined && !validate) values.push(`{${expression}}`);
|
|
100
111
|
return (operator === '+' || values.length === 0 ? '' : operator) + values.join(separator(operator));
|
|
101
112
|
}
|
|
102
113
|
return encodeReserved(literal);
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -40,7 +40,7 @@ It returns `true` or throws an `error`.
|
|
|
40
40
|
```js title="js"
|
|
41
41
|
const { inspect } = require('url-templates');
|
|
42
42
|
try {
|
|
43
|
-
console.dir(inspect('/search{?q*,lang:2}'), { depth: null });
|
|
43
|
+
console.dir(inspect('/search{?q*,lang:2}'), { depth: null });
|
|
44
44
|
// [ '/search', { '?': [ { key: 'q', explode: true }, { key: 'lang', limit: 2 } ] } ]
|
|
45
45
|
} catch (error) {
|
|
46
46
|
console.error('invalid:', error.message);
|
|
@@ -69,9 +69,36 @@ If valid returns the `expand(vars)` function which returns the expanded `url-tem
|
|
|
69
69
|
```js title="js"
|
|
70
70
|
const { compile } = require('url-templates');
|
|
71
71
|
console.log(compile('/broken{').expand({})); // returns '/broken{'; invalid parts left for postprocessing
|
|
72
|
-
console.log(compile('/good{id}').expand({id:42})); // returns '/good42';
|
|
72
|
+
console.log(compile('/good{id}').expand({ id: 42 })); // returns '/good42';
|
|
73
|
+
console.log(compile('/undefined{id}').expand({ id: undefined })); // returns '/undefined{id}';
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
**Note:**
|
|
76
|
-
Returns a usable expander without validation
|
|
77
|
+
Returns a usable expander without validation for cases where validation is done elsewhere, or for the cases where some sort of postprocessing will follow. A good example of postprocessing is described next:
|
|
77
78
|
|
|
79
|
+
### Multi pass expansion without validation
|
|
80
|
+
|
|
81
|
+
**Example 1**
|
|
82
|
+
|
|
83
|
+
```js title="js"
|
|
84
|
+
const { compile } = require('url-templates');
|
|
85
|
+
const vars1 = { anotherPattern: '{foo}', andAnotherPattern: '{bar,baz}' };
|
|
86
|
+
const vars2 = { foo: 1, bar: 2, baz: 3 };
|
|
87
|
+
const firstPass = decodeURIComponent(compile('[{anotherPattern},{andAnotherPattern}]').expand(vars1));
|
|
88
|
+
console.log(firstPass); // returns '[{foo},{bar,baz}]';
|
|
89
|
+
console.log(compile(firstPass).expand(vars2)); // returns '[1,2,3]';
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Example 2**
|
|
93
|
+
|
|
94
|
+
```js title="js"
|
|
95
|
+
const { compile } = require('url-templates');
|
|
96
|
+
const vars1 = { foo: 1 };
|
|
97
|
+
const vars2 = { bar: 2, baz: 3 };
|
|
98
|
+
const firstPass = decodeURIComponent(compile('[{foo},{bar,baz}]').expand(vars1));
|
|
99
|
+
console.log(firstPass); // returns '[1,{bar,baz}]';
|
|
100
|
+
console.log(compile(firstPass).expand(vars2)); // returns '[1,2,3]';
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Important Note:**
|
|
104
|
+
The first pass will preserve the `{bar,baz}` expression only if the supplied variable has **none** of its members. This method can also be used to preserve quantifiers like `{1,4}` in regular expresions.
|
package/#/tests/0/.gitkeep
DELETED
|
File without changes
|