The league/commonmark
package currently doesn't support alerts (also known as admonitions), but we can add support for alerts using CommonMark Extensions.
Extensions provide a way to group related parsers, renderers, and other components together with pre-defined priorities, configuration settings, and more.
In this blog post, I’ll walk you through the implementation of the alert extension that recognises all the alert types typically supported by GitHub.
To create the alert extension, we need to define the following classes.
Create a Abstract Block
This class represents the Alert
block within the AST (Abstract Syntax Tree).
use League\CommonMark\Node\Block\AbstractBlock;
class Alert extends AbstractBlock
{
public function __construct(string $type)
{
parent::__construct();
$this->data->set('attributes', ['class' => ['alert', "alert-{$type}"]]);
}
}
The $data
property is a convenient way to handle various things related to the block. In this case, we only need to know the type of alert to style our HTML element using the CSS classes we parse in the attributes (e.g., alert-important
).
Create a Block Start Parser
This parser helps identify where the block begins.
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use AlertParser;
class AlertStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
if ($cursor->getNextNonSpaceCharacter() !== '>') {
return BlockStart::none();
}
if (! preg_match('/\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/', $cursor->getLine(), $matches)) {
return BlockStart::none();
}
$alertType = strtolower($matches[1]);
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceToEnd();
$cursor->advanceBySpaceOrTab();
return BlockStart::of(new AlertParser($alertType))->at($cursor);
}
}
Create a Block Continue Parser
This parser helps recognise where the block ends.
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
use League\CommonMark\Parser\Block\BlockContinue;
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
use League\CommonMark\Parser\Cursor;
use Alert;
class AlertParser extends AbstractBlockContinueParser
{
private Alert $block;
public function __construct(readonly string $type)
{
$this->block = new Alert($type);
}
public function getBlock(): Alert
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === '>') {
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
return BlockContinue::at($cursor);
}
return BlockContinue::none();
}
public function isContainer(): bool
{
return true;
}
public function canContain(AbstractBlock $childBlock): bool
{
return true;
}
}
Create a Node Renderer
Once the AlertParser
recognises the end of the block, the node renderer will render the corresponding HTML element with the relevant CSS class.
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use Stringable;
use Alert;
class AlertRenderer implements NodeRendererInterface
{
public function render(Node $node, ChildNodeRendererInterface $childRenderer): Stringable
{
Alert::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$filling = $childRenderer->renderNodes($node->children());
$innerSeparator = $childRenderer->getInnerSeparator();
if ($filling === '') {
return new HtmlElement('div', $attrs, $innerSeparator);
}
return new HtmlElement(
'div',
$attrs,
$innerSeparator.$filling.$innerSeparator
);
}
}
Create a Extension
As I mentioned at the beginning, this class group everything in one place.
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;
use AlertStartParser;
use AlertRenderer;
use Alert;
final class AlertExtension implements ExtensionInterface
{
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addBlockStartParser(new AlertStartParser, 80);
$environment->addRenderer(Alert::class, new AlertRenderer);
}
}
If you're using this extension in an existing project (such as one with custom extensions), make sure the priorities are set in the correct order.
Add to the Environment
Finally, It’s time to use the alert extension in your environment.
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\MarkdownConverter;
use AlertExtension;
$environment = new Environment($config);
$environment->addExtension(new AlertExtension);
If you want to see a real-world usage of this extension, feel free to explore the source code of my website on GitHub. (by the way, I've named it "admonition" 😉)
Hope I saved you several hours! See you in another blog post!
Don’t forget to style your CSS classes!