Commit 5c4fec30 authored by Romain Courteaud's avatar Romain Courteaud

Allow to extract level 0/1 template variables.

parent 01ff518d
...@@ -63,7 +63,8 @@ ...@@ -63,7 +63,8 @@
var TARGET_UNCOMPRESSED_DEPENDENCIES = (function () { var TARGET_UNCOMPRESSED_DEPENDENCIES = (function () {
var all = new jake.FileList(); var all = new jake.FileList();
all.include('./Jakefile.js', 'own-testcases.json'); all.include('./Jakefile.js', 'own-testcases.json',
'extract-spec-examples.json');
all.include('src/**'); all.include('src/**');
all.include('test/**'); all.include('test/**');
all.exclude(TARGET_COMPRESSED); all.exclude(TARGET_COMPRESSED);
......
This diff is collapsed.
...@@ -559,13 +559,21 @@ var parse = (function () { ...@@ -559,13 +559,21 @@ var parse = (function () {
return new VariableExpression(expressionText, operator, varspecs); return new VariableExpression(expressionText, operator, varspecs);
} }
function escape_regexp_string(string) {
// http://simonwillison.net/2006/Jan/20/escape/
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
function parse (uriTemplateText) { function parse (uriTemplateText) {
// assert filled string // assert filled string
var var
index, index,
chr, chr,
expressions = [], expressions = [],
expression,
braceOpenIndex = null, braceOpenIndex = null,
regexp_string = '',
can_match = true,
literalStart = 0; literalStart = 0;
for (index = 0; index < uriTemplateText.length; index += 1) { for (index = 0; index < uriTemplateText.length; index += 1) {
chr = uriTemplateText.charAt(index); chr = uriTemplateText.charAt(index);
...@@ -575,7 +583,10 @@ var parse = (function () { ...@@ -575,7 +583,10 @@ var parse = (function () {
} }
if (chr === '{') { if (chr === '{') {
if (literalStart < index) { if (literalStart < index) {
expressions.push(new LiteralExpression(uriTemplateText.substring(literalStart, index))); expression = new LiteralExpression(uriTemplateText.substring(literalStart, index));
expressions.push(expression);
regexp_string += escape_regexp_string(
expression.literal);
} }
literalStart = null; literalStart = null;
braceOpenIndex = index; braceOpenIndex = index;
...@@ -593,7 +604,7 @@ var parse = (function () { ...@@ -593,7 +604,7 @@ var parse = (function () {
throw new UriTemplateError({templateText: uriTemplateText, message: "empty braces", position: braceOpenIndex}); throw new UriTemplateError({templateText: uriTemplateText, message: "empty braces", position: braceOpenIndex});
} }
try { try {
expressions.push(parseExpression(uriTemplateText.substring(braceOpenIndex + 1, index))); expression = parseExpression(uriTemplateText.substring(braceOpenIndex + 1, index));
} }
catch (error) { catch (error) {
if (error.prototype === UriTemplateError.prototype) { if (error.prototype === UriTemplateError.prototype) {
...@@ -601,6 +612,12 @@ var parse = (function () { ...@@ -601,6 +612,12 @@ var parse = (function () {
} }
throw error; throw error;
} }
expressions.push(expression);
if (expression.operator.symbol.length === 0) {
regexp_string += "([^/]+)";
} else {
can_match = false;
}
braceOpenIndex = null; braceOpenIndex = null;
literalStart = index + 1; literalStart = index + 1;
} }
...@@ -612,9 +629,14 @@ var parse = (function () { ...@@ -612,9 +629,14 @@ var parse = (function () {
throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex}); throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex});
} }
if (literalStart < uriTemplateText.length) { if (literalStart < uriTemplateText.length) {
expressions.push(new LiteralExpression(uriTemplateText.substr(literalStart))); expression = new LiteralExpression(uriTemplateText.substring(literalStart));
expressions.push(expression);
regexp_string += escape_regexp_string(expression.literal);
} }
return new UriTemplate(uriTemplateText, expressions); if (can_match === false) {
regexp_string = undefined;
}
return new UriTemplate(uriTemplateText, expressions, regexp_string);
} }
return parse; return parse;
...@@ -836,9 +858,14 @@ var VariableExpression = (function () { ...@@ -836,9 +858,14 @@ var VariableExpression = (function () {
}()); }());
var UriTemplate = (function () { var UriTemplate = (function () {
function UriTemplate (templateText, expressions) { function UriTemplate (templateText, expressions, regexp_string) {
this.templateText = templateText; this.templateText = templateText;
this.expressions = expressions; this.expressions = expressions;
if (regexp_string !== undefined) {
this.regexp = new RegExp("^" + regexp_string + "$");
}
objectHelper.deepFreeze(this); objectHelper.deepFreeze(this);
} }
...@@ -857,6 +884,44 @@ var UriTemplate = (function () { ...@@ -857,6 +884,44 @@ var UriTemplate = (function () {
return result; return result;
}; };
UriTemplate.prototype.extract = function (text) {
var expression_index,
extracted_index = 1,
expression,
varspec,
matched = true,
variables = {},
result;
if ((this.regexp !== undefined) && (this.regexp.test(text))) {
result = this.regexp.exec(text);
for (expression_index = 0; expression_index < this.expressions.length; expression_index += 1) {
expression = this.expressions[expression_index];
if (expression.literal === undefined) {
if ((expression.operator !== undefined) && (expression.operator.symbol.length === 0) && (expression.varspecs.length === 1)) {
varspec = expression.varspecs[0];
if ((varspec.exploded === false) && (varspec.maxLength === null)) {
if (result[extracted_index].indexOf(',') === -1) {
variables[varspec.varname] = decodeURIComponent(result[extracted_index]);
extracted_index += 1;
} else {
matched = false;
}
} else {
matched = false;
}
} else {
matched = false;
}
}
}
if (matched) {
return variables;
}
}
return false;
};
UriTemplate.parse = parse; UriTemplate.parse = parse;
UriTemplate.UriTemplateError = UriTemplateError; UriTemplate.UriTemplateError = UriTemplateError;
return UriTemplate; return UriTemplate;
......
{
"Level 0 Examples" :
{
"level": 0,
"variables": {
},
"testcases" : [
["var", "var", []],
["hello", "hello", []],
["varhello", "var", false],
["var", "varhello", false]
]
},
"Level 1 Examples" :
{
"level": 1,
"variables": {
"var" : "value",
"hello" : "Hello World!"
},
"testcases" : [
["{var}", "value", ["var"]],
["{hello}", "Hello%20World%21", ["hello"]],
["{hello}foo{var}", "Hello%20World%21foovalue", ["hello", "var"]]
]
},
"Level 2 Examples" :
{
"level": 2,
"variables": {
"var" : "value",
"hello" : "Hello World!",
"path" : "/foo/bar"
},
"testcases" : [
["{+var}", "value", false],
["{+hello}", "Hello%20World!", false],
["{+path}/here", "/foo/bar/here", false],
["here?ref={+path}", "here?ref=/foo/bar", false]
]
},
"Level 3 Examples" :
{
"level": 3,
"variables": {
"var" : "value",
"hello" : "Hello World!",
"empty" : "",
"path" : "/foo/bar",
"x" : "1024",
"y" : "768"
},
"testcases" : [
["map?{x,y}", "map?1024,768", false],
["{x,hello,y}", "1024,Hello%20World%21,768", false],
["{+x,hello,y}", "1024,Hello%20World!,768", false],
["{+path,x}/here", "/foo/bar,1024/here", false],
["{#x,hello,y}", "#1024,Hello%20World!,768", false],
["{#path,x}/here", "#/foo/bar,1024/here", false],
["X{.var}", "X.value", false],
["X{.x,y}", "X.1024.768", false],
["{/var}", "/value", false],
["{/var,x}/here", "/value/1024/here", false],
["{;x,y}", ";x=1024;y=768", false],
["{;x,y,empty}", ";x=1024;y=768;empty", false],
["{?x,y}", "?x=1024&y=768", false],
["{?x,y,empty}", "?x=1024&y=768&empty=", false],
["?fixed=yes{&x}", "?fixed=yes&x=1024", false],
["{&x,y,empty}", "&x=1024&y=768&empty=", false]
]
},
"Level 4 Examples" :
{
"level": 4,
"variables": {
"var": "value",
"hello": "Hello World!",
"path": "/foo/bar",
"list": ["red", "green", "blue"],
"keys": {"semi": ";", "dot": ".", "comma":","}
},
"testcases": [
["{var:3}", "val", false],
["{var:30}", "value", false],
["{list}", "red,green,blue", false],
["{list*}", "red,green,blue", false],
["{keys}", [
"comma,%2C,dot,.,semi,%3B",
"comma,%2C,semi,%3B,dot,.",
"dot,.,comma,%2C,semi,%3B",
"dot,.,semi,%3B,comma,%2C",
"semi,%3B,comma,%2C,dot,.",
"semi,%3B,dot,.,comma,%2C"
], false],
["{keys*}", [
"comma=%2C,dot=.,semi=%3B",
"comma=%2C,semi=%3B,dot=.",
"dot=.,comma=%2C,semi=%3B",
"dot=.,semi=%3B,comma=%2C",
"semi=%3B,comma=%2C,dot=.",
"semi=%3B,dot=.,comma=%2C"
], false],
["{+path:6}/here", "/foo/b/here", false],
["{+list}", "red,green,blue", false],
["{+list*}", "red,green,blue", false],
["{+keys}", [
"comma,,,dot,.,semi,;",
"comma,,,semi,;,dot,.",
"dot,.,comma,,,semi,;",
"dot,.,semi,;,comma,,",
"semi,;,comma,,,dot,.",
"semi,;,dot,.,comma,,"
], false],
["{+keys*}", [
"comma=,,dot=.,semi=;",
"comma=,,semi=;,dot=.",
"dot=.,comma=,,semi=;",
"dot=.,semi=;,comma=,",
"semi=;,comma=,,dot=.",
"semi=;,dot=.,comma=,"
], false],
["{#path:6}/here", "#/foo/b/here", false],
["{#list}", "#red,green,blue", false],
["{#list*}", "#red,green,blue", false],
["{#keys}", [
"#comma,,,dot,.,semi,;",
"#comma,,,semi,;,dot,.",
"#dot,.,comma,,,semi,;",
"#dot,.,semi,;,comma,,",
"#semi,;,comma,,,dot,.",
"#semi,;,dot,.,comma,,"
], false],
["{#keys*}", [
"#comma=,,dot=.,semi=;",
"#comma=,,semi=;,dot=.",
"#dot=.,comma=,,semi=;",
"#dot=.,semi=;,comma=,",
"#semi=;,comma=,,dot=.",
"#semi=;,dot=.,comma=,"
], false],
["X{.var:3}", "X.val", false],
["X{.list}", "X.red,green,blue", false],
["X{.list*}", "X.red.green.blue", false],
["X{.keys}", [
"X.comma,%2C,dot,.,semi,%3B",
"X.comma,%2C,semi,%3B,dot,.",
"X.dot,.,comma,%2C,semi,%3B",
"X.dot,.,semi,%3B,comma,%2C",
"X.semi,%3B,comma,%2C,dot,.",
"X.semi,%3B,dot,.,comma,%2C"
], false],
["{/var:1,var}", "/v/value", false],
["{/list}", "/red,green,blue", false],
["{/list*}", "/red/green/blue", false],
["{/list*,path:4}", "/red/green/blue/%2Ffoo", false],
["{/keys}", [
"/comma,%2C,dot,.,semi,%3B",
"/comma,%2C,semi,%3B,dot,.",
"/dot,.,comma,%2C,semi,%3B",
"/dot,.,semi,%3B,comma,%2C",
"/semi,%3B,comma,%2C,dot,.",
"/semi,%3B,dot,.,comma,%2C"
], false],
["{/keys*}", [
"/comma=%2C/dot=./semi=%3B",
"/comma=%2C/semi=%3B/dot=.",
"/dot=./comma=%2C/semi=%3B",
"/dot=./semi=%3B/comma=%2C",
"/semi=%3B/comma=%2C/dot=.",
"/semi=%3B/dot=./comma=%2C"
], false],
["{;hello:5}", ";hello=Hello", false],
["{;list}", ";list=red,green,blue", false],
["{;list*}", ";list=red;list=green;list=blue", false],
["{;keys}", [
";keys=comma,%2C,dot,.,semi,%3B",
";keys=comma,%2C,semi,%3B,dot,.",
";keys=dot,.,comma,%2C,semi,%3B",
";keys=dot,.,semi,%3B,comma,%2C",
";keys=semi,%3B,comma,%2C,dot,.",
";keys=semi,%3B,dot,.,comma,%2C"
], false],
["{;keys*}", [
";comma=%2C;dot=.;semi=%3B",
";comma=%2C;semi=%3B;dot=.",
";dot=.;comma=%2C;semi=%3B",
";dot=.;semi=%3B;comma=%2C",
";semi=%3B;comma=%2C;dot=.",
";semi=%3B;dot=.;comma=%2C"
], false],
["{?var:3}", "?var=val", false],
["{?list}", "?list=red,green,blue", false],
["{?list*}", "?list=red&list=green&list=blue", false],
["{?keys}", [
"?keys=comma,%2C,dot,.,semi,%3B",
"?keys=comma,%2C,semi,%3B,dot,.",
"?keys=dot,.,comma,%2C,semi,%3B",
"?keys=dot,.,semi,%3B,comma,%2C",
"?keys=semi,%3B,comma,%2C,dot,.",
"?keys=semi,%3B,dot,.,comma,%2C"
], false],
["{?keys*}", [
"?comma=%2C&dot=.&semi=%3B",
"?comma=%2C&semi=%3B&dot=.",
"?dot=.&comma=%2C&semi=%3B",
"?dot=.&semi=%3B&comma=%2C",
"?semi=%3B&comma=%2C&dot=.",
"?semi=%3B&dot=.&comma=%2C"
], false],
["{&var:3}", "&var=val", false],
["{&list}", "&list=red,green,blue", false],
["{&list*}", "&list=red&list=green&list=blue", false],
["{&keys}", [
"&keys=comma,%2C,dot,.,semi,%3B",
"&keys=comma,%2C,semi,%3B,dot,.",
"&keys=dot,.,comma,%2C,semi,%3B",
"&keys=dot,.,semi,%3B,comma,%2C",
"&keys=semi,%3B,comma,%2C,dot,.",
"&keys=semi,%3B,dot,.,comma,%2C"
], false],
["{&keys*}", [
"&comma=%2C&dot=.&semi=%3B",
"&comma=%2C&semi=%3B&dot=.",
"&dot=.&comma=%2C&semi=%3B",
"&dot=.&semi=%3B&comma=%2C",
"&semi=%3B&comma=%2C&dot=.",
"&semi=%3B&dot=.&comma=%2C"
], false]
]
}
}
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
"uritemplate-test", "uritemplate-test",
"Jakefile.js", "Jakefile.js",
"own-testcases.json", "own-testcases.json",
"extract-spec-examples.json",
"demo.html", "demo.html",
"demo_AMD.html", "demo_AMD.html",
"README.md", "README.md",
......
/*jshint unused:false */ /*jshint unused:false */
/*global parse, objectHelper, UriTemplateError*/ /*global parse, objectHelper, UriTemplateError */
var UriTemplate = (function () { var UriTemplate = (function () {
"use strict"; "use strict";
function UriTemplate (templateText, expressions) { function UriTemplate (templateText, expressions, regexp_string) {
this.templateText = templateText; this.templateText = templateText;
this.expressions = expressions; this.expressions = expressions;
if (regexp_string !== undefined) {
this.regexp = new RegExp("^" + regexp_string + "$");
}
objectHelper.deepFreeze(this); objectHelper.deepFreeze(this);
} }
...@@ -23,6 +28,44 @@ var UriTemplate = (function () { ...@@ -23,6 +28,44 @@ var UriTemplate = (function () {
return result; return result;
}; };
UriTemplate.prototype.extract = function (text) {
var expression_index,
extracted_index = 1,
expression,
varspec,
matched = true,
variables = {},
result;
if ((this.regexp !== undefined) && (this.regexp.test(text))) {
result = this.regexp.exec(text);
for (expression_index = 0; expression_index < this.expressions.length; expression_index += 1) {
expression = this.expressions[expression_index];
if (expression.literal === undefined) {
if ((expression.operator !== undefined) && (expression.operator.symbol.length === 0) && (expression.varspecs.length === 1)) {
varspec = expression.varspecs[0];
if ((varspec.exploded === false) && (varspec.maxLength === null)) {
if (result[extracted_index].indexOf(',') === -1) {
variables[varspec.varname] = decodeURIComponent(result[extracted_index]);
extracted_index += 1;
} else {
matched = false;
}
} else {
matched = false;
}
} else {
matched = false;
}
}
}
if (matched) {
return variables;
}
}
return false;
};
UriTemplate.parse = parse; UriTemplate.parse = parse;
UriTemplate.UriTemplateError = UriTemplateError; UriTemplate.UriTemplateError = UriTemplateError;
return UriTemplate; return UriTemplate;
......
...@@ -112,13 +112,21 @@ var parse = (function () { ...@@ -112,13 +112,21 @@ var parse = (function () {
return new VariableExpression(expressionText, operator, varspecs); return new VariableExpression(expressionText, operator, varspecs);
} }
function escape_regexp_string(string) {
// http://simonwillison.net/2006/Jan/20/escape/
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
function parse (uriTemplateText) { function parse (uriTemplateText) {
// assert filled string // assert filled string
var var
index, index,
chr, chr,
expressions = [], expressions = [],
expression,
braceOpenIndex = null, braceOpenIndex = null,
regexp_string = '',
can_match = true,
literalStart = 0; literalStart = 0;
for (index = 0; index < uriTemplateText.length; index += 1) { for (index = 0; index < uriTemplateText.length; index += 1) {
chr = uriTemplateText.charAt(index); chr = uriTemplateText.charAt(index);
...@@ -128,7 +136,10 @@ var parse = (function () { ...@@ -128,7 +136,10 @@ var parse = (function () {
} }
if (chr === '{') { if (chr === '{') {
if (literalStart < index) { if (literalStart < index) {
expressions.push(new LiteralExpression(uriTemplateText.substring(literalStart, index))); expression = new LiteralExpression(uriTemplateText.substring(literalStart, index));
expressions.push(expression);
regexp_string += escape_regexp_string(
expression.literal);
} }
literalStart = null; literalStart = null;
braceOpenIndex = index; braceOpenIndex = index;
...@@ -146,7 +157,7 @@ var parse = (function () { ...@@ -146,7 +157,7 @@ var parse = (function () {
throw new UriTemplateError({templateText: uriTemplateText, message: "empty braces", position: braceOpenIndex}); throw new UriTemplateError({templateText: uriTemplateText, message: "empty braces", position: braceOpenIndex});
} }
try { try {
expressions.push(parseExpression(uriTemplateText.substring(braceOpenIndex + 1, index))); expression = parseExpression(uriTemplateText.substring(braceOpenIndex + 1, index));
} }
catch (error) { catch (error) {
if (error.prototype === UriTemplateError.prototype) { if (error.prototype === UriTemplateError.prototype) {
...@@ -154,6 +165,12 @@ var parse = (function () { ...@@ -154,6 +165,12 @@ var parse = (function () {
} }
throw error; throw error;
} }
expressions.push(expression);
if (expression.operator.symbol.length === 0) {
regexp_string += "([^/]+)";
} else {
can_match = false;
}
braceOpenIndex = null; braceOpenIndex = null;
literalStart = index + 1; literalStart = index + 1;
} }
...@@ -165,9 +182,14 @@ var parse = (function () { ...@@ -165,9 +182,14 @@ var parse = (function () {
throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex}); throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex});
} }
if (literalStart < uriTemplateText.length) { if (literalStart < uriTemplateText.length) {
expressions.push(new LiteralExpression(uriTemplateText.substr(literalStart))); expression = new LiteralExpression(uriTemplateText.substring(literalStart));
expressions.push(expression);
regexp_string += escape_regexp_string(expression.literal);
}
if (can_match === false) {
regexp_string = undefined;
} }
return new UriTemplate(uriTemplateText, expressions); return new UriTemplate(uriTemplateText, expressions, regexp_string);
} }
return parse; return parse;
......
...@@ -77,6 +77,44 @@ module.exports = (function () { ...@@ -77,6 +77,44 @@ module.exports = (function () {
} }
} }
function assertExtractMatches (test, template, variables, expected, extracted, chapterName, UriTemplate) {
var
uriTemplate,
actual,
expected_extracted = {},
index;
try {
uriTemplate = UriTemplate.parse(template);
}
catch (error) {
if (expected === false && error.constructor.prototype === UriTemplate.UriTemplateError.prototype) {
log('ok. expected error found', error);
return;
}
console.log('error', error);
console.log('error.constructor.prototype', error.constructor.prototype);
test.fail('chapter ' + chapterName + ', template ' + template + ' threw error: ' + error);
return;
}
test.ok(!!uriTemplate, 'uri template could not be parsed');
try {
actual = uriTemplate.extract(expected);
}
catch (error) {
console.log('error', error);
test.fail('chapter ' + chapterName + ', template "' + template + '" threw error, when extracting: ' + JSON.stringify(error, null, 4));
return;
}
if (extracted === false) {
expected_extracted = false;
} else {
for (index = 0; index < extracted.length; index += 1) {
expected_extracted[extracted[index]] = variables[extracted[index]];
}
}
test.deepEqual(actual, expected_extracted, 'actual: "' + JSON.stringify(actual) + '", expected_extracted: "' + JSON.stringify(expected_extracted) + '", template: "' + template + '"');
}
function runTestFile (test, filename) { function runTestFile (test, filename) {
var var
tests, tests,
...@@ -106,6 +144,38 @@ module.exports = (function () { ...@@ -106,6 +144,38 @@ module.exports = (function () {
test.done(); test.done();
} }
function runExtractTestFile (test, filename) {
var
tests,
chapterName,
chapter,
variables,
index,
template,
expexted,
extracted,
UriTemplate;
log(filename);
UriTemplate = loadUriTemplate();
tests = loadTestFile(filename);
for (chapterName in tests) {
if (tests.hasOwnProperty(chapterName)) {
log('-> ' + chapterName);
chapter = tests[chapterName];
variables = chapter.variables;
for (index = 0; index < chapter.testcases.length; index += 1) {
template = chapter.testcases[index][0];
expexted = chapter.testcases[index][1];
extracted = chapter.testcases[index][2];
log(' -> ' + template);
assertExtractMatches(test, template, variables, expexted,
extracted, chapterName, UriTemplate);
}
}
}
test.done();
}
var SPEC_HOME = 'uritemplate-test'; var SPEC_HOME = 'uritemplate-test';
return { return {
...@@ -127,6 +197,9 @@ module.exports = (function () { ...@@ -127,6 +197,9 @@ module.exports = (function () {
'testfile': function (test) { 'testfile': function (test) {
test.ok(global.URI_TEMPLATE_FILE.substr(0, 3), 'tmp'); test.ok(global.URI_TEMPLATE_FILE.substr(0, 3), 'tmp');
test.done(); test.done();
},
'extract spec examples': function (test) {
runExtractTestFile(test, 'extract-spec-examples.json');
} }
}; };
}()); }());
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment