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 }