Add Goldmark as the new default markdown handler

This commit adds the fast and CommonMark compliant Goldmark as the new default markdown handler in Hugo.

If you want to continue using BlackFriday as the default for md/markdown extensions, you can use this configuration:

```toml
[markup]
defaultMarkdownHandler="blackfriday"
```

Fixes #5963
Fixes #1778
Fixes #6355
This commit is contained in:
Bjørn Erik Pedersen
2019-11-06 20:10:47 +01:00
parent a3fe5e5e35
commit bfb9613a14
69 changed files with 3424 additions and 1668 deletions

View File

@@ -32,7 +32,6 @@ import (
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
jww "github.com/spf13/jwalterweatherman"
"strings"
)
@@ -58,9 +57,6 @@ type ContentSpec struct {
BuildExpired bool
BuildDrafts bool
Highlight func(code, lang, optsStr string) (string, error)
defatultPygmentsOpts map[string]string
Cfg config.Provider
}
@@ -77,36 +73,10 @@ func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero
Cfg: cfg,
}
// Highlighting setup
options, err := parseDefaultPygmentsOpts(cfg)
if err != nil {
return nil, err
}
spec.defatultPygmentsOpts = options
// Use the Pygmentize on path if present
useClassic := false
h := newHiglighters(spec)
if cfg.GetBool("pygmentsUseClassic") {
if !hasPygments() {
jww.WARN.Println("Highlighting with pygmentsUseClassic set requires Pygments to be installed and in the path")
} else {
useClassic = true
}
}
if useClassic {
spec.Highlight = h.pygmentsHighlight
} else {
spec.Highlight = h.chromaHighlight
}
converterProvider, err := markup.NewConverterProvider(converter.ProviderConfig{
Cfg: cfg,
ContentFs: contentFs,
Logger: logger,
Highlight: spec.Highlight,
})
if err != nil {
return nil, err
@@ -220,6 +190,21 @@ func (c *ContentSpec) RenderMarkdown(src []byte) ([]byte, error) {
return b.Bytes(), nil
}
func (c *ContentSpec) ResolveMarkup(in string) string {
in = strings.ToLower(in)
switch in {
case "md", "markdown", "mdown":
return "markdown"
case "html", "htm":
return "html"
default:
if conv := c.Converters.Get(in); conv != nil {
return conv.Name()
}
}
return ""
}
// TotalWords counts instance of one or more consecutive white space
// characters, as defined by unicode.IsSpace, in s.
// This is a cheaper way of word counting than the obvious len(strings.Fields(s)).

View File

@@ -44,11 +44,6 @@ import (
// FilePathSeparator as defined by os.Separator.
const FilePathSeparator = string(filepath.Separator)
// Strips carriage returns from third-party / external processes (useful for Windows)
func normalizeExternalHelperLineFeeds(content []byte) []byte {
return bytes.Replace(content, []byte("\r"), []byte(""), -1)
}
// FindAvailablePort returns an available and valid TCP port.
func FindAvailablePort() (*net.TCPAddr, error) {
l, err := net.Listen("tcp", ":0")
@@ -74,28 +69,6 @@ func InStringArray(arr []string, el string) bool {
return false
}
// GuessType attempts to guess the type of file from a given string.
func GuessType(in string) string {
switch strings.ToLower(in) {
case "md", "markdown", "mdown":
return "markdown"
case "asciidoc", "adoc", "ad":
return "asciidoc"
case "mmark":
return "mmark"
case "rst":
return "rst"
case "pandoc", "pdc":
return "pandoc"
case "html", "htm":
return "html"
case "org":
return "org"
}
return ""
}
// FirstUpper returns a string with the first character as upper case.
func FirstUpper(s string) string {
if s == "" {

View File

@@ -19,11 +19,20 @@ import (
"strings"
"testing"
"github.com/spf13/viper"
"github.com/gohugoio/hugo/common/loggers"
qt "github.com/frankban/quicktest"
"github.com/spf13/afero"
)
func TestGuessType(t *testing.T) {
func TestResolveMarkup(t *testing.T) {
c := qt.New(t)
cfg := viper.New()
spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs())
c.Assert(err, qt.IsNil)
for i, this := range []struct {
in string
expect string
@@ -43,7 +52,7 @@ func TestGuessType(t *testing.T) {
{"org", "org"},
{"excel", ""},
} {
result := GuessType(this.in)
result := spec.ResolveMarkup(this.in)
if result != this.expect {
t.Errorf("[%d] got %s but expected %s", i, result, this.expect)
}

View File

@@ -1,402 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"bytes"
"crypto/sha1"
"fmt"
"io"
"io/ioutil"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugofs"
jww "github.com/spf13/jwalterweatherman"
)
const pygmentsBin = "pygmentize"
// hasPygments checks to see if Pygments is installed and available
// on the system.
func hasPygments() bool {
if _, err := exec.LookPath(pygmentsBin); err != nil {
return false
}
return true
}
type highlighters struct {
cs *ContentSpec
ignoreCache bool
cacheDir string
}
func newHiglighters(cs *ContentSpec) highlighters {
return highlighters{cs: cs, ignoreCache: cs.Cfg.GetBool("ignoreCache"), cacheDir: cs.Cfg.GetString("cacheDir")}
}
func (h highlighters) chromaHighlight(code, lang, optsStr string) (string, error) {
opts, err := h.cs.parsePygmentsOpts(optsStr)
if err != nil {
jww.ERROR.Print(err.Error())
return code, err
}
style, found := opts["style"]
if !found || style == "" {
style = "friendly"
}
f, err := h.cs.chromaFormatterFromOptions(opts)
if err != nil {
jww.ERROR.Print(err.Error())
return code, err
}
b := bp.GetBuffer()
defer bp.PutBuffer(b)
err = chromaHighlight(b, code, lang, style, f)
if err != nil {
jww.ERROR.Printf("Highlight failed: %s\nLang: %q\nCode: \n%s", err, lang, code)
return code, err
}
return h.injectCodeTag(`<div class="highlight">`+b.String()+"</div>", lang), nil
}
func (h highlighters) pygmentsHighlight(code, lang, optsStr string) (string, error) {
options, err := h.cs.createPygmentsOptionsString(optsStr)
if err != nil {
jww.ERROR.Print(err.Error())
return code, nil
}
// Try to read from cache first
hash := sha1.New()
io.WriteString(hash, code)
io.WriteString(hash, lang)
io.WriteString(hash, options)
fs := hugofs.Os
var cachefile string
if !h.ignoreCache && h.cacheDir != "" {
cachefile = filepath.Join(h.cacheDir, fmt.Sprintf("pygments-%x", hash.Sum(nil)))
exists, err := Exists(cachefile, fs)
if err != nil {
jww.ERROR.Print(err.Error())
return code, nil
}
if exists {
f, err := fs.Open(cachefile)
if err != nil {
jww.ERROR.Print(err.Error())
return code, nil
}
s, err := ioutil.ReadAll(f)
if err != nil {
jww.ERROR.Print(err.Error())
return code, nil
}
return string(s), nil
}
}
// No cache file, render and cache it
var out bytes.Buffer
var stderr bytes.Buffer
var langOpt string
if lang == "" {
langOpt = "-g" // Try guessing the language
} else {
langOpt = "-l" + lang
}
cmd := exec.Command(pygmentsBin, langOpt, "-fhtml", "-O", options)
cmd.Stdin = strings.NewReader(code)
cmd.Stdout = &out
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
jww.ERROR.Print(stderr.String())
return code, err
}
str := string(normalizeExternalHelperLineFeeds(out.Bytes()))
str = h.injectCodeTag(str, lang)
if !h.ignoreCache && cachefile != "" {
// Write cache file
if err := WriteToDisk(cachefile, strings.NewReader(str), fs); err != nil {
jww.ERROR.Print(stderr.String())
}
}
return str, nil
}
var preRe = regexp.MustCompile(`(?s)(.*?<pre.*?>)(.*?)(</pre>)`)
func (h highlighters) injectCodeTag(code, lang string) string {
if lang == "" {
return code
}
codeTag := fmt.Sprintf(`<code class="language-%s" data-lang="%s">`, lang, lang)
return preRe.ReplaceAllString(code, fmt.Sprintf("$1%s$2</code>$3", codeTag))
}
func chromaHighlight(w io.Writer, source, lexer, style string, f chroma.Formatter) error {
l := lexers.Get(lexer)
if l == nil {
l = lexers.Analyse(source)
}
if l == nil {
l = lexers.Fallback
}
l = chroma.Coalesce(l)
if f == nil {
f = formatters.Fallback
}
s := styles.Get(style)
if s == nil {
s = styles.Fallback
}
it, err := l.Tokenise(nil, source)
if err != nil {
return err
}
return f.Format(w, s, it)
}
var pygmentsKeywords = make(map[string]bool)
func init() {
pygmentsKeywords["encoding"] = true
pygmentsKeywords["outencoding"] = true
pygmentsKeywords["nowrap"] = true
pygmentsKeywords["full"] = true
pygmentsKeywords["title"] = true
pygmentsKeywords["style"] = true
pygmentsKeywords["noclasses"] = true
pygmentsKeywords["classprefix"] = true
pygmentsKeywords["cssclass"] = true
pygmentsKeywords["cssstyles"] = true
pygmentsKeywords["prestyles"] = true
pygmentsKeywords["linenos"] = true
pygmentsKeywords["hl_lines"] = true
pygmentsKeywords["linenostart"] = true
pygmentsKeywords["linenostep"] = true
pygmentsKeywords["linenospecial"] = true
pygmentsKeywords["nobackground"] = true
pygmentsKeywords["lineseparator"] = true
pygmentsKeywords["lineanchors"] = true
pygmentsKeywords["linespans"] = true
pygmentsKeywords["anchorlinenos"] = true
pygmentsKeywords["startinline"] = true
}
func parseOptions(defaults map[string]string, in string) (map[string]string, error) {
in = strings.Trim(in, " ")
opts := make(map[string]string)
for k, v := range defaults {
opts[k] = v
}
if in == "" {
return opts, nil
}
for _, v := range strings.Split(in, ",") {
keyVal := strings.Split(v, "=")
key := strings.ToLower(strings.Trim(keyVal[0], " "))
if len(keyVal) != 2 || !pygmentsKeywords[key] {
return opts, fmt.Errorf("invalid Pygments option: %s", key)
}
opts[key] = keyVal[1]
}
return opts, nil
}
func createOptionsString(options map[string]string) string {
var keys []string
for k := range options {
keys = append(keys, k)
}
sort.Strings(keys)
var optionsStr string
for i, k := range keys {
optionsStr += fmt.Sprintf("%s=%s", k, options[k])
if i < len(options)-1 {
optionsStr += ","
}
}
return optionsStr
}
func parseDefaultPygmentsOpts(cfg config.Provider) (map[string]string, error) {
options, err := parseOptions(nil, cfg.GetString("pygmentsOptions"))
if err != nil {
return nil, err
}
if cfg.IsSet("pygmentsStyle") {
options["style"] = cfg.GetString("pygmentsStyle")
}
if cfg.IsSet("pygmentsUseClasses") {
if cfg.GetBool("pygmentsUseClasses") {
options["noclasses"] = "false"
} else {
options["noclasses"] = "true"
}
}
if _, ok := options["encoding"]; !ok {
options["encoding"] = "utf8"
}
return options, nil
}
func (cs *ContentSpec) chromaFormatterFromOptions(pygmentsOpts map[string]string) (chroma.Formatter, error) {
var options = []html.Option{html.TabWidth(4)}
if pygmentsOpts["noclasses"] == "false" {
options = append(options, html.WithClasses())
}
lineNumbers := pygmentsOpts["linenos"]
if lineNumbers != "" {
options = append(options, html.WithLineNumbers())
if lineNumbers != "inline" {
options = append(options, html.LineNumbersInTable())
}
}
startLineStr := pygmentsOpts["linenostart"]
var startLine = 1
if startLineStr != "" {
line, err := strconv.Atoi(strings.TrimSpace(startLineStr))
if err == nil {
startLine = line
options = append(options, html.BaseLineNumber(startLine))
}
}
hlLines := pygmentsOpts["hl_lines"]
if hlLines != "" {
ranges, err := hlLinesToRanges(startLine, hlLines)
if err == nil {
options = append(options, html.HighlightLines(ranges))
}
}
return html.New(options...), nil
}
func (cs *ContentSpec) parsePygmentsOpts(in string) (map[string]string, error) {
opts, err := parseOptions(cs.defatultPygmentsOpts, in)
if err != nil {
return nil, err
}
return opts, nil
}
func (cs *ContentSpec) createPygmentsOptionsString(in string) (string, error) {
opts, err := cs.parsePygmentsOpts(in)
if err != nil {
return "", err
}
return createOptionsString(opts), nil
}
// startLine compansates for https://github.com/alecthomas/chroma/issues/30
func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
var ranges [][2]int
s = strings.TrimSpace(s)
if s == "" {
return ranges, nil
}
// Variants:
// 1 2 3 4
// 1-2 3-4
// 1-2 3
// 1 3-4
// 1 3-4
fields := strings.Split(s, " ")
for _, field := range fields {
field = strings.TrimSpace(field)
if field == "" {
continue
}
numbers := strings.Split(field, "-")
var r [2]int
first, err := strconv.Atoi(numbers[0])
if err != nil {
return ranges, err
}
first = first + startLine - 1
r[0] = first
if len(numbers) > 1 {
second, err := strconv.Atoi(numbers[1])
if err != nil {
return ranges, err
}
second = second + startLine - 1
r[1] = second
} else {
r[1] = first
}
ranges = append(ranges, r)
}
return ranges, nil
}

View File

@@ -1,300 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"fmt"
"reflect"
"testing"
"github.com/alecthomas/chroma/formatters/html"
qt "github.com/frankban/quicktest"
"github.com/spf13/viper"
)
func TestParsePygmentsArgs(t *testing.T) {
c := qt.New(t)
for i, this := range []struct {
in string
pygmentsStyle string
pygmentsUseClasses bool
expect1 interface{}
}{
{"", "foo", true, "encoding=utf8,noclasses=false,style=foo"},
{"style=boo,noclasses=true", "foo", true, "encoding=utf8,noclasses=true,style=boo"},
{"Style=boo, noClasses=true", "foo", true, "encoding=utf8,noclasses=true,style=boo"},
{"noclasses=true", "foo", true, "encoding=utf8,noclasses=true,style=foo"},
{"style=boo", "foo", true, "encoding=utf8,noclasses=false,style=boo"},
{"boo=invalid", "foo", false, false},
{"style", "foo", false, false},
} {
v := viper.New()
v.Set("pygmentsStyle", this.pygmentsStyle)
v.Set("pygmentsUseClasses", this.pygmentsUseClasses)
spec, err := NewContentSpec(v, nil, nil)
c.Assert(err, qt.IsNil)
result1, err := spec.createPygmentsOptionsString(this.in)
if b, ok := this.expect1.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] parsePygmentArgs didn't return an expected error", i)
}
} else {
if err != nil {
t.Errorf("[%d] parsePygmentArgs failed: %s", i, err)
continue
}
if result1 != this.expect1 {
t.Errorf("[%d] parsePygmentArgs got %v but expected %v", i, result1, this.expect1)
}
}
}
}
func TestParseDefaultPygmentsArgs(t *testing.T) {
c := qt.New(t)
expect := "encoding=utf8,noclasses=false,style=foo"
for i, this := range []struct {
in string
pygmentsStyle interface{}
pygmentsUseClasses interface{}
pygmentsOptions string
}{
{"", "foo", true, "style=override,noclasses=override"},
{"", nil, nil, "style=foo,noclasses=false"},
{"style=foo,noclasses=false", nil, nil, "style=override,noclasses=override"},
{"style=foo,noclasses=false", "override", false, "style=override,noclasses=override"},
} {
v := viper.New()
v.Set("pygmentsOptions", this.pygmentsOptions)
if s, ok := this.pygmentsStyle.(string); ok {
v.Set("pygmentsStyle", s)
}
if b, ok := this.pygmentsUseClasses.(bool); ok {
v.Set("pygmentsUseClasses", b)
}
spec, err := NewContentSpec(v, nil, nil)
c.Assert(err, qt.IsNil)
result, err := spec.createPygmentsOptionsString(this.in)
if err != nil {
t.Errorf("[%d] parsePygmentArgs failed: %s", i, err)
continue
}
if result != expect {
t.Errorf("[%d] parsePygmentArgs got %v but expected %v", i, result, expect)
}
}
}
type chromaInfo struct {
classes bool
lineNumbers bool
lineNumbersInTable bool
highlightRangesLen int
highlightRangesStr string
baseLineNumber int
}
func formatterChromaInfo(f *html.Formatter) chromaInfo {
v := reflect.ValueOf(f).Elem()
c := chromaInfo{}
// Hack:
c.classes = f.Classes
c.lineNumbers = v.FieldByName("lineNumbers").Bool()
c.lineNumbersInTable = v.FieldByName("lineNumbersInTable").Bool()
c.baseLineNumber = int(v.FieldByName("baseLineNumber").Int())
vv := v.FieldByName("highlightRanges")
c.highlightRangesLen = vv.Len()
c.highlightRangesStr = fmt.Sprint(vv)
return c
}
func TestChromaHTMLHighlight(t *testing.T) {
c := qt.New(t)
v := viper.New()
v.Set("pygmentsUseClasses", true)
spec, err := NewContentSpec(v, nil, nil)
c.Assert(err, qt.IsNil)
result, err := spec.Highlight(`echo "Hello"`, "bash", "")
c.Assert(err, qt.IsNil)
c.Assert(result, qt.Contains, `<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nb">echo</span> <span class="s2">&#34;Hello&#34;</span></code></pre></div>`)
}
func TestChromaHTMLFormatterFromOptions(t *testing.T) {
c := qt.New(t)
for i, this := range []struct {
in string
pygmentsStyle interface{}
pygmentsUseClasses interface{}
pygmentsOptions string
assert func(c chromaInfo)
}{
{"", "monokai", true, "style=manni,noclasses=true", func(ci chromaInfo) {
c.Assert(ci.classes, qt.Equals, true)
c.Assert(ci.lineNumbers, qt.Equals, false)
c.Assert(ci.highlightRangesLen, qt.Equals, 0)
}},
{"", nil, nil, "style=monokai,noclasses=false", func(ci chromaInfo) {
c.Assert(ci.classes, qt.Equals, true)
}},
{"linenos=sure,hl_lines=1 2 3", nil, nil, "style=monokai,noclasses=false", func(ci chromaInfo) {
c.Assert(ci.classes, qt.Equals, true)
c.Assert(ci.lineNumbers, qt.Equals, true)
c.Assert(ci.highlightRangesLen, qt.Equals, 3)
c.Assert(ci.highlightRangesStr, qt.Equals, "[[1 1] [2 2] [3 3]]")
c.Assert(ci.baseLineNumber, qt.Equals, 1)
}},
{"linenos=inline,hl_lines=1,linenostart=4", nil, nil, "style=monokai,noclasses=false", func(ci chromaInfo) {
c.Assert(ci.classes, qt.Equals, true)
c.Assert(ci.lineNumbers, qt.Equals, true)
c.Assert(ci.lineNumbersInTable, qt.Equals, false)
c.Assert(ci.highlightRangesLen, qt.Equals, 1)
// This compansates for https://github.com/alecthomas/chroma/issues/30
c.Assert(ci.highlightRangesStr, qt.Equals, "[[4 4]]")
c.Assert(ci.baseLineNumber, qt.Equals, 4)
}},
{"linenos=table", nil, nil, "style=monokai", func(ci chromaInfo) {
c.Assert(ci.lineNumbers, qt.Equals, true)
c.Assert(ci.lineNumbersInTable, qt.Equals, true)
}},
{"style=monokai,noclasses=false", nil, nil, "style=manni,noclasses=true", func(ci chromaInfo) {
c.Assert(ci.classes, qt.Equals, true)
}},
{"style=monokai,noclasses=true", "friendly", false, "style=manni,noclasses=false", func(ci chromaInfo) {
c.Assert(ci.classes, qt.Equals, false)
}},
} {
v := viper.New()
v.Set("pygmentsOptions", this.pygmentsOptions)
if s, ok := this.pygmentsStyle.(string); ok {
v.Set("pygmentsStyle", s)
}
if b, ok := this.pygmentsUseClasses.(bool); ok {
v.Set("pygmentsUseClasses", b)
}
spec, err := NewContentSpec(v, nil, nil)
c.Assert(err, qt.IsNil)
opts, err := spec.parsePygmentsOpts(this.in)
if err != nil {
t.Fatalf("[%d] parsePygmentsOpts failed: %s", i, err)
}
chromaFormatter, err := spec.chromaFormatterFromOptions(opts)
if err != nil {
t.Fatalf("[%d] chromaFormatterFromOptions failed: %s", i, err)
}
this.assert(formatterChromaInfo(chromaFormatter.(*html.Formatter)))
}
}
func TestHlLinesToRanges(t *testing.T) {
var zero [][2]int
for _, this := range []struct {
in string
startLine int
expected interface{}
}{
{"", 1, zero},
{"1 4", 1, [][2]int{{1, 1}, {4, 4}}},
{"1 4", 2, [][2]int{{2, 2}, {5, 5}}},
{"1-4 5-8", 1, [][2]int{{1, 4}, {5, 8}}},
{" 1 4 ", 1, [][2]int{{1, 1}, {4, 4}}},
{"1-4 5-8 ", 1, [][2]int{{1, 4}, {5, 8}}},
{"1-4 5", 1, [][2]int{{1, 4}, {5, 5}}},
{"4 5-9", 1, [][2]int{{4, 4}, {5, 9}}},
{" 1 -4 5 - 8 ", 1, true},
{"a b", 1, true},
} {
got, err := hlLinesToRanges(this.startLine, this.in)
if expectErr, ok := this.expected.(bool); ok && expectErr {
if err == nil {
t.Fatal("No error")
}
} else if err != nil {
t.Fatalf("Got error: %s", err)
} else if !reflect.DeepEqual(this.expected, got) {
t.Fatalf("Expected\n%v but got\n%v", this.expected, got)
}
}
}
func BenchmarkChromaHighlight(b *testing.B) {
c := qt.New(b)
v := viper.New()
v.Set("pygmentsstyle", "trac")
v.Set("pygmentsuseclasses", false)
v.Set("pygmentsuseclassic", false)
code := `// GetTitleFunc returns a func that can be used to transform a string to
// title case.
//
// The supported styles are
//
// - "Go" (strings.Title)
// - "AP" (see https://www.apstylebook.com/)
// - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
//
// If an unknown or empty style is provided, AP style is what you get.
func GetTitleFunc(style string) func(s string) string {
switch strings.ToLower(style) {
case "go":
return strings.Title
case "chicago":
tc := transform.NewTitleConverter(transform.ChicagoStyle)
return tc.Title
default:
tc := transform.NewTitleConverter(transform.APStyle)
return tc.Title
}
}
`
spec, err := NewContentSpec(v, nil, nil)
c.Assert(err, qt.IsNil)
for i := 0; i < b.N; i++ {
_, err := spec.Highlight(code, "go", "linenos=inline,hl_lines=8 15-17")
if err != nil {
b.Fatal(err)
}
}
}