github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/common/herrors/error_locator.go (about)

     1  // Copyright 2022 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 herrors contains common Hugo errors and error related utilities.
    15  package herrors
    16  
    17  import (
    18  	"io"
    19  	"path/filepath"
    20  	"strings"
    21  
    22  	"github.com/gohugoio/hugo/common/text"
    23  )
    24  
    25  // LineMatcher contains the elements used to match an error to a line
    26  type LineMatcher struct {
    27  	Position text.Position
    28  	Error    error
    29  
    30  	LineNumber int
    31  	Offset     int
    32  	Line       string
    33  }
    34  
    35  // LineMatcherFn is used to match a line with an error.
    36  // It returns the column number or 0 if the line was found, but column could not be determined. Returns -1 if no line match.
    37  type LineMatcherFn func(m LineMatcher) int
    38  
    39  // SimpleLineMatcher simply matches by line number.
    40  var SimpleLineMatcher = func(m LineMatcher) int {
    41  	if m.Position.LineNumber == m.LineNumber {
    42  		// We found the line, but don't know the column.
    43  		return 0
    44  	}
    45  	return -1
    46  }
    47  
    48  // NopLineMatcher is a matcher that always returns 1.
    49  // This will effectively give line 1, column 1.
    50  var NopLineMatcher = func(m LineMatcher) int {
    51  	return 1
    52  }
    53  
    54  // OffsetMatcher is a line matcher that matches by offset.
    55  var OffsetMatcher = func(m LineMatcher) int {
    56  	if m.Offset+len(m.Line) >= m.Position.Offset {
    57  		// We found the line, but return 0 to signal that we want to determine
    58  		// the column from the error.
    59  		return 0
    60  	}
    61  	return -1
    62  }
    63  
    64  // ContainsMatcher is a line matcher that matches by line content.
    65  func ContainsMatcher(text string) func(m LineMatcher) int {
    66  	return func(m LineMatcher) int {
    67  		if idx := strings.Index(m.Line, text); idx != -1 {
    68  			return idx + 1
    69  		}
    70  		return -1
    71  	}
    72  }
    73  
    74  // ErrorContext contains contextual information about an error. This will
    75  // typically be the lines surrounding some problem in a file.
    76  type ErrorContext struct {
    77  
    78  	// If a match will contain the matched line and up to 2 lines before and after.
    79  	// Will be empty if no match.
    80  	Lines []string
    81  
    82  	// The position of the error in the Lines above. 0 based.
    83  	LinesPos int
    84  
    85  	// The position of the content in the file. Note that this may be different from the error's position set
    86  	// in FileError.
    87  	Position text.Position
    88  
    89  	// The lexer to use for syntax highlighting.
    90  	// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
    91  	ChromaLexer string
    92  }
    93  
    94  func chromaLexerFromType(fileType string) string {
    95  	switch fileType {
    96  	case "html", "htm":
    97  		return "go-html-template"
    98  	}
    99  	return fileType
   100  }
   101  
   102  func extNoDelimiter(filename string) string {
   103  	return strings.TrimPrefix(filepath.Ext(filename), ".")
   104  }
   105  
   106  func chromaLexerFromFilename(filename string) string {
   107  	if strings.Contains(filename, "layouts") {
   108  		return "go-html-template"
   109  	}
   110  
   111  	ext := extNoDelimiter(filename)
   112  	return chromaLexerFromType(ext)
   113  }
   114  
   115  func locateErrorInString(src string, matcher LineMatcherFn) *ErrorContext {
   116  	return locateError(strings.NewReader(src), &fileError{}, matcher)
   117  }
   118  
   119  func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext {
   120  	if le == nil {
   121  		panic("must provide an error")
   122  	}
   123  
   124  	ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}}
   125  
   126  	b, err := io.ReadAll(r)
   127  	if err != nil {
   128  		return ectx
   129  	}
   130  
   131  	lines := strings.Split(string(b), "\n")
   132  
   133  	lineNo := 0
   134  	posBytes := 0
   135  
   136  	for li, line := range lines {
   137  		lineNo = li + 1
   138  		m := LineMatcher{
   139  			Position:   le.Position(),
   140  			Error:      le,
   141  			LineNumber: lineNo,
   142  			Offset:     posBytes,
   143  			Line:       line,
   144  		}
   145  		v := matches(m)
   146  		if ectx.LinesPos == -1 && v != -1 {
   147  			ectx.Position.LineNumber = lineNo
   148  			ectx.Position.ColumnNumber = v
   149  			break
   150  		}
   151  
   152  		posBytes += len(line)
   153  	}
   154  
   155  	if ectx.Position.LineNumber > 0 {
   156  		low := ectx.Position.LineNumber - 3
   157  		if low < 0 {
   158  			low = 0
   159  		}
   160  
   161  		if ectx.Position.LineNumber > 2 {
   162  			ectx.LinesPos = 2
   163  		} else {
   164  			ectx.LinesPos = ectx.Position.LineNumber - 1
   165  		}
   166  
   167  		high := ectx.Position.LineNumber + 2
   168  		if high > len(lines) {
   169  			high = len(lines)
   170  		}
   171  
   172  		ectx.Lines = lines[low:high]
   173  
   174  	}
   175  
   176  	return ectx
   177  }