├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── BracketPaddingType.ts ├── CommentPolicy.ts ├── ConvertDataToDom.ts ├── EolStyle.ts ├── Formatter.ts ├── FracturedJsonError.ts ├── FracturedJsonOptions.ts ├── IBuffer.ts ├── InputPosition.ts ├── JsonItem.ts ├── JsonItemType.ts ├── JsonToken.ts ├── NumberListAlignment.ts ├── PaddedFormattingTokens.ts ├── Parser.ts ├── ScannerState.ts ├── StringJoinBuffer.ts ├── TableCommaPlacement.ts ├── TableTemplate.ts ├── TokenEnumerator.ts ├── TokenGenerator.ts ├── TokenType.ts └── index.ts ├── test ├── AlwaysExpandFormatting.test.ts ├── CommentFormatting.test.ts ├── EastAsianWideCharacters.test.ts ├── EndingCommaFormatting.test.ts ├── FilesWithComments │ ├── 0.jsonc │ ├── 1.jsonc │ ├── 2.jsonc │ └── 3.jsonc ├── Helpers.ts ├── LengthAndComplexity.test.ts ├── NumberFormatting.test.ts ├── ObjectSerialization.test.ts ├── PadFormatting.test.ts ├── Parser.test.ts ├── StandardJsonFiles │ ├── 0.json │ ├── 1.json │ ├── 2.json │ ├── 3.json │ ├── 4.json │ ├── 5.json │ └── 6.json ├── TableFormatting.test.ts ├── TokenGenerator.test.ts ├── TopLevelItems.test.ts └── Universal.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | dist 4 | out 5 | node_modules 6 | *.user 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gitignore 3 | .npmignore 4 | .vscode 5 | node_modules 6 | *.user 7 | src 8 | test 9 | jest.config.js 10 | tsconfig.json 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # FracturedJsonJs Change Log 2 | 3 | ## 4.1.0 4 | 5 | ### Features 6 | 7 | Added a new option for where commas are placed with table formatting. The old (and still default) behavior puts commas after the column padding. This causes all the commas for a column to line up, which sometimes looks silly, especially when it's just a single column of strings being formatted as a table. 8 | 9 | Now you can set `TableCommaPlacement` to `TableCommaPlacement.BeforePadding` to make the commas cling tightly to the values to their left. Or, if you prefer, you can use `TableCommaPlacement.BeforePaddingExceptNumbers` to leave your number columns unaffected. (That option is only meaningful when `NumberListAlignment` is `Left` or `Decimal`.) 10 | 11 | Also added was a new factory method, `FracturedJsonOptions.Recommended()` which, unlike the `FracturedJsonOptions` constructor, will always point to the new best defaults without regard for backward compatibility. 12 | 13 | ### Added 14 | 15 | * New setting `FracturedJsonOptions.TableCommaPlacement`. 16 | * New method `FracturedJsonOptions.Recommended()`. 17 | 18 | ## 4.0.2 19 | 20 | ### Bug Fixes 21 | 22 | * Fixed a [bug](https://github.com/j-brooke/FracturedJsonJs/issues/13) involving an exception serializing data containing a sparse array. 23 | 24 | ## 4.0.1 25 | 26 | ### Bug Fixes 27 | 28 | * Fixed a [bug](https://github.com/j-brooke/FracturedJson/issues/32) where no exception is thrown when there's a property name but no value at the end of an object. 29 | * Fixed a [bug](https://github.com/j-brooke/FracturedJson/issues/31) where object contents with `toJSON` methods were missing their property names, results in invalid JSON. 30 | 31 | ## 4.0.0 32 | 33 | ### Features 34 | 35 | Replaced setting `DontJustifyNumbers` with a new enum, `NumberListAlignment`, to control how arrays or table columns of numbers are handled. 36 | 37 | * `Normalize` is the default and it behaves like previous versions when `DontJustifyNumbers==false`. With it, number lists or columns are rewritten with the same number of digits after the decimal place. 38 | * `Decimal` lines the numbers up according to their decimal points, preserving the number exactly as it appeared in the input document. For regular numbers this usually looks like `Normalize`, except with spaces padding out the extra decimal places instead of zeros. 39 | * `Left` lines up the values on the left, preserving the exactly values from the input document. 40 | * `Right` lines up the values on the right, preserving the exactly values from the input document. 41 | 42 | ### Added 43 | 44 | New setting, `NumberListAlignment`. 45 | 46 | ### Removed 47 | 48 | Removed setting `DontJustifyNumbers`. 49 | 50 | 51 | ## 3.1.1 52 | 53 | ### Bug Fixes 54 | 55 | * Fixed a [bug](https://github.com/j-brooke/FracturedJson/issues/27) where numbers that overflow or underflow a 64-bit float could (depending on settings) be written to the output as `Infinity` or `0`. In the overflow case, that caused output to be invalid JSON. With this fix, FracturedJson recognizes that it can't safely reform numbers like this, and uses the exact number representation from the original document. 56 | 57 | 58 | ## 3.1.0 59 | 60 | ### Added 61 | 62 | * New setting: `OmitTrailingWhitespace`. When true, the output JSON won't have any trailing spaces or tabs. This is probably the preferred behavior in most cases, but the default is `false` for backward compatibility. 63 | 64 | 65 | ## 3.0.0 66 | 67 | ### Features 68 | 69 | * Support for comments (sometimes called JSON-with-comments or .jsonc). Where possible, comments stay stuck to the elements that they're closest to in the input. 70 | * Deep table formatting. In version 2, only the immediate children of table rows were lined up. Now, if space permits and the types are consistent, all descendents are aligned as table columns. 71 | * New length limit option: `MaxTotalLineLength`. 72 | * Option to preserve blank lines. 73 | * Option to allow trailing commas. 74 | 75 | ### Added 76 | 77 | * New settings: `MaxTotalLineLength`, `MaxTableRowComplexity`, `MinCompactArrayRowItems`, `CommentPolicy`, `PreserveBlankLines`, `AllowTrailingCommas`. 78 | 79 | ### Removed 80 | 81 | * Removed settings: `TableObjectMinimumSimilarity`, `TableArrayMinimumSimilarity`, `AlignExpandedPropertyNames`, `JsonSerializerOptions`. 82 | * Support for East Asian Full-width characters is no longer built-in. I did this to eliminate coupling with any specific library. You can easily recreate the functionality by providing your own `StringLengthFunc`. (See the `EastAsianWideCharacters.test` test file for an example.) 83 | 84 | ### Changed 85 | 86 | * All of the settings are now bundled in a single class, `FracturedJsonOptions`. They are now set all at once to `Formatter.Options` rather than being separate properties of `Formatter`. 87 | * Method names have changed. Use `Reformat` when you're providing JSON text, or `Serialize` when providing JavaScript values. 88 | 89 | 90 | ## 2.2.1 91 | 92 | ### Bug Fixes 93 | 94 | Fixed https://github.com/j-brooke/FracturedJsonJs/issues/5 - an exception was being thrown when the input data contained `undefined`. This is only relevant to the JavaScript version. In fixing this I tried to mimic the behavior of `JSON.stringify()`, and succeeded in two out of three cases: 95 | 96 | * If an array contains `undefined`, `null` is used instead. 97 | * If an object property's value is `undefined`, the property is skipped. (Property names can't be `undefined`.) 98 | * If `undefined` is passed as the root value to `JSON.stringify()`, the return value is `undefined`. As of this version, FracturedJsonJs' `serialize()` will return the string `null`. To make it behave like `stringify` in this case would have required changing the TypeScript signature, which would require a major version increase. 99 | 100 | ## 2.2.0 101 | 102 | ### Added 103 | 104 | * New property `stringWidthFunc` determines how many spaces are used as padding to line up columns when formatted as a table. 105 | * Static method `Formatter.StringWidthWithEastAsian` (default) uses two spaces for East Asian "fullwidth" symbols, and one space for others. 106 | * Static method `Formatter.StringWidthByCharacterCount` treats each character as having the width of one space. 107 | * New property `simpleBracketPadding` controls whether brackets should have spaces inside when they contain only simple elements. (The old property `NestedBracketPadding` is used when they contain other arrays/objects.) 108 | 109 | ## 2.0.1 110 | 111 | ### Bug Fixes 112 | 113 | * Escape sequences in property names are not preserved (#2) 114 | 115 | ## 2.0.0 116 | 117 | Re-written to support table-formatting. When an expanded array or object is composed of highly similar inline arrays or objects, FracturedJson tries to format them in a tabular format, sorting properties and justifying values to make everything line up neatly. 118 | 119 | The module structure has changed and several things renamed to behave in a more standard way. 120 | 121 | ### Added 122 | 123 | * TypeScript support 124 | * New properties `indentSpaces` and `useTabToIndent` to control indentation. 125 | * New properties `tableObjectMinimumSimilarity` and `tableArrayMinimumSimilarity` control how alike inline sibling elements need to be to be formatted as a table. 126 | * New property `alignExpandedPropertyNames` to line up expanded object property names even when not treated as a table. 127 | * New property `dontJustifyNumbers` prevents numbers from being right-justified and set to matching precision. 128 | 129 | ### Removed 130 | 131 | * `JustifyNumberLists` property has been removed. The new table formatting covers this functionality better. 132 | * `IndentString` property has been removed. `indentSpaces` and `useTabToIndent` are used to control indentation instead. The flexibility intended for `IndentString` turned out not to be worth the confusion. 133 | 134 | ### Changed 135 | 136 | * Property names and methods now begin with lowercase letters. 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jesse Brooke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FracturedJsonJs 2 | 3 | FracturedJson is a family of utilities that format [JSON data](https://www.json.org) in a way that's easy for 4 | humans to read, but fairly compact. Arrays and objects are written on single lines, as long as they're 5 | neither too long nor too complex. When several such lines are similar in structure, they're written with 6 | fields aligned like a table. Long arrays are written with multiple items per line across multiple lines. 7 | 8 | This `npm` module is part of a family of FracturedJson tools. 9 | * [Home page and Browser-based Formatter](https://j-brooke.github.io/FracturedJson/) 10 | * [FracturedJsonJs GitHub Page](https://github.com/j-brooke/FracturedJsonJs) 11 | * [FracturedJson GitHub Page](https://github.com/j-brooke/FracturedJson) 12 | * [FracturedJson Wiki](https://github.com/j-brooke/FracturedJson/wiki) 13 | * [npm Package](https://www.npmjs.com/package/fracturedjsonjs) 14 | * [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=j-brooke.fracturedjsonvsc) 15 | 16 | ## Example 17 | 18 | Here's a sample of output using nearly-default settings: 19 | 20 | ```json 21 | { 22 | "SimpleArray": [ 23 | 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 24 | 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113 25 | ], 26 | "ObjectColumnsArrayRows": { 27 | "Katherine": ["blue" , "lightblue", "black" ], 28 | "Logan" : ["yellow" , "blue" , "black", "red"], 29 | "Erik" : ["red" , "purple" ], 30 | "Jean" : ["lightgreen", "yellow" , "black" ] 31 | }, 32 | "ArrayColumnsObjectRows": [ 33 | { "type": "turret" , "hp": 400, "loc": {"x": 47, "y": -4}, "flags": "S" }, 34 | { "type": "assassin" , "hp": 80, "loc": {"x": 12, "y": 6}, "flags": "Q" }, 35 | { "type": "berserker", "hp": 150, "loc": {"x": 0, "y": 0} }, 36 | { "type": "pittrap" , "loc": {"x": 10, "y": -14}, "flags": "S,I" } 37 | ], 38 | "ComplexArray": [ 39 | [19, 2], 40 | [ 3, 8], 41 | [14, 0], 42 | [ 9, 9], 43 | [ 9, 9], 44 | [ 0, 3], 45 | [10, 1], 46 | [ 9, 1], 47 | [ 9, 2], 48 | [ 6, 13], 49 | [18, 5], 50 | [ 4, 11], 51 | [12, 2] 52 | ] 53 | } 54 | ``` 55 | 56 | If enabled in the settings, it can also handle JSON-with-comments (which isn't real JSON). 57 | 58 | ```jsonc 59 | { 60 | /* 61 | * Multi-line comments 62 | * are fun! 63 | */ 64 | "NumbersWithHex": [ 65 | 254 /*00FE*/, 1450 /*5AA*/ , 0 /*0000*/, 36000 /*8CA0*/, 10 /*000A*/, 66 | 199 /*00C7*/, 15001 /*3A99*/, 6540 /*198C*/ 67 | ], 68 | /* Elements are keen */ 69 | "Elements": [ 70 | { /*Carbon*/ "Symbol": "C" , "Number": 6, "Isotopes": [11, 12, 13, 14] }, 71 | { /*Oxygen*/ "Symbol": "O" , "Number": 8, "Isotopes": [16, 18, 17 ] }, 72 | { /*Hydrogen*/ "Symbol": "H" , "Number": 1, "Isotopes": [ 1, 2, 3 ] }, 73 | { /*Iron*/ "Symbol": "Fe", "Number": 26, "Isotopes": [56, 54, 57, 58] } 74 | // Not a complete list... 75 | ], 76 | 77 | "Beatles Songs": [ 78 | "Taxman" , // George 79 | "Hey Jude" , // Paul 80 | "Act Naturally" , // Ringo 81 | "Ticket To Ride" // John 82 | ] 83 | } 84 | ``` 85 | 86 | ## Install 87 | 88 | ```sh 89 | npm i fracturedjsonjs 90 | ``` 91 | 92 | ## Usage 93 | 94 | ```js 95 | import { 96 | Formatter, 97 | FracturedJsonOptions, 98 | CommentPolicy, 99 | EolStyle, 100 | NumberListAlignment, 101 | TableCommaPlacement, 102 | } from 'fracturedjsonjs'; 103 | 104 | // The constructor below will give default behavior that is consistent across minor version 105 | // changes. But if you don't care about backward compatibility and just want the newest best 106 | // settings whatever they are, use this instead: 107 | // const options = FracturedJsonOptions.Recommended(); 108 | const options = new FracturedJsonOptions(); 109 | 110 | // For examples of the options, see: 111 | // https://github.com/j-brooke/FracturedJson/wiki/Options 112 | // Or experiment interactively with the web formatter: 113 | // https://j-brooke.github.io/FracturedJson/ 114 | options.MaxTotalLineLength = 80; 115 | options.MaxInlineComplexity = 1; 116 | options.JsonEolStyle = EolStyle.Crlf; 117 | options.NumberListAlignment = NumberListAlignment.Decimal; 118 | options.TableCommaPlacement = TableCommaPlacement.BeforePadding; 119 | 120 | const formatter = new Formatter(); 121 | formatter.Options = options; 122 | 123 | // Use Serialize to go from JavaScript data to JSON text. 124 | const inputObj = [ 125 | { val: 123.456 }, 126 | { val: 234567.8 }, 127 | { val: 3 }, 128 | { val: null }, 129 | { val: 5.67890123 } 130 | ]; 131 | 132 | const textFromObj = formatter.Serialize(inputObj); 133 | 134 | console.log("From inputObj:"); 135 | console.log(textFromObj); 136 | 137 | // Comments aren't allowed by default, but they're easy to enable. 138 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 139 | formatter.Options.IndentSpaces = 2; 140 | 141 | // Use Reformat to go from JSON text to JSON text. 142 | const inputText = '[{"a":123.456,"b":0,"c":0},{"a":234567.8,"b":0,"c":0},' 143 | + '{"a":3,"b":0.00000,"c":7e2},{"a":null,"b":2e-1,"c":80e1},{"a":5.6789,"b":3.5e-1,"c":0}]'; 144 | const textFromText = formatter.Reformat(inputText); 145 | 146 | console.log("From inputText:"); 147 | console.log(textFromText); 148 | ``` 149 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fracturedjsonjs", 3 | "version": "4.1.0", 4 | "description": "JSON formatter that produces highly readable but fairly compact output", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "tsc" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/j-brooke/FracturedJsonJs.git" 13 | }, 14 | "keywords": [ 15 | "JSON", 16 | "JSONC", 17 | "formatter", 18 | "pretty printer", 19 | "beautifier", 20 | "stringify", 21 | "compact", 22 | "comments" 23 | ], 24 | "author": "j-brooke", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/j-brooke/FracturedJsonJs/issues" 28 | }, 29 | "homepage": "https://j-brooke.github.io/FracturedJson/", 30 | "devDependencies": { 31 | "@types/jest": "^29.1.2", 32 | "eastasianwidth": "^0.2.0", 33 | "jest": "^29.5.0", 34 | "ts-jest": "^29.1.0", 35 | "typescript": "^4.8.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/BracketPaddingType.ts: -------------------------------------------------------------------------------- 1 | export enum BracketPaddingType 2 | { 3 | Empty = 0, 4 | Simple = 1, 5 | Complex = 2, 6 | } 7 | -------------------------------------------------------------------------------- /src/CommentPolicy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Instructions on what to do about comments found in the input text. According to the JSON standard, comments 3 | * aren't allowed. But "JSON with comments" is pretty wide-spread these days, thanks largely to Microsoft, 4 | * so it's nice to have options. 5 | */ 6 | export enum CommentPolicy 7 | { 8 | /** 9 | * An exception will be thrown if comments are found in the input. 10 | */ 11 | TreatAsError, 12 | 13 | /** 14 | * Comments are allowed in the input, but won't be included in the output. 15 | */ 16 | Remove, 17 | 18 | /** 19 | * Comments found in the input should be included in the output. 20 | */ 21 | Preserve, 22 | } -------------------------------------------------------------------------------- /src/ConvertDataToDom.ts: -------------------------------------------------------------------------------- 1 | import {JsonItem} from "./JsonItem"; 2 | import {JsonItemType} from "./JsonItemType"; 3 | import {FracturedJsonError} from "./FracturedJsonError"; 4 | 5 | /** 6 | * Converts from JavaScript data (objects, strings, etc) to FracturedJson's DOM, to allow it to be formatted. 7 | */ 8 | export function ConvertDataToDom(element:any, propName?: string, recursionLimit:number = 100): JsonItem | undefined { 9 | if (recursionLimit <= 0) 10 | throw new FracturedJsonError("Depth limit exceeded - possible circular reference"); 11 | 12 | const elementType = typeof element; 13 | switch (elementType) { 14 | case "function": 15 | case "symbol": 16 | case "undefined": 17 | return undefined; 18 | } 19 | 20 | // If whatever it is has a custom "toJSON" method (like the built-in Date class), let the native JSON code 21 | // figure it all out. toJSON could, in theory, give the string representation of a complex object or null or 22 | // who knows what, so we need to parse it out again before dealing with it. 23 | if (element && element["toJSON"]) { 24 | const convertedElement = JSON.parse(JSON.stringify(element)); 25 | return ConvertDataToDom(convertedElement, propName, recursionLimit-1); 26 | } 27 | 28 | // Let native JSON deal with escapes and such in the prop names. 29 | const item = new JsonItem(); 30 | item.Name = (propName)? JSON.stringify(propName) : ""; 31 | 32 | if (element === null) { 33 | item.Type = JsonItemType.Null; 34 | item.Value = "null"; 35 | } 36 | else if (Array.isArray(element)) { 37 | // In arrays, undefined (including anything that can't be converted) are treated as null and take up space 38 | // in the array. 39 | item.Type = JsonItemType.Array; 40 | item.Children = (element as any[]).map(ch => 41 | ConvertDataToDom(ch, undefined, recursionLimit-1) 42 | ?? ConvertDataToDom(null, undefined, recursionLimit-1)!); 43 | item.Children = []; 44 | for (let i=0; i 0) { 76 | const highestChildComplexity = item.Children.map(ch => ch.Complexity) 77 | .reduce((p:number, v:number) => Math.max(p, v)); 78 | item.Complexity = highestChildComplexity + 1; 79 | } 80 | 81 | return item; 82 | } 83 | -------------------------------------------------------------------------------- /src/EolStyle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Specifies what sort of line endings to use. 3 | */ 4 | export enum EolStyle 5 | { 6 | /** 7 | * Carriage Return, followed by a line feed. Windows-style. 8 | */ 9 | Crlf, 10 | 11 | /** 12 | * Just a line feed. Unix-style (including Mac). 13 | */ 14 | Lf, 15 | } 16 | -------------------------------------------------------------------------------- /src/FracturedJsonError.ts: -------------------------------------------------------------------------------- 1 | import {InputPosition} from "./InputPosition"; 2 | 3 | export class FracturedJsonError extends Error { 4 | InputPosition: InputPosition | undefined; 5 | constructor(message?: string, pos?: InputPosition) { 6 | const msgWithPos = (pos)? `${message} at idx=${pos.Index}, row=${pos.Row}, col=${pos.Column}` 7 | : message; 8 | super(msgWithPos); 9 | this.InputPosition = pos; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/FracturedJsonOptions.ts: -------------------------------------------------------------------------------- 1 | import {EolStyle} from "./EolStyle"; 2 | import {CommentPolicy} from "./CommentPolicy"; 3 | import {NumberListAlignment} from "./NumberListAlignment"; 4 | import {TableCommaPlacement} from "./TableCommaPlacement"; 5 | 6 | /** 7 | * Settings controlling the output of FracturedJson-formatted JSON documents. 8 | * Note that the constructor will give defaults that with stable behavior within all releases with the same major 9 | * version number. If new features are added in a minor version release, you can use the static factory method 10 | * FracturedJsonOptions.Recommended() (instead of new) to get the most up-to-date preferred behavior. This might not 11 | * be backward compatible, though. 12 | */ 13 | export class FracturedJsonOptions 14 | { 15 | /** 16 | * Dictates which characters to use for line breaks. 17 | */ 18 | JsonEolStyle: EolStyle = EolStyle.Lf; 19 | 20 | /** 21 | * Maximum length that the formatter can use when combining complex elements into a single line. This 22 | * includes comments, property names, etc. - everything except indentation and any PrefixString. Note that 23 | * lines containing only a single element can exceed this: a long string, or an element with a long prefix 24 | * or postfix comment, for example. 25 | */ 26 | MaxInlineLength: number = 2000000000; 27 | 28 | /** 29 | * Maximum length that the formatter can use when combining complex elements into a single line, from the start 30 | * of the line. This is identical to MaxInlineLength except that this one DOES count indentation 31 | * and any PrefixString. 32 | */ 33 | MaxTotalLineLength: number = 120; 34 | 35 | /** 36 | * Maximum degree of nesting of arrays/objects that may be written on a single line. 0 disables inlining (but see 37 | * related settings). 1 allows inlining of arrays/objects that contain only simple items. 2 allows inlining of 38 | * arrays/objects that contain other arrays/objects as long as the child containers only contain simple items. Etc. 39 | */ 40 | MaxInlineComplexity: number = 2; 41 | 42 | /** 43 | * Maximum degree of nesting of arrays formatted as with multiple items per row across multiple rows. 44 | */ 45 | MaxCompactArrayComplexity: number = 1; 46 | 47 | /** 48 | * Maximum degree of nesting of arrays/objects formatted as table rows. 49 | */ 50 | MaxTableRowComplexity:number = 2; 51 | 52 | /** 53 | * Determines whether commas in table-formatted elements are lined up in their own column or right next to the 54 | * element that precedes them. 55 | */ 56 | TableCommaPlacement: TableCommaPlacement = TableCommaPlacement.AfterPadding; 57 | 58 | /** 59 | * Minimum number of items allowed per row to format an array as with multiple items per line across multiple 60 | * lines. This is an approximation, not a hard rule. The idea is that if there will be too few items per row, 61 | * you'd probably rather see it as a table. 62 | */ 63 | MinCompactArrayRowItems: number = 3; 64 | 65 | /** 66 | * Depth at which lists/objects are always fully expanded, regardless of other settings. 67 | * -1 = none; 0 = root node only; 1 = root node and its children. 68 | */ 69 | AlwaysExpandDepth: number = -1; 70 | 71 | /** 72 | * If an inlined array or object contains other arrays or objects, setting NestedBracketPadding to true 73 | * will include spaces inside the outer brackets. 74 | */ 75 | NestedBracketPadding: boolean = true; 76 | 77 | /** 78 | * If an inlined array or object does NOT contain other arrays/objects, setting SimpleBracketPadding to true 79 | * will include spaces inside the brackets. 80 | */ 81 | SimpleBracketPadding: boolean = false; 82 | 83 | /** 84 | * If true, includes a space after property colons. 85 | */ 86 | ColonPadding: boolean = true; 87 | 88 | /** 89 | * If true, includes a space after commas separating array items and object properties. 90 | */ 91 | CommaPadding: boolean = true; 92 | 93 | /** 94 | * If true, spaces are included between prefix and postfix comments and their content. 95 | */ 96 | CommentPadding: boolean = true; 97 | 98 | /** 99 | * If true, there won't be any spaces or tabs at the end of lines. Normally there are a variety of cases where 100 | * whitespace can be created or preserved at the ends of lines. The most noticeable case is when 101 | * CommaPadding is true. Setting this to true gets rid of all of that (including inside multi- 102 | * line comments). 103 | */ 104 | OmitTrailingWhitespace: boolean = false; 105 | 106 | /** 107 | * Controls how lists or columns of numbers (possibly with nulls) are aligned, and whether their precision 108 | * may be normalized. 109 | */ 110 | NumberListAlignment: NumberListAlignment = NumberListAlignment.Normalize; 111 | 112 | /** 113 | * Number of spaces to use per indent level. If UseTabToIndent is true, spaces won't be used but 114 | * this number will still be used in length computations. 115 | */ 116 | IndentSpaces: number = 4; 117 | 118 | /** 119 | * Uses a single tab per indent level, instead of spaces. 120 | */ 121 | UseTabToIndent: boolean = false; 122 | 123 | /** 124 | * String attached to the beginning of every line, before regular indentation. If this string contains anything 125 | * other than whitespace, this will probably make the output invalid JSON, but it might be useful for output 126 | * to documentation, for instance. 127 | */ 128 | PrefixString: string = ""; 129 | 130 | /** 131 | * Determines how the parser and formatter should treat comments. The JSON standard does not allow comments, 132 | * but it's a common unofficial extension. (Such files are often given the extension ".jsonc".) 133 | */ 134 | CommentPolicy: CommentPolicy = CommentPolicy.TreatAsError; 135 | 136 | /** 137 | * If true, blank lines in the original input should be preserved in the output. 138 | */ 139 | PreserveBlankLines: boolean = false; 140 | 141 | /** 142 | * If true, arrays and objects that contain a comma after their last element are permitting. The JSON standard 143 | * does not allow commas after the final element of an array or object, but some systems permit it, so 144 | * it's nice to have the option here. 145 | */ 146 | AllowTrailingCommas: boolean = false; 147 | 148 | /** 149 | * Returns a new FracturedJsonOptions object with the recommended default settings without concern 150 | * for backward compatibility. The constructor's defaults should preserve the same behavior from one minor 151 | * revision to the next even if new features are added. The instance created by this method will be updated 152 | * with new settings if they are more sensible for most cases. 153 | */ 154 | static Recommended(): FracturedJsonOptions { 155 | const newObj = new FracturedJsonOptions(); 156 | newObj.TableCommaPlacement = TableCommaPlacement.BeforePadding; 157 | newObj.OmitTrailingWhitespace = true; 158 | return newObj; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/IBuffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A place where strings are piled up sequentially to eventually make one big string. Or maybe straight to a 3 | * stream or whatever. 4 | */ 5 | export interface IBuffer { 6 | Add(...values: string[]): IBuffer; 7 | EndLine(eolString: string): IBuffer; 8 | Flush(): IBuffer; 9 | } 10 | -------------------------------------------------------------------------------- /src/InputPosition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Structure representing a location in an input stream. 3 | */ 4 | export interface InputPosition { 5 | Index: number; 6 | Row: number; 7 | Column: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/JsonItem.ts: -------------------------------------------------------------------------------- 1 | import {JsonItemType} from "./JsonItemType"; 2 | import {InputPosition} from "./InputPosition"; 3 | 4 | /** 5 | * A distinct thing that can be where ever JSON values are expected in a JSON-with-comments doc. This could be an 6 | * actual data value, such as a string, number, array, etc. (generally referred to here as "elements"), or it could be 7 | * a blank line or standalone comment. In some cases, comments won't be stand-alone JsonItems, but will instead 8 | * be attached to elements to which they seem to belong. 9 | * 10 | * Much of this data is produced by the Parser, but some of the properties - like all of the 11 | * length ones - are not set by Parser, but rather, provided for use by Formatter. 12 | */ 13 | export class JsonItem { 14 | Type: JsonItemType = JsonItemType.Null; 15 | 16 | /** 17 | * Line number from the input - if available - where this element began. 18 | */ 19 | InputPosition: InputPosition = {Index:0, Row:0, Column:0}; 20 | 21 | /** 22 | * Nesting level of this item's contents if any. A simple item, or an empty array or object, has a complexity of 23 | * zero. Non-empty arrays/objects have a complexity 1 greater than that of their child with the greatest 24 | * complexity. 25 | */ 26 | Complexity:number = 0; 27 | 28 | /** 29 | * Property name, if this is an element (real JSON value) that is contained in an object. 30 | */ 31 | Name:string = ""; 32 | 33 | /** 34 | * The text value of this item, non-recursively. Null for objects and arrays. 35 | */ 36 | Value:string = ""; 37 | 38 | /** 39 | * Comment that belongs in front of this element on the same line, if any. 40 | */ 41 | PrefixComment:string = ""; 42 | 43 | /** 44 | * Comment (or, possibly many of them) that belongs in between the property name and value, if any. 45 | */ 46 | MiddleComment:string = ""; 47 | 48 | /** 49 | * Comment that belongs in front of this element on the same line, if any. 50 | */ 51 | PostfixComment:string = ""; 52 | 53 | /** 54 | * True if the postfix comment is to-end-of-line rather than block style. 55 | */ 56 | IsPostCommentLineStyle: boolean = false; 57 | 58 | NameLength:number = 0; 59 | ValueLength:number = 0; 60 | PrefixCommentLength:number = 0; 61 | MiddleCommentLength:number = 0; 62 | PostfixCommentLength:number = 0; 63 | 64 | /** 65 | * The smallest possible size this item - including all comments and children if appropriate - can be written. 66 | */ 67 | MinimumTotalLength:number = 0; 68 | 69 | /** 70 | * True if this item can't be written on a single line. 71 | * For example, an item ending in a postfix line comment 72 | * (like // ) can often be written on a single line, because the comment is the last thing. But if it's a 73 | * container with such an item inside it, it's impossible to inline the container, because there's no way to 74 | * write the line comment and then a closing bracket. 75 | */ 76 | RequiresMultipleLines:boolean = false; 77 | 78 | /** 79 | * List of this item's contents, if it's an array or object. 80 | */ 81 | Children:JsonItem[] = []; 82 | } 83 | -------------------------------------------------------------------------------- /src/JsonItemType.ts: -------------------------------------------------------------------------------- 1 | export enum JsonItemType 2 | { 3 | Null, 4 | False, 5 | True, 6 | String, 7 | Number, 8 | Object, 9 | Array, 10 | BlankLine, 11 | LineComment, 12 | BlockComment, 13 | } 14 | -------------------------------------------------------------------------------- /src/JsonToken.ts: -------------------------------------------------------------------------------- 1 | import {TokenType} from "./TokenType"; 2 | import {InputPosition} from "./InputPosition"; 3 | 4 | /** 5 | * A piece of JSON text that makes sense to treat as a whole thing when analyzing a document's structure. 6 | * For example, a string is a token, regardless of whether it represents a value or an object key. 7 | */ 8 | export interface JsonToken { 9 | Type: TokenType; 10 | Text: string; 11 | InputPosition: InputPosition; 12 | } 13 | -------------------------------------------------------------------------------- /src/NumberListAlignment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options for how lists or columns of numbers should be aligned, and whether the precision may be changed 3 | * to be consistent. 4 | */ 5 | export enum NumberListAlignment 6 | { 7 | /** 8 | * Left-aligns numbers, keeping each exactly as it appears in the input document. 9 | */ 10 | Left, 11 | 12 | /** 13 | * Right-aligns numbers, keeping each exactly as it appears in the input document. 14 | */ 15 | Right, 16 | 17 | /** 18 | * Arranges the numbers so that the decimal points line up, but keeps each value exactly as it appears in the 19 | * input document. Numbers expressed in scientific notation are aligned according to the significand's decimal 20 | * point, if any, or the "e". 21 | */ 22 | Decimal, 23 | 24 | /** 25 | * Tries to rewrite all numbers in the list in regular, non-scientific notation, all with the same number of digits 26 | * after the decimal point, all lined up. If any of the numbers have too many digits or can't be written without 27 | * scientific notation, left-alignment is used as a fallback. 28 | */ 29 | Normalize, 30 | } -------------------------------------------------------------------------------- /src/PaddedFormattingTokens.ts: -------------------------------------------------------------------------------- 1 | import {FracturedJsonOptions} from "./FracturedJsonOptions"; 2 | import {BracketPaddingType} from "./BracketPaddingType"; 3 | import {EolStyle} from "./EolStyle"; 4 | import {JsonItemType} from "./JsonItemType"; 5 | 6 | 7 | export class PaddedFormattingTokens { 8 | get Comma() { return this._comma; }; 9 | get Colon() { return this._colon; }; 10 | get Comment() { return this._comment; }; 11 | get EOL() { return this._eol; }; 12 | get CommaLen() { return this._commaLen; }; 13 | get ColonLen() { return this._colonLen; }; 14 | get CommentLen() { return this._commentLen; }; 15 | get LiteralNullLen() { return this._literalNullLen; } 16 | get LiteralTrueLen() { return this._literalTrueLen; } 17 | get LiteralFalseLen() { return this._literalFalseLen; } 18 | get PrefixStringLen() { return this._prefixStringLen; }; 19 | get DummyComma() { return this._dummyComma; }; 20 | 21 | constructor(opts: FracturedJsonOptions, strLenFunc: (val: string) => number) { 22 | this._arrStart = []; 23 | this._arrStart[BracketPaddingType.Empty] = "["; 24 | this._arrStart[BracketPaddingType.Simple] = (opts.SimpleBracketPadding) ? "[ " : "["; 25 | this._arrStart[BracketPaddingType.Complex] = (opts.NestedBracketPadding) ? "[ " : "["; 26 | 27 | this._arrEnd = []; 28 | this._arrEnd[BracketPaddingType.Empty] = "]"; 29 | this._arrEnd[BracketPaddingType.Simple] = (opts.SimpleBracketPadding) ? " ]" : "]"; 30 | this._arrEnd[BracketPaddingType.Complex] = (opts.NestedBracketPadding) ? " ]" : "]"; 31 | 32 | this._objStart = []; 33 | this._objStart[BracketPaddingType.Empty] = "{"; 34 | this._objStart[BracketPaddingType.Simple] = (opts.SimpleBracketPadding) ? "{ " : "{"; 35 | this._objStart[BracketPaddingType.Complex] = (opts.NestedBracketPadding) ? "{ " : "{"; 36 | 37 | this._objEnd = []; 38 | this._objEnd[BracketPaddingType.Empty] = "}"; 39 | this._objEnd[BracketPaddingType.Simple] = (opts.SimpleBracketPadding) ? " }" : "}"; 40 | this._objEnd[BracketPaddingType.Complex] = (opts.NestedBracketPadding) ? " }" : "}"; 41 | 42 | 43 | this._comma = (opts.CommaPadding) ? ", " : ","; 44 | this._colon = (opts.ColonPadding) ? ": " : ":"; 45 | this._comment = (opts.CommentPadding) ? " " : ""; 46 | this._eol = (opts.JsonEolStyle===EolStyle.Crlf) ? "\r\n" : "\n"; 47 | 48 | this._arrStartLen = this._arrStart.map(strLenFunc); 49 | this._arrEndLen = this._arrEnd.map(strLenFunc); 50 | this._objStartLen = this._objStart.map(strLenFunc); 51 | this._objEndLen = this._objEnd.map(strLenFunc); 52 | 53 | // Create pre-made indent strings for levels 0 and 1 now. We'll construct and cache others as needed. 54 | this._indentStrings = [ 55 | "", 56 | (opts.UseTabToIndent)? "\t" : " ".repeat(opts.IndentSpaces), 57 | ]; 58 | 59 | // Same with spaces 60 | this._spaceStrings = [ 61 | "", 62 | " ", 63 | ]; 64 | 65 | this._commaLen = strLenFunc(this._comma); 66 | this._colonLen = strLenFunc(this._colon); 67 | this._commentLen = strLenFunc(this._comment); 68 | this._literalNullLen = strLenFunc("null"); 69 | this._literalTrueLen = strLenFunc("true"); 70 | this._literalFalseLen = strLenFunc("false"); 71 | this._prefixStringLen = strLenFunc(opts.PrefixString); 72 | this._dummyComma = " ".repeat(this._commaLen); 73 | } 74 | 75 | ArrStart(type: BracketPaddingType): string { 76 | return this._arrStart[type]; 77 | } 78 | ArrEnd(type: BracketPaddingType): string { 79 | return this._arrEnd[type]; 80 | } 81 | ObjStart(type: BracketPaddingType): string { 82 | return this._objStart[type]; 83 | } 84 | ObjEnd(type: BracketPaddingType): string { 85 | return this._objEnd[type]; 86 | } 87 | 88 | Start(elemType: JsonItemType, bracketType: BracketPaddingType): string { 89 | return (elemType===JsonItemType.Array)? this.ArrStart(bracketType) : this.ObjStart(bracketType); 90 | } 91 | 92 | End(elemType: JsonItemType, bracketType: BracketPaddingType): string { 93 | return (elemType===JsonItemType.Array)? this.ArrEnd(bracketType) : this.ObjEnd(bracketType); 94 | } 95 | 96 | ArrStartLen(type: BracketPaddingType): number { 97 | return this._arrStartLen[type]; 98 | } 99 | ArrEndLen(type: BracketPaddingType): number { 100 | return this._arrEndLen[type]; 101 | } 102 | ObjStartLen(type: BracketPaddingType): number { 103 | return this._objStartLen[type]; 104 | } 105 | ObjEndLen(type: BracketPaddingType): number { 106 | return this._objEndLen[type]; 107 | } 108 | 109 | StartLen(elemType: JsonItemType, bracketType: BracketPaddingType): number { 110 | return (elemType===JsonItemType.Array)? this.ArrStartLen(bracketType) : this.ObjStartLen(bracketType); 111 | } 112 | 113 | EndLen(elemType: JsonItemType, bracketType: BracketPaddingType): number { 114 | return (elemType===JsonItemType.Array)? this.ArrEndLen(bracketType) : this.ObjEndLen(bracketType); 115 | } 116 | 117 | Indent(level: number): string { 118 | if (level >= this._indentStrings.length) { 119 | for (let i = this._indentStrings.length; i <= level; ++i) 120 | this._indentStrings.push(this._indentStrings[i-1] + this._indentStrings[1]); 121 | } 122 | 123 | return this._indentStrings[level]; 124 | } 125 | 126 | Spaces(level: number): string { 127 | if (level >= this._spaceStrings.length) { 128 | for (let i = this._spaceStrings.length; i <= level; ++i) 129 | this._spaceStrings.push(" ".repeat(i)); 130 | } 131 | 132 | return this._spaceStrings[level]; 133 | } 134 | 135 | private readonly _comma: string; 136 | private readonly _colon: string; 137 | private readonly _comment: string; 138 | private readonly _eol: string; 139 | private readonly _dummyComma: string; 140 | private readonly _commaLen: number; 141 | private readonly _colonLen: number; 142 | private readonly _commentLen: number; 143 | private readonly _literalNullLen: number; 144 | private readonly _literalTrueLen: number; 145 | private readonly _literalFalseLen: number; 146 | private readonly _prefixStringLen: number; 147 | 148 | private readonly _arrStart: string[]; 149 | private readonly _arrEnd: string[]; 150 | private readonly _objStart: string[]; 151 | private readonly _objEnd: string[]; 152 | private readonly _arrStartLen: number[]; 153 | private readonly _arrEndLen: number[]; 154 | private readonly _objStartLen: number[]; 155 | private readonly _objEndLen: number[]; 156 | private readonly _indentStrings: string[]; 157 | private readonly _spaceStrings: string[]; 158 | } 159 | -------------------------------------------------------------------------------- /src/Parser.ts: -------------------------------------------------------------------------------- 1 | import {FracturedJsonOptions} from "./FracturedJsonOptions"; 2 | import {TokenEnumerator} from "./TokenEnumerator"; 3 | import {JsonItem} from "./JsonItem"; 4 | import {JsonToken} from "./JsonToken"; 5 | import {JsonItemType} from "./JsonItemType"; 6 | import {TokenType} from "./TokenType"; 7 | import {FracturedJsonError} from "./FracturedJsonError"; 8 | import {InputPosition} from "./InputPosition"; 9 | import {CommentPolicy} from "./CommentPolicy"; 10 | import {TokenGenerator} from "./TokenGenerator"; 11 | 12 | 13 | export class Parser { 14 | Options:FracturedJsonOptions = new FracturedJsonOptions(); 15 | 16 | public ParseTopLevel(inputJson: string, stopAfterFirstElem: boolean): JsonItem[] { 17 | const tokenStream = new TokenEnumerator(TokenGenerator(inputJson)); 18 | return this.ParseTopLevelFromEnum(tokenStream, stopAfterFirstElem); 19 | } 20 | 21 | private ParseTopLevelFromEnum(enumerator: TokenEnumerator, stopAfterFirstElem: boolean): JsonItem[] { 22 | const topLevelItems: JsonItem[] = []; 23 | 24 | let topLevelElemSeen = false; 25 | while (true) { 26 | if (!enumerator.MoveNext()) 27 | return topLevelItems; 28 | 29 | const item = this.ParseItem(enumerator); 30 | const isComment = item.Type === JsonItemType.BlockComment || item.Type === JsonItemType.LineComment; 31 | const isBlank = item.Type === JsonItemType.BlankLine; 32 | 33 | if (isBlank) { 34 | if (this.Options.PreserveBlankLines) 35 | topLevelItems.push(item); 36 | } 37 | else if (isComment) { 38 | if (this.Options.CommentPolicy === CommentPolicy.TreatAsError) 39 | throw new FracturedJsonError("Comments not allowed with current options", 40 | item.InputPosition); 41 | if (this.Options.CommentPolicy === CommentPolicy.Preserve) 42 | topLevelItems.push(item); 43 | } 44 | else { 45 | if (stopAfterFirstElem && topLevelElemSeen) 46 | throw new FracturedJsonError("Unexpected start of second top level element", 47 | item.InputPosition); 48 | topLevelItems.push(item); 49 | topLevelElemSeen = true; 50 | } 51 | } 52 | } 53 | 54 | private ParseItem(enumerator: TokenEnumerator): JsonItem { 55 | switch (enumerator.Current.Type) { 56 | case TokenType.BeginArray: 57 | return this.ParseArray(enumerator); 58 | case TokenType.BeginObject: 59 | return this.ParseObject(enumerator); 60 | default: 61 | return this.ParseSimple(enumerator.Current); 62 | } 63 | } 64 | 65 | private ParseSimple(token: JsonToken): JsonItem { 66 | const item = new JsonItem(); 67 | item.Type = Parser.ItemTypeFromTokenType(token); 68 | item.Value = token.Text; 69 | item.InputPosition = token.InputPosition; 70 | item.Complexity = 0; 71 | 72 | return item; 73 | } 74 | 75 | /** 76 | * Parse the stream of tokens into a JSON array (recursively). The enumerator should be pointing to the open 77 | * square bracket token at the start of the call. It will be pointing to the closing bracket when the call 78 | * returns. 79 | */ 80 | private ParseArray(enumerator: TokenEnumerator): JsonItem { 81 | if (enumerator.Current.Type !== TokenType.BeginArray) 82 | throw new FracturedJsonError("Parser logic error", enumerator.Current.InputPosition); 83 | 84 | const startingInputPosition = enumerator.Current.InputPosition; 85 | 86 | // Holder for an element that was already added to the child list that is eligible for a postfix comment. 87 | let elemNeedingPostComment: JsonItem | undefined = undefined; 88 | let elemNeedingPostEndRow = -1; 89 | 90 | // A single-line block comment that HAS NOT been added to the child list, that might serve as a prefix comment. 91 | let unplacedComment: JsonItem | undefined = undefined; 92 | 93 | const childList: JsonItem[] = []; 94 | let commaStatus = CommaStatus.EmptyCollection; 95 | let endOfArrayFound = false; 96 | let thisArrayComplexity = 0; 97 | while (!endOfArrayFound) { 98 | // Get the next token, or throw an error if the input ends. 99 | const token = Parser.GetNextTokenOrThrow(enumerator, startingInputPosition); 100 | 101 | // 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 102 | // array, this is our last chance to find a place for that comment. 103 | const unplacedCommentNeedsHome = unplacedComment 104 | && (unplacedComment?.InputPosition.Row !== token.InputPosition.Row || token.Type===TokenType.EndArray); 105 | if (unplacedCommentNeedsHome) { 106 | if (elemNeedingPostComment) { 107 | // So there's a comment we don't have a place for yet, and a previous element that doesn't have 108 | // a postfix comment. And since the new token is on a new line (or end of array), the comment 109 | // doesn't belong to whatever is coming up next. So attach the unplaced comment to the old 110 | // element. (This is probably a comment at the end of a line after a comma.) 111 | elemNeedingPostComment.PostfixComment = unplacedComment!.Value; 112 | elemNeedingPostComment.IsPostCommentLineStyle = (unplacedComment!.Type === JsonItemType.LineComment); 113 | } 114 | else { 115 | // There's no old element to attach it to, so just add the comment as a standalone child. 116 | childList.push(unplacedComment!); 117 | } 118 | 119 | unplacedComment = undefined; 120 | } 121 | 122 | // If the token we're about to deal with isn't on the same line as the last element, the new token obviously 123 | // won't be a postfix comment. 124 | if (elemNeedingPostComment && elemNeedingPostEndRow !== token.InputPosition.Row) 125 | elemNeedingPostComment = undefined; 126 | 127 | switch (token.Type) { 128 | case TokenType.EndArray: 129 | if (commaStatus === CommaStatus.CommaSeen && !this.Options.AllowTrailingCommas) 130 | throw new FracturedJsonError("Array may not end with a comma with current options", 131 | token.InputPosition); 132 | endOfArrayFound = true; 133 | break; 134 | 135 | case TokenType.Comma: 136 | if (commaStatus !== CommaStatus.ElementSeen) 137 | throw new FracturedJsonError("Unexpected comma in array", token.InputPosition); 138 | commaStatus = CommaStatus.CommaSeen; 139 | break; 140 | 141 | case TokenType.BlankLine: 142 | if (!this.Options.PreserveBlankLines) 143 | break; 144 | childList.push(this.ParseSimple(token)); 145 | break; 146 | 147 | case TokenType.BlockComment: 148 | if (this.Options.CommentPolicy === CommentPolicy.Remove) 149 | break; 150 | if (this.Options.CommentPolicy === CommentPolicy.TreatAsError) 151 | throw new FracturedJsonError("Comments not allowed with current options", 152 | token.InputPosition); 153 | 154 | if (unplacedComment) { 155 | // There was a block comment before this one. Add it as a standalone comment to make room. 156 | childList.push(unplacedComment); 157 | unplacedComment = undefined; 158 | } 159 | 160 | // If this is a multiline comment, add it as standalone. 161 | const commentItem = this.ParseSimple(token); 162 | if (Parser.IsMultilineComment(commentItem)) { 163 | childList.push(commentItem); 164 | break; 165 | } 166 | 167 | // If this comment came after an element and before a comma, attach it to that element. 168 | if (elemNeedingPostComment && commaStatus === CommaStatus.ElementSeen) { 169 | elemNeedingPostComment.PostfixComment = commentItem.Value; 170 | elemNeedingPostComment.IsPostCommentLineStyle = false; 171 | elemNeedingPostComment = undefined; 172 | break; 173 | } 174 | 175 | // Hold on to it for now. Even if elemNeedingPostComment !== null, it's possible that this comment 176 | // should be attached to the next element, not that one. (For instance, two elements on the same 177 | // line, with a comment between them.) 178 | unplacedComment = commentItem; 179 | break; 180 | 181 | case TokenType.LineComment: 182 | if (this.Options.CommentPolicy === CommentPolicy.Remove) 183 | break; 184 | if (this.Options.CommentPolicy === CommentPolicy.TreatAsError) 185 | throw new FracturedJsonError("Comments not allowed with current options", 186 | token.InputPosition); 187 | 188 | if (unplacedComment) { 189 | // A previous comment followed by a line-ending comment? Add them both as standalone comments 190 | childList.push(unplacedComment); 191 | childList.push(this.ParseSimple(token)); 192 | unplacedComment = undefined; 193 | break; 194 | } 195 | 196 | if (elemNeedingPostComment) { 197 | // Since this is a line comment, we know there isn't anything else on the line after this. 198 | // So if there was an element before this that can take a comment, attach it. 199 | elemNeedingPostComment.PostfixComment = token.Text; 200 | elemNeedingPostComment.IsPostCommentLineStyle = true; 201 | elemNeedingPostComment = undefined; 202 | break; 203 | } 204 | 205 | childList.push(this.ParseSimple(token)); 206 | break; 207 | 208 | case TokenType.False: 209 | case TokenType.True: 210 | case TokenType.Null: 211 | case TokenType.String: 212 | case TokenType.Number: 213 | case TokenType.BeginArray: 214 | case TokenType.BeginObject: 215 | if (commaStatus === CommaStatus.ElementSeen) 216 | throw new FracturedJsonError("Comma missing while processing array", token.InputPosition); 217 | 218 | const element = this.ParseItem(enumerator); 219 | commaStatus = CommaStatus.ElementSeen 220 | thisArrayComplexity = Math.max(thisArrayComplexity, element.Complexity+1); 221 | 222 | if (unplacedComment) { 223 | element.PrefixComment = unplacedComment.Value; 224 | unplacedComment = undefined; 225 | } 226 | 227 | childList.push(element); 228 | 229 | // Remember this element and the row it ended on (not token.InputPosition.Row). 230 | elemNeedingPostComment = element; 231 | elemNeedingPostEndRow = enumerator.Current.InputPosition.Row; 232 | break; 233 | 234 | default: 235 | throw new FracturedJsonError("Unexpected token in array", token.InputPosition); 236 | } 237 | } 238 | 239 | const arrayItem = new JsonItem(); 240 | arrayItem.Type = JsonItemType.Array; 241 | arrayItem.InputPosition = startingInputPosition; 242 | arrayItem.Complexity = thisArrayComplexity; 243 | arrayItem.Children = childList; 244 | 245 | return arrayItem; 246 | } 247 | 248 | /** 249 | * Parse the stream of tokens into a JSON object (recursively). The enumerator should be pointing to the open 250 | * curly bracket token at the start of the call. It will be pointing to the closing bracket when the call 251 | * returns. 252 | */ 253 | private ParseObject(enumerator: TokenEnumerator): JsonItem { 254 | if (enumerator.Current.Type !== TokenType.BeginObject) 255 | throw new FracturedJsonError("Parser logic error", enumerator.Current.InputPosition); 256 | 257 | const startingInputPosition = enumerator.Current.InputPosition; 258 | const childList: JsonItem[] = []; 259 | 260 | let propertyName: JsonToken | undefined = undefined; 261 | let propertyValue: JsonItem | undefined = undefined; 262 | let linePropValueEnds = -1; 263 | let beforePropComments: JsonItem[] = []; 264 | let midPropComments: JsonToken[] = []; 265 | let afterPropComment: JsonItem | undefined = undefined; 266 | let afterPropCommentWasAfterComma = false; 267 | 268 | let phase = ObjectPhase.BeforePropName; 269 | let thisObjComplexity = 0; 270 | let endOfObject = false; 271 | while (!endOfObject) { 272 | const token = Parser.GetNextTokenOrThrow(enumerator, startingInputPosition); 273 | 274 | // We may have collected a bunch of stuff that should be combined into a single JsonItem. If we have a 275 | // property name and value, then we're just waiting for potential postfix comments. But it might be time 276 | // to bundle it all up and add it to childList before going on. 277 | const isNewLine = (linePropValueEnds !== token.InputPosition.Row); 278 | const isEndOfObject = (token.Type === TokenType.EndObject); 279 | const startingNextPropName = (token.Type === TokenType.String && phase === ObjectPhase.AfterComma); 280 | const isExcessPostComment = afterPropComment 281 | && (token.Type===TokenType.BlockComment || token.Type===TokenType.LineComment); 282 | const needToFlush = propertyName && propertyValue 283 | && (isNewLine || isEndOfObject || startingNextPropName || isExcessPostComment); 284 | if (needToFlush) { 285 | let commentToHoldForNextElem:JsonItem | undefined; 286 | if (startingNextPropName && afterPropCommentWasAfterComma && !isNewLine) { 287 | // We've got an afterPropComment that showed up after the comma, and we're about to process 288 | // another element on this same line. The comment should go with the next one, to honor the 289 | // comma placement. 290 | commentToHoldForNextElem = afterPropComment; 291 | afterPropComment = undefined; 292 | } 293 | 294 | Parser.AttachObjectValuePieces(childList, propertyName!, propertyValue!, linePropValueEnds, 295 | beforePropComments, midPropComments, afterPropComment); 296 | thisObjComplexity = Math.max(thisObjComplexity, propertyValue!.Complexity + 1); 297 | propertyName = undefined; 298 | propertyValue = undefined; 299 | beforePropComments = []; 300 | midPropComments = []; 301 | afterPropComment = undefined; 302 | 303 | if (commentToHoldForNextElem) 304 | beforePropComments.push(commentToHoldForNextElem); 305 | } 306 | 307 | switch (token.Type) { 308 | case TokenType.BlankLine: 309 | if (!this.Options.PreserveBlankLines) 310 | break; 311 | if (phase === ObjectPhase.AfterPropName || phase === ObjectPhase.AfterColon) 312 | break; 313 | 314 | // If we were hanging on to comments to maybe be prefix comments, add them as standalone before 315 | // adding a blank line item. 316 | childList.push(...beforePropComments); 317 | beforePropComments = []; 318 | childList.push(this.ParseSimple(token)); 319 | break; 320 | case TokenType.BlockComment: 321 | case TokenType.LineComment: 322 | if (this.Options.CommentPolicy === CommentPolicy.Remove) 323 | break; 324 | if (this.Options.CommentPolicy === CommentPolicy.TreatAsError) 325 | throw new FracturedJsonError("Comments not allowed with current options", 326 | token.InputPosition); 327 | if (phase === ObjectPhase.BeforePropName || !propertyName) { 328 | beforePropComments.push(this.ParseSimple(token)); 329 | } 330 | else if (phase === ObjectPhase.AfterPropName || phase === ObjectPhase.AfterColon) { 331 | midPropComments.push(token); 332 | } 333 | else { 334 | afterPropComment = this.ParseSimple(token); 335 | afterPropCommentWasAfterComma = (phase === ObjectPhase.AfterComma); 336 | } 337 | break; 338 | case TokenType.EndObject: 339 | if (phase === ObjectPhase.AfterPropName || phase === ObjectPhase.AfterColon) 340 | throw new FracturedJsonError("Unexpected end of object", token.InputPosition); 341 | endOfObject = true; 342 | break; 343 | case TokenType.String: 344 | if (phase === ObjectPhase.BeforePropName || phase === ObjectPhase.AfterComma) { 345 | propertyName = token; 346 | phase = ObjectPhase.AfterPropName; 347 | } 348 | else if (phase === ObjectPhase.AfterColon) { 349 | propertyValue = this.ParseItem(enumerator); 350 | linePropValueEnds = enumerator.Current.InputPosition.Row; 351 | phase = ObjectPhase.AfterPropValue; 352 | } 353 | else { 354 | throw new FracturedJsonError("Unexpected string found while processing object", 355 | token.InputPosition); 356 | } 357 | break; 358 | case TokenType.False: 359 | case TokenType.True: 360 | case TokenType.Null: 361 | case TokenType.Number: 362 | case TokenType.BeginArray: 363 | case TokenType.BeginObject: 364 | if (phase !== ObjectPhase.AfterColon) 365 | throw new FracturedJsonError("Unexpected element while processing object", 366 | token.InputPosition); 367 | propertyValue = this.ParseItem(enumerator); 368 | linePropValueEnds = enumerator.Current.InputPosition.Row; 369 | phase = ObjectPhase.AfterPropValue; 370 | break; 371 | case TokenType.Colon: 372 | if (phase !== ObjectPhase.AfterPropName) 373 | throw new FracturedJsonError("Unexpected colon while processing object", 374 | token.InputPosition); 375 | phase = ObjectPhase.AfterColon; 376 | break; 377 | case TokenType.Comma: 378 | if (phase !== ObjectPhase.AfterPropValue) 379 | throw new FracturedJsonError("Unexpected comma while processing object", 380 | token.InputPosition); 381 | phase = ObjectPhase.AfterComma; 382 | break; 383 | default: 384 | throw new FracturedJsonError("Unexpected token while processing object", 385 | token.InputPosition); 386 | } 387 | } 388 | 389 | if (!this.Options.AllowTrailingCommas && phase === ObjectPhase.AfterComma) 390 | throw new FracturedJsonError("Object may not end with comma with current options", 391 | enumerator.Current.InputPosition); 392 | 393 | const objItem = new JsonItem(); 394 | objItem.Type = JsonItemType.Object; 395 | objItem.InputPosition = startingInputPosition; 396 | objItem.Complexity = thisObjComplexity; 397 | objItem.Children = childList; 398 | 399 | return objItem; 400 | } 401 | 402 | 403 | 404 | 405 | private static ItemTypeFromTokenType(token: JsonToken): JsonItemType { 406 | switch (token.Type) { 407 | case TokenType.False: return JsonItemType.False; 408 | case TokenType.True: return JsonItemType.True; 409 | case TokenType.Null: return JsonItemType.Null; 410 | case TokenType.Number: return JsonItemType.Number; 411 | case TokenType.String: return JsonItemType.String; 412 | case TokenType.BlankLine: return JsonItemType.BlankLine; 413 | case TokenType.BlockComment: return JsonItemType.BlockComment; 414 | case TokenType.LineComment: return JsonItemType.LineComment; 415 | default: throw new FracturedJsonError("Unexpected Token", token.InputPosition); 416 | } 417 | } 418 | 419 | private static GetNextTokenOrThrow(enumerator:TokenEnumerator, startPosition:InputPosition) { 420 | if (!enumerator.MoveNext()) 421 | throw new FracturedJsonError("Unexpected end of input while processing array or object starting", 422 | startPosition); 423 | return enumerator.Current; 424 | } 425 | 426 | private static IsMultilineComment(item: JsonItem): boolean { 427 | return item.Type===JsonItemType.BlockComment && item.Value.includes("\n"); 428 | } 429 | 430 | private static AttachObjectValuePieces(objItemList: JsonItem[], name: JsonToken, element: JsonItem, 431 | valueEndingLine: number, beforeComments: JsonItem[], midComments: JsonToken[], afterComment?: JsonItem) { 432 | element.Name = name.Text; 433 | 434 | // Deal with any comments between the property name and its element. If there's more than one, squish them 435 | // together. If it's a line comment, make sure it ends in a \n (which isn't how it's handled elsewhere, alas.) 436 | if (midComments.length > 0) { 437 | let combined = ""; 438 | for (let i=0; i 0) { 450 | const lastOfBefore = beforeComments.pop(); 451 | if (lastOfBefore!.Type === JsonItemType.BlockComment 452 | && lastOfBefore!.InputPosition.Row === element.InputPosition.Row) { 453 | element.PrefixComment = lastOfBefore!.Value; 454 | objItemList.push(...beforeComments); 455 | } 456 | else { 457 | objItemList.push(...beforeComments); 458 | objItemList.push(lastOfBefore!); 459 | } 460 | } 461 | 462 | objItemList.push(element); 463 | 464 | // Figure out if the first of the comments after the element should be attached to the element, and add 465 | // the others as unattached comment items. 466 | if (afterComment) { 467 | if (!this.IsMultilineComment(afterComment) && afterComment.InputPosition.Row === valueEndingLine) { 468 | element.PostfixComment = afterComment.Value; 469 | element.IsPostCommentLineStyle = (afterComment.Type === JsonItemType.LineComment); 470 | } 471 | else { 472 | objItemList.push(afterComment); 473 | } 474 | } 475 | } 476 | } 477 | 478 | enum CommaStatus 479 | { 480 | EmptyCollection, 481 | ElementSeen, 482 | CommaSeen, 483 | } 484 | 485 | enum ObjectPhase 486 | { 487 | BeforePropName, 488 | AfterPropName, 489 | AfterColon, 490 | AfterPropValue, 491 | AfterComma, 492 | } -------------------------------------------------------------------------------- /src/ScannerState.ts: -------------------------------------------------------------------------------- 1 | import {InputPosition} from "./InputPosition"; 2 | import {TokenType} from "./TokenType"; 3 | import {JsonToken} from "./JsonToken"; 4 | import {FracturedJsonError} from "./FracturedJsonError"; 5 | 6 | /** 7 | * Class for keeping track of info while scanning text into JSON tokens. 8 | */ 9 | export class ScannerState { 10 | CurrentPosition: InputPosition = { Index: 0, Row:0, Column:0 }; 11 | TokenPosition: InputPosition = { Index: 0, Row:0, Column:0 }; 12 | NonWhitespaceSinceLastNewline: boolean = false; 13 | 14 | constructor(originalText: string) { 15 | this._originalText = originalText; 16 | } 17 | 18 | Advance(isWhitespace: boolean): void { 19 | if (this.CurrentPosition.Index>= MaxDocSize) 20 | throw new Error("Maximum document length exceeded"); 21 | this.CurrentPosition.Index += 1; 22 | this.CurrentPosition.Column += 1; 23 | this.NonWhitespaceSinceLastNewline ||= !isWhitespace; 24 | } 25 | 26 | NewLine(): void { 27 | if (this.CurrentPosition.Index>= MaxDocSize) 28 | throw new Error("Maximum document length exceeded"); 29 | this.CurrentPosition.Index += 1; 30 | this.CurrentPosition.Row += 1; 31 | this.CurrentPosition.Column = 0; 32 | this.NonWhitespaceSinceLastNewline = false; 33 | } 34 | 35 | SetTokenStart() : void { 36 | this.TokenPosition = { ...this.CurrentPosition }; 37 | } 38 | 39 | MakeTokenFromBuffer(type: TokenType, trimEnd: boolean = false): JsonToken { 40 | const substring = this._originalText.substring(this.TokenPosition.Index, this.CurrentPosition.Index); 41 | return { 42 | Type: type, 43 | Text: (trimEnd) ? substring.trimEnd() : substring, 44 | InputPosition: {...this.TokenPosition}, 45 | }; 46 | } 47 | 48 | MakeToken(type: TokenType, text: string): JsonToken { 49 | return { 50 | Type: type, 51 | Text: text, 52 | InputPosition: {...this.TokenPosition}, 53 | } 54 | } 55 | 56 | Current(): number { 57 | return (this.AtEnd())? NaN : this._originalText.charCodeAt(this.CurrentPosition.Index); 58 | } 59 | 60 | AtEnd(): boolean { 61 | return this.CurrentPosition.Index >= this._originalText.length; 62 | } 63 | 64 | Throw(message: string): void { 65 | throw new FracturedJsonError(message, this.CurrentPosition); 66 | } 67 | 68 | private _originalText: string; 69 | } 70 | 71 | const MaxDocSize: number = 2000000000; 72 | -------------------------------------------------------------------------------- /src/StringJoinBuffer.ts: -------------------------------------------------------------------------------- 1 | import {IBuffer} from "./IBuffer"; 2 | 3 | /** 4 | * A place where strings are piled up sequentially to eventually make one big string. 5 | */ 6 | export class StringJoinBuffer implements IBuffer { 7 | constructor(trimTrailingWhitespace: boolean) { 8 | this._trimTrailingWhitespace = trimTrailingWhitespace; 9 | } 10 | Add(...values: string[]): IBuffer { 11 | this._lineBuff.push(...values); 12 | return this; 13 | } 14 | 15 | EndLine(eolString: string): IBuffer { 16 | this.AddLineToWriter(eolString); 17 | return this; 18 | } 19 | 20 | Flush(): IBuffer { 21 | this.AddLineToWriter(""); 22 | return this; 23 | } 24 | 25 | AsString(): string { 26 | // I experimented with a few approaches to try to make this faster, but none of them made much difference 27 | // for an 8MB file. Turns out Array.join is really quite good. 28 | return this._docBuff.join(""); 29 | } 30 | 31 | private _lineBuff: string[] = []; 32 | private readonly _docBuff: string[] = []; 33 | private readonly _trimTrailingWhitespace: boolean; 34 | 35 | /** 36 | * Takes the contents of _lineBuff and merges them into a string and adds it to _docBuff. If desired, 37 | * we trim trailing whitespace in the process. 38 | */ 39 | private AddLineToWriter(eolString: string): void { 40 | if (this._lineBuff.length===0 && eolString.length===0) 41 | return; 42 | 43 | let line = this._lineBuff.join(""); 44 | if (this._trimTrailingWhitespace) 45 | line = line.trimEnd(); 46 | 47 | this._docBuff.push(line + eolString); 48 | this._lineBuff = []; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/TableCommaPlacement.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Specifies where commas should be in table-formatted elements. 3 | */ 4 | export enum TableCommaPlacement 5 | { 6 | /** 7 | * Commas come right after the element that comes before them. 8 | */ 9 | BeforePadding, 10 | 11 | /** 12 | * Commas come after the column padding, all lined with each other. 13 | */ 14 | AfterPadding, 15 | 16 | /** 17 | * Commas come right after the element, except in the case of columns of numbers. 18 | */ 19 | BeforePaddingExceptNumbers, 20 | } 21 | -------------------------------------------------------------------------------- /src/TableTemplate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Collects spacing information about the columns of a potential table. Each TableTemplate corresponds to 3 | * a part of a row, and they're nested recursively to match the JSON structure. (Also used in complex multiline 4 | * arrays to try to fit them all nicely together.) 5 | * 6 | * Say you have an object/array where each item would make a nice row all by itself. We want to try to line up 7 | * everything about it - comments, prop names, values. If the row items are themselves objects/arrays, ideally 8 | * we'd like to line up all of their children, too, recursively. This only works as long as the structure/types 9 | * are consistent. 10 | */ 11 | import {JsonItemType} from "./JsonItemType"; 12 | import {BracketPaddingType} from "./BracketPaddingType"; 13 | import {PaddedFormattingTokens} from "./PaddedFormattingTokens"; 14 | import {JsonItem} from "./JsonItem"; 15 | import {NumberListAlignment} from "./NumberListAlignment"; 16 | import {IBuffer} from "./IBuffer"; 17 | 18 | export class TableTemplate { 19 | /** 20 | * The property name in the table that this segment matches up with. 21 | */ 22 | LocationInParent: string | undefined = undefined; 23 | 24 | /** 25 | * The type of values in the column, if they're uniform. There's some wiggle-room here: for instance, 26 | * true and false have different JsonItemTypes but are considered the same type for table purposes. 27 | */ 28 | Type: JsonItemType = JsonItemType.Null; 29 | 30 | /** 31 | * Assessment of whether this is a viable column. The main qualifying factor is that all corresponding pieces 32 | * of each row are the same type. 33 | */ 34 | IsRowDataCompatible: boolean = true; 35 | RowCount: number = 0; 36 | 37 | NameLength: number = 0; 38 | SimpleValueLength: number = 0; 39 | PrefixCommentLength: number = 0; 40 | MiddleCommentLength: number = 0; 41 | PostfixCommentLength: number = 0; 42 | IsAnyPostCommentLineStyle: boolean = false; 43 | PadType: BracketPaddingType = BracketPaddingType.Simple; 44 | 45 | /** 46 | * True if this is a number column and we're allowed by settings to normalize numbers (rewrite them with the same 47 | * precision), and if none of the numbers have too many digits or require scientific notation. 48 | */ 49 | AllowNumberNormalization: boolean = false; 50 | 51 | /** 52 | * True if this column contains only numbers and nulls. Number columns are formatted specially, depending on 53 | * settings. 54 | */ 55 | IsNumberList: boolean = false; 56 | 57 | /** 58 | * Length of the value for this template when things are complicated. For arrays and objects, it's the sum of 59 | * all the child templates' lengths, plus brackets and commas and such. For number lists, it's the space 60 | * required to align them as appropriate. 61 | */ 62 | CompositeValueLength: number = 0; 63 | 64 | /** 65 | * Length of the entire template, including space for the value, property name, and all comments. 66 | */ 67 | TotalLength: number = 0; 68 | 69 | /** 70 | * If this TableTemplate corresponds to an object or array, Children contains sub-templates 71 | * for the array/object's children. 72 | */ 73 | Children: TableTemplate[] = []; 74 | 75 | constructor(pads: PaddedFormattingTokens, numberListAlignment: NumberListAlignment) { 76 | this._pads = pads; 77 | this._numberListAlignment = numberListAlignment; 78 | this.AllowNumberNormalization = (numberListAlignment===NumberListAlignment.Normalize); 79 | this.IsNumberList = true; 80 | } 81 | 82 | /** 83 | * Analyzes an object/array for formatting as a potential table. The tableRoot is a container that 84 | * is split out across many lines. Each "row" is a single child written inline. 85 | */ 86 | MeasureTableRoot(tableRoot: JsonItem): void { 87 | // For each row of the potential table, measure it and its children, making room for everything. 88 | // (Or, if there are incompatible types at any level, set CanBeUsedInTable to false.) 89 | for (const child of tableRoot.Children) 90 | this.MeasureRowSegment(child); 91 | 92 | // Get rid of incomplete junk and figure out our size. 93 | this.PruneAndRecompute(Number.MAX_VALUE); 94 | 95 | // If there are fewer than 2 actual data rows (i.e., not standalone comments), no point making a table. 96 | this.IsRowDataCompatible &&= (this.RowCount >= 2); 97 | } 98 | 99 | TryToFit(maximumLength: number): boolean { 100 | for (let complexity = this.GetTemplateComplexity(); complexity >= 0; --complexity) { 101 | if (this.TotalLength <= maximumLength) 102 | return true; 103 | this.PruneAndRecompute(complexity - 1); 104 | } 105 | 106 | return false; 107 | } 108 | 109 | /** 110 | * Added the number, properly aligned and possibly reformatted, according to our measurements. 111 | * This assumes that the segment is a number list, and therefore that the item is a number or null. 112 | */ 113 | FormatNumber(buffer: IBuffer, item: JsonItem, commaBeforePadType: string): void { 114 | const formatType = (this._numberListAlignment===NumberListAlignment.Normalize && !this.AllowNumberNormalization) 115 | ? NumberListAlignment.Left 116 | : this._numberListAlignment; 117 | 118 | // The easy cases. Use the value exactly as it was in the source doc. 119 | switch (formatType) { 120 | case NumberListAlignment.Left: 121 | buffer.Add(item.Value, commaBeforePadType, 122 | this._pads.Spaces(this.SimpleValueLength - item.ValueLength)); 123 | return; 124 | case NumberListAlignment.Right: 125 | buffer.Add(this._pads.Spaces(this.SimpleValueLength - item.ValueLength), item.Value, 126 | commaBeforePadType); 127 | return; 128 | } 129 | 130 | let maxDigBefore: number; 131 | let valueStr: string; 132 | let valueLength: number; 133 | 134 | if (formatType === NumberListAlignment.Normalize) { 135 | // Normalize case - rewrite the number with the appropriate precision. 136 | if (item.Type === JsonItemType.Null) { 137 | buffer.Add(this._pads.Spaces(this._maxDigBeforeDecNorm - item.ValueLength), item.Value, 138 | commaBeforePadType, this._pads.Spaces(this.CompositeValueLength - this._maxDigBeforeDecNorm)); 139 | return; 140 | } 141 | 142 | maxDigBefore = this._maxDigBeforeDecNorm; 143 | const numericVal = Number(item.Value); 144 | valueStr = numericVal.toFixed(this._maxDigAfterDecNorm); 145 | valueLength = valueStr.length; 146 | } 147 | else { 148 | // Decimal case - line up the decimals (or E's) but leave the value exactly as it was in the source. 149 | maxDigBefore = this._maxDigBeforeDecRaw; 150 | valueStr = item.Value; 151 | valueLength = item.ValueLength; 152 | } 153 | 154 | let leftPad: number; 155 | let rightPad: number; 156 | const indexOfDot = valueStr.search(TableTemplate._dotOrE); 157 | if (indexOfDot > 0) { 158 | leftPad = maxDigBefore - indexOfDot; 159 | rightPad = this.CompositeValueLength - leftPad - valueLength; 160 | } 161 | else { 162 | leftPad = maxDigBefore - valueLength; 163 | rightPad = this.CompositeValueLength - maxDigBefore; 164 | } 165 | 166 | buffer.Add(this._pads.Spaces(leftPad), valueStr, commaBeforePadType, this._pads.Spaces(rightPad)); 167 | } 168 | 169 | private static readonly _trulyZeroValString = new RegExp("^-?[0.]+([eE].*)?$"); 170 | private static readonly _dotOrE = new RegExp("[.eE]"); 171 | private readonly _pads: PaddedFormattingTokens; 172 | private _numberListAlignment: NumberListAlignment; 173 | private _maxDigBeforeDecRaw: number = 0; 174 | private _maxDigAfterDecRaw: number = 0; 175 | private _maxDigBeforeDecNorm: number = 0; 176 | private _maxDigAfterDecNorm: number = 0; 177 | 178 | /** 179 | * Adjusts this TableTemplate (and its children) to make room for the given rowSegment (and its children). 180 | */ 181 | private MeasureRowSegment(rowSegment: JsonItem): void { 182 | // Standalone comments and blank lines don't figure into template measurements 183 | if (rowSegment.Type === JsonItemType.BlankLine || rowSegment.Type === JsonItemType.BlockComment 184 | || rowSegment.Type === JsonItemType.LineComment) 185 | return; 186 | 187 | // Make sure this rowSegment's type is compatible with the ones we've seen so far. Null is compatible 188 | // with all types. It the types aren't compatible, we can still align this element and its comments, 189 | // but not any children for arrays/objects. 190 | if (rowSegment.Type === JsonItemType.False || rowSegment.Type === JsonItemType.True) { 191 | this.IsRowDataCompatible &&= (this.Type === JsonItemType.True || this.Type === JsonItemType.Null); 192 | this.Type = JsonItemType.True; 193 | this.IsNumberList = false; 194 | } 195 | else if (rowSegment.Type === JsonItemType.Number) { 196 | this.IsRowDataCompatible &&= (this.Type === JsonItemType.Number || this.Type === JsonItemType.Null); 197 | this.Type = JsonItemType.Number; 198 | } 199 | else if (rowSegment.Type === JsonItemType.Null) { 200 | this._maxDigBeforeDecNorm = Math.max(this._maxDigBeforeDecNorm, this._pads.LiteralNullLen); 201 | this._maxDigBeforeDecRaw = Math.max(this._maxDigBeforeDecRaw, this._pads.LiteralNullLen); 202 | } 203 | else { 204 | this.IsRowDataCompatible &&= (this.Type === rowSegment.Type || this.Type === JsonItemType.Null); 205 | if (this.Type === JsonItemType.Null) 206 | this.Type = rowSegment.Type; 207 | this.IsNumberList = false; 208 | } 209 | 210 | // If multiple lines are necessary for a row (probably due to pesky comments), we can't make a table. 211 | this.IsRowDataCompatible &&= !rowSegment.RequiresMultipleLines; 212 | 213 | // Update the numbers 214 | this.RowCount += 1; 215 | this.NameLength = Math.max(this.NameLength, rowSegment.NameLength); 216 | this.SimpleValueLength = Math.max(this.SimpleValueLength, rowSegment.ValueLength); 217 | this.MiddleCommentLength = Math.max(this.MiddleCommentLength, rowSegment.MiddleCommentLength); 218 | this.PrefixCommentLength = Math.max(this.PrefixCommentLength, rowSegment.PrefixCommentLength); 219 | this.PostfixCommentLength = Math.max(this.PostfixCommentLength, rowSegment.PostfixCommentLength); 220 | this.IsAnyPostCommentLineStyle ||= rowSegment.IsPostCommentLineStyle; 221 | 222 | if (rowSegment.Complexity >= 2) 223 | this.PadType = BracketPaddingType.Complex; 224 | 225 | // Everything after this is moot if the column doesn't have a uniform type. 226 | if (!this.IsRowDataCompatible) 227 | return; 228 | 229 | if (rowSegment.Type === JsonItemType.Array) { 230 | // For each row in this rowSegment, find or create this TableTemplate's child template for 231 | // the that array index, and then measure recursively. 232 | for (let i = 0; i < rowSegment.Children.length; ++i) { 233 | if (this.Children.length <= i) 234 | this.Children.push(new TableTemplate(this._pads, this._numberListAlignment)); 235 | this.Children[i].MeasureRowSegment(rowSegment.Children[i]); 236 | } 237 | } 238 | else if (rowSegment.Type === JsonItemType.Object) { 239 | // If this object has multiple children with the same property name, which is allowed by the JSON standard 240 | // although it's hard to imagine anyone would deliberately do it, we can't format it as part of a table. 241 | if (this.ContainsDuplicateKeys(rowSegment.Children)) { 242 | this.IsRowDataCompatible = false; 243 | return; 244 | } 245 | 246 | // For each property in rowSegment, check whether there's sub-template with the same name. If not 247 | // found, create one. Then measure recursively. 248 | for (const rowSegChild of rowSegment.Children) { 249 | let subTemplate = this.Children.find(tt => tt.LocationInParent === rowSegChild.Name); 250 | if (!subTemplate) { 251 | subTemplate = new TableTemplate(this._pads, this._numberListAlignment); 252 | subTemplate.LocationInParent = rowSegChild.Name; 253 | this.Children.push(subTemplate); 254 | } 255 | subTemplate.MeasureRowSegment(rowSegChild); 256 | } 257 | } 258 | else if (rowSegment.Type === JsonItemType.Number && this.IsNumberList) { 259 | // So far, everything in this column is a number (or null). We need to reevaluate whether we're allowed 260 | // to normalize the numbers - write them all with the same number of digits after the decimal point. 261 | // We also need to take some measurements for both contingencies. 262 | const maxChars = 15; 263 | const parsedVal = Number(rowSegment.Value); 264 | const normalizedStr = parsedVal.toString(); 265 | this.AllowNumberNormalization &&= !isNaN(parsedVal) 266 | && parsedVal !== Infinity && parsedVal !== -Infinity 267 | && normalizedStr.length <= maxChars 268 | && normalizedStr.indexOf("e") < 0 269 | && (parsedVal!==0.0 || TableTemplate._trulyZeroValString.test(rowSegment.Value)); 270 | 271 | // Measure the number of digits before and after the decimal point if we write it as a standard, 272 | // non-scientific notation number. 273 | const indexOfDotNorm = normalizedStr.indexOf('.'); 274 | this._maxDigBeforeDecNorm = 275 | Math.max(this._maxDigBeforeDecNorm, (indexOfDotNorm >= 0) ? indexOfDotNorm : normalizedStr.length); 276 | this._maxDigAfterDecNorm = 277 | Math.max(this._maxDigAfterDecNorm, (indexOfDotNorm >= 0) ? normalizedStr.length - indexOfDotNorm - 1 : 0); 278 | 279 | // Measure the number of digits before and after the decimal point (or E scientific notation with not 280 | // decimal point), using the number exactly as it was in the input document. 281 | const indexOfDotRaw = rowSegment.Value.search(TableTemplate._dotOrE); 282 | this._maxDigBeforeDecRaw = 283 | Math.max(this._maxDigBeforeDecRaw, (indexOfDotRaw >= 0) ? indexOfDotRaw : rowSegment.ValueLength); 284 | this._maxDigAfterDecRaw = 285 | Math.max(this._maxDigAfterDecRaw, (indexOfDotRaw >= 0) ? rowSegment.ValueLength - indexOfDotRaw - 1 : 0); 286 | } 287 | 288 | this.AllowNumberNormalization &&= this.IsNumberList; 289 | } 290 | 291 | 292 | private PruneAndRecompute(maxAllowedComplexity: number): void { 293 | if (maxAllowedComplexity <= 0 || !this.IsRowDataCompatible) 294 | this.Children = []; 295 | 296 | for (const subTemplate of this.Children) 297 | subTemplate.PruneAndRecompute(maxAllowedComplexity-1); 298 | 299 | if (this.IsNumberList) { 300 | this.CompositeValueLength = this.GetNumberFieldWidth(); 301 | } 302 | else if (this.Children.length>0) { 303 | const totalChildLen = this.Children.map(ch => ch.TotalLength).reduce((prev:number, cur:number) => prev + cur); 304 | this.CompositeValueLength = totalChildLen 305 | + Math.max(0, this._pads.CommaLen * (this.Children.length-1)) 306 | + this._pads.ArrStartLen(this.PadType) 307 | + this._pads.ArrEndLen(this.PadType); 308 | } 309 | else { 310 | this.CompositeValueLength = this.SimpleValueLength; 311 | } 312 | 313 | this.TotalLength = 314 | ((this.PrefixCommentLength > 0)? this.PrefixCommentLength + this._pads.CommentLen : 0) 315 | + ((this.NameLength > 0)? this.NameLength + this._pads.ColonLen : 0) 316 | + ((this.MiddleCommentLength > 0)? this.MiddleCommentLength + this._pads.CommentLen : 0) 317 | + this.CompositeValueLength 318 | + ((this.PostfixCommentLength > 0)? this.PostfixCommentLength + this._pads.CommentLen : 0); 319 | } 320 | 321 | private GetTemplateComplexity(): number { 322 | if (this.Children.length === 0) 323 | return 0; 324 | const childComplexities = this.Children.map(ch => ch.GetTemplateComplexity()); 325 | return 1 + Math.max(...childComplexities); 326 | } 327 | 328 | private ContainsDuplicateKeys(list: JsonItem[]): boolean { 329 | const keys = list.map(ji => ji.Name); 330 | return keys.some((v:string, i:number) => keys.indexOf(v)!==i); 331 | } 332 | 333 | private GetNumberFieldWidth(): number { 334 | if (this._numberListAlignment === NumberListAlignment.Normalize && this.AllowNumberNormalization) 335 | { 336 | const normDecLen = (this._maxDigAfterDecNorm > 0) ? 1 : 0; 337 | return this._maxDigBeforeDecNorm + normDecLen + this._maxDigAfterDecNorm; 338 | } 339 | else if (this._numberListAlignment === NumberListAlignment.Decimal) 340 | { 341 | const rawDecLen = (this._maxDigAfterDecRaw > 0) ? 1 : 0; 342 | return this._maxDigBeforeDecRaw + rawDecLen + this._maxDigAfterDecRaw; 343 | } 344 | 345 | return this.SimpleValueLength; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/TokenEnumerator.ts: -------------------------------------------------------------------------------- 1 | import {JsonToken} from "./JsonToken"; 2 | import {FracturedJsonError} from "./FracturedJsonError"; 3 | 4 | /** 5 | * Provided .NET-like Enumerator semantics wrapped around a TypeScript Generator. 6 | */ 7 | export class TokenEnumerator { 8 | public get Current():JsonToken { 9 | if (!this._current) 10 | throw new FracturedJsonError("Illegal enumerator usage"); 11 | return this._current; 12 | } 13 | 14 | constructor(generator: Generator) { 15 | this._generator = generator; 16 | } 17 | 18 | MoveNext(): boolean { 19 | const genItem = this._generator.next(); 20 | this._current = genItem.value; 21 | return !genItem.done; 22 | } 23 | 24 | private _generator:Generator; 25 | private _current?: JsonToken; 26 | } 27 | -------------------------------------------------------------------------------- /src/TokenGenerator.ts: -------------------------------------------------------------------------------- 1 | import {JsonToken} from "./JsonToken"; 2 | import {ScannerState} from "./ScannerState"; 3 | import {TokenType} from "./TokenType"; 4 | 5 | /** 6 | * Converts a sequence of characters into a sequence of JSON tokens. There's no guarantee that the tokens make 7 | * sense - just that they're lexically correct. 8 | */ 9 | export function* TokenGenerator(inputJson: string): Generator { 10 | const state = new ScannerState(inputJson); 11 | 12 | while (true) { 13 | if (state.AtEnd()) 14 | return; 15 | 16 | // With the exception of whitespace, all of the characters examined in the switch below will send us to 17 | // a function that will potentially read more characters and either return the appropriate token, or 18 | // throw a FracturedJsonError. If there is no error, state.Current() will be pointing to the character 19 | // *after* the last one in the token that was read. 20 | // 21 | // Note that we're comparing the numeric (UTF16, I guess) form of the character to constants - or as close 22 | // as we can reasonable come to them. The alternative is to create a new single-character string at every 23 | // step and then do string comparisons. I'm assuming the numbers are faster, but who knows. 24 | const ch = state.Current(); 25 | switch (ch) { 26 | case _codeSpace: 27 | case _codeTab: 28 | case _codeCR: 29 | // Regular unremarkable whitespace. 30 | state.Advance(true); 31 | break; 32 | 33 | case _codeLF: 34 | // If a line contained only whitespace, return a blank line. Note that we're ignoring CRs. If 35 | // we get a Window's style CRLF, we throw away the CR, and then trigger on the LF just like we would 36 | // for Unix. 37 | if (!state.NonWhitespaceSinceLastNewline) 38 | yield state.MakeToken(TokenType.BlankLine, "\n"); 39 | 40 | state.NewLine(); 41 | 42 | // If this new line turns out to be nothing but whitespace, we want to report the blank line 43 | // token as starting at the beginning of the line. Otherwise you get into \r\n vs. \n issues. 44 | state.SetTokenStart(); 45 | break; 46 | 47 | case _codeOpenCurly: 48 | yield ProcessSingleChar(state, "{", TokenType.BeginObject); 49 | break; 50 | case _codeCloseCurly: 51 | yield ProcessSingleChar(state, "}", TokenType.EndObject); 52 | break; 53 | case _codeOpenSquare: 54 | yield ProcessSingleChar(state, "[", TokenType.BeginArray); 55 | break; 56 | case _codeCloseSquare: 57 | yield ProcessSingleChar(state, "]", TokenType.EndArray); 58 | break; 59 | 60 | case _codeColon: 61 | yield ProcessSingleChar(state, ":", TokenType.Colon); 62 | break; 63 | case _codeComma: 64 | yield ProcessSingleChar(state, ",", TokenType.Comma); 65 | break; 66 | 67 | case _codeLittleT: 68 | yield ProcessKeyword(state, "true", TokenType.True); 69 | break; 70 | case _codeLittleF: 71 | yield ProcessKeyword(state, "false", TokenType.False); 72 | break; 73 | case _codeLittleN: 74 | yield ProcessKeyword(state, "null", TokenType.Null); 75 | break; 76 | 77 | case _codeSlash: 78 | yield ProcessComment(state); 79 | break; 80 | 81 | case _codeQuote: 82 | yield ProcessString(state); 83 | break; 84 | 85 | case _codeMinus: 86 | yield ProcessNumber(state); 87 | break; 88 | 89 | default: 90 | if (!isDigit(ch)) 91 | state.Throw("Unexpected character"); 92 | yield ProcessNumber(state); 93 | break; 94 | } 95 | } 96 | } 97 | 98 | function ProcessSingleChar(state: ScannerState, symbol: string, type: TokenType): JsonToken { 99 | state.SetTokenStart(); 100 | const token = state.MakeToken(type, symbol); 101 | state.Advance(false); 102 | return token; 103 | } 104 | 105 | function ProcessKeyword(state: ScannerState, keyword: string, type: TokenType): JsonToken { 106 | state.SetTokenStart(); 107 | for (let i=1; i 0) { 174 | if (!isHex(ch)) 175 | state.Throw("Bad unicode escape in string"); 176 | expectedHexCount -= 1; 177 | state.Advance(false); 178 | continue; 179 | } 180 | 181 | // Only certain characters are allowed after backslashes. The only ones that affect us here are 182 | // \u, which needs to be followed by 4 hex digits, and \", which should not end the string. 183 | if (lastCharBeganEscape) { 184 | if (!isLegalAfterBackslash(ch)) 185 | state.Throw("Bad escaped character in string"); 186 | if (ch === _codeLittleU) 187 | expectedHexCount = 4; 188 | lastCharBeganEscape = false; 189 | state.Advance(false); 190 | continue; 191 | } 192 | 193 | if (isControl(ch)) 194 | state.Throw("Control characters are not allowed in strings"); 195 | 196 | state.Advance(false); 197 | if (ch===_codeQuote) 198 | return state.MakeTokenFromBuffer(TokenType.String); 199 | if (ch===_codeBackSlash) 200 | lastCharBeganEscape = true; 201 | } 202 | } 203 | 204 | function ProcessNumber(state: ScannerState): JsonToken { 205 | state.SetTokenStart(); 206 | 207 | let phase = NumberPhase.Beginning; 208 | while (true) { 209 | const ch = state.Current(); 210 | let handling = CharHandling.ValidAndConsumed; 211 | 212 | switch (phase) { 213 | case NumberPhase.Beginning: 214 | if (ch === _codeMinus) 215 | phase = NumberPhase.PastLeadingSign; 216 | else if (ch === _codeZero) 217 | phase = NumberPhase.PastWhole; 218 | else if (isDigit(ch)) 219 | phase = NumberPhase.PastFirstDigitOfWhole; 220 | else 221 | handling = CharHandling.InvalidatesToken; 222 | break; 223 | 224 | case NumberPhase.PastLeadingSign: 225 | if (!isDigit(ch)) 226 | handling = CharHandling.InvalidatesToken; 227 | else if (ch === _codeZero) 228 | phase = NumberPhase.PastWhole; 229 | else 230 | phase = NumberPhase.PastFirstDigitOfWhole; 231 | break; 232 | 233 | // We've started with a 1-9 and more digits are welcome. 234 | case NumberPhase.PastFirstDigitOfWhole: 235 | if (ch === _codeDecimal) 236 | phase = NumberPhase.PastDecimalPoint; 237 | else if (ch === _codeLittleE || ch === _codeBigE) 238 | phase = NumberPhase.PastE; 239 | else if (!isDigit(ch)) 240 | handling = CharHandling.StartOfNewToken; 241 | break; 242 | 243 | // We started with a 0. Another digit at this point would not be part of this token. 244 | case NumberPhase.PastWhole: 245 | if (ch === _codeDecimal) 246 | phase = NumberPhase.PastDecimalPoint; 247 | else if (ch === _codeLittleE || ch === _codeBigE) 248 | phase = NumberPhase.PastE; 249 | else 250 | handling = CharHandling.StartOfNewToken; 251 | break; 252 | 253 | case NumberPhase.PastDecimalPoint: 254 | if (isDigit(ch)) 255 | phase = NumberPhase.PastFirstDigitOfFractional; 256 | else 257 | handling = CharHandling.InvalidatesToken; 258 | break; 259 | 260 | case NumberPhase.PastFirstDigitOfFractional: 261 | if (ch === _codeLittleE || ch === _codeBigE) 262 | phase = NumberPhase.PastE; 263 | else if (!isDigit(ch)) 264 | handling = CharHandling.StartOfNewToken; 265 | break; 266 | 267 | // An E must be followed by either a digit or +/- 268 | case NumberPhase.PastE: 269 | if (ch === _codePlus || ch === _codeMinus) 270 | phase = NumberPhase.PastExpSign; 271 | else if (isDigit(ch)) 272 | phase = NumberPhase.PastFirstDigitOfExponent; 273 | else 274 | handling = CharHandling.InvalidatesToken; 275 | break; 276 | 277 | // E and a +/- must still be followed by one or more digits. 278 | case NumberPhase.PastExpSign: 279 | if (isDigit(ch)) 280 | phase = NumberPhase.PastFirstDigitOfExponent; 281 | else 282 | handling = CharHandling.InvalidatesToken; 283 | break; 284 | 285 | case NumberPhase.PastFirstDigitOfExponent: 286 | if (!isDigit(ch)) 287 | handling = CharHandling.StartOfNewToken; 288 | break; 289 | } 290 | 291 | if (handling === CharHandling.InvalidatesToken) 292 | state.Throw("Bad character while processing number"); 293 | 294 | if (handling === CharHandling.StartOfNewToken) { 295 | // We're done processing the number, and the enumerator is pointed to the character after it. 296 | return state.MakeTokenFromBuffer(TokenType.Number); 297 | } 298 | 299 | if (!state.AtEnd()) { 300 | state.Advance(false); 301 | continue; 302 | } 303 | 304 | // We've reached the end of the input. Figure out if we read a complete token or not. 305 | switch (phase) { 306 | case NumberPhase.PastFirstDigitOfWhole: 307 | case NumberPhase.PastWhole: 308 | case NumberPhase.PastFirstDigitOfFractional: 309 | case NumberPhase.PastFirstDigitOfExponent: 310 | return state.MakeTokenFromBuffer(TokenType.Number); 311 | default: 312 | state.Throw("Unexpected end of input while processing number"); 313 | break; 314 | } 315 | } 316 | } 317 | 318 | // Number versions of various important characters. I assume it's quicker to compare against these than doing 319 | // a bunch of single-character string compared. But it's possible the JS engine has some slick optimizations for 320 | // that case. 321 | const _codeSpace = " ".charCodeAt(0); 322 | const _codeLF = "\n".charCodeAt(0); 323 | const _codeCR = "\r".charCodeAt(0); 324 | const _codeTab = "\t".charCodeAt(0); 325 | const _codeSlash = "/".charCodeAt(0); 326 | const _codeStar = "*".charCodeAt(0); 327 | const _codeBackSlash = "\\".charCodeAt(0); 328 | const _codeQuote = "\"".charCodeAt(0); 329 | const _codeOpenCurly = "{".charCodeAt(0); 330 | const _codeCloseCurly = "}".charCodeAt(0); 331 | const _codeOpenSquare = "[".charCodeAt(0); 332 | const _codeCloseSquare = "]".charCodeAt(0); 333 | const _codeColon = ":".charCodeAt(0); 334 | const _codeComma = ",".charCodeAt(0); 335 | const _codePlus = "+".charCodeAt(0); 336 | const _codeMinus = "-".charCodeAt(0); 337 | const _codeDecimal = ".".charCodeAt(0); 338 | const _codeZero = "0".charCodeAt(0); 339 | const _codeNine = "9".charCodeAt(0); 340 | const _codeLittleA = "a".charCodeAt(0); 341 | const _codeBigA = "A".charCodeAt(0); 342 | const _codeLittleB = "b".charCodeAt(0); 343 | const _codeLittleE = "e".charCodeAt(0); 344 | const _codeBigE = "E".charCodeAt(0); 345 | const _codeLittleF = "f".charCodeAt(0); 346 | const _codeBigF = "F".charCodeAt(0); 347 | const _codeLittleN = "n".charCodeAt(0); 348 | const _codeLittleR = "r".charCodeAt(0); 349 | const _codeLittleT = "t".charCodeAt(0); 350 | const _codeLittleU = "u".charCodeAt(0); 351 | 352 | 353 | function isDigit(charCode:number): boolean { 354 | return charCode>=_codeZero && charCode<=_codeNine; 355 | } 356 | 357 | function isHex(charCode:number): boolean { 358 | return (charCode>=_codeZero && charCode<=_codeNine) 359 | || (charCode>=_codeLittleA && charCode<=_codeLittleF) 360 | || (charCode>=_codeBigA && charCode<=_codeBigF); 361 | } 362 | 363 | function isLegalAfterBackslash(charCode:number): boolean { 364 | switch (charCode) { 365 | case _codeQuote: 366 | case _codeBackSlash: 367 | case _codeSlash: 368 | case _codeLittleB: 369 | case _codeLittleF: 370 | case _codeLittleN: 371 | case _codeLittleR: 372 | case _codeLittleT: 373 | case _codeLittleU: 374 | return true; 375 | default: 376 | return false; 377 | } 378 | } 379 | 380 | function isControl(charCode:number): boolean { 381 | return (charCode>=0x00 && charCode<=0x1F) 382 | || (charCode === 0x7F) 383 | || (charCode>=0x80 && charCode<=0x9F); 384 | } 385 | 386 | enum NumberPhase { 387 | Beginning, 388 | PastLeadingSign, 389 | PastFirstDigitOfWhole, 390 | PastWhole, 391 | PastDecimalPoint, 392 | PastFirstDigitOfFractional, 393 | PastE, 394 | PastExpSign, 395 | PastFirstDigitOfExponent, 396 | } 397 | 398 | enum CharHandling { 399 | InvalidatesToken, 400 | ValidAndConsumed, 401 | StartOfNewToken, 402 | } 403 | 404 | -------------------------------------------------------------------------------- /src/TokenType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types of tokens that can be read from a stream of JSON text. Comments aren't part of the official JSON 3 | * standard, but we're supporting them anyway. BlankLine isn't typically a token by itself, but we want to 4 | * try to preserve those. 5 | */ 6 | export enum TokenType { 7 | Invalid, 8 | BeginArray, 9 | EndArray, 10 | BeginObject, 11 | EndObject, 12 | String, 13 | Number, 14 | Null, 15 | True, 16 | False, 17 | BlockComment, 18 | LineComment, 19 | BlankLine, 20 | Comma, 21 | Colon, 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Exporting just the basics here. 2 | export {FracturedJsonOptions} from './FracturedJsonOptions'; 3 | export {CommentPolicy} from './CommentPolicy'; 4 | export {EolStyle} from './EolStyle'; 5 | export {NumberListAlignment} from './NumberListAlignment'; 6 | export {TableCommaPlacement} from './TableCommaPlacement'; 7 | 8 | export {FracturedJsonError} from './FracturedJsonError'; 9 | 10 | export {Formatter} from './Formatter'; 11 | -------------------------------------------------------------------------------- /test/AlwaysExpandFormatting.test.ts: -------------------------------------------------------------------------------- 1 | import {Formatter} from "../src"; 2 | // @ts-ignore 3 | import {DoInstancesLineUp} from "./Helpers"; 4 | 5 | // Tests for the AlwaysExpandDepth setting. 6 | describe("Always Expand Formatting Tests", () => { 7 | test("Always expand depth honored", () => { 8 | const inputLines = [ 9 | "[", 10 | "[ {'x':1}, false ],", 11 | "{ 'a':[2], 'b':[3] }", 12 | "]" 13 | ]; 14 | const input = inputLines.join("\n").replace(/'/g, '"'); 15 | const formatter = new Formatter(); 16 | formatter.Options.MaxInlineComplexity = 100; 17 | formatter.Options.MaxTotalLineLength = Number.MAX_VALUE; 18 | 19 | let output = formatter.Reformat(input, 0); 20 | let outputLines = output.trimEnd().split('\n'); 21 | 22 | // With high maximum complexity and long line length, it should all be in one line. 23 | expect(outputLines.length).toBe(1); 24 | 25 | formatter.Options.AlwaysExpandDepth = 0; 26 | output = formatter.Reformat(input, 0); 27 | outputLines = output.trimEnd().split('\n'); 28 | 29 | // If we force expanding at depth 0, we should get 4 lines (more or less like the input). 30 | expect(outputLines.length).toBe(4); 31 | 32 | formatter.Options.AlwaysExpandDepth = 1; 33 | output = formatter.Reformat(input, 0); 34 | outputLines = output.trimEnd().split('\n'); 35 | 36 | // If we force expanding at depth 1, we'll get lots of lines. 37 | expect(outputLines.length).toBe(10); 38 | }); 39 | 40 | test("AlwaysExpandDepth doesn't prevent table formatting", () => { 41 | const input = "[ [1, 22, 9 ], [333, 4, 9 ] ]"; 42 | 43 | const formatter = new Formatter(); 44 | formatter.Options.AlwaysExpandDepth = 0; 45 | 46 | let output = formatter.Reformat(input, 0); 47 | let outputLines = output.trimEnd().split('\n'); 48 | 49 | expect(outputLines.length).toBe(4); 50 | expect(DoInstancesLineUp(outputLines, ",")).toBeTruthy(); 51 | expect(DoInstancesLineUp(outputLines, "9")).toBeTruthy(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/CommentFormatting.test.ts: -------------------------------------------------------------------------------- 1 | import {Formatter} from "../src"; 2 | import {CommentPolicy} from "../src"; 3 | // @ts-ignore 4 | import {DoInstancesLineUp} from "./Helpers"; 5 | 6 | describe("Comment formatting tests", () => { 7 | test("Pre and Post comments stay with elems", () => { 8 | const inputLines = [ 9 | "{", 10 | " /*1*/ 'a': [true, true], /*2*/", 11 | " 'b': [false, false], ", 12 | " /*3*/ 'c': [false, true] /*4*/", 13 | "}" 14 | ]; 15 | const input = inputLines.join("\n").replace(/'/g, '"'); 16 | const formatter = new Formatter(); 17 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 18 | formatter.Options.MaxInlineComplexity = 2; 19 | 20 | let output = formatter.Reformat(input, 0); 21 | let outputLines = output.trimEnd().split('\n'); 22 | 23 | // With these options, they should all be written on one line. 24 | expect(outputLines.length).toBe(1); 25 | 26 | formatter.Options.MaxInlineComplexity = 1; 27 | output = formatter.Reformat(input, 0); 28 | outputLines = output.trimEnd().split('\n'); 29 | 30 | // With MaxInlineComplexity=1, the output should be much like the input (except with a little padding). 31 | // Importantly, the comment 2 should stay with the 'a' line, and comment 3 should be with 'c'. 32 | expect(outputLines.length).toBe(5); 33 | expect(outputLines[1]).toContain('"a"'); 34 | expect(outputLines[1]).toContain('/*2*/'); 35 | expect(outputLines[3]).toContain('"c"'); 36 | expect(outputLines[3]).toContain('/*3*/'); 37 | 38 | // With no inlining possible, every subarray element gets its own line. But the comments before the property 39 | // names and after the array-ending brackets need to stick to those things. 40 | formatter.Options.MaxInlineComplexity = 0; 41 | formatter.Options.MaxCompactArrayComplexity = 0; 42 | formatter.Options.MaxTableRowComplexity = 0; 43 | 44 | output = formatter.Reformat(input, 0); 45 | outputLines = output.trimEnd().split('\n'); 46 | 47 | expect(outputLines.length).toBe(14); 48 | expect(outputLines[1]).toContain('/*1*/ "a"'); 49 | expect(outputLines[4]).toContain('] /*2*/,'); 50 | expect(outputLines[9]).toContain('/*3*/ "c"'); 51 | expect(outputLines[12]).toContain('] /*4*/'); 52 | }); 53 | 54 | 55 | test("Blank lines force expanded", () => { 56 | const inputLines = [ 57 | " [ 1,", 58 | " ", 59 | " 2 ]", 60 | ]; 61 | const input = inputLines.join("\n").replace(/'/g, '"'); 62 | const formatter = new Formatter(); 63 | 64 | let output = formatter.Reformat(input, 0); 65 | let outputLines = output.trimEnd().split('\n'); 66 | 67 | // By default, blank lines are ignored like any other whitespace, so this whole thing gets inlined. 68 | expect(outputLines.length).toBe(1); 69 | 70 | formatter.Options.PreserveBlankLines = true; 71 | output = formatter.Reformat(input, 0); 72 | outputLines = output.trimEnd().split('\n'); 73 | 74 | // If we're preserving blank lines, the array has to be written as expanded, so 1 line for each element, 75 | // 1 for the blank line, and 1 each for []. 76 | expect(outputLines.length).toBe(5); 77 | }); 78 | 79 | test("Can inline middle comments if no line break", () => { 80 | const inputLines = [ 81 | "{'a': /*1*/", 82 | "[true,true]}", 83 | ]; 84 | const input = inputLines.join("\n").replace(/'/g, '"'); 85 | const formatter = new Formatter(); 86 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 87 | 88 | let output = formatter.Reformat(input, 0); 89 | let outputLines = output.trimEnd().split('\n'); 90 | 91 | // There's a comment between the property name and the prop value, but it doesn't require line breaks, 92 | // so the whole thing can be written inline. 93 | expect(outputLines.length).toBe(1); 94 | expect(outputLines[0]).toContain("/*1*/"); 95 | 96 | // If we disallow inlining, it'll be handled as a compact multiline array. 97 | formatter.Options.MaxInlineComplexity = 0; 98 | output = formatter.Reformat(input, 0); 99 | outputLines = output.trimEnd().split('\n'); 100 | 101 | expect(outputLines[1]).toContain('"a": /*1*/ ['); 102 | }); 103 | 104 | test("Split when middle comment requires break 1", () => { 105 | const inputLines = [ 106 | "{'a': //1", 107 | "[true,true]}", 108 | ]; 109 | const input = inputLines.join("\n").replace(/'/g, '"'); 110 | const formatter = new Formatter(); 111 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 112 | 113 | let output = formatter.Reformat(input, 0); 114 | let outputLines = output.trimEnd().split('\n'); 115 | 116 | // Since there's a comment that requires a line break between the property name and its value, the 117 | // comment gets put on a new line with an extra indent level, and the value is written as expanded, 118 | // also with the extra indent level 119 | expect(outputLines.length).toBe(8); 120 | expect(outputLines[1].indexOf('"a"')).toBe(4); 121 | expect(outputLines[2].indexOf('//1')).toBe(8); 122 | expect(outputLines[3].indexOf('[')).toBe(8); 123 | }); 124 | 125 | test("Split when middle comment requires break 2", () => { 126 | const inputLines = [ 127 | "{'a': /*1", 128 | "2*/ [true,true]}", 129 | ]; 130 | const input = inputLines.join("\n").replace(/'/g, '"'); 131 | const formatter = new Formatter(); 132 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 133 | 134 | let output = formatter.Reformat(input, 0); 135 | let outputLines = output.trimEnd().split('\n'); 136 | 137 | // Since there's a comment that requires a line break between the property name and its value, the 138 | // comment gets put on a new line with an extra indent level, and the value is written as expanded, 139 | // also with the extra indent level 140 | expect(outputLines.length).toBe(9); 141 | expect(outputLines[1].indexOf('"a"')).toBe(4); 142 | expect(outputLines[2].indexOf('/*1')).toBe(8); 143 | expect(outputLines[4].indexOf('[')).toBe(8); 144 | }); 145 | 146 | test("Multiline comments preserve relative spacing", () => { 147 | const inputLines = [ 148 | "[ 1,", 149 | " /* +", 150 | " +", 151 | " + */", 152 | " 2]", 153 | ]; 154 | const input = inputLines.join("\n").replace(/'/g, '"'); 155 | const formatter = new Formatter(); 156 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 157 | 158 | let output = formatter.Reformat(input, 0); 159 | let outputLines = output.trimEnd().split('\n'); 160 | 161 | // The +'s should stay lined up in the output. 162 | expect(outputLines.length).toBe(7); 163 | expect(DoInstancesLineUp(outputLines, "+")).toBeTruthy(); 164 | }); 165 | 166 | test("Ambiguous comments in arrays respect commas", () => { 167 | const inputLines = [ 168 | "[ [ 'a' /*1*/, 'b' ],", 169 | " [ 'c', /*2*/ 'd' ] ]", 170 | ]; 171 | const input = inputLines.join("\n").replace(/'/g, '"'); 172 | const formatter = new Formatter(); 173 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 174 | formatter.Options.AlwaysExpandDepth = 99; 175 | 176 | let output = formatter.Reformat(input, 0); 177 | let outputLines = output.trimEnd().split('\n'); 178 | 179 | // We split all of the elements onto separate lines, but the comments should stay with them. The comment 180 | // should stick to the element that's on the same side of the comma. 181 | expect(outputLines.length).toBe(10); 182 | expect(output).toContain('"a" /*1*/,'); 183 | expect(output).toContain('/*2*/ "d"'); 184 | }); 185 | 186 | test("Ambiguous comments in objects respect commas", () => { 187 | const inputLines = [ 188 | "[ { 'a':'a' /*1*/, 'b':'b' },", 189 | " { 'c':'c', /*2*/ 'd':'d'} ]", 190 | ]; 191 | const input = inputLines.join("\n").replace(/'/g, '"'); 192 | const formatter = new Formatter(); 193 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 194 | formatter.Options.AlwaysExpandDepth = 99; 195 | 196 | let output = formatter.Reformat(input, 0); 197 | let outputLines = output.trimEnd().split('\n'); 198 | 199 | // We split all of the elements onto separate lines, but the comments should stay with them. The comment 200 | // should stick to the element that's on the same side of the comma. 201 | expect(outputLines.length).toBe(10); 202 | expect(output).toContain('"a" /*1*/,'); 203 | expect(output).toContain('/*2*/ "d"'); 204 | }); 205 | 206 | test("Top level comments ignored if set", () => { 207 | const inputLines = [ 208 | "//a", 209 | "[1,2, //b", 210 | "3]", 211 | "//c" 212 | ]; 213 | const input = inputLines.join("\n").replace(/'/g, '"'); 214 | const formatter = new Formatter(); 215 | formatter.Options.CommentPolicy = CommentPolicy.Remove; 216 | formatter.Options.AlwaysExpandDepth = 99; 217 | 218 | let output = formatter.Reformat(input, 0); 219 | 220 | expect(output.length).not.toContain("//"); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/EastAsianWideCharacters.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as eaw from 'eastasianwidth'; 3 | import {Formatter} from "../src"; 4 | // @ts-ignore 5 | import {DoInstancesLineUp} from "./Helpers"; 6 | 7 | describe("East Asian wide character tests", function () { 8 | test("Pads wide chars correctly", () => { 9 | const inputLines = [ 10 | "[", 11 | " {'Name': '李小龍', 'Job': 'Actor', 'Born': 1940},", 12 | " {'Name': 'Mark Twain', 'Job': 'Writer', 'Born': 1835},", 13 | " {'Name': '孫子', 'Job': 'General', 'Born': -544}", 14 | "]" 15 | ]; 16 | const input = inputLines.join("\n").replace(/'/g, '"'); 17 | const formatter = new Formatter(); 18 | 19 | let output = formatter.Reformat(input, 0); 20 | let outputLines = output.trimEnd().split('\n'); 21 | 22 | // With the default StringLengthFunc, all characters are treated as having the same width as space, so 23 | // String.IndexOf should give the same number for each row. 24 | expect(DoInstancesLineUp(outputLines, "Job")).toBeTruthy(); 25 | expect(DoInstancesLineUp(outputLines, "Born")).toBeTruthy(); 26 | 27 | formatter.StringLengthFunc = WideCharStringLength; 28 | output = formatter.Reformat(input, 0); 29 | outputLines = output.trimEnd().split('\n'); 30 | 31 | // In using the WideCharStringLength function, the Asian characters are each treated as 2 spaces wide. 32 | // Whether these line up visually for you depends on your font and the rendering policies of your app. 33 | // (It looks right on a Mac terminal.) 34 | expect(outputLines[1].indexOf("Job")).toBe(25); 35 | expect(outputLines[2].indexOf("Job")).toBe(28); 36 | expect(outputLines[3].indexOf("Job")).toBe(26); 37 | }); 38 | }); 39 | 40 | 41 | function WideCharStringLength(str: string): number { 42 | return eaw.length(str); 43 | } 44 | -------------------------------------------------------------------------------- /test/EndingCommaFormatting.test.ts: -------------------------------------------------------------------------------- 1 | import {Formatter} from "../src"; 2 | import {CommentPolicy} from "../src"; 3 | 4 | // Tests to make sure commas are only where they're supposed to be. 5 | describe("Ending comma formatting tests", () => { 6 | // Tests that comments at the end of an expanded object/array don't cause commas before them. 7 | test("No commas for comments expanded", () => { 8 | const inputLines = [ 9 | "[", 10 | "/*a*/", 11 | "1, false", 12 | "/*b*/", 13 | "]" 14 | ]; 15 | const input = inputLines.join("\n").replace(/'/g, '"'); 16 | const formatter = new Formatter(); 17 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 18 | 19 | let output = formatter.Reformat(input, 0); 20 | let outputLines = output.trimEnd().split('\n'); 21 | 22 | // Both comments here are standalone, so we're not allowed to format this as inline or compact-array. 23 | // The row types are dissimilar, so they won't be table-formatted either. 24 | expect(outputLines.length).toBe(6); 25 | 26 | // There should only be one comma - between the 1 and false. 27 | const commaCount = output.match(/,/g)?.length ?? 0; 28 | expect(commaCount).toBe(1); 29 | }); 30 | 31 | // Tests that comments at the end of a table-formatted object/array don't cause commas before them. 32 | test("No commas for comments table", () => { 33 | const inputLines = [ 34 | "[", 35 | "/*a*/", 36 | "[1], [false]", 37 | "/*b*/", 38 | "]" 39 | ]; 40 | const input = inputLines.join("\n").replace(/'/g, '"'); 41 | const formatter = new Formatter(); 42 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 43 | 44 | let output = formatter.Reformat(input, 0); 45 | let outputLines = output.trimEnd().split('\n'); 46 | 47 | // Both comments here are standalone, so we're not allowed to format this as inline or compact-array. 48 | // The row types are both array, so it should be table-formatted. 49 | expect(outputLines.length).toBe(6); 50 | expect(output).toContain("[1 ]"); 51 | 52 | // There should only be one comma - between the [1] and [false]. 53 | const commaCount = output.match(/,/g)?.length ?? 0; 54 | expect(commaCount).toBe(1); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/FilesWithComments/0.jsonc: -------------------------------------------------------------------------------- 1 | /* 2 | * multiline 1 3 | * before root 4 | */ 5 | { 6 | "BannerText": [ 7 | "Sometimes you'll have to protect multiple enemy goals." 8 | ], 9 | "AttackPlans": [ 10 | { 11 | // standalone 1 12 | "TeamId": 1, 13 | "Spawns": [ 14 | { "Time": 0, "UnitType": "Grunt", "SpawnPointIndex": 0 }, // trailing 1 15 | { "Time": 0, "UnitType": "Grunt", "SpawnPointIndex": 0 }, // trailing 2 16 | { "Time": 0, "UnitType": "Grunt", "SpawnPointIndex": 0 }, // trailing 3 17 | 18 | // standalone 2 in the middle somewhere 19 | { "Time": 0, "UnitType": "Grunt", "SpawnPointIndex": 0 }, // trailing 4 20 | { "Time": 0, "UnitType": "Grunt", "SpawnPointIndex": 0 } // trailing 5 21 | ] 22 | }, 23 | { 24 | // standalone 3 25 | "TeamId": 2, "Spawns": [ /* inlineable 1 */ null, /* inlineable 2 */ null ] 26 | } 27 | ], 28 | 29 | /* multiline 2 30 | In the middle somewhere */ 31 | 32 | "DefensePlans": [ 33 | { 34 | "TeamId": 2, 35 | "Placements": [ 36 | /* leading 1 */ { "UnitType": "Archer" , "Position": [41, 7] }, // trailing 6 37 | /* leading 2 */ { "UnitType": "Archer" , "Position": [41, 8] }, 38 | 39 | // standalone 4 in the middle somewhere 40 | { "UnitType": "Archer" , "Position": [41, 9] }, 41 | /* leading 3 */ { "UnitType": "Pikeman" , "Position": [40, 9] } // trailing 7 42 | ] 43 | } 44 | ] 45 | } 46 | 47 | // standalone 5 after root -------------------------------------------------------------------------------- /test/FilesWithComments/1.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | /* a */ 3 | "thing1" // b 4 | : /* c */ 1.234e14, // d 5 | "thing2": 6 | /* 7 | * e 8 | */ 9 | [[19,2], [3,8], [14, 0], [9,9], [9,9], [0,3], [10,1], [9,1], [9,2], [6,13], [18,5], [4,11], [12,2]], 10 | "thing3": // f 11 | false 12 | } 13 | -------------------------------------------------------------------------------- /test/FilesWithComments/2.jsonc: -------------------------------------------------------------------------------- 1 | [ 2 | 1, 3 | /*a*/ 2, 4 | 3, 5 | 4 /*b*/, 6 | 5, 7 | 6, /*c*/ 8 | 7, 9 | 8, /*d*/ 10 | /*e*/ 9 11 | ] 12 | -------------------------------------------------------------------------------- /test/FilesWithComments/3.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | * Multi-line comments 4 | * are fun! 5 | */ 6 | "NumbersWithHex": [ 7 | 254 /*00FE*/, 1450 /*5AA*/, 0 /*0000*/, 36000 /*8CA0*/, 10 /*000A*/, 199 /*00C7*/, 8 | 15001 /*3A99*/, 6540 /*198C*/ 9 | ], 10 | /* Elements are keen */ 11 | "Elements": [ 12 | { /*Carbon*/ "Symbol": "C", "Number": 6, "Isotopes": [11, 12, 13, 14] }, 13 | { /*Oxygen*/ "Symbol": "O", "Number": 8, "Isotopes": [16, 18, 17] }, 14 | { /*Hydrogen*/ "Symbol": "H", "Number": 1, "Isotopes": [1, 2, 3] }, 15 | { /*Iron*/ "Symbol": "Fe", "Number": 26, "Isotopes": [56, 54, 57, 58] } 16 | // Not a complete list... 17 | ], 18 | 19 | "Beatles Songs": [ 20 | "Taxman", // George 21 | "Hey Jude", // Paul 22 | "Act Naturally", // Ringo 23 | "Ticket To Ride" // John 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/Helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests that the first occurence of the substring occurs at the same index in each line, if the line contains it. 3 | */ 4 | export function DoInstancesLineUp(lines: string[], substring: string): boolean { 5 | const indices = lines.map(str => str.indexOf(substring)) 6 | .filter(num => num >= 0); 7 | return indices.length==0 || indices.every(num => num == indices[0]); 8 | } 9 | -------------------------------------------------------------------------------- /test/LengthAndComplexity.test.ts: -------------------------------------------------------------------------------- 1 | import {Formatter} from "../src"; 2 | 3 | 4 | describe("Length and complexity tests", () => { 5 | // Test a specific piece of input with a variety of MaxInlineComplexity settings, and compare 6 | // the number of lines in the output to the expected values. 7 | test.each([ 8 | [4, 1], // All on one line 9 | [3, 3], // Outer-most brackets on their own lines 10 | [2, 6], // Q & R each get their own rows, plus outer [ {...} ] 11 | [1, 9], // Q gets broken up. R stays inline. 12 | [0, 14] // Maximum expansion, basically 13 | ])("Correct line count for inline complexity", (maxComp: number, expNumLines: number) => { 14 | const inputLines = [ 15 | "[", 16 | " { 'Q': [ {'foo': 'bar'}, 678 ], 'R': [ {}, 'asdf'] }", 17 | "]", 18 | ]; 19 | const input = inputLines.join("\n").replace(/'/g, '"'); 20 | const formatter = new Formatter(); 21 | formatter.Options.MaxTotalLineLength = 90; 22 | formatter.Options.MaxInlineComplexity = maxComp; 23 | formatter.Options.MaxCompactArrayComplexity = -1; 24 | formatter.Options.MaxTableRowComplexity = -1; 25 | 26 | let output = formatter.Reformat(input, 0); 27 | let outputLines = output.trimEnd().split('\n'); 28 | 29 | expect(outputLines.length).toBe(expNumLines); 30 | }); 31 | 32 | // Tests a known piece of input against multiple values of MaxCompactArrayComplexity. 33 | test.each([ 34 | [2, 5], // 3 formatted columns across 3 lines plus the outer [] 35 | [1, 9] // Each subarray gets its own line, plus the outer [] 36 | ])("Correct line count for multiline compact", (maxComp: number, expNumLines: number) => { 37 | const inputLines = [ 38 | "[", 39 | " [1,2,3], [4,5,6], [7,8,9], [null,11,12], [13,14,15], [16,17,18], [19,null,21]", 40 | "]", 41 | ]; 42 | const input = inputLines.join("\n").replace(/'/g, '"'); 43 | const formatter = new Formatter(); 44 | formatter.Options.MaxTotalLineLength = 60; 45 | formatter.Options.MaxInlineComplexity = 2; 46 | formatter.Options.MaxCompactArrayComplexity = maxComp; 47 | formatter.Options.MaxTableRowComplexity = -1; 48 | 49 | let output = formatter.Reformat(input, 0); 50 | let outputLines = output.trimEnd().split('\n'); 51 | 52 | expect(outputLines.length).toBe(expNumLines); 53 | }); 54 | 55 | // Tests a single piece of sample data with multiple length settings, and compares the number of output 56 | // lines with the expected output. 57 | test.each([ 58 | [120, 100, 3, 1], // All on one line 59 | [120, 90, 3, 4], // Two row compact multiline array, + two for [] 60 | [120, 70, 3, 5], // Three row compact multiline array, + two for [] 61 | [120, 50, 3, 9], // Not a compact multiline array. 1 per inner array, + two for []. 62 | [120, 50, 2, 6], // Four row compact multiline array, + two for [] 63 | [90, 120, 3, 4], // Two row compact multiline array, + two for [] 64 | [70, 120, 3, 4], // Also two row compact multiline array, + two for [] 65 | [65, 120, 3, 5], // Three row compact multiline array, + two for [] 66 | ])("Correct line count for line length", (inlineLen:number, totalLen:number, itemsPerRow:number, expLines:number) => { 67 | const inputLines = [ 68 | "[", 69 | " [1,2,3], [4,5,6], [7,8,9], [null,11,12], [13,14,15], [16,17,18], [19,null,21]", 70 | "]", 71 | ]; 72 | const input = inputLines.join("\n").replace(/'/g, '"'); 73 | const formatter = new Formatter(); 74 | formatter.Options.MaxInlineLength = inlineLen; 75 | formatter.Options.MaxTotalLineLength = totalLen; 76 | formatter.Options.MaxInlineComplexity = 2; 77 | formatter.Options.MaxCompactArrayComplexity = 2; 78 | formatter.Options.MaxTableRowComplexity = 2; 79 | formatter.Options.MinCompactArrayRowItems = itemsPerRow; 80 | 81 | let output = formatter.Reformat(input, 0); 82 | let outputLines = output.trimEnd().split('\n'); 83 | 84 | expect(outputLines.length).toBe(expLines); 85 | }) 86 | }); 87 | -------------------------------------------------------------------------------- /test/NumberFormatting.test.ts: -------------------------------------------------------------------------------- 1 | import {EolStyle, Formatter, FracturedJsonOptions, NumberListAlignment} from "../src"; 2 | 3 | describe('Number formatting tests', function () { 4 | test("Inline array doesn't justify numbers", () => { 5 | const input = "[1, 2.1, 3, -99]"; 6 | const expectedOutput = "[1, 2.1, 3, -99]"; 7 | 8 | // With default options, this will be inlined, so no attempt is made to reformat or justify the numbers. 9 | const formatter = new Formatter(); 10 | let output = formatter.Reformat(input, 0); 11 | 12 | expect(output.trimEnd()).toBe(expectedOutput); 13 | }); 14 | 15 | test("Compact array does justify numbers", () => { 16 | const input = "[1, 2.1, 3, -99]"; 17 | const expectedOutput = "[\n 1.0, 2.1, 3.0, -99.0\n]"; 18 | 19 | // Here, it's formatted as a compact multiline array (but not really multiline). All elements are formatted 20 | // alike, which means padding spaces on the left and zeros on the right. 21 | const formatter = new Formatter(); 22 | formatter.Options.MaxInlineComplexity = -1; 23 | 24 | let output = formatter.Reformat(input, 0); 25 | 26 | expect(output.trimEnd()).toBe(expectedOutput); 27 | }); 28 | 29 | test("Table array does justify numbers", () => { 30 | const input = "[[1, 2.1, 3, -99],[5, 6, 7, 8]]"; 31 | const expectedOutput = 32 | "[\n" + 33 | " [1, 2.1, 3, -99], \n" + 34 | " [5, 6.0, 7, 8] \n" + 35 | "]"; 36 | 37 | // Since this is table formatting, each column is consistent, but not siblings in the same array. 38 | const formatter = new Formatter(); 39 | formatter.Options.MaxInlineComplexity = -1; 40 | 41 | let output = formatter.Reformat(input, 0); 42 | 43 | expect(output.trimEnd()).toBe(expectedOutput); 44 | }); 45 | 46 | test("Big numbers invalidate alignment 1", () => { 47 | const input = "[1, 2.1, 3, 1e+99]"; 48 | const expectedOutput = "[\n 1 , 2.1 , 3 , 1e+99\n]"; 49 | 50 | // If there's a number that requires an "E", don't try to justify the numbers. 51 | const formatter = new Formatter(); 52 | formatter.Options.MaxInlineComplexity = -1; 53 | 54 | let output = formatter.Reformat(input, 0); 55 | 56 | expect(output.trimEnd()).toBe(expectedOutput); 57 | }); 58 | 59 | test("Big numbers invalidate alignment 2", () => { 60 | const input = "[1, 2.1, 3, 1234567890123456]"; 61 | const expectedOutput = "[\n 1 , 2.1 , 3 , 1234567890123456\n]"; 62 | 63 | // If there's a number with too many significant digits, don't try to justify the numbers. 64 | const formatter = new Formatter(); 65 | formatter.Options.MaxInlineComplexity = -1; 66 | 67 | let output = formatter.Reformat(input, 0); 68 | 69 | expect(output.trimEnd()).toBe(expectedOutput); 70 | }); 71 | 72 | test("Nulls respected when aligning numbers", () => { 73 | const input = "[1, 2, null, -99]"; 74 | const expectedOutput = "[\n 1, 2, null, -99\n]"; 75 | 76 | // In general, if an array contains stuff other than numbers, we don't try to justify them. Null is an 77 | // exception though: an array of numbers and nulls should be justified as numbers. 78 | const formatter = new Formatter(); 79 | formatter.Options.MaxInlineComplexity = -1; 80 | 81 | let output = formatter.Reformat(input, 0); 82 | 83 | expect(output.trimEnd()).toBe(expectedOutput); 84 | }); 85 | 86 | test("Overflow Double Invalidates Alignment", () => { 87 | const input = "[1e500, 4.0]"; 88 | const expectedOutput = "[\n 1e500, 4.0 \n]"; 89 | 90 | // If a number is too big to fit in a 64-bit float, we shouldn't try to reformat its column/array. 91 | // If we did, it would turn into "Infinity", isn't a valid JSON token. 92 | const formatter = new Formatter(); 93 | formatter.Options.MaxInlineComplexity = -1; 94 | 95 | let output = formatter.Reformat(input, 0); 96 | 97 | expect(output.trimEnd()).toBe(expectedOutput); 98 | }); 99 | 100 | test("Underflow Double Invalidates Alignment", () => { 101 | const input = "[1e-500, 4.0]"; 102 | const expectedOutput = "[\n 1e-500, 4.0 \n]"; 103 | 104 | // If a number is too small to fit in a 64-bit float, we shouldn't try to reformat its column/array. 105 | // Doing so would change it to zero, which might be an unwelcome loss of precision. 106 | const formatter = new Formatter(); 107 | formatter.Options.MaxInlineComplexity = -1; 108 | 109 | let output = formatter.Reformat(input, 0); 110 | 111 | expect(output.trimEnd()).toBe(expectedOutput); 112 | }); 113 | 114 | test("AccurateCompositeLengthForNormalized", () => { 115 | // Make sure the the inner TableTemplate is accurately reporting the CompositeLength. Otherwise, 116 | // the null row won't have the right number of spaces. 117 | const inputRows = [ 118 | "[", 119 | " { \"a\": {\"val\": 12345} },", 120 | " { \"a\": {\"val\": 6.78901} },", 121 | " { \"a\": null },", 122 | " { \"a\": {\"val\": 1e500} }", 123 | "]", 124 | ]; 125 | 126 | const input = inputRows.join(""); 127 | const opts = new FracturedJsonOptions() 128 | opts.MaxTotalLineLength = 40; 129 | opts.JsonEolStyle = EolStyle.Lf; 130 | opts.OmitTrailingWhitespace = true; 131 | opts.NumberListAlignment = NumberListAlignment.Normalize; 132 | 133 | const formatter = new Formatter(); 134 | formatter.Options = opts; 135 | const output = formatter.Reformat(input, 0); 136 | const outputRows = output.trimEnd().split('\n'); 137 | 138 | expect(outputRows.length).toBe(6); 139 | expect(outputRows[2].length).toEqual(outputRows[3].length); 140 | }); 141 | 142 | test("Left Align Matches Expected", () => { 143 | const expectedRows = [ 144 | "[", 145 | " [123.456 , 0 , 0 ],", 146 | " [234567.8, 0 , 0 ],", 147 | " [3 , 0.00000, 7e2 ],", 148 | " [null , 2e-1 , 80e1],", 149 | " [5.6789 , 3.5e-1 , 0 ]", 150 | "]" 151 | ]; 152 | 153 | TestAlignment(NumberListAlignment.Left, expectedRows); 154 | }); 155 | 156 | test("Right Align Matches Expected", () => { 157 | const expectedRows = [ 158 | "[", 159 | " [ 123.456, 0, 0],", 160 | " [234567.8, 0, 0],", 161 | " [ 3, 0.00000, 7e2],", 162 | " [ null, 2e-1, 80e1],", 163 | " [ 5.6789, 3.5e-1, 0]", 164 | "]" 165 | ]; 166 | 167 | TestAlignment(NumberListAlignment.Right, expectedRows); 168 | }); 169 | 170 | test("Decimal Align Matches Expected", () => { 171 | const expectedRows = [ 172 | "[", 173 | " [ 123.456 , 0 , 0 ],", 174 | " [234567.8 , 0 , 0 ],", 175 | " [ 3 , 0.00000, 7e2],", 176 | " [ null , 2e-1 , 80e1],", 177 | " [ 5.6789, 3.5e-1 , 0 ]", 178 | "]" 179 | ]; 180 | 181 | TestAlignment(NumberListAlignment.Decimal, expectedRows); 182 | }); 183 | 184 | test("Normalize Align Matches Expected", () => { 185 | const expectedRows = [ 186 | "[", 187 | " [ 123.4560, 0.00, 0],", 188 | " [234567.8000, 0.00, 0],", 189 | " [ 3.0000, 0.00, 700],", 190 | " [ null , 0.20, 800],", 191 | " [ 5.6789, 0.35, 0]", 192 | "]" 193 | ]; 194 | 195 | TestAlignment(NumberListAlignment.Normalize, expectedRows); 196 | }); 197 | }); 198 | 199 | function TestAlignment(align: NumberListAlignment, expectedRows: string[]): void { 200 | const inputRows = [ 201 | "[", 202 | " [ 123.456, 0, 0 ],", 203 | " [ 234567.8, 0, 0 ],", 204 | " [ 3, 0.00000, 7e2 ],", 205 | " [ null, 2e-1, 80e1 ],", 206 | " [ 5.6789, 3.5e-1, 0 ]", 207 | "]", 208 | ]; 209 | const input = inputRows.join(""); 210 | 211 | const formatter = new Formatter(); 212 | formatter.Options.MaxTotalLineLength = 60; 213 | formatter.Options.JsonEolStyle = EolStyle.Lf; 214 | formatter.Options.OmitTrailingWhitespace = true; 215 | formatter.Options.NumberListAlignment = align; 216 | 217 | const output = formatter.Reformat(input); 218 | const outputRows = output!.trimEnd().split('\n'); 219 | 220 | expect(outputRows).toEqual(expectedRows); 221 | } -------------------------------------------------------------------------------- /test/ObjectSerialization.test.ts: -------------------------------------------------------------------------------- 1 | import {Formatter, NumberListAlignment} from "../src"; 2 | import {readdirSync, readFileSync} from "fs"; 3 | 4 | // Tests Formatter's Serialize method for writing JSON straight from objects/arrays/strings/etc. 5 | describe("Object serialization tests", () => { 6 | const someDate = new Date(); 7 | const simpleTestCases: any[] = [ 8 | null, 9 | undefined, 10 | "shoehorn with teeth", 11 | 18, 12 | [], 13 | {}, 14 | true, 15 | "", 16 | someDate, // Has custom toJSON function 17 | Symbol.for("xyz"), // symbol - JSON.stringify returns undefined 18 | () => 8, // function - JSON.stringify returns undefined 19 | { a: "foo", b: false, c: NaN }, // NaN converts to null 20 | [[1,2,null], [4,null,6], {x:7,y:8,z:9}], 21 | [someDate, {d: someDate}] // Nested stuff with toJSON functions 22 | ]; 23 | 24 | // Serialize. Then minify. Then compared to the native minified version. 25 | test.each(simpleTestCases)("Matches native stringify when minimized", (element:any) => { 26 | const nativeMinified = JSON.stringify(element); 27 | 28 | const formatter = new Formatter(); 29 | formatter.Options.NumberListAlignment = NumberListAlignment.Left; 30 | const nicelyFormatted = formatter.Serialize(element, 0); 31 | 32 | if (!nativeMinified) { 33 | // There are some cases where JSON.stringify returns undefined. We should match that. 34 | expect(nicelyFormatted).toBeUndefined(); 35 | } 36 | else { 37 | expect(nicelyFormatted).toBeDefined(); 38 | 39 | // No point in comparing the nicely-formatted version to anything native. But the minified versions 40 | // should be identical, except maybe for a line ending. 41 | const fjMinified = formatter.Minify(nicelyFormatted!); 42 | expect(fjMinified).toBe(nativeMinified); 43 | } 44 | }); 45 | 46 | test("Throws if circular reference", () => { 47 | const foo:any[] = []; 48 | const bar:any[] = [foo]; 49 | foo.push(bar); 50 | 51 | const formatter = new Formatter(); 52 | expect(() => formatter.Serialize(foo)).toThrowError(); 53 | }); 54 | 55 | test("Handles sparse arrays", () => { 56 | const arr = ['val1',,,'val2']; 57 | 58 | const formatter = new Formatter(); 59 | const nice = formatter.Serialize(arr); 60 | expect(nice).toBe('["val1", null, null, "val2"]\n'); 61 | }); 62 | 63 | // Serialize. Then minify. Then compared to the native minified version. 64 | test.each(ReadJsonFromFiles())("File data matches native stringify when minimized", (fileData:string) => { 65 | // Yeah, this is convoluted. Read the JSON data from files, parse it into objects, then create a minified 66 | // string form, using the native functions. 67 | const element = JSON.parse(fileData); 68 | const nativeMinified = JSON.stringify(element); 69 | 70 | // Use Formatter.Serialize to convert from the object form to nicely formatted JSON text that we can't 71 | // directly test. We have to turn off justifying numbers because it can add digits, and we have to turn off 72 | // table formatting since it can reorder object children. 73 | const formatter = new Formatter(); 74 | formatter.Options.NumberListAlignment = NumberListAlignment.Left; 75 | formatter.Options.MaxTableRowComplexity = -1; 76 | const nicelyFormatted = formatter.Serialize(element, 0); 77 | 78 | if (!nativeMinified) { 79 | // There are some cases where JSON.stringify returns undefined. We should match that. 80 | expect(nicelyFormatted).toBeUndefined(); 81 | } 82 | else { 83 | expect(nicelyFormatted).toBeDefined(); 84 | 85 | // Minify the nicely formatted string so we can compare it to the native results. 86 | const fjMinified = formatter.Minify(nicelyFormatted!); 87 | expect(fjMinified).toBe(nativeMinified); 88 | } 89 | }); 90 | }); 91 | 92 | function ReadJsonFromFiles(): string[] { 93 | const standardBaseDir = "./test/StandardJsonFiles/"; 94 | const standardFileList = readdirSync(standardBaseDir); 95 | 96 | return standardFileList.map(filename => readFileSync(standardBaseDir + filename).toString()); 97 | } 98 | -------------------------------------------------------------------------------- /test/PadFormatting.test.ts: -------------------------------------------------------------------------------- 1 |  2 | // Unit tests for various padding functionality and maybe indentation 3 | import {readFileSync} from "fs"; 4 | import {Formatter} from "../src"; 5 | 6 | describe("Pad formatting tests", () => { 7 | test("No spaces anywhere", () => { 8 | const filePath = "./test/StandardJsonFiles/1.json"; 9 | const input = readFileSync(filePath).toString(); 10 | 11 | // Turn off all padding (except comments - not worrying about that here). Use tabs to indent. Disable 12 | // compact multiline arrays. There will be no spaces anywhere. 13 | const formatter = new Formatter(); 14 | formatter.Options.UseTabToIndent = true; 15 | formatter.Options.ColonPadding = false; 16 | formatter.Options.CommaPadding = false; 17 | formatter.Options.NestedBracketPadding = false; 18 | formatter.Options.SimpleBracketPadding = false; 19 | formatter.Options.MaxCompactArrayComplexity = 0; 20 | formatter.Options.MaxTableRowComplexity = -1; 21 | 22 | let output = formatter.Reformat(input, 0); 23 | 24 | expect(output).not.toContain(" "); 25 | }); 26 | 27 | test("SimpleBracketPadding works for tables", () => { 28 | const input = "[[1, 2],[3, 4]]"; 29 | 30 | // Limit the complexity to make sure we format this as a table, but set SimpleBracketPadding to true. 31 | const formatter = new Formatter(); 32 | formatter.Options.MaxInlineComplexity = 1; 33 | formatter.Options.SimpleBracketPadding = true; 34 | 35 | let output = formatter.Reformat(input, 0); 36 | let outputLines = output.trimEnd().split('\n'); 37 | 38 | // There should be spaces between the brackets and the numbers. 39 | expect(outputLines.length).toBe(4); 40 | expect(outputLines[1]).toContain("[ 1, 2 ]"); 41 | expect(outputLines[2]).toContain("[ 3, 4 ]"); 42 | 43 | formatter.Options.SimpleBracketPadding = false; 44 | output = formatter.Reformat(input, 0); 45 | outputLines = output.trimEnd().split('\n'); 46 | 47 | // There should NOT be spaces between the brackets and the numbers. 48 | expect(outputLines.length).toBe(4); 49 | expect(outputLines[1]).toContain("[1, 2]"); 50 | expect(outputLines[2]).toContain("[3, 4]"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/StandardJsonFiles/0.json: -------------------------------------------------------------------------------- 1 | { 2 | "BannerText": [ 3 | "Sometimes you'll have to protect multiple enemy goals." 4 | ], 5 | "Terrain": { 6 | "TileTypes": [ 7 | { 8 | "BlocksMovement": false, 9 | "BlocksVision": false, 10 | "Appearance": " ", 11 | "Name": "Open" 12 | }, 13 | { 14 | "BlocksMovement": true, 15 | "BlocksVision": true, 16 | "Appearance": "*", 17 | "Name": "Stone" 18 | }, 19 | { 20 | "BlocksMovement": true, 21 | "BlocksVision": false, 22 | "Appearance": "~", 23 | "Name": "Water" 24 | }, 25 | { 26 | "BlocksMovement": false, 27 | "BlocksVision": true, 28 | "Appearance": "@", 29 | "Name": "Fog" 30 | } 31 | ], 32 | "Width": 45, 33 | "Height": 20, 34 | "Tiles": [ 35 | " *** *** ******** ", 36 | " ******* **** ", 37 | "*********** **** ", 38 | "*********** *******", 39 | "********* *********** ", 40 | " ****** ******** ", 41 | " *** ****", 42 | " ", 43 | " ", 44 | " ", 45 | " * ****", 46 | " * ***** ** ", 47 | "**** ****** **** ", 48 | "***** **** ** ", 49 | "****** *** ** ", 50 | "****** * ", 51 | " ** ****** ****", 52 | " * * ", 53 | " **** ", 54 | " ** " 55 | ], 56 | "SpawnPointsMap": { 57 | "1": [ 58 | [ 59 | 1, 60 | 8 61 | ] 62 | ] 63 | }, 64 | "GoalPointsMap": { 65 | "1": [ 66 | [ 67 | 43, 68 | 8 69 | ], 70 | [ 71 | 43, 72 | 18 73 | ] 74 | ] 75 | } 76 | }, 77 | "AttackPlans": [ 78 | { 79 | "TeamId": 1, 80 | "Spawns": [ 81 | { 82 | "Time": 0.0, 83 | "UnitType": "Grunt", 84 | "SpawnPointIndex": 0 85 | }, 86 | { 87 | "Time": 0.0, 88 | "UnitType": "Grunt", 89 | "SpawnPointIndex": 0 90 | }, 91 | { 92 | "Time": 0.0, 93 | "UnitType": "Grunt", 94 | "SpawnPointIndex": 0 95 | }, 96 | { 97 | "Time": 0.0, 98 | "UnitType": "Grunt", 99 | "SpawnPointIndex": 0 100 | }, 101 | { 102 | "Time": 0.0, 103 | "UnitType": "Grunt", 104 | "SpawnPointIndex": 0 105 | } 106 | ] 107 | }, 108 | { 109 | "TeamId": 2, 110 | "Spawns": [] 111 | } 112 | ], 113 | "DefensePlans": [ 114 | { 115 | "TeamId": 2, 116 | "Placements": [ 117 | { 118 | "UnitType": "Archer", 119 | "Position": [ 120 | 41, 121 | 7 122 | ] 123 | }, 124 | { 125 | "UnitType": "Archer", 126 | "Position": [ 127 | 41, 128 | 8 129 | ] 130 | }, 131 | { 132 | "UnitType": "Archer", 133 | "Position": [ 134 | 41, 135 | 9 136 | ] 137 | }, 138 | { 139 | "UnitType": "Pikeman", 140 | "Position": [ 141 | 40, 142 | 9 143 | ] 144 | }, 145 | { 146 | "UnitType": "Pikeman", 147 | "Position": [ 148 | 40, 149 | 8 150 | ] 151 | }, 152 | { 153 | "UnitType": "Pikeman", 154 | "Position": [ 155 | 40, 156 | 7 157 | ] 158 | }, 159 | { 160 | "UnitType": "Barricade", 161 | "Position": [ 162 | 39, 163 | 7 164 | ] 165 | }, 166 | { 167 | "UnitType": "Barricade", 168 | "Position": [ 169 | 39, 170 | 8 171 | ] 172 | }, 173 | { 174 | "UnitType": "Barricade", 175 | "Position": [ 176 | 39, 177 | 9 178 | ] 179 | }, 180 | { 181 | "UnitType": "Archer", 182 | "Position": [ 183 | 41, 184 | 18 185 | ] 186 | } 187 | ] 188 | } 189 | ], 190 | "Challenges": [ 191 | { 192 | "Name": "*", 193 | "PlayerTeamId": 2, 194 | "AttackersMustNotReachGoal": false, 195 | "MaximumUnitTypeCount": {} 196 | }, 197 | { 198 | "Name": "**", 199 | "PlayerTeamId": 2, 200 | "AttackersMustNotReachGoal": true, 201 | "MaximumUnitTypeCount": {} 202 | } 203 | ] 204 | } -------------------------------------------------------------------------------- /test/StandardJsonFiles/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "SimpleItem": 77, 3 | "ShortArray": ["blue", "blue", "orange", "gray"], 4 | "ShortArray2": [6.02e23, 5000000000, 4], 5 | "LongArray": [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997], 6 | "LongArray2": [[19,2], [3,8], [14, 0], [9,9], [9,9], [0,3], [10,1], [9,1], [9,2], [6,13], [18,5], [4,11], [12,2]], 7 | "ComplexObject": { 8 | "Subthing1": { "X": 55, "Y": 19, "Z": -4 }, 9 | "Subthing2": { "Q": null, "W": [-2, -1, 0, 1]}, 10 | "Distraction": [ [], null, null ] 11 | } 12 | } -------------------------------------------------------------------------------- /test/StandardJsonFiles/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "ObjectColumnsObjectRows": { 3 | "Vera": { 4 | "street": "12 Madeup St.", 5 | "city": "Boston", 6 | "state": "MA", 7 | "zip": "02127" 8 | }, 9 | "Chuck": { 10 | "street": "994 Fake Place", 11 | "unit": "102", 12 | "city": "Las Vegas", 13 | "state": "NV", 14 | "zip": "89102" 15 | }, 16 | "Dave": { 17 | "street": "1967 Void Rd.", 18 | "unit": "B", 19 | "city": "Athens", 20 | "state": "GA" 21 | } 22 | }, 23 | "ObjectColumnsArrayRows": { 24 | "Katherine": ["blue", "lightblue", "black"], 25 | "Logan": ["yellow", "blue", "black", "red"], 26 | "Erik": ["red", "purple"], 27 | "Jean": ["lightgreen", "yellow", "black"] 28 | }, 29 | "ArrayColumnsObjectRows": [ 30 | { 31 | "type": "turret", 32 | "hp": 400, 33 | "loc": { 34 | "x": 47, 35 | "y": -4 36 | }, 37 | "flags": ["stationary"] 38 | }, 39 | { 40 | "type": "assassin", 41 | "hp": 80, 42 | "loc": { 43 | "x": 102, 44 | "y": 6 45 | }, 46 | "flags": ["stealth"] 47 | }, 48 | { 49 | "type": "berserker", 50 | "hp": 150, 51 | "loc": { 52 | "x": 0, 53 | "y": 0 54 | } 55 | }, 56 | { 57 | "type": "pittrap", 58 | "loc": { 59 | "x": 10, 60 | "y": -14 61 | }, 62 | "flags": ["invulnerable", "stationary"] 63 | } 64 | ], 65 | "ArrayColumnsArrayRows": [ 66 | [0.0, 3.5, 10.5, 6.5, 2.5, 0.6], 67 | [0.0, 0.0, 1.2, 2.1, 6.7, 4.4], 68 | [0.4, 1.9, 4.4, 5.4, 2.35, 2.0], 69 | [7.4, 1.2, 0.01, 0.0, 2.91, 0.2] 70 | ], 71 | "DissimilarObjectRows": { 72 | "lamp": { 73 | "x": 4, 74 | "y": 1002, 75 | "r": 75, 76 | "g": 0, 77 | "b": 130, 78 | "state": 1 79 | }, 80 | "address": { 81 | "city": "San Diego", 82 | "state": "CA" 83 | }, 84 | "font": { 85 | "r": 0, 86 | "g": 12, 87 | "b": 40, 88 | "size": 18, 89 | "style": "italic" 90 | } 91 | }, 92 | "DissimilarArrayRows": { 93 | "primes": [2, 3, 5, 7, 11], 94 | "powersOf2": [1, 2, 4, 8, 16, 32, 64, 128, 256], 95 | "factorsOf12": [2, 2, 3], 96 | "someZeros": [0, 0, 0, 0] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/StandardJsonFiles/3.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /test/StandardJsonFiles/4.json: -------------------------------------------------------------------------------- 1 | "A single string is a valid JSON element" -------------------------------------------------------------------------------- /test/StandardJsonFiles/5.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "李小龍", 4 | "Job": "Actor", 5 | "Born": 1940 6 | }, 7 | { 8 | "Name": "Mark Twain", 9 | "Job": "Writer", 10 | "Born": 1835 11 | }, 12 | { 13 | "Name": "孫子", 14 | "Job": "General", 15 | "Born": -544 16 | } 17 | ] -------------------------------------------------------------------------------- /test/StandardJsonFiles/6.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rect" : { "position": {"x": -44, "y": 3.4} , "color": [0, 255, 255] }, 3 | "Point": { "position": {"y": 22, "z": 3} }, 4 | "Oval" : { "position": {"x": 140, "y": 0.04}, "color": "#7f3e96" }, 5 | "Plane": { "position": null, "color": [0, 64, 64] } 6 | } 7 | -------------------------------------------------------------------------------- /test/TableFormatting.test.ts: -------------------------------------------------------------------------------- 1 | // Tests about formatting things in tables, so that corresponding properties and array positions are neatly 2 | // lined up, when possible. 3 | import {CommentPolicy, EolStyle, Formatter, NumberListAlignment} from "../src"; 4 | // @ts-ignore 5 | import {DoInstancesLineUp} from "./Helpers"; 6 | import {TableCommaPlacement} from "../src/TableCommaPlacement"; 7 | 8 | describe("Table formatting tests", () => { 9 | test("Nested elements line up", () => { 10 | const inputLines = [ 11 | "{", 12 | " 'Rect' : { 'position': {'x': -44, 'y': 3.4}, 'color': [0, 255, 255] }, ", 13 | " 'Point': { 'position': {'y': 22, 'z': 3} }, ", 14 | " 'Oval' : { 'position': {'x': 140, 'y': 0.04}, 'color': '#7f3e96' } ", 15 | "}", 16 | ]; 17 | const input = inputLines.join("\n").replace(/'/g, '"'); 18 | 19 | // With default options, this will be neatly formatted as a table. 20 | const formatter = new Formatter(); 21 | 22 | let output = formatter.Reformat(input, 0); 23 | let outputLines = output.trimEnd().split('\n'); 24 | 25 | // Everything should line up. 26 | expect(DoInstancesLineUp(outputLines, "x")).toBeTruthy(); 27 | expect(DoInstancesLineUp(outputLines, "y")).toBeTruthy(); 28 | expect(DoInstancesLineUp(outputLines, "z")).toBeTruthy(); 29 | expect(DoInstancesLineUp(outputLines, "position")).toBeTruthy(); 30 | expect(DoInstancesLineUp(outputLines, "color")).toBeTruthy(); 31 | 32 | // The numbers of the y column will be justified. 33 | expect(outputLines[2]).toContain("22.00,"); 34 | }); 35 | 36 | test("Nested elements compact when needed", () => { 37 | const inputLines = [ 38 | "{", 39 | " 'Rect' : { 'position': {'x': -44, 'y': 3.4}, 'color': [0, 255, 255] }, ", 40 | " 'Point': { 'position': {'y': 22, 'z': 3} }, ", 41 | " 'Oval' : { 'position': {'x': 140, 'y': 0.04}, 'color': '#7f3e96' } ", 42 | "}", 43 | ]; 44 | const input = inputLines.join("\n").replace(/'/g, '"'); 45 | 46 | // Smaller rows, so there's not enough room to do a full table. 47 | const formatter = new Formatter(); 48 | formatter.Options.MaxTotalLineLength = 77; 49 | 50 | let output = formatter.Reformat(input, 0); 51 | let outputLines = output.trimEnd().split('\n'); 52 | 53 | // Since the available size is reduced, x,y,z will no longer line up, but position and color will. 54 | expect(DoInstancesLineUp(outputLines, "position")).toBeTruthy(); 55 | expect(DoInstancesLineUp(outputLines, "color")).toBeTruthy(); 56 | 57 | // The numbers of the y column will be justified. 58 | expect(outputLines[2]).toContain("22,"); 59 | }); 60 | 61 | test("Nested elements compact when needed", () => { 62 | const inputLines = [ 63 | "{", 64 | " 'Rect' : { 'position': {'x': -44, 'y': 3.4}, 'color': [0, 255, 255] }, ", 65 | " 'Point': { 'position': {'y': 22, 'z': 3} }, ", 66 | " 'Oval' : { 'position': {'x': 140, 'y': 0.04}, 'color': '#7f3e96' } ", 67 | "}", 68 | ]; 69 | const input = inputLines.join("\n").replace(/'/g, '"'); 70 | 71 | // In this case, it's too small to do any table formatting. But each row should still be inlined. 72 | const formatter = new Formatter(); 73 | formatter.Options.MaxTotalLineLength = 74; 74 | 75 | let output = formatter.Reformat(input, 0); 76 | let outputLines = output.trimEnd().split('\n'); 77 | 78 | // All rows should be inlined, so a total of 5 rows. 79 | expect(outputLines.length).toBe(5); 80 | 81 | // Not even position lines up here. 82 | expect(outputLines[1].indexOf("position")).not.toBe(outputLines[2].indexOf("position")); 83 | }); 84 | 85 | test("Tables with comments line up", () => { 86 | const inputLines = [ 87 | "{", 88 | "'Firetruck': /* red */ { 'color': '#CC0000' }, ", 89 | "'Dumptruck': /* yellow */ { 'color': [255, 255, 0] }, ", 90 | "'Godzilla': /* green */ { 'color': '#336633' }, // Not a truck", 91 | "/* ! */ 'F150': { 'color': null } ", 92 | "}" 93 | ]; 94 | const input = inputLines.join("\n").replace(/'/g, '"'); 95 | 96 | // Need to be wide enough and allow comments. 97 | const formatter = new Formatter(); 98 | formatter.Options.MaxTotalLineLength = 100; 99 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 100 | 101 | let output = formatter.Reformat(input, 0); 102 | let outputLines = output.trimEnd().split('\n'); 103 | 104 | // All rows should be inlined, so a total of 6 rows. 105 | expect(outputLines.length).toBe(6); 106 | 107 | // Lots of stuff to line up here. 108 | expect(DoInstancesLineUp(outputLines, '"')).toBeTruthy(); 109 | expect(DoInstancesLineUp(outputLines, ":")).toBeTruthy(); 110 | expect(DoInstancesLineUp(outputLines, " {")).toBeTruthy(); 111 | expect(DoInstancesLineUp(outputLines, " }")).toBeTruthy(); 112 | expect(DoInstancesLineUp(outputLines, "color")).toBeTruthy(); 113 | }); 114 | 115 | test("Tables with blank lines line up", () => { 116 | const inputLines = [ 117 | "{'a': [7,8],", 118 | "", 119 | "//1", 120 | "'b': [9,10]}", 121 | ]; 122 | const input = inputLines.join("\n").replace(/'/g, '"'); 123 | 124 | const formatter = new Formatter(); 125 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 126 | formatter.Options.PreserveBlankLines = true; 127 | 128 | let output = formatter.Reformat(input, 0); 129 | let outputLines = output.trimEnd().split('\n'); 130 | 131 | // All rows should be inlined, so a total of 6 rows. 132 | expect(outputLines.length).toBe(6); 133 | 134 | // The presence of comments and blank lines shouldn't prevent table formatting. 135 | expect(DoInstancesLineUp(outputLines, ":")).toBeTruthy(); 136 | expect(DoInstancesLineUp(outputLines, "[")).toBeTruthy(); 137 | expect(DoInstancesLineUp(outputLines, "]")).toBeTruthy(); 138 | }); 139 | 140 | test("Reject objects with duplicate keys", () => { 141 | // Here we have an object with duplicate 'z' keys. This is legal in JSON, even though it's hard to imagine 142 | // any case where it would actually happen. Still, we want to reproduce the data faithfully, so 143 | // we mustn't try to format it as a table. 144 | const inputLines = [ 145 | "[ { 'x': 1, 'y': 2, 'z': 3 },", 146 | "{ 'y': 44, 'z': 55, 'z': 66 } ]", 147 | ]; 148 | const input = inputLines.join("\n").replace(/'/g, '"'); 149 | 150 | const formatter = new Formatter(); 151 | formatter.Options.MaxInlineComplexity = 1; 152 | 153 | let output = formatter.Reformat(input, 0); 154 | let outputLines = output.trimEnd().split('\n'); 155 | 156 | // The brackets and each object get their own rows. 157 | expect(outputLines.length).toBe(4); 158 | 159 | // We don't expect the y's to line up. 160 | expect(outputLines[1].indexOf("y")).not.toBe(outputLines[2].indexOf("y")); 161 | 162 | // There should be 3 z's in the output, just like in the input. 163 | const zCount = output.match(/z/g)?.length ?? 0; 164 | expect(zCount).toBe(3); 165 | }); 166 | 167 | test("Commas before padding works", () => { 168 | const inputLines = [ 169 | "{", 170 | " 'Rect' : { 'glow': 'steady', 'position': {'x': -44, 'y': 4}, 'color': [0, 255, 255] }, ", 171 | " 'Point': { 'glow': 'pulse', 'position': {'y': 22, 'z': 3} }, ", 172 | " 'Oval' : { 'glow': 'gradient', 'position': {'x': 140.33, 'y': 0.1}, 'color': '#7f3e96' } ", 173 | "}", 174 | ]; 175 | const input = inputLines.join("\n").replace(/'/g, '"'); 176 | 177 | const formatter = new Formatter(); 178 | formatter.Options.MaxTotalLineLength = 120; 179 | formatter.Options.JsonEolStyle = EolStyle.Lf; 180 | formatter.Options.NumberListAlignment = NumberListAlignment.Decimal; 181 | formatter.Options.TableCommaPlacement = TableCommaPlacement.BeforePadding; 182 | 183 | let output = formatter.Reformat(input, 0); 184 | let outputLines = output.trimEnd().split('\n'); 185 | 186 | // In this case, the commas should be right next to values. 187 | expect(outputLines.length).toBe(5); 188 | expect(outputLines[1]).toContain('"steady",'); 189 | expect(outputLines[2]).toContain('"pulse",'); 190 | expect(outputLines[3]).toContain('"gradient",'); 191 | 192 | expect(outputLines[1]).toContain('-44,'); 193 | expect(outputLines[2]).toContain('22,'); 194 | }); 195 | 196 | test("Commas after padding works", () => { 197 | const inputLines = [ 198 | "{", 199 | " 'Rect' : { 'glow': 'steady', 'position': {'x': -44, 'y': 4}, 'color': [0, 255, 255] }, ", 200 | " 'Point': { 'glow': 'pulse', 'position': {'y': 22, 'z': 3} }, ", 201 | " 'Oval' : { 'glow': 'gradient', 'position': {'x': 140.33, 'y': 0.1}, 'color': '#7f3e96' } ", 202 | "}", 203 | ]; 204 | const input = inputLines.join("\n").replace(/'/g, '"'); 205 | 206 | const formatter = new Formatter(); 207 | formatter.Options.MaxTotalLineLength = 120; 208 | formatter.Options.JsonEolStyle = EolStyle.Lf; 209 | formatter.Options.NumberListAlignment = NumberListAlignment.Decimal; 210 | formatter.Options.TableCommaPlacement = TableCommaPlacement.AfterPadding; 211 | 212 | let output = formatter.Reformat(input, 0); 213 | let outputLines = output.trimEnd().split('\n'); 214 | 215 | // In this case, many values will have spaces after them. 216 | expect(outputLines.length).toBe(5); 217 | expect(outputLines[1]).toContain('"steady" '); 218 | expect(outputLines[2]).toContain('"pulse" '); 219 | expect(outputLines[3]).toContain('"gradient",'); 220 | 221 | expect(outputLines[1]).toContain('-44 '); 222 | expect(outputLines[2]).toContain('22 '); 223 | expect(outputLines[3]).toContain('140.33,'); 224 | 225 | // And the first set of commas should line up. 226 | expect(DoInstancesLineUp(outputLines, ",")).toBeTruthy(); 227 | }); 228 | 229 | test("Commas before padding except numbers works", () => { 230 | const inputLines = [ 231 | "{", 232 | " 'Rect' : { 'glow': 'steady', 'position': {'x': -44, 'y': 4}, 'color': [0, 255, 255] }, ", 233 | " 'Point': { 'glow': 'pulse', 'position': {'y': 22, 'z': 3} }, ", 234 | " 'Oval' : { 'glow': 'gradient', 'position': {'x': 140.33, 'y': 0.1}, 'color': '#7f3e96' } ", 235 | "}", 236 | ]; 237 | const input = inputLines.join("\n").replace(/'/g, '"'); 238 | 239 | const formatter = new Formatter(); 240 | formatter.Options.MaxTotalLineLength = 120; 241 | formatter.Options.JsonEolStyle = EolStyle.Lf; 242 | formatter.Options.NumberListAlignment = NumberListAlignment.Decimal; 243 | formatter.Options.TableCommaPlacement = TableCommaPlacement.BeforePaddingExceptNumbers; 244 | 245 | let output = formatter.Reformat(input, 0); 246 | let outputLines = output.trimEnd().split('\n'); 247 | 248 | // For strings, the commas should be right next to values. 249 | expect(outputLines.length).toBe(5); 250 | expect(outputLines[1]).toContain('"steady",'); 251 | expect(outputLines[2]).toContain('"pulse",'); 252 | expect(outputLines[3]).toContain('"gradient",'); 253 | 254 | // For numbers, many will have space after. 255 | expect(outputLines[1]).toContain('-44 '); 256 | expect(outputLines[2]).toContain('22 '); 257 | expect(outputLines[3]).toContain('140.33,'); 258 | 259 | // And the commas should line up before the "y" column. 260 | expect(DoInstancesLineUp(outputLines, ', "y":')).toBeTruthy(); 261 | }); 262 | 263 | test("Commas before padding works with comments", () => { 264 | const input = ` 265 | [ 266 | [ 1 /* q */, "a" ], /* w */ 267 | [ 22, "bbb" ], // x 268 | [ 3.33 /* sss */, "cc" ] /* y */ 269 | ] 270 | `; 271 | 272 | const formatter = new Formatter(); 273 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 274 | formatter.Options.MaxTotalLineLength = 40; 275 | formatter.Options.JsonEolStyle = EolStyle.Lf; 276 | formatter.Options.NumberListAlignment = NumberListAlignment.Decimal; 277 | formatter.Options.TableCommaPlacement = TableCommaPlacement.BeforePadding; 278 | 279 | let output = formatter.Reformat(input, 0); 280 | let outputLines = output.trimEnd().split('\n'); 281 | 282 | // The commas should come immediately after the 22, and after the first comments on the other lines. 283 | expect(outputLines.length).toBe(5); 284 | expect(outputLines[1]).toContain('*/,'); 285 | expect(outputLines[2]).toContain('22,'); 286 | expect(outputLines[3]).toContain('*/,'); 287 | 288 | // The outer commas and comments should line up. 289 | expect(outputLines[1].indexOf("],")).toBe(outputLines[2].indexOf("],")); 290 | expect(outputLines[1].indexOf("/* w")).toBe(outputLines[2].indexOf("// x")); 291 | expect(outputLines[2].indexOf("// x")).toBe(outputLines[3].indexOf("/* y")); 292 | }); 293 | 294 | test("Commas after padding works with comments", () => { 295 | const input = ` 296 | [ 297 | [ 1 /* q */, "a" ], /* w */ 298 | [ 22, "bbb" ], // x 299 | [ 3.33 /* sss */, "cc" ] /* y */ 300 | ] 301 | `; 302 | 303 | const formatter = new Formatter(); 304 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 305 | formatter.Options.MaxTotalLineLength = 40; 306 | formatter.Options.JsonEolStyle = EolStyle.Lf; 307 | formatter.Options.NumberListAlignment = NumberListAlignment.Decimal; 308 | formatter.Options.TableCommaPlacement = TableCommaPlacement.AfterPadding; 309 | 310 | let output = formatter.Reformat(input, 0); 311 | let outputLines = output.trimEnd().split('\n'); 312 | 313 | // The first row of commas should be in a line after room for all comments. 314 | expect(DoInstancesLineUp(outputLines, ',')).toBeTruthy(); 315 | 316 | // The outer commas and comments should line up. 317 | expect(outputLines[1].indexOf("],")).toBe(outputLines[2].indexOf("],")); 318 | expect(outputLines[1].indexOf("/* w")).toBe(outputLines[2].indexOf("// x")); 319 | expect(outputLines[2].indexOf("// x")).toBe(outputLines[3].indexOf("/* y")); 320 | }); 321 | }); 322 | -------------------------------------------------------------------------------- /test/TokenGenerator.test.ts: -------------------------------------------------------------------------------- 1 | import {TokenGenerator} from "../src/TokenGenerator"; 2 | import {TokenType} from "../src/TokenType"; 3 | import {FracturedJsonError} from "../src"; 4 | import {JsonToken} from "../src/JsonToken"; 5 | 6 | // Tests for the TokenGenerator. 7 | describe("Tokenizer Tests", () => { 8 | test.each([ 9 | ["{", TokenType.BeginObject], 10 | ["}", TokenType.EndObject], 11 | ["[", TokenType.BeginArray], 12 | ["]", TokenType.EndArray], 13 | [":", TokenType.Colon], 14 | [",", TokenType.Comma], 15 | ["true", TokenType.True], 16 | ["false", TokenType.False], 17 | ["null", TokenType.Null], 18 | ['"simple"', TokenType.String], 19 | ['"with \\t escapes\\u80fE\\r\\n"', TokenType.String], 20 | ['""', TokenType.String], 21 | ["3", TokenType.Number], 22 | ["3.0", TokenType.Number], 23 | ["-3", TokenType.Number], 24 | ["-3.0", TokenType.Number], 25 | ["0", TokenType.Number], 26 | ["-0", TokenType.Number], 27 | ["0.0", TokenType.Number], 28 | ["9000", TokenType.Number], 29 | ["3e2", TokenType.Number], 30 | ["3.01e+2", TokenType.Number], 31 | ["3e-2", TokenType.Number], 32 | ["-3.01E-2", TokenType.Number], 33 | ["\n", TokenType.BlankLine], 34 | ["//\n", TokenType.LineComment], 35 | ["// comment\n", TokenType.LineComment], 36 | ["// comment", TokenType.LineComment], 37 | ["/**/", TokenType.BlockComment], 38 | ["/* comment */", TokenType.BlockComment], 39 | ["/* comment\n *with* newline */", TokenType.BlockComment], 40 | ])('Echoes token %s', (input:string, type:TokenType) => { 41 | // The only case where we don't expect an exact match: a line comment token won't include the terminal \n 42 | const possiblyTrimmedInput = (type==TokenType.LineComment)? input.trimEnd() : input; 43 | 44 | const results = [...TokenGenerator(input)]; 45 | expect(results.length).toBe(1); 46 | expect(results[0].Text).toBe(possiblyTrimmedInput); 47 | expect(results[0].Type).toBe(type); 48 | }); 49 | 50 | test.each([ 51 | ["{,", 1, 0, 1], 52 | ["null,", 4, 0, 4], 53 | ["3,", 1, 0, 1], 54 | ["3.12,", 4, 0, 4], 55 | ["3e2,", 3, 0, 3], 56 | ['"st",', 4, 0, 4], 57 | ["null ,", 5, 0, 5], 58 | ["null\t,", 5, 0, 5], 59 | ["null\n,", 5, 1, 0], 60 | [" null \r\n ,", 9, 1, 1], 61 | ["//co\n,", 5, 1, 0], 62 | ["/**/,", 4, 0, 4], 63 | ["/*1*/,", 5, 0, 5], 64 | ["/*1\n*/,", 6, 1, 2], 65 | ["\n\n", 1, 1, 0], 66 | ])("Correct position for second token in %s", (input:string, index:number, row:number, column:number) => { 67 | const results = [...TokenGenerator(input)]; 68 | 69 | expect(results.length).toBe(2); 70 | expect(results[1].InputPosition.Index).toBe(index); 71 | expect(results[1].InputPosition.Row).toBe(row); 72 | expect(results[1].InputPosition.Column).toBe(column); 73 | 74 | const expectedText = (results[0].Type==TokenType.BlankLine)? input.substring(0, index) 75 | : input.substring(0, index).trim(); 76 | expect(results[0].Text).toBe(expectedText); 77 | }); 78 | 79 | test.each([ 80 | ["t"], 81 | ["nul"], 82 | ["/"], 83 | ["/*"], 84 | ["/* comment *"], 85 | ['"'], 86 | ['"string'], 87 | ['"string with escaped quote\\"'], 88 | ["1."], 89 | ["-"], 90 | ["1.0e"], 91 | ["1.0e+"], 92 | ])("Throw if unexpected end in %s", (input:string) => { 93 | let exceptionHappened = false; 94 | try { 95 | [...TokenGenerator(input)]; 96 | } 97 | catch (err: unknown) { 98 | expect(err).toBeInstanceOf(FracturedJsonError); 99 | 100 | const fjErr = err as FracturedJsonError; 101 | expect(fjErr.InputPosition).toBeTruthy(); 102 | expect(fjErr.InputPosition?.Index).toBe(input.length); 103 | exceptionHappened = true; 104 | } 105 | 106 | expect(exceptionHappened).toBeTruthy(); 107 | }); 108 | 109 | test("Token sequences match sample", () => { 110 | // Keep each row 28 characters (plus 2 for eol) to make it easy to figure the expected index. 111 | const inputRows = [ 112 | '{ ', 113 | ' // A line comment ', 114 | ' "item1": "a string", ', 115 | ' ', 116 | ' /* a block ', 117 | ' comment */ ', 118 | ' "item2": [null, -2.0] ', 119 | '} ' 120 | ]; 121 | const inputString = inputRows.join('\r\n'); 122 | const blockCommentText = inputRows[4].trimStart() + '\r\n' + inputRows[5].trimEnd(); 123 | 124 | const expectedTokens:JsonToken[] = [ 125 | {Type:TokenType.BeginObject, Text:"{", InputPosition: {Index:0, Row:0, Column:0}}, 126 | {Type:TokenType.LineComment, Text:"// A line comment", InputPosition: {Index:34, Row:1, Column:4}}, 127 | {Type:TokenType.String, Text:"\"item1\"", InputPosition: {Index:64, Row:2, Column:4}}, 128 | {Type:TokenType.Colon, Text:":", InputPosition: {Index:71, Row:2, Column:11}}, 129 | {Type:TokenType.String, Text:"\"a string\"", InputPosition: {Index:73, Row:2, Column:13}}, 130 | {Type:TokenType.Comma, Text:",", InputPosition: {Index:83, Row:2, Column:23}}, 131 | {Type:TokenType.BlankLine, Text:"\n", InputPosition: {Index:90, Row:3, Column:0}}, 132 | {Type:TokenType.BlockComment, Text:blockCommentText, InputPosition: {Index:124, Row:4, Column:4}}, 133 | {Type:TokenType.String, Text:"\"item2\"", InputPosition: {Index:184, Row:6, Column:4}}, 134 | {Type:TokenType.Colon, Text:":", InputPosition: {Index:191, Row:6, Column:11}}, 135 | {Type:TokenType.BeginArray, Text:"[", InputPosition: {Index:193, Row:6, Column:13}}, 136 | {Type:TokenType.Null, Text:"null", InputPosition: {Index:194, Row:6, Column:14}}, 137 | {Type:TokenType.Comma, Text:",", InputPosition: {Index:198, Row:6, Column:18}}, 138 | {Type:TokenType.Number, Text:"-2.0", InputPosition: {Index:200, Row:6, Column:20}}, 139 | {Type:TokenType.EndArray, Text:"]", InputPosition: {Index:204, Row:6, Column:24}}, 140 | {Type:TokenType.EndObject, Text:"}", InputPosition: {Index:210, Row:7, Column:0}}, 141 | ]; 142 | 143 | const results = [...TokenGenerator(inputString)]; 144 | expect(results).toEqual(expectedTokens); 145 | }); 146 | 147 | test("Empty input is handled", () => { 148 | const results = [...TokenGenerator("")]; 149 | expect(results.length).toBe(0); 150 | }); 151 | }) 152 | -------------------------------------------------------------------------------- /test/TopLevelItems.test.ts: -------------------------------------------------------------------------------- 1 | import {CommentPolicy, Formatter} from "../src"; 2 | 3 | describe("Top level items tests", () => { 4 | test("Error if multiple top level elements", () => { 5 | const input = "[1,2] [3,4]"; 6 | 7 | // There are two top-level element. It should throw. 8 | const formatter = new Formatter(); 9 | expect(() => formatter.Reformat(input)).toThrowError(); 10 | expect(() => formatter.Minify(input)).toThrowError(); 11 | }); 12 | 13 | test("Error if multiple top level elements with comma", () => { 14 | const input = "[1,2], [3,4]"; 15 | 16 | // There are two top-level element with a comma. It should throw. 17 | const formatter = new Formatter(); 18 | expect(() => formatter.Reformat(input)).toThrowError(); 19 | expect(() => formatter.Minify(input)).toThrowError(); 20 | }); 21 | 22 | test("Comments after the top level element are preserved", () => { 23 | const input = "/*a*/ [1,2] /*b*/ //c"; 24 | 25 | // There's only one top level element, but there are several comments. 26 | const formatter = new Formatter(); 27 | formatter.Options.CommentPolicy = CommentPolicy.Preserve; 28 | const output = formatter.Reformat(input); 29 | 30 | expect(output).toContain("/*a*/"); 31 | expect(output).toContain("/*b*/"); 32 | expect(output).toContain("//c"); 33 | 34 | const minifiedOutput = formatter.Reformat(input); 35 | 36 | expect(minifiedOutput).toContain("/*a*/"); 37 | expect(minifiedOutput).toContain("/*b*/"); 38 | expect(minifiedOutput).toContain("//c"); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/Universal.test.ts: -------------------------------------------------------------------------------- 1 | import {readdirSync, readFileSync} from "fs"; 2 | import {CommentPolicy, EolStyle, Formatter, FracturedJsonOptions, NumberListAlignment} from "../src"; 3 | import {TableCommaPlacement} from "../src/TableCommaPlacement"; 4 | 5 | /** 6 | * Tests that should pass with ANY input and ANY settings, within a few constraints. These aren't particularly 7 | * focused tests. The idea is to throw a variety of inputs with a wide variety of settings and make sure 8 | * nothing horribly unexpected happens. 9 | * 10 | * Constraints: 11 | * * The input is valid JSON 12 | * * Input strings may not contain any of []{}:,\n 13 | * * Values given to PrefixString" may only contain whitespace. 14 | * 15 | * Those rules exist to make the output easy to test without understanding the grammar. Other files might contain 16 | * tests that don't impose these restrictions. 17 | */ 18 | describe("Universal Tests", () => { 19 | // Tests that the output is actually valid JSON. 20 | test.each(GenerateUniversalParams())("Is well formed", (params) => { 21 | const formatter = new Formatter(); 22 | formatter.Options = params.Opts; 23 | 24 | // For this test we can't have comments present. 25 | if (formatter.Options.CommentPolicy == CommentPolicy.Preserve) 26 | formatter.Options.CommentPolicy = CommentPolicy.Remove; 27 | 28 | const outputText = formatter.Reformat(params.Text); 29 | 30 | JSON.parse(outputText); 31 | }); 32 | 33 | // Any string that exists in the input should exist somewhere in the output. 34 | test.each(GenerateUniversalParams())("All strings exist", (params) => { 35 | const formatter = new Formatter(); 36 | formatter.Options = params.Opts; 37 | const outputText = formatter.Reformat(params.Text); 38 | 39 | let startPos = 0; 40 | while (true) { 41 | while (startPos < params.Text.length && params.Text[startPos] != '"') 42 | startPos += 1; 43 | 44 | let endPos = startPos + 1; 45 | while (endPos < params.Text.length && params.Text[endPos] != '"') 46 | endPos += 1; 47 | 48 | if (endPos >= params.Text.length) 49 | return; 50 | 51 | const stringFromSource = params.Text.substring(startPos+1, endPos); 52 | expect(outputText).toContain(stringFromSource); 53 | 54 | startPos = endPos + 1; 55 | } 56 | }); 57 | 58 | // Makes sure that the length restriction properties are respected. 59 | test.each(GenerateUniversalParams())("Max length respected", (params) => { 60 | const formatter = new Formatter(); 61 | formatter.Options = params.Opts; 62 | const outputText = formatter.Reformat(params.Text); 63 | const outputLines = outputText.trimEnd().split(EolString(params.Opts)); 64 | 65 | for (const line of outputLines) { 66 | const content = SkipPrefixAndIndent(params.Opts, line); 67 | 68 | // If the content is shorter than the max, it's all good. 69 | if (content.length <= params.Opts.MaxInlineLength && line.length <= params.Opts.MaxTotalLineLength) 70 | continue; 71 | 72 | // We'll consider it a single element if there's no more than one comma. 73 | const commaCount = content.match(/,/g)?.length ?? 0; 74 | expect(commaCount).toBeLessThanOrEqual(1); 75 | } 76 | }); 77 | 78 | 79 | test.each(GenerateUniversalParams())("Max inline complexity respected", (params) => { 80 | const formatter = new Formatter(); 81 | formatter.Options = params.Opts; 82 | const outputText = formatter.Reformat(params.Text); 83 | const outputLines = outputText.trimEnd().split(EolString(params.Opts)); 84 | 85 | const generalComplexity = Math.max(params.Opts.MaxInlineLength, params.Opts.MaxCompactArrayComplexity, 86 | params.Opts.MaxTableRowComplexity); 87 | 88 | // Look at each line of the output separately, counting the nesting level in each. 89 | for (const line of outputLines) { 90 | const content = SkipPrefixAndIndent(params.Opts, line); 91 | 92 | // Keep a running total of opens vs closes. Since Formatter treats empty arrays and objects as complexity 93 | // zero just like primitives, we don't update nestLevel until we see something other than an empty. 94 | let openCount = 0; 95 | let nestLevel = 0; 96 | let topLevelCommaSeen = false; 97 | let multipleTopLevelItems = false; 98 | for (let i = 0; i < content.length; ++i) { 99 | const ch = content[i]; 100 | switch (ch) { 101 | case ' ': 102 | case '\t': 103 | break; 104 | case '[': 105 | case '{': 106 | multipleTopLevelItems ||= (topLevelCommaSeen && openCount==0); 107 | openCount += 1; 108 | break; 109 | case ']': 110 | case '}': 111 | openCount -= 1; 112 | nestLevel = Math.max(nestLevel, openCount); 113 | break; 114 | default: 115 | multipleTopLevelItems ||= (topLevelCommaSeen && openCount==0); 116 | if (ch==',') 117 | topLevelCommaSeen ||= (openCount==0); 118 | nestLevel = Math.max(nestLevel, openCount); 119 | break; 120 | } 121 | } 122 | 123 | // If there were multiple top-level items on the line, this must be a compact array case. Comments mess 124 | // with the "top level" heuristic though. 125 | if (multipleTopLevelItems && params.Opts.CommentPolicy != CommentPolicy.Preserve) { 126 | expect(nestLevel).toBeLessThanOrEqual(params.Opts.MaxCompactArrayComplexity); 127 | continue; 128 | } 129 | 130 | // Otherwise, we can't actually tell if it's a compact array, table, or inline by looking at just the one line. 131 | expect(nestLevel).toBeLessThanOrEqual(generalComplexity); 132 | } 133 | }); 134 | 135 | test.each(GenerateUniversalParams())("Repeated formatting is stable", (params) => { 136 | const mainFormatter = new Formatter(); 137 | mainFormatter.Options = params.Opts; 138 | const initialOutput = mainFormatter.Reformat(params.Text); 139 | 140 | const crunchOutput = mainFormatter.Minify(initialOutput); 141 | const backToStartOutput1 = mainFormatter.Reformat(crunchOutput); 142 | 143 | // We formatted it, then minified that, then reformatted that. It should be the same. 144 | expect(backToStartOutput1).toBe(initialOutput); 145 | 146 | const expandOptions = new FracturedJsonOptions(); 147 | expandOptions.AlwaysExpandDepth = Number.MAX_VALUE; 148 | expandOptions.CommentPolicy = CommentPolicy.Preserve; 149 | expandOptions.PreserveBlankLines = true; 150 | expandOptions.NumberListAlignment = NumberListAlignment.Decimal; 151 | 152 | const expandFormatter = new Formatter(); 153 | expandFormatter.Options = expandOptions; 154 | 155 | const expandOutput = expandFormatter.Reformat(crunchOutput); 156 | const backToStartOutput2 = mainFormatter.Reformat(expandOutput); 157 | 158 | // For good measure, we took the minified output and expanded it as much as possible, and then formatted that. 159 | // Again, it should be the same as our original formatting. 160 | expect(backToStartOutput2).toBe(initialOutput); 161 | }); 162 | 163 | test.each(GenerateUniversalParams())("No trailing whitespace when option set", (params) => { 164 | const modifiedOptions = Object.assign({}, params.Opts); 165 | modifiedOptions.OmitTrailingWhitespace = true; 166 | 167 | const formatter = new Formatter(); 168 | formatter.Options = modifiedOptions; 169 | const outputText = formatter.Reformat(params.Text); 170 | const outputLines = outputText.trimEnd().split(EolString(params.Opts)); 171 | 172 | for (const line of outputLines) { 173 | const trimmedLine = line.trimEnd(); 174 | expect(line).toBe(trimmedLine); 175 | } 176 | }); 177 | }); 178 | 179 | 180 | interface IUniversalTestParams { 181 | Text: string; 182 | Opts: FracturedJsonOptions; 183 | } 184 | 185 | /** 186 | * Generates combos of input JSON and Formatter options to feed to all of the tests. 187 | */ 188 | function GenerateUniversalParams(): IUniversalTestParams[] { 189 | const standardBaseDir = "./test/StandardJsonFiles/"; 190 | const standardFileList = readdirSync(standardBaseDir); 191 | 192 | const paramArray: IUniversalTestParams[] = [] 193 | 194 | const standardContentList = standardFileList.map(filename => readFileSync(standardBaseDir + filename).toString()); 195 | const standardOptionsList = GenerateOptions(); 196 | 197 | for (const fileContents of standardContentList) { 198 | for (const option of standardOptionsList) 199 | paramArray.push({ Text: fileContents, Opts: option }); 200 | } 201 | 202 | const commentsBaseDir = "./test/FilesWithComments/"; 203 | const commentsFileList = readdirSync(commentsBaseDir); 204 | 205 | const commentsContentList = commentsFileList.map(filename => readFileSync(commentsBaseDir + filename, {encoding:"utf-8"}).toString()); 206 | const commentsOptionsList = GenerateOptions(); 207 | for (const opts of commentsOptionsList) { 208 | opts.CommentPolicy = CommentPolicy.Preserve; 209 | opts.PreserveBlankLines = true; 210 | } 211 | 212 | for (const fileContents of commentsContentList) { 213 | for (const option of commentsOptionsList) 214 | paramArray.push({ Text: fileContents, Opts: option }); 215 | } 216 | 217 | return paramArray; 218 | } 219 | 220 | function GenerateOptions(): FracturedJsonOptions[] { 221 | const optsList: FracturedJsonOptions[] = []; 222 | 223 | let opts = new FracturedJsonOptions(); 224 | optsList.push(opts); 225 | 226 | opts = new FracturedJsonOptions(); 227 | opts.MaxInlineComplexity = 10000; 228 | optsList.push(opts); 229 | 230 | opts = new FracturedJsonOptions(); 231 | opts.MaxInlineLength = Number.MAX_VALUE; 232 | optsList.push(opts); 233 | 234 | opts = new FracturedJsonOptions(); 235 | opts.MaxInlineLength = 23; 236 | optsList.push(opts); 237 | 238 | opts = new FracturedJsonOptions(); 239 | opts.MaxInlineLength = 59; 240 | optsList.push(opts); 241 | 242 | opts = new FracturedJsonOptions(); 243 | opts.MaxTotalLineLength = 59; 244 | optsList.push(opts); 245 | 246 | opts = new FracturedJsonOptions(); 247 | opts.JsonEolStyle = EolStyle.Crlf; 248 | optsList.push(opts); 249 | 250 | opts = new FracturedJsonOptions(); 251 | opts.JsonEolStyle = EolStyle.Lf; 252 | optsList.push(opts); 253 | 254 | opts = new FracturedJsonOptions(); 255 | opts.MaxInlineLength = 0; 256 | opts.MaxCompactArrayComplexity = 0; 257 | opts.MaxTableRowComplexity = 0; 258 | optsList.push(opts); 259 | 260 | opts = new FracturedJsonOptions(); 261 | opts.MaxInlineLength = 2; 262 | opts.MaxCompactArrayComplexity = 0; 263 | opts.MaxTableRowComplexity = 0; 264 | optsList.push(opts); 265 | 266 | opts = new FracturedJsonOptions(); 267 | opts.MaxInlineLength = 0; 268 | opts.MaxCompactArrayComplexity = 2; 269 | opts.MaxTableRowComplexity = 0; 270 | optsList.push(opts); 271 | 272 | opts = new FracturedJsonOptions(); 273 | opts.MaxInlineLength = 0; 274 | opts.MaxCompactArrayComplexity = 0; 275 | opts.MaxTableRowComplexity = 2; 276 | optsList.push(opts); 277 | 278 | opts = new FracturedJsonOptions(); 279 | opts.MaxInlineLength = 10; 280 | opts.MaxCompactArrayComplexity = 10; 281 | opts.MaxTableRowComplexity = 10; 282 | opts.MaxTotalLineLength = 1000; 283 | optsList.push(opts); 284 | 285 | opts = new FracturedJsonOptions(); 286 | opts.NestedBracketPadding = false; 287 | opts.SimpleBracketPadding = true; 288 | opts.ColonPadding = false; 289 | opts.CommentPadding = false; 290 | opts.IndentSpaces = 3; 291 | opts.PrefixString = "\t\t"; 292 | optsList.push(opts); 293 | 294 | opts = new FracturedJsonOptions(); 295 | opts.TableCommaPlacement = TableCommaPlacement.BeforePadding; 296 | opts.NumberListAlignment = NumberListAlignment.Left; 297 | optsList.push(opts); 298 | 299 | opts = new FracturedJsonOptions(); 300 | opts.TableCommaPlacement = TableCommaPlacement.BeforePaddingExceptNumbers; 301 | opts.NumberListAlignment = NumberListAlignment.Decimal; 302 | optsList.push(opts); 303 | 304 | opts = new FracturedJsonOptions(); 305 | opts.TableCommaPlacement = TableCommaPlacement.BeforePaddingExceptNumbers; 306 | opts.NumberListAlignment = NumberListAlignment.Normalize; 307 | optsList.push(opts); 308 | 309 | optsList.push(FracturedJsonOptions.Recommended()); 310 | 311 | return optsList; 312 | } 313 | 314 | function EolString(options: FracturedJsonOptions) { 315 | switch (options.JsonEolStyle) { 316 | case EolStyle.Crlf: 317 | return "\r\n"; 318 | default: 319 | return "\n"; 320 | } 321 | } 322 | 323 | function SkipPrefixAndIndent(options: FracturedJsonOptions, line: string): string { 324 | // Skip past the prefix string and whitespace. 325 | if (line.indexOf(options.PrefixString) != 0) 326 | throw new Error("Output line does not begin with prefix string"); 327 | return line.substring(options.PrefixString.length).trimStart(); 328 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig to read more about this file */ 6 | 7 | /* Projects */ 8 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 9 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 10 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 11 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 12 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 13 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 14 | 15 | /* Language and Environment */ 16 | "target": "ES2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 17 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 18 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | 29 | /* Modules */ 30 | "module": "commonjs", /* Specify what module code is generated. */ 31 | "rootDir": "./src", /* Specify the root folder within your source files. */ 32 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "resolveJsonModule": true, /* Enable importing .json files. */ 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | 43 | /* JavaScript Support */ 44 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 45 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 46 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 47 | 48 | /* Emit */ 49 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 50 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 51 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 52 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 53 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 54 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | // "noEmit": true, /* Disable emitting files from a compilation. */ 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 72 | 73 | /* Interop Constraints */ 74 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 75 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 76 | // "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 79 | 80 | /* Type Checking */ 81 | "strict": true, /* Enable all strict type-checking options. */ 82 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 83 | "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 84 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 86 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 87 | "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 88 | "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 89 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 90 | "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 91 | "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 92 | "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 93 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 94 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 95 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 96 | "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 97 | "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 98 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 99 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 100 | 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | // "skipLibCheck": true /* Skip type checking all .d.ts files. */ 104 | } 105 | } 106 | --------------------------------------------------------------------------------