summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/bible/books.go19
-rw-r--r--internal/bible/books.toml167
-rw-r--r--internal/bible/books_test.go14
-rw-r--r--internal/bible/booktable.go163
-rw-r--r--internal/bible/booktable_test.go88
-rw-r--r--internal/cli/cli.go90
-rw-r--r--internal/cli/cli_test.go87
-rw-r--r--internal/config/config.go47
-rw-r--r--internal/config/config.toml1
-rw-r--r--internal/config/config_test.go21
10 files changed, 595 insertions, 102 deletions
diff --git a/internal/bible/books.go b/internal/bible/books.go
index f3af4af..037a5c3 100644
--- a/internal/bible/books.go
+++ b/internal/bible/books.go
@@ -138,22 +138,3 @@ var canonOrder = []string{
"1 Peter", "2 Peter", "1 John", "2 John", "3 John",
"Jude", "Revelation",
}
-
-// BookInfo is one Biblical book's display data for the --list-pl/--list-en
-// commands (and, in later phases, the reader book pickers).
-type BookInfo struct {
- English string // canonical English name, e.g. "John" (the aliasSeed key)
- Polish string // Polish name, e.g. "Jan" (aliasSeed value [1])
- Abbrev string // Polish citation abbreviation, e.g. "J" (aliasSeed value [0])
-}
-
-// Books returns every Biblical book in scriptural order with its English name,
-// Polish name and citation abbreviation, all derived from aliasSeed.
-func Books() []BookInfo {
- out := make([]BookInfo, 0, len(canonOrder))
- for _, name := range canonOrder {
- a := aliasSeed[name]
- out = append(out, BookInfo{English: name, Polish: a[1], Abbrev: a[0]})
- }
- return out
-}
diff --git a/internal/bible/books.toml b/internal/bible/books.toml
new file mode 100644
index 0000000..a9305d1
--- /dev/null
+++ b/internal/bible/books.toml
@@ -0,0 +1,167 @@
+# lectio book names & abbreviations, per UI language.
+#
+# This is the built-in default, embedded in the binary. To customise, copy any
+# lines you want to change into ~/.config/lectio/books.toml (next to config.toml).
+# 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", "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", "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"]
diff --git a/internal/bible/books_test.go b/internal/bible/books_test.go
index e1a65ca..e89cf6e 100644
--- a/internal/bible/books_test.go
+++ b/internal/bible/books_test.go
@@ -50,17 +50,3 @@ func TestBooksTableIntegrity(t *testing.T) {
}
}
}
-
-func TestBooksResolve(t *testing.T) {
- for _, b := range Books() {
- if b.English == "" || b.Polish == "" || b.Abbrev == "" {
- t.Errorf("incomplete BookInfo %+v", b)
- }
- if c, ok := ResolveBook(b.Abbrev); !ok || c != b.English {
- t.Errorf("abbrev %q resolved to (%q,%v), want %q", b.Abbrev, c, ok, b.English)
- }
- }
- if got := len(Books()); got != 73 {
- t.Errorf("Books() len = %d, want 73", got)
- }
-}
diff --git a/internal/bible/booktable.go b/internal/bible/booktable.go
new file mode 100644
index 0000000..eb3b4e4
--- /dev/null
+++ b/internal/bible/booktable.go
@@ -0,0 +1,163 @@
+package bible
+
+import (
+ "embed"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/pelletier/go-toml/v2"
+)
+
+// booksFS embeds the built-in default book table (names + abbreviations per UI
+// language). Users override it per book via ~/.config/lectio/books.toml.
+//
+//go:embed books.toml
+var booksFS embed.FS
+
+// BookInfo is one book's display data in one dialect, for `lectio --list` and
+// (later) the reader book pickers.
+type BookInfo struct {
+ Canonical string // English canonical name (the Bible-corpus key), e.g. "John"
+ Name string // full display name in the dialect, e.g. "John" / "Jana"
+ 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
+// 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)
+ resolve map[string]map[string]string // dialect -> lowercased form -> canonical
+ sorted map[string][]string // dialect -> lowercased forms, longest first (book matching)
+}
+
+// LoadBookTable builds the table from the embedded default, merging userTOML
+// over it per book (a book the user lists replaces that book's default forms;
+// unlisted books keep the default). userTOML may be nil/empty (defaults only).
+// 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) {
+ def, err := parseBooks(mustReadEmbedded())
+ if err != nil {
+ return nil, fmt.Errorf("embedded books.toml: %w", err) // our bug, not the user's
+ }
+ var uerr error
+ if len(strings.TrimSpace(string(userTOML))) > 0 {
+ user, perr := parseBooks(userTOML)
+ if perr != nil {
+ uerr = fmt.Errorf("ignoring malformed books.toml: %w", perr)
+ } else {
+ for dialect, books := range user {
+ if def[dialect] == nil {
+ def[dialect] = map[string][]string{}
+ }
+ for canon, forms := range books {
+ def[dialect][canon] = forms
+ }
+ }
+ }
+ }
+ t := &BookTable{
+ forms: def,
+ resolve: map[string]map[string]string{},
+ sorted: map[string][]string{},
+ }
+ for dialect, books := range def {
+ t.resolve[dialect] = map[string]string{}
+ var forms []string
+ for canon, fs := range books {
+ for _, f := range fs {
+ k := strings.ToLower(strings.TrimSpace(f))
+ if k == "" {
+ continue
+ }
+ t.resolve[dialect][k] = canon
+ forms = append(forms, k)
+ }
+ }
+ sort.SliceStable(forms, func(i, j int) bool { return len(forms[i]) > len(forms[j]) })
+ t.sorted[dialect] = forms
+ }
+ return t, uerr
+}
+
+func mustReadEmbedded() []byte {
+ b, _ := booksFS.ReadFile("books.toml") // embedded: always present
+ return b
+}
+
+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 {
+ return nil, err
+ }
+ return raw, nil
+}
+
+// Books returns every book of the dialect in scriptural (canonOrder) order.
+// A dialect with no entry for a canonical book simply omits it.
+func (t *BookTable) Books(dialect string) []BookInfo {
+ books := t.forms[dialect]
+ out := make([]BookInfo, 0, len(canonOrder))
+ for _, canon := range canonOrder {
+ forms := books[canon]
+ if len(forms) == 0 {
+ continue
+ }
+ out = append(out, BookInfo{
+ Canonical: canon,
+ Shortcut: forms[0],
+ Name: forms[len(forms)-1],
+ })
+ }
+ return out
+}
+
+// ParseRef parses a user-typed reference in the dialect into the English,
+// colon-style reference bible.Lookup expects ("John 5:15-17,20-22"), or
+// ok=false if the book is not recognised in this dialect or there is no
+// chapter:verse tail. Book resolution is dialect-scoped (idiomatic): only the
+// dialect's own forms match. The number syntax is dialect-specific -- see
+// normalizeSigla.
+func (t *BookTable) ParseRef(dialect, input string) (string, bool) {
+ low := strings.ToLower(strings.Join(strings.Fields(input), " "))
+ if low == "" {
+ return "", false
+ }
+ for _, form := range t.sorted[dialect] {
+ if low == form {
+ return "", false // book only, no chapter:verse
+ }
+ if strings.HasPrefix(low, form+" ") {
+ tail := normalizeSigla(dialect, low[len(form)+1:])
+ if tail == "" || !strings.Contains(tail, ":") {
+ return "", false
+ }
+ return t.resolve[dialect][form] + " " + tail, true
+ }
+ }
+ return "", false
+}
+
+// normalizeSigla rewrites a dialect's verse-reference tail into the colon/comma
+// form bible.Lookup expects: "<chap>:<groups>", groups comma-separated, ranges
+// with "-". Polish uses a comma (with or without a space) as the chapter/verse
+// separator and a period between disjoint groups (and " i " for "and");
+// English uses a colon for chapter/verse and a period or comma between groups.
+// Reuses the shared regexes iRe/dashRe/verseLetterRe from convert.go.
+func normalizeSigla(dialect, tail string) string {
+ tail = strings.TrimSpace(tail)
+ if dialect == "pl" {
+ tail = strings.ReplaceAll(tail, ",", ":") // chapter,verse -> chapter:verse
+ tail = iRe.ReplaceAllString(tail, ",") // Polish 'and'
+ tail = strings.ReplaceAll(tail, ".", ",") // disjoint groups
+ } else {
+ tail = strings.ReplaceAll(tail, ".", ",") // disjoint groups (comma already fine)
+ }
+ tail = dashRe.ReplaceAllString(tail, "-")
+ tail = verseLetterRe.ReplaceAllString(tail, "$1")
+ tail = strings.ReplaceAll(tail, " ", "")
+ return tail
+}
diff --git a/internal/bible/booktable_test.go b/internal/bible/booktable_test.go
new file mode 100644
index 0000000..ededad3
--- /dev/null
+++ b/internal/bible/booktable_test.go
@@ -0,0 +1,88 @@
+package bible
+
+import (
+ "testing"
+)
+
+func TestBookTableDefaults(t *testing.T) {
+ tbl, err := LoadBookTable(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, d := range []string{"en", "pl"} {
+ if got := len(tbl.Books(d)); got != 73 {
+ t.Errorf("Books(%q) len=%d want 73", d, got)
+ }
+ }
+ en := tbl.Books("en")
+ if en[0].Canonical != "Genesis" || en[len(en)-1].Canonical != "Revelation" {
+ t.Errorf("en order: first=%q last=%q", en[0].Canonical, en[len(en)-1].Canonical)
+ }
+ // John display: en shortcut "Jn"/name "John"; pl shortcut "J"/name "Jana".
+ find := func(bs []BookInfo, canon string) BookInfo {
+ for _, b := range bs {
+ if b.Canonical == canon {
+ return b
+ }
+ }
+ return BookInfo{}
+ }
+ if b := find(en, "John"); b.Shortcut != "Jn" || b.Name != "John" {
+ t.Errorf("en John = %+v", b)
+ }
+ if b := find(tbl.Books("pl"), "John"); b.Shortcut != "J" || b.Name != "Jana" {
+ t.Errorf("pl John = %+v", b)
+ }
+}
+
+func TestParseRefDialects(t *testing.T) {
+ tbl, _ := LoadBookTable(nil)
+ cases := []struct {
+ dialect, in, want string
+ ok bool
+ }{
+ {"en", "Jn 3:16", "John 3:16", true},
+ {"en", "Jn 5:15-17.20-22", "John 5:15-17,20-22", true},
+ {"en", "1 Cor 13:4-7", "1 Corinthians 13:4-7", true},
+ {"pl", "J 3,16", "John 3:16", true},
+ {"pl", "J 3, 16", "John 3:16", true},
+ {"pl", "J 6,6", "John 6:6", true},
+ {"pl", "J 5,15-17. 20-22", "John 5:15-17,20-22", true},
+ {"pl", "1 Kor 13,4-7", "1 Corinthians 13:4-7", true},
+ {"pl", "Łk 1,46-55", "Luke 1:46-55", true},
+ // dialect scoping: Polish abbrev/sep must fail in en, English in pl.
+ {"en", "Łk 3:16", "", false},
+ {"pl", "Lk 3,16", "", false},
+ {"en", "Zzz 1:1", "", false},
+ {"en", "John", "", false}, // no chapter:verse
+ }
+ for _, c := range cases {
+ got, ok := tbl.ParseRef(c.dialect, c.in)
+ if ok != c.ok || got != c.want {
+ t.Errorf("ParseRef(%q,%q)=%q,%v want %q,%v", c.dialect, c.in, got, ok, c.want, c.ok)
+ }
+ }
+}
+
+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")
+ tbl, err := LoadBookTable(user)
+ if err != nil {
+ t.Fatalf("merge err: %v", err)
+ }
+ if got, ok := tbl.ParseRef("en", "Jhn 3:16"); !ok || got != "John 3:16" {
+ t.Errorf("merged alias: got %q,%v", got, ok)
+ }
+ 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"))
+ if err == nil {
+ t.Error("expected error for malformed user toml")
+ }
+ 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)
+ }
+}
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index bd080b9..76ebd5b 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -42,8 +42,7 @@ Flags:
-C, --clean prune cached readings older than a year
-P, --pager page reading output (like git); default from config
--no-pager never page, even if config sets one
- --list-pl list all books with Polish names + abbreviations
- --list-en list all books with English names + abbreviations
+ --list list all books + abbreviations (dialect from sigla_style)
-v, --version print the version and exit
-h, --help this help
@@ -57,9 +56,9 @@ Examples:
lectio -b vul -a Vulgate text, all readings
lectio -l trad -a traditional lectionary, all readings
lectio -u harvest sigla to the horizon
- lectio -p "J 3:16" -b vul look up a passage in one version
- lectio -p "Ps 23:1" -c wuj,drb compare a passage across versions
- lectio --list-en list every book (English) + abbreviations
+ lectio -p "Jn 3:16" -b vul look up a passage (English sigla)
+ lectio -p "J 3,16" -c wuj,drb Polish sigla when sigla_style=polish/auto+pl UI
+ lectio --list list every book + abbreviations
Flags override config. Exit codes: 0 ok, 1 runtime error (fetch/parse),
2 usage error (bad flag, bad date, bad version, bad --lectionary/--lang).
@@ -100,7 +99,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var all, raw, refresh, offline, update, clean, pagerFlag, noPager bool
var bibleVer, compareList, lectionary, lang string
var width int
- var listPL, listEN bool
+ var list bool
var ref string
fs := flag.NewFlagSet("lectio", flag.ContinueOnError)
@@ -134,8 +133,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs.BoolVar(&noPager, "no-pager", false, "never page, even if config sets one")
fs.StringVar(&ref, "p", "", "look up a passage, e.g. \"J 3:16\" (with -b/-c)")
fs.StringVar(&ref, "ref", "", "look up a passage, e.g. \"J 3:16\" (with -b/-c)")
- fs.BoolVar(&listPL, "list-pl", false, "list all books with Polish names + abbreviations")
- fs.BoolVar(&listEN, "list-en", false, "list all books with English names + abbreviations")
+ fs.BoolVar(&list, "list", false, "list all books + abbreviations (in your sigla_style)")
if err := fs.Parse(rest); err != nil {
return 2
@@ -217,6 +215,15 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
}
}
+ var bookTbl *bible.BookTable
+ if list || ref != "" {
+ tbl, terr := bible.LoadBookTable(userBooksTOML())
+ if terr != nil {
+ fmt.Fprintln(stderr, "lectio:", terr) // warn; tbl is still a usable defaults table
+ }
+ bookTbl = tbl
+ }
+
out := stdout
finish := func() {}
if pagerRequested(pagerFlag, noPager, cfg) && isTerminalWriter(stdout) {
@@ -228,10 +235,10 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var code int
switch {
- case listPL || listEN:
- code = runList(listEN && !listPL, out)
+ case list:
+ code = runList(bookTbl, cfg.SiglaLang(), out)
case ref != "":
- code = lookupRef(cfg, ref, refVersions, raw, effWidth, out, stderr)
+ code = lookupRef(cfg, bookTbl, ref, refVersions, raw, effWidth, out, stderr)
case compareList != "":
code = renderCompare(cfg, compareList, date, effAll, raw, effWidth, refresh, out, stderr)
case bibleVer != "":
@@ -243,6 +250,20 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return code
}
+// 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
+}
+
// wantsHelp reports whether -h/--help appears anywhere in args.
func wantsHelp(args []string) bool {
for _, a := range args {
@@ -603,24 +624,26 @@ func refLookupVersions(cfg config.Config, bibleVer, compareList string) ([]strin
return nil, fmt.Errorf("--ref needs a corpus version; pass -b wuj|vul|grb|drb")
}
-// lookupRef renders a passage lookup (-p/--ref). A --ref citation is already in
-// target form (a "Book chap:verse" reference), exactly like a traditional
-// (missalemeum) citation, so it reuses the same render path with
-// lectionary="traditional": render.GatherVersion/Compare then resolve the ref
-// literally via bible.Lookup (no Polish->English niedziela conversion). versions
-// is the already-validated corpus set (never "bt"). raw omits the header for
-// piping. Exit 1 if NO requested version yields any verse.
-func lookupRef(cfg config.Config, ref string, versions []string, raw bool, width int, stdout, stderr io.Writer) int {
+// lookupRef renders a passage lookup (-p/--ref). It parses the typed reference
+// in the resolved sigla dialect (bookTbl.ParseRef -- dialect-scoped book names
+// and number syntax) into an English colon-style reference, then reuses the
+// same render path as the readings with lectionary="traditional" (the ref is
+// already in target form, looked up literally per version -- no niedziela
+// conversion, no cross-version psalm renumbering). versions is the validated
+// corpus set (never "bt"). raw omits the header. Exit 2 on an unparseable ref,
+// exit 1 if no requested version has the passage.
+func lookupRef(cfg config.Config, tbl *bible.BookTable, ref string, versions []string, raw bool, width int, stdout, stderr io.Writer) int {
ref = strings.TrimSpace(ref)
- if ref == "" {
- fmt.Fprintln(stderr, "lectio: empty --ref")
+ engRef, ok := tbl.ParseRef(cfg.SiglaLang(), ref)
+ if !ok {
+ fmt.Fprintf(stderr, "lectio: could not read reference %q in the %s dialect; see 'lectio --list'\n", ref, cfg.SiglaLang())
return 2
}
- sec := liturgy.Section{Citation: ref, Heading: ref}
+ sec := liturgy.Section{Citation: engRef, Heading: engRef}
found := false
for _, v := range versions {
- if vs, _ := bible.Lookup(v, ref); len(vs) > 0 {
+ if vs, _ := bible.Lookup(v, engRef); len(vs) > 0 {
found = true
break
}
@@ -663,22 +686,17 @@ func lookupRef(cfg config.Config, ref string, versions []string, raw bool, width
return 0
}
-// runList handles --list-pl/--list-en: print every Biblical book (scriptural
-// order) as "<abbrev> <name>", the name in Polish when pl, English otherwise.
-// The abbreviation column is padded by rune count (not bytes) so diacritics
-// like "Łk"/"Kpł" still line up.
-func runList(en bool, stdout io.Writer) int {
- const col = 6
- for _, b := range bible.Books() {
- name := b.Polish
- if en {
- name = b.English
- }
- pad := col - len([]rune(b.Abbrev))
+// runList handles --list: print every book of the sigla dialect (scriptural
+// order) as "<shortcut> <name>". The shortcut column is padded by rune count
+// so diacritics ("Łk") still line up.
+func runList(tbl *bible.BookTable, dialect string, stdout io.Writer) int {
+ const col = 8
+ for _, b := range tbl.Books(dialect) {
+ pad := col - len([]rune(b.Shortcut))
if pad < 1 {
pad = 1
}
- fmt.Fprintf(stdout, "%s%s%s\n", b.Abbrev, strings.Repeat(" ", pad), name)
+ fmt.Fprintf(stdout, "%s%s%s\n", b.Shortcut, strings.Repeat(" ", pad), b.Name)
}
return 0
}
diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go
index c76d61c..bc7dfdb 100644
--- a/internal/cli/cli_test.go
+++ b/internal/cli/cli_test.go
@@ -9,6 +9,7 @@ import (
"strings"
"testing"
+ "github.com/lukaszkasprzak/lectio/internal/bible"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
)
@@ -268,64 +269,86 @@ func TestExtractDateDefaultsToday(t *testing.T) {
}
}
-func TestListEN(t *testing.T) {
+func TestRefSingle(t *testing.T) {
+ var out, errb bytes.Buffer
+ code := Run([]string{"-p", "Jn 3:16", "-b", "vul"}, nil, &out, &errb)
+ if code != 0 {
+ t.Fatalf("ref code=%d stderr=%q", code, errb.String())
+ }
+ if s := out.String(); !strings.Contains(s, "3:16") {
+ t.Errorf("ref output missing verse 3:16:\n%s", s)
+ }
+}
+
+func TestRefNotFound(t *testing.T) {
var out, errb bytes.Buffer
- if code := Run([]string{"--list-en"}, nil, &out, &errb); code != 0 {
- t.Fatalf("--list-en code=%d stderr=%q", code, errb.String())
+ // An unknown book -> ParseRef fails to resolve it in the dialect -> exit 2.
+ if code := Run([]string{"-p", "Zzz 9:9", "-b", "vul"}, nil, &out, &errb); code != 2 {
+ t.Errorf("ref unknown-book code=%d want 2 (stderr=%q)", code, errb.String())
+ }
+}
+
+func TestRunListEnglish(t *testing.T) {
+ tbl, _ := bible.LoadBookTable(nil)
+ var out bytes.Buffer
+ if code := runList(tbl, "en", &out); code != 0 {
+ t.Fatalf("runList en code=%d", code)
}
- s := out.String()
- if !strings.Contains(s, "John") || !strings.Contains(s, "Genesis") {
- t.Errorf("--list-en missing expected books:\n%s", s)
+ if s := out.String(); !strings.Contains(s, "Jn") || !strings.Contains(s, "John") {
+ t.Errorf("en list missing Jn/John:\n%s", s)
}
- // abbrev-then-name, one book per line; expect a J -> John line.
- if !strings.Contains(s, "J") {
- t.Errorf("--list-en missing John abbrev")
+}
+
+func TestRunListPolish(t *testing.T) {
+ tbl, _ := bible.LoadBookTable(nil)
+ var out bytes.Buffer
+ runList(tbl, "pl", &out)
+ if s := out.String(); !strings.Contains(s, "Jana") || !strings.Contains(s, "Rodzaju") {
+ t.Errorf("pl list missing Jana/Rodzaju:\n%s", s)
}
}
-func TestListPL(t *testing.T) {
+func TestListFlag(t *testing.T) {
var out, errb bytes.Buffer
- if code := Run([]string{"--list-pl"}, nil, &out, &errb); code != 0 {
- t.Fatalf("--list-pl code=%d stderr=%q", code, errb.String())
+ if code := Run([]string{"--list"}, nil, &out, &errb); code != 0 {
+ t.Fatalf("--list code=%d stderr=%q", code, errb.String())
}
- if s := out.String(); !strings.Contains(s, "Jan") || !strings.Contains(s, "Rodzaju") {
- t.Errorf("--list-pl missing Polish names:\n%s", s)
+ if n := strings.Count(strings.TrimSpace(out.String()), "\n") + 1; n < 73 {
+ t.Errorf("--list printed %d lines, want >=73", n)
}
}
-func TestRefSingle(t *testing.T) {
+func TestRefEnglishDialect(t *testing.T) {
var out, errb bytes.Buffer
- code := Run([]string{"-p", "J 3:16", "-b", "vul"}, nil, &out, &errb)
- if code != 0 {
+ if code := Run([]string{"-p", "Jn 3:16", "-b", "vul"}, nil, &out, &errb); code != 0 {
t.Fatalf("ref code=%d stderr=%q", code, errb.String())
}
- if s := out.String(); !strings.Contains(s, "3:16") {
- t.Errorf("ref output missing verse 3:16:\n%s", s)
+ if !strings.Contains(out.String(), "3:16") {
+ t.Errorf("ref output missing 3:16:\n%s", out.String())
+ }
+}
+
+func TestRefRejectsPolishAbbrevInEnglish(t *testing.T) {
+ var out, errb bytes.Buffer
+ // English dialect (default): the Polish abbrev "Łk" must not resolve.
+ if code := Run([]string{"-p", "Łk 3:16", "-b", "vul"}, nil, &out, &errb); code != 2 {
+ t.Errorf("Łk in en dialect code=%d want 2 (stderr=%q)", code, errb.String())
}
}
func TestRefRejectsBT(t *testing.T) {
var out, errb bytes.Buffer
- if code := Run([]string{"-p", "J 3:16", "-b", "bt"}, nil, &out, &errb); code != 2 {
+ if code := Run([]string{"-p", "Jn 3:16", "-b", "bt"}, nil, &out, &errb); code != 2 {
t.Errorf("ref -b bt code=%d want 2 (stderr=%q)", code, errb.String())
}
}
func TestRefCompare(t *testing.T) {
var out, errb bytes.Buffer
- code := Run([]string{"-p", "J 3:16", "-c", "vul,drb"}, nil, &out, &errb)
- if code != 0 {
+ if code := Run([]string{"-p", "Jn 3:16", "-c", "vul,drb"}, nil, &out, &errb); code != 0 {
t.Fatalf("ref compare code=%d stderr=%q", code, errb.String())
}
- if s := out.String(); !strings.Contains(s, "3:16") {
- t.Errorf("ref compare missing verse:\n%s", s)
- }
-}
-
-func TestRefNotFound(t *testing.T) {
- var out, errb bytes.Buffer
- // A book/verse the corpus won't have text for -> exit 1.
- if code := Run([]string{"-p", "Zzz 9:9", "-b", "vul"}, nil, &out, &errb); code != 1 {
- t.Errorf("ref not-found code=%d want 1 (stderr=%q)", code, errb.String())
+ if !strings.Contains(out.String(), "3:16") {
+ t.Errorf("ref compare missing verse:\n%s", out.String())
}
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 616af97..8ee15e1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -24,7 +24,7 @@ var seedTOML []byte
// Version is lectio's release version, shared by every binary's
// -v/--version output (lectio, lectio-ui, lectio-web).
-const Version = "0.6.0"
+const Version = "0.7.0"
// validVersions are the five scripture versions lectio understands.
var validVersions = map[string]bool{
@@ -92,6 +92,20 @@ func NormalizeUILanguage(lang string) string {
return l
}
+// NormalizeSiglaStyle maps a sigla_style setting to one of "auto", "polish",
+// "english"; anything unrecognised (incl. "") falls back to "auto". Accepts the
+// short aliases "pl"/"en" for convenience.
+func NormalizeSiglaStyle(s string) string {
+ switch strings.ToLower(strings.TrimSpace(s)) {
+ case "polish", "pl":
+ return "polish"
+ case "english", "en":
+ return "english"
+ default:
+ return "auto"
+ }
+}
+
// Config holds lectio's user-configurable settings.
type Config struct {
SchemaVersion int `toml:"schema_version"`
@@ -103,6 +117,7 @@ type Config struct {
All bool `toml:"all"`
Offline bool `toml:"offline"`
UILanguage string `toml:"ui_language"`
+ SiglaStyle string `toml:"sigla_style"`
WebTheme string `toml:"web_theme"`
WebPort int `toml:"web_port"`
WebDisplay string `toml:"web_display"`
@@ -122,6 +137,23 @@ func (c Config) PartShown(lectionary, partID string) bool {
return true
}
+// SiglaLang resolves the book dialect (a books.toml section code, "en"/"pl")
+// that `lectio --ref`/`--list` use: the explicit sigla_style, or -- when "auto"
+// -- the UI language.
+func (c Config) SiglaLang() string {
+ switch c.SiglaStyle {
+ case "polish":
+ return "pl"
+ case "english":
+ return "en"
+ default:
+ if c.UILanguage == "pl" {
+ return "pl"
+ }
+ return "en"
+ }
+}
+
// Default returns lectio's built-in configuration, used when no config file
// is found and as the base that a partial config file overrides.
func Default() Config {
@@ -135,6 +167,7 @@ func Default() Config {
All: false,
Offline: false,
UILanguage: "en",
+ SiglaStyle: "auto",
WebTheme: "transfiguration",
WebPort: 0,
WebDisplay: "horizontal",
@@ -159,6 +192,17 @@ func configPath() (path string, isDefault bool, err error) {
return filepath.Join(dir, "lectio", "config.toml"), true, nil
}
+// BooksPath returns the path to the optional user books.toml (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) {
+ p, _, err := configPath()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(filepath.Dir(p), "books.toml"), nil
+}
+
// seedIfMissing writes the embedded default config to path if nothing is
// there yet.
func seedIfMissing(path string) error {
@@ -208,6 +252,7 @@ func Load() (Config, error) {
}
cfg.WebDisplay = NormalizeDisplay(cfg.WebDisplay)
cfg.UILanguage = NormalizeUILanguage(cfg.UILanguage)
+ cfg.SiglaStyle = NormalizeSiglaStyle(cfg.SiglaStyle)
if err := validate(cfg); err != nil {
return Config{}, err
diff --git a/internal/config/config.toml b/internal/config/config.toml
index 656ddc5..8324032 100644
--- a/internal/config/config.toml
+++ b/internal/config/config.toml
@@ -7,6 +7,7 @@ width = 0 # CLI wrap width; 0 = detect terminal
all = false # default to all parts (true) or just the gospel (false)
offline = false # true = never fetch; read only harvested sigla + cache
ui_language = "en" # interface language (labels/keybar/banner): "en" or "pl". Readings stay source-language.
+sigla_style = "auto" # citation dialect for `lectio --ref`/`--list`: "auto" (follow ui_language), "polish" (J 3,16), or "english" (Jn 3:16)
web_theme = "transfiguration" # default lectio-web theme (built-in order/season name, or a user theme in ~/.config/lectio/themes/)
web_port = 0 # lectio-web port; 0 = try 1099, then any free port
web_display = "horizontal" # default lectio-web layout ("uklad"): "horizontal" (stacked), "vertical" (columns), "interlinear" (verse-by-verse)
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index a72383b..cc0c941 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -178,6 +178,27 @@ func TestPagerLoadsSetting(t *testing.T) {
}
}
+func TestSiglaStyleDefaultsAndDialect(t *testing.T) {
+ if got := Default().SiglaStyle; got != "auto" {
+ t.Errorf("default SiglaStyle=%q want auto", got)
+ }
+ cases := []struct{ style, ui, want string }{
+ {"auto", "en", "en"},
+ {"auto", "pl", "pl"},
+ {"polish", "en", "pl"},
+ {"english", "pl", "en"},
+ }
+ for _, c := range cases {
+ cfg := Config{SiglaStyle: NormalizeSiglaStyle(c.style), UILanguage: c.ui}
+ if got := cfg.SiglaLang(); got != c.want {
+ t.Errorf("SiglaLang(style=%q,ui=%q)=%q want %q", c.style, c.ui, got, c.want)
+ }
+ }
+ if NormalizeSiglaStyle("nonsense") != "auto" {
+ t.Error("unknown sigla_style should normalize to auto")
+ }
+}
+
func TestPartShown(t *testing.T) {
var empty Config
if !empty.PartShown("new", "psalm") {