Skip to content

Commit 8488aa4

Browse files
authored
Merge pull request #13 from nineinchnick/templates
Implement template encoder
2 parents dd2031a + 8ee4150 commit 8488aa4

File tree

6 files changed

+252
-21
lines changed

6 files changed

+252
-21
lines changed

encode.go

+95-13
Original file line numberDiff line numberDiff line change
@@ -784,9 +784,6 @@ type JSONEncoder struct {
784784
// formatter handles formatting values prior to output.
785785
formatter Formatter
786786

787-
// title is the title value.
788-
title *Value
789-
790787
// empty is the empty value.
791788
empty *Value
792789
}
@@ -979,9 +976,6 @@ type CSVEncoder struct {
979976
// formatter handles formatting values prior to output.
980977
formatter Formatter
981978

982-
// title is the title value.
983-
title *Value
984-
985979
// empty is the empty value.
986980
empty *Value
987981
}
@@ -1124,6 +1118,16 @@ type TemplateEncoder struct {
11241118

11251119
// empty is the empty value.
11261120
empty *Value
1121+
1122+
// template is the parsed template
1123+
template Executor
1124+
1125+
// attributes are extra table attributes
1126+
attributes string
1127+
}
1128+
1129+
type Executor interface {
1130+
Execute(io.Writer, interface{}) error
11271131
}
11281132

11291133
// NewTemplateEncoder creates a new template encoder using the provided options.
@@ -1134,7 +1138,7 @@ func NewTemplateEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) {
11341138
newline: newline,
11351139
formatter: NewEscapeFormatter(),
11361140
empty: &Value{
1137-
Tabs: make([][][2]int, 1),
1141+
Buf: []byte(""),
11381142
},
11391143
}
11401144
for _, o := range opts {
@@ -1151,7 +1155,73 @@ func (enc *TemplateEncoder) Encode(w io.Writer) error {
11511155
if enc.resultSet == nil {
11521156
return ErrResultSetIsNil
11531157
}
1154-
return nil
1158+
1159+
// get and check columns
1160+
cols, err := enc.resultSet.Columns()
1161+
if err != nil {
1162+
return err
1163+
}
1164+
clen := len(cols)
1165+
if clen == 0 {
1166+
return ErrResultSetHasNoColumns
1167+
}
1168+
1169+
headers, err := enc.formatter.Header(cols)
1170+
if err != nil {
1171+
return err
1172+
}
1173+
1174+
title := enc.title
1175+
if title == nil {
1176+
title = enc.empty
1177+
}
1178+
1179+
stop := make(chan struct{})
1180+
data := struct {
1181+
Title *Value
1182+
Attributes string
1183+
Headers []*Value
1184+
Rows <-chan []cell
1185+
}{
1186+
Title: title,
1187+
Attributes: enc.attributes,
1188+
Headers: headers,
1189+
Rows: enc.rows(headers, stop),
1190+
}
1191+
err = enc.template.Execute(w, data)
1192+
close(stop)
1193+
return err
1194+
}
1195+
1196+
type cell struct {
1197+
Name string
1198+
Value *Value
1199+
}
1200+
1201+
func (enc *TemplateEncoder) rows(headers []*Value, stop <-chan struct{}) chan []cell {
1202+
// set up storage for results
1203+
r := make([]interface{}, len(headers))
1204+
for i := range headers {
1205+
r[i] = new(interface{})
1206+
}
1207+
result := make(chan []cell)
1208+
go func() {
1209+
defer close(result)
1210+
for enc.resultSet.Next() {
1211+
row := make([]cell, len(headers))
1212+
err := enc.scanAndFormat(headers, r, row)
1213+
if err != nil {
1214+
return
1215+
}
1216+
select {
1217+
case result <- row:
1218+
// sent successfully
1219+
case <-stop:
1220+
return
1221+
}
1222+
}
1223+
}()
1224+
return result
11551225
}
11561226

11571227
// EncodeAll encodes all result sets to the writer using the encoder settings.
@@ -1179,13 +1249,25 @@ func (enc *TemplateEncoder) EncodeAll(w io.Writer) error {
11791249
}
11801250

11811251
// scanAndFormat scans and formats values from the result set.
1182-
func (enc *TemplateEncoder) scanAndFormat(vals []interface{}) ([]*Value, error) {
1252+
// vals and result are passed as args to avoid allocation
1253+
func (enc *TemplateEncoder) scanAndFormat(headers []*Value, buf []interface{}, row []cell) error {
11831254
var err error
11841255
if err = enc.resultSet.Err(); err != nil {
1185-
return nil, err
1256+
return err
11861257
}
1187-
if err = enc.resultSet.Scan(vals...); err != nil {
1188-
return nil, err
1258+
if err = enc.resultSet.Scan(buf...); err != nil {
1259+
return err
11891260
}
1190-
return enc.formatter.Format(vals)
1261+
vals, err := enc.formatter.Format(buf)
1262+
if err != nil {
1263+
return err
1264+
}
1265+
for i, h := range headers {
1266+
v := vals[i]
1267+
if v == nil {
1268+
v = enc.empty
1269+
}
1270+
row[i] = cell{Name: string(h.Buf), Value: v}
1271+
}
1272+
return nil
11911273
}

encode_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,35 @@ func TestJSONEncoder(t *testing.T) {
3232
i++
3333
}
3434
}
35+
36+
func TestTemplateEncoder(t *testing.T) {
37+
expected := `
38+
Row 0:
39+
author_id = "15"
40+
name = "aoeu
41+
test
42+
"
43+
z = ""
44+
45+
Row 1:
46+
author_id = "15"
47+
name = "aoeu
48+
test
49+
"
50+
z = ""
51+
52+
`
53+
template := `
54+
{{ range $i, $r := .Rows }}Row {{ $i }}:
55+
{{ range . }} {{ .Name }} = "{{ .Value }}"
56+
{{ end }}
57+
{{ end }}`
58+
buf := new(bytes.Buffer)
59+
if err := EncodeTemplateAll(buf, rs(), WithTextTemplate(template)); err != nil {
60+
t.Fatalf("expected no error when Template encoding, got: %v", err)
61+
}
62+
actual := buf.String()
63+
if actual != expected {
64+
t.Fatalf("expected encoder to return:\n-- expected --\n%v\n-- end --\n\nbut got:\n-- encoded --\n%s\n-- end --", expected, actual)
65+
}
66+
}

fmt.go

+4
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,10 @@ type Value struct {
435435
Raw bool
436436
}
437437

438+
func (v Value) String() string {
439+
return string(v.Buf)
440+
}
441+
438442
// LineWidth returns the line width (in runes) of line l.
439443
func (v *Value) LineWidth(l, offset, tab int) int {
440444
var width int

opts.go

+77-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package tblfmt
22

33
import (
4+
html "html/template"
45
"io"
56
"strconv"
7+
txt "text/template"
68
"unicode/utf8"
79

810
"github.com/nathan-fiscaletti/consolesize-go"
@@ -31,8 +33,11 @@ func FromMap(opts map[string]string) (Builder, []Option) {
3133
return NewCSVEncoder, csvOpts
3234

3335
case "html", "asciidoc", "latex", "latex-longtable", "troff-ms":
34-
//return newErrEncoder, []Option{withError(fmt.Errorf("%q format not implemented", opts["format"]))}
35-
return NewTemplateEncoder, []Option{WithNamedTemplate(opts["format"])}
36+
return NewTemplateEncoder, []Option{
37+
WithNamedTemplate(opts["format"]),
38+
WithTableAttributes(opts["tableattr"]),
39+
WithTitle(opts["title"]),
40+
}
3641

3742
case "unaligned":
3843
fallthrough
@@ -150,13 +155,33 @@ func WithInline(inline bool) Option {
150155
// WithTitle is a encoder option to set the title value used.
151156
func WithTitle(title string) Option {
152157
return func(v interface{}) error {
158+
var formatter Formatter
159+
var val *Value
153160
switch enc := v.(type) {
154161
case *TableEncoder:
155-
vals, err := enc.formatter.Header([]string{title})
162+
formatter = enc.formatter
163+
val = enc.empty
164+
case *ExpandedEncoder:
165+
formatter = enc.formatter
166+
val = enc.empty
167+
case *TemplateEncoder:
168+
formatter = enc.formatter
169+
val = enc.empty
170+
}
171+
if title != "" {
172+
vals, err := formatter.Header([]string{title})
156173
if err != nil {
157174
return err
158175
}
159-
enc.title = vals[0]
176+
val = vals[0]
177+
}
178+
switch enc := v.(type) {
179+
case *TableEncoder:
180+
enc.title = val
181+
case *ExpandedEncoder:
182+
enc.title = val
183+
case *TemplateEncoder:
184+
enc.title = val
160185
}
161186
return nil
162187
}
@@ -255,11 +280,31 @@ func WithBorder(border int) Option {
255280
}
256281
}
257282

258-
// WithTemplate is a encoder option to set the raw template used.
259-
func WithTemplate(template string) Option {
283+
// WithTextTemplate is a encoder option to set the raw text template used.
284+
func WithTextTemplate(t string) Option {
285+
return func(v interface{}) error {
286+
switch enc := v.(type) {
287+
case *TemplateEncoder:
288+
var err error
289+
enc.template, err = txt.New("main").Parse(t)
290+
if err != nil {
291+
return err
292+
}
293+
}
294+
return nil
295+
}
296+
}
297+
298+
// WithHtmlTemplate is a encoder option to set the raw html template used.
299+
func WithHtmlTemplate(t string) Option {
260300
return func(v interface{}) error {
261-
switch v.(type) {
301+
switch enc := v.(type) {
262302
case *TemplateEncoder:
303+
var err error
304+
enc.template, err = html.New("main").Funcs(htmlFuncMap).Parse(t)
305+
if err != nil {
306+
return err
307+
}
263308
}
264309
return nil
265310
}
@@ -268,8 +313,32 @@ func WithTemplate(template string) Option {
268313
// WithNamedTemplate is a encoder option to set the template used.
269314
func WithNamedTemplate(name string) Option {
270315
return func(v interface{}) error {
271-
switch v.(type) {
316+
template, ok := templates[name]
317+
if !ok {
318+
return ErrUnknownTemplate
319+
}
320+
switch enc := v.(type) {
321+
case *TemplateEncoder:
322+
var err error
323+
if name == "html" {
324+
enc.template, err = html.New(name).Funcs(htmlFuncMap).Parse(template)
325+
} else {
326+
enc.template, err = txt.New(name).Parse(template)
327+
}
328+
if err != nil {
329+
return err
330+
}
331+
}
332+
return nil
333+
}
334+
}
335+
336+
// WithTableAttributes is a encoder option to set the table attributes.
337+
func WithTableAttributes(a string) Option {
338+
return func(v interface{}) error {
339+
switch enc := v.(type) {
272340
case *TemplateEncoder:
341+
enc.attributes = a
273342
}
274343
return nil
275344
}

templates.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package tblfmt
2+
3+
import html "html/template"
4+
5+
var (
6+
templates = map[string]string{
7+
"html": `
8+
<table {{.Attributes | attr}}>
9+
<caption>{{.Title}}</caption>
10+
<thead>
11+
<tr>
12+
{{range .Headers}} <th align="{{.Align}}">{{.}}</th>
13+
{{end}}
14+
</tr>
15+
</thead>
16+
<tbody>
17+
{{range .Rows}} <tr>
18+
{{range .}} <td align="{{.Value.Align}}">{{.Value}}</td>
19+
{{end}} </tr>
20+
{{end}}
21+
</tbody>
22+
</table>`,
23+
"asciidoc": `
24+
[%header]
25+
{{if .Title.Buf}}.{{.Title}}
26+
{{end}}|===
27+
{{range .Headers}}|{{.}}
28+
{{end}}
29+
{{range .Rows}}{{range .}}|{{.Value}}
30+
{{end}}
31+
{{end}}|===`,
32+
}
33+
htmlFuncMap = html.FuncMap{
34+
"attr": func(s string) html.HTMLAttr {
35+
return html.HTMLAttr(s)
36+
},
37+
"safe": func(s string) html.HTML {
38+
return html.HTML(s)
39+
},
40+
}
41+
)

util.go

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ const (
4141

4242
// ErrInvalidLineStyle is the invalid line style error.
4343
ErrInvalidLineStyle Error = "invalid line style"
44+
45+
// ErrUnknownTemplate is the unknown template error.
46+
ErrUnknownTemplate Error = "unknown template"
4447
)
4548

4649
// errEncoder provides a no-op encoder that always returns the wrapped error.

0 commit comments

Comments
 (0)