Skip to content

Commit fb33d82

Browse files
authored
Use Chroma as new default syntax highlighter
If you want to use Pygments, set `pygmentsUseClassic=true` in your site config. Fixes #3888
1 parent 81ed564 commit fb33d82

18 files changed

+649
-105
lines changed

‎commands/genchromastyles.go‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2017-present The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package commands
15+
16+
import (
17+
"os"
18+
19+
"github.com/alecthomas/chroma"
20+
"github.com/alecthomas/chroma/formatters/html"
21+
"github.com/alecthomas/chroma/styles"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
type genChromaStyles struct {
26+
style string
27+
highlightStyle string
28+
linesStyle string
29+
cmd *cobra.Command
30+
}
31+
32+
// TODO(bep) highlight
33+
func createGenChromaStyles() *genChromaStyles {
34+
g := &genChromaStyles{
35+
cmd: &cobra.Command{
36+
Use: "chromastyles",
37+
Short: "Generate CSS stylesheet for the Chroma code highlighter",
38+
Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if pygmentsUseClasses is enabled in config.
39+
40+
See https://help.farbox.com/pygments.html for preview of available styles`,
41+
},
42+
}
43+
44+
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
45+
return g.generate()
46+
}
47+
48+
g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://help.farbox.com/pygments.html)")
49+
g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
50+
g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
51+
52+
return g
53+
}
54+
55+
func (g *genChromaStyles) generate() error {
56+
builder := styles.Get(g.style).Builder()
57+
if g.highlightStyle != "" {
58+
builder.Add(chroma.LineHighlight, g.highlightStyle)
59+
}
60+
if g.linesStyle != "" {
61+
builder.Add(chroma.LineNumbers, g.linesStyle)
62+
}
63+
style, err := builder.Build()
64+
if err != nil {
65+
return err
66+
}
67+
formatter := html.New(html.WithClasses())
68+
formatter.WriteCSS(os.Stdout, style)
69+
return nil
70+
}

‎commands/hugo.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func AddCommands() {
199199
genCmd.AddCommand(gendocCmd)
200200
genCmd.AddCommand(genmanCmd)
201201
genCmd.AddCommand(createGenDocsHelper().cmd)
202+
genCmd.AddCommand(createGenChromaStyles().cmd)
202203
}
203204

204205
// initHugoBuilderFlags initializes all common flags, typically used by the

‎deps/deps.go‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,19 @@ func New(cfg DepsCfg) (*Deps, error) {
114114
return nil, err
115115
}
116116

117+
contentSpec, err := helpers.NewContentSpec(cfg.Language)
118+
if err != nil {
119+
return nil, err
120+
}
121+
117122
d := &Deps{
118123
Fs: fs,
119124
Log: logger,
120125
templateProvider: cfg.TemplateProvider,
121126
translationProvider: cfg.TranslationProvider,
122127
WithTemplate: cfg.WithTemplate,
123128
PathSpec: ps,
124-
ContentSpec: helpers.NewContentSpec(cfg.Language),
129+
ContentSpec: contentSpec,
125130
Cfg: cfg.Language,
126131
Language: cfg.Language,
127132
}
@@ -139,7 +144,11 @@ func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) {
139144
return nil, err
140145
}
141146

142-
d.ContentSpec = helpers.NewContentSpec(l)
147+
d.ContentSpec, err = helpers.NewContentSpec(l)
148+
if err != nil {
149+
return nil, err
150+
}
151+
143152
d.Cfg = l
144153
d.Language = l
145154

‎helpers/content.go‎

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,49 @@ type ContentSpec struct {
4848
footnoteAnchorPrefix string
4949
footnoteReturnLinkContents string
5050

51+
Highlight func(code, lang, optsStr string) (string, error)
52+
defatultPygmentsOpts map[string]string
53+
5154
cfg config.Provider
5255
}
5356

5457
// NewContentSpec returns a ContentSpec initialized
5558
// with the appropriate fields from the given config.Provider.
56-
func NewContentSpec(cfg config.Provider) *ContentSpec {
57-
return &ContentSpec{
59+
func NewContentSpec(cfg config.Provider) (*ContentSpec, error) {
60+
spec := &ContentSpec{
5861
blackfriday: cfg.GetStringMap("blackfriday"),
5962
footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"),
6063
footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"),
6164

6265
cfg: cfg,
6366
}
67+
68+
// Highlighting setup
69+
options, err := parseDefaultPygmentsOpts(cfg)
70+
if err != nil {
71+
return nil, err
72+
}
73+
spec.defatultPygmentsOpts = options
74+
75+
// Use the Pygmentize on path if present
76+
useClassic := false
77+
h := newHiglighters(spec)
78+
79+
if cfg.GetBool("pygmentsUseClassic") {
80+
if !hasPygments() {
81+
jww.WARN.Println("Highlighting with pygmentsUseClassic set requires Pygments to be installed and in the path")
82+
} else {
83+
useClassic = true
84+
}
85+
}
86+
87+
if useClassic {
88+
spec.Highlight = h.pygmentsHighlight
89+
} else {
90+
spec.Highlight = h.chromaHighlight
91+
}
92+
93+
return spec, nil
6494
}
6595

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

200230
// getHTMLRenderer creates a new Blackfriday HTML Renderer with the given configuration.
201-
func (c ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer {
231+
func (c *ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer {
202232
renderParameters := blackfriday.HtmlRendererParameters{
203233
FootnoteAnchorPrefix: c.footnoteAnchorPrefix,
204234
FootnoteReturnLinkContents: c.footnoteReturnLinkContents,
@@ -248,6 +278,7 @@ func (c ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) bl
248278
}
249279

250280
return &HugoHTMLRenderer{
281+
cs: c,
251282
RenderingContext: ctx,
252283
Renderer: blackfriday.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
253284
}
@@ -299,7 +330,7 @@ func (c ContentSpec) markdownRender(ctx *RenderingContext) []byte {
299330
}
300331

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

322353
return &HugoMmarkHTMLRenderer{
323-
mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
324-
c.cfg,
354+
cs: c,
355+
Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
356+
Cfg: c.cfg,
325357
}
326358
}
327359

‎helpers/content_renderer.go‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package helpers
1616
import (
1717
"bytes"
1818
"html"
19+
"strings"
1920

2021
"github.com/gohugoio/hugo/config"
2122
"github.com/miekg/mmark"
@@ -25,6 +26,7 @@ import (
2526
// HugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html
2627
// Enabling Hugo to customise the rendering experience
2728
type HugoHTMLRenderer struct {
29+
cs *ContentSpec
2830
*RenderingContext
2931
blackfriday.Renderer
3032
}
@@ -34,8 +36,9 @@ type HugoHTMLRenderer struct {
3436
func (r *HugoHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
3537
if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
3638
opts := r.Cfg.GetString("pygmentsOptions")
37-
str := html.UnescapeString(string(text))
38-
out.WriteString(Highlight(r.RenderingContext.Cfg, str, lang, opts))
39+
str := strings.Trim(html.UnescapeString(string(text)), "\n\r")
40+
highlighted, _ := r.cs.Highlight(str, lang, opts)
41+
out.WriteString(highlighted)
3942
} else {
4043
r.Renderer.BlockCode(out, text, lang)
4144
}
@@ -88,6 +91,7 @@ func (r *HugoHTMLRenderer) List(out *bytes.Buffer, text func() bool, flags int)
8891
// HugoMmarkHTMLRenderer wraps a mmark.Renderer, typically a mmark.html,
8992
// enabling Hugo to customise the rendering experience.
9093
type HugoMmarkHTMLRenderer struct {
94+
cs *ContentSpec
9195
mmark.Renderer
9296
Cfg config.Provider
9397
}
@@ -96,8 +100,9 @@ type HugoMmarkHTMLRenderer struct {
96100
// Pygments is used if it is setup to handle code fences.
97101
func (r *HugoMmarkHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string, caption []byte, subfigure bool, callouts bool) {
98102
if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
99-
str := html.UnescapeString(string(text))
100-
out.WriteString(Highlight(r.Cfg, str, lang, ""))
103+
str := strings.Trim(html.UnescapeString(string(text)), "\n\r")
104+
highlighted, _ := r.cs.Highlight(str, lang, "")
105+
out.WriteString(highlighted)
101106
} else {
102107
r.Renderer.BlockCode(out, text, lang, caption, subfigure, callouts)
103108
}

‎helpers/content_renderer_test.go‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"testing"
2020

2121
"github.com/spf13/viper"
22+
"github.com/stretchr/testify/require"
2223
)
2324

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

4445
func TestCodeFence(t *testing.T) {
45-
46-
if !HasPygments() {
47-
t.Skip("Skipping Pygments test as Pygments is not installed or available.")
48-
return
49-
}
46+
assert := require.New(t)
5047

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

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

62-
for i, d := range data {
63-
v := viper.New()
59+
for _, useClassic := range []bool{false, true} {
60+
for i, d := range data {
61+
v := viper.New()
62+
v.Set("pygmentsStyle", "monokai")
63+
v.Set("pygmentsUseClasses", true)
64+
v.Set("pygmentsCodeFences", d.enabled)
65+
v.Set("pygmentsUseClassic", useClassic)
6466

65-
v.Set("pygmentsStyle", "monokai")
66-
v.Set("pygmentsUseClasses", true)
67-
v.Set("pygmentsCodeFences", d.enabled)
67+
c, err := NewContentSpec(v)
68+
assert.NoError(err)
6869

69-
c := NewContentSpec(v)
70+
result := c.render(d.input)
7071

71-
result := c.render(d.input)
72+
expectedRe, err := regexp.Compile(d.expected)
7273

73-
expectedRe, err := regexp.Compile(d.expected)
74+
if err != nil {
75+
t.Fatal("Invalid regexp", err)
76+
}
77+
matched := expectedRe.MatchString(result)
7478

75-
if err != nil {
76-
t.Fatal("Invalid regexp", err)
77-
}
78-
matched := expectedRe.MatchString(result)
79-
80-
if !matched {
81-
t.Errorf("Test %d failed. BlackFriday enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
82-
}
79+
if !matched {
80+
t.Errorf("Test %d failed. BlackFriday enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
81+
}
8382

84-
result = c.renderWithMmark(d.input)
85-
matched = expectedRe.MatchString(result)
86-
if !matched {
87-
t.Errorf("Test %d failed. Mmark enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
83+
result = c.renderWithMmark(d.input)
84+
matched = expectedRe.MatchString(result)
85+
if !matched {
86+
t.Errorf("Test %d failed. Mmark enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
87+
}
8888
}
8989
}
9090
}

0 commit comments

Comments
 (0)