Skip to content

Commit

Permalink
Use Chroma as new default syntax highlighter
Browse files Browse the repository at this point in the history
If you want to use Pygments, set `pygmentsUseClassic=true` in your site config.

Fixes #3888
  • Loading branch information
bep committed Sep 25, 2017
1 parent 81ed564 commit fb33d82
Show file tree
Hide file tree
Showing 18 changed files with 649 additions and 105 deletions.
70 changes: 70 additions & 0 deletions commands/genchromastyles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2017-present 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 commands

import (
"os"

"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/styles"
"github.com/spf13/cobra"
)

type genChromaStyles struct {
style string
highlightStyle string
linesStyle string
cmd *cobra.Command
}

// TODO(bep) highlight
func createGenChromaStyles() *genChromaStyles {
g := &genChromaStyles{
cmd: &cobra.Command{
Use: "chromastyles",
Short: "Generate CSS stylesheet for the Chroma code highlighter",
Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if pygmentsUseClasses is enabled in config.
See https://help.farbox.com/pygments.html for preview of available styles`,
},
}

g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return g.generate()
}

g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://help.farbox.com/pygments.html)")
g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")

return g
}

func (g *genChromaStyles) generate() error {
builder := styles.Get(g.style).Builder()
if g.highlightStyle != "" {
builder.Add(chroma.LineHighlight, g.highlightStyle)
}
if g.linesStyle != "" {
builder.Add(chroma.LineNumbers, g.linesStyle)
}
style, err := builder.Build()
if err != nil {
return err
}
formatter := html.New(html.WithClasses())
formatter.WriteCSS(os.Stdout, style)
return nil
}
1 change: 1 addition & 0 deletions commands/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func AddCommands() {
genCmd.AddCommand(gendocCmd)
genCmd.AddCommand(genmanCmd)
genCmd.AddCommand(createGenDocsHelper().cmd)
genCmd.AddCommand(createGenChromaStyles().cmd)
}

// initHugoBuilderFlags initializes all common flags, typically used by the
Expand Down
13 changes: 11 additions & 2 deletions deps/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,19 @@ func New(cfg DepsCfg) (*Deps, error) {
return nil, err
}

contentSpec, err := helpers.NewContentSpec(cfg.Language)
if err != nil {
return nil, err
}

d := &Deps{
Fs: fs,
Log: logger,
templateProvider: cfg.TemplateProvider,
translationProvider: cfg.TranslationProvider,
WithTemplate: cfg.WithTemplate,
PathSpec: ps,
ContentSpec: helpers.NewContentSpec(cfg.Language),
ContentSpec: contentSpec,
Cfg: cfg.Language,
Language: cfg.Language,
}
Expand All @@ -139,7 +144,11 @@ func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) {
return nil, err
}

d.ContentSpec = helpers.NewContentSpec(l)
d.ContentSpec, err = helpers.NewContentSpec(l)
if err != nil {
return nil, err
}

d.Cfg = l
d.Language = l

Expand Down
44 changes: 38 additions & 6 deletions helpers/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,49 @@ type ContentSpec struct {
footnoteAnchorPrefix string
footnoteReturnLinkContents string

Highlight func(code, lang, optsStr string) (string, error)
defatultPygmentsOpts map[string]string

cfg config.Provider
}

// NewContentSpec returns a ContentSpec initialized
// with the appropriate fields from the given config.Provider.
func NewContentSpec(cfg config.Provider) *ContentSpec {
return &ContentSpec{
func NewContentSpec(cfg config.Provider) (*ContentSpec, error) {
spec := &ContentSpec{
blackfriday: cfg.GetStringMap("blackfriday"),
footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"),
footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"),

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
}

return spec, nil
}

// Blackfriday holds configuration values for Blackfriday rendering.
Expand Down Expand Up @@ -198,7 +228,7 @@ func BytesToHTML(b []byte) template.HTML {
}

// getHTMLRenderer creates a new Blackfriday HTML Renderer with the given configuration.
func (c ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer {
func (c *ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer {
renderParameters := blackfriday.HtmlRendererParameters{
FootnoteAnchorPrefix: c.footnoteAnchorPrefix,
FootnoteReturnLinkContents: c.footnoteReturnLinkContents,
Expand Down Expand Up @@ -248,6 +278,7 @@ func (c ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) bl
}

return &HugoHTMLRenderer{
cs: c,
RenderingContext: ctx,
Renderer: blackfriday.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
}
Expand Down Expand Up @@ -299,7 +330,7 @@ func (c ContentSpec) markdownRender(ctx *RenderingContext) []byte {
}

// getMmarkHTMLRenderer creates a new mmark HTML Renderer with the given configuration.
func (c ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingContext) mmark.Renderer {
func (c *ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingContext) mmark.Renderer {
renderParameters := mmark.HtmlRendererParameters{
FootnoteAnchorPrefix: c.footnoteAnchorPrefix,
FootnoteReturnLinkContents: c.footnoteReturnLinkContents,
Expand All @@ -320,8 +351,9 @@ func (c ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingContex
htmlFlags |= mmark.HTML_FOOTNOTE_RETURN_LINKS

return &HugoMmarkHTMLRenderer{
mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
c.cfg,
cs: c,
Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
Cfg: c.cfg,
}
}

Expand Down
13 changes: 9 additions & 4 deletions helpers/content_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package helpers
import (
"bytes"
"html"
"strings"

"github.com/gohugoio/hugo/config"
"github.com/miekg/mmark"
Expand All @@ -25,6 +26,7 @@ import (
// HugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html
// Enabling Hugo to customise the rendering experience
type HugoHTMLRenderer struct {
cs *ContentSpec
*RenderingContext
blackfriday.Renderer
}
Expand All @@ -34,8 +36,9 @@ type HugoHTMLRenderer struct {
func (r *HugoHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
opts := r.Cfg.GetString("pygmentsOptions")
str := html.UnescapeString(string(text))
out.WriteString(Highlight(r.RenderingContext.Cfg, str, lang, opts))
str := strings.Trim(html.UnescapeString(string(text)), "\n\r")
highlighted, _ := r.cs.Highlight(str, lang, opts)
out.WriteString(highlighted)
} else {
r.Renderer.BlockCode(out, text, lang)
}
Expand Down Expand Up @@ -88,6 +91,7 @@ func (r *HugoHTMLRenderer) List(out *bytes.Buffer, text func() bool, flags int)
// HugoMmarkHTMLRenderer wraps a mmark.Renderer, typically a mmark.html,
// enabling Hugo to customise the rendering experience.
type HugoMmarkHTMLRenderer struct {
cs *ContentSpec
mmark.Renderer
Cfg config.Provider
}
Expand All @@ -96,8 +100,9 @@ type HugoMmarkHTMLRenderer struct {
// Pygments is used if it is setup to handle code fences.
func (r *HugoMmarkHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string, caption []byte, subfigure bool, callouts bool) {
if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
str := html.UnescapeString(string(text))
out.WriteString(Highlight(r.Cfg, str, lang, ""))
str := strings.Trim(html.UnescapeString(string(text)), "\n\r")
highlighted, _ := r.cs.Highlight(str, lang, "")
out.WriteString(highlighted)
} else {
r.Renderer.BlockCode(out, text, lang, caption, subfigure, callouts)
}
Expand Down
54 changes: 27 additions & 27 deletions helpers/content_renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)

// Renders a codeblock using Blackfriday
Expand All @@ -42,11 +43,7 @@ func (c ContentSpec) renderWithMmark(input string) string {
}

func TestCodeFence(t *testing.T) {

if !HasPygments() {
t.Skip("Skipping Pygments test as Pygments is not installed or available.")
return
}
assert := require.New(t)

type test struct {
enabled bool
Expand All @@ -55,36 +52,39 @@ func TestCodeFence(t *testing.T) {

// Pygments 2.0 and 2.1 have slightly different outputs so only do partial matching
data := []test{
{true, "<html></html>", `(?s)^<div class="highlight"><pre><code class="language-html" data-lang="html">.*?</code></pre></div>\n$`},
{false, "<html></html>", `(?s)^<pre><code class="language-html">.*?</code></pre>\n$`},
{true, "<html></html>", `(?s)^<div class="highlight">\n?<pre.*><code class="language-html" data-lang="html">.*?</code></pre>\n?</div>\n?$`},
{false, "<html></html>", `(?s)^<pre.*><code class="language-html">.*?</code></pre>\n$`},
}

for i, d := range data {
v := viper.New()
for _, useClassic := range []bool{false, true} {
for i, d := range data {
v := viper.New()
v.Set("pygmentsStyle", "monokai")
v.Set("pygmentsUseClasses", true)
v.Set("pygmentsCodeFences", d.enabled)
v.Set("pygmentsUseClassic", useClassic)

v.Set("pygmentsStyle", "monokai")
v.Set("pygmentsUseClasses", true)
v.Set("pygmentsCodeFences", d.enabled)
c, err := NewContentSpec(v)
assert.NoError(err)

c := NewContentSpec(v)
result := c.render(d.input)

result := c.render(d.input)
expectedRe, err := regexp.Compile(d.expected)

expectedRe, err := regexp.Compile(d.expected)
if err != nil {
t.Fatal("Invalid regexp", err)
}
matched := expectedRe.MatchString(result)

if err != nil {
t.Fatal("Invalid regexp", err)
}
matched := expectedRe.MatchString(result)

if !matched {
t.Errorf("Test %d failed. BlackFriday enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
}
if !matched {
t.Errorf("Test %d failed. BlackFriday enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
}

result = c.renderWithMmark(d.input)
matched = expectedRe.MatchString(result)
if !matched {
t.Errorf("Test %d failed. Mmark enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
result = c.renderWithMmark(d.input)
matched = expectedRe.MatchString(result)
if !matched {
t.Errorf("Test %d failed. Mmark enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
}
}
}
}
Expand Down
Loading

0 comments on commit fb33d82

Please sign in to comment.