From 24a13c4efe35223c0aea30ac4a5c596359ff7d7e Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Sat, 23 Dec 2017 13:05:33 -0500 Subject: [PATCH] add JSON formatter and editor from VS Code Adds the JSON formatter and editor from VS Code in src/vs/base/common/{jsonFormatter,jsonEdit}.ts, respectively, and their respective tests. --- src/edit.ts | 142 +++++++++++++ src/format.ts | 214 +++++++++++++++++++ src/test/edit.test.ts | 165 +++++++++++++++ src/test/format.test.ts | 443 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 964 insertions(+) create mode 100644 src/edit.ts create mode 100644 src/format.ts create mode 100644 src/test/edit.test.ts create mode 100644 src/test/format.test.ts diff --git a/src/edit.ts b/src/edit.ts new file mode 100644 index 0000000..09bcb7b --- /dev/null +++ b/src/edit.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { ParseError, Node, parseTree, findNodeAtLocation, JSONPath, Segment } from './main'; +import { Edit, FormattingOptions, format, applyEdit } from './format'; + +export function removeProperty(text: string, path: JSONPath, formattingOptions: FormattingOptions): Edit[] { + return setProperty(text, path, void 0, formattingOptions); +} + +export function setProperty(text: string, path: JSONPath, value: any, formattingOptions: FormattingOptions, getInsertionIndex?: (properties: string[]) => number): Edit[] { + let errors: ParseError[] = []; + let root = parseTree(text, errors); + let parent: Node = void 0; + + let lastSegment: Segment = void 0; + while (path.length > 0) { + lastSegment = path.pop(); + parent = findNodeAtLocation(root, path); + if (parent === void 0 && value !== void 0) { + if (typeof lastSegment === 'string') { + value = { [lastSegment]: value }; + } else { + value = [value]; + } + } else { + break; + } + } + + if (!parent) { + // empty document + if (value === void 0) { // delete + throw new Error('Can not delete in empty document'); + } + return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, formattingOptions); + } else if (parent.type === 'object' && typeof lastSegment === 'string') { + let existing = findNodeAtLocation(parent, [lastSegment]); + if (existing !== void 0) { + if (value === void 0) { // delete + let propertyIndex = parent.children.indexOf(existing.parent); + let removeBegin: number; + let removeEnd = existing.parent.offset + existing.parent.length; + if (propertyIndex > 0) { + // remove the comma of the previous node + let previous = parent.children[propertyIndex - 1]; + removeBegin = previous.offset + previous.length; + } else { + removeBegin = parent.offset + 1; + if (parent.children.length > 1) { + // remove the comma of the next node + let next = parent.children[1]; + removeEnd = next.offset; + } + } + return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: '' }, formattingOptions); + } else { + // set value of existing property + return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, formattingOptions); + } + } else { + if (value === void 0) { // delete + return []; // property does not exist, nothing to do + } + let newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`; + let index = getInsertionIndex ? getInsertionIndex(parent.children.map(p => p.children[0].value)) : parent.children.length; + let edit: Edit; + if (index > 0) { + let previous = parent.children[index - 1]; + edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; + } else if (parent.children.length === 0) { + edit = { offset: parent.offset + 1, length: 0, content: newProperty }; + } else { + edit = { offset: parent.offset + 1, length: 0, content: newProperty + ',' }; + } + return withFormatting(text, edit, formattingOptions); + } + } else if (parent.type === 'array' && typeof lastSegment === 'number') { + let insertIndex = lastSegment; + if (insertIndex === -1) { + // Insert + let newProperty = `${JSON.stringify(value)}`; + let edit: Edit; + if (parent.children.length === 0) { + edit = { offset: parent.offset + 1, length: 0, content: newProperty }; + } else { + let previous = parent.children[parent.children.length - 1]; + edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; + } + return withFormatting(text, edit, formattingOptions); + } else { + if (value === void 0 && parent.children.length >= 0) { + //Removal + let removalIndex = lastSegment; + let toRemove = parent.children[removalIndex]; + let edit: Edit; + if (parent.children.length === 1) { + // only item + edit = { offset: parent.offset + 1, length: parent.length - 2, content: '' }; + } else if (parent.children.length - 1 === removalIndex) { + // last item + let previous = parent.children[removalIndex - 1]; + let offset = previous.offset + previous.length; + let parentEndOffset = parent.offset + parent.length; + edit = { offset, length: parentEndOffset - 2 - offset, content: '' }; + } else { + edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: '' }; + } + return withFormatting(text, edit, formattingOptions); + } else { + throw new Error('Array modification not supported yet'); + } + } + } else { + throw new Error(`Can not add ${typeof lastSegment !== 'number' ? 'index' : 'property'} to parent of type ${parent.type}`); + } +} + +function withFormatting(text: string, edit: Edit, formattingOptions: FormattingOptions): Edit[] { + // apply the edit + let newText = applyEdit(text, edit); + + // format the new text + let begin = edit.offset; + let end = edit.offset + edit.content.length; + let edits = format(newText, { offset: begin, length: end - begin }, formattingOptions); + + // apply the formatting edits and track the begin and end offsets of the changes + for (let i = edits.length - 1; i >= 0; i--) { + let edit = edits[i]; + newText = applyEdit(newText, edit); + begin = Math.min(begin, edit.offset); + end = Math.max(end, edit.offset + edit.length); + end += edit.content.length - edit.length; + } + // create a single edit with all changes + let editLength = text.length - (newText.length - end) - begin; + return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }]; +} \ No newline at end of file diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..5f092d4 --- /dev/null +++ b/src/format.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as Json from './main'; + +export interface FormattingOptions { + /** + * If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent? + */ + tabSize: number; + /** + * Is indentation based on spaces? + */ + insertSpaces: boolean; + /** + * The default end of line line character + */ + eol: string; +} + +export interface Edit { + offset: number; + length: number; + content: string; +} + +export function applyEdit(text: string, edit: Edit): string { + return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length); +} + +export function applyEdits(text: string, edits: Edit[]): string { + for (let i = edits.length - 1; i >= 0; i--) { + text = applyEdit(text, edits[i]); + } + return text; +} + +export function format(documentText: string, range: { offset: number, length: number }, options: FormattingOptions): Edit[] { + let initialIndentLevel: number; + let value: string; + let rangeStart: number; + let rangeEnd: number; + if (range) { + rangeStart = range.offset; + rangeEnd = rangeStart + range.length; + while (rangeStart > 0 && !isEOL(documentText, rangeStart - 1)) { + rangeStart--; + } + let scanner = Json.createScanner(documentText, true); + scanner.setPosition(rangeEnd); + scanner.scan(); + rangeEnd = scanner.getPosition(); + + value = documentText.substring(rangeStart, rangeEnd); + initialIndentLevel = computeIndentLevel(value, 0, options); + } else { + value = documentText; + rangeStart = 0; + rangeEnd = documentText.length; + initialIndentLevel = 0; + } + let eol = getEOL(options, documentText); + + let lineBreak = false; + let indentLevel = 0; + let indentValue: string; + if (options.insertSpaces) { + indentValue = repeat(' ', options.tabSize); + } else { + indentValue = '\t'; + } + + let scanner = Json.createScanner(value, false); + + function newLineAndIndent(): string { + return eol + repeat(indentValue, initialIndentLevel + indentLevel); + } + function scanNext(): Json.SyntaxKind { + let token = scanner.scan(); + lineBreak = false; + while (token === Json.SyntaxKind.Trivia || token === Json.SyntaxKind.LineBreakTrivia) { + lineBreak = lineBreak || (token === Json.SyntaxKind.LineBreakTrivia); + token = scanner.scan(); + } + return token; + } + let editOperations: Edit[] = []; + function addEdit(text: string, startOffset: number, endOffset: number) { + if (documentText.substring(startOffset, endOffset) !== text) { + editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text }); + } + } + + let firstToken = scanNext(); + if (firstToken !== Json.SyntaxKind.EOF) { + let firstTokenStart = scanner.getTokenOffset() + rangeStart; + let initialIndent = repeat(indentValue, initialIndentLevel); + addEdit(initialIndent, rangeStart, firstTokenStart); + } + + while (firstToken !== Json.SyntaxKind.EOF) { + let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + rangeStart; + let secondToken = scanNext(); + + let replaceContent = ''; + while (!lineBreak && (secondToken === Json.SyntaxKind.LineCommentTrivia || secondToken === Json.SyntaxKind.BlockCommentTrivia)) { + // comments on the same line: keep them on the same line, but ignore them otherwise + let commentTokenStart = scanner.getTokenOffset() + rangeStart; + addEdit(' ', firstTokenEnd, commentTokenStart); + firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + rangeStart; + replaceContent = secondToken === Json.SyntaxKind.LineCommentTrivia ? newLineAndIndent() : ''; + secondToken = scanNext(); + } + + if (secondToken === Json.SyntaxKind.CloseBraceToken) { + if (firstToken !== Json.SyntaxKind.OpenBraceToken) { + indentLevel--; + replaceContent = newLineAndIndent(); + } + } else if (secondToken === Json.SyntaxKind.CloseBracketToken) { + if (firstToken !== Json.SyntaxKind.OpenBracketToken) { + indentLevel--; + replaceContent = newLineAndIndent(); + } + } else if (secondToken !== Json.SyntaxKind.EOF) { + switch (firstToken) { + case Json.SyntaxKind.OpenBracketToken: + case Json.SyntaxKind.OpenBraceToken: + indentLevel++; + replaceContent = newLineAndIndent(); + break; + case Json.SyntaxKind.CommaToken: + case Json.SyntaxKind.LineCommentTrivia: + replaceContent = newLineAndIndent(); + break; + case Json.SyntaxKind.BlockCommentTrivia: + if (lineBreak) { + replaceContent = newLineAndIndent(); + } else { + // symbol following comment on the same line: keep on same line, separate with ' ' + replaceContent = ' '; + } + break; + case Json.SyntaxKind.ColonToken: + replaceContent = ' '; + break; + case Json.SyntaxKind.NullKeyword: + case Json.SyntaxKind.TrueKeyword: + case Json.SyntaxKind.FalseKeyword: + case Json.SyntaxKind.NumericLiteral: + if (secondToken === Json.SyntaxKind.NullKeyword || secondToken === Json.SyntaxKind.FalseKeyword || secondToken === Json.SyntaxKind.NumericLiteral) { + replaceContent = ' '; + } + break; + } + if (lineBreak && (secondToken === Json.SyntaxKind.LineCommentTrivia || secondToken === Json.SyntaxKind.BlockCommentTrivia)) { + replaceContent = newLineAndIndent(); + } + + } + let secondTokenStart = scanner.getTokenOffset() + rangeStart; + addEdit(replaceContent, firstTokenEnd, secondTokenStart); + firstToken = secondToken; + } + return editOperations; +} + +function repeat(s: string, count: number): string { + let result = ''; + for (let i = 0; i < count; i++) { + result += s; + } + return result; +} + +function computeIndentLevel(content: string, offset: number, options: FormattingOptions): number { + let i = 0; + let nChars = 0; + let tabSize = options.tabSize || 4; + while (i < content.length) { + let ch = content.charAt(i); + if (ch === ' ') { + nChars++; + } else if (ch === '\t') { + nChars += tabSize; + } else { + break; + } + i++; + } + return Math.floor(nChars / tabSize); +} + +function getEOL(options: FormattingOptions, text: string): string { + for (let i = 0; i < text.length; i++) { + let ch = text.charAt(i); + if (ch === '\r') { + if (i + 1 < text.length && text.charAt(i + 1) === '\n') { + return '\r\n'; + } + return '\r'; + } else if (ch === '\n') { + return '\n'; + } + } + return (options && options.eol) || '\n'; +} + +function isEOL(text: string, offset: number) { + return '\r\n'.indexOf(text.charAt(offset)) !== -1; +} \ No newline at end of file diff --git a/src/test/edit.test.ts b/src/test/edit.test.ts new file mode 100644 index 0000000..f1e0331 --- /dev/null +++ b/src/test/edit.test.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { FormattingOptions, Edit } from '../format'; +import { setProperty, removeProperty } from '../edit'; + +suite('JSON - edits', () => { + + function assertEdit(content: string, edits: Edit[], expected: string) { + assert(edits); + let lastEditOffset = content.length; + for (let i = edits.length - 1; i >= 0; i--) { + let edit = edits[i]; + assert(edit.offset >= 0 && edit.length >= 0 && edit.offset + edit.length <= content.length); + assert(typeof edit.content === 'string'); + assert(lastEditOffset >= edit.offset + edit.length); // make sure all edits are ordered + lastEditOffset = edit.offset; + content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); + } + assert.equal(content, expected); + } + + let formatterOptions: FormattingOptions = { + insertSpaces: true, + tabSize: 2, + eol: '\n' + }; + + test('set property', () => { + let content = '{\n "x": "y"\n}'; + let edits = setProperty(content, ['x'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "x": "bar"\n}'); + + content = 'true'; + edits = setProperty(content, [], 'bar', formatterOptions); + assertEdit(content, edits, '"bar"'); + + content = '{\n "x": "y"\n}'; + edits = setProperty(content, ['x'], { key: true }, formatterOptions); + assertEdit(content, edits, '{\n "x": {\n "key": true\n }\n}'); + content = '{\n "a": "b", "x": "y"\n}'; + edits = setProperty(content, ['a'], null, formatterOptions); + assertEdit(content, edits, '{\n "a": null, "x": "y"\n}'); + }); + + test('insert property', () => { + let content = '{}'; + let edits = setProperty(content, ['foo'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "foo": "bar"\n}'); + + edits = setProperty(content, ['foo', 'foo2'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "foo": {\n "foo2": "bar"\n }\n}'); + + content = '{\n}'; + edits = setProperty(content, ['foo'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "foo": "bar"\n}'); + + content = ' {\n }'; + edits = setProperty(content, ['foo'], 'bar', formatterOptions); + assertEdit(content, edits, ' {\n "foo": "bar"\n }'); + + content = '{\n "x": "y"\n}'; + edits = setProperty(content, ['foo'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "x": "y",\n "foo": "bar"\n}'); + + content = '{\n "x": "y"\n}'; + edits = setProperty(content, ['e'], 'null', formatterOptions); + assertEdit(content, edits, '{\n "x": "y",\n "e": "null"\n}'); + + edits = setProperty(content, ['x'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "x": "bar"\n}'); + + content = '{\n "x": {\n "a": 1,\n "b": true\n }\n}\n'; + edits = setProperty(content, ['x'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "x": "bar"\n}\n'); + + edits = setProperty(content, ['x', 'b'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "b": "bar"\n }\n}\n'); + + edits = setProperty(content, ['x', 'c'], 'bar', formatterOptions, () => 0); + assertEdit(content, edits, '{\n "x": {\n "c": "bar",\n "a": 1,\n "b": true\n }\n}\n'); + + edits = setProperty(content, ['x', 'c'], 'bar', formatterOptions, () => 1); + assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "c": "bar",\n "b": true\n }\n}\n'); + + edits = setProperty(content, ['x', 'c'], 'bar', formatterOptions, () => 2); + assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "b": true,\n "c": "bar"\n }\n}\n'); + + edits = setProperty(content, ['c'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "b": true\n },\n "c": "bar"\n}\n'); + + content = '{\n "a": [\n {\n } \n ] \n}'; + edits = setProperty(content, ['foo'], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "a": [\n {\n } \n ],\n "foo": "bar"\n}'); + + content = ''; + edits = setProperty(content, ['foo', 0], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "foo": [\n "bar"\n ]\n}'); + + content = '//comment'; + edits = setProperty(content, ['foo', 0], 'bar', formatterOptions); + assertEdit(content, edits, '{\n "foo": [\n "bar"\n ]\n} //comment\n'); + }); + + test('remove property', () => { + let content = '{\n "x": "y"\n}'; + let edits = removeProperty(content, ['x'], formatterOptions); + assertEdit(content, edits, '{}'); + + content = '{\n "x": "y", "a": []\n}'; + edits = removeProperty(content, ['x'], formatterOptions); + assertEdit(content, edits, '{\n "a": []\n}'); + + content = '{\n "x": "y", "a": []\n}'; + edits = removeProperty(content, ['a'], formatterOptions); + assertEdit(content, edits, '{\n "x": "y"\n}'); + }); + + test('insert item to empty array', () => { + let content = '[\n]'; + let edits = setProperty(content, [-1], 'bar', formatterOptions); + assertEdit(content, edits, '[\n "bar"\n]'); + }); + + test('insert item', () => { + let content = '[\n 1,\n 2\n]'; + let edits = setProperty(content, [-1], 'bar', formatterOptions); + assertEdit(content, edits, '[\n 1,\n 2,\n "bar"\n]'); + }); + + test('remove item in array with one item', () => { + let content = '[\n 1\n]'; + let edits = setProperty(content, [0], void 0, formatterOptions); + assertEdit(content, edits, '[]'); + }); + + test('remove item in the middle of the array', () => { + let content = '[\n 1,\n 2,\n 3\n]'; + let edits = setProperty(content, [1], void 0, formatterOptions); + assertEdit(content, edits, '[\n 1,\n 3\n]'); + }); + + test('remove last item in the array', () => { + let content = '[\n 1,\n 2,\n "bar"\n]'; + let edits = setProperty(content, [2], void 0, formatterOptions); + assertEdit(content, edits, '[\n 1,\n 2\n]'); + }); + + test('remove last item in the array if ends with comma', () => { + let content = '[\n 1,\n "foo",\n "bar",\n]'; + let edits = setProperty(content, [2], void 0, formatterOptions); + assertEdit(content, edits, '[\n 1,\n "foo"\n]'); + }); + + test('remove last item in the array if there is a comment in the beginning', () => { + let content = '// This is a comment\n[\n 1,\n "foo",\n "bar"\n]'; + let edits = setProperty(content, [2], void 0, formatterOptions); + assertEdit(content, edits, '// This is a comment\n[\n 1,\n "foo"\n]'); + }); + +}); \ No newline at end of file diff --git a/src/test/format.test.ts b/src/test/format.test.ts new file mode 100644 index 0000000..4608bd0 --- /dev/null +++ b/src/test/format.test.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import * as Formatter from '../format'; + +suite('JSON - formatter', () => { + + function format(content: string, expected: string, insertSpaces = true) { + let range = void 0; + var rangeStart = content.indexOf('|'); + var rangeEnd = content.lastIndexOf('|'); + if (rangeStart !== -1 && rangeEnd !== -1) { + content = content.substring(0, rangeStart) + content.substring(rangeStart + 1, rangeEnd) + content.substring(rangeEnd + 1); + range = { offset: rangeStart, length: rangeEnd - rangeStart }; + } + + var edits = Formatter.format(content, range, { tabSize: 2, insertSpaces: insertSpaces, eol: '\n' }); + + let lastEditOffset = content.length; + for (let i = edits.length - 1; i >= 0; i--) { + let edit = edits[i]; + assert(edit.offset >= 0 && edit.length >= 0 && edit.offset + edit.length <= content.length); + assert(typeof edit.content === 'string'); + assert(lastEditOffset >= edit.offset + edit.length); // make sure all edits are ordered + lastEditOffset = edit.offset; + content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); + } + + assert.equal(content, expected); + } + + test('object - single property', () => { + var content = [ + '{"x" : 1}' + ].join('\n'); + + var expected = [ + '{', + ' "x": 1', + '}' + ].join('\n'); + + format(content, expected); + }); + test('object - multiple properties', () => { + var content = [ + '{"x" : 1, "y" : "foo", "z" : true}' + ].join('\n'); + + var expected = [ + '{', + ' "x": 1,', + ' "y": "foo",', + ' "z": true', + '}' + ].join('\n'); + + format(content, expected); + }); + test('object - no properties ', () => { + var content = [ + '{"x" : { }, "y" : {}}' + ].join('\n'); + + var expected = [ + '{', + ' "x": {},', + ' "y": {}', + '}' + ].join('\n'); + + format(content, expected); + }); + test('object - nesting', () => { + var content = [ + '{"x" : { "y" : { "z" : { }}, "a": true}}' + ].join('\n'); + + var expected = [ + '{', + ' "x": {', + ' "y": {', + ' "z": {}', + ' },', + ' "a": true', + ' }', + '}' + ].join('\n'); + + format(content, expected); + }); + + test('array - single items', () => { + var content = [ + '["[]"]' + ].join('\n'); + + var expected = [ + '[', + ' "[]"', + ']' + ].join('\n'); + + format(content, expected); + }); + + test('array - multiple items', () => { + var content = [ + '[true,null,1.2]' + ].join('\n'); + + var expected = [ + '[', + ' true,', + ' null,', + ' 1.2', + ']' + ].join('\n'); + + format(content, expected); + }); + + test('array - no items', () => { + var content = [ + '[ ]' + ].join('\n'); + + var expected = [ + '[]' + ].join('\n'); + + format(content, expected); + }); + + test('array - nesting', () => { + var content = [ + '[ [], [ [ {} ], "a" ] ]' + ].join('\n'); + + var expected = [ + '[', + ' [],', + ' [', + ' [', + ' {}', + ' ],', + ' "a"', + ' ]', + ']', + ].join('\n'); + + format(content, expected); + }); + + test('syntax errors', () => { + var content = [ + '[ null 1.2 ]' + ].join('\n'); + + var expected = [ + '[', + ' null 1.2', + ']', + ].join('\n'); + + format(content, expected); + }); + + test('empty lines', () => { + var content = [ + '{', + '"a": true,', + '', + '"b": true', + '}', + ].join('\n'); + + var expected = [ + '{', + '\t"a": true,', + '\t"b": true', + '}', + ].join('\n'); + + format(content, expected, false); + }); + test('single line comment', () => { + var content = [ + '[ ', + '//comment', + '"foo", "bar"', + '] ' + ].join('\n'); + + var expected = [ + '[', + ' //comment', + ' "foo",', + ' "bar"', + ']', + ].join('\n'); + + format(content, expected); + }); + test('block line comment', () => { + var content = [ + '[{', + ' /*comment*/ ', + '"foo" : true', + '}] ' + ].join('\n'); + + var expected = [ + '[', + ' {', + ' /*comment*/', + ' "foo": true', + ' }', + ']', + ].join('\n'); + + format(content, expected); + }); + test('single line comment on same line', () => { + var content = [ + ' { ', + ' "a": {}// comment ', + ' } ' + ].join('\n'); + + var expected = [ + '{', + ' "a": {} // comment ', + '}', + ].join('\n'); + + format(content, expected); + }); + test('single line comment on same line 2', () => { + var content = [ + '{ //comment', + '}' + ].join('\n'); + + var expected = [ + '{ //comment', + '}' + ].join('\n'); + + format(content, expected); + }); + test('block comment on same line', () => { + var content = [ + '{ "a": {}, /*comment*/ ', + ' /*comment*/ "b": {}, ', + ' "c": {/*comment*/} } ', + ].join('\n'); + + var expected = [ + '{', + ' "a": {}, /*comment*/', + ' /*comment*/ "b": {},', + ' "c": { /*comment*/}', + '}', + ].join('\n'); + + format(content, expected); + }); + + test('block comment on same line advanced', () => { + var content = [ + ' { "d": [', + ' null', + ' ] /*comment*/', + ' ,"e": /*comment*/ [null] }', + ].join('\n'); + + var expected = [ + '{', + ' "d": [', + ' null', + ' ] /*comment*/,', + ' "e": /*comment*/ [', + ' null', + ' ]', + '}', + ].join('\n'); + + format(content, expected); + }); + + test('multiple block comments on same line', () => { + var content = [ + '{ "a": {} /*comment*/, /*comment*/ ', + ' /*comment*/ "b": {} /*comment*/ } ' + ].join('\n'); + + var expected = [ + '{', + ' "a": {} /*comment*/, /*comment*/', + ' /*comment*/ "b": {} /*comment*/', + '}', + ].join('\n'); + + format(content, expected); + }); + test('multiple mixed comments on same line', () => { + var content = [ + '[ /*comment*/ /*comment*/ // comment ', + ']' + ].join('\n'); + + var expected = [ + '[ /*comment*/ /*comment*/ // comment ', + ']' + ].join('\n'); + + format(content, expected); + }); + + test('range', () => { + var content = [ + '{ "a": {},', + '|"b": [null, null]|', + '} ' + ].join('\n'); + + var expected = [ + '{ "a": {},', + '"b": [', + ' null,', + ' null', + ']', + '} ', + ].join('\n'); + + format(content, expected); + }); + + test('range with existing indent', () => { + var content = [ + '{ "a": {},', + ' |"b": [null],', + '"c": {}', + '} |' + ].join('\n'); + + var expected = [ + '{ "a": {},', + ' "b": [', + ' null', + ' ],', + ' "c": {}', + '}', + ].join('\n'); + + format(content, expected); + }); + + test('range with existing indent - tabs', () => { + var content = [ + '{ "a": {},', + '| "b": [null], ', + '"c": {}', + '} | ' + ].join('\n'); + + var expected = [ + '{ "a": {},', + '\t"b": [', + '\t\tnull', + '\t],', + '\t"c": {}', + '}', + ].join('\n'); + + format(content, expected, false); + }); + + + test('block comment none-line breaking symbols', () => { + var content = [ + '{ "a": [ 1', + '/* comment */', + ', 2', + '/* comment */', + ']', + '/* comment */', + ',', + ' "b": true', + '/* comment */', + '}' + ].join('\n'); + + var expected = [ + '{', + ' "a": [', + ' 1', + ' /* comment */', + ' ,', + ' 2', + ' /* comment */', + ' ]', + ' /* comment */', + ' ,', + ' "b": true', + ' /* comment */', + '}', + ].join('\n'); + + format(content, expected); + }); + test('line comment after none-line breaking symbols', () => { + var content = [ + '{ "a":', + '// comment', + 'null,', + ' "b"', + '// comment', + ': null', + '// comment', + '}' + ].join('\n'); + + var expected = [ + '{', + ' "a":', + ' // comment', + ' null,', + ' "b"', + ' // comment', + ' : null', + ' // comment', + '}', + ].join('\n'); + + format(content, expected); + }); +}); \ No newline at end of file