diff --git a/README.md b/README.md index 823d198..3ec8f73 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,7 @@ the formatter falls back to a compact style with no extra spaces or indentation. |------------------------------|----------------------|--------------------------------------------------------| | `indentationLevel` | `number` | Base indentation level for the document. | | `indentationCharacter` | `'space'\|'tab'` | Character used for indentation. | +| `lineEnding` | `'lf'\|'crlf'` | Newline sequence to use in the document. | | `string.quote` | `'single'\|'double'` | Quotation style for string values. | | `property.quote` | `'single'\|'double'` | Quotation style for property keys. | | `property.unquoted` | `boolean` | Allow unquoted property keys when valid. | diff --git a/src/formatting.ts b/src/formatting.ts index 93c7298..ab75f65 100644 --- a/src/formatting.ts +++ b/src/formatting.ts @@ -11,6 +11,7 @@ export type BlockFormatting = { export type Formatting = { indentationLevel?: number, indentationCharacter?: 'space' | 'tab', + lineEnding?: 'lf' | 'crlf', string?: { quote?: 'single' | 'double', }, diff --git a/src/node/structureNode.ts b/src/node/structureNode.ts index bef98e2..c88515f 100644 --- a/src/node/structureNode.ts +++ b/src/node/structureNode.ts @@ -164,12 +164,7 @@ export abstract class JsonStructureNode extends JsonValueNode { if (indentationSize > 0 && manipulator.matchesNext(node => endToken.isEquivalent(node))) { // If the following token is the end token, always indent. // This ensures it won't consume the indentation of the end delimiter. - manipulator.node( - new JsonTokenNode({ - type: JsonTokenType.NEWLINE, - value: '\n', - }), - ); + manipulator.node(this.getNewlineToken(formatting)); if ( manipulator.matchesToken(JsonTokenType.WHITESPACE) @@ -187,12 +182,7 @@ export abstract class JsonStructureNode extends JsonValueNode { previousMatched = currentMatched; if (manipulator.matchesPreviousToken(JsonTokenType.LINE_COMMENT)) { - manipulator.insert( - new JsonTokenNode({ - type: JsonTokenType.NEWLINE, - value: '\n', - }), - ); + manipulator.insert(this.getNewlineToken(formatting)); } else if ( manipulator.position > 1 && !currentMatched @@ -373,6 +363,10 @@ export abstract class JsonStructureNode extends JsonValueNode { } } + if (NEWLINE(token)) { + formatting.lineEnding = token.value.includes('\r\n') ? 'crlf' : 'lf'; + } + if (inlineComma && index > 0 && tokens[index - 1].depth === 0) { if (!NEWLINE(token)) { blockFormatting.commaSpacing = WHITESPACE(token); @@ -485,10 +479,7 @@ export abstract class JsonStructureNode extends JsonValueNode { return; } - const newLine = new JsonTokenNode({ - type: JsonTokenType.NEWLINE, - value: '\n', - }); + const newLine = this.getNewlineToken(formatting); manipulator.token(newLine, optional); @@ -511,6 +502,13 @@ export abstract class JsonStructureNode extends JsonValueNode { }); } + private getNewlineToken(formatting: Formatting): JsonTokenNode { + return new JsonTokenNode({ + type: JsonTokenType.NEWLINE, + value: formatting.lineEnding === 'crlf' ? '\r\n' : '\n', + }); + } + private static* iterate( parent: JsonCompositeNode, maxDepth: number, diff --git a/test/functional.test.ts b/test/functional.test.ts index 7609264..b9a18c2 100644 --- a/test/functional.test.ts +++ b/test/functional.test.ts @@ -214,6 +214,13 @@ describe('Functional test', () => { input: "'\uD83C\uDFBC'", expected: '🎼', }, + { + input: '{\r\n "foo": "1",\r\n "bar": "2"\r\n}', + expected: { + foo: '1', + bar: '2', + }, + }, ])('should parse $input', ({input, expected}) => { const parser = new JsonParser(input); const node = parser.parseValue(); @@ -334,6 +341,17 @@ describe('Functional test', () => { node.set('bar', 2); }, }, + { + description: 'use \\r\\n line endings when detected', + // language=JSON + input: '{\r\n "foo": "1"\r\n}', + // language=JSON + output: '{\r\n "foo": "1",\r\n "bar": "2"\r\n}', + type: JsonObjectNode, + mutation: (node: JsonObjectNode): void => { + node.set('bar', '2'); + }, + }, { description: 'use the same indentation character as the the parent', // language=JSON @@ -3028,6 +3046,28 @@ describe('Functional test', () => { node.set('bar', 'qux'); }, }, + { + description: 'preserve carriage return and line feed as line ending', + // language=JSON5 + input: '{\r\n "foo": 1,\r\n "bar": 2\r\n}', + // language=JSON5 + output: '{\r\n "foo": 1\r\n}', + type: JsonObjectNode, + mutation: (node: JsonObjectNode): void => { + node.delete('bar'); + }, + }, + { + description: 'detect carriage return and line feed as line ending', + // language=JSON5 + input: '{\r\n "foo": 1,\r\n "bar": 2\r\n}', + // language=JSON5 + output: '{\r\n "foo": 1\r\n}', + type: JsonObjectNode, + mutation: (node: JsonObjectNode): void => { + node.delete('bar'); + }, + }, ])('should $description', ({input, output, type, mutation, format}) => { const node = JsonParser.parse(input, type);