Skip to content

Commit

Permalink
hugolib: Add .Position to shortcode
Browse files Browse the repository at this point in the history
To allow for better error logging in shortcodes. Note that this may be expensive to calculate, so this is primarily for error situations.

See gohugoio#5371
  • Loading branch information
bep committed Nov 1, 2018
1 parent 0293347 commit 234e73a
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 7 deletions.
25 changes: 21 additions & 4 deletions hugolib/page_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"bytes"
"io"

"github.com/gohugoio/hugo/source"

errors "github.com/pkg/errors"

bp "github.com/gohugoio/hugo/bufferpool"
Expand Down Expand Up @@ -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 …
Expand Down Expand Up @@ -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)
}
22 changes: 19 additions & 3 deletions hugolib/shortcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"fmt"
"html/template"

"github.com/gohugoio/hugo/source"

"reflect"

"regexp"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions hugolib/shortcode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

}
33 changes: 33 additions & 0 deletions source/position.go
Original file line number Diff line number Diff line change
@@ -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)

}

0 comments on commit 234e73a

Please sign in to comment.