Skip to content

Commit

Permalink
Support typed bool, int and float in shortcode params
Browse files Browse the repository at this point in the history
This means that you now can do:

    {{< vidur 9KvBeKu false true 32 3.14 >}}

And the boolean and numeric values will be converted to `bool`, `int` and `float64`.

If you want these to be  strings, they must be quoted:

    {{< vidur 9KvBeKu "false" "true" "32" "3.14" >}}

Fixes #6371
  • Loading branch information
bep committed Sep 29, 2019
1 parent e073f4e commit 3ec5904
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 51 deletions.
33 changes: 13 additions & 20 deletions hugolib/shortcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,7 @@ func (scp *ShortcodeWithPage) Get(key interface{}) interface{} {
}
}

switch x.Kind() {
case reflect.String:
return x.String()
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
return x.Int()
default:
return x
}
return x.Interface()

}

Expand Down Expand Up @@ -219,17 +212,17 @@ func (sc shortcode) String() string {
// for testing (mostly), so any change here will break tests!
var params interface{}
switch v := sc.params.(type) {
case map[string]string:
case map[string]interface{}:
// sort the keys so test assertions won't fail
var keys []string
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
var tmp = make([]string, len(keys))
var tmp = make(map[string]interface{})

for i, k := range keys {
tmp[i] = k + ":" + v[k]
for _, k := range keys {
tmp[k] = v[k]
}
params = tmp

Expand Down Expand Up @@ -539,12 +532,12 @@ Loop:
} else if pt.Peek().IsShortcodeParamVal() {
// named params
if sc.params == nil {
params := make(map[string]string)
params[currItem.ValStr()] = pt.Next().ValStr()
params := make(map[string]interface{})
params[currItem.ValStr()] = pt.Next().ValTyped()
sc.params = params
} else {
if params, ok := sc.params.(map[string]string); ok {
params[currItem.ValStr()] = pt.Next().ValStr()
if params, ok := sc.params.(map[string]interface{}); ok {
params[currItem.ValStr()] = pt.Next().ValTyped()
} else {
return sc, errShortCodeIllegalState
}
Expand All @@ -553,12 +546,12 @@ Loop:
} else {
// positional params
if sc.params == nil {
var params []string
params = append(params, currItem.ValStr())
var params []interface{}
params = append(params, currItem.ValTyped())
sc.params = params
} else {
if params, ok := sc.params.([]string); ok {
params = append(params, currItem.ValStr())
if params, ok := sc.params.([]interface{}); ok {
params = append(params, currItem.ValTyped())
sc.params = params
} else {
return sc, errShortCodeIllegalState
Expand Down
39 changes: 38 additions & 1 deletion hugolib/shortcode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ import (
)

func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) {
t.Helper()
CheckShortCodeMatchAndError(t, input, expected, withTemplate, false)
}

func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {

t.Helper()
cfg, fs := newTestCfg()
c := qt.New(t)

Expand Down Expand Up @@ -1158,3 +1159,39 @@ title: "Hugo Rocks!"
"test/hello: test/hello",
)
}

func TestShortcodeTypedParams(t *testing.T) {
t.Parallel()
c := qt.New(t)

builder := newTestSitesBuilder(t).WithSimpleConfigFile()

builder.WithContent("page.md", `---
title: "Hugo Rocks!"
---
# doc
types positional: {{< hello true false 33 3.14 >}}
types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}}
types string: {{< hello "true" trues "33" "3.14" >}}
`).WithTemplatesAdded(
"layouts/shortcodes/hello.html",
`{{ range $i, $v := .Params }}
- {{ printf "%v: %v (%T)" $i $v $v }}
{{ end }}
{{ $b1 := .Get "b1" }}
Get: {{ printf "%v (%T)" $b1 $b1 | safeHTML }}
`).Build(BuildCfg{})

s := builder.H.Sites[0]
c.Assert(len(s.RegularPages()), qt.Equals, 1)

builder.AssertFileContent("public/page/index.html",
"types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)",
"types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int) Get: true (bool) ",
"types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ",
)
}
45 changes: 42 additions & 3 deletions parser/pageparser/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ package pageparser
import (
"bytes"
"fmt"
"regexp"
"strconv"
)

type Item struct {
Type ItemType
Pos int
Val []byte
Type ItemType
Pos int
Val []byte
isString bool
}

type Items []Item
Expand All @@ -30,6 +33,36 @@ func (i Item) ValStr() string {
return string(i.Val)
}

func (i Item) ValTyped() interface{} {
str := i.ValStr()
if i.isString {
// A quoted value that is a string even if it looks like a number etc.
return str
}

if boolRe.MatchString(str) {
return str == "true"
}

if intRe.MatchString(str) {
num, err := strconv.Atoi(str)
if err != nil {
return str
}
return num
}

if floatRe.MatchString(str) {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
return str
}
return num
}

return str
}

func (i Item) IsText() bool {
return i.Type == tText
}
Expand Down Expand Up @@ -132,3 +165,9 @@ const (
// preserved for later - keywords come after this
tKeywordMarker
)

var (
boolRe = regexp.MustCompile(`^(true$)|(false$)`)
intRe = regexp.MustCompile(`^[-+]?\d+$`)
floatRe = regexp.MustCompile(`^[-+]?\d*\.\d+$`)
)
35 changes: 35 additions & 0 deletions parser/pageparser/item_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2019 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 pageparser

import (
"testing"

qt "github.com/frankban/quicktest"
)

func TestItemValTyped(t *testing.T) {
c := qt.New(t)

c.Assert(Item{Val: []byte("3.14")}.ValTyped(), qt.Equals, float64(3.14))
c.Assert(Item{Val: []byte(".14")}.ValTyped(), qt.Equals, float64(.14))
c.Assert(Item{Val: []byte("314")}.ValTyped(), qt.Equals, 314)
c.Assert(Item{Val: []byte("314x")}.ValTyped(), qt.Equals, "314x")
c.Assert(Item{Val: []byte("314 ")}.ValTyped(), qt.Equals, "314 ")
c.Assert(Item{Val: []byte("314"), isString: true}.ValTyped(), qt.Equals, "314")
c.Assert(Item{Val: []byte("true")}.ValTyped(), qt.Equals, true)
c.Assert(Item{Val: []byte("false")}.ValTyped(), qt.Equals, false)
c.Assert(Item{Val: []byte("trues")}.ValTyped(), qt.Equals, "trues")

}
24 changes: 20 additions & 4 deletions parser/pageparser/pagelexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,13 @@ func (l *pageLexer) backup() {

// sends an item back to the client.
func (l *pageLexer) emit(t ItemType) {
l.items = append(l.items, Item{t, l.start, l.input[l.start:l.pos]})
l.items = append(l.items, Item{t, l.start, l.input[l.start:l.pos], false})
l.start = l.pos
}

// sends a string item back to the client.
func (l *pageLexer) emitString(t ItemType) {
l.items = append(l.items, Item{t, l.start, l.input[l.start:l.pos], true})
l.start = l.pos
}

Expand All @@ -151,14 +157,14 @@ func (l *pageLexer) isEOF() bool {
}

// special case, do not send '\\' back to client
func (l *pageLexer) ignoreEscapesAndEmit(t ItemType) {
func (l *pageLexer) ignoreEscapesAndEmit(t ItemType, isString bool) {
val := bytes.Map(func(r rune) rune {
if r == '\\' {
return -1
}
return r
}, l.input[l.start:l.pos])
l.items = append(l.items, Item{t, l.start, val})
l.items = append(l.items, Item{t, l.start, val, isString})
l.start = l.pos
}

Expand All @@ -176,7 +182,7 @@ var lf = []byte("\n")

// nil terminates the parser
func (l *pageLexer) errorf(format string, args ...interface{}) stateFunc {
l.items = append(l.items, Item{tError, l.start, []byte(fmt.Sprintf(format, args...))})
l.items = append(l.items, Item{tError, l.start, []byte(fmt.Sprintf(format, args...)), true})
return nil
}

Expand All @@ -201,6 +207,16 @@ func (l *pageLexer) consumeToNextLine() {
}
}

func (l *pageLexer) consumeToSpace() {
for {
r := l.next()
if r == eof || unicode.IsSpace(r) {
l.backup()
return
}
}
}

func (l *pageLexer) consumeSpace() {
for {
r := l.next()
Expand Down
19 changes: 15 additions & 4 deletions parser/pageparser/pagelexer_shortcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func lexShortcodeParam(l *pageLexer, escapedQuoteStart bool) stateFunc {
break
}

if !isAlphaNumericOrHyphen(r) {
if !isAlphaNumericOrHyphen(r) && r != '.' { // Floats have period
l.backup()
break
}
Expand All @@ -137,6 +137,12 @@ func lexShortcodeParam(l *pageLexer, escapedQuoteStart bool) stateFunc {

}

func lexShortcodeParamVal(l *pageLexer) stateFunc {
l.consumeToSpace()
l.emit(tScParamVal)
return lexInsideShortcode
}

func lexShortcodeQuotedParamVal(l *pageLexer, escapedQuotedValuesAllowed bool, typ ItemType) stateFunc {
openQuoteFound := false
escapedInnerQuoteFound := false
Expand Down Expand Up @@ -176,9 +182,9 @@ Loop:
}

if escapedInnerQuoteFound {
l.ignoreEscapesAndEmit(typ)
l.ignoreEscapesAndEmit(typ, true)
} else {
l.emit(typ)
l.emitString(typ)
}

r := l.next()
Expand Down Expand Up @@ -273,8 +279,13 @@ func lexInsideShortcode(l *pageLexer) stateFunc {
case isSpace(r), isEndOfLine(r):
l.ignore()
case r == '=':
l.consumeSpace()
l.ignore()
return lexShortcodeQuotedParamVal(l, l.peek() != '\\', tScParamVal)
peek := l.peek()
if peek == '"' || peek == '\\' {
return lexShortcodeQuotedParamVal(l, peek != '\\', tScParamVal)
}
return lexShortcodeParamVal
case r == '/':
if l.currShortcodeName == "" {
return l.errorf("got closing shortcode, but none is open")
Expand Down
2 changes: 1 addition & 1 deletion parser/pageparser/pageparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (t *Iterator) Input() []byte {
return t.l.Input()
}

var errIndexOutOfBounds = Item{tError, 0, []byte("no more tokens")}
var errIndexOutOfBounds = Item{tError, 0, []byte("no more tokens"), true}

// Current will repeatably return the current item.
func (t *Iterator) Current() Item {
Expand Down
3 changes: 2 additions & 1 deletion parser/pageparser/pageparser_intro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type lexerTest struct {
}

func nti(tp ItemType, val string) Item {
return Item{tp, 0, []byte(val)}
return Item{tp, 0, []byte(val), false}
}

var (
Expand Down Expand Up @@ -119,6 +119,7 @@ func equal(i1, i2 []Item) bool {
if i1[k].Type != i2[k].Type {
return false
}

if !reflect.DeepEqual(i1[k].Val, i2[k].Val) {
return false
}
Expand Down
Loading

0 comments on commit 3ec5904

Please sign in to comment.