Skip to content

Commit 1ff72c6

Browse files
committed
feat: Add support for implied string literals
1 parent 59f3de2 commit 1ff72c6

File tree

4 files changed

+209
-61
lines changed

4 files changed

+209
-61
lines changed

src/JsonURL.js

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,39 @@ function parseLiteralLength(text, i, end, errmsg) {
275275
return end;
276276
}
277277

278+
function parseStringLiteral(text, pos, end, removeQuotes) {
279+
let ret = removeQuotes
280+
? text.substring(pos + 1, end - 1)
281+
: text.substring(pos, end);
282+
283+
ret = ret.replace(rx_decode_space, " ");
284+
ret = decodeURIComponent(ret);
285+
286+
return ret;
287+
}
288+
289+
function encodeStringLiteral(text) {
290+
let ret = encodeURIComponent(text);
291+
ret = ret.replace(rx_encode_pctspace, "+");
292+
ret = ret.replace(",", "%x2C");
293+
ret = ret.replace(":", "%x3A");
294+
ret = ret.replace("(", "%x28");
295+
ret = ret.replace(")", "%x29");
296+
297+
return ret;
298+
}
299+
278300
function toJsonURLText_Boolean() {
279301
return this === true ? "true" : "false";
280302
}
281303
function toJsonURLText_Number() {
282304
return String(this);
283305
}
284306
function toJsonURLText_String(options, depth, isKey) {
307+
if (options.impliedStringLiterals) {
308+
return encodeStringLiteral(this);
309+
}
310+
285311
if (this.length === 0) {
286312
return "''";
287313
}
@@ -324,8 +350,7 @@ function toJsonURLText_String(options, depth, isKey) {
324350
return "'" + this.replace(rx_encode_space, "+") + "'";
325351
}
326352

327-
let ret = encodeURIComponent(this);
328-
ret = ret.replace(rx_encode_pctspace, "+");
353+
let ret = encodeStringLiteral(this);
329354

330355
if (ret.charCodeAt(0) == CHAR_QUOTE) {
331356
//
@@ -638,7 +663,17 @@ class JsonURL {
638663
* @param {boolean} forceString True if the resulting literal should be
639664
* a string, such as in the case of an object key.
640665
*/
641-
parseLiteral(text, pos = 0, end = text.length, forceString = false) {
666+
parseLiteral(
667+
text,
668+
pos = 0,
669+
end = text.length,
670+
forceString = false,
671+
impliedStringLiteral = false
672+
) {
673+
if (impliedStringLiteral === true) {
674+
return parseStringLiteral(text, pos, end, false);
675+
}
676+
642677
let c1, c2, c3, c4, c5;
643678

644679
switch (end - pos) {
@@ -707,18 +742,7 @@ class JsonURL {
707742
//
708743
// this is a string
709744
//
710-
var ret;
711-
712-
if (isQuotedString) {
713-
ret = text.substring(pos + 1, end - 1);
714-
} else {
715-
ret = text.substring(pos, end);
716-
}
717-
718-
ret = ret.replace(rx_decode_space, " ");
719-
ret = decodeURIComponent(ret);
720-
721-
return ret;
745+
return parseStringLiteral(text, pos, end, isQuotedString);
722746
}
723747

724748
/**
@@ -745,6 +769,8 @@ class JsonURL {
745769
* is may use ampersand and equal characters as the value and member
746770
* separator characters, respetively, at the top-level. This may be
747771
* combined with prop.impliedArray or prop.impliedObject.
772+
* @param {boolean} options.impliedStringLiterals Assume all literals
773+
* are strings.
748774
* @throws SyntaxError if there is a syntax error in the given text
749775
* @throws Error if a limit given in the constructor (or its default)
750776
* is exceeded.
@@ -798,7 +824,13 @@ class JsonURL {
798824
throw new SyntaxError(errorMessage(ERR_MSG_EXPECT_LITERAL, 0));
799825
}
800826

801-
return this.parseLiteral(text, 0, end, false);
827+
return this.parseLiteral(
828+
text,
829+
0,
830+
end,
831+
false,
832+
options.impliedStringLiterals
833+
);
802834
} else {
803835
stateStack.push(STATE_PAREN);
804836
pos = 1;
@@ -878,7 +910,13 @@ class JsonURL {
878910
valueStack.checkValueLimit(pos);
879911

880912
c = text.charCodeAt(lvpos);
881-
lv = this.parseLiteral(text, pos, lvpos, c === CHAR_COLON);
913+
lv = this.parseLiteral(
914+
text,
915+
pos,
916+
lvpos,
917+
c === CHAR_COLON,
918+
options.impliedStringLiterals
919+
);
882920
pos = lvpos;
883921

884922
switch (c) {
@@ -963,7 +1001,13 @@ class JsonURL {
9631001
lvpos = parseLiteralLength(text, pos, end, ERR_MSG_EXPECT_VALUE);
9641002

9651003
valueStack.checkValueLimit(pos);
966-
lv = this.parseLiteral(text, pos, lvpos, false);
1004+
lv = this.parseLiteral(
1005+
text,
1006+
pos,
1007+
lvpos,
1008+
false,
1009+
options.impliedStringLiterals
1010+
);
9671011
pos = lvpos;
9681012

9691013
if (pos === end) {
@@ -1039,7 +1083,13 @@ class JsonURL {
10391083
lvpos = parseLiteralLength(text, pos, end, ERR_MSG_EXPECT_VALUE);
10401084

10411085
valueStack.checkValueLimit(pos);
1042-
lv = this.parseLiteral(text, pos, lvpos, false);
1086+
lv = this.parseLiteral(
1087+
text,
1088+
pos,
1089+
lvpos,
1090+
false,
1091+
options.impliedStringLiterals
1092+
);
10431093
pos = lvpos;
10441094

10451095
if (lvpos === end) {
@@ -1132,7 +1182,13 @@ class JsonURL {
11321182
throw new SyntaxError((ERR_MSG_EXPECT_OBJVALUE, lvpos));
11331183
}
11341184

1135-
lv = this.parseLiteral(text, pos, lvpos, true);
1185+
lv = this.parseLiteral(
1186+
text,
1187+
pos,
1188+
lvpos,
1189+
true,
1190+
options.impliedStringLiterals
1191+
);
11361192
pos = lvpos + 1;
11371193

11381194
stateStack.replace(STATE_OBJECT_HAVE_KEY);
@@ -1167,10 +1223,18 @@ class JsonURL {
11671223
* structural characters.
11681224
* @param {boolean} options.isImplied Create JSON->URL text for an implied
11691225
* array or object.
1226+
* @param {boolean} options.impliedStringLiterals Assume all literals
1227+
* are strings.
11701228
* @returns {string} JSON->URL text, or undefined if the given value
11711229
* is undefined.
11721230
*/
1173-
static stringify(value, options = { ignoreUndefinedObjectMembers: true }) {
1231+
static stringify(
1232+
value,
1233+
options = {
1234+
ignoreUndefinedObjectMembers: true,
1235+
impliedStringLiterals: false,
1236+
}
1237+
) {
11741238
if (value === undefined) {
11751239
return undefined;
11761240
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
MIT License
3+
4+
Copyright (c) 2020 David MacCormack
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
*/
24+
25+
import JsonURL from "../src/JsonURL.js";
26+
27+
const u = new JsonURL();
28+
29+
test.each([
30+
["()", u.emptyValue],
31+
[
32+
"(true:true,false:false,null:null,empty:(),single:(0),nested:((1)),many:(-1,2.0,3e1,4e-2,5e+0))",
33+
{
34+
true: "true",
35+
false: "false",
36+
null: "null",
37+
empty: {},
38+
single: ["0"],
39+
nested: [["1"]],
40+
many: ["-1", "2.0", "3e1", "4e-2", "5e 0"],
41+
},
42+
],
43+
["(1)", ["1"]],
44+
["(1,(2))", ["1", ["2"]]],
45+
["(1,(a:2),3)", ["1", { a: "2" }, "3"]],
46+
[
47+
"(age:64,name:(first:Fred))",
48+
{
49+
age: "64",
50+
name: { first: "Fred" },
51+
},
52+
],
53+
["(null,null)", ["null", "null"]],
54+
["(a:b,c:d,e:f)", { a: "b", c: "d", e: "f" }],
55+
["1e%2B1", "1e+1"],
56+
["'Hello,+(World)!'", "'Hello, (World)!'"],
57+
["Bob's+house", "Bob's house"],
58+
["('Hello,+(World)!')", ["'Hello, (World)!'"]],
59+
["('','')", ["''", "''"]],
60+
["('qkey':g)", { "'qkey'": "g" }],
61+
])("JsonURL.parse(%s)", (text, expected) => {
62+
expect(u.parse(text, { impliedStringLiterals: true })).toEqual(expected);
63+
64+
//
65+
// verify that stringify returns the same content as the
66+
// original text.
67+
//
68+
// expect(JsonURL.stringify(expected)).toBe(
69+
// text
70+
// .replace("3e1", "30")
71+
// .replace("2.0", "2")
72+
// .replace("4e-2", "0.04")
73+
// .replace("5e+0", "5")
74+
// .replace("'qkey'", "qkey")
75+
// );
76+
});

test/parseLiteral.test.js

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ function encodedString(s) {
3434
.replace(/:/, "%3A");
3535
}
3636

37-
function runTest(text, value, keyValue) {
37+
function runTest(text, value, keyValue, strLitValue) {
3838
expect(u.parseLiteral(text)).toBe(value);
3939
expect(u.parseLiteral(text, 0, text.length, true)).toBe(keyValue);
40+
expect(u.parseLiteral(text, 0, text.length, true, true)).toBe(strLitValue);
4041

4142
//
4243
// verify that parseLiteral() and parse() return the same thing (as
@@ -116,36 +117,39 @@ test.each([
116117
])("JsonURL.parseLiteral(%p)", (value) => {
117118
let keyValue = String(value);
118119
let text = typeof value === "string" ? encodedString(keyValue) : keyValue;
119-
runTest(text, value, keyValue);
120+
runTest(text, value, keyValue, keyValue);
120121
});
121122

122123
test.each([
123124
//
124125
// fixed point
125126
//
126-
["-3e0", -3],
127-
["1e+2", 1e2],
128-
["-2e+1", -2e1],
127+
["-3e0", -3, "-3e0"],
128+
["1e+2", 1e2, "1e 2"],
129+
["-2e+1", -2e1, "-2e 1"],
129130

130131
//
131132
// floating point
132133
//
133-
["156.911e+2", 156.911e2],
134+
["156.911e+2", 156.911e2, "156.911e 2"],
134135

135136
//
136137
// string
137138
//
138-
["'hello'", "hello"],
139-
["hello%2Bworld", "hello+world"],
140-
["y+%3D+mx+%2B+b", "y = mx + b"],
141-
["a%3Db%26c%3Dd", "a=b&c=d"],
142-
["hello%F0%9F%8D%95world", "hello\uD83C\uDF55world"],
143-
["-e+", "-e "],
144-
["-e+1", "-e 1"],
145-
["1e%2B1", "1e+1"],
146-
])("JsonURL.parseLiteral(%p)", (text, value) => {
139+
["'hello'", "hello", "'hello'", undefined],
140+
["hello%2Bworld", "hello+world", undefined],
141+
["y+%3D+mx+%2B+b", "y = mx + b", undefined],
142+
["a%3Db%26c%3Dd", "a=b&c=d", undefined],
143+
["hello%F0%9F%8D%95world", "hello\uD83C\uDF55world", undefined],
144+
["-e+", "-e ", undefined],
145+
["-e+1", "-e 1", undefined],
146+
["1e%2B1", "1e+1", undefined],
147+
])("JsonURL.parseLiteral(%p)", (text, value, strLitValue) => {
147148
let keyValue = typeof value === "string" ? value : text;
148-
runTest(text, value, keyValue);
149+
if (strLitValue === undefined) {
150+
strLitValue = value;
151+
}
152+
runTest(text, value, keyValue, strLitValue);
149153
});
150154

151155
test("JsonURL.parseLiteral('null')", () => {

test/stringify.test.js

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,35 @@ test("JsonURL.stringify(undefined)", () => {
3333
});
3434

3535
test.each([
36-
[true, "true"],
37-
[false, "false"],
38-
[null, "null"],
39-
["true", "'true'"],
40-
["false", "'false'"],
41-
["null", "'null'"],
42-
["()", "'()'"],
43-
["{}", "%7B%7D"],
44-
[0, "0"],
45-
[1.1, "1.1"],
46-
["a", "a"],
47-
["1", "'1'"],
48-
["2.3", "'2.3'"],
49-
["2e1", "'2e1'"],
50-
["-4", "'-4'"],
51-
["5a", "5a"],
52-
["'1+2'", "%271%2B2'"],
53-
["1e+1", "1e%2B1"],
54-
["a b c", "a+b+c"],
55-
["a,b", "'a,b'"],
56-
["a,b c", "'a,b+c'"],
57-
["Bob & Frank", "Bob+%26+Frank"],
58-
["'hello", "%27hello"],
59-
])("JsonURL.stringify(%p)", (value, expected) => {
36+
[true, "true", "true"],
37+
[false, "false", "false"],
38+
[null, "null", "null"],
39+
["true", "'true'", "true"],
40+
["false", "'false'", "false"],
41+
["null", "'null'", "null"],
42+
["()", "'()'", "%x28%x29"],
43+
["{}", "%7B%7D", "%7B%7D"],
44+
[0, "0", "0"],
45+
[1.1, "1.1", "1.1"],
46+
["a", "a", "a"],
47+
["1", "'1'", "1"],
48+
["2.3", "'2.3'", "2.3"],
49+
["2e1", "'2e1'", "2e1"],
50+
["-4", "'-4'", "-4"],
51+
["5a", "5a", "5a"],
52+
["'1+2'", "%271%2B2'", "'1%2B2'"],
53+
["1e+1", "1e%2B1", "1e%2B1"],
54+
["a b c", "a+b+c", "a+b+c"],
55+
["a,b", "'a,b'", "a%2Cb"],
56+
["a,b c", "'a,b+c'", "a%2Cb+c"],
57+
["Bob & Frank", "Bob+%26+Frank", "Bob+%26+Frank"],
58+
["'hello", "%27hello", "'hello"],
59+
])("JsonURL.stringify(%p)", (value, expected, expectedISL) => {
6060
expect(JsonURL.stringify(value)).toBe(expected);
61+
62+
expect(JsonURL.stringify(value, { impliedStringLiterals: true })).toBe(
63+
expectedISL
64+
);
6165
});
6266

6367
test.each([

0 commit comments

Comments
 (0)