mirror of
https://github.com/golang/go
synced 2024-11-25 05:47:57 -07:00
template: support string, int and float literals
This enables customizing the behavior of formatters with logic such as {"template"|import} or even {Field1 Field2 "%.2f 0x%X"|printf} Thanks to Roger Peppe for some debate on this. R=golang-dev, r, r CC=golang-dev https://golang.org/cl/4536059
This commit is contained in:
parent
bddd092dc3
commit
29b246c644
@ -76,6 +76,10 @@
|
|||||||
executed sequentially, with each formatter receiving the bytes
|
executed sequentially, with each formatter receiving the bytes
|
||||||
emitted by the one to its left.
|
emitted by the one to its left.
|
||||||
|
|
||||||
|
As well as field names, one may use literals with Go syntax.
|
||||||
|
Integer, floating-point, and string literals are supported.
|
||||||
|
Raw strings may not span newlines.
|
||||||
|
|
||||||
The delimiter strings get their default value, "{" and "}", from
|
The delimiter strings get their default value, "{" and "}", from
|
||||||
JSON-template. They may be set to any non-empty, space-free
|
JSON-template. They may be set to any non-empty, space-free
|
||||||
string using the SetDelims method. Their value can be printed
|
string using the SetDelims method. Their value can be printed
|
||||||
@ -91,6 +95,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"utf8"
|
"utf8"
|
||||||
@ -151,10 +156,13 @@ 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.
|
args []interface{} // The fields and literals in the invocation.
|
||||||
fmts []string // Names of formatters to apply. len(fmts) > 0
|
fmts []string // Names of formatters to apply. len(fmts) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A variableElement arg to be evaluated as a field name
|
||||||
|
type fieldName string
|
||||||
|
|
||||||
// A .section block, possibly with a .or
|
// A .section block, possibly with a .or
|
||||||
type sectionElement struct {
|
type sectionElement struct {
|
||||||
linenum int // of .section itself
|
linenum int // of .section itself
|
||||||
@ -245,6 +253,31 @@ func equal(s []byte, n int, t []byte) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isQuote returns true if c is a string- or character-delimiting quote character.
|
||||||
|
func isQuote(c byte) bool {
|
||||||
|
return c == '"' || c == '`' || c == '\''
|
||||||
|
}
|
||||||
|
|
||||||
|
// endQuote returns the end quote index for the quoted string that
|
||||||
|
// starts at n, or -1 if no matching end quote is found before the end
|
||||||
|
// of the line.
|
||||||
|
func endQuote(s []byte, n int) int {
|
||||||
|
quote := s[n]
|
||||||
|
for n++; n < len(s); n++ {
|
||||||
|
switch s[n] {
|
||||||
|
case '\\':
|
||||||
|
if quote == '"' || quote == '\'' {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
case '\n':
|
||||||
|
return -1
|
||||||
|
case quote:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
// nextItem returns the next item from the input buffer. If the returned
|
// nextItem returns the next item from the input buffer. If the returned
|
||||||
// item is empty, we are at EOF. The item will be either a
|
// item is empty, we are at EOF. The item will be either a
|
||||||
// delimited string or a non-empty string between delimited
|
// delimited string or a non-empty string between delimited
|
||||||
@ -282,6 +315,14 @@ func (t *Template) nextItem() []byte {
|
|||||||
if t.buf[i] == '\n' {
|
if t.buf[i] == '\n' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if isQuote(t.buf[i]) {
|
||||||
|
i = endQuote(t.buf, i)
|
||||||
|
if i == -1 {
|
||||||
|
t.parseError("unmatched quote")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if equal(t.buf, i, t.rdelim) {
|
if equal(t.buf, i, t.rdelim) {
|
||||||
i += len(t.rdelim)
|
i += len(t.rdelim)
|
||||||
right = i
|
right = i
|
||||||
@ -333,23 +374,33 @@ func (t *Template) nextItem() []byte {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn a byte array into a white-space-split array of strings.
|
// Turn a byte array into a white-space-split array of strings,
|
||||||
|
// taking into account quoted strings.
|
||||||
func words(buf []byte) []string {
|
func words(buf []byte) []string {
|
||||||
s := make([]string, 0, 5)
|
s := make([]string, 0, 5)
|
||||||
p := 0 // position in buf
|
for i := 0; i < len(buf); {
|
||||||
// one word per loop
|
// One word per loop
|
||||||
for i := 0; ; i++ {
|
for i < len(buf) && white(buf[i]) {
|
||||||
// skip white space
|
i++
|
||||||
for ; p < len(buf) && white(buf[p]); p++ {
|
|
||||||
}
|
}
|
||||||
// grab word
|
if i == len(buf) {
|
||||||
start := p
|
|
||||||
for ; p < len(buf) && !white(buf[p]); p++ {
|
|
||||||
}
|
|
||||||
if start == p { // no text left
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
s = append(s, string(buf[start:p]))
|
// Got a word
|
||||||
|
start := i
|
||||||
|
if isQuote(buf[i]) {
|
||||||
|
i = endQuote(buf, i)
|
||||||
|
if i < 0 {
|
||||||
|
i = len(buf)
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i < len(buf) && !white(buf[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s = append(s, string(buf[start:i]))
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@ -381,11 +432,17 @@ func (t *Template) analyze(item []byte) (tok int, w []string) {
|
|||||||
t.parseError("empty directive")
|
t.parseError("empty directive")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(w) > 0 && w[0][0] != '.' {
|
first := w[0]
|
||||||
|
if first[0] != '.' {
|
||||||
tok = tokVariable
|
tok = tokVariable
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch w[0] {
|
if len(first) > 1 && first[1] >= '0' && first[1] <= '9' {
|
||||||
|
// Must be a float.
|
||||||
|
tok = tokVariable
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch first {
|
||||||
case ".meta-left", ".meta-right", ".space", ".tab":
|
case ".meta-left", ".meta-right", ".space", ".tab":
|
||||||
tok = tokLiteral
|
tok = tokLiteral
|
||||||
return
|
return
|
||||||
@ -447,6 +504,37 @@ func (t *Template) newVariable(words []string) *variableElement {
|
|||||||
formatters = strings.Split(lastWord[bar+1:], "|", -1)
|
formatters = strings.Split(lastWord[bar+1:], "|", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args := make([]interface{}, len(words))
|
||||||
|
|
||||||
|
// Build argument list, processing any literals
|
||||||
|
for i, word := range words {
|
||||||
|
var lerr os.Error
|
||||||
|
switch word[0] {
|
||||||
|
case '"', '`', '\'':
|
||||||
|
v, err := strconv.Unquote(word)
|
||||||
|
if err == nil && word[0] == '\'' {
|
||||||
|
args[i] = []int(v)[0]
|
||||||
|
} else {
|
||||||
|
args[i], lerr = v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case '.', '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||||
|
v, err := strconv.Btoi64(word, 0)
|
||||||
|
if err == nil {
|
||||||
|
args[i] = v
|
||||||
|
} else {
|
||||||
|
v, err := strconv.Atof64(word)
|
||||||
|
args[i], lerr = v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
args[i] = fieldName(word)
|
||||||
|
}
|
||||||
|
if lerr != nil {
|
||||||
|
t.parseError("invalid literal: %q: %s", word, lerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -457,7 +545,8 @@ func (t *Template) newVariable(words []string) *variableElement {
|
|||||||
t.parseError("unknown formatter: %q", f)
|
t.parseError("unknown formatter: %q", f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &variableElement{t.linenum, words, formatters}
|
|
||||||
|
return &variableElement{t.linenum, args, formatters}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -753,7 +842,7 @@ func (t *Template) varValue(name string, st *state) reflect.Value {
|
|||||||
func (t *Template) format(wr io.Writer, fmt string, val []interface{}, v *variableElement, st *state) {
|
func (t *Template) format(wr io.Writer, fmt string, val []interface{}, v *variableElement, st *state) {
|
||||||
fn := t.formatter(fmt)
|
fn := t.formatter(fmt)
|
||||||
if fn == nil {
|
if fn == nil {
|
||||||
t.execError(st, v.linenum, "missing formatter %s for variable %s", fmt, v.word[0])
|
t.execError(st, v.linenum, "missing formatter %s for variable", fmt)
|
||||||
}
|
}
|
||||||
fn(wr, fmt, val...)
|
fn(wr, fmt, val...)
|
||||||
}
|
}
|
||||||
@ -761,12 +850,15 @@ func (t *Template) format(wr io.Writer, fmt string, val []interface{}, v *variab
|
|||||||
// 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) {
|
||||||
// Turn the words of the invocation into values.
|
// Resolve field names
|
||||||
val := make([]interface{}, len(v.word))
|
val := make([]interface{}, len(v.args))
|
||||||
for i, word := range v.word {
|
for i, arg := range v.args {
|
||||||
val[i] = t.varValue(word, st).Interface()
|
if name, ok := arg.(fieldName); ok {
|
||||||
|
val[i] = t.varValue(string(name), st).Interface()
|
||||||
|
} else {
|
||||||
|
val[i] = arg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, fmt := range v.fmts[:len(v.fmts)-1] {
|
for i, fmt := range v.fmts[:len(v.fmts)-1] {
|
||||||
b := &st.buf[i&1]
|
b := &st.buf[i&1]
|
||||||
b.Reset()
|
b.Reset()
|
||||||
|
@ -94,10 +94,15 @@ func multiword(w io.Writer, format string, value ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printf(w io.Writer, format string, v ...interface{}) {
|
||||||
|
io.WriteString(w, fmt.Sprintf(v[0].(string), v[1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
var formatters = FormatterMap{
|
var formatters = FormatterMap{
|
||||||
"uppercase": writer(uppercase),
|
"uppercase": writer(uppercase),
|
||||||
"+1": writer(plus1),
|
"+1": writer(plus1),
|
||||||
"multiword": multiword,
|
"multiword": multiword,
|
||||||
|
"printf": printf,
|
||||||
}
|
}
|
||||||
|
|
||||||
var tests = []*Test{
|
var tests = []*Test{
|
||||||
@ -138,6 +143,36 @@ var tests = []*Test{
|
|||||||
out: "nil pointer: <nil>=77\n",
|
out: "nil pointer: <nil>=77\n",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
&Test{
|
||||||
|
in: `{"Strings" ":"} {""} {"\t\u0123 \x23\\"} {"\"}{\\"}`,
|
||||||
|
|
||||||
|
out: "Strings: \t\u0123 \x23\\ \"}{\\",
|
||||||
|
},
|
||||||
|
|
||||||
|
&Test{
|
||||||
|
in: "{`Raw strings` `:`} {``} {`\\t\\u0123 \\x23\\`} {`}{\\`}",
|
||||||
|
|
||||||
|
out: "Raw strings: \\t\\u0123 \\x23\\ }{\\",
|
||||||
|
},
|
||||||
|
|
||||||
|
&Test{
|
||||||
|
in: "Characters: {'a'} {'\\u0123'} {' '} {'}'} {'{'}",
|
||||||
|
|
||||||
|
out: "Characters: 97 291 32 125 123",
|
||||||
|
},
|
||||||
|
|
||||||
|
&Test{
|
||||||
|
in: "Integers: {1} {-2} {+42} {0777} {0x0a}",
|
||||||
|
|
||||||
|
out: "Integers: 1 -2 42 511 10",
|
||||||
|
},
|
||||||
|
|
||||||
|
&Test{
|
||||||
|
in: "Floats: {.5} {-.5} {1.1} {-2.2} {+42.1} {1e10} {1.2e-3} {1.2e3} {-1.2e3}",
|
||||||
|
|
||||||
|
out: "Floats: 0.5 -0.5 1.1 -2.2 42.1 1e+10 0.0012 1200 -1200",
|
||||||
|
},
|
||||||
|
|
||||||
// Method at top level
|
// Method at top level
|
||||||
&Test{
|
&Test{
|
||||||
in: "ptrmethod={PointerMethod}\n",
|
in: "ptrmethod={PointerMethod}\n",
|
||||||
@ -723,6 +758,10 @@ var formatterTests = []Test{
|
|||||||
in: "{Integer|||||}", // empty string is a valid formatter
|
in: "{Integer|||||}", // empty string is a valid formatter
|
||||||
out: "77",
|
out: "77",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
in: `{"%.02f 0x%02X" 1.1 10|printf}`,
|
||||||
|
out: "1.10 0x0A",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatters(t *testing.T) {
|
func TestFormatters(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user