diff options
Diffstat (limited to 'libgo/go/template')
-rw-r--r-- | libgo/go/template/format.go | 22 | ||||
-rw-r--r-- | libgo/go/template/template.go | 74 | ||||
-rw-r--r-- | libgo/go/template/template_test.go | 269 |
3 files changed, 223 insertions, 142 deletions
diff --git a/libgo/go/template/format.go b/libgo/go/template/format.go index 8a31de970a3..9156b080816 100644 --- a/libgo/go/template/format.go +++ b/libgo/go/template/format.go @@ -16,12 +16,14 @@ import ( // It is stored under the name "str" and is the default formatter. // You can override the default formatter by storing your default // under the name "" in your custom formatter map. -func StringFormatter(w io.Writer, value interface{}, format string) { - if b, ok := value.([]byte); ok { - w.Write(b) - return +func StringFormatter(w io.Writer, format string, value ...interface{}) { + if len(value) == 1 { + if b, ok := value[0].([]byte); ok { + w.Write(b) + return + } } - fmt.Fprint(w, value) + fmt.Fprint(w, value...) } var ( @@ -60,11 +62,15 @@ func HTMLEscape(w io.Writer, s []byte) { } // HTMLFormatter formats arbitrary values for HTML -func HTMLFormatter(w io.Writer, value interface{}, format string) { - b, ok := value.([]byte) +func HTMLFormatter(w io.Writer, format string, value ...interface{}) { + ok := false + var b []byte + if len(value) == 1 { + b, ok = value[0].([]byte) + } if !ok { var buf bytes.Buffer - fmt.Fprint(&buf, value) + fmt.Fprint(&buf, value...) b = buf.Bytes() } HTMLEscape(w, b) diff --git a/libgo/go/template/template.go b/libgo/go/template/template.go index 082c06261b0..a67dbf8ad24 100644 --- a/libgo/go/template/template.go +++ b/libgo/go/template/template.go @@ -44,9 +44,11 @@ is present, ZZZ is executed between iterations of XXX. {field} + {field1 field2 ...} {field|formatter} + {field1 field2...|formatter} - Insert the value of the field into the output. Field is + 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. @@ -55,9 +57,11 @@ map passed to the template set up routines or in the default set ("html","str","") and is used to process the data for output. The formatter function has signature - func(wr io.Writer, data interface{}, formatter string) - where wr is the destination for output, data is the field - value, and formatter is its name at the invocation site. + func(wr io.Writer, formatter string, data ...interface{}) + where wr is the destination for output, data holds the field + values at the instantiation, and formatter is its name at + the invocation site. The default formatter just concatenates + the string representations of the fields. */ package template @@ -69,6 +73,8 @@ import ( "os" "reflect" "strings" + "unicode" + "utf8" ) // Errors returned during parsing and execution. Users may extract the information and reformat @@ -101,7 +107,7 @@ const ( // FormatterMap is the type describing the mapping from formatter // names to the functions that implement them. -type FormatterMap map[string]func(io.Writer, interface{}, string) +type FormatterMap map[string]func(io.Writer, string, ...interface{}) // Built-in formatters. var builtins = FormatterMap{ @@ -123,11 +129,11 @@ type literalElement struct { text []byte } -// A variable to be evaluated +// A variable invocation to be evaluated type variableElement struct { linenum int - name string - formatter string // TODO(r): implement pipelines + word []string // The fields in the invocation. + formatter string // TODO(r): implement pipelines } // A .section block, possibly with a .or @@ -194,6 +200,12 @@ func (t *Template) parseError(err string, args ...interface{}) { panic(&Error{t.linenum, fmt.Sprintf(err, args...)}) } +// Is this an exported - upper case - name? +func isExported(name string) bool { + rune, _ := utf8.DecodeRuneInString(name) + return unicode.IsUpper(rune) +} + // -- Lexical analysis // Is c a white space character? @@ -350,7 +362,7 @@ func (t *Template) analyze(item []byte) (tok int, w []string) { t.parseError("empty directive") return } - if len(w) == 1 && w[0][0] != '.' { + if len(w) > 0 && w[0][0] != '.' { tok = tokVariable return } @@ -393,16 +405,18 @@ func (t *Template) analyze(item []byte) (tok int, w []string) { // -- Parsing // Allocate a new variable-evaluation element. -func (t *Template) newVariable(name_formatter string) (v *variableElement) { - name := name_formatter +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 := "" - bar := strings.Index(name_formatter, "|") + lastWord := words[len(words)-1] + bar := strings.Index(lastWord, "|") if bar >= 0 { - name = name_formatter[0:bar] - formatter = name_formatter[bar+1:] + words[len(words)-1] = lastWord[0:bar] + formatter = lastWord[bar+1:] } // Probably ok, so let's build it. - v = &variableElement{t.linenum, name, formatter} + 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. @@ -448,7 +462,7 @@ func (t *Template) parseSimple(item []byte) (done bool, tok int, w []string) { } return case tokVariable: - t.elems.Push(t.newVariable(w[0])) + t.elems.Push(t.newVariable(w)) return } return false, tok, w @@ -582,7 +596,7 @@ func (t *Template) parse() { // Evaluate interfaces and pointers looking for a value that can look up the name, via a // struct field, method, or map key, and return the result of the lookup. -func lookup(v reflect.Value, name string) reflect.Value { +func (t *Template) lookup(st *state, v reflect.Value, name string) reflect.Value { for v != nil { typ := v.Type() if n := v.Type().NumMethod(); n > 0 { @@ -590,6 +604,9 @@ func lookup(v reflect.Value, name string) reflect.Value { m := typ.Method(i) mtyp := m.Type if m.Name == name && mtyp.NumIn() == 1 && mtyp.NumOut() == 1 { + if !isExported(name) { + t.execError(st, t.linenum, "name not exported: %s in type %s", name, st.data.Type()) + } return v.Method(i).Call(nil)[0] } } @@ -600,6 +617,9 @@ func lookup(v reflect.Value, name string) reflect.Value { case *reflect.InterfaceValue: v = av.Elem() case *reflect.StructValue: + if !isExported(name) { + t.execError(st, t.linenum, "name not exported: %s in type %s", name, st.data.Type()) + } return av.FieldByName(name) case *reflect.MapValue: return av.Elem(reflect.NewValue(name)) @@ -632,14 +652,14 @@ loop: // 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. -func (st *state) findVar(s string) reflect.Value { +func (t *Template) findVar(st *state, s string) reflect.Value { if s == "@" { return st.data } data := st.data for _, elem := range strings.Split(s, ".", -1) { // Look up field; data must be a struct or map. - data = lookup(data, elem) + data = t.lookup(st, data, elem) if data == nil { return nil } @@ -667,12 +687,12 @@ func empty(v reflect.Value) bool { case *reflect.SliceValue: return v.Len() == 0 } - return true + return false } // Look up a variable or method, up through the parent if necessary. func (t *Template) varValue(name string, st *state) reflect.Value { - field := st.findVar(name) + field := t.findVar(st, name) if field == nil { if st.parent == nil { t.execError(st, t.linenum, "name not found: %s in type %s", name, st.data.Type()) @@ -686,20 +706,24 @@ func (t *Template) varValue(name string, st *state) reflect.Value { // If it has a formatter attached ({var|formatter}) run that too. func (t *Template) writeVariable(v *variableElement, st *state) { formatter := v.formatter - val := t.varValue(v.name, st).Interface() + // 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, val, formatter) + fn(st.wr, formatter, val...) return } } // is it in builtin map? if fn, ok := builtins[formatter]; ok { - fn(st.wr, val, formatter) + fn(st.wr, formatter, val...) return } - t.execError(st, v.linenum, "missing formatter %s for variable %s", formatter, v.name) + t.execError(st, v.linenum, "missing formatter %s for variable %s", formatter, v.word[0]) } // Execute element i. Return next index to execute. diff --git a/libgo/go/template/template_test.go b/libgo/go/template/template_test.go index 00fd69a0296..57f297e8f0a 100644 --- a/libgo/go/template/template_test.go +++ b/libgo/go/template/template_test.go @@ -12,6 +12,7 @@ import ( "io/ioutil" "json" "os" + "strings" "testing" ) @@ -20,40 +21,40 @@ type Test struct { } type T struct { - item string - value string + Item string + Value string } type U struct { - mp map[string]int + Mp map[string]int } type S struct { - header string - integer int - raw string - innerT T - innerPointerT *T - data []T - pdata []*T - empty []*T - emptystring string - null []*T - vec *vector.Vector - true bool - false bool - mp map[string]string - json interface{} - innermap U - stringmap map[string]string - bytes []byte - iface interface{} - ifaceptr interface{} + Header string + Integer int + Raw string + InnerT T + InnerPointerT *T + Data []T + Pdata []*T + Empty []*T + Emptystring string + Null []*T + Vec *vector.Vector + True bool + False bool + Mp map[string]string + JSON interface{} + Innermap U + Stringmap map[string]string + Bytes []byte + Iface interface{} + Ifaceptr interface{} } -func (s *S) pointerMethod() string { return "ptrmethod!" } +func (s *S) PointerMethod() string { return "ptrmethod!" } -func (s S) valueMethod() string { return "valmethod!" } +func (s S) ValueMethod() string { return "valmethod!" } var t1 = T{"ItemNumber1", "ValueNumber1"} var t2 = T{"ItemNumber2", "ValueNumber2"} @@ -76,16 +77,25 @@ func plus1(v interface{}) string { return fmt.Sprint(i + 1) } -func writer(f func(interface{}) string) func(io.Writer, interface{}, string) { - return func(w io.Writer, v interface{}, format string) { - io.WriteString(w, f(v)) +func writer(f func(interface{}) string) func(io.Writer, string, ...interface{}) { + return func(w io.Writer, format string, v ...interface{}) { + if len(v) != 1 { + panic("test writer expected one arg") + } + io.WriteString(w, f(v[0])) } } +func multiword(w io.Writer, format string, value ...interface{}) { + for _, v := range value { + fmt.Fprintf(w, "<%v>", v) + } +} var formatters = FormatterMap{ "uppercase": writer(uppercase), "+1": writer(plus1), + "multiword": multiword, } var tests = []*Test{ @@ -103,48 +113,48 @@ var tests = []*Test{ // Variables at top level &Test{ - in: "{header}={integer}\n", + in: "{Header}={Integer}\n", out: "Header=77\n", }, // Method at top level &Test{ - in: "ptrmethod={pointerMethod}\n", + in: "ptrmethod={PointerMethod}\n", out: "ptrmethod=ptrmethod!\n", }, &Test{ - in: "valmethod={valueMethod}\n", + in: "valmethod={ValueMethod}\n", out: "valmethod=valmethod!\n", }, // Section &Test{ - in: "{.section data }\n" + + in: "{.section Data }\n" + "some text for the section\n" + "{.end}\n", out: "some text for the section\n", }, &Test{ - in: "{.section data }\n" + - "{header}={integer}\n" + + in: "{.section Data }\n" + + "{Header}={Integer}\n" + "{.end}\n", out: "Header=77\n", }, &Test{ - in: "{.section pdata }\n" + - "{header}={integer}\n" + + in: "{.section Pdata }\n" + + "{Header}={Integer}\n" + "{.end}\n", out: "Header=77\n", }, &Test{ - in: "{.section pdata }\n" + + in: "{.section Pdata }\n" + "data present\n" + "{.or}\n" + "data not present\n" + @@ -153,7 +163,7 @@ var tests = []*Test{ out: "data present\n", }, &Test{ - in: "{.section empty }\n" + + in: "{.section Empty }\n" + "data present\n" + "{.or}\n" + "data not present\n" + @@ -162,7 +172,7 @@ var tests = []*Test{ out: "data not present\n", }, &Test{ - in: "{.section null }\n" + + in: "{.section Null }\n" + "data present\n" + "{.or}\n" + "data not present\n" + @@ -171,10 +181,10 @@ var tests = []*Test{ out: "data not present\n", }, &Test{ - in: "{.section pdata }\n" + - "{header}={integer}\n" + + in: "{.section Pdata }\n" + + "{Header}={Integer}\n" + "{.section @ }\n" + - "{header}={integer}\n" + + "{Header}={Integer}\n" + "{.end}\n" + "{.end}\n", @@ -183,16 +193,23 @@ var tests = []*Test{ }, &Test{ - in: "{.section data}{.end} {header}\n", + in: "{.section Data}{.end} {Header}\n", out: " Header\n", }, + &Test{ + in: "{.section Integer}{@}{.end}", + + out: "77", + }, + + // Repeated &Test{ - in: "{.section pdata }\n" + + in: "{.section Pdata }\n" + "{.repeated section @ }\n" + - "{item}={value}\n" + + "{Item}={Value}\n" + "{.end}\n" + "{.end}\n", @@ -200,9 +217,9 @@ var tests = []*Test{ "ItemNumber2=ValueNumber2\n", }, &Test{ - in: "{.section pdata }\n" + + in: "{.section Pdata }\n" + "{.repeated section @ }\n" + - "{item}={value}\n" + + "{Item}={Value}\n" + "{.or}\n" + "this should not appear\n" + "{.end}\n" + @@ -213,8 +230,8 @@ var tests = []*Test{ }, &Test{ in: "{.section @ }\n" + - "{.repeated section empty }\n" + - "{item}={value}\n" + + "{.repeated section Empty }\n" + + "{Item}={Value}\n" + "{.or}\n" + "this should appear: empty field\n" + "{.end}\n" + @@ -223,8 +240,8 @@ var tests = []*Test{ out: "this should appear: empty field\n", }, &Test{ - in: "{.repeated section pdata }\n" + - "{item}\n" + + in: "{.repeated section Pdata }\n" + + "{Item}\n" + "{.alternates with}\n" + "is\nover\nmultiple\nlines\n" + "{.end}\n", @@ -234,8 +251,8 @@ var tests = []*Test{ "ItemNumber2\n", }, &Test{ - in: "{.repeated section pdata }\n" + - "{item}\n" + + in: "{.repeated section Pdata }\n" + + "{Item}\n" + "{.alternates with}\n" + "is\nover\nmultiple\nlines\n" + " {.end}\n", @@ -245,9 +262,9 @@ var tests = []*Test{ "ItemNumber2\n", }, &Test{ - in: "{.section pdata }\n" + + in: "{.section Pdata }\n" + "{.repeated section @ }\n" + - "{item}={value}\n" + + "{Item}={Value}\n" + "{.alternates with}DIVIDER\n" + "{.or}\n" + "this should not appear\n" + @@ -259,7 +276,7 @@ var tests = []*Test{ "ItemNumber2=ValueNumber2\n", }, &Test{ - in: "{.repeated section vec }\n" + + in: "{.repeated section Vec }\n" + "{@}\n" + "{.end}\n", @@ -268,28 +285,28 @@ var tests = []*Test{ }, // Same but with a space before {.end}: was a bug. &Test{ - in: "{.repeated section vec }\n" + + in: "{.repeated section Vec }\n" + "{@} {.end}\n", out: "elt1 elt2 \n", }, &Test{ - in: "{.repeated section integer}{.end}", + in: "{.repeated section Integer}{.end}", - err: "line 1: .repeated: cannot repeat integer (type int)", + err: "line 1: .repeated: cannot repeat Integer (type int)", }, // Nested names &Test{ in: "{.section @ }\n" + - "{innerT.item}={innerT.value}\n" + + "{InnerT.Item}={InnerT.Value}\n" + "{.end}", out: "ItemNumber1=ValueNumber1\n", }, &Test{ in: "{.section @ }\n" + - "{innerT.item}={.section innerT}{.section value}{@}{.end}{.end}\n" + + "{InnerT.Item}={.section InnerT}{.section Value}{@}{.end}{.end}\n" + "{.end}", out: "ItemNumber1=ValueNumber1\n", @@ -298,9 +315,9 @@ var tests = []*Test{ // Formatters &Test{ - in: "{.section pdata }\n" + - "{header|uppercase}={integer|+1}\n" + - "{header|html}={integer|str}\n" + + in: "{.section Pdata }\n" + + "{Header|uppercase}={Integer|+1}\n" + + "{Header|html}={Integer|str}\n" + "{.end}\n", out: "HEADER=78\n" + @@ -308,29 +325,41 @@ var tests = []*Test{ }, &Test{ - in: "{raw}\n" + - "{raw|html}\n", + 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" + "&<>!@ #$%^\n", }, &Test{ - in: "{.section emptystring}emptystring{.end}\n" + - "{.section header}header{.end}\n", + in: "{.section Emptystring}emptystring{.end}\n" + + "{.section Header}header{.end}\n", out: "\nheader\n", }, &Test{ - in: "{.section true}1{.or}2{.end}\n" + - "{.section false}3{.or}4{.end}\n", + in: "{.section True}1{.or}2{.end}\n" + + "{.section False}3{.or}4{.end}\n", out: "1\n4\n", }, &Test{ - in: "{bytes}", + in: "{Bytes}", out: "hello", }, @@ -338,32 +367,32 @@ var tests = []*Test{ // Maps &Test{ - in: "{mp.mapkey}\n", + in: "{Mp.mapkey}\n", out: "Ahoy!\n", }, &Test{ - in: "{innermap.mp.innerkey}\n", + in: "{Innermap.Mp.innerkey}\n", out: "55\n", }, &Test{ - in: "{.section innermap}{.section mp}{innerkey}{.end}{.end}\n", + in: "{.section Innermap}{.section Mp}{innerkey}{.end}{.end}\n", out: "55\n", }, &Test{ - in: "{.section json}{.repeated section maps}{a}{b}{.end}{.end}\n", + in: "{.section JSON}{.repeated section maps}{a}{b}{.end}{.end}\n", out: "1234\n", }, &Test{ - in: "{stringmap.stringkey1}\n", + in: "{Stringmap.stringkey1}\n", out: "stringresult\n", }, &Test{ - in: "{.repeated section stringmap}\n" + + in: "{.repeated section Stringmap}\n" + "{@}\n" + "{.end}", @@ -371,7 +400,7 @@ var tests = []*Test{ "stringresult\n", }, &Test{ - in: "{.repeated section stringmap}\n" + + in: "{.repeated section Stringmap}\n" + "\t{@}\n" + "{.end}", @@ -382,22 +411,22 @@ var tests = []*Test{ // Interface values &Test{ - in: "{iface}", + in: "{Iface}", out: "[1 2 3]", }, &Test{ - in: "{.repeated section iface}{@}{.alternates with} {.end}", + in: "{.repeated section Iface}{@}{.alternates with} {.end}", out: "1 2 3", }, &Test{ - in: "{.section iface}{@}{.end}", + in: "{.section Iface}{@}{.end}", out: "[1 2 3]", }, &Test{ - in: "{.section ifaceptr}{item} {value}{.end}", + in: "{.section Ifaceptr}{Item} {Value}{.end}", out: "Item Value", }, @@ -430,30 +459,30 @@ func TestAll(t *testing.T) { func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) { s := new(S) // initialized by hand for clarity. - s.header = "Header" - s.integer = 77 - s.raw = "&<>!@ #$%^" - s.innerT = t1 - s.data = []T{t1, t2} - s.pdata = []*T{&t1, &t2} - s.empty = []*T{} - s.null = nil - s.vec = new(vector.Vector) - s.vec.Push("elt1") - s.vec.Push("elt2") - s.true = true - s.false = false - s.mp = make(map[string]string) - s.mp["mapkey"] = "Ahoy!" - json.Unmarshal([]byte(`{"maps":[{"a":1,"b":2},{"a":3,"b":4}]}`), &s.json) - s.innermap.mp = make(map[string]int) - s.innermap.mp["innerkey"] = 55 - 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.iface = []int{1, 2, 3} - s.ifaceptr = &T{"Item", "Value"} + s.Header = "Header" + s.Integer = 77 + s.Raw = "&<>!@ #$%^" + s.InnerT = t1 + s.Data = []T{t1, t2} + s.Pdata = []*T{&t1, &t2} + s.Empty = []*T{} + s.Null = nil + s.Vec = new(vector.Vector) + s.Vec.Push("elt1") + s.Vec.Push("elt2") + s.True = true + s.False = false + s.Mp = make(map[string]string) + s.Mp["mapkey"] = "Ahoy!" + json.Unmarshal([]byte(`{"maps":[{"a":1,"b":2},{"a":3,"b":4}]}`), &s.JSON) + s.Innermap.Mp = make(map[string]int) + s.Innermap.Mp["innerkey"] = 55 + 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.Iface = []int{1, 2, 3} + s.Ifaceptr = &T{"Item", "Value"} var buf bytes.Buffer for _, test := range tests { @@ -579,10 +608,10 @@ func TestCustomDelims(t *testing.T) { func TestVarIndirection(t *testing.T) { s := new(S) // initialized by hand for clarity. - s.innerPointerT = &t1 + s.InnerPointerT = &t1 var buf bytes.Buffer - input := "{.section @}{innerPointerT}{.end}" + input := "{.section @}{InnerPointerT}{.end}" tmpl, err := Parse(input, nil) if err != nil { t.Fatal("unexpected parse error:", err) @@ -601,9 +630,31 @@ func TestHTMLFormatterWithByte(t *testing.T) { s := "Test string." b := []byte(s) var buf bytes.Buffer - HTMLFormatter(&buf, b, "") + HTMLFormatter(&buf, "", b) bs := buf.String() if bs != s { t.Errorf("munged []byte, expected: %s got: %s", s, bs) } } + +type UF struct { + I int + s string +} + +func TestReferenceToUnexported(t *testing.T) { + u := &UF{3, "hello"} + var buf bytes.Buffer + input := "{.section @}{I}{s}{.end}" + tmpl, err := Parse(input, nil) + if err != nil { + t.Fatal("unexpected parse error:", err) + } + err = tmpl.Execute(u, &buf) + if err == nil { + t.Fatal("expected execute error, got none") + } + if strings.Index(err.String(), "not exported") < 0 { + t.Fatal("expected unexported error; got", err) + } +} |