From 329e88db1f6d043d32c7083570773dccfd4f11fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 29 Sep 2019 14:51:51 +0200 Subject: [PATCH] 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 --- hugolib/shortcode.go | 33 ++++++-------- hugolib/shortcode_test.go | 39 +++++++++++++++- parser/pageparser/item.go | 45 +++++++++++++++++-- parser/pageparser/item_test.go | 35 +++++++++++++++ parser/pageparser/pagelexer.go | 24 ++++++++-- parser/pageparser/pagelexer_shortcode.go | 19 ++++++-- parser/pageparser/pageparser.go | 2 +- parser/pageparser/pageparser_intro_test.go | 3 +- .../pageparser/pageparser_shortcode_test.go | 42 ++++++++++------- tpl/tplimpl/embedded/templates.autogen.go | 2 +- .../templates/shortcodes/twitter.html | 2 +- tpl/urls/urls.go | 9 +++- 12 files changed, 202 insertions(+), 53 deletions(-) create mode 100644 parser/pageparser/item_test.go 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,