mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
33d1f2c853
Closes #15167
172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
// Copyright 2026 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package highlight
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/alecthomas/chroma/v2"
|
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
|
"github.com/alecthomas/chroma/v2/styles"
|
|
)
|
|
|
|
// ChromaStylesOptions holds options for ChromaStylesCSS.
|
|
type ChromaStylesOptions struct {
|
|
// Highlighter style, e.g. "monokai".
|
|
Style string
|
|
|
|
// Style mode, "light" or "dark".
|
|
Mode string
|
|
|
|
// Scope selectors under a top level mode class, e.g. ".dark .chroma".
|
|
ModeSelector bool
|
|
|
|
// Class name used by ModeSelector for dark styles, defaults to "dark".
|
|
ClassDark string
|
|
|
|
// Class name used by ModeSelector for light styles, defaults to "light".
|
|
ClassLight string
|
|
|
|
// Foreground and background colors for highlighted lines, e.g. "#fff000 bg:#000fff".
|
|
HighlightStyle string
|
|
|
|
// Foreground and background colors for inline line numbers.
|
|
LineNumbersInlineStyle string
|
|
|
|
// Foreground and background colors for table line numbers.
|
|
LineNumbersTableStyle string
|
|
|
|
// Omit CSS class comment prefixes in the generated CSS.
|
|
OmitClassComments bool
|
|
}
|
|
|
|
// ChromaStylesCSS generates a CSS stylesheet for the Chroma code highlighter.
|
|
// This stylesheet is needed if markup.highlight.noClasses is disabled in config.
|
|
func ChromaStylesCSS(opts ChromaStylesOptions) (string, error) {
|
|
name := strings.ToLower(opts.Style)
|
|
if !slices.Contains(styles.Names(), name) {
|
|
return "", fmt.Errorf("invalid style: %s", name)
|
|
}
|
|
var chromaStyle *chroma.Style
|
|
if opts.Mode != "" {
|
|
var mode chroma.Mode
|
|
switch opts.Mode {
|
|
case "light":
|
|
mode = chroma.Light
|
|
case "dark":
|
|
mode = chroma.Dark
|
|
default:
|
|
return "", fmt.Errorf("invalid mode: %s", opts.Mode)
|
|
}
|
|
|
|
chromaStyle = styles.GetForMode(name, mode)
|
|
if chromaStyle.Mode() != mode {
|
|
return "", fmt.Errorf("style %q does not have a %q mode", name, opts.Mode)
|
|
}
|
|
} else {
|
|
chromaStyle = styles.Get(name)
|
|
}
|
|
builder := chromaStyle.Builder()
|
|
if opts.HighlightStyle != "" {
|
|
builder.Add(chroma.LineHighlight, opts.HighlightStyle)
|
|
}
|
|
if opts.LineNumbersInlineStyle != "" {
|
|
builder.Add(chroma.LineNumbers, opts.LineNumbersInlineStyle)
|
|
}
|
|
if opts.LineNumbersTableStyle != "" {
|
|
builder.Add(chroma.LineNumbersTable, opts.LineNumbersTableStyle)
|
|
}
|
|
style, err := builder.Build()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
options := []html.Option{
|
|
html.WithCSSComments(!opts.OmitClassComments),
|
|
html.WithCustomCSS(chromaCSSOverrides(style)),
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := html.New(options...).WriteCSS(&buf, style); err != nil {
|
|
return "", err
|
|
}
|
|
css := buf.String()
|
|
if opts.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.
|
|
classDark, classLight := opts.ClassDark, opts.ClassLight
|
|
if classDark == "" {
|
|
classDark = "dark"
|
|
}
|
|
if classLight == "" {
|
|
classLight = "light"
|
|
}
|
|
var prefix string
|
|
switch style.Mode() {
|
|
case chroma.Light:
|
|
prefix = "." + classLight + " "
|
|
case chroma.Dark:
|
|
prefix = "." + classDark + " "
|
|
default:
|
|
return "", fmt.Errorf("style %q does not have a light or dark mode", style.Name)
|
|
}
|
|
replacer := strings.NewReplacer(
|
|
".bg {", prefix+".bg {",
|
|
".chroma ", prefix+".chroma ",
|
|
)
|
|
css = replacer.Replace(css)
|
|
}
|
|
|
|
return css, nil
|
|
}
|
|
|
|
// chromaCSSOverrides re-emits leaf token colors that Chroma's minifier drops
|
|
// because they equal the style's default foreground (the .chroma color).
|
|
// In a paired light/dark setup, the other sheet's explicit rule (e.g. .chroma .nx)
|
|
// would otherwise leak into this mode, since an explicit declaration beats
|
|
// inheritance from .chroma.
|
|
// TODO(bep): replace with an upstream option, see issue 15161.
|
|
func chromaCSSOverrides(style *chroma.Style) map[chroma.TokenType]string {
|
|
bg := style.Get(chroma.Background)
|
|
m := make(map[chroma.TokenType]string)
|
|
for tt := range chroma.StandardTypes {
|
|
if tt == chroma.Background || !style.Has(tt) || !chromaLeafToken(tt) {
|
|
continue
|
|
}
|
|
entry := style.Get(tt)
|
|
if !entry.Sub(bg).IsZero() || !entry.Colour.IsSet() {
|
|
continue
|
|
}
|
|
if css := html.StyleEntryToCSS(chroma.StyleEntry{Colour: entry.Colour}); css != "" {
|
|
m[tt] = css
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func chromaLeafToken(tt chroma.TokenType) bool {
|
|
for other := range chroma.StandardTypes {
|
|
if other != tt && (other.Category() == tt || other.SubCategory() == tt) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|