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

Torchlight Commonmark Laravel Package

hydephp/torchlight-commonmark

Adds Torchlight syntax highlighting to the CommonMark PHP parser. It swaps the default code block renderer so fenced code blocks are sent to Torchlight's API and rendered with highlighting, line numbers, diffs, and other features.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a thin CommonMark extension that proxies fenced code blocks through the Torchlight highlighting API. It is a fork of torchlight/torchlight-commonmark updated for Laravel 11 / league/commonmark 2.x and PHP 8.2+ (used primarily inside HydePHP). Treat it as an implementation detail: for a vanilla Laravel app, install the upstream torchlight/torchlight-commonmark unless you specifically need this fork's dependency constraints. The public API is identical either way.

Minimal setup

composer require hydephp/torchlight-commonmark

Create a token at torchlight.dev and add it to .env:

TORCHLIGHT_TOKEN=your-token-here

Wire the converter — do this once, in a service provider, not per-request:

// app/Providers/AppServiceProvider.php
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\MarkdownConverter;
use Torchlight\Commonmark\V2\TorchlightExtension;

$this->app->singleton(MarkdownConverter::class, function () {
    $environment = new Environment([
        'torchlight' => [
            'token' => config('services.torchlight.token'),
        ],
    ]);

    $environment->addExtension(new CommonMarkCoreExtension());
    $environment->addExtension(new TorchlightExtension());

    return new MarkdownConverter($environment);
});

Add the token to config/services.php — not straight into env() at the call site, or it breaks under config:cache:

'torchlight' => [
    'token' => env('TORCHLIGHT_TOKEN'),
],

First use case

A blog/CMS Post model stores markdown in a body column. Rendering it in a controller:

public function show(Post $post, MarkdownConverter $markdown)
{
    return view('posts.show', [
        'post' => $post,
        'html' => (string) $markdown->convert($post->body),
    ]);
}
{{-- resources/views/posts/show.blade.php --}}
<article class="prose">
    {!! $html !!}
</article>

The {!! !!} is mandatory — {{ }} will escape the highlight markup and you'll see raw <pre> tags on screen.

Look first at the upstream Torchlight docs (torchlight.dev/docs) for the fence options syntax; the fork adds nothing there. Also check composer why hydephp/torchlight-commonmark — if it's only present as a Hyde transitive dep, you probably shouldn't be calling it directly at all.

Implementation Patterns

Wrap it in a domain service, not a global helper. You will want to add caching and testing seams quickly.

namespace App\Support;

use League\CommonMark\MarkdownConverter;
use Illuminate\Support\Facades\Cache;

class MarkdownRenderer
{
    public function __construct(private MarkdownConverter $converter) {}

    public function render(string $markdown): string
    {
        return Cache::remember(
            'md:' . hash('xxh128', $markdown),
            now()->addDay(),
            fn () => (string) $this->converter->convert($markdown)
        );
    }
}

The cache key must include the markdown and anything that changes output (theme, extensions). If you later add a theme= default, salt the key.

Pre-render on write, not on read. Torchlight is a synchronous HTTP round-trip per code fence. Rendering on every page view is a latency bug waiting to happen. Store the rendered HTML:

// app/Models/Post.php
protected static function booted(): void
{
    static::saving(function (Post $post) {
        if ($post->isDirty('body')) {
            $post->body_html = app(MarkdownRenderer::class)->render($post->body);
        }
    });
}

Or dispatch a RenderPostMarkdown job on save if posts are large, and render from body_html in the view.

Fence options are your main day-to-day lever. Torchlight parses the info string after the language:

```php title="app/Models/User.php" lines=3,7-9
```

```blade theme="nord" link=https://example.com
```

title gets you a filename header, lines highlights ranges, theme overrides the default. This is where most of the perceived value lives — the syntax highlighting itself is table stakes, the annotations are why people pick Torchlight over a local highlighter.

Keep the default theme in the environment config so you don't repeat it in every fence:

'torchlight' => [
    'token' => config('services.torchlight.token'),
    'theme' => 'material-theme-palenight',
],

Blade directive alternative. If your code samples live in Blade views rather than markdown, skip this package and use torchlight/torchlight-laravel's @torchlight directive. Mixing both in one app is fine but confusing — pick per content-type, not per page.

Testing. The converter hits the network. Don't let that leak into the suite:

// tests/TestCase.php
protected function setUp(): void
{
    parent::setUp();

    if (! env('TORCHLIGHT_TOKEN')) {
        $this->markTestSkipped('No Torchlight token; skipping real API calls.');
    }
}

Better: assert against HTML you stored as a fixture, so the suite never calls Torchlight at all. If you need to exercise the converter itself, snapshot one response and stub your MarkdownRenderer to return it.

Multi-version content. If the same markdown renders on a public site and in a docs sidebar with different themes, parameterize the renderer rather than building two converters — the environment config is cheap to clone, but the singleton as written won't let you.

Gotchas and Tips

The extension is not a Laravel package. No service provider, no facade, no config publish. You wire it yourself, as above. The name is misleading.

env() outside config files silently returns null once config:cache runs. Token becomes null, Torchlight returns a 401, and the failure surfaces as an exception mid-render on a production page. This is the single most common breakage with this class of package.

CommonMark v1 vs v2 namespaces. league/commonmark 2.x needs Torchlight\Commonmark\V2\TorchlightExtension. Using the unversioned TorchlightExtension against 2.x throws a class-not-found or a silent no-op depending on the version. Laravel 11 ships league/commonmark ^2.x, so always use the V2 class. This is, in fact, the entire reason the fork exists — the original package had a stale constraint.

Str::markdown() will not torchlight anything. Laravel's built-in helper constructs its own environment internally. You must use your own MarkdownConverter instance. There's no config switch.

Fenced blocks with no language are not sent to the API and come back as a plain <pre><code>. If your authors write ``` bare fences, those blocks will look inconsistent with the rest of the page. Consider a lint step or defaulting the language to text.

Torchlight's CSS/JS assets are separate. The inline styles cover token colors, so code won't be unstyled, but the copy button and any interactive chrome come from Torchlight's stylesheet and script. Pull them from torchlight.dev (or the Laravel package's published assets) in your layout. If you skip this, it looks fine but feels broken when a user tries to copy an import.

Do not run the output through Purifier / HTML sanitization. You'll strip the inline style attributes and the highlighter's wrapper classes. Sanitize the input markdown if you must, or trust your authors.

Rate limits and latency. Torchlight bills/limits per request; a page with 30 code blocks is 30 API calls on a cold cache. If you can't pre-render, at least cache at the per-block level — but you don't control that here, the extension batches per-convert, not per-block. The practical answer is content-level caching, which is why the MarkdownRenderer wrapper above matters.

Version pinning. Zero stars, zero dependents, one release. It is a compatibility shim, not a maintained library. Pin it ("hydephp/torchlight-commonmark": "1.0.0") and add a note to revisit if/when you upgrade Laravel or CommonMark again — that's exactly the scenario that spawned the fork.

Extension points are thin. There's no public hook to swap the HTTP client or the block renderer without subclassing TorchlightExtension and re-registering renderers by hand. If you need offline rendering in CI, the realistic options are (a) cache the rendered HTML in fixtures, or (b) drop Torchlight for a local highlighter such as spatie/commonmark-highlighter or Shiki via shiki-php. Weigh whether the annotation syntax (lines=, title=) is actually worth the network dependency — for many Laravel apps it isn't.

Debugging a blank block: convert a single fence in php artisan tinker with the token hardcoded, and dd() the raw response. Nine times out of ten it's the token (env/config caching) or the V1/V2 extension mismatch, not the markdown.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
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