在之前的一篇文章中,我描述了如何使用Go从markdown源创建语法高亮的HTML。该帖子的代码可以在这里找到。
然而,我最近写了几篇关于kotlin 的文章,并计划在将来写一些关于rust ,也许还有c 的东西,而到目前为止,我用于语法高亮的库在不同语言方面的支持非常有限。
幸运的是,现在有一个基于神奇的pygments python语法高亮库的Go库。这个库叫做chroma,这篇文章将展示一个例子,说明如何使用chroma ,从markdown源创建语法高亮的HTML。
Chroma 语法高亮库是相当强大的。它提供了大量不同的语言和风格,可以按照你想要的方式来格式化代码。它的使用也很简单,所以使用它是一种乐趣。
对于降价到html的转换,我们将再次使用blackfriday。
所以让我们开始吧!
实施
实现的基本结构保持不变。我们加载一个markdown 文件,并将其转换为HTML 。然后我们将搜索包含代码的部分,并用高亮的代码替换它们,这些代码将使用chroma 。
我们将渲染代码的模板是如下。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
<title>Syntax Highlighting from Markdown with Chroma</title>
{{.Style}}
</head>
<body>
{{.Content}}
</body>
</html>
Content 是实际转换为HTML的markdown将被呈现的地方,而 是我们将添加由 生成的 ,用于我们在这个例子中想要使用的样式。Style chroma CSS
但在这之前,我们需要先做一些步骤,比如加载markdown文件。
func main() {
// load markdown file
mdFile, err := ioutil.ReadFile("./example.md")
if err != nil {
log.Fatal(err)
}
解析模板文件。
t, err := template.ParseFiles("./template.html")
if err != nil {
log.Fatal(err)
}
并将markdown转换为HTML。
// convert markdown to html
html := blackfriday.MarkdownCommon(mdFile)
现在我们遇到了与chroma 有关的第一个变化。在旧的语法高亮中,我手动复制了我想要的样式的CSS。Chroma 有动态生成这种内置的功能。
// write css
hlbuf := bytes.Buffer{}
hlw := bufio.NewWriter(&hlbuf)
formatter := html.New(html.WithClasses())
if err := formatter.WriteCSS(hlw, styles.MonokaiLight); err != nil {
log.Fatal(err)
}
hlw.Flush()
这个片段为我们在这个例子中想要使用的样式创建了CSS ,称为MonokaiLight 。hlbuf 的内容稍后将被写入模板中的Style 变量。
好了,现在我们进入了实际的语法高亮部分。我们的想法是要找到这样的代码部分。
<pre><code class="language-go">
...some Code...
</code></pre>
一旦我们使用goquery库找到这样的代码部分,我们就从class 中解析出所使用的语言,并尝试对<code> 标签中的代码进行语法高亮,用新的、高亮的内容替换旧的内容。
我们创建了一个replaceCodeParts ,该函数接收转换后的HTML,并返回一个包含HTML与高亮代码部分的字符串。
首先,我们读入转换后的HTML并从中创建一个goquery 文档,我们可以用它来搜索代码部分。
func replaceCodeParts(mdFile []byte) (string, error) {
byteReader := bytes.NewReader(mdFile)
doc, err := goquery.NewDocumentFromReader(byteReader)
if err != nil {
return "", err
}
然后,我们使用一个goquery 选择器来寻找我们感兴趣的代码部分。
// find code-parts via selector and replace them with highlighted versions
doc.Find("code[class*=\"language-\"]").Each(func(i int, s *goquery.Selection) {
...
})
现在是实际的高亮代码。首先,我们解析要使用的语言,并选择正确的词法。请记住,我在这里省略了任何错误处理代码,但几乎所有下面的步骤都可能失败,需要进行相应处理。GitHub上的代码示例包含了适当的错误处理。
class, _ := s.Attr("class")
lang := strings.TrimPrefix(class, "language-")
lexer := lexers.Get(lang)
现在我们有了正确的lexer ,这是必要的,这样我们的代码才能被正确标记。接下来,我们要做的就是,从Selector 中抓取code ,并将其标记化。
oldCode := s.Text()
iterator, _ := lexer.Tokenise(nil, string(oldCode))
现在,剩下的就是实例化一个formatter - 在我们的例子中,我们想输出html ,但chroma 也提供了其他选项。格式化器是chroma 的一部分,它实际上是根据输入的代码和使用的lexer ,生成高亮输出。
formatter := html.New(html.WithClasses())
b := bytes.Buffer{}
buf := bufio.NewWriter(&b)
formatter.Format(buf, styles.GitHub, iterator)
buf.Flush()
s.SetHtml(b.String())
上面的片段用WithClasses 选项创建了HTML格式化器,这意味着我们不希望有inline-CSS,而是希望使用类。这也意味着,我们需要在某个地方包含CSS(我们在本例的开头已经这样做了)。然后,我们对代码进行格式化并将其写入我们的缓冲区。
一旦完成,缓冲区的内容就会被写入Selector ,从而用我们新的、语法高亮的代码来替换之前的内容。
替换完代码后,剩下的就是创建一个新的HTML文档,把它返回给调用者。
new, err := doc.Html()
if err != nil {
return "", err
}
return new, nil
}
好了,我们现在要做的就是调用函数,并在main 函数中创建输出的HTML。
// replace code-parts with syntax-highlighted parts
replaced, err := replaceCodeParts(htmlSrc)
if err != nil {
log.Fatal(err)
}
// write html output
if err := t.Execute(os.Stdout, struct {
Content template.HTML
CSS template.CSS
}{
Content: template.HTML(replaced),
Style: template.CSS("<style>" + hlbuf.String() + "</style>"),
}); err != nil {
log.Fatal(err)
}
这里没有什么花哨的事情发生--我们用HTML输入来调用函数,用上面创建的CSS 和我们新的HTML 来执行我们的template 。
就这样了。你可以在这里找到完整的代码。
总结
chroma 库非常棒。当我创建这个库的第一个实现时,我也考虑过咬牙使用pygments ,为我的博客生成器接受python-dependency,但我决定不这样做,尽管旧的实现有限制。
我很高兴现在有了一个原生的Go选项来做全功能的语法高亮,如果你正在读这篇文章,你已经看到了chroma 版本的博客语法高亮的效果。)