Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b613365

Browse files
bepclaude
andcommitted
markup/highlight: Allow overriding type and code via options
Treat type and code as highlighting options in both transform.Highlight and transform.HighlightCodeBlock. The type option overrides the language and code overrides the code, so the two functions now share the same options handling. transform.Highlight's LANG argument is now optional: transform.Highlight CODE [LANG] [OPTIONS] Fixes #11872 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 4bc7cae commit b613365

5 files changed

Lines changed: 149 additions & 27 deletions

File tree

markup/highlight/config.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -148,33 +148,54 @@ func (cfg Config) toHTMLOptions() ([]html.Option, error) {
148148
return options, nil
149149
}
150150

151-
func applyOptions(opts any, cfg *Config) error {
151+
func applyOptionsFromString(opts string, cfg *Config) error {
152+
optsm, err := parseHighlightOptions(opts)
153+
if err != nil {
154+
return err
155+
}
156+
return mapstructure.WeakDecode(optsm, cfg)
157+
}
158+
159+
func applyOptionsFromMap(optsm map[string]any, cfg *Config) error {
160+
normalizeHighlightOptions(optsm)
161+
return mapstructure.WeakDecode(optsm, cfg)
162+
}
163+
164+
// applyOptions applies opts (a string or a map) to cfg. The type and code
165+
// options, if set, are not part of Config and instead override lang and code
166+
// respectively. Shared by Highlight and HighlightCodeBlock. See issue 11872.
167+
func applyOptions(opts any, cfg *Config, lang, code *string) error {
152168
if opts == nil {
153169
return nil
154170
}
171+
172+
var optsm map[string]any
155173
switch vv := opts.(type) {
156174
case map[string]any:
157-
return applyOptionsFromMap(vv, cfg)
175+
optsm = make(map[string]any, len(vv))
176+
for k, v := range vv {
177+
optsm[strings.ToLower(k)] = v
178+
}
158179
default:
159180
s, err := cast.ToStringE(opts)
160181
if err != nil {
161182
return err
162183
}
163-
return applyOptionsFromString(s, cfg)
184+
if optsm, err = parseHighlightOptions(s); err != nil {
185+
return err
186+
}
164187
}
165-
}
166188

167-
func applyOptionsFromString(opts string, cfg *Config) error {
168-
optsm, err := parseHighlightOptions(opts)
169-
if err != nil {
170-
return err
189+
if v, found := optsm["type"]; found {
190+
*lang = cast.ToString(v)
191+
delete(optsm, "type")
192+
}
193+
if v, found := optsm["code"]; found {
194+
*code = cast.ToString(v)
195+
delete(optsm, "code")
171196
}
172-
return mapstructure.WeakDecode(optsm, cfg)
173-
}
174197

175-
func applyOptionsFromMap(optsm map[string]any, cfg *Config) error {
176-
normalizeHighlightOptions(optsm)
177-
return mapstructure.WeakDecode(optsm, cfg)
198+
return applyOptionsFromMap(optsm, cfg)
178199
}
179200

180201
func applyOptionsFromCodeBlockContext(ctx hooks.CodeblockContext, cfg *Config) error {

markup/highlight/highlight.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ type chromaHighlighter struct {
7171

7272
func (h chromaHighlighter) Highlight(code, lang string, opts any) (string, error) {
7373
cfg := h.cfg
74-
if err := applyOptions(opts, &cfg); err != nil {
74+
if err := applyOptions(opts, &cfg, &lang, &code); err != nil {
7575
return "", err
7676
}
7777
var b strings.Builder
@@ -90,22 +90,22 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a
9090

9191
attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
9292

93-
options := ctx.Options()
94-
95-
if err := applyOptionsFromMap(options, &cfg); err != nil {
93+
if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil {
9694
return HighlightResult{}, err
9795
}
9896

99-
// Apply these last so the user can override them.
100-
if err := applyOptions(opts, &cfg); err != nil {
97+
lang, code := ctx.Type(), ctx.Inner()
98+
99+
// Apply these last so the user can override them, including the type and code.
100+
if err := applyOptions(opts, &cfg, &lang, &code); err != nil {
101101
return HighlightResult{}, err
102102
}
103103

104104
if err := applyOptionsFromCodeBlockContext(ctx, &cfg); err != nil {
105105
return HighlightResult{}, err
106106
}
107107

108-
low, high, err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg)
108+
low, high, err := highlight(&b, code, lang, attributes, cfg)
109109
if err != nil {
110110
return HighlightResult{}, err
111111
}

markup/highlight/highlight_integration_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,50 @@ HighlightCodeBlock: Wrapped:{{ $result.Wrapped }}|Inner:{{ $result.Inner }}
8080
)
8181
}
8282

83+
// See issue 11872.
84+
func TestCodeblockWithTypeOverride(t *testing.T) {
85+
t.Parallel()
86+
87+
files := `
88+
-- hugo.toml --
89+
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
90+
[markup.highlight]
91+
noClasses = false # to reduce size of assertion string
92+
-- content/p1.md --
93+
---
94+
title: p1
95+
---
96+
§§§go {style=monokai class=my-class tabWidth=8}
97+
i = 42
98+
§§§
99+
-- content/p2.md --
100+
---
101+
title: p2
102+
---
103+
§§§{style=monokai class=my-class tabWidth=8}
104+
i = 42
105+
§§§
106+
-- layouts/page.html --
107+
{{ .Content }}
108+
-- layouts/_markup/render-codeblock.html --
109+
{{- $opts := dict }}
110+
{{- if not (transform.CanHighlight .Type) }}
111+
{{- $opts = dict "type" "text" }}
112+
{{- end }}
113+
{{- $result := transform.HighlightCodeBlock . $opts }}
114+
{{- $result.Wrapped -}}
115+
`
116+
117+
b := hugolib.Test(t, files)
118+
119+
b.AssertFileContent("public/p1/index.html",
120+
`<div class="highlight my-class"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">i</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">42</span></span></span></code></pre></div>`,
121+
)
122+
b.AssertFileContent("public/p2/index.html",
123+
`<div class="highlight my-class"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">i = 42</span></span></code></pre></div>`,
124+
)
125+
}
126+
83127
// Issue #11311
84128
func TestIssue11311(t *testing.T) {
85129
t.Parallel()

tpl/transform/transform.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,21 +94,50 @@ func (ns *Namespace) Emojify(s any) (template.HTML, error) {
9494
return template.HTML(helpers.Emojify([]byte(ss))), nil
9595
}
9696

97-
// Highlight returns a copy of s as an HTML string with syntax
97+
// Highlight returns a copy of CODE as an HTML string with syntax
9898
// highlighting applied.
99-
func (ns *Namespace) Highlight(s any, lang string, opts ...any) (template.HTML, error) {
100-
ss, err := cast.ToStringE(s)
99+
//
100+
// transform.Highlight CODE [LANG] [OPTIONS]
101+
//
102+
// LANG is optional; it can also be set via the type option in OPTIONS, which
103+
// makes this work the same way as HighlightCodeBlock.
104+
func (ns *Namespace) Highlight(s any, args ...any) (template.HTML, error) {
105+
code, err := cast.ToStringE(s)
101106
if err != nil {
102107
return "", err
103108
}
104109

105-
var optsv any
106-
if len(opts) > 0 {
107-
optsv = opts[0]
110+
var lang string
111+
var opts any
112+
113+
switch len(args) {
114+
case 0:
115+
case 1:
116+
// A single argument is either OPTIONS (a map or option string) or LANG.
117+
if _, ok := args[0].(map[string]any); ok {
118+
opts = args[0]
119+
} else {
120+
var arg string
121+
if arg, err = cast.ToStringE(args[0]); err != nil {
122+
return "", err
123+
}
124+
if strings.Contains(arg, "=") || strings.Contains(arg, ",") {
125+
opts = arg
126+
} else {
127+
lang = arg
128+
}
129+
}
130+
case 2:
131+
if lang, err = cast.ToStringE(args[0]); err != nil {
132+
return "", err
133+
}
134+
opts = args[1]
135+
default:
136+
return "", errors.New("transform.Highlight: expects at most 3 arguments")
108137
}
109138

110139
hl := ns.deps.ContentSpec.Converters.GetHighlighter()
111-
highlighted, err := hl.Highlight(ss, lang, optsv)
140+
highlighted, err := hl.Highlight(code, lang, opts)
112141
if err != nil {
113142
return "", err
114143
}

tpl/transform/transform_integration_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,34 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term']
103103
b.Assert(err.Error(), qt.Contains, "error calling highlight: invalid Highlight option: 0")
104104
}
105105

106+
// transform.Highlight: LANG is optional and may be set via the type option, and
107+
// the code option overrides CODE — consistent with transform.HighlightCodeBlock.
108+
// See issue 11872.
109+
func TestHighlightTypeAndCodeOptions(t *testing.T) {
110+
t.Parallel()
111+
112+
files := `
113+
-- hugo.toml --
114+
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
115+
[markup.highlight]
116+
noClasses = false
117+
-- layouts/home.html --
118+
lang:{{ transform.Highlight "i = 42" "go" }}
119+
type:{{ transform.Highlight "i = 42" (dict "type" "go") }}
120+
code:{{ transform.Highlight "" (dict "type" "go" "code" "i = 42") }}
121+
`
122+
123+
b := hugolib.Test(t, files)
124+
125+
want := `<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">i</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">42</span></span></span></code></pre></div>`
126+
127+
b.AssertFileContent("public/index.html",
128+
"lang:"+want,
129+
"type:"+want,
130+
"code:"+want,
131+
)
132+
}
133+
106134
// Issue #11884
107135
func TestUnmarshalCSVLazyDecoding(t *testing.T) {
108136
t.Parallel()

0 commit comments

Comments
 (0)