summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 14:59:14 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 14:59:14 +0200
commitcb3379f41c06a94085c77fe359709b46712850dc (patch)
tree032801111764c83c3ab089ed101ba4d1222a9abd /internal
parenta13dd6224a095242da54062e4775570c4f16a718 (diff)
downloadlectio-cb3379f41c06a94085c77fe359709b46712850dc.tar.gz
lectio-cb3379f41c06a94085c77fe359709b46712850dc.zip
feat: books.toml -> books.ini, multi-language (add Latin dialect), config-scoped citation display
books.ini with [en]/[pl]/[la]; parseBooks reads INI (bible drops go-toml). Latin dialect resolves EF citations (Eccli->Sirach, Apoc->Revelation, Luc...); sigla_style gains 'latin'. Reading citations now DISPLAY in the configured sigla (FormatRef(SiglaLang)), not raw English. One-shot books.toml->books.ini user migration (go-toml now config-only). Web settings editor -> books.ini.
Diffstat (limited to 'internal')
-rw-r--r--internal/bible/books.ini243
-rw-r--r--internal/bible/booktable.go46
-rw-r--r--internal/bible/booktable_test.go10
-rw-r--r--internal/cli/cli.go16
-rw-r--r--internal/cli/liturgy.go21
-rw-r--r--internal/config/config.go74
-rw-r--r--internal/web/server.go16
-rw-r--r--internal/web/server_test.go10
-rw-r--r--internal/web/templates/settings.html4
9 files changed, 373 insertions, 67 deletions
diff --git a/internal/bible/books.ini b/internal/bible/books.ini
new file mode 100644
index 0000000..5d8f633
--- /dev/null
+++ b/internal/bible/books.ini
@@ -0,0 +1,243 @@
+# lectio book names & abbreviations, per dialect (en, pl, la).
+#
+# This is the built-in default, embedded in the binary. To customise, copy any
+# lines you want to change into ~/.config/lectio/books.ini (next to config.ini).
+# Your file is MERGED over these defaults per book: you only list the books you
+# want to change; every book you don't mention keeps the default below.
+#
+# Each entry is a flat list of forms for one book, in ONE language:
+# - the FIRST form is the shortcut shown by `lectio --list`
+# - the LAST form is the full name shown by `lectio --list`
+# - EVERY form (first, last, and any in between) resolves in `lectio --ref`
+# Resolution is language-scoped and idiomatic: in English mode only the [en]
+# forms resolve (so "Jn" works, "Łk" does not); in Polish mode only [pl].
+#
+# The table KEY is the book's English canonical name -- it must match the Bible
+# corpus, so do not change the keys, only the forms. Books appear in `--list`
+# in scriptural order regardless of the order here.
+
+[en]
+Genesis = Gen, Genesis
+Exodus = Exod, Exodus
+Leviticus = Lev, Leviticus
+Numbers = Num, Numbers
+Deuteronomy = Deut, Deuteronomy
+Joshua = Josh, Joshua
+Judges = Judg, Judges
+Ruth = Ru, Ruth
+1 Samuel = 1 Sam, 1 Samuel
+2 Samuel = 2 Sam, 2 Samuel
+1 Kings = 1 Kgs, 1 Kings
+2 Kings = 2 Kgs, 2 Kings
+1 Chronicles = 1 Chr, 1 Chronicles
+2 Chronicles = 2 Chr, 2 Chronicles
+Ezra = Ezra, Ezra
+Nehemiah = Neh, Nehemiah
+Tobit = Tob, Tobit
+Judith = Jdt, Judith
+Esther = Esth, Esther
+1 Maccabees = 1 Macc, 1 Maccabees
+2 Maccabees = 2 Macc, 2 Maccabees
+Job = Jb, Job
+Psalms = Ps, Psalm, Psalms
+Proverbs = Prov, Proverbs
+Ecclesiastes = Eccl, Ecclesiastes
+Song of Solomon = Song, Song of Songs
+Wisdom = Wis, Wisdom
+Sirach = Sir, Ecclus, Eccli, Ecclesiasticus, Sirach
+Isaiah = Isa, Isaiah
+Jeremiah = Jer, Jeremiah
+Lamentations = Lam, Lamentations
+Baruch = Bar, Baruch
+Ezekiel = Ezek, Ezekiel
+Daniel = Dan, Daniel
+Hosea = Hos, Hosea
+Joel = Jl, Joel
+Amos = Am, Amos
+Obadiah = Obad, Obadiah
+Jonah = Jon, Jonah
+Micah = Mic, Micah
+Nahum = Nah, Nahum
+Habakkuk = Hab, Habakkuk
+Zephaniah = Zeph, Zephaniah
+Haggai = Hag, Haggai
+Zechariah = Zech, Zechariah
+Malachi = Mal, Malachi
+Matthew = Matt, Matthew
+Mark = Mk, Mark
+Luke = Lk, Luke
+John = Jn, John
+The Acts = Acts, Acts
+Romans = Rom, Romans
+1 Corinthians = 1 Cor, 1 Corinthians
+2 Corinthians = 2 Cor, 2 Corinthians
+Galatians = Gal, Galatians
+Ephesians = Eph, Ephesians
+Philippians = Phil, Philippians
+Colossians = Col, Colossians
+1 Thessalonians = 1 Thess, 1 Thessalonians
+2 Thessalonians = 2 Thess, 2 Thessalonians
+1 Timothy = 1 Tim, 1 Timothy
+2 Timothy = 2 Tim, 2 Timothy
+Titus = Ti, Titus
+Philemon = Phlm, Philemon
+Hebrews = Heb, Hebrews
+James = Jas, James
+1 Peter = 1 Pet, 1 Peter
+2 Peter = 2 Pet, 2 Peter
+1 John = 1 Jn, 1 John
+2 John = 2 Jn, 2 John
+3 John = 3 Jn, 3 John
+Jude = Jude, Jude
+Revelation = Rev, Apoc, Apocalypse, Revelation
+
+[pl]
+Genesis = Rdz, Rodzaju
+Exodus = Wj, Wyjścia
+Leviticus = Kpł, Kapłańska
+Numbers = Lb, Liczb
+Deuteronomy = Pwt, Powtórzonego Prawa
+Joshua = Joz, Jozuego
+Judges = Sdz, Sędziów
+Ruth = Rt, Rut
+1 Samuel = 1 Sm, 1 Samuela
+2 Samuel = 2 Sm, 2 Samuela
+1 Kings = 1 Krl, 1 Królewska
+2 Kings = 2 Krl, 2 Królewska
+1 Chronicles = 1 Krn, 1 Kronik
+2 Chronicles = 2 Krn, 2 Kronik
+Ezra = Ezd, Ezdrasza
+Nehemiah = Ne, Nehemiasza
+Tobit = Tb, Tobiasza
+Judith = Jdt, Judyty
+Esther = Est, Estery
+1 Maccabees = 1 Mch, 1 Machabejska
+2 Maccabees = 2 Mch, 2 Machabejska
+Job = Hi, Hioba
+Psalms = Ps, Psalm, Psalmy
+Proverbs = Prz, Przysłów
+Ecclesiastes = Koh, Koheleta
+Song of Solomon = Pnp, Pieśń nad Pieśniami
+Wisdom = Mdr, Mądrości
+Sirach = Syr, Syracha
+Isaiah = Iz, Izajasza
+Jeremiah = Jr, Jeremiasza
+Lamentations = Lm, Lamentacje
+Baruch = Ba, Barucha
+Ezekiel = Ez, Ezechiela
+Daniel = Dn, Daniela
+Hosea = Oz, Ozeasza
+Joel = Jl, Joela
+Amos = Am, Amosa
+Obadiah = Ab, Abdiasza
+Jonah = Jon, Jonasza
+Micah = Mi, Micheasza
+Nahum = Na, Nahuma
+Habakkuk = Ha, Habakuka
+Zephaniah = So, Sofoniasza
+Haggai = Ag, Aggeusza
+Zechariah = Za, Zachariasza
+Malachi = Ml, Malachiasza
+Matthew = Mt, Mateusza
+Mark = Mk, Marka
+Luke = Łk, Łukasza
+John = J, Jana
+The Acts = Dz, Dzieje Apostolskie
+Romans = Rz, Rzymian
+1 Corinthians = 1 Kor, 1 Koryntian
+2 Corinthians = 2 Kor, 2 Koryntian
+Galatians = Ga, Galatów
+Ephesians = Ef, Efezjan
+Philippians = Flp, Filipian
+Colossians = Kol, Kolosan
+1 Thessalonians = 1 Tes, 1 Tesaloniczan
+2 Thessalonians = 2 Tes, 2 Tesaloniczan
+1 Timothy = 1 Tm, 1 Tymoteusza
+2 Timothy = 2 Tm, 2 Tymoteusza
+Titus = Tt, Tytusa
+Philemon = Flm, Filemona
+Hebrews = Hbr, Hebrajczyków
+James = Jk, Jakuba
+1 Peter = 1 P, 1 Piotra
+2 Peter = 2 P, 2 Piotra
+1 John = 1 J, 1 Jana
+2 John = 2 J, 2 Jana
+3 John = 3 J, 3 Jana
+Jude = Jud, Judy
+Revelation = Ap, Apokalipsa
+
+# Latin (Vulgate) — book names/abbreviations for EF citations and Latin sigla.
+[la]
+Genesis = Gen, Genesis
+Exodus = Ex, Exodus
+Leviticus = Lev, Leviticus
+Numbers = Num, Numeri
+Deuteronomy = Deut, Deuteronomium
+Joshua = Ios, Iosue
+Judges = Idc, Iudicum
+Ruth = Ruth
+1 Samuel = 1 Reg, 1 Sam, 1 Samuelis, 1 Regum
+2 Samuel = 2 Reg, 2 Sam, 2 Samuelis, 2 Regum
+1 Kings = 3 Reg, 3 Regum
+2 Kings = 4 Reg, 4 Regum
+1 Chronicles = 1 Par, 1 Paralipomenon
+2 Chronicles = 2 Par, 2 Paralipomenon
+Ezra = Esd, 1 Esdrae, Esdrae
+Nehemiah = Neh, 2 Esd, Nehemiae
+Tobit = Tob, Tobiae, Tobias
+Judith = Idt, Iudith
+Esther = Esth, Esther
+1 Maccabees = 1 Mac, 1 Machabaeorum
+2 Maccabees = 2 Mac, 2 Machabaeorum
+Job = Iob
+Psalms = Ps, Psalmi
+Proverbs = Prov, Proverbia
+Ecclesiastes = Eccle, Ecclesiastes
+Song of Solomon = Cant, Canticum, Canticum Canticorum
+Wisdom = Sap, Sapientia
+Sirach = Eccli, Ecclus, Sir, Ecclesiasticus
+Isaiah = Is, Isaias
+Jeremiah = Ier, Ieremias
+Lamentations = Lam, Threni, Lamentationes
+Baruch = Bar, Baruch
+Ezekiel = Ez, Ezechiel
+Daniel = Dan, Daniel
+Hosea = Os, Osee
+Joel = Ioel
+Amos = Am, Amos
+Obadiah = Abd, Abdias
+Jonah = Ion, Ionas
+Micah = Mich, Michaeas
+Nahum = Nah, Nahum
+Habakkuk = Hab, Habacuc
+Zephaniah = Soph, Sophonias
+Haggai = Agg, Aggaeus
+Zechariah = Zach, Zacharias
+Malachi = Mal, Malachias
+Matthew = Matt, Matthaeus
+Mark = Marc, Marcus
+Luke = Luc, Lucas
+John = Ioann, Ioannes
+The Acts = Act, Actus, Actus Apostolorum
+Romans = Rom, Romanos
+1 Corinthians = 1 Cor, 1 Corinthios
+2 Corinthians = 2 Cor, 2 Corinthios
+Galatians = Gal, Galatas
+Ephesians = Eph, Ephesios
+Philippians = Phil, Philippenses
+Colossians = Col, Colossenses
+1 Thessalonians = 1 Thess, 1 Thessalonicenses
+2 Thessalonians = 2 Thess, 2 Thessalonicenses
+1 Timothy = 1 Tim, 1 Timotheum
+2 Timothy = 2 Tim, 2 Timotheum
+Titus = Tit, Titum
+Philemon = Philem, Philemonem
+Hebrews = Hebr, Hebraeos
+James = Iac, Iacobi
+1 Peter = 1 Pet, 1 Petri
+2 Peter = 2 Pet, 2 Petri
+1 John = 1 Ioann, 1 Ioannis
+2 John = 2 Ioann, 2 Ioannis
+3 John = 3 Ioann, 3 Ioannis
+Jude = Iud, Iudae
+Revelation = Apoc, Apocalypsis
diff --git a/internal/bible/booktable.go b/internal/bible/booktable.go
index ac7ea95..ae005a5 100644
--- a/internal/bible/booktable.go
+++ b/internal/bible/booktable.go
@@ -6,13 +6,13 @@ import (
"sort"
"strings"
- "github.com/pelletier/go-toml/v2"
+ "github.com/lukaszkasprzak/lectio/internal/ini"
)
-// booksFS embeds the built-in default book table (names + abbreviations per UI
-// language). Users override it per book via ~/.config/lectio/books.toml.
+// booksFS embeds the built-in default book table (names + abbreviations per
+// dialect: en/pl/la). Users override it per book via ~/.config/lectio/books.ini.
//
-//go:embed books.toml
+//go:embed books.ini
var booksFS embed.FS
// BookInfo is one book's display data in one dialect, for `lectio --list` and
@@ -23,8 +23,8 @@ type BookInfo struct {
Shortcut string // primary abbreviation in the dialect, e.g. "Jn" / "J"
}
-// BookTable resolves book references and lists books per "dialect" -- a books.toml
-// section, keyed by the same codes as the UI languages ("en"/"pl"). It is built
+// BookTable resolves book references and lists books per "dialect" -- a books.ini
+// section, keyed by dialect ("en"/"pl"/"la"). It is built
// from the embedded default merged with an optional user override.
type BookTable struct {
forms map[string]map[string][]string // dialect -> canonical -> forms (first=shortcut, last=name)
@@ -38,16 +38,16 @@ type BookTable struct {
// It always returns a usable table; a non-nil error means userTOML was present
// but unparseable (the returned table is defaults-only, so callers can warn and
// proceed).
-func LoadBookTable(userTOML []byte) (*BookTable, error) {
+func LoadBookTable(userINI []byte) (*BookTable, error) {
def, err := parseBooks(mustReadEmbedded())
if err != nil {
- return nil, fmt.Errorf("embedded books.toml: %w", err) // our bug, not the user's
+ return nil, fmt.Errorf("embedded books.ini: %w", err) // our bug, not the user's
}
var uerr error
- if len(strings.TrimSpace(string(userTOML))) > 0 {
- user, perr := parseBooks(userTOML)
+ if len(strings.TrimSpace(string(userINI))) > 0 {
+ user, perr := parseBooks(userINI)
if perr != nil {
- uerr = fmt.Errorf("ignoring malformed books.toml: %w", perr)
+ uerr = fmt.Errorf("ignoring malformed books.ini: %w", perr)
} else {
for dialect, books := range user {
if def[dialect] == nil {
@@ -84,20 +84,32 @@ func LoadBookTable(userTOML []byte) (*BookTable, error) {
}
func mustReadEmbedded() []byte {
- b, _ := booksFS.ReadFile("books.toml") // embedded: always present
+ b, _ := booksFS.ReadFile("books.ini") // embedded: always present
return b
}
-// DefaultBooksTOML returns the embedded default books.toml (the seed the
+// DefaultBooksTOML returns the embedded default books.ini (the seed the
// /settings editor shows when the user has no override yet).
-func DefaultBooksTOML() []byte { return mustReadEmbedded() }
+func DefaultBooksINI() []byte { return mustReadEmbedded() }
func parseBooks(data []byte) (map[string]map[string][]string, error) {
- var raw map[string]map[string][]string
- if err := toml.Unmarshal(data, &raw); err != nil {
+ secs, err := ini.Parse(data)
+ if err != nil {
return nil, err
}
- return raw, nil
+ out := map[string]map[string][]string{}
+ for _, s := range secs {
+ if s.Name == "" { // no top-level pairs in the book table
+ continue
+ }
+ if out[s.Name] == nil {
+ out[s.Name] = map[string][]string{}
+ }
+ for _, p := range s.Pairs {
+ out[s.Name][p.Key] = ini.List(p.Val)
+ }
+ }
+ return out, nil
}
// Books returns every book of the dialect in scriptural (canonOrder) order.
diff --git a/internal/bible/booktable_test.go b/internal/bible/booktable_test.go
index 2a57d80..2fdd891 100644
--- a/internal/bible/booktable_test.go
+++ b/internal/bible/booktable_test.go
@@ -97,7 +97,7 @@ func TestFormatRef(t *testing.T) {
func TestBookTableMergeAndMalformed(t *testing.T) {
// User adds an alias "Jhn" to English John; other books stay default.
- user := []byte("[en]\nJohn = [\"Jn\", \"Jhn\", \"John\"]\n")
+ user := []byte("[en]\nJohn = Jn, Jhn, John\n")
tbl, err := LoadBookTable(user)
if err != nil {
t.Fatalf("merge err: %v", err)
@@ -108,12 +108,12 @@ func TestBookTableMergeAndMalformed(t *testing.T) {
if got, ok := tbl.ParseRef("en", "Gen 1:1"); !ok || got != "Genesis 1:1" {
t.Errorf("unlisted book lost after merge: %q,%v", got, ok)
}
- // Malformed user TOML -> non-nil error but a usable defaults table.
- tbl2, err := LoadBookTable([]byte("this is not [valid toml"))
+ // Malformed user INI -> non-nil error but a usable defaults table.
+ tbl2, err := LoadBookTable([]byte("this is not valid ini"))
if err == nil {
- t.Error("expected error for malformed user toml")
+ t.Error("expected error for malformed user ini")
}
if got, ok := tbl2.ParseRef("en", "Jn 3:16"); !ok || got != "John 3:16" {
- t.Errorf("defaults unusable after malformed user toml: %q,%v", got, ok)
+ t.Errorf("defaults unusable after malformed user ini: %q,%v", got, ok)
}
}
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 575fba9..65be274 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -227,7 +227,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
}
if citation || week || randV || randCh {
- tbl, _ := bible.LoadBookTable(userBooksTOML())
+ tbl, _ := bible.LoadBookTable(userBooksINI())
switch {
case randV || randCh:
ver, verr := randVersion(cfg, bibleVer)
@@ -300,7 +300,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var bookTbl *bible.BookTable
if list || ref != "" {
- tbl, terr := bible.LoadBookTable(userBooksTOML())
+ tbl, terr := bible.LoadBookTable(userBooksINI())
if terr != nil {
fmt.Fprintln(stderr, "lectio:", terr) // warn; tbl is still a usable defaults table
}
@@ -516,17 +516,7 @@ func runWeek(cfg config.Config, tbl *bible.BookTable, date string, refresh bool,
// userBooksTOML returns the bytes of the optional user books.toml, or nil if
// it is absent/unreadable (built-in defaults are used).
-func userBooksTOML() []byte {
- p, err := config.BooksPath()
- if err != nil {
- return nil
- }
- b, err := os.ReadFile(p)
- if err != nil {
- return nil
- }
- return b
-}
+func userBooksINI() []byte { return config.UserBooksINI() }
// wantsHelp reports whether -h/--help appears anywhere in args.
func wantsHelp(args []string) bool {
diff --git a/internal/cli/liturgy.go b/internal/cli/liturgy.go
index fc3efae..840e9a6 100644
--- a/internal/cli/liturgy.go
+++ b/internal/cli/liturgy.go
@@ -31,7 +31,7 @@ func runLiturgy(cfg config.Config, date string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, "lectio: warning:", e)
}
day := calendar.Compute(d.UTC(), cfg.Selection(), layers)
- tbl, _ := bible.LoadBookTable(userBooksTOML())
+ tbl, _ := bible.LoadBookTable(userBooksINI())
printLiturgicalDay(stdout, cfg, tbl, day)
return 0
}
@@ -45,19 +45,23 @@ func vernacularVersion(cfg config.Config) string {
return "drb"
}
-// readingText resolves an English citation and returns its text from version,
-// or "" if it does not resolve.
-func readingText(tbl *bible.BookTable, version, citation string) string {
+// readingLine resolves an English-authored citation and returns (display,
+// text): display is the citation re-formatted in the user's configured sigla
+// dialect (SiglaLang); text is the verses from version (or "" if unresolved).
+// The reading data is authored in English, so it is parsed in the "en" dialect;
+// only the display honors the configured dialect's abbreviations.
+func readingLine(tbl *bible.BookTable, cfg config.Config, version, citation string) (string, string) {
engRef, ok := tbl.ParseRef("en", citation)
if !ok {
- return ""
+ return citation, "" // unparseable: show as authored
}
+ display := tbl.FormatRef(cfg.SiglaLang(), engRef)
vs, _ := bible.Lookup(version, engRef)
parts := make([]string, 0, len(vs))
for _, v := range vs {
parts = append(parts, strings.TrimSpace(v.Text))
}
- return strings.Join(parts, " ")
+ return display, strings.Join(parts, " ")
}
func printLiturgicalDay(out io.Writer, cfg config.Config, tbl *bible.BookTable, day calendar.LiturgicalDay) {
@@ -81,8 +85,9 @@ func printLiturgicalDay(out io.Writer, cfg config.Config, tbl *bible.BookTable,
if m.Variant != "" {
label = m.Variant + " " + label
}
- fmt.Fprintf(out, "\n %s (%s):\n", label, r.Citation)
- if txt := readingText(tbl, vern, r.Citation); txt != "" {
+ disp, txt := readingLine(tbl, cfg, vern, r.Citation)
+ fmt.Fprintf(out, "\n %s (%s):\n", label, disp)
+ if txt != "" {
for _, line := range strings.Split(render.Wrap(txt, 72), "\n") {
fmt.Fprintf(out, " %s\n", line)
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 54185d1..8d2ddca 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -38,7 +38,7 @@ const configHeader = `# lectio configuration (INI). Full-line comments only (# o
# all true | false (all readings, or just the gospel)
# offline true | false (never fetch; cache/sigla only)
# ui_language en | pl (interface chrome; readings stay source-language)
-# sigla_style auto | polish | english (citation dialect; auto follows ui_language)
+# sigla_style auto | polish | english | latin (citation dialect; auto follows ui_language)
# web_theme a theme name (see docs/THEMES.md)
# web_port integer; 0 = try 1099, then any free port
# web_display horizontal | vertical | interlinear
@@ -135,6 +135,8 @@ func NormalizeSiglaStyle(s string) string {
switch strings.ToLower(strings.TrimSpace(s)) {
case "polish", "pl":
return "polish"
+ case "latin", "la":
+ return "latin"
case "english", "en":
return "english"
default:
@@ -190,6 +192,8 @@ func (c Config) SiglaLang() string {
switch c.SiglaStyle {
case "polish":
return "pl"
+ case "latin":
+ return "la"
case "english":
return "en"
default:
@@ -270,7 +274,7 @@ func CalendarsDir() (string, error) {
return filepath.Join(filepath.Dir(p), "calendars"), nil
}
-// BooksPath returns the path to the optional user books.toml (in the same
+// BooksPath returns the path to the optional user books.ini (in the same
// directory as the config file). There is no embedded seed on disk -- absence
// means "use the built-in defaults".
func BooksPath() (string, error) {
@@ -278,22 +282,74 @@ func BooksPath() (string, error) {
if err != nil {
return "", err
}
- return filepath.Join(filepath.Dir(p), "books.toml"), nil
+ return filepath.Join(filepath.Dir(p), "books.ini"), nil
}
-// UserBooksTOML returns the bytes of the optional user books.toml (see
-// BooksPath), or nil when it is absent or unreadable -- callers then fall back
-// to bible's embedded default table.
-func UserBooksTOML() []byte {
+// UserBooksINI returns the bytes of the optional user books.ini (see BooksPath),
+// or nil when absent/unreadable -- callers then fall back to bible's embedded
+// default table. A former books.toml at the same location is converted to
+// books.ini once, on first read (the old file is kept).
+func UserBooksINI() []byte {
p, err := BooksPath()
if err != nil {
return nil
}
- b, err := os.ReadFile(p)
+ if b, err := os.ReadFile(p); err == nil {
+ return b
+ }
+ tomlPath := filepath.Join(filepath.Dir(p), "books.toml")
+ data, err := os.ReadFile(tomlPath)
if err != nil {
return nil
}
- return b
+ out := booksTOMLtoINI(data)
+ if out == nil {
+ return nil
+ }
+ _ = os.WriteFile(p, out, 0o644)
+ fmt.Fprintf(os.Stderr, "lectio: migrated %s -> books.ini (old file kept)\n", tomlPath)
+ return out
+}
+
+// booksTOMLtoINI converts a former books.toml (dialect -> book -> [forms]) into
+// books.ini. Returns nil if the TOML is unparseable.
+func booksTOMLtoINI(data []byte) []byte {
+ var raw map[string]map[string][]string
+ if err := toml.Unmarshal(data, &raw); err != nil {
+ return nil
+ }
+ var b strings.Builder
+ b.WriteString("# lectio book names & abbreviations, per dialect (migrated from books.toml).\n")
+ emit := func(d string) {
+ books := raw[d]
+ if books == nil {
+ return
+ }
+ fmt.Fprintf(&b, "\n[%s]\n", d)
+ keys := make([]string, 0, len(books))
+ for k := range books {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ fmt.Fprintf(&b, "%s = %s\n", k, strings.Join(books[k], ", "))
+ }
+ }
+ seen := map[string]bool{"en": true, "pl": true, "la": true}
+ emit("en")
+ emit("pl")
+ emit("la")
+ others := make([]string, 0)
+ for d := range raw {
+ if !seen[d] {
+ others = append(others, d)
+ }
+ }
+ sort.Strings(others)
+ for _, d := range others {
+ emit(d)
+ }
+ return []byte(b.String())
}
// seedIfMissing writes the embedded default config to path if nothing is
diff --git a/internal/web/server.go b/internal/web/server.go
index a586534..2674802 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -62,7 +62,7 @@ func (s *server) apply(cfg config.Config, tbl *bible.BookTable) {
// query parameters; it is never mutated -- /settings applies changes to a
// live, mutable copy held by server, which every handler reads per request.
func NewServer(cfg config.Config) http.Handler {
- tbl, _ := bible.LoadBookTable(config.UserBooksTOML())
+ tbl, _ := bible.LoadBookTable(config.UserBooksINI())
s := &server{cfg: cfg, tbl: tbl, bm: bookmarks.Open()}
mux := http.NewServeMux()
@@ -719,7 +719,7 @@ func themeCSSHandler(cfg config.Config) http.HandlerFunc {
// settingsData drives templates/settings.html.
type settingsData struct {
Cfg config.Config
- Books string // current books.toml text (user override, else the embedded default)
+ Books string // current books.ini text (user override, else the embedded default)
ThemeOpts []themeOpt // for the web_theme <select>
VersionOpts []versionOpt // all five, Checked if in Cfg.Versions
WebVersionOpts []versionOpt // all five, Checked if in Cfg.WebVersions
@@ -768,10 +768,10 @@ func newSettingsData(cfg config.Config, books, errMsg string, saved bool) settin
}
func currentBooksText() string {
- if b := config.UserBooksTOML(); len(b) > 0 {
+ if b := config.UserBooksINI(); len(b) > 0 {
return string(b)
}
- return string(bible.DefaultBooksTOML())
+ return string(bible.DefaultBooksINI())
}
// settingsGet renders the settings form for the live config.
@@ -785,7 +785,7 @@ func settingsGet(s *server) http.HandlerFunc {
}
}
-// settingsPost validates and persists the submitted settings + books.toml, then
+// settingsPost validates and persists the submitted settings + books.ini, then
// applies them to the running server and redirects (PRG). On a validation error
// it re-renders the form with the message and the user's edits, HTTP 200.
func settingsPost(s *server) http.HandlerFunc {
@@ -819,12 +819,12 @@ func settingsPost(s *server) http.HandlerFunc {
renderSettingsError(w, cfg, booksText, err.Error())
return
}
- // Validate books.toml (parse via the same loader the app uses).
+ // Validate books.ini (parse via the same loader the app uses).
var newTbl *bible.BookTable
if strings.TrimSpace(booksText) != "" {
t, berr := bible.LoadBookTable([]byte(booksText))
if berr != nil {
- renderSettingsError(w, cfg, booksText, "books.toml: "+berr.Error())
+ renderSettingsError(w, cfg, booksText, "books.ini: "+berr.Error())
return
}
newTbl = t
@@ -835,7 +835,7 @@ func settingsPost(s *server) http.HandlerFunc {
renderSettingsError(w, cfg, booksText, "saving config: "+err.Error())
return
}
- // Only write books.toml when the editor actually carried content, so a
+ // Only write books.ini when the editor actually carried content, so a
// submit with an empty/absent books field never clobbers an existing
// override (the form always pre-fills the textarea).
if strings.TrimSpace(booksText) != "" {
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index 284a454..12542e5 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -561,19 +561,19 @@ func TestSettingsPostInvalidBooks(t *testing.T) {
post.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, post)
- if rec.Code != 200 || !strings.Contains(rec.Body.String(), "books.toml") {
+ if rec.Code != 200 || !strings.Contains(rec.Body.String(), "books.ini") {
t.Errorf("invalid books should re-render with an error, got %d", rec.Code)
}
}
-func bibleDefaultBooks() []byte { return bible.DefaultBooksTOML() }
+func bibleDefaultBooks() []byte { return bible.DefaultBooksINI() }
// TestSettingsPostEmptyBooksPreservesFile guards that a submit without a books
-// field never clobbers an existing books.toml override.
+// field never clobbers an existing books.ini override.
func TestSettingsPostEmptyBooksPreservesFile(t *testing.T) {
dir := t.TempDir()
t.Setenv("LECTIO_CONFIG", filepath.Join(dir, "config.toml"))
- booksPath := filepath.Join(dir, "books.toml")
+ booksPath := filepath.Join(dir, "books.ini")
original := []byte("[en]\nJohn = [\"Jn\", \"John\"]\n")
if err := os.WriteFile(booksPath, original, 0o644); err != nil {
t.Fatal(err)
@@ -594,7 +594,7 @@ func TestSettingsPostEmptyBooksPreservesFile(t *testing.T) {
t.Fatalf("status %d", rec.Code)
}
if got, _ := os.ReadFile(booksPath); string(got) != string(original) {
- t.Errorf("empty books submit clobbered books.toml: got %q", got)
+ t.Errorf("empty books submit clobbered books.ini: got %q", got)
}
}
diff --git a/internal/web/templates/settings.html b/internal/web/templates/settings.html
index be08e6d..e2ee39e 100644
--- a/internal/web/templates/settings.html
+++ b/internal/web/templates/settings.html
@@ -1,5 +1,5 @@
{{/* settings.html — lectio-web /settings page: a form for every config
- setting plus a raw books.toml editor. A plain POST form (no htmx) --
+ setting plus a raw books.ini editor. A plain POST form (no htmx) --
saving redirects (PRG) back here with ?saved=1. On a validation error
the form re-renders with the submitted values and an inline message
instead of redirecting. Self-contained and theme-aware, like index.html. */}}
@@ -100,7 +100,7 @@
<input type="text" name="pager" value="{{.Cfg.Pager}}">
</label>
- <label>books.toml
+ <label>books.ini
<textarea name="books" rows="20">{{.Books}}</textarea>
</label>