diff --git a/.travis.yml b/.travis.yml index 0833941..4f6673c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ before_install: - go get -t -v ./... script: - - go test -race -coverprofile=coverage.txt -covermode=atomic + - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... - go vet -v ./... - gofmt -l . && test -z $(gofmt -l .) diff --git a/element.go b/element.go index 22f2ae9..2db8a7d 100644 --- a/element.go +++ b/element.go @@ -3,38 +3,36 @@ package emailt import ( "bytes" "fmt" - "html/template" "io" - "strings" + "text/template" + + "github.com/gochore/emailt/internal/rend" + "github.com/gochore/emailt/style" ) type Element interface { - Render(writer io.Writer, themes ...Theme) error -} - -type StringElement string - -func NewStringElement(format string, a ...interface{}) StringElement { - return StringElement(fmt.Sprintf(format, a...)) -} - -func (e StringElement) Render(writer io.Writer, themes ...Theme) error { - return htmlRender(strings.NewReader(string(e)), writer, mergeThemes(themes)) + Render(writer io.Writer, themes ...style.Theme) error } type TemplateElement struct { Data interface{} Template string + Funcs template.FuncMap } -func (e TemplateElement) Render(writer io.Writer, themes ...Theme) error { - t, err := template.New("").Parse(e.Template) +func (e TemplateElement) Render(writer io.Writer, themes ...style.Theme) error { + errPrefix := "TemplateElement.Render: " + + t, err := template.New("").Funcs(e.Funcs).Parse(e.Template) if err != nil { - return fmt.Errorf("parse template: %w", err) + return fmt.Errorf(errPrefix+"parse template: %w", err) } buffer := &bytes.Buffer{} if err := t.Execute(buffer, e.Data); err != nil { - return fmt.Errorf("template execute: %w", err) + return fmt.Errorf(errPrefix+"template execute: %w", err) + } + if err := rend.RenderTheme(buffer, writer, rend.MergeThemes(themes)); err != nil { + return fmt.Errorf(errPrefix+"%w", err) } - return htmlRender(buffer, writer, mergeThemes(themes)) + return nil } diff --git a/element_test.go b/element_test.go index a63586a..1af8267 100644 --- a/element_test.go +++ b/element_test.go @@ -2,58 +2,16 @@ package emailt import ( "bytes" + "strings" "testing" - "time" + "text/template" ) -func TestStringElement_Render(t *testing.T) { - tests := []struct { - name string - e StringElement - style Theme - wantWriter string - wantErr bool - }{ - { - name: "regular", - e: "

test

", - wantWriter: "

test

", - wantErr: false, - }, - { - name: "with_style", - e: "

test

", - style: MapTheme{ - "p": Attributes{ - { - Name: "style", - Value: "background-color:#dedede;", - }, - }, - }, - wantWriter: `

test

`, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - writer := &bytes.Buffer{} - err := tt.e.Render(writer, tt.style) - if (err != nil) != tt.wantErr { - t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotWriter := writer.String(); gotWriter != tt.wantWriter { - t.Errorf("Render() gotWriter = %v, want %v", gotWriter, tt.wantWriter) - } - }) - } -} - func TestTemplateElement_Render(t *testing.T) { type fields struct { Data interface{} Template string + Funcs template.FuncMap } tests := []struct { name string @@ -100,12 +58,31 @@ func TestTemplateElement_Render(t *testing.T) { wantWriter: "", wantErr: true, }, + { + name: "with_funcs", + fields: fields{ + Data: struct { + A string + B int + }{ + A: "hello", + B: 1, + }, + Template: "A:{{title .A}}, B:{{.B}}", + Funcs: template.FuncMap{ + "title": strings.Title, + }, + }, + wantWriter: "A:Hello, B:1", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := TemplateElement{ Data: tt.fields.Data, Template: tt.fields.Template, + Funcs: tt.fields.Funcs, } writer := &bytes.Buffer{} err := e.Render(writer) @@ -119,31 +96,3 @@ func TestTemplateElement_Render(t *testing.T) { }) } } - -func TestNewStringElement(t *testing.T) { - type args struct { - format string - a []interface{} - } - tests := []struct { - name string - args args - want StringElement - }{ - { - name: "regular", - args: args{ - format: "%v %v", - a: []interface{}{1, time.Minute}, - }, - want: "1 1m0s", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewStringElement(tt.args.format, tt.args.a...); got != tt.want { - t.Errorf("NewStringElement() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/email.go b/email.go index 3f6d569..3128eef 100644 --- a/email.go +++ b/email.go @@ -3,16 +3,19 @@ package emailt import ( "fmt" "io" + + "github.com/gochore/emailt/internal/rend" + "github.com/gochore/emailt/style" ) type Email struct { elements []Element - theme Theme + theme style.Theme } type Option func(email *Email) -func WithTheme(theme Theme) Option { +func WithTheme(theme style.Theme) Option { return func(email *Email) { email.theme = theme } @@ -34,7 +37,7 @@ func (e *Email) AddElements(element ...Element) *Email { } func (e *Email) Render(writer io.Writer) error { - render := newFmtWriter(writer) + render := rend.NewFmtWriter(writer) render.Print(` diff --git a/headline.go b/headline.go deleted file mode 100644 index dfe3cee..0000000 --- a/headline.go +++ /dev/null @@ -1,18 +0,0 @@ -package emailt - -import ( - "fmt" -) - -type Headline = StringElement - -func NewHeadline(level int, format string, a ...interface{}) Headline { - if level < 1 { - level = 1 - } - if level > 6 { - level = 6 - } - format = fmt.Sprintf("%s", level, format, level) - return Headline(fmt.Sprintf(format, a...)) -} diff --git a/headline_test.go b/headline_test.go deleted file mode 100644 index e8e0e4e..0000000 --- a/headline_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package emailt - -import ( - "testing" -) - -func TestNewHeadline(t *testing.T) { - type args struct { - level int - format string - a []interface{} - } - tests := []struct { - name string - args args - want Headline - }{ - { - name: "regular", - args: args{ - level: 5, - format: "%v %v", - a: []interface{}{1, "A"}, - }, - want: "
1 A
", - }, - { - name: "h0", - args: args{ - level: 0, - format: "%v %v", - a: []interface{}{1, "A"}, - }, - want: "

1 A

", - }, - { - name: "h7", - args: args{ - level: 7, - format: "%v %v", - a: []interface{}{1, "A"}, - }, - want: "
1 A
", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewHeadline(tt.args.level, tt.args.format, tt.args.a...); got != tt.want { - t.Errorf("NewHeadline() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/htmlt/html.go b/htmlt/html.go new file mode 100644 index 0000000..a29cb1a --- /dev/null +++ b/htmlt/html.go @@ -0,0 +1,112 @@ +package htmlt + +import ( + "fmt" + "io" + "strings" + + "github.com/gochore/emailt/internal/rend" + "github.com/gochore/emailt/style" +) + +type Html string + +// Sprintf return a html element +func Sprintf(format string, a ...interface{}) Html { + return Html(fmt.Sprintf(format, a...)) +} + +// Render implement email.Element +func (e Html) Render(writer io.Writer, themes ...style.Theme) error { + errPrefix := "Html.Render: " + if err := rend.RenderTheme(strings.NewReader(string(e)), writer, rend.MergeThemes(themes)); err != nil { + return fmt.Errorf(errPrefix+"%w", err) + } + return nil +} + +// T return a html element with specified tag +func T(tag string, format string, a ...interface{}) Html { + return Sprintf(fmt.Sprintf("<%s>%s", tag, format, tag), a...) +} + +// H return a html element +func H(level int, format string, a ...interface{}) Html { + if level < 1 { + level = 1 + } + if level > 6 { + level = 6 + } + return T(fmt.Sprintf("h%d", level), format, a...) +} + +// Hr return a html element
+func Hr() Html { + return Sprintf("
") +} + +// P return a html element

+func P(format string, a ...interface{}) Html { + return T("p", format, a...) +} + +// Pre return a html element

+func Pre(format string, a ...interface{}) Html {
+	return T("pre", format, a...)
+}
+
+// A return a html element 
+func A(href, format string, a ...interface{}) Html {
+	return Sprintf(fmt.Sprintf(`%s`, href, format), a...)
+}
+
+// B return a html element 
+func B(format string, a ...interface{}) Html {
+	return T("b", format, a...)
+}
+
+// Br return a html element 
+func Br() Html { + return Sprintf("
") +} + +// Code return a html element +func Code(format string, a ...interface{}) Html { + return T("code", format, a...) +} + +// Em return a html element +func Em(format string, a ...interface{}) Html { + return T("em", format, a...) +} + +// I return a html element +func I(format string, a ...interface{}) Html { + return T("i", format, a...) +} + +// Small return a html element +func Small(format string, a ...interface{}) Html { + return T("small", format, a...) +} + +// Strong return a html element +func Strong(format string, a ...interface{}) Html { + return T("strong", format, a...) +} + +// Img return a html element +func Img(src, alt, format string, a ...interface{}) Html { + return Sprintf(fmt.Sprintf(`%s`, src, alt, format), a...) +} + +// Del return a html element +func Del(format string, a ...interface{}) Html { + return T("del", format, a...) +} + +// Ins return a html element +func Ins(format string, a ...interface{}) Html { + return T("ins", format, a...) +} diff --git a/htmlt/html_test.go b/htmlt/html_test.go new file mode 100644 index 0000000..4088021 --- /dev/null +++ b/htmlt/html_test.go @@ -0,0 +1,569 @@ +package htmlt + +import ( + "bytes" + "fmt" + "io" + "testing" + + "golang.org/x/net/html" + + "github.com/gochore/emailt/style" +) + +func TestT(t *testing.T) { + type args struct { + tag string + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + tag: "h1", + format: "abc%d", + a: []interface{}{1}, + }, + want: "

abc1

", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := T(tt.args.tag, tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("T() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestH(t *testing.T) { + type args struct { + level int + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + level: 5, + format: "%v %v", + a: []interface{}{1, "A"}, + }, + want: "
1 A
", + }, + { + name: "h0", + args: args{ + level: 0, + format: "%v %v", + a: []interface{}{1, "A"}, + }, + want: "

1 A

", + }, + { + name: "h7", + args: args{ + level: 7, + format: "%v %v", + a: []interface{}{1, "A"}, + }, + want: "
1 A
", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := H(tt.args.level, tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("NewHeadline() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHr(t *testing.T) { + tests := []struct { + name string + want Html + }{ + { + name: "regular", + want: "
", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Hr(); got != tt.want { + t.Errorf("Hr() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestP(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "

abc1

", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := P(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("P() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPre(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "
abc1
", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Pre(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Pre() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestA(t *testing.T) { + type args struct { + href string + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + href: "http://example.com", + format: "abc%d", + a: []interface{}{1}, + }, + want: `abc1`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := A(tt.args.href, tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("A() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestB(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := B(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("B() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBr(t *testing.T) { + tests := []struct { + name string + want Html + }{ + { + name: "regular", + want: "
", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Br(); got != tt.want { + t.Errorf("Br() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCode(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Code(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Code() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDel(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Del(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Del() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEm(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Em(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Em() = %v, want %v", got, tt.want) + } + }) + } +} + +type errWriter struct{} + +func (errWriter) Write(p []byte) (int, error) { + return 0, fmt.Errorf("error") +} + +func TestHtml_Render(t *testing.T) { + type args struct { + writer io.Writer + themes []style.Theme + } + tests := []struct { + name string + e Html + args args + wantWriter string + wantErr bool + }{ + { + name: "regular", + e: "

abc

", + args: args{ + writer: &bytes.Buffer{}, + themes: []style.Theme{style.MapTheme{ + "p": []html.Attribute{ + { + Key: "a", + Val: "1", + }, + }, + }}, + }, + wantWriter: `

abc

`, + wantErr: false, + }, + { + name: "writer with error", + e: "

abc

", + args: args{ + writer: errWriter{}, + themes: []style.Theme{style.MapTheme{ + "p": []html.Attribute{ + { + Key: "a", + Val: "1", + }, + }, + }}, + }, + wantWriter: `

abc

`, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.e.Render(tt.args.writer, tt.args.themes...) + if (err != nil) != tt.wantErr { + t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + stringer := tt.args.writer.(fmt.Stringer) + if gotWriter := stringer.String(); gotWriter != tt.wantWriter { + t.Errorf("Render() gotWriter = %v, want %v", gotWriter, tt.wantWriter) + } + }) + } +} + +func TestI(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := I(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("I() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestImg(t *testing.T) { + type args struct { + src string + alt string + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + src: "http://example.com", + alt: "example", + format: "abc%d", + a: []interface{}{1}, + }, + want: `abc1`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Img(tt.args.src, tt.args.alt, tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Img() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIns(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Ins(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Ins() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSmall(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Small(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Small() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSprintf(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Sprintf(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Sprintf() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStrong(t *testing.T) { + type args struct { + format string + a []interface{} + } + tests := []struct { + name string + args args + want Html + }{ + { + name: "regular", + args: args{ + format: "abc%d", + a: []interface{}{1}, + }, + want: "abc1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Strong(tt.args.format, tt.args.a...); got != tt.want { + t.Errorf("Strong() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/rend/chain_theme.go b/internal/rend/chain_theme.go new file mode 100644 index 0000000..30c5f5f --- /dev/null +++ b/internal/rend/chain_theme.go @@ -0,0 +1,26 @@ +package rend + +import "github.com/gochore/emailt/style" + +type ChainTheme struct { + Upstream style.Theme + Inner style.Theme +} + +func (t ChainTheme) Attributes(tag string) style.Attributes { + if theme := t.Inner; theme != nil && theme.Exists(tag) { + return theme.Attributes(tag) + } + if theme := t.Upstream; theme != nil { + return theme.Attributes(tag) + } + return nil +} + +func (t ChainTheme) Exists(tag string) bool { + if theme := t.Inner; theme != nil && theme.Exists(tag) { + return true + } + theme := t.Upstream + return theme != nil && theme.Exists(tag) +} diff --git a/internal/rend/chain_theme_test.go b/internal/rend/chain_theme_test.go new file mode 100644 index 0000000..9eb1a20 --- /dev/null +++ b/internal/rend/chain_theme_test.go @@ -0,0 +1,85 @@ +package rend + +import ( + "testing" + + "github.com/gochore/emailt/style" +) + +func TestChainTheme_Exists(t1 *testing.T) { + type fields struct { + upstream style.Theme + inner style.Theme + } + type args struct { + tag string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "regular", + fields: fields{ + upstream: style.MapTheme{ + "b": style.Attributes{ + { + Key: "bn", + Val: "bv", + }, + }, + }, + inner: style.MapTheme{ + "a": style.Attributes{ + { + Key: "an", + Val: "av", + }, + }, + }, + }, + args: args{ + tag: "a", + }, + want: true, + }, + { + name: "not exist", + fields: fields{ + upstream: style.MapTheme{ + "b": style.Attributes{ + { + Key: "bn", + Val: "bv", + }, + }, + }, + inner: style.MapTheme{ + "a": style.Attributes{ + { + Key: "an", + Val: "av", + }, + }, + }, + }, + args: args{ + tag: "c", + }, + want: false, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t := ChainTheme{ + Upstream: tt.fields.upstream, + Inner: tt.fields.inner, + } + if got := t.Exists(tt.args.tag); got != tt.want { + t1.Errorf("Exists() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/util.go b/internal/rend/rend.go similarity index 50% rename from util.go rename to internal/rend/rend.go index 98cdc30..7db063c 100644 --- a/util.go +++ b/internal/rend/rend.go @@ -1,4 +1,4 @@ -package emailt +package rend import ( "fmt" @@ -6,21 +6,23 @@ import ( "golang.org/x/net/html" "golang.org/x/net/html/atom" + + "github.com/gochore/emailt/style" ) -type fmtWriter struct { +type FmtWriter struct { writer io.Writer err error } -func newFmtWriter(writer io.Writer) *fmtWriter { - return &fmtWriter{ +func NewFmtWriter(writer io.Writer) *FmtWriter { + return &FmtWriter{ writer: writer, err: nil, } } -func (w fmtWriter) Write(p []byte) (n int, err error) { +func (w FmtWriter) Write(p []byte) (n int, err error) { if w.err != nil { return 0, w.err } @@ -29,72 +31,71 @@ func (w fmtWriter) Write(p []byte) (n int, err error) { return } -func (w *fmtWriter) Err() error { +func (w *FmtWriter) Err() error { return w.err } -func (w *fmtWriter) Print(a ...interface{}) { +func (w *FmtWriter) Print(a ...interface{}) { if w.err != nil { return } _, w.err = fmt.Fprint(w.writer, a...) } -func (w *fmtWriter) Println(a ...interface{}) { +func (w *FmtWriter) Println(a ...interface{}) { if w.err != nil { return } _, w.err = fmt.Fprintln(w.writer, a...) } -func (w *fmtWriter) Printlnf(format string, a ...interface{}) { +func (w *FmtWriter) Printlnf(format string, a ...interface{}) { w.Printf(format+"\n", a...) } -func (w *fmtWriter) Printf(format string, a ...interface{}) { +func (w *FmtWriter) Printf(format string, a ...interface{}) { if w.err != nil { return } _, w.err = fmt.Fprintf(w.writer, format, a...) } -func writeStyles(node *html.Node, theme Theme) { +func WriteTheme(node *html.Node, theme style.Theme) { if node == nil { return } if node.Type == html.ElementNode { - attrs := theme.Attributes(node.Data).Merge(parseHtmlAttributes(node.Attr)) - node.Attr = attrs.exportHtmlAttributes() + node.Attr = theme.Attributes(node.Data).Merge(node.Attr) } - writeStyles(node.FirstChild, theme) - writeStyles(node.NextSibling, theme) + WriteTheme(node.FirstChild, theme) + WriteTheme(node.NextSibling, theme) } -func mergeThemes(themes []Theme) Theme { +func MergeThemes(themes []style.Theme) style.Theme { theme := ChainTheme{} for _, m := range themes { theme = ChainTheme{ - upstream: theme, - inner: m, + Upstream: theme, + Inner: m, } } return theme } -func htmlRender(reader io.Reader, writer io.Writer, theme Theme) error { +func RenderTheme(reader io.Reader, writer io.Writer, theme style.Theme) error { nodes, err := html.ParseFragment(reader, &html.Node{ Type: html.ElementNode, DataAtom: atom.Div, Data: "div", }) if err != nil { - return fmt.Errorf("ParseFragment: %w", err) + return fmt.Errorf("html.ParseFragment: %w", err) } for _, node := range nodes { - writeStyles(node, theme) + WriteTheme(node, theme) if err := html.Render(writer, node); err != nil { - return fmt.Errorf("html render: %w", err) + return fmt.Errorf("html.Render: %w", err) } } return nil diff --git a/list.go b/list.go index caff57f..ac71dd8 100644 --- a/list.go +++ b/list.go @@ -3,6 +3,9 @@ package emailt import ( "fmt" "io" + + "github.com/gochore/emailt/internal/rend" + "github.com/gochore/emailt/style" ) type List struct { @@ -28,10 +31,12 @@ func (l *List) Add(item ...Element) { l.items = append(l.items, item...) } -func (l *List) Render(writer io.Writer, themes ...Theme) error { - theme := mergeThemes(themes) +func (l *List) Render(writer io.Writer, themes ...style.Theme) error { + errPrefix := "List.Render: " + + theme := rend.MergeThemes(themes) - render := newFmtWriter(writer) + render := rend.NewFmtWriter(writer) tag := "ul" if l.ordered { @@ -43,12 +48,15 @@ func (l *List) Render(writer io.Writer, themes ...Theme) error { for _, item := range l.items { render.Printf("
  • ", theme.Attributes("li")) if err := item.Render(render, theme); err != nil { - return fmt.Errorf("render: %w", err) + return fmt.Errorf(errPrefix+"render li: %w", err) } render.Printlnf("
  • ") } render.Printlnf("", tag) - return render.Err() + if err := render.Err(); err != nil { + return fmt.Errorf(errPrefix+"%w", err) + } + return nil } diff --git a/list_test.go b/list_test.go index 6ca89e8..fbfb51e 100644 --- a/list_test.go +++ b/list_test.go @@ -9,6 +9,9 @@ import ( "testing" "golang.org/x/net/html" + + "github.com/gochore/emailt/htmlt" + "github.com/gochore/emailt/style" ) func TestList_Render(t *testing.T) { @@ -17,7 +20,7 @@ func TestList_Render(t *testing.T) { ordered bool } type args struct { - themes []Theme + themes []style.Theme } tests := []struct { name string @@ -29,8 +32,8 @@ func TestList_Render(t *testing.T) { name: "unordered", fields: fields{ items: []Element{ - NewStringElement("A"), - NewStringElement("B"), + htmlt.Sprintf("A"), + htmlt.Sprintf("B"), }, ordered: false, }, @@ -41,8 +44,8 @@ func TestList_Render(t *testing.T) { name: "ordered", fields: fields{ items: []Element{ - NewStringElement("A"), - NewStringElement("B"), + htmlt.Sprintf("A"), + htmlt.Sprintf("B"), }, ordered: true, }, diff --git a/style/attribute.go b/style/attribute.go new file mode 100644 index 0000000..3aa2b93 --- /dev/null +++ b/style/attribute.go @@ -0,0 +1,37 @@ +package style + +import ( + "fmt" + "strings" + + "golang.org/x/net/html" +) + +type Attributes []html.Attribute + +func (as Attributes) String() string { + var strs []string + for _, a := range as { + strs = append(strs, fmt.Sprintf(`%s="%s"`, a.Key, a.Val)) + } + return strings.Join(strs, " ") +} + +func (as Attributes) Merge(newAs Attributes) Attributes { + ret := make(Attributes, len(as)) + copy(ret, as) + for _, v := range newAs { + found := false + for i, v2 := range ret { + if v2.Key == v.Key { + ret[i].Val = v.Val + found = true + break + } + } + if !found { + ret = append(ret, v) + } + } + return ret +} diff --git a/style/theme.go b/style/theme.go new file mode 100644 index 0000000..faa98cb --- /dev/null +++ b/style/theme.go @@ -0,0 +1,28 @@ +package style + +type Theme interface { + Attributes(tag string) Attributes + Exists(tag string) bool +} + +type MapTheme map[string]Attributes + +func (t MapTheme) Attributes(tag string) Attributes { + if len(t) == 0 { + return nil + } + return t[tag] +} + +func (t MapTheme) Exists(tag string) bool { + if len(t) == 0 { + return false + } + _, ok := t[tag] + return ok +} + +type Attribute struct { + Key string + Val string +} diff --git a/theme_test.go b/style/theme_test.go similarity index 55% rename from theme_test.go rename to style/theme_test.go index b07a444..923c763 100644 --- a/theme_test.go +++ b/style/theme_test.go @@ -1,4 +1,4 @@ -package emailt +package style import ( "reflect" @@ -15,12 +15,12 @@ func TestAttributes_String(t *testing.T) { name: "regular", as: Attributes{ { - Name: "a", - Value: "aa", + Key: "a", + Val: "aa", }, { - Name: "b", - Value: "bb", + Key: "b", + Val: "bb", }, }, want: `a="aa" b="bb"`, @@ -55,8 +55,8 @@ func TestMapTheme_Attributes(t *testing.T) { t: MapTheme{ "a": Attributes{ { - Name: "an", - Value: "av", + Key: "an", + Val: "av", }, }, }, @@ -65,8 +65,8 @@ func TestMapTheme_Attributes(t *testing.T) { }, want: Attributes{ { - Name: "an", - Value: "av", + Key: "an", + Val: "av", }, }, }, @@ -103,8 +103,8 @@ func TestMapTheme_Exists(t *testing.T) { t: MapTheme{ "a": Attributes{ { - Name: "an", - Value: "av", + Key: "an", + Val: "av", }, }, }, @@ -130,81 +130,3 @@ func TestMapTheme_Exists(t *testing.T) { }) } } - -func TestChainTheme_Exists(t1 *testing.T) { - type fields struct { - upstream Theme - inner Theme - } - type args struct { - tag string - } - tests := []struct { - name string - fields fields - args args - want bool - }{ - { - name: "regular", - fields: fields{ - upstream: MapTheme{ - "b": Attributes{ - { - Name: "bn", - Value: "bv", - }, - }, - }, - inner: MapTheme{ - "a": Attributes{ - { - Name: "an", - Value: "av", - }, - }, - }, - }, - args: args{ - tag: "a", - }, - want: true, - }, - { - name: "not exist", - fields: fields{ - upstream: MapTheme{ - "b": Attributes{ - { - Name: "bn", - Value: "bv", - }, - }, - }, - inner: MapTheme{ - "a": Attributes{ - { - Name: "an", - Value: "av", - }, - }, - }, - }, - args: args{ - tag: "c", - }, - want: false, - }, - } - for _, tt := range tests { - t1.Run(tt.name, func(t1 *testing.T) { - t := ChainTheme{ - upstream: tt.fields.upstream, - inner: tt.fields.inner, - } - if got := t.Exists(tt.args.tag); got != tt.want { - t1.Errorf("Exists() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/table.go b/table.go index f079702..6658e5a 100644 --- a/table.go +++ b/table.go @@ -5,6 +5,10 @@ import ( "io" "reflect" "sort" + "text/template" + + "github.com/gochore/emailt/internal/rend" + "github.com/gochore/emailt/style" ) type Column struct { @@ -17,6 +21,7 @@ type Columns []Column type Table struct { dataset interface{} columns Columns + funcs template.FuncMap } func NewTable() *Table { @@ -31,16 +36,22 @@ func (t *Table) SetColumns(columns Columns) { t.columns = columns } -func (t *Table) Render(writer io.Writer, themes ...Theme) error { - theme := mergeThemes(themes) +func (t *Table) SetFuncs(funcs template.FuncMap) { + t.funcs = funcs +} + +func (t *Table) Render(writer io.Writer, themes ...style.Theme) error { + errPrefix := "Table.Render: " + + theme := rend.MergeThemes(themes) dataset := reflect.ValueOf(t.dataset) if dataset.Kind() != reflect.Slice { - return fmt.Errorf("%v is not a slice", dataset.Type()) + return fmt.Errorf(errPrefix+"%v is not a slice", dataset.Type()) } if dataset.Len() == 0 { - return fmt.Errorf("empty data") + return fmt.Errorf(errPrefix + "empty data") } mapItem := false @@ -51,15 +62,15 @@ func (t *Table) Render(writer io.Writer, themes ...Theme) error { case reflect.Struct: // do nothing default: - return fmt.Errorf("unsupported slice item type: %v", typ) + return fmt.Errorf(errPrefix+"unsupported slice item type: %v", typ) } if typ.Kind() != reflect.Struct && typ.Kind() != reflect.Map { - return fmt.Errorf("%v is not a struct", typ) + return fmt.Errorf(errPrefix+"%v is not a struct", typ) } for i := 0; i < dataset.Len(); i++ { if t := dataset.Index(i).Type(); t != typ { - return fmt.Errorf("item %v is %v, not %v", i, t, typ) + return fmt.Errorf(errPrefix+"item %v is %v, not %v", i, t, typ) } } @@ -89,7 +100,7 @@ func (t *Table) Render(writer io.Writer, themes ...Theme) error { } } - render := newFmtWriter(writer) + render := rend.NewFmtWriter(writer) render.Printlnf("", theme.Attributes("table")) @@ -107,9 +118,10 @@ func (t *Table) Render(writer io.Writer, themes ...Theme) error { e := TemplateElement{ Data: dataset.Index(i), Template: column.Template, + Funcs: t.funcs, } if err := e.Render(writer, theme); err != nil { - return fmt.Errorf("render: %w", err) + return fmt.Errorf(errPrefix+"render td: %w", err) } render.Println("\n") } @@ -118,5 +130,8 @@ func (t *Table) Render(writer io.Writer, themes ...Theme) error { render.Println("
    ") - return render.Err() + if err := render.Err(); err != nil { + return fmt.Errorf(errPrefix+"%w", err) + } + return nil } diff --git a/table_test.go b/table_test.go index b4d215e..4241f4a 100644 --- a/table_test.go +++ b/table_test.go @@ -6,7 +6,9 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" + "text/template" "golang.org/x/net/html" ) @@ -20,6 +22,7 @@ func TestTable_Render(t1 *testing.T) { type fields struct { Dataset interface{} Columns []Column + Funcs template.FuncMap } tests := []struct { name string @@ -102,12 +105,50 @@ func TestTable_Render(t1 *testing.T) { }, wantErr: false, }, + { + name: "with_funcs", + fields: fields{ + Dataset: []TestStruct1{ + { + A: "hello", + B: 1, + }, + { + A: "a2", + B: 2, + }, + { + A: "a3", + B: 3, + }, + }, + Columns: []Column{ + { + Name: "列1", + Template: "{{title .A}}", + }, + { + Name: "列2", + Template: "{{.B}}", + }, + { + Name: "列3", + Template: "{{.A}}({{.B}})", + }, + }, + Funcs: template.FuncMap{ + "title": strings.Title, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { t := NewTable() t.SetColumns(tt.fields.Columns) t.SetDataset(tt.fields.Dataset) + t.SetFuncs(tt.fields.Funcs) got := bytes.NewBuffer(nil) err := t.Render(got) if (err != nil) != tt.wantErr { diff --git a/theme.go b/theme.go index e80c9a2..dc3473a 100644 --- a/theme.go +++ b/theme.go @@ -1,123 +1,17 @@ package emailt -import ( - "fmt" - "strings" - - "golang.org/x/net/html" -) - -type Theme interface { - Attributes(tag string) Attributes - Exists(tag string) bool -} - -type MapTheme map[string]Attributes - -func (t MapTheme) Attributes(tag string) Attributes { - if len(t) == 0 { - return nil - } - return t[tag] -} - -func (t MapTheme) Exists(tag string) bool { - if len(t) == 0 { - return false - } - _, ok := t[tag] - return ok -} - -type ChainTheme struct { - upstream Theme - inner Theme -} - -func (t ChainTheme) Attributes(tag string) Attributes { - if theme := t.inner; theme != nil && theme.Exists(tag) { - return theme.Attributes(tag) - } - if theme := t.upstream; theme != nil { - return theme.Attributes(tag) - } - return nil -} - -func (t ChainTheme) Exists(tag string) bool { - if theme := t.inner; theme != nil && theme.Exists(tag) { - return true - } - theme := t.upstream - return theme != nil && theme.Exists(tag) -} +import "github.com/gochore/emailt/style" var ( - DefaultTheme Theme = MapTheme{ - "table": Attributes{ - {Name: "style", Value: "border:1px black solid; padding:3px 3px 3px 3px; border-collapse:collapse;"}, + DefaultTheme style.Theme = style.MapTheme{ + "table": style.Attributes{ + {Key: "style", Val: "border:1px black solid; padding:3px 3px 3px 3px; border-collapse:collapse;"}, }, - "th": Attributes{ - {Name: "style", Value: "border:1px black solid; padding:3px 3px 3px 3px; border-collapse:collapse; background-color:#dedede;"}, + "th": style.Attributes{ + {Key: "style", Val: "border:1px black solid; padding:3px 3px 3px 3px; border-collapse:collapse; background-color:#dedede;"}, }, - "td": Attributes{ - {Name: "style", Value: "border:1px black solid; padding:3px 3px 3px 3px; border-collapse:collapse;"}, + "td": style.Attributes{ + {Key: "style", Val: "border:1px black solid; padding:3px 3px 3px 3px; border-collapse:collapse;"}, }, } ) - -type Attribute struct { - Name string - Value string -} - -type Attributes []Attribute - -func (as Attributes) String() string { - var strs []string - for _, a := range as { - strs = append(strs, fmt.Sprintf(`%s="%s"`, a.Name, a.Value)) - } - return strings.Join(strs, " ") -} - -func (as Attributes) Merge(newAs Attributes) Attributes { - ret := make(Attributes, len(as)) - copy(ret, as) - for _, v := range newAs { - found := false - for i, v2 := range ret { - if v2.Name == v.Name { - ret[i].Value = v.Value - found = true - break - } - } - if !found { - ret = append(ret, v) - } - } - return ret -} - -func (as Attributes) exportHtmlAttributes() []html.Attribute { - var ret []html.Attribute - for _, v := range as { - ret = append(ret, html.Attribute{ - Key: v.Name, - Val: v.Value, - }) - } - return ret -} - -func parseHtmlAttributes(attributes []html.Attribute) Attributes { - var ret Attributes - for _, v := range attributes { - ret = append(ret, Attribute{ - Name: v.Key, - Value: v.Val, - }) - } - return ret -}