Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/content/en/functions/transform/Highlight.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,22 @@ params:
functions_and_methods:
aliases: [highlight]
returnType: template.HTML
signatures: ['transform.Highlight CODE LANG [OPTIONS]']
signatures: ['transform.Highlight CODE [LANG] [OPTIONS]']
aliases: [/functions/highlight]
---

The `transform.Highlight` function uses the [`alecthomas/chroma`][] package to generate syntax-highlighted HTML from the provided code, [language][], and [options][].

## Arguments

The `transform.Highlight` function takes three arguments.

CODE
: (`string`) The code to highlight.

LANG
: (`string`) The [language][] of the code to highlight. This value is case-insensitive.
: (`string`) The [language][] of the code to highlight. This value is case-insensitive. Optional; you can also set the language with the `type` key in OPTIONS. {{< new-in "0.162.0" />}}

OPTIONS
: (`map or string`) A map or comma-separated key-value pairs wrapped in quotation marks. You can set default values for each option in your [project configuration][]. The key names are case-insensitive.
: (`map or string`) A map or comma-separated key-value pairs wrapped in quotation marks. You can set default values for each option in your [project configuration][]. The key names are case-insensitive. In addition to the [highlighting options](#options-1), the `type` and `code` keys override the `LANG` and `CODE` arguments respectively. {{< new-in "0.162.0" />}}

## Examples

Expand All @@ -40,6 +38,10 @@ OPTIONS
{{ $lang := "bash" }}
{{ $opts := dict "lineNos" "table" "style" "dracula" }}
{{ transform.Highlight $input $lang $opts }}

{{ $input := `print("Hello World!")` }}
{{ $opts := dict "type" "python" "style" "dracula" }}
{{ transform.Highlight $input $opts }}
```

## Options
Expand Down
11 changes: 11 additions & 0 deletions docs/content/en/functions/transform/HighlightCodeBlock.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,15 @@ To override the default [highlighting options]:
{{ $result.Wrapped }}
```

The `type` and `code` keys are special: they override the language and the code received from the code block respectively. {{< new-in "0.162.0" />}} For example, to fall back to plain text when the language is not supported by the highlighter:

```go-html-template
{{ $opts := dict }}
{{ if not (transform.CanHighlight .Type) }}
{{ $opts = dict "type" "text" }}
{{ end }}
{{ $result := transform.HighlightCodeBlock . $opts }}
{{ $result.Wrapped }}
```

[highlighting options]: /functions/transform/highlight/#options
47 changes: 34 additions & 13 deletions markup/highlight/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,33 +148,54 @@ func (cfg Config) toHTMLOptions() ([]html.Option, error) {
return options, nil
}

func applyOptions(opts any, cfg *Config) error {
func applyOptionsFromString(opts string, cfg *Config) error {
optsm, err := parseHighlightOptions(opts)
if err != nil {
return err
}
return mapstructure.WeakDecode(optsm, cfg)
}

func applyOptionsFromMap(optsm map[string]any, cfg *Config) error {
normalizeHighlightOptions(optsm)
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:
return applyOptionsFromMap(vv, cfg)
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
}
return applyOptionsFromString(s, cfg)
if optsm, err = parseHighlightOptions(s); err != nil {
return err
}
}
}

func applyOptionsFromString(opts string, cfg *Config) error {
optsm, err := parseHighlightOptions(opts)
if 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 mapstructure.WeakDecode(optsm, cfg)
}

func applyOptionsFromMap(optsm map[string]any, cfg *Config) error {
normalizeHighlightOptions(optsm)
return mapstructure.WeakDecode(optsm, cfg)
return applyOptionsFromMap(optsm, cfg)
}

func applyOptionsFromCodeBlockContext(ctx hooks.CodeblockContext, cfg *Config) error {
Expand Down
14 changes: 7 additions & 7 deletions markup/highlight/highlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -90,22 +90,22 @@ 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
}

if err := applyOptionsFromCodeBlockContext(ctx, &cfg); err != nil {
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
}
Expand Down
44 changes: 44 additions & 0 deletions markup/highlight/highlight_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
43 changes: 36 additions & 7 deletions tpl/transform/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
28 changes: 28 additions & 0 deletions tpl/transform/transform_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading