当我为这个博客编写基于markdown的静态博客生成器时,我使用了highlightJS来进行语法高亮,因为这是我以前用过的东西,看起来是一个足够简单的解决方案。
然而,highlight现在是这个博客上唯一的一段JavaScript代码,我问自己,难道我不能在页面生成过程中进行语法高亮,并从网站上删除最后一点JS,以减少其大小的几千字节。
虽然这看起来有些矫枉过正,但我相信让网络更简单、更容易被广大用户访问有很多好处,尤其是对博客而言。
我的博客生成器使用Go,所以我寻找一些现有的语法高亮包,并意识到在这个领域真的没有什么可用的东西(还没有? 甚至Hugo也使用基于python的pygments(绝对令人惊讶)或外部JavaScript,如高亮或google-prettify。
我想要一个原生的Go解决方案,不需要依赖python或任何其他语言,因为这总是让人更难使用和分享一个工具。有一个针对pygments的Go封装器,但这并没有摆脱依赖性。
那么,我们有什么选择呢?
我想到的一个办法是用grumpy 将 pygments 移植到 Go 上,但我对它没有任何经验,而且对于 pygments 这样一个大的、老的项目来说,它似乎还处于开发初期。
另一个方法是使用一个基于Go的JavaScript解释器,比如otto,然后让highlight或google-prettify在Go二进制中做它的工作。虽然我认为这可行,但我不认为这是一个特别漂亮的解决方案。
另外,到目前为止,我在非JS环境中使用JS的一点经验告诉我,这也不是开箱即用的。
幸运的是,我发现了sourcegraph的伟大的syntaxhighlight包,它是一个用于高亮代码的本地Go库,具有极其简单的API。 该包在其输出中使用了google-prettify类,因此可以使用现有的模板来进行样式设计。
目前,syntaxhighlight只支持JavaScript、Java、Ruby、Python、Go和C,这对我来说已经足够了,但当然无法与pygment的语言范围竞争。
说了这么多,让我们看看我使用的概念验证。
- syntaxhighlight- 用于语法高亮
- blackfriday- 用于标记到网页的转换
- google-prettify--用于模板化(只有CSS)。
实施
基本的想法很简单。我们加载一个markdown 文件,将其转换为HTML ,找到代码部分,高亮显示,用高亮显示的部分替换它们,并将其渲染到一些包括google-prettify CSS的模板。
所以,模板是最简单的部分,可以看起来像这样。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
<title>Syntax Highlighting from Markdown with Go</title>
<style>
...google prettify CSS...
</style>
</head>
<body>
{{.Content}}
</body>
</html>
下一步是加载文件。在这个例子中,我只是用ioutil 来读取文件。
func main() {
// load markdown file
mdFile, err := ioutil.ReadFile("./example.md")
if err != nil {
log.Fatal(err)
}
然后用blackfriday 将markdown转换为HTML。
// convert markdown to html
html := blackfriday.MarkdownCommon(mdFile)
好的。在这一点上,我们有了给定的markdown文件的HTML 输出,其中有一些代码块,它们的格式是这样的。
<pre><code class="language-go">
...some Code...
</code></pre>
下一步是找到这些部分,把代码拿出来,高亮显示,再把它替换回去。不幸的是,我们不应该用RegEx来解析HTML,因为比如说,标记可能包含我们要找的同样的标签。
另一个解决方案是使用Go的html 包进行解析,或者像我在这个案例中所做的那样,使用goquery,这是用Go实现的一个类似jQuery的包。
有很多方法可以实现解析和替换代码标记的任务,所以只要选择你的毒药就可以了!
使用goquery,它可以是这样的。
func replaceCodeParts(mdFile []byte) (string, error) {
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(mdFile))
if err != nil {
return "", err
}
// find code-parts via css selector and replace them with highlighted versions
doc.Find("code[class*=\"language-\"]").Each(func(i int, s *goquery.Selection) {
oldCode := s.Text()
formatted, err := syntaxhighlight.AsHTML([]byte(oldCode))
if err != nil {
log.Fatal(err)
}
s.SetHtml(string(formatted))
})
new, err := doc.Html()
if err != nil {
return "", err
}
// replace unnecessarily added html tags
new = strings.Replace(new, "<html><head></head><body>", "", 1)
new = strings.Replace(new, "</body></html>", "", 1)
return new, nil
}
首先,我们创建一个新的goquery Document 。goquery API使我们很容易使用CSS选择器找到代码块,如code[class*=\"language-\"] 。
在这个选择器中,我们选择节点的Text ,使用syntaxhighlight的AsHTML 方法对其进行格式化,并在节点上设置结果HTML 。
这样,goqueryDocument 是转换后的、经过语法高亮的markdown文件的有效HTML表示。我们用doc.Html() 得到这个Document 的字符串表示。
在这个片段的最后,我们删除了goquery添加的<html> 、<head> 和<body> 标签,因为我们的模板中不需要它们。
好的。现在我们只需要调用replaceCodeParts 函数并将模板渲染为os.Stdout 。
// replace code-parts with syntax-highlighted parts
replaced, err := replaceCodeParts(html)
if err != nil {
log.Fatal(err)
}
// create template and write to stdout
t, err := template.ParseFiles("./template.html")
if err != nil {
log.Fatal(err)
}
err = t.Execute(os.Stdout, struct{ Content string }{Content: replaced})
if err != nil {
log.Fatal(err)
}
}
总结
有许多不同的方法可以让语法高亮的猫脱胎换骨。这篇文章展示了一个简单的解决方案,它只使用Go,没有任何外部依赖性。
当然,这种方法并不接近pygments的功能,但对于外部依赖是一种负担的情况下,这可能是一个合适的选择。
我将尝试在这个博客中实现这篇文章中的概念验证,以进一步减少其文件大小和复杂性,我希望行业中更多的人开始采用这种思维方式。
玩得开心点!:)