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.
1 import hljs from 'highlight.js'; 2 import Markdown from 'markdown-it'; 3 4 const 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">'Hello'</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'World'</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.
1 const 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
1 const 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">'Hello'</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">'World'</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.
1 pre.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 ✌️