• Das Erstellen neuer Accounts wurde ausgesetzt. Bei berechtigtem Interesse bitte Kontaktaufnahme über die üblichen Wege. Beste Grüße der Admin

queryString Parser und Stringifier

kkapsner

Super Moderator
Hi Leute,

ihr kennt doch alle die PHP-Syntax für GET und POST (key=value) und die Besonderheit mit den eckigen Klammern (arr[]=1&arr[]=2 => arr ist ein Array mit [1, 2] - aber auch Abgefahreneres: arr[hallo][][was]=2&arr[hallo][0][das]=3 => arr ist ein "Array" mit {hallo: [{was: 2, das:3}]}).
Hab' das Ganze mal so gut wie möglich in JS nachgebildet (es werden sogar, wo möglich, echte Arrays erzeugt) und bitte euch, es auf Herz und Nieren zu testen:
Code:
var queryString = {
	parse: function(str){
		var ret = {};
		var retInfo = {};
		if (str.charAt(0) === "?") str = str.substr(1);
		var param = str.split("&");
		for (var i = 0; i < param.length; i++){
			var par = param[i].split("=");
			switch (par.length){
				case 1:
					par[1] = "";
					break;
				case 2:
					break;
				default:
					par[1] = par.slice(1).join("=");
			}
			var parts = [];
			var name = decodeURIComponent(par[0]).replace(/\][^\[].+/, "]").replace(/\[([^\]]*)\]/g, function(m, c){parts.push(c); return "";});
			var value = decodeURIComponent(par[1]);
			if (!name) continue;
			var partsLength = parts.length;
			if (!partsLength){
				ret[name] = value;
			}
			else {
				if (!is.object(ret[name])){
					ret[name] = {};
					retInfo[name] = {nextIndex: 0, subObjects: {}, onlyDiggits: true};
				}
				var obj = ret[name];
				var objInfo = retInfo[name];
				parts.forEach(function(name, i){
					if (!name){
						name = objInfo.nextIndex;
						objInfo.nextIndex++;
					}
					
					var diggitName = /^\d+$/.test(name);
					if (!diggitName) objInfo.onlyDiggits = false;
					
					if (i == partsLength - 1) obj[name] = value;
					else {
						if (!is.object(obj[name])){
							obj[name] = {};
							objInfo.subObjects[name] = {nextIndex: 0, subObjects: {}, onlyDiggits: true};
						}
						if (diggitName){
							if (name > objInfo.nextIndex){
								objInfo.nextIndex = parseInt(name, 10) + 1;
							}
						}
						obj = obj[name];
						objInfo = objInfo.subObjects[name];
					}
				});
			}
		}
		function toArrayIfPossible(obj, objInfo){
			if (!is.object(obj)) return obj;
			var ret = objInfo.onlyDiggits? []: obj;
			for (var i in obj){
				if (obj.hasOwnProperty(i)){
					ret[i] = toArrayIfPossible(obj[i], objInfo.subObjects[i]);
				}
			}
			return ret;
		}
		return toArrayIfPossible(ret, {onlyDiggits: false, subObjects: retInfo});
	},
	stringify: function stringify(value){
		var stringifyStack = [];
		function process(value, namescope){
			// recursion detection
			if (stringifyStack.indexOf(value) > -1) throw new TypeError("recursive structure");
			stringifyStack.push(value);
			
			namescope = (namescope || "");
			var ret = [];
			if (namescope && is.array(value)){
				var name = namescope + "[";
				value.forEach(function(value, i){
					ret.push(process(value, name + i + "]"));
				});
			}
			else if (is.object(value)){
				for (var name in value){
					if (value.hasOwnProperty(name)){
						var v = value[name];
						if (namescope) name = namescope + "[" + name + "]";
						ret.push(process(v, name));
					}
				}
			}
			else {
				stringifyStack.pop();
				return encodeURIComponent(namescope) + "=" + encodeURIComponent(value)
			}
			
			stringifyStack.pop();
			return ret.join("&");
		}
		
		return process(value);
	}
};
Für (konstruktive) Kritik, Verbesserungsvorschläge und v.A. beobachtete Bugs bin ich euch sehr dankbar.

EDIT: rekursive Objekte jetzt "erlaubt"... es kommt hald eine Fehlermeldung...
 
Zuletzt bearbeitet:
MIST - ich hab' natürlich das is-Objekt vergessen:
Code:
var OptS = Object.prototype.toString;
var is = {
	array: function(obj){
		return OptS.call(obj) == "[object Array]";
	},
	object: function(obj){
		return (typeof obj == "object" && obj !== null);
	},
	number: function(num){
		switch (typeof num){
			case "number": return !isNaN(num);
			case "object": return OptS.call(num) == "[object Number]" && !isNaN(num);
			default: return false;
		}
	},
	string: function(str){
		switch (typeof str){
			case "string": return true;
			case "object": return OptS.call(str) == "[object String]";
			default: return false;
		}
	},
	'boolean': function(bool){
		switch (typeof bool){
			case "boolean": return true;
			case "object": return OptS.call(bool) == "[object Boolean]";
			default: return false;
		}
	},
	'function': function(func){
		switch (typeof func){
			case "function": return true;
			case "object": return OptS.call(func) == "[object Function]";
			default: return false;
		}
	},
	'true': function(tr){
		return (tr && is['boolean'](tr));
	},
	'false': function(fl){
		return (!fl && is['boolean'](fl));
	},
	'undefined': function(un){
		return (un === null || (typeof un == "undefined"));
	},
	defined: function(def){
		return !is.undefined(def);
	},
	'null': function(nu){
		return (nu === null);
	},
	key: function(array, key){
		if (is['undefined'](array)) return false;
		if (key in array) return true;
		return (typeof array[key] != "undefined");
	},
	node: function(node){
		if (!is.object(node)) return false;
		try {
			/*problem in IE6 with DOM.compatible created Element*/
			if (typeof Element != "undefined" && node instanceof Element) return true;
		}catch(e){}
		return (
			is.key(node, "nodeName") &&
			is.key(node, "nodeType") &&
			is.key(node, "nodeValue")
		);
	},
	opera: !!window.opera && window.opera.toString() == "[object Opera]", // /opera/i.test(navigator.userAgent),
	safari: /safari/i.test(navigator.userAgent),
	ie: /*@cc_on @if (@_jscript) true, @else @*/ (/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)), /*@end @*/
	version: (/(msie|firefox|opera)[\/\s]((\d+\.?)+)/i.exec(navigator.userAgent))?/(msie|firefox|opera)[\/\s]((\d+\.?)+)/i.exec(navigator.userAgent)[2]: Number.NaN,
	gecko: /gecko/i.test(navigator.userAgent),
	ff: /firefox/i.test(navigator.userAgent)
};
- ohne das wird's natürlich schwer mit testen...

Hab' euch noch eine Testumgebung gebastelt: http://kkjs.kkapsner.de/tests/QueryString_test.html
 
Also mir fehlt im Moment so der Use-Case hinter der Sache, bislang habe ich eine solche Funktionalität noch nicht wirklich vermisst.
Und ich vermute vielen Benutzern fehlen konkrete Beispiele, wie sie den Code überhaupt zum Einsatz bringen können. ;)
 
OK - ich hab' sowas ähnliches schon öfter gebaut (nur nicht so explizit mit den eckigen Klammern).
Man kann mit der .parse-Funktion beliebige GET-Parameter ins JS holen (z.B. s. http://forum.jswelt.de/javascript/55237-url-parameter-variable-speichern.html oder ich verwende das auch in meinem Framework um Module zu laden: .../modules/kkjs.load.js?modules=QueryString) und man muss sich nicht serverseitig darum kümmern, dass kompliziertere Strukturen auch ins JS kommen.
Bei der .stringify-Funktion hab' ich v.A. an AJAX gedacht: ich empfinde es im Quelltext lesbarer, die übergebenen POST-Parameter in einem Objekt zu schreiben, als in einem String - aber man kann damit natürlich auch location.query sehr elegant manipulieren (mal angenommen, man will einen Parameter key=value haben, dann muss man sich keine Gedanken machen, ob dieser schon vorhanden ist - einfach ein .parse(location.query), im Objekt .key = value (wenn schon vorhanden wird's einfach überschrieben) und dann location.query = .stringify(Objekt) - sollte theoretisch nicht einmal die Seite neu laden, wenn key=value schon vorhanden ist, da die meisten Browser im for...in die Eintragsreihenfolge beibehalten).
 
Zurück
Oben