summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-09-29 14:51:51 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-09-29 23:22:41 +0200
commit329e88db1f6d043d32c7083570773dccfd4f11fc (patch)
treea4bab342aa28523be8c854ceb88fece17f634410
parente073f4efb1345f6408000ef3f389873f8cf7179e (diff)
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
-rw-r--r--hugolib/shortcode.go33
-rw-r--r--hugolib/shortcode_test.go39
-rw-r--r--parser/pageparser/item.go45
-rw-r--r--parser/pageparser/item_test.go35
-rw-r--r--parser/pageparser/pagelexer.go24
-rw-r--r--parser/pageparser/pagelexer_shortcode.go19
-rw-r--r--parser/pageparser/pageparser.go2
-rw-r--r--parser/pageparser/pageparser_intro_test.go3
-rw-r--r--parser/pageparser/pageparser_shortcode_test.go42
-rw-r--r--tpl/tplimpl/embedded/templates.autogen.go2
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/twitter.html2
-rw-r--r--tpl/urls/urls.go9
12 files changed, 202 insertions, 53 deletions
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index 8323962c0..d0cdf3950 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -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
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index 13cbd1fd8..36f004253 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -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) ",
+ )
+}
diff --git a/parser/pageparser/item.go b/parser/pageparser/item.go
index 3877ee6d9..48003ee86 100644
--- a/parser/pageparser/item.go
+++ b/parser/pageparser/item.go
@@ -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
index 000000000..a30860f17
--- /dev/null
+++ b/parser/pageparser/item_test.go
@@ -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")
+
+}
diff --git a/parser/pageparser/pagelexer.go b/parser/pageparser/pagelexer.go
index 2da8ebdc3..f994286d9 100644
--- a/parser/pageparser/pagelexer.go
+++ b/parser/pageparser/pagelexer.go
@@ -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()
diff --git a/parser/pageparser/pagelexer_shortcode.go b/parser/pageparser/pagelexer_shortcode.go
index d503d1797..dea1b317e 100644
--- a/parser/pageparser/pagelexer_shortcode.go
+++ b/parser/pageparser/pagelexer_shortcode.go
@@ -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")
diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go
index db563d44c..acdb09587 100644
--- a/parser/pageparser/pageparser.go
+++ b/parser/pageparser/pageparser.go
@@ -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 {
diff --git a/parser/pageparser/pageparser_intro_test.go b/parser/pageparser/pageparser_intro_test.go
index 3e5bac872..0f20ae5a1 100644
--- a/parser/pageparser/pageparser_intro_test.go
+++ b/parser/pageparser/pageparser_intro_test.go
@@ -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
}
diff --git a/parser/pageparser/pageparser_shortcode_test.go b/parser/pageparser/pageparser_shortcode_test.go
index 75ee56090..4ce4bae31 100644
--- a/parser/pageparser/pageparser_shortcode_test.go
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -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{
diff --git a/tpl/tplimpl/embedded/templates.autogen.go b/tpl/tplimpl/embedded/templates.autogen.go
index 0b57077bb..50016764f 100644
--- a/tpl/tplimpl/embedded/templates.autogen.go
+++ b/tpl/tplimpl/embedded/templates.autogen.go
@@ -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 -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
index ea7f10c38..e2c4983d7 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
+++ b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
@@ -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 -}}
diff --git a/tpl/urls/urls.go b/tpl/urls/urls.go
index 754114b2b..eaa6538b3 100644
--- a/tpl/urls/urls.go
+++ b/tpl/urls/urls.go
@@ -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,