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.

To push or not to push, this shall no longer be the question

Pushing your code might indicate that you are ready or close to ready, working towards a solution. And some argue it's unacceptable to push code to a branch that might throw an error, leave the application in an unusable state, etc.

On the other hand, accidentally deleting your local, unpushed branch after days of work is a major ... pain.

If the feature is large enough, it takes days, weeks to complete it. Inevitably, one day, you'll leave the code in an undesirable state, close the computer, and leave the house. What do you do? Do you push your code to the feature/xyz branch even if it's not working at that point?

How can the tension between "always push your code" and "don't leave your code in unfit shape" be resolved?

If you have not opened a PR yet, one solution is to rename your branch from feature/ to wip/, prototype/, or anything that sets the right expectation for others.

Or always start with the wip/ and keep it until you are confident enough, then rename it to the feature/ prefix.

The wip/ is a good middle way between pushing and not pushing.

CSS custom property to offset the WordPress admin toolbar height

Some niceties pop up in WordPress versions that are easy to miss.

Here's one that I almost did: "a CSS custom property to offset the admin toolbar height" introduced in version 5.9.

Let's take this CSS code from a default theme:

.screen-height {
min-height: 100vh;
}

.admin-bar .screen-height {
min-height: calc(100vh - 32px);
}

@media (max-width: 782px) {
.admin-bar .screen-height {
min-height: calc(100vh - 46px);
}
}

All this work is required because we might or might not have the admin bar. And if we do, the height of the admin bar changes based on the screen size.

Something similar is found in many themes; it's almost boilerplate code.

With the introduction of --wp-admin--admin-bar--height, if we would refactor this piece of code, we could do the following:

.screen-height {
min-height: calc(100vh - var(--wp-admin--admin-bar--height, 0px));
}

It's much more convenient because we no longer have to care about the logic behind all the cases; we only have to retrieve the exposed calculated height.

RichText and plain text pasting

RichText is at the heart of many WordPress blocks where "text has to be introduced".

But not all RichTexts have to have formatting options, like bold, italic, etc.

The documentation says:

If you want to limit the formats allowed, you can specify using allowedFormats property in your code.

Since allowedFormats accepts an array, if you don't want any formatting, you can pass an empty value:

<RichText
allowedFormats={[]}
/>

The problem is that many copy-paste texts from different sources. If they paste a text containing some formatting, it will be preserved by default.

You will end up with elements that are formatted but with no way of removing them!

The solution is to add __unstablePastePlainText, which is not documented. This will remove all formatting when pasting.

Bonus tip: there is also the __unstableDisableFormats.

<RichText
__unstablePastePlainText
__unstableDisableFormats
/>

Be aware: this is still "unstable" in WordPress 6.1. It might become part of the API or be removed.

WebFinger and why it makes things simpler

If you are playing with the idea of creating a distributed social network or service, there are standards that you have to be aware of.

One of them is the WebFinger protocol.

Let's look at an example of "a decentralised, minimalist microblogging service for hackers" that does not use the WebFinger protocol.

If you want to follow someone with twtxt, you do:

twtxt follow bob http://bobsplace.xyz/twtxt

But Alice, because twtxt is configurable, can choose to expose their posts somewhere other than twtxt.

So when you want to follow Alice, you'll type:

twtxt follow alice http://alice.space/updates.txt

The question: how do you know each individual's twtxt URL if there's no standard, only a sensible default?

You have to find out. But how? The answer is: depends. You can try the default, make an educated guess, you can ask Alice, or Alice can share the link on their website.

This is where WebFinger comes into a play.

WebFinger (...) can be used to discover information about people or other entities on the Internet using standard HTTP methods.

For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar.

Information returned via WebFinger might be for direct human consumption (e.g., looking up someone's phone number), or it might be used by systems to help carry out some operation (...)

To translate this into practice, if twtxt had implemented WebFinger, the following part would look like this:

twtxt follow bob@bobsplace.xyz
twtxt follow alice@alice.space

There would be no need to know the exact location of the twtxt files.

By the way, even though they look like email addresses, they are not. In the same way, those things that look like email addresses on Mastodon (account usernames) are not. Yes, indeed! Mastodon uses WebFinger.

With WebFinger, you can have a standardized protocol to discover the URLs "for you". Since URLs are retrieved instead of directly used, they can be changed without worry since they can be "rediscovered" anytime.

But as twtxt proves, it's not a requirement. While somebody misses it, the project had its fair share of success without it.

Nevertheless, IMHO, if you are playing with the idea of creating a distributed social network or service, use WebFinger.