When reading technical blogpost around the web, you might have noticed that code highlighting is not always perfect.
Shiki is the code highlighter that uses the textmate parser VSCode uses under the hood. The code highlighting it provides is near perfect, even when using modern syntax. It supports 100+ languages (via our package Blade is supported too), and all VS Code themes.
I’m proud to announce that we have released three new Spatie packages that make it easy to use Shiki in your PHP projects:
- shiki-php: makes it easy to call Shiki from PHP to highlight a given code snippet
- commonmark-shiki-highlighter: allows commonmark to highlight all code snippets in a markdown fragment
- laravel-markdown: a batteries included Laravel package that offers a Blade component to easily render Markdown with highlighted code snippets and a class to render Markdown manually.
We’re already using this package to render all our documentation pages, our guidelines, and this very blog you are reading.
A little bit of history
On this very blog, spatie.be and related sites, we have relied on our trusty commonmark-highlighter package for the past couple of years.
This package offers a renderer class that can be used with the league’s popular commonmark package. This renderer uses highlight.php to render code blocks in Markdown. Highlight.php is inspired by hightlight.js. While the highlighting provides by highlight.php/js is pretty good, it’s not perfect.
A couple of months ago, Miguel Piedrafita blogged about highlighting using Shiki. Shiki is the code renderer behind VSCode. Its highlighting is near perfect, and it can even handle modern PHP Syntax. Miguel’s blog post mentioned how you could use Shiki in a JS / Node environment. My colleague Rias used that blog post as inspiration to toy with Shiki and bringing its magic to the PHP world.
Introducing Shiki PHP
The first package we released, spatie/shiki-php, allows you to highlight any given code snippet.
Here’s a good first example where we are going to highlight a simple PHP script. Using Shiki is simple, you pass it some code, the language of the code you are passing and one of the many available themes.
<span class="line"><span>use</span><span> </span><span>SpatieShikiPhpShiki</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>Shiki</span><span>::</span><span>highlight</span><span>(</span></span>
<span class="line"><span> </span><span>code</span><span>: </span><span>'<?php echo "Hello World"; ?>'</span><span>,</span></span>
<span class="line"><span> </span><span>language</span><span>: </span><span>'php'</span><span>,</span></span>
<span class="line"><span> </span><span>theme</span><span>: </span><span>'github-light'</span><span>,</span></span>
<span class="line"><span>);</span></span>
<span class="line"></span>
This is the html that Shiki will be returned:
<span class="line"><span><</span><span>pre</span><span> </span><span>class</span><span>=</span><span>"shiki"</span><span> </span><span>style</span><span>=</span><span>"</span><span>background-color: #2e3440ff"</span><span>><</span><span>code</span><span>><</span><span>span</span><span> </span><span>class</span><span>=</span><span>"line"</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #81A1C1"</span><span>></span><span><</span><span>?</</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #D8DEE9FF"</span><span>>php </</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #81A1C1"</span><span>>echo</</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #D8DEE9FF"</span><span>> </</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #ECEFF4"</span><span>></span><span>"</span><span></</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #A3BE8C"</span><span>>Hello World</</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #ECEFF4"</span><span>></span><span>"</span><span></</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #81A1C1"</span><span>>;</</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #D8DEE9FF"</span><span>> </</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #81A1C1"</span><span>>?</span><span>></span><span></</span><span>span</span><span>></</span><span>span</span><span>></</span><span>code</span><span>></</span><span>pre</span><span>></span></span>
<span class="line"></span>
In the browser, this will be displayed as:
<span class="line"><span><?</span><span>php</span><span> </span><span>echo</span><span> </span><span>"Hello World"</span><span>; </span><span>?></span></span>
<span class="line"></span>
This very blog you are reading uses Shiki to highlight code snippets. Let’s try a few more advanced examples. Here’s a very meta example in which we highlight the code of the Shiki
class from the package.
<span class="line"><span>namespace</span><span> </span><span>SpatieShikiPhp</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>use</span><span> </span><span>SymfonyComponentProcessExceptionProcessFailedException</span><span>;</span></span>
<span class="line"><span>use</span><span> </span><span>SymfonyComponentProcessExecutableFinder</span><span>;</span></span>
<span class="line"><span>use</span><span> </span><span>SymfonyComponentProcessProcess</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>class</span><span> </span><span>Shiki</span></span>
<span class="line"><span>{</span></span>
<span class="line"><span> </span><span>public</span><span> </span><span>static</span><span> </span><span>function</span><span> </span><span>highlight</span><span>(</span></span>
<span class="line"><span> </span><span>string</span><span> $code,</span></span>
<span class="line"><span> </span><span>string</span><span> $language </span><span>=</span><span> </span><span>'php'</span><span>,</span></span>
<span class="line"><span> </span><span>string</span><span> $theme </span><span>=</span><span> </span><span>'nord'</span><span>,</span></span>
<span class="line"><span> </span><span>array</span><span> $highlightLines </span><span>=</span><span> [],</span></span>
<span class="line"><span> </span><span>array</span><span> $addLines </span><span>=</span><span> [],</span></span>
<span class="line"><span> </span><span>array</span><span> $deleteLines </span><span>=</span><span> [],</span></span>
<span class="line"><span> </span><span>array</span><span> $focusLines </span><span>=</span><span> [],</span></span>
<span class="line"><span> )</span><span>:</span><span> </span><span>string</span><span> {</span></span>
<span class="line"><span> </span><span>return</span><span> (</span><span>new</span><span> </span><span>static</span><span>())</span><span>-></span><span>highlightCode</span><span>($code, $language, $theme, [</span></span>
<span class="line"><span> </span><span>'highlightLines'</span><span> </span><span>=></span><span> $highlightLines,</span></span>
<span class="line"><span> </span><span>'addLines'</span><span> </span><span>=></span><span> $addLines,</span></span>
<span class="line"><span> </span><span>'deleteLines'</span><span> </span><span>=></span><span> $deleteLines,</span></span>
<span class="line"><span> </span><span>'focusLines'</span><span> </span><span>=></span><span> $focusLines,</span></span>
<span class="line"><span> ]);</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>getAvailableLanguages</span><span>()</span><span>:</span><span> </span><span>array</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> $shikiResult </span><span>=</span><span> </span><span>$this</span><span>-></span><span>callShiki</span><span>(</span><span>'languages'</span><span>);</span></span>
<span class="line"></span>
<span class="line"><span> $languageProperties </span><span>=</span><span> </span><span>json_decode</span><span>($shikiResult, </span><span>true</span><span>);</span></span>
<span class="line"></span>
<span class="line"><span> $languages </span><span>=</span><span> </span><span>array_map</span><span>(</span></span>
<span class="line"><span> </span><span>fn</span><span> ($properties) => $properties[</span><span>'id'</span><span>],</span></span>
<span class="line"><span> $languageProperties</span></span>
<span class="line"><span> );</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>sort</span><span>($languages);</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> $languages;</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>__construct</span><span>(</span></span>
<span class="line"><span> </span><span>protected</span><span> </span><span>string</span><span> $defaultTheme </span><span>=</span><span> </span><span>'nord'</span></span>
<span class="line"><span> ) {</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>getAvailableThemes</span><span>()</span><span>:</span><span> </span><span>array</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> $shikiResult </span><span>=</span><span> </span><span>$this</span><span>-></span><span>callShiki</span><span>(</span><span>'themes'</span><span>);</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> </span><span>json_decode</span><span>($shikiResult, </span><span>true</span><span>);</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>languageIsAvailable</span><span>(</span><span>string</span><span> $language)</span><span>:</span><span> </span><span>bool</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> </span><span>return</span><span> </span><span>in_array</span><span>($language, </span><span>$this</span><span>-></span><span>getAvailableLanguages</span><span>());</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>themeIsAvailable</span><span>(</span><span>string</span><span> $theme)</span><span>:</span><span> </span><span>bool</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> </span><span>return</span><span> </span><span>in_array</span><span>($theme, </span><span>$this</span><span>-></span><span>getAvailableThemes</span><span>());</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>highlightCode</span><span>(</span><span>string</span><span> $code, </span><span>string</span><span> $language, </span><span>?string</span><span> $theme </span><span>=</span><span> </span><span>null</span><span>, </span><span>?array</span><span> $options </span><span>=</span><span> [])</span><span>:</span><span> </span><span>string</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> $theme </span><span>=</span><span> $theme </span><span>??</span><span> </span><span>$this</span><span>-></span><span>defaultTheme;</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> </span><span>$this</span><span>-></span><span>callShiki</span><span>($code, $language, $theme, $options);</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>protected</span><span> </span><span>function</span><span> </span><span>callShiki</span><span>(</span><span>...</span><span>$arguments)</span><span>:</span><span> </span><span>string</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> $command </span><span>=</span><span> [</span></span>
<span class="line"><span> (</span><span>new</span><span> </span><span>ExecutableFinder</span><span>)</span><span>-></span><span>find</span><span>(</span><span>'node'</span><span>, </span><span>'node'</span><span>),</span></span>
<span class="line"><span> </span><span>'shiki.js'</span><span>,</span></span>
<span class="line"><span> </span><span>json_encode</span><span>(</span><span>array_values</span><span>($arguments)),</span></span>
<span class="line"><span> ];</span></span>
<span class="line"></span>
<span class="line"><span> $process </span><span>=</span><span> </span><span>new</span><span> </span><span>Process</span><span>(</span></span>
<span class="line"><span> </span><span>command</span><span>: $command,</span></span>
<span class="line"><span> </span><span>cwd</span><span>: </span><span>realpath</span><span>(</span><span>__DIR__</span><span> </span><span>.</span><span> </span><span>'/../bin'</span><span>),</span></span>
<span class="line"><span> </span><span>timeout</span><span>: </span><span>null</span><span>,</span></span>
<span class="line"><span> );</span></span>
<span class="line"></span>
<span class="line"><span> $process</span><span>-></span><span>run</span><span>();</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>if</span><span> (</span><span>!</span><span> $process</span><span>-></span><span>isSuccessful</span><span>()) {</span></span>
<span class="line"><span> </span><span>throw</span><span> </span><span>new</span><span> </span><span>ProcessFailedException</span><span>($process);</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> $process</span><span>-></span><span>getOutput</span><span>();</span></span>
<span class="line"><span> }</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
Notice that the highlighting in the code above is perfect. Even the features that were added in PHP 8, named arguments, are highlighted correctly.
Let’s now try to highlight the Blade view used to render this very page you are reading.
<span class="line"><span><</span><span>x-app-layout</span><span> </span><span>:title</span><span>=</span><span>"$post->title"</span><span>></span></span>
<span class="line"><span> <</span><span>x-ad</span><span>/></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>x-post-header</span><span> </span><span>:post</span><span>=</span><span>"$post"</span><span> </span><span>class</span><span>=</span><span>"mb-8"</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> </span><span>{!!</span><span> $post</span><span>-></span><span>html </span><span>!!}</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>@unless</span><span>($post</span><span>-></span><span>isTweet</span><span>())</span></span>
<span class="line"><span> </span><span>@if</span><span>($post</span><span>-></span><span>external_url)</span></span>
<span class="line"><span> <</span><span>p</span><span> </span><span>class</span><span>=</span><span>"mt-6"</span><span>></span></span>
<span class="line"><span> <</span><span>a</span><span> </span><span>href</span><span>=</span><span>"</span><span>{{</span><span> $post</span><span>-></span><span>external_url </span><span>}}</span><span>"</span><span>></span></span>
<span class="line"><span> Read more</</span><span>a</span><span>></span></span>
<span class="line"><span> <</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-xs text-gray-700"</span><span>>[</span><span>{{</span><span> $post</span><span>-></span><span>external_url_host </span><span>}}</span><span>]</</span><span>span</span><span>></span></span>
<span class="line"><span> </</span><span>p</span><span>></span></span>
<span class="line"><span> </span><span>@endif</span></span>
<span class="line"><span> </span><span>@endunless</span></span>
<span class="line"><span> </</span><span>x-post-header</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> </span><span>@include</span><span>(</span><span>'front.newsletter.partials.block'</span><span>, [</span></span>
<span class="line"><span> </span><span>'class'</span><span> </span><span>=></span><span> </span><span>'mb-8'</span><span>,</span></span>
<span class="line"><span> ])</span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"mb-8"</span><span>></span></span>
<span class="line"><span> </span><span>@include</span><span>(</span><span>'front.posts.partials.comments'</span><span>)</span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>x-slot</span><span> </span><span>name</span><span>=</span><span>"seo"</span><span>></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>property</span><span>=</span><span>"og:title"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> $post</span><span>-></span><span>title </span><span>}}</span><span> | freek.dev"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>property</span><span>=</span><span>"og:description"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> $post</span><span>-></span><span>plain_text_excerpt </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"og:image"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> </span><span>url</span><span>($post</span><span>-></span><span>getFirstMediaUrl</span><span>(</span><span>'ogImage'</span><span>)) </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"></span>
<span class="line"><span> </span><span>@foreach</span><span>($post</span><span>-></span><span>tags </span><span>as</span><span> $tag)</span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>property</span><span>=</span><span>"article:tag"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> $tag</span><span>-></span><span>name </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"><span> </span><span>@endforeach</span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>property</span><span>=</span><span>"article:published_time"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> </span><span>optional</span><span>($post</span><span>-></span><span>publish_date)</span><span>-></span><span>toIso8601String</span><span>() </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>property</span><span>=</span><span>"og:updated_time"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> $post</span><span>-></span><span>updated_at</span><span>-></span><span>toIso8601String</span><span>() </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"twitter:card"</span><span> </span><span>content</span><span>=</span><span>"summary_large_image"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"twitter:description"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> $post</span><span>-></span><span>plain_text_excerpt </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"twitter:title"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> $post</span><span>-></span><span>title </span><span>}}</span><span> | freek.dev"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"twitter:site"</span><span> </span><span>content</span><span>=</span><span>"</span><span>@freekmurze</span><span>"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"twitter:image"</span><span> </span><span>content</span><span>=</span><span>"</span><span>{{</span><span> </span><span>url</span><span>($post</span><span>-></span><span>getFirstMediaUrl</span><span>(</span><span>'ogImage'</span><span>)) </span><span>}}</span><span>"</span><span>/></span></span>
<span class="line"><span> <</span><span>meta</span><span> </span><span>name</span><span>=</span><span>"twitter:creator"</span><span> </span><span>content</span><span>=</span><span>"</span><span>@freekmurze</span><span>"</span><span>/></span></span>
<span class="line"><span> </</span><span>x-slot</span><span>></span></span>
<span class="line"><span></</span><span>x-app-layout</span><span>></span></span>
<span class="line"></span>
Again, perfect! Finally, let’s try a bit of Vue taken from medialibrary.pro.
<span class="line"><span><</span><span>template</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>heading</span><span> </span><span>class</span><span>=</span><span>"mb-6"</span><span>></span><span>Generate</span><span> </span><span>Newsletter</span><span></</span><span>heading</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>card</span><span> </span><span>class</span><span>=</span><span>"py-3 px-6"</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex border-b border-40"</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-1/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span>></span></span>
<span class="line"><span> <</span><span>h</span><span>4 </span><span>class</span><span>=</span><span>"font-normal text-80"</span><span>></span></span>
<span class="line"><span> </span><span>Edition</span><span> </span><span>number</span></span>
<span class="line"><span> </</span><span>h</span><span>4></span></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-3/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span> </span><span>name</span><span>=</span><span>"value"</span><span>></span></span>
<span class="line"><span> <</span><span>input</span></span>
<span class="line"><span> </span><span>v-model</span><span>=</span><span>"</span><span>form.editionNumber</span><span>"</span></span>
<span class="line"><span> </span><span>type</span><span>=</span><span>"text"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"w-full form-control form-input form-input-bordered flatpickr-input active"</span></span>
<span class="line"><span> /></span></span>
<span class="line"></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex border-b border-40"</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-1/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span>></span></span>
<span class="line"><span> <</span><span>h</span><span>4 </span><span>class</span><span>=</span><span>"font-normal text-80"</span><span>></span></span>
<span class="line"><span> </span><span>Start</span><span> </span><span>date</span></span>
<span class="line"><span> </</span><span>h</span><span>4></span></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-3/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span> </span><span>name</span><span>=</span><span>"value"</span><span>></span></span>
<span class="line"><span> <</span><span>input</span></span>
<span class="line"><span> </span><span>v-model</span><span>=</span><span>"</span><span>form.startDate</span><span>"</span></span>
<span class="line"><span> </span><span>type</span><span>=</span><span>"text"</span></span>
<span class="line"><span> </span><span>placeholder</span><span>=</span><span>"2018-01-01"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"w-full form-control form-input form-input-bordered flatpickr-input active"</span></span>
<span class="line"><span> /></span></span>
<span class="line"></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex border-b border-40"</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-1/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span>></span></span>
<span class="line"><span> <</span><span>h</span><span>4 </span><span>class</span><span>=</span><span>"font-normal text-80"</span><span>></span></span>
<span class="line"><span> </span><span>End</span><span> </span><span>date</span></span>
<span class="line"><span> </</span><span>h</span><span>4></span></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-3/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span> </span><span>name</span><span>=</span><span>"value"</span><span>></span></span>
<span class="line"><span> <</span><span>input</span></span>
<span class="line"><span> </span><span>v-model</span><span>=</span><span>"</span><span>form.endDate</span><span>"</span></span>
<span class="line"><span> </span><span>type</span><span>=</span><span>"text"</span></span>
<span class="line"><span> </span><span>placeholder</span><span>=</span><span>"2018-01-01"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"w-full form-control form-input form-input-bordered flatpickr-input active"</span></span>
<span class="line"><span> /></span></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex border-b border-40"</span><span> </span><span>v-if</span><span>=</span><span>"</span><span>newsletterHtml </span><span>!==</span><span> </span><span>''"</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-1/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span>></span></span>
<span class="line"><span> <</span><span>h</span><span>4 </span><span>class</span><span>=</span><span>"font-normal text-80"</span><span>></span></span>
<span class="line"><span> </span><span>Newsletter</span><span> </span><span>html</span></span>
<span class="line"><span> </</span><span>h</span><span>4></span></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-3/4 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>slot</span><span> </span><span>name</span><span>=</span><span>"value"</span><span>></span></span>
<span class="line"><span> <</span><span>textarea</span></span>
<span class="line"><span> </span><span>style</span><span>=</span><span>"height: 270px"</span></span>
<span class="line"><span> </span><span>v-model</span><span>=</span><span>"</span><span>newsletterHtml</span><span>"</span></span>
<span class="line"><span> </span><span>type</span><span>=</span><span>"text"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"w-full form-control form-input form-input-bordered flatpickr-input active"</span></span>
<span class="line"><span> ></</span><span>textarea</span><span>></span></span>
<span class="line"><span> </</span><span>slot</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> </</span><span>card</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"bg-30 flex px-8 py-4"</span><span>></span></span>
<span class="line"><span> <</span><span>button</span></span>
<span class="line"><span> @</span><span>click</span><span>=</span><span>"</span><span>generateNewsletter</span><span>"</span></span>
<span class="line"><span> </span><span>type</span><span>=</span><span>"button"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"ml-auto btn btn-default btn-primary mr-3"</span><span>></span></span>
<span class="line"><span> </span><span>Generate</span><span> </span><span>newsletter</span></span>
<span class="line"><span> </</span><span>button</span><span>></</span><span>div</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span></</span><span>template</span><span>></span></span>
<span class="line"></span>
<span class="line"><span><</span><span>script</span><span>></span></span>
<span class="line"><span>export</span><span> </span><span>default</span><span> {</span></span>
<span class="line"><span> </span><span>data</span><span>() {</span></span>
<span class="line"><span> </span><span>return</span><span> {</span></span>
<span class="line"><span> form: {</span></span>
<span class="line"><span> editionNumber: </span><span>null</span><span>,</span></span>
<span class="line"><span> startDate: </span><span>null</span><span>,</span></span>
<span class="line"><span> endDate: </span><span>null</span><span>,</span></span>
<span class="line"><span> },</span></span>
<span class="line"></span>
<span class="line"><span> newsletterHtml: </span><span>''</span><span>,</span></span>
<span class="line"><span> };</span></span>
<span class="line"><span> },</span></span>
<span class="line"></span>
<span class="line"><span> methods: {</span></span>
<span class="line"><span> </span><span>async</span><span> </span><span>generateNewsletter</span><span>() {</span></span>
<span class="line"><span> </span><span>let</span><span> response </span><span>=</span><span> </span><span>await</span><span> window.axios.</span><span>post</span><span>(</span></span>
<span class="line"><span> </span><span>'/nova-vendor/freekmurze/generate-newsletter'</span><span>,</span></span>
<span class="line"><span> </span><span>this</span><span>.form</span></span>
<span class="line"><span> );</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>this</span><span>.newsletterHtml </span><span>=</span><span> response.data;</span></span>
<span class="line"><span> },</span></span>
<span class="line"><span> },</span></span>
<span class="line"><span>};</span></span>
<span class="line"><span></</span><span>script</span><span>></span></span>
<span class="line"></span>
The package has few additional features. Head over to the spatie/shiki-php readme to learn more.
Highlighting code snippets in Markdown
Using the spatie/shiki-php package, you can highlight a given single code snippet. In most cases, you’ll not use it directly. Most blogs and documentation pages use Markdown to store content. That Markdown can contain code snippets, like this page from our docs.
To render Markdown to HTML, the popular league/commonmark package can be used. One of the nice things about that package is that it is very extensible.
Our second new package, spatie/commonmark-shiki-highlighter, contains an extension for the commonmark package that will highlight all code snippets in a markdown snippet using Shiki PHP.
Here’s how you can create a function that can converts Markdown to HTML with all code snippets highlighted. Inside the function will create a new CommonMarkConverter
that uses the HighlightCodeExtension
provided by our package.
<span class="line"><span>use</span><span> </span><span>LeagueCommonMarkCommonMarkConverter</span><span>;</span></span>
<span class="line"><span>use</span><span> </span><span>LeagueCommonMarkEnvironment</span><span>;</span></span>
<span class="line"><span>use</span><span> </span><span>SpatieCommonMarkShikiHighlighterHighlightCodeExtension</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>function</span><span> </span><span>convertToHtml</span><span>(</span><span>string</span><span> $markdown, </span><span>string</span><span> $theme </span><span>=</span><span> </span><span>'github-light'</span><span>)</span><span>:</span><span> </span><span>string</span></span>
<span class="line"><span>{</span></span>
<span class="line"><span> $environment </span><span>=</span><span> </span><span>Environment</span><span>::</span><span>createCommonMarkEnvironment</span><span>()</span></span>
<span class="line"><span> </span><span>-></span><span>addExtension</span><span>(</span><span>new</span><span> </span><span>HighlightCodeExtension</span><span>($theme));</span></span>
<span class="line"></span>
<span class="line"><span> $commonMarkConverter </span><span>=</span><span> </span><span>new</span><span> </span><span>CommonMarkConverter</span><span>(</span><span>environment</span><span>: $environment);</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> $commonMarkConverter</span><span>-></span><span>convertToHtml</span><span>($markdown);</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
With that function setup, you can easily convert Markdown to HTML anywhere in your project.
<span class="line"><span>$markdown </span><span>=</span><span> </span><span><<<</span><span>MD</span></span>
<span class="line"><span># A code snippet</span></span>
<span class="line"></span>
<span class="line"><span>Here is a code snippet that will be highlighted correctly.</span></span>
<span class="line"></span>
<span class="line"><span>```php</span></span>
<span class="line"><span>echo 'Hello world';</span></span>
<span class="line"><span>```</span></span>
<span class="line"><span>MD</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>$html </span><span>=</span><span> </span><span>convertToHtml</span><span>($markdown)</span></span>
<span class="line"></span>
The package also has support for [marking lines as highlighted, added, deleted and focused].
To highlight a line, simply add the line number that you want to highlight between brackets after the language of a code snippet, for example:
<span class="line"><span>```php{2}</span></span>
<span class="line"></span>
Here’s a bit of code in which we highlighted the second line.
<span class="line"><span>function</span><span> </span><span>myFunction</span><span>() {</span></span>
<span class="line highlight"><span> </span><span>return</span><span> </span><span>'this line will be highlighted'</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
You can also mark lines as added en deleted by prefix a line with +
or -
. This piece of Markdown…
<span class="line"><span>```php</span></span>
<span class="line"><span><?</span><span>php</span></span>
<span class="line add"><span>echo</span><span> </span><span>"This line is marked as added"</span><span>;</span></span>
<span class="line del"><span>echo</span><span> </span><span>"This line is marked as deleted"</span><span>;</span></span>
<span class="line"><span>```</span></span>
<span class="line"></span>
… will be rendered as:
<span class="line"><span><?</span><span>php</span></span>
<span class="line add"><span>echo</span><span> </span><span>"This line is marked as added"</span><span>;</span></span>
<span class="line del"><span>echo</span><span> </span><span>"This line is marked as deleted"</span><span>;</span></span>
<span class="line"></span>
These highlighting features work because Shiki will apply some CSS classes to the highlighted lines. You should write a bit of CSS of your own that determines how highlighted lines look like. Here are the classes I use on this very blog (my colleague Rias originally wrote this CSS).
<span class="line"><span>.shiki</span><span> </span><span>.highlight</span><span> {</span></span>
<span class="line"><span> </span><span>background-color</span><span>: </span><span>hsl</span><span>(</span><span>197</span><span>, </span><span>88</span><span>%</span><span>, </span><span>94</span><span>%</span><span>);</span></span>
<span class="line"><span> </span><span>padding</span><span>: </span><span>3</span><span>px</span><span> </span><span>0</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>.shiki</span><span> </span><span>.add</span><span> {</span></span>
<span class="line"><span> </span><span>background-color</span><span>: </span><span>hsl</span><span>(</span><span>136</span><span>, </span><span>100</span><span>%</span><span>, </span><span>96</span><span>%</span><span>);</span></span>
<span class="line"><span> </span><span>padding</span><span>: </span><span>3</span><span>px</span><span> </span><span>0</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>.shiki</span><span> </span><span>.del</span><span> {</span></span>
<span class="line"><span> </span><span>background-color</span><span>: </span><span>hsl</span><span>(</span><span>354</span><span>, </span><span>100</span><span>%</span><span>, </span><span>96</span><span>%</span><span>);</span></span>
<span class="line"><span> </span><span>padding</span><span>: </span><span>3</span><span>px</span><span> </span><span>0</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>.shiki.focus</span><span> </span><span>.line:not</span><span>(</span><span>.focus</span><span>) {</span></span>
<span class="line"><span> </span><span>transition</span><span>: </span><span>all</span><span> </span><span>250</span><span>ms</span><span>;</span></span>
<span class="line"><span> </span><span>filter</span><span>: </span><span>blur</span><span>(</span><span>2</span><span>px</span><span>);</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>.shiki.focus:hover</span><span> </span><span>.line</span><span> {</span></span>
<span class="line"><span> </span><span>transition</span><span>: </span><span>all</span><span> </span><span>250</span><span>ms</span><span>;</span></span>
<span class="line"><span> </span><span>filter</span><span>: </span><span>blur</span><span>(</span><span>0</span><span>);</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
Did you notice those last two CSS classes. They are used to style focussed code. It’s pretty cool that Shiki supports this. Let’s see an example of that in action.
To focus on particular pieces of code, simply put the line numbers in the second pair of brackets.
<span class="line"><span>```css{}{16-24}</span></span>
<span class="line"></span>
Let’s again go meta a focus on the CSS bits that enable focus.
<span class="line"><span>.shiki</span><span> </span><span>.highlight</span><span> {</span></span>
<span class="line"><span> </span><span>background-color</span><span>: </span><span>hsl</span><span>(</span><span>197</span><span>, </span><span>88</span><span>%</span><span>, </span><span>94</span><span>%</span><span>);</span></span>
<span class="line"><span> </span><span>padding</span><span>: </span><span>3</span><span>px</span><span> </span><span>0</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>.shiki</span><span> </span><span>.add</span><span> {</span></span>
<span class="line"><span> </span><span>background-color</span><span>: </span><span>hsl</span><span>(</span><span>136</span><span>, </span><span>100</span><span>%</span><span>, </span><span>96</span><span>%</span><span>);</span></span>
<span class="line"><span> </span><span>padding</span><span>: </span><span>3</span><span>px</span><span> </span><span>0</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>.shiki</span><span> </span><span>.del</span><span> {</span></span>
<span class="line"><span> </span><span>background-color</span><span>: </span><span>hsl</span><span>(</span><span>354</span><span>, </span><span>100</span><span>%</span><span>, </span><span>96</span><span>%</span><span>);</span></span>
<span class="line"><span> </span><span>padding</span><span>: </span><span>3</span><span>px</span><span> </span><span>0</span><span>;</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line focus"><span>.shiki.focus</span><span> </span><span>.line:not</span><span>(</span><span>.focus</span><span>) {</span></span>
<span class="line focus"><span> </span><span>transition</span><span>: </span><span>all</span><span> </span><span>250</span><span>ms</span><span>;</span></span>
<span class="line focus"><span> </span><span>filter</span><span>: </span><span>blur</span><span>(</span><span>2</span><span>px</span><span>);</span></span>
<span class="line focus"><span>}</span></span>
<span class="line focus"></span>
<span class="line focus"><span>.shiki.focus:hover</span><span> </span><span>.line</span><span> {</span></span>
<span class="line focus"><span> </span><span>transition</span><span>: </span><span>all</span><span> </span><span>250</span><span>ms</span><span>;</span></span>
<span class="line focus"><span> </span><span>filter</span><span>: </span><span>blur</span><span>(</span><span>0</span><span>);</span></span>
<span class="line focus"><span>}</span></span>
<span class="line"></span>
To know more about highlighting, focussing, … head over to the docs on commonmark-shiki-highlighting.
A Blade component to render Markdown with highlighted code
Using our commonmark shiki extension is not that hard, but for Laravel apps, we can make the usage even easier.
Let’s look at the third package we released: spatie/laravel-markdown. This package contains:
- a Blade component that can render markdown
- a highly configurable class that you can use to render Markdown
Let’s start with an example of the provided x-markdown
Blade component. The component can convert this chunk of Markdown…
<span class="line"><span><</span><span>x-markdown</span><span>></span></span>
<span class="line"><span>This is a [link to our website](https://spatie.be)</span></span>
<span class="line"></span>
<span class="line"><span>```php</span></span>
<span class="line"><span>echo 'Hello world';</span></span>
<span class="line"><span>```</span></span>
<span class="line"><span></</span><span>x-markdown</span><span>></span></span>
<span class="line"></span>
… to this chunk of HTML:
<span class="line"><span><</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>h1</span><span> </span><span>id</span><span>=</span><span>"my-title"</span><span>>My title</</span><span>h1</span><span>></span></span>
<span class="line"><span> <</span><span>p</span><span>>This is a <</span><span>a</span><span> </span><span>href</span><span>=</span><span>"https://spatie.be"</span><span>>link to our website</</span><span>a</span><span>></</span><span>p</span><span>></span></span>
<span class="line"><span> <</span><span>pre</span><span> </span><span>class</span><span>=</span><span>"shiki"</span><span> </span><span>style</span><span>=</span><span>"</span><span>background-color: #fff"</span><span>><</span><span>code</span><span>><</span><span>span</span><span> </span><span>class</span><span>=</span><span>"line"</span><span>><</span><span>span</span></span>
<span class="line"><span> </span><span>style</span><span>=</span><span>"</span><span>color: #005CC5"</span><span>>echo</</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #24292E"</span><span>> </</span><span>span</span><span>><</span><span>span</span><span> </span><span>style</span><span>=</span><span>"</span><span>color: #032F62"</span><span>></span><span>'</span><span>Hello world</span><span>'</span><span></</span><span>span</span><span>><</span><span>span</span></span>
<span class="line"><span> </span><span>style</span><span>=</span><span>"</span><span>color: #24292E"</span><span>>;</</span><span>span</span><span>></</span><span>span</span><span>></span></span>
<span class="line"><span><</span><span>span</span><span> </span><span>class</span><span>=</span><span>"line"</span><span>></</span><span>span</span><span>></</span><span>code</span><span>></</span><span>pre</span><span>></span></span>
<span class="line"><span></</span><span>div</span><span>></span></span>
<span class="line"></span>
Which will be displayed in the browser as:
This is a link to our website
<span class="line"><span>echo</span><span> </span><span>'Hello world'</span><span>;</span></span>
<span class="line"></span>
Of course, all code blocks are highlighted using Shiki.
Using the x-markdown
component is the easiest way to render Markdown in a Laravel app. Previously, I’ve already stated that rendering markdown using Shiki can be resource-intensive. You’ll be happy to know that the x-markdown
Blade component has built-in caching. A unique piece of Markdown will only get rendered once, so you’ll only have the performance hit the first time.
If you want to take care of the markdown rendering yourself, without using the Blade component, you can use the MarkdownRenderer
class that ships with the package. Here’s an example of how you can use it.
<span class="line"><span>$html </span><span>=</span><span> </span><span>app</span><span>(</span><span>SpatieLaravelMarkdownMarkdownRenderer</span><span>::class</span><span>)</span><span>-></span><span>toHtml</span><span>($markdown);</span></span>
<span class="line"></span>
Of course, the MarkdownRenderer
will highlight code snippets in the markdown, and cache the results. It’s also easy to customise the rendering:
<span class="line"><span>$html </span><span>=</span><span> </span><span>app</span><span>(</span><span>SpatieLaravelMarkdownMarkdownRenderer</span><span>::class</span><span>)</span></span>
<span class="line"><span> </span><span>-></span><span>commonmarkOptions</span><span>($arryWithOptions)</span></span>
<span class="line"><span> </span><span>-></span><span>highlightTheme</span><span>(</span><span>'github-dark'</span><span>)</span></span>
<span class="line"><span> </span><span>-></span><span>toHtml</span><span>($markdown);</span></span>
<span class="line"></span>
To know more about these options, head over to the docs of Laravel Markdown.
In closing
We’re very impressed with the quality of highlighting Shiki offers. Even modern PHP and JS code snippets are highlighted perfectly.
As mentioned above, Shiki can be resource-intensive. It’s not recommended to run Shiki for every request (unless you like slow requests). If you’re running Shiki locally, make sure you’re using some form of caching.
Alternatively, you could opt to use a service like Torchlight. They take care of the syntax highlighting at their end, so you don’t need to worry about any performance issues. They even got a commonmark package to get you started as well.
If you want to take care of syntax highlighting locally, check out our three packages:
Our company website and this blog are open-sourced. Both sites make use of spatie/laravel-markdown Here’s the code that converts Markdown to HTML:
Be sure also to check out these Markdown related packages:
- spatie/sheets: stores and retrieves static content in plain text files
- spatie/yaml-front-matter: Can parse metadata at the top of a markdown file
I want to thank my colleague Rias, who kickstarted the work on the shiki-php
package. Like always, Rias did an excellent job. You can see a few more examples of what Shiki can do, on his blog.
These packages aren’t the first ones we’ve built. Check out this extensive list of packages our team has made previously. I’m pretty sure there will be something useful for your next PHP or Laravel project.