shithub: hugo

Download patch

ref: be24457acfd3eb0b798edda36c89632564e981c7
parent: be540f5b8fb340f1edfb1ed10c11334bbb85927b
author: bep <[email protected]>
date: Wed Apr 15 16:31:05 EDT 2015

Add more options to highlight

Fixes #1021

--- a/docs/content/extras/highlighting.md
+++ b/docs/content/extras/highlighting.md
@@ -44,10 +44,7 @@
 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.
-
 ```
 {{</* highlight html */>}}
 <section id="main">
@@ -72,15 +69,28 @@
       <span style="color: #f92672">&lt;/div&gt;</span>
     <span style="color: #f92672">&lt;/section&gt;</span>
 
+### 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.
+
+```
+{{</* highlight go "linenos=inline,hl_lines=2 3" */>}}
+var a string
+var b string
+var c string
+var d string
+{{</* / highlight */>}}
+```
+
+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
 
--- a/helpers/pygments.go
+++ b/helpers/pygments.go
@@ -17,15 +17,15 @@
 	"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,7 +40,7 @@
 }
 
 // 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")
@@ -47,23 +47,23 @@
 		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 @@
 	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
@@ -106,4 +105,69 @@
 	}
 
 	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
 }
--- /dev/null
+++ b/helpers/pygments_test.go
@@ -1,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)
+			}
+
+		}
+	}
+}
--- a/tpl/template_embedded.go
+++ b/tpl/template_embedded.go
@@ -21,7 +21,13 @@
 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", `<!-- image -->
 <figure {{ with .Get "class" }}class="{{.}}"{{ end }}>
--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -875,7 +875,7 @@
 	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 @@
 		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("<p>")