“...I've been working since 2008 with Ruby / Ruby on Rails, love a bit of Elixir / Phoenix and learning Rust. I also poke through other people's code and make PRs for OpenSource Ruby projects that sometimes make it. Currently working for InPay...”

Rob Lacey (contact@robl.me)
Senior Software Engineer, Brighton, UK

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.