From 5cef5137b26eabffda9169248267a900197feefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 17:51:53 +0200 Subject: [PATCH 01/10] Export trailing snippet output comments --- lib/runner.php | 52 +++++++++++++++++++ tests/phpunit/tests/export/docblocks.inc | 5 +- tests/phpunit/tests/export/docblocks.php | 63 ++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 70e77d9..89a7c68 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -996,6 +996,11 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence 'type' => 'php-code-snippet', 'code' => $fences[ $i ]['code'], ); + $output_comment = extract_docblock_php_snippet_output_comment( $snippet['code'] ); + $snippet['code'] = $output_comment['code']; + if ( null !== $output_comment['expected_output'] ) { + $snippet['expected_output'] = $output_comment['expected_output']; + } if ( null !== $fences[ $i ]['referenced_setup'] ) { $snippet['blueprint'] = $fences[ $i ]['referenced_setup']; @@ -1031,6 +1036,13 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence if ( $fences[ $j ]['is_expected_output'] ) { // First expected-output fence ends the run, so a snippet takes one. + if ( array_key_exists( 'expected_output', $snippet ) ) { + throw new \InvalidArgumentException( + 'Interactive PHP fence on line ' . ( $fences[ $i ]['start'] + 1 ) . + ' of the long description declares output both in code and in an expected-output fence.' + ); + } + $snippet['expected_output'] = $fences[ $j ]['code']; $consumed_fences[ $j ] = true; break; @@ -1085,6 +1097,46 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence return $snippets; } +/** + * Extracts a trailing output comment from an interactive PHP snippet. + * + * The output value uses JSON-string syntax so punctuation, whitespace, and + * escaped newlines retain their exact value in the exported JSON. + * + * @param string $code Snippet code extracted from a DocBlock fence. + * + * @throws \InvalidArgumentException When an Outputs comment is not a JSON string. + * + * @return array{code: string, expected_output: string|null} Runnable code and its optional output. + */ +function extract_docblock_php_snippet_output_comment( $code ) { + $lines = explode( "\n", $code ); + $last_line = count( $lines ) - 1; + $comment = $lines[ $last_line ]; + $has_output = preg_match( '/^[ \t]*\/\/ Outputs:(.*)$/', $comment, $matches ); + + if ( ! $has_output ) { + return array( + 'code' => $code, + 'expected_output' => null, + ); + } + + $output = json_decode( trim( $matches[1] ), true ); + if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $output ) ) { + throw new \InvalidArgumentException( + 'The trailing Outputs comment must contain one JSON string.' + ); + } + + array_pop( $lines ); + + return array( + 'code' => rtrim( implode( "\n", $lines ), "\n" ), + 'expected_output' => $output, + ); +} + /** * Checks whether two fences are separated only by blank DocBlock lines. * diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index 2762463..c5b6251 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -107,10 +107,7 @@ class Test_Class { * " );', + '$p->next_tag();', + 'foreach ( $p->class_list() as $class_name ) {', + ' echo "{$class_name} ";', + '}', + '// Outputs: "free lang-en "', + '```', + ) + ) + ); + + $this->assertSame( + array( + array( + 'type' => 'php-code-snippet', + 'code' => "\$p = WP_HTML_Processor::create_fragment( \"
\" );\n" . + "\$p->next_tag();\n" . + "foreach ( \$p->class_list() as \$class_name ) {\n" . + " echo \"{\$class_name} \";\n" . + '}', + 'expected_output' => 'free lang-en ', + ), + ), + $snippets + ); + } + + /** + * Test that a trailing Outputs comment must use JSON-string syntax. + */ + public function test_code_snippet_output_comment_requires_json_string() { + + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'The trailing Outputs comment must contain one JSON string.' ); + + \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'one';\n// Outputs: one\n```" + ); + } + + /** + * Test that code-comment output cannot conflict with an expected-output fence. + */ + public function test_code_snippet_output_comment_and_fence_cannot_both_define_output() { + + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'declares output both in code and in an expected-output fence' ); + + \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'one';\n// Outputs: \"one\"\n```\n```expected-output\none\n```" + ); + } + /** * Test that unsupported info strings remain ordinary documentation. * From 77ed4116b5854593caa2be5da79112e9cc3cc5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 18:12:17 +0200 Subject: [PATCH 02/10] Cover escaped snippet output comments --- tests/phpunit/tests/export/docblocks.php | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index 11a7afc..411d4ba 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -547,6 +547,70 @@ public function test_code_snippet_output_comment() { ); } + /** + * Test that JSON escapes in a trailing Outputs comment retain their output value. + * + * @dataProvider trailing_code_snippet_output_comments + */ + public function test_trailing_code_snippet_output_comments( $comment, $expected_output ) { + + $snippets = \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'example';\n" . $comment . "\n```" + ); + + $this->assertSame( + array( + array( + 'type' => 'php-code-snippet', + 'code' => "echo 'example';", + 'expected_output' => $expected_output, + ), + ), + $snippets + ); + } + + /** + * Returns JSON-string output comments with escaping that must survive export. + */ + public function trailing_code_snippet_output_comments() { + + return array( + 'multiline output with a trailing newline' => array( + '// Outputs: "first\\nsecond\\n"', + "first\nsecond\n", + ), + 'escaped quotes and tabs' => array( + '// Outputs: "A \\"quote\\" and a \\t tab"', + "A \"quote\" and a \t tab", + ), + 'whitespace after the JSON string is not output' => array( + '// Outputs: "done" ', + 'done', + ), + ); + } + + /** + * Test that an Outputs comment before further code remains part of the snippet. + */ + public function test_non_trailing_code_snippet_output_comment_remains_code() { + + $snippets = \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'before';\n// Outputs: \"before\"\necho 'after';\n```" + ); + + $this->assertSame( + array( + array( + 'type' => 'php-code-snippet', + 'code' => "echo 'before';\n// Outputs: \"before\"\necho 'after';", + ), + ), + $snippets + ); + } + /** * Test that a trailing Outputs comment must use JSON-string syntax. */ From 4eb31d226eefbe96f4fc3602e7f9f050fe4472d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 18:25:45 +0200 Subject: [PATCH 03/10] Accept multiline snippet output comments --- lib/runner.php | 53 +++++++++++++++++++----- tests/phpunit/tests/export/docblocks.inc | 3 +- tests/phpunit/tests/export/docblocks.php | 51 ++++++++++++++++++++++- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 89a7c68..6235f9a 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1098,10 +1098,14 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence } /** - * Extracts a trailing output comment from an interactive PHP snippet. + * Extracts trailing Outputs metadata from an interactive PHP snippet. * - * The output value uses JSON-string syntax so punctuation, whitespace, and - * escaped newlines retain their exact value in the exported JSON. + * A human-readable output block starts with `// Outputs:` and continues through + * the final consecutive `//` comment lines. One space after each `//` is a + * comment delimiter, while any additional indentation becomes part of the + * output. A final empty comment line preserves a final output newline. + * + * The older one-line form continues to accept a JSON string. * * @param string $code Snippet code extracted from a DocBlock fence. * @@ -1110,12 +1114,41 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence * @return array{code: string, expected_output: string|null} Runnable code and its optional output. */ function extract_docblock_php_snippet_output_comment( $code ) { - $lines = explode( "\n", $code ); - $last_line = count( $lines ) - 1; - $comment = $lines[ $last_line ]; - $has_output = preg_match( '/^[ \t]*\/\/ Outputs:(.*)$/', $comment, $matches ); + $lines = explode( "\n", $code ); + $last_line = count( $lines ) - 1; + $trailing_comment_start = $last_line + 1; + $trailing_comment_lines = array(); + + for ( $line = $last_line; $line >= 0; $line-- ) { + if ( ! preg_match( '/^[ \t]*\/\/(.*)$/', $lines[ $line ], $matches ) ) { + break; + } + + $trailing_comment_start = $line; + $trailing_comment_lines[ $line ] = $matches[1]; + } + + for ( $line = $trailing_comment_start; $line <= $last_line; $line++ ) { + if ( ! isset( $trailing_comment_lines[ $line ] ) || ! preg_match( '/^ Outputs:[ \t]*$/', $trailing_comment_lines[ $line ] ) ) { + continue; + } - if ( ! $has_output ) { + $output_lines = array(); + for ( $output_line = $line + 1; $output_line <= $last_line; $output_line++ ) { + $value = $trailing_comment_lines[ $output_line ]; + if ( 0 === strpos( $value, ' ' ) ) { + $value = substr( $value, 1 ); + } + $output_lines[] = $value; + } + + return array( + 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $line ) ), "\n" ), + 'expected_output' => implode( "\n", $output_lines ), + ); + } + + if ( ! preg_match( '/^[ \t]*\/\/ Outputs:(.*)$/', $lines[ $last_line ], $matches ) ) { return array( 'code' => $code, 'expected_output' => null, @@ -1129,10 +1162,8 @@ function extract_docblock_php_snippet_output_comment( $code ) { ); } - array_pop( $lines ); - return array( - 'code' => rtrim( implode( "\n", $lines ), "\n" ), + 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), 'expected_output' => $output, ); } diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index c5b6251..5c27ccd 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -107,7 +107,8 @@ class Test_Class { * 'apple',", + ');', + 'print_r( $values );', + '// Outputs:', + '// Array', + '// (', + '// [fruit] => apple', + '// )', + '//', + '```', + ) + ) + ); + + $this->assertSame( + array( + array( + 'type' => 'php-code-snippet', + 'code' => "\$values = array(\n\t'fruit' => 'apple',\n);\nprint_r( \$values );", + 'expected_output' => "Array\n(\n [fruit] => apple\n)\n", + ), + ), + $snippets + ); + } + + /** + * Test that a human-readable output block preserves literal Unicode text. + */ + public function test_multiline_code_snippet_output_comment_preserves_unicode() { + + $snippets = \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'done';\n// Outputs:\n// ✅ Complete\n```" + ); + + $this->assertSame( '✅ Complete', $snippets[0]['expected_output'] ); + } + + /** + * Test that older JSON-string Outputs comments retain their output value. * * @dataProvider trailing_code_snippet_output_comments */ From 2e1c5fde7ed965dacd598e0859e9e935586cf4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 18:32:29 +0200 Subject: [PATCH 04/10] Accept raw inline snippet output --- lib/runner.php | 22 ++++++++++++------ tests/phpunit/tests/export/docblocks.php | 29 ++++++++++++++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 6235f9a..49b1126 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1105,11 +1105,12 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence * comment delimiter, while any additional indentation becomes part of the * output. A final empty comment line preserves a final output newline. * - * The older one-line form continues to accept a JSON string. + * Text after `// Outputs:` is one raw output line. The older one-line + * JSON-string form continues to work when that text starts with a double quote. * * @param string $code Snippet code extracted from a DocBlock fence. * - * @throws \InvalidArgumentException When an Outputs comment is not a JSON string. + * @throws \InvalidArgumentException When a quoted Outputs comment is not a JSON string. * * @return array{code: string, expected_output: string|null} Runnable code and its optional output. */ @@ -1155,11 +1156,18 @@ function extract_docblock_php_snippet_output_comment( $code ) { ); } - $output = json_decode( trim( $matches[1] ), true ); - if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $output ) ) { - throw new \InvalidArgumentException( - 'The trailing Outputs comment must contain one JSON string.' - ); + $output = $matches[1]; + if ( 0 === strpos( $output, ' ' ) ) { + $output = substr( $output, 1 ); + } + + if ( 0 === strpos( $output, '"' ) ) { + $output = json_decode( trim( $output ), true ); + if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $output ) ) { + throw new \InvalidArgumentException( + 'The trailing quoted Outputs comment must contain one JSON string.' + ); + } } return array( diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index b8e33a2..aabd225 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -547,6 +547,27 @@ public function test_code_snippet_output_comment() { ); } + /** + * Test that text after Outputs is exported as a raw one-line value. + */ + public function test_inline_code_snippet_output_comment() { + + $snippets = \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho esc_html( '' );\n// Outputs: \n```" + ); + + $this->assertSame( + array( + array( + 'type' => 'php-code-snippet', + 'code' => "echo esc_html( '' );", + 'expected_output' => '', + ), + ), + $snippets + ); + } + /** * Test that a trailing Outputs comment block exports human-readable output. */ @@ -661,15 +682,15 @@ public function test_non_trailing_code_snippet_output_comment_remains_code() { } /** - * Test that a trailing Outputs comment must use JSON-string syntax. + * Test that a quoted Outputs comment must use valid JSON-string syntax. */ - public function test_code_snippet_output_comment_requires_json_string() { + public function test_quoted_code_snippet_output_comment_requires_json_string() { $this->expectException( \InvalidArgumentException::class ); - $this->expectExceptionMessage( 'The trailing Outputs comment must contain one JSON string.' ); + $this->expectExceptionMessage( 'The trailing quoted Outputs comment must contain one JSON string.' ); \WP_Parser\export_docblock_code_snippets( - "```php interactive\necho 'one';\n// Outputs: one\n```" + "```php interactive\necho 'one';\n// Outputs: \"one\n```" ); } From 13c17b0b80aa90e0af918514a61e68fea77ba7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 18:34:14 +0200 Subject: [PATCH 05/10] Document inferred snippet output forms --- lib/runner.php | 6 ++++-- tests/phpunit/tests/export/docblocks.php | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 49b1126..fcadffe 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1105,8 +1105,9 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence * comment delimiter, while any additional indentation becomes part of the * output. A final empty comment line preserves a final output newline. * - * Text after `// Outputs:` is one raw output line. The older one-line - * JSON-string form continues to work when that text starts with a double quote. + * The output form is inferred from the text after `// Outputs:`: a double quote + * starts a JSON string, while other text is one raw output line. Quoting makes + * trailing whitespace and escaped characters visible in a one-line output. * * @param string $code Snippet code extracted from a DocBlock fence. * @@ -1162,6 +1163,7 @@ function extract_docblock_php_snippet_output_comment( $code ) { } if ( 0 === strpos( $output, '"' ) ) { + // A quoted inline value uses JSON to preserve the exact output string. $output = json_decode( trim( $output ), true ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $output ) ) { throw new \InvalidArgumentException( diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index aabd225..cf961bf 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -618,11 +618,11 @@ public function test_multiline_code_snippet_output_comment_preserves_unicode() { } /** - * Test that older JSON-string Outputs comments retain their output value. + * Test that quoted Outputs comments retain their output value. * - * @dataProvider trailing_code_snippet_output_comments + * @dataProvider quoted_code_snippet_output_comments */ - public function test_trailing_code_snippet_output_comments( $comment, $expected_output ) { + public function test_quoted_code_snippet_output_comments( $comment, $expected_output ) { $snippets = \WP_Parser\export_docblock_code_snippets( "```php interactive\necho 'example';\n" . $comment . "\n```" @@ -641,11 +641,15 @@ public function test_trailing_code_snippet_output_comments( $comment, $expected_ } /** - * Returns JSON-string output comments with escaping that must survive export. + * Returns quoted output comments with formatting that must survive export. */ - public function trailing_code_snippet_output_comments() { + public function quoted_code_snippet_output_comments() { return array( + 'quoted output preserves trailing whitespace' => array( + '// Outputs: "ends with a space "', + 'ends with a space ', + ), 'multiline output with a trailing newline' => array( '// Outputs: "first\\nsecond\\n"', "first\nsecond\n", From 3d50e029d9e0082951479098cf3da46b8d505f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 18:38:09 +0200 Subject: [PATCH 06/10] Cover multiline output whitespace --- lib/runner.php | 45 ++++++++++++++++-------- tests/phpunit/tests/export/docblocks.php | 14 +++++++- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index fcadffe..76af0d6 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1105,9 +1105,9 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence * comment delimiter, while any additional indentation becomes part of the * output. A final empty comment line preserves a final output newline. * - * The output form is inferred from the text after `// Outputs:`: a double quote - * starts a JSON string, while other text is one raw output line. Quoting makes - * trailing whitespace and escaped characters visible in a one-line output. + * Each output line is inferred separately: a double quote starts a JSON string, + * while other text is raw output. Quoting makes trailing whitespace and escaped + * characters visible in either a one-line or multiline output. * * @param string $code Snippet code extracted from a DocBlock fence. * @@ -1141,7 +1141,7 @@ function extract_docblock_php_snippet_output_comment( $code ) { if ( 0 === strpos( $value, ' ' ) ) { $value = substr( $value, 1 ); } - $output_lines[] = $value; + $output_lines[] = decode_docblock_php_snippet_output_line( $value ); } return array( @@ -1162,22 +1162,37 @@ function extract_docblock_php_snippet_output_comment( $code ) { $output = substr( $output, 1 ); } - if ( 0 === strpos( $output, '"' ) ) { - // A quoted inline value uses JSON to preserve the exact output string. - $output = json_decode( trim( $output ), true ); - if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $output ) ) { - throw new \InvalidArgumentException( - 'The trailing quoted Outputs comment must contain one JSON string.' - ); - } - } - return array( 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), - 'expected_output' => $output, + 'expected_output' => decode_docblock_php_snippet_output_line( $output ), ); } +/** + * Decodes one line of output comment text. + * + * @param string $output One output line after its comment delimiter. + * + * @throws \InvalidArgumentException When a quoted output line is not a JSON string. + * + * @return string Raw or decoded output text. + */ +function decode_docblock_php_snippet_output_line( $output ) { + if ( 0 !== strpos( $output, '"' ) ) { + return $output; + } + + // A quoted value uses JSON to preserve the exact output string. + $decoded = json_decode( trim( $output ), true ); + if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $decoded ) ) { + throw new \InvalidArgumentException( + 'A quoted Outputs comment must contain one JSON string.' + ); + } + + return $decoded; +} + /** * Checks whether two fences are separated only by blank DocBlock lines. * diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index cf961bf..dbfe391 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -617,6 +617,18 @@ public function test_multiline_code_snippet_output_comment_preserves_unicode() { $this->assertSame( '✅ Complete', $snippets[0]['expected_output'] ); } + /** + * Test that quoted output lines preserve trailing whitespace and newlines. + */ + public function test_multiline_code_snippet_output_comment_preserves_trailing_whitespace() { + + $snippets = \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'done';\n// Outputs:\n// first\n// \"second \"\n//\n//\n```" + ); + + $this->assertSame( "first\nsecond \n\n", $snippets[0]['expected_output'] ); + } + /** * Test that quoted Outputs comments retain their output value. * @@ -691,7 +703,7 @@ public function test_non_trailing_code_snippet_output_comment_remains_code() { public function test_quoted_code_snippet_output_comment_requires_json_string() { $this->expectException( \InvalidArgumentException::class ); - $this->expectExceptionMessage( 'The trailing quoted Outputs comment must contain one JSON string.' ); + $this->expectExceptionMessage( 'A quoted Outputs comment must contain one JSON string.' ); \WP_Parser\export_docblock_code_snippets( "```php interactive\necho 'one';\n// Outputs: \"one\n```" From aa72b38137fdcada229f88382546cf7c0e383b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 18:56:52 +0200 Subject: [PATCH 07/10] Make output comment encoding explicit --- lib/runner.php | 61 ++++++++++----------- tests/phpunit/tests/export/docblocks.php | 69 +++++++++++++++++------- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 76af0d6..800ed12 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1100,18 +1100,19 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence /** * Extracts trailing Outputs metadata from an interactive PHP snippet. * - * A human-readable output block starts with `// Outputs:` and continues through - * the final consecutive `//` comment lines. One space after each `//` is a - * comment delimiter, while any additional indentation becomes part of the - * output. A final empty comment line preserves a final output newline. + * `// Outputs:` declares literal output. Text on the same line is a one-line + * value. An empty header starts a block that continues through the final + * consecutive `//` comment lines. One space after each `//` is a comment + * delimiter, while any additional indentation becomes part of the output. A + * final empty comment line preserves a final output newline. * - * Each output line is inferred separately: a double quote starts a JSON string, - * while other text is raw output. Quoting makes trailing whitespace and escaped - * characters visible in either a one-line or multiline output. + * `// Outputs (JSON-encoded):` declares one JSON string. This explicit form + * makes trailing whitespace and escaped characters visible without assigning + * special meaning to any literal output text. * * @param string $code Snippet code extracted from a DocBlock fence. * - * @throws \InvalidArgumentException When a quoted Outputs comment is not a JSON string. + * @throws \InvalidArgumentException When an Outputs (JSON-encoded) comment is not a JSON string. * * @return array{code: string, expected_output: string|null} Runnable code and its optional output. */ @@ -1141,7 +1142,7 @@ function extract_docblock_php_snippet_output_comment( $code ) { if ( 0 === strpos( $value, ' ' ) ) { $value = substr( $value, 1 ); } - $output_lines[] = decode_docblock_php_snippet_output_line( $value ); + $output_lines[] = $value; } return array( @@ -1150,7 +1151,19 @@ function extract_docblock_php_snippet_output_comment( $code ) { ); } - if ( ! preg_match( '/^[ \t]*\/\/ Outputs:(.*)$/', $lines[ $last_line ], $matches ) ) { + if ( preg_match( '/^[ \t]*\/\/ Outputs:(.*)$/', $lines[ $last_line ], $matches ) ) { + $output = $matches[1]; + if ( 0 === strpos( $output, ' ' ) ) { + $output = substr( $output, 1 ); + } + + return array( + 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), + 'expected_output' => $output, + ); + } + + if ( ! preg_match( '/^[ \t]*\/\/ Outputs \(JSON-encoded\):(.*)$/', $lines[ $last_line ], $matches ) ) { return array( 'code' => $code, 'expected_output' => null, @@ -1162,35 +1175,17 @@ function extract_docblock_php_snippet_output_comment( $code ) { $output = substr( $output, 1 ); } - return array( - 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), - 'expected_output' => decode_docblock_php_snippet_output_line( $output ), - ); -} - -/** - * Decodes one line of output comment text. - * - * @param string $output One output line after its comment delimiter. - * - * @throws \InvalidArgumentException When a quoted output line is not a JSON string. - * - * @return string Raw or decoded output text. - */ -function decode_docblock_php_snippet_output_line( $output ) { - if ( 0 !== strpos( $output, '"' ) ) { - return $output; - } - - // A quoted value uses JSON to preserve the exact output string. $decoded = json_decode( trim( $output ), true ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_string( $decoded ) ) { throw new \InvalidArgumentException( - 'A quoted Outputs comment must contain one JSON string.' + 'The Outputs (JSON-encoded) comment must contain one JSON string.' ); } - return $decoded; + return array( + 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), + 'expected_output' => $decoded, + ); } /** diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index dbfe391..6e5aa8e 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -525,7 +525,7 @@ public function test_code_snippet_output_comment() { 'foreach ( $p->class_list() as $class_name ) {', ' echo "{$class_name} ";', '}', - '// Outputs: "free lang-en "', + '// Outputs (JSON-encoded): "free lang-en "', '```', ) ) @@ -568,6 +568,18 @@ public function test_inline_code_snippet_output_comment() { ); } + /** + * Test that quotes in literal one-line output remain output text. + */ + public function test_inline_code_snippet_output_comment_preserves_quotes() { + + $snippets = \WP_Parser\export_docblock_code_snippets( + "```php interactive\necho 'example';\n// Outputs: \"second \"\n```" + ); + + $this->assertSame( '"second "', $snippets[0]['expected_output'] ); + } + /** * Test that a trailing Outputs comment block exports human-readable output. */ @@ -618,23 +630,23 @@ public function test_multiline_code_snippet_output_comment_preserves_unicode() { } /** - * Test that quoted output lines preserve trailing whitespace and newlines. + * Test that literal output preserves quotes and trailing newlines. */ - public function test_multiline_code_snippet_output_comment_preserves_trailing_whitespace() { + public function test_multiline_code_snippet_output_comment_preserves_literal_text() { $snippets = \WP_Parser\export_docblock_code_snippets( "```php interactive\necho 'done';\n// Outputs:\n// first\n// \"second \"\n//\n//\n```" ); - $this->assertSame( "first\nsecond \n\n", $snippets[0]['expected_output'] ); + $this->assertSame( "first\n\"second \"\n\n", $snippets[0]['expected_output'] ); } /** - * Test that quoted Outputs comments retain their output value. + * Test that JSON-encoded Outputs comments retain their output value. * - * @dataProvider quoted_code_snippet_output_comments + * @dataProvider json_encoded_code_snippet_output_comments */ - public function test_quoted_code_snippet_output_comments( $comment, $expected_output ) { + public function test_json_encoded_code_snippet_output_comments( $comment, $expected_output ) { $snippets = \WP_Parser\export_docblock_code_snippets( "```php interactive\necho 'example';\n" . $comment . "\n```" @@ -653,27 +665,31 @@ public function test_quoted_code_snippet_output_comments( $comment, $expected_ou } /** - * Returns quoted output comments with formatting that must survive export. + * Returns JSON-encoded output comments with formatting that must survive export. */ - public function quoted_code_snippet_output_comments() { + public function json_encoded_code_snippet_output_comments() { return array( - 'quoted output preserves trailing whitespace' => array( - '// Outputs: "ends with a space "', + 'JSON string preserves trailing whitespace' => array( + '// Outputs (JSON-encoded): "ends with a space "', 'ends with a space ', ), - 'multiline output with a trailing newline' => array( - '// Outputs: "first\\nsecond\\n"', + 'JSON string preserves multiline output with a trailing newline' => array( + '// Outputs (JSON-encoded): "first\\nsecond\\n"', "first\nsecond\n", ), 'escaped quotes and tabs' => array( - '// Outputs: "A \\"quote\\" and a \\t tab"', + '// Outputs (JSON-encoded): "A \\"quote\\" and a \\t tab"', "A \"quote\" and a \t tab", ), 'whitespace after the JSON string is not output' => array( - '// Outputs: "done" ', + '// Outputs (JSON-encoded): "done" ', 'done', ), + 'literal Unicode remains readable' => array( + '// Outputs (JSON-encoded): "✅ Complete "', + '✅ Complete ', + ), ); } @@ -698,15 +714,28 @@ public function test_non_trailing_code_snippet_output_comment_remains_code() { } /** - * Test that a quoted Outputs comment must use valid JSON-string syntax. + * Test that a JSON-encoded Outputs comment must contain a JSON string. + * + * @dataProvider invalid_json_encoded_code_snippet_output_comments */ - public function test_quoted_code_snippet_output_comment_requires_json_string() { + public function test_json_encoded_code_snippet_output_comment_requires_json_string( $output ) { $this->expectException( \InvalidArgumentException::class ); - $this->expectExceptionMessage( 'A quoted Outputs comment must contain one JSON string.' ); + $this->expectExceptionMessage( 'The Outputs (JSON-encoded) comment must contain one JSON string.' ); \WP_Parser\export_docblock_code_snippets( - "```php interactive\necho 'one';\n// Outputs: \"one\n```" + "```php interactive\necho 'one';\n// Outputs (JSON-encoded): " . $output . "\n```" + ); + } + + /** + * Returns invalid JSON-encoded output values. + */ + public function invalid_json_encoded_code_snippet_output_comments() { + + return array( + 'malformed JSON' => array( '"one' ), + 'JSON value is not a string' => array( '1' ), ); } @@ -719,7 +748,7 @@ public function test_code_snippet_output_comment_and_fence_cannot_both_define_ou $this->expectExceptionMessage( 'declares output both in code and in an expected-output fence' ); \WP_Parser\export_docblock_code_snippets( - "```php interactive\necho 'one';\n// Outputs: \"one\"\n```\n```expected-output\none\n```" + "```php interactive\necho 'one';\n// Outputs (JSON-encoded): \"one\"\n```\n```expected-output\none\n```" ); } From 0a40a832b453623fa6d619c26ed686aa0e62c6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 19:03:49 +0200 Subject: [PATCH 08/10] Remove expected-output fence support --- lib/runner.php | 29 +---- tests/phpunit/tests/export/docblocks.inc | 25 +--- tests/phpunit/tests/export/docblocks.php | 113 +++--------------- .../tests/export/fence-first-docblocks.inc | 5 +- 4 files changed, 25 insertions(+), 147 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 800ed12..696f1f1 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -811,14 +811,12 @@ function get_docblock_code_fences( $text ) { validate_docblock_setup_blueprint_name( $setup_name, $fence['start'] ); } - $is_expected_output = 'expected-output' === $fence['language'] && 1 === count( $info_parts ); - $is_blueprint = 'setup-blueprint' === $fence['language'] && 1 === count( $info_parts ); + $is_blueprint = 'setup-blueprint' === $fence['language'] && 1 === count( $info_parts ); $fences[ $key ]['referenced_setup'] = $referenced_setup; $fences[ $key ]['is_interactive_php'] = $is_interactive_php; - $fences[ $key ]['is_expected_output'] = $is_expected_output; $fences[ $key ]['is_blueprint'] = $is_blueprint; $fences[ $key ]['setup_name'] = $setup_name; - $fences[ $key ]['is_code_snippet'] = $is_interactive_php || $is_expected_output || $is_blueprint || null !== $setup_name; + $fences[ $key ]['is_code_snippet'] = $is_interactive_php || $is_blueprint || null !== $setup_name; } // Number the interactive PHP fences so the exporter and the stripper agree on each @@ -1034,20 +1032,6 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence break; } - if ( $fences[ $j ]['is_expected_output'] ) { - // First expected-output fence ends the run, so a snippet takes one. - if ( array_key_exists( 'expected_output', $snippet ) ) { - throw new \InvalidArgumentException( - 'Interactive PHP fence on line ' . ( $fences[ $i ]['start'] + 1 ) . - ' of the long description declares output both in code and in an expected-output fence.' - ); - } - - $snippet['expected_output'] = $fences[ $j ]['code']; - $consumed_fences[ $j ] = true; - break; - } - if ( null !== $fences[ $j ]['setup_name'] ) { break; } @@ -1079,13 +1063,6 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence continue; } - if ( $fence['is_expected_output'] ) { - throw new \InvalidArgumentException( - 'Expected-output fence on line ' . ( $fence['start'] + 1 ) . - ' of the long description is not attached to an interactive PHP fence.' - ); - } - if ( $fence['is_blueprint'] ) { throw new \InvalidArgumentException( 'Inline setup Blueprint on line ' . ( $fence['start'] + 1 ) . @@ -1239,7 +1216,7 @@ function strip_docblock_code_snippet_fences( $text, $fences = null ) { // Interactive PHP fences become `code_snippets` entries. A plain HTML // comment survives Markdown rendering, `the_content`, and block parsing, // allowing the theme to replace it in place between the surrounding prose. - // Snippet-metadata fences (expected-output, Blueprints) are removed. + // Snippet-metadata fences containing Blueprints are removed. for ( $i = $fence['start']; $i <= $fence['end']; $i++ ) { if ( $fence['is_interactive_php'] && $i === $fence['start'] ) { // Keep a nested fence's indentation so Markdown leaves the replacement diff --git a/tests/phpunit/tests/export/docblocks.inc b/tests/phpunit/tests/export/docblocks.inc index 5c27ccd..fbafe08 100644 --- a/tests/phpunit/tests/export/docblocks.inc +++ b/tests/phpunit/tests/export/docblocks.inc @@ -60,10 +60,7 @@ class Test_Class { * @unlink( '/tmp/phpdoc-parser-property' ); * require '/wordpress/wp-load.php'; * echo docs_file_greeting(); - * ``` - * - * ```expected-output - * Hello from the file setup + * // Outputs: Hello from the file setup * ``` * * @since 3.0.0 @@ -135,20 +132,14 @@ class Test_Class { * array( - "````php interactive\n array( - "```php interactive\n array( - " ```php interactive\n array( - " ```php interactive\nexpectException( \InvalidArgumentException::class ); - $this->expectExceptionMessage( 'declares output both in code and in an expected-output fence' ); - - \WP_Parser\export_docblock_code_snippets( - "```php interactive\necho 'one';\n// Outputs (JSON-encoded): \"one\"\n```\n```expected-output\none\n```" - ); - } - /** * Test that unsupported info strings remain ordinary documentation. * @@ -782,6 +769,7 @@ public function unrecognized_code_fence_info_strings() { 'collapsed Blueprint reference' => array( 'php interactive setupblueprint=shared', $php ), 'unsupported interactive option' => array( 'php interactive editable=false', $php ), 'output alias' => array( 'output', 'output' ), + 'expected output fence' => array( 'expected-output', 'output' ), 'underscored expected output' => array( 'expected_output', 'output' ), 'typed expected output' => array( 'text/expected-output', 'output' ), 'uppercase PHP' => array( 'PHP interactive', $php ), @@ -799,8 +787,8 @@ public function unrecognized_code_fence_info_strings() { /** * Test that each PHP fence is replaced with an inline placeholder, in order, * so the theme can render each snippet between the surrounding prose instead - * of collapsing every snippet to the end of the description. Snippet-metadata - * fences (expected-output, Blueprints) are removed. + * of collapsing every snippet to the end of the description. Setup Blueprint + * fences are removed. */ public function test_code_snippet_inline_placeholders() { @@ -819,10 +807,7 @@ public function test_code_snippet_inline_placeholders() { '```php interactive', 'assertGreaterThan( $first, strpos( $stripped, 'Middle prose.' ) ); $this->assertGreaterThan( $second, strpos( $stripped, 'Closing prose.' ) ); - // No raw PHP fence or metadata fence is left behind in the description. + // No raw PHP fence is left behind in the description. $this->assertStringNotContainsString( '```', $stripped ); $this->assertStringNotContainsString( 'assertEquals( array( @@ -1035,9 +1014,6 @@ public function test_code_snippet_metadata_rejects_extra_arguments() { 'assertEquals( - array( - array( - 'type' => 'php-code-snippet', - 'code' => " 'First', - ), - array( - 'type' => 'php-code-snippet', - 'code' => " array( - 'steps' => array( - array( - 'step' => 'writeFile', - 'path' => '/tmp/second.php', - 'data' => ' array( "```expected-output\n1\n```\n" . $php ), - 'expected output after prose' => array( $php . "\nProse.\n```expected-output\n1\n```" ), 'inline Blueprint before prose' => array( "```setup-blueprint\n{}\n```\nProse.\n" . $php ), 'inline Blueprint after prose' => array( $php . "\nProse.\n```setup-blueprint\n{}\n```" ), - 'duplicate expected output' => array( $php . "\n```expected-output\n1\n```\n```expected-output\n2\n```" ), ); } @@ -1391,16 +1314,12 @@ public function test_code_snippet_named_setup_blueprints() { '```php interactive setup-blueprint=shared', ' Date: Mon, 31 Aug 2026 19:17:00 +0200 Subject: [PATCH 09/10] Parse output comments as PHP tokens --- lib/runner.php | 104 ++++++++++++++++++----- tests/phpunit/tests/export/docblocks.php | 48 ++++++++--- 2 files changed, 120 insertions(+), 32 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index 696f1f1..fb91fd5 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1094,28 +1094,30 @@ function export_docblock_code_snippets( $text, &$setup_blueprints = null, $fence * @return array{code: string, expected_output: string|null} Runnable code and its optional output. */ function extract_docblock_php_snippet_output_comment( $code ) { - $lines = explode( "\n", $code ); - $last_line = count( $lines ) - 1; - $trailing_comment_start = $last_line + 1; - $trailing_comment_lines = array(); - - for ( $line = $last_line; $line >= 0; $line-- ) { - if ( ! preg_match( '/^[ \t]*\/\/(.*)$/', $lines[ $line ], $matches ) ) { - break; - } + if ( false === strpos( $code, '// Outputs' ) ) { + return array( + 'code' => $code, + 'expected_output' => null, + ); + } - $trailing_comment_start = $line; - $trailing_comment_lines[ $line ] = $matches[1]; + $comments = parse_trailing_docblock_php_snippet_line_comments( $code ); + if ( empty( $comments ) ) { + return array( + 'code' => $code, + 'expected_output' => null, + ); } - for ( $line = $trailing_comment_start; $line <= $last_line; $line++ ) { - if ( ! isset( $trailing_comment_lines[ $line ] ) || ! preg_match( '/^ Outputs:[ \t]*$/', $trailing_comment_lines[ $line ] ) ) { + foreach ( $comments as $comment_index => $comment ) { + $value = substr( $comment['text'], 2 ); + if ( ' Outputs:' !== rtrim( $value, " \t" ) ) { continue; } $output_lines = array(); - for ( $output_line = $line + 1; $output_line <= $last_line; $output_line++ ) { - $value = $trailing_comment_lines[ $output_line ]; + foreach ( array_slice( $comments, $comment_index + 1 ) as $output_comment ) { + $value = substr( $output_comment['text'], 2 ); if ( 0 === strpos( $value, ' ' ) ) { $value = substr( $value, 1 ); } @@ -1123,31 +1125,33 @@ function extract_docblock_php_snippet_output_comment( $code ) { } return array( - 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $line ) ), "\n" ), + 'code' => rtrim( substr( $code, 0, $comment['line_start'] ), "\n" ), 'expected_output' => implode( "\n", $output_lines ), ); } - if ( preg_match( '/^[ \t]*\/\/ Outputs:(.*)$/', $lines[ $last_line ], $matches ) ) { - $output = $matches[1]; + $comment = end( $comments ); + $value = substr( $comment['text'], 2 ); + if ( 0 === strpos( $value, ' Outputs:' ) ) { + $output = substr( $value, strlen( ' Outputs:' ) ); if ( 0 === strpos( $output, ' ' ) ) { $output = substr( $output, 1 ); } return array( - 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), + 'code' => rtrim( substr( $code, 0, $comment['line_start'] ), "\n" ), 'expected_output' => $output, ); } - if ( ! preg_match( '/^[ \t]*\/\/ Outputs \(JSON-encoded\):(.*)$/', $lines[ $last_line ], $matches ) ) { + if ( 0 !== strpos( $value, ' Outputs (JSON-encoded):' ) ) { return array( 'code' => $code, 'expected_output' => null, ); } - $output = $matches[1]; + $output = substr( $value, strlen( ' Outputs (JSON-encoded):' ) ); if ( 0 === strpos( $output, ' ' ) ) { $output = substr( $output, 1 ); } @@ -1160,11 +1164,67 @@ function extract_docblock_php_snippet_output_comment( $code ) { } return array( - 'code' => rtrim( implode( "\n", array_slice( $lines, 0, $last_line ) ), "\n" ), + 'code' => rtrim( substr( $code, 0, $comment['line_start'] ), "\n" ), 'expected_output' => $decoded, ); } +/** + * Parses the consecutive standalone PHP line comments at the end of a snippet. + * + * @param string $code Snippet code extracted from a DocBlock fence. + * + * @return array Comments in source order. + */ +function parse_trailing_docblock_php_snippet_line_comments( $code ) { + // Force snippets without an opening tag into PHP mode. Do not use TOKEN_PARSE: + // documentation snippets may be partial programs that are still valid examples. + $tokens = token_get_all( " $comment_text, + 'start' => $offset, + 'line_start' => $line_start, + ); + } elseif ( T_WHITESPACE !== $id ) { + $comments = array(); + } + + $last_newline = strrpos( $text, "\n" ); + if ( false !== $last_newline ) { + $line_start = $offset + $last_newline + 1; + } + $offset += strlen( $text ); + } + + return $comments; +} + /** * Checks whether two fences are separated only by blank DocBlock lines. * diff --git a/tests/phpunit/tests/export/docblocks.php b/tests/phpunit/tests/export/docblocks.php index e914b85..6a71212 100644 --- a/tests/phpunit/tests/export/docblocks.php +++ b/tests/phpunit/tests/export/docblocks.php @@ -349,36 +349,31 @@ public function test_method_file_setup_blueprint() { * * @dataProvider code_snippet_fence_delimiters */ - public function test_code_snippet_fence_delimiters( $description, $expected_code, $expected_output ) { + public function test_code_snippet_fence_delimiters( $description, $expected_code ) { $snippets = \WP_Parser\export_docblock_code_snippets( $description ); $this->assertCount( 1, $snippets ); $this->assertSame( $expected_code, $snippets[0]['code'] ); - $this->assertSame( $expected_output, $snippets[0]['expected_output'] ); } public function code_snippet_fence_delimiters() { return array( 'smaller runs stay inside a larger fence' => array( - "````php interactive\n array( - "```php interactive\n array( - " ```php interactive\n array( - " ```php interactive\nassertSame( + array( + array( + 'type' => 'php-code-snippet', + 'code' => $code, + ), + ), + \WP_Parser\export_docblock_code_snippets( "```php interactive\n" . $code . "\n```" ) + ); + } + + /** + * Returns PHP tokens in which Outputs text is ordinary program text. + */ + public function php_code_containing_non_metadata_outputs_text() { + + return array( + 'string literal' => array( "echo '// Outputs: not metadata';" ), + 'heredoc body' => array( "echo << array( "echo 'done';\n/* // Outputs: not metadata */" ), + 'comment after code on the same line' => array( "echo 'done'; // Outputs: not metadata" ), + 'ordinary line comment' => array( '// Example containing // Outputs: not metadata' ), + 'text after a PHP closing tag' => array( "\n// Outputs: not PHP" ), + ); + } + /** * Test that a JSON-encoded Outputs comment must contain a JSON string. * From 40d6833ca6035a08763a8d827cd62c593017107c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 31 Aug 2026 19:26:56 +0200 Subject: [PATCH 10/10] Use PHP-Parser's emulative lexer --- lib/runner.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/runner.php b/lib/runner.php index fb91fd5..296b585 100644 --- a/lib/runner.php +++ b/lib/runner.php @@ -1177,26 +1177,28 @@ function extract_docblock_php_snippet_output_comment( $code ) { * @return array Comments in source order. */ function parse_trailing_docblock_php_snippet_line_comments( $code ) { - // Force snippets without an opening tag into PHP mode. Do not use TOKEN_PARSE: - // documentation snippets may be partial programs that are still valid examples. - $tokens = token_get_all( "tokenize( + "id; + $text = $token->text; $is_standalone_line_comment = T_COMMENT === $id && 0 === strpos( $text, '//' ) && '' === trim( substr( $code, $line_start, $offset - $line_start ), " \t" ); if ( $is_standalone_line_comment ) { - // PHP versions differ on whether a line comment token includes its newline. - $comment_text = "\n" === substr( $text, -1 ) ? substr( $text, 0, -1 ) : $text; - if ( ! empty( $comments ) ) { $previous = end( $comments ); $previous_end = $previous['start'] + strlen( $previous['text'] ); @@ -1207,7 +1209,7 @@ function parse_trailing_docblock_php_snippet_line_comments( $code ) { } $comments[] = array( - 'text' => $comment_text, + 'text' => $text, 'start' => $offset, 'line_start' => $line_start, );