diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 1109f8568dd52..d7f146e3f9b6a 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -597,8 +597,12 @@ private function getNextEmbedBlock(?int $indentation = null, bool $inSequence = } $data = []; + $isInMultiLineQuote = false; if ($this->getCurrentLineIndentation() >= $newIndent) { + if ($this->isCurrentLineMultiLineQuoteStart()) { + $isInMultiLineQuote = true; + } $data[] = substr($this->currentLine, $newIndent ?? 0); } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) { $data[] = $this->currentLine; @@ -635,6 +639,16 @@ private function getNextEmbedBlock(?int $indentation = null, bool $inSequence = if ($this->isCurrentLineBlank()) { $data[] = substr($this->currentLine, $newIndent); continue; + } elseif (!$isInMultiLineQuote && $this->isCurrentLineMultiLineQuoteStart()) { + $isInMultiLineQuote = true; + $data[] = substr($this->currentLine, $newIndent); + continue; + } elseif ($isInMultiLineQuote) { + $data[] = $this->currentLine; + if ("'" === (rtrim($this->currentLine)[-1] ?? '')) { + $isInMultiLineQuote = false; + } + continue; } if ($indent >= $newIndent) { @@ -965,6 +979,49 @@ private function isCurrentLineLastLineInDocument(): bool return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1); } + /** + * Returns true if the current line is the beginning of a multiline quoted block. + */ + private function isCurrentLineMultiLineQuoteStart(): bool + { + $trimmedLine = trim($this->currentLine); + $trimmedLineLength = \strlen($trimmedLine); + $quoteCount = 0; + $value = ''; + // check if the key is quoted + for ($i = 0; $i < $trimmedLineLength; ++$i) { + $char = $trimmedLine[$i]; + if ("'" === $char) { + ++$quoteCount; + } elseif (':' === $char && 0 === $quoteCount % 2 && ($i === $trimmedLineLength - 1 || ' ' === $trimmedLine[$i + 1])) { + // key and value are separated by the first colon after the (quoted) key followed by a space or linebreak + $value = trim(substr($trimmedLine, ++$i), ' '); + break; + } + } + + if (0 !== strpos($value, "'")) { + return false; + } + + $lineEndQuoteCount = 0; + for ($i = \strlen($value) - 1; $i > 0; --$i) { + $char = $value[$i]; + if ("'" === $char) { + ++$lineEndQuoteCount; + } else { + break; + } + } + + return 0 === $lineEndQuoteCount % 2; + } + + /** + * Cleanups a YAML string to be parsed. + * + * @param string $value The input YAML string + */ private function cleanup(string $value): string { $value = str_replace(["\r\n", "\r"], "\n", $value); diff --git a/src/Symfony/Component/Yaml/Tests/YamlTest.php b/src/Symfony/Component/Yaml/Tests/YamlTest.php index 151b5b9deb824..e057703afefd8 100644 --- a/src/Symfony/Component/Yaml/Tests/YamlTest.php +++ b/src/Symfony/Component/Yaml/Tests/YamlTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Yaml\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; class YamlTest extends TestCase @@ -24,6 +25,56 @@ public function testParseAndDump() $this->assertEquals($data, $parsed); } + public function testParseWithMultilineQuotes() + { + $yaml = <<assertSame(['foo' => [ + 'bar' => "baz biz\n", + 'baz' => "Lorem\nipsum", + 'error' => "Une erreur s'est produite.", + 'trialMode' => "période d'essai", + 'double_line' => "Les utilisateurs sélectionnés n'ont pas d'email.\n", + 'a' => "b' c", + 'empty' => '', + 'foo:bar' => 'foobar', + ]], Yaml::parse($yaml)); + } + + public function testParseWithMultilineQuotesExpectException() + { + $yaml = <<expectException(ParseException::class); + $this->expectExceptionMessage('Unable to parse at line 5 (near "\'").'); + Yaml::parse($yaml); + } + public function testZeroIndentationThrowsException() { $this->expectException(\InvalidArgumentException::class);