jose.omg.lol

Better Code Blocks For Your Ruby Blog

I went on the longest yak shave to try and get code blocks working the way I wanted on this blog.

I wanted a solution that...

  • Made it obvious when a block could be scrolled
  • Had a color scheme I liked
  • Was readable
  • Worked reasonably well for HTML, Ruby, CSS, and JS.

Here's some throwaway code to show what I landed on:

<form class="d-flex gs4 gsy fd-column">
    <label class="flex--item s-label" for="question-title">Question title</label>
    <div class="d-flex ps-relative">
        <input class="flex--item s-input" type="text" id="question-title" placeholder="e.g. Why doesn’t Stack Overflow use a custom web font?"/>
    </div>
</form>
class Customer < ApplicationRecord
   @@no_of_customers = 0

   def initialize(id, name, addr)
      @cust_id = id
      @cust_name = name
      @cust_addr = addr
   end

   def function(arg1, **opts)
      statement(arg1)
      puts "Hello Ruby!"
      self[:hello] = "world"
   end
end

cust1 = Customer.new("1", "John", "Wisdom Apartments, Ludhiya")
cust2 = Customer.new("2", "Poul", "New Empire road, Khandala")
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static targets = ["item"]
  static values = {something: String}

  connect() {
    let foobar = "123"
    this.itemTargets.forEach(e => e.classList.toggle("className"));
    console.log("testing")
  }
}

It might not be for everybody. But I like how it turned out. So here's how I did it in case you'd like to use some (or all) of it for your own blog.

Making scrolling obvious

This is a pet peeve of mine. Specially on mobile. The space is so tight that line wrapping just feels counterproductive. But the alternative isn't much better because you never know whether a statement really ends right before the block cuts off or if it just seems like it.

This solution consists on making scrollbars visible and adding a small "scrollable" indicator.

Making scrollbars visible

It's crazy how many things differ across browser implementations. Styling scrollbars is one of those things. So what I'm about to show you won't work for all browsers (which is partly why we'll also be implementing the scrollable indicator).

In any case, WebCore browsers such as Chrome, Safari, and Opera should look something like this:

Code block with scrollbar as it appears on WebCore browsers
Fig. 1: Code block with scrollbar as it appears on WebCore browsers

Here's the code to make it happen:

pre {
  &.highlight {
    scrollbar-color: var(--color-syntax-scrollbar) transparent;

    &::-webkit-scrollbar {
      width: 10px;
      height: 10px;
      background-color: transparent;
    }

    &::-webkit-scrollbar-track {
      border-radius: 10px;
      background-color: transparent;
    }

    &::-webkit-scrollbar-thumb {
      border-radius: 10px;
      background-color: var(--color-syntax-scrollbar);
      border: 2px solid var(--color-syntax-background); // Looks like padding. H/T css-for-js.dev
    }

    &::-webkit-scrollbar-corner {
      background-color: transparent;
      border-color: transparent;
    }

    ::-webkit-scrollbar {
      width: 9px;
      height: 11px;
      background-color: transparent;
    }

    ::-webkit-scrollbar-thumb {
      border-radius: 10px;
      background-color: var(--color-gray);
      border: 2px solid var(--color-background);

      &:hover {
        background-color: var(--color-light-gray);
      }
    }
  }
}

Giving the scrollbar specific properties will make it so it's always visible on most browsers. However, iOS won't allow you to override scrollbar styles and Firefox will allow some customization but won't always display the scrollbar. So I added a small badge on the top left of the block to indicate that the block is scrollable.

Adding a scrollable indicator (bonus: showing the language beside it)

The end result looks like this:

Code block with scrollbar and a small badge at the top left that says
Fig. 2: Code block with scrollbar and "scrollable" indicator

Achieving it is an effort in two parts:

First, we have to add the actual markup to the code block. This blog is built with Middleman, which uses a Markdown processor to convert posts to HTML. I chose Redcarpet as my processor.

Redcarpet lets you implement your own custom renderer. Which is exactly what I did:

require 'rouge'
require 'rouge/plugins/redcarpet'

class BlogRenderer < Redcarpet::Render::HTML
  include Rouge::Plugins::Redcarpet

  def block_code(code, language)
    html = Nokogiri::HTML(super)
    code_node = html.xpath('//pre/code')
    code_node.before(<<~HTML.strip)
      <div class="code-metadata code-metadata__container">
        <span class="code-metadata__language">#{language}</span>
        <span class="code-metadata__overflow-badge u-hidden" data-code-overflow-target="badge">scrollable</span>
      </div>
    HTML
    html.to_s
  end
end

block_code is implemented in Rouge::Plugins::Redcarpet (which is a dependency of middleman-syntax, we'll get to what that is later). We're leveraging that implementation by calling super, then adding a new node with Nokogiri. Admittedly, Nokogiri might be a bit much for what we're doing but I don't really mind it.

The second part of the effort is displaying the indicator.

As you might have picked up on, the indicator has a u-hidden class (which simply applies display: none;). We now have to remove that class when appropriate so that we can actually see the element.

I chose to do that with a stimulus controller, plus a simple isOverflown helper:

function isOverflown(element) {
  return element.scrollWidth > element.clientWidth
}

export default class extends Controller {
  static targets = ["badge"]

  badgeTargetConnected(badge) {
    if (isOverflown(badge.parentElement.parentElement)) {
      badge.classList.remove("u-hidden")
    }
  }
}

Note that this won't always work in development, as there might be race conditions between rendering the code block and calculating element.scrollWidth. But this should be stable enough in production environments.

That's it! Overflowing codeblocks can now be easily identified even when scanning the page.

Implementing syntax highlighting

I'm not 100% satisfied with my current color scheme. But I like it enough to go with it for now. I might recommend using Shiki (see this repo for reference) as an alternative if you're not sold either. I stuck with my current solution because I didn't like Shiki enough to make the switch.

Middleman has its own extension for syntax highlighting called Middleman-Syntax. It's built on top of Rouge, which you might remember from earlier.

Middleman-Syntax is compatible with Pygments, a generic syntax highlighter built with Python. You can generate a CSS stylesheet from any pygments theme by running:

pygmentize -f html -S <theme-name> -a .syntax

Linked here is a repo where @richleland generated stylesheets for lots of different themes, although it hasn't been updated in a while.

Implementing syntax highlighting is just a matter of configuring Middleman-Syntax and importing any of these stylesheets in your project.

For this blog, I started out with the Github light theme and then went in and changed some styles manually. You can see my SCSS file by going to my blog's repo.

And we're done! My other requirements (readability and working well with HTML, Ruby, CSS, and JS) were addressed by making text sufficiently large, choosing a readable color scheme and choosing a good syntax highlighter (Rouge, in my case).

I'll probably tinker around with this some more as I write more blog posts. But this feels good enough for now.

Get notified about new posts