如何用Go从Markdown源头上进行高亮显示

307 阅读5分钟

当我为这个博客编写基于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的语言范围竞争。

说了这么多,让我们看看我使用的概念验证。

实施

基本的想法很简单。我们加载一个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的功能,但对于外部依赖是一种负担的情况下,这可能是一个合适的选择。

我将尝试在这个博客中实现这篇文章中的概念验证,以进一步减少其文件大小和复杂性,我希望行业中更多的人开始采用这种思维方式。

玩得开心点!:)