Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit d11a3c2

Browse files
committed
feat: initial implement
1 parent 8830379 commit d11a3c2

13 files changed

Lines changed: 4632 additions & 1 deletion

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,5 @@ typings/
5757
# dotenv environment variables file
5858
.env
5959

60+
*.js
61+
*.map

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
registry=https://registry.npmjs.org/

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
language: node_js
2+
node_js:
3+
- "6"
4+
5+

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# querystring
2-
Parse and stringify URL query strings
2+
Parse and stringify URL query strings. Forked from [query-string](https://github.com/sindresorhus/query-string)

index.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
export type QueryStringValue = string | null;
2+
export type QueryStringInputValue = QueryStringValue | undefined | number;
3+
export interface IParseReturn {
4+
[key: string]: QueryStringValue | QueryStringValue[];
5+
}
6+
export interface IParseInput {
7+
[key: string]: QueryStringInputValue | QueryStringInputValue[];
8+
}
9+
10+
export interface IStringifyOptions {
11+
sort?: false | ((a: string, b: string) => number);
12+
}
13+
14+
function decodeUriComponent(input: string) {
15+
try {
16+
return decodeURIComponent(input);
17+
} catch (e) {
18+
return "";
19+
}
20+
}
21+
22+
export function encodeRFC3986ValueChars(str: string): string {
23+
return encodeURIComponent(str).replace(
24+
/[!'()*]/g,
25+
x =>
26+
`%${x
27+
.charCodeAt(0)
28+
.toString(16)
29+
.toUpperCase()}`
30+
);
31+
}
32+
33+
export function arrayFormatEncoder(
34+
key: string,
35+
value: QueryStringValue
36+
): string {
37+
return value === null
38+
? encodeRFC3986ValueChars(key)
39+
: `${encodeRFC3986ValueChars(key)}=${encodeRFC3986ValueChars(value)}`;
40+
}
41+
42+
export function arrayFormatParser(
43+
key: string,
44+
value: string,
45+
ret: IParseReturn
46+
): IParseReturn {
47+
if (ret[key] === undefined) {
48+
ret[key] = value;
49+
return;
50+
}
51+
ret[key] = [].concat(ret[key], value);
52+
}
53+
54+
export function parse(input: string): IParseReturn {
55+
const ret: IParseReturn = {};
56+
57+
input = input.trim().replace(/^[?#&]/, "");
58+
59+
if (!input) {
60+
return ret;
61+
}
62+
63+
for (const param of input.split("&")) {
64+
const [key, value] = param.replace(/\+/g, " ").split("=");
65+
66+
// Missing `=` should be `null`:
67+
// http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
68+
arrayFormatParser(
69+
decodeUriComponent(key),
70+
value === undefined ? null : decodeUriComponent(value),
71+
ret
72+
);
73+
}
74+
75+
return ret;
76+
}
77+
78+
export function stringify(
79+
obj?: IParseInput,
80+
options: IStringifyOptions = {}
81+
): string {
82+
if (!obj) {
83+
return "";
84+
}
85+
const keys = Object.keys(obj);
86+
if (options.sort !== false) {
87+
keys.sort(options.sort);
88+
}
89+
return keys
90+
.map(key => {
91+
const value = obj[key];
92+
93+
if (value === undefined) {
94+
return "";
95+
}
96+
97+
if (value === null) {
98+
return encodeRFC3986ValueChars(key);
99+
}
100+
101+
if (Array.isArray(value)) {
102+
return value
103+
.filter(v => v !== undefined)
104+
.map(v => {
105+
if (typeof v === "number") {
106+
v = v.toString();
107+
}
108+
return arrayFormatEncoder(key, v);
109+
})
110+
.join("&");
111+
}
112+
113+
return `${encodeRFC3986ValueChars(key)}=${encodeRFC3986ValueChars(
114+
value.toString()
115+
)}`;
116+
})
117+
.filter(x => x.length > 0)
118+
.join("&");
119+
}

package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@chaitin/querystring",
3+
"version": "1.0.0",
4+
"description": "Parse and stringify URL query strings",
5+
"main": "index.js",
6+
"scripts": {
7+
"precommit": "lint-staged",
8+
"test": "tsc && ava"
9+
},
10+
"author": "",
11+
"license": "MIT",
12+
"publishConfig": {
13+
"registry": "https://registry.npmjs.org",
14+
"access": "public"
15+
},
16+
"repository": {
17+
"type": "git",
18+
"url": "git+https://github.com/chaitin/querystring"
19+
},
20+
"lint-staged": {
21+
"linters": {
22+
"*.ts": ["prettier --write", "tslint --fix", "git add"],
23+
"*.json": ["prettier --write", "git add"],
24+
"**/*.ts": ["prettier --write", "tslint --fix", "git add"]
25+
},
26+
"concurrent": false
27+
},
28+
"devDependencies": {
29+
"ava": "^0.25.0",
30+
"husky": "^0.14.3",
31+
"lint-staged": "^7.0.4",
32+
"prettier": "^1.12.0",
33+
"tslint": "^5.9.1",
34+
"tslint-config-prettier": "^1.10.0",
35+
"typescript": "^2.8.1"
36+
},
37+
"dependencies": {}
38+
}

test/test_array.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import test from 'ava'
2+
3+
import {arrayFormatEncoder, arrayFormatParser} from '..'
4+
5+
test('encode', t => {
6+
t.is(arrayFormatEncoder('a', null), 'a')
7+
t.is(arrayFormatEncoder('a', 'ddd'), 'a=ddd')
8+
t.is(arrayFormatEncoder('!233!', '!@#"**abc'), '%21233%21=%21%40%23%22%2A%2Aabc')
9+
})

test/test_parse.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import test from "ava";
2+
3+
import { parse, stringify } from "..";
4+
5+
test("query strings starting with a `?`", t => {
6+
t.deepEqual(parse("?foo=bar"), { foo: "bar" });
7+
});
8+
9+
test("query strings starting with a `#`", t => {
10+
t.deepEqual(parse("#foo=bar"), { foo: "bar" });
11+
});
12+
13+
test("query strings starting with a `&`", t => {
14+
t.deepEqual(parse("&foo=bar&foo=baz"), { foo: ["bar", "baz"] });
15+
});
16+
17+
test("parse a query string", t => {
18+
t.deepEqual(parse("foo=bar"), { foo: "bar" });
19+
});
20+
21+
test("parse multiple query string", t => {
22+
t.deepEqual(parse("foo=bar&key=val"), {
23+
foo: "bar",
24+
key: "val"
25+
});
26+
});
27+
28+
test("parse query string without a value", t => {
29+
t.deepEqual(parse("foo"), { foo: null });
30+
t.deepEqual(parse("foo&key"), {
31+
foo: null,
32+
key: null
33+
});
34+
t.deepEqual(parse("foo=bar&key"), {
35+
foo: "bar",
36+
key: null
37+
});
38+
t.deepEqual(parse("a&a"), { a: [null, null] });
39+
t.deepEqual(parse("a=&a"), { a: ["", null] });
40+
});
41+
42+
test("return empty object if no qss can be found", t => {
43+
t.deepEqual(parse("?"), {});
44+
t.deepEqual(parse("&"), {});
45+
t.deepEqual(parse("#"), {});
46+
t.deepEqual(parse(" "), {});
47+
});
48+
49+
test("handle `+` correctly", t => {
50+
t.deepEqual(parse("foo+faz=bar+baz++"), { "foo faz": "bar baz " });
51+
});
52+
53+
test("handle multiple of the same key", t => {
54+
t.deepEqual(parse("foo=bar&foo=baz"), { foo: ["bar", "baz"] });
55+
});
56+
57+
test("handle multiple values and preserve appearence order", t => {
58+
t.deepEqual(parse("a=value&a="), { a: ["value", ""] });
59+
t.deepEqual(parse("a=&a=value"), { a: ["", "value"] });
60+
});
61+
62+
test("query strings params including embedded `=`", t => {
63+
t.deepEqual(parse("?param=https%3A%2F%2Fsomeurl%3Fid%3D2837"), {
64+
param: "https://someurl?id=2837"
65+
});
66+
});
67+
68+
test("query strings having indexed arrays", t => {
69+
t.deepEqual(parse("foo[0]=bar&foo[1]=baz"), {
70+
"foo[0]": "bar",
71+
"foo[1]": "baz"
72+
});
73+
});
74+
75+
test("query strings having brackets arrays", t => {
76+
t.deepEqual(parse("foo[]=bar&foo[]=baz"), { "foo[]": ["bar", "baz"] });
77+
});
78+
79+
test("query strings having indexed arrays keeping index order", t => {
80+
t.deepEqual(parse("foo[1]=bar&foo[0]=baz"), {
81+
"foo[0]": "baz",
82+
"foo[1]": "bar"
83+
});
84+
});
85+
86+
test("circuit parse -> stringify", t => {
87+
const original = "foo=foo&foo&foo=one&foo=&bat=buz";
88+
const sortedOriginal = "bat=buz&foo=foo&foo&foo=one&foo=";
89+
const expected = { bat: "buz", foo: ["foo", null, "one", ""] };
90+
t.deepEqual(parse(original), expected);
91+
92+
t.is(stringify(expected), sortedOriginal);
93+
});
94+
95+
test("circuit original -> parse - > stringify -> sorted original", t => {
96+
const original =
97+
"foo[21474836471]=foo&foo[21474836470]&foo[1]=one&foo[0]=&bat=buz";
98+
const sortedOriginal =
99+
"bat=buz&foo%5B0%5D=&foo%5B1%5D=one&foo%5B21474836470%5D&foo%5B21474836471%5D=foo";
100+
t.deepEqual(stringify(parse(original)), sortedOriginal);
101+
});
102+
103+
test("decode keys and values", t => {
104+
t.deepEqual(parse("st%C3%A5le=foo"), { ståle: "foo" });
105+
t.deepEqual(parse("foo=%7B%25ab%25%7C%25de%25%7D%20%25%7Bst%C3%A5le%7D%25"), {
106+
foo: "{%ab%|%de%} %{ståle}%"
107+
});
108+
});

test/test_stringify.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import test from 'ava'
2+
3+
import { stringify } from '..'
4+
5+
test('stringify', t => {
6+
t.is(stringify({foo: 'bar'}), 'foo=bar');
7+
t.is(stringify({
8+
foo: 'bar',
9+
bar: 'baz'
10+
}), 'bar=baz&foo=bar');
11+
t.is(stringify({'foo bar': 'baz faz'}), 'foo%20bar=baz%20faz');
12+
t.is(stringify({'foo bar': 'baz\'faz'}), 'foo%20bar=baz%27faz');
13+
})
14+
15+
test('handle number', t => {
16+
t.is(stringify({a: 1}), 'a=1')
17+
})
18+
19+
test('handle array value', t => {
20+
t.is(stringify({
21+
page: "10",
22+
pageSize: 200,
23+
filter: ["aaa", "bbb", "ccc", 255]
24+
}), 'filter=aaa&filter=bbb&filter=ccc&filter=255&page=10&pageSize=200')
25+
});
26+
27+
test('no sort', t => {
28+
t.is(stringify({
29+
page: "10",
30+
pageSize: 200,
31+
filter: ["aaa", "bbb", "ccc", 255]
32+
}, {sort: false}), 'page=10&pageSize=200&filter=aaa&filter=bbb&filter=ccc&filter=255')
33+
})
34+
35+
test('handle empty array value', t => {
36+
t.is(stringify({
37+
abc: 'abc',
38+
foo: []
39+
}), 'abc=abc');
40+
});
41+
42+
test('handle null', t => {
43+
t.is(stringify({
44+
pdd: null
45+
}), 'pdd')
46+
t.is(stringify({
47+
c: '1',
48+
d: null,
49+
e: ['1', 2, null, 4]
50+
}), 'c=1&d&e=1&e=2&e&e=4')
51+
})
52+
53+
test('should not encode undefined values', t => {
54+
t.is(stringify({
55+
abc: undefined,
56+
foo: 'baz'
57+
}), 'foo=baz');
58+
});
59+
60+
test('should encode null values as just a key', t => {
61+
t.is(stringify({
62+
'x y z': null,
63+
abc: null,
64+
foo: 'baz'
65+
}), 'abc&foo=baz&x%20y%20z');
66+
});
67+
68+
test('handle null values in array', t => {
69+
t.is(stringify({
70+
foo: null,
71+
bar: [null, 'baz']
72+
}), 'bar&bar=baz&foo');
73+
});
74+
75+
test('handle undefined values in array', t => {
76+
t.is(stringify({
77+
foo: null,
78+
bar: [undefined, 'baz']
79+
}), 'bar=baz&foo');
80+
});
81+
82+
test('handle undefined and null values in array', t => {
83+
t.is(stringify({
84+
foo: null,
85+
bar: [null, 'baz']
86+
}), 'bar&bar=baz&foo');
87+
});
88+
89+
test('strict encoding', t => {
90+
t.is(stringify({foo: '\'bar\''}), 'foo=%27bar%27');
91+
t.is(stringify({foo: ["'bar'", "!baz"]}), 'foo=%27bar%27&foo=%21baz');
92+
});

0 commit comments

Comments
 (0)