Shortcode cleanup. Added a ton of tests. Much more flexible with input. Doesn't crash...
authorspf13 <steve.francia@gmail.com>
Wed, 26 Feb 2014 04:57:31 +0000 (23:57 -0500)
committerspf13 <steve.francia@gmail.com>
Wed, 26 Feb 2014 04:57:31 +0000 (23:57 -0500)
Also added the .Get function to short codes and documentation for that function.

docs/content/extras/shortcodes.md
hugolib/shortcode.go
hugolib/shortcode_test.go [new file with mode: 0644]
template/bundle/embedded.go
template/bundle/template.go

index dee8d12daee2e87241cd72569e7a0b072a537175..7e2c548c77a96da9a8665f4429f52403be0c9e4c 100644 (file)
@@ -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:
 
index b71fdb4270d6b1970cacebc0782b6fbcc0688892..bea89fe2a0e2fea5bf34c221e3ddb740913f2d9b 100644 (file)
@@ -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 (file)
index 0000000..453d95b
--- /dev/null
@@ -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)
+}
index a0374df7af2f61dd425d0a16c2319f1ed07d9ba1..5beeb8d1f006554f7fef9632e15c7a0ea001f775 100644 (file)
@@ -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 -->`)
-
 }
index 89d49c35233e7aa382268bb7a95f385c0e75f3a0..d7e135b89308f901b88d4bb40ecf925bd81a372f 100644 (file)
@@ -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 {