From b6133657e006290375e587bfb6566469a14911af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 21 May 2026 11:46:47 +0200 Subject: [PATCH] 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) --- markup/highlight/config.go | 53 +++++++++++++------ markup/highlight/highlight.go | 14 ++--- .../highlight/highlight_integration_test.go | 44 +++++++++++++++ tpl/transform/transform.go | 43 ++++++++++++--- tpl/transform/transform_integration_test.go | 28 ++++++++++ 5 files changed, 152 insertions(+), 30 deletions(-) diff --git a/markup/highlight/config.go b/markup/highlight/config.go index 6407cdff6..184042ae5 100644 --- a/markup/highlight/config.go +++ b/markup/highlight/config.go @@ -148,22 +148,6 @@ func (cfg Config) toHTMLOptions() ([]html.Option, error) { return options, nil } -func applyOptions(opts any, cfg *Config) error { - if opts == nil { - return nil - } - switch vv := opts.(type) { - case map[string]any: - return applyOptionsFromMap(vv, cfg) - default: - s, err := cast.ToStringE(opts) - if err != nil { - return err - } - return applyOptionsFromString(s, cfg) - } -} - func applyOptionsFromString(opts string, cfg *Config) error { optsm, err := parseHighlightOptions(opts) if err != nil { @@ -177,6 +161,43 @@ func applyOptionsFromMap(optsm map[string]any, cfg *Config) error { return mapstructure.WeakDecode(optsm, cfg) } +// applyOptions applies opts (a string or a map) to cfg. The type and code +// options, if set, are not part of Config and instead override lang and code +// respectively. Shared by Highlight and HighlightCodeBlock. See issue 11872. +func applyOptions(opts any, cfg *Config, lang, code *string) error { + if opts == nil { + return nil + } + + var optsm map[string]any + switch vv := opts.(type) { + case map[string]any: + optsm = make(map[string]any, len(vv)) + for k, v := range vv { + optsm[strings.ToLower(k)] = v + } + default: + s, err := cast.ToStringE(opts) + if err != nil { + return err + } + if optsm, err = parseHighlightOptions(s); err != nil { + return err + } + } + + if v, found := optsm["type"]; found { + *lang = cast.ToString(v) + delete(optsm, "type") + } + if v, found := optsm["code"]; found { + *code = cast.ToString(v) + delete(optsm, "code") + } + + return applyOptionsFromMap(optsm, cfg) +} + func applyOptionsFromCodeBlockContext(ctx hooks.CodeblockContext, cfg *Config) error { if cfg.LineAnchors == "" { const lineAnchorPrefix = "hl-" diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go index bb58b9ca0..77bc52cf0 100644 --- a/markup/highlight/highlight.go +++ b/markup/highlight/highlight.go @@ -71,7 +71,7 @@ type chromaHighlighter struct { func (h chromaHighlighter) Highlight(code, lang string, opts any) (string, error) { cfg := h.cfg - if err := applyOptions(opts, &cfg); err != nil { + if err := applyOptions(opts, &cfg, &lang, &code); err != nil { return "", err } var b strings.Builder @@ -90,14 +90,14 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice() - options := ctx.Options() - - if err := applyOptionsFromMap(options, &cfg); err != nil { + if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil { return HighlightResult{}, err } - // Apply these last so the user can override them. - if err := applyOptions(opts, &cfg); err != nil { + lang, code := ctx.Type(), ctx.Inner() + + // Apply these last so the user can override them, including the type and code. + if err := applyOptions(opts, &cfg, &lang, &code); err != nil { return HighlightResult{}, err } @@ -105,7 +105,7 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a return HighlightResult{}, err } - low, high, err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg) + low, high, err := highlight(&b, code, lang, attributes, cfg) if err != nil { return HighlightResult{}, err } diff --git a/markup/highlight/highlight_integration_test.go b/markup/highlight/highlight_integration_test.go index 19779fd2f..4f72b51c1 100644 --- a/markup/highlight/highlight_integration_test.go +++ b/markup/highlight/highlight_integration_test.go @@ -80,6 +80,50 @@ HighlightCodeBlock: Wrapped:{{ $result.Wrapped }}|Inner:{{ $result.Inner }} ) } +// See issue 11872. +func TestCodeblockWithTypeOverride(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +[markup.highlight] +noClasses = false # to reduce size of assertion string +-- content/p1.md -- +--- +title: p1 +--- +§§§go {style=monokai class=my-class tabWidth=8} +i = 42 +§§§ +-- content/p2.md -- +--- +title: p2 +--- +§§§{style=monokai class=my-class tabWidth=8} +i = 42 +§§§ +-- layouts/page.html -- +{{ .Content }} +-- layouts/_markup/render-codeblock.html -- +{{- $opts := dict }} +{{- if not (transform.CanHighlight .Type) }} + {{- $opts = dict "type" "text" }} +{{- end }} +{{- $result := transform.HighlightCodeBlock . $opts }} +{{- $result.Wrapped -}} +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", + `
i = 42
`, + ) + b.AssertFileContent("public/p2/index.html", + `
i = 42
`, + ) +} + // Issue #11311 func TestIssue11311(t *testing.T) { t.Parallel() diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go index b5a2d3b4c..5c99b322c 100644 --- a/tpl/transform/transform.go +++ b/tpl/transform/transform.go @@ -94,21 +94,50 @@ func (ns *Namespace) Emojify(s any) (template.HTML, error) { return template.HTML(helpers.Emojify([]byte(ss))), nil } -// Highlight returns a copy of s as an HTML string with syntax +// Highlight returns a copy of CODE as an HTML string with syntax // highlighting applied. -func (ns *Namespace) Highlight(s any, lang string, opts ...any) (template.HTML, error) { - ss, err := cast.ToStringE(s) +// +// transform.Highlight CODE [LANG] [OPTIONS] +// +// LANG is optional; it can also be set via the type option in OPTIONS, which +// makes this work the same way as HighlightCodeBlock. +func (ns *Namespace) Highlight(s any, args ...any) (template.HTML, error) { + code, err := cast.ToStringE(s) if err != nil { return "", err } - var optsv any - if len(opts) > 0 { - optsv = opts[0] + var lang string + var opts any + + switch len(args) { + case 0: + case 1: + // A single argument is either OPTIONS (a map or option string) or LANG. + if _, ok := args[0].(map[string]any); ok { + opts = args[0] + } else { + var arg string + if arg, err = cast.ToStringE(args[0]); err != nil { + return "", err + } + if strings.Contains(arg, "=") || strings.Contains(arg, ",") { + opts = arg + } else { + lang = arg + } + } + case 2: + if lang, err = cast.ToStringE(args[0]); err != nil { + return "", err + } + opts = args[1] + default: + return "", errors.New("transform.Highlight: expects at most 3 arguments") } hl := ns.deps.ContentSpec.Converters.GetHighlighter() - highlighted, err := hl.Highlight(ss, lang, optsv) + highlighted, err := hl.Highlight(code, lang, opts) if err != nil { return "", err } diff --git a/tpl/transform/transform_integration_test.go b/tpl/transform/transform_integration_test.go index 9e7f7006b..9109e6c3e 100644 --- a/tpl/transform/transform_integration_test.go +++ b/tpl/transform/transform_integration_test.go @@ -103,6 +103,34 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term'] b.Assert(err.Error(), qt.Contains, "error calling highlight: invalid Highlight option: 0") } +// transform.Highlight: LANG is optional and may be set via the type option, and +// the code option overrides CODE — consistent with transform.HighlightCodeBlock. +// See issue 11872. +func TestHighlightTypeAndCodeOptions(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +[markup.highlight] +noClasses = false +-- layouts/home.html -- +lang:{{ transform.Highlight "i = 42" "go" }} +type:{{ transform.Highlight "i = 42" (dict "type" "go") }} +code:{{ transform.Highlight "" (dict "type" "go" "code" "i = 42") }} +` + + b := hugolib.Test(t, files) + + want := `
i = 42
` + + b.AssertFileContent("public/index.html", + "lang:"+want, + "type:"+want, + "code:"+want, + ) +} + // Issue #11884 func TestUnmarshalCSVLazyDecoding(t *testing.T) { t.Parallel()