+
+
+
diff --git a/internal/writing/html/writer.go b/internal/writing/html/writer.go
index 720e642..81c33ef 100644
--- a/internal/writing/html/writer.go
+++ b/internal/writing/html/writer.go
@@ -1,79 +1,73 @@
// Package html renders an internal.AggregatorWriter as a complete,
// standalone HTML document built around the same table fragment produced by
-// pretty.PrettyPrinter.RenderHTML. The document includes the Bootstrap 5.3
+// table.Printer.RenderHTML. The document includes the Bootstrap 5.3
// stylesheet via CDN and embeds the raw table fragment without HTML-escaping
// it so the result opens directly in a browser.
package html
import (
+ _ "embed"
"fmt"
"html/template"
"io"
"time"
"github.com/nmoniz/any2anexoj/internal"
- "github.com/nmoniz/any2anexoj/internal/writing/pretty"
+ "github.com/nmoniz/any2anexoj/internal/writing/table"
)
+// translator is the contract the HTML writer relies on for localized
+// document text such as the page title. *writing.Localizer satisfies it.
+type translator interface {
+ Translate(key string, count int, values map[string]any) string
+}
+
// bootstrapCSSURL is the jsDelivr CDN URL for Bootstrap 5.3.3.
const bootstrapCSSURL = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
// bootstrapIntegrity is the official SRI hash for Bootstrap 5.3.3 css.
const bootstrapIntegrity = "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
-// HTMLWriter writes a complete HTML document for an AggregatorWriter.
-type HTMLWriter struct {
- output io.Writer
- printer *pretty.PrettyPrinter
- title string
- lang string
+// Writer writes a complete HTML document for an AggregatorWriter.
+type Writer struct {
+ output io.Writer
+ printer *table.Printer
+ translator translator
+ lang string
}
-// NewHTMLWriter builds an HTMLWriter that writes a complete HTML document to
+// NewWriter builds a Writer that writes a complete HTML document to
// w. The printer is used purely as a source for the raw
fragment via
// RenderHTML; this writer never invokes Render or any method that writes to
-// printer's own io.Writer.
-func NewHTMLWriter(w io.Writer, printer *pretty.PrettyPrinter, title, lang string) *HTMLWriter {
- return &HTMLWriter{
- output: w,
- printer: printer,
- title: title,
- lang: lang,
+// printer's own io.Writer. The document title is looked up from the
+// "report_title" translation via tr, so callers never pass a pre-baked title.
+func NewWriter(w io.Writer, printer *table.Printer, tr translator, lang string) *Writer {
+ return &Writer{
+ output: w,
+ printer: printer,
+ translator: tr,
+ lang: lang,
}
}
-// documentTemplate renders the full HTML page. The table fragment is typed as
-// template.HTML so the template engine emits it verbatim without escaping
-// the angle brackets in the
-
-
-
-`))
+//go:embed document.tmpl
+var documentTemplateBody string
+
+// documentTemplate renders the full HTML page from document.tmpl. The table
+// fragment is typed as template.HTML so the template engine emits it
+// verbatim without escaping the angle brackets in the
markup.
+var documentTemplate = template.Must(template.New("html").Parse(documentTemplateBody))
// Render writes a complete standalone HTML document for aw to the configured
-// output. The same HTMLWriter may be reused; each call rebuilds the table
+// output. The same Writer may be reused; each call rebuilds the table
// fragment from aw so the rendered
count reflects aw's current items and
// no rows are duplicated across invocations.
-func (hw *HTMLWriter) Render(aw *internal.AggregatorWriter) error {
+func (hw *Writer) Render(aw *internal.AggregatorWriter) error {
if hw.printer == nil {
return fmt.Errorf("html: nil printer")
}
fragment := hw.printer.RenderHTML(aw)
+ title := hw.translator.Translate("report_title", 1, nil)
data := struct {
Lang string
Title string
@@ -83,7 +77,7 @@ func (hw *HTMLWriter) Render(aw *internal.AggregatorWriter) error {
GeneratedAt string
}{
Lang: hw.lang,
- Title: hw.title,
+ Title: title,
CSSURL: bootstrapCSSURL,
Integrity: bootstrapIntegrity,
TableFragment: template.HTML(fragment),
diff --git a/internal/writing/html/writer_test.go b/internal/writing/html/writer_test.go
index 561bb0c..1fec216 100644
--- a/internal/writing/html/writer_test.go
+++ b/internal/writing/html/writer_test.go
@@ -7,7 +7,7 @@ import (
"time"
htmlwriter "github.com/nmoniz/any2anexoj/internal/writing/html"
- "github.com/nmoniz/any2anexoj/internal/writing/pretty"
+ "github.com/nmoniz/any2anexoj/internal/writing/table"
"github.com/nmoniz/any2anexoj/internal/writing"
"github.com/nmoniz/any2anexoj/internal"
"github.com/shopspring/decimal"
@@ -16,7 +16,7 @@ import (
// buildTestAggregator returns an AggregatorWriter seeded with two
// deterministic ReportItems so every test can assert on a stable document
// shape. Mirrors the fixture pattern used in
-// internal/writing/pretty/printer_test.go.
+// internal/writing/table/printer_test.go.
func buildTestAggregator(t *testing.T) *internal.AggregatorWriter {
t.Helper()
aw := internal.NewAggregatorWriter()
@@ -57,7 +57,7 @@ func buildTestAggregator(t *testing.T) *internal.AggregatorWriter {
return aw
}
-func TestHTMLWriter_Render(t *testing.T) {
+func TestWriter_Render(t *testing.T) {
loc, err := writing.NewLocalizer("en")
if err != nil {
t.Fatalf("create localizer: %v", err)
@@ -65,8 +65,8 @@ func TestHTMLWriter_Render(t *testing.T) {
var buf bytes.Buffer
// RenderHTML never writes to the printer's output, so Discard is safe.
- pp := pretty.NewPrettyPrinter(&buf, loc)
- hw := htmlwriter.NewHTMLWriter(&buf, pp, "Tax report", "en")
+ pp := table.NewPrinter(&buf, loc)
+ hw := htmlwriter.NewWriter(&buf, pp, loc, "en")
if err := hw.Render(buildTestAggregator(t)); err != nil {
t.Fatalf("Render returned an error: %v", err)
@@ -123,18 +123,18 @@ func TestHTMLWriter_Render(t *testing.T) {
}
}
-// TestHTMLWriter_NoRowDuplication verifies that calling Render twice on the
+// TestWriter_NoRowDuplication verifies that calling Render twice on the
// same writer produces documents with the same number of
rows, just
-// like pretty.PrettyPrinter's no-duplication guarantee.
-func TestHTMLWriter_NoRowDuplication(t *testing.T) {
+// like table.Printer's no-duplication guarantee.
+func TestWriter_NoRowDuplication(t *testing.T) {
loc, err := writing.NewLocalizer("en")
if err != nil {
t.Fatalf("create localizer: %v", err)
}
var buf bytes.Buffer
- pp := pretty.NewPrettyPrinter(&buf, loc)
- hw := htmlwriter.NewHTMLWriter(&buf, pp, "Tax report", "en")
+ pp := table.NewPrinter(&buf, loc)
+ hw := htmlwriter.NewWriter(&buf, pp, loc, "en")
aw := buildTestAggregator(t)
@@ -160,15 +160,15 @@ func TestHTMLWriter_NoRowDuplication(t *testing.T) {
}
}
-func TestHTMLWriter_LangAttribute(t *testing.T) {
+func TestWriter_LangAttribute(t *testing.T) {
loc, err := writing.NewLocalizer("pt")
if err != nil {
t.Fatalf("create localizer: %v", err)
}
var buf bytes.Buffer
- pp := pretty.NewPrettyPrinter(&buf, loc)
- hw := htmlwriter.NewHTMLWriter(&buf, pp, "Relatório fiscal", "pt")
+ pp := table.NewPrinter(&buf, loc)
+ hw := htmlwriter.NewWriter(&buf, pp, loc, "pt")
if err := hw.Render(buildTestAggregator(t)); err != nil {
t.Fatalf("Render returned an error: %v", err)
diff --git a/internal/writing/table/printer.go b/internal/writing/table/printer.go
index 3a360e4..7326ee9 100644
--- a/internal/writing/table/printer.go
+++ b/internal/writing/table/printer.go
@@ -1,3 +1,6 @@
+// Package table renders an internal.AggregatorWriter as the human-readable
+// Anexo J report table, for the terminal (Render) and for embedding in HTML
+// documents (RenderHTML).
package table
import (
@@ -10,22 +13,22 @@ import (
"github.com/nmoniz/any2anexoj/internal"
)
-// translator is the contract the pretty printer relies on for human-readable
+// translator is the contract the table printer relies on for human-readable
// column labels. *Localizer satisfies it.
type translator interface {
Translate(key string, count int, values map[string]any) string
}
-// PrettyPrinter writes a simple, human readable, table row to the provided io.Writer for each
+// Printer writes a simple, human readable, table row to the provided io.Writer for each
// ReportItem received. Render and RenderHTML each build a fresh table.Writer so that
// invoking them (in any order or repeatedly) on the same instance never duplicates rows.
-type PrettyPrinter struct {
+type Printer struct {
output io.Writer
translator translator
}
-func NewPrettyPrinter(w io.Writer, tr translator) *PrettyPrinter {
- return &PrettyPrinter{
+func NewPrinter(w io.Writer, tr translator) *Printer {
+ return &Printer{
output: w,
translator: tr,
}
@@ -36,7 +39,7 @@ func NewPrettyPrinter(w io.Writer, tr translator) *PrettyPrinter {
// multiple times without duplicating rows. The returned table is not wired to
// pp.output via an output mirror; callers that want the rendered output
// written to pp.output must set the mirror themselves before calling .Render().
-func (pp *PrettyPrinter) buildTable(aw *internal.AggregatorWriter) table.Writer {
+func (pp *Printer) buildTable(aw *internal.AggregatorWriter) table.Writer {
tw := table.NewWriter()
tw.SetAutoIndex(true)
tw.SetStyle(table.StyleLight)
@@ -81,12 +84,16 @@ func (pp *PrettyPrinter) buildTable(aw *internal.AggregatorWriter) table.Writer
}
sumTxt := pp.translator.Translate("sum", 1, nil)
- tw.AppendFooter(table.Row{sumTxt, sumTxt, sumTxt, sumTxt, sumTxt, aw.TotalEarned(), "", "", "", aw.TotalSpent(), aw.TotalFees(), aw.TotalTaxes()}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
+ // The footer lists one cell per column (13) so it never relies on
+ // go-pretty's implicit short-row padding; the trailing empty cell is the
+ // counter-country column, which has no total. Totals are formatted with
+ // StringFixed(2) so they match the two-decimal data rows.
+ tw.AppendFooter(table.Row{sumTxt, sumTxt, sumTxt, sumTxt, sumTxt, aw.TotalEarned().StringFixed(2), "", "", "", aw.TotalSpent().StringFixed(2), aw.TotalFees().StringFixed(2), aw.TotalTaxes().StringFixed(2), ""}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
return tw
}
-func (pp *PrettyPrinter) Render(aw *internal.AggregatorWriter) {
+func (pp *Printer) Render(aw *internal.AggregatorWriter) {
t := pp.buildTable(aw)
t.SetOutputMirror(pp.output)
t.Render()
@@ -94,7 +101,7 @@ func (pp *PrettyPrinter) Render(aw *internal.AggregatorWriter) {
// RenderHTML builds a fresh table from aw and renders it as an HTML string
// using go-pretty's HTMLOptions.
-func (pp *PrettyPrinter) RenderHTML(aw *internal.AggregatorWriter) string {
+func (pp *Printer) RenderHTML(aw *internal.AggregatorWriter) string {
t := pp.buildTable(aw)
t.Style().HTML = table.HTMLOptions{
CSSClass: "table table-striped table-hover table-bordered",
diff --git a/internal/writing/table/printer_test.go b/internal/writing/table/printer_test.go
index 415e49e..78d7834 100644
--- a/internal/writing/table/printer_test.go
+++ b/internal/writing/table/printer_test.go
@@ -12,7 +12,7 @@ import (
"github.com/shopspring/decimal"
)
-func TestPrettyPrinter_Render(t *testing.T) {
+func TestPrinter_Render(t *testing.T) {
// Create test data
aw := internal.NewAggregatorWriter()
ctx := t.Context()
@@ -58,7 +58,7 @@ func TestPrettyPrinter_Render(t *testing.T) {
// Create writing printer with buffer
var buf bytes.Buffer
- pp := table.NewPrettyPrinter(&buf, localizer)
+ pp := table.NewPrinter(&buf, localizer)
// Render the table
pp.Render(aw)
@@ -75,13 +75,13 @@ func TestPrettyPrinter_Render(t *testing.T) {
│ 1 │ 840 - United States │ G01 │ 2023 │ 6 │ 20 │ 150.75 € │ 2023 │ 1 │ 15 │ 100.50 € │ 2.50 € │ 5.00 € │ 826 - United Kingdom │
│ 2 │ 840 - United States │ G20 │ 2023 │ 9 │ 5 │ 225.50 € │ 2023 │ 3 │ 10 │ 200.00 € │ 3.00 € │ 7.50 € │ 826 - United Kingdom │
├───┼─────────────────────┴──────┴──────┴───────┴─────┼──────────────┼──────┴───────┴─────┼──────────────┼─────────────────┼─────────────────┼──────────────────────┤
-│ │ SUM │ 376.25 € │ │ 300.5 € │ 5.5 € │ 12.5 € │ │
+│ │ SUM │ 376.25 € │ │ 300.50 € │ 5.50 € │ 12.50 € │ │
└───┴─────────────────────────────────────────────────┴──────────────┴────────────────────┴──────────────┴─────────────────┴─────────────────┴──────────────────────┘
`
// Compare output
if got != want {
- t.Errorf("PrettyPrinter.Render() output doesn't match expected.\n\nGot:\n%s\n\nWant:\n%s", got, want)
+ t.Errorf("Printer.Render() output doesn't match expected.\n\nGot:\n%s\n\nWant:\n%s", got, want)
}
}
@@ -115,7 +115,7 @@ func TestRenderHTML(t *testing.T) {
// Output buffer used only to satisfy the constructor contract; RenderHTML
// must not write to it.
var buf bytes.Buffer
- pp := table.NewPrettyPrinter(&buf, localizer)
+ pp := table.NewPrinter(&buf, localizer)
html := pp.RenderHTML(aw)