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:
- line wrapping must be visually apparent;
- selecting a chunk of code implies ignoring line numbers themselves;
- 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:
- get
innerHTML
of a <pre>
element;
- break the string into an array of substrings by searching for newlines;
- prepend each substring with a number;
- 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:
We define a new class using HTMLElement
as a prototype.
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.
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