fracturedjsonjs
Version:
JSON formatter that produces highly readable but fairly compact output
767 lines (766 loc) • 42.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Formatter = void 0;
const FracturedJsonOptions_1 = require("./FracturedJsonOptions");
const StringJoinBuffer_1 = require("./StringJoinBuffer");
const PaddedFormattingTokens_1 = require("./PaddedFormattingTokens");
const JsonItemType_1 = require("./JsonItemType");
const BracketPaddingType_1 = require("./BracketPaddingType");
const TableTemplate_1 = require("./TableTemplate");
const FracturedJsonError_1 = require("./FracturedJsonError");
const Parser_1 = require("./Parser");
const ConvertDataToDom_1 = require("./ConvertDataToDom");
const TableCommaPlacement_1 = require("./TableCommaPlacement");
const TableColumnType_1 = require("./TableColumnType");
/**
* Class that writes JSON data in a human-friendly format. Comments are optionally supported. While many options
* are supported through FracturedJsonOptions, generally this class should "just work", producing
* reasonable output for any JSON doc.
*/
class Formatter {
constructor() {
/**
* Settings to control the appearance of the output and what sort of input is permissible.
*/
this.Options = new FracturedJsonOptions_1.FracturedJsonOptions();
/**
* Function that measures strings for alignment purposes. The number returned should be the number of space-
* equivalent units. This is provided for use with East Asian languages, where some single characters
* are rendered as taking up two spaces. It could also be important for unicode surrogate.
*/
this.StringLengthFunc = Formatter.StringLengthByCharCount;
this._buffer = new StringJoinBuffer_1.StringJoinBuffer();
this._pads = new PaddedFormattingTokens_1.PaddedFormattingTokens(new FracturedJsonOptions_1.FracturedJsonOptions(), Formatter.StringLengthByCharCount);
}
/**
* The default string length function for use with StringLengthFunc. It returns str.length.
*/
static StringLengthByCharCount(str) {
return str.length;
}
/**
* Reads in JSON text (or JSON-with-comments), and returns a nicely-formatted string of the same content.
* @param jsonText a JSON document in text form that should be reformatted
* @param startingDepth starting indentation level for output
*/
Reformat(jsonText, startingDepth = 0) {
const buffer = new StringJoinBuffer_1.StringJoinBuffer();
const parser = new Parser_1.Parser();
parser.Options = this.Options;
const docModel = parser.ParseTopLevel(jsonText, true);
this.FormatTopLevel(docModel, startingDepth, buffer);
buffer.Flush();
return buffer.AsString();
}
/**
* Writes the serialized object as a nicely-formatted string.
* @param element the data to be written as JSON
* @param startingDepth starting indentation level for output
* @param recursionLimit nesting level at which we give up and assume we were given a circular reference
*/
Serialize(element, startingDepth = 0, recursionLimit = 100) {
const buffer = new StringJoinBuffer_1.StringJoinBuffer();
const docModel = (0, ConvertDataToDom_1.ConvertDataToDom)(element, undefined, recursionLimit);
if (!docModel)
return undefined;
this.FormatTopLevel([docModel], startingDepth, buffer);
buffer.Flush();
return buffer.AsString();
}
/**
* Writes a version of the given JSON (or JSON-with-comments) text that has all unnecessary space removed while
* still preserving comments and blank lines, if that's what the settings require.
* @param jsonText a JSON document in text form that should be reformatted
* @constructor
*/
Minify(jsonText) {
const buffer = new StringJoinBuffer_1.StringJoinBuffer();
const parser = new Parser_1.Parser();
parser.Options = this.Options;
const docModel = parser.ParseTopLevel(jsonText, true);
this.MinifyTopLevel(docModel, buffer);
buffer.Flush();
return buffer.AsString();
}
FormatTopLevel(docModel, startingDepth, buffer) {
this._buffer = buffer;
this._pads = new PaddedFormattingTokens_1.PaddedFormattingTokens(this.Options, this.StringLengthFunc);
for (const item of docModel) {
this.ComputeItemLengths(item);
this.FormatItem(item, startingDepth, false, null);
}
this._buffer = new StringJoinBuffer_1.StringJoinBuffer();
}
MinifyTopLevel(docModel, buffer) {
this._buffer = buffer;
this._pads = new PaddedFormattingTokens_1.PaddedFormattingTokens(this.Options, this.StringLengthFunc);
let atStartOfNewLine = true;
for (const item of docModel)
atStartOfNewLine = this.MinifyItem(item, atStartOfNewLine);
this._buffer = new StringJoinBuffer_1.StringJoinBuffer();
}
/**
* Runs StringLengthFunc on every part of every item and stores the value. Also computes the total minimum
* length, which for arrays and objects includes their child lengths. We're going to use these values a lot,
* and we don't want to run StringLengthFunc more than needed in case it's expensive.
*/
ComputeItemLengths(item) {
const newline = "\n";
for (const child of item.Children)
this.ComputeItemLengths(child);
switch (item.Type) {
case JsonItemType_1.JsonItemType.Null:
item.ValueLength = this._pads.LiteralNullLen;
break;
case JsonItemType_1.JsonItemType.True:
item.ValueLength = this._pads.LiteralTrueLen;
break;
case JsonItemType_1.JsonItemType.False:
item.ValueLength = this._pads.LiteralFalseLen;
break;
default:
item.ValueLength = this.StringLengthFunc(item.Value);
break;
}
item.NameLength = this.StringLengthFunc(item.Name);
item.PrefixCommentLength = this.StringLengthFunc(item.PrefixComment);
item.MiddleCommentLength = this.StringLengthFunc(item.MiddleComment);
item.PostfixCommentLength = this.StringLengthFunc(item.PostfixComment);
item.RequiresMultipleLines =
Formatter.IsCommentOrBlankLine(item.Type)
|| item.Children.some(ch => ch.RequiresMultipleLines || ch.IsPostCommentLineStyle)
|| item.PrefixComment.indexOf(newline) >= 0
|| item.MiddleComment.indexOf(newline) >= 0
|| item.PostfixComment.indexOf(newline) >= 0
|| item.Value.indexOf(newline) >= 0;
if (item.Type === JsonItemType_1.JsonItemType.Array || item.Type === JsonItemType_1.JsonItemType.Object) {
const padType = Formatter.GetPaddingType(item);
item.ValueLength =
this._pads.StartLen(item.Type, padType)
+ this._pads.EndLen(item.Type, padType)
+ item.Children.map(ch => ch.MinimumTotalLength).reduce((p, c) => p + c, 0)
+ Math.max(0, this._pads.CommaLen * (item.Children.length - 1));
}
// Note that we're not considering this item's own trailing comma, if any. But we are considering
// commas between children.
item.MinimumTotalLength =
((item.PrefixCommentLength > 0) ? item.PrefixCommentLength + this._pads.CommentLen : 0)
+ ((item.NameLength > 0) ? item.NameLength + this._pads.ColonLen : 0)
+ ((item.MiddleCommentLength > 0) ? item.MiddleCommentLength + this._pads.CommentLen : 0)
+ item.ValueLength
+ ((item.PostfixCommentLength > 0) ? item.PostfixCommentLength + this._pads.CommentLen : 0);
}
/**
* Adds a formatted version of any item to the buffer, including indentation and newlines as needed. This
* could span multiple lines.
*/
FormatItem(item, depth, includeTrailingComma, parentTemplate) {
switch (item.Type) {
case JsonItemType_1.JsonItemType.Array:
case JsonItemType_1.JsonItemType.Object:
this.FormatContainer(item, depth, includeTrailingComma, parentTemplate);
break;
case JsonItemType_1.JsonItemType.BlankLine:
this.FormatBlankLine();
break;
case JsonItemType_1.JsonItemType.BlockComment:
case JsonItemType_1.JsonItemType.LineComment:
this.FormatStandaloneComment(item, depth);
break;
default:
if (item.RequiresMultipleLines)
this.FormatSplitKeyValue(item, depth, includeTrailingComma, parentTemplate);
else
this.FormatInlineElement(item, depth, includeTrailingComma, parentTemplate);
break;
}
}
/**
* Adds the representation for an array or object to the buffer, including all necessary indents, newlines, etc.
* The array/object might be formatted inline, compact multiline, table, or expanded, according to circumstances.
*/
FormatContainer(item, depth, includeTrailingComma, parentTemplate) {
// Try to inline or compact-multiline format, as long as we're deeper than AlwaysExpandDepth. Of course,
// there may be other disqualifying factors that are discovered along the way.
if (depth > this.Options.AlwaysExpandDepth) {
if (this.FormatContainerInline(item, depth, includeTrailingComma, parentTemplate))
return;
}
// Create a helper object to measure how much space we'll need. If there's a chance that we'll be able to
// format this container as a table or compact array, we need to measure recursively. Otherwise, we still
// might need top level measurements for aligning properties and such.
const recursiveTemplate = (item.Complexity <= this.Options.MaxCompactArrayComplexity)
|| (item.Complexity <= this.Options.MaxTableRowComplexity + 1);
const template = new TableTemplate_1.TableTemplate(this._pads, this.Options.NumberListAlignment);
template.MeasureTableRoot(item, recursiveTemplate);
if (depth > this.Options.AlwaysExpandDepth) {
if (this.FormatContainerCompactMultiline(item, depth, includeTrailingComma, template, parentTemplate))
return;
}
// Allow table formatting at the specified depth, too. So if this is a root level array and
// AlwaysExpandDepth=0, we can table format it. But if AlwaysExpandDepth=1, we can't format the root
// as a table, since a table's children are always inlined (and thus not expanded).
if (depth >= this.Options.AlwaysExpandDepth) {
if (this.FormatContainerTable(item, depth, includeTrailingComma, template, parentTemplate))
return;
}
this.FormatContainerExpanded(item, depth, includeTrailingComma, template, parentTemplate);
}
/**
* Tries to add the representation for an array or object to the buffer, including all necessary indents, newlines,
* etc., if the array/object qualifies.
* Returns true if the content was added.
*/
FormatContainerInline(item, depth, includeTrailingComma, parentTemplate) {
if (item.RequiresMultipleLines)
return false;
// If we need to line up this item's value with others from the parent container, use the parentTemplate's
// measurements to account for padding.
let prefixLength;
let nameLength;
if (parentTemplate) {
prefixLength = (parentTemplate.PrefixCommentLength > 0)
? parentTemplate.PrefixCommentLength + this._pads.CommentLen
: 0;
nameLength = (parentTemplate.NameLength > 0) ? parentTemplate.NameLength + this._pads.ColonLen : 0;
}
else {
prefixLength = (item.PrefixCommentLength > 0) ? item.PrefixCommentLength + this._pads.CommentLen : 0;
nameLength = (item.NameLength > 0) ? item.NameLength + this._pads.ColonLen : 0;
}
const lengthToConsider = prefixLength
+ nameLength
+ +((item.MiddleCommentLength > 0) ? item.MiddleCommentLength + this._pads.CommentLen : 0)
+ item.ValueLength
+ ((item.PostfixCommentLength > 0) ? item.PostfixCommentLength + this._pads.CommentLen : 0)
+ ((includeTrailingComma) ? this._pads.CommaLen : 0);
if (item.Complexity > this.Options.MaxInlineComplexity || lengthToConsider > this.AvailableLineSpace(depth))
return false;
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depth));
this.InlineElement(item, includeTrailingComma, parentTemplate);
this._buffer.EndLine(this._pads.EOL);
return true;
}
/**
* Tries to add the representation of this array to the buffer, including indents and things, spanning multiple
* lines but with each child written inline and several of them per line.
* Returns true if the content was added
*/
FormatContainerCompactMultiline(item, depth, includeTrailingComma, template, parentTemplate) {
if (item.Type !== JsonItemType_1.JsonItemType.Array)
return false;
if (item.Children.length === 0 || item.Children.length < this.Options.MinCompactArrayRowItems)
return false;
if (item.Complexity > this.Options.MaxCompactArrayComplexity)
return false;
if (item.RequiresMultipleLines)
return false;
const useTableFormatting = template.Type !== TableColumnType_1.TableColumnType.Unknown && template.Type !== TableColumnType_1.TableColumnType.Mixed;
// If we can't fit lots of them on a line, compact multiline isn't a good choice. Table would likely
// be better.
const likelyAvailableLineSpace = this.AvailableLineSpace(depth + 1);
let avgItemWidth = this._pads.CommaLen;
if (useTableFormatting) {
avgItemWidth += template.TotalLength;
}
else {
avgItemWidth += item.Children.map(ch => ch.MinimumTotalLength).reduce((p, c) => p + c, 0)
/ item.Children.length;
}
if (avgItemWidth * this.Options.MinCompactArrayRowItems > likelyAvailableLineSpace)
return false;
// Add prefixString, indent, prefix comment, starting bracket (with no EOL).
const depthAfterColon = this.StandardFormatStart(item, depth, parentTemplate);
this._buffer.Add(this._pads.Start(item.Type, BracketPaddingType_1.BracketPaddingType.Empty));
const availableLineSpace = this.AvailableLineSpace(depthAfterColon + 1);
let remainingLineSpace = -1;
for (let i = 0; i < item.Children.length; ++i) {
// Figure out whether the next item fits on the current line. If not, start a new one.
const child = item.Children[i];
const needsComma = (i < item.Children.length - 1);
const spaceNeededForNext = ((needsComma) ? this._pads.CommaLen : 0)
+ ((useTableFormatting) ? template.TotalLength : child.MinimumTotalLength);
if (remainingLineSpace < spaceNeededForNext) {
this._buffer.EndLine(this._pads.EOL).Add(this.Options.PrefixString, this._pads.Indent(depthAfterColon + 1));
remainingLineSpace = availableLineSpace;
}
// Write it out
if (useTableFormatting)
this.InlineTableRowSegment(template, child, needsComma, false);
else
this.InlineElement(child, needsComma, null);
remainingLineSpace -= spaceNeededForNext;
}
// The previous line won't have ended yet, so do a line feed and indent before the closing bracket.
this._buffer.EndLine(this._pads.EOL).Add(this.Options.PrefixString, this._pads.Indent(depthAfterColon), this._pads.End(item.Type, BracketPaddingType_1.BracketPaddingType.Empty));
this.StandardFormatEnd(item, includeTrailingComma);
return true;
}
/**
* Tries to format this array/object as a table. That is, each of this JsonItem's children are each written
* as a single line, with their pieces formatted to line up. This only works if the structures and types
* are consistent for all rows. Returns true if the content was added.
*/
FormatContainerTable(item, depth, includeTrailingComma, template, parentTemplate) {
// If this element's children are too complex to be written inline, don't bother.
if (item.Complexity > this.Options.MaxTableRowComplexity + 1)
return false;
// If any particular row would require multiple lines, we can't table format this as a table.
if (template.RequiresMultipleLines)
return false;
// Figure out the space available to each row, not counting ending commas. Note that if there's a middle
// comment with a newline, we'll be indenting more than normal.
const availableSpaceDepth = (item.MiddleCommentHasNewLine) ? depth + 2 : depth + 1;
const availableSpace = this.AvailableLineSpace(availableSpaceDepth) - this._pads.CommaLen;
// If any child element is too long even without formatting, don't bother.
const isChildTooLong = item.Children.filter(ch => !Formatter.IsCommentOrBlankLine(ch.Type))
.some(ch => ch.MinimumTotalLength > availableSpace);
if (isChildTooLong)
return false;
// If the rows don't fit with everything (including descendents) tabular, try dropping the columns for
// the deepest nested items, repeatedly, until it either fits or we give up.
//
// For instance, here's an example of what fully tabular would look like:
// [
// { "a": 3, "b": { "x": 19, "y": -4 } },
// { "a": 147, "b": { "y": 111, "z": -99 } }
// ]
// If that doesn't work, we try this:
// [
// { "a": 3, "b": { "x": 19, "y": -4 } },
// { "a": 147, "b": { "y": 111, "z": -99 } }
// ]
if (template.Type === TableColumnType_1.TableColumnType.Mixed || !template.TryToFit(availableSpace))
return false;
const depthAfterColon = this.StandardFormatStart(item, depth, parentTemplate);
this._buffer.Add(this._pads.Start(item.Type, BracketPaddingType_1.BracketPaddingType.Empty)).EndLine(this._pads.EOL);
// Take note of the position of the last actual element, for comma decisions. The last element
// might not be the last item.
const lastElementIndex = Formatter.IndexOfLastElement(item.Children);
for (let i = 0; i < item.Children.length; ++i) {
const rowItem = item.Children[i];
if (rowItem.Type === JsonItemType_1.JsonItemType.BlankLine) {
this.FormatBlankLine();
continue;
}
if (rowItem.Type === JsonItemType_1.JsonItemType.LineComment || rowItem.Type === JsonItemType_1.JsonItemType.BlockComment) {
this.FormatStandaloneComment(rowItem, depthAfterColon + 1);
continue;
}
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depthAfterColon + 1));
this.InlineTableRowSegment(template, rowItem, (i < lastElementIndex), true);
this._buffer.EndLine(this._pads.EOL);
}
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depthAfterColon), this._pads.End(item.Type, BracketPaddingType_1.BracketPaddingType.Empty));
this.StandardFormatEnd(item, includeTrailingComma);
return true;
}
/**
* Adds the representation for an array or object to the buffer, including all necessary indents, newlines, etc.,
* broken out on separate lines. This is the most general case that always works.
*/
FormatContainerExpanded(item, depth, includeTrailingComma, template, parentTemplate) {
const depthAfterColon = this.StandardFormatStart(item, depth, parentTemplate);
this._buffer.Add(this._pads.Start(item.Type, BracketPaddingType_1.BracketPaddingType.Empty)).EndLine(this._pads.EOL);
// Decide whether to align this container's property values. If so, pass this container's template along
// to its children so they know how to align their property values.
const alignProps = item.Type === JsonItemType_1.JsonItemType.Object
&& template.NameLength - template.NameMinimum <= this.Options.MaxPropNamePadding
&& !template.AnyMiddleCommentHasNewline
&& this.AvailableLineSpace(depth + 1) >= template.AtomicItemSize();
const templateToPass = (alignProps) ? template : null;
const lastElementIndex = Formatter.IndexOfLastElement(item.Children);
for (let i = 0; i < item.Children.length; ++i)
this.FormatItem(item.Children[i], depthAfterColon + 1, (i < lastElementIndex), templateToPass);
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depthAfterColon), this._pads.End(item.Type, BracketPaddingType_1.BracketPaddingType.Empty));
this.StandardFormatEnd(item, includeTrailingComma);
}
/**
* Adds a (possibly multiline) standalone comment to the buffer, with indents and newlines on each line.
*/
FormatStandaloneComment(item, depth) {
const commentRows = Formatter.NormalizeMultilineComment(item.Value, item.InputPosition.Column);
for (const line of commentRows)
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depth), line).EndLine(this._pads.EOL);
}
FormatBlankLine() {
this._buffer.Add(this.Options.PrefixString).EndLine(this._pads.EOL);
}
/**
* Adds an element to the buffer that can be written as a single line, including indents and newlines.
*/
FormatInlineElement(item, depth, includeTrailingComma, parentTemplate) {
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depth));
this.InlineElement(item, includeTrailingComma, parentTemplate);
this._buffer.EndLine(this._pads.EOL);
}
/**
* Adds an item to the buffer, including comments and indents and such, where a comment between the
* prop name and prop value needs to span multiple lines.
*/
FormatSplitKeyValue(item, depth, includeTrailingComma, parentTemplate) {
this.StandardFormatStart(item, depth, parentTemplate);
this._buffer.Add(item.Value);
this.StandardFormatEnd(item, includeTrailingComma);
}
/**
* Do the stuff that's the same for the start of every formatted item, like indents and prefix comments.
* Returns the depth number to be used for everything after this. In some cases, we print a prop label
* on one line, and then the value on another, at a greater indentation level.
*/
StandardFormatStart(item, depth, parentTemplate) {
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depth));
if (parentTemplate) {
this.AddToBufferFixed(item.PrefixComment, item.PrefixCommentLength, parentTemplate.PrefixCommentLength, this._pads.Comment, false);
this.AddToBufferFixed(item.Name, item.NameLength, parentTemplate.NameLength, this._pads.Colon, this.Options.ColonBeforePropNamePadding);
}
else {
this.AddToBuffer(item.PrefixComment, item.PrefixCommentLength, this._pads.Comment);
this.AddToBuffer(item.Name, item.NameLength, this._pads.Colon);
}
if (item.MiddleCommentLength === 0)
return depth;
// If there's an inlineable middle comment, we write it on the same line and move along. Easy.
if (!item.MiddleCommentHasNewLine) {
const middlePad = (parentTemplate)
? parentTemplate.MiddleCommentLength - item.MiddleCommentLength
: 0;
this._buffer.Add(item.MiddleComment).Spaces(middlePad).Add(this._pads.Comment);
return depth;
}
// If the middle comment requires multiple lines, start a new line and indent everything after this.
const commentRows = Formatter.NormalizeMultilineComment(item.MiddleComment, Number.MAX_VALUE);
this._buffer.EndLine(this._pads.EOL);
for (const row of commentRows)
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depth + 1), row).EndLine(this._pads.EOL);
this._buffer.Add(this.Options.PrefixString, this._pads.Indent(depth + 1));
return depth + 1;
}
/**
* Do the stuff that's usually the same for the end of all formatted items, like trailing commas and postfix
* comments. This is only called when it's the last thing on the line.
*/
StandardFormatEnd(item, includeTrailingComma) {
if (includeTrailingComma && item.IsPostCommentLineStyle)
this._buffer.Add(this._pads.Comma);
if (item.PostfixCommentLength > 0)
this._buffer.Add(this._pads.Comment, item.PostfixComment);
if (includeTrailingComma && !item.IsPostCommentLineStyle)
this._buffer.Add(this._pads.Comma);
this._buffer.EndLine(this._pads.EOL);
}
/**
* Adds the inline representation of this item to the buffer. This includes all of this element's
* comments and children when appropriate. It DOES NOT include indentation, newlines, or any of that. This
* should only be called if item.RequiresMultipleLines is false.
*/
InlineElement(item, includeTrailingComma, parentTemplate) {
if (item.RequiresMultipleLines)
throw new FracturedJsonError_1.FracturedJsonError("Logic error - trying to inline invalid element");
// If parentTemplate is provided, we need to align this item's value with its siblings on other rows. (This
// typically means that the parent container can't be table formatted, but we are aligning property values.)
if (parentTemplate) {
this.AddToBufferFixed(item.PrefixComment, item.PrefixCommentLength, parentTemplate.PrefixCommentLength, this._pads.Comment, false);
this.AddToBufferFixed(item.Name, item.NameLength, parentTemplate.NameLength, this._pads.Colon, this.Options.ColonBeforePropNamePadding);
this.AddToBufferFixed(item.MiddleComment, item.MiddleCommentLength, parentTemplate.MiddleCommentLength, this._pads.Comment, false);
}
else {
this.AddToBuffer(item.PrefixComment, item.PrefixCommentLength, this._pads.Comment);
this.AddToBuffer(item.Name, item.NameLength, this._pads.Colon);
this.AddToBuffer(item.MiddleComment, item.MiddleCommentLength, this._pads.Comment);
}
this.InlineElementRaw(item);
if (includeTrailingComma && item.IsPostCommentLineStyle)
this._buffer.Add(this._pads.Comma);
if (item.PostfixCommentLength > 0)
this._buffer.Add(this._pads.Comment, item.PostfixComment);
if (includeTrailingComma && !item.IsPostCommentLineStyle)
this._buffer.Add(this._pads.Comma);
}
/**
* Adds just this element's value to be buffer, inlined. (Possibly recursively.) This does not include
* the item's comments (although it will include child elements' comments), or indentation.
*/
InlineElementRaw(item) {
if (item.Type === JsonItemType_1.JsonItemType.Array) {
const padType = Formatter.GetPaddingType(item);
this._buffer.Add(this._pads.ArrStart(padType));
for (let i = 0; i < item.Children.length; ++i)
this.InlineElement(item.Children[i], (i < item.Children.length - 1), null);
this._buffer.Add(this._pads.ArrEnd(padType));
}
else if (item.Type === JsonItemType_1.JsonItemType.Object) {
const padType = Formatter.GetPaddingType(item);
this._buffer.Add(this._pads.ObjStart(padType));
for (let i = 0; i < item.Children.length; ++i)
this.InlineElement(item.Children[i], (i < item.Children.length - 1), null);
this._buffer.Add(this._pads.ObjEnd(padType));
}
else {
this._buffer.Add(item.Value);
}
}
/**
* Adds this item's representation to the buffer inlined, formatted according to the given TableTemplate.
*/
InlineTableRowSegment(template, item, includeTrailingComma, isWholeRow) {
this.AddToBufferFixed(item.PrefixComment, item.PrefixCommentLength, template.PrefixCommentLength, this._pads.Comment, false);
this.AddToBufferFixed(item.Name, item.NameLength, template.NameLength, this._pads.Colon, this.Options.ColonBeforePropNamePadding);
this.AddToBufferFixed(item.MiddleComment, item.MiddleCommentLength, template.MiddleCommentLength, this._pads.Comment, false);
// Where to place the comma (if any) relative to the postfix comment (if any) and various padding.
const commaBeforePad = this.Options.TableCommaPlacement == TableCommaPlacement_1.TableCommaPlacement.BeforePadding
|| this.Options.TableCommaPlacement == TableCommaPlacement_1.TableCommaPlacement.BeforePaddingExceptNumbers
&& (template.Type !== TableColumnType_1.TableColumnType.Number);
let commaPos;
if (template.PostfixCommentLength > 0 && !template.IsAnyPostCommentLineStyle) {
if (item.PostfixCommentLength > 0)
commaPos = (commaBeforePad) ? CommaPosition.BeforeCommentPadding : CommaPosition.AfterCommentPadding;
else
commaPos = (commaBeforePad) ? CommaPosition.BeforeValuePadding : CommaPosition.AfterCommentPadding;
}
else {
commaPos = (commaBeforePad) ? CommaPosition.BeforeValuePadding : CommaPosition.AfterValuePadding;
}
// If we're asked to include a comma, do it. For internal segments, if we don't supply a comma, the padding
// will work out elsewhere. But if this segment is the whole row of a table, we need to supply a dummy
// comma due to possible end-of-line comment complications.
const commaType = (includeTrailingComma)
? this._pads.Comma
: (isWholeRow)
? this._pads.DummyComma
: "";
if (template.Children.length > 0 && item.Type !== JsonItemType_1.JsonItemType.Null) {
if (template.Type === TableColumnType_1.TableColumnType.Array)
this.InlineTableRawArray(template, item);
else
this.InlineTableRawObject(template, item);
if (commaPos == CommaPosition.BeforeValuePadding)
this._buffer.Add(commaType);
// Special adjustment if the object/array is shorter than the literal "null".
if (template.ShorterThanNullAdjustment > 0) {
this._buffer.Spaces(template.ShorterThanNullAdjustment);
}
}
else if (template.Type === TableColumnType_1.TableColumnType.Number) {
const numberCommaType = (commaPos == CommaPosition.BeforeValuePadding) ? commaType : "";
template.FormatNumber(this._buffer, item, numberCommaType);
}
else {
this.InlineElementRaw(item);
if (commaPos == CommaPosition.BeforeValuePadding)
this._buffer.Add(commaType);
this._buffer.Spaces(template.CompositeValueLength - item.ValueLength);
}
if (commaPos == CommaPosition.AfterValuePadding)
this._buffer.Add(commaType);
if (template.PostfixCommentLength > 0)
this._buffer.Add(this._pads.Comment, item.PostfixComment);
if (commaPos == CommaPosition.BeforeCommentPadding)
this._buffer.Add(commaType);
this._buffer.Spaces((template.PostfixCommentLength - item.PostfixCommentLength));
if (commaPos == CommaPosition.AfterCommentPadding)
this._buffer.Add(commaType);
}
/**
* Adds just this ARRAY's value inlined, not worrying about comments and prop names and stuff.
*/
InlineTableRawArray(template, item) {
this._buffer.Add(this._pads.ArrStart(template.PadType));
for (let i = 0; i < template.Children.length; ++i) {
const isLastInTemplate = (i === template.Children.length - 1);
const isLastInArray = (i === item.Children.length - 1);
const isPastEndOfArray = (i >= item.Children.length);
const subTemplate = template.Children[i];
if (isPastEndOfArray) {
// We're done writing this array's children out. Now we just need to add space to line up with others.
this._buffer.Spaces(subTemplate.TotalLength);
if (!isLastInTemplate)
this._buffer.Add(this._pads.DummyComma);
}
else {
this.InlineTableRowSegment(subTemplate, item.Children[i], !isLastInArray, false);
if (isLastInArray && !isLastInTemplate)
this._buffer.Add(this._pads.DummyComma);
}
}
this._buffer.Add(this._pads.ArrEnd(template.PadType));
}
/**
* Adds just this OBJECT's value inlined, not worrying about comments and prop names and stuff.
*/
InlineTableRawObject(template, item) {
function MatchingChild(temp) {
return item.Children.find(ch => ch.Name === temp.LocationInParent);
}
// For every property in the template, find the corresponding element in this object, if any.
const matches = template.Children.map(sub => { return { tt: sub, ji: MatchingChild(sub) }; });
// We need to know the last item in the sequence that has a real value, in order to make the commas work out.
let lastNonNullIdx = matches.length - 1;
while (lastNonNullIdx >= 0 && !matches[lastNonNullIdx].ji)
lastNonNullIdx -= 1;
this._buffer.Add(this._pads.ObjStart(template.PadType));
for (let i = 0; i < matches.length; ++i) {
const subTemplate = matches[i].tt;
const subItem = matches[i].ji;
const isLastInObject = (i === lastNonNullIdx);
const isLastInTemplate = (i === matches.length - 1);
if (subItem) {
this.InlineTableRowSegment(subTemplate, subItem, !isLastInObject, false);
if (isLastInObject && !isLastInTemplate)
this._buffer.Add(this._pads.DummyComma);
}
else {
this._buffer.Spaces(subTemplate.TotalLength);
if (!isLastInTemplate)
this._buffer.Add(this._pads.DummyComma);
}
}
this._buffer.Add(this._pads.ObjEnd(template.PadType));
}
/**
* Figures out how much room is allowed for inlining at this indentation level, considering
* MaxTotalLineLength, indentation size, and prefix string length.
*/
AvailableLineSpace(depth) {
return this.Options.MaxTotalLineLength - this._pads.PrefixStringLen - this.Options.IndentSpaces * depth;
}
/**
* Recursively write a minified version of the item to the buffer, while preserving comments.
*/
MinifyItem(item, atStartOfNewLine) {
const newline = "\n";
this._buffer.Add(item.PrefixComment);
if (item.Name.length > 0)
this._buffer.Add(item.Name, ":");
if (item.MiddleComment.indexOf(newline) >= 0) {
const normalizedComment = Formatter.NormalizeMultilineComment(item.MiddleComment, Number.MAX_VALUE);
for (const line of normalizedComment)
this._buffer.Add(line, newline);
}
else {
this._buffer.Add(item.MiddleComment);
}
if (item.Type === JsonItemType_1.JsonItemType.Array || item.Type === JsonItemType_1.JsonItemType.Object) {
let closeBracket;
if (item.Type === JsonItemType_1.JsonItemType.Array) {
this._buffer.Add("[");
closeBracket = "]";
}
else {
this._buffer.Add("{");
closeBracket = "}";
}
// Loop through children. Print commas when needed. Keep track of when we've started a new line -
// that's important for blank lines.
let needsComma = false;
let atStartOfNewLine = false;
for (const child of item.Children) {
if (!Formatter.IsCommentOrBlankLine(child.Type)) {
if (needsComma)
this._buffer.Add(",");
needsComma = true;
}
atStartOfNewLine = this.MinifyItem(child, atStartOfNewLine);
}
this._buffer.Add(closeBracket);
}
else if (item.Type === JsonItemType_1.JsonItemType.BlankLine) {
// Make sure we're starting on a new line before inserting a blank line. Otherwise, some can be lost.
if (!atStartOfNewLine)
this._buffer.Add(newline);
this._buffer.Add(newline);
return true;
}
else if (item.Type === JsonItemType_1.JsonItemType.LineComment) {
// Make sure we start on a new line for the comment, so that it will definitely be parsed as standalone.
if (!atStartOfNewLine)
this._buffer.Add(newline);
this._buffer.Add(item.Value, newline);
return true;
}
else if (item.Type === JsonItemType_1.JsonItemType.BlockComment) {
// Make sure we start on a new line for the comment, so that it will definitely be parsed as standalone.
if (!atStartOfNewLine)
this._buffer.Add(newline);
if (item.Value.indexOf(newline) >= 0) {
const normalizedComment = Formatter.NormalizeMultilineComment(item.Value, item.InputPosition.Column);
for (const line of normalizedComment)
this._buffer.Add(line, newline);
return true;
}
this._buffer.Add(item.Value, newline);
return true;
}
else {
this._buffer.Add(item.Value);
}
this._buffer.Add(item.PostfixComment);
if (item.PostfixComment.length > 0 && item.IsPostCommentLineStyle) {
this._buffer.Add(newline);
return true;
}
return false;
}
AddToBuffer(value, valueWidth, separator) {
if (valueWidth <= 0)
return;
this._buffer.Add(value, separator);
}
AddToBufferFixed(value, valueWidth, fieldWidth, separator, separatorBeforePadding) {
if (fieldWidth <= 0)
return;
const padWidth = fieldWidth - valueWidth;
if (separatorBeforePadding)
this._buffer.Add(value, separator).Spaces(padWidth);
else
this._buffer.Add(value).Spaces(padWidth).Add(separator);
}
static GetPaddingType(arrOrObj) {
if (arrOrObj.Children.length === 0)
return BracketPaddingType_1.BracketPaddingType.Empty;
return (arrOrObj.Complexity >= 2) ? BracketPaddingType_1.BracketPaddingType.Complex : BracketPaddingType_1.BracketPaddingType.Simple;
}
/**
* Returns a multiline comment string as an array of strings where newlines have been removed and leading space
* on each line has been trimmed as smartly as possible.
*/
static NormalizeMultilineComment(comment, firstLineColumn) {
// Split the comment into separate lines, and get rid of that nasty \r\n stuff. We'll write the
// line endings that the user wants ourselves.
const normalized = comment.replace(/\r/g, "");
const commentRows = normalized.split("\n")
.filter(line => line.length > 0);
/*
* The first line doesn't include any leading whitespace, but subsequent lines probably do.
* We want to remove leading whitespace from those rows, but only up to where the first line began.
* The idea is to preserve spaces used to line up comments, like the ones before the asterisks
* in THIS VERY COMMENT that you're reading RIGHT NOW.
*/
for (let i = 1; i < commentRows.length; ++i) {
const line = commentRows[i];
let nonWsIdx = 0;
while (nonWsIdx < line.length && nonWsIdx < firstLineColumn && /\s/.test(line[nonWsIdx]))
nonWsIdx += 1;
commentRows[i] = line.substring(nonWsIdx);
}
return commentRows;
}
static IndexOfLastElement(itemList) {
for (let i = itemList.length - 1; i >= 0; --i) {
if (!this.IsCommentOrBlankLine(itemList[i].Type))
return i;
}
return -1;
}
static IsCommentOrBlankLine(type) {
return type === JsonItemType_1.JsonItemType.BlankLine || type === JsonItemType_1.JsonItemType.BlockComment || type === JsonItemType_1.JsonItemType.LineComment;
}
}
exports.Formatter = Formatter;
var CommaPosition;
(function (CommaPosition) {
CommaPosition[CommaPosition["BeforeValuePadding"] = 0] = "BeforeValuePadding";
CommaPosition[CommaPosition["AfterValuePadding"] = 1] = "AfterValuePadding";
CommaPosition[CommaPosition["BeforeCommentPadding"] = 2] = "BeforeCommentPadding";
CommaPosition[CommaPosition["AfterCommentPadding"] = 3] = "AfterCommentPadding";
})(CommaPosition || (CommaPosition = {}));