In my continued effort to spruce up the site, I added a way to render 🔥 tips and callouts directly from markdown based content. This is all made possible via a CommonMark extension. Turns out it's pretty easy to do because we can extend another CommonMark extension to get most of the functionality we need. Let's jump in and see how it's done!
First of all, here's an example of the new "tip" block we're adding:
This is an example of the new tip block. Pretty cool, huh?
From a high level, here's how we're going to accomplish it:
As mentioned in the Statamic docs, a great hint extension already exists. Let's install it.
1composer require ueberdosis/commonmark-hint-extension
1composer require ueberdosis/commonmark-hint-extension
This extension may work for you as-is, but I wasn't happy with the fact that it added <h2>
tags for the title of tip blocks. Additionally, I wanted to add the not-prose
class to the containing div to undo the Tailwind CSS typography styles so that we can style it ourselves.
The custom extension consists of two parts:
Here's the renderer class, I placed it in app/Extensions/HintRenderer.php
:
1<?php23namespace App\Extensions;45use League\CommonMark\Node\Node;6use League\CommonMark\Renderer\ChildNodeRendererInterface;7use League\CommonMark\Renderer\NodeRendererInterface;8use League\CommonMark\Util\HtmlElement;9use Ueberdosis\CommonMark\Hint;1011class HintRenderer implements NodeRendererInterface12{13 /**14 * Render the hint node and its children15 */16 public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable17 {18 Hint::assertInstanceOf($node);1920 $title = $node->getTitle();21 $title = $title22 ? new HtmlElement(23 tagName: 'div',24 attributes: ['class' => 'hint-title'],25 contents: $title,26 )27 : '';2829 $content = new HtmlElement(30 tagName: 'p',31 attributes: ['class' => 'hint-content'],32 contents: $childRenderer->renderNodes($node->children())33 );3435 return new HtmlElement(36 tagName: 'div',37 attributes: $this->containerAttributes($node),38 contents: sprintf("\n%s\n%s\n", $title, $content)39 );40 }4142 /**43 * The attributes for the hint's containing div44 */45 protected function containerAttributes($node)46 {47 return tap($node->data->get('attributes'), function (&$attrs) use ($node) {48 isset($attrs['class']) ? $attrs['class'] .= ' hint' : $attrs['class'] = 'hint';4950 if ($type = $node->getType()) {51 $attrs['class'] = isset($attrs['class']) ? $attrs['class'].' ' : '';52 $attrs['class'] .= $type;53 }5455 $attrs['class'] .= ' not-prose';5657 return $attrs;58 });59 }60}
1<?php23namespace App\Extensions;45use League\CommonMark\Node\Node;6use League\CommonMark\Renderer\ChildNodeRendererInterface;7use League\CommonMark\Renderer\NodeRendererInterface;8use League\CommonMark\Util\HtmlElement;9use Ueberdosis\CommonMark\Hint;1011class HintRenderer implements NodeRendererInterface12{13 /**14 * Render the hint node and its children15 */16 public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable17 {18 Hint::assertInstanceOf($node);1920 $title = $node->getTitle();21 $title = $title22 ? new HtmlElement(23 tagName: 'div',24 attributes: ['class' => 'hint-title'],25 contents: $title,26 )27 : '';2829 $content = new HtmlElement(30 tagName: 'p',31 attributes: ['class' => 'hint-content'],32 contents: $childRenderer->renderNodes($node->children())33 );3435 return new HtmlElement(36 tagName: 'div',37 attributes: $this->containerAttributes($node),38 contents: sprintf("\n%s\n%s\n", $title, $content)39 );40 }4142 /**43 * The attributes for the hint's containing div44 */45 protected function containerAttributes($node)46 {47 return tap($node->data->get('attributes'), function (&$attrs) use ($node) {48 isset($attrs['class']) ? $attrs['class'] .= ' hint' : $attrs['class'] = 'hint';4950 if ($type = $node->getType()) {51 $attrs['class'] = isset($attrs['class']) ? $attrs['class'].' ' : '';52 $attrs['class'] .= $type;53 }5455 $attrs['class'] .= ' not-prose';5657 return $attrs;58 });59 }60}
Most of the code is taken from the base extension's renderer, but since it's marked as final
, we can't extend from it 😤. I cleaned up the code a bit to use Named Arguments, and changed the title's tagName
to div
instead of h2
. That's it for the renderer, now let's look at the extension class.
This class is a straightforward CommonMark extension. CommonMark extensions generally contain two parts -- a parser to identify the syntax and a renderer to convert it to HTML. In this case we're able to use the parser directly from the Ueberdosis Hint Extension. Then we simply and add our custom renderer instead of the package's default renderer.
1<?php23namespace App\Extensions;45use League\CommonMark\Environment\EnvironmentBuilderInterface;6use Ueberdosis\CommonMark\Hint;7use Ueberdosis\CommonMark\HintExtension as BaseExtension;8use Ueberdosis\CommonMark\HintStartParser;910class HintExtension extends BaseExtension11{12 public function register(EnvironmentBuilderInterface $environment): void13 {14 $environment15 ->addBlockStartParser(new HintStartParser())16 ->addRenderer(Hint::class, new HintRenderer());17 }18}
1<?php23namespace App\Extensions;45use League\CommonMark\Environment\EnvironmentBuilderInterface;6use Ueberdosis\CommonMark\Hint;7use Ueberdosis\CommonMark\HintExtension as BaseExtension;8use Ueberdosis\CommonMark\HintStartParser;910class HintExtension extends BaseExtension11{12 public function register(EnvironmentBuilderInterface $environment): void13 {14 $environment15 ->addBlockStartParser(new HintStartParser())16 ->addRenderer(Hint::class, new HintRenderer());17 }18}
Now that we have our extension's files in place, we can add it in the AppServiceProvider
. This registers the extension and makes Statamic use it automatically while parsing markdown content.
1<?php23namespace App\Providers;45use App\Extensions\HintExtension;6use Illuminate\Support\ServiceProvider;7use Statamic\Facades\Markdown;89class AppServiceProvider extends ServiceProvider10{11 /**12 * Bootstrap any application services.13 *14 * @return void15 */16 public function boot()17 {18 Markdown::addExtensions(fn () => [19 new HintExtension,20 ]);21 }22}
1<?php23namespace App\Providers;45use App\Extensions\HintExtension;6use Illuminate\Support\ServiceProvider;7use Statamic\Facades\Markdown;89class AppServiceProvider extends ServiceProvider10{11 /**12 * Bootstrap any application services.13 *14 * @return void15 */16 public function boot()17 {18 Markdown::addExtensions(fn () => [19 new HintExtension,20 ]);21 }22}
Finally, we can add some styling for our blocks. Here's the styles I landed on (for now):
1.hint {2 @apply border-l-4 p-4 mb-5 shadow rounded-sm text-base;3}45.hint.tip, .hint.important {6 @apply border-primary-500 bg-primary-100;7}89.hint-title {10 @apply font-semibold;11}1213.hint-content {14 @apply mt-1 opacity-80;15}
1.hint {2 @apply border-l-4 p-4 mb-5 shadow rounded-sm text-base;3}45.hint.tip, .hint.important {6 @apply border-primary-500 bg-primary-100;7}89.hint-title {10 @apply font-semibold;11}1213.hint-content {14 @apply mt-1 opacity-80;15}
Now we can render tip blocks using the following syntax!
1:::tip Example Tip Title2Wow, how neat is that?!3:::
1:::tip Example Tip Title2Wow, how neat is that?!3:::
Here's how it looks:
Wow, how neat is that?!
That's how I added tip blocks to markdown. Reach out if you have any questions or find any mistakes in this post. Thanks, and Happy Coding!