URITemplate.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /*!
  2. * URI.js - Mutating URLs
  3. * URI Template Support - http://tools.ietf.org/html/rfc6570
  4. *
  5. * Version: 1.19.11
  6. *
  7. * Author: Rodney Rehm
  8. * Web: http://medialize.github.io/URI.js/
  9. *
  10. * Licensed under
  11. * MIT License http://www.opensource.org/licenses/mit-license
  12. *
  13. */
  14. (function (root, factory) {
  15. 'use strict';
  16. // https://github.com/umdjs/umd/blob/master/returnExports.js
  17. if (typeof module === 'object' && module.exports) {
  18. // Node
  19. module.exports = factory(require('./URI'));
  20. } else if (typeof define === 'function' && define.amd) {
  21. // AMD. Register as an anonymous module.
  22. define(['./URI'], factory);
  23. } else {
  24. // Browser globals (root is window)
  25. root.URITemplate = factory(root.URI, root);
  26. }
  27. }(this, function (URI, root) {
  28. 'use strict';
  29. // FIXME: v2.0.0 renamce non-camelCase properties to uppercase
  30. /*jshint camelcase: false */
  31. // save current URITemplate variable, if any
  32. var _URITemplate = root && root.URITemplate;
  33. var hasOwn = Object.prototype.hasOwnProperty;
  34. function URITemplate(expression) {
  35. // serve from cache where possible
  36. if (URITemplate._cache[expression]) {
  37. return URITemplate._cache[expression];
  38. }
  39. // Allow instantiation without the 'new' keyword
  40. if (!(this instanceof URITemplate)) {
  41. return new URITemplate(expression);
  42. }
  43. this.expression = expression;
  44. URITemplate._cache[expression] = this;
  45. return this;
  46. }
  47. function Data(data) {
  48. this.data = data;
  49. this.cache = {};
  50. }
  51. var p = URITemplate.prototype;
  52. // list of operators and their defined options
  53. var operators = {
  54. // Simple string expansion
  55. '' : {
  56. prefix: '',
  57. separator: ',',
  58. named: false,
  59. empty_name_separator: false,
  60. encode : 'encode'
  61. },
  62. // Reserved character strings
  63. '+' : {
  64. prefix: '',
  65. separator: ',',
  66. named: false,
  67. empty_name_separator: false,
  68. encode : 'encodeReserved'
  69. },
  70. // Fragment identifiers prefixed by '#'
  71. '#' : {
  72. prefix: '#',
  73. separator: ',',
  74. named: false,
  75. empty_name_separator: false,
  76. encode : 'encodeReserved'
  77. },
  78. // Name labels or extensions prefixed by '.'
  79. '.' : {
  80. prefix: '.',
  81. separator: '.',
  82. named: false,
  83. empty_name_separator: false,
  84. encode : 'encode'
  85. },
  86. // Path segments prefixed by '/'
  87. '/' : {
  88. prefix: '/',
  89. separator: '/',
  90. named: false,
  91. empty_name_separator: false,
  92. encode : 'encode'
  93. },
  94. // Path parameter name or name=value pairs prefixed by ';'
  95. ';' : {
  96. prefix: ';',
  97. separator: ';',
  98. named: true,
  99. empty_name_separator: false,
  100. encode : 'encode'
  101. },
  102. // Query component beginning with '?' and consisting
  103. // of name=value pairs separated by '&'; an
  104. '?' : {
  105. prefix: '?',
  106. separator: '&',
  107. named: true,
  108. empty_name_separator: true,
  109. encode : 'encode'
  110. },
  111. // Continuation of query-style &name=value pairs
  112. // within a literal query component.
  113. '&' : {
  114. prefix: '&',
  115. separator: '&',
  116. named: true,
  117. empty_name_separator: true,
  118. encode : 'encode'
  119. }
  120. // The operator characters equals ("="), comma (","), exclamation ("!"),
  121. // at sign ("@"), and pipe ("|") are reserved for future extensions.
  122. };
  123. // storage for already parsed templates
  124. URITemplate._cache = {};
  125. // pattern to identify expressions [operator, variable-list] in template
  126. URITemplate.EXPRESSION_PATTERN = /\{([^a-zA-Z0-9%_]?)([^\}]+)(\}|$)/g;
  127. // pattern to identify variables [name, explode, maxlength] in variable-list
  128. URITemplate.VARIABLE_PATTERN = /^([^*:.](?:\.?[^*:.])*)((\*)|:(\d+))?$/;
  129. // pattern to verify variable name integrity
  130. URITemplate.VARIABLE_NAME_PATTERN = /[^a-zA-Z0-9%_.]/;
  131. // pattern to verify literal integrity
  132. URITemplate.LITERAL_PATTERN = /[<>{}"`^| \\]/;
  133. // expand parsed expression (expression, not template!)
  134. URITemplate.expand = function(expression, data, opts) {
  135. // container for defined options for the given operator
  136. var options = operators[expression.operator];
  137. // expansion type (include keys or not)
  138. var type = options.named ? 'Named' : 'Unnamed';
  139. // list of variables within the expression
  140. var variables = expression.variables;
  141. // result buffer for evaluating the expression
  142. var buffer = [];
  143. var d, variable, i;
  144. for (i = 0; (variable = variables[i]); i++) {
  145. // fetch simplified data source
  146. d = data.get(variable.name);
  147. if (d.type === 0 && opts && opts.strict) {
  148. throw new Error('Missing expansion value for variable "' + variable.name + '"');
  149. }
  150. if (!d.val.length) {
  151. if (d.type) {
  152. // empty variables (empty string)
  153. // still lead to a separator being appended!
  154. buffer.push('');
  155. }
  156. // no data, no action
  157. continue;
  158. }
  159. if (d.type > 1 && variable.maxlength) {
  160. // composite variable cannot specify maxlength
  161. throw new Error('Invalid expression: Prefix modifier not applicable to variable "' + variable.name + '"');
  162. }
  163. // expand the given variable
  164. buffer.push(URITemplate['expand' + type](
  165. d,
  166. options,
  167. variable.explode,
  168. variable.explode && options.separator || ',',
  169. variable.maxlength,
  170. variable.name
  171. ));
  172. }
  173. if (buffer.length) {
  174. return options.prefix + buffer.join(options.separator);
  175. } else {
  176. // prefix is not prepended for empty expressions
  177. return '';
  178. }
  179. };
  180. // expand a named variable
  181. URITemplate.expandNamed = function(d, options, explode, separator, length, name) {
  182. // variable result buffer
  183. var result = '';
  184. // peformance crap
  185. var encode = options.encode;
  186. var empty_name_separator = options.empty_name_separator;
  187. // flag noting if values are already encoded
  188. var _encode = !d[encode].length;
  189. // key for named expansion
  190. var _name = d.type === 2 ? '': URI[encode](name);
  191. var _value, i, l;
  192. // for each found value
  193. for (i = 0, l = d.val.length; i < l; i++) {
  194. if (length) {
  195. // maxlength must be determined before encoding can happen
  196. _value = URI[encode](d.val[i][1].substring(0, length));
  197. if (d.type === 2) {
  198. // apply maxlength to keys of objects as well
  199. _name = URI[encode](d.val[i][0].substring(0, length));
  200. }
  201. } else if (_encode) {
  202. // encode value
  203. _value = URI[encode](d.val[i][1]);
  204. if (d.type === 2) {
  205. // encode name and cache encoded value
  206. _name = URI[encode](d.val[i][0]);
  207. d[encode].push([_name, _value]);
  208. } else {
  209. // cache encoded value
  210. d[encode].push([undefined, _value]);
  211. }
  212. } else {
  213. // values are already encoded and can be pulled from cache
  214. _value = d[encode][i][1];
  215. if (d.type === 2) {
  216. _name = d[encode][i][0];
  217. }
  218. }
  219. if (result) {
  220. // unless we're the first value, prepend the separator
  221. result += separator;
  222. }
  223. if (!explode) {
  224. if (!i) {
  225. // first element, so prepend variable name
  226. result += URI[encode](name) + (empty_name_separator || _value ? '=' : '');
  227. }
  228. if (d.type === 2) {
  229. // without explode-modifier, keys of objects are returned comma-separated
  230. result += _name + ',';
  231. }
  232. result += _value;
  233. } else {
  234. // only add the = if it is either default (?&) or there actually is a value (;)
  235. result += _name + (empty_name_separator || _value ? '=' : '') + _value;
  236. }
  237. }
  238. return result;
  239. };
  240. // expand an unnamed variable
  241. URITemplate.expandUnnamed = function(d, options, explode, separator, length) {
  242. // variable result buffer
  243. var result = '';
  244. // performance crap
  245. var encode = options.encode;
  246. var empty_name_separator = options.empty_name_separator;
  247. // flag noting if values are already encoded
  248. var _encode = !d[encode].length;
  249. var _name, _value, i, l;
  250. // for each found value
  251. for (i = 0, l = d.val.length; i < l; i++) {
  252. if (length) {
  253. // maxlength must be determined before encoding can happen
  254. _value = URI[encode](d.val[i][1].substring(0, length));
  255. } else if (_encode) {
  256. // encode and cache value
  257. _value = URI[encode](d.val[i][1]);
  258. d[encode].push([
  259. d.type === 2 ? URI[encode](d.val[i][0]) : undefined,
  260. _value
  261. ]);
  262. } else {
  263. // value already encoded, pull from cache
  264. _value = d[encode][i][1];
  265. }
  266. if (result) {
  267. // unless we're the first value, prepend the separator
  268. result += separator;
  269. }
  270. if (d.type === 2) {
  271. if (length) {
  272. // maxlength also applies to keys of objects
  273. _name = URI[encode](d.val[i][0].substring(0, length));
  274. } else {
  275. // at this point the name must already be encoded
  276. _name = d[encode][i][0];
  277. }
  278. result += _name;
  279. if (explode) {
  280. // explode-modifier separates name and value by "="
  281. result += (empty_name_separator || _value ? '=' : '');
  282. } else {
  283. // no explode-modifier separates name and value by ","
  284. result += ',';
  285. }
  286. }
  287. result += _value;
  288. }
  289. return result;
  290. };
  291. URITemplate.noConflict = function() {
  292. if (root.URITemplate === URITemplate) {
  293. root.URITemplate = _URITemplate;
  294. }
  295. return URITemplate;
  296. };
  297. // expand template through given data map
  298. p.expand = function(data, opts) {
  299. var result = '';
  300. if (!this.parts || !this.parts.length) {
  301. // lazilyy parse the template
  302. this.parse();
  303. }
  304. if (!(data instanceof Data)) {
  305. // make given data available through the
  306. // optimized data handling thingie
  307. data = new Data(data);
  308. }
  309. for (var i = 0, l = this.parts.length; i < l; i++) {
  310. /*jshint laxbreak: true */
  311. result += typeof this.parts[i] === 'string'
  312. // literal string
  313. ? this.parts[i]
  314. // expression
  315. : URITemplate.expand(this.parts[i], data, opts);
  316. /*jshint laxbreak: false */
  317. }
  318. return result;
  319. };
  320. // parse template into action tokens
  321. p.parse = function() {
  322. // performance crap
  323. var expression = this.expression;
  324. var ePattern = URITemplate.EXPRESSION_PATTERN;
  325. var vPattern = URITemplate.VARIABLE_PATTERN;
  326. var nPattern = URITemplate.VARIABLE_NAME_PATTERN;
  327. var lPattern = URITemplate.LITERAL_PATTERN;
  328. // token result buffer
  329. var parts = [];
  330. // position within source template
  331. var pos = 0;
  332. var variables, eMatch, vMatch;
  333. var checkLiteral = function(literal) {
  334. if (literal.match(lPattern)) {
  335. throw new Error('Invalid Literal "' + literal + '"');
  336. }
  337. return literal;
  338. };
  339. // RegExp is shared accross all templates,
  340. // which requires a manual reset
  341. ePattern.lastIndex = 0;
  342. // I don't like while(foo = bar()) loops,
  343. // to make things simpler I go while(true) and break when required
  344. while (true) {
  345. eMatch = ePattern.exec(expression);
  346. if (eMatch === null) {
  347. // push trailing literal
  348. parts.push(checkLiteral(expression.substring(pos)));
  349. break;
  350. } else {
  351. // push leading literal
  352. parts.push(checkLiteral(expression.substring(pos, eMatch.index)));
  353. pos = eMatch.index + eMatch[0].length;
  354. }
  355. if (!operators[eMatch[1]]) {
  356. throw new Error('Unknown Operator "' + eMatch[1] + '" in "' + eMatch[0] + '"');
  357. } else if (!eMatch[3]) {
  358. throw new Error('Unclosed Expression "' + eMatch[0] + '"');
  359. }
  360. // parse variable-list
  361. variables = eMatch[2].split(',');
  362. for (var i = 0, l = variables.length; i < l; i++) {
  363. vMatch = variables[i].match(vPattern);
  364. if (vMatch === null) {
  365. throw new Error('Invalid Variable "' + variables[i] + '" in "' + eMatch[0] + '"');
  366. } else if (vMatch[1].match(nPattern)) {
  367. throw new Error('Invalid Variable Name "' + vMatch[1] + '" in "' + eMatch[0] + '"');
  368. }
  369. variables[i] = {
  370. name: vMatch[1],
  371. explode: !!vMatch[3],
  372. maxlength: vMatch[4] && parseInt(vMatch[4], 10)
  373. };
  374. }
  375. if (!variables.length) {
  376. throw new Error('Expression Missing Variable(s) "' + eMatch[0] + '"');
  377. }
  378. parts.push({
  379. expression: eMatch[0],
  380. operator: eMatch[1],
  381. variables: variables
  382. });
  383. }
  384. if (!parts.length) {
  385. // template doesn't contain any expressions
  386. // so it is a simple literal string
  387. // this probably should fire a warning or something?
  388. parts.push(checkLiteral(expression));
  389. }
  390. this.parts = parts;
  391. return this;
  392. };
  393. // simplify data structures
  394. Data.prototype.get = function(key) {
  395. // performance crap
  396. var data = this.data;
  397. // cache for processed data-point
  398. var d = {
  399. // type of data 0: undefined/null, 1: string, 2: object, 3: array
  400. type: 0,
  401. // original values (except undefined/null)
  402. val: [],
  403. // cache for encoded values (only for non-maxlength expansion)
  404. encode: [],
  405. encodeReserved: []
  406. };
  407. var i, l, value;
  408. if (this.cache[key] !== undefined) {
  409. // we've already processed this key
  410. return this.cache[key];
  411. }
  412. this.cache[key] = d;
  413. if (String(Object.prototype.toString.call(data)) === '[object Function]') {
  414. // data itself is a callback (global callback)
  415. value = data(key);
  416. } else if (String(Object.prototype.toString.call(data[key])) === '[object Function]') {
  417. // data is a map of callbacks (local callback)
  418. value = data[key](key);
  419. } else {
  420. // data is a map of data
  421. value = data[key];
  422. }
  423. // generalize input into [ [name1, value1], [name2, value2], … ]
  424. // so expansion has to deal with a single data structure only
  425. if (value === undefined || value === null) {
  426. // undefined and null values are to be ignored completely
  427. return d;
  428. } else if (String(Object.prototype.toString.call(value)) === '[object Array]') {
  429. for (i = 0, l = value.length; i < l; i++) {
  430. if (value[i] !== undefined && value[i] !== null) {
  431. // arrays don't have names
  432. d.val.push([undefined, String(value[i])]);
  433. }
  434. }
  435. if (d.val.length) {
  436. // only treat non-empty arrays as arrays
  437. d.type = 3; // array
  438. }
  439. } else if (String(Object.prototype.toString.call(value)) === '[object Object]') {
  440. for (i in value) {
  441. if (hasOwn.call(value, i) && value[i] !== undefined && value[i] !== null) {
  442. // objects have keys, remember them for named expansion
  443. d.val.push([i, String(value[i])]);
  444. }
  445. }
  446. if (d.val.length) {
  447. // only treat non-empty objects as objects
  448. d.type = 2; // object
  449. }
  450. } else {
  451. d.type = 1; // primitive string (could've been string, number, boolean and objects with a toString())
  452. // arrays don't have names
  453. d.val.push([undefined, String(value)]);
  454. }
  455. return d;
  456. };
  457. // hook into URI for fluid access
  458. URI.expand = function(expression, data) {
  459. var template = new URITemplate(expression);
  460. var expansion = template.expand(data);
  461. return new URI(expansion);
  462. };
  463. return URITemplate;
  464. }));