From bc6f36580d3f16a0d51ee15ff831ab9d69621ad0 Mon Sep 17 00:00:00 2001 From: Rodney Rehm Date: Sat, 9 Jun 2012 14:18:09 +0200 Subject: [PATCH 1/2] adding hooks for query string parsing/building with php-conforming example implementation --- src/URI.js | 28 +++++++- src/URIhooks.js | 173 +++++++++++++++++++++++++++++++++++++++++++++ test/index.html | 2 + test/test_hooks.js | 53 ++++++++++++++ 4 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 src/URIhooks.js create mode 100644 test/test_hooks.js diff --git a/src/URI.js b/src/URI.js index 7f04eeb8..e41bc26b 100644 --- a/src/URI.js +++ b/src/URI.js @@ -21,6 +21,7 @@ var _use_module = typeof module !== "undefined" && module.exports, punycode = _load_module('punycode'), IPv6 = _load_module('IPv6'), SLD = _load_module('SecondLevelDomains'), + hooks = _load_module('URIhooks'), URI = function(url, base) { // Allow instantiation without the 'new' keyword if (!(this instanceof URI)) { @@ -74,6 +75,8 @@ function filterArrayValues(data, value) { return data; } +URI.hooks = hooks; + // static properties URI.idn_expression = /[^a-z0-9\.-]/i; URI.punycode_expression = /(xn--)/i; @@ -299,7 +302,12 @@ URI.parseQuery = function(string) { name = URI.decodeQuery(v.shift()), // no "=" is null according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#collect-url-parameters value = v.length ? URI.decodeQuery(v.join('=')) : null; - + + // run queryHook + if (URI.queryHook && URI.queryHook.parse(name, value, items)) { + continue; + } + if (items[name]) { if (typeof items[name] === "string") { items[name] = [items[name]]; @@ -310,6 +318,10 @@ URI.parseQuery = function(string) { items[name] = value; } } + + if (URI.queryHook) { + items = URI.queryHook.clean(items); + } return items; }; @@ -393,7 +405,17 @@ URI.buildQuery = function(data, duplicates) { var t = ""; for (var key in data) { - if (Object.hasOwnProperty.call(data, key) && key) { + if (Object.prototype.hasOwnProperty.call(data, key) && key) { + + // run queryHook + if (URI.queryHook) { + var res = URI.queryHook.toString(key, data[key], duplicates); + if (typeof res === 'string') { + t += res; + continue; + } + } + if (isArray(data[key])) { var unique = {}; for (var i = 0, length = data[key].length; i < length; i++) { @@ -628,7 +650,7 @@ p.href = function(href, build) { } else if (_URI || _object) { var src = _URI ? href._parts : href; for (key in src) { - if (Object.hasOwnProperty.call(this._parts, key)) { + if (Object.prototype.hasOwnProperty.call(this._parts, key)) { this._parts[key] = src[key]; } } diff --git a/src/URIhooks.js b/src/URIhooks.js new file mode 100644 index 00000000..8e442512 --- /dev/null +++ b/src/URIhooks.js @@ -0,0 +1,173 @@ +/*! + * URI.js - Mutating URLs + * Query String Hooks + * + * Version: 1.6.1 + * + * Author: Rodney Rehm + * Web: http://medialize.github.com/URI.js/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +(function(undefined) { + +/* object type detection borrowed from jQuery */ +var is_numeric = /^[0-9]+$/, + hasOwn = Object.prototype.hasOwnProperty, + class2type = (function(){ + var map = {}, + classes = "Boolean Number String Function Array Date RegExp Object".split(" "); + + for (var i = 0, c; c = classes[i]; i++) { + map[ "[object " + c + "]" ] = c.toLowerCase(); + } + + return map; + })(); + +function type(obj) { + var type = class2type[String(Object.prototype.toString.call(obj))]; + if (type !== 'object') { + return type; + } + + // test if object is a plain object, if so, call it "map" + + try { + // Not own constructor property must be Object + if (obj.constructor + && !hasOwn.call(obj, "constructor") + && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) { + return type; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return type; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) {} + return key === undefined || hasOwn.call( obj, key ) ? 'map' : type; +} + +var modifiers = { + php: { + // somewhat analogous to http://php.net/parsestr + parse: function(key, value, data) { + key = key.replace(/\]$/, '').split(/\]\[|\[/g); + if (key.length < 2) { + return false; + } + + // walk keys to create structure + // we can't rely on the fact that a numeric index would create an array, + // because list[0], list[123] would be a map. + // thus we're not creating arrays at all + // we'll clean that up in modifiers.php.clean() + var t = data; + for (var i=0, length = key.length; i < length; i++) { + var _key = key[i], + last = i + 1 == length; + + // no key means auto-increment + if (_key === '') { + var k = 0; + for (var j in t) { k++; } + _key = k + ""; + } + + // create the nested structure + if (t[_key] === undefined) { + t[_key] = last ? value : {}; + } else if (typeof t[_key] !== 'object') { + t[_key] = last ? value : {}; + } + + t = t[_key]; + } + + return true; + }, + clean: function(data) { + // we've previously created an object structure + // that we now reduce to arrays where possible + + var keys = [], + isArray = true; + + for (var i in data) { + if (isArray) { + if (i.match(is_numeric)) { + keys.push(parseInt(i, 10)); + } else { + isArray = false; + } + } + + if (typeof data[i] !== 'string') { + data[i] = modifiers.php.clean(data[i]); + } + } + + if (isArray) { + var a = []; + keys.sort(); + for (i=0, length = keys.length; i < length; i++) { + a.push(data[keys[i]]); + } + + return a; + } + + + return data; + }, + // somewhat analogous to http://php.net/http_build_query + toString: function(key, value, duplicates) { + var res = ""; + + if (value === null) { + return false; + } + + switch (type(value)) { + case 'array': + for (var i = 0, length = value.length; i < length; i++) { + res += modifiers.php.toString(key + '[' + i + ']', value[i], duplicates); + } + + return res; + + case 'map': + for (var _key in value) { + // skip the hasOwn call, as it's already been done by type() + res += modifiers.php.toString(key + '[' + _key + ']', value[_key], duplicates); + } + + return res; + + case 'string': + case 'number': + // FIXME: how to export so URI is accessible? + return '&' + URI.buildQueryParameter(key, value); + + default: + // FIXME: how to export so URI is accessible? + return '&' + URI.buildQueryParameter(key, value.toString ? value.toString() : undefined); + } + } + } +}; + +(typeof module !== 'undefined' && module.exports + ? module.exports = modifiers + : window.URIhooks = modifiers +); + +})(); \ No newline at end of file diff --git a/test/index.html b/test/index.html index c3fcc2f0..1f846a4b 100644 --- a/test/index.html +++ b/test/index.html @@ -7,6 +7,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/test/test_hooks.js b/test/test_hooks.js new file mode 100644 index 00000000..ad361ca1 --- /dev/null +++ b/test/test_hooks.js @@ -0,0 +1,53 @@ + +function encodeBrackets(s) { + var map = { + '[' : '%5B', + ']' : '%5D' + }; + return s.replace(/\[|\]/g, function(m) { + return map[m]; + }); +} +function decodeBrackets(s) { + var map = { + '%5B' : '[', + '%5D' : ']' + }; + return s.replace(/%5[BD]/gi, function(m) { + return map[m.toUpperCase()]; + }); +} + +module("URI.queryHook"); +test("URI.hooks.php", function() { + var d, r, s; + + URI.queryHook = URI.hooks.php; + + d = encodeBrackets("foo=bar&list[0]=one&list[1]=two"); + r = {foo: "bar", list: ['one', 'two']}; + s = URI.parseQuery(d); + equal(JSON.stringify(s), JSON.stringify(r), "parsing query with array"); + equal(URI.buildQuery(s), d, "reverse - parsing query with array"); + + d = encodeBrackets("foo=bar&list[]=one&list[]=two"); + s = URI.parseQuery(d); + equal(JSON.stringify(s), JSON.stringify(r), "parsing query with auto-index array"); + d = encodeBrackets("foo=bar&list[0]=one&list[1]=two"); + equal(URI.buildQuery(s), d, "reverse - parsing query with auto-index array"); + + d = encodeBrackets("list[0]=one&list[1]=two&foo[bar]=one&foo[baz]=two"); + r = {list: ['one', 'two'], foo: {bar: "one", baz: 'two'}}; + s = URI.parseQuery(d); + equal(JSON.stringify(s), JSON.stringify(r), "building query with map"); + equal(URI.buildQuery(s), d, "reverse - parsing query with map"); + + d = encodeBrackets("list[0]=one&list[1][a]=two&list[1][b]=three&foo[bar]=one&foo[baz][0]=aa&foo[baz][1]=bb"); + r = {list: ['one', {a: 'two', b: 'three'}], foo: {bar: "one", baz: ['aa', 'bb']}}; + s = URI.parseQuery(d); + equal(JSON.stringify(s), JSON.stringify(r), "building query with nesting"); + equal(URI.buildQuery(s), d, "reverse - building query with nesting"); + + URI.queryHook = null; +}); + From 11027ef8c093c2145134116a0cd44563d1826109 Mon Sep 17 00:00:00 2001 From: Rodney Rehm Date: Sat, 10 Nov 2012 13:41:42 +0100 Subject: [PATCH 2/2] more php --- src/URIhooks.js | 11 ++++++++++- test/test_hooks.js | 23 +++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/URIhooks.js b/src/URIhooks.js index 8e442512..48a5d7b2 100644 --- a/src/URIhooks.js +++ b/src/URIhooks.js @@ -119,10 +119,19 @@ var modifiers = { var a = []; keys.sort(); for (i=0, length = keys.length; i < length; i++) { + // arrays have consecutive indexes, + // if the input is not numbered consequtively, it's an object! + if (i && keys[i] -1 != keys[i - 1]) { + console.log(keys, data); + a = undefined; + break; + } a.push(data[keys[i]]); } - return a; + if (a) { + return a; + } } diff --git a/test/test_hooks.js b/test/test_hooks.js index ad361ca1..ac729f97 100644 --- a/test/test_hooks.js +++ b/test/test_hooks.js @@ -23,31 +23,42 @@ test("URI.hooks.php", function() { var d, r, s; URI.queryHook = URI.hooks.php; - + + // correctly numbered array d = encodeBrackets("foo=bar&list[0]=one&list[1]=two"); r = {foo: "bar", list: ['one', 'two']}; s = URI.parseQuery(d); - equal(JSON.stringify(s), JSON.stringify(r), "parsing query with array"); + deepEqual(s, r, "parsing query with array"); equal(URI.buildQuery(s), d, "reverse - parsing query with array"); + // implicitly numbered array d = encodeBrackets("foo=bar&list[]=one&list[]=two"); s = URI.parseQuery(d); - equal(JSON.stringify(s), JSON.stringify(r), "parsing query with auto-index array"); + deepEqual(s, r, "parsing query with auto-index array"); d = encodeBrackets("foo=bar&list[0]=one&list[1]=two"); - equal(URI.buildQuery(s), d, "reverse - parsing query with auto-index array"); + deepEqual(URI.buildQuery(s), d, "reverse - parsing query with auto-index array"); + // array and object d = encodeBrackets("list[0]=one&list[1]=two&foo[bar]=one&foo[baz]=two"); r = {list: ['one', 'two'], foo: {bar: "one", baz: 'two'}}; s = URI.parseQuery(d); - equal(JSON.stringify(s), JSON.stringify(r), "building query with map"); + deepEqual(s, r, "building query with map"); equal(URI.buildQuery(s), d, "reverse - parsing query with map"); + // nested array and object d = encodeBrackets("list[0]=one&list[1][a]=two&list[1][b]=three&foo[bar]=one&foo[baz][0]=aa&foo[baz][1]=bb"); r = {list: ['one', {a: 'two', b: 'three'}], foo: {bar: "one", baz: ['aa', 'bb']}}; s = URI.parseQuery(d); - equal(JSON.stringify(s), JSON.stringify(r), "building query with nesting"); + deepEqual(s, r, "building query with nesting"); equal(URI.buildQuery(s), d, "reverse - building query with nesting"); + // incorrectly numbered array -> object + d = encodeBrackets("foo=bar&list[0]=one&list[5]=two"); + r = {foo: "bar", list: {"0": "one", "5": "two"}}; + s = URI.parseQuery(d); + deepEqual(s, r, "parsing query with non-consequtively indexed array"); + equal(decodeBrackets(URI.buildQuery(s)), decodeBrackets(d), "reverse - parsing query with non-consequtively indexed array"); + URI.queryHook = null; });