mirror of https://github.com/gohugoio/hugo
298 lines
6.7 KiB
Go
298 lines
6.7 KiB
Go
// Copyright 2019 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 provides code highlighting.
|
|
package highlight
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
|
"github.com/spf13/cast"
|
|
|
|
"github.com/gohugoio/hugo/config"
|
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
const (
|
|
lineanchorsKey = "lineanchors"
|
|
lineNosKey = "linenos"
|
|
hlLinesKey = "hl_lines"
|
|
linosStartKey = "linenostart"
|
|
noHlKey = "nohl"
|
|
)
|
|
|
|
var DefaultConfig = Config{
|
|
// The highlighter style to use.
|
|
// See https://xyproto.github.io/splash/docs/all.html
|
|
Style: "monokai",
|
|
LineNoStart: 1,
|
|
CodeFences: true,
|
|
NoClasses: true,
|
|
LineNumbersInTable: true,
|
|
TabWidth: 4,
|
|
}
|
|
|
|
type Config struct {
|
|
Style string
|
|
|
|
CodeFences bool
|
|
|
|
// Use inline CSS styles.
|
|
NoClasses bool
|
|
|
|
// No highlighting.
|
|
NoHl bool
|
|
|
|
// When set, line numbers will be printed.
|
|
LineNos bool
|
|
LineNumbersInTable bool
|
|
|
|
// When set, add links to line numbers
|
|
AnchorLineNos bool
|
|
LineAnchors string
|
|
|
|
// Start the line numbers from this value (default is 1).
|
|
LineNoStart int
|
|
|
|
// A space separated list of line numbers, e.g. “3-8 10-20”.
|
|
Hl_Lines string
|
|
|
|
// If set, the markup will not be wrapped in any container.
|
|
Hl_inline bool
|
|
|
|
// A parsed and ready to use list of line ranges.
|
|
HL_lines_parsed [][2]int `json:"-"`
|
|
|
|
// TabWidth sets the number of characters for a tab. Defaults to 4.
|
|
TabWidth int
|
|
|
|
GuessSyntax bool
|
|
}
|
|
|
|
func (cfg Config) toHTMLOptions() []html.Option {
|
|
var lineAnchors string
|
|
if cfg.LineAnchors != "" {
|
|
lineAnchors = cfg.LineAnchors + "-"
|
|
}
|
|
options := []html.Option{
|
|
html.TabWidth(cfg.TabWidth),
|
|
html.WithLineNumbers(cfg.LineNos),
|
|
html.BaseLineNumber(cfg.LineNoStart),
|
|
html.LineNumbersInTable(cfg.LineNumbersInTable),
|
|
html.WithClasses(!cfg.NoClasses),
|
|
html.WithLinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
|
|
html.InlineCode(cfg.Hl_inline),
|
|
}
|
|
|
|
if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
|
|
var ranges [][2]int
|
|
if cfg.HL_lines_parsed != nil {
|
|
ranges = cfg.HL_lines_parsed
|
|
} else {
|
|
var err error
|
|
ranges, err = hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
|
|
if err != nil {
|
|
ranges = nil
|
|
}
|
|
}
|
|
|
|
if ranges != nil {
|
|
options = append(options, html.HighlightLines(ranges))
|
|
}
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
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 {
|
|
return err
|
|
}
|
|
return mapstructure.WeakDecode(optsm, cfg)
|
|
}
|
|
|
|
func applyOptionsFromMap(optsm map[string]any, cfg *Config) error {
|
|
normalizeHighlightOptions(optsm)
|
|
return mapstructure.WeakDecode(optsm, cfg)
|
|
}
|
|
|
|
func applyOptionsFromCodeBlockContext(ctx hooks.CodeblockContext, cfg *Config) error {
|
|
if cfg.LineAnchors == "" {
|
|
const lineAnchorPrefix = "hl-"
|
|
// Set it to the ordinal with a prefix.
|
|
cfg.LineAnchors = fmt.Sprintf("%s%d", lineAnchorPrefix, ctx.Ordinal())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApplyLegacyConfig applies legacy config from back when we had
|
|
// Pygments.
|
|
func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
|
|
if conf.Style == DefaultConfig.Style {
|
|
if s := cfg.GetString("pygmentsStyle"); s != "" {
|
|
conf.Style = s
|
|
}
|
|
}
|
|
|
|
if conf.NoClasses == DefaultConfig.NoClasses && cfg.IsSet("pygmentsUseClasses") {
|
|
conf.NoClasses = !cfg.GetBool("pygmentsUseClasses")
|
|
}
|
|
|
|
if conf.CodeFences == DefaultConfig.CodeFences && cfg.IsSet("pygmentsCodeFences") {
|
|
conf.CodeFences = cfg.GetBool("pygmentsCodeFences")
|
|
}
|
|
|
|
if conf.GuessSyntax == DefaultConfig.GuessSyntax && cfg.IsSet("pygmentsCodefencesGuessSyntax") {
|
|
conf.GuessSyntax = cfg.GetBool("pygmentsCodefencesGuessSyntax")
|
|
}
|
|
|
|
if cfg.IsSet("pygmentsOptions") {
|
|
if err := applyOptionsFromString(cfg.GetString("pygmentsOptions"), conf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseHighlightOptions(in string) (map[string]any, error) {
|
|
in = strings.Trim(in, " ")
|
|
opts := make(map[string]any)
|
|
|
|
if in == "" {
|
|
return opts, nil
|
|
}
|
|
|
|
for _, v := range strings.Split(in, ",") {
|
|
keyVal := strings.Split(v, "=")
|
|
key := strings.Trim(keyVal[0], " ")
|
|
if len(keyVal) != 2 {
|
|
return opts, fmt.Errorf("invalid Highlight option: %s", key)
|
|
}
|
|
opts[key] = keyVal[1]
|
|
|
|
}
|
|
|
|
normalizeHighlightOptions(opts)
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
func normalizeHighlightOptions(m map[string]any) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
|
|
// lowercase all keys
|
|
for k, v := range m {
|
|
delete(m, k)
|
|
m[strings.ToLower(k)] = v
|
|
}
|
|
|
|
baseLineNumber := 1
|
|
if v, ok := m[linosStartKey]; ok {
|
|
baseLineNumber = cast.ToInt(v)
|
|
}
|
|
|
|
for k, v := range m {
|
|
switch k {
|
|
case noHlKey:
|
|
m[noHlKey] = cast.ToBool(v)
|
|
case lineNosKey:
|
|
if v == "table" || v == "inline" {
|
|
m["lineNumbersInTable"] = v == "table"
|
|
}
|
|
if vs, ok := v.(string); ok {
|
|
m[k] = vs != "false"
|
|
}
|
|
case hlLinesKey:
|
|
if hlRanges, ok := v.([][2]int); ok {
|
|
for i := range hlRanges {
|
|
hlRanges[i][0] += baseLineNumber
|
|
hlRanges[i][1] += baseLineNumber
|
|
}
|
|
delete(m, k)
|
|
m[k+"_parsed"] = hlRanges
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// startLine compensates for https://github.com/alecthomas/chroma/issues/30
|
|
func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
|
|
var ranges [][2]int
|
|
s = strings.TrimSpace(s)
|
|
|
|
if s == "" {
|
|
return ranges, nil
|
|
}
|
|
|
|
// Variants:
|
|
// 1 2 3 4
|
|
// 1-2 3-4
|
|
// 1-2 3
|
|
// 1 3-4
|
|
// 1 3-4
|
|
fields := strings.Split(s, " ")
|
|
for _, field := range fields {
|
|
field = strings.TrimSpace(field)
|
|
if field == "" {
|
|
continue
|
|
}
|
|
numbers := strings.Split(field, "-")
|
|
var r [2]int
|
|
first, err := strconv.Atoi(numbers[0])
|
|
if err != nil {
|
|
return ranges, err
|
|
}
|
|
first = first + startLine - 1
|
|
r[0] = first
|
|
if len(numbers) > 1 {
|
|
second, err := strconv.Atoi(numbers[1])
|
|
if err != nil {
|
|
return ranges, err
|
|
}
|
|
second = second + startLine - 1
|
|
r[1] = second
|
|
} else {
|
|
r[1] = first
|
|
}
|
|
|
|
ranges = append(ranges, r)
|
|
}
|
|
return ranges, nil
|
|
}
|