deps: Add Chroma dark/light mode support
Test / test (1.26.x, ubuntu-latest) (push) Has been cancelled
Test / test (1.26.x, windows-latest) (push) Has been cancelled

Closes #15017
This commit is contained in:
Bjørn Erik Pedersen
2026-06-09 13:38:19 +02:00
parent fe06735214
commit 790a8aa474
8 changed files with 389 additions and 105 deletions
+64 -5
View File
@@ -47,6 +47,8 @@ func newGenCommand() *genCommand {
// Chroma flags.
style string
mode string
modeSelector bool
highlightStyle string
lineNumbersInlineStyle string
lineNumbersTableStyle string
@@ -67,7 +69,26 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
if !slices.Contains(styles.Names(), style) {
return fmt.Errorf("invalid style: %s", style)
}
builder := styles.Get(style).Builder()
var chromaStyle *chroma.Style
if mode != "" {
var chromaMode chroma.Mode
switch mode {
case "light":
chromaMode = chroma.Light
case "dark":
chromaMode = chroma.Dark
default:
return fmt.Errorf("invalid mode: %s", mode)
}
chromaStyle = styles.GetForMode(style, chromaMode)
if chromaStyle.Mode() != chromaMode {
return fmt.Errorf("style %q does not have a %q mode", style, mode)
}
} else {
chromaStyle = styles.Get(style)
}
builder := chromaStyle.Builder()
if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle)
}
@@ -88,19 +109,52 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
}
options := []html.Option{
html.WithCSSComments(!omitClassComments),
html.WithCustomCSS(chromaCSSOverrides(style)),
}
if !modeSelector {
// Only needed for the shared-scope overlay; --modeSelector
// gives each mode its own scope and makes this redundant.
options = append(options, html.WithCustomCSS(chromaCSSOverrides(style)))
}
formatter := html.New(options...)
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
formatter.WriteCSS(w, style)
var buf bytes.Buffer
fmt.Fprintf(&buf, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
formatter.WriteCSS(&buf, style)
css := buf.String()
if modeSelector {
// Scope every selector under a top level mode class, e.g. ".dark .chroma".
// This allows generating both light and dark stylesheets and toggling
// them with a parent class on the page.
// There's no upstream option for this (I think), so do string replacements for now.
// TODO(bep) upstream option for this.
var prefix string
switch style.Mode() {
case chroma.Light:
prefix = ".light "
case chroma.Dark:
prefix = ".dark "
default:
return fmt.Errorf("style %q does not have a %q mode", style.Name, mode)
}
replacer := strings.NewReplacer(
".bg {", prefix+".bg {",
".chroma ", prefix+".chroma ",
)
css = replacer.Replace(css)
}
fmt.Print(css)
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&mode, "mode", "", `style mode ("light", "dark")`)
_ = cmd.RegisterFlagCompletionFunc("mode", cobra.FixedCompletions([]string{"light", "dark"}, cobra.ShellCompDirectiveNoFileComp))
cmd.PersistentFlags().BoolVar(&modeSelector, "modeSelector", false, `scope selectors under a top level mode class, e.g. ".dark .chroma"`)
_ = cmd.RegisterFlagCompletionFunc("modeSelector", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
@@ -276,6 +330,11 @@ url: %s
}
}
// chromaCSSOverrides re-emits leaf token colors that Chroma's minifier drops
// because they equal the style's default foreground (the .chroma color). Without
// modeSelector scoping, a paired light/dark stylesheet shares the same .chroma
// scope, and the light sheet's explicit rule (e.g. .chroma .nx) would otherwise
// leak into dark mode, since an explicit declaration beats inheritance.
func chromaCSSOverrides(style *chroma.Style) map[chroma.TokenType]string {
bg := style.Get(chroma.Background)
m := make(map[chroma.TokenType]string)
+243 -74
View File
@@ -57,6 +57,10 @@ chroma:
- Aliases:
- armasm
Name: ArmAsm
- Aliases:
- arturo
- art
Name: Arturo
- Aliases:
- atl
Name: ATL
@@ -317,6 +321,10 @@ chroma:
- gdscript3
- gd3
Name: GDScript3
- Aliases:
- gemfile-lock
- gemfilelock
Name: Gemfile.lock
- Aliases:
- gemtext
- gmi
@@ -455,6 +463,7 @@ chroma:
Name: JavaScript
- Aliases:
- json
- jsonl
Name: JSON
- Aliases:
- jsonata
@@ -493,6 +502,9 @@ chroma:
- lighty
- lighttpd
Name: Lighttpd configuration file
- Aliases:
- lilypond
Name: LilyPond
- Aliases:
- llvm
Name: LLVM
@@ -934,6 +946,9 @@ chroma:
- tcsh
- csh
Name: Tcsh
- Aliases:
- templ
Name: Templ
- Aliases:
- termcap
Name: Termcap
@@ -1050,6 +1065,12 @@ chroma:
- Aliases:
- yaml
Name: YAML
- Aliases:
- yaml+jinja
- salt
- sls
- ansible
Name: YAML+Jinja
- Aliases:
- yang
Name: YANG
@@ -1063,80 +1084,228 @@ chroma:
- zig
Name: Zig
styles:
- abap
- algol
- algol_nu
- arduino
- ashen
- aura-theme-dark
- aura-theme-dark-soft
- autumn
- average
- base16-snazzy
- borland
- bw
- catppuccin-frappe
- catppuccin-latte
- catppuccin-macchiato
- catppuccin-mocha
- colorful
- darcula
- doom-one
- doom-one2
- dracula
- emacs
- evergarden
- friendly
- fruity
- github
- github-dark
- gruvbox
- gruvbox-light
- hr_high_contrast
- hrdark
- igor
- kanagawa-dragon
- kanagawa-lotus
- kanagawa-wave
- lovelace
- manni
- modus-operandi
- modus-vivendi
- monokai
- monokailight
- murphy
- native
- nord
- nordic
- onedark
- onesenterprise
- paraiso-dark
- paraiso-light
- pastie
- perldoc
- pygments
- rainbow_dash
- rose-pine
- rose-pine-dawn
- rose-pine-moon
- rpgle
- rrt
- solarized-dark
- solarized-dark256
- solarized-light
- swapoff
- tango
- tokyonight-day
- tokyonight-moon
- tokyonight-night
- tokyonight-storm
- trac
- vim
- vs
- vulcan
- witchhazel
- xcode
- xcode-dark
- counterpart: ''
mode: light
name: abap
- counterpart: ''
mode: light
name: algol
- counterpart: ''
mode: light
name: algol_nu
- counterpart: ''
mode: light
name: arduino
- counterpart: ''
mode: dark
name: ashen
- counterpart: ''
mode: dark
name: aura-theme-dark
- counterpart: ''
mode: dark
name: aura-theme-dark-soft
- counterpart: ''
mode: light
name: autumn
- counterpart: ''
mode: dark
name: average
- counterpart: ''
mode: dark
name: base16-snazzy
- counterpart: ''
mode: light
name: borland
- counterpart: ''
mode: light
name: bw
- counterpart: ''
mode: dark
name: catppuccin-frappe
- counterpart: catppuccin-mocha
mode: light
name: catppuccin-latte
- counterpart: ''
mode: dark
name: catppuccin-macchiato
- counterpart: catppuccin-latte
mode: dark
name: catppuccin-mocha
- counterpart: ''
mode: light
name: colorful
- counterpart: ''
mode: dark
name: darcula
- counterpart: ''
mode: dark
name: doom-one
- counterpart: ''
mode: dark
name: doom-one2
- counterpart: ''
mode: dark
name: dracula
- counterpart: ''
mode: light
name: emacs
- counterpart: ''
mode: dark
name: evergarden
- counterpart: ''
mode: light
name: friendly
- counterpart: ''
mode: dark
name: fruity
- counterpart: github-dark
mode: light
name: github
- counterpart: github
mode: dark
name: github-dark
- counterpart: gruvbox-light
mode: dark
name: gruvbox
- counterpart: gruvbox
mode: light
name: gruvbox-light
- counterpart: ''
mode: light
name: hr_high_contrast
- counterpart: ''
mode: light
name: hrdark
- counterpart: ''
mode: light
name: igor
- counterpart: ''
mode: dark
name: kanagawa-dragon
- counterpart: kanagawa-wave
mode: light
name: kanagawa-lotus
- counterpart: kanagawa-lotus
mode: dark
name: kanagawa-wave
- counterpart: ''
mode: light
name: lovelace
- counterpart: ''
mode: light
name: manni
- counterpart: modus-vivendi
mode: light
name: modus-operandi
- counterpart: modus-operandi
mode: dark
name: modus-vivendi
- counterpart: monokailight
mode: dark
name: monokai
- counterpart: monokai
mode: light
name: monokailight
- counterpart: ''
mode: light
name: murphy
- counterpart: ''
mode: dark
name: native
- counterpart: ''
mode: dark
name: nord
- counterpart: ''
mode: dark
name: nordic
- counterpart: ''
mode: dark
name: onedark
- counterpart: ''
mode: light
name: onesenterprise
- counterpart: paraiso-light
mode: dark
name: paraiso-dark
- counterpart: paraiso-dark
mode: light
name: paraiso-light
- counterpart: ''
mode: light
name: pastie
- counterpart: ''
mode: light
name: perldoc
- counterpart: ''
mode: light
name: pygments
- counterpart: ''
mode: light
name: rainbow_dash
- counterpart: rose-pine-dawn
mode: dark
name: rose-pine
- counterpart: rose-pine
mode: light
name: rose-pine-dawn
- counterpart: ''
mode: dark
name: rose-pine-moon
- counterpart: ''
mode: light
name: rpgle
- counterpart: ''
mode: dark
name: rrt
- counterpart: solarized-light
mode: dark
name: solarized-dark
- counterpart: ''
mode: dark
name: solarized-dark256
- counterpart: solarized-dark
mode: light
name: solarized-light
- counterpart: ''
mode: dark
name: swapoff
- counterpart: ''
mode: light
name: tango
- counterpart: tokyonight-night
mode: light
name: tokyonight-day
- counterpart: ''
mode: dark
name: tokyonight-moon
- counterpart: tokyonight-day
mode: dark
name: tokyonight-night
- counterpart: ''
mode: dark
name: tokyonight-storm
- counterpart: ''
mode: light
name: trac
- counterpart: ''
mode: dark
name: vim
- counterpart: ''
mode: light
name: vs
- counterpart: ''
mode: dark
name: vulcan
- counterpart: ''
mode: dark
name: witchhazel
- counterpart: xcode-dark
mode: light
name: xcode
- counterpart: xcode
mode: dark
name: xcode-dark
config:
HTTPCache:
cache:
+2 -1
View File
@@ -3,7 +3,7 @@ module github.com/gohugoio/hugo
require (
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1
github.com/alecthomas/chroma/v2 v2.24.1
github.com/alecthomas/chroma/v2 v2.27.0
github.com/aws/aws-sdk-go-v2 v1.41.6
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1
github.com/bep/clocks v0.5.0
@@ -128,6 +128,7 @@ require (
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
+8
View File
@@ -100,6 +100,10 @@ github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8v
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
@@ -211,6 +215,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+23 -1
View File
@@ -31,11 +31,33 @@ func init() {
}
var styleInfos []styleInfo
for name := range styles.Registry {
style := styles.Registry[name]
styleInfos = append(styleInfos, styleInfo{
Name: name,
Counterpart: style.Counterpart,
Mode: style.Mode().String(),
})
}
sort.Slice(styleInfos, func(i, j int) bool {
return styleInfos[i].Name < styleInfos[j].Name
})
return docshelper.DocProvider{"chroma": map[string]any{
"lexers": chromaLexers,
"styles": styles.Names(),
"styles": styleInfos,
}}
}
docshelper.AddDocProviderFunc(docsProvider)
}
type styleInfo struct {
Name string `json:"name"`
Mode string `json:"mode"`
Counterpart string `json:"counterpart"`
}
-24
View File
@@ -8,27 +8,3 @@ hugo gen man -h
stdout 'up-to-date man pages'
hugo gen man --dir manpages
hugo gen chromastyles -h
stdout 'Generate CSS stylesheet for the Chroma code highlighter'
hugo gen chromastyles --style monokai
stdout 'Generated using: hugo gen chromastyles --style monokai'
! hugo gen chromastyles --style __invalid_style__
stderr 'invalid style: __invalid_style__'
# Not empty classes.
# From Hugo 0.149.0 empty classes are always removed, so no need for the omitEmpty flag.
hugo gen chromastyles --style monokai
! stdout '{ }'
hugo gen chromastyles --omitEmpty --style monokai --logLevel info
stderr 'deprecated: --omitEmpty was deprecated'
# Keep same-colour token classes in generated CSS. This is needed when pairing
# styles such as github and github-dark in dual-theme sites.
hugo gen chromastyles --style github-dark
stdout 'NameOther.*\.chroma \.nx \{ color:#e6edf3 \}'
# Disable class comments.
hugo gen chromastyles --style monokai
stdout 'GenericSubheading'
hugo gen chromastyles --omitClassComments --style monokai
! stdout 'GenericSubheading'
+25
View File
@@ -0,0 +1,25 @@
hugo gen chromastyles -h
stdout 'Generate CSS stylesheet for the Chroma code highlighter'
hugo gen chromastyles --style monokai
stdout 'Generated using: hugo gen chromastyles --style monokai'
! hugo gen chromastyles --style __invalid_style__
stderr 'invalid style: __invalid_style__'
# Not empty classes.
# From Hugo 0.149.0 empty classes are always removed, so no need for the omitEmpty flag.
hugo gen chromastyles --style monokai
! stdout '{ }'
hugo gen chromastyles --omitEmpty --style monokai --logLevel info
stderr 'deprecated: --omitEmpty was deprecated'
# Keep same-colour token classes in generated CSS. This is needed when pairing
# styles such as github and github-dark in dual-theme sites.
hugo gen chromastyles --style github-dark
stdout 'NameOther.*\.chroma \.nx \{ color:#e6edf3 \}'
# Disable class comments.
hugo gen chromastyles --style monokai
stdout 'GenericSubheading'
hugo gen chromastyles --omitClassComments --style monokai
! stdout 'GenericSubheading'
@@ -0,0 +1,24 @@
# Dark mode, no mode selector: plain ".chroma" selectors.
hugo gen chromastyles --style github --mode dark
stdout '\.chroma'
! stdout '\.dark'
# Dark mode for style with no dark mode.
! hugo gen chromastyles --style friendly --mode dark
stderr 'style "friendly" does not have a "dark" mode'
# Dark mode with mode selector: ".dark .chroma".
hugo gen chromastyles --style github --mode dark --modeSelector
stdout '\.dark \.chroma'
# Light mode with mode selector: ".light .chroma".
hugo gen chromastyles --style github --mode light --modeSelector
stdout '\.light \.chroma'
# --modeSelector without --mode on a default light style.
hugo gen chromastyles --style github --modeSelector
stdout '\.light \.chroma'
# --modeSelector without --mode on a default dark style.
hugo gen chromastyles --style ashen --modeSelector
stdout '\.dark \.chroma'