Support typed bool, int and float in shortcode params

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
This commit is contained in:
Bjørn Erik Pedersen 2019-09-29 14:51:51 +02:00
parent e073f4efb1
commit 329e88db1f
12 changed files with 202 additions and 53 deletions

View file

@ -151,14 +151,7 @@ func (scp *ShortcodeWithPage) Get(key interface{}) interface{} {
} }
} }
switch x.Kind() { return x.Interface()
case reflect.String:
return x.String()
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
return x.Int()
default:
return x
}
} }
@ -219,17 +212,17 @@ func (sc shortcode) String() string {
// for testing (mostly), so any change here will break tests! // for testing (mostly), so any change here will break tests!
var params interface{} var params interface{}
switch v := sc.params.(type) { switch v := sc.params.(type) {
case map[string]string: case map[string]interface{}:
// sort the keys so test assertions won't fail // sort the keys so test assertions won't fail
var keys []string var keys []string
for k := range v { for k := range v {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
var tmp = make([]string, len(keys)) var tmp = make(map[string]interface{})
for i, k := range keys { for _, k := range keys {
tmp[i] = k + ":" + v[k] tmp[k] = v[k]
} }
params = tmp params = tmp
@ -539,12 +532,12 @@ Loop:
} else if pt.Peek().IsShortcodeParamVal() { } else if pt.Peek().IsShortcodeParamVal() {
// named params // named params
if sc.params == nil { if sc.params == nil {
params := make(map[string]string) params := make(map[string]interface{})
params[currItem.ValStr()] = pt.Next().ValStr() params[currItem.ValStr()] = pt.Next().ValTyped()
sc.params = params sc.params = params
} else { } else {
if params, ok := sc.params.(map[string]string); ok { if params, ok := sc.params.(map[string]interface{}); ok {
params[currItem.ValStr()] = pt.Next().ValStr() params[currItem.ValStr()] = pt.Next().ValTyped()
} else { } else {
return sc, errShortCodeIllegalState return sc, errShortCodeIllegalState
} }
@ -553,12 +546,12 @@ Loop:
} else { } else {
// positional params // positional params
if sc.params == nil { if sc.params == nil {
var params []string var params []interface{}
params = append(params, currItem.ValStr()) params = append(params, currItem.ValTyped())
sc.params = params sc.params = params
} else { } else {
if params, ok := sc.params.([]string); ok { if params, ok := sc.params.([]interface{}); ok {
params = append(params, currItem.ValStr()) params = append(params, currItem.ValTyped())
sc.params = params sc.params = params
} else { } else {
return sc, errShortCodeIllegalState return sc, errShortCodeIllegalState

View file

@ -34,11 +34,12 @@ import (
) )
func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) { func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) {
t.Helper()
CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) CheckShortCodeMatchAndError(t, input, expected, withTemplate, false)
} }
func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) { func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {
t.Helper()
cfg, fs := newTestCfg() cfg, fs := newTestCfg()
c := qt.New(t) c := qt.New(t)
@ -1158,3 +1159,39 @@ title: "Hugo Rocks!"
"test/hello: test/hello", "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) ",
)
}

View file

@ -16,12 +16,15 @@ package pageparser
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"regexp"
"strconv"
) )
type Item struct { type Item struct {
Type ItemType Type ItemType
Pos int Pos int
Val []byte Val []byte
isString bool
} }
type Items []Item type Items []Item
@ -30,6 +33,36 @@ func (i Item) ValStr() string {
return string(i.Val) 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 { func (i Item) IsText() bool {
return i.Type == tText return i.Type == tText
} }
@ -132,3 +165,9 @@ const (
// preserved for later - keywords come after this // preserved for later - keywords come after this
tKeywordMarker tKeywordMarker
) )
var (
boolRe = regexp.MustCompile(`^(true$)|(false$)`)
intRe = regexp.MustCompile(`^[-+]?\d+$`)
floatRe = regexp.MustCompile(`^[-+]?\d*\.\d+$`)
)

View file

@ -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")
}

View file

@ -142,7 +142,13 @@ func (l *pageLexer) backup() {
// sends an item back to the client. // sends an item back to the client.
func (l *pageLexer) emit(t ItemType) { 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 l.start = l.pos
} }
@ -151,14 +157,14 @@ func (l *pageLexer) isEOF() bool {
} }
// special case, do not send '\\' back to client // 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 { val := bytes.Map(func(r rune) rune {
if r == '\\' { if r == '\\' {
return -1 return -1
} }
return r return r
}, l.input[l.start:l.pos]) }, 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 l.start = l.pos
} }
@ -176,7 +182,7 @@ var lf = []byte("\n")
// nil terminates the parser // nil terminates the parser
func (l *pageLexer) errorf(format string, args ...interface{}) stateFunc { 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 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() { func (l *pageLexer) consumeSpace() {
for { for {
r := l.next() r := l.next()

View file

@ -112,7 +112,7 @@ func lexShortcodeParam(l *pageLexer, escapedQuoteStart bool) stateFunc {
break break
} }
if !isAlphaNumericOrHyphen(r) { if !isAlphaNumericOrHyphen(r) && r != '.' { // Floats have period
l.backup() l.backup()
break 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 { func lexShortcodeQuotedParamVal(l *pageLexer, escapedQuotedValuesAllowed bool, typ ItemType) stateFunc {
openQuoteFound := false openQuoteFound := false
escapedInnerQuoteFound := false escapedInnerQuoteFound := false
@ -176,9 +182,9 @@ Loop:
} }
if escapedInnerQuoteFound { if escapedInnerQuoteFound {
l.ignoreEscapesAndEmit(typ) l.ignoreEscapesAndEmit(typ, true)
} else { } else {
l.emit(typ) l.emitString(typ)
} }
r := l.next() r := l.next()
@ -273,8 +279,13 @@ func lexInsideShortcode(l *pageLexer) stateFunc {
case isSpace(r), isEndOfLine(r): case isSpace(r), isEndOfLine(r):
l.ignore() l.ignore()
case r == '=': case r == '=':
l.consumeSpace()
l.ignore() l.ignore()
return lexShortcodeQuotedParamVal(l, l.peek() != '\\', tScParamVal) peek := l.peek()
if peek == '"' || peek == '\\' {
return lexShortcodeQuotedParamVal(l, peek != '\\', tScParamVal)
}
return lexShortcodeParamVal
case r == '/': case r == '/':
if l.currShortcodeName == "" { if l.currShortcodeName == "" {
return l.errorf("got closing shortcode, but none is open") return l.errorf("got closing shortcode, but none is open")

View file

@ -80,7 +80,7 @@ func (t *Iterator) Input() []byte {
return t.l.Input() 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. // Current will repeatably return the current item.
func (t *Iterator) Current() Item { func (t *Iterator) Current() Item {

View file

@ -27,7 +27,7 @@ type lexerTest struct {
} }
func nti(tp ItemType, val string) Item { func nti(tp ItemType, val string) Item {
return Item{tp, 0, []byte(val)} return Item{tp, 0, []byte(val), false}
} }
var ( var (
@ -119,6 +119,7 @@ func equal(i1, i2 []Item) bool {
if i1[k].Type != i2[k].Type { if i1[k].Type != i2[k].Type {
return false return false
} }
if !reflect.DeepEqual(i1[k].Val, i2[k].Val) { if !reflect.DeepEqual(i1[k].Val, i2[k].Val) {
return false return false
} }

View file

@ -30,6 +30,10 @@ var (
tstSCSlash = nti(tScName, "sc/sub") tstSCSlash = nti(tScName, "sc/sub")
tstParam1 = nti(tScParam, "param1") tstParam1 = nti(tScParam, "param1")
tstParam2 = nti(tScParam, "param2") 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") tstVal = nti(tScParamVal, "Hello World")
tstText = nti(tText, "Hello World") tstText = nti(tText, "Hello World")
) )
@ -69,6 +73,12 @@ var shortCodeLexerTests = []lexerTest{
{"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []Item{ {"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []Item{
tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1,
nti(tError, "unclosed shortcode")}}, 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{ {"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []Item{
tstLeftNoMD, tstSC1, nti(tScParam, "-ziL-Q_456igdO-4"), tstRightNoMD, tstEOF}}, tstLeftNoMD, tstSC1, nti(tScParam, "-ziL-Q_456igdO-4"), tstRightNoMD, tstEOF}},
{"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []Item{ {"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []Item{

View file

@ -422,7 +422,7 @@ if (!doNotTrack) {
{{- if $pc.Simple -}} {{- if $pc.Simple -}}
{{ template "_internal/shortcodes/twitter_simple.html" . }} {{ template "_internal/shortcodes/twitter_simple.html" . }}
{{- else -}} {{- 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 := getJSON $url -}}
{{ $json.html | safeHTML }} {{ $json.html | safeHTML }}
{{- end -}} {{- end -}}

View file

@ -3,7 +3,7 @@
{{- if $pc.Simple -}} {{- if $pc.Simple -}}
{{ template "_internal/shortcodes/twitter_simple.html" . }} {{ template "_internal/shortcodes/twitter_simple.html" . }}
{{- else -}} {{- 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 := getJSON $url -}}
{{ $json.html | safeHTML }} {{ $json.html | safeHTML }}
{{- end -}} {{- end -}}

View file

@ -126,7 +126,13 @@ func (ns *Namespace) refArgsToMap(args interface{}) (map[string]interface{}, err
s string s string
of 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{}: case map[string]interface{}:
return v, nil return v, nil
case map[string]string: case map[string]string:
@ -152,6 +158,7 @@ func (ns *Namespace) refArgsToMap(args interface{}) (map[string]interface{}, err
} }
} }
return map[string]interface{}{ return map[string]interface{}{
"path": s, "path": s,
"outputFormat": of, "outputFormat": of,