fracturedjsonjs
Version:
JSON formatter that produces highly readable but fairly compact output
313 lines (312 loc) • 17.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TableTemplate = void 0;
/**
* Collects spacing information about the columns of a potential table. Each TableTemplate corresponds to
* a part of a row, and they're nested recursively to match the JSON structure. (Also used in complex multiline
* arrays to try to fit them all nicely together.)
*
* Say you have an object/array where each item would make a nice row all by itself. We want to try to line up
* everything about it - comments, prop names, values. If the row items are themselves objects/arrays, ideally
* we'd like to line up all of their children, too, recursively. This only works as long as the structure/types
* are consistent.
*/
const JsonItemType_1 = require("./JsonItemType");
const BracketPaddingType_1 = require("./BracketPaddingType");
const NumberListAlignment_1 = require("./NumberListAlignment");
class TableTemplate {
constructor(pads, numberListAlignment) {
/**
* The property name in the table that this segment matches up with.
*/
this.LocationInParent = undefined;
/**
* The type of values in the column, if they're uniform. There's some wiggle-room here: for instance,
* true and false have different JsonItemTypes but are considered the same type for table purposes.
*/
this.Type = JsonItemType_1.JsonItemType.Null;
/**
* Assessment of whether this is a viable column. The main qualifying factor is that all corresponding pieces
* of each row are the same type.
*/
this.IsRowDataCompatible = true;
this.RowCount = 0;
this.NameLength = 0;
this.SimpleValueLength = 0;
this.PrefixCommentLength = 0;
this.MiddleCommentLength = 0;
this.PostfixCommentLength = 0;
this.IsAnyPostCommentLineStyle = false;
this.PadType = BracketPaddingType_1.BracketPaddingType.Simple;
/**
* True if this is a number column and we're allowed by settings to normalize numbers (rewrite them with the same
* precision), and if none of the numbers have too many digits or require scientific notation.
*/
this.AllowNumberNormalization = false;
/**
* True if this column contains only numbers and nulls. Number columns are formatted specially, depending on
* settings.
*/
this.IsNumberList = false;
/**
* Length of the value for this template when things are complicated. For arrays and objects, it's the sum of
* all the child templates' lengths, plus brackets and commas and such. For number lists, it's the space
* required to align them as appropriate.
*/
this.CompositeValueLength = 0;
/**
* If the row contains non-empty array or objects whose value is shorter than the literal null, an extra adjustment
* is needed.
*/
this.ShorterThanNullAdjustment = 0;
/**
* True if at least one row in the column this represents has a null value.
*/
this.ContainsNull = false;
/**
* Length of the entire template, including space for the value, property name, and all comments.
*/
this.TotalLength = 0;
/**
* If this TableTemplate corresponds to an object or array, Children contains sub-templates
* for the array/object's children.
*/
this.Children = [];
this._maxDigBeforeDecRaw = 0;
this._maxDigAfterDecRaw = 0;
this._maxDigBeforeDecNorm = 0;
this._maxDigAfterDecNorm = 0;
this._pads = pads;
this._numberListAlignment = numberListAlignment;
this.AllowNumberNormalization = (numberListAlignment === NumberListAlignment_1.NumberListAlignment.Normalize);
this.IsNumberList = true;
}
/**
* Analyzes an object/array for formatting as a potential table. The tableRoot is a container that
* is split out across many lines. Each "row" is a single child written inline.
*/
MeasureTableRoot(tableRoot) {
// For each row of the potential table, measure it and its children, making room for everything.
// (Or, if there are incompatible types at any level, set CanBeUsedInTable to false.)
for (const child of tableRoot.Children)
this.MeasureRowSegment(child);
// Get rid of incomplete junk and figure out our size.
this.PruneAndRecompute(Number.MAX_VALUE);
// If there are fewer than 2 actual data rows (i.e., not standalone comments), no point making a table.
this.IsRowDataCompatible && (this.IsRowDataCompatible = this.RowCount >= 2);
}
TryToFit(maximumLength) {
for (let complexity = this.GetTemplateComplexity(); complexity >= 0; --complexity) {
if (this.TotalLength <= maximumLength)
return true;
this.PruneAndRecompute(complexity - 1);
}
return false;
}
/**
* Added the number, properly aligned and possibly reformatted, according to our measurements.
* This assumes that the segment is a number list, and therefore that the item is a number or null.
*/
FormatNumber(buffer, item, commaBeforePadType) {
const formatType = (this._numberListAlignment === NumberListAlignment_1.NumberListAlignment.Normalize && !this.AllowNumberNormalization)
? NumberListAlignment_1.NumberListAlignment.Left
: this._numberListAlignment;
// The easy cases. Use the value exactly as it was in the source doc.
switch (formatType) {
case NumberListAlignment_1.NumberListAlignment.Left:
buffer.Add(item.Value, commaBeforePadType, this._pads.Spaces(this.SimpleValueLength - item.ValueLength));
return;
case NumberListAlignment_1.NumberListAlignment.Right:
buffer.Add(this._pads.Spaces(this.SimpleValueLength - item.ValueLength), item.Value, commaBeforePadType);
return;
}
let maxDigBefore;
let valueStr;
let valueLength;
if (formatType === NumberListAlignment_1.NumberListAlignment.Normalize) {
// Normalize case - rewrite the number with the appropriate precision.
if (item.Type === JsonItemType_1.JsonItemType.Null) {
buffer.Add(this._pads.Spaces(this._maxDigBeforeDecNorm - item.ValueLength), item.Value, commaBeforePadType, this._pads.Spaces(this.CompositeValueLength - this._maxDigBeforeDecNorm));
return;
}
maxDigBefore = this._maxDigBeforeDecNorm;
const numericVal = Number(item.Value);
valueStr = numericVal.toFixed(this._maxDigAfterDecNorm);
valueLength = valueStr.length;
}
else {
// Decimal case - line up the decimals (or E's) but leave the value exactly as it was in the source.
maxDigBefore = this._maxDigBeforeDecRaw;
valueStr = item.Value;
valueLength = item.ValueLength;
}
let leftPad;
let rightPad;
const indexOfDot = valueStr.search(TableTemplate._dotOrE);
if (indexOfDot > 0) {
leftPad = maxDigBefore - indexOfDot;
rightPad = this.CompositeValueLength - leftPad - valueLength;
}
else {
leftPad = maxDigBefore - valueLength;
rightPad = this.CompositeValueLength - maxDigBefore;
}
buffer.Add(this._pads.Spaces(leftPad), valueStr, commaBeforePadType, this._pads.Spaces(rightPad));
}
/**
* Adjusts this TableTemplate (and its children) to make room for the given rowSegment (and its children).
*/
MeasureRowSegment(rowSegment) {
// Standalone comments and blank lines don't figure into template measurements
if (rowSegment.Type === JsonItemType_1.JsonItemType.BlankLine || rowSegment.Type === JsonItemType_1.JsonItemType.BlockComment
|| rowSegment.Type === JsonItemType_1.JsonItemType.LineComment)
return;
// Make sure this rowSegment's type is compatible with the ones we've seen so far. Null is compatible
// with all types. It the types aren't compatible, we can still align this element and its comments,
// but not any children for arrays/objects.
if (rowSegment.Type === JsonItemType_1.JsonItemType.False || rowSegment.Type === JsonItemType_1.JsonItemType.True) {
this.IsRowDataCompatible && (this.IsRowDataCompatible = this.Type === JsonItemType_1.JsonItemType.True || this.Type === JsonItemType_1.JsonItemType.Null);
this.Type = JsonItemType_1.JsonItemType.True;
this.IsNumberList = false;
}
else if (rowSegment.Type === JsonItemType_1.JsonItemType.Number) {
this.IsRowDataCompatible && (this.IsRowDataCompatible = this.Type === JsonItemType_1.JsonItemType.Number || this.Type === JsonItemType_1.JsonItemType.Null);
this.Type = JsonItemType_1.JsonItemType.Number;
}
else if (rowSegment.Type === JsonItemType_1.JsonItemType.Null) {
this._maxDigBeforeDecNorm = Math.max(this._maxDigBeforeDecNorm, this._pads.LiteralNullLen);
this._maxDigBeforeDecRaw = Math.max(this._maxDigBeforeDecRaw, this._pads.LiteralNullLen);
this.ContainsNull = true;
}
else {
this.IsRowDataCompatible && (this.IsRowDataCompatible = this.Type === rowSegment.Type || this.Type === JsonItemType_1.JsonItemType.Null);
if (this.Type === JsonItemType_1.JsonItemType.Null)
this.Type = rowSegment.Type;
this.IsNumberList = false;
}
// If multiple lines are necessary for a row (probably due to pesky comments), we can't make a table.
this.IsRowDataCompatible && (this.IsRowDataCompatible = !rowSegment.RequiresMultipleLines);
// Update the numbers
this.RowCount += 1;
this.NameLength = Math.max(this.NameLength, rowSegment.NameLength);
this.SimpleValueLength = Math.max(this.SimpleValueLength, rowSegment.ValueLength);
this.MiddleCommentLength = Math.max(this.MiddleCommentLength, rowSegment.MiddleCommentLength);
this.PrefixCommentLength = Math.max(this.PrefixCommentLength, rowSegment.PrefixCommentLength);
this.PostfixCommentLength = Math.max(this.PostfixCommentLength, rowSegment.PostfixCommentLength);
this.IsAnyPostCommentLineStyle || (this.IsAnyPostCommentLineStyle = rowSegment.IsPostCommentLineStyle);
if (rowSegment.Complexity >= 2)
this.PadType = BracketPaddingType_1.BracketPaddingType.Complex;
// Everything after this is moot if the column doesn't have a uniform type.
if (!this.IsRowDataCompatible)
return;
if (rowSegment.Type === JsonItemType_1.JsonItemType.Array) {
// For each row in this rowSegment, find or create this TableTemplate's child template for
// the that array index, and then measure recursively.
for (let i = 0; i < rowSegment.Children.length; ++i) {
if (this.Children.length <= i)
this.Children.push(new TableTemplate(this._pads, this._numberListAlignment));
this.Children[i].MeasureRowSegment(rowSegment.Children[i]);
}
}
else if (rowSegment.Type === JsonItemType_1.JsonItemType.Object) {
// If this object has multiple children with the same property name, which is allowed by the JSON standard
// although it's hard to imagine anyone would deliberately do it, we can't format it as part of a table.
if (this.ContainsDuplicateKeys(rowSegment.Children)) {
this.IsRowDataCompatible = false;
return;
}
// For each property in rowSegment, check whether there's sub-template with the same name. If not
// found, create one. Then measure recursively.
for (const rowSegChild of rowSegment.Children) {
let subTemplate = this.Children.find(tt => tt.LocationInParent === rowSegChild.Name);
if (!subTemplate) {
subTemplate = new TableTemplate(this._pads, this._numberListAlignment);
subTemplate.LocationInParent = rowSegChild.Name;
this.Children.push(subTemplate);
}
subTemplate.MeasureRowSegment(rowSegChild);
}
}
else if (rowSegment.Type === JsonItemType_1.JsonItemType.Number && this.IsNumberList) {
// So far, everything in this column is a number (or null). We need to reevaluate whether we're allowed
// to normalize the numbers - write them all with the same number of digits after the decimal point.
// We also need to take some measurements for both contingencies.
const maxChars = 15;
const parsedVal = Number(rowSegment.Value);
const normalizedStr = parsedVal.toString();
this.AllowNumberNormalization && (this.AllowNumberNormalization = !isNaN(parsedVal)
&& parsedVal !== Infinity && parsedVal !== -Infinity
&& normalizedStr.length <= maxChars
&& normalizedStr.indexOf("e") < 0
&& (parsedVal !== 0.0 || TableTemplate._trulyZeroValString.test(rowSegment.Value)));
// Measure the number of digits before and after the decimal point if we write it as a standard,
// non-scientific notation number.
const indexOfDotNorm = normalizedStr.indexOf('.');
this._maxDigBeforeDecNorm =
Math.max(this._maxDigBeforeDecNorm, (indexOfDotNorm >= 0) ? indexOfDotNorm : normalizedStr.length);
this._maxDigAfterDecNorm =
Math.max(this._maxDigAfterDecNorm, (indexOfDotNorm >= 0) ? normalizedStr.length - indexOfDotNorm - 1 : 0);
// Measure the number of digits before and after the decimal point (or E scientific notation with not
// decimal point), using the number exactly as it was in the input document.
const indexOfDotRaw = rowSegment.Value.search(TableTemplate._dotOrE);
this._maxDigBeforeDecRaw =
Math.max(this._maxDigBeforeDecRaw, (indexOfDotRaw >= 0) ? indexOfDotRaw : rowSegment.ValueLength);
this._maxDigAfterDecRaw =
Math.max(this._maxDigAfterDecRaw, (indexOfDotRaw >= 0) ? rowSegment.ValueLength - indexOfDotRaw - 1 : 0);
}
this.AllowNumberNormalization && (this.AllowNumberNormalization = this.IsNumberList);
}
PruneAndRecompute(maxAllowedComplexity) {
if (maxAllowedComplexity <= 0 || !this.IsRowDataCompatible)
this.Children = [];
for (const subTemplate of this.Children)
subTemplate.PruneAndRecompute(maxAllowedComplexity - 1);
if (this.IsNumberList) {
this.CompositeValueLength = this.GetNumberFieldWidth();
}
else if (this.Children.length > 0) {
const totalChildLen = this.Children.map(ch => ch.TotalLength).reduce((prev, cur) => prev + cur);
this.CompositeValueLength = totalChildLen
+ Math.max(0, this._pads.CommaLen * (this.Children.length - 1))
+ this._pads.ArrStartLen(this.PadType)
+ this._pads.ArrEndLen(this.PadType);
if (this.ContainsNull && this.CompositeValueLength < this._pads.LiteralNullLen) {
this.ShorterThanNullAdjustment = this._pads.LiteralNullLen - this.CompositeValueLength;
this.CompositeValueLength = this._pads.LiteralNullLen;
}
}
else {
this.CompositeValueLength = this.SimpleValueLength;
}
this.TotalLength =
((this.PrefixCommentLength > 0) ? this.PrefixCommentLength + this._pads.CommentLen : 0)
+ ((this.NameLength > 0) ? this.NameLength + this._pads.ColonLen : 0)
+ ((this.MiddleCommentLength > 0) ? this.MiddleCommentLength + this._pads.CommentLen : 0)
+ this.CompositeValueLength
+ ((this.PostfixCommentLength > 0) ? this.PostfixCommentLength + this._pads.CommentLen : 0);
}
GetTemplateComplexity() {
if (this.Children.length === 0)
return 0;
const childComplexities = this.Children.map(ch => ch.GetTemplateComplexity());
return 1 + Math.max(...childComplexities);
}
ContainsDuplicateKeys(list) {
const keys = list.map(ji => ji.Name);
return keys.some((v, i) => keys.indexOf(v) !== i);
}
GetNumberFieldWidth() {
if (this._numberListAlignment === NumberListAlignment_1.NumberListAlignment.Normalize && this.AllowNumberNormalization) {
const normDecLen = (this._maxDigAfterDecNorm > 0) ? 1 : 0;
return this._maxDigBeforeDecNorm + normDecLen + this._maxDigAfterDecNorm;
}
else if (this._numberListAlignment === NumberListAlignment_1.NumberListAlignment.Decimal) {
const rawDecLen = (this._maxDigAfterDecRaw > 0) ? 1 : 0;
return this._maxDigBeforeDecRaw + rawDecLen + this._maxDigAfterDecRaw;
}
return this.SimpleValueLength;
}
}
exports.TableTemplate = TableTemplate;
TableTemplate._trulyZeroValString = new RegExp("^-?[0.]+([eE].*)?$");
TableTemplate._dotOrE = new RegExp("[.eE]");