From 29e244c48222278c2f255ff2adf45e3af7c9f895 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 16:50:45 +0200 Subject: export: internal/export (Markdown/Text/ReadingsPDF via pure-Go go-pdf/fpdf + embedded DejaVu Sans, covers pl/la/gr); cli --md/--pdf/--out; web /export + download links; v0.22.0 --- internal/export/export.go | 128 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 internal/export/export.go (limited to 'internal/export/export.go') diff --git a/internal/export/export.go b/internal/export/export.go new file mode 100644 index 0000000..96491db --- /dev/null +++ b/internal/export/export.go @@ -0,0 +1,128 @@ +// Package export renders a day's readings (or a passage) to shareable formats: +// plain text, Markdown, and PDF -- plus a printable A4 month calendar. The PDF +// uses an embedded DejaVu Sans (regular + bold) so Polish, Latin and Greek all +// render; it is pure Go (no cgo), keeping the single-binary cross-compile. +package export + +import ( + "bytes" + _ "embed" + "fmt" + "strings" + + "github.com/go-pdf/fpdf" + + "github.com/lukaszkasprzak/lectio/internal/i18n" + "github.com/lukaszkasprzak/lectio/internal/liturgy" + "github.com/lukaszkasprzak/lectio/internal/render" +) + +// versionLabel is the localized name of a version (i18n), or the bare code. +func versionLabel(version, lang string) string { + if l, ok := i18n.Get(lang).Version[version]; ok { + return l + } + return version +} + +// title is the document heading: the celebration name if the source carries +// one, else a plain "readings for DATE" style line. +func title(date string, info liturgy.DayInfo) string { + if info.Name != "" { + return info.Name + } + return date +} + +// Text renders a day's readings as plain UTF-8 text: a title, the day-info, +// then each section's localized heading and its version blocks. +func Text(date string, info liturgy.DayInfo, secs []liturgy.Section, version, lectionary, lang string) string { + var b strings.Builder + fmt.Fprintf(&b, "%s\n", title(date, info)) + if info.Season != "" { + fmt.Fprintf(&b, "%s\n", info.Season) + } + fmt.Fprintf(&b, "%s · %s\n\n", date, versionLabel(version, lang)) + for _, sec := range secs { + fmt.Fprintf(&b, "%s\n\n", render.HeadingWithRef(sec, lang)) + _, blocks := render.GatherVersion(version, sec, lectionary, lang) + for _, blk := range blocks { + fmt.Fprintf(&b, "%s\n", blk) + } + b.WriteString("\n") + } + return b.String() +} + +// Markdown renders a day's readings as Markdown (# title, ## section headings, +// verse/paragraph blocks). +func Markdown(date string, info liturgy.DayInfo, secs []liturgy.Section, version, lectionary, lang string) string { + var b strings.Builder + fmt.Fprintf(&b, "# %s\n\n", title(date, info)) + if info.Season != "" { + fmt.Fprintf(&b, "*%s*\n\n", info.Season) + } + fmt.Fprintf(&b, "**%s · %s**\n\n", date, versionLabel(version, lang)) + for _, sec := range secs { + fmt.Fprintf(&b, "## %s\n\n", render.HeadingWithRef(sec, lang)) + _, blocks := render.GatherVersion(version, sec, lectionary, lang) + for _, blk := range blocks { + fmt.Fprintf(&b, "%s\n\n", blk) + } + } + return b.String() +} + +// ReadingsPDF renders a day's readings as a clean printable PDF. +func ReadingsPDF(date string, info liturgy.DayInfo, secs []liturgy.Section, version, lectionary, lang string) ([]byte, error) { + pdf := newPDF() + pdf.AddPage() + + pdf.SetFont(fontFamily, "B", 18) + pdf.MultiCell(0, 9, title(date, info), "", "L", false) + if info.Season != "" { + pdf.SetFont(fontFamily, "", 10) + pdf.MultiCell(0, 6, info.Season, "", "L", false) + } + pdf.SetFont(fontFamily, "", 10) + pdf.MultiCell(0, 6, date+" · "+versionLabel(version, lang), "", "L", false) + pdf.Ln(3) + + for _, sec := range secs { + pdf.Ln(2) + pdf.SetFont(fontFamily, "B", 13) + pdf.MultiCell(0, 7, render.HeadingWithRef(sec, lang), "", "L", false) + pdf.Ln(1) + pdf.SetFont(fontFamily, "", 11) + _, blocks := render.GatherVersion(version, sec, lectionary, lang) + for _, blk := range blocks { + pdf.MultiCell(0, 6, blk, "", "L", false) + pdf.Ln(1) + } + } + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +//go:embed fonts/DejaVuSans.ttf +var fontRegular []byte + +//go:embed fonts/DejaVuSans-Bold.ttf +var fontBold []byte + +const fontFamily = "dejavu" + +// newPDF returns an A4 portrait document with the embedded Unicode font +// registered (regular + bold) and sensible margins/auto page break. +func newPDF() *fpdf.Fpdf { + pdf := fpdf.New("P", "mm", "A4", "") + pdf.AddUTF8FontFromBytes(fontFamily, "", fontRegular) + pdf.AddUTF8FontFromBytes(fontFamily, "B", fontBold) + pdf.SetMargins(18, 16, 18) + pdf.SetAutoPageBreak(true, 16) + return pdf +} -- cgit v1.3