├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── lib ├── url-template.d.ts └── url-template.js ├── package.json └── test ├── uritemplate-test.js └── url-template-test.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | open-pull-requests-limit: 999 6 | rebase-strategy: disabled 7 | schedule: 8 | interval: weekly 9 | labels: 10 | - dependencies 11 | - package-ecosystem: github-actions 12 | directory: / 13 | open-pull-requests-limit: 999 14 | rebase-strategy: disabled 15 | schedule: 16 | interval: weekly 17 | labels: 18 | - dependencies 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: [18, 20] 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: true 18 | 19 | - name: Set up Node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node }} 23 | check-latest: true 24 | 25 | - name: Run tests 26 | run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "uritemplate-test"] 2 | path = uritemplate-test 3 | url = https://github.com/uri-templates/uritemplate-test 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012-2014, Bram Stein 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A JavaScript URI template implementation 2 | 3 | This is a simple URI template implementation following the [RFC 6570 URI Template specification](http://tools.ietf.org/html/rfc6570). The implementation supports all levels defined in the specification and is extensively tested. 4 | 5 | ## Installation 6 | 7 | For use with Node.js or build tools you can install it through npm: 8 | 9 | ```sh 10 | $ npm install url-template 11 | ``` 12 | 13 | If you want to use it directly in a browser use a CDN like [Skypack](https://www.skypack.dev/view/url-template). 14 | 15 | ## Example 16 | 17 | ```js 18 | import { parseTemplate } from 'url-template'; 19 | 20 | const emailUrlTemplate = parseTemplate('/{email}/{folder}/{id}'); 21 | const emailUrl = emailUrlTemplate.expand({ 22 | email: 'user@domain', 23 | folder: 'test', 24 | id: 42 25 | }); 26 | 27 | console.log(emailUrl); 28 | // Returns '/user@domain/test/42' 29 | ``` 30 | 31 | ## A note on error handling and reporting 32 | 33 | The RFC states that errors in the templates could optionally be handled and reported to the user. This implementation takes a slightly different approach in that it tries to do a best effort template expansion and leaves erroneous expressions in the returned URI instead of throwing errors. So for example, the incorrect expression `{unclosed` will return `{unclosed` as output. The leaves incorrect URLs to be handled by your URL library of choice. 34 | 35 | ## Supported Node.js versions 36 | 37 | The same versions that are [actively supported by Node.js](https://github.com/nodejs/release#release-schedule) are also supported by `url-template`, older versions of Node.js might be compatible as well, but are not actively tested against. 38 | -------------------------------------------------------------------------------- /lib/url-template.d.ts: -------------------------------------------------------------------------------- 1 | export type PrimitiveValue = string | number | boolean | null; 2 | 3 | export interface Template { 4 | expand(context: Record>): string; 5 | } 6 | 7 | export function parseTemplate(template: string): Template; 8 | -------------------------------------------------------------------------------- /lib/url-template.js: -------------------------------------------------------------------------------- 1 | function encodeReserved(str) { 2 | return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) { 3 | if (!/%[0-9A-Fa-f]/.test(part)) { 4 | part = encodeURI(part).replace(/%5B/g, '[').replace(/%5D/g, ']'); 5 | } 6 | return part; 7 | }).join(''); 8 | } 9 | 10 | function encodeUnreserved(str) { 11 | return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { 12 | return '%' + c.charCodeAt(0).toString(16).toUpperCase(); 13 | }); 14 | } 15 | 16 | function encodeValue(operator, value, key) { 17 | value = (operator === '+' || operator === '#') ? encodeReserved(value) : encodeUnreserved(value); 18 | 19 | if (key) { 20 | return encodeUnreserved(key) + '=' + value; 21 | } else { 22 | return value; 23 | } 24 | } 25 | 26 | function isDefined(value) { 27 | return value !== undefined && value !== null; 28 | } 29 | 30 | function isKeyOperator(operator) { 31 | return operator === ';' || operator === '&' || operator === '?'; 32 | } 33 | 34 | function getValues(context, operator, key, modifier) { 35 | var value = context[key], 36 | result = []; 37 | 38 | if (isDefined(value) && value !== '') { 39 | if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 40 | value = value.toString(); 41 | 42 | if (modifier && modifier !== '*') { 43 | value = value.substring(0, parseInt(modifier, 10)); 44 | } 45 | 46 | result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null)); 47 | } else { 48 | if (modifier === '*') { 49 | if (Array.isArray(value)) { 50 | value.filter(isDefined).forEach(function (value) { 51 | result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null)); 52 | }); 53 | } else { 54 | Object.keys(value).forEach(function (k) { 55 | if (isDefined(value[k])) { 56 | result.push(encodeValue(operator, value[k], k)); 57 | } 58 | }); 59 | } 60 | } else { 61 | var tmp = []; 62 | 63 | if (Array.isArray(value)) { 64 | value.filter(isDefined).forEach(function (value) { 65 | tmp.push(encodeValue(operator, value)); 66 | }); 67 | } else { 68 | Object.keys(value).forEach(function (k) { 69 | if (isDefined(value[k])) { 70 | tmp.push(encodeUnreserved(k)); 71 | tmp.push(encodeValue(operator, value[k].toString())); 72 | } 73 | }); 74 | } 75 | 76 | if (isKeyOperator(operator)) { 77 | result.push(encodeUnreserved(key) + '=' + tmp.join(',')); 78 | } else if (tmp.length !== 0) { 79 | result.push(tmp.join(',')); 80 | } 81 | } 82 | } 83 | } else { 84 | if (operator === ';') { 85 | if (isDefined(value)) { 86 | result.push(encodeUnreserved(key)); 87 | } 88 | } else if (value === '' && (operator === '&' || operator === '?')) { 89 | result.push(encodeUnreserved(key) + '='); 90 | } else if (value === '') { 91 | result.push(''); 92 | } 93 | } 94 | return result; 95 | } 96 | 97 | export function parseTemplate(template) { 98 | var operators = ['+', '#', '.', '/', ';', '?', '&']; 99 | 100 | return { 101 | expand: function (context) { 102 | return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) { 103 | if (expression) { 104 | var operator = null, 105 | values = []; 106 | 107 | if (operators.indexOf(expression.charAt(0)) !== -1) { 108 | operator = expression.charAt(0); 109 | expression = expression.substr(1); 110 | } 111 | 112 | expression.split(/,/g).forEach(function (variable) { 113 | var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable); 114 | values.push.apply(values, getValues(context, operator, tmp[1], tmp[2] || tmp[3])); 115 | }); 116 | 117 | if (operator && operator !== '+') { 118 | var separator = ','; 119 | 120 | if (operator === '?') { 121 | separator = '&'; 122 | } else if (operator !== '#') { 123 | separator = operator; 124 | } 125 | return (values.length !== 0 ? operator : '') + values.join(separator); 126 | } else { 127 | return values.join(','); 128 | } 129 | } else { 130 | return encodeReserved(literal); 131 | } 132 | }); 133 | } 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-template", 3 | "version": "3.1.1", 4 | "description": "A URI template implementation (RFC 6570 compliant)", 5 | "author": "Bram Stein (https://www.bramstein.com)", 6 | "keywords": [ 7 | "uri-template", 8 | "uri template", 9 | "uri", 10 | "url", 11 | "rfc 6570", 12 | "url template", 13 | "url-template" 14 | ], 15 | "license": "BSD-3-Clause", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/bramstein/url-template.git" 19 | }, 20 | "type": "module", 21 | "main": "./lib/url-template.js", 22 | "exports": "./lib/url-template.js", 23 | "types": "./lib/url-template.d.ts", 24 | "sideEffects": false, 25 | "engines": { 26 | "node": ">=18" 27 | }, 28 | "scripts": { 29 | "test": "node --test" 30 | }, 31 | "files": [ 32 | "lib" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/uritemplate-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, test } from 'node:test'; 3 | import { parseTemplate } from 'url-template'; 4 | import examples from '../uritemplate-test/spec-examples-by-section.json' assert { type: 'json' }; 5 | 6 | function createTestContext(context) { 7 | return (template, result) => { 8 | if (typeof result === 'string') { 9 | assert.equal(parseTemplate(template).expand(context), result); 10 | } else { 11 | assert.ok(result.includes(parseTemplate(template).expand(context))); 12 | } 13 | }; 14 | } 15 | 16 | describe('spec-examples', () => { 17 | for (const [section, example] of Object.entries(examples)) { 18 | const assert = createTestContext(example.variables); 19 | 20 | for (const [template, result] of example.testcases) { 21 | test(`${section} ${template}`, () => assert(template, result)); 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /test/url-template-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, test } from 'node:test'; 3 | import { parseTemplate } from 'url-template'; 4 | 5 | function createTestContext(context) { 6 | return (template, result) => { 7 | assert.equal(parseTemplate(template).expand(context), result); 8 | }; 9 | } 10 | 11 | describe('uri-template', () => { 12 | describe('Level 1', () => { 13 | const assert = createTestContext({ 14 | 'var': 'value', 15 | 'some.value': 'some', 16 | 'some_value': 'value', 17 | 'Some%20Thing': 'hello', 18 | 'foo': 'bar', 19 | 'hello': 'Hello World!', 20 | 'bool': false, 21 | 'toString': 'string', 22 | 'number': 42, 23 | 'float': 3.14, 24 | 'undef': undefined, 25 | 'null': null, 26 | 'chars': 'šö䟜ñꀣ¥‡ÑÒÓÔÕÖרÙÚàáâãäåæçÿü', 27 | 'surrogatepairs': '\uD834\uDF06' 28 | }); 29 | 30 | test('empty string', () => { 31 | assert('', ''); 32 | }); 33 | 34 | test('encodes non expressions correctly', () => { 35 | assert('hello/world', 'hello/world'); 36 | assert('Hello World!/{foo}', 'Hello%20World!/bar'); 37 | assert(':/?#[]@!$&()*+,;=\'', ':/?#[]@!$&()*+,;=\''); 38 | assert('%20', '%20'); 39 | assert('%xyz', '%25xyz'); 40 | assert('%', '%25'); 41 | }); 42 | 43 | test('expand plain ASCII strings', () => { 44 | assert('{var}', 'value'); 45 | }); 46 | 47 | test('expand non-ASCII strings', () => { 48 | assert('{chars}', '%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF%C3%BC'); 49 | }); 50 | 51 | test('expands and encodes surrogate pairs correctly', () => { 52 | assert('{surrogatepairs}', '%F0%9D%8C%86'); 53 | }); 54 | 55 | test('expand expressions with dot and underscore', () => { 56 | assert('{some.value}', 'some'); 57 | assert('{some_value}', 'value'); 58 | }); 59 | 60 | test('expand expressions with encoding', () => { 61 | assert('{Some%20Thing}', 'hello'); 62 | }); 63 | 64 | test('expand expressions with reserved JavaScript names', () => { 65 | assert('{toString}', 'string'); 66 | }); 67 | 68 | test('expand variables that are not strings', () => { 69 | assert('{number}', '42'); 70 | assert('{float}', '3.14'); 71 | assert('{bool}', 'false'); 72 | }); 73 | 74 | test('expand variables that are undefined or null', () => { 75 | assert('{undef}', ''); 76 | assert('{null}', ''); 77 | }); 78 | 79 | test('expand multiple values', () => { 80 | assert('{var}/{foo}', 'value/bar'); 81 | }); 82 | 83 | test('escape invalid characters correctly', () => { 84 | assert('{hello}', 'Hello%20World%21'); 85 | }); 86 | }); 87 | 88 | describe('Level 2', () => { 89 | const assert = createTestContext({ 90 | 'var': 'value', 91 | 'hello': 'Hello World!', 92 | 'path': '/foo/bar' 93 | }); 94 | 95 | test('reserved expansion of basic strings', () => { 96 | assert('{+var}', 'value'); 97 | assert('{+hello}', 'Hello%20World!'); 98 | }); 99 | 100 | test('preserves paths', () => { 101 | assert('{+path}/here', '/foo/bar/here'); 102 | assert('here?ref={+path}', 'here?ref=/foo/bar'); 103 | }); 104 | }); 105 | 106 | describe('Level 3', () => { 107 | const assert = createTestContext({ 108 | 'var' : 'value', 109 | 'hello' : 'Hello World!', 110 | 'empty' : '', 111 | 'path' : '/foo/bar', 112 | 'x' : '1024', 113 | 'y' : '768' 114 | }); 115 | 116 | test('variables without an operator', () => { 117 | assert('map?{x,y}', 'map?1024,768'); 118 | assert('{x,hello,y}', '1024,Hello%20World%21,768'); 119 | }); 120 | 121 | test('variables with the reserved expansion operator', () => { 122 | assert('{+x,hello,y}', '1024,Hello%20World!,768'); 123 | assert('{+path,x}/here', '/foo/bar,1024/here'); 124 | }); 125 | 126 | test('variables with the fragment expansion operator', () => { 127 | assert('{#x,hello,y}', '#1024,Hello%20World!,768'); 128 | assert('{#path,x}/here', '#/foo/bar,1024/here'); 129 | }); 130 | 131 | test('variables with the dot operator', () => { 132 | assert('X{.var}', 'X.value'); 133 | assert('X{.x,y}', 'X.1024.768'); 134 | }); 135 | 136 | test('variables with the path operator', () => { 137 | assert('{/var}', '/value'); 138 | assert('{/var,x}/here', '/value/1024/here'); 139 | }); 140 | 141 | test('variables with the parameter operator', () => { 142 | assert('{;x,y}', ';x=1024;y=768'); 143 | assert('{;x,y,empty}', ';x=1024;y=768;empty'); 144 | }); 145 | 146 | test('variables with the query operator', () => { 147 | assert('{?x,y}', '?x=1024&y=768'); 148 | assert('{?x,y,empty}', '?x=1024&y=768&empty='); 149 | }); 150 | 151 | test('variables with the query continuation operator', () => { 152 | assert('?fixed=yes{&x}', '?fixed=yes&x=1024'); 153 | assert('{&x,y,empty}', '&x=1024&y=768&empty='); 154 | }); 155 | }); 156 | 157 | describe('Level 4', () => { 158 | const assert = createTestContext({ 159 | 'var': 'value', 160 | 'hello': 'Hello World!', 161 | 'path': '/foo/bar', 162 | 'list': ['red', 'green', 'blue'], 163 | 'keys': { 164 | 'semi': ';', 165 | 'dot': '.', 166 | 'comma': ',' 167 | }, 168 | "chars": { 169 | 'ü': 'ü' 170 | }, 171 | 'number': 2133, 172 | 'emptystring': '', 173 | 'emptylist': [], 174 | 'emptyobject': {}, 175 | 'undefinedlistitem': [1,,2], 176 | 'undefinedobjectitem': { key: null, hello: 'world', 'empty': '', '': 'nothing' } 177 | }); 178 | 179 | test('variable empty list', () => { 180 | assert('{/emptylist}', ''); 181 | assert('{/emptylist*}', ''); 182 | assert('{?emptylist}', '?emptylist='); 183 | assert('{?emptylist*}', ''); 184 | }); 185 | 186 | test('variable empty object', () => { 187 | assert('{/emptyobject}', ''); 188 | assert('{/emptyobject*}', ''); 189 | assert('{?emptyobject}', '?emptyobject='); 190 | assert('{?emptyobject*}', ''); 191 | }); 192 | 193 | test('variable undefined list item', () => { 194 | assert('{undefinedlistitem}', '1,2'); 195 | assert('{undefinedlistitem*}', '1,2'); 196 | assert('{?undefinedlistitem*}', '?undefinedlistitem=1&undefinedlistitem=2'); 197 | }); 198 | 199 | test('variable undefined object item', () => { 200 | assert('{undefinedobjectitem}', 'hello,world,empty,,,nothing'); 201 | assert('{undefinedobjectitem*}', 'hello=world,empty=,nothing'); 202 | }); 203 | 204 | test('variable empty string', () => { 205 | assert('{emptystring}', ''); 206 | assert('{+emptystring}', ''); 207 | assert('{#emptystring}', '#'); 208 | assert('{.emptystring}', '.'); 209 | assert('{/emptystring}', '/'); 210 | assert('{;emptystring}', ';emptystring'); 211 | assert('{?emptystring}', '?emptystring='); 212 | assert('{&emptystring}', '&emptystring='); 213 | }); 214 | 215 | test('variable modifiers prefix', () => { 216 | assert('{var:3}', 'val'); 217 | assert('{var:30}', 'value'); 218 | assert('{+path:6}/here', '/foo/b/here'); 219 | assert('{#path:6}/here', '#/foo/b/here'); 220 | assert('X{.var:3}', 'X.val'); 221 | assert('{/var:1,var}', '/v/value'); 222 | assert('{;hello:5}', ';hello=Hello'); 223 | assert('{?var:3}', '?var=val'); 224 | assert('{&var:3}', '&var=val'); 225 | }); 226 | 227 | test('variable modifier prefix converted to string', () => { 228 | assert('{number:3}', '213'); 229 | }); 230 | 231 | test('variable list expansion', () => { 232 | assert('{list}', 'red,green,blue'); 233 | assert('{+list}', 'red,green,blue'); 234 | assert('{#list}', '#red,green,blue'); 235 | assert('{/list}', '/red,green,blue'); 236 | assert('{;list}', ';list=red,green,blue'); 237 | assert('{.list}', '.red,green,blue'); 238 | assert('{?list}', '?list=red,green,blue'); 239 | assert('{&list}', '&list=red,green,blue'); 240 | }); 241 | 242 | test('variable associative array expansion', () => { 243 | assert('{keys}', 'semi,%3B,dot,.,comma,%2C'); 244 | assert('{keys*}', 'semi=%3B,dot=.,comma=%2C'); 245 | assert('{+keys}', 'semi,;,dot,.,comma,,'); 246 | assert('{#keys}', '#semi,;,dot,.,comma,,'); 247 | assert('{.keys}', '.semi,%3B,dot,.,comma,%2C'); 248 | assert('{/keys}', '/semi,%3B,dot,.,comma,%2C'); 249 | assert('{;keys}', ';keys=semi,%3B,dot,.,comma,%2C'); 250 | assert('{?keys}', '?keys=semi,%3B,dot,.,comma,%2C'); 251 | assert('{&keys}', '&keys=semi,%3B,dot,.,comma,%2C'); 252 | }); 253 | 254 | test('variable list explode', () => { 255 | assert('{list*}', 'red,green,blue'); 256 | assert('{+list*}', 'red,green,blue'); 257 | assert('{#list*}', '#red,green,blue'); 258 | assert('{/list*}', '/red/green/blue'); 259 | assert('{;list*}', ';list=red;list=green;list=blue'); 260 | assert('{.list*}', '.red.green.blue'); 261 | assert('{?list*}', '?list=red&list=green&list=blue'); 262 | assert('{&list*}', '&list=red&list=green&list=blue'); 263 | 264 | assert('{/list*,path:4}', '/red/green/blue/%2Ffoo'); 265 | }); 266 | 267 | test('variable associative array explode', () => { 268 | assert('{+keys*}', 'semi=;,dot=.,comma=,'); 269 | assert('{#keys*}', '#semi=;,dot=.,comma=,'); 270 | assert('{/keys*}', '/semi=%3B/dot=./comma=%2C'); 271 | assert('{;keys*}', ';semi=%3B;dot=.;comma=%2C'); 272 | assert('{?keys*}', '?semi=%3B&dot=.&comma=%2C'); 273 | assert('{&keys*}', '&semi=%3B&dot=.&comma=%2C') 274 | }); 275 | 276 | test('encodes associative arrays correctly', () => { 277 | assert('{chars*}', '%C3%BC=%C3%BC'); 278 | }); 279 | }); 280 | 281 | describe('Encoding', () => { 282 | const assert = createTestContext({ 283 | restricted: ":/?#[]@!$&()*+,;='", 284 | percent: '%', 285 | encoded: '%25', 286 | 'pctencoded%20name': '', 287 | mapWithEncodedName: { 288 | 'encoded%20name': '' 289 | }, 290 | mapWithRestrictedName: { 291 | 'restricted=name': '' 292 | }, 293 | mapWidthUmlautName: { 294 | 'ümlaut': '' 295 | } 296 | }); 297 | 298 | test('passes through percent encoded values', () => { 299 | assert('{percent}', '%25'); 300 | assert('{+encoded}', '%25'); 301 | }); 302 | 303 | test('encodes restricted characters correctly', () => { 304 | assert('{restricted}', '%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D%27'); 305 | assert('{+restricted}', ':/?#[]@!$&()*+,;=\''); 306 | assert('{#restricted}', '#:/?#[]@!$&()*+,;=\''); 307 | assert('{/restricted}', '/%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D%27'); 308 | assert('{;restricted}', ';restricted=%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D%27'); 309 | assert('{.restricted}', '.%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D%27'); 310 | assert('{?restricted}', '?restricted=%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D%27'); 311 | assert('{&restricted}', '&restricted=%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D%27'); 312 | }); 313 | }); 314 | 315 | describe('Error handling (or the lack thereof)', () => { 316 | const assert = createTestContext({ 317 | foo: 'test', 318 | keys: { 319 | foo: 'bar' 320 | } 321 | }); 322 | 323 | test('does not expand invalid expressions', () => { 324 | assert('{test', '{test'); 325 | assert('test}', 'test}'); 326 | assert('{{test}}', '{}'); // TODO: Is this acceptable? 327 | }); 328 | 329 | test('does not expand with incorrect operators', () => { 330 | assert('{@foo}', ''); // TODO: This will try to match a variable called `@foo` which will fail because it is not in our context. We could catch this by ignoring reserved operators? 331 | assert('{$foo}', ''); // TODO: Same story, but $ is not a reserved operator. 332 | assert('{++foo}', ''); 333 | }); 334 | 335 | test('ignores incorrect prefixes', () => { 336 | assert('{foo:test}', 'test'); // TODO: Invalid prefixes are ignored. We could throw an error. 337 | assert('{foo:2test}', 'te'); // TODO: Best effort is OK? 338 | }); 339 | 340 | test('prefix applied to the wrong context', () => { 341 | assert('{keys:1}', 'foo,bar'); 342 | }); 343 | }); 344 | 345 | describe('Skipping undefined arguments', () => { 346 | const assert = createTestContext({ 347 | 'var': 'value', 348 | 'number': 2133, 349 | 'emptystring': '', 350 | 'emptylist': [], 351 | 'emptyobject': {}, 352 | 'undefinedlistitem': [1,,2], 353 | }); 354 | 355 | test('variable undefined list item', () => { 356 | assert('{undefinedlistitem}', '1,2'); 357 | assert('{undefinedlistitem*}', '1,2'); 358 | assert('{?undefinedlistitem*}', '?undefinedlistitem=1&undefinedlistitem=2'); 359 | }); 360 | 361 | test('query with empty/undefined arguments', () => { 362 | assert('{?var,number}', '?var=value&number=2133'); 363 | assert('{?undef}', ''); 364 | assert('{?emptystring}', '?emptystring='); 365 | assert('{?emptylist}', '?emptylist='); 366 | assert('{?emptyobject}', '?emptyobject='); 367 | assert('{?undef,var,emptystring}', '?var=value&emptystring='); 368 | }); 369 | }); 370 | }); 371 | --------------------------------------------------------------------------------