aboutsummaryrefslogtreecommitdiff
path: root/internal/web/server.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 12:48:05 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 12:48:05 +0200
commit55a5c793d04b55892fcddfc753d8526bd5748f03 (patch)
treeb045946a62a0b849b5c77a4100294d935ab741dd /internal/web/server.go
parented34fd574b4fc2e0a52c655ececddd1be29b271e (diff)
downloadlectio-55a5c793d04b55892fcddfc753d8526bd5748f03.tar.gz
lectio-55a5c793d04b55892fcddfc753d8526bd5748f03.zip
web reader: /reader book picker + chapter nav + version compare, reusing pane templates; v0.9.0
Diffstat (limited to 'internal/web/server.go')
-rw-r--r--internal/web/server.go168
1 files changed, 168 insertions, 0 deletions
diff --git a/internal/web/server.go b/internal/web/server.go
index d8b451a..667e986 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -15,9 +15,11 @@ import (
"os/exec"
"regexp"
"runtime"
+ "strconv"
"strings"
"time"
+ "github.com/lukaszkasprzak/lectio/internal/bible"
"github.com/lukaszkasprzak/lectio/internal/config"
"github.com/lukaszkasprzak/lectio/internal/i18n"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
@@ -34,10 +36,13 @@ var bibleVersions = []string{"bt", "wuj", "vul", "grb", "drb"}
// (lectionary, versions, theme, offline) that requests can override via
// query parameters; it is never mutated.
func NewServer(cfg config.Config) http.Handler {
+ tbl, _ := bible.LoadBookTable(config.UserBooksTOML())
+
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", indexHandler(cfg))
mux.HandleFunc("GET /readings", readingsHandler(cfg))
+ mux.HandleFunc("GET /reader", readerHandler(cfg, tbl))
mux.HandleFunc("GET /theme.css", themeCSSHandler(cfg))
staticSub, err := fs.Sub(staticFS, "static")
@@ -282,6 +287,169 @@ func renderOrError(secs []liturgy.Section, versions []string, lectionary, displa
return RenderReadings(secs, versions, lectionary, display, lang, dayInfo)
}
+// requestReaderVersions returns the corpus versions the reader request asks for
+// (repeated ?v=), dropping "bt" (no corpus) and anything invalid; falling back
+// to the configured default (or "wuj" when that is bt/invalid) so a plain visit
+// shows exactly one column.
+func requestReaderVersions(cfg config.Config, r *http.Request) []string {
+ var out []string
+ for _, v := range r.URL.Query()["v"] {
+ if v != "bt" && config.ValidVersion(v) {
+ out = append(out, v)
+ }
+ }
+ if len(out) == 0 {
+ d := cfg.DefaultVersion
+ if d == "bt" || !config.ValidVersion(d) {
+ d = "wuj"
+ }
+ out = []string{d}
+ }
+ return out
+}
+
+// findBook returns the BookInfo whose Canonical matches, and whether found.
+func findBook(books []bible.BookInfo, canonical string) (bible.BookInfo, bool) {
+ for _, b := range books {
+ if b.Canonical == canonical {
+ return b, true
+ }
+ }
+ return bible.BookInfo{}, false
+}
+
+// clampChap keeps chap within the book's available chapters (contiguous in
+// practice); 0 when the book has none in any corpus version.
+func clampChap(chap int, chaps []int) int {
+ if len(chaps) == 0 {
+ return 0
+ }
+ if chap < chaps[0] {
+ return chaps[0]
+ }
+ last := chaps[len(chaps)-1]
+ if chap > last {
+ return last
+ }
+ return chap
+}
+
+// atoiDefault parses s as an int, returning def when it is empty/unparsable.
+func atoiDefault(s string, def int) int {
+ if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil {
+ return n
+ }
+ return def
+}
+
+type readerData struct {
+ BookOpts []bookOpt
+ Chap int
+ PrevChap, NextChap int
+ ChapOpts []chapOpt
+ VersionOpts []versionOpt
+ Display string
+ ThemeOpts []themeOpt
+ Theme string
+ Mono bool
+ Reading template.HTML
+ L i18n.UI
+ Lang string
+}
+
+type bookOpt struct {
+ Value string // canonical name (corpus/query value)
+ Label string // dialect display name
+ Selected bool
+}
+
+type chapOpt struct {
+ N int
+ Selected bool
+}
+
+// readerHandler serves GET /reader: a book picker (dialect names), chapter
+// navigation and corpus-version compare, rendering the same reading-pane
+// templates/themes as the daily view. The controls form re-fetches /reader and
+// hx-selects #reader-root, so book/chapter/version selects stay in sync.
+func readerHandler(cfg config.Config, tbl *bible.BookTable) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ books := tbl.Books(cfg.SiglaLang())
+ if len(books) == 0 { // defensive: embedded table always has books
+ http.Error(w, "web: no books", http.StatusInternalServerError)
+ return
+ }
+
+ info, ok := findBook(books, r.URL.Query().Get("book"))
+ if !ok {
+ info = books[0]
+ }
+ versions := requestReaderVersions(cfg, r)
+ chaps := UnionChapters(info.Canonical)
+ first := 1
+ if len(chaps) > 0 {
+ first = chaps[0]
+ }
+ chap := clampChap(atoiDefault(r.URL.Query().Get("chap"), first), chaps)
+ display := requestDisplay(cfg, r)
+ theme := r.URL.Query().Get("theme")
+ if theme == "" {
+ theme = cfg.WebTheme
+ }
+
+ reading := RenderPassage(info.Canonical, info.Name, chap, versions, display, cfg.UILanguage)
+
+ bookOpts := make([]bookOpt, 0, len(books))
+ for _, b := range books {
+ bookOpts = append(bookOpts, bookOpt{Value: b.Canonical, Label: b.Name, Selected: b.Canonical == info.Canonical})
+ }
+ chapOpts := make([]chapOpt, 0, len(chaps))
+ for _, c := range chaps {
+ chapOpts = append(chapOpts, chapOpt{N: c, Selected: c == chap})
+ }
+ selected := map[string]bool{}
+ for _, v := range versions {
+ selected[v] = true
+ }
+ versionOpts := make([]versionOpt, 0, len(readerCorpusVersions))
+ for _, v := range readerCorpusVersions {
+ versionOpts = append(versionOpts, versionOpt{Code: v, Checked: selected[v]})
+ }
+ themes := Themes()
+ themeOpts := make([]themeOpt, 0, len(themes))
+ for _, name := range themes {
+ themeOpts = append(themeOpts, themeOpt{Name: name, Selected: name == theme})
+ }
+
+ prev, next := chap-1, chap+1
+ if len(chaps) > 0 {
+ prev = clampChap(chap-1, chaps)
+ next = clampChap(chap+1, chaps)
+ }
+
+ data := readerData{
+ BookOpts: bookOpts,
+ Chap: chap,
+ PrevChap: prev,
+ NextChap: next,
+ ChapOpts: chapOpts,
+ VersionOpts: versionOpts,
+ Display: display,
+ ThemeOpts: themeOpts,
+ Theme: theme,
+ Mono: queryBool(r, "mono", cfg.WebMono),
+ Reading: reading,
+ L: i18n.Get(cfg.UILanguage),
+ Lang: cfg.UILanguage,
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err := tmpl.ExecuteTemplate(w, "reader.html", data); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ }
+}
+
// themeCSSHandler serves one theme's stylesheet: the requested name, or
// (unknown/invalid) cfg.WebTheme, or (that also unknown) the built-in
// default -- so an unrecognized ?name= degrades to a working theme instead