diff --git a/docs/content/extras/shortcodes.md b/docs/content/extras/shortcodes.md index dee8d12da..7e2c548c7 100644 --- a/docs/content/extras/shortcodes.md +++ b/docs/content/extras/shortcodes.md @@ -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 - -
- {{ if isset .Params "link"}}{{ end }} - - {{ if isset .Params "link"}}{{ end }} - {{ if or (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")}} +
+ {{ with .Get "link"}}{{ end }} + + {{ if .Get "link"}}{{ end }} + {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}
{{ if isset .Params "title" }} -

{{ index .Params "title" }}

{{ end }} - {{ if or (isset .Params "caption") (isset .Params "attr")}}

- {{ index .Params "caption" }} - {{ if isset .Params "attrlink"}} {{ end }} - {{ index .Params "attr" }} - {{ if isset .Params "attrlink"}} {{ end }} +

{{ .Get "title" }}

{{ end }} + {{ if or (.Get "caption") (.Get "attr")}}

+ {{ .Get "caption" }} + {{ with .Get "attrlink"}} {{ end }} + {{ .Get "attr" }} + {{ if .Get "attrlink"}} {{ end }}

{{ end }}
{{ 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: diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index b71fdb427..bea89fe2a 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -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() } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go new file mode 100644 index 000000000..453d95b11 --- /dev/null +++ b/hugolib/shortcode_test.go @@ -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", ``) + + CheckShortCodeMatch(t, `{{% img src="one" %}}`, ``, tem) + CheckShortCodeMatch(t, `{{% img class="aspen" %}}`, ``, tem) + CheckShortCodeMatch(t, `{{% img src= "one" %}}`, ``, tem) + CheckShortCodeMatch(t, `{{% img src ="one" %}}`, ``, tem) + CheckShortCodeMatch(t, `{{% img src = "one" %}}`, ``, tem) + CheckShortCodeMatch(t, `{{% img src = "one" class = "aspen grove" %}}`, ``, tem) +} + +func TestInnerSC(t *testing.T) { + tem := bundle.NewTemplate() + tem.AddInternalShortcode("inside.html", `{{ .Inner }}`) + + CheckShortCodeMatch(t, `{{% inside class="aspen" %}}`, `
`, tem) + CheckShortCodeMatch(t, `{{% inside class="aspen" %}}More Here{{% /inside %}}`, `
More Here
`, tem) + CheckShortCodeMatch(t, `{{% inside %}}More Here{{% /inside %}}`, `
More Here
`, 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
\n \n \n \n \n
\n", tem) + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "\n
\n \n \"This\n \n \n
\n

\n This is a caption\n \n \n \n

\n
\n \n
\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
\n \n \"MongoSV\n \n \n
\n", tem) +} diff --git a/template/bundle/embedded.go b/template/bundle/embedded.go index a0374df7a..5beeb8d1f 100644 --- a/template/bundle/embedded.go +++ b/template/bundle/embedded.go @@ -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", ` -
- {{ if isset .Params "link"}}{{ end }} - - {{ if isset .Params "link"}}{{ 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", ` +
+ {{ with .Get "link"}}{{ end }} + + {{ if .Get "link"}}{{ end }} + {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}
{{ if isset .Params "title" }} -

{{ index .Params "title" }}

{{ end }} - {{ if or (isset .Params "caption") (isset .Params "attr")}}

- {{ index .Params "caption" }} - {{ if isset .Params "attrlink"}} {{ end }} - {{ index .Params "attr" }} - {{ if isset .Params "attrlink"}} {{ end }} +

{{ .Get "title" }}

{{ end }} + {{ if or (.Get "caption") (.Get "attr")}}

+ {{ .Get "caption" }} + {{ with .Get "attrlink"}} {{ end }} + {{ .Get "attr" }} + {{ if .Get "attrlink"}} {{ end }}

{{ end }}
{{ end }}
`) - } diff --git a/template/bundle/template.go b/template/bundle/template.go index 89d49c352..d7e135b89 100644 --- a/template/bundle/template.go +++ b/template/bundle/template.go @@ -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 {