diff --git a/hugolib/page_content.go b/hugolib/page_content.go index 5a8258279fb..b3e8668ef58 100644 --- a/hugolib/page_content.go +++ b/hugolib/page_content.go @@ -17,6 +17,8 @@ import ( "bytes" "io" + "github.com/gohugoio/hugo/source" + errors "github.com/pkg/errors" bp "github.com/gohugoio/hugo/bufferpool" @@ -68,7 +70,7 @@ func (p *Page) mapContent() error { iter := p.source.parsed.Iterator() fail := func(err error, i pageparser.Item) error { - return parseError(err, iter.Input(), i.Pos) + return p.parseError(err, iter.Input(), i.Pos) } // the parser is guaranteed to return items in proper order or fail, so … @@ -194,15 +196,30 @@ func (p *Page) parse(reader io.Reader) error { return nil } -func parseError(err error, input []byte, pos int) error { +func (p *Page) parseError(err error, input []byte, offset int) error { if herrors.UnwrapFileError(err) != nil { // Use the most specific location. return err } + pos := p.posFromInput(input, offset) + return herrors.NewFileError("md", -1, pos.LineNumber, pos.ColumnNumber, err) + +} + +func (p *Page) posFromInput(input []byte, offset int) source.Position { lf := []byte("\n") - input = input[:pos] + input = input[:offset] lineNumber := bytes.Count(input, lf) + 1 endOfLastLine := bytes.LastIndex(input, lf) - return herrors.NewFileError("md", -1, lineNumber, pos-endOfLastLine, err) + return source.Position{ + Filename: p.pathOrTitle(), + LineNumber: lineNumber, + ColumnNumber: offset - endOfLastLine, + Offset: offset, + } +} + +func (p *Page) posFromPage(offset int) source.Position { + return p.posFromInput(p.source.parsed.Input(), offset) } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 024a919ed45..41d8d76c44c 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -19,6 +19,8 @@ import ( "fmt" "html/template" + "github.com/gohugoio/hugo/source" + "reflect" "regexp" @@ -53,9 +55,23 @@ type ShortcodeWithPage struct { // this ordinal will represent the position of this shortcode in the page content. Ordinal int + // pos is the position in bytes in the source file. Used for error logging. + posInit sync.Once + posOffset int + pos source.Position + scratch *maps.Scratch } +// Position returns this shortcode's detailed position. Note that this information +// may be expensive to calculate, so only use this in error situations. +func (scp *ShortcodeWithPage) Position() source.Position { + scp.posInit.Do(func() { + scp.pos = scp.Page.posFromPage(scp.posOffset) + }) + return scp.pos +} + // Site returns information about the current site. func (scp *ShortcodeWithPage) Site() *SiteInfo { return scp.Page.Site @@ -313,7 +329,7 @@ func renderShortcode( return "", nil } - data := &ShortcodeWithPage{Ordinal: sc.ordinal, Params: sc.params, Page: p, Parent: parent} + data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, Params: sc.params, Page: p, Parent: parent} if sc.params != nil { data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map } @@ -463,7 +479,7 @@ func (s *shortcodeHandler) executeShortcodesForDelta(p *PageWithoutContent) erro if err != nil { sc := s.shortcodes.getShortcode(k.(scKey).ShortcodePlaceholder) if sc != nil { - err = p.errWithFileContext(parseError(_errors.Wrapf(err, "failed to render shortcode %q", sc.name), p.source.parsed.Input(), sc.pos)) + err = p.errWithFileContext(p.parseError(_errors.Wrapf(err, "failed to render shortcode %q", sc.name), p.source.parsed.Input(), sc.pos)) } p.s.SendError(err) @@ -505,7 +521,7 @@ func (s *shortcodeHandler) extractShortcode(ordinal int, pt *pageparser.Iterator var nestedOrdinal = 0 fail := func(err error, i pageparser.Item) error { - return parseError(err, pt.Input(), i.Pos) + return p.parseError(err, pt.Input(), i.Pos) } Loop: diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 0d397f9eeee..2ddecc2ffe8 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -1026,3 +1026,39 @@ ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2 ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`) } + +func TestShortcodePosition(t *testing.T) { + t.Parallel() + assert := require.New(t) + + builder := newTestSitesBuilder(t).WithSimpleConfigFile() + + builder.WithContent("page.md", `--- +title: "Hugo Rocks!" +--- + +# doc + + {{< s1 >}} + +`).WithTemplatesAdded("layouts/shortcodes/s1.html", ` +{{ with .Position }} +File: {{ .Filename }} +Offset: {{ .Offset }} +Line: {{ .LineNumber }} +Column: {{ .ColumnNumber }} +String: {{ . | safeHTML }} +{{ end }} + +`).CreateSites().Build(BuildCfg{}) + + s := builder.H.Sites[0] + assert.Equal(1, len(s.RegularPages)) + + builder.AssertFileContent("public/page/index.html", + "File: content/page.md", + "Line: 7", "Column: 4", "Offset: 40", + "String: content/page.md:7:4", + ) + +} diff --git a/source/position.go b/source/position.go new file mode 100644 index 00000000000..8c1ea3d2bf2 --- /dev/null +++ b/source/position.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 source + +import "fmt" + +// Position holds a source position. +type Position struct { + Filename string // filename, if any + Offset int // byte offset, starting at 0 + LineNumber int // line number, starting at 1 + ColumnNumber int // column number, starting at 1 (character count per line) +} + +func (pos Position) String() string { + filename := pos.Filename + if filename == "" { + filename = "" + } + return fmt.Sprintf("%s:%d:%d", filename, pos.LineNumber, pos.ColumnNumber) + +}