mirror of
https://github.com/golang/go
synced 2024-11-21 17:54:39 -07:00
template: Add simple formatter chaining.
Fixes #676. R=r, rsc, r2 CC=golang-dev https://golang.org/cl/4127043
This commit is contained in:
parent
eeafc06538
commit
42973dddf4
@ -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.
|
||||
@ -69,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"
|
||||
@ -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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
// is it in builtin map?
|
||||
if fn, ok := builtins[formatter]; ok {
|
||||
fn(st.wr, formatter, val...)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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" +
|
||||
"&<>!@ #$%^\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 <&> b\n",
|
||||
},
|
||||
{
|
||||
in: "{Bytes}",
|
||||
out: "hello",
|
||||
},
|
||||
{
|
||||
in: "{Raw|uppercase|html|html}",
|
||||
out: "A &lt;&amp;&gt; B",
|
||||
},
|
||||
{
|
||||
in: "{Header Integer|multiword|html}",
|
||||
out: "<Header><77>",
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user