webuni/commonmark-attributes-extension
Adds Kramdown-style attribute lists to League/CommonMark markdown, letting you assign HTML ids, classes, and other attributes to block and span elements. Deprecated: use the built-in Attributes extension in league/commonmark 1.5+ instead.
league/commonmark (if not already on v1.5+):
composer require league/commonmark:^1.5
AppServiceProvider):
use League\CommonMark\Extension\Attributes\AttributesExtension;
use League\CommonMark\MarkdownConverter;
public function boot()
{
$this->app->singleton(MarkdownConverter::class, function ($app) {
$config = new \League\CommonMark\Config\Config();
$config->addExtension(new AttributesExtension());
return new MarkdownConverter($config);
});
}
$markdown = "# Heading {#id .class}\nThis is *text*{style=\"color:red\"}.";
echo $this->app->make(MarkdownConverter::class)->convert($markdown);
Markdown facade (if using spatie/laravel-markdown):
// app/Providers/AppServiceProvider.php
use Spatie\Markdown\Markdown;
public function boot()
{
Markdown::defaultConfig(function ($config) {
$config->addExtension(new AttributesExtension());
});
}
Dynamic Attribute Assignment Use attributes for conditional styling or tooling hooks:
{:data-tooltip="Hover text"}
Click me
Output:
<p data-tooltip="Hover text">Click me</p>
Syntax Highlighting in Docs Annotate code blocks for Prism.js or Highlight.js:
```php {.language-php .line-numbers}
echo "Highlighted code";
Semantic Annotations
Add data-* attributes for JavaScript interactions:
[Link](#){.btn .btn-primary data-action="modal"}
Laravel-Specific: Blade + Markdown Combine with Blade directives for dynamic attributes:
@markdown
# Dynamic Title {#title-"{{ $dynamicId }}"}
@endmarkdown
Extension Priority
Register AttributesExtension before other extensions that modify HTML (e.g., GithubFlavoredMarkdownExtension) to avoid attribute loss:
$config->addExtension(new AttributesExtension());
$config->addExtension(new GithubFlavoredMarkdownExtension());
Validation Rules
Use Laravel’s FormRequest to validate Markdown with attributes:
public function rules()
{
return [
'content' => ['required', function ($attribute, $value, $fail) {
if (str_contains($value, '{#invalid}')) {
$fail('Invalid attributes in Markdown.');
}
}],
];
}
Caching Cache the converter instance in Laravel:
$this->app->singleton(MarkdownConverter::class, function () {
static $converter;
return $converter ?? new MarkdownConverter($config);
});
Testing
Use League\CommonMark\Test\TestCase for unit tests:
public function testAttributes()
{
$markdown = "Text {style=\"color:red\"}";
$expected = '<p>Text <span style="color:red">...</span></p>';
$this->assertEquals($expected, $this->converter->convert($markdown));
}
Deprecated Package Risk
league/commonmark’s built-in Attributes extension.composer why-not league/commonmark:^1.5 to check constraints.Attribute Scope Confusion
# Header {#id} # Correct
# Header
{#id} # Incorrect (ignored)
markdownlint).HTML Escaping
{style="<script>alert()</script>"}) can inject XSS.Str::of($value)->markdown() with htmlspecialchars.Extension Conflicts
TableOfContents) may strip attributes.AttributesExtension last or use a custom renderer to preserve attributes.PHP 8.x Compatibility
league/commonmark:^1.5 (PHP 8.0+ compatible) and its Attributes extension.Inspect Parsed AST
Use League\CommonMark\Node\Node::dump() to debug parsing:
$document = $parser->parse($markdown);
$document->dump(); // Outputs AST structure
Enable Debug Renderer
Extend HtmlRenderer to log rendered HTML:
$renderer = new class($environment) extends HtmlRenderer {
public function renderNode($node) {
$html = parent::renderNode($node);
\Log::debug("Rendered: {$node->getType()} => {$html}");
return $html;
}
};
CommonMark CLI Test syntax interactively:
vendor/bin/commonmark --extensions=AttributesExtension "Test {#id}"
Custom Attribute Validation
Extend AttributesExtension to validate attributes:
use League\CommonMark\Extension\Attributes\AttributesExtension;
use League\CommonMark\Extension\Attributes\AttributesListener;
class CustomAttributesExtension extends AttributesExtension {
protected function getListeners(): array {
return [
new CustomAttributesListener(),
];
}
}
class CustomAttributesListener extends AttributesListener {
public function onRenderAttribute($attribute, $element) {
if ($attribute->getName() === 'data-role' && $attribute->getValue() !== 'safe') {
throw new \RuntimeException('Unsafe data-role attribute');
}
}
}
Dynamic Attribute Transformation Modify attributes during rendering:
$renderer = new class($environment) extends HtmlRenderer {
public function renderAttribute($attribute, $element) {
$value = $attribute->getValue();
if ($attribute->getName() === 'data-id') {
$value = 'dynamic-' . Str::uuid();
}
return $attribute->setValue($value)->render();
}
};
Laravel Service Provider Hooks Bind the converter to Laravel’s container with dynamic config:
$this->app->bind(MarkdownConverter::class, function ($app) {
$config = new \League\CommonMark\Config\Config();
$config->addExtension(new AttributesExtension());
if ($app->environment('production')) {
$config->setOption('html_input', 'allow');
}
return new MarkdownConverter($config);
});
Attribute Parsing Overhead Attributes add minimal overhead (~5–10% parsing time). Benchmark with:
$start = microtime(true);
$converter->convert($largeMarkdown);
\Log::info("Parsing time: " . (microtime(true) - $start) . "s");
Caching Converter Instances Reuse the converter instance to avoid re-parsing the environment:
$converter = app(MarkdownConverter::class);
$html = $converter->convert($markdown); // Reuses cached config
Avoid Redundant Extensions
If using league/commonmark-html, ensure AttributesExtension is registered after it to avoid duplicate processing.
How can I help you explore Laravel packages today?