Skip to content

Commit 12fde0c

Browse files
committed
add invoice renderer
1 parent 534bc64 commit 12fde0c

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed

MIGRATION.md

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,244 @@ func main() {
416416
</table>
417417
```
418418

419+
420+
#### Custom Invoice Renderer
421+
422+
```go
423+
424+
package main
425+
426+
import (
427+
"fmt"
428+
"io"
429+
"os"
430+
"strings"
431+
432+
"github.com/olekukonko/ll"
433+
"github.com/olekukonko/tablewriter"
434+
"github.com/olekukonko/tablewriter/tw"
435+
)
436+
437+
// InvoiceRenderer implements tw.Renderer for a basic invoice style.
438+
type InvoiceRenderer struct {
439+
writer io.Writer
440+
logger *ll.Logger
441+
rendition tw.Rendition
442+
}
443+
444+
func NewInvoiceRenderer() *InvoiceRenderer {
445+
rendition := tw.Rendition{
446+
Borders: tw.BorderNone,
447+
Symbols: tw.NewSymbols(tw.StyleNone),
448+
Settings: tw.Settings{Separators: tw.SeparatorsNone, Lines: tw.LinesNone},
449+
Streaming: false,
450+
}
451+
defaultLogger := ll.New("simple-invoice-renderer")
452+
return &InvoiceRenderer{logger: defaultLogger, rendition: rendition}
453+
}
454+
455+
func (r *InvoiceRenderer) Logger(logger *ll.Logger) {
456+
if logger != nil {
457+
r.logger = logger
458+
}
459+
}
460+
461+
func (r *InvoiceRenderer) Config() tw.Rendition {
462+
return r.rendition
463+
}
464+
465+
func (r *InvoiceRenderer) Start(w io.Writer) error {
466+
r.writer = w
467+
r.logger.Debug("InvoiceRenderer: Start")
468+
return nil
469+
}
470+
471+
func (r *InvoiceRenderer) formatLine(cells []string, widths tw.Mapper[int, int], cellContexts map[int]tw.CellContext) string {
472+
var sb strings.Builder
473+
numCols := 0
474+
if widths != nil { // Ensure widths is not nil before calling Len
475+
numCols = widths.Len()
476+
}
477+
478+
for i := 0; i < numCols; i++ {
479+
data := ""
480+
if i < len(cells) {
481+
data = cells[i]
482+
}
483+
484+
width := 0
485+
if widths != nil { // Check again before Get
486+
width = widths.Get(i)
487+
}
488+
489+
align := tw.AlignDefault
490+
if cellContexts != nil { // Check cellContexts
491+
if ctx, ok := cellContexts[i]; ok {
492+
align = ctx.Align
493+
}
494+
}
495+
496+
paddedCell := tw.Pad(data, " ", width, align)
497+
sb.WriteString(paddedCell)
498+
499+
if i < numCols-1 {
500+
sb.WriteString(" ") // Column separator
501+
}
502+
}
503+
return sb.String()
504+
}
505+
506+
func (r *InvoiceRenderer) Header(headers [][]string, ctx tw.Formatting) {
507+
if r.writer == nil {
508+
return
509+
}
510+
r.logger.Debugf("InvoiceRenderer: Header (lines: %d)", len(headers))
511+
512+
for _, headerLineCells := range headers {
513+
lineStr := r.formatLine(headerLineCells, ctx.Row.Widths, ctx.Row.Current)
514+
fmt.Fprintln(r.writer, lineStr)
515+
}
516+
517+
if len(headers) > 0 {
518+
totalWidth := 0
519+
if ctx.Row.Widths != nil {
520+
ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w })
521+
if ctx.Row.Widths.Len() > 1 {
522+
totalWidth += (ctx.Row.Widths.Len() - 1) * 3 // Separator spaces
523+
}
524+
}
525+
if totalWidth > 0 {
526+
fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth))
527+
}
528+
}
529+
}
530+
531+
func (r *InvoiceRenderer) Row(row []string, ctx tw.Formatting) {
532+
if r.writer == nil {
533+
return
534+
}
535+
r.logger.Debug("InvoiceRenderer: Row")
536+
lineStr := r.formatLine(row, ctx.Row.Widths, ctx.Row.Current)
537+
fmt.Fprintln(r.writer, lineStr)
538+
}
539+
540+
func (r *InvoiceRenderer) Footer(footers [][]string, ctx tw.Formatting) {
541+
if r.writer == nil {
542+
return
543+
}
544+
r.logger.Debugf("InvoiceRenderer: Footer (lines: %d)", len(footers))
545+
546+
if len(footers) > 0 {
547+
totalWidth := 0
548+
if ctx.Row.Widths != nil {
549+
ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w })
550+
if ctx.Row.Widths.Len() > 1 {
551+
totalWidth += (ctx.Row.Widths.Len() - 1) * 3
552+
}
553+
}
554+
if totalWidth > 0 {
555+
fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth))
556+
}
557+
}
558+
559+
for _, footerLineCells := range footers {
560+
lineStr := r.formatLine(footerLineCells, ctx.Row.Widths, ctx.Row.Current)
561+
fmt.Fprintln(r.writer, lineStr)
562+
}
563+
}
564+
565+
func (r *InvoiceRenderer) Line(ctx tw.Formatting) {
566+
r.logger.Debug("InvoiceRenderer: Line (no-op)")
567+
// This simple renderer draws its own lines in Header/Footer.
568+
}
569+
570+
func (r *InvoiceRenderer) Close() error {
571+
r.logger.Debug("InvoiceRenderer: Close")
572+
r.writer = nil
573+
return nil
574+
}
575+
576+
func main() {
577+
data := [][]string{
578+
{"Product A", "2", "10.00", "20.00"},
579+
{"Super Long Product Name B", "1", "125.50", "125.50"},
580+
{"Item C", "10", "1.99", "19.90"},
581+
}
582+
583+
header := []string{"Description", "Qty", "Unit Price", "Total Price"}
584+
footer := []string{"", "", "Subtotal:\nTax (10%):\nGRAND TOTAL:", "165.40\n16.54\n181.94"}
585+
invoiceRenderer := NewInvoiceRenderer()
586+
587+
// Create table and set custom renderer using Options
588+
table := tablewriter.NewTable(os.Stdout,
589+
tablewriter.WithRenderer(invoiceRenderer),
590+
tablewriter.WithAlignment([]tw.Align{
591+
tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight,
592+
}),
593+
)
594+
595+
table.Header(header)
596+
for _, v := range data {
597+
table.Append(v)
598+
}
599+
600+
// Use the Footer method with strings containing newlines for multi-line cells
601+
table.Footer(footer)
602+
603+
fmt.Println("Rendering with InvoiceRenderer:")
604+
table.Render()
605+
606+
// For comparison, render with default Blueprint renderer
607+
// Re-create the table or reset it to use a different renderer
608+
table2 := tablewriter.NewTable(os.Stdout,
609+
tablewriter.WithAlignment([]tw.Align{
610+
tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight,
611+
}),
612+
)
613+
614+
table2.Header(header)
615+
for _, v := range data {
616+
table2.Append(v)
617+
}
618+
table2.Footer(footer)
619+
fmt.Println("\nRendering with Default Blueprint Renderer (for comparison):")
620+
table2.Render()
621+
}
622+
623+
```
624+
625+
626+
```
627+
Rendering with InvoiceRenderer:
628+
DESCRIPTION QTY UNIT PRICE TOTAL PRICE
629+
--------------------------------------------------------------------
630+
Product A 2 10.00 20.00
631+
Super Long Product Name B 1 125.50 125.50
632+
Item C 10 1.99 19.90
633+
--------------------------------------------------------------------
634+
Subtotal: 165.40
635+
--------------------------------------------------------------------
636+
Tax (10%): 16.54
637+
--------------------------------------------------------------------
638+
GRAND TOTAL: 181.94
639+
```
640+
641+
642+
```
643+
Rendering with Default Blueprint Renderer (for comparison):
644+
┌───────────────────────────┬─────┬──────────────┬─────────────┐
645+
│ DESCRIPTION │ QTY │ UNIT PRICE │ TOTAL PRICE │
646+
├───────────────────────────┼─────┼──────────────┼─────────────┤
647+
│ Product A │ 2 │ 10.00 │ 20.00 │
648+
│ Super Long Product Name B │ 1 │ 125.50 │ 125.50 │
649+
│ Item C │ 10 │ 1.99 │ 19.90 │
650+
├───────────────────────────┼─────┼──────────────┼─────────────┤
651+
│ │ │ Subtotal: │ 165.40 │
652+
│ │ │ Tax (10%): │ 16.54 │
653+
│ │ │ GRAND TOTAL: │ 181.94 │
654+
└───────────────────────────┴─────┴──────────────┴─────────────┘
655+
656+
```
419657
**Notes**:
420658
- The `renderer.NewBlueprint()` is sufficient for most text-based use cases.
421659
- Custom renderers require implementing all interface methods to handle table structure correctly. `tw.Formatting` (which includes `tw.RowContext`) provides cell content and metadata.

0 commit comments

Comments
 (0)