Skip to content

Commit

Permalink
Implement template encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
nineinchnick committed Mar 14, 2021
1 parent dd2031a commit 8ee4150
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 21 deletions.
108 changes: 95 additions & 13 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
32 changes: 32 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
4 changes: 4 additions & 0 deletions fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 77 additions & 8 deletions opts.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package tblfmt

import (
html "html/template"
"io"
"strconv"
txt "text/template"
"unicode/utf8"

"github.com/nathan-fiscaletti/consolesize-go"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
41 changes: 41 additions & 0 deletions templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tblfmt

import html "html/template"

var (
templates = map[string]string{
"html": `
<table {{.Attributes | attr}}>
<caption>{{.Title}}</caption>
<thead>
<tr>
{{range .Headers}} <th align="{{.Align}}">{{.}}</th>
{{end}}
</tr>
</thead>
<tbody>
{{range .Rows}} <tr>
{{range .}} <td align="{{.Value.Align}}">{{.Value}}</td>
{{end}} </tr>
{{end}}
</tbody>
</table>`,
"asciidoc": `
[%header]
{{if .Title.Buf}}.{{.Title}}
{{end}}|===
{{range .Headers}}|{{.}}
{{end}}
{{range .Rows}}{{range .}}|{{.Value}}
{{end}}
{{end}}|===`,
}
htmlFuncMap = html.FuncMap{
"attr": func(s string) html.HTMLAttr {
return html.HTMLAttr(s)
},
"safe": func(s string) html.HTML {
return html.HTML(s)
},
}
)
3 changes: 3 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (

// ErrInvalidLineStyle is the invalid line style error.
ErrInvalidLineStyle Error = "invalid line style"

// ErrUnknownTemplate is the unknown template error.
ErrUnknownTemplate Error = "unknown template"
)

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

0 comments on commit 8ee4150

Please sign in to comment.