Files
any2anexoj/internal/writing/html/writer.go
T
natercio 1b5a68d290
Generate check / check-changes (pull_request) Successful in 3s
Generate check / verify-generate (pull_request) Skipped
Quality / check-changes (pull_request) Successful in 3s
Quality / static-checks (pull_request) Successful in 20s
Quality / run-tests (pull_request) Successful in 53s
embed bootstrap css
2026-10-05 15:44:40 +01:00

86 lines
3.0 KiB
Go

// Package html renders an internal.AggregatorWriter as a complete,
// standalone HTML document: the report table fragment (built from the
// shared table definition in internal/writing/internal/tabledef, so it can
// never drift from the terminal layout) embedded in a Bootstrap-styled page
// with a translated title and generation timestamp. The official Bootstrap
// 5.3.3 stylesheet is inlined via go:embed, so the document renders fully
// offline.
package html
import (
_ "embed"
"fmt"
"html/template"
"io"
"time"
"github.com/nmoniz/any2anexoj/internal"
"github.com/nmoniz/any2anexoj/internal/writing/internal/tabledef"
)
// bootstrapMinCSS is the official Bootstrap 5.3.3 minified CSS distribution
// file, embedded so every rendered document renders fully offline. After any
// version bump, re-verify provenance with:
//
// openssl dgst -sha384 -binary bootstrap.min.css | base64
//
// It must print the official SRI hash published at
// https://getbootstrap.com/docs/5.3/getting-started/download/
// (sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH).
//
//go:embed bootstrap.min.css
var bootstrapMinCSS string
// Writer writes a complete HTML document for an AggregatorWriter.
type Writer struct {
output io.Writer
translator tabledef.Translator
lang string
}
// NewWriter builds a Writer that writes a complete HTML document to w. The
// report table is rendered from the shared table definition via tr, and 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, tr tabledef.Translator, lang string) *Writer {
return &Writer{
output: w,
translator: tr,
lang: lang,
}
}
//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 <table> 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 Writer may be reused; each call rebuilds the table
// fragment from aw so the rendered <tr> count reflects aw's current items and
// no rows are duplicated across invocations.
func (hw *Writer) Render(aw *internal.AggregatorWriter) error {
fragment := hw.renderTableFragment(aw)
title := hw.translator.Translate("report_title", 1, nil)
data := struct {
Lang string
Title string
CSS template.CSS
TableFragment template.HTML
GeneratedAt string
}{
Lang: hw.lang,
Title: title,
CSS: template.CSS(bootstrapMinCSS),
TableFragment: template.HTML(fragment),
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
}
if err := documentTemplate.Execute(hw.output, data); err != nil {
return fmt.Errorf("render html document: %w", err)
}
return nil
}