Skip to content

Commit 2d3f52f

Browse files
committed
feat(@angular-devkit/core): better support for JSON5
Supports for new lines and hexa was useful for doc and descriptions (including schematics and builder declarations). Adding the other numerical constants was easy enough. This fully completes JSON5 support.
1 parent eaccc04 commit 2d3f52f

File tree

2 files changed

+177
-13
lines changed

2 files changed

+177
-13
lines changed

packages/angular_devkit/core/src/json/parser.ts

Lines changed: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,38 @@ function _readExpNumber(context: JsonParserContext,
157157
}
158158

159159

160+
/**
161+
* Read the hexa part of a 0xBADCAFE hexadecimal number.
162+
* @private
163+
*/
164+
function _readHexaNumber(context: JsonParserContext,
165+
isNegative: boolean,
166+
start: Position,
167+
comments: (JsonAstComment | JsonAstMultilineComment)[]): JsonAstNumber {
168+
// Read an hexadecimal number, until it's not hexadecimal.
169+
let hexa = '';
170+
const valid = '0123456789abcdefABCDEF';
171+
172+
for (let ch = _peek(context); ch && valid.includes(ch); ch = _peek(context)) {
173+
// Add it to the hexa string.
174+
hexa += ch;
175+
// Move the position of the context to the next character.
176+
_next(context);
177+
}
178+
179+
const value = Number.parseInt(hexa, 16);
180+
181+
// We're done reading this number.
182+
return {
183+
kind: 'number',
184+
start,
185+
end: context.position,
186+
text: context.original.substring(start.offset, context.position.offset),
187+
value: isNegative ? -value : value,
188+
comments,
189+
};
190+
}
191+
160192
/**
161193
* Read a number from the context.
162194
* @private
@@ -175,6 +207,21 @@ function _readNumber(context: JsonParserContext, comments = _readBlanks(context)
175207
if (str != '') {
176208
throw new InvalidJsonCharacterException(context);
177209
}
210+
} else if (char == 'I'
211+
&& (str == '-' || str == '' || str == '+')
212+
&& (context.mode & JsonParseMode.NumberConstantsAllowed) != 0) {
213+
// Infinity?
214+
// _token(context, 'I'); Already read.
215+
_token(context, 'n');
216+
_token(context, 'f');
217+
_token(context, 'i');
218+
_token(context, 'n');
219+
_token(context, 'i');
220+
_token(context, 't');
221+
_token(context, 'y');
222+
223+
str += 'Infinity';
224+
break;
178225
} else if (char == '0') {
179226
if (str == '0' || str == '-0') {
180227
throw new InvalidJsonCharacterException(context);
@@ -184,29 +231,40 @@ function _readNumber(context: JsonParserContext, comments = _readBlanks(context)
184231
if (str == '0' || str == '-0') {
185232
throw new InvalidJsonCharacterException(context);
186233
}
234+
} else if (char == '+' && str == '') {
235+
// Pass over.
187236
} else if (char == '.') {
188237
if (dotted) {
189238
throw new InvalidJsonCharacterException(context);
190239
}
191240
dotted = true;
192241
} else if (char == 'e' || char == 'E') {
193242
return _readExpNumber(context, start, str + char, comments);
243+
} else if (char == 'x' && (str == '0' || str == '-0')
244+
&& (context.mode & JsonParseMode.HexadecimalNumberAllowed) != 0) {
245+
return _readHexaNumber(context, str == '-0', start, comments);
194246
} else {
195-
// We're done reading this number.
247+
// We read one too many characters, so rollback the last character.
196248
context.position = context.previous;
197-
198-
return {
199-
kind: 'number',
200-
start,
201-
end: context.position,
202-
text: context.original.substring(start.offset, context.position.offset),
203-
value: Number.parseFloat(str),
204-
comments,
205-
};
249+
break;
206250
}
207251

208252
str += char;
209253
}
254+
255+
// We're done reading this number.
256+
if (str.endsWith('.') && (context.mode & JsonParseMode.HexadecimalNumberAllowed) == 0) {
257+
throw new InvalidJsonCharacterException(context);
258+
}
259+
260+
return {
261+
kind: 'number',
262+
start,
263+
end: context.position,
264+
text: context.original.substring(start.offset, context.position.offset),
265+
value: Number.parseFloat(str),
266+
comments,
267+
};
210268
}
211269

212270

@@ -224,8 +282,6 @@ function _readString(context: JsonParserContext, comments = _readBlanks(context)
224282
if (delim == '\'') {
225283
throw new InvalidJsonCharacterException(context);
226284
}
227-
} else if (delim != '\'' && delim != '"') {
228-
throw new InvalidJsonCharacterException(context);
229285
}
230286

231287
let str = '';
@@ -265,6 +321,15 @@ function _readString(context: JsonParserContext, comments = _readBlanks(context)
265321

266322
case undefined:
267323
throw new UnexpectedEndOfInputException(context);
324+
325+
case '\n':
326+
// Only valid when multiline strings are allowed.
327+
if ((context.mode & JsonParseMode.MultiLineStringAllowed) == 0) {
328+
throw new InvalidJsonCharacterException(context);
329+
}
330+
str += char;
331+
break;
332+
268333
default:
269334
throw new InvalidJsonCharacterException(context);
270335
}
@@ -356,6 +421,31 @@ function _readNull(context: JsonParserContext,
356421
}
357422

358423

424+
/**
425+
* Read the constant `NaN` from the context.
426+
* @private
427+
*/
428+
function _readNaN(context: JsonParserContext,
429+
comments = _readBlanks(context)): JsonAstNumber {
430+
const start = context.position;
431+
432+
_token(context, 'N');
433+
_token(context, 'a');
434+
_token(context, 'N');
435+
436+
const end = context.position;
437+
438+
return {
439+
kind: 'number',
440+
start,
441+
end,
442+
text: context.original.substring(start.offset, end.offset),
443+
value: NaN,
444+
comments: comments,
445+
};
446+
}
447+
448+
359449
/**
360450
* Read an array of JSON values from the context.
361451
* @private
@@ -638,11 +728,33 @@ function _readValue(context: JsonParserContext, comments = _readBlanks(context))
638728
result = _readNumber(context, comments);
639729
break;
640730

731+
case '.':
732+
case '+':
733+
if ((context.mode & JsonParseMode.LaxNumberParsingAllowed) == 0) {
734+
throw new InvalidJsonCharacterException(context);
735+
}
736+
result = _readNumber(context, comments);
737+
break;
738+
641739
case '\'':
642740
case '"':
643741
result = _readString(context, comments);
644742
break;
645743

744+
case 'I':
745+
if ((context.mode & JsonParseMode.NumberConstantsAllowed) == 0) {
746+
throw new InvalidJsonCharacterException(context);
747+
}
748+
result = _readNumber(context, comments);
749+
break;
750+
751+
case 'N':
752+
if ((context.mode & JsonParseMode.NumberConstantsAllowed) == 0) {
753+
throw new InvalidJsonCharacterException(context);
754+
}
755+
result = _readNaN(context, comments);
756+
break;
757+
646758
case 't':
647759
result = _readTrue(context, comments);
648760
break;
@@ -681,10 +793,19 @@ export enum JsonParseMode {
681793
SingleQuotesAllowed = 1 << 1, // Allow single quoted strings.
682794
IdentifierKeyNamesAllowed = 1 << 2, // Allow identifiers as objectp properties.
683795
TrailingCommasAllowed = 1 << 3,
796+
HexadecimalNumberAllowed = 1 << 4,
797+
MultiLineStringAllowed = 1 << 5,
798+
LaxNumberParsingAllowed = 1 << 6, // Allow `.` or `+` as the first character of a number.
799+
NumberConstantsAllowed = 1 << 7, // Allow -Infinity, Infinity and NaN.
684800

685801
Default = Strict,
686802
Loose = CommentsAllowed | SingleQuotesAllowed |
687-
IdentifierKeyNamesAllowed | TrailingCommasAllowed,
803+
IdentifierKeyNamesAllowed | TrailingCommasAllowed |
804+
HexadecimalNumberAllowed | MultiLineStringAllowed |
805+
LaxNumberParsingAllowed | NumberConstantsAllowed,
806+
807+
Json = Strict,
808+
Json5 = Loose,
688809
}
689810

690811

packages/angular_devkit/core/src/json/parser_spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ describe('parseJson and parseJsonAst', () => {
6767
'-0-0',
6868
'0.0.0',
6969
'0\n.0\n.0',
70+
'0.',
71+
'+1',
72+
'Infinity',
73+
'NaN',
74+
'-Infinity',
75+
'+Infinity',
7076
];
7177

7278
for (const [n, [start, end, text]] of entries(numbers)) {
@@ -105,6 +111,7 @@ describe('parseJson and parseJsonAst', () => {
105111
'"a\\zb"',
106112
'"a',
107113
'"a\nb"',
114+
'"\\\n "',
108115
];
109116

110117
for (const [n, [start, end, text]] of entries(strings)) {
@@ -248,11 +255,20 @@ describe('parseJson and parseJsonAst', () => {
248255
'{hi:["hello",/* */]}': [[0, 0, 0], [20, 0, 20], {hi: ['hello']}],
249256
'{hi:["hello"/* */,]}': [[0, 0, 0], [20, 0, 20], {hi: ['hello']}],
250257
'{hi:["hello" , ] , }': [[0, 0, 0], [20, 0, 20], {hi: ['hello']}],
258+
'{hi:"\\\n "}': [[0, 0, 0], [10, 1, 3], {hi: '\n '}],
259+
'{d: -0xdecaf, e: Infinity, f: -Infinity, g: +Infinity, h: NaN,}': [[0, 0, 0], [63, 0, 63], {
260+
d: -0xdecaf,
261+
e: Infinity,
262+
f: -Infinity,
263+
g: Infinity,
264+
h: NaN,
265+
}],
251266
};
252267
const errors = [
253268
'{1b: 0}',
254269
' /*',
255270
'',
271+
'.Infinity',
256272
];
257273

258274
for (const [n, [start, end, value, text]] of entries(strings)) {
@@ -300,5 +316,32 @@ describe('parseJson and parseJsonAst', () => {
300316
c: 2,
301317
});
302318
});
319+
320+
it('works with json5.org example', () => {
321+
const input = `{
322+
// comments
323+
unquoted: 'and you can quote me on that',
324+
'singleQuotes': 'I can use "double quotes" here',
325+
lineBreaks: "Look, Mom! \\
326+
No \\\\n's!",
327+
hexadecimal: 0xdecaf,
328+
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
329+
positiveSign: +1,
330+
trailingComma: 'in objects', andIn: ['arrays',],
331+
"backwardsCompatible": "with JSON",
332+
}`;
333+
334+
expect(parseJson(input, JsonParseMode.Json5)).toEqual({
335+
unquoted: 'and you can quote me on that',
336+
singleQuotes: 'I can use "double quotes" here',
337+
lineBreaks: "Look, Mom! \nNo \\n's!",
338+
hexadecimal: 0xdecaf,
339+
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
340+
positiveSign: +1,
341+
trailingComma: 'in objects',
342+
andIn: ['arrays'],
343+
backwardsCompatible: 'with JSON',
344+
});
345+
});
303346
});
304347
});

0 commit comments

Comments
 (0)