aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 13:33:11 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 13:33:11 +0200
commitcb1f49117d15b9690b46f344c0d6777e558b9021 (patch)
tree9a5e618ba198fcf8376944bc9630298ce75a2263
parent0351f4f188207d0204e1e0124d32127e820e33d5 (diff)
downloadlectio-cb1f49117d15b9690b46f344c0d6777e558b9021.tar.gz
lectio-cb1f49117d15b9690b46f344c0d6777e558b9021.zip
render: gather versions + compare + dedup
-rw-r--r--internal/render/render.go250
-rw-r--r--internal/render/render_test.go53
2 files changed, 303 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])
+}
diff --git a/internal/render/render_test.go b/internal/render/render_test.go
new file mode 100644
index 0000000..87f9ed2
--- /dev/null
+++ b/internal/render/render_test.go
@@ -0,0 +1,53 @@
+package render
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/lukaszkasprzak/lectio/internal/liturgy"
+)
+
+func TestGatherPLDedup(t *testing.T) {
+ sec := liturgy.Section{
+ Heading: "Psalm (Ps 1)",
+ Paragraphs: [][]string{{"stanza one"}, {"refrain"}, {"stanza two"}, {"refrain"}},
+ }
+ _, blocks := GatherVersion("pl", sec, "new")
+ n := 0
+ for _, b := range blocks {
+ if b == "refrain" {
+ n++
+ }
+ }
+ if n != 1 {
+ t.Errorf("refrain appears %d times, want 1 (deduped)", n)
+ }
+}
+
+func TestGatherBible(t *testing.T) {
+ sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)"}
+ label, blocks := GatherVersion("wuj", sec, "new")
+ if !strings.Contains(label, "Wujek") {
+ t.Errorf("label = %q", label)
+ }
+ if len(blocks) == 0 || !strings.HasPrefix(blocks[0], "20:1") {
+ t.Errorf("first block = %q", blocks)
+ }
+}
+
+func TestGatherTraditional(t *testing.T) {
+ sec := liturgy.Section{Citation: "Luke 7:36-50"}
+ _, blocks := GatherVersion("vul", sec, "traditional")
+ if len(blocks) == 0 || !strings.HasPrefix(blocks[0], "7:36") {
+ t.Errorf("first block = %q", blocks)
+ }
+}
+
+func TestOfflineVersions(t *testing.T) {
+ got := OfflineVersions([]string{"pl", "wuj", "vul"})
+ for _, v := range got {
+ if v == "pl" {
+ t.Error("pl not dropped offline")
+ }
+ }
+}