mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
+37
-16
@@ -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-"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
`<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>`,
|
||||
)
|
||||
b.AssertFileContent("public/p2/index.html",
|
||||
`<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>`,
|
||||
)
|
||||
}
|
||||
|
||||
// Issue #11311
|
||||
func TestIssue11311(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 := `<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>`
|
||||
|
||||
b.AssertFileContent("public/index.html",
|
||||
"lang:"+want,
|
||||
"type:"+want,
|
||||
"code:"+want,
|
||||
)
|
||||
}
|
||||
|
||||
// Issue #11884
|
||||
func TestUnmarshalCSVLazyDecoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Reference in New Issue
Block a user