Markdown博客站内引用问题
涛叔使用 Markdown 写博客绕不过引用链接的问题。源文件扩展名是.md
,但转换后会变成.html
。为了正常跳转,写站内引用链接时只能使用 HTML 文件对应的路径。但这样一来,在编辑的时候就没法跳转到对应的.md
文件了。最近抽空给 pandoc 写了个 lua-filter,算是比较完美地解决了这个问题。今天分享给大家。
举个例子说明一下。比如我们有如下目录结构:
demo
├── bar
│ └── baz.md
└── foo.md
我们想在baz.md
文件中引用foo.md
文件。如果不做任何处理,我们需要在baz.md
写 [foo](../foo.html)
。因为foo.md
最终会转换成foo.html
,如果写成[foo](../foo.md)
,读者就无法正常打开foo.html
文件。但如果照顾了读者,写成[foo](../foo.html)
,在编辑bar.md
的时候就没法切换到foo.md
文件,因为扩展名不匹配。
我用过 hugo 框架,它会把 Markdown 转换成文件夹。比如foo.md
最终会变成foo/index.html
。这样做的好处是不需要在引用的时候指定扩展名,可以直接写成[foo](../foo/)
。但依然无法在编辑器内切换到对应的foo.md
文件。
问题的关键是不论链接写成[foo](../foo.html)
还是[foo](../foo/)
,都是对应转换后的 HTML 文件,并没有对应 Markdown 源文件。解决这个问题的终极方案是在 Markdown 文件中直接引用其他.md
文件路径,然后在生成 HTML 文件的时候再将其转换成.html
文件对应的路径。
本站使用 pandoc1,而 pandoc 支持使用 lua-filter 扩展 Markdown 转 HTML 的功能。今天我们就使用 lua-filter 将.md
链接转换成对应的.html
链接。
最简单的 lua-filter 如下:
return {{
Link = function (link)
-- do something
return link
end
}}
它需要返回一个 table,每个元素都是 pandoc 对象。pandoc 对象的 key 对应 Markdown 的语法单元。比如我们希望处理所有链接,所以需要给Link
设置回调函数。
讲道理 lua-filter 还是比较复杂的,本文只用到一点皮毛。有志深入研究的读者可以仔细阅读官方文档2。
Link
对象最重要的属性是target
,里面保存的就是链接的目标地址。Markdown 中的 [foo](../foo.md)
对应的target
属性就是../foo.md
。Link
的其他属性请参考官方3。
接下来的问题就是怎么在 lua 中将.md
转换成.html
。这里不能无脑把所有的.md
都替换成.html
,我们有可能真得会引用外站的 Markdown 文件。我想到的处理方案是只看站内引用,也就是检查链接是否以http
开头。只对站内链接做转换。但这种办法也有问题,就是无法引用站内 Markdown 文件了。考虑到我的平台会把所有站内.md
转换成.html
文件,而且不允许直接访问 Markdown 源文件,所以问题不大😜
lua 的语法比较古怪,跟 C 家族语言差距比较大。注释以--
开头。不相等用~=
表示,而不是常见的!=
。
字符串截取子串使用sub
函数,第一个参数表示开始位置,第二个表示子串长度。lua 还有一个特殊的地方是下标从1
而非0
开始。
此外,sub
函数还支持传入一个参数。sub(3)
表示跳过前两个字符,sub(-3)
表示从结尾提取长度为3
的子串。而更复杂的sub(1,-3)
则表示提取从开始到倒数第3
个位置为止的子串,也就是剔除最后3-1
个字符的子串。
我们需要的 lua 代码如下:
-- 检查链接开头不是 http
-- 而且结尾必须是 .md
if t:sub(1,4) ~= "http" and t:sub(-3) == ".md" then
-- 剔除最后的 md 再附加 .html
link.target = t:sub(1,-3).."html"
end
return link
在转换的时候需要使用--lua-filter
参数指定 lua 文件路径:
pandoc --lua-filter $LUA_FILTER bar.md -o bar.html
问题到这里基本就解决了。我们在写 Markdown 文件时可以直接根据相对路径填写对应的.md
文件路径。pandoc 在转 HTML 文件时会自动将.md
转换成.html
,同时解决了读者和作者双方的跳转问题。
注意,如果你的 pandoc 不是最新版本,则可能会碰到超链接<a>
变成<embed>
的问题,具体可以参考这个4。最新版本的 pandoc 修复了这个问题。
上面的方案还有一点小暇疵,那就是写链接的时候必须指定名字。也就是说[foo](../foo.md)
里面方括号中的foo
不能省略,不太方便。最简单的方案是用圆括号内的地址来填充方括号的内容。如果写成[](../foo.md)
效果跟写成[../foo.md](../foo.md)
一样就好。
这个功能也可以通过 lua 实现。我们在上面代码中return
之前插入下面的代码就可以了:
if #link.content == 0 then
link.content = pandoc.List:new({pandoc.Str(link.target)})
end
这样一来引用起来就更加方便。
相对路径虽然方便,维护上也很容易,却不利于 SEO。说白了就是别人转载了你文章,里面的相对链接就失效了。为了尽可能多地增加外链,最好还是把所有相对路径改为绝对路径。最好把站内路径全都转换成完整的URL。但这么一来不但会引入大量的维护成本,还会再一次导致无法在编辑器内跳转的问题。
但使用绝对链接也会带来新的问题。比如你想给博客做个镜像托管在 GitHub 或才 Cloudflare 上。只要域名发生变化,所有的站内链接都会失效。现在我的博客已经不再生成绝对链接了。
既然可以用 lua-filter 修改连接的后缀,那就一定可以将相对链接改为 URL。我们一鼓作气,完整地解决这个问题。
这里有两个核心问题。第一个是在 lua 脚本中获取当前 Markdown 文件的路径。第二个是根据当前路径和相对路径计算完整 URL。
第一个问题查了半天,它隐藏在PANDOC_STATE
这个全局变量的input_files
属性里。这是一个数组,文档没细讲它的构成。但实验发现数组的第一个元素就是 Markdown 文件的路径。所以文件路径如下:
local p = PANDOC_STATE.input_files[1]
第二个问题需要自己实现路径替换函数,因为 lua 标准库没有路径计算相关的函数😂
算法也比较简单。假设当前路径为a=/a/b/c/d.md
,相对路径为b=../../../1.md
,我们希望得到/1.md
。对a
来说,/d.md
不参与计算,应该事先跳过。然后遍历b
,每碰到一个../
就将其剔除同时还要把a
对应的路径也向上移动一级。整个过程如下:
b a
../../../1.md "/a/b/c"
../../1.md "/a/b"
../1.md "/a"
1.md ""
但还有一些特殊情况需要处理,比如应该剔除b
中的./
前缀等。
逻辑上没有问题了,编码时还会遇到另一个拦路虎find
函数。lua 中查看子串使用find
函数,但这个函数只能从左向右查。而我们处理当前路径的时候是从右向左查的。我搜了一下,发现大家是先把字符串反过来,然后从左往右处理,最后再反转回去😂完整代码如下:
-- realpath convert path with .. to absolute path
--
-- "/1.md" == realpath("a.md", "1.md")
-- "/1.md" == realpath("/a.md", "1.md")
-- "/1.md" == realpath("/a.md", "./1.md")
-- "/1.md" == realpath("/a/b.md", "../1.md")
-- "/1.md" == realpath("/a/b/c.md", "../../1.md")
-- "/1.md" == realpath("/a/b/c/d.md", "../../../1.md")
function realpath(a, b)
if a:sub(1,1) ~= '/' then a = '/' .. a end
c = a:reverse()
_,j = c:find("/")
c = c:sub(j+1,-1)
if b:sub(1,2) == "./" then
b = b:sub(3)
end
while (true) do
_,i = b:find("../")
_,j = c:find("/")
if i == nil or j == nil then break end
b = b:sub(i,-1)
c = c:sub(j+1,-1)
end
if b:sub(1,1) ~= '/' then b = '/' .. b end
return c:reverse()..b
end
上面的 lua-filter 修改链接的部分需要改成:
-- 提取环境变量
local envs = pandoc.system.environment()
local url = envs["site_url"]
local p = PANDOC_STATE.input_files[1]
-- 计算完整路径
t = realpath(p, t)
-- 拼接完整URL
t = url .. t
link.target = t:sub(1,-3).."html"
到这里才算完整解决了 Markdown 博客系统的引用链接问题。希望能对大家有启发。