diff --git a/encode.go b/encode.go index 0be8d8c..0590da4 100644 --- a/encode.go +++ b/encode.go @@ -784,9 +784,6 @@ type JSONEncoder struct { // formatter handles formatting values prior to output. formatter Formatter - // title is the title value. - title *Value - // empty is the empty value. empty *Value } @@ -979,9 +976,6 @@ type CSVEncoder struct { // formatter handles formatting values prior to output. formatter Formatter - // title is the title value. - title *Value - // empty is the empty value. empty *Value } @@ -1124,6 +1118,16 @@ type TemplateEncoder struct { // empty is the empty value. empty *Value + + // template is the parsed template + template Executor + + // attributes are extra table attributes + attributes string +} + +type Executor interface { + Execute(io.Writer, interface{}) error } // NewTemplateEncoder creates a new template encoder using the provided options. @@ -1134,7 +1138,7 @@ func NewTemplateEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) { newline: newline, formatter: NewEscapeFormatter(), empty: &Value{ - Tabs: make([][][2]int, 1), + Buf: []byte(""), }, } for _, o := range opts { @@ -1151,7 +1155,73 @@ func (enc *TemplateEncoder) Encode(w io.Writer) error { if enc.resultSet == nil { return ErrResultSetIsNil } - return nil + + // get and check columns + cols, err := enc.resultSet.Columns() + if err != nil { + return err + } + clen := len(cols) + if clen == 0 { + return ErrResultSetHasNoColumns + } + + headers, err := enc.formatter.Header(cols) + if err != nil { + return err + } + + title := enc.title + if title == nil { + title = enc.empty + } + + stop := make(chan struct{}) + data := struct { + Title *Value + Attributes string + Headers []*Value + Rows <-chan []cell + }{ + Title: title, + Attributes: enc.attributes, + Headers: headers, + Rows: enc.rows(headers, stop), + } + err = enc.template.Execute(w, data) + close(stop) + return err +} + +type cell struct { + Name string + Value *Value +} + +func (enc *TemplateEncoder) rows(headers []*Value, stop <-chan struct{}) chan []cell { + // set up storage for results + r := make([]interface{}, len(headers)) + for i := range headers { + r[i] = new(interface{}) + } + result := make(chan []cell) + go func() { + defer close(result) + for enc.resultSet.Next() { + row := make([]cell, len(headers)) + err := enc.scanAndFormat(headers, r, row) + if err != nil { + return + } + select { + case result <- row: + // sent successfully + case <-stop: + return + } + } + }() + return result } // EncodeAll encodes all result sets to the writer using the encoder settings. @@ -1179,13 +1249,25 @@ func (enc *TemplateEncoder) EncodeAll(w io.Writer) error { } // scanAndFormat scans and formats values from the result set. -func (enc *TemplateEncoder) scanAndFormat(vals []interface{}) ([]*Value, error) { +// vals and result are passed as args to avoid allocation +func (enc *TemplateEncoder) scanAndFormat(headers []*Value, buf []interface{}, row []cell) error { var err error if err = enc.resultSet.Err(); err != nil { - return nil, err + return err } - if err = enc.resultSet.Scan(vals...); err != nil { - return nil, err + if err = enc.resultSet.Scan(buf...); err != nil { + return err } - return enc.formatter.Format(vals) + vals, err := enc.formatter.Format(buf) + if err != nil { + return err + } + for i, h := range headers { + v := vals[i] + if v == nil { + v = enc.empty + } + row[i] = cell{Name: string(h.Buf), Value: v} + } + return nil } diff --git a/encode_test.go b/encode_test.go index a6c9d0a..c856239 100644 --- a/encode_test.go +++ b/encode_test.go @@ -32,3 +32,35 @@ func TestJSONEncoder(t *testing.T) { i++ } } + +func TestTemplateEncoder(t *testing.T) { + expected := ` +Row 0: + author_id = "15" + name = "aoeu +test +" + z = "" + +Row 1: + author_id = "15" + name = "aoeu +test +" + z = "" + +` + template := ` +{{ range $i, $r := .Rows }}Row {{ $i }}: +{{ range . }} {{ .Name }} = "{{ .Value }}" +{{ end }} +{{ end }}` + buf := new(bytes.Buffer) + if err := EncodeTemplateAll(buf, rs(), WithTextTemplate(template)); err != nil { + t.Fatalf("expected no error when Template encoding, got: %v", err) + } + actual := buf.String() + if actual != expected { + t.Fatalf("expected encoder to return:\n-- expected --\n%v\n-- end --\n\nbut got:\n-- encoded --\n%s\n-- end --", expected, actual) + } +} diff --git a/fmt.go b/fmt.go index 1c8043c..ecacea5 100644 --- a/fmt.go +++ b/fmt.go @@ -435,6 +435,10 @@ type Value struct { Raw bool } +func (v Value) String() string { + return string(v.Buf) +} + // LineWidth returns the line width (in runes) of line l. func (v *Value) LineWidth(l, offset, tab int) int { var width int diff --git a/opts.go b/opts.go index 1c54856..2a62fc6 100644 --- a/opts.go +++ b/opts.go @@ -1,8 +1,10 @@ package tblfmt import ( + html "html/template" "io" "strconv" + txt "text/template" "unicode/utf8" "github.com/nathan-fiscaletti/consolesize-go" @@ -31,8 +33,11 @@ func FromMap(opts map[string]string) (Builder, []Option) { return NewCSVEncoder, csvOpts case "html", "asciidoc", "latex", "latex-longtable", "troff-ms": - //return newErrEncoder, []Option{withError(fmt.Errorf("%q format not implemented", opts["format"]))} - return NewTemplateEncoder, []Option{WithNamedTemplate(opts["format"])} + return NewTemplateEncoder, []Option{ + WithNamedTemplate(opts["format"]), + WithTableAttributes(opts["tableattr"]), + WithTitle(opts["title"]), + } case "unaligned": fallthrough @@ -150,13 +155,33 @@ func WithInline(inline bool) Option { // WithTitle is a encoder option to set the title value used. func WithTitle(title string) Option { return func(v interface{}) error { + var formatter Formatter + var val *Value switch enc := v.(type) { case *TableEncoder: - vals, err := enc.formatter.Header([]string{title}) + formatter = enc.formatter + val = enc.empty + case *ExpandedEncoder: + formatter = enc.formatter + val = enc.empty + case *TemplateEncoder: + formatter = enc.formatter + val = enc.empty + } + if title != "" { + vals, err := formatter.Header([]string{title}) if err != nil { return err } - enc.title = vals[0] + val = vals[0] + } + switch enc := v.(type) { + case *TableEncoder: + enc.title = val + case *ExpandedEncoder: + enc.title = val + case *TemplateEncoder: + enc.title = val } return nil } @@ -255,11 +280,31 @@ func WithBorder(border int) Option { } } -// WithTemplate is a encoder option to set the raw template used. -func WithTemplate(template string) Option { +// WithTextTemplate is a encoder option to set the raw text template used. +func WithTextTemplate(t string) Option { + return func(v interface{}) error { + switch enc := v.(type) { + case *TemplateEncoder: + var err error + enc.template, err = txt.New("main").Parse(t) + if err != nil { + return err + } + } + return nil + } +} + +// WithHtmlTemplate is a encoder option to set the raw html template used. +func WithHtmlTemplate(t string) Option { return func(v interface{}) error { - switch v.(type) { + switch enc := v.(type) { case *TemplateEncoder: + var err error + enc.template, err = html.New("main").Funcs(htmlFuncMap).Parse(t) + if err != nil { + return err + } } return nil } @@ -268,8 +313,32 @@ func WithTemplate(template string) Option { // WithNamedTemplate is a encoder option to set the template used. func WithNamedTemplate(name string) Option { return func(v interface{}) error { - switch v.(type) { + template, ok := templates[name] + if !ok { + return ErrUnknownTemplate + } + switch enc := v.(type) { + case *TemplateEncoder: + var err error + if name == "html" { + enc.template, err = html.New(name).Funcs(htmlFuncMap).Parse(template) + } else { + enc.template, err = txt.New(name).Parse(template) + } + if err != nil { + return err + } + } + return nil + } +} + +// WithTableAttributes is a encoder option to set the table attributes. +func WithTableAttributes(a string) Option { + return func(v interface{}) error { + switch enc := v.(type) { case *TemplateEncoder: + enc.attributes = a } return nil } diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..7455aa5 --- /dev/null +++ b/templates.go @@ -0,0 +1,41 @@ +package tblfmt + +import html "html/template" + +var ( + templates = map[string]string{ + "html": ` +
{{.}} | +{{end}} +
---|
{{.Value}} | +{{end}}