github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/common/herrors/file_error.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 lfmtaw 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 15 16 import ( 17 "encoding/json" 18 19 godartsassv1 "github.com/bep/godartsass" 20 21 "fmt" 22 "io" 23 "path/filepath" 24 25 "github.com/bep/godartsass/v2" 26 "github.com/bep/golibsass/libsass/libsasserrors" 27 "github.com/gohugoio/hugo/common/paths" 28 "github.com/gohugoio/hugo/common/text" 29 "github.com/pelletier/go-toml/v2" 30 "github.com/spf13/afero" 31 "github.com/tdewolff/parse/v2" 32 33 "errors" 34 ) 35 36 // FileError represents an error when handling a file: Parsing a config file, 37 // execute a template etc. 38 type FileError interface { 39 error 40 41 // ErrorContext holds some context information about the error. 42 ErrorContext() *ErrorContext 43 44 text.Positioner 45 46 // UpdatePosition updates the position of the error. 47 UpdatePosition(pos text.Position) FileError 48 49 // UpdateContent updates the error with a new ErrorContext from the content of the file. 50 UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError 51 } 52 53 // Unwrapper can unwrap errors created with fmt.Errorf. 54 type Unwrapper interface { 55 Unwrap() error 56 } 57 58 var ( 59 _ FileError = (*fileError)(nil) 60 _ Unwrapper = (*fileError)(nil) 61 ) 62 63 func (fe *fileError) UpdatePosition(pos text.Position) FileError { 64 oldFilename := fe.Position().Filename 65 if pos.Filename != "" && fe.fileType == "" { 66 _, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) 67 } 68 if pos.Filename == "" { 69 pos.Filename = oldFilename 70 } 71 fe.position = pos 72 return fe 73 } 74 75 func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError { 76 if linematcher == nil { 77 linematcher = SimpleLineMatcher 78 } 79 80 var ( 81 posle = fe.position 82 ectx *ErrorContext 83 ) 84 85 if posle.LineNumber <= 1 && posle.Offset > 0 { 86 // Try to locate the line number from the content if offset is set. 87 ectx = locateError(r, fe, func(m LineMatcher) int { 88 if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) { 89 lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber 90 m.Position = text.Position{LineNumber: lno} 91 return linematcher(m) 92 } 93 return -1 94 }) 95 } else { 96 ectx = locateError(r, fe, linematcher) 97 } 98 99 if ectx.ChromaLexer == "" { 100 if fe.fileType != "" { 101 ectx.ChromaLexer = chromaLexerFromType(fe.fileType) 102 } else { 103 ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename) 104 } 105 } 106 107 fe.errorContext = ectx 108 109 if ectx.Position.LineNumber > 0 { 110 fe.position.LineNumber = ectx.Position.LineNumber 111 } 112 113 if ectx.Position.ColumnNumber > 0 { 114 fe.position.ColumnNumber = ectx.Position.ColumnNumber 115 } 116 117 return fe 118 119 } 120 121 type fileError struct { 122 position text.Position 123 errorContext *ErrorContext 124 125 fileType string 126 127 cause error 128 } 129 130 func (e *fileError) ErrorContext() *ErrorContext { 131 return e.errorContext 132 } 133 134 // Position returns the text position of this error. 135 func (e fileError) Position() text.Position { 136 return e.position 137 } 138 139 func (e *fileError) Error() string { 140 return fmt.Sprintf("%s: %s", e.position, e.causeString()) 141 } 142 143 func (e *fileError) causeString() string { 144 if e.cause == nil { 145 return "" 146 } 147 switch v := e.cause.(type) { 148 // Avoid repeating the file info in the error message. 149 case godartsass.SassError: 150 return v.Message 151 case godartsassv1.SassError: 152 return v.Message 153 case libsasserrors.Error: 154 return v.Message 155 default: 156 return v.Error() 157 } 158 } 159 160 func (e *fileError) Unwrap() error { 161 return e.cause 162 } 163 164 // NewFileError creates a new FileError that wraps err. 165 // It will try to extract the filename and line number from err. 166 func NewFileError(err error) FileError { 167 // Filetype is used to determine the Chroma lexer to use. 168 fileType, pos := extractFileTypePos(err) 169 return &fileError{cause: err, fileType: fileType, position: pos} 170 } 171 172 // NewFileErrorFromName creates a new FileError that wraps err. 173 // The value for name should identify the file, the best 174 // being the full filename to the file on disk. 175 func NewFileErrorFromName(err error, name string) FileError { 176 // Filetype is used to determine the Chroma lexer to use. 177 fileType, pos := extractFileTypePos(err) 178 pos.Filename = name 179 if fileType == "" { 180 _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name)) 181 } 182 183 return &fileError{cause: err, fileType: fileType, position: pos} 184 185 } 186 187 // NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err. 188 func NewFileErrorFromPos(err error, pos text.Position) FileError { 189 // Filetype is used to determine the Chroma lexer to use. 190 fileType, _ := extractFileTypePos(err) 191 if fileType == "" { 192 _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) 193 } 194 return &fileError{cause: err, fileType: fileType, position: pos} 195 196 } 197 198 func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError { 199 fe := NewFileError(err) 200 pos := fe.Position() 201 if pos.Filename == "" { 202 return fe 203 } 204 205 f, realFilename, err2 := openFile(pos.Filename, fs) 206 if err2 != nil { 207 return fe 208 } 209 210 pos.Filename = realFilename 211 defer f.Close() 212 return fe.UpdateContent(f, linematcher) 213 } 214 215 func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError { 216 if err == nil { 217 panic("err is nil") 218 } 219 f, realFilename, err2 := openFile(pos.Filename, fs) 220 if err2 != nil { 221 return NewFileErrorFromPos(err, pos) 222 } 223 pos.Filename = realFilename 224 defer f.Close() 225 return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher) 226 } 227 228 // NewFileErrorFromFile is a convenience method to create a new FileError from a file. 229 func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError { 230 if err == nil { 231 panic("err is nil") 232 } 233 f, realFilename, err2 := openFile(filename, fs) 234 if err2 != nil { 235 return NewFileErrorFromName(err, realFilename) 236 } 237 defer f.Close() 238 return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher) 239 } 240 241 func openFile(filename string, fs afero.Fs) (afero.File, string, error) { 242 realFilename := filename 243 244 // We want the most specific filename possible in the error message. 245 fi, err2 := fs.Stat(filename) 246 if err2 == nil { 247 if s, ok := fi.(interface { 248 Filename() string 249 }); ok { 250 realFilename = s.Filename() 251 } 252 253 } 254 255 f, err2 := fs.Open(filename) 256 if err2 != nil { 257 return nil, realFilename, err2 258 } 259 260 return f, realFilename, nil 261 } 262 263 // Cause returns the underlying error or itself if it does not implement Unwrap. 264 func Cause(err error) error { 265 if u := errors.Unwrap(err); u != nil { 266 return u 267 } 268 return err 269 } 270 271 func extractFileTypePos(err error) (string, text.Position) { 272 err = Cause(err) 273 274 var fileType string 275 276 // LibSass, DartSass 277 if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 { 278 _, fileType = paths.FileAndExtNoDelimiter(pos.Filename) 279 return fileType, pos 280 } 281 282 // Default to line 1 col 1 if we don't find any better. 283 pos := text.Position{ 284 Offset: -1, 285 LineNumber: 1, 286 ColumnNumber: 1, 287 } 288 289 // JSON errors. 290 offset, typ := extractOffsetAndType(err) 291 if fileType == "" { 292 fileType = typ 293 } 294 295 if offset >= 0 { 296 pos.Offset = offset 297 } 298 299 // The error type from the minifier contains line number and column number. 300 if line, col := extractLineNumberAndColumnNumber(err); line >= 0 { 301 pos.LineNumber = line 302 pos.ColumnNumber = col 303 return fileType, pos 304 } 305 306 // Look in the error message for the line number. 307 for _, handle := range lineNumberExtractors { 308 lno, col := handle(err) 309 if lno > 0 { 310 pos.ColumnNumber = col 311 pos.LineNumber = lno 312 break 313 } 314 } 315 316 if fileType == "" && pos.Filename != "" { 317 _, fileType = paths.FileAndExtNoDelimiter(pos.Filename) 318 } 319 320 return fileType, pos 321 } 322 323 // UnwrapFileError tries to unwrap a FileError from err. 324 // It returns nil if this is not possible. 325 func UnwrapFileError(err error) FileError { 326 for err != nil { 327 switch v := err.(type) { 328 case FileError: 329 return v 330 default: 331 err = errors.Unwrap(err) 332 } 333 } 334 return nil 335 } 336 337 // UnwrapFileErrors tries to unwrap all FileError. 338 func UnwrapFileErrors(err error) []FileError { 339 var errs []FileError 340 for err != nil { 341 if v, ok := err.(FileError); ok { 342 errs = append(errs, v) 343 } 344 err = errors.Unwrap(err) 345 } 346 return errs 347 } 348 349 // UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext. 350 func UnwrapFileErrorsWithErrorContext(err error) []FileError { 351 var errs []FileError 352 for err != nil { 353 if v, ok := err.(FileError); ok && v.ErrorContext() != nil { 354 errs = append(errs, v) 355 } 356 err = errors.Unwrap(err) 357 } 358 return errs 359 } 360 361 func extractOffsetAndType(e error) (int, string) { 362 switch v := e.(type) { 363 case *json.UnmarshalTypeError: 364 return int(v.Offset), "json" 365 case *json.SyntaxError: 366 return int(v.Offset), "json" 367 default: 368 return -1, "" 369 } 370 } 371 372 func extractLineNumberAndColumnNumber(e error) (int, int) { 373 switch v := e.(type) { 374 case *parse.Error: 375 return v.Line, v.Column 376 case *toml.DecodeError: 377 return v.Position() 378 379 } 380 381 return -1, -1 382 } 383 384 func extractPosition(e error) (pos text.Position) { 385 switch v := e.(type) { 386 case godartsass.SassError: 387 span := v.Span 388 start := span.Start 389 filename, _ := paths.UrlToFilename(span.Url) 390 pos.Filename = filename 391 pos.Offset = start.Offset 392 pos.ColumnNumber = start.Column 393 case godartsassv1.SassError: 394 span := v.Span 395 start := span.Start 396 filename, _ := paths.UrlToFilename(span.Url) 397 pos.Filename = filename 398 pos.Offset = start.Offset 399 pos.ColumnNumber = start.Column 400 case libsasserrors.Error: 401 pos.Filename = v.File 402 pos.LineNumber = v.Line 403 pos.ColumnNumber = v.Column 404 } 405 return 406 } 407 408 // TextSegmentError is an error with a text segment attached. 409 type TextSegmentError struct { 410 Segment string 411 Err error 412 } 413 414 func (e TextSegmentError) Unwrap() error { 415 return e.Err 416 } 417 418 func (e TextSegmentError) Error() string { 419 return e.Err.Error() 420 }