diff options
Diffstat (limited to 'common')
-rw-r--r-- | common/herrors/error_locator.go | 129 | ||||
-rw-r--r-- | common/herrors/error_locator_test.go | 43 | ||||
-rw-r--r-- | common/herrors/file_error.go | 53 | ||||
-rw-r--r-- | common/herrors/file_error_test.go | 8 | ||||
-rw-r--r-- | common/text/position.go | 99 | ||||
-rw-r--r-- | common/text/position_test.go | 33 | ||||
-rw-r--r-- | common/urls/ref.go | 22 |
7 files changed, 236 insertions, 151 deletions
diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go index 5d3f079be..88cb06c8c 100644 --- a/common/herrors/error_locator.go +++ b/common/herrors/error_locator.go @@ -15,72 +15,21 @@ package herrors import ( - "fmt" "io" "io/ioutil" - "os" "strings" - "github.com/gohugoio/hugo/common/terminal" + "github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/helpers" "github.com/spf13/afero" ) -var fileErrorFormatFunc func(e ErrorContext) string - -func createFileLogFormatter(formatStr string) func(e ErrorContext) string { - - if formatStr == "" { - formatStr = "\":file::line::col\"" - } - - var identifiers = []string{":file", ":line", ":col"} - var identifiersFound []string - - for i := range formatStr { - for _, id := range identifiers { - if strings.HasPrefix(formatStr[i:], id) { - identifiersFound = append(identifiersFound, id) - } - } - } - - replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d") - format := replacer.Replace(formatStr) - - f := func(e ErrorContext) string { - args := make([]interface{}, len(identifiersFound)) - for i, id := range identifiersFound { - switch id { - case ":file": - args[i] = e.Filename - case ":line": - args[i] = e.LineNumber - case ":col": - args[i] = e.ColumnNumber - } - } - - msg := fmt.Sprintf(format, args...) - - if terminal.IsTerminal(os.Stdout) { - return terminal.Notice(msg) - } - - return msg - } - - return f -} - -func init() { - fileErrorFormatFunc = createFileLogFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT")) -} - // LineMatcher contains the elements used to match an error to a line type LineMatcher struct { - FileError FileError + Position text.Position + Error error + LineNumber int Offset int Line string @@ -91,33 +40,34 @@ type LineMatcherFn func(m LineMatcher) bool // SimpleLineMatcher simply matches by line number. var SimpleLineMatcher = func(m LineMatcher) bool { - return m.FileError.LineNumber() == m.LineNumber + return m.Position.LineNumber == m.LineNumber } +var _ text.Positioner = ErrorContext{} + // ErrorContext contains contextual information about an error. This will // typically be the lines surrounding some problem in a file. type ErrorContext struct { - // The source filename. - Filename string // If a match will contain the matched line and up to 2 lines before and after. // Will be empty if no match. Lines []string // The position of the error in the Lines above. 0 based. - Pos int - - // The linenumber in the source file from where the Lines start. Starting at 1. - LineNumber int + LinesPos int - // The column number in the source file. Starting at 1. - ColumnNumber int + position text.Position // The lexer to use for syntax highlighting. // https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages ChromaLexer string } +// Position returns the text position of this error. +func (e ErrorContext) Position() text.Position { + return e.position +} + var _ causer = (*ErrorWithFileContext)(nil) // ErrorWithFileContext is an error with some additional file context related @@ -128,7 +78,11 @@ type ErrorWithFileContext struct { } func (e *ErrorWithFileContext) Error() string { - return fileErrorFormatFunc(e.ErrorContext) + ": " + e.cause.Error() + pos := e.Position() + if pos.IsValid() { + return pos.String() + ": " + e.cause.Error() + } + return e.cause.Error() } func (e *ErrorWithFileContext) Cause() error { @@ -163,24 +117,27 @@ func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatc var errCtx ErrorContext - if le.Offset() != -1 { + posle := le.Position() + + if posle.Offset != -1 { errCtx = locateError(r, le, func(m LineMatcher) bool { - if le.Offset() >= m.Offset && le.Offset() < m.Offset+len(m.Line) { - fe := m.FileError - m.FileError = ToFileErrorWithOffset(fe, -fe.LineNumber()+m.LineNumber) + if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) { + lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber + m.Position = text.Position{LineNumber: lno} } return matcher(m) }) - } else { errCtx = locateError(r, le, matcher) } - if errCtx.LineNumber == -1 { + pos := &errCtx.position + + if pos.LineNumber == -1 { return e, false } - errCtx.Filename = realFilename + pos.Filename = realFilename if le.Type() != "" { errCtx.ChromaLexer = chromaLexerFromType(le.Type()) @@ -233,17 +190,20 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) ErrorContext panic("must provide an error") } - errCtx := ErrorContext{LineNumber: -1, ColumnNumber: 1, Pos: -1} + errCtx := ErrorContext{position: text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1}, LinesPos: -1} b, err := ioutil.ReadAll(r) if err != nil { return errCtx } + pos := &errCtx.position + lepos := le.Position() + lines := strings.Split(string(b), "\n") - if le != nil && le.ColumnNumber() >= 0 { - errCtx.ColumnNumber = le.ColumnNumber() + if le != nil && lepos.ColumnNumber >= 0 { + pos.ColumnNumber = lepos.ColumnNumber } lineNo := 0 @@ -252,32 +212,33 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) ErrorContext for li, line := range lines { lineNo = li + 1 m := LineMatcher{ - FileError: le, + Position: le.Position(), + Error: le, LineNumber: lineNo, Offset: posBytes, Line: line, } - if errCtx.Pos == -1 && matches(m) { - errCtx.LineNumber = lineNo + if errCtx.LinesPos == -1 && matches(m) { + pos.LineNumber = lineNo break } posBytes += len(line) } - if errCtx.LineNumber != -1 { - low := errCtx.LineNumber - 3 + if pos.LineNumber != -1 { + low := pos.LineNumber - 3 if low < 0 { low = 0 } - if errCtx.LineNumber > 2 { - errCtx.Pos = 2 + if pos.LineNumber > 2 { + errCtx.LinesPos = 2 } else { - errCtx.Pos = errCtx.LineNumber - 1 + errCtx.LinesPos = pos.LineNumber - 1 } - high := errCtx.LineNumber + 2 + high := pos.LineNumber + 2 if high > len(lines) { high = len(lines) } diff --git a/common/herrors/error_locator_test.go b/common/herrors/error_locator_test.go index 84c0faf89..2d007016d 100644 --- a/common/herrors/error_locator_test.go +++ b/common/herrors/error_locator_test.go @@ -21,18 +21,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateFileLogFormatter(t *testing.T) { - assert := require.New(t) - - ctx := ErrorContext{Filename: "/my/file.txt", LineNumber: 12, ColumnNumber: 13} - - assert.Equal("/my/file.txt|13|12", createFileLogFormatter(":file|:col|:line")(ctx)) - assert.Equal("13|/my/file.txt|12", createFileLogFormatter(":col|:file|:line")(ctx)) - assert.Equal("好:13", createFileLogFormatter("好::col")(ctx)) - assert.Equal("\"/my/file.txt:12:13\"", createFileLogFormatter("")(ctx)) - -} - func TestErrorLocator(t *testing.T) { assert := require.New(t) @@ -53,8 +41,9 @@ LINE 8 location := locateErrorInString(lines, lineMatcher) assert.Equal([]string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}, location.Lines) - assert.Equal(5, location.LineNumber) - assert.Equal(2, location.Pos) + pos := location.Position() + assert.Equal(5, pos.LineNumber) + assert.Equal(2, location.LinesPos) assert.Equal([]string{"This is THEONE"}, locateErrorInString(`This is THEONE`, lineMatcher).Lines) @@ -62,32 +51,32 @@ LINE 8 This is THEONE L2 `, lineMatcher) - assert.Equal(2, location.LineNumber) - assert.Equal(1, location.Pos) + assert.Equal(2, location.Position().LineNumber) + assert.Equal(1, location.LinesPos) assert.Equal([]string{"L1", "This is THEONE", "L2", ""}, location.Lines) location = locateErrorInString(`This is THEONE L2 `, lineMatcher) - assert.Equal(0, location.Pos) + assert.Equal(0, location.LinesPos) assert.Equal([]string{"This is THEONE", "L2", ""}, location.Lines) location = locateErrorInString(`L1 This THEONE `, lineMatcher) assert.Equal([]string{"L1", "This THEONE", ""}, location.Lines) - assert.Equal(1, location.Pos) + assert.Equal(1, location.LinesPos) location = locateErrorInString(`L1 L2 This THEONE `, lineMatcher) assert.Equal([]string{"L1", "L2", "This THEONE", ""}, location.Lines) - assert.Equal(2, location.Pos) + assert.Equal(2, location.LinesPos) location = locateErrorInString("NO MATCH", lineMatcher) - assert.Equal(-1, location.LineNumber) - assert.Equal(-1, location.Pos) + assert.Equal(-1, location.Position().LineNumber) + assert.Equal(-1, location.LinesPos) assert.Equal(0, len(location.Lines)) lineMatcher = func(m LineMatcher) bool { @@ -106,8 +95,8 @@ I J`, lineMatcher) assert.Equal([]string{"D", "E", "F", "G", "H"}, location.Lines) - assert.Equal(6, location.LineNumber) - assert.Equal(2, location.Pos) + assert.Equal(6, location.Position().LineNumber) + assert.Equal(2, location.LinesPos) // Test match EOF lineMatcher = func(m LineMatcher) bool { @@ -120,8 +109,8 @@ C `, lineMatcher) assert.Equal([]string{"B", "C", ""}, location.Lines) - assert.Equal(4, location.LineNumber) - assert.Equal(2, location.Pos) + assert.Equal(4, location.Position().LineNumber) + assert.Equal(2, location.LinesPos) offsetMatcher := func(m LineMatcher) bool { return m.Offset == 1 @@ -134,7 +123,7 @@ D E`, offsetMatcher) assert.Equal([]string{"A", "B", "C", "D"}, location.Lines) - assert.Equal(2, location.LineNumber) - assert.Equal(1, location.Pos) + assert.Equal(2, location.Position().LineNumber) + assert.Equal(1, location.LinesPos) } diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go index 49b9f808a..929cc800f 100644 --- a/common/herrors/file_error.go +++ b/common/herrors/file_error.go @@ -16,25 +16,21 @@ package herrors import ( "encoding/json" + "github.com/gohugoio/hugo/common/text" + "github.com/pkg/errors" ) -var _ causer = (*fileError)(nil) +var ( + _ causer = (*fileError)(nil) +) // FileError represents an error when handling a file: Parsing a config file, // execute a template etc. type FileError interface { error - // Offset gets the error location offset in bytes, starting at 0. - // It will return -1 if not provided. - Offset() int - - // LineNumber gets the error location, starting at line 1. - LineNumber() int - - // Column number gets the column location, starting at 1. - ColumnNumber() int + text.Positioner // A string identifying the type of file, e.g. JSON, TOML, markdown etc. Type() string @@ -43,33 +39,16 @@ type FileError interface { var _ FileError = (*fileError)(nil) type fileError struct { - offset int - lineNumber int - columnNumber int - fileType string - - cause error -} + position text.Position -type fileErrorWithLineOffset struct { - FileError - offset int -} - -func (e *fileErrorWithLineOffset) LineNumber() int { - return e.FileError.LineNumber() + e.offset -} + fileType string -func (e *fileError) LineNumber() int { - return e.lineNumber -} - -func (e *fileError) Offset() int { - return e.offset + cause error } -func (e *fileError) ColumnNumber() int { - return e.columnNumber +// Position returns the text position of this error. +func (e fileError) Position() text.Position { + return e.position } func (e *fileError) Type() string { @@ -89,7 +68,8 @@ func (f *fileError) Cause() error { // NewFileError creates a new FileError. func NewFileError(fileType string, offset, lineNumber, columnNumber int, err error) FileError { - return &fileError{cause: err, fileType: fileType, offset: offset, lineNumber: lineNumber, columnNumber: columnNumber} + pos := text.Position{Offset: offset, LineNumber: lineNumber, ColumnNumber: columnNumber} + return &fileError{cause: err, fileType: fileType, position: pos} } // UnwrapFileError tries to unwrap a FileError from err. @@ -111,7 +91,9 @@ func UnwrapFileError(err error) FileError { // ToFileErrorWithOffset will return a new FileError with a line number // with the given offset from the original. func ToFileErrorWithOffset(fe FileError, offset int) FileError { - return &fileErrorWithLineOffset{FileError: fe, offset: offset} + pos := fe.Position() + pos.LineNumber = pos.LineNumber + offset + return &fileError{cause: fe, fileType: fe.Type(), position: pos} } // ToFileError will convert the given error to an error supporting @@ -123,6 +105,7 @@ func ToFileError(fileType string, err error) FileError { if fileType == "" { fileType = typ } + if lno > 0 || offset != -1 { return NewFileError(fileType, offset, lno, col, err) } diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go index 6acb49603..4108983d3 100644 --- a/common/herrors/file_error_test.go +++ b/common/herrors/file_error_test.go @@ -42,17 +42,15 @@ func TestToLineNumberError(t *testing.T) { } { got := ToFileError("template", test.in) - if test.offset > 0 { - got = ToFileErrorWithOffset(got.(FileError), test.offset) - } errMsg := fmt.Sprintf("[%d][%T]", i, got) le, ok := got.(FileError) assert.True(ok) assert.True(ok, errMsg) - assert.Equal(test.lineNumber, le.LineNumber(), errMsg) - assert.Equal(test.columnNumber, le.ColumnNumber(), errMsg) + pos := le.Position() + assert.Equal(test.lineNumber, pos.LineNumber, errMsg) + assert.Equal(test.columnNumber, pos.ColumnNumber, errMsg) assert.Error(errors.Cause(got)) } diff --git a/common/text/position.go b/common/text/position.go new file mode 100644 index 000000000..0c43c5ae7 --- /dev/null +++ b/common/text/position.go @@ -0,0 +1,99 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package text + +import ( + "fmt" + "os" + "strings" + + "github.com/gohugoio/hugo/common/terminal" +) + +// Positioner represents a thing that knows its position in a text file or stream, +// typically an error. +type Positioner interface { + Position() Position +} + +// Position holds a source position in a text file or stream. +type Position struct { + Filename string // filename, if any + Offset int // byte offset, starting at 0. It's set to -1 if not provided. + LineNumber int // line number, starting at 1 + ColumnNumber int // column number, starting at 1 (character count per line) +} + +func (pos Position) String() string { + if pos.Filename == "" { + pos.Filename = "<stream>" + } + return positionStringFormatfunc(pos) +} + +// IsValid returns true if line number is > 0. +func (pos Position) IsValid() bool { + return pos.LineNumber > 0 +} + +var positionStringFormatfunc func(p Position) string + +func createPositionStringFormatter(formatStr string) func(p Position) string { + + if formatStr == "" { + formatStr = "\":file::line::col\"" + } + + var identifiers = []string{":file", ":line", ":col"} + var identifiersFound []string + + for i := range formatStr { + for _, id := range identifiers { + if strings.HasPrefix(formatStr[i:], id) { + identifiersFound = append(identifiersFound, id) + } + } + } + + replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d") + format := replacer.Replace(formatStr) + + f := func(pos Position) string { + args := make([]interface{}, len(identifiersFound)) + for i, id := range identifiersFound { + switch id { + case ":file": + args[i] = pos.Filename + case ":line": + args[i] = pos.LineNumber + case ":col": + args[i] = pos.ColumnNumber + } + } + + msg := fmt.Sprintf(format, args...) + + if terminal.IsTerminal(os.Stdout) { + return terminal.Notice(msg) + } + + return msg + } + + return f +} + +func init() { + positionStringFormatfunc = createPositionStringFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT")) +} diff --git a/common/text/position_test.go b/common/text/position_test.go new file mode 100644 index 000000000..a25a3edbd --- /dev/null +++ b/common/text/position_test.go @@ -0,0 +1,33 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package text + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPositionStringFormatter(t *testing.T) { + assert := require.New(t) + + pos := Position{Filename: "/my/file.txt", LineNumber: 12, ColumnNumber: 13, Offset: 14} + + assert.Equal("/my/file.txt|13|12", createPositionStringFormatter(":file|:col|:line")(pos)) + assert.Equal("13|/my/file.txt|12", createPositionStringFormatter(":col|:file|:line")(pos)) + assert.Equal("好:13", createPositionStringFormatter("好::col")(pos)) + assert.Equal("\"/my/file.txt:12:13\"", createPositionStringFormatter("")(pos)) + assert.Equal("\"/my/file.txt:12:13\"", pos.String()) + +} diff --git a/common/urls/ref.go b/common/urls/ref.go new file mode 100644 index 000000000..71b00b71d --- /dev/null +++ b/common/urls/ref.go @@ -0,0 +1,22 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package urls + +// RefLinker is implemented by those who support reference linking. +// args must contain a path, but can also point to the target +// language or output format. +type RefLinker interface { + Ref(args map[string]interface{}) (string, error) + RelRef(args map[string]interface{}) (string, error) +} |