avatar Nihil

Nichts Hsu

  • 首页
  • 子域
  • 分类
  • 标签
  • 归档
  • 关于
首页 为使用 Rouge 的 Jekyll 主题添加高亮指定行的功能
文章

为使用 Rouge 的 Jekyll 主题添加高亮指定行的功能

发表于 2022/09/27 更新于 2022/12/01
作者 Nichts Hsu
7 分钟阅读
为使用 Rouge 的 Jekyll 主题添加高亮指定行的功能
为使用 Rouge 的 Jekyll 主题添加高亮指定行的功能

前言

实际上,这个功能在 Jekyll 的 Issue #8621 上早有讨论,但是时至今日这个功能迟迟没有进展。

相关的讨论者提出了一些简易的 Ruby 脚本,但是这些脚本功能并不完整,例如不支持行号。但是奈何本人又不会 Ruby,只能考虑另辟蹊径,通过 JS 来实现行高亮。

二月时写了一个效果较差的版本,近日重新写了一个更好一点的版本,因此重写本帖。

Ruby 脚本在生成站点时静态地添加高亮块,没有运行时时间成本。JS 脚本在浏览者打开网页时才动态计算并添加高亮块,有一定的运行时时间成本。如果有比较合适的 Ruby 脚本,还是使用 Ruby 的好。

实现思路

  1. 通过 Kramdown 的语法为代码框增加属性:

    1
    2
    3
    4
    5
    6
    
    ```c
    int main(int argc, char* argv[]) {
       return 0;
    }
    ```
    {: highlight-lines="2" }
    

    highlight-lines 支持复数行的选择,例如 highlight-lines="2-4, 7, 9-16, 21"。

  2. 在 JS 中,通过正则匹配,将 highlight-lines 的值拆为具体的行数,例如将 "2-4, 7, 9-16, 21" 拆成 [2, 3, 4, 7, 9, 10, 11, 12, 13, 14, 15, 16, 21]。
  3. 遍历所有 $(".highlighter-rouge"),找到每个代码块内的最后一个 <pre> 子元素(因为行号也是一个 <pre>,要排除行号的影响)。
  4. 遍历所有子元素(使用 nextSibling 而不是 nextElementSibling,因为我们要匹配纯字符元素)。匹配 \n,并且从 \n 处将当前元素截断,把它拆成两个或更多个同类型的元素。之后,如果当前的行数在高亮行的列表中,则新增一个 class="hll" 的 <span> 元素,将一行内所有元素都加入其中。
  5. 在需要的页面中调用该脚本,所有高亮行会被 <span class="hll"> 元素包裹。
  6. 在 CSS 中为 .hll 配置背景色。

具体实现可参考(比较烂,感觉优化空间挺大):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
$(function () {
    let highlightLines = function (codeBlock, highlight_lines) {
        let current_line = null;
        let current_lineno = 1;
        for (let cur = codeBlock.firstChild; cur != null; cur = cur.nextSibling) {
            if (current_line == null)
                current_line = cur;
            let contents = cur.textContent.split(/\n/g);
            if ((contents || []).length > 1) {
                let newline_count = contents.length - 1;
                let subNodes = [];
                for (let i = 0; i < newline_count + 1; i++) {
                    let subNode = cur.cloneNode(false);
                    subNode.textContent = contents[i];
                    if (i != newline_count)
                        subNode.textContent += '\n';
                    codeBlock.insertBefore(subNode, cur);
                    subNodes.push(subNode);
                }
                codeBlock.removeChild(cur);
                cur = subNodes[newline_count];
                for (let i = 0; i < newline_count; i++) {
                    if (highlight_lines.includes(current_lineno)) {
                        let hll_node = document.createElement("span");
                        hll_node.setAttribute("class", "hll");
                        codeBlock.insertBefore(hll_node, current_line);
                        for (let next = hll_node.nextSibling; next != subNodes[i]; next = hll_node.nextSibling)
                            hll_node.appendChild(next);
                        hll_node.appendChild(subNodes[i]);
                    }
                    current_line = subNodes[i + 1];
                    current_lineno++;
                }
            }
        }
    };

    $(".highlighter-rouge").each(function () {
        const attr_highlight_lines = $(this).attr("highlight-lines");
        if (attr_highlight_lines && attr_highlight_lines.length > 0) {
            let lines = [];
            let scopes = ("," + attr_highlight_lines).match(/(?<=\s|,)\d+(-\d+)?/g)
            scopes.forEach(function (val) {
                let pos = val.split('-');
                let start = parseInt(pos[0]);
                if (pos.length > 1) {
                    let end = parseInt(pos[1]);
                    if (end >= start) {
                        for (let i = start; i <= end; i++) {
                            lines.push(i);
                        }
                    }
                } else if (pos.length == 1) {
                    lines.push(start);
                }
            })
            let pre = $("pre", $(this));
            pre = pre[pre.length - 1];
            highlightLines(pre, lines);
        }
    })
})

CSS 配置:

1
.highlighter-rouge .hll { background-color: #ffff83; }

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
```rust
fn r<A, R>(g: impl Fn(&dyn Fn(A) -> R, A) -> R) -> impl Fn(A) -> R {
    fn r_inner<'a, A, R>(g: &'a dyn Fn(&dyn Fn(A) -> R, A) -> R) -> impl Fn(A) -> R + 'a {
        move |x| g(&r_inner(g), x)
    }
    move |x| r_inner(&g)(x)
}

fn main() {
    let g = |f: &dyn Fn(usize) -> usize, x| match x {
        0 => 1,
        _ => x * f(x - 1),
    };

    let fact = r(g);
    println!("{}", fact(5)); // 将会输出 120
}
```
{: highlight-lines="3, 9-12" }

效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn r<A, R>(g: impl Fn(&dyn Fn(A) -> R, A) -> R) -> impl Fn(A) -> R {
    fn r_inner<'a, A, R>(g: &'a dyn Fn(&dyn Fn(A) -> R, A) -> R) -> impl Fn(A) -> R + 'a {
        move |x| g(&r_inner(g), x)
    }
    move |x| r_inner(&g)(x)
}

fn main() {
    let g = |f: &dyn Fn(usize) -> usize, x| match x {
        0 => 1,
        _ => x * f(x - 1),
    };

    let fact = r(g);
    println!("{}", fact(5)); // 将会输出 120
}

即使面对整个代码块只有一个元素的纯文本代码块,也能正常工作:

1
2
3
4
5
6
7
```plaintext
由于 Rouge 的特性,不会对纯文本代码段进行分割,
因此虽然该代码段有很多行,但是 Rouge 会使用一整个 #text 来包装他们。
这也是为什么我们需要先将元素以 `\n` 分割为多个元素,
否则像这样的纯文本,很难对其进行行高亮。
```
{: highlight-lines="2" }

效果:

1
2
3
4
由于 Rouge 的特性,不会对纯文本代码段进行分割,
因此虽然该代码段有很多行,但是 Rouge 会使用一整个 #text 来包装他们。
这也是为什么我们需要先将元素以 `\n` 分割为多个元素,
否则像这样的纯文本,很难对其进行行高亮。

即使有多个连续的空行,也能正常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
```python
# -*- coding: UTF-8 -*-



def just_print( a ):
    print(a)




if __name__ == "__main__":
    just_print("Hello World")
```
{: highlight-lines="5, 12" }

效果:

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: UTF-8 -*-



def just_print( a ):
    print(a)




if __name__ == "__main__":
    just_print("Hello World")
教程, 网站
jekyll 教程 网站 javascript
本文由作者按照 CC BY 4.0 进行授权
分享

最近更新

  • 『I Wanna』 Best Bye To 2016
  • [译] Rust 中的内联
  • [Rust] 幽灵索引类型与匿名结构体
  • [C++] 深入了解左值与右值
  • Android.bp 中启用 openmp
外部链接
  • 996.icu
  •  此博客的 Github 仓库
  •  Olimi 的个人博客

文章内容

相关文章

2021/03/21

使用 Valine 替换 Chirpy 主题中的 Disqus 评论系统

由于 Chirpy 版本更替,该帖子内容已失去时效性,请谨慎参考 前置工作 根据 Valine 官方教程注册 LeanCloud 以获取 APP ID 和 APP Key。注:注册国内版 LeanCloud 需要绑定已备案的域名,而注册国际版 LeanCloud 则不需要。 如果是 fork 主题搭建博客,修改对应文件即可。如果是使用 theme 或者 remote_theme,则需...

2022/11/01

在高版本系统上为 Qt6 生成 AppImage

前言 目前,大部分的 AppImage 的教程与工具都建议你在最低所支持的系统上进行编译打包,这是由于 Linux 系统的兼容性,在旧版本系统打包的软件可以正常在新版本系统中运行,反过来则不行。 但是这一点对于 Qt 用户尤其是 Qt6 用户而言很不友好:一方面,在旧版本的 Linux 系统上很难安装高版本的 Qt,另一方面,过于老旧的 GCC 不支持大量的 C++ 新特性,需要对代码进...

2022/01/11

Git 从基础到进阶

前言 Git 是什么 想象这么一个场景,你和你的几个同事一起开发一个应用,假设同事 A 修改了代码 1,同事 B 修改了代码 2,你自己修改了代码 3,要如何将你们的修改安全、准确地同步到所有人的电脑上?如果有一天,应用突然运行不了了,你尝试 debug 无果,想要回退到上一次能够正常运行的代码版本,你要如何操作? 这就是为什么我们需要版本控制工具。它们对代码的提交和修改进行纪录,方便...

USB redriver: NB7VPQ904M 浅析

GNU C 一些有趣的扩展语法

© 2024 Nichts Hsu. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

热门标签

编程语言 教程 rust c++ android c++20 usb 翻译 linux qt

发现新版本的内容。