From be24457acfd3eb0b798edda36c89632564e981c7 Mon Sep 17 00:00:00 2001 From: bep Date: Wed, 15 Apr 2015 20:31:05 +0200 Subject: [PATCH] Add more options to highlight Fixes #1021 --- docs/content/extras/highlighting.md | 26 +++++--- helpers/pygments.go | 96 ++++++++++++++++++++++++----- helpers/pygments_test.go | 42 +++++++++++++ tpl/template_embedded.go | 8 ++- tpl/template_funcs.go | 4 +- 5 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 helpers/pygments_test.go diff --git a/docs/content/extras/highlighting.md b/docs/content/extras/highlighting.md index 37b9fc394..8296861d4 100644 --- a/docs/content/extras/highlighting.md +++ b/docs/content/extras/highlighting.md @@ -44,9 +44,6 @@ Highlighting is carried out via the in-built shortcode `highlight`. `highlight` closing shortcode. ### Example -If you want to highlight code, you need to either fence the code with ``` according to GitHub Flavored Markdown or preceed each line with 4 spaces to identify each line as a line of code. - -Not doing either will result in the text being rendered as HTML. This will prevent Pygments highlighting from working. ``` {{}} @@ -72,15 +69,28 @@ Not doing either will result in the text being rendered as HTML. This will preve </div> </section> +### Options + +Options to control highlighting can be added as a quoted, comma separated key-value list as the second argument in the shortcode. The example below will highlight as language `go` with inline line numbers, with line number 2 and 3 highlighted. + +``` +{{}} +var a string +var b string +var c string +var d string +{{}} +``` + +Supported keywords: `style`, `encoding`, `noclasses`, `hl_lines`, `linenos`. Note that `style` and `noclasses` will override the similar setting in the global config. + +The keywords are the same you would using with Pygments from the command line, see the [Pygments doc](http://pygments.org/docs/) for more info. + ### Disclaimers - * **Warning:** Pygments is relatively slow. Expect much longer build times when using server-side highlighting. + * Pygments is relatively slow, but Hugo will cache the results to disk. * Languages available depends on your Pygments installation. - * We have sought to have the simplest interface possible, which consequently -limits configuration. An ambitious user is encouraged to extend the current -functionality to offer more customization. - ## Client-side diff --git a/helpers/pygments.go b/helpers/pygments.go index 8223759c5..74327c6f5 100644 --- a/helpers/pygments.go +++ b/helpers/pygments.go @@ -17,15 +17,15 @@ import ( "bytes" "crypto/sha1" "fmt" + "github.com/spf13/hugo/hugofs" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" "io" "io/ioutil" "os/exec" "path/filepath" + "sort" "strings" - - "github.com/spf13/hugo/hugofs" - jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/viper" ) const pygmentsBin = "pygmentize" @@ -40,30 +40,30 @@ func HasPygments() bool { } // Highlight takes some code and returns highlighted code. -func Highlight(code string, lexer string) string { +func Highlight(code, lang, optsStr string) string { if !HasPygments() { jww.WARN.Println("Highlighting requires Pygments to be installed and in the path") return code } - fs := hugofs.OsFs + options, err := parsePygmentsOpts(optsStr) - style := viper.GetString("PygmentsStyle") - - noclasses := "true" - if viper.GetBool("PygmentsUseClasses") { - noclasses = "false" + if err != nil { + jww.ERROR.Print(err.Error()) + return code } // Try to read from cache first hash := sha1.New() - io.WriteString(hash, lexer) io.WriteString(hash, code) - io.WriteString(hash, style) - io.WriteString(hash, noclasses) + io.WriteString(hash, lang) + io.WriteString(hash, options) cachefile := filepath.Join(viper.GetString("CacheDir"), fmt.Sprintf("pygments-%x", hash.Sum(nil))) + + fs := hugofs.OsFs + exists, err := Exists(cachefile, fs) if err != nil { jww.ERROR.Print(err.Error()) @@ -89,8 +89,7 @@ func Highlight(code string, lexer string) string { var out bytes.Buffer var stderr bytes.Buffer - cmd := exec.Command(pygmentsBin, "-l"+lexer, "-fhtml", "-O", - fmt.Sprintf("style=%s,noclasses=%s,encoding=utf8", style, noclasses)) + cmd := exec.Command(pygmentsBin, "-l"+lang, "-fhtml", "-O", options) cmd.Stdin = strings.NewReader(code) cmd.Stdout = &out cmd.Stderr = &stderr @@ -107,3 +106,68 @@ func Highlight(code string, lexer string) string { return out.String() } + +var pygmentsKeywords = make(map[string]bool) + +func init() { + pygmentsKeywords["style"] = true + pygmentsKeywords["encoding"] = true + pygmentsKeywords["noclasses"] = true + pygmentsKeywords["hl_lines"] = true + pygmentsKeywords["linenos"] = true +} + +func parsePygmentsOpts(in string) (string, error) { + + in = strings.Trim(in, " ") + + style := viper.GetString("PygmentsStyle") + + noclasses := "true" + if viper.GetBool("PygmentsUseClasses") { + noclasses = "false" + } + + if len(in) == 0 { + return fmt.Sprintf("style=%s,noclasses=%s,encoding=utf8", style, noclasses), nil + } + + options := make(map[string]string) + + o := strings.Split(in, ",") + for _, v := range o { + keyVal := strings.Split(v, "=") + key := strings.ToLower(strings.Trim(keyVal[0], " ")) + if len(keyVal) != 2 || !pygmentsKeywords[key] { + return "", fmt.Errorf("invalid Pygments option: %s", key) + } + options[key] = keyVal[1] + } + + if _, ok := options["style"]; !ok { + options["style"] = style + } + + if _, ok := options["noclasses"]; !ok { + options["noclasses"] = noclasses + } + + if _, ok := options["encoding"]; !ok { + options["encoding"] = "utf8" + } + + 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, nil +} diff --git a/helpers/pygments_test.go b/helpers/pygments_test.go new file mode 100644 index 000000000..be0c1a7dc --- /dev/null +++ b/helpers/pygments_test.go @@ -0,0 +1,42 @@ +package helpers + +import ( + "github.com/spf13/viper" + "testing" +) + +func TestParsePygmentsArgs(t *testing.T) { + for i, this := range []struct { + in string + pygmentsStyle string + pygmentsUseClasses bool + expect1 interface{} + }{ + {"", "foo", true, "style=foo,noclasses=false,encoding=utf8"}, + {"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}, + } { + viper.Set("PygmentsStyle", this.pygmentsStyle) + viper.Set("PygmentsUseClasses", this.pygmentsUseClasses) + + result1, err := parsePygmentsOpts(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) + } + + } + } +} diff --git a/tpl/template_embedded.go b/tpl/template_embedded.go index 94d54cab4..48de62cb6 100644 --- a/tpl/template_embedded.go +++ b/tpl/template_embedded.go @@ -21,7 +21,13 @@ type Tmpl struct { func (t *GoHTMLTemplate) EmbedShortcodes() { t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`) t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`) - t.AddInternalShortcode("highlight.html", `{{ .Get 0 | highlight .Inner }}`) + t.AddInternalShortcode("highlight.html", ` + {{ if len .Params | eq 2 }} + {{ highlight .Inner (.Get 0) (.Get 1) }} + {{ else }} + {{ highlight .Inner (.Get 0) "" }} + {{ end }} + `) t.AddInternalShortcode("test.html", `This is a simple Test`) t.AddInternalShortcode("figure.html", `
diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index 996456f9f..ea069b2af 100644 --- a/tpl/template_funcs.go +++ b/tpl/template_funcs.go @@ -875,7 +875,7 @@ func ReturnWhenSet(a, k interface{}) interface{} { return "" } -func Highlight(in interface{}, lang string) template.HTML { +func Highlight(in interface{}, lang, opts string) template.HTML { var str string av := reflect.ValueOf(in) switch av.Kind() { @@ -883,7 +883,7 @@ func Highlight(in interface{}, lang string) template.HTML { str = av.String() } - return template.HTML(helpers.Highlight(html.UnescapeString(str), lang)) + return template.HTML(helpers.Highlight(html.UnescapeString(str), lang, opts)) } var markdownTrimPrefix = []byte("

")