Skip to content

Commit df01e74

Browse files
fix: set language on CodeNode for markdown fenced code blocks
`CodeBlockParser::parse()` stored the fenced code info string in `options['caption']` via `withOptions()`, but nothing ever read that value — `CodeNode::getCaption()` returns the typed `$caption` property, not `getOption('caption')`. As a result, `CodeNode::getLanguage()` always returned `null` for markdown code blocks, producing `class="language-"` in the rendered HTML and preventing syntax highlighting. Replace the dead `withOptions(['caption' => ...])` call with `setLanguage()` using `getInfoWords()[0]` from the CommonMark `FencedCode` node, matching both the RST parser's behavior (`CodeBlockDirective`) and league/commonmark's own `FencedCodeRenderer`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8bba205 commit df01e74

File tree

5 files changed

+146
-8
lines changed

5 files changed

+146
-8
lines changed

packages/guides-markdown/src/Markdown/Parsers/CodeBlockParser.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use phpDocumentor\Guides\Nodes\CodeNode;
2323

2424
use function assert;
25+
use function count;
2526
use function explode;
2627

2728
/** @extends AbstractBlockParser<CodeNode> */
@@ -32,8 +33,11 @@ public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMa
3233
assert($current instanceof IndentedCode || $current instanceof FencedCode);
3334
$walker->next();
3435
$codeNode = new CodeNode(explode("\n", $current->getLiteral()));
35-
if ($current instanceof FencedCode && $current->getInfo() !== null) {
36-
$codeNode = $codeNode->withOptions(['caption' => $current->getInfo()]);
36+
if ($current instanceof FencedCode) {
37+
$infoWords = $current->getInfoWords();
38+
if (count($infoWords) !== 0 && $infoWords[0] !== '') {
39+
$codeNode->setLanguage($infoWords[0]);
40+
}
3741
}
3842

3943
return $codeNode;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\Markdown\Parsers;
15+
16+
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
17+
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
18+
use League\CommonMark\Node\NodeWalker;
19+
use League\CommonMark\Node\NodeWalkerEvent;
20+
use phpDocumentor\Guides\MarkupLanguageParser;
21+
use PHPUnit\Framework\Attributes\DataProvider;
22+
use PHPUnit\Framework\TestCase;
23+
24+
final class CodeBlockParserTest extends TestCase
25+
{
26+
private CodeBlockParser $parser;
27+
28+
protected function setUp(): void
29+
{
30+
$this->parser = new CodeBlockParser();
31+
}
32+
33+
/** @return array<string, array{string, string}> */
34+
public static function languageProvider(): array
35+
{
36+
return [
37+
'php' => ['php', 'php'],
38+
'javascript' => ['javascript', 'javascript'],
39+
'c++' => ['c++', 'c++'],
40+
'first word only from info string' => ['ruby startline=1', 'ruby'],
41+
];
42+
}
43+
44+
#[DataProvider('languageProvider')]
45+
public function test_fenced_code_block_sets_language_from_info_string(string $info, string $expectedLanguage): void
46+
{
47+
$fencedCode = new FencedCode(3, '`', 0);
48+
$fencedCode->setInfo($info);
49+
$fencedCode->setLiteral("echo \"Hello\";\n");
50+
51+
$result = $this->parser->parse(
52+
$this->createMock(MarkupLanguageParser::class),
53+
new NodeWalker($fencedCode),
54+
$fencedCode,
55+
);
56+
57+
self::assertSame($expectedLanguage, $result->getLanguage());
58+
self::assertSame("echo \"Hello\";\n", $result->getValue());
59+
}
60+
61+
public function test_fenced_code_block_without_info_has_null_language(): void
62+
{
63+
$fencedCode = new FencedCode(3, '`', 0);
64+
$fencedCode->setLiteral("some code\n");
65+
66+
$result = $this->parser->parse(
67+
$this->createMock(MarkupLanguageParser::class),
68+
new NodeWalker($fencedCode),
69+
$fencedCode,
70+
);
71+
72+
self::assertNull($result->getLanguage());
73+
self::assertSame("some code\n", $result->getValue());
74+
}
75+
76+
public function test_fenced_code_block_with_empty_info_has_null_language(): void
77+
{
78+
$fencedCode = new FencedCode(3, '`', 0);
79+
$fencedCode->setInfo('');
80+
$fencedCode->setLiteral("some code\n");
81+
82+
$result = $this->parser->parse(
83+
$this->createMock(MarkupLanguageParser::class),
84+
new NodeWalker($fencedCode),
85+
$fencedCode,
86+
);
87+
88+
self::assertNull($result->getLanguage());
89+
}
90+
91+
public function test_indented_code_block_has_null_language(): void
92+
{
93+
$indentedCode = new IndentedCode();
94+
$indentedCode->setLiteral("some code\n");
95+
96+
$result = $this->parser->parse(
97+
$this->createMock(MarkupLanguageParser::class),
98+
new NodeWalker($indentedCode),
99+
$indentedCode,
100+
);
101+
102+
self::assertNull($result->getLanguage());
103+
self::assertSame("some code\n", $result->getValue());
104+
}
105+
106+
public function test_supports_fenced_code(): void
107+
{
108+
$fencedCode = new FencedCode(3, '`', 0);
109+
$event = new NodeWalkerEvent($fencedCode, true);
110+
111+
self::assertTrue($this->parser->supports($event));
112+
}
113+
114+
public function test_supports_indented_code(): void
115+
{
116+
$indentedCode = new IndentedCode();
117+
$event = new NodeWalkerEvent($indentedCode, true);
118+
119+
self::assertTrue($this->parser->supports($event));
120+
}
121+
}

tests/Integration/tests/markdown/code-md/expected/index.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@ <h2>Fenced Code Blocks</h2>
1818
</code></pre>
1919

2020
</div>
21-
<div class="section" id="fenced-code-block-with-caption">
22-
<h2>Fenced Code Block with caption</h2>
23-
<pre><code class="language-">procedure startSwinging(swing, child)
21+
<div class="section" id="fenced-code-block-with-language">
22+
<h2>Fenced Code Block with language</h2>
23+
<pre><code class="language-pseudocode">procedure startSwinging(swing, child)
2424
while child.isComfortable()
2525
swing.giveGentlePush()
2626
waitForNextIteration()
2727
end while
2828
end procedure
2929
</code></pre>
3030

31+
</div>
32+
<div class="section" id="fenced-code-block-with-php">
33+
<h2>Fenced Code Block with PHP</h2>
34+
<pre><code class="language-php">echo &quot;Hello world!&quot;;
35+
</code></pre>
36+
3137
</div>
3238
</div>
3339
<!-- content end -->

tests/Integration/tests/markdown/code-md/input/index.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@
1515
}
1616
```
1717

18-
## Fenced Code Block with caption
18+
## Fenced Code Block with language
1919

2020
```pseudocode
2121
procedure startSwinging(swing, child)
2222
while child.isComfortable()
2323
swing.giveGentlePush()
2424
waitForNextIteration()
2525
end while
26-
end procedure
26+
end procedure
27+
```
28+
29+
## Fenced Code Block with PHP
30+
31+
```php
32+
echo "Hello world!";
33+
```

tests/Integration/tests/markdown/readme-md/expected/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ <h2>Usage</h2>
4242
<li><strong>Hold onto the Swing:</strong> Provide support and hold onto the swing until your child is comfortably seated and ready to swing.</li>
4343
<li>
4444
<p><strong>Start Swinging:</strong> Give a gentle push to start the swinging motion. Observe your child&#039;s comfort level and adjust the swinging speed accordingly.</p>
45-
<pre><code class="language-">procedure startSwinging(swing, child)
45+
<pre><code class="language-pseudocode">procedure startSwinging(swing, child)
4646
while child.isComfortable()
4747
swing.giveGentlePush()
4848
waitForNextIteration()

0 commit comments

Comments
 (0)