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()