Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Commonmark Extras Laravel Package

league/commonmark-extras

league/commonmark-extras provides additional CommonMark extensions for PHP's league/commonmark parser, adding features like autolinks, smart punctuation, strikethrough, tables, and task lists so you can render richer Markdown with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

First, confirm you actually need this package. league/commonmark-extras is archived (last release 2020-04-04, 0 dependents) and exists only to bundle extensions for league/commonmark 1.x. Every extension it ships now lives in commonmark 2.x core or in a maintained standalone package. If your app is on commonmark 2.x, composer require will fail resolution — that is the package telling you to migrate, not a bug to work around.

Where to look first:

  1. composer.json — check your league/commonmark constraint. If it is ^2.0, stop; go to the migration table below.
  2. vendor/league/commonmark-extras/src/ — the directory listing is the documentation. Confirms which extensions you actually use (typically Autolink, SmartPunctuation, Strikethrough, Table, TaskList) and the exact class names (League\CommonMark\Extras\*, plus a bundle extension class that registers them all).
  3. vendor/league/commonmark-extras/README.md — the install snippet is the canonical wiring example for 1.x.

Minimal 1.x wiring (the only shape that matters here):

use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;

$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new League\CommonMark\Extras\CommonMarkExtrasExtension()); // or individual Extras\*Extension classes

$converter = new CommonMarkConverter([
    'html_input'         => 'strip',
    'allow_unsafe_links' => false,
], $environment);

echo $converter->convertToHtml($markdown);

First use case to target: user-authored content with GFM-ish needs — tables in docs, ~~strikethrough~~, task lists in issue bodies, bare URLs in notes. That is the whole value proposition.

If you are starting fresh or already on 2.x, skip this package entirely:

Extras (1.x) Use instead (2.x core)
Autolink League\CommonMark\Extension\Autolink\AutolinkExtension
SmartPunctuation League\CommonMark\Extension\SmartPunctuation\SmartPunctuationExtension
Strikethrough League\CommonMark\Extension\Strikethrough\StrikethroughExtension
Table League\CommonMark\Extension\Table\TableExtension
TaskList League\CommonMark\Extension\TaskList\TaskListExtension
$environment = new Environment([
    'html_input' => 'strip',
    'allow_unsafe_links' => false,
]);
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$converter = new MarkdownConverter($environment);

Implementation Patterns

Wrap the converter in one bound singleton. Never instantiate CommonMarkConverter per call site — you want a single immutable environment and a single place to change config.

// app/Markdown/MarkdownRenderer.php
final class MarkdownRenderer
{
    public function __construct(private MarkdownConverterInterface $converter) {}

    public function toHtml(?string $markdown): string
    {
        return $this->converter->convertToHtml($markdown ?? '')->getContent();
    }
}

// app/Providers/MarkdownServiceProvider.php
$this->app->singleton(MarkdownRenderer::class, function () {
    $environment = Environment::createCommonMarkEnvironment();
    $environment->addExtension(new CommonMarkExtrasExtension());
    $environment->mergeConfig([
        'html_input'         => 'strip',
        'allow_unsafe_links' => false,
    ]);
    return new MarkdownRenderer(new CommonMarkConverter([], $environment));
});

Render-on-write, not on-read. Parsing tables and autolinks on every page view is pure waste. Store both the source and the rendered HTML, and invalidate on the source hash:

protected static function booted(): void
{
    static::saving(function (Post $post) {
        if ($post->isDirty('body')) {
            $post->body_html = app(MarkdownRenderer::class)->toHtml($post->body);
            $post->body_hash = hash('xxh128', $post->body);
        }
    });
}

For content you cannot persist, Cache::remember('md:'.hash('xxh128', $md), now()->addDay(), fn () => $renderer->toHtml($md)) gets you 95% of the benefit with zero schema changes.

Per-surface configuration. Comments and docs want different rules. Register two named converters rather than one global one — e.g. markdown.comments (no tables, no raw HTML, no autolink of javascript:-adjacent protocols) and markdown.docs (everything on). In 1.x that means two Environment instances; each addExtension() call adds parser/renderer passes, so keeping the comment surface lean is a real perf and attack-surface win.

Sanitization is your job, not the package's. The extras widen the HTML surface (tables emit <table>/<th>/<td>, task lists emit <input type="checkbox" disabled>). html_input => 'strip' handles raw HTML in the source, but for untrusted authors run the output through HTML Purifier or mews/purifier with a whitelist that includes table, thead, tbody, tr, th, td, input[type=checkbox][disabled], del. Do not rely on allow_unsafe_links alone.

Blade usage — escape deliberately. {!! $renderer->toHtml($post->body) !!} is correct only if the output is already sanitized. Pair it with a dedicated @markdown($post->body) directive so the unescaped echo appears in exactly one grep-able place.

Testing. Snapshot the AST or the HTML per extension, one data provider per feature, so a commonmark upgrade that changes whitespace or attribute order shows up as a small diff rather than a mystery layout bug:

#[DataProvider('tableCases')]
public function test_table_renders(string $md, string $expected): void
{
    $this->assertSame($expected, app(MarkdownRenderer::class)->toHtml($md));
}

Gotchas and Tips

Composer conflicts are the #1 time sink. league/commonmark-extras pins league/commonmark to the 1.x line. If another package (or a fresh composer update) pulls 2.x, you get an unsatisfiable constraint and often a cascade of "downgrade" proposals touching unrelated packages. Diagnose with composer why-not league/commonmark 2.0 and composer show -a league/commonmark-extras before touching anything else. Pin league/commonmark explicitly in composer.json while you plan the migration.

PHP 8.1+ emits deprecations from the 1.x tree (implicit nullable params, dynamic properties, \ReturnTypeWillChange). You will see them in logs and in PHPUnit's strict deprecation mode. Do not "fix" them by editing vendor/; either suppress the specific deprecation IDs or migrate. Editing vendor guarantees the next composer install reintroduces the problem.

Extension order and inline priority matter. In 1.x, autolinking and strikethrough both register inline parsers with priorities; if you hand-register individual Extras\*Extension classes out of order or mix them with your own custom inline parser, you can get ~~text~~ left literally rendered or http:// inside an existing link double-wrapped. Register the bundle extension, or register in the order the README shows. If a custom inline parser needs to win, set its priority above the autolink parser rather than reordering extensions.

SmartPunctuation surprises people. Curly quotes and en/em dashes change the output bytes, which means: search over rendered HTML fails for "quoted" strings; clipboard/copy tests break; any downstream str_replace on the HTML silently no-ops. Keep smart punctuation off for anything user-searchable, or search the Markdown source, not the HTML.

Autolink has narrow, opinionated rules. It does not linkify bare emails in all positions, does not touch text already inside a link, and prefixes www. with http:// (not https://). If you need https:// or a custom scheme whitelist, that is a config/extension tweak, not a package bug.

Tables are pipe-only. No colspan/rowspan, alignment only via :---:/:---:/---:, and no block-level content inside cells (no lists, no code fences). Users will try; write your help text accordingly. Escaping a literal pipe is \|.

Task lists emit raw <input type="checkbox" disabled> with no wrapping form or label association. If you sanitize output you must whitelist it explicitly or checkboxes vanish silently — a classic "it worked in dev" bug. Also expect <li> class names to differ from what your CSS was written against if you switch packages.

TaskList/Table styling expectations don't survive migration. Custom renderers that override Block\ElementRenderer or HtmlBlockRenderer are the most common casualty when moving to 2.x — the renderer class hierarchy changed. Audit for instanceof checks and addRenderer() calls before upgrading.

Debugging the config tree. When an option appears ignored, dump the resolved configuration from the environment object rather than guessing; the merged config (defaults + your overrides + extension-registered defaults) is the source of truth. Extension-registered defaults can silently win over your array if you pass config to the wrong factory argument.

Extension points worth knowing. In 1.x the Environment is the whole API: addExtension(), addRenderer(), mergeConfig(), plus dispatch() for the event system. If you need behavior the bundle doesn't provide, write a custom ExtensionInterface implementation against the environment rather than subclassing the Extras classes — the Extras classes are not designed for inheritance and are frozen.

Treat this as frozen. It is archived: no security fixes, no PHP 8.x/9 support guarantees, no commonmark 2/3 compatibility ever. Budget the migration as ordinary maintenance debt, and put a composer.json comment or an ADR next to the dependency so the next developer doesn't rediscover all of the above from scratch.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings