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*\[(?