“...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
Senior Software Engineer, 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.

GPK of the Day Wrappin' RUTH