summaryrefslogtreecommitdiff
path: root/internal/web/render.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 15:00:17 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 15:00:17 +0200
commitaa6c9dad764089a3f0e287b5f90937e97dde3921 (patch)
tree213f7f56bf3c5ca1db82f0eee7c482886fa9ce43 /internal/web/render.go
parent66a17fda33e44e9022d6724f850323f754715259 (diff)
downloadlectio-aa6c9dad764089a3f0e287b5f90937e97dde3921.tar.gz
lectio-aa6c9dad764089a3f0e287b5f90937e97dde3921.zip
web: horizontal/vertical/interlinear display modes + web_display config
Adds a display-layout option to lectio-web alongside the existing colour themes: - config: WebDisplay field (toml web_display), default "horizontal", normalized on load via the new exported config.NormalizeDisplay (unknown -> "horizontal", same lenient style as the other web_* fields). - render: extracts the citation/ToEnglishRef resolution shared by GatherVersion into an unexported resolveRef helper (GatherVersion's signature/behavior unchanged) and adds GatherVerses(version, sec, lectionary) returning raw bible.Verse structs plus a versified flag, for column/interlinear alignment. - web/render: RenderReadings gains a display parameter. "horizontal" is the original stacked layout, byte-for-byte the same code path as before. "vertical" reuses the same per-version column data in a .display-vertical/.vcol grid (the CLI compare view, browser-side). "interlinear" maps versions through render.OfflineVersions' pl->wuj substitution (pl has no verse numbers), gathers GatherVerses per version, and interleaves them by chapter:verse into .ilverse/.illine blocks (ordered union of keys, first versified version's order first). - server: adds a display <select> to the top bar wired into the existing #controls HTMX form; a resolveQuery(cfg, r) helper replaces the duplicated date/lectionary/all/versions(+now display) resolution in indexHandler and readingsHandler. - fold-in from the B2 review: indexHandler now populates indexData.Lookup via a shared renderLookup(ref, versions) helper (also used by lookupHandler), so a bookmarked "/?ref=...&v=..." link shows its lookup result instead of an empty pane. - base.css: layout-only rules for the two new modes (no colours; themes stay colour-only). go test ./..., go vet ./..., gofmt clean; go build ./cmd/lectio-web ok.
Diffstat (limited to 'internal/web/render.go')
-rw-r--r--internal/web/render.go160
1 files changed, 146 insertions, 14 deletions
diff --git a/internal/web/render.go b/internal/web/render.go
index 67c7a37..63c1da4 100644
--- a/internal/web/render.go
+++ b/internal/web/render.go
@@ -18,6 +18,7 @@ import (
"sort"
"strings"
+ "github.com/lukaszkasprzak/lectio/internal/bible"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
"github.com/lukaszkasprzak/lectio/internal/render"
)
@@ -59,13 +60,70 @@ type blockView struct {
Refrain bool
}
-// RenderReadings builds the reading pane: for each section, a heading (and
-// subtitle if present) followed by one column per version, each built by
-// calling render.GatherVersion(v, sec, lectionary). Heading, citation
-// (subtitle), verse-number and refrain text are wrapped in
-// class="heading|citation|vnum|refrain" spans so theme CSS can restyle
-// them; verse/paragraph text is escaped by html/template.
-func RenderReadings(secs []liturgy.Section, versions []string, lectionary string) template.HTML {
+// ilSectionView, ilVerseView and ilLineView are what
+// templates/readings-interlinear.html ranges over: one ilSectionView per
+// liturgy.Section, one ilVerseView per chapter:verse key (in ordered-union
+// order, see buildInterlinearViews), one ilLineView per version that carries
+// that verse. Note holds a short escaped message in place of Verses when no
+// requested version could be interleaved for the section.
+type ilSectionView struct {
+ Heading, Subtitle, PartID string
+ Verses []ilVerseView
+ Note string
+}
+
+type ilVerseView struct {
+ VNum string
+ Lines []ilLineView
+}
+
+type ilLineView struct {
+ Label, Text string
+}
+
+// RenderReadings builds the reading pane fragment for one of three layouts:
+//
+// - "horizontal" (or anything unrecognized): the original stacked layout,
+// one column per version rendered under a shared heading, unchanged.
+// - "vertical": the same per-version columns side by side in a
+// ".display-vertical" grid, like the CLI `compare` view.
+// - "interlinear": versions interleaved verse-by-verse by chapter:verse
+// (see buildInterlinearViews); "pl" cannot participate (no verse
+// numbers) and is substituted/dropped via render.OfflineVersions'
+// pl->wuj transform before gathering.
+//
+// In every mode, heading, citation (subtitle), verse-number and refrain
+// text are wrapped in class="heading|citation|vnum|refrain|version-label"
+// spans so theme CSS can restyle them; verse/paragraph text is escaped by
+// html/template.
+func RenderReadings(secs []liturgy.Section, versions []string, lectionary, display string) template.HTML {
+ switch display {
+ case "vertical":
+ return renderTemplate("readings-vertical.html", buildColumnViews(secs, versions, lectionary))
+ case "interlinear":
+ return renderTemplate("readings-interlinear.html", buildInterlinearViews(secs, versions, lectionary))
+ default:
+ return renderTemplate("readings.html", buildColumnViews(secs, versions, lectionary))
+ }
+}
+
+// renderTemplate executes the named embedded template with data, degrading
+// to a visible, escaped error paragraph on failure (should be unreachable:
+// the templates are embedded and fixed at build time) rather than panicking
+// a request handler in the caller.
+func renderTemplate(name string, data any) template.HTML {
+ var buf bytes.Buffer
+ if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
+ return template.HTML("<p class=\"error\">" + template.HTMLEscapeString(err.Error()) + "</p>")
+ }
+ return template.HTML(buf.String())
+}
+
+// buildColumnViews gathers each section's per-version columns via
+// render.GatherVersion -- the shared data both the "horizontal"
+// (readings.html) and "vertical" (readings-vertical.html) templates range
+// over; only the surrounding markup differs between the two layouts.
+func buildColumnViews(secs []liturgy.Section, versions []string, lectionary string) []sectionView {
views := make([]sectionView, 0, len(secs))
for _, sec := range secs {
isPsalm := sec.PartID == "psalm"
@@ -96,15 +154,89 @@ func RenderReadings(secs []liturgy.Section, versions []string, lectionary string
Columns: cols,
})
}
+ return views
+}
- var buf bytes.Buffer
- if err := tmpl.ExecuteTemplate(&buf, "readings.html", views); err != nil {
- // Should be unreachable (the template is embedded and fixed at
- // build time); degrade to a visible, escaped error rather than
- // panicking a request handler in the caller.
- return template.HTML("<p class=\"error\">" + template.HTMLEscapeString(err.Error()) + "</p>")
+// interlinearVersions maps versions through the same pl->wuj substitution
+// render.OfflineVersions performs for offline mode: "pl" (niedziela.pl
+// paragraph text) carries no verse numbers and cannot interleave, so it is
+// dropped, substituting "wuj" (the Polish-language bible version) in its
+// place unless "wuj" was already selected. Reuses render.OfflineVersions
+// rather than duplicating its two-line transform.
+func interlinearVersions(versions []string) []string {
+ return render.OfflineVersions(versions)
+}
+
+// buildInterlinearViews gathers each requested version's verses via
+// render.GatherVerses (after the pl->wuj substitution, see
+// interlinearVersions) and interleaves them by chapter:verse: the ordered
+// union of keys is taken from the first versified version's own verse
+// order, then any keys that only appear in a later version are appended in
+// that version's order (stable, no duplicates). A version that comes back
+// unversified (a bible lookup miss) is simply skipped -- the remaining
+// versions still render. A section where nothing could be interleaved gets
+// a short escaped Note instead of an empty Verses list.
+func buildInterlinearViews(secs []liturgy.Section, versions []string, lectionary string) []ilSectionView {
+ mapped := interlinearVersions(versions)
+
+ type verseSet struct {
+ label string
+ verses []bible.Verse
}
- return template.HTML(buf.String())
+
+ views := make([]ilSectionView, 0, len(secs))
+ for _, sec := range secs {
+ view := ilSectionView{Heading: sec.Heading, Subtitle: sec.Subtitle, PartID: sec.PartID}
+
+ var sets []verseSet
+ for _, v := range mapped {
+ label, verses, versified := render.GatherVerses(v, sec, lectionary)
+ if !versified {
+ continue
+ }
+ sets = append(sets, verseSet{label: label, verses: verses})
+ }
+
+ if len(sets) == 0 {
+ view.Note = "(brak wersetów do zestawienia interlinearnego)"
+ views = append(views, view)
+ 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
+ }
+
+ 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,
+ })
+ }
+ view.Verses = vviews
+ views = append(views, view)
+ }
+ return views
}
// Themes returns the sorted, deduplicated union of the embedded theme