From 55a5c793d04b55892fcddfc753d8526bd5748f03 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 12:48:05 +0200 Subject: web reader: /reader book picker + chapter nav + version compare, reusing pane templates; v0.9.0 --- internal/web/render.go | 160 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 125 insertions(+), 35 deletions(-) (limited to 'internal/web/render.go') diff --git a/internal/web/render.go b/internal/web/render.go index 9e6280c..35c30b9 100644 --- a/internal/web/render.go +++ b/internal/web/render.go @@ -201,6 +201,46 @@ func interlinearVersions(versions []string) []string { return render.OfflineVersions(versions) } +// verseSet is one version's verses plus its column label, the unit both the +// interlinear readings view and the reader passage view interleave over. +type verseSet struct { + label string + verses []bible.Verse +} + +// interleaveVerses interleaves several versions' verses by chapter:verse: the +// ordered union of keys follows the first set's verse order, then any key only +// a later set has is appended in that set's order (stable, no duplicates). +func interleaveVerses(sets []verseSet) []ilVerseView { + type vkey struct{ chapter, verse int } + order := make([]vkey, 0) + seen := map[vkey]bool{} + bySet := make([]map[vkey]bible.Verse, len(sets)) + for i, s := range sets { + m := make(map[vkey]bible.Verse, len(s.verses)) + for _, v := range s.verses { + k := vkey{v.Chapter, v.Verse} + m[k] = v + if !seen[k] { + seen[k] = true + order = append(order, k) + } + } + bySet[i] = m + } + vviews := make([]ilVerseView, 0, len(order)) + for _, k := range order { + var lines []ilLineView + for i, s := range sets { + if v, ok := bySet[i][k]; ok { + lines = append(lines, ilLineView{Label: s.label, Text: v.Text}) + } + } + vviews = append(vviews, ilVerseView{VNum: fmt.Sprintf("%d:%d", k.chapter, k.verse), Lines: lines}) + } + return vviews +} + // buildInterlinearViews gathers each requested version's verses via // render.GatherVerses (after the bt->wuj substitution, see // interlinearVersions) and interleaves them by chapter:verse: the ordered @@ -214,11 +254,6 @@ func interlinearVersions(versions []string) []string { func buildInterlinearViews(secs []liturgy.Section, versions []string, lectionary, lang string) []ilSectionView { mapped := interlinearVersions(versions) - type verseSet struct { - label string - verses []bible.Verse - } - views := make([]ilSectionView, 0, len(secs)) for _, sec := range secs { view := ilSectionView{Heading: render.LocalizeHeading(sec.Heading, sec.PartID, lang), Subtitle: sec.Subtitle, PartID: sec.PartID} @@ -238,40 +273,95 @@ func buildInterlinearViews(secs []liturgy.Section, versions []string, lectionary continue } - type vkey struct{ chapter, verse int } - order := make([]vkey, 0) - seen := map[vkey]bool{} - bySet := make([]map[vkey]bible.Verse, len(sets)) - for i, s := range sets { - m := make(map[vkey]bible.Verse, len(s.verses)) - for _, v := range s.verses { - k := vkey{v.Chapter, v.Verse} - m[k] = v - if !seen[k] { - seen[k] = true - order = append(order, k) - } - } - bySet[i] = m + view.Verses = interleaveVerses(sets) + views = append(views, view) + } + return views +} + +// webVersionLabel is the localized column label for a version, falling back to +// the bare code (mirrors render's unexported versionLabel, which web can't reach). +func webVersionLabel(v, lang string) string { + if l, ok := i18n.Get(lang).Version[v]; ok { + return l + } + return v +} + +// readerCorpusVersions are the versions the /reader offers: the four with an +// embedded full-text corpus. "bt" (the niedziela.pl scrape) has no corpus and +// cannot be read chapter-by-chapter. +var readerCorpusVersions = []string{"wuj", "vul", "grb", "drb"} + +// UnionChapters returns the sorted union of chapter numbers a book has across +// all corpus versions, so the reader's chapter navigation is stable regardless +// of which versions are currently selected (a version lacking the book just +// shows a note). +func UnionChapters(canonical string) []int { + seen := map[int]bool{} + for _, v := range readerCorpusVersions { + for _, c := range bible.Chapters(v, canonical) { + seen[c] = true } + } + chaps := make([]int, 0, len(seen)) + for c := range seen { + chaps = append(chaps, c) + } + sort.Ints(chaps) + return chaps +} - vviews := make([]ilVerseView, 0, len(order)) - for _, k := range order { - var lines []ilLineView - for i, s := range sets { - if v, ok := bySet[i][k]; ok { - lines = append(lines, ilLineView{Label: s.label, Text: v.Text}) - } - } - vviews = append(vviews, ilVerseView{ - VNum: fmt.Sprintf("%d:%d", k.chapter, k.verse), - Lines: lines, - }) +// RenderPassage builds the reader pane for one book+chapter across versions, in +// the same three layouts as RenderReadings (columns / vertical / interlinear), +// reusing the very same templates and role classes. name is the book's display +// name in the sigla dialect (the section heading is " "). versions +// are corpus versions; one lacking the chapter shows a localized "(not in X)" +// note (columns) or is skipped (interlinear). lang localizes the version labels. +func RenderPassage(canonical, name string, chap int, versions []string, display, lang string) template.HTML { + heading := fmt.Sprintf("%s %d", name, chap) + switch display { + case "interlinear": + return renderTemplate("readings-interlinear.html", passageInterlinear(canonical, heading, chap, versions, lang)) + case "vertical": + return renderTemplate("readings-vertical.html", passageColumns(canonical, heading, chap, versions, lang)) + default: + return renderTemplate("readings.html", passageColumns(canonical, heading, chap, versions, lang)) + } +} + +func passageColumns(canonical, heading string, chap int, versions []string, lang string) []sectionView { + cols := make([]columnView, 0, len(versions)) + for _, v := range versions { + verses := bible.Verses(v, canonical, chap) + var blocks []blockView + if len(verses) == 0 { + blocks = append(blocks, blockView{Text: fmt.Sprintf(i18n.Get(lang).NoVersion, v)}) } - view.Verses = vviews - views = append(views, view) + for _, ve := range verses { + blocks = append(blocks, blockView{VNum: fmt.Sprintf("%d:%d", ve.Chapter, ve.Verse), Text: ve.Text}) + } + cols = append(cols, columnView{Label: webVersionLabel(v, lang), Blocks: blocks}) } - return views + return []sectionView{{Heading: heading, PartID: "reader", Columns: cols}} +} + +func passageInterlinear(canonical, heading string, chap int, versions []string, lang string) []ilSectionView { + var sets []verseSet + for _, v := range versions { + verses := bible.Verses(v, canonical, chap) + if len(verses) == 0 { + continue + } + sets = append(sets, verseSet{label: webVersionLabel(v, lang), verses: verses}) + } + view := ilSectionView{Heading: heading, PartID: "reader"} + if len(sets) == 0 { + view.Note = i18n.Get(lang).NoInterlinearVerses + return []ilSectionView{view} + } + view.Verses = interleaveVerses(sets) + return []ilSectionView{view} } // Themes returns the sorted, deduplicated union of the embedded theme -- cgit v1.3