diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 14:59:14 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 14:59:14 +0200 |
| commit | cb3379f41c06a94085c77fe359709b46712850dc (patch) | |
| tree | 032801111764c83c3ab089ed101ba4d1222a9abd | |
| parent | a13dd6224a095242da54062e4775570c4f16a718 (diff) | |
| download | lectio-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.
| -rw-r--r-- | cmd/lectio-ui/main.go | 2 | ||||
| -rw-r--r-- | internal/bible/books.ini | 243 | ||||
| -rw-r--r-- | internal/bible/booktable.go | 46 | ||||
| -rw-r--r-- | internal/bible/booktable_test.go | 10 | ||||
| -rw-r--r-- | internal/cli/cli.go | 16 | ||||
| -rw-r--r-- | internal/cli/liturgy.go | 21 | ||||
| -rw-r--r-- | internal/config/config.go | 74 | ||||
| -rw-r--r-- | internal/web/server.go | 16 | ||||
| -rw-r--r-- | internal/web/server_test.go | 10 | ||||
| -rw-r--r-- | internal/web/templates/settings.html | 4 |
10 files changed, 374 insertions, 68 deletions
diff --git a/cmd/lectio-ui/main.go b/cmd/lectio-ui/main.go index dd91117..43fc5ab 100644 --- a/cmd/lectio-ui/main.go +++ b/cmd/lectio-ui/main.go @@ -120,7 +120,7 @@ func run(args []string, stdout, stderr io.Writer) int { var mdl tea.Model = tui.New(cfg, date, bibleVer) if reader { - tbl, _ := bible.LoadBookTable(config.UserBooksTOML()) + tbl, _ := bible.LoadBookTable(config.UserBooksINI()) mdl = tui.NewReader(cfg, tbl, bookmarks.Open()) } if _, err := tea.NewProgram(mdl, tea.WithAltScreen()).Run(); err != nil { 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> |
