diff options
Diffstat (limited to 'src/pkg/template')
-rw-r--r-- | src/pkg/template/Makefile | 2 | ||||
-rw-r--r-- | src/pkg/template/format.go | 22 | ||||
-rw-r--r-- | src/pkg/template/template.go | 178 | ||||
-rw-r--r-- | src/pkg/template/template_test.go | 295 |
4 files changed, 307 insertions, 190 deletions
diff --git a/src/pkg/template/Makefile b/src/pkg/template/Makefile index c9c79f799..4915527b4 100644 --- a/src/pkg/template/Makefile +++ b/src/pkg/template/Makefile @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -include ../../Make.$(GOARCH) +include ../../Make.inc TARG=template GOFILES=\ diff --git a/src/pkg/template/format.go b/src/pkg/template/format.go index 8a31de970..9156b0808 100644 --- a/src/pkg/template/format.go +++ b/src/pkg/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/src/pkg/template/template.go b/src/pkg/template/template.go index 11371abe7..a67dbf8ad 100644 --- a/src/pkg/template/template.go +++ b/src/pkg/template/template.go @@ -18,10 +18,11 @@ indirection. In the following, 'field' is one of several things, according to the data. - - the name of a field of a struct (result = data.field) - - the value stored in a map under that key (result = data[field]) - - the result of invoking a niladic single-valued method with that name - (result = data.field()) + + - The name of a field of a struct (result = data.field), + - The value stored in a map under that key (result = data[field]), or + - The result of invoking a niladic single-valued method with that name + (result = data.field()) Major constructs ({} are metacharacters; [] marks optional elements): @@ -43,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. @@ -54,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 @@ -68,6 +73,8 @@ import ( "os" "reflect" "strings" + "unicode" + "utf8" ) // Errors returned during parsing and execution. Users may extract the information and reformat @@ -100,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{ @@ -122,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 @@ -184,13 +191,19 @@ func New(fmap FormatterMap) *Template { // Report error and stop executing. The line number must be provided explicitly. func (t *Template) execError(st *state, line int, err string, args ...interface{}) { - panic(&Error{line, fmt.Sprintf(err, args)}) + panic(&Error{line, fmt.Sprintf(err, args...)}) } // Report error, panic to terminate parsing. // The line number comes from the template state. func (t *Template) parseError(err string, args ...interface{}) { - panic(&Error{t.linenum, fmt.Sprintf(err, args)}) + 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 @@ -216,12 +229,10 @@ func equal(s []byte, n int, t []byte) bool { // item is empty, we are at EOF. The item will be either a // delimited string or a non-empty string between delimited // strings. Tokens stop at (but include, if plain text) a newline. -// Action tokens on a line by themselves drop the white space on +// Action tokens on a line by themselves drop any space on // either side, up to and including the newline. func (t *Template) nextItem() []byte { - special := false // is this a {.foo} directive, which means trim white space? - // Delete surrounding white space if this {.foo} is the only thing on the line. - trimSpace := t.p == 0 || t.buf[t.p-1] == '\n' + startOfLine := t.p == 0 || t.buf[t.p-1] == '\n' start := t.p var i int newline := func() { @@ -234,13 +245,7 @@ func (t *Template) nextItem() []byte { break } } - if trimSpace { - start = i - } else if i > start { - // white space is valid text - t.p = i - return t.buf[start:i] - } + leadingSpace := i > start // What's left is nothing, newline, delimited string, or plain text Switch: switch { @@ -249,21 +254,50 @@ Switch: case t.buf[i] == '\n': newline() case equal(t.buf, i, t.ldelim): - i += len(t.ldelim) // position after delimiter - if i+1 < len(t.buf) && (t.buf[i] == '.' || t.buf[i] == '#') { - special = true - } + left := i // Start of left delimiter. + right := -1 // Will be (immediately after) right delimiter. + haveText := false // Delimiters contain text. + i += len(t.ldelim) + // Find the end of the action. for ; i < len(t.buf); i++ { if t.buf[i] == '\n' { break } if equal(t.buf, i, t.rdelim) { i += len(t.rdelim) - break Switch + right = i + break + } + haveText = true + } + if right < 0 { + t.parseError("unmatched opening delimiter") + return nil + } + // Is this a special action (starts with '.' or '#') and the only thing on the line? + if startOfLine && haveText { + firstChar := t.buf[left+len(t.ldelim)] + if firstChar == '.' || firstChar == '#' { + // It's special and the first thing on the line. Is it the last? + for j := right; j < len(t.buf) && white(t.buf[j]); j++ { + if t.buf[j] == '\n' { + // Yes it is. Drop the surrounding space and return the {.foo} + t.linenum++ + t.p = j + 1 + return t.buf[left:right] + } + } } } - t.parseError("unmatched opening delimiter") - return nil + // No it's not. If there's leading space, return that. + if leadingSpace { + // not trimming space: return leading white space if there is some. + t.p = left + return t.buf[start:left] + } + // Return the word, leave the trailing space. + start = left + break default: for ; i < len(t.buf); i++ { if t.buf[i] == '\n' { @@ -276,15 +310,6 @@ Switch: } } item := t.buf[start:i] - if special && trimSpace { - // consume trailing white space - for ; i < len(t.buf) && white(t.buf[i]); i++ { - if t.buf[i] == '\n' { - newline() - break // stop before newline - } - } - } t.p = i return item } @@ -305,15 +330,7 @@ func words(buf []byte) []string { if start == p { // no text left break } - if i == cap(s) { - ns := make([]string, 2*cap(s)) - for j := range s { - ns[j] = s[j] - } - s = ns - } - s = s[0 : i+1] - s[i] = string(buf[start:p]) + s = append(s, string(buf[start:p])) } return s } @@ -345,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 } @@ -388,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. @@ -443,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 @@ -577,17 +596,17 @@ 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 { for i := 0; i < n; i++ { m := typ.Method(i) mtyp := m.Type - // We must check receiver type because of a bug in the reflection type tables: - // it should not be possible to find a method with the wrong receiver type but - // this can happen due to value/pointer receiver mismatch. - if m.Name == name && mtyp.NumIn() == 1 && mtyp.NumOut() == 1 && mtyp.In(0) == typ { + 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] } } @@ -598,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)) @@ -630,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 } @@ -665,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()) @@ -684,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. @@ -888,6 +914,16 @@ func (t *Template) Parse(s string) (err os.Error) { return nil } +// ParseFile is like Parse but reads the template definition from the +// named file. +func (t *Template) ParseFile(filename string) (err os.Error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + return t.Parse(string(b)) +} + // 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) { diff --git a/src/pkg/template/template_test.go b/src/pkg/template/template_test.go index a6267bfcc..57f297e8f 100644 --- a/src/pkg/template/template_test.go +++ b/src/pkg/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{ @@ -98,51 +108,53 @@ var tests = []*Test{ &Test{" {.space} \n", " ", ""}, &Test{" {.tab} \n", "\t", ""}, &Test{" {#comment} \n", "", ""}, + &Test{"\tSome Text\t\n", "\tSome Text\t\n", ""}, + &Test{" {.meta-right} {.meta-right} {.meta-right} \n", " } } } \n", ""}, // 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" + @@ -151,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" + @@ -160,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" + @@ -169,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", @@ -181,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", @@ -198,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" + @@ -211,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" + @@ -221,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", @@ -232,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", @@ -243,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" + @@ -257,7 +276,7 @@ var tests = []*Test{ "ItemNumber2=ValueNumber2\n", }, &Test{ - in: "{.repeated section vec }\n" + + in: "{.repeated section Vec }\n" + "{@}\n" + "{.end}\n", @@ -266,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", @@ -296,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" + @@ -306,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", }, @@ -336,58 +367,66 @@ 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}", out: "stringresult\n" + "stringresult\n", }, + &Test{ + in: "{.repeated section Stringmap}\n" + + "\t{@}\n" + + "{.end}", + + out: "\tstringresult\n" + + "\tstringresult\n", + }, // 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", }, @@ -398,45 +437,59 @@ func TestAll(t *testing.T) { testAll(t, func(test *Test) (*Template, os.Error) { return Parse(test.in, formatters) }) // ParseFile testAll(t, func(test *Test) (*Template, os.Error) { - ioutil.WriteFile("_test/test.tmpl", []byte(test.in), 0600) + err := ioutil.WriteFile("_test/test.tmpl", []byte(test.in), 0600) + if err != nil { + t.Error("unexpected write error:", err) + return nil, err + } return ParseFile("_test/test.tmpl", formatters) }) + // tmpl.ParseFile + testAll(t, func(test *Test) (*Template, os.Error) { + err := ioutil.WriteFile("_test/test.tmpl", []byte(test.in), 0600) + if err != nil { + t.Error("unexpected write error:", err) + return nil, err + } + tmpl := New(formatters) + return tmpl, tmpl.ParseFile("_test/test.tmpl") + }) } 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 { buf.Reset() tmpl, err := parseFunc(test) if err != nil { - t.Error("unexpected parse error:", err) + t.Error("unexpected parse error: ", err) continue } err = tmpl.Execute(s, &buf) @@ -555,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) @@ -577,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) + } +} |