代码高亮
前段时间对 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 <stdio.h>
<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 预览效果,为了加载速度就不放图了。