mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-15 18:13:38 +08:00
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.
This commit is contained in:
parent
eaccc04852
commit
2d3f52f942
@ -157,6 +157,38 @@ function _readExpNumber(context: JsonParserContext,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read the hexa part of a 0xBADCAFE hexadecimal number.
|
||||
* @private
|
||||
*/
|
||||
function _readHexaNumber(context: JsonParserContext,
|
||||
isNegative: boolean,
|
||||
start: Position,
|
||||
comments: (JsonAstComment | JsonAstMultilineComment)[]): JsonAstNumber {
|
||||
// Read an hexadecimal number, until it's not hexadecimal.
|
||||
let hexa = '';
|
||||
const valid = '0123456789abcdefABCDEF';
|
||||
|
||||
for (let ch = _peek(context); ch && valid.includes(ch); ch = _peek(context)) {
|
||||
// Add it to the hexa string.
|
||||
hexa += ch;
|
||||
// Move the position of the context to the next character.
|
||||
_next(context);
|
||||
}
|
||||
|
||||
const value = Number.parseInt(hexa, 16);
|
||||
|
||||
// We're done reading this number.
|
||||
return {
|
||||
kind: 'number',
|
||||
start,
|
||||
end: context.position,
|
||||
text: context.original.substring(start.offset, context.position.offset),
|
||||
value: isNegative ? -value : value,
|
||||
comments,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a number from the context.
|
||||
* @private
|
||||
@ -175,6 +207,21 @@ function _readNumber(context: JsonParserContext, comments = _readBlanks(context)
|
||||
if (str != '') {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
} else if (char == 'I'
|
||||
&& (str == '-' || str == '' || str == '+')
|
||||
&& (context.mode & JsonParseMode.NumberConstantsAllowed) != 0) {
|
||||
// Infinity?
|
||||
// _token(context, 'I'); Already read.
|
||||
_token(context, 'n');
|
||||
_token(context, 'f');
|
||||
_token(context, 'i');
|
||||
_token(context, 'n');
|
||||
_token(context, 'i');
|
||||
_token(context, 't');
|
||||
_token(context, 'y');
|
||||
|
||||
str += 'Infinity';
|
||||
break;
|
||||
} else if (char == '0') {
|
||||
if (str == '0' || str == '-0') {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
@ -184,6 +231,8 @@ function _readNumber(context: JsonParserContext, comments = _readBlanks(context)
|
||||
if (str == '0' || str == '-0') {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
} else if (char == '+' && str == '') {
|
||||
// Pass over.
|
||||
} else if (char == '.') {
|
||||
if (dotted) {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
@ -191,22 +240,31 @@ function _readNumber(context: JsonParserContext, comments = _readBlanks(context)
|
||||
dotted = true;
|
||||
} else if (char == 'e' || char == 'E') {
|
||||
return _readExpNumber(context, start, str + char, comments);
|
||||
} else if (char == 'x' && (str == '0' || str == '-0')
|
||||
&& (context.mode & JsonParseMode.HexadecimalNumberAllowed) != 0) {
|
||||
return _readHexaNumber(context, str == '-0', start, comments);
|
||||
} else {
|
||||
// We're done reading this number.
|
||||
// We read one too many characters, so rollback the last character.
|
||||
context.position = context.previous;
|
||||
|
||||
return {
|
||||
kind: 'number',
|
||||
start,
|
||||
end: context.position,
|
||||
text: context.original.substring(start.offset, context.position.offset),
|
||||
value: Number.parseFloat(str),
|
||||
comments,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
str += char;
|
||||
}
|
||||
|
||||
// We're done reading this number.
|
||||
if (str.endsWith('.') && (context.mode & JsonParseMode.HexadecimalNumberAllowed) == 0) {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'number',
|
||||
start,
|
||||
end: context.position,
|
||||
text: context.original.substring(start.offset, context.position.offset),
|
||||
value: Number.parseFloat(str),
|
||||
comments,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -224,8 +282,6 @@ function _readString(context: JsonParserContext, comments = _readBlanks(context)
|
||||
if (delim == '\'') {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
} else if (delim != '\'' && delim != '"') {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
|
||||
let str = '';
|
||||
@ -265,6 +321,15 @@ function _readString(context: JsonParserContext, comments = _readBlanks(context)
|
||||
|
||||
case undefined:
|
||||
throw new UnexpectedEndOfInputException(context);
|
||||
|
||||
case '\n':
|
||||
// Only valid when multiline strings are allowed.
|
||||
if ((context.mode & JsonParseMode.MultiLineStringAllowed) == 0) {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
str += char;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
@ -356,6 +421,31 @@ function _readNull(context: JsonParserContext,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read the constant `NaN` from the context.
|
||||
* @private
|
||||
*/
|
||||
function _readNaN(context: JsonParserContext,
|
||||
comments = _readBlanks(context)): JsonAstNumber {
|
||||
const start = context.position;
|
||||
|
||||
_token(context, 'N');
|
||||
_token(context, 'a');
|
||||
_token(context, 'N');
|
||||
|
||||
const end = context.position;
|
||||
|
||||
return {
|
||||
kind: 'number',
|
||||
start,
|
||||
end,
|
||||
text: context.original.substring(start.offset, end.offset),
|
||||
value: NaN,
|
||||
comments: comments,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read an array of JSON values from the context.
|
||||
* @private
|
||||
@ -638,11 +728,33 @@ function _readValue(context: JsonParserContext, comments = _readBlanks(context))
|
||||
result = _readNumber(context, comments);
|
||||
break;
|
||||
|
||||
case '.':
|
||||
case '+':
|
||||
if ((context.mode & JsonParseMode.LaxNumberParsingAllowed) == 0) {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
result = _readNumber(context, comments);
|
||||
break;
|
||||
|
||||
case '\'':
|
||||
case '"':
|
||||
result = _readString(context, comments);
|
||||
break;
|
||||
|
||||
case 'I':
|
||||
if ((context.mode & JsonParseMode.NumberConstantsAllowed) == 0) {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
result = _readNumber(context, comments);
|
||||
break;
|
||||
|
||||
case 'N':
|
||||
if ((context.mode & JsonParseMode.NumberConstantsAllowed) == 0) {
|
||||
throw new InvalidJsonCharacterException(context);
|
||||
}
|
||||
result = _readNaN(context, comments);
|
||||
break;
|
||||
|
||||
case 't':
|
||||
result = _readTrue(context, comments);
|
||||
break;
|
||||
@ -681,10 +793,19 @@ export enum JsonParseMode {
|
||||
SingleQuotesAllowed = 1 << 1, // Allow single quoted strings.
|
||||
IdentifierKeyNamesAllowed = 1 << 2, // Allow identifiers as objectp properties.
|
||||
TrailingCommasAllowed = 1 << 3,
|
||||
HexadecimalNumberAllowed = 1 << 4,
|
||||
MultiLineStringAllowed = 1 << 5,
|
||||
LaxNumberParsingAllowed = 1 << 6, // Allow `.` or `+` as the first character of a number.
|
||||
NumberConstantsAllowed = 1 << 7, // Allow -Infinity, Infinity and NaN.
|
||||
|
||||
Default = Strict,
|
||||
Loose = CommentsAllowed | SingleQuotesAllowed |
|
||||
IdentifierKeyNamesAllowed | TrailingCommasAllowed,
|
||||
IdentifierKeyNamesAllowed | TrailingCommasAllowed |
|
||||
HexadecimalNumberAllowed | MultiLineStringAllowed |
|
||||
LaxNumberParsingAllowed | NumberConstantsAllowed,
|
||||
|
||||
Json = Strict,
|
||||
Json5 = Loose,
|
||||
}
|
||||
|
||||
|
||||
|
@ -67,6 +67,12 @@ describe('parseJson and parseJsonAst', () => {
|
||||
'-0-0',
|
||||
'0.0.0',
|
||||
'0\n.0\n.0',
|
||||
'0.',
|
||||
'+1',
|
||||
'Infinity',
|
||||
'NaN',
|
||||
'-Infinity',
|
||||
'+Infinity',
|
||||
];
|
||||
|
||||
for (const [n, [start, end, text]] of entries(numbers)) {
|
||||
@ -105,6 +111,7 @@ describe('parseJson and parseJsonAst', () => {
|
||||
'"a\\zb"',
|
||||
'"a',
|
||||
'"a\nb"',
|
||||
'"\\\n "',
|
||||
];
|
||||
|
||||
for (const [n, [start, end, text]] of entries(strings)) {
|
||||
@ -248,11 +255,20 @@ describe('parseJson and parseJsonAst', () => {
|
||||
'{hi:["hello",/* */]}': [[0, 0, 0], [20, 0, 20], {hi: ['hello']}],
|
||||
'{hi:["hello"/* */,]}': [[0, 0, 0], [20, 0, 20], {hi: ['hello']}],
|
||||
'{hi:["hello" , ] , }': [[0, 0, 0], [20, 0, 20], {hi: ['hello']}],
|
||||
'{hi:"\\\n "}': [[0, 0, 0], [10, 1, 3], {hi: '\n '}],
|
||||
'{d: -0xdecaf, e: Infinity, f: -Infinity, g: +Infinity, h: NaN,}': [[0, 0, 0], [63, 0, 63], {
|
||||
d: -0xdecaf,
|
||||
e: Infinity,
|
||||
f: -Infinity,
|
||||
g: Infinity,
|
||||
h: NaN,
|
||||
}],
|
||||
};
|
||||
const errors = [
|
||||
'{1b: 0}',
|
||||
' /*',
|
||||
'',
|
||||
'.Infinity',
|
||||
];
|
||||
|
||||
for (const [n, [start, end, value, text]] of entries(strings)) {
|
||||
@ -300,5 +316,32 @@ describe('parseJson and parseJsonAst', () => {
|
||||
c: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with json5.org example', () => {
|
||||
const input = `{
|
||||
// comments
|
||||
unquoted: 'and you can quote me on that',
|
||||
'singleQuotes': 'I can use "double quotes" here',
|
||||
lineBreaks: "Look, Mom! \\
|
||||
No \\\\n's!",
|
||||
hexadecimal: 0xdecaf,
|
||||
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
|
||||
positiveSign: +1,
|
||||
trailingComma: 'in objects', andIn: ['arrays',],
|
||||
"backwardsCompatible": "with JSON",
|
||||
}`;
|
||||
|
||||
expect(parseJson(input, JsonParseMode.Json5)).toEqual({
|
||||
unquoted: 'and you can quote me on that',
|
||||
singleQuotes: 'I can use "double quotes" here',
|
||||
lineBreaks: "Look, Mom! \nNo \\n's!",
|
||||
hexadecimal: 0xdecaf,
|
||||
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
|
||||
positiveSign: +1,
|
||||
trailingComma: 'in objects',
|
||||
andIn: ['arrays'],
|
||||
backwardsCompatible: 'with JSON',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user