mirror of
https://github.com/golang/go
synced 2024-11-25 03:07:56 -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 ...}
|
{field1 field2 ...}
|
||||||
{field|formatter}
|
{field|formatter}
|
||||||
{field1 field2...|formatter}
|
{field1 field2...|formatter}
|
||||||
|
{field|formatter1|formatter2}
|
||||||
|
|
||||||
Insert the value of the fields into the output. Each 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.
|
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
|
values at the instantiation, and formatter is its name at
|
||||||
the invocation site. The default formatter just concatenates
|
the invocation site. The default formatter just concatenates
|
||||||
the string representations of the fields.
|
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
|
package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"container/vector"
|
"container/vector"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -138,9 +144,9 @@ type literalElement struct {
|
|||||||
|
|
||||||
// A variable invocation to be evaluated
|
// A variable invocation to be evaluated
|
||||||
type variableElement struct {
|
type variableElement struct {
|
||||||
linenum int
|
linenum int
|
||||||
word []string // The fields in the invocation.
|
word []string // The fields in the invocation.
|
||||||
formatter string // TODO(r): implement pipelines
|
fmts []string // Names of formatters to apply. len(fmts) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// A .section block, possibly with a .or
|
// 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.
|
// the data item descends into the fields associated with sections, etc.
|
||||||
// Parent is used to walk upwards to find variables higher in the tree.
|
// Parent is used to walk upwards to find variables higher in the tree.
|
||||||
type state struct {
|
type state struct {
|
||||||
parent *state // parent in hierarchy
|
parent *state // parent in hierarchy
|
||||||
data reflect.Value // the driver data for this section etc.
|
data reflect.Value // the driver data for this section etc.
|
||||||
wr io.Writer // where to send output
|
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 {
|
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
|
// 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
|
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
|
// -- Parsing
|
||||||
|
|
||||||
// Allocate a new variable-evaluation element.
|
// Allocate a new variable-evaluation element.
|
||||||
func (t *Template) newVariable(words []string) (v *variableElement) {
|
func (t *Template) newVariable(words []string) *variableElement {
|
||||||
// The words are tokenized elements from the {item}. The last one may be of
|
// After the final space-separated argument, formatters may be specified separated
|
||||||
// the form "|fmt". For example: {a b c|d}
|
// by pipe symbols, for example: {a b c|d|e}
|
||||||
formatter := ""
|
|
||||||
|
// Until we learn otherwise, formatters contains a single name: "", the default formatter.
|
||||||
|
formatters := []string{""}
|
||||||
lastWord := words[len(words)-1]
|
lastWord := words[len(words)-1]
|
||||||
bar := strings.Index(lastWord, "|")
|
bar := strings.IndexRune(lastWord, '|')
|
||||||
if bar >= 0 {
|
if bar >= 0 {
|
||||||
words[len(words)-1] = lastWord[0:bar]
|
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,
|
// 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.
|
// but it's more dynamic to let the user change the map contents underfoot.
|
||||||
// We do require the name to be present, though.
|
// We do require the name to be present, though.
|
||||||
|
|
||||||
// Is it in user-supplied map?
|
// Is it in user-supplied map?
|
||||||
if t.fmap != nil {
|
for _, f := range formatters {
|
||||||
if _, ok := t.fmap[formatter]; ok {
|
if t.formatter(f) == nil {
|
||||||
return
|
t.parseError("unknown formatter: %q", f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Is it in builtin map?
|
return &variableElement{t.linenum, words, formatters}
|
||||||
if _, ok := builtins[formatter]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.parseError("unknown formatter: %s", formatter)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab the next item. If it's simple, just append it to the template.
|
// 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
|
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.
|
// Evaluate a variable, looking up through the parent if necessary.
|
||||||
// If it has a formatter attached ({var|formatter}) run that too.
|
// If it has a formatter attached ({var|formatter}) run that too.
|
||||||
func (t *Template) writeVariable(v *variableElement, st *state) {
|
func (t *Template) writeVariable(v *variableElement, st *state) {
|
||||||
formatter := v.formatter
|
|
||||||
// Turn the words of the invocation into values.
|
// Turn the words of the invocation into values.
|
||||||
val := make([]interface{}, len(v.word))
|
val := make([]interface{}, len(v.word))
|
||||||
for i, word := range v.word {
|
for i, word := range v.word {
|
||||||
val[i] = t.varValue(word, st).Interface()
|
val[i] = t.varValue(word, st).Interface()
|
||||||
}
|
}
|
||||||
// is it in user-supplied map?
|
|
||||||
if t.fmap != nil {
|
for i, fmt := range v.fmts[:len(v.fmts)-1] {
|
||||||
if fn, ok := t.fmap[formatter]; ok {
|
b := &st.buf[i&1]
|
||||||
fn(st.wr, formatter, val...)
|
b.Reset()
|
||||||
return
|
t.format(b, fmt, val, v, st)
|
||||||
}
|
val = val[0:1]
|
||||||
|
val[0] = b.Bytes()
|
||||||
}
|
}
|
||||||
// is it in builtin map?
|
t.format(st.wr, v.fmts[len(v.fmts)-1], val, v, st)
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute element i. Return next index to execute.
|
// 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)
|
val := reflect.NewValue(data)
|
||||||
defer checkError(&err)
|
defer checkError(&err)
|
||||||
t.p = 0
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ type S struct {
|
|||||||
Integer int
|
Integer int
|
||||||
IntegerPtr *int
|
IntegerPtr *int
|
||||||
NilPtr *int
|
NilPtr *int
|
||||||
Raw string
|
|
||||||
InnerT T
|
InnerT T
|
||||||
InnerPointerT *T
|
InnerPointerT *T
|
||||||
Data []T
|
Data []T
|
||||||
@ -51,7 +50,6 @@ type S struct {
|
|||||||
Innermap U
|
Innermap U
|
||||||
Stringmap map[string]string
|
Stringmap map[string]string
|
||||||
Ptrmap map[string]*string
|
Ptrmap map[string]*string
|
||||||
Bytes []byte
|
|
||||||
Iface interface{}
|
Iface interface{}
|
||||||
Ifaceptr interface{}
|
Ifaceptr interface{}
|
||||||
}
|
}
|
||||||
@ -334,38 +332,6 @@ var tests = []*Test{
|
|||||||
out: "ItemNumber1=ValueNumber1\n",
|
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{
|
&Test{
|
||||||
in: "{.section Emptystring}emptystring{.end}\n" +
|
in: "{.section Emptystring}emptystring{.end}\n" +
|
||||||
"{.section Header}header{.end}\n",
|
"{.section Header}header{.end}\n",
|
||||||
@ -380,12 +346,6 @@ var tests = []*Test{
|
|||||||
out: "1\n4\n",
|
out: "1\n4\n",
|
||||||
},
|
},
|
||||||
|
|
||||||
&Test{
|
|
||||||
in: "{Bytes}",
|
|
||||||
|
|
||||||
out: "hello",
|
|
||||||
},
|
|
||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
|
|
||||||
&Test{
|
&Test{
|
||||||
@ -499,7 +459,6 @@ func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
|
|||||||
s.HeaderPtr = &s.Header
|
s.HeaderPtr = &s.Header
|
||||||
s.Integer = 77
|
s.Integer = 77
|
||||||
s.IntegerPtr = &s.Integer
|
s.IntegerPtr = &s.Integer
|
||||||
s.Raw = "&<>!@ #$%^"
|
|
||||||
s.InnerT = t1
|
s.InnerT = t1
|
||||||
s.Data = []T{t1, t2}
|
s.Data = []T{t1, t2}
|
||||||
s.Pdata = []*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"
|
x := "pointedToString"
|
||||||
s.Ptrmap["stringkey1"] = &x // the same value so repeated section is order-independent
|
s.Ptrmap["stringkey1"] = &x // the same value so repeated section is order-independent
|
||||||
s.Ptrmap["stringkey2"] = &x
|
s.Ptrmap["stringkey2"] = &x
|
||||||
s.Bytes = []byte("hello")
|
|
||||||
s.Iface = []int{1, 2, 3}
|
s.Iface = []int{1, 2, 3}
|
||||||
s.Ifaceptr = &T{"Item", "Value"}
|
s.Ifaceptr = &T{"Item", "Value"}
|
||||||
|
|
||||||
@ -719,3 +677,87 @@ func TestReferenceToUnexported(t *testing.T) {
|
|||||||
t.Fatal("expected unexported error; got", err)
|
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