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 Marker Extension Laravel Package

n0sz/commonmark-marker-extension

A Laravel and PHP CommonMark extension that adds marker-style highlighting to your Markdown. Wrap text with marker syntax and render it as highlighted output, making it easy to emphasize key passages in docs, notes, and user-generated content.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is. A tiny league/commonmark extension that adds an inline "marker" syntax (highlight-style ==text== delimiters) to the CommonMark parser. It is a drop-in extension, not a Markdown renderer — it plugs into the league/commonmark environment that Laravel already depends on indirectly (via Str::markdown(), GrahamCampbell\Markdown, or your own converter).

Install and verify the constraint first.

composer require n0sz/commonmark-marker-extension
composer show n0sz/commonmark-marker-extension   # confirm package name, version, requires

The GitHub org (noah1400) and the Packagist vendor (n0sz) differ, and the last release was 2022-09-27. Before writing any code, read vendor/n0sz/commonmark-marker-extension/composer.json and confirm two things: the league/commonmark constraint (v1 vs v2 are not API-compatible for extensions) and the PHP constraint against your Laravel version. If the constraint is ^1.0 and your app is on Laravel 10/11 (commonmark v2), stop here — fork it or copy the ~100 lines of parser code into app/Support/Markdown/.

Find the entry point. The extension class is the only thing you need:

ls vendor/n0sz/commonmark-marker-extension/src

First use case. Render user-authored notes where ==important== becomes <mark>important</mark>:

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\MarkdownConverter;

$environment = new Environment([
    'html_input' => 'escape',
    'allow_unsafe_links' => false,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new \Noah1400\CommonMarkMarker\MarkerExtension());

$html = (new MarkdownConverter($environment))->convert($request->input('note'));

Once that works end-to-end, wire it into a singleton (see below) — don't call new MarkdownConverter in controllers.

Implementation Patterns

Bind one configured converter in a service provider. This is the single most important pattern: one environment, configured once, injected everywhere. It also centralises the sanitisation config so no endpoint can accidentally render unescaped HTML.

// app/Providers/MarkdownServiceProvider.php
public function register(): void
{
    $this->app->singleton(MarkdownConverter::class, function () {
        $environment = new Environment(config('markdown'));
        $environment->addExtension(new CommonMarkCoreExtension());
        $environment->addExtension(new GithubFlavoredMarkdownExtension());
        $environment->addExtension(new MarkerExtension());

        // Optional: custom renderer so output is <span class="marker"> not <mark>
        $environment->addRenderer(MarkerNode::class, new MarkerRenderer());

        return new MarkdownConverter($environment);
    });
}

config/markdown.php:

return [
    'html_input' => 'escape',        // never 'allow' on user content
    'allow_unsafe_links' => false,
    'max_nesting_level' => 20,
];

Blade component instead of a directive. A component gives you escaping-by-default and a place to attach classes, and it's testable.

{{-- resources/views/components/markdown.blade.php --}}
<div class="prose">{{ app(\League\CommonMark\MarkdownConverter::class)->convert($slot) }}</div>
<x-markdown>{{ $post->body }}</x-markdown>

Store Markdown, cache HTML. Parsing on every render is wasted CPU on list pages. Render once in an accessor and key the cache on updated_at so edits invalidate naturally:

public function getBodyHtmlAttribute(): string
{
    return Cache::remember(
        "post:{$this->id}:html:{$this->updated_at?->timestamp}",
        now()->addDay(),
        fn () => app(MarkdownConverter::class)->convert($this->body),
    );
}

Swap the rendered element. <mark> carries the browser's yellow-highlight default, which rarely matches a design system. Either add a renderer producing <span class="marker"> (as above) or leave <mark> and style it with a class via a custom renderer — do this in the provider, not by post-processing HTML with str_replace.

Replacing Str::markdown(). If you're on Laravel 9+ and using Str::markdown(), it builds its own GithubFlavoredMarkdownConverter and will not see your extension. Add a macro that delegates to your singleton, and migrate call sites:

Str::macro('marked', fn (?string $text) => app(MarkdownConverter::class)->convert($text ?? ''));

Test at the conversion boundary. These are cheap, fast unit tests and they're your regression net when you eventually fork the package:

it('renders markers', function () {
    expect(app(MarkdownConverter::class)->convert('==hi=='))
        ->toContain('<mark>hi</mark>');
});

it('does not parse markers inside code', function () {
    expect(app(MarkdownConverter::class)->convert('`==hi==`'))
        ->not->toContain('<mark>');
});

Rendering contexts. The converter is pure and stateless after construction, so it's safe to reuse for queued mail, PDF generation, and API resources — but cache the string, not the converter output object, if you serialise.

Gotchas and Tips

Version skew is the real risk. This package has 3 stars, zero dependents, and no release since September 2022. league/commonmark v2 changed the extension contract (EnvironmentBuilderInterface, ConfigurableExtensionInterface); a 2022-era extension may target v1. Check the constraint before you build on it. If it doesn't fit, the honest answer is to vendor the parser into your own App\Markdown\MarkerExtension — it's a few dozen lines, and you gain control over the delimiter rules.

Delimiter precedence beats features. Inline parsers run in registration order. If markers render inside code spans or fight with ~~strikethrough~~ / *emphasis*, the fix is ordering and $cursor->peek() checks in the parser, not more extensions. Test ==a==b==, ====, == unclosed, and \==escaped== explicitly — small extensions usually get unmatched-delimiter handling wrong, and a parser that loops on an unmatched == will hang the request.

XSS surface. The marker output itself is safe, but the extension does not constrain html_input. Leave html_input => 'escape' and allow_unsafe_links => false in the environment config. On top of that, run stored user content through an HTML sanitiser after conversion if you ever flip those flags or add raw HTML support — CommonMark's escaping is not a substitute.

Debugging a bad render. Convert to a document and walk the AST rather than staring at HTML:

$document = app(MarkdownConverter::class)->convertToDocument($markdown);
foreach ($document->iterator() as $node) {
    dump(get_class($node), $node->getLiteral());
}

That tells you instantly whether the marker node was produced and the renderer is at fault, or the parser never fired.

No config to publish. Extensions of this size typically expose no schema, so php artisan vendor:publish gives you nothing and config/commonmark.php won't contain a marker section. Configure via the Environment array you own, not via the package.

Extension points if you need more. $environment->addRenderer(YourNode::class, new YourRenderer()) for markup changes; subclass the inline parser and re-register it for delimiter behaviour changes. Neither requires forking the package.

Unmaintained — pin and plan. Use an exact version ("n0sz/commonmark-marker-extension": "1.0.0"), commit the lock file, and add an it('renders markers') test that will fail loudly if a future composer update of league/commonmark breaks the extension. Given the size, an in-house parser is usually cheaper than the maintenance debt — but for a single feature shipped this week, the package is fine.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo