diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 13:33:11 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 13:33:11 +0200 |
| commit | cb1f49117d15b9690b46f344c0d6777e558b9021 (patch) | |
| tree | 9a5e618ba198fcf8376944bc9630298ce75a2263 /internal/render/render.go | |
| parent | 0351f4f188207d0204e1e0124d32127e820e33d5 (diff) | |
| download | lectio-cb1f49117d15b9690b46f344c0d6777e558b9021.tar.gz lectio-cb1f49117d15b9690b46f344c0d6777e558b9021.zip | |
render: gather versions + compare + dedup
Diffstat (limited to 'internal/render/render.go')
| -rw-r--r-- | internal/render/render.go | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/internal/render/render.go b/internal/render/render.go new file mode 100644 index 0000000..2807272 --- /dev/null +++ b/internal/render/render.go @@ -0,0 +1,250 @@ +// Package render turns a liturgy.Section, one version at a time, into text +// output: the deduped Polish paragraphs or a bible tool's verse lines, and +// lays several versions out side by side for comparison. It is shared by +// the CLI and the TUI (which re-styles the same gathered data). +package render + +import ( + "fmt" + "strings" + + "github.com/lukaszkasprzak/lectio/internal/bible" + "github.com/lukaszkasprzak/lectio/internal/liturgy" +) + +// versionLabels names each version for column headers / prose. Ported +// verbatim from ewangelia.py VERSION_LABELS. +var versionLabels = map[string]string{ + "pl": "Polski (niedziela.pl)", + "wuj": "Wujek (pol.)", + "vul": "Wulgata (lac.)", + "grb": "Grecki", + "drb": "Douay-Rheims (ang.)", +} + +// versionSystem maps a bible version to the Psalter system bible.ToEnglishRef +// expects: vul/grb/wuj follow the Vulgate/Septuagint numbering, drb follows +// the Hebrew chapter with the DRB title-fold verse shift. Ported from +// ewangelia.py PSALM_SYSTEM. +var versionSystem = map[string]string{ + "vul": "vulgate", + "grb": "vulgate", + "wuj": "vulgate", + "drb": "drb", +} + +func system(version string) string { + if s, ok := versionSystem[version]; ok { + return s + } + return "vulgate" +} + +const incipit = "Słowa Ewangelii" + +// GatherVersion returns (label, blocks) for one version of one reading +// section. Each block is a paragraph (Polish) or a verse line (bible +// versions); on any failure the blocks hold a single short note instead. +// +// lectionary selects how sec.Citation is read: "new" (modern/niedziela.pl) +// citations are Polish and go through bible.ToEnglishRef; "traditional" +// (missalemeum) citations are already English kjv-style and are used as-is. +func GatherVersion(version string, sec liturgy.Section, lectionary string) (label string, blocks []string) { + label = versionLabels[version] + if version == "pl" { + return label, gatherPL(sec) + } + + citation := sec.Citation + if citation == "" { + if c, err := liturgy.ExtractCitation(sec.Heading); err == nil { + citation = c + } + } + if citation == "" { + return label, []string{"(brak odwołania)"} + } + + ref := citation + if lectionary == "new" { + r, err := bible.ToEnglishRef(citation, system(version)) + if err != nil { + return label, []string{fmt.Sprintf("(brak odwołania: %v)", err)} + } + ref = r + } + + verses, missing := bible.Lookup(version, ref) + if len(verses) == 0 { + return label, []string{fmt.Sprintf("(brak w „%s”)", version)} + } + + blocks = make([]string, 0, len(verses)) + for _, v := range verses { + blocks = append(blocks, fmt.Sprintf("%d:%d %s", v.Chapter, v.Verse, v.Text)) + } + if len(missing) > 0 { + blocks = append(blocks, fmt.Sprintf("(brak w „%s”: %s)", version, strings.Join(missing, ", "))) + } + return label, blocks +} + +// gatherPL returns the section's paragraphs, one block per paragraph, with +// the liturgical incipit ("Słowa Ewangelii według ...") dropped and repeated +// blocks (a responsorial psalm's refrain) deduped to their first occurrence. +func gatherPL(sec liturgy.Section) []string { + var blocks []string + for _, p := range sec.Paragraphs { + b := strings.Join(p, " ") + if strings.HasPrefix(b, incipit) { + continue + } + blocks = append(blocks, b) + } + + seen := map[string]bool{} + deduped := make([]string, 0, len(blocks)) + for _, b := range blocks { + if seen[b] { + continue + } + seen[b] = true + deduped = append(deduped, b) + } + return deduped +} + +// OfflineVersions drops "pl" (which needs the network fetch of the liturgy +// page) from versions. If "pl" was present but "wuj" (the Polish-language +// bible version) was not, "wuj" takes pl's place, so the offline set still +// carries a Polish column. +func OfflineVersions(versions []string) []string { + hadWuj := false + for _, v := range versions { + if v == "wuj" { + hadWuj = true + break + } + } + + out := make([]string, 0, len(versions)) + for _, v := range versions { + if v == "pl" { + if hadWuj { + continue + } + out = append(out, "wuj") + continue + } + out = append(out, v) + } + return out +} + +// Compare lays the versions of one reading section out as parallel columns, +// side by side, wrapped to fit width. Ports render_compare. +func Compare(secs []liturgy.Section, versions []string, width int, lectionary string) string { + var out []string + for _, sec := range secs { + out = append(out, compareSection(sec, versions, width, lectionary)) + } + return strings.Join(out, "\n\n") +} + +func compareSection(sec liturgy.Section, versions []string, width int, lectionary string) string { + type column struct { + label string + lines []string + } + + n := len(versions) + if n == 0 { + return "" + } + const gap = " " + w := (width - len(gap)*(n-1)) / n + if w < 24 { + w = 24 + } + + cols := make([]column, 0, n) + height := 0 + for _, v := range versions { + label, blocks := GatherVersion(v, sec, lectionary) + var lines []string + for _, b := range blocks { + lines = append(lines, strings.Split(wrap(b, w), "\n")...) + lines = append(lines, "") + } + if len(lines) > 0 { + lines = lines[:len(lines)-1] // drop trailing blank + } + if len(lines) > height { + height = len(lines) + } + cols = append(cols, column{label: label, lines: lines}) + } + + labelCells := make([]string, n) + dashCells := make([]string, n) + for i, c := range cols { + labelCells[i] = ljust(truncate(c.label, w), w) + dashCells[i] = strings.Repeat("-", w) + } + rows := []string{ + strings.TrimRight(strings.Join(labelCells, gap), " "), + strings.Join(dashCells, gap), + } + + for i := 0; i < height; i++ { + cells := make([]string, n) + for ci, c := range cols { + cell := "" + if i < len(c.lines) { + cell = c.lines[i] + } + cells[ci] = ljust(cell, w) + } + rows = append(rows, strings.TrimRight(strings.Join(cells, gap), " ")) + } + + return strings.Join(rows, "\n") +} + +// wrap wraps line to width, greedily packing whole words onto each output +// line and never splitting a word (even one longer than width), matching +// ewangelia.py's textwrap.fill(..., break_long_words=False, +// break_on_hyphens=False). +func wrap(line string, width int) string { + words := strings.Fields(line) + if len(words) == 0 { + return "" + } + var out []string + cur := words[0] + for _, word := range words[1:] { + if len([]rune(cur))+1+len([]rune(word)) <= width { + cur += " " + word + } else { + out = append(out, cur) + cur = word + } + } + out = append(out, cur) + return strings.Join(out, "\n") +} + +func ljust(s string, w int) string { + if n := w - len([]rune(s)); n > 0 { + return s + strings.Repeat(" ", n) + } + return s +} + +func truncate(s string, w int) string { + r := []rune(s) + if len(r) <= w { + return s + } + return string(r[:w]) +} |
