aboutsummaryrefslogtreecommitdiff
path: root/lib/querystring.js
blob: 5220b53ae99bbfad0fb9d53ffb12c52589a1e4c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Query String Utilities

var QueryString = exports;

QueryString.unescape = function (str, decodeSpaces) {
  return decodeURIComponent(decodeSpaces ? str.replace(/\+/g, " ") : str);
};

QueryString.escape = function (str) {
  return encodeURIComponent(str);
};


var stack = [];
/**
 * <p>Converts an arbitrary value to a Query String representation.</p>
 *
 * <p>Objects with cyclical references will trigger an exception.</p>
 *
 * @method stringify
 * @param obj {Variant} any arbitrary value to convert to query string
 * @param sep {String} (optional) Character that should join param k=v pairs together. Default: "&"
 * @param eq  {String} (optional) Character that should join keys to their values. Default: "="
 * @param name {String} (optional) Name of the current key, for handling children recursively.
 * @static
 */
QueryString.stringify = function (obj, sep, eq, name) {
  sep = sep || "&";
  eq = eq || "=";
  if (isA(obj, null) || isA(obj, undefined) || typeof(obj) === 'function') {
    return name ? encodeURIComponent(name) + eq : '';
  }

  if (isBool(obj)) obj = +obj;
  if (isNumber(obj) || isString(obj)) {
    return encodeURIComponent(name) + eq + encodeURIComponent(obj);
  }
  if (isA(obj, [])) {
    var s = [];
    name = name+'[]';
    for (var i = 0, l = obj.length; i < l; i ++) {
      s.push( QueryString.stringify(obj[i], sep, eq, name) );
    }
    return s.join(sep);
  }
  // now we know it's an object.

  // Check for cyclical references in nested objects
  for (var i = stack.length - 1; i >= 0; --i) if (stack[i] === obj) {
    throw new Error("querystring.stringify. Cyclical reference");
  }

  stack.push(obj);

  var s = [];
  var begin = name ? name + '[' : '';
  var end = name ? ']' : '';
  for (var i in obj) if (obj.hasOwnProperty(i)) {
    var n = begin + i + end;
    s.push(QueryString.stringify(obj[i], sep, eq, n));
  }

  stack.pop();

  s = s.join(sep);
  if (!s && name) return name + "=";
  return s;
};

QueryString.parseQuery = QueryString.parse = function (qs, sep, eq) {
  return (qs || '')
    .split(sep||"&")
    .map(pieceParser(eq||"="))
    .reduce(mergeParams);
};

// Parse a key=val string.
// These can get pretty hairy
// example flow:
// parse(foo[bar][][bla]=baz)
// return parse(foo[bar][][bla],"baz")
// return parse(foo[bar][], {bla : "baz"})
// return parse(foo[bar], [{bla:"baz"}])
// return parse(foo, {bar:[{bla:"baz"}]})
// return {foo:{bar:[{bla:"baz"}]}}
var trimmerPattern = /^\s+|\s+$/g,
  slicerPattern = /(.*)\[([^\]]*)\]$/;
var pieceParser = function (eq) {
  return function parsePiece (key, val) {
    if (arguments.length !== 2) {
      // key=val, called from the map/reduce
       key = key.split(eq);
      return parsePiece(
        QueryString.unescape(key.shift(), true),
        QueryString.unescape(key.join(eq), true)
      );
    }
    key = key.replace(trimmerPattern, '');
    if (isString(val)) {
      val = val.replace(trimmerPattern, '');
      // convert numerals to numbers
      if (!isNaN(val)) {
        var numVal = +val;
        if (val === numVal.toString(10)) val = numVal;
      }
    }
    var sliced = slicerPattern.exec(key);
    if (!sliced) {
      var ret = {};
      if (key) ret[key] = val;
      return ret;
    }
    // ["foo[][bar][][baz]", "foo[][bar][]", "baz"]
    var tail = sliced[2], head = sliced[1];

    // array: key[]=val
    if (!tail) return parsePiece(head, [val]);

    // obj: key[subkey]=val
    var ret = {};
    ret[tail] = val;
    return parsePiece(head, ret);
  };
};

// the reducer function that merges each query piece together into one set of params
function mergeParams (params, addition) {
  return (
    // if it's uncontested, then just return the addition.
    (!params) ? addition
    // if the existing value is an array, then concat it.
    : (isA(params, [])) ? params.concat(addition)
    // if the existing value is not an array, and either are not objects, arrayify it.
    : (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition)
    // else merge them as objects, which is a little more complex
    : mergeObjects(params, addition)
  );
};

// Merge two *objects* together. If this is called, we've already ruled
// out the simple cases, and need to do the for-in business.
function mergeObjects (params, addition) {
  for (var i in addition) if (i && addition.hasOwnProperty(i)) {
    params[i] = mergeParams(params[i], addition[i]);
  }
  return params;
};

// duck typing
function isA (thing, canon) {
  return (
    // truthiness. you can feel it in your gut.
    (!thing === !canon)
    // typeof is usually "object"
    && typeof(thing) === typeof(canon)
    // check the constructor
    && Object.prototype.toString.call(thing) === Object.prototype.toString.call(canon)
  );
};
function isBool (thing) {
  return (
    typeof(thing) === "boolean"
    || isA(thing, new Boolean(thing))
  );
};
function isNumber (thing) {
  return (
    typeof(thing) === "number"
    || isA(thing, new Number(thing))
  ) && isFinite(thing);
};
function isString (thing) {
  return (
    typeof(thing) === "string"
    || isA(thing, new String(thing))
  );
};