Torchlight is a syntax highlighting service created by the legendary Aaron Francis (Thank you Aaron, it's awesome!). It generates beautiful code blocks and I love how they look. However, I wanted to take it a step further and add a "Copy to Clipboard" button to make sharing code snippets even easier. Here's how we can do it!
Most of the code blocks are generated from markdown content via Statamic's markdown parser, so it's not as simple as using <x-torchlight-code>
. Instead, we have to extend Torchlight's base markdown parser to add the button.
To do this, we'll create a file in app/Extensions called TorchlightWithCopyButtonExtension.php
and add an extension of the defaultBlockRenderer
function. This function returns an HTML string that includes the Copy to Clipboard button and all the necessary HTML, AlpineJS and Tailwind CSS code to make it functional.
The key part is that we're wrapping the default Torchlight block renderer $torchlight($block)
in an Alpine component (x-data="codeBlock"
). Also included is a button which calls the copyToClipboard
method from the Alpine component when clicked.
1<?php23namespace App\Extensions;45use Torchlight\Block;6use Torchlight\Commonmark\V2\TorchlightExtension;78class TorchlightWithCopyExtension extends TorchlightExtension9{10 public function defaultBlockRenderer()11 {12 return function (Block $block) {13 $torchlight = parent::defaultBlockRenderer();1415 return <<<HTML16 <div x-data="codeBlock" class="relative">17 <div class="hidden lg:flex lg:space-x-2 lg:items-center absolute top-0 right-0 mr-3 mt-3">18 <div x-cloak x-show="showMessage" x-transition class="animate-bounce transition duration-300 mt-1 text-teal-400 text-xs">Copied!</div>19 <button20 type="button"21 title="Copy to clipboard"22 class="hidden md:block transition duration-300"23 @click.prevent="copyToClipboard"24 :class="{25 'text-white/30 hover:text-white/80': !showMessage,26 'text-teal-400 hover:text-teal-400': showMessage,27 }">28 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="block w-6 h-6">29 <path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0118 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3l1.5 1.5 3-3.75" />30 </svg>31 </button>32 </div>33 {$torchlight($block)}34 </div>35 HTML;36 };37 }38}
1<?php23namespace App\Extensions;45use Torchlight\Block;6use Torchlight\Commonmark\V2\TorchlightExtension;78class TorchlightWithCopyExtension extends TorchlightExtension9{10 public function defaultBlockRenderer()11 {12 return function (Block $block) {13 $torchlight = parent::defaultBlockRenderer();1415 return <<<HTML16 <div x-data="codeBlock" class="relative">17 <div class="hidden lg:flex lg:space-x-2 lg:items-center absolute top-0 right-0 mr-3 mt-3">18 <div x-cloak x-show="showMessage" x-transition class="animate-bounce transition duration-300 mt-1 text-teal-400 text-xs">Copied!</div>19 <button20 type="button"21 title="Copy to clipboard"22 class="hidden md:block transition duration-300"23 @click.prevent="copyToClipboard"24 :class="{25 'text-white/30 hover:text-white/80': !showMessage,26 'text-teal-400 hover:text-teal-400': showMessage,27 }">28 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="block w-6 h-6">29 <path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0118 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3l1.5 1.5 3-3.75" />30 </svg>31 </button>32 </div>33 {$torchlight($block)}34 </div>35 HTML;36 };37 }38}
Your styling may vary of course.
Now that we've extended the Torchlight renderer, let's take a look at how the Alpine component functions.
First, we have to add an option to the Torchlight config -- torchlight.options.copyable
. This option makes Torchlight send an additional (unformatted, hidden) code block back that we can use as a copy target. Big thanks to Jason Beggs for pointing this option out! It makes copying the code to the user's clipboard much simpler.
Edit your config/torchlight.php
file to include the following in the options
array:
1'options' => [2 'copyable' => true,3],
1'options' => [2 'copyable' => true,3],
Next, we'll extract the Alpine code into a reusable chunk. First, create a file placed at resources/js/codeBlock.js
. This file handles the actual copying of the code to the user's clipboard and then shows the "Copied!" message to the user.
1// resources/js/codeBlock.js2export default () => ({3 showMessage: false,4 copyToClipboard() {5 navigator.clipboard.writeText(6 // This requires torchlight.options.copyable to be "true" on the PHP side.7 $this.root.querySelector('.torchlight-copy-target').textContent8 )9 // show the "copied" message for 2 seconds10 this.showMessage = true11 setTimeout(() => (this.showMessage = false), 2000)12 },13})
1// resources/js/codeBlock.js2export default () => ({3 showMessage: false,4 copyToClipboard() {5 navigator.clipboard.writeText(6 // This requires torchlight.options.copyable to be "true" on the PHP side.7 $this.root.querySelector('.torchlight-copy-target').textContent8 )9 // show the "copied" message for 2 seconds10 this.showMessage = true11 setTimeout(() => (this.showMessage = false), 2000)12 },13})
We can simply grab the textContent
from the .torchlight-copy-target
selector and pass it to the clipboard using navigator.clipboard.writeText
from the Clipboard API. Simple AND effective!
Next, make sure to register codeBlock
as Alpine.data in our app.js
file.
1// resources/js/app.js2import Alpine from 'alpinejs'3import codeBlock from './codeBlock'45window.Alpine = Alpine67Alpine.data('codeBlock', codeBlock)8Alpine.start()
1// resources/js/app.js2import Alpine from 'alpinejs'3import codeBlock from './codeBlock'45window.Alpine = Alpine67Alpine.data('codeBlock', codeBlock)8Alpine.start()
There's one last step. We need to register our extension in the boot()
method of our application's AppServiceProvider
. Then, whenever we generate code blocks from markdown content, the copy button will automatically be included.
1use App\Extensions\TorchlightWithCopyButtonExtension;23public function boot()4{5 Markdown::addExtensions(fn () => [6 new TorchlightWithCopyButtonExtension,7 ]);8}
1use App\Extensions\TorchlightWithCopyButtonExtension;23public function boot()4{5 Markdown::addExtensions(fn () => [6 new TorchlightWithCopyButtonExtension,7 ]);8}
That's all there is to it! With just a few steps, we were able to enhance markdown-generated code blocks with a convenient Copy to Clipboard feature. Enjoy the new code blocks, and Happy Coding!