// 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]) }