shithub: hugo

Download patch

ref: 329e88db1f6d043d32c7083570773dccfd4f11fc
parent: e073f4efb1345f6408000ef3f389873f8cf7179e
author: Bjørn Erik Pedersen <[email protected]>
date: Sun Sep 29 10:51:51 EDT 2019

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

--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -151,14 +151,7 @@
 		}
 	}
 
-	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,7 +212,7 @@
 	// 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 {
@@ -226,10 +219,10 @@
 			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 @@
 			} 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 @@
 			} 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
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -34,11 +34,12 @@
 )
 
 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)
 
@@ -1156,5 +1157,41 @@
 	builder.AssertFileContent("public/page/index.html",
 		"hello: 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) ",
 	)
 }
--- a/parser/pageparser/item.go
+++ b/parser/pageparser/item.go
@@ -16,12 +16,15 @@
 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 @@
 	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
 }
@@ -131,4 +164,10 @@
 
 	// preserved for later - keywords come after this
 	tKeywordMarker
+)
+
+var (
+	boolRe  = regexp.MustCompile(`^(true$)|(false$)`)
+	intRe   = regexp.MustCompile(`^[-+]?\d+$`)
+	floatRe = regexp.MustCompile(`^[-+]?\d*\.\d+$`)
 )
--- /dev/null
+++ b/parser/pageparser/item_test.go
@@ -1,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")
+
+}
--- a/parser/pageparser/pagelexer.go
+++ b/parser/pageparser/pagelexer.go
@@ -142,16 +142,22 @@
 
 // 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
+}
+
 func (l *pageLexer) isEOF() bool {
 	return l.pos >= len(l.input)
 }
 
 // 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
@@ -158,7 +164,7 @@
 		}
 		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 @@
 
 // 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
 }
 
@@ -196,6 +202,16 @@
 	for {
 		r := l.next()
 		if r == eof || isEndOfLine(r) {
+			return
+		}
+	}
+}
+
+func (l *pageLexer) consumeToSpace() {
+	for {
+		r := l.next()
+		if r == eof || unicode.IsSpace(r) {
+			l.backup()
 			return
 		}
 	}
--- a/parser/pageparser/pagelexer_shortcode.go
+++ b/parser/pageparser/pagelexer_shortcode.go
@@ -112,7 +112,7 @@
 			break
 		}
 
-		if !isAlphaNumericOrHyphen(r) {
+		if !isAlphaNumericOrHyphen(r) && r != '.' { // Floats have period
 			l.backup()
 			break
 		}
@@ -137,6 +137,12 @@
 
 }
 
+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 @@
 	}
 
 	if escapedInnerQuoteFound {
-		l.ignoreEscapesAndEmit(typ)
+		l.ignoreEscapesAndEmit(typ, true)
 	} else {
-		l.emit(typ)
+		l.emitString(typ)
 	}
 
 	r := l.next()
@@ -273,8 +279,13 @@
 	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")
--- a/parser/pageparser/pageparser.go
+++ b/parser/pageparser/pageparser.go
@@ -80,7 +80,7 @@
 	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 {
--- a/parser/pageparser/pageparser_intro_test.go
+++ b/parser/pageparser/pageparser_intro_test.go
@@ -27,7 +27,7 @@
 }
 
 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 @@
 		if i1[k].Type != i2[k].Type {
 			return false
 		}
+
 		if !reflect.DeepEqual(i1[k].Val, i2[k].Val) {
 			return false
 		}
--- a/parser/pageparser/pageparser_shortcode_test.go
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -16,22 +16,26 @@
 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 @@
 	{"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{
--- a/tpl/tplimpl/embedded/templates.autogen.go
+++ b/tpl/tplimpl/embedded/templates.autogen.go
@@ -422,7 +422,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 -}}
--- 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 -}}
--- a/tpl/urls/urls.go
+++ b/tpl/urls/urls.go
@@ -126,7 +126,13 @@
 		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 @@
 		}
 
 	}
+
 	return map[string]interface{}{
 		"path":         s,
 		"outputFormat": of,