Skip to content

Commit 1f9cf78

Browse files
committed
feat: refactor table builder and plaintext builder
1 parent bd33242 commit 1f9cf78

File tree

22 files changed

+548
-760
lines changed

22 files changed

+548
-760
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ go.work.sum
2929
.env
3030

3131
# Editor/IDE
32-
# .idea/
33-
# .vscode/
32+
.idea/
33+
.vscode/

builder.go

Lines changed: 93 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"bytes"
55
"fmt"
66
htmltemplate "html/template"
7+
"maps"
78
"regexp"
89
"strings"
910
"sync/atomic"
10-
texttemplate "text/template"
1111
"time"
1212

13+
"github.com/ahmadfaizk/go-mailgen/component"
1314
"github.com/ahmadfaizk/go-mailgen/templates"
1415
"github.com/vanng822/go-premailer/premailer"
1516
)
@@ -22,6 +23,21 @@ type Product struct {
2223
Copyright string
2324
}
2425

26+
type Table struct {
27+
Data [][]Entry
28+
Columns Columns
29+
}
30+
31+
type Entry struct {
32+
Key string
33+
Value string
34+
}
35+
36+
type Columns struct {
37+
CustomWidth map[string]string
38+
CustomAlign map[string]string
39+
}
40+
2541
// Builder represents an email message with various fields such as subject, recipients, and content.
2642
// It provides methods to set these fields and generate the HTML content for the email.
2743
type Builder struct {
@@ -35,26 +51,28 @@ type Builder struct {
3551
preheader string
3652
greeting string
3753
salutation string
38-
actions []Action
39-
components []Component
54+
actions []*component.Action
55+
components []component.Component
4056
product Product
4157
}
4258

4359
var defaultBuilder atomic.Pointer[Builder]
4460

4561
func init() {
46-
defaultBuilder.Store(newdefaultBuilder())
62+
defaultBuilder.Store(newDefaultBuilder())
4763
}
4864

49-
func newdefaultBuilder() *Builder {
50-
builder := &Builder{}
51-
return builder.Theme("default").
52-
Greeting("Hello").
53-
Salutation("Best regards").
54-
Product(Product{
55-
Name: "GoMailgen",
56-
URL: "https://github.com/ahmadfaizk/go-mailgen",
57-
})
65+
func newDefaultBuilder() *Builder {
66+
return &Builder{
67+
theme: "default",
68+
greeting: "Hello",
69+
salutation: "Best regards",
70+
product: Product{
71+
Name: "Go-Mailgen",
72+
URL: "https://github.com/ahmadfaizk/go-mailgen",
73+
Copyright: fmt.Sprintf("© %d Go-Mailgen. All rights reserved.", time.Now().Year()),
74+
},
75+
}
5876
}
5977

6078
func (b *Builder) clone() *Builder {
@@ -68,8 +86,8 @@ func (b *Builder) clone() *Builder {
6886
preheader: b.preheader,
6987
greeting: b.greeting,
7088
salutation: b.salutation,
71-
actions: append([]Action{}, b.actions...),
72-
components: append([]Component{}, b.components...),
89+
actions: append([]*component.Action{}, b.actions...),
90+
components: append([]component.Component{}, b.components...),
7391
product: b.product,
7492
}
7593
}
@@ -84,11 +102,11 @@ func SetDefault(b *Builder) {
84102
defaultBuilder.Store(b)
85103
}
86104

87-
// NewMessage creates a new Message instance with default values for greeting, salutation, and product.
105+
// New creates a new Message instance with default values for greeting, salutation, and product.
88106
//
89107
// Example usage:
90108
//
91-
// messsage := mailer.NewMessage().
109+
// message := mailer.NewMessage().
92110
// Subject("Reset Password").
93111
// To("recipient@example.com").
94112
// Line("Click the button below to reset your password").
@@ -117,7 +135,7 @@ func (b *Builder) From(address string, name ...string) *Builder {
117135
return b
118136
}
119137

120-
// To adds a recipient's email address to the email message.
138+
// To add a recipient's email address to the email message.
121139
func (b *Builder) To(to string, others ...string) *Builder {
122140
values := b.filterRecipients(to, others...)
123141
if len(values) == 0 {
@@ -143,7 +161,8 @@ func (b *Builder) filterRecipients(first string, others ...string) []string {
143161
return filtered
144162
}
145163

146-
func (b *Builder) CC(cc string, others ...string) *Builder {
164+
// Cc adds a carbon copy (CC) recipient's email address to the email message.
165+
func (b *Builder) Cc(cc string, others ...string) *Builder {
147166
values := b.filterRecipients(cc, others...)
148167
if len(values) == 0 {
149168
return b
@@ -152,7 +171,8 @@ func (b *Builder) CC(cc string, others ...string) *Builder {
152171
return b
153172
}
154173

155-
func (b *Builder) BCC(bcc string, others ...string) *Builder {
174+
// Bcc adds a blind carbon copy (BCC) recipient's email address to the email message.
175+
func (b *Builder) Bcc(bcc string, others ...string) *Builder {
156176
values := b.filterRecipients(bcc, others...)
157177
if len(values) == 0 {
158178
return b
@@ -179,7 +199,7 @@ func (b *Builder) Preheader(preheader string) *Builder {
179199
}
180200

181201
// Greeting sets the greeting line of the email message.
182-
// Default is "Hello".
202+
// The default is "Hello".
183203
func (b *Builder) Greeting(greeting string) *Builder {
184204
b.greeting = greeting
185205
return b
@@ -195,7 +215,7 @@ func (b *Builder) Salutation(salutation string) *Builder {
195215
// Line adds a line of text to the email message.
196216
// If an action is set, it will be added to the outro lines; otherwise, it will be added to the intro lines.
197217
func (b *Builder) Line(text string) *Builder {
198-
b.components = append(b.components, Line{Text: text})
218+
b.components = append(b.components, component.Line{Text: text})
199219
return b
200220
}
201221

@@ -210,7 +230,7 @@ func (b *Builder) Linef(format string, args ...interface{}) *Builder {
210230
// It creates a button in the email that links to the specified URL.
211231
// The action can also include an optional instruction and color for the button.
212232
func (b *Builder) Action(text, url string, color ...string) *Builder {
213-
action := Action{
233+
action := &component.Action{
214234
Text: text,
215235
URL: url,
216236
Color: "#3869D4",
@@ -225,9 +245,11 @@ func (b *Builder) Action(text, url string, color ...string) *Builder {
225245

226246
// Product sets the product information for the email message.
227247
func (b *Builder) Product(product Product) *Builder {
248+
defaultProduct := defaultBuilder.Load().product
249+
228250
b.product = product
229251
if b.product.Name == "" {
230-
b.product.Name = "GoMailgen"
252+
b.product.Name = defaultProduct.Name
231253
}
232254
if b.product.URL == "" {
233255
b.product.URL = "#"
@@ -253,23 +275,27 @@ func (b *Builder) Product(product Product) *Builder {
253275
// },
254276
// })
255277
func (b *Builder) Table(table Table) *Builder {
256-
// Ensure headers have default values for width and alignment
257-
for i, header := range table.Headers {
258-
if header.Width == "" {
259-
table.Headers[i].Width = "auto"
260-
}
261-
if header.Align == "" {
262-
table.Headers[i].Align = "left"
263-
}
264-
}
265-
if table.Rows == nil {
266-
table.Rows = [][]string{}
267-
}
268-
if len(table.Rows) == 0 && len(table.Headers) == 0 {
278+
if len(table.Data) == 0 {
269279
return b // No table to add
270280
}
271-
272-
b.components = append(b.components, table)
281+
// headers := make([]string, 0, len(table.Data[0]))
282+
// for _, entry := range table.Data[0] {
283+
// headers = append(headers, entry.Key)
284+
285+
// width, ok := table.Columns.CustomWidth[entry.Key]
286+
// if !ok || width == "" {
287+
// width = "auto"
288+
// }
289+
// table.Columns.CustomWidth[entry.Key] = width
290+
291+
// align, ok := table.Columns.CustomAlign[entry.Key]
292+
// if !ok || align == "" {
293+
// align = "left"
294+
// }
295+
// table.Columns.CustomAlign[entry.Key] = align
296+
// }
297+
298+
b.components = append(b.components, table.component())
273299
return b
274300
}
275301

@@ -303,7 +329,7 @@ type templateData struct {
303329
Salutation string
304330
ComponentsHTML []htmltemplate.HTML
305331
ComponentsText []string
306-
Actions []Action // Used for sub-copy in HTML
332+
Actions []*component.Action // Used for sub-copy in HTML
307333
Product Product
308334
}
309335

@@ -314,13 +340,6 @@ func (b *Builder) htmlTemplate() *htmltemplate.Template {
314340
return templates.DefaultHtmlTmpl
315341
}
316342

317-
func (b *Builder) plainTextTemplate() *texttemplate.Template {
318-
if b.theme == "plain" {
319-
return templates.PlainPlainTextTmpl
320-
}
321-
return templates.DefaultPlainTextTmpl
322-
}
323-
324343
func (b *Builder) generateHTML() (string, error) {
325344
tmpl := b.htmlTemplate()
326345

@@ -359,15 +378,18 @@ func (b *Builder) generateHTML() (string, error) {
359378
}
360379

361380
func (b *Builder) generatePlaintext() (string, error) {
362-
tmpl := b.plainTextTemplate()
381+
tmpl := templates.PlainTextTmpl
363382
var componentsText []string
364383
for _, comp := range b.components {
365-
text := comp.PlainText()
384+
text, err := comp.PlainText(tmpl)
385+
if err != nil {
386+
return "", err
387+
}
366388
componentsText = append(componentsText, text)
367389
}
368390

369391
data := templateData{
370-
Greeting: boxString(fmt.Sprintf("%s,", b.greeting)),
392+
Greeting: b.greeting,
371393
Preheader: b.preheader,
372394
Salutation: b.salutation,
373395
Product: b.product,
@@ -386,24 +408,28 @@ func (b *Builder) generatePlaintext() (string, error) {
386408
return text, nil
387409
}
388410

389-
func boxString(s string) string {
390-
// Find the max line length (in case of multi-line input)
391-
lines := strings.Split(s, "\n")
392-
maxLen := 0
393-
for _, line := range lines {
394-
if len(line) > maxLen {
395-
maxLen = len(line)
411+
func (t Table) component() component.Component {
412+
if len(t.Data) == 0 {
413+
return nil
414+
}
415+
tableComponent := component.Table{
416+
Data: make([][]component.Entry, len(t.Data)),
417+
Columns: component.Columns{
418+
CustomWidth: make(map[string]string),
419+
CustomAlign: make(map[string]string),
420+
},
421+
}
422+
for i, row := range t.Data {
423+
tableComponent.Data[i] = make([]component.Entry, len(row))
424+
for j, entry := range row {
425+
tableComponent.Data[i][j] = component.Entry{
426+
Key: entry.Key,
427+
Value: entry.Value,
428+
}
396429
}
397430
}
431+
maps.Copy(tableComponent.Columns.CustomWidth, t.Columns.CustomWidth)
432+
maps.Copy(tableComponent.Columns.CustomAlign, t.Columns.CustomAlign)
398433

399-
// The border should match the longest line length
400-
border := strings.Repeat("*", maxLen)
401-
402-
// Combine
403-
var b strings.Builder
404-
b.WriteString(border + "\n")
405-
b.WriteString(s + "\n")
406-
b.WriteString(border)
407-
408-
return b.String()
434+
return &tableComponent
409435
}

0 commit comments

Comments
 (0)