/src/query.js

https://github.com/raidrush-dev/JNode · JavaScript · 461 lines · 254 code · 83 blank · 124 comment · 39 complexity · 30611d2cec1b825fe4d1f87379bb18e0 MD5 · raw file

  1. // standalone-version:
  2. // https://github.com/raidrush-dev/querystring-parser
  3. /**
  4. * @static
  5. */
  6. JNode.Query = (function(undefined) {
  7. var T_ASSIGN = 1,
  8. T_ARR_OPEN = 2,
  9. T_ARR_CLOSE = 4,
  10. T_DELIM = 8,
  11. T_STRING = 16, // stuff between "=" and "&" (delim) or EOF
  12. T_NUMBER = 32;
  13. var RE_OPERATOR = /[=\[\]]/,
  14. RE_NUMBER = /^[0-9]+$/;
  15. var toString = Object.prototype.toString;
  16. var STRING_TYPE = toString.call(''),
  17. OBJECT_TYPE = toString.call({}),
  18. ARRAY_TYPE = toString.call([]),
  19. NUMBER_TYPE = toString.call(0),
  20. BOOLEAN_TYPE = toString.call(true),
  21. DATE_TYPE = toString.call(new Date);
  22. // ------------------------
  23. // tokenizer
  24. var Tokenizer = {
  25. /**
  26. * returns the next char of `data`
  27. *
  28. * @returns {String}
  29. */
  30. next: function next()
  31. {
  32. if (this.offs + 1 > this.slen)
  33. return null;
  34. return this.data.charAt(this.offs++);
  35. },
  36. /**
  37. * generates tokens for `data`
  38. *
  39. * @param {String} data
  40. * @param {String|undefined} delim
  41. * @returns {Array}
  42. */
  43. tokenize: function tokenize(data, delim)
  44. {
  45. var tokens = [], token, split = data.split(delim || '&');
  46. for (var i = 0, l = split.length; i < l; ++i) {
  47. this.data = split[i];
  48. this.slen = this.data.length;
  49. this.offs = 0;
  50. while (token = this.next()) {
  51. switch (token) {
  52. case '=':
  53. tokens.push(T_ASSIGN);
  54. break;
  55. case '[':
  56. tokens.push(T_ARR_OPEN);
  57. break;
  58. case ']':
  59. tokens.push(T_ARR_CLOSE);
  60. break;
  61. default:
  62. var value = token;
  63. while (token = this.next()) {
  64. if (RE_OPERATOR.test(token)) {
  65. --this.offs;
  66. break;
  67. }
  68. value += token;
  69. }
  70. if (RE_NUMBER.test(value))
  71. tokens.push(T_NUMBER, parseInt(value));
  72. else
  73. tokens.push(T_STRING, decodeURIComponent(value));
  74. }
  75. }
  76. tokens.push(T_DELIM);
  77. }
  78. // free memory
  79. delete this.data, this.slen, this.offs;
  80. return tokens;
  81. }
  82. };
  83. // ------------------------
  84. // decoder
  85. var Decoder = {
  86. /**
  87. * parses the query-string
  88. *
  89. * @param {String} query
  90. * @param {String|undefined} delim
  91. * @returns {Object}
  92. */
  93. parse: function parse(query, delim)
  94. {
  95. this.delim = delim || '&';
  96. this.tokens = Tokenizer.tokenize(query, delim);
  97. var res = {};
  98. // parse AST
  99. while (this.tokens.length) {
  100. this.expect(T_STRING);
  101. var name = this.next();
  102. if (typeof res[name] === "undefined")
  103. res[name] = this.init();
  104. this.collect(res[name], res, name);
  105. }
  106. return res;
  107. },
  108. /**
  109. * collects all properties
  110. *
  111. * @param {Array|Object} host
  112. * @param {Object} root
  113. * @param {String} key
  114. */
  115. collect: function collect(host, root, key)
  116. {
  117. var token;
  118. switch (token = this.next()) {
  119. case T_ARR_OPEN:
  120. this.access(host);
  121. break;
  122. case T_ASSIGN:
  123. this.expect(T_STRING|T_NUMBER);
  124. root[key] = this.next();
  125. this.expect(T_DELIM);
  126. break;
  127. case T_DELIM:
  128. root[key] = true;
  129. break;
  130. default:
  131. throw new Error("Syntax error: unexpected " + this.lookup(token)
  132. + ', expected "[", "=" or "' + this.delim + '"');
  133. }
  134. },
  135. /**
  136. * parses access "[" "]" expressions
  137. *
  138. * @param {Array|Object} host
  139. */
  140. access: function access(host)
  141. {
  142. var token;
  143. switch (token = this.next()) {
  144. case T_ARR_CLOSE:
  145. // alias for push()
  146. var key = host.push(this.init()) - 1;
  147. this.collect(host[key], host, key);
  148. break;
  149. case T_NUMBER:
  150. // numeric access
  151. var index = this.next();
  152. this.expect(T_ARR_CLOSE);
  153. if (host.length <= index) {
  154. for (var i = host.length; i < index; ++i)
  155. host.push(null);
  156. host.push(this.init());
  157. }
  158. this.collect(host[index], host, index);
  159. break;
  160. case T_STRING:
  161. // object access
  162. var name = this.next();
  163. this.expect(T_ARR_CLOSE);
  164. if (typeof host[name] == "undefined")
  165. host[name] = this.init();
  166. this.collect(host[name], host, name);
  167. break;
  168. default:
  169. throw new Error("Syntax error: unexpected " + this.lookup(token)
  170. + ', expected "]", (number) or (string)');
  171. }
  172. },
  173. /**
  174. * returns the next token without removing it from the stack
  175. *
  176. * @return {Number|String}
  177. */
  178. ahead: function ahead(seek)
  179. {
  180. return this.tokens[seek || 0];
  181. },
  182. /**
  183. * looks ahead and returns the type of the next expression
  184. *
  185. * @return {Array|Object}
  186. */
  187. init: function init()
  188. {
  189. var token;
  190. switch (this.ahead()) {
  191. case T_ARR_OPEN:
  192. // we must go deeper *inception*
  193. switch (token = this.ahead(1)) {
  194. case T_ARR_CLOSE:
  195. case T_NUMBER:
  196. return [];
  197. case T_STRING:
  198. return {};
  199. default:
  200. // syntax error
  201. throw new Error('Syntax error: unexpected ' + this.lookup(token)
  202. + ', expecting "]", (number) or (string)');
  203. }
  204. break;
  205. default:
  206. return;
  207. }
  208. },
  209. /**
  210. * returns a readable representation of a token-type
  211. *
  212. * @param {Number} type
  213. * @returns {String}
  214. */
  215. lookup: function lookup(type)
  216. {
  217. switch (type) {
  218. case T_ASSIGN:
  219. return '"="';
  220. case T_ARR_OPEN:
  221. return '"["';
  222. case T_ARR_CLOSE:
  223. return '"]"';
  224. case T_STRING:
  225. return '(string)';
  226. case T_NUMBER:
  227. return '(number)';
  228. case T_DELIM:
  229. return '"' + this.delim + '"';
  230. default:
  231. return '?(' + type + ')?';
  232. }
  233. },
  234. /**
  235. * returns the top-token in stack
  236. *
  237. * @returns {Number|String}
  238. */
  239. next: function next()
  240. {
  241. return this.tokens.length ? this.tokens.shift() : null;
  242. },
  243. /**
  244. * validates the next token
  245. *
  246. * @throws {Error}
  247. * @param {Number} tokens
  248. */
  249. expect: function expect(tokens)
  250. {
  251. if (this.tokens.length && (this.tokens[0] & tokens) === 0) {
  252. var expecting = [];
  253. for (var i = 0; i <= 32; i += i)
  254. if (i & tokens !== 0)
  255. expecting.push(lookup(i));
  256. throw new Error("Syntax error: unexpected " + this.lookup(this.tokens[0])
  257. + ", expecting " + expecting.join(" or "));
  258. }
  259. if (this.tokens.length) this.tokens.shift();
  260. }
  261. };
  262. // ------------------------
  263. // encoder
  264. var Encoder = {
  265. /**
  266. * creates the query-string
  267. *
  268. * @param {Object} object
  269. * @param {String|undefined} delim
  270. * @reutrns {String}
  271. */
  272. parse: function parse(object, delim)
  273. {
  274. this.delim = delim || '&';
  275. var result = [], value;
  276. for (var i in object) {
  277. if (!object.hasOwnProperty(i))
  278. continue;
  279. if ((value = this.serialize(object[i], i)) !== "")
  280. result.push(value);
  281. }
  282. return result.join(this.delim);
  283. },
  284. /**
  285. * serializes the value of the current object
  286. *
  287. * @param {Scalar|Array|Object} value
  288. * @param {String} label
  289. * @returns {String}
  290. */
  291. serialize: function serialize(value, label)
  292. {
  293. if (typeof value === "undefined" || value === null)
  294. return "";
  295. switch (toString.call(value)) {
  296. case DATE_TYPE:
  297. return label + "=" + (+value);
  298. case STRING_TYPE:
  299. value = this.encode(value);
  300. case NUMBER_TYPE:
  301. return label + "=" + value;
  302. case BOOLEAN_TYPE:
  303. return label + "=" + (value ? 1 : 0);
  304. case ARRAY_TYPE:
  305. case OBJECT_TYPE:
  306. return this.access(value, label);
  307. default:
  308. throw new Error('Parse error: value for key "' + label + '" is not serializable');
  309. }
  310. },
  311. /**
  312. * parses arrays and objects
  313. *
  314. * @param {Array|Object} value
  315. * @param {String} label
  316. * @reutrns {String}
  317. */
  318. access: function access(value, label)
  319. {
  320. var result = [];
  321. if (toString.call(value) === ARRAY_TYPE)
  322. for (var i = 0, l = value.length; i < l; ++i)
  323. this.handle(result, label, value[i], i);
  324. else
  325. for (var i in value)
  326. if (value.hasOwnProperty(i))
  327. this.handle(result, label, value[i], i);
  328. return result.join(this.delim);
  329. },
  330. /**
  331. * serializes an array/object property
  332. *
  333. * @
  334. */
  335. handle: function handle(stack, label, value, prop)
  336. {
  337. var res;
  338. if ((res = this.serialize(value, label + "[" + prop + "]")) !== "")
  339. stack.push(res);
  340. },
  341. /**
  342. * uses encodeURIComponent
  343. *
  344. * @param {String} value
  345. * @returns {String}
  346. */
  347. encode: function encode(value)
  348. {
  349. return encodeURIComponent(value);
  350. }
  351. };
  352. // ------------------------
  353. // exports
  354. return {
  355. /**
  356. * decodes a query-string
  357. *
  358. * @param {String} query
  359. * @param {String|undefined} delim
  360. * @returns {Object}
  361. */
  362. decode: function decode(query, delim)
  363. {
  364. return Decoder.parse(query, delim);
  365. },
  366. /**
  367. * encodes an object
  368. *
  369. * @param {Object} object
  370. * @param {String|undefined} delim
  371. * @returns {String}
  372. */
  373. encode: function encode(object, delim)
  374. {
  375. return Encoder.parse(object, delim);
  376. }
  377. }
  378. })();