Support typed bool, int and float in shortcode params
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 29 Sep 2019 12:51:51 +0000 (14:51 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 29 Sep 2019 21:22:41 +0000 (23:22 +0200)
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

12 files changed:
hugolib/shortcode.go
hugolib/shortcode_test.go
parser/pageparser/item.go
parser/pageparser/item_test.go [new file with mode: 0644]
parser/pageparser/pagelexer.go
parser/pageparser/pagelexer_shortcode.go
parser/pageparser/pageparser.go
parser/pageparser/pageparser_intro_test.go
parser/pageparser/pageparser_shortcode_test.go
tpl/tplimpl/embedded/templates.autogen.go
tpl/tplimpl/embedded/templates/shortcodes/twitter.html
tpl/urls/urls.go

index 8323962c0c7b29980e228fcff7b95e9ef6bcdede..d0cdf39505824bb91cd63579104c02f6333b83cc 100644 (file)
@@ -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()
 
 }
 
@@ -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
 
@@ -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
                                        }
@@ -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
index 13cbd1fd870d9b258318a7dc3ad2b82fca1cd5ee..36f0042534a371e57111cb940fad6d8ff6af6f91 100644 (file)
@@ -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)
 
@@ -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) ",
+       )
+}
index 3877ee6d911a2643c1382dfa455d938e74a0559d..48003ee860e5069076f51b9874957ecbd62e539a 100644 (file)
@@ -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
@@ -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
 }
@@ -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+$`)
+)
diff --git a/parser/pageparser/item_test.go b/parser/pageparser/item_test.go
new file mode 100644 (file)
index 0000000..a30860f
--- /dev/null
@@ -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")
+
+}
index 2da8ebdc3bfa2978521c93b032ac3d45bf998336..f994286d994249a1f5de270affc8f952a1015e71 100644 (file)
@@ -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
 }
 
@@ -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
 }
 
@@ -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
 }
 
@@ -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()
index d503d1797ee8050cfe0b98975098287575ad4298..dea1b317e1b3feca8f8e6e907fabb49d8872274f 100644 (file)
@@ -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
                }
@@ -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
@@ -176,9 +182,9 @@ Loop:
        }
 
        if escapedInnerQuoteFound {
-               l.ignoreEscapesAndEmit(typ)
+               l.ignoreEscapesAndEmit(typ, true)
        } else {
-               l.emit(typ)
+               l.emitString(typ)
        }
 
        r := l.next()
@@ -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")
index db563d44c7f973686e25dfebd424e244a3bef9e6..acdb095873660371d9c1a5dbd2da67cde318b555 100644 (file)
@@ -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 {
index 3e5bac8729fa16396592c2b00e7c46ba2d9c7fa5..0f20ae5a12c312c8949ad0d245663c2eeb367326 100644 (file)
@@ -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 (
@@ -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
                }
index 75ee560906864e9d1221b3fba5d2c645683468e0..4ce4bae313ee8ca800cbff9e41fb5d722edb1250 100644 (file)
@@ -16,22 +16,26 @@ package pageparser
 import "testing"
 
 var (
-       tstEOF       = nti(tEOF, "")
-       tstLeftNoMD  = nti(tLeftDelimScNoMarkup, "{{<")
-       tstRightNoMD = nti(tRightDelimScNoMarkup, ">}}")
-       tstLeftMD    = nti(tLeftDelimScWithMarkup, "{{%")
-       tstRightMD   = nti(tRightDelimScWithMarkup, "%}}")
-       tstSCClose   = nti(tScClose, "/")
-       tstSC1       = nti(tScName, "sc1")
-       tstSC1Inline = nti(tScNameInline, "sc1.inline")
-       tstSC2Inline = nti(tScNameInline, "sc2.inline")
-       tstSC2       = nti(tScName, "sc2")
-       tstSC3       = nti(tScName, "sc3")
-       tstSCSlash   = nti(tScName, "sc/sub")
-       tstParam1    = nti(tScParam, "param1")
-       tstParam2    = nti(tScParam, "param2")
-       tstVal       = nti(tScParamVal, "Hello World")
-       tstText      = nti(tText, "Hello World")
+       tstEOF            = nti(tEOF, "")
+       tstLeftNoMD       = nti(tLeftDelimScNoMarkup, "{{<")
+       tstRightNoMD      = nti(tRightDelimScNoMarkup, ">}}")
+       tstLeftMD         = nti(tLeftDelimScWithMarkup, "{{%")
+       tstRightMD        = nti(tRightDelimScWithMarkup, "%}}")
+       tstSCClose        = nti(tScClose, "/")
+       tstSC1            = nti(tScName, "sc1")
+       tstSC1Inline      = nti(tScNameInline, "sc1.inline")
+       tstSC2Inline      = nti(tScNameInline, "sc2.inline")
+       tstSC2            = nti(tScName, "sc2")
+       tstSC3            = nti(tScName, "sc3")
+       tstSCSlash        = nti(tScName, "sc/sub")
+       tstParam1         = nti(tScParam, "param1")
+       tstParam2         = nti(tScParam, "param2")
+       tstParamBoolTrue  = nti(tScParam, "true")
+       tstParamBoolFalse = nti(tScParam, "false")
+       tstParamInt       = nti(tScParam, "32")
+       tstParamFloat     = nti(tScParam, "3.14")
+       tstVal            = nti(tScParamVal, "Hello World")
+       tstText           = nti(tText, "Hello World")
 )
 
 var shortCodeLexerTests = []lexerTest{
@@ -69,6 +73,12 @@ var shortCodeLexerTests = []lexerTest{
        {"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []Item{
                tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1,
                nti(tError, "unclosed shortcode")}},
+       {"float param, positional", `{{< sc1 3.14 >}}`, []Item{
+               tstLeftNoMD, tstSC1, nti(tScParam, "3.14"), tstRightNoMD, tstEOF}},
+       {"float param, named", `{{< sc1 param1=3.14 >}}`, []Item{
+               tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, "3.14"), tstRightNoMD, tstEOF}},
+       {"float param, named, space before", `{{< sc1 param1= 3.14 >}}`, []Item{
+               tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, "3.14"), tstRightNoMD, tstEOF}},
        {"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []Item{
                tstLeftNoMD, tstSC1, nti(tScParam, "-ziL-Q_456igdO-4"), tstRightNoMD, tstEOF}},
        {"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []Item{
index 0b57077bb8f6c2ec44a2a6eebe2c31ed885629a5..50016764f58fd97192daa7186c551ed74edbce0e 100644 (file)
@@ -422,7 +422,7 @@ if (!doNotTrack) {
 {{- if $pc.Simple -}}
 {{ template "_internal/shortcodes/twitter_simple.html" . }}
 {{- else -}}
-{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%s&dnt=%t" (index .Params 0) $pc.EnableDNT -}}
+{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%v&dnt=%t" (index .Params 0) $pc.EnableDNT -}}
 {{- $json := getJSON $url -}}
 {{ $json.html | safeHTML }}
 {{- end -}}
index ea7f10c383d7b493d625931397b3874afbde7823..e2c4983d75466e7567fa42fddc23e1b8ed417994 100644 (file)
@@ -3,7 +3,7 @@
 {{- if $pc.Simple -}}
 {{ template "_internal/shortcodes/twitter_simple.html" . }}
 {{- else -}}
-{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%s&dnt=%t" (index .Params 0) $pc.EnableDNT -}}
+{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%v&dnt=%t" (index .Params 0) $pc.EnableDNT -}}
 {{- $json := getJSON $url -}}
 {{ $json.html | safeHTML }}
 {{- end -}}
index 754114b2b7ec29371e2d79f51b889a00a38534ba..eaa6538b30a8419315b42d167cb107327fd48e40 100644 (file)
@@ -126,7 +126,13 @@ func (ns *Namespace) refArgsToMap(args interface{}) (map[string]interface{}, err
                s  string
                of string
        )
-       switch v := args.(type) {
+
+       v := args
+       if _, ok := v.([]interface{}); ok {
+               v = cast.ToStringSlice(v)
+       }
+
+       switch v := v.(type) {
        case map[string]interface{}:
                return v, nil
        case map[string]string:
@@ -152,6 +158,7 @@ func (ns *Namespace) refArgsToMap(args interface{}) (map[string]interface{}, err
                }
 
        }
+
        return map[string]interface{}{
                "path":         s,
                "outputFormat": of,