Skip to content

Commit 202fb34

Browse files
committed
update: Add parse and stringify option noEmptyComposite
1 parent 7652416 commit 202fb34

File tree

2 files changed

+134
-11
lines changed

2 files changed

+134
-11
lines changed

src/JsonURL.js

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ function parseIntegerLength(text, i, len) {
214214
return parseDigitsLength(text, i, len);
215215
}
216216

217-
function newEmptyValue(p) {
217+
function newEmptyValue(p, options) {
218+
if (options.noEmptyComposite) {
219+
return [];
220+
}
218221
if (typeof p.emptyValue === "function") {
219222
return p.emptyValue();
220223
}
@@ -600,6 +603,9 @@ function toJsonURLText_Object(options = {}, depth = 0) {
600603
});
601604

602605
if (!options.isImplied || depth > 0) {
606+
if (options.noEmptyComposite && ret === undefined) {
607+
ret = ":";
608+
}
603609
return ret === undefined ? "()" : "(" + ret + ")";
604610
}
605611

@@ -898,6 +904,11 @@ class JsonURL {
898904
* than back-to-back single quotes.
899905
* @param {boolean} options.coerceNullToEmptyString Replace
900906
* instances of the null value with an empty string.
907+
* @param {boolean} options.noEmptyComposite Distinguish
908+
* between empty array and empty object. Empty array is back-to-back parens,
909+
* e.g. (). Empty object is two parens with a single colon inside, e.g. (:).
910+
* Note that this prevents the parser from recognizing (:) as an object
911+
* with a single member whose key and value is the unquoted empty string.
901912
* @param {function} options.getMissingValue Provides a value for a
902913
* missing, top-level value.
903914
* @throws SyntaxError if there is a syntax error in the given text
@@ -990,9 +1001,11 @@ class JsonURL {
9901001
let c = text.charCodeAt(pos);
9911002

9921003
//
993-
// literal value and literal value position
1004+
// literal value
1005+
// literal value position
1006+
// empty object bool
9941007
//
995-
let lv, lvpos;
1008+
let lv, lvpos, isEmptyObject;
9961009

9971010
switch (stateStack[stateStack.depth()]) {
9981011
case STATE_PAREN:
@@ -1019,12 +1032,12 @@ class JsonURL {
10191032

10201033
if (stateStack.depth(true) === -1) {
10211034
if (pos === end) {
1022-
return newEmptyValue(this);
1035+
return newEmptyValue(this, options);
10231036
}
10241037
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
10251038
}
10261039

1027-
valueStack.appendArrayValue(pos, newEmptyValue(this));
1040+
valueStack.appendArrayValue(pos, newEmptyValue(this, options));
10281041

10291042
if (stateStack.depth() === 0) {
10301043
if (skipAmps) {
@@ -1056,12 +1069,35 @@ class JsonURL {
10561069
}
10571070

10581071
//
1059-
// run the limit check before parsing the literal
1072+
// run the limit check
10601073
//
10611074
valueStack.checkValueLimit(pos);
10621075

1076+
isEmptyObject =
1077+
options.noEmptyComposite &&
1078+
pos == lvpos &&
1079+
pos + 2 <= end &&
1080+
text.charCodeAt(lvpos) == CHAR_COLON &&
1081+
text.charCodeAt(lvpos + 1) == CHAR_PAREN_CLOSE;
1082+
1083+
if (isEmptyObject) {
1084+
// skip the colon so that we hit the close paren case in the
1085+
// switch below
1086+
lvpos++;
1087+
}
1088+
1089+
//
1090+
// char immediately following the literal
1091+
//
10631092
c = text.charCodeAt(lvpos);
1064-
lv = this.parseLiteral(text, pos, lvpos, c === CHAR_COLON, options);
1093+
1094+
if (!isEmptyObject) {
1095+
//
1096+
// parse as a literal if it's a literal
1097+
//
1098+
lv = this.parseLiteral(text, pos, lvpos, c === CHAR_COLON, options);
1099+
}
1100+
10651101
pos = lvpos;
10661102

10671103
switch (c) {
@@ -1087,10 +1123,14 @@ class JsonURL {
10871123
case CHAR_PAREN_CLOSE:
10881124
pos++;
10891125

1090-
//
1091-
// single element array
1092-
//
1093-
valueStack.appendArrayValue(pos, [lv]);
1126+
if (isEmptyObject) {
1127+
valueStack.push({});
1128+
} else {
1129+
//
1130+
// single element array
1131+
//
1132+
valueStack.appendArrayValue(pos, [lv]);
1133+
}
10941134

10951135
switch (stateStack.depth(true)) {
10961136
case -1:
@@ -1404,6 +1444,9 @@ class JsonURL {
14041444
* bac-to-back single quotes.
14051445
* @param {boolean} options.coerceNullToEmptyString Replace instances
14061446
* of the null value with an empty string.
1447+
* @param {boolean} options.noEmptyComposite Distinguish
1448+
* between empty array and empty object. Empty array is back-to-back parens,
1449+
* e.g. (). Empty object is two parens with a single colon inside, e.g. (:).
14071450
* @returns {string} JSON->URL text, or undefined if the given value
14081451
* is undefined.
14091452
*/

test/parseNoComposite.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
["()", { noEmptyComposite: true }, []],
31+
["(:)", { noEmptyComposite: true }, {}],
32+
["('':'')", { noEmptyComposite: true }, { "": "" }],
33+
[
34+
"('':)",
35+
{ noEmptyComposite: true, allowEmptyUnquotedValues: true },
36+
{ "": "" },
37+
],
38+
[
39+
"(:'')",
40+
{ noEmptyComposite: true, allowEmptyUnquotedKeys: true },
41+
{ "": "" },
42+
],
43+
[
44+
"(:)",
45+
{
46+
noEmptyComposite: true,
47+
allowEmptyUnquotedValues: true,
48+
allowEmptyUnquotedKeys: true,
49+
},
50+
{},
51+
],
52+
[
53+
"(a:b,:)",
54+
{
55+
noEmptyComposite: true,
56+
allowEmptyUnquotedValues: true,
57+
allowEmptyUnquotedKeys: true,
58+
},
59+
{ a: "b", "": "" },
60+
],
61+
[
62+
"a:(:),b:()",
63+
{ noEmptyComposite: true, impliedObject: {} },
64+
{ a: {}, b: [] },
65+
],
66+
["(:)", { noEmptyComposite: true, impliedArray: [] }, [{}]],
67+
["()", { noEmptyComposite: true, impliedArray: [] }, [[]]],
68+
["(:),()", { noEmptyComposite: true, impliedArray: [] }, [{}, []]],
69+
["(),(:)", { noEmptyComposite: true, impliedArray: [] }, [[], {}]],
70+
])("JsonURL.parse(%s)", (text, options, expected) => {
71+
const parseOptions = JSON.parse(JSON.stringify(options));
72+
const actual = u.parse(text, parseOptions);
73+
expect(actual).toEqual(expected);
74+
75+
let stringifyOptions = JSON.parse(JSON.stringify(options));
76+
if (options.impliedArray || options.impliedObject) {
77+
stringifyOptions.isImplied = true;
78+
}
79+
expect(JsonURL.stringify(actual, stringifyOptions)).toEqual(text);
80+
});

0 commit comments

Comments
 (0)