123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- 'use strict';
- import { createScanner } from './scanner';
- var ParseOptions;
- (function (ParseOptions) {
- ParseOptions.DEFAULT = {
- allowTrailingComma: false
- };
- })(ParseOptions || (ParseOptions = {}));
- /**
- * For a given offset, evaluate the location in the JSON document. Each segment in the location path is either a property name or an array index.
- */
- export function getLocation(text, position) {
- const segments = []; // strings or numbers
- const earlyReturnException = new Object();
- let previousNode = undefined;
- const previousNodeInst = {
- value: {},
- offset: 0,
- length: 0,
- type: 'object',
- parent: undefined
- };
- let isAtPropertyKey = false;
- function setPreviousNode(value, offset, length, type) {
- previousNodeInst.value = value;
- previousNodeInst.offset = offset;
- previousNodeInst.length = length;
- previousNodeInst.type = type;
- previousNodeInst.colonOffset = undefined;
- previousNode = previousNodeInst;
- }
- try {
- visit(text, {
- onObjectBegin: (offset, length) => {
- if (position <= offset) {
- throw earlyReturnException;
- }
- previousNode = undefined;
- isAtPropertyKey = position > offset;
- segments.push(''); // push a placeholder (will be replaced)
- },
- onObjectProperty: (name, offset, length) => {
- if (position < offset) {
- throw earlyReturnException;
- }
- setPreviousNode(name, offset, length, 'property');
- segments[segments.length - 1] = name;
- if (position <= offset + length) {
- throw earlyReturnException;
- }
- },
- onObjectEnd: (offset, length) => {
- if (position <= offset) {
- throw earlyReturnException;
- }
- previousNode = undefined;
- segments.pop();
- },
- onArrayBegin: (offset, length) => {
- if (position <= offset) {
- throw earlyReturnException;
- }
- previousNode = undefined;
- segments.push(0);
- },
- onArrayEnd: (offset, length) => {
- if (position <= offset) {
- throw earlyReturnException;
- }
- previousNode = undefined;
- segments.pop();
- },
- onLiteralValue: (value, offset, length) => {
- if (position < offset) {
- throw earlyReturnException;
- }
- setPreviousNode(value, offset, length, getNodeType(value));
- if (position <= offset + length) {
- throw earlyReturnException;
- }
- },
- onSeparator: (sep, offset, length) => {
- if (position <= offset) {
- throw earlyReturnException;
- }
- if (sep === ':' && previousNode && previousNode.type === 'property') {
- previousNode.colonOffset = offset;
- isAtPropertyKey = false;
- previousNode = undefined;
- }
- else if (sep === ',') {
- const last = segments[segments.length - 1];
- if (typeof last === 'number') {
- segments[segments.length - 1] = last + 1;
- }
- else {
- isAtPropertyKey = true;
- segments[segments.length - 1] = '';
- }
- previousNode = undefined;
- }
- }
- });
- }
- catch (e) {
- if (e !== earlyReturnException) {
- throw e;
- }
- }
- return {
- path: segments,
- previousNode,
- isAtPropertyKey,
- matches: (pattern) => {
- let k = 0;
- for (let i = 0; k < pattern.length && i < segments.length; i++) {
- if (pattern[k] === segments[i] || pattern[k] === '*') {
- k++;
- }
- else if (pattern[k] !== '**') {
- return false;
- }
- }
- return k === pattern.length;
- }
- };
- }
- /**
- * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
- * Therefore always check the errors list to find out if the input was valid.
- */
- export function parse(text, errors = [], options = ParseOptions.DEFAULT) {
- let currentProperty = null;
- let currentParent = [];
- const previousParents = [];
- function onValue(value) {
- if (Array.isArray(currentParent)) {
- currentParent.push(value);
- }
- else if (currentProperty !== null) {
- currentParent[currentProperty] = value;
- }
- }
- const visitor = {
- onObjectBegin: () => {
- const object = {};
- onValue(object);
- previousParents.push(currentParent);
- currentParent = object;
- currentProperty = null;
- },
- onObjectProperty: (name) => {
- currentProperty = name;
- },
- onObjectEnd: () => {
- currentParent = previousParents.pop();
- },
- onArrayBegin: () => {
- const array = [];
- onValue(array);
- previousParents.push(currentParent);
- currentParent = array;
- currentProperty = null;
- },
- onArrayEnd: () => {
- currentParent = previousParents.pop();
- },
- onLiteralValue: onValue,
- onError: (error, offset, length) => {
- errors.push({ error, offset, length });
- }
- };
- visit(text, visitor, options);
- return currentParent[0];
- }
- /**
- * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
- */
- export function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
- let currentParent = { type: 'array', offset: -1, length: -1, children: [], parent: undefined }; // artificial root
- function ensurePropertyComplete(endOffset) {
- if (currentParent.type === 'property') {
- currentParent.length = endOffset - currentParent.offset;
- currentParent = currentParent.parent;
- }
- }
- function onValue(valueNode) {
- currentParent.children.push(valueNode);
- return valueNode;
- }
- const visitor = {
- onObjectBegin: (offset) => {
- currentParent = onValue({ type: 'object', offset, length: -1, parent: currentParent, children: [] });
- },
- onObjectProperty: (name, offset, length) => {
- currentParent = onValue({ type: 'property', offset, length: -1, parent: currentParent, children: [] });
- currentParent.children.push({ type: 'string', value: name, offset, length, parent: currentParent });
- },
- onObjectEnd: (offset, length) => {
- ensurePropertyComplete(offset + length); // in case of a missing value for a property: make sure property is complete
- currentParent.length = offset + length - currentParent.offset;
- currentParent = currentParent.parent;
- ensurePropertyComplete(offset + length);
- },
- onArrayBegin: (offset, length) => {
- currentParent = onValue({ type: 'array', offset, length: -1, parent: currentParent, children: [] });
- },
- onArrayEnd: (offset, length) => {
- currentParent.length = offset + length - currentParent.offset;
- currentParent = currentParent.parent;
- ensurePropertyComplete(offset + length);
- },
- onLiteralValue: (value, offset, length) => {
- onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
- ensurePropertyComplete(offset + length);
- },
- onSeparator: (sep, offset, length) => {
- if (currentParent.type === 'property') {
- if (sep === ':') {
- currentParent.colonOffset = offset;
- }
- else if (sep === ',') {
- ensurePropertyComplete(offset);
- }
- }
- },
- onError: (error, offset, length) => {
- errors.push({ error, offset, length });
- }
- };
- visit(text, visitor, options);
- const result = currentParent.children[0];
- if (result) {
- delete result.parent;
- }
- return result;
- }
- /**
- * Finds the node at the given path in a JSON DOM.
- */
- export function findNodeAtLocation(root, path) {
- if (!root) {
- return undefined;
- }
- let node = root;
- for (let segment of path) {
- if (typeof segment === 'string') {
- if (node.type !== 'object' || !Array.isArray(node.children)) {
- return undefined;
- }
- let found = false;
- for (const propertyNode of node.children) {
- if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment && propertyNode.children.length === 2) {
- node = propertyNode.children[1];
- found = true;
- break;
- }
- }
- if (!found) {
- return undefined;
- }
- }
- else {
- const index = segment;
- if (node.type !== 'array' || index < 0 || !Array.isArray(node.children) || index >= node.children.length) {
- return undefined;
- }
- node = node.children[index];
- }
- }
- return node;
- }
- /**
- * Gets the JSON path of the given JSON DOM node
- */
- export function getNodePath(node) {
- if (!node.parent || !node.parent.children) {
- return [];
- }
- const path = getNodePath(node.parent);
- if (node.parent.type === 'property') {
- const key = node.parent.children[0].value;
- path.push(key);
- }
- else if (node.parent.type === 'array') {
- const index = node.parent.children.indexOf(node);
- if (index !== -1) {
- path.push(index);
- }
- }
- return path;
- }
- /**
- * Evaluates the JavaScript object of the given JSON DOM node
- */
- export function getNodeValue(node) {
- switch (node.type) {
- case 'array':
- return node.children.map(getNodeValue);
- case 'object':
- const obj = Object.create(null);
- for (let prop of node.children) {
- const valueNode = prop.children[1];
- if (valueNode) {
- obj[prop.children[0].value] = getNodeValue(valueNode);
- }
- }
- return obj;
- case 'null':
- case 'string':
- case 'number':
- case 'boolean':
- return node.value;
- default:
- return undefined;
- }
- }
- export function contains(node, offset, includeRightBound = false) {
- return (offset >= node.offset && offset < (node.offset + node.length)) || includeRightBound && (offset === (node.offset + node.length));
- }
- /**
- * Finds the most inner node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset.
- */
- export function findNodeAtOffset(node, offset, includeRightBound = false) {
- if (contains(node, offset, includeRightBound)) {
- const children = node.children;
- if (Array.isArray(children)) {
- for (let i = 0; i < children.length && children[i].offset <= offset; i++) {
- const item = findNodeAtOffset(children[i], offset, includeRightBound);
- if (item) {
- return item;
- }
- }
- }
- return node;
- }
- return undefined;
- }
- /**
- * Parses the given text and invokes the visitor functions for each object, array and literal reached.
- */
- export function visit(text, visitor, options = ParseOptions.DEFAULT) {
- const _scanner = createScanner(text, false);
- // Important: Only pass copies of this to visitor functions to prevent accidental modification, and
- // to not affect visitor functions which stored a reference to a previous JSONPath
- const _jsonPath = [];
- // Depth of onXXXBegin() callbacks suppressed. onXXXEnd() decrements this if it isn't 0 already.
- // Callbacks are only called when this value is 0.
- let suppressedCallbacks = 0;
- function toNoArgVisit(visitFunction) {
- return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
- }
- function toOneArgVisit(visitFunction) {
- return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
- }
- function toOneArgVisitWithPath(visitFunction) {
- return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
- }
- function toBeginVisit(visitFunction) {
- return visitFunction ?
- () => {
- if (suppressedCallbacks > 0) {
- suppressedCallbacks++;
- }
- else {
- let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
- if (cbReturn === false) {
- suppressedCallbacks = 1;
- }
- }
- }
- : () => true;
- }
- function toEndVisit(visitFunction) {
- return visitFunction ?
- () => {
- if (suppressedCallbacks > 0) {
- suppressedCallbacks--;
- }
- if (suppressedCallbacks === 0) {
- visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
- }
- }
- : () => true;
- }
- const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
- const disallowComments = options && options.disallowComments;
- const allowTrailingComma = options && options.allowTrailingComma;
- function scanNext() {
- while (true) {
- const token = _scanner.scan();
- switch (_scanner.getTokenError()) {
- case 4 /* ScanError.InvalidUnicode */:
- handleError(14 /* ParseErrorCode.InvalidUnicode */);
- break;
- case 5 /* ScanError.InvalidEscapeCharacter */:
- handleError(15 /* ParseErrorCode.InvalidEscapeCharacter */);
- break;
- case 3 /* ScanError.UnexpectedEndOfNumber */:
- handleError(13 /* ParseErrorCode.UnexpectedEndOfNumber */);
- break;
- case 1 /* ScanError.UnexpectedEndOfComment */:
- if (!disallowComments) {
- handleError(11 /* ParseErrorCode.UnexpectedEndOfComment */);
- }
- break;
- case 2 /* ScanError.UnexpectedEndOfString */:
- handleError(12 /* ParseErrorCode.UnexpectedEndOfString */);
- break;
- case 6 /* ScanError.InvalidCharacter */:
- handleError(16 /* ParseErrorCode.InvalidCharacter */);
- break;
- }
- switch (token) {
- case 12 /* SyntaxKind.LineCommentTrivia */:
- case 13 /* SyntaxKind.BlockCommentTrivia */:
- if (disallowComments) {
- handleError(10 /* ParseErrorCode.InvalidCommentToken */);
- }
- else {
- onComment();
- }
- break;
- case 16 /* SyntaxKind.Unknown */:
- handleError(1 /* ParseErrorCode.InvalidSymbol */);
- break;
- case 15 /* SyntaxKind.Trivia */:
- case 14 /* SyntaxKind.LineBreakTrivia */:
- break;
- default:
- return token;
- }
- }
- }
- function handleError(error, skipUntilAfter = [], skipUntil = []) {
- onError(error);
- if (skipUntilAfter.length + skipUntil.length > 0) {
- let token = _scanner.getToken();
- while (token !== 17 /* SyntaxKind.EOF */) {
- if (skipUntilAfter.indexOf(token) !== -1) {
- scanNext();
- break;
- }
- else if (skipUntil.indexOf(token) !== -1) {
- break;
- }
- token = scanNext();
- }
- }
- }
- function parseString(isValue) {
- const value = _scanner.getTokenValue();
- if (isValue) {
- onLiteralValue(value);
- }
- else {
- onObjectProperty(value);
- // add property name afterwards
- _jsonPath.push(value);
- }
- scanNext();
- return true;
- }
- function parseLiteral() {
- switch (_scanner.getToken()) {
- case 11 /* SyntaxKind.NumericLiteral */:
- const tokenValue = _scanner.getTokenValue();
- let value = Number(tokenValue);
- if (isNaN(value)) {
- handleError(2 /* ParseErrorCode.InvalidNumberFormat */);
- value = 0;
- }
- onLiteralValue(value);
- break;
- case 7 /* SyntaxKind.NullKeyword */:
- onLiteralValue(null);
- break;
- case 8 /* SyntaxKind.TrueKeyword */:
- onLiteralValue(true);
- break;
- case 9 /* SyntaxKind.FalseKeyword */:
- onLiteralValue(false);
- break;
- default:
- return false;
- }
- scanNext();
- return true;
- }
- function parseProperty() {
- if (_scanner.getToken() !== 10 /* SyntaxKind.StringLiteral */) {
- handleError(3 /* ParseErrorCode.PropertyNameExpected */, [], [2 /* SyntaxKind.CloseBraceToken */, 5 /* SyntaxKind.CommaToken */]);
- return false;
- }
- parseString(false);
- if (_scanner.getToken() === 6 /* SyntaxKind.ColonToken */) {
- onSeparator(':');
- scanNext(); // consume colon
- if (!parseValue()) {
- handleError(4 /* ParseErrorCode.ValueExpected */, [], [2 /* SyntaxKind.CloseBraceToken */, 5 /* SyntaxKind.CommaToken */]);
- }
- }
- else {
- handleError(5 /* ParseErrorCode.ColonExpected */, [], [2 /* SyntaxKind.CloseBraceToken */, 5 /* SyntaxKind.CommaToken */]);
- }
- _jsonPath.pop(); // remove processed property name
- return true;
- }
- function parseObject() {
- onObjectBegin();
- scanNext(); // consume open brace
- let needsComma = false;
- while (_scanner.getToken() !== 2 /* SyntaxKind.CloseBraceToken */ && _scanner.getToken() !== 17 /* SyntaxKind.EOF */) {
- if (_scanner.getToken() === 5 /* SyntaxKind.CommaToken */) {
- if (!needsComma) {
- handleError(4 /* ParseErrorCode.ValueExpected */, [], []);
- }
- onSeparator(',');
- scanNext(); // consume comma
- if (_scanner.getToken() === 2 /* SyntaxKind.CloseBraceToken */ && allowTrailingComma) {
- break;
- }
- }
- else if (needsComma) {
- handleError(6 /* ParseErrorCode.CommaExpected */, [], []);
- }
- if (!parseProperty()) {
- handleError(4 /* ParseErrorCode.ValueExpected */, [], [2 /* SyntaxKind.CloseBraceToken */, 5 /* SyntaxKind.CommaToken */]);
- }
- needsComma = true;
- }
- onObjectEnd();
- if (_scanner.getToken() !== 2 /* SyntaxKind.CloseBraceToken */) {
- handleError(7 /* ParseErrorCode.CloseBraceExpected */, [2 /* SyntaxKind.CloseBraceToken */], []);
- }
- else {
- scanNext(); // consume close brace
- }
- return true;
- }
- function parseArray() {
- onArrayBegin();
- scanNext(); // consume open bracket
- let isFirstElement = true;
- let needsComma = false;
- while (_scanner.getToken() !== 4 /* SyntaxKind.CloseBracketToken */ && _scanner.getToken() !== 17 /* SyntaxKind.EOF */) {
- if (_scanner.getToken() === 5 /* SyntaxKind.CommaToken */) {
- if (!needsComma) {
- handleError(4 /* ParseErrorCode.ValueExpected */, [], []);
- }
- onSeparator(',');
- scanNext(); // consume comma
- if (_scanner.getToken() === 4 /* SyntaxKind.CloseBracketToken */ && allowTrailingComma) {
- break;
- }
- }
- else if (needsComma) {
- handleError(6 /* ParseErrorCode.CommaExpected */, [], []);
- }
- if (isFirstElement) {
- _jsonPath.push(0);
- isFirstElement = false;
- }
- else {
- _jsonPath[_jsonPath.length - 1]++;
- }
- if (!parseValue()) {
- handleError(4 /* ParseErrorCode.ValueExpected */, [], [4 /* SyntaxKind.CloseBracketToken */, 5 /* SyntaxKind.CommaToken */]);
- }
- needsComma = true;
- }
- onArrayEnd();
- if (!isFirstElement) {
- _jsonPath.pop(); // remove array index
- }
- if (_scanner.getToken() !== 4 /* SyntaxKind.CloseBracketToken */) {
- handleError(8 /* ParseErrorCode.CloseBracketExpected */, [4 /* SyntaxKind.CloseBracketToken */], []);
- }
- else {
- scanNext(); // consume close bracket
- }
- return true;
- }
- function parseValue() {
- switch (_scanner.getToken()) {
- case 3 /* SyntaxKind.OpenBracketToken */:
- return parseArray();
- case 1 /* SyntaxKind.OpenBraceToken */:
- return parseObject();
- case 10 /* SyntaxKind.StringLiteral */:
- return parseString(true);
- default:
- return parseLiteral();
- }
- }
- scanNext();
- if (_scanner.getToken() === 17 /* SyntaxKind.EOF */) {
- if (options.allowEmptyContent) {
- return true;
- }
- handleError(4 /* ParseErrorCode.ValueExpected */, [], []);
- return false;
- }
- if (!parseValue()) {
- handleError(4 /* ParseErrorCode.ValueExpected */, [], []);
- return false;
- }
- if (_scanner.getToken() !== 17 /* SyntaxKind.EOF */) {
- handleError(9 /* ParseErrorCode.EndOfFileExpected */, [], []);
- }
- return true;
- }
- /**
- * Takes JSON with JavaScript-style comments and remove
- * them. Optionally replaces every none-newline character
- * of comments with a replaceCharacter
- */
- export function stripComments(text, replaceCh) {
- let _scanner = createScanner(text), parts = [], kind, offset = 0, pos;
- do {
- pos = _scanner.getPosition();
- kind = _scanner.scan();
- switch (kind) {
- case 12 /* SyntaxKind.LineCommentTrivia */:
- case 13 /* SyntaxKind.BlockCommentTrivia */:
- case 17 /* SyntaxKind.EOF */:
- if (offset !== pos) {
- parts.push(text.substring(offset, pos));
- }
- if (replaceCh !== undefined) {
- parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh));
- }
- offset = _scanner.getPosition();
- break;
- }
- } while (kind !== 17 /* SyntaxKind.EOF */);
- return parts.join('');
- }
- export function getNodeType(value) {
- switch (typeof value) {
- case 'boolean': return 'boolean';
- case 'number': return 'number';
- case 'string': return 'string';
- case 'object': {
- if (!value) {
- return 'null';
- }
- else if (Array.isArray(value)) {
- return 'array';
- }
- return 'object';
- }
- default: return 'null';
- }
- }
|