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