Alexander Gromnitsky's Blog

Adding Line Numbers to Code Blocks

Latest update:

(I originally submitted this 'article' to CSS-Tricks in the autumn of 2022, but they rejected it without providing any explanations. A couple of days ago, I stumbled upon it while doing some spring cleaning.)

On narrow screens, code blocks can be either wrapped or left to their own horizontal scroll. The latter could be painful, and the degree of pain depends proportionally on the length of the longest line.

If you choose to wrap long lines, how would you indicate that a line was indeed wrapped? I've heard about a clever 10-year-old hack that mimics Emacs-style fringe symbols but have never seen it implemented anywhere.

Another way is to add line numbers to the left of the code block. Then, if a line wraps, a reader immediately sees where:

This, of course, only works if you do the whole mechanism correctly.

I've come up with several recipes, including 2 broken ones. We finish with a tiny custom html element, that reformats its child nodes automatically.

Our requirements are:

  1. line wrapping must be visually apparent;
  2. selecting a chunk of code implies ignoring line numbers themselves;
  3. when JavaScript is turned off, users should see original code blocks untouched.

Simple, but with broken usability

You can still found this method employed among godforsaken C++ blogs, or semi-abandoned websites. The idea is very straightforward:

  1. get innerHTML of a <pre> element;
  2. break the string into an array of substrings by searching for newlines;
  3. prepend each substring with a number;
  4. join the substrings, update innerHTML.
// bad, don't use it
let node = document.querySelector('<pre>')
let rows = node.innerHTML.split("\n")
    .map( (line, idx) => `${idx+1}. ${line}`)
node.innerHTML = rows.join`\n`

It is bad not because the code does something we have not intended it to do, but because it violates our requirement #2: if you select multiple lines, you get syntactically incorrect result in the clipboard:

Its only possible usage may be in a custom xhtml post-processor for epub production, for as it does not require complex CSS (something like pre { overflow-wrap: break-word; white-space: pre-wrap; } would suffice), any epub reader (or Kindle) would be able to render it satisfactory.

With a predefined width for a line numbers column

We can expand the previous bad example by wrapping each line number in a <span>, like so:

<pre>
  <span>1</<span>Lucy Locket lost her pocket,
  <span>2</<span>Kitty Fisher found it;
  <span>3</<span>Not a penny was there in it,
  <span>4</<span>Only ribbon round it.
</pre>

Then we set display for each span to inline-block, specify the desired width, set user-select property to none, add border, &c.

See the Pen a line numbers column with a predefined width by Alexander Gromnitsky (@sigwait) on CodePen.

There are 2 thing I don't like about it:

  • wrapped lines look like as if they have escaped from their allotted space;
  • the width of the spans is predefined.

Both issues can be abated by

Counters & grid

What if we wrap everything in <span>s?

<pre>
  <span></span><span>Little Tommy Tucker</span>
  <span></span><span>Sings for his supper.</span>
</pre>

Such a DOM structure is a good candidate for a 2-column grid. Also, this time we don't embed physical numbers anywhere, but utilise CSS Counters & ::before pseudo-elements.

Recall, that we can initialise a counter variable for any html element that produces a box, then increment the variable, and get back its value. For example, if we name our variable line:

pre {
  counter-reset: line;
}
pre > span:nth-child(odd) {
  counter-increment: line;
}
pre > span:nth-child(odd)::before {
  content: counter(line);
}

(Function counter() converts the integer argument to a string.)

See the Pen a line numbers column using counters & grid by Alexander Gromnitsky (@sigwait) on CodePen.

Note that we change the class name after the reformatting because of the item #3 in our requirements list: no JavaScript but display: grid wreaks havoc in code blocks.

If you decide to use this method, understand its limitations: starting and ending tags in existing markup are not allowed to cross multiple lines, or a column mixup will occur:

<pre> <!-- expect a hilarious catastrophe -->
<b>first
second</b>
third
</pre>

Positional

This method is used by Line Number plugin for PrismJS. Instead of wrapping code in spans, it tucks an exact amount of spans into the very end of a <pre> block:

<pre>
Oh, the grand old Duke of York,
He had ten thousand men;
He marched them up to the top of the hill,
And he marched them down again.
  <span>
    <span></span>
    <span></span>
    <span></span>
    <span></span>
  </span>
</pre>

Then you declare <pre> as relative & position the absolute span wrapper in the top left corner.

See the Pen a positional line numbers column by Alexander Gromnitsky (@sigwait) on CodePen.

Unfortunately, this works only when code blocks are not wrapped, but horizontally scrolled. Otherwise, line numbers stop matching real lines of code:

Custom element

Such a trivial task--prepending line numbers--is a nice example of a little Web Component.

<pre><with-line-numbers>
void strcpy(char *s, char *t) {
  while (*s++ = *t++)
    ;
}
</with-line-numbers></pre>

If JavaScript is off, <pre> still renders as usual. We can even provide a CSS variable for line numbers colour as a user preference.

pre {
  color: darkblue;
  --wln--color: red;
}

The structure of our custom element is fairy typical:

  1. We define a new class using HTMLElement as a prototype.

  2. In the constructor, we create shadow DOM, put our styles in it (from Counters & grid example above), add a single slot (a place where users write their markup), & a <div> for a transformed output.

  3. We move the (wrapped with spans) content from the slot to <div>.

Such a web component should be loaded as an ES6 module:

<script src="with-line-numbers.js" type="module"></script>

(i.e. evaluated when a page has finished parsing & its DOM is ready.)

<%= File.read 'with-line-numbers.js' -%>

See the Pen Untitled by Alexander Gromnitsky (@sigwait) on CodePen.


Tags: ойті
Authors: ag