使用Chroma从Go中的Markdown源头进行语法加亮的方法

500 阅读5分钟

之前的一篇文章中,我描述了如何使用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 ,称为MonokaiLighthlbuf 的内容稍后将被写入模板中的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 版本的博客语法高亮的效果。)

资源