Skip to content

Commit d0ff51b

Browse files
committed
markup/rst: Make syntax highlighting configurable
Add markup.rst.syntaxHighlight and pass Docutils' --syntax-highlight option only when configured away from the rst2html default. Fixes #5349
1 parent 656fc04 commit d0ff51b

9 files changed

Lines changed: 201 additions & 2 deletions

File tree

docs/content/en/configuration/markup.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,19 @@ Run `hugo build --logLevel debug` to examine Hugo's call to the Asciidoctor exec
304304
INFO 2019/12/22 09:08:48 Rendering book-as-pdf.adoc with C:\Ruby26-x64\bin\asciidoctor.bat using asciidoc args [--no-header-footer -r asciidoctor-html5s -b html5s -r asciidoctor-diagram --base-dir D:\prototypes\hugo_asciidoc_ddd\docs -a outdir=D:\prototypes\hugo_asciidoc_ddd\build -] ...
305305
```
306306

307+
## reStructuredText
308+
309+
This is the default configuration for the reStructuredText renderer:
310+
311+
{{< code-toggle config=markup.rst />}}
312+
313+
### reStructuredText settings explained
314+
315+
syntaxHighlight
316+
: (`string`) The token name set passed to Docutils' `--syntax-highlight` option, one of `long`, `short`, or `none`. Default is `long`.
317+
318+
To highlight code blocks, Docutils must be able to import [Pygments].
319+
307320
## Highlight
308321

309322
This is the default configuration.
@@ -359,6 +372,7 @@ ordered
359372
[Pandoc]: https://pandoc.org/
360373
[PHP Markdown Extra: Definition lists]: https://michelf.ca/projects/php-markdown/extra/#def-list
361374
[PHP Markdown Extra: Footnotes]: https://michelf.ca/projects/php-markdown/extra/#footnotes
375+
[Pygments]: https://pygments.org/
362376
[reStructuredText]: https://docutils.sourceforge.io/rst.html
363377
[security policy]: /configuration/security/
364378
[subscript]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub

docs/content/en/content-management/formats.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,17 @@ Hugo passes these CLI flags when calling the rst2html executable:
118118
--leave-comments --initial-header-level=2
119119
```
120120

121+
To use short syntax highlighting class names, configure the reStructuredText renderer:
122+
123+
{{< code-toggle file=hugo >}}
124+
[markup.rst]
125+
syntaxHighlight = 'short'
126+
{{< /code-toggle >}}
127+
128+
Docutils requires [Pygments] to highlight code blocks.
129+
121130
[Docutils]: https://docutils.sourceforge.io/
131+
[Pygments]: https://pygments.org/
122132
[reStructuredText]: https://docutils.sourceforge.io/rst.html
123133

124134
## Classification

docs/data/docs.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,8 @@ config:
13661366
style: monokai
13671367
tabWidth: 4
13681368
wrapperClass: highlight
1369+
rst:
1370+
syntaxHighlight: long
13691371
tableOfContents:
13701372
endLevel: 3
13711373
ordered: false

hugolib/rst_integration_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2026 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 hugolib
15+
16+
import (
17+
"runtime"
18+
"strings"
19+
"testing"
20+
21+
qt "github.com/frankban/quicktest"
22+
"github.com/gohugoio/hugo/markup/rst"
23+
)
24+
25+
func TestRSTSyntaxHighlightConfigIssue5349(t *testing.T) {
26+
if !rst.Supports() {
27+
t.Skip("rst not installed")
28+
}
29+
30+
execAllow := `'^rst2html(\.py)?$'`
31+
if runtime.GOOS == "windows" {
32+
execAllow = `'^python(\.exe)?$'`
33+
}
34+
35+
files := strings.ReplaceAll(`
36+
-- hugo.toml --
37+
baseURL = "https://example.org"
38+
disableKinds = ["home", "section", "taxonomy", "term", "rss", "sitemap"]
39+
[security.exec]
40+
allow = [RST_EXEC_ALLOW]
41+
[markup.rst]
42+
syntaxHighlight = 'none'
43+
-- layouts/page.html --
44+
{{ .Content }}
45+
-- content/p.rst --
46+
---
47+
title: p
48+
---
49+
50+
.. code:: go
51+
52+
if true {}
53+
`, "RST_EXEC_ALLOW", execAllow)
54+
55+
b := Test(t, files)
56+
content := b.FileContent("public/p/index.html")
57+
b.Assert(content, qt.Contains, `if true {}`)
58+
b.Assert(content, qt.Not(qt.Contains), `Pygments package not found`)
59+
b.Assert(content, qt.Not(qt.Contains), `<span class="keyword">if</span>`)
60+
b.Assert(content, qt.Not(qt.Contains), `<span class="k">if</span>`)
61+
}

markup/markup_config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
2020
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
2121
"github.com/gohugoio/hugo/markup/highlight"
22+
"github.com/gohugoio/hugo/markup/rst/rst_config"
2223
"github.com/gohugoio/hugo/markup/tableofcontents"
2324
"github.com/mitchellh/mapstructure"
2425
)
@@ -39,6 +40,9 @@ type Config struct {
3940

4041
// Configuration for the AsciiDoc external markdown engine.
4142
AsciiDocExt asciidocext_config.Config
43+
44+
// Configuration for the reStructuredText external markdown engine.
45+
RST rst_config.Config
4246
}
4347

4448
func (c *Config) Init() error {
@@ -119,4 +123,5 @@ var Default = Config{
119123

120124
Goldmark: goldmark_config.Default,
121125
AsciiDocExt: asciidocext_config.Default,
126+
RST: rst_config.Default,
122127
}

markup/markup_config/config_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ func TestConfig(t *testing.T) {
3939
"safeMode": "save",
4040
"extensions": []string{"asciidoctor-html5s"},
4141
},
42+
"rst": map[string]any{
43+
"syntaxHighlight": "short",
44+
},
4245
})
4346

4447
conf, err := Decode(v)
@@ -50,6 +53,7 @@ func TestConfig(t *testing.T) {
5053

5154
c.Assert(conf.AsciiDocExt.WorkingFolderCurrent, qt.Equals, true)
5255
c.Assert(conf.AsciiDocExt.Extensions[0], qt.Equals, "asciidoctor-html5s")
56+
c.Assert(conf.RST.SyntaxHighlight, qt.Equals, "short")
5357
})
5458

5559
// We changed the typographer extension config from a bool to a struct in 0.112.0.

markup/rst/convert.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"github.com/gohugoio/hugo/markup/converter"
2727
"github.com/gohugoio/hugo/markup/internal"
28+
"github.com/gohugoio/hugo/markup/rst/rst_config"
2829
)
2930

3031
// Provider is the package entry point.
@@ -81,10 +82,10 @@ func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext)
8182
// handle Windows manually because it doesn't do shebangs
8283
if runtime.GOOS == "windows" {
8384
pythonBinary, _ := internal.GetPythonBinaryAndExecPath()
84-
args := []string{binaryPath, "--leave-comments", "--initial-header-level=2"}
85+
args := append([]string{binaryPath}, c.parseArgs()...)
8586
result, err = internal.ExternallyRenderContent(c.cfg, ctx, src, pythonBinary, args)
8687
} else {
87-
args := []string{"--leave-comments", "--initial-header-level=2"}
88+
args := c.parseArgs()
8889
result, err = internal.ExternallyRenderContent(c.cfg, ctx, src, binaryName, args)
8990
}
9091

@@ -106,6 +107,36 @@ func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext)
106107
return result[bodyStart+7 : bodyEnd], err
107108
}
108109

110+
func (c *rstConverter) parseArgs() []string {
111+
cfg := rst_config.Default
112+
if c.cfg.Conf != nil {
113+
cfg = c.cfg.MarkupConfig().RST
114+
}
115+
116+
if cfg.SyntaxHighlight != rst_config.CliDefault.SyntaxHighlight && !rst_config.AllowedSyntaxHighlight[cfg.SyntaxHighlight] {
117+
if c.cfg.Logger != nil {
118+
c.cfg.Logger.Errorf(
119+
"Unsupported reStructuredText value %q for option %q was passed in and will be ignored.",
120+
cfg.SyntaxHighlight,
121+
"syntaxHighlight",
122+
)
123+
}
124+
cfg.SyntaxHighlight = rst_config.CliDefault.SyntaxHighlight
125+
}
126+
127+
return parseArgs(cfg)
128+
}
129+
130+
func parseArgs(cfg rst_config.Config) []string {
131+
args := []string{"--leave-comments", "--initial-header-level=2"}
132+
133+
if cfg.SyntaxHighlight != rst_config.CliDefault.SyntaxHighlight && rst_config.AllowedSyntaxHighlight[cfg.SyntaxHighlight] {
134+
args = append(args, "--syntax-highlight="+cfg.SyntaxHighlight)
135+
}
136+
137+
return args
138+
}
139+
109140
var rst2Binaries = []string{"rst2html", "rst2html.py"}
110141

111142
func getRstBinaryNameAndPath() (string, string) {

markup/rst/convert_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/gohugoio/hugo/config/security"
2222

2323
"github.com/gohugoio/hugo/markup/converter"
24+
"github.com/gohugoio/hugo/markup/rst/rst_config"
2425

2526
qt "github.com/frankban/quicktest"
2627
)
@@ -45,3 +46,35 @@ func TestConvert(t *testing.T) {
4546
c.Assert(err, qt.IsNil)
4647
c.Assert(string(b.Bytes()), qt.Equals, "<div class=\"document\">\n\n\n<p>testContent</p>\n</div>")
4748
}
49+
50+
func TestParseArgs(t *testing.T) {
51+
c := qt.New(t)
52+
53+
for _, test := range []struct {
54+
name string
55+
value string
56+
expected []string
57+
}{
58+
{
59+
name: "Default",
60+
value: "long",
61+
expected: []string{"--leave-comments", "--initial-header-level=2"},
62+
},
63+
{
64+
name: "Short",
65+
value: "short",
66+
expected: []string{"--leave-comments", "--initial-header-level=2", "--syntax-highlight=short"},
67+
},
68+
{
69+
name: "None",
70+
value: "none",
71+
expected: []string{"--leave-comments", "--initial-header-level=2", "--syntax-highlight=none"},
72+
},
73+
} {
74+
c.Run(test.name, func(c *qt.C) {
75+
c.Parallel()
76+
77+
c.Assert(parseArgs(rst_config.Config{SyntaxHighlight: test.value}), qt.DeepEquals, test.expected)
78+
})
79+
}
80+
}

markup/rst/rst_config/config.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2026 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 rst_config holds reStructuredText related configuration.
15+
package rst_config
16+
17+
var (
18+
// Default holds Hugo's default reStructuredText configuration.
19+
Default = Config{
20+
SyntaxHighlight: "long",
21+
}
22+
23+
// CliDefault holds rst2html CLI defaults.
24+
CliDefault = Config{
25+
SyntaxHighlight: "long",
26+
}
27+
28+
AllowedSyntaxHighlight = map[string]bool{
29+
"long": true,
30+
"short": true,
31+
"none": true,
32+
}
33+
)
34+
35+
// Config configures reStructuredText.
36+
type Config struct {
37+
// SyntaxHighlight sets Docutils' Pygments token names: "long", "short", or "none".
38+
SyntaxHighlight string
39+
}

0 commit comments

Comments
 (0)