diff --git a/commands/gen.go b/commands/gen.go index d95f190c1..8d9224a08 100644 --- a/commands/gen.go +++ b/commands/gen.go @@ -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) diff --git a/docs/data/docs.yaml b/docs/data/docs.yaml index 50443444f..19b950dd5 100644 --- a/docs/data/docs.yaml +++ b/docs/data/docs.yaml @@ -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: diff --git a/go.mod b/go.mod index a2165b918..37ee48af9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 00ef402e8..218df97c1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/helpers/docshelper.go b/helpers/docshelper.go index 168f8995d..31810eee3 100644 --- a/helpers/docshelper.go +++ b/helpers/docshelper.go @@ -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"` +} diff --git a/testscripts/commands/gen.txt b/testscripts/commands/gen.txt index 3316219e9..736d0f569 100644 --- a/testscripts/commands/gen.txt +++ b/testscripts/commands/gen.txt @@ -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' diff --git a/testscripts/commands/gen_chromastyles.txt b/testscripts/commands/gen_chromastyles.txt new file mode 100644 index 000000000..52fec26f0 --- /dev/null +++ b/testscripts/commands/gen_chromastyles.txt @@ -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' + diff --git a/testscripts/commands/gen_chromastyles_mode.txt b/testscripts/commands/gen_chromastyles_mode.txt new file mode 100644 index 000000000..229f5f8cc --- /dev/null +++ b/testscripts/commands/gen_chromastyles_mode.txt @@ -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'