Skip to content

Commit 41b1001

Browse files
committed
feat: code groups in markdown
1 parent ef88e3a commit 41b1001

14 files changed

+331
-21
lines changed

src/Markdown/Alerts/AlertBlockStartParser.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace App\Markdown\Alerts;
44

5-
use Override;
65
use League\CommonMark\Parser\Block\BlockStart;
76
use League\CommonMark\Parser\Block\BlockStartParserInterface;
87
use League\CommonMark\Parser\Cursor;
98
use League\CommonMark\Parser\MarkdownParserStateInterface;
109
use League\CommonMark\Util\RegexHelper;
10+
use Override;
1111

1212
final class AlertBlockStartParser implements BlockStartParserInterface
1313
{
@@ -24,6 +24,10 @@ public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserSta
2424
return BlockStart::none();
2525
}
2626

27+
if (str_starts_with($match[0], needle: ':::code-group')) {
28+
return BlockStart::none();
29+
}
30+
2731
$cursor->advanceToEnd();
2832

2933
$alertType = $match['type'];

src/Markdown/CodeBlockRenderer.php renamed to src/Markdown/CodeBlocks/CodeBlockRenderer.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
declare(strict_types=1);
44

5-
namespace App\Markdown;
5+
namespace App\Markdown\CodeBlocks;
66

7-
use Override;
87
use InvalidArgumentException;
98
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
109
use League\CommonMark\Node\Node;
1110
use League\CommonMark\Renderer\ChildNodeRendererInterface;
1211
use League\CommonMark\Renderer\NodeRendererInterface;
12+
use Override;
1313
use Tempest\Highlight\Highlighter;
1414
use Tempest\Highlight\WebTheme;
1515

@@ -44,13 +44,17 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer): s
4444
if ($node->getInfoWords()[1] ?? false) {
4545
return <<<HTML
4646
<div class="code-block named-code-block">
47-
<div class="code-block-name">{$node->getInfoWords()[1]}</div>
47+
<div class="code-block-name">{$node->getInfoWords()[1]}</div>
4848
{$pre}
4949
</div>
5050
HTML;
5151
}
5252

53-
return $pre;
53+
return <<<HTML
54+
<div class="code-block named-code-block">
55+
{$pre}
56+
</div>
57+
HTML;
5458
}
5559

5660
return '<pre data-lang="' . $language . '" class="notranslate">' . $parsed . '</pre>';

src/Markdown/InlineCodeBlockRenderer.php renamed to src/Markdown/CodeBlocks/InlineCodeBlockRenderer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<?php
22

3-
namespace App\Markdown;
3+
namespace App\Markdown\CodeBlocks;
44

5-
use Override;
65
use InvalidArgumentException;
76
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
87
use League\CommonMark\Node\Node;
98
use League\CommonMark\Renderer\ChildNodeRendererInterface;
109
use League\CommonMark\Renderer\NodeRendererInterface;
10+
use Override;
1111
use Tempest\Highlight\Highlighter;
1212

1313
final class InlineCodeBlockRenderer implements NodeRendererInterface
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace App\Markdown\CodeGroups;
4+
5+
use League\CommonMark\Node\Block\AbstractBlock;
6+
7+
final class CodeGroupBlock extends AbstractBlock
8+
{
9+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace App\Markdown\CodeGroups;
4+
5+
use League\CommonMark\Node\Block\AbstractBlock;
6+
use League\CommonMark\Parser\Block\BlockContinue;
7+
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
8+
use League\CommonMark\Parser\Cursor;
9+
use League\CommonMark\Util\RegexHelper;
10+
use Override;
11+
12+
final class CodeGroupBlockParser implements BlockContinueParserInterface
13+
{
14+
private CodeGroupBlock $block;
15+
private bool $finished = false;
16+
17+
public function __construct()
18+
{
19+
$this->block = new CodeGroupBlock();
20+
}
21+
22+
#[Override]
23+
public function addLine(string $line): void
24+
{
25+
}
26+
27+
#[Override]
28+
public function getBlock(): AbstractBlock
29+
{
30+
return $this->block;
31+
}
32+
33+
#[Override]
34+
public function isContainer(): bool
35+
{
36+
return true;
37+
}
38+
39+
#[Override]
40+
public function canContain(AbstractBlock $child_block): bool
41+
{
42+
return true;
43+
}
44+
45+
#[Override]
46+
public function canHaveLazyContinuationLines(): bool
47+
{
48+
return false;
49+
}
50+
51+
#[Override]
52+
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active_block_parser): ?BlockContinue
53+
{
54+
if ($cursor->isIndented()) {
55+
return BlockContinue::at($cursor);
56+
}
57+
58+
$match = RegexHelper::matchFirst('/^:::$/', $cursor->getLine());
59+
60+
if ($match !== null) {
61+
$this->finished = true;
62+
63+
return BlockContinue::finished();
64+
}
65+
66+
return BlockContinue::at($cursor);
67+
}
68+
69+
#[Override]
70+
public function closeBlock(): void
71+
{
72+
}
73+
74+
public function isFinished(): bool
75+
{
76+
return $this->finished;
77+
}
78+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Markdown\CodeGroups;
6+
7+
use App\Markdown\CodeBlocks\CodeBlockRenderer;
8+
use InvalidArgumentException;
9+
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
10+
use League\CommonMark\Node\Node;
11+
use League\CommonMark\Renderer\ChildNodeRendererInterface;
12+
use League\CommonMark\Renderer\NodeRendererInterface;
13+
use League\CommonMark\Util\HtmlElement;
14+
use Override;
15+
16+
final class CodeGroupBlockRenderer implements NodeRendererInterface
17+
{
18+
public function __construct(
19+
private CodeBlockRenderer $code_block_renderer,
20+
) {}
21+
22+
#[Override]
23+
public function render(Node $node, ChildNodeRendererInterface $child_renderer): mixed
24+
{
25+
if (! ($node instanceof CodeGroupBlock)) {
26+
throw new InvalidArgumentException('Incompatible node type: ' . $node::class);
27+
}
28+
29+
$tabs = [];
30+
$panels = [];
31+
$index = 0;
32+
33+
foreach ($node->children() as $child) {
34+
if (! ($child instanceof FencedCode)) {
35+
continue;
36+
}
37+
38+
$info_words = $child->getInfoWords();
39+
$filename = $info_words[1] ?? "Tab {$index}";
40+
41+
$is_active = $index === 0;
42+
$tab_id = 'tab-' . md5($filename . $index);
43+
$panel_id = 'panel-' . md5($filename . $index);
44+
45+
$tabs[] = new HtmlElement(
46+
tagName: 'button',
47+
attributes: [
48+
'class' => 'code-group-tab' . ($is_active ? ' active' : ''),
49+
'role' => 'tab',
50+
'aria-selected' => $is_active ? 'true' : 'false',
51+
'aria-controls' => $panel_id,
52+
'id' => $tab_id,
53+
'data-panel' => $panel_id,
54+
],
55+
contents: $filename,
56+
);
57+
58+
$rendered_code = $this->code_block_renderer->render($child, $child_renderer);
59+
60+
$panels[] = new HtmlElement(
61+
tagName: 'div',
62+
attributes: array_filter([
63+
'class' => 'code-group-panel' . ($is_active ? ' active' : ''),
64+
'role' => 'tabpanel',
65+
'aria-labelledby' => $tab_id,
66+
'id' => $panel_id,
67+
'hidden' => $is_active ? null : 'hidden',
68+
]),
69+
contents: $rendered_code,
70+
);
71+
72+
$index++;
73+
}
74+
75+
if ($tabs === []) {
76+
return '';
77+
}
78+
79+
$tab_list = new HtmlElement(
80+
tagName: 'div',
81+
attributes: [
82+
'class' => 'code-group-tabs',
83+
'role' => 'tablist',
84+
],
85+
contents: $tabs,
86+
);
87+
88+
return new HtmlElement(
89+
tagName: 'div',
90+
attributes: ['class' => 'code-group'],
91+
contents: [$tab_list, ...$panels],
92+
);
93+
}
94+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Markdown\CodeGroups;
4+
5+
use League\CommonMark\Parser\Block\BlockStart;
6+
use League\CommonMark\Parser\Block\BlockStartParserInterface;
7+
use League\CommonMark\Parser\Cursor;
8+
use League\CommonMark\Parser\MarkdownParserStateInterface;
9+
use League\CommonMark\Util\RegexHelper;
10+
use Override;
11+
12+
final class CodeGroupBlockStartParser implements BlockStartParserInterface
13+
{
14+
#[Override]
15+
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parser_state): ?BlockStart
16+
{
17+
if ($cursor->isIndented()) {
18+
return BlockStart::none();
19+
}
20+
21+
$match = RegexHelper::matchFirst('/^:::code-group$/', $cursor->getLine());
22+
23+
if ($match === null) {
24+
return BlockStart::none();
25+
}
26+
27+
$cursor->advanceToEnd();
28+
29+
return BlockStart::of(new CodeGroupBlockParser())->at($cursor);
30+
}
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace App\Markdown\CodeGroups;
4+
5+
use League\CommonMark\Environment\EnvironmentBuilderInterface;
6+
use League\CommonMark\Extension\ExtensionInterface;
7+
use Override;
8+
9+
final class CodeGroupExtension implements ExtensionInterface
10+
{
11+
#[Override]
12+
public function register(EnvironmentBuilderInterface $environment): void
13+
{
14+
$environment->addBlockStartParser(new CodeGroupBlockStartParser());
15+
}
16+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
document.querySelectorAll<HTMLDivElement>('.code-group').forEach((codeGroup) => {
3+
const tabs = codeGroup.querySelectorAll<HTMLButtonElement>('.code-group-tab')
4+
const panels = codeGroup.querySelectorAll<HTMLDivElement>('.code-group-panel')
5+
6+
tabs.forEach((tab) => {
7+
tab.addEventListener('click', () => {
8+
if (!tab.dataset.panel) {
9+
return
10+
}
11+
12+
tabs.forEach((tab) => {
13+
tab.classList.remove('active')
14+
tab.setAttribute('aria-selected', 'false')
15+
})
16+
17+
panels.forEach((panel) => {
18+
panel.classList.remove('active')
19+
panel.setAttribute('hidden', 'hidden')
20+
})
21+
22+
tab.classList.add('active')
23+
tab.setAttribute('aria-selected', 'true')
24+
25+
const targetPanel = document.getElementById(tab.dataset.panel)
26+
if (targetPanel) {
27+
targetPanel.classList.add('active')
28+
targetPanel.removeAttribute('hidden')
29+
}
30+
})
31+
})
32+
})
33+
})

src/Markdown/LinkRenderer.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
namespace App\Markdown;
44

5-
use Override;
6-
use Stringable;
7-
use function Tempest\Support\Regex\replace;
85
use InvalidArgumentException;
96
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
107
use League\CommonMark\Extension\CommonMark\Renderer\Inline\LinkRenderer as InlineLinkRenderer;
@@ -14,7 +11,10 @@
1411
use League\CommonMark\Xml\XmlNodeRendererInterface;
1512
use League\Config\ConfigurationAwareInterface;
1613
use League\Config\ConfigurationInterface;
17-
use Tempest\Support\Regex;
14+
use Override;
15+
use Stringable;
16+
17+
use function Tempest\Support\Regex\replace;
1818

1919
final class LinkRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
2020
{

0 commit comments

Comments
 (0)