mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-29 22:51:32 -05:00
Shortcode cleanup. Added a ton of tests. Much more flexible with input. Doesn't crash with bad input. Fixed #193
Also added the .Get function to short codes and documentation for that function.
This commit is contained in:
parent
dc068ccb87
commit
64572d2d60
5 changed files with 202 additions and 49 deletions
|
@ -120,15 +120,24 @@ parameters named parameters work best.
|
|||
|
||||
**Inside the template**
|
||||
|
||||
To access a parameter by either position or name the index method can be used.
|
||||
To access a parameter by position the .Get method can be used.
|
||||
|
||||
{{ index .Params 0 }}
|
||||
or
|
||||
{{ index .Params "class" }}
|
||||
{{ .Get 0 }}
|
||||
|
||||
To check if a parameter has been provided use the isset method provided by Hugo.
|
||||
To access a parameter by name the .Get method should be utilized
|
||||
|
||||
{{ if isset .Params "class"}} class="{{ index .Params "class"}}" {{ end }}
|
||||
{{ .Get "class" }}
|
||||
|
||||
|
||||
With is great when the output depends on a parameter being set
|
||||
|
||||
{{ with .Get "class"}} class="{{.}}"{{ end }}
|
||||
|
||||
Get can also be used to check if a parameter has been provided. This is
|
||||
most helpful when the condition depends on either one value or another...
|
||||
or both.
|
||||
|
||||
{{ or .Get "title" | .Get "alt" | if }} alt="{{ with .Get "alt"}}{{.}}{{else}}{{.Get "title"}}{{end}}"{{ end }}
|
||||
|
||||
If a closing shortcode is used, the variable .Inner will be populated with all
|
||||
of the content between the opening and closing shortcodes. If a closing
|
||||
|
@ -162,20 +171,19 @@ This would be rendered as
|
|||
{{ % img src="/media/spf13.jpg" title="Steve Francia" %}}
|
||||
|
||||
Would load the template /layouts/shortcodes/img.html
|
||||
|
||||
<!-- image -->
|
||||
<figure {{ if isset .Params "class" }}class="{{ index .Params "class" }}"{{ end }}>
|
||||
{{ if isset .Params "link"}}<a href="{{ index .Params "link"}}">{{ end }}
|
||||
<img src="{{ index .Params "src" }}" {{ if or (isset .Params "alt") (isset .Params "caption") }}alt="{{ if isset .Params "alt"}}{{ index .Params "alt"}}{{else}}{{ index .Params "caption" }}{{ end }}"{{ end }} />
|
||||
{{ if isset .Params "link"}}</a>{{ end }}
|
||||
{{ if or (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")}}
|
||||
<figure {{ with .Get "class" }}class="{{.}}"{{ end }}>
|
||||
{{ with .Get "link"}}<a href="{{.}}">{{ end }}
|
||||
<img src="{{ .Get "src" }}" {{ if or (.Get "alt") (.Get "caption") }}alt="{{ with .Get "alt"}}{{.}}{{else}}{{ .Get "caption" }}{{ end }}"{{ end }} />
|
||||
{{ if .Get "link"}}</a>{{ end }}
|
||||
{{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}
|
||||
<figcaption>{{ if isset .Params "title" }}
|
||||
<h4>{{ index .Params "title" }}</h4>{{ end }}
|
||||
{{ if or (isset .Params "caption") (isset .Params "attr")}}<p>
|
||||
{{ index .Params "caption" }}
|
||||
{{ if isset .Params "attrlink"}}<a href="{{ index .Params "attrlink"}}"> {{ end }}
|
||||
{{ index .Params "attr" }}
|
||||
{{ if isset .Params "attrlink"}}</a> {{ end }}
|
||||
<h4>{{ .Get "title" }}</h4>{{ end }}
|
||||
{{ if or (.Get "caption") (.Get "attr")}}<p>
|
||||
{{ .Get "caption" }}
|
||||
{{ with .Get "attrlink"}}<a href="{{.}}"> {{ end }}
|
||||
{{ .Get "attr" }}
|
||||
{{ if .Get "attrlink"}}</a> {{ end }}
|
||||
</p> {{ end }}
|
||||
</figcaption>
|
||||
{{ end }}
|
||||
|
@ -203,8 +211,7 @@ Would be rendered as:
|
|||
{{% /highlight %}}
|
||||
|
||||
The template for this utilizes the following code (already include in hugo)
|
||||
|
||||
{{ $lang := index .Params 0 }}{{ highlight .Inner $lang }}
|
||||
{{ .Get 0 | highlight .Inner }}
|
||||
|
||||
And will be rendered as:
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"fmt"
|
||||
"github.com/spf13/hugo/template/bundle"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
@ -37,6 +38,45 @@ type ShortcodeWithPage struct {
|
|||
Page *Page
|
||||
}
|
||||
|
||||
func (scp *ShortcodeWithPage) Get(key interface{}) interface{} {
|
||||
if reflect.ValueOf(scp.Params).Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var x reflect.Value
|
||||
|
||||
switch key.(type) {
|
||||
case int64, int32, int16, int8, int:
|
||||
if reflect.TypeOf(scp.Params).Kind() == reflect.Map {
|
||||
return "error: cannot access named params by position"
|
||||
} else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice {
|
||||
x = reflect.ValueOf(scp.Params).Index(int(reflect.ValueOf(key).Int()))
|
||||
}
|
||||
case string:
|
||||
if reflect.TypeOf(scp.Params).Kind() == reflect.Map {
|
||||
x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key))
|
||||
if !x.IsValid() {
|
||||
return ""
|
||||
}
|
||||
} else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice {
|
||||
if reflect.ValueOf(scp.Params).Len() == 1 && reflect.ValueOf(scp.Params).Index(0).String() == "" {
|
||||
return nil
|
||||
}
|
||||
return "error: cannot access positional params by string name"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type Shortcodes map[string]ShortcodeFunc
|
||||
|
||||
func ShortcodesHandle(stringToParse string, p *Page, t bundle.Template) string {
|
||||
|
@ -127,12 +167,44 @@ func StripShortcodes(stringToParse string) string {
|
|||
return stringToParse
|
||||
}
|
||||
|
||||
func CleanupSpacesAroundEquals(rawfirst []string) []string {
|
||||
var first = make([]string, 0)
|
||||
|
||||
for i := 0; i < len(rawfirst); i++ {
|
||||
v := rawfirst[i]
|
||||
index := strings.Index(v, "=")
|
||||
|
||||
if index == len(v)-1 {
|
||||
// Trailing '='
|
||||
if len(rawfirst) > i {
|
||||
if v == "=" {
|
||||
first[len(first)-1] = first[len(first)-1] + v + rawfirst[i+1] // concat prior with this and next
|
||||
i++ // Skip next
|
||||
} else {
|
||||
// Trailing ' = '
|
||||
first = append(first, v+rawfirst[i+1]) // append this token and the next
|
||||
i++ // Skip next
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if index == 0 {
|
||||
// Leading '='
|
||||
first[len(first)-1] = first[len(first)-1] + v // concat this token to the prior one
|
||||
continue
|
||||
} else {
|
||||
first = append(first, v)
|
||||
}
|
||||
}
|
||||
|
||||
return first
|
||||
}
|
||||
|
||||
func Tokenize(in string) interface{} {
|
||||
first := strings.Fields(in)
|
||||
var final = make([]string, 0)
|
||||
|
||||
// if don't need to parse, don't parse.
|
||||
if strings.Index(in, " ") < 0 && strings.Index(in, "=") < 1 {
|
||||
// if there isn't a space or an equal sign, no need to parse
|
||||
if strings.Index(in, " ") < 0 && strings.Index(in, "=") < 0 {
|
||||
return append(final, in)
|
||||
}
|
||||
|
||||
|
@ -140,14 +212,10 @@ func Tokenize(in string) interface{} {
|
|||
inQuote := false
|
||||
start := 0
|
||||
|
||||
first := CleanupSpacesAroundEquals(strings.Fields(in))
|
||||
|
||||
for i, v := range first {
|
||||
index := strings.Index(v, "=")
|
||||
|
||||
if index < 0 {
|
||||
fmt.Printf("Shortcode parameters must be key=value pairs (no spaces) (saw '%s')\n", v)
|
||||
continue
|
||||
}
|
||||
|
||||
if !inQuote {
|
||||
if index > 1 {
|
||||
keys = append(keys, v[:index])
|
||||
|
@ -198,7 +266,8 @@ func Tokenize(in string) interface{} {
|
|||
}
|
||||
|
||||
if len(keys) > 0 && (len(keys) != len(final)) {
|
||||
panic("keys and final different lengths")
|
||||
// This will happen if the quotes aren't balanced
|
||||
return final
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
|
@ -214,12 +283,13 @@ func Tokenize(in string) interface{} {
|
|||
}
|
||||
|
||||
func SplitParams(in string) (name string, par2 string) {
|
||||
i := strings.IndexFunc(strings.TrimSpace(in), unicode.IsSpace)
|
||||
newIn := strings.TrimSpace(in)
|
||||
i := strings.IndexFunc(newIn, unicode.IsSpace)
|
||||
if i < 1 {
|
||||
return strings.TrimSpace(in), ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(in[:i+1]), strings.TrimSpace(in[i+1:])
|
||||
return strings.TrimSpace(newIn[:i+1]), strings.TrimSpace(newIn[i+1:])
|
||||
}
|
||||
|
||||
func ShortcodeRender(tmpl *template.Template, data *ShortcodeWithPage) string {
|
||||
|
@ -227,6 +297,7 @@ func ShortcodeRender(tmpl *template.Template, data *ShortcodeWithPage) string {
|
|||
err := tmpl.Execute(buffer, data)
|
||||
if err != nil {
|
||||
fmt.Println("error processing shortcode", tmpl.Name(), "\n ERR:", err)
|
||||
fmt.Println(data)
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
|
72
hugolib/shortcode_test.go
Normal file
72
hugolib/shortcode_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package hugolib
|
||||
|
||||
import (
|
||||
"github.com/spf13/hugo/template/bundle"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func pageFromString(in, filename string) (*Page, error) {
|
||||
return ReadFrom(strings.NewReader(in), filename)
|
||||
}
|
||||
|
||||
func CheckShortCodeMatch(t *testing.T, input, expected string, template bundle.Template) {
|
||||
|
||||
p, _ := pageFromString(SIMPLE_PAGE, "simple.md")
|
||||
output := ShortcodesHandle(input, p, template)
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("Shortcode render didn't match. Expected: %q, Got: %q", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonSC(t *testing.T) {
|
||||
tem := bundle.NewTemplate()
|
||||
|
||||
CheckShortCodeMatch(t, "{{% movie 47238zzb %}}", "{{% movie 47238zzb %}}", tem)
|
||||
}
|
||||
|
||||
func TestPositionalParamSC(t *testing.T) {
|
||||
tem := bundle.NewTemplate()
|
||||
tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 0 }}`)
|
||||
|
||||
CheckShortCodeMatch(t, "{{% video 47238zzb %}}", "Playing Video 47238zzb", tem)
|
||||
CheckShortCodeMatch(t, "{{% video 47238zzb 132 %}}", "Playing Video 47238zzb", tem)
|
||||
CheckShortCodeMatch(t, "{{%video 47238zzb%}}", "Playing Video 47238zzb", tem)
|
||||
CheckShortCodeMatch(t, "{{%video 47238zzb %}}", "Playing Video 47238zzb", tem)
|
||||
CheckShortCodeMatch(t, "{{% video 47238zzb %}}", "Playing Video 47238zzb", tem)
|
||||
}
|
||||
|
||||
func TestNamedParamSC(t *testing.T) {
|
||||
tem := bundle.NewTemplate()
|
||||
tem.AddInternalShortcode("img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)
|
||||
|
||||
CheckShortCodeMatch(t, `{{% img src="one" %}}`, `<img src="one">`, tem)
|
||||
CheckShortCodeMatch(t, `{{% img class="aspen" %}}`, `<img class="aspen">`, tem)
|
||||
CheckShortCodeMatch(t, `{{% img src= "one" %}}`, `<img src="one">`, tem)
|
||||
CheckShortCodeMatch(t, `{{% img src ="one" %}}`, `<img src="one">`, tem)
|
||||
CheckShortCodeMatch(t, `{{% img src = "one" %}}`, `<img src="one">`, tem)
|
||||
CheckShortCodeMatch(t, `{{% img src = "one" class = "aspen grove" %}}`, `<img src="one" class="aspen grove">`, tem)
|
||||
}
|
||||
|
||||
func TestInnerSC(t *testing.T) {
|
||||
tem := bundle.NewTemplate()
|
||||
tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
|
||||
CheckShortCodeMatch(t, `{{% inside class="aspen" %}}`, `<div class="aspen"></div>`, tem)
|
||||
CheckShortCodeMatch(t, `{{% inside class="aspen" %}}More Here{{% /inside %}}`, `<div class="aspen">More Here</div>`, tem)
|
||||
CheckShortCodeMatch(t, `{{% inside %}}More Here{{% /inside %}}`, `<div>More Here</div>`, tem)
|
||||
}
|
||||
|
||||
func TestEmbeddedSC(t *testing.T) {
|
||||
tem := bundle.NewTemplate()
|
||||
CheckShortCodeMatch(t, "{{% test %}}", "This is a simple Test", tem)
|
||||
CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" />\n \n \n</figure>\n", tem)
|
||||
CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"This is a caption\" />\n \n \n <figcaption>\n <p>\n This is a caption\n \n \n \n </p> \n </figcaption>\n \n</figure>\n", tem)
|
||||
}
|
||||
|
||||
func TestUnbalancedQuotes(t *testing.T) {
|
||||
tem := bundle.NewTemplate()
|
||||
|
||||
CheckShortCodeMatch(t, `{{% figure src="/uploads/2011/12/spf13-mongosv-speaking-copy-1024x749.jpg "Steve Francia speaking at OSCON 2012" alt="MongoSV 2011" %}}`, "\n<figure >\n \n <img src=\"/uploads/2011/12/spf13-mongosv-speaking-copy-1024x749.jpg%20%22Steve%20Francia%20speaking%20at%20OSCON%202012\" alt=\"MongoSV 2011\" />\n \n \n</figure>\n", tem)
|
||||
}
|
|
@ -19,27 +19,24 @@ type Tmpl struct {
|
|||
}
|
||||
|
||||
func (t *GoHtmlTemplate) EmbedShortcodes() {
|
||||
const k = "shortcodes"
|
||||
|
||||
t.AddInternalTemplate(k, "highlight.html", `{{ $lang := index .Params 0 }}{{ highlight .Inner $lang }}`)
|
||||
t.AddInternalTemplate(k, "test.html", `This is a simple Test`)
|
||||
t.AddInternalTemplate(k, "figure.html", `<!-- image -->
|
||||
<figure {{ if isset .Params "class" }}class="{{ index .Params "class" }}"{{ end }}>
|
||||
{{ if isset .Params "link"}}<a href="{{ index .Params "link"}}">{{ end }}
|
||||
<img src="{{ index .Params "src" }}" {{ if or (isset .Params "alt") (isset .Params "caption") }}alt="{{ if isset .Params "alt"}}{{ index .Params "alt"}}{{else}}{{ index .Params "caption" }}{{ end }}"{{ end }} />
|
||||
{{ if isset .Params "link"}}</a>{{ end }}
|
||||
{{ if or (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")}}
|
||||
t.AddInternalShortcode("highlight.html", `{{ .Get 0 | highlight .Inner }}`)
|
||||
t.AddInternalShortcode("test.html", `This is a simple Test`)
|
||||
t.AddInternalShortcode("figure.html", `<!-- image -->
|
||||
<figure {{ with .Get "class" }}class="{{.}}"{{ end }}>
|
||||
{{ with .Get "link"}}<a href="{{.}}">{{ end }}
|
||||
<img src="{{ .Get "src" }}" {{ if or (.Get "alt") (.Get "caption") }}alt="{{ with .Get "alt"}}{{.}}{{else}}{{ .Get "caption" }}{{ end }}"{{ end }} />
|
||||
{{ if .Get "link"}}</a>{{ end }}
|
||||
{{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}
|
||||
<figcaption>{{ if isset .Params "title" }}
|
||||
<h4>{{ index .Params "title" }}</h4>{{ end }}
|
||||
{{ if or (isset .Params "caption") (isset .Params "attr")}}<p>
|
||||
{{ index .Params "caption" }}
|
||||
{{ if isset .Params "attrlink"}}<a href="{{ index .Params "attrlink"}}"> {{ end }}
|
||||
{{ index .Params "attr" }}
|
||||
{{ if isset .Params "attrlink"}}</a> {{ end }}
|
||||
<h4>{{ .Get "title" }}</h4>{{ end }}
|
||||
{{ if or (.Get "caption") (.Get "attr")}}<p>
|
||||
{{ .Get "caption" }}
|
||||
{{ with .Get "attrlink"}}<a href="{{.}}"> {{ end }}
|
||||
{{ .Get "attr" }}
|
||||
{{ if .Get "attrlink"}}</a> {{ end }}
|
||||
</p> {{ end }}
|
||||
</figcaption>
|
||||
{{ end }}
|
||||
</figure>
|
||||
<!-- image -->`)
|
||||
|
||||
}
|
||||
|
|
|
@ -138,6 +138,8 @@ type Template interface {
|
|||
New(name string) *template.Template
|
||||
LoadTemplates(absPath string)
|
||||
AddTemplate(name, tpl string) error
|
||||
AddInternalTemplate(prefix, name, tpl string) error
|
||||
AddInternalShortcode(name, tpl string) error
|
||||
}
|
||||
|
||||
type templateErr struct {
|
||||
|
@ -189,6 +191,10 @@ func (t *GoHtmlTemplate) AddInternalTemplate(prefix, name, tpl string) error {
|
|||
return t.AddTemplate("_internal/"+prefix+"/"+name, tpl)
|
||||
}
|
||||
|
||||
func (t *GoHtmlTemplate) AddInternalShortcode(name, content string) error {
|
||||
return t.AddInternalTemplate("shortcodes", name, content)
|
||||
}
|
||||
|
||||
func (t *GoHtmlTemplate) AddTemplate(name, tpl string) error {
|
||||
_, err := t.New(name).Parse(tpl)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue