UNPKG

fracturedjsonjs

Version:

JSON formatter that produces highly readable but fairly compact output

418 lines (417 loc) 24.4 kB
"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 = {}));