diff --git a/composer.json b/composer.json index fec0190..1e2bc6d 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "nette/neon": "^3.4", "nikic/php-parser": "^5.7", "phiki/phiki": "^2.1", - "php-collective/djot": "^0.1.15", + "php-collective/djot": "^0.1.21", "php": ">=8.2", "symfony/process": "^7.3" }, diff --git a/src/Render/Djot/CodeGroupExtension.php b/src/Render/Djot/CodeGroupExtension.php index d3fa600..64a302c 100644 --- a/src/Render/Djot/CodeGroupExtension.php +++ b/src/Render/Djot/CodeGroupExtension.php @@ -13,6 +13,12 @@ /** * Djot extension that renders `::: code-group` blocks as tabbed code panes. + * + * Supports language hints with optional `[label]` suffix: + * - `php` -> language: php, label: php + * - `php [Installation]` -> language: php, label: Installation + * - `[Custom Label]` -> language: null, label: Custom Label + * - `c++`, `c#`, `text/html` -> works with special characters */ final class CodeGroupExtension implements ExtensionInterface { @@ -52,7 +58,7 @@ public function register(DjotConverter $converter): void return; } - $event->setHtml($this->renderCodeGroup($codeBlocks)); + $event->setHtml($this->renderCodeGroup($node, $codeBlocks)); }); } @@ -96,14 +102,17 @@ protected function extractCodeBlocks(Div $node): array /** * Render grouped code blocks as a DaisyUI tabs layout. * + * @param \Djot\Node\Block\Div $wrapper Original div node for attribute preservation. * @param list<\Djot\Node\Block\CodeBlock> $blocks Code blocks. */ - protected function renderCodeGroup(array $blocks): string + protected function renderCodeGroup(Div $wrapper, array $blocks): string { $this->groupIndex++; $groupName = 'glaze-code-group-' . $this->groupIndex; - $html = '
'; + $attrs = $this->buildWrapperAttributes($wrapper); + $html = ''; + foreach ($blocks as $index => $block) { $metadata = $this->parseLanguageMetadata($block->getLanguage(), $index + 1); $label = $this->escapeAttribute($metadata['label']); @@ -123,9 +132,42 @@ protected function renderCodeGroup(array $blocks): string return $html . '
'; } + /** + * Build wrapper div attributes, preserving custom attributes from the original div. + * + * @param \Djot\Node\Block\Div $wrapper Original div node. + */ + protected function buildWrapperAttributes(Div $wrapper): string + { + $classes = ['glaze-code-group']; + + // Add any additional classes from the original div (except 'code-group') + $existingClasses = (string)$wrapper->getAttribute('class'); + foreach (preg_split('/\s+/', $existingClasses) ?: [] as $class) { + $class = trim($class); + if ($class !== '' && $class !== 'code-group' && !in_array($class, $classes, true)) { + $classes[] = $class; + } + } + + $attrs = ' class="' . $this->escapeAttribute(implode(' ', $classes)) . '"'; + + // Copy other attributes (except class) + foreach ($wrapper->getAttributes() as $name => $value) { + if ($name === 'class') { + continue; + } + $attrs .= ' ' . $this->escapeAttribute($name) . '="' . $this->escapeAttribute((string)$value) . '"'; + } + + return $attrs; + } + /** * Parse a Djot code fence language hint with optional `[label]` suffix. * + * Supports any non-whitespace characters in language names (c++, c#, text/html, etc.) + * * @param string|null $language Raw language hint from Djot code fence. * @param int $position One-based position in the group. * @return array{language: string|null, label: string} @@ -137,16 +179,19 @@ protected function parseLanguageMetadata(?string $language, int $position): arra return ['language' => null, 'label' => 'Code ' . $position]; } - $matches = []; - preg_match('/^(?[A-Za-z0-9_-]+)?(?:\s*\[(?