vrana/jush
Jush is a lightweight, standalone syntax highlighter written in JavaScript that runs in the browser with no dependencies. It highlights PHP, JavaScript, CSS, SQL, XML and more, using simple regex rules and customizable styles.
Install it with Composer and check what actually lands in vendor/vrana/jush before writing code — the package ships a historical global function (jush_highlight($language, $code) in jush.php) alongside a thin PHP class wrapper in some versions. Look at the autoload section of the package's composer.json first, then open the entry file; that single file is the entire highlighter, and reading its language table tells you exactly which identifiers (sql, php, js, json, html, css, http, ini, xml, …) are supported.
The second thing to copy is jush.css. The highlighter emits <span class="jush-keyword">-style markup and nothing else — without the stylesheet (or your own mapping of those classes) the output is visually identical to plain text. Put it in resources/css/vendor/jush.css and import it through Vite, or scope it under a .code-view wrapper so the generic class names don't leak into the rest of your app.
A good first use case is the one the library was built for: rendering a SQL query in an admin panel or audit log. One call, no JavaScript, no build step, no browser-side syntax library:
$html = Jush::highlight('sql', $query); // or jush_highlight('sql', $query)
return view('admin.query', ['html' => $html]);
<pre class="code-view jush jush-sql">{!! $html !!}</pre>
If you see unhighlighted text after that, the language id is wrong or the CSS isn't loaded — in that order.
Store raw code, highlight at render time. Never persist the highlighted HTML. Keep the original source (query text, snippet body, migration file contents) in the database and run the highlighter in the presenter/view layer. This lets you change themes, upgrade the library, or fix a grammar without a data migration.
Wrap it in a Blade component. Rather than sprinkling {!! !!} through views, make a <x-code-block :language="'php'" :code="$snippet" /> component that owns the <pre>, the wrapper classes, the empty-state handling, and the CSS. Every raw-HTML output point in the app then flows through one audited place.
Cache highlighted output for hot pages. Highlighting is pure — same language + same string always produces the same HTML. On dashboards showing recent queries or log entries, cache sha1($language.$code) → HTML for an hour. This is the single biggest win, because a query-log page can easily render hundreds of snippets per request.
Map languages once. Build a small extension → jush language id array (php, .blade.php → php, yml → ini, log → http) and centralize it in a helper. Unknown ids silently produce escaped-but-unhighlighted output, which is easy to miss in review.
Use it where JavaScript can't go. Because it's server-side string→string, it works in queued HTML emails (query digests, error notifications), DomPDF exports, and Slack/webhook payload blocks — places where shipping a client-side highlighter is awkward or impossible.
Diff views. Highlight each side independently and then run your diff algorithm on the raw text, using the highlighted HTML only for display of the two panes. Don't diff the HTML; span boundaries will wreck it.
Theming. Don't fight jush.css. Copy it and rewrite the .jush-keyword, .jush-string, .jush-comment, .jush-variable, .jush-function rules to your design tokens, or generate those rules from your Tailwind/DaisyUI palette. The class contract is stable and small.
The output is HTML — never re-escape it. {{ $html }} in Blade will render visible <span> tags. Use {!! !!}, and only for values that came out of the highlighter. The highlighter does escape the source text itself, but any other user-controlled string you concatenate around it does not get that treatment for free.
Multi-line spans break naïve line numbering. A /* ... */ comment or a heredoc is emitted as a single <span> that contains newlines. If you split the highlighted HTML on \n to build a line-number gutter, you'll cut spans in half and corrupt the markup. Either (a) use a CSS counter / a flex gutter on a <pre> with white-space: pre and let the browser handle wrapping, or (b) highlight line-by-line — accepting that block comments lose their continuation state.
Watch input size. The grammars are regex-driven. Multi-megabyte files, or pathological strings full of unmatched quotes, can be slow or blow the PCRE backtrack limit (you'll get null/partial output). Truncate snippets at, say, 20–50 KB in the presenter and show a "view full source" link.
UTF-8 only. The patterns are byte-oriented. Non-UTF-8 database columns, or Latin-1 log files, will mangle the output. Normalize with mb_convert_encoding or Str::ascii-free UTF-8 validation on the way in.
Whitespace is your job, not the library's. It emits literal tabs and newlines. Always render inside <pre> and decide explicitly between white-space: pre (horizontal scroll, correct indentation) and pre-wrap (no scroll, wrapped long SQL). Pick per component, not globally.
There is no editing mode. This is the static-rendering half of the jush project. If you need live highlighting in a textarea as the user types, you want the client-side jush.js instead, or CodeMirror/Prism — don't try to round-trip through PHP per keystroke.
Extension points are limited to adding languages. There's no plugin system: you edit the grammar table in the highlighter file or subclass/fork. If you need custom tokens (e.g. Blade directives, Eloquent model names), a post-highlight preg_replace on the escaped output is usually more practical than maintaining a fork — but be careful not to match inside existing tags.
Debug by echoing the raw string. When highlighting looks wrong, dump the pre-<pre> output and check whether the problem is the grammar (token class missing) or the presentation (class present, no CSS rule). Nine times out of ten it's the second one, because the library was installed without its stylesheet.
How can I help you explore Laravel packages today?