Use CommonMark Markdown parser with Jigsaw

When I was considering using Jigsaw, a PHP static site generator, quite early, I ran into a detail I didn't like. It's the default Markdown parser that it uses.

There's nothing wrong with the PHP Markdown library from Michel Fortin per se. It just does not support the GitHub-Flavored Markdown (GFM). One extension that GFM has, and I use, is the Task list items:

- [ ] foo
- [x] bar

I'm not the only one who would prefer a parser that supports GFM. Somebody already brought this topic up, and supposedly it will come in version 2 of Jigsaw.

Since there's no timeline for version 2, that can mean anything.

But if you want it today and not tomorrow, what can you do? Swap the parser yourself.

Jigsaw uses Laravel's service container, and it exposes it during the bootstrapping process.

Here's the provider where the Markdown service is registered:

<?php
 
namespace TightenCo\Jigsaw\Providers;
 
use TightenCo\Jigsaw\Support\ServiceProvider;
use Mni\FrontYAML\Markdown\MarkdownParser as FrontYAMLMarkdownParser;
 
class MarkdownServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton('markdownParser', fn (Container $app) => new MarkdownParser);
$this->app->bind(FrontYAMLMarkdownParser::class, fn (Container $app) => $app['markdownParser']);
}
}

And here is where the container is made available for us:

<?php
 
namespace TightenCo\Jigsaw\Providers;
 
use TightenCo\Jigsaw\Support\ServiceProvider;
 
class BootstrapFileServiceProvider extends ServiceProvider
{
public function register(): void
{
if (file_exists($bootstrapFile = $this->app->path('bootstrap.php'))) {
$container = $this->app;
 
include $bootstrapFile;
}
}
}

All these results in us being able to do, for example, this in the bootstrap.php

<?php
 
/** @var \Illuminate\Container\Container $container */
$container->has('markdownParser'); // -> true

Using the CommonMark package

Laravel's service container implements PSR-11 (Container Interface). But the PSR does not dictate how something is added or bound to the container. Also, it does not specify if there should be a way to overwrite something. These details are always specific to the implementation.

From our service container implementation perspective, overwriting something is the same as adding it, and we already saw how the default parser is bound.

CommonMark does not support the tasks list extension by default, but the configuration is well documented.

Besides this, the only thing that is left, is satisfying the interfaces we are working with, similary to the default:

use Mni\FrontYAML\Markdown\MarkdownParser as FrontYAMLMarkdownParser;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Attributes\AttributesExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
 
$container->singleton(
'markdownParser',
fn() => new class implements FrontYAMLMarkdownParser {
readonly private MarkdownConverter $parser;
 
public function __construct()
{
$environment = new Environment([]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AttributesExtension());
$environment->addExtension(new TaskListExtension());
 
$this->parser = new MarkdownConverter($environment);
}
 
public function parse($markdown)
{
return $this->parser->convert($markdown);
}
}
);

Because I did not work extensively, I'm unsure if it's an absolute requirement to implement the magic __get and __set methods. With a simple blog, it worked without those.