From 606d725c112519a2a9c4f128f9811ac226f2e184 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 11 Sep 2024 11:49:35 +0200 Subject: [PATCH 01/10] [Yaml] Add support for dumping `null` as an empty value by using the `Yaml::DUMP_NULL_AS_EMPTY` flag --- CHANGELOG.md | 1 + Dumper.php | 25 ++++++++++++------- Inline.php | 10 +++++--- Tests/DumperTest.php | 57 ++++++++++++++++++++++++++++++++++++++++++++ Tests/ParserTest.php | 27 +++++++++++++++++++++ Yaml.php | 1 + 6 files changed, 110 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b23cd7..284c49ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Deprecate parsing duplicate mapping keys whose value is `null` + * Add support for dumping `null` as an empty value by using the `Yaml::DUMP_NULL_AS_EMPTY` flag 7.1 --- diff --git a/Dumper.php b/Dumper.php index f8ea205a..91b2ec29 100644 --- a/Dumper.php +++ b/Dumper.php @@ -41,6 +41,15 @@ public function __construct(private int $indentation = 4) * @param int-mask-of $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string */ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags = 0): string + { + if ($flags & Yaml::DUMP_NULL_AS_EMPTY && $flags & Yaml::DUMP_NULL_AS_TILDE) { + throw new \InvalidArgumentException('The Yaml::DUMP_NULL_AS_EMPTY and Yaml::DUMP_NULL_AS_TILDE flags cannot be used together.'); + } + + return $this->doDump($input, $inline, $indent, $flags); + } + + private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $flags = 0, int $nestingLevel = 0): string { $output = ''; $prefix = $indent ? str_repeat(' ', $indent) : ''; @@ -51,9 +60,9 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags } if ($inline <= 0 || (!\is_array($input) && !$input instanceof TaggedValue && $dumpObjectAsInlineMap) || !$input) { - $output .= $prefix.Inline::dump($input, $flags); + $output .= $prefix.Inline::dump($input, $flags, 0 === $nestingLevel); } elseif ($input instanceof TaggedValue) { - $output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix); + $output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix, $nestingLevel); } else { $dumpAsMap = Inline::isHash($input); @@ -105,10 +114,10 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags } if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) { - $output .= ' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n"; + $output .= ' '.$this->doDump($value->getValue(), $inline - 1, 0, $flags, $nestingLevel + 1)."\n"; } else { $output .= "\n"; - $output .= $this->dump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags); + $output .= $this->doDump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags, $nestingLevel + 1); } continue; @@ -126,7 +135,7 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $willBeInlined ? ' ' : "\n", - $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags) + $this->doDump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags, $nestingLevel + 1) ).($willBeInlined ? "\n" : ''); } } @@ -134,7 +143,7 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags return $output; } - private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix): string + private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix, int $nestingLevel): string { $output = \sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag()); @@ -150,10 +159,10 @@ private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, i } if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) { - return $output.' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n"; + return $output.' '.$this->doDump($value->getValue(), $inline - 1, 0, $flags, $nestingLevel + 1)."\n"; } - return $output."\n".$this->dump($value->getValue(), $inline - 1, $indent, $flags); + return $output."\n".$this->doDump($value->getValue(), $inline - 1, $indent, $flags, $nestingLevel + 1); } private function getBlockIndentationIndicator(string $value): string diff --git a/Inline.php b/Inline.php index 34ef66e6..c310fe12 100644 --- a/Inline.php +++ b/Inline.php @@ -100,7 +100,7 @@ public static function parse(string $value, int $flags = 0, array &$references = * * @throws DumpException When trying to dump PHP resource */ - public static function dump(mixed $value, int $flags = 0): string + public static function dump(mixed $value, int $flags = 0, bool $rootLevel = false): string { switch (true) { case \is_resource($value): @@ -138,7 +138,7 @@ public static function dump(mixed $value, int $flags = 0): string case \is_array($value): return self::dumpArray($value, $flags); case null === $value: - return self::dumpNull($flags); + return self::dumpNull($flags, $rootLevel); case true === $value: return 'true'; case false === $value: @@ -253,12 +253,16 @@ private static function dumpHashArray(array|\ArrayObject|\stdClass $value, int $ return \sprintf('{ %s }', implode(', ', $output)); } - private static function dumpNull(int $flags): string + private static function dumpNull(int $flags, bool $rootLevel = false): string { if (Yaml::DUMP_NULL_AS_TILDE & $flags) { return '~'; } + if (Yaml::DUMP_NULL_AS_EMPTY & $flags && !$rootLevel) { + return ''; + } + return 'null'; } diff --git a/Tests/DumperTest.php b/Tests/DumperTest.php index 24758b81..bb3ba62f 100644 --- a/Tests/DumperTest.php +++ b/Tests/DumperTest.php @@ -216,6 +216,63 @@ public function testObjectSupportDisabledWithExceptions() $this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE); } + public function testDumpWithMultipleNullFlagsFormatsThrows() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The Yaml::DUMP_NULL_AS_EMPTY and Yaml::DUMP_NULL_AS_TILDE flags cannot be used together.'); + + $this->dumper->dump(['foo' => 'bar'], 0, 0, Yaml::DUMP_NULL_AS_EMPTY | Yaml::DUMP_NULL_AS_TILDE); + } + + public function testDumpNullAsEmptyInExpandedMapping() + { + $expected = "qux:\n foo: bar\n baz: \n"; + + $this->assertSame($expected, $this->dumper->dump(['qux' => ['foo' => 'bar', 'baz' => null]], 2, flags: Yaml::DUMP_NULL_AS_EMPTY)); + } + + public function testDumpNullAsEmptyWithObject() + { + $class = new \stdClass(); + $class->foo = 'bar'; + $class->baz = null; + + $this->assertSame("foo: bar\nbaz: \n", $this->dumper->dump($class, 2, flags: Yaml::DUMP_NULL_AS_EMPTY | Yaml::DUMP_OBJECT_AS_MAP)); + } + + public function testDumpNullAsEmptyDumpsWhenInInlineMapping() + { + $expected = "foo: \nqux: { foo: bar, baz: }\n"; + + $this->assertSame($expected, $this->dumper->dump(['foo' => null, 'qux' => ['foo' => 'bar', 'baz' => null]], 1, flags: Yaml::DUMP_NULL_AS_EMPTY)); + } + + public function testDumpNullAsEmptyDumpsNestedMaps() + { + $expected = "foo: \nqux:\n foo: bar\n baz: \n"; + + $this->assertSame($expected, $this->dumper->dump(['foo' => null, 'qux' => ['foo' => 'bar', 'baz' => null]], 10, flags: Yaml::DUMP_NULL_AS_EMPTY)); + } + + public function testDumpNullAsEmptyInExpandedSequence() + { + $expected = "qux:\n - foo\n - \n - bar\n"; + + $this->assertSame($expected, $this->dumper->dump(['qux' => ['foo', null, 'bar']], 2, flags: Yaml::DUMP_NULL_AS_EMPTY)); + } + + public function testDumpNullAsEmptyWhenInInlineSequence() + { + $expected = "foo: \nqux: [foo, , bar]\n"; + + $this->assertSame($expected, $this->dumper->dump(['foo' => null, 'qux' => ['foo', null, 'bar']], 1, flags: Yaml::DUMP_NULL_AS_EMPTY)); + } + + public function testDumpNullAsEmptyAtRoot() + { + $this->assertSame('null', $this->dumper->dump(null, 2, flags: Yaml::DUMP_NULL_AS_EMPTY)); + } + /** * @dataProvider getEscapeSequences */ diff --git a/Tests/ParserTest.php b/Tests/ParserTest.php index fad94694..3850f041 100644 --- a/Tests/ParserTest.php +++ b/Tests/ParserTest.php @@ -52,6 +52,33 @@ public function testTopLevelNull() $this->assertSameData($expected, $data); } + public function testEmptyValueInExpandedMappingIsSupported() + { + $yml = <<<'YAML' +foo: + bar: + baz: qux +YAML; + + $data = $this->parser->parse($yml); + $expected = ['foo' => ['bar' => null, 'baz' => 'qux']]; + $this->assertSameData($expected, $data); + } + + public function testEmptyValueInExpandedSequenceIsSupported() + { + $yml = <<<'YAML' +foo: + - bar + - + - baz +YAML; + + $data = $this->parser->parse($yml); + $expected = ['foo' => ['bar', null, 'baz']]; + $this->assertSameData($expected, $data); + } + public function testTaggedValueTopLevelNumber() { $yml = '!number 5'; diff --git a/Yaml.php b/Yaml.php index 36b45198..03395b97 100644 --- a/Yaml.php +++ b/Yaml.php @@ -35,6 +35,7 @@ class Yaml public const DUMP_EMPTY_ARRAY_AS_SEQUENCE = 1024; public const DUMP_NULL_AS_TILDE = 2048; public const DUMP_NUMERIC_KEY_AS_STRING = 4096; + public const DUMP_NULL_AS_EMPTY = 8192; /** * Parses a YAML file into a PHP value. From 254b973833997a8aa93f1ba87fd1f4ed6e968278 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 02/10] chore: PHP CS Fixer fixes --- Escaper.php | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Escaper.php b/Escaper.php index e42034aa..8cc492c5 100644 --- a/Escaper.php +++ b/Escaper.php @@ -28,22 +28,24 @@ class Escaper // first to ensure proper escaping because str_replace operates iteratively // on the input arrays. This ordering of the characters avoids the use of strtr, // which performs more slowly. - private const ESCAPEES = ['\\', '\\\\', '\\"', '"', - "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", - "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", - "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", - "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", - "\x7f", - "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9", - ]; - private const ESCAPED = ['\\\\', '\\"', '\\\\', '\\"', - '\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a', - '\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f', - '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17', - '\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f', - '\\x7f', - '\\N', '\\_', '\\L', '\\P', - ]; + private const ESCAPEES = [ + '\\', '\\\\', '\\"', '"', + "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", + "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", + "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", + "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", + "\x7f", + "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9", + ]; + private const ESCAPED = [ + '\\\\', '\\"', '\\\\', '\\"', + '\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a', + '\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f', + '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17', + '\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f', + '\\x7f', + '\\N', '\\_', '\\L', '\\P', + ]; /** * Determines if a PHP value would require double quoting in YAML. From 7a9c072bdb56cd0c3e95993f67abc3dc923dc267 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 13 Dec 2024 22:36:21 +0100 Subject: [PATCH 03/10] chore: PHP CS Fixer fixes --- Tests/ParserTest.php | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Tests/ParserTest.php b/Tests/ParserTest.php index fad94694..85160b82 100644 --- a/Tests/ParserTest.php +++ b/Tests/ParserTest.php @@ -1155,7 +1155,7 @@ public function testNestedFoldedStringBlockWithComments() footer # comment3 -EOT +EOT, ]], Yaml::parse(<<<'EOF' - title: some title @@ -1495,13 +1495,13 @@ public static function getBinaryData() <<<'EOT' data: !!binary | SGVsbG8gd29ybGQ= -EOT +EOT, ], 'containing spaces in block scalar' => [ <<<'EOT' data: !!binary | SGVs bG8gd 29ybGQ= -EOT +EOT, ], ]; } @@ -1602,7 +1602,7 @@ public static function parserThrowsExceptionWithCorrectLineNumberProvider() - # bar bar: "123", -YAML +YAML, ], [ 5, @@ -1612,7 +1612,7 @@ public static function parserThrowsExceptionWithCorrectLineNumberProvider() # bar # bar bar: "123", -YAML +YAML, ], [ 8, @@ -1625,7 +1625,7 @@ public static function parserThrowsExceptionWithCorrectLineNumberProvider() - # bar bar: "123", -YAML +YAML, ], [ 10, @@ -1640,7 +1640,7 @@ public static function parserThrowsExceptionWithCorrectLineNumberProvider() # bar # bar bar: "123", -YAML +YAML, ], ]; } @@ -1940,7 +1940,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array << [ [ @@ -1952,7 +1952,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array ['entry1', {}], ['entry2'] ] -YAML +YAML, ], 'sequence nested in mapping' => [ ['foo' => ['bar', 'foobar'], 'bar' => ['baz']], @@ -1994,7 +1994,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array bar, ] bar: baz -YAML +YAML, ], 'nested sequence nested in mapping starting on the same line' => [ [ @@ -2121,7 +2121,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array foo: 'bar baz' -YAML +YAML, ], 'mixed mapping with inline notation having separated lines' => [ [ @@ -2137,7 +2137,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array a: "b" } param: "some" -YAML +YAML, ], 'mixed mapping with inline notation on one line' => [ [ @@ -2150,7 +2150,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array << [ [ @@ -2164,7 +2164,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array map: {key: "value", a: "b"} param: "some" -YAML +YAML, ], 'nested collections containing strings with bracket chars' => [ [ @@ -2204,7 +2204,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array foo: 'bar}' } ] -YAML +YAML, ], 'escaped characters in quoted strings' => [ [ @@ -2225,7 +2225,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array ['te''st'], ["te\"st]"] ] -YAML +YAML, ], ]; } @@ -2276,7 +2276,7 @@ public static function taggedValuesProvider() quz: !long > this is a long text -YAML +YAML, ], 'sequences' => [ [new TaggedValue('foo', ['yaml']), new TaggedValue('quz', ['bar'])], @@ -2284,7 +2284,7 @@ public static function taggedValuesProvider() - !foo - yaml - !quz [bar] -YAML +YAML, ], 'mappings' => [ new TaggedValue('foo', ['foo' => new TaggedValue('quz', ['bar']), 'quz' => new TaggedValue('foo', ['quz' => 'bar'])]), @@ -2293,14 +2293,14 @@ public static function taggedValuesProvider() foo: !quz [bar] quz: !foo quz: bar -YAML +YAML, ], 'inline' => [ [new TaggedValue('foo', ['foo', 'bar']), new TaggedValue('quz', ['foo' => 'bar', 'quz' => new TaggedValue('bar', ['one' => 'bar'])])], << [ [new TaggedValue('foo', 'bar')], @@ -2316,7 +2316,7 @@ public static function taggedValuesProvider() baz #bar ]] -YAML +YAML, ], 'with-comments-trailing-comma' => [ [ @@ -2328,7 +2328,7 @@ public static function taggedValuesProvider() baz, #bar ]] -YAML +YAML, ], ]; } From b5176af9060131a2305e37220c8b5dec50b43b58 Mon Sep 17 00:00:00 2001 From: gr8b Date: Sun, 29 Dec 2024 01:39:39 +0200 Subject: [PATCH 04/10] [Yaml] Add compact nested mapping support to `Dumper` --- CHANGELOG.md | 5 +++ Dumper.php | 5 ++- Tests/DumperTest.php | 91 ++++++++++++++++++++++++++++++++++++++++++++ Yaml.php | 1 + 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 284c49ed..b1e9decb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add compact nested mapping support by using the `Yaml::DUMP_COMPACT_NESTED_MAPPING` flag + 7.2 --- diff --git a/Dumper.php b/Dumper.php index 91b2ec29..13f21442 100644 --- a/Dumper.php +++ b/Dumper.php @@ -65,6 +65,7 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla $output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix, $nestingLevel); } else { $dumpAsMap = Inline::isHash($input); + $compactNestedMapping = Yaml::DUMP_COMPACT_NESTED_MAPPING & $flags && !$dumpAsMap; foreach ($input as $key => $value) { if ('' !== $output && "\n" !== $output[-1]) { @@ -134,8 +135,8 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla $output .= \sprintf('%s%s%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', - $willBeInlined ? ' ' : "\n", - $this->doDump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags, $nestingLevel + 1) + $willBeInlined || ($compactNestedMapping && \is_array($value)) ? ' ' : "\n", + $compactNestedMapping && \is_array($value) ? substr($this->doDump($value, $inline - 1, $indent + 2, $flags, $nestingLevel + 1), $indent + 2) : $this->doDump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags, $nestingLevel + 1) ).($willBeInlined ? "\n" : ''); } } diff --git a/Tests/DumperTest.php b/Tests/DumperTest.php index bb3ba62f..0128fa4c 100644 --- a/Tests/DumperTest.php +++ b/Tests/DumperTest.php @@ -1061,6 +1061,97 @@ public static function getDateTimeData() ]; } + public static function getDumpCompactNestedMapping() + { + $data = [ + 'planets' => [ + [ + 'name' => 'Mercury', + 'distance' => 57910000, + 'properties' => [ + ['name' => 'size', 'value' => 4879], + ['name' => 'moons', 'value' => 0], + [[[]]], + ], + ], + [ + 'name' => 'Jupiter', + 'distance' => 778500000, + 'properties' => [ + ['name' => 'size', 'value' => 139820], + ['name' => 'moons', 'value' => 79], + [[]], + ], + ], + ], + ]; + $expected = << [ + $data, + strtr($expected, ["\t" => str_repeat(' ', $indentation)]), + $indentation, + ]; + } + + $indentation = 2; + $inline = 4; + $expected = << [ + $data, + $expected, + $indentation, + $inline, + ]; + } + + /** + * @dataProvider getDumpCompactNestedMapping + */ + public function testDumpCompactNestedMapping(array $data, string $expected, int $indentation, int $inline = 10) + { + $dumper = new Dumper($indentation); + $actual = $dumper->dump($data, $inline, 0, Yaml::DUMP_COMPACT_NESTED_MAPPING); + $this->assertSame($expected, $actual); + $this->assertSameData($data, $this->parser->parse($actual)); + } + private function assertSameData($expected, $actual) { $this->assertEquals($expected, $actual); diff --git a/Yaml.php b/Yaml.php index 03395b97..0a156ae2 100644 --- a/Yaml.php +++ b/Yaml.php @@ -36,6 +36,7 @@ class Yaml public const DUMP_NULL_AS_TILDE = 2048; public const DUMP_NUMERIC_KEY_AS_STRING = 4096; public const DUMP_NULL_AS_EMPTY = 8192; + public const DUMP_COMPACT_NESTED_MAPPING = 16384; /** * Parses a YAML file into a PHP value. From 8e7138531d7a7c1917088b10cc4148cf5f34aca4 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 10 Jan 2025 15:17:09 +0100 Subject: [PATCH 05/10] chore: PHP CS Fixer fixes --- Tests/ParserTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ParserTest.php b/Tests/ParserTest.php index 61ce749d..37cc4c70 100644 --- a/Tests/ParserTest.php +++ b/Tests/ParserTest.php @@ -1771,14 +1771,14 @@ public static function wrappedUnquotedStringsProvider() [ 'foo' => 'bar bar', 'fiz' => 'cat cat', - ] + ], ], 'sequence' => [ '[ bar bar, cat cat ]', [ 'bar bar', 'cat cat', - ] + ], ], ]; } @@ -2218,7 +2218,7 @@ public static function inlineNotationSpanningMultipleLinesProvider(): array << [ [ From 5fbfc829a31080bc572aa559dce19ea9bdc3a971 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 18 Jan 2025 17:45:23 +0100 Subject: [PATCH 06/10] chore: PHP CS Fixer fixes --- Parser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Parser.php b/Parser.php index 266b89e8..d951e895 100644 --- a/Parser.php +++ b/Parser.php @@ -1213,8 +1213,8 @@ private function lexInlineStructure(int &$cursor, string $closingTag, bool $cons $value .= $this->currentLine[$cursor]; ++$cursor; - if ($consumeUntilEol && isset($this->currentLine[$cursor]) && ($whitespaces = strspn($this->currentLine, ' ', $cursor) + $cursor) < strlen($this->currentLine) && '#' !== $this->currentLine[$whitespaces]) { - throw new ParseException(sprintf('Unexpected token "%s".', trim(substr($this->currentLine, $cursor)))); + if ($consumeUntilEol && isset($this->currentLine[$cursor]) && ($whitespaces = strspn($this->currentLine, ' ', $cursor) + $cursor) < \strlen($this->currentLine) && '#' !== $this->currentLine[$whitespaces]) { + throw new ParseException(\sprintf('Unexpected token "%s".', trim(substr($this->currentLine, $cursor)))); } return $value; From 669059babfa6524a9fe5432fbb8534e603cca325 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 5 Jan 2025 22:01:08 +0100 Subject: [PATCH 07/10] [Yaml] Yaml::DUMP_COMPACT_NESTED_MAPPING does not apply when dumping sequences --- Dumper.php | 4 +- Tests/DumperTest.php | 150 +++++++++++++++++++++++++++++++++---------- 2 files changed, 117 insertions(+), 37 deletions(-) diff --git a/Dumper.php b/Dumper.php index 13f21442..cd5a1f6c 100644 --- a/Dumper.php +++ b/Dumper.php @@ -135,8 +135,8 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla $output .= \sprintf('%s%s%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', - $willBeInlined || ($compactNestedMapping && \is_array($value)) ? ' ' : "\n", - $compactNestedMapping && \is_array($value) ? substr($this->doDump($value, $inline - 1, $indent + 2, $flags, $nestingLevel + 1), $indent + 2) : $this->doDump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags, $nestingLevel + 1) + $willBeInlined || ($compactNestedMapping && \is_array($value) && Inline::isHash($value)) ? ' ' : "\n", + $compactNestedMapping && \is_array($value) && Inline::isHash($value) ? substr($this->doDump($value, $inline - 1, $indent + 2, $flags, $nestingLevel + 1), $indent + 2) : $this->doDump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags, $nestingLevel + 1) ).($willBeInlined ? "\n" : ''); } } diff --git a/Tests/DumperTest.php b/Tests/DumperTest.php index 0128fa4c..fe07e10a 100644 --- a/Tests/DumperTest.php +++ b/Tests/DumperTest.php @@ -1085,38 +1085,122 @@ public static function getDumpCompactNestedMapping() ], ], ]; - $expected = << [ + $data, + << [ - $data, - strtr($expected, ["\t" => str_repeat(' ', $indentation)]), - $indentation, - ]; - } + yield 'Compact nested mapping 2' => [ + $data, + << [ + $data, + << [ + $data, + << [ + $data, + << [ - $data, - $expected, - $indentation, - $inline, +YAML, + 2, + 4, ]; } From a2f18c22d630be5b63299c8339fc116b2f0fae34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Karlovi=C4=87?= Date: Fri, 28 Feb 2025 13:32:37 +0100 Subject: [PATCH 08/10] [Yaml] Add the `Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES` flag to enforce double quotes around string values --- CHANGELOG.md | 1 + Inline.php | 4 ++- Tests/DumperTest.php | 81 ++++++++++++++++++++++++++++++++++++++++++++ Yaml.php | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e9decb..364bf66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add compact nested mapping support by using the `Yaml::DUMP_COMPACT_NESTED_MAPPING` flag + * Add the `Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES` flag to enforce double quotes around string values 7.2 --- diff --git a/Inline.php b/Inline.php index c310fe12..bfa910c7 100644 --- a/Inline.php +++ b/Inline.php @@ -173,6 +173,7 @@ public static function dump(mixed $value, int $flags = 0, bool $rootLevel = fals case self::isBinaryString($value): return '!!binary '.base64_encode($value); case Escaper::requiresDoubleQuoting($value): + case Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES & $flags: return Escaper::escapeWithDoubleQuotes($value); case Escaper::requiresSingleQuoting($value): $singleQuoted = Escaper::escapeWithSingleQuotes($value); @@ -242,12 +243,13 @@ private static function dumpArray(array $value, int $flags): string private static function dumpHashArray(array|\ArrayObject|\stdClass $value, int $flags): string { $output = []; + $keyFlags = $flags &~ Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES; foreach ($value as $key => $val) { if (\is_int($key) && Yaml::DUMP_NUMERIC_KEY_AS_STRING & $flags) { $key = (string) $key; } - $output[] = \sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); + $output[] = \sprintf('%s: %s', self::dump($key, $keyFlags), self::dump($val, $flags)); } return \sprintf('{ %s }', implode(', ', $output)); diff --git a/Tests/DumperTest.php b/Tests/DumperTest.php index fe07e10a..cb163b67 100644 --- a/Tests/DumperTest.php +++ b/Tests/DumperTest.php @@ -910,6 +910,87 @@ public function testDumpNullAsTilde() $this->assertSame('{ foo: ~ }', $this->dumper->dump(['foo' => null], 0, 0, Yaml::DUMP_NULL_AS_TILDE)); } + /** + * @dataProvider getForceQuotesOnValuesData + */ + public function testCanForceQuotesOnValues(array $input, string $expected) + { + $this->assertSame($expected, $this->dumper->dump($input, 0, 0, Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES)); + } + + public function getForceQuotesOnValuesData(): iterable + { + yield 'empty string' => [ + ['foo' => ''], + '{ foo: \'\' }', + ]; + + yield 'double quote' => [ + ['foo' => '"'], + '{ foo: "\"" }', + ]; + + yield 'single quote' => [ + ['foo' => "'"], + '{ foo: "\'" }', + ]; + + yield 'line break' => [ + ['foo' => "line\nbreak"], + '{ foo: "line\nbreak" }', + ]; + + yield 'tab character' => [ + ['foo' => "tab\tcharacter"], + '{ foo: "tab\tcharacter" }', + ]; + + yield 'backslash' => [ + ['foo' => "back\\slash"], + '{ foo: "back\\\\slash" }', + ]; + + yield 'colon' => [ + ['foo' => 'colon: value'], + '{ foo: "colon: value" }', + ]; + + yield 'dash' => [ + ['foo' => '- dash'], + '{ foo: "- dash" }', + ]; + + yield 'numeric' => [ + ['foo' => 23], + '{ foo: 23 }', + ]; + + yield 'boolean' => [ + ['foo' => true], + '{ foo: true }', + ]; + + yield 'null' => [ + ['foo' => null], + '{ foo: null }', + ]; + + yield 'nested' => [ + ['foo' => ['bar' => 'bat', 'baz' => 23]], + '{ foo: { bar: "bat", baz: 23 } }', + ]; + + yield 'mix of values' => [ + ['foo' => 'bat', 'bar' => 23, 'baz' => true, 'qux' => "line\nbreak"], + '{ foo: "bat", bar: 23, baz: true, qux: "line\nbreak" }', + ]; + + yield 'special YAML characters' => [ + ['foo' => 'colon: value', 'bar' => '- dash', 'baz' => '? question', 'qux' => '# hash'], + '{ foo: "colon: value", bar: "- dash", baz: "? question", qux: "# hash" }', + ]; + } + /** * @dataProvider getNumericKeyData */ diff --git a/Yaml.php b/Yaml.php index 0a156ae2..57625a53 100644 --- a/Yaml.php +++ b/Yaml.php @@ -37,6 +37,7 @@ class Yaml public const DUMP_NUMERIC_KEY_AS_STRING = 4096; public const DUMP_NULL_AS_EMPTY = 8192; public const DUMP_COMPACT_NESTED_MAPPING = 16384; + public const DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES = 32768; /** * Parses a YAML file into a PHP value. From e359788bfa955e78737b475e794bbf5484e6a98d Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 8 Mar 2025 00:16:33 +0100 Subject: [PATCH 09/10] chore: PHP CS Fixer fixes --- Parser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Parser.php b/Parser.php index e80ff299..be589082 100644 --- a/Parser.php +++ b/Parser.php @@ -1166,8 +1166,8 @@ private function lexUnquotedString(int &$cursor): string { $offset = $cursor; - while ($cursor < strlen($this->currentLine)) { - if (in_array($this->currentLine[$cursor], ['[', ']', '{', '}', ',', ':'], true)) { + while ($cursor < \strlen($this->currentLine)) { + if (\in_array($this->currentLine[$cursor], ['[', ']', '{', '}', ',', ':'], true)) { break; } From feaf385cbf558a9c0da157e212cf039ba5581a6e Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 4 Apr 2025 11:23:34 +0200 Subject: [PATCH 10/10] make data provider static --- Tests/DumperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DumperTest.php b/Tests/DumperTest.php index cb163b67..e937336c 100644 --- a/Tests/DumperTest.php +++ b/Tests/DumperTest.php @@ -918,7 +918,7 @@ public function testCanForceQuotesOnValues(array $input, string $expected) $this->assertSame($expected, $this->dumper->dump($input, 0, 0, Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES)); } - public function getForceQuotesOnValuesData(): iterable + public static function getForceQuotesOnValuesData(): iterable { yield 'empty string' => [ ['foo' => ''],