summaryrefslogtreecommitdiff
path: root/src/pkg/template
diff options
context:
space:
mode:
authorOndřej Surý <ondrej@sury.org>2011-02-18 09:50:58 +0100
committerOndřej Surý <ondrej@sury.org>2011-02-18 09:50:58 +0100
commitc072558b90f1bbedc2022b0f30c8b1ac4712538e (patch)
tree67767591619e4bd8111fb05fac185cde94fb7378 /src/pkg/template
parent5859517b767c99749a45651c15d4bae5520ebae8 (diff)
downloadgolang-upstream/2011.02.15.tar.gz
Imported Upstream version 2011.02.15upstream/2011.02.15
Diffstat (limited to 'src/pkg/template')
-rw-r--r--src/pkg/template/template.go129
-rw-r--r--src/pkg/template/template_test.go186
2 files changed, 221 insertions, 94 deletions
diff --git a/src/pkg/template/template.go b/src/pkg/template/template.go
index 078463aaf..36fd06dc2 100644
--- a/src/pkg/template/template.go
+++ b/src/pkg/template/template.go
@@ -47,12 +47,20 @@
{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.
If it is not found, the search continues in outer sections
until the top level is reached.
+ If the field value is a pointer, leading asterisks indicate
+ that the value to be inserted should be evaluated through the
+ pointer. For example, if x.p is of type *int, {x.p} will
+ insert the value of the pointer but {*x.p} will insert the
+ value of the underlying integer. If the value is nil or not a
+ pointer, asterisks have no effect.
+
If a formatter is specified, it must be named in the formatter
map passed to the template set up routines or in the default
set ("html","str","") and is used to process the data for
@@ -62,10 +70,15 @@
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"
@@ -131,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
@@ -169,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
@@ -402,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.
@@ -633,6 +652,23 @@ func (t *Template) lookup(st *state, v reflect.Value, name string) reflect.Value
return v
}
+// indirectPtr returns the item numLevels levels of indirection below the value.
+// It is forgiving: if the value is not a pointer, it returns it rather than giving
+// an error. If the pointer is nil, it is returned as is.
+func indirectPtr(v reflect.Value, numLevels int) reflect.Value {
+ for i := numLevels; v != nil && i > 0; i++ {
+ if p, ok := v.(*reflect.PtrValue); ok {
+ if p.IsNil() {
+ return v
+ }
+ v = p.Elem()
+ } else {
+ break
+ }
+ }
+ return v
+}
+
// Walk v through pointers and interfaces, extracting the elements within.
func indirect(v reflect.Value) reflect.Value {
loop:
@@ -654,12 +690,16 @@ loop:
// The special name "@" (the "cursor") denotes the current data.
// The value coming in (st.data) might need indirecting to reach
// a struct while the return value is not indirected - that is,
-// it represents the actual named field.
+// it represents the actual named field. Leading stars indicate
+// levels of indirection to be applied to the value.
func (t *Template) findVar(st *state, s string) reflect.Value {
+ data := st.data
+ flattenedName := strings.TrimLeft(s, "*")
+ numStars := len(s) - len(flattenedName)
+ s = flattenedName
if s == "@" {
- return st.data
+ return indirectPtr(data, numStars)
}
- data := st.data
for _, elem := range strings.Split(s, ".", -1) {
// Look up field; data must be a struct or map.
data = t.lookup(st, data, elem)
@@ -667,7 +707,7 @@ func (t *Template) findVar(st *state, s string) reflect.Value {
return nil
}
}
- return data
+ return indirectPtr(data, numStars)
}
// Is there no data to look at?
@@ -705,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.
@@ -929,12 +972,12 @@ func (t *Template) ParseFile(filename string) (err os.Error) {
// Execute applies a parsed template to the specified data object,
// generating output to wr.
-func (t *Template) Execute(data interface{}, wr io.Writer) (err os.Error) {
+func (t *Template) Execute(wr io.Writer, data interface{}) (err os.Error) {
// Extract the driver data.
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
}
diff --git a/src/pkg/template/template_test.go b/src/pkg/template/template_test.go
index 3842b6d6b..d21a5397a 100644
--- a/src/pkg/template/template_test.go
+++ b/src/pkg/template/template_test.go
@@ -31,8 +31,10 @@ type U struct {
type S struct {
Header string
+ HeaderPtr *string
Integer int
- Raw string
+ IntegerPtr *int
+ NilPtr *int
InnerT T
InnerPointerT *T
Data []T
@@ -47,7 +49,7 @@ type S struct {
JSON interface{}
Innermap U
Stringmap map[string]string
- Bytes []byte
+ Ptrmap map[string]*string
Iface interface{}
Ifaceptr interface{}
}
@@ -118,6 +120,24 @@ var tests = []*Test{
out: "Header=77\n",
},
+ &Test{
+ in: "Pointers: {*HeaderPtr}={*IntegerPtr}\n",
+
+ out: "Pointers: Header=77\n",
+ },
+
+ &Test{
+ in: "Stars but not pointers: {*Header}={*Integer}\n",
+
+ out: "Stars but not pointers: Header=77\n",
+ },
+
+ &Test{
+ in: "nil pointer: {*NilPtr}={*Integer}\n",
+
+ out: "nil pointer: <nil>=77\n",
+ },
+
// Method at top level
&Test{
in: "ptrmethod={PointerMethod}\n",
@@ -312,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",
@@ -358,12 +346,6 @@ var tests = []*Test{
out: "1\n4\n",
},
- &Test{
- in: "{Bytes}",
-
- out: "hello",
- },
-
// Maps
&Test{
@@ -407,6 +389,20 @@ var tests = []*Test{
out: "\tstringresult\n" +
"\tstringresult\n",
},
+ &Test{
+ in: "{*Ptrmap.stringkey1}\n",
+
+ out: "pointedToString\n",
+ },
+ &Test{
+ in: "{.repeated section Ptrmap}\n" +
+ "{*@}\n" +
+ "{.end}",
+
+ out: "pointedToString\n" +
+ "pointedToString\n",
+ },
+
// Interface values
@@ -460,8 +456,9 @@ func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
s := new(S)
// initialized by hand for clarity.
s.Header = "Header"
+ s.HeaderPtr = &s.Header
s.Integer = 77
- s.Raw = "&<>!@ #$%^"
+ s.IntegerPtr = &s.Integer
s.InnerT = t1
s.Data = []T{t1, t2}
s.Pdata = []*T{&t1, &t2}
@@ -480,7 +477,10 @@ func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
s.Stringmap = make(map[string]string)
s.Stringmap["stringkey1"] = "stringresult" // the same value so repeated section is order-independent
s.Stringmap["stringkey2"] = "stringresult"
- s.Bytes = []byte("hello")
+ s.Ptrmap = make(map[string]*string)
+ x := "pointedToString"
+ s.Ptrmap["stringkey1"] = &x // the same value so repeated section is order-independent
+ s.Ptrmap["stringkey2"] = &x
s.Iface = []int{1, 2, 3}
s.Ifaceptr = &T{"Item", "Value"}
@@ -492,7 +492,7 @@ func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
t.Error("unexpected parse error: ", err)
continue
}
- err = tmpl.Execute(s, &buf)
+ err = tmpl.Execute(&buf, s)
if test.err == "" {
if err != nil {
t.Error("unexpected execute error:", err)
@@ -517,7 +517,7 @@ func TestMapDriverType(t *testing.T) {
t.Error("unexpected parse error:", err)
}
var b bytes.Buffer
- err = tmpl.Execute(mp, &b)
+ err = tmpl.Execute(&b, mp)
if err != nil {
t.Error("unexpected execute error:", err)
}
@@ -535,7 +535,7 @@ func TestMapNoEntry(t *testing.T) {
t.Error("unexpected parse error:", err)
}
var b bytes.Buffer
- err = tmpl.Execute(mp, &b)
+ err = tmpl.Execute(&b, mp)
if err != nil {
t.Error("unexpected execute error:", err)
}
@@ -552,7 +552,7 @@ func TestStringDriverType(t *testing.T) {
t.Error("unexpected parse error:", err)
}
var b bytes.Buffer
- err = tmpl.Execute("hello", &b)
+ err = tmpl.Execute(&b, "hello")
if err != nil {
t.Error("unexpected execute error:", err)
}
@@ -569,7 +569,7 @@ func TestTwice(t *testing.T) {
t.Error("unexpected parse error:", err)
}
var b bytes.Buffer
- err = tmpl.Execute("hello", &b)
+ err = tmpl.Execute(&b, "hello")
if err != nil {
t.Error("unexpected parse error:", err)
}
@@ -578,7 +578,7 @@ func TestTwice(t *testing.T) {
if s != expect {
t.Errorf("failed passing string as data: expected %q got %q", expect, s)
}
- err = tmpl.Execute("hello", &b)
+ err = tmpl.Execute(&b, "hello")
if err != nil {
t.Error("unexpected parse error:", err)
}
@@ -614,7 +614,7 @@ func TestCustomDelims(t *testing.T) {
continue
}
var b bytes.Buffer
- err = tmpl.Execute("hello", &b)
+ err = tmpl.Execute(&b, "hello")
s := b.String()
if s != "template: hello"+ldelim+rdelim {
t.Errorf("failed delim check(%q %q) %q got %q", ldelim, rdelim, text, s)
@@ -635,7 +635,7 @@ func TestVarIndirection(t *testing.T) {
if err != nil {
t.Fatal("unexpected parse error:", err)
}
- err = tmpl.Execute(s, &buf)
+ err = tmpl.Execute(&buf, s)
if err != nil {
t.Fatal("unexpected execute error:", err)
}
@@ -669,7 +669,7 @@ func TestReferenceToUnexported(t *testing.T) {
if err != nil {
t.Fatal("unexpected parse error:", err)
}
- err = tmpl.Execute(u, &buf)
+ err = tmpl.Execute(&buf, u)
if err == nil {
t.Fatal("expected execute error, got none")
}
@@ -677,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.Errorf("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(buf, data)
+ 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)
+ }
+ }
+ }
+}