// Package web renders lectio's daily readings as an HTML fragment and // serves a set of "colour only" theme stylesheets (see docs/THEMES.md) that // restyle a fixed set of role classes/variables; it never lays out or types // anything itself -- that lives in the embedded base.css. It consumes // liturgy.Section and render.GatherVersion, never the reverse. package web import ( "bytes" "embed" "fmt" "html/template" "io/fs" "os" "path" "path/filepath" "regexp" "sort" "strings" "github.com/lukaszkasprzak/lectio/internal/liturgy" "github.com/lukaszkasprzak/lectio/internal/render" ) //go:embed templates var templatesFS embed.FS //go:embed static var staticFS embed.FS // tmpl holds every parsed templates/*.html, keyed by base file name (so // "readings.html" is looked up as "readings.html"). var tmpl = template.Must(template.ParseFS(templatesFS, "templates/*.html")) // verseNumRe matches a bible verse block's "chapter:verse " prefix, as // produced by render.GatherVersion (e.g. "3:16 Tak bowiem..."). Mirrors // internal/tui's verseNumRe. var verseNumRe = regexp.MustCompile(`^(\d+:\d+) (.*)$`) // sectionView, columnView and blockView are the data readings.html ranges // over: one sectionView per liturgy.Section, one columnView per requested // version, one blockView per render.GatherVersion block. type sectionView struct { Heading, Subtitle, PartID string Columns []columnView } type columnView struct { Label string Blocks []blockView } // blockView is one paragraph or verse line. VNum is set (and Text holds // only the verse text) when the block was a bible verse line; otherwise // VNum is empty and Text holds the whole block, with Refrain set for a // responsorial psalm's deduped first ("pl") block. type blockView struct { VNum, Text string 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 { views := make([]sectionView, 0, len(secs)) for _, sec := range secs { isPsalm := sec.PartID == "psalm" cols := make([]columnView, 0, len(versions)) for _, v := range versions { label, blocks := render.GatherVersion(v, sec, lectionary) bviews := make([]blockView, 0, len(blocks)) for bi, b := range blocks { // A bible verse line always carries its "chapter:verse " // prefix; only text without one (i.e. the "pl" version's // paragraphs) can be the deduped psalm refrain -- matches // internal/tui's styleBlock precedence. if m := verseNumRe.FindStringSubmatch(b); m != nil { bviews = append(bviews, blockView{VNum: m[1], Text: m[2]}) continue } bviews = append(bviews, blockView{Text: b, Refrain: isPsalm && bi == 0}) } cols = append(cols, columnView{Label: label, Blocks: bviews}) } views = append(views, sectionView{ Heading: sec.Heading, Subtitle: sec.Subtitle, PartID: sec.PartID, Columns: cols, }) } 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("
" + template.HTMLEscapeString(err.Error()) + "
") } return template.HTML(buf.String()) } // Themes returns the sorted, deduplicated union of the embedded theme // stems (static/themes/*.css) and the *.css stems found in the user theme // directory (${XDG_CONFIG_HOME:-~/.config}/lectio/themes/); a user theme // overrides a built-in of the same name but does not add a second entry. func Themes() []string { set := map[string]bool{} for _, name := range themeStems(staticFS, "static/themes") { set[name] = true } if dir, err := userThemesDir(); err == nil { for _, name := range themeStems(os.DirFS(dir), ".") { set[name] = true } } names := make([]string, 0, len(set)) for name := range set { names = append(names, name) } sort.Strings(names) return names } // themeNameRe is the allowlist a theme name must match: letters, digits, // underscore, hyphen only. Legitimate theme stems (built-in or user) already // fit this shape; it is deliberately stricter than "no path separators" so // it rejects "." / ".." / any other filesystem metacharacter outright // instead of trying to enumerate what's unsafe. var themeNameRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) // themeCSS returns one theme's CSS: the user file // ${XDG_CONFIG_HOME:-~/.config}/lectio/themes/