Highlighting with HighlightJS
Syntax Highlighting is important, it just makes code all warm and fluffy and more engaging. I currently use HighlightJS in my Rails 7 app.
./bin/importmap pin highlightjs
Which adds the following to `config/importmap.rb`
pin "highlightjs", to: "https://ga.jspm.io/npm:highlight.js@11.4.0/es/index.js"
Over the years I have used a number of different ways to organise my code examples in blog posts. They are a mixture of RedCloth and HTML and I used to used to use CodeRay and then SyntaxHighlighter to render code blocks nicely.
<x-source:ruby>
class Cat; end
</x-source>
but eventually settled on pre > code and HighlightJS<pre>
<code>
class Rabbit; end
</code>
</pre>
As an aside I never updated the old blog posts but created a renderer to update the body of the posts to comply to the new format, and even ensure that I could embed my pre > code html in examples too by escaping the contents of every pre > code element.
# frozen_string_literal: true
class Content
def initialize(string)
@string = string
end
def render
::Haml::Helpers.preserve(segments.join('')).html_safe
end
private
def string
@string.gsub(/<source(:[a-z]+)?>/, '<pre><code>')
.gsub(%r{</code></pre>}, '</code></pre>')
.gsub(%r{<code>([\s])+}, "<code data-controller='highlight'>")
end
def segments
string.split(/(<pre><code>.*?<\/code><\/pre>)/m).map do |s|
if s =~ /<pre><code>.*?<\/code><\/pre>/m
s.gsub(/<pre><code>(.*)?<\/code><\/pre>/m) { "<pre><code data-controller='highlight'>#{CGI::escapeHTML($1)}</code></pre>" }
else
RedCloth.new(s).to_html
end
end
end
end
I could then use just a turbo:load EventListener to apply the highlight the code blocks.
app/javascript/application.js
import "@hotwired/turbo-rails"
import "controllers"
// Turbo.session.drive = true
import HighlightJS from "highlightjs"
window.HighlightJS = HighlightJS
document.addEventListener('turbo:load', (event) => {
console.log('turbo:load')
document.querySelectorAll('pre code').forEach((el) => {
HighlightJS.highlightElement(el);
});
});
However, this doesn’t work with Turbo-Frame, the `turbo:load` callback is only run on full page loads and I want to use them in Turbo-Frame(s). I am sure there is another callback I could run but honestly I’d prefer to use a Stimulus Controller and know that it will always trigger when the DOM changes and my code block is added to the DOM.
app/javascript/controllers/highlight_controller.js
import { Controller } from "@hotwired/stimulus"
import HighlightJS from "highlightjs"
export default class extends Controller {
connect() {
HighlightJS.highlightElement(this.element)
}
}
With code blocks looking like
<pre>
<code data-controller="highlight">
class Rabbit; end
</code>
</pre>
This seemed to work in development but failed in production. AS it turns out I was caching the content of every blog post, so it wouldn’t need to re-render every
Rails.cache.clear
For bonus points I’ve added support for triple back-tick ```language delimited blocks for code.
# frozen_string_literal: true
class Content
def initialize(string)
@string = string
end
def render
::Haml::Helpers.preserve(segments.join('')).html_safe
# rescue => e
# 'BROKEN'
end
private
def string
@string.gsub(/<source(:[a-z]+)?>/, '<pre><code>')
.gsub(%r{</code></pre>}, '</code></pre>')
end
def segments
rewritten = string.split(/(```[a-z]+?\n.*?```)/m).map do |segment|
if segment =~ /```([a-z]+)?\n.*?```/m
segment.gsub(/```([a-z]+)?\n(.*)?```/m) { "<pre><code data-controller='highlight'>#{CGI::escapeHTML($2)}</code></pre>" }
else
segment
end
end.join('')
rewritten.split(/(<pre><code[^>]+?>.*?<\/code><\/pre>)/m).map do |s|
if s =~ /<pre><code>.*?<\/code><\/pre>/m
s.gsub(/<pre><code>(.*)?<\/code><\/pre>/m) { "<pre><code data-controller='highlight'>#{CGI::escapeHTML($1)}</code></pre>" }
elsif s =~ /<pre><code data-controller='highlight'>.*?<\/code><\/pre>/m
s
else
RedCloth.new(s).to_html
end
end
end
end
However, I should just bulk update the content of the blog bodies to not use the old tag syntax I implemented years ago anyway.
Fun Thursday.