diff --git a/README.md b/README.md index f6f30ea..5074fe7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,28 @@ In browser: var template2 = new UriTemplate("/prefix/{?params*}"); ``` +## Configuration +You can provide a configuration object as the second argument: +```javascript +new UriTemplate("/prefix/{?params*}", { + leaveUnmatchedPlaceholders: true +}); +``` + +`leaveUnmatchedPlaceholders` defaults to `false`. When `true` any placeholders which do not have a corresponding key in the object, or their value is `null`, will be left in the built URL. + +E.g. +```javascript +var template = new UriTemplate("/prefix/{a}?arg1={arg1Value}", { + leaveUnmatchedPlaceholders: true +}); + +console.log(template.fill({ + a: "something" +})); +``` +will output "/prefix/something?arg1={arg1Value}". + ## Substitution using an object ```javascript // "/categories/green/round/" diff --git a/test/custom-tests.js b/test/custom-tests.js index 29316fd..154b1f4 100644 --- a/test/custom-tests.js +++ b/test/custom-tests.js @@ -2,41 +2,41 @@ var UriTemplate = require('../uri-templates'); var assert = require('proclaim'); describe("Guessing variable priority", function () { - it('GitHub issue #8', function () { - var template = new UriTemplate("{+path}/c/capture{/date,id,page}"); - var guess = template.fromUri('/a/b/c/capture/20140101/1'); - - // we already test elsewhere that this reconstructs correctly - we just want to make sure variables are prioritised left-to-right - assert.strictEqual(guess.date, '20140101'); - assert.strictEqual(guess.id, '1'); - assert.strictEqual(guess.page, undefined); - }) + it('GitHub issue #8', function () { + var template = new UriTemplate("{+path}/c/capture{/date,id,page}"); + var guess = template.fromUri('/a/b/c/capture/20140101/1'); + + // we already test elsewhere that this reconstructs correctly - we just want to make sure variables are prioritised left-to-right + assert.strictEqual(guess.date, '20140101'); + assert.strictEqual(guess.id, '1'); + assert.strictEqual(guess.page, undefined); + }) }); describe("Original string available", function () { - it('GitHub issue #7', function () { - var template = new UriTemplate("{+path}/c/capture{/date,id,page}"); + it('GitHub issue #7', function () { + var template = new UriTemplate("{+path}/c/capture{/date,id,page}"); - assert.strictEqual(template.template, '{+path}/c/capture{/date,id,page}'); - assert.strictEqual(template + "", '{+path}/c/capture{/date,id,page}'); - }) + assert.strictEqual(template.template, '{+path}/c/capture{/date,id,page}'); + assert.strictEqual(template + "", '{+path}/c/capture{/date,id,page}'); + }) }); describe("Query optional when decoding", function () { - it('GitHub issue #12', function () { - var template = new UriTemplate("{/type,ids,field}{?query*}"); + it('GitHub issue #12', function () { + var template = new UriTemplate("{/type,ids,field}{?query*}"); - var uri = '/user/1,2,3/posts'; - var guess = template.fromUri(uri); - assert.isObject(guess); + var uri = '/user/1,2,3/posts'; + var guess = template.fromUri(uri); + assert.isObject(guess); - var trimmed = template.fill(guess).replace(/\?$/, ''); - assert.strictEqual(trimmed, uri); - }); + var trimmed = template.fill(guess).replace(/\?$/, ''); + assert.strictEqual(trimmed, uri); + }); }); describe("Decode empty query", function () { - it('Must return a empty object', function () { + it('Must return a empty object', function () { var template = new UriTemplate('{?query}'); var uri = '?'; @@ -45,7 +45,7 @@ describe("Decode empty query", function () { assert.isUndefined(guess['']); }); - it('Must return a empty object in property', function () { + it('Must return a empty object in property', function () { var template = new UriTemplate('{?query*}'); var uri = '?'; @@ -54,3 +54,30 @@ describe("Decode empty query", function () { assert.isUndefined(guess['']); }); }); + +describe("'leaveUnmatchedPlaceholders' option enabled", function () { + it('Placeholder remains when property missing', function () { + var template = new UriTemplate('{a}/{b}{c}', { + leaveUnmatchedPlaceholders: true + }); + + var result = template.fill({ + a: "1", + b: "2" + }); + assert.strictEqual(result, "1/2{c}"); + }); + + it('Placeholder remains when property is null', function () { + var template = new UriTemplate('{a}/{b}{c}', { + leaveUnmatchedPlaceholders: true + }); + + var result = template.fill({ + a: "1", + b: "2", + c: null + }); + assert.strictEqual(result, "1/2{c}"); + }); +}); \ No newline at end of file diff --git a/uri-templates.js b/uri-templates.js index 96fce41..6f8ec35 100644 --- a/uri-templates.js +++ b/uri-templates.js @@ -69,6 +69,7 @@ var varSpecMap = {}; for (var i = 0; i < varList.length; i++) { var varName = varList[i]; + var completeVarName = varName; var truncate = null; if (varName.indexOf(":") != -1) { var parts = varName.split(":"); @@ -83,7 +84,8 @@ var varSpec = { truncate: truncate, name: varName, - suffices: suffices + suffices: suffices, + completeVarName: completeVarName }; varSpecs.push(varSpec); varSpecMap[varName] = varSpec; @@ -96,6 +98,10 @@ var varSpec = varSpecs[i]; var value = valueFunction(varSpec.name); if (value == null || (Array.isArray(value) && value.length == 0) || (typeof value == 'object' && Object.keys(value).length == 0)) { + if (value == null && this.options.leaveUnmatchedPlaceholders) { + // leave this template placeholder in the final url + result += "{"+varSpec.completeVarName+"}"; + } startIndex++; continue; } @@ -317,7 +323,8 @@ }; } - function UriTemplate(template) { + function UriTemplate(template, options) { + options = options || {}; if (!(this instanceof UriTemplate)) { return new UriTemplate(template); } @@ -349,7 +356,7 @@ var result = textParts[0]; for (var i = 0; i < substitutions.length; i++) { var substitution = substitutions[i]; - result += substitution(valueFunction); + result += substitution.call(this, valueFunction); result += textParts[i + 1]; } return result; @@ -405,6 +412,7 @@ } this.varNames = varNames; this.template = template; + this.options = options; } UriTemplate.prototype = { toString: function () { diff --git a/uri-templates.min.js b/uri-templates.min.js index 89e7a76..b991afb 100644 --- a/uri-templates.min.js +++ b/uri-templates.min.js @@ -1 +1 @@ -!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.UriTemplate=b()}(this,function(){function a(a){return encodeURI(a).replace(/%25[0-9][0-9]/g,function(a){return"%"+a.substring(3)})}function b(b){var c="";d[b.charAt(0)]&&(c=b.charAt(0),b=b.substring(1));var f="",g="",h=!0,i=!1,j=!1;"+"==c?h=!1:"."==c?(g=".",f="."):"/"==c?(g="/",f="/"):"#"==c?(g="#",h=!1):";"==c?(g=";",f=";",i=!0,j=!0):"?"==c?(g="?",f="&",i=!0):"&"==c&&(g="&",f="&",i=!0);for(var k=[],l=b.split(","),m=[],n={},o=0;o0&&(c+=k.suffices["*"]?f||",":",",k.suffices["*"]&&i&&(c+=k.name+"=")),c+=h?encodeURIComponent(l[n]).replace(/!/g,"%21"):a(l[n])}else if("object"==typeof l){i&&!k.suffices["*"]&&(c+=k.name+"=");var o=!0;for(var p in l)o||(c+=k.suffices["*"]?f||",":","),o=!1,c+=h?encodeURIComponent(p).replace(/!/g,"%21"):a(p),c+=k.suffices["*"]?"=":",",c+=h?encodeURIComponent(l[p]).replace(/!/g,"%21"):a(l[p])}else i&&(c+=k.name,j&&""==l||(c+="=")),null!=k.truncate&&(l=l.substring(0,k.truncate)),c+=h?encodeURIComponent(l).replace(/!/g,"%21"):a(l)}return c},v=function(a,b){if(g){if(a.substring(0,g.length)!=g)return null;a=a.substring(g.length)}if(1==m.length&&m[0].suffices["*"]){for(var c=m[0],d=c.name,e=c.suffices["*"]?a.split(f||","):[a],j=h&&-1!=a.indexOf("="),k=1;kt&&!m[t].suffices["*"];t++);if(t!=k){for(var u=m.length-1;u>0&&m.length-u0;){var j=d.shift(),k=j.split("}")[0],l=j.substring(k.length+1),m=b(k);g.push(m.substitution),h.push(m.unSubstitution),f.push(m.prefix),e.push(l),i=i.concat(m.substitution.varNames)}this.fill=function(a){if(a&&"function"!=typeof a){var b=a;a=function(a){return b[a]}}for(var c=e[0],d=0;d=e.length-1){if(""==a)break;return void 0}for(var g=e[c+1],i=c;;){if(i==e.length-2){var j=a.substring(a.length-g.length);if(j!==g)return void 0;var k=a.substring(0,a.length-g.length);a=j}else if(g){var l=a.indexOf(g),k=a.substring(0,l);a=a.substring(l)}else if(f[i+1]){var l=a.indexOf(f[i+1]);-1===l&&(l=a.length);var k=a.substring(0,l);a=a.substring(l)}else{if(e.length>i+2){i++,g=e[i+1];continue}var k=a;a=""}break}h[c](k,b)}return b},this.varNames=i,this.template=a}var d={"+":!0,"#":!0,".":!0,"/":!0,";":!0,"?":!0,"&":!0},e={"*":!0};return c.prototype={toString:function(){return this.template},fillFromObject:function(a){return this.fill(a)}},c}); \ No newline at end of file +!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.UriTemplate=b()}(this,function(){function a(a){return encodeURI(a).replace(/%25[0-9][0-9]/g,function(a){return"%"+a.substring(3)})}function b(b){var c="";d[b.charAt(0)]&&(c=b.charAt(0),b=b.substring(1));var f="",g="",h=!0,i=!1,j=!1;"+"==c?h=!1:"."==c?(g=".",f="."):"/"==c?(g="/",f="/"):"#"==c?(g="#",h=!1):";"==c?(g=";",f=";",i=!0,j=!0):"?"==c?(g="?",f="&",i=!0):"&"==c&&(g="&",f="&",i=!0);for(var k=[],l=b.split(","),m=[],n={},o=0;o0&&(c+=k.suffices["*"]?f||",":",",k.suffices["*"]&&i&&(c+=k.name+"=")),c+=h?encodeURIComponent(l[n]).replace(/!/g,"%21"):a(l[n])}else if("object"==typeof l){i&&!k.suffices["*"]&&(c+=k.name+"=");var o=!0;for(var p in l)o||(c+=k.suffices["*"]?f||",":","),o=!1,c+=h?encodeURIComponent(p).replace(/!/g,"%21"):a(p),c+=k.suffices["*"]?"=":",",c+=h?encodeURIComponent(l[p]).replace(/!/g,"%21"):a(l[p])}else i&&(c+=k.name,j&&""==l||(c+="=")),null!=k.truncate&&(l=l.substring(0,k.truncate)),c+=h?encodeURIComponent(l).replace(/!/g,"%21"):a(l)}return c},w=function(a,b){if(g){if(a.substring(0,g.length)!=g)return null;a=a.substring(g.length)}if(1==m.length&&m[0].suffices["*"]){for(var c=m[0],d=c.name,e=c.suffices["*"]?a.split(f||","):[a],j=h&&-1!=a.indexOf("="),k=1;kt&&!m[t].suffices["*"];t++);if(t!=k){for(var u=m.length-1;u>0&&m.length-u0;){var k=e.shift(),l=k.split("}")[0],m=k.substring(l.length+1),n=b(l);h.push(n.substitution),i.push(n.unSubstitution),g.push(n.prefix),f.push(m),j=j.concat(n.substitution.varNames)}this.fill=function(a){if(a&&"function"!=typeof a){var b=a;a=function(a){return b[a]}}for(var c=f[0],d=0;d=f.length-1){if(""==a)break;return void 0}for(var e=f[c+1],h=c;;){if(h==f.length-2){var j=a.substring(a.length-e.length);if(j!==e)return void 0;var k=a.substring(0,a.length-e.length);a=j}else if(e){var l=a.indexOf(e),k=a.substring(0,l);a=a.substring(l)}else if(g[h+1]){var l=a.indexOf(g[h+1]);-1===l&&(l=a.length);var k=a.substring(0,l);a=a.substring(l)}else{if(f.length>h+2){h++,e=f[h+1];continue}var k=a;a=""}break}i[c](k,b)}return b},this.varNames=j,this.template=a,this.options=d}var d={"+":!0,"#":!0,".":!0,"/":!0,";":!0,"?":!0,"&":!0},e={"*":!0};return c.prototype={toString:function(){return this.template},fillFromObject:function(a){return this.fill(a)}},c}); \ No newline at end of file