Add Line Numbers To Markdown Code Blocks

Inspiration

I needed line numbers in my previous blog post... So here we are

Existing Solution

There is a project already that kind of does what we need, highlightjs-line-numbers.js. However, we can't exactly use it as is since we need it to run on the server.

This problem didn't seem pretty straightforward/interesting so I wanted to try to solve it with another approach instead. Why reuse code when you can do it yourself... amirite 😅

The Approach

I was going to try creating a plugin for markdown-it but then wondered if there was a simpler/lazier way. After diving into the source code and looking for the implementation of highlight, I found it, the special 3rd parameter langAttrs. This wasn't in the readme or the docs, maybe they're deprecating it 🤷‍♂️ but I'll continue being lazy until then.

Libraries

No Line Numbers

Let's use the following markdown file for the demo

```javascript showLineNumbers
console.log('Hello');
console.log('World');
```

Here's a typical implementation of markdown-it with highlight.js (for syntax highlighting). This is copied over from our previous markdown blog post.

1import hljs from 'highlight.js';
2import Markdown from 'markdown-it';
3
4const md = Markdown({
5 highlight: (
6 str: string,
7 lang: string,
8 ) => {
9 const code = lang && hljs.getLanguage(lang)
10 ? hljs.highlight(str, {
11 language: lang,
12 ignoreIllegals: true,
13 }).value
14 : md.utils.escapeHtml(str);
15 return `<pre class="hljs"><code>${code}</code></pre>`;
16 },
17});

Currently, if we run this...

md.render(markdown);

Our output will be the following.

<pre class="hljs">
  <code>
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Hello&#x27;</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;World&#x27;</span>);
  </code>
</pre>

No line numbers 😔

With Line Numbers

Now let's make use of the 3rd parameter, langAttrs, and add the ability to show line numbers.

1const md = Markdown({
2 highlight: (
3 str: string,
4 lang: string,
5 attrRaw: string = ''
6 ) => {
7 const attrs = attrRaw.split(/\s+/g);
8 const showLineNumbers = attrs.includes('showLineNumbers');
9
10 let code = lang && hljs.getLanguage(lang)
11 ? hljs.highlight(str, {
12 language: lang,
13 ignoreIllegals: true,
14 }).value
15 : md.utils.escapeHtml(str);
16
17 if (showLineNumbers) {
18 code = applyLineNumbers(code);
19 }
20
21 return `<pre class="hljs"><code>${code}</code></pre>`;
22 },
23});

And the helper function applyLineNumbers

1const applyLineNumbers = (code: string) => {
2 const lines = code.trim().split('\n');
3
4 const rows = lines.map((line, idx) => {
5 const lineNumber = idx + 1;
6
7 let html = '<tr>';
8 html += `<td class="line-number">${lineNumber}</td>`;
9 html += `<td class="code-line">${line}</td>`;
10 html += '</tr>';
11 return html;
12 });
13
14 return `<table><tbody>${rows.join('')}</tbody></table>`;
15};

If we run this again on the same markdown, we now get the following.

<pre class="hljs">
  <code>
    <table>
      <tbody>
        <tr>
          <td class="line-number">1</td>
          <td class="code-line">
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;Hello&#x27;</span>);
          </td>
        </tr>
        <tr>
          <td class="line-number">2</td>
          <td class="code-line">
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">&#x27;World&#x27;</span>);
          </td>
        </tr>
      </tbody>
    </table>
  </code>
</pre>

Line numbers 🎉

I've added some classes to the <td/> so we can apply some basic styling as needed. This is what I currently use for this blog.

1pre.hljs code {
2 table {
3 width: 100%;
4 }
5
6 .line-number {
7 min-width: 22px;
8 text-align: right;
9 width: 1%;
10 }
11
12 .code-line {
13 padding-left: 20px;
14 }
15}

And Bam! Pretty code blocks.

Conclusion

Pretty easy eh? We could make this guy a bit more powerful by treating langAttrs like a cli argv string. So for example, if we wanted to add a feature to highlight specific lines we can do something like...

```javascript showLineNumbers highlightLine=1-5,8
// @todo Add 8+ lines of amazing javascript code
```

This would mean we want line numbers and highlight lines 1 to 5 and 8.

I'll try this next time or build a markdown-it plugin or use PrismJS... We'll see... Till then ✌️