]> Cypherpunks repositories - gostls13.git/commitdiff
template: Add simple formatter chaining.
authorKyle Consalus <consalus@gmail.com>
Sat, 5 Feb 2011 00:37:30 +0000 (16:37 -0800)
committerRob Pike <r@golang.org>
Sat, 5 Feb 2011 00:37:30 +0000 (16:37 -0800)
Fixes #676.

R=r, rsc, r2
CC=golang-dev
https://golang.org/cl/4127043

src/pkg/template/template.go
src/pkg/template/template_test.go

index 1874851668ae1f06142e25b61c364e3b78d46f78..f71d76c08899f02c28f7aec3e1db14185801c05f 100644 (file)
@@ -47,6 +47,7 @@
                {field1 field2 ...}
                {field|formatter}
                {field1 field2...|formatter}
+               {field|formatter1|formatter2}
 
        Insert the value of the fields into the output. Each field is
        first looked for in the cursor, as in .section and .repeated.
        values at the instantiation, and formatter is its name at
        the invocation site.  The default formatter just concatenates
        the string representations of the fields.
+
+       Multiple formatters separated by the pipeline character | are
+       executed sequentially, with each formatter receiving the bytes
+       emitted by the one to its left.
 */
 package template
 
 import (
+       "bytes"
        "container/vector"
        "fmt"
        "io"
@@ -138,9 +144,9 @@ type literalElement struct {
 
 // A variable invocation to be evaluated
 type variableElement struct {
-       linenum   int
-       word      []string // The fields in the invocation.
-       formatter string   // TODO(r): implement pipelines
+       linenum int
+       word    []string // The fields in the invocation.
+       fmts    []string // Names of formatters to apply. len(fmts) > 0
 }
 
 // A .section block, possibly with a .or
@@ -176,13 +182,14 @@ type Template struct {
 // the data item descends into the fields associated with sections, etc.
 // Parent is used to walk upwards to find variables higher in the tree.
 type state struct {
-       parent *state        // parent in hierarchy
-       data   reflect.Value // the driver data for this section etc.
-       wr     io.Writer     // where to send output
+       parent *state          // parent in hierarchy
+       data   reflect.Value   // the driver data for this section etc.
+       wr     io.Writer       // where to send output
+       buf    [2]bytes.Buffer // alternating buffers used when chaining formatters
 }
 
 func (parent *state) clone(data reflect.Value) *state {
-       return &state{parent, data, parent.wr}
+       return &state{parent: parent, data: data, wr: parent.wr}
 }
 
 // New creates a new template with the specified formatter map (which
@@ -409,38 +416,43 @@ func (t *Template) analyze(item []byte) (tok int, w []string) {
        return
 }
 
+// formatter returns the Formatter with the given name in the Template, or nil if none exists.
+func (t *Template) formatter(name string) func(io.Writer, string, ...interface{}) {
+       if t.fmap != nil {
+               if fn := t.fmap[name]; fn != nil {
+                       return fn
+               }
+       }
+       return builtins[name]
+}
+
 // -- Parsing
 
 // Allocate a new variable-evaluation element.
-func (t *Template) newVariable(words []string) (v *variableElement) {
-       // The words are tokenized elements from the {item}. The last one may be of
-       // the form "|fmt".  For example: {a b c|d}
-       formatter := ""
+func (t *Template) newVariable(words []string) *variableElement {
+       // After the final space-separated argument, formatters may be specified separated
+       // by pipe symbols, for example: {a b c|d|e}
+
+       // Until we learn otherwise, formatters contains a single name: "", the default formatter.
+       formatters := []string{""}
        lastWord := words[len(words)-1]
-       bar := strings.Index(lastWord, "|")
+       bar := strings.IndexRune(lastWord, '|')
        if bar >= 0 {
                words[len(words)-1] = lastWord[0:bar]
-               formatter = lastWord[bar+1:]
+               formatters = strings.Split(lastWord[bar+1:], "|", -1)
        }
-       // Probably ok, so let's build it.
-       v = &variableElement{t.linenum, words, formatter}
 
        // We could remember the function address here and avoid the lookup later,
        // but it's more dynamic to let the user change the map contents underfoot.
        // We do require the name to be present, though.
 
        // Is it in user-supplied map?
-       if t.fmap != nil {
-               if _, ok := t.fmap[formatter]; ok {
-                       return
+       for _, f := range formatters {
+               if t.formatter(f) == nil {
+                       t.parseError("unknown formatter: %q", f)
                }
        }
-       // Is it in builtin map?
-       if _, ok := builtins[formatter]; ok {
-               return
-       }
-       t.parseError("unknown formatter: %s", formatter)
-       return
+       return &variableElement{t.linenum, words, formatters}
 }
 
 // Grab the next item.  If it's simple, just append it to the template.
@@ -733,28 +745,31 @@ func (t *Template) varValue(name string, st *state) reflect.Value {
        return field
 }
 
+func (t *Template) format(wr io.Writer, fmt string, val []interface{}, v *variableElement, st *state) {
+       fn := t.formatter(fmt)
+       if fn == nil {
+               t.execError(st, v.linenum, "missing formatter %s for variable %s", fmt, v.word[0])
+       }
+       fn(wr, fmt, val...)
+}
+
 // Evaluate a variable, looking up through the parent if necessary.
 // If it has a formatter attached ({var|formatter}) run that too.
 func (t *Template) writeVariable(v *variableElement, st *state) {
-       formatter := v.formatter
        // Turn the words of the invocation into values.
        val := make([]interface{}, len(v.word))
        for i, word := range v.word {
                val[i] = t.varValue(word, st).Interface()
        }
-       // is it in user-supplied map?
-       if t.fmap != nil {
-               if fn, ok := t.fmap[formatter]; ok {
-                       fn(st.wr, formatter, val...)
-                       return
-               }
-       }
-       // is it in builtin map?
-       if fn, ok := builtins[formatter]; ok {
-               fn(st.wr, formatter, val...)
-               return
+
+       for i, fmt := range v.fmts[:len(v.fmts)-1] {
+               b := &st.buf[i&1]
+               b.Reset()
+               t.format(b, fmt, val, v, st)
+               val = val[0:1]
+               val[0] = b.Bytes()
        }
-       t.execError(st, v.linenum, "missing formatter %s for variable %s", formatter, v.word[0])
+       t.format(st.wr, v.fmts[len(v.fmts)-1], val, v, st)
 }
 
 // Execute element i.  Return next index to execute.
@@ -962,7 +977,7 @@ func (t *Template) Execute(data interface{}, wr io.Writer) (err os.Error) {
        val := reflect.NewValue(data)
        defer checkError(&err)
        t.p = 0
-       t.execute(0, t.elems.Len(), &state{nil, val, wr})
+       t.execute(0, t.elems.Len(), &state{parent: nil, data: val, wr: wr})
        return nil
 }
 
index c8707e6617ad542fbcda42db88b5c5f1c4d018a4..a89fceb4cb23a0eefeb57a7d58e944a5899bed53 100644 (file)
@@ -35,7 +35,6 @@ type S struct {
        Integer       int
        IntegerPtr    *int
        NilPtr        *int
-       Raw           string
        InnerT        T
        InnerPointerT *T
        Data          []T
@@ -51,7 +50,6 @@ type S struct {
        Innermap      U
        Stringmap     map[string]string
        Ptrmap        map[string]*string
-       Bytes         []byte
        Iface         interface{}
        Ifaceptr      interface{}
 }
@@ -334,38 +332,6 @@ var tests = []*Test{
                out: "ItemNumber1=ValueNumber1\n",
        },
 
-
-       // Formatters
-       &Test{
-               in: "{.section Pdata }\n" +
-                       "{Header|uppercase}={Integer|+1}\n" +
-                       "{Header|html}={Integer|str}\n" +
-                       "{.end}\n",
-
-               out: "HEADER=78\n" +
-                       "Header=77\n",
-       },
-
-       &Test{
-               in: "{.section Pdata }\n" +
-                       "{Header|uppercase}={Integer Header|multiword}\n" +
-                       "{Header|html}={Header Integer|multiword}\n" +
-                       "{Header|html}={Header Integer}\n" +
-                       "{.end}\n",
-
-               out: "HEADER=<77><Header>\n" +
-                       "Header=<Header><77>\n" +
-                       "Header=Header77\n",
-       },
-
-       &Test{
-               in: "{Raw}\n" +
-                       "{Raw|html}\n",
-
-               out: "&<>!@ #$%^\n" +
-                       "&amp;&lt;&gt;!@ #$%^\n",
-       },
-
        &Test{
                in: "{.section Emptystring}emptystring{.end}\n" +
                        "{.section Header}header{.end}\n",
@@ -380,12 +346,6 @@ var tests = []*Test{
                out: "1\n4\n",
        },
 
-       &Test{
-               in: "{Bytes}",
-
-               out: "hello",
-       },
-
        // Maps
 
        &Test{
@@ -499,7 +459,6 @@ func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
        s.HeaderPtr = &s.Header
        s.Integer = 77
        s.IntegerPtr = &s.Integer
-       s.Raw = "&<>!@ #$%^"
        s.InnerT = t1
        s.Data = []T{t1, t2}
        s.Pdata = []*T{&t1, &t2}
@@ -522,7 +481,6 @@ func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
        x := "pointedToString"
        s.Ptrmap["stringkey1"] = &x // the same value so repeated section is order-independent
        s.Ptrmap["stringkey2"] = &x
-       s.Bytes = []byte("hello")
        s.Iface = []int{1, 2, 3}
        s.Ifaceptr = &T{"Item", "Value"}
 
@@ -719,3 +677,87 @@ func TestReferenceToUnexported(t *testing.T) {
                t.Fatal("expected unexported error; got", err)
        }
 }
+
+var formatterTests = []Test{
+       {
+               in: "{Header|uppercase}={Integer|+1}\n" +
+                       "{Header|html}={Integer|str}\n",
+
+               out: "HEADER=78\n" +
+                       "Header=77\n",
+       },
+
+       {
+               in: "{Header|uppercase}={Integer Header|multiword}\n" +
+                       "{Header|html}={Header Integer|multiword}\n" +
+                       "{Header|html}={Header Integer}\n",
+
+               out: "HEADER=<77><Header>\n" +
+                       "Header=<Header><77>\n" +
+                       "Header=Header77\n",
+       },
+       {
+               in: "{Raw}\n" +
+                       "{Raw|html}\n",
+
+               out: "a <&> b\n" +
+                       "a &lt;&amp;&gt; b\n",
+       },
+       {
+               in:  "{Bytes}",
+               out: "hello",
+       },
+       {
+               in:  "{Raw|uppercase|html|html}",
+               out: "A &amp;lt;&amp;amp;&amp;gt; B",
+       },
+       {
+               in:  "{Header Integer|multiword|html}",
+               out: "&lt;Header&gt;&lt;77&gt;",
+       },
+       {
+               in:  "{Integer|no_formatter|html}",
+               err: `unknown formatter: "no_formatter"`,
+       },
+       {
+               in:  "{Integer|||||}", // empty string is a valid formatter
+               out: "77",
+       },
+}
+
+func TestFormatters(t *testing.T) {
+       data := map[string]interface{}{
+               "Header":  "Header",
+               "Integer": 77,
+               "Raw":     "a <&> b",
+               "Bytes":   []byte("hello"),
+       }
+       for _, c := range formatterTests {
+               tmpl, err := Parse(c.in, formatters)
+               if err != nil {
+                       if c.err == "" {
+                               t.Error("unexpected parse error:", err)
+                               continue
+                       }
+                       if strings.Index(err.String(), c.err) < 0 {
+                               t.Error("unexpected error: expected %q, got %q", c.err, err.String())
+                               continue
+                       }
+               } else {
+                       if c.err != "" {
+                               t.Errorf("For %q, expected error, got none.", c.in)
+                               continue
+                       }
+                       buf := bytes.NewBuffer(nil)
+                       err = tmpl.Execute(data, buf)
+                       if err != nil {
+                               t.Error("unexpected Execute error: ", err)
+                               continue
+                       }
+                       actual := buf.String()
+                       if actual != c.out {
+                               t.Errorf("for %q: expected %q but got %q.", c.in, c.out, actual)
+                       }
+               }
+       }
+}