fracturedjsonjs
Version:
JSON formatter that produces highly readable but fairly compact output
418 lines (417 loc) • 24.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
const FracturedJsonOptions_1 = require("./FracturedJsonOptions");
const TokenEnumerator_1 = require("./TokenEnumerator");
const JsonItem_1 = require("./JsonItem");
const JsonItemType_1 = require("./JsonItemType");
const TokenType_1 = require("./TokenType");
const FracturedJsonError_1 = require("./FracturedJsonError");
const CommentPolicy_1 = require("./CommentPolicy");
const TokenGenerator_1 = require("./TokenGenerator");
class Parser {
constructor() {
this.Options = new FracturedJsonOptions_1.FracturedJsonOptions();
}
ParseTopLevel(inputJson, stopAfterFirstElem) {
const tokenStream = new TokenEnumerator_1.TokenEnumerator((0, TokenGenerator_1.TokenGenerator)(inputJson));
return this.ParseTopLevelFromEnum(tokenStream, stopAfterFirstElem);
}
ParseTopLevelFromEnum(enumerator, stopAfterFirstElem) {
const topLevelItems = [];
let topLevelElemSeen = false;
while (true) {
if (!enumerator.MoveNext())
return topLevelItems;
const item = this.ParseItem(enumerator);
const isComment = item.Type === JsonItemType_1.JsonItemType.BlockComment || item.Type === JsonItemType_1.JsonItemType.LineComment;
const isBlank = item.Type === JsonItemType_1.JsonItemType.BlankLine;
if (isBlank) {
if (this.Options.PreserveBlankLines)
topLevelItems.push(item);
}
else if (isComment) {
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.TreatAsError)
throw new FracturedJsonError_1.FracturedJsonError("Comments not allowed with current options", item.InputPosition);
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.Preserve)
topLevelItems.push(item);
}
else {
if (stopAfterFirstElem && topLevelElemSeen)
throw new FracturedJsonError_1.FracturedJsonError("Unexpected start of second top level element", item.InputPosition);
topLevelItems.push(item);
topLevelElemSeen = true;
}
}
}
ParseItem(enumerator) {
switch (enumerator.Current.Type) {
case TokenType_1.TokenType.BeginArray:
return this.ParseArray(enumerator);
case TokenType_1.TokenType.BeginObject:
return this.ParseObject(enumerator);
default:
return this.ParseSimple(enumerator.Current);
}
}
ParseSimple(token) {
const item = new JsonItem_1.JsonItem();
item.Type = Parser.ItemTypeFromTokenType(token);
item.Value = token.Text;
item.InputPosition = token.InputPosition;
item.Complexity = 0;
return item;
}
/**
* Parse the stream of tokens into a JSON array (recursively). The enumerator should be pointing to the open
* square bracket token at the start of the call. It will be pointing to the closing bracket when the call
* returns.
*/
ParseArray(enumerator) {
if (enumerator.Current.Type !== TokenType_1.TokenType.BeginArray)
throw new FracturedJsonError_1.FracturedJsonError("Parser logic error", enumerator.Current.InputPosition);
const startingInputPosition = enumerator.Current.InputPosition;
// Holder for an element that was already added to the child list that is eligible for a postfix comment.
let elemNeedingPostComment = undefined;
let elemNeedingPostEndRow = -1;
// A single-line block comment that HAS NOT been added to the child list, that might serve as a prefix comment.
let unplacedComment = undefined;
const childList = [];
let commaStatus = CommaStatus.EmptyCollection;
let endOfArrayFound = false;
let thisArrayComplexity = 0;
while (!endOfArrayFound) {
// Get the next token, or throw an error if the input ends.
const token = Parser.GetNextTokenOrThrow(enumerator, startingInputPosition);
// If the token we're about to deal with isn't on the same line as an unplaced comment or is the end of the
// array, this is our last chance to find a place for that comment.
const unplacedCommentNeedsHome = unplacedComment
&& ((unplacedComment === null || unplacedComment === void 0 ? void 0 : unplacedComment.InputPosition.Row) !== token.InputPosition.Row || token.Type === TokenType_1.TokenType.EndArray);
if (unplacedCommentNeedsHome) {
if (elemNeedingPostComment) {
// So there's a comment we don't have a place for yet, and a previous element that doesn't have
// a postfix comment. And since the new token is on a new line (or end of array), the comment
// doesn't belong to whatever is coming up next. So attach the unplaced comment to the old
// element. (This is probably a comment at the end of a line after a comma.)
elemNeedingPostComment.PostfixComment = unplacedComment.Value;
elemNeedingPostComment.IsPostCommentLineStyle = (unplacedComment.Type === JsonItemType_1.JsonItemType.LineComment);
}
else {
// There's no old element to attach it to, so just add the comment as a standalone child.
childList.push(unplacedComment);
}
unplacedComment = undefined;
}
// If the token we're about to deal with isn't on the same line as the last element, the new token obviously
// won't be a postfix comment.
if (elemNeedingPostComment && elemNeedingPostEndRow !== token.InputPosition.Row)
elemNeedingPostComment = undefined;
switch (token.Type) {
case TokenType_1.TokenType.EndArray:
if (commaStatus === CommaStatus.CommaSeen && !this.Options.AllowTrailingCommas)
throw new FracturedJsonError_1.FracturedJsonError("Array may not end with a comma with current options", token.InputPosition);
endOfArrayFound = true;
break;
case TokenType_1.TokenType.Comma:
if (commaStatus !== CommaStatus.ElementSeen)
throw new FracturedJsonError_1.FracturedJsonError("Unexpected comma in array", token.InputPosition);
commaStatus = CommaStatus.CommaSeen;
break;
case TokenType_1.TokenType.BlankLine:
if (!this.Options.PreserveBlankLines)
break;
childList.push(this.ParseSimple(token));
break;
case TokenType_1.TokenType.BlockComment:
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.Remove)
break;
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.TreatAsError)
throw new FracturedJsonError_1.FracturedJsonError("Comments not allowed with current options", token.InputPosition);
if (unplacedComment) {
// There was a block comment before this one. Add it as a standalone comment to make room.
childList.push(unplacedComment);
unplacedComment = undefined;
}
// If this is a multiline comment, add it as standalone.
const commentItem = this.ParseSimple(token);
if (Parser.IsMultilineComment(commentItem)) {
childList.push(commentItem);
break;
}
// If this comment came after an element and before a comma, attach it to that element.
if (elemNeedingPostComment && commaStatus === CommaStatus.ElementSeen) {
elemNeedingPostComment.PostfixComment = commentItem.Value;
elemNeedingPostComment.IsPostCommentLineStyle = false;
elemNeedingPostComment = undefined;
break;
}
// Hold on to it for now. Even if elemNeedingPostComment !== null, it's possible that this comment
// should be attached to the next element, not that one. (For instance, two elements on the same
// line, with a comment between them.)
unplacedComment = commentItem;
break;
case TokenType_1.TokenType.LineComment:
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.Remove)
break;
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.TreatAsError)
throw new FracturedJsonError_1.FracturedJsonError("Comments not allowed with current options", token.InputPosition);
if (unplacedComment) {
// A previous comment followed by a line-ending comment? Add them both as standalone comments
childList.push(unplacedComment);
childList.push(this.ParseSimple(token));
unplacedComment = undefined;
break;
}
if (elemNeedingPostComment) {
// Since this is a line comment, we know there isn't anything else on the line after this.
// So if there was an element before this that can take a comment, attach it.
elemNeedingPostComment.PostfixComment = token.Text;
elemNeedingPostComment.IsPostCommentLineStyle = true;
elemNeedingPostComment = undefined;
break;
}
childList.push(this.ParseSimple(token));
break;
case TokenType_1.TokenType.False:
case TokenType_1.TokenType.True:
case TokenType_1.TokenType.Null:
case TokenType_1.TokenType.String:
case TokenType_1.TokenType.Number:
case TokenType_1.TokenType.BeginArray:
case TokenType_1.TokenType.BeginObject:
if (commaStatus === CommaStatus.ElementSeen)
throw new FracturedJsonError_1.FracturedJsonError("Comma missing while processing array", token.InputPosition);
const element = this.ParseItem(enumerator);
commaStatus = CommaStatus.ElementSeen;
thisArrayComplexity = Math.max(thisArrayComplexity, element.Complexity + 1);
if (unplacedComment) {
element.PrefixComment = unplacedComment.Value;
unplacedComment = undefined;
}
childList.push(element);
// Remember this element and the row it ended on (not token.InputPosition.Row).
elemNeedingPostComment = element;
elemNeedingPostEndRow = enumerator.Current.InputPosition.Row;
break;
default:
throw new FracturedJsonError_1.FracturedJsonError("Unexpected token in array", token.InputPosition);
}
}
const arrayItem = new JsonItem_1.JsonItem();
arrayItem.Type = JsonItemType_1.JsonItemType.Array;
arrayItem.InputPosition = startingInputPosition;
arrayItem.Complexity = thisArrayComplexity;
arrayItem.Children = childList;
return arrayItem;
}
/**
* Parse the stream of tokens into a JSON object (recursively). The enumerator should be pointing to the open
* curly bracket token at the start of the call. It will be pointing to the closing bracket when the call
* returns.
*/
ParseObject(enumerator) {
if (enumerator.Current.Type !== TokenType_1.TokenType.BeginObject)
throw new FracturedJsonError_1.FracturedJsonError("Parser logic error", enumerator.Current.InputPosition);
const startingInputPosition = enumerator.Current.InputPosition;
const childList = [];
let propertyName = undefined;
let propertyValue = undefined;
let linePropValueEnds = -1;
let beforePropComments = [];
let midPropComments = [];
let afterPropComment = undefined;
let afterPropCommentWasAfterComma = false;
let phase = ObjectPhase.BeforePropName;
let thisObjComplexity = 0;
let endOfObject = false;
while (!endOfObject) {
const token = Parser.GetNextTokenOrThrow(enumerator, startingInputPosition);
// We may have collected a bunch of stuff that should be combined into a single JsonItem. If we have a
// property name and value, then we're just waiting for potential postfix comments. But it might be time
// to bundle it all up and add it to childList before going on.
const isNewLine = (linePropValueEnds !== token.InputPosition.Row);
const isEndOfObject = (token.Type === TokenType_1.TokenType.EndObject);
const startingNextPropName = (token.Type === TokenType_1.TokenType.String && phase === ObjectPhase.AfterComma);
const isExcessPostComment = afterPropComment
&& (token.Type === TokenType_1.TokenType.BlockComment || token.Type === TokenType_1.TokenType.LineComment);
const needToFlush = propertyName && propertyValue
&& (isNewLine || isEndOfObject || startingNextPropName || isExcessPostComment);
if (needToFlush) {
let commentToHoldForNextElem;
if (startingNextPropName && afterPropCommentWasAfterComma && !isNewLine) {
// We've got an afterPropComment that showed up after the comma, and we're about to process
// another element on this same line. The comment should go with the next one, to honor the
// comma placement.
commentToHoldForNextElem = afterPropComment;
afterPropComment = undefined;
}
Parser.AttachObjectValuePieces(childList, propertyName, propertyValue, linePropValueEnds, beforePropComments, midPropComments, afterPropComment);
thisObjComplexity = Math.max(thisObjComplexity, propertyValue.Complexity + 1);
propertyName = undefined;
propertyValue = undefined;
beforePropComments = [];
midPropComments = [];
afterPropComment = undefined;
if (commentToHoldForNextElem)
beforePropComments.push(commentToHoldForNextElem);
}
switch (token.Type) {
case TokenType_1.TokenType.BlankLine:
if (!this.Options.PreserveBlankLines)
break;
if (phase === ObjectPhase.AfterPropName || phase === ObjectPhase.AfterColon)
break;
// If we were hanging on to comments to maybe be prefix comments, add them as standalone before
// adding a blank line item.
childList.push(...beforePropComments);
beforePropComments = [];
childList.push(this.ParseSimple(token));
break;
case TokenType_1.TokenType.BlockComment:
case TokenType_1.TokenType.LineComment:
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.Remove)
break;
if (this.Options.CommentPolicy === CommentPolicy_1.CommentPolicy.TreatAsError)
throw new FracturedJsonError_1.FracturedJsonError("Comments not allowed with current options", token.InputPosition);
if (phase === ObjectPhase.BeforePropName || !propertyName) {
beforePropComments.push(this.ParseSimple(token));
}
else if (phase === ObjectPhase.AfterPropName || phase === ObjectPhase.AfterColon) {
midPropComments.push(token);
}
else {
afterPropComment = this.ParseSimple(token);
afterPropCommentWasAfterComma = (phase === ObjectPhase.AfterComma);
}
break;
case TokenType_1.TokenType.EndObject:
if (phase === ObjectPhase.AfterPropName || phase === ObjectPhase.AfterColon)
throw new FracturedJsonError_1.FracturedJsonError("Unexpected end of object", token.InputPosition);
endOfObject = true;
break;
case TokenType_1.TokenType.String:
if (phase === ObjectPhase.BeforePropName || phase === ObjectPhase.AfterComma) {
propertyName = token;
phase = ObjectPhase.AfterPropName;
}
else if (phase === ObjectPhase.AfterColon) {
propertyValue = this.ParseItem(enumerator);
linePropValueEnds = enumerator.Current.InputPosition.Row;
phase = ObjectPhase.AfterPropValue;
}
else {
throw new FracturedJsonError_1.FracturedJsonError("Unexpected string found while processing object", token.InputPosition);
}
break;
case TokenType_1.TokenType.False:
case TokenType_1.TokenType.True:
case TokenType_1.TokenType.Null:
case TokenType_1.TokenType.Number:
case TokenType_1.TokenType.BeginArray:
case TokenType_1.TokenType.BeginObject:
if (phase !== ObjectPhase.AfterColon)
throw new FracturedJsonError_1.FracturedJsonError("Unexpected element while processing object", token.InputPosition);
propertyValue = this.ParseItem(enumerator);
linePropValueEnds = enumerator.Current.InputPosition.Row;
phase = ObjectPhase.AfterPropValue;
break;
case TokenType_1.TokenType.Colon:
if (phase !== ObjectPhase.AfterPropName)
throw new FracturedJsonError_1.FracturedJsonError("Unexpected colon while processing object", token.InputPosition);
phase = ObjectPhase.AfterColon;
break;
case TokenType_1.TokenType.Comma:
if (phase !== ObjectPhase.AfterPropValue)
throw new FracturedJsonError_1.FracturedJsonError("Unexpected comma while processing object", token.InputPosition);
phase = ObjectPhase.AfterComma;
break;
default:
throw new FracturedJsonError_1.FracturedJsonError("Unexpected token while processing object", token.InputPosition);
}
}
if (!this.Options.AllowTrailingCommas && phase === ObjectPhase.AfterComma)
throw new FracturedJsonError_1.FracturedJsonError("Object may not end with comma with current options", enumerator.Current.InputPosition);
const objItem = new JsonItem_1.JsonItem();
objItem.Type = JsonItemType_1.JsonItemType.Object;
objItem.InputPosition = startingInputPosition;
objItem.Complexity = thisObjComplexity;
objItem.Children = childList;
return objItem;
}
static ItemTypeFromTokenType(token) {
switch (token.Type) {
case TokenType_1.TokenType.False: return JsonItemType_1.JsonItemType.False;
case TokenType_1.TokenType.True: return JsonItemType_1.JsonItemType.True;
case TokenType_1.TokenType.Null: return JsonItemType_1.JsonItemType.Null;
case TokenType_1.TokenType.Number: return JsonItemType_1.JsonItemType.Number;
case TokenType_1.TokenType.String: return JsonItemType_1.JsonItemType.String;
case TokenType_1.TokenType.BlankLine: return JsonItemType_1.JsonItemType.BlankLine;
case TokenType_1.TokenType.BlockComment: return JsonItemType_1.JsonItemType.BlockComment;
case TokenType_1.TokenType.LineComment: return JsonItemType_1.JsonItemType.LineComment;
default: throw new FracturedJsonError_1.FracturedJsonError("Unexpected Token", token.InputPosition);
}
}
static GetNextTokenOrThrow(enumerator, startPosition) {
if (!enumerator.MoveNext())
throw new FracturedJsonError_1.FracturedJsonError("Unexpected end of input while processing array or object starting", startPosition);
return enumerator.Current;
}
static IsMultilineComment(item) {
return item.Type === JsonItemType_1.JsonItemType.BlockComment && item.Value.includes("\n");
}
static AttachObjectValuePieces(objItemList, name, element, valueEndingLine, beforeComments, midComments, afterComment) {
element.Name = name.Text;
// Deal with any comments between the property name and its element. If there's more than one, squish them
// together. If it's a line comment, make sure it ends in a \n (which isn't how it's handled elsewhere, alas.)
if (midComments.length > 0) {
let combined = "";
for (let i = 0; i < midComments.length; ++i) {
combined += midComments[i].Text;
if (i < midComments.length - 1 || midComments[i].Type === TokenType_1.TokenType.LineComment)
combined += "\n";
}
element.MiddleComment = combined;
}
// Figure out if the last of the comments before the prop name should be attached to this element.
// Any others should be added as unattached comment items.
if (beforeComments.length > 0) {
const lastOfBefore = beforeComments.pop();
if (lastOfBefore.Type === JsonItemType_1.JsonItemType.BlockComment
&& lastOfBefore.InputPosition.Row === element.InputPosition.Row) {
element.PrefixComment = lastOfBefore.Value;
objItemList.push(...beforeComments);
}
else {
objItemList.push(...beforeComments);
objItemList.push(lastOfBefore);
}
}
objItemList.push(element);
// Figure out if the first of the comments after the element should be attached to the element, and add
// the others as unattached comment items.
if (afterComment) {
if (!this.IsMultilineComment(afterComment) && afterComment.InputPosition.Row === valueEndingLine) {
element.PostfixComment = afterComment.Value;
element.IsPostCommentLineStyle = (afterComment.Type === JsonItemType_1.JsonItemType.LineComment);
}
else {
objItemList.push(afterComment);
}
}
}
}
exports.Parser = Parser;
var CommaStatus;
(function (CommaStatus) {
CommaStatus[CommaStatus["EmptyCollection"] = 0] = "EmptyCollection";
CommaStatus[CommaStatus["ElementSeen"] = 1] = "ElementSeen";
CommaStatus[CommaStatus["CommaSeen"] = 2] = "CommaSeen";
})(CommaStatus || (CommaStatus = {}));
var ObjectPhase;
(function (ObjectPhase) {
ObjectPhase[ObjectPhase["BeforePropName"] = 0] = "BeforePropName";
ObjectPhase[ObjectPhase["AfterPropName"] = 1] = "AfterPropName";
ObjectPhase[ObjectPhase["AfterColon"] = 2] = "AfterColon";
ObjectPhase[ObjectPhase["AfterPropValue"] = 3] = "AfterPropValue";
ObjectPhase[ObjectPhase["AfterComma"] = 4] = "AfterComma";
})(ObjectPhase || (ObjectPhase = {}));