为博客添加 GitHub Alerts 扩展
涛叔我在日常写作过程中除了要引用别人的内容外,有时候需要加入一些提示、技巧或者警告之类的内容。Markdown 标准语法中不包括这些功能,但有一些厂家已经开发出自己的扩展。其中 GitHub 家的 Alerts 扩展用得比较多。我周末闲来无事,就开自己的博客系统也加上了 Alerts 支持。
熟悉我的朋友应该知道,我的博客平台是基于 Pandoc 自行研发1的。自己搞的缺点是功能简陋,颜值一般。但优点也相当突出,那就是可以根据自己的需要定制各种功能。比如我之前介绍过的 Markdown 站内引用功能2和矢量绘图功能3,到现在都算是自己的得意之作。今天就来个锦上添花,实现 Alerts 扩展。
在开发 Alerts 功能之前,提示信息只能通过引用块展示,效果平平。比如这段:
有朋自远方来,不变乐乎!
如果加上 Alerts 功能,效果就会好上那么一丢丢~至少色彩更丰富,而且空间视觉分割效果也更加明显:
💡Tip有朋自远方来,不变乐乎!
完整语法请参考乐乎平台。说完效果,我们开始分享实现原理。
Pandoc 本身其实支持 GitHub 的 Alerts 扩展4,但目前需要在转码的时候使用 GitHub Flavored Markdown 格式。但我的博客依赖 Pandoc 扩展后的 Markdown 格式,所以没办法使用现成的 Alerts 功能。
根据 Issue 上的提示,我们可以通过 Pandoc 的 lua filter 来实现 Alerts 功能。如果你对 lua filter 不熟悉,可以本文底部的引用链接。
Pandoc 转码时会为每一个引用块生成 BlockQuote 对象。我们还可以为 BlockQuote 对象注册回调函数,这样 Pandoc 每碰到一个引用块就会调用我们的回调函数。我们在回调函数里将类似[!TIP]
这类的标记转换成对应的 HTML 结构就可以了。
首先,我们需要注册回调函数:
function github_alerts (block)
print(block)
return nil
end
return {{
BlockQuote = github_alerts,
}}
没错,就这么简单。我们这里注册了名为github_alerts
的回调函数。所有的引用块都会经过这个函数来处理。我们可以使用以下命令来测试回调函数:
pandoc --from markdown a.md --lua-filter ./a.lua
假设 a.md 的内容如下:
> [!NOTE]
> hello
>
> world
pandoc 就会输出如下 BlockQuote 对象:
BlockQuote [Para [Str "[!NOTE]",SoftBreak,Str "hello"],Para [Str "world"]]
注意观察 BlockQuote 对象的结果。它是一个列表,列表中的每个对象表示一个段落。段落对象又是一个列表,列表中有多种类型的对象。其中第一行的[!NOTE]
对应 Str 对象。紧随其后的是一个 SoftBreak 对象,也就是单个换行符。最后面是内容正文。
现在思路就很明确了。我们要根据第一个 Str 对象的内容来生成对应的 Alerts HTML 结构。
原始的 BlockQuote 转换成 HTML 后对应如下结构:
<blockquote>
<p>[!NOTE] hello</p>
<p>world</p>
</blockquote>
我们期望的结果为:
<blockquote>
<span class="alerting Note">
<span class="icon">📝</span>
<span class="alert">Note</span>
</span>
<p>hello</p>
<p>world</p>
</blockquote>
为了实现方便,我直接用 Emoji 当作图标。好处是比较省事,坏处则是不同平台显示效果不统一。不过这不算大毛病。
在 pandoc 中可以用 pandoc.Span 函数来构造 span 对象:
pandoc.Span("📝", {class="icon"})
该对象会生成如下 HTML 标签:
<span class="icon">📝</span>
因为我们要分类型生成复合标签结构,所以我封装了一个函数:
local new_alert = function (icon, name)
return pandoc.Span({
pandoc.Span(icon, {class="icon"}),
pandoc.Span(name, {class="alert"}),
}, {class="alerting "..name})
end
只需要传入图标和名字就可以生成前面说的 HTML 结构。所以我们需要的五种标签可以通过如下函数调用来生成:
("📝", "Note")
new_alert("💡", "Tip")
new_alert("⚠️", "Important")
new_alert("⛔", "Warning")
new_alert("🚧", "Caution") new_alert
现在我们需要把生成的标签结构嵌入到转码结果中。这需要用到table
相关操作。前面讲的列表对象都需要通过.content
字段来引用。我们需要把生成的 Span 对象插入到 BlockQuote 列表中最前面的位置。对于原来的[!NOTE]
和后面的软换行,我们需要要删除,不然生成 HTML 后还会看到它们。
-- 插入 Span 对象
table.insert(block.content, 1, t)
-- 删除 `[!NOTE]`
table.remove(block.content[1].content, 1)
-- 删除软换行
table.remove(block.content[1].content, 1)
最后把修改后的 BlockQuote 返回给 pandoc 就可以了。除此之外,我们还需要实现根据不同的 Alerts 类型生成不同的 HTML 代码。这部分就比较简单了。
-- 提取第一个段落
local para = block.content[1]
if not para then return nil end
-- 提取首段中第一个 str 对象
local str = para.content[1]
if not str then return nil end
-- 根据 str 对象的内容生成 Alerts 结构
local i = string.lower(str.text)
if i == '[!note]' then
t = new_alert("📝", "Note")
elseif i == '[!tip]' then
t = new_alert("💡", "Tip")
elseif i == '[!important]' then
t = new_alert("⚠️", "Important")
elseif i == '[!warning]' then
t = new_alert("⛔", "Warning")
elseif i == '[!caution]' then
t = new_alert("🚧", "Caution")
else
-- 不是 Alerts 语法不要修改
return nil
end
table.remove(block.content[1].content, 1)
table.remove(block.content[1].content, 1)
table.insert(block.content, 1, t)
return block
到此我们就完成了 lua filter 部分,生成了预想的 HTML 结构。接下来我们还需要加一点 CSS 样式才能实现刚开始的显示效果。
让我们再回顾一下新生成的 HTML 结构。
<blockquote>
<span class="alerting Note">
<span class="icon">📝</span>
<span class="alert">Note</span>
</span>
<p>hello</p>
<p>world</p>
</blockquote>
我的博客使用 water.css 构架,默认会给 blockquote 设置浅灰色的左边框。但 GitHub 的 Alerts 扩展会根据类型设置不同的边框颜色,而且标题部分的颜色也要根据边框颜色保持一致。
这里最好的办法是给生成的 blockquote 添加专门的类。但可惜 pandoc 中的 BlockQuote 对象好像不支持设置 class。所以我就想到了使用:has
伪类。简单来说通过:has
伪类可以实现根据子节点的类型来设置父节点样式的效果。
比如我们要把 Note 类型的左边框设置为蓝色,可以添加如下代码:
:has(span.alerting.Note) {
blockquoteborder-left-color: rgb(9, 105, 218);
}
色值是从 GitHub 上抄的,不用纠结。这里的关键是:has(span.alerting.Note)
,也就是只把包含有 span 子节点,而且这个 span 同时具有 alerting 和 Note 这两个 class 的 blockquote,我们才给它添加蓝色左边框。
相比之下,修改标题颜色就显得朴实无华了:
> span.alerting.Note {
blockquote color: rgb(9, 105, 218);
}
以上基本上就讲完了 Alerts 的所有实现细节。从中我们不难看出 pandoc 这套工具算是相当灵活了。对于 Markdown 这种没有严格标准的标记格式来说,pandoc 这种灵活性更为关键。我们可以实现任意厂家制定的扩展格式,甚至我们也可以制定自己的扩展格式。大浪淘沙,好的东西会不断沉淀下来,进入到 Markdown 标准。我想这也是 Markdown 生态的活力所在。
✏️Note因为需要添加 CSS 样式,所以服务端代码更新之后大家不能立刻用上 Alerts 功能。所有乐乎用户需要从 /style.css 中提取对应的 CSS 代码添加到自己的样式表中。这算是一个个遗憾,后面一定想办法解决。