hugolib: Add .Position to shortcode
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 1 Nov 2018 09:39:44 +0000 (10:39 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 1 Nov 2018 20:06:35 +0000 (21:06 +0100)
To allow for better error logging in shortcodes. Note that this may be expensive to calculate, so this is primarily for error situations.

See #5371

hugolib/page_content.go
hugolib/shortcode.go
hugolib/shortcode_test.go
source/position.go [new file with mode: 0644]

index 5a8258279fb0d37da57bad276263f8d6dfc0a942..b3e8668ef588676a0fcca1ac77030db8fd689321 100644 (file)
@@ -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)
 }
index 024a919ed45970c0398ba98fba684900c135ba67..41d8d76c44cc041dbea882f6552c332497c37e10 100644 (file)
@@ -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:
index 0d397f9eeee6d4b1b8444ea9d3d31b4d1a82ae05..2ddecc2ffe8f4448a2fd7ed0713de42e9962f17d 100644 (file)
@@ -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 (file)
index 0000000..8c1ea3d
--- /dev/null
@@ -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 = "<stream>"
+       }
+       return fmt.Sprintf("%s:%d:%d", filename, pos.LineNumber, pos.ColumnNumber)
+
+}