代码高亮

Posted on Jul 17, 2026

前段时间对 Hugo 不太满意,恰好了解到 Project Gemini 和 gemtext,想从头写一个静态网页生成器,将 gemtext 转为 HTML。
大概是离期末还有一个月的时候,在学校想了几天如何实现 <highlight> </highlight> 内的高亮。虽然最后生成器因为太复杂而放弃了,但还是想记录下那几天的想法。

Hugo syntax highlighting

在 Markdown 中,用以下语法创建一个代码块,Hugo 将对 CODE 里的内容进行语法高亮。

```LANG [OPTIONS]
CODE
```

以 C 语言为例,生成类似下方的 HTML

<div class="highlight">
    <pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#a6e22e">SDL_SetHint</span>(SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR, <span style="color:#e6db74">"0"</span>);
</span></span></code></pre>
</div>

<pre> 标签内设置了代码块的背景颜色(#272822),Tab 宽度(4),未被高亮的文字的颜色(#f8f8f2) 等属性

<code> 标签指定语言(language-c) 和布局样式(style="display:flex;")。

...
<code class="language-c" data-lang="c">
    
    <span style="display:flex;">...</span>
    <span style="display:flex;">...</span>
    <span style="display:flex;">...</span>
    <span style="display:flex;">...</span>
    
</code>
...

从 <span style="display:flex;"> 开始到 </span> 结束代表一行代码;每新增一行,会添加一个 <span style="display:flex;"> </span> 元素。

<span style="display:flex;"><span><span style="color:#a6e22e">SDL_SetHint</span>(SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR, <span style="color:#e6db74">"0"</span>);
</span></span>
SDL_SetHint(SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR, "0");

在一行内,用 <span style="...">TEXT</span> 显示带有高亮/斜体等样式的文本,无需高亮的文本将遵循 <pre> 标签内设置的的样式。

如 <span style="color:#a6e22e">SDL_SetHint</span> 中函数名 SDL_SetHint 的颜色被设置为 #a6e22e; SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR,(),;,没有被高亮,遵循 <pre> 中设置的颜色 #f8f8f2。

也可以用 <span style="font-style:italic">// This is a comment</span> 显示一个字体样式为斜体的注释。

通用高亮实现

根据上文,实现一个简单的高亮只需要对文本进行词法分析,匹配特定的字符并生成 Token 流,根据每个 Token 的类型,生成对应的 HTML 即可。

enum Token_Kind {
    RAW, // 无高亮
    S_COMMENT, // 单行注释
    M_COMMENT, // 多行注释
    STRING, // 字符串
    NUMBER, // 数字
    COLON, // 冒号
    OPERATOR, // 运算符
};

struct Token {
    Token_Kind kind;
    size_t start;
    size_t end;
};

struct TokenList {
    size_t capacity;
    size_t idx;
    Token *list;
};

用 start 和 end 表示一个 Token 的值,一个二维数组记录每行的开始与结尾引索,一个临时储存 Token 的变量 token_tmp。

  • 字符串: start 为 " 第一次出现时的引索,end 为 " 第二次出现时的引索
  • 数字: 先判断是否为参数/字面量,然后将状态设为 NUMBER,在扫描到第一个非数字时停止,将此时的引索写入 end
  • 单行注释: 扫描到 / 时判断后方是否为 /,是则将本行结尾引索写入 end
  • 多行注释: 扫描到 /* 后设置状态,用于判断嵌套注释,出现 */ 时停止,写入引索
  • 运算符: * 需判断表示的是注释还是单纯的运算符
  • 冒号: 将 end 设为冒号的引索,start 为本行开头

字符串/数字/多行注释在识别到特定字符时只进行了一次状态的转换(RAW -> <KIND>),在首次扫描到 <KIND> 类型的 Token 时,会将现有的 token_tmp 写入 Token 流并重置,状态设为 <KIND>;等到扫描到非 <KIND> 类型,将新的 Token 写入并恢复 RAW 。

而冒号/运算符/单行注释有两次转换(RAW -> <KIND> -> RAW),在扫描到特定字符时就可将新 Token 写入。所以需要先将上一 Token(token_tmp) 写入流并重置,再在流中写入新的 Token,最后恢复状态。

以上规则可以生成如下 Token 流

Total: 21

---
RAW 0 - 19
#include <stdio.h>


---
---
M_COMMENT 20 - 47
/*
 * Multi-line comment
 */
---
---
RAW 48 - 74

int main(void)
{
    char
---
---
OPERATOR 75 - 75
*
---
---
RAW 76 - 81
str =
---
---
STRING 82 - 97
"Hello from GCC"
---
---
RAW 98 - 111
;
    int a =
---
---
NUMBER 112 - 115
6969
---
---
RAW 116 - 117
;
---
---
S_COMMENT 118 - 138
// Declare variable a
---
---
RAW 139 - 153

    float b =
---
---
NUMBER 154 - 157
69.0
---
---
RAW 158 - 173
;
    int c = a
---
---
OPERATOR 174 - 174
+
---
---
RAW 175 - 175

---
---
NUMBER 176 - 176
1
---
---
RAW 177 - 178
;

---
---
COLON 180 - 183
end:
---
---
RAW 184 - 196


    return
---
---
NUMBER 197 - 197
0
---
---
RAW 198 - 201
;
}

---

之后根据每个 Token 的类型和值,生成不同的 span 元素。注意需要将含有 < > 的代码转义后再生成。

#include <stdio.h>

/*
 * Multi-line comment
 */
int main(void)
{
    char *str = "Hello from GCC";
    int a = 6969; // Declare variable a
    float b = 69.0;
    int c = a + 1;

end:

    return 0;
}

以上测试代码可以生成如下 HTML

<div class="highlight">
<pre style="color: #f8f8f2; background-color: #272822;"><code class="language-c" data-lang="c"><span>#include &lt;stdio.h&gt;

<span style="color: #0086d2; font-style: italic;">/*
 * Multi-line comment
 */</span>
int main(void)
{
    char <span style="color: red;">*</span>str = <span style="color: #9ece6a;">"Hello from GCC"</span>;
    int a = <span style="color: #2aa198;">6969</span>; <span style="color: #0086d2; font-style: italic;">// Declare variable a</span>
    float b = <span style="color: #2aa198;">69.0</span>;
    int c = a <span style="color: red;">+</span> <span style="color: #2aa198;">1</span>;
<span style="color: #990000;">end:</span>

    return <span style="color: #2aa198;">0</span>;
}
</span></code></pre></div>

为了方便生成,删除了一些样式和每行开头的 display:flex;。

可以在 HTML Online Viewer 预览效果,为了加载速度就不放图了。