前端代码高亮插件 highlight.js,默认是不带行号显示功能的。如果要显示行号,我们得自己实现,或是使用第三方插件。下面就来介绍几种实现方式,并分析优缺点。

1、第一种,使用 <li></li> 标签实现行号

当时还在写这个博客程序时,考察了行号显示功能,发现网上一些文章采用这样的方式自己实现行号显示:以换行符 \n 分割,将每行代码用 <li></li> 标签进行包裹,再在最外层用 <ol></ol> 标签包裹,设置 css 样式,利用 ol 列表内的 li 自动编号的特性,轻松的实现了行号显示。

这个方法最简单,但是有缺陷:遇到多行文本高亮着色时,highlight.js 是在第一行前面加 <span class="xxx"> 标签,在最后一行末尾加 </span> 闭合。如果简单的用 <li></li> 对每行进行包裹,就会造成 <span> 标签没有正常闭合,又根据 html 标签自动闭合的特性,仅有第一行能正常着色!

添加行号前:

添加行号后:

因为这个缺陷,所以我放弃使用这个方法。

2、第二种,使用 highlightjs-line-numbers.js 插件显示行号

highlightjs-line-numbers.js 这个插件应该是用得比较多的了,目前在 github 上有 243 个 star,本博客程序最初也是使用这个插件。

他的实现方式是,将 highlight.js 处理过的每行代码用 <td></td> 标签包裹,且每行前面又加一个新的 <td></td> 作为行号,结构就是 <tr><td>{行号}</td><td>{代码}</td></tr>,最后在最外层使用 <table></table> 包裹。也就是,行号在表格的第一列,代码在表格的第二列。行号是通过 css 设置的 :before{content:attr(data-line-number)},且设置了不可选中 user-select: none

此插件的作者考虑到了多行文本的 <span> 标签闭合问题,所以不会出现像第一种方法那样的缺陷。这也是当初我为什么选择这个插件的原因。

但是,在上周,我仔细观察时发现,这个插件在处理空行的时候,把空行替换成了空格。肉眼观察,当然不会有什么问题,只有复制粘贴的时候,才会发现多出来的这个空格。可是有些时候,我们的空行是有特殊意义的,插件最好不要对我的代码进行任何改动!

基于这个原因,我决定放弃 highlightjs-line-numbers.js 插件,用自己的办法实现行号显示。

3、第三种,我自己的实现方式

我的思路是,在每一行代码的行首加上 <span class="line-num" data-num="xxx"></span> 标签,然后通过 css 设置 :before 伪元素样式 content:attr(data-num) 显示行号,并对伪元素的位置设置偏移,增加行号和代码的间距。

由于我增加的这个标签是在每行的行首,所以它一定是在最左边(自动对齐),且不会破坏前面所讲 <span class="hljs-string"></span> 多行文本标签的闭合。又由于我增加的标签不包含新的文本内容,所以并未改动原代码。

下面就是我自己写的行号插件代码(参考或直接引用了 highlightjs-line-numbers.js 的部分代码)。

javascript 代码:

(function (w, d) {
    w.hljsln = {
        initLineNumbersOnLoad: initLineNumbersOnLoad,
        addLineNumbersForCode: addLineNumbersForCode
    };

    function initLineNumbersOnLoad() {
        if (d.readyState === 'interactive' || d.readyState === 'complete') {
            documentReady();
        } else {
            w.addEventListener('DOMContentLoaded', function () {
                documentReady();
            });
        }
    }

    function addLineNumbersForCode(html) {
        var num = 1;
        if (/\r|\n$/.test(html)) {
            html += '<span class="ln-eof"></span>';
        }
        html = html.replace(/\r\n|\r|\n/g, function (a) {
            num++;
            var text = ('  ' + num).substr(-3);
            return a + '<span class="ln-num" data-num="' + text + '"></span>';
        });
        html = '<span class="ln-num" data-num="  1"></span>' + html;
        html = '<span class="ln-bg"></span>' + html;
        return html;
    }

    function documentReady() {
        var elements = d.querySelectorAll('pre code');
        for (var i = 0; i < elements.length; i++) {
            if (elements[i].className.indexOf('hljsln') == -1) {
                var html = elements[i].innerHTML;
                html = addLineNumbersForCode(html);
                elements[i].innerHTML = html;
                elements[i].className += ' hljsln';
            }
        }
    }
}(window, document));

css 代码:

pre {
    position: relative;
}
.hljsln {
    display: block;
    margin-left: 2.4em;
    padding-left: 0.7em !important;
}
.hljsln::-webkit-scrollbar {
    height: 15px;
}
.hljsln::-webkit-scrollbar-thumb {
    background: #666;
}
.hljsln::-webkit-scrollbar-thumb:hover {
    background: #797979;
}
.hljsln::-webkit-scrollbar-thumb:active {
    background: #949494;
}
.hljsln .ln-bg {
    position: absolute;
    z-index: 1;
    top: 0;
    left: 0;
    width: 2.4em;
    height: 100%;
    background: #333;
}
.hljsln .ln-num {
    position: absolute;
    z-index: 2;
    left: 0;
    width: 2.4em;
    height: 1em;
    text-align: center;
    display: inline-block;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.hljsln .ln-num::before {
    color: #777;
    font-style: normal;
    font-weight: normal;
    content: attr(data-num);
}
.hljsln .ln-eof {
    display: inline-block;
}

将 js 代码保存为文件 highlight.line-numbers.js,加载并执行初始化如下:

<!-- 引入代码高亮插件 highlight.js -->
<script src="highlight.pack.js"></script>

<!-- 引入代码行号显示插件 highlight.line-numbers.js -->
<script src="highlight.line-numbers.js"></script>

<script>
// 初始化代码高亮插件
hljs.initHighlightingOnLoad();

// 初始化代码行号显示插件
hljsln.initLineNumbersOnLoad();
</script>

跟 highlightjs-line-numbers.js 对比,我的 js 代码少了很多,像 css 隔行变色、:hover 当前行变色等效果暂无法实现。

下面是添加行号后的代码效果如下:

插件已提交至码云:https://gitee.com/yangrz/highlight.line-numbers.js

如果您有更好的建议,欢迎留言!