From 06ea78fdf0773629112820fce2d6420a530697d8 Mon Sep 17 00:00:00 2001 From: Stef Rommes Date: Fri, 28 Nov 2025 13:54:35 +0100 Subject: [PATCH 1/4] Fix for issue #9 --- src/formatting.ts | 1 + src/node/structureNode.ts | 35 +++++++++++++++++++---------------- test/functional.test.ts | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/formatting.ts b/src/formatting.ts index 93c7298..40ba4fc 100644 --- a/src/formatting.ts +++ b/src/formatting.ts @@ -11,6 +11,7 @@ export type BlockFormatting = { export type Formatting = { indentationLevel?: number, indentationCharacter?: 'space' | 'tab', + newlineCharacter?: 'LF' | 'CRLF', string?: { quote?: 'single' | 'double', }, diff --git a/src/node/structureNode.ts b/src/node/structureNode.ts index bef98e2..5708902 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,14 @@ export abstract class JsonStructureNode extends JsonValueNode { } } + if (NEWLINE(token)) { + if (token.value.includes('\r\n')) { + formatting.newlineCharacter = 'CRLF'; + } else { + formatting.newlineCharacter = 'LF'; + } + } + if (inlineComma && index > 0 && tokens[index - 1].depth === 0) { if (!NEWLINE(token)) { blockFormatting.commaSpacing = WHITESPACE(token); @@ -485,10 +483,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 +506,14 @@ export abstract class JsonStructureNode extends JsonValueNode { }); } + private getNewlineToken(formatting: Formatting): JsonTokenNode { + const newlineChar = formatting.newlineCharacter === 'CRLF' ? '\r\n' : '\n'; + return new JsonTokenNode({ + type: JsonTokenType.NEWLINE, + value: newlineChar, + }); + } + private static* iterate( parent: JsonCompositeNode, maxDepth: number, diff --git a/test/functional.test.ts b/test/functional.test.ts index 7609264..39ec3a9 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 From 78fc0ba002b6e3c1088fc67e71d5fc70319567b9 Mon Sep 17 00:00:00 2001 From: Stef Rommes Date: Fri, 28 Nov 2025 14:41:25 +0100 Subject: [PATCH 2/4] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 823d198..a0f7bdb 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. | +| `newlineCharacter` | `'LF'\|'CRLF'` | Newline character 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. | From 7ba72f061fe96342f7ba59b43268785a5198b49b Mon Sep 17 00:00:00 2001 From: Stef Rommes Date: Fri, 28 Nov 2025 14:56:31 +0100 Subject: [PATCH 3/4] fix a typo --- README.md | 2 +- src/formatting.ts | 2 +- src/node/structureNode.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a0f7bdb..a50309e 100644 --- a/README.md +++ b/README.md @@ -252,7 +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. | -| `newlineCharacter` | `'LF'\|'CRLF'` | Newline character to use in the document. | +| `newLineCharacter` | `'LF'\|'CRLF'` | Newline character 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 40ba4fc..3e83a89 100644 --- a/src/formatting.ts +++ b/src/formatting.ts @@ -11,7 +11,7 @@ export type BlockFormatting = { export type Formatting = { indentationLevel?: number, indentationCharacter?: 'space' | 'tab', - newlineCharacter?: 'LF' | 'CRLF', + newLineCharacter?: 'LF' | 'CRLF', string?: { quote?: 'single' | 'double', }, diff --git a/src/node/structureNode.ts b/src/node/structureNode.ts index 5708902..aa52447 100644 --- a/src/node/structureNode.ts +++ b/src/node/structureNode.ts @@ -365,9 +365,9 @@ export abstract class JsonStructureNode extends JsonValueNode { if (NEWLINE(token)) { if (token.value.includes('\r\n')) { - formatting.newlineCharacter = 'CRLF'; + formatting.newLineCharacter = 'CRLF'; } else { - formatting.newlineCharacter = 'LF'; + formatting.newLineCharacter = 'LF'; } } @@ -507,7 +507,7 @@ export abstract class JsonStructureNode extends JsonValueNode { } private getNewlineToken(formatting: Formatting): JsonTokenNode { - const newlineChar = formatting.newlineCharacter === 'CRLF' ? '\r\n' : '\n'; + const newlineChar = formatting.newLineCharacter === 'CRLF' ? '\r\n' : '\n'; return new JsonTokenNode({ type: JsonTokenType.NEWLINE, value: newlineChar, From 0a2863d4c167d5d8d7bd871dba66569c710c9c71 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Mon, 1 Dec 2025 10:42:03 -0300 Subject: [PATCH 4/4] Apply PR suggestions --- README.md | 2 +- src/formatting.ts | 2 +- src/node/structureNode.ts | 11 +++-------- test/functional.test.ts | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a50309e..3ec8f73 100644 --- a/README.md +++ b/README.md @@ -252,7 +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. | -| `newLineCharacter` | `'LF'\|'CRLF'` | Newline character to use in the document. | +| `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 3e83a89..ab75f65 100644 --- a/src/formatting.ts +++ b/src/formatting.ts @@ -11,7 +11,7 @@ export type BlockFormatting = { export type Formatting = { indentationLevel?: number, indentationCharacter?: 'space' | 'tab', - newLineCharacter?: 'LF' | 'CRLF', + lineEnding?: 'lf' | 'crlf', string?: { quote?: 'single' | 'double', }, diff --git a/src/node/structureNode.ts b/src/node/structureNode.ts index aa52447..c88515f 100644 --- a/src/node/structureNode.ts +++ b/src/node/structureNode.ts @@ -164,7 +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(this.getNewlineToken(formatting)); + manipulator.node(this.getNewlineToken(formatting)); if ( manipulator.matchesToken(JsonTokenType.WHITESPACE) @@ -364,11 +364,7 @@ export abstract class JsonStructureNode extends JsonValueNode { } if (NEWLINE(token)) { - if (token.value.includes('\r\n')) { - formatting.newLineCharacter = 'CRLF'; - } else { - formatting.newLineCharacter = 'LF'; - } + formatting.lineEnding = token.value.includes('\r\n') ? 'crlf' : 'lf'; } if (inlineComma && index > 0 && tokens[index - 1].depth === 0) { @@ -507,10 +503,9 @@ export abstract class JsonStructureNode extends JsonValueNode { } private getNewlineToken(formatting: Formatting): JsonTokenNode { - const newlineChar = formatting.newLineCharacter === 'CRLF' ? '\r\n' : '\n'; return new JsonTokenNode({ type: JsonTokenType.NEWLINE, - value: newlineChar, + value: formatting.lineEnding === 'crlf' ? '\r\n' : '\n', }); } diff --git a/test/functional.test.ts b/test/functional.test.ts index 39ec3a9..b9a18c2 100644 --- a/test/functional.test.ts +++ b/test/functional.test.ts @@ -3046,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);