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 Sup Sub Extensions Laravel Package

ows/commonmark-sup-sub-extensions

Add superscript and subscript support to your CommonMark markdown with this lightweight PHP extension. It registers ^text^ and text style syntax so you can render scientific notation, footnotes, and chemical formulas cleanly in Laravel or any CommonMark-powered app.

Deep Wiki
Context7

Getting Started

1. Install and check the version constraint first

composer require ows/commonmark-sup-sub-extensions
composer show ows/commonmark-sup-sub-extensions
composer show league/commonmark

This package is a thin wrapper over league/commonmark inline parsing, so the only thing that matters up front is which major version of league/commonmark it targets. ^2.0 uses EnvironmentBuilderInterface / ConfigurableExtensionInterface; ^1.x uses ConfigurableEnvironmentInterface. Mismatched majors manifest as a fatal TypeError or "class not found" the moment you register the extension — check composer.json's require block before writing any integration code.

2. Look at src/ before the README

Ten stars and an unknown repo means the README is not the contract — the source is. Spend two minutes confirming:

  • the exact FQCNs (typically something like Ows\CommonMark\Sup\SupExtension and a sibling SubExtension, or one combined class),
  • whether you enable sup and sub independently,
  • the delimiter characters chosen (^…^ for sup, ~…~ for sub is the near-universal convention),
  • whether they emit raw strings or HtmlElement nodes (this decides how you customise output later).

3. Smallest possible conversion

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\MarkdownConverter;
use Ows\CommonMark\Sup\SupExtension;   // verify FQCN in src/
use Ows\CommonMark\Sub\SubExtension;

$environment = new Environment([
    'html_input' => 'escape',
    'allow_unsafe_links' => false,
]);

$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new SupExtension());
$environment->addExtension(new SubExtension());

$converter = new MarkdownConverter($environment);

echo $converter->convert('H~2~O and 2^32^'); // H<sub>2</sub>O and 2<sup>32</sup>

First real use case: chemistry and math notation in user- or editor-authored content — H~2~SO~4~, CO~2~, 10^-9^, x^2^. It's a two-character win over typing raw HTML, and it keeps stored content portable (see the storage pattern below).


Implementation Patterns

Pattern 1 — Register once as a singleton (any framework, or plain PHP)

Building an Environment is the expensive part. Never do it per-render inside a loop or a Blade partial.

// config/services.php-ish / bootstrap
$container->singleton(MarkdownConverter::class, function () {
    $environment = new Environment([
        'html_input' => 'escape',
        'allow_unsafe_links' => false,
        'max_nesting_level' => 20,
    ]);
    $environment->addExtension(new CommonMarkCoreExtension());
    $environment->addExtension(new SupExtension());
    $environment->addExtension(new SubExtension());
    // ... your other extensions, in a deliberate order

    return new MarkdownConverter($environment);
});

Pattern 2 — Laravel via laravel-markdown

If the app already uses graham-campbell/laravel-markdown, do not build a second environment. Publish config/markdown.php and append to the extensions array — that array takes extension class-strings and is the supported extension point:

// config/markdown.php
'extensions' => [
    Ows\CommonMark\Sup\SupExtension::class,
    Ows\CommonMark\Sub\SubExtension::class,
],

Then Markdown::render($text) picks them up everywhere, and you inherit its Blade directive, Str::markdown() macro, and any sanitisation config for free.

Pattern 3 — Store Markdown, render on read, cache the HTML

The best reason to use delimiters instead of inline <sup> HTML is portability: the stored source stays readable and re-renderable if you later swap renderers or tighten the sanitiser.

// write
$post->body = $request->validated('body');   // raw markdown, incl. H~2~O

// read
$html = Cache::remember("post.html.{$post->id}.v{$post->updated_at->timestamp}", 3600,
    fn () => Markdown::render($post->body)->getContent());

Bump a version key when you change the environment (adding an extension, changing html_input) or stale HTML survives the deploy.

Pattern 4 — Override the output without forking the package

The sup/sub nodes are ordinary inline nodes, so you can re-render them with your own NodeRendererInterface and win specificity by priority:

use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;

final class SupRenderer implements NodeRendererInterface
{
    public function render(Node $node, ChildNodeRendererInterface $childRenderer): HtmlElement
    {
        return new HtmlElement(
            'sup',
            ['class' => 'notation-sup'],
            $childRenderer->renderNodes($node->children())
        );
    }
}

$environment->addRenderer(Sup::class, new SupRenderer(), 10); // priority > 0

Reach for this rather than subclassing the extension: it survives package updates, and it keeps your markup changes (Tailwind classes, aria-*, data-*) in one file.

Pattern 5 — Composition with GFM and friends

Load order matters only where delimiter characters collide (see Gotchas). A safe, boring order:

$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension()); // tables, autolinks, ~~strike~~
$environment->addExtension(new SupExtension());
$environment->addExtension(new SubExtension());
$environment->addExtension(new AttributesExtension());            // if you need {: .class }

If GFM strikethrough is enabled you almost certainly cannot also enable this package's subscript extension — resolve that deliberately, not by trial and error in production.

Pattern 6 — Test the contract, not the implementation

public function test_subscript_renders(): void
{
    self::assertSame(
        "<p>H<sub>2</sub>O</p>\n",
        $this->converter()->convert('H~2~O')->getContent()
    );
}

Pin these with assertSame on the exact string. Inline delimiter parsing is where edge cases hide (a^b^c, 2^32^,, escaped \~), and a one-line regression test per case is cheaper than discovering it in a rendered email.


Gotchas and Tips

Licence is the first thing to check, not the last. GPL-3.0 is copyleft and this is a library you link into your app. For a closed-source commercial Laravel product this is a genuine compliance question, not a footnote — get it cleared before it ships, or plan to reimplement. The delimiters are ~40 lines of DelimiterProcessorInterface; a clean-room implementation is often cheaper than a licence review.

Delimiter collision with ~~strikethrough~~. Both extensions want ~. league/commonmark's DelimiterProcessorCollection rejects two processors for the same opening character, so enabling the GFM strikethrough extension and this subscript extension together throws at environment build time — a hard failure, not a silent mis-render. If you need both, keep strikethrough and drop subscript, or patch the processor to require ~ while GFM owns ~~ only, and write tests for both.

Accidental matches in prose. ^ and ~ appear in shell snippets, tilde-home paths and regexes. ~/config won't match (no closing delimiter), but foo~bar~baz will silently become foo<sub>bar</sub>baz. Escape with a backslash (\~) and, if this is user content, say so in your editor help text rather than after the first bug report.

html_input is your real XSS boundary, not the extension. The extension only produces <sup>/<sub> around already-escaped inline content. Set 'html_input' => 'escape' (or 'strip') and 'allow_unsafe_links' => false explicitly — the defaults are permissive, and a custom renderer that concatenates raw strings instead of building HtmlElement nodes reintroduces the hole.

Reuse the Environment; rebuild after config changes. Per-request instantiation is the classic performance trap in Laravel apps that render many snippets. But the corollary bites too: if the environment is a singleton and you change extensions mid-request (feature flag, tenant config), later renders see the mutated parser. Build one environment per configuration, not one per request.

max_nesting_level will throw on pathological input. Deeply nested emphasis plus delimiters can blow the limit and raise an exception rather than degrading. Set it deliberately (10–20 is plenty for prose) and wrap rendering of untrusted content in a try/catch that falls back to plain escaped text.

Low-traffic packages go unmaintained quietly. Pin an exact version in composer.lock, and note the release date looks anomalous — verify tags actually exist on Packagist and that the tag matches the code you read. If the project stalls, the fallback is cheap: one DelimiterProcessorInterface per delimiter plus one small NodeRendererInterface, roughly an afternoon, and you own the licence.

Debugging tips. InvalidArgumentException: Cannot add two delimiter processors for the same character means a delimiter collision, not a bad install. TypeError in addExtension() means a league/commonmark major mismatch. When output looks wrong, dump $converter->convert($src) for a minimal snippet rather than the real document — delimiter bugs are almost always local to a few characters of context.

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.
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
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core