torchlight/engine
Torchlight Engine is a standalone PHP syntax highlighting engine built on TextMate grammars and VS Code themes. It renders accurate, themeable code blocks without Node.js or client-side JavaScript, and powers Torchlight's Laravel package while working in any PHP project.
You almost certainly won't composer require torchlight/engine directly. In a Laravel app you install torchlight/torchlight-laravel, which pulls this package in as the actual rendering engine. Requiring engine alone means wiring up config, caching, and Blade rendering yourself. Only do that if you're building a non-Laravel integration or a static-site generator on top of it.
Minimal path:
composer require torchlight/torchlight-laravelphp artisan torchlight:install (publishes config/torchlight.php, registers the Blade components).env as TORCHLIGHT_TOKEN — free for open source/public repos, paid otherwise. The release notes for 1.0.0 are thin ("consult the readme"), so confirm the exact driver/engine config key and its default before you wire anything up.<x-torchlight-code language="php">
$user->update(['name' => 'Ada']);
</x-torchlight-code>
Where to look first in this repo, in order:
src/Engine.php / src/Torchlight.php — the render entry point, request assembly, response handlingsrc/Generators/ — the pipeline stages that turn a raw API response into HTML (attributes, block wrappers, diffs, line numbers)src/Annotations/ — one class per [tl! ...] annotation; this is the real feature surfacesrc/Cache/ — the cache interface plus the array/file/PSR implementationsFirst real use case: annotated diffs in documentation. The thing Torchlight gives you over plain Shiki/Pygments is inline annotations — highlight a line, mark a line as added/removed, collapse a range — without hand-writing HTML:
// app/Services/Billing.php
$invoice->markPaid(); // [tl! highlight]
That single trailing comment is the 90% use case. Start there, then reach for per-block options.
Per-block options via the torchlight! comment. Rather than passing an options array everywhere, the engine reads a JSON comment at the top of the code block. This keeps rendering config with the snippet, which matters when docs live in Markdown or the DB:
// torchlight! {"lineNumbers": true, "diffMarkers": true}
Programmatic rendering for stored content. If code snippets live in a CMS or model, don't rely on Blade components — call the engine through the Laravel facade and store the rendered HTML:
use Torchlight\Torchlight;
$html = Torchlight::render($snippet->code, $snippet->language, [
'theme' => 'github-dark',
'lineNumbers' => true,
]);
Render at write time (or in a queued job), persist the HTML, and invalidate on save. Rendering on every page view means a highlight pass per block — an HTTP round trip if you're on the hosted API, a CPU cost if you're on Phiki. Either way it's the biggest performance mistake people make with this package.
Pick one engine per environment and pin it in config. With Phiki 2 available you can sensibly run local highlighting in tests/CI and the hosted API in production — or Phiki everywhere if you want to drop the token entirely. Set this in config/torchlight.php, not per block, so a given snippet always renders through the same pipeline. Mixing engines within one page gives you inconsistent class names and subtly different output.
Annotation stacking. Annotations compose; put multiple on one line separated however the syntax allows, and use [tl! collapse:start] / [tl! collapse:end] around long boilerplate:
use Illuminate\Support\Facades\Http; // [tl! collapse:start]
use Illuminate\Support\Str;
// [tl! collapse:end]
$response = Http::get('https://api.example.com'); // [tl! focus]
$response->json(); // [tl! highlight]
Diff-style snippets. [tl! diff] at the block level and per-line [tl! ++] / [tl! --] give you add/remove markers without maintaining two copies of a file. Combine with [tl! reindex] when you're showing lines 400–412 of a file but want the gutter to read 1–13.
Caching pattern. Wrap rendering in your own cache keyed on a hash of (code, language, options, engine name, engine version, theme). The engine ships cache backends, but the cache key correctness is yours — include the engine identity and version, because rendered markup changes between releases and differs between the hosted API and Phiki.
$key = 'torchlight:'.hash('xxh128', implode('|', [
$code, $language, json_encode($options), $engine, Torchlight::version(),
]));
return Cache::rememberForever($key, fn () => Torchlight::render($code, $language, $options));
Queue it for bulk. Importing 500 snippets in a seeder or migration? Chunk them into a job. The hosted engine will rate-limit you if you fan out unbounded; the Phiki engine will happily peg a CPU core instead. Either way, cap concurrency.
Integration tip — Markdown pipelines. If you render Markdown to HTML (CommonMark, league/commonmark), do the Torchlight pass after Markdown conversion or as a custom fenced-code renderer. Trying to run annotations through the Markdown parser first will mangle the // comments.
v1.0.0 is a major bump with a two-line changelog. "Refactors, bug fixes and new features! Consult the readme :)" tells you nothing about what broke. Before upgrading, diff the README against the version you're on, run your rendering tests, and if you snapshot-test HTML, expect diffs — Phiki 2 alone can change theme names, the supported language set, and generated class names. Read the actual source diff for anything you extend (custom annotations, custom generators, cache adapters); those are the surfaces a refactor is most likely to move.
Missing token behaviour is now config-dependent in a new way. If you're on the hosted API and TORCHLIGHT_TOKEN is absent, you get either an exception or un-highlighted <pre><code> depending on config — if your blocks suddenly look flat in CI or a fresh clone, check the token first. But with Phiki 2 you can remove the token from the equation entirely for tests and local dev. Add a test that asserts your chosen engine is actually the one being used; a silent fallback is the failure mode that wastes an afternoon.
Network dependency in tests and CI. Every uncached render against the hosted API is an outbound HTTP call. In tests, either run the Phiki engine, seed the cache, stub the HTTP client, or point at the array cache. Otherwise your suite becomes flaky and slow the moment torchlight.dev has a blip. This remains the #1 operational trap — v1.0.0 just gives you a real escape hatch.
Switching engines poisons your cache. Hosted-API HTML and Phiki HTML are not byte-identical. If you cache rendered output and change the engine (or upgrade Phiki), old rows stay stale: new annotations missing, changed CSS classes, different wrapper markup. Namespace your cache key with the engine name and version so both upgrades and engine switches self-invalidate. Also key on the theme — theme is frequently the thing people forget.
Feature parity between engines is not guaranteed. Phiki is a PHP port, not the hosted renderer. Verify every annotation you rely on (diff, collapse, focus, highlight, reindex, wordWrap) renders the same on the engine you ship with, in a browser and not just against class-name assertions.
Annotation syntax is comment-token dependent. [tl! highlight] only works as a trailing comment in a language the highlighter knows. In JSON or plain text there's no comment token, so annotations silently don't apply. Per-block options via // torchlight! {...} have the same constraint — use the language attribute to force a grammar when the file extension lies. Phiki 2 changes the bundled grammar set, so a language that used to resolve may now not.
Diff and highlight overlap. Stacking [tl! highlight] on a line already marked [tl! ++] produces combined styling that can look muddy depending on theme. Verify against your actual theme in a browser, not just in tests — assertions on class names pass while the output looks wrong.
reindex is per-block stateful. It rewrites gutter numbers for the whole block. Mixing it with lineNumbers: false is noise, and combining it with collapse ranges gets confusing fast. Keep annotated snippets short and you'll never need it.
Output is trusted HTML. The generated markup is not escaped for you in the sense you might expect from a templating engine — don't pipe user-controlled content through the renderer and then into a context where injected HTML is dangerous. Treat code snippets as developer-authored content.
Extension points worth knowing about: the annotation registry (add a custom [tl! yourAnnotation] by implementing the annotation base class and registering it), the cache interface (swap in Redis/Memcached via the PSR adapter), the engine/driver selection (hosted API vs local Phiki), and the generator pipeline (post-process HTML — e.g. add copy buttons or permalinks — without forking the package). If you find yourself string-replacing the engine's output, you probably want a custom generator instead.
Debug the request, not the HTML. When a block renders wrong, dump the outgoing payload (code + options + theme + which engine handled it) first. Nine times out of ten it's an option name typo, an annotation the parser didn't recognize, or the wrong engine for that environment — the HTML is just downstream symptom.
How can I help you explore Laravel packages today?