github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/markup/highlight/config.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package highlight provides code highlighting.
    15  package highlight
    16  
    17  import (
    18  	"fmt"
    19  	"strconv"
    20  	"strings"
    21  
    22  	"github.com/alecthomas/chroma/v2/formatters/html"
    23  	"github.com/spf13/cast"
    24  
    25  	"github.com/gohugoio/hugo/config"
    26  	"github.com/gohugoio/hugo/markup/converter/hooks"
    27  
    28  	"github.com/mitchellh/mapstructure"
    29  )
    30  
    31  const (
    32  	lineanchorsKey = "lineanchors"
    33  	lineNosKey     = "linenos"
    34  	hlLinesKey     = "hl_lines"
    35  	linosStartKey  = "linenostart"
    36  	noHlKey        = "nohl"
    37  )
    38  
    39  var DefaultConfig = Config{
    40  	// The highlighter style to use.
    41  	// See https://xyproto.github.io/splash/docs/all.html
    42  	Style:              "monokai",
    43  	LineNoStart:        1,
    44  	CodeFences:         true,
    45  	NoClasses:          true,
    46  	LineNumbersInTable: true,
    47  	TabWidth:           4,
    48  }
    49  
    50  type Config struct {
    51  	Style string
    52  
    53  	CodeFences bool
    54  
    55  	// Use inline CSS styles.
    56  	NoClasses bool
    57  
    58  	// No highlighting.
    59  	NoHl bool
    60  
    61  	// When set, line numbers will be printed.
    62  	LineNos            bool
    63  	LineNumbersInTable bool
    64  
    65  	// When set, add links to line numbers
    66  	AnchorLineNos bool
    67  	LineAnchors   string
    68  
    69  	// Start the line numbers from this value (default is 1).
    70  	LineNoStart int
    71  
    72  	// A space separated list of line numbers, e.g. “3-8 10-20”.
    73  	Hl_Lines string
    74  
    75  	// If set, the markup will not be wrapped in any container.
    76  	Hl_inline bool
    77  
    78  	// A parsed and ready to use list of line ranges.
    79  	HL_lines_parsed [][2]int `json:"-"`
    80  
    81  	// TabWidth sets the number of characters for a tab. Defaults to 4.
    82  	TabWidth int
    83  
    84  	GuessSyntax bool
    85  }
    86  
    87  func (cfg Config) toHTMLOptions() []html.Option {
    88  	var lineAnchors string
    89  	if cfg.LineAnchors != "" {
    90  		lineAnchors = cfg.LineAnchors + "-"
    91  	}
    92  	options := []html.Option{
    93  		html.TabWidth(cfg.TabWidth),
    94  		html.WithLineNumbers(cfg.LineNos),
    95  		html.BaseLineNumber(cfg.LineNoStart),
    96  		html.LineNumbersInTable(cfg.LineNumbersInTable),
    97  		html.WithClasses(!cfg.NoClasses),
    98  		html.WithLinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
    99  		html.InlineCode(cfg.Hl_inline),
   100  	}
   101  
   102  	if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
   103  		var ranges [][2]int
   104  		if cfg.HL_lines_parsed != nil {
   105  			ranges = cfg.HL_lines_parsed
   106  		} else {
   107  			var err error
   108  			ranges, err = hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
   109  			if err != nil {
   110  				ranges = nil
   111  			}
   112  		}
   113  
   114  		if ranges != nil {
   115  			options = append(options, html.HighlightLines(ranges))
   116  		}
   117  	}
   118  
   119  	return options
   120  }
   121  
   122  func applyOptions(opts any, cfg *Config) error {
   123  	if opts == nil {
   124  		return nil
   125  	}
   126  	switch vv := opts.(type) {
   127  	case map[string]any:
   128  		return applyOptionsFromMap(vv, cfg)
   129  	default:
   130  		s, err := cast.ToStringE(opts)
   131  		if err != nil {
   132  			return err
   133  		}
   134  		return applyOptionsFromString(s, cfg)
   135  	}
   136  }
   137  
   138  func applyOptionsFromString(opts string, cfg *Config) error {
   139  	optsm, err := parseHighlightOptions(opts)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	return mapstructure.WeakDecode(optsm, cfg)
   144  }
   145  
   146  func applyOptionsFromMap(optsm map[string]any, cfg *Config) error {
   147  	normalizeHighlightOptions(optsm)
   148  	return mapstructure.WeakDecode(optsm, cfg)
   149  }
   150  
   151  func applyOptionsFromCodeBlockContext(ctx hooks.CodeblockContext, cfg *Config) error {
   152  	if cfg.LineAnchors == "" {
   153  		const lineAnchorPrefix = "hl-"
   154  		// Set it to the ordinal with a prefix.
   155  		cfg.LineAnchors = fmt.Sprintf("%s%d", lineAnchorPrefix, ctx.Ordinal())
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  // ApplyLegacyConfig applies legacy config from back when we had
   162  // Pygments.
   163  func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
   164  	if conf.Style == DefaultConfig.Style {
   165  		if s := cfg.GetString("pygmentsStyle"); s != "" {
   166  			conf.Style = s
   167  		}
   168  	}
   169  
   170  	if conf.NoClasses == DefaultConfig.NoClasses && cfg.IsSet("pygmentsUseClasses") {
   171  		conf.NoClasses = !cfg.GetBool("pygmentsUseClasses")
   172  	}
   173  
   174  	if conf.CodeFences == DefaultConfig.CodeFences && cfg.IsSet("pygmentsCodeFences") {
   175  		conf.CodeFences = cfg.GetBool("pygmentsCodeFences")
   176  	}
   177  
   178  	if conf.GuessSyntax == DefaultConfig.GuessSyntax && cfg.IsSet("pygmentsCodefencesGuessSyntax") {
   179  		conf.GuessSyntax = cfg.GetBool("pygmentsCodefencesGuessSyntax")
   180  	}
   181  
   182  	if cfg.IsSet("pygmentsOptions") {
   183  		if err := applyOptionsFromString(cfg.GetString("pygmentsOptions"), conf); err != nil {
   184  			return err
   185  		}
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  func parseHighlightOptions(in string) (map[string]any, error) {
   192  	in = strings.Trim(in, " ")
   193  	opts := make(map[string]any)
   194  
   195  	if in == "" {
   196  		return opts, nil
   197  	}
   198  
   199  	for _, v := range strings.Split(in, ",") {
   200  		keyVal := strings.Split(v, "=")
   201  		key := strings.Trim(keyVal[0], " ")
   202  		if len(keyVal) != 2 {
   203  			return opts, fmt.Errorf("invalid Highlight option: %s", key)
   204  		}
   205  		opts[key] = keyVal[1]
   206  
   207  	}
   208  
   209  	normalizeHighlightOptions(opts)
   210  
   211  	return opts, nil
   212  }
   213  
   214  func normalizeHighlightOptions(m map[string]any) {
   215  	if m == nil {
   216  		return
   217  	}
   218  
   219  	// lowercase all keys
   220  	for k, v := range m {
   221  		delete(m, k)
   222  		m[strings.ToLower(k)] = v
   223  	}
   224  
   225  	baseLineNumber := 1
   226  	if v, ok := m[linosStartKey]; ok {
   227  		baseLineNumber = cast.ToInt(v)
   228  	}
   229  
   230  	for k, v := range m {
   231  		switch k {
   232  		case noHlKey:
   233  			m[noHlKey] = cast.ToBool(v)
   234  		case lineNosKey:
   235  			if v == "table" || v == "inline" {
   236  				m["lineNumbersInTable"] = v == "table"
   237  			}
   238  			if vs, ok := v.(string); ok {
   239  				m[k] = vs != "false"
   240  			}
   241  		case hlLinesKey:
   242  			if hlRanges, ok := v.([][2]int); ok {
   243  				for i := range hlRanges {
   244  					hlRanges[i][0] += baseLineNumber
   245  					hlRanges[i][1] += baseLineNumber
   246  				}
   247  				delete(m, k)
   248  				m[k+"_parsed"] = hlRanges
   249  			}
   250  		}
   251  	}
   252  }
   253  
   254  // startLine compensates for https://github.com/alecthomas/chroma/issues/30
   255  func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
   256  	var ranges [][2]int
   257  	s = strings.TrimSpace(s)
   258  
   259  	if s == "" {
   260  		return ranges, nil
   261  	}
   262  
   263  	// Variants:
   264  	// 1 2 3 4
   265  	// 1-2 3-4
   266  	// 1-2 3
   267  	// 1 3-4
   268  	// 1    3-4
   269  	fields := strings.Split(s, " ")
   270  	for _, field := range fields {
   271  		field = strings.TrimSpace(field)
   272  		if field == "" {
   273  			continue
   274  		}
   275  		numbers := strings.Split(field, "-")
   276  		var r [2]int
   277  		first, err := strconv.Atoi(numbers[0])
   278  		if err != nil {
   279  			return ranges, err
   280  		}
   281  		first = first + startLine - 1
   282  		r[0] = first
   283  		if len(numbers) > 1 {
   284  			second, err := strconv.Atoi(numbers[1])
   285  			if err != nil {
   286  				return ranges, err
   287  			}
   288  			second = second + startLine - 1
   289  			r[1] = second
   290  		} else {
   291  			r[1] = first
   292  		}
   293  
   294  		ranges = append(ranges, r)
   295  	}
   296  	return ranges, nil
   297  }