From d1863b6956f8b9106e35b4681a06d8682fd5d3bf Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 30 Mar 2026 22:37:43 +0200 Subject: [PATCH] Add encoder formatting options: integerGrouping, trailingComma, dottedKeys New EncoderOptions: - integerGrouping: Format large integers with underscores (1_000_000) - trailingComma: Add trailing commas to arrays ([1, 2, 3,]) - dottedKeys: Use dotted key syntax instead of table headers (a.b.c = 1) All options default to false, preserving existing behavior. --- src/Encoder/Encoder.php | 28 ++++++- src/Encoder/EncoderOptions.php | 3 + tests/Encoder/EncoderTest.php | 129 +++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index 6c76e2c..530982b 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -88,7 +88,10 @@ private function encodeTable(array $data, array $path, array &$lines): void continue; } if (!is_array($value) || $this->isInlineArray($value)) { - $lines[] = $this->encodeKey((string)$key) . ' = ' . $this->encodeValue($value); + $keyPath = $this->options->dottedKeys && $path !== [] + ? $this->encodePath([...$path, (string)$key]) + : $this->encodeKey((string)$key); + $lines[] = $keyPath . ' = ' . $this->encodeValue($value); } } @@ -104,6 +107,8 @@ private function encodeTable(array $data, array $path, array &$lines): void $lines[] = '[[' . $this->encodePath($newPath) . ']]'; $this->encodeTable($item, $newPath, $lines); } + } elseif ($this->options->dottedKeys) { + $this->encodeTable($value, $newPath, $lines); } else { $lines[] = ''; $lines[] = '[' . $this->encodePath($newPath) . ']'; @@ -124,7 +129,7 @@ private function encodeValue(mixed $value): string } if (is_int($value)) { - return (string)$value; + return $this->encodeInteger($value); } if (is_float($value)) { @@ -1200,6 +1205,21 @@ private function endsWithNewline(string $value): bool return str_ends_with($value, "\n") || str_ends_with($value, "\r\n"); } + private function encodeInteger(int $value): string + { + if (!$this->options->integerGrouping) { + return (string)$value; + } + + $sign = $value < 0 ? '-' : ''; + $absolute = (string)abs($value); + + // Add underscores every 3 digits from the right + $grouped = preg_replace('/\B(?=(\d{3})+(?!\d))/', '_', $absolute); + + return $sign . $grouped; + } + private function encodeString(string $value): string { // Use basic string with escaping @@ -1222,7 +1242,9 @@ private function encodeArray(array $value): string } $items = array_map(fn ($v) => $this->encodeValue($v), $value); - return '[' . implode(', ', $items) . ']'; + $trailing = $this->options->trailingComma && $items !== [] ? ',' : ''; + + return '[' . implode(', ', $items) . $trailing . ']'; } /** diff --git a/src/Encoder/EncoderOptions.php b/src/Encoder/EncoderOptions.php index 5889a11..17d6d37 100644 --- a/src/Encoder/EncoderOptions.php +++ b/src/Encoder/EncoderOptions.php @@ -14,6 +14,9 @@ public function __construct( public DocumentFormattingMode $documentFormatting = DocumentFormattingMode::Normalized, public bool $skipNulls = false, public TomlVersion $version = TomlVersion::V11, + public bool $integerGrouping = false, + public bool $trailingComma = false, + public bool $dottedKeys = false, ) { } } diff --git a/tests/Encoder/EncoderTest.php b/tests/Encoder/EncoderTest.php index e7716de..4a7167e 100644 --- a/tests/Encoder/EncoderTest.php +++ b/tests/Encoder/EncoderTest.php @@ -321,4 +321,133 @@ public function testEncodeDocumentCanUseSourceAwareFormatting(): void $this->assertSame('count = 1' . "\n", $result); } + + public function testEncodeIntegerGrouping(): void + { + $encoder = new Encoder(new EncoderOptions(integerGrouping: true)); + + $result = $encoder->encode([ + 'small' => 42, + 'medium' => 1000, + 'large' => 1000000, + 'huge' => 1234567890, + 'negative' => -9876543, + ]); + + $this->assertStringContainsString('small = 42', $result); + $this->assertStringContainsString('medium = 1_000', $result); + $this->assertStringContainsString('large = 1_000_000', $result); + $this->assertStringContainsString('huge = 1_234_567_890', $result); + $this->assertStringContainsString('negative = -9_876_543', $result); + } + + public function testEncodeIntegerGroupingDisabledByDefault(): void + { + $encoder = new Encoder(new EncoderOptions()); + + $result = $encoder->encode([ + 'large' => 1000000, + ]); + + $this->assertStringContainsString('large = 1000000', $result); + } + + public function testEncodeTrailingComma(): void + { + $encoder = new Encoder(new EncoderOptions(trailingComma: true)); + + $result = $encoder->encode([ + 'items' => [1, 2, 3], + ]); + + $this->assertStringContainsString('items = [1, 2, 3,]', $result); + } + + public function testEncodeTrailingCommaEmptyArray(): void + { + $encoder = new Encoder(new EncoderOptions(trailingComma: true)); + + $result = $encoder->encode([ + 'empty' => [], + ]); + + $this->assertStringContainsString('empty = []', $result); + } + + public function testEncodeTrailingCommaDisabledByDefault(): void + { + $encoder = new Encoder(new EncoderOptions()); + + $result = $encoder->encode([ + 'items' => [1, 2, 3], + ]); + + $this->assertStringContainsString('items = [1, 2, 3]', $result); + $this->assertStringNotContainsString('[1, 2, 3,]', $result); + } + + public function testEncodeDottedKeys(): void + { + $encoder = new Encoder(new EncoderOptions(dottedKeys: true)); + + $result = $encoder->encode([ + 'database' => [ + 'host' => 'localhost', + 'port' => 5432, + ], + ]); + + $this->assertStringContainsString('database.host = "localhost"', $result); + $this->assertStringContainsString('database.port = 5432', $result); + $this->assertStringNotContainsString('[database]', $result); + } + + public function testEncodeDottedKeysDeepNesting(): void + { + $encoder = new Encoder(new EncoderOptions(dottedKeys: true)); + + $result = $encoder->encode([ + 'a' => [ + 'b' => [ + 'c' => 'value', + ], + ], + ]); + + $this->assertStringContainsString('a.b.c = "value"', $result); + $this->assertStringNotContainsString('[a]', $result); + $this->assertStringNotContainsString('[a.b]', $result); + } + + public function testEncodeDottedKeysWithArrayOfTables(): void + { + $encoder = new Encoder(new EncoderOptions(dottedKeys: true)); + + $result = $encoder->encode([ + 'servers' => [ + ['name' => 'alpha'], + ['name' => 'beta'], + ], + ]); + + // Array of tables still requires [[]] syntax + $this->assertStringContainsString('[[servers]]', $result); + $this->assertStringContainsString('name = "alpha"', $result); + $this->assertStringContainsString('name = "beta"', $result); + } + + public function testEncodeDottedKeysDisabledByDefault(): void + { + $encoder = new Encoder(new EncoderOptions()); + + $result = $encoder->encode([ + 'database' => [ + 'host' => 'localhost', + ], + ]); + + $this->assertStringContainsString('[database]', $result); + $this->assertStringContainsString('host = "localhost"', $result); + $this->assertStringNotContainsString('database.host', $result); + } }