From a26fed000ad9f292618f9cecfd2b505dddfd004a Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 11:29:57 +0200 Subject: books: language-scoped --ref/--list via customizable books.toml + sigla_style config; v0.7.0 --- internal/cli/cli.go | 90 +++++++++++++++++++++++++++++------------------- internal/cli/cli_test.go | 87 +++++++++++++++++++++++++++++----------------- 2 files changed, 109 insertions(+), 68 deletions(-) (limited to 'internal/cli') 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 " ", 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 " ". 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()) } } -- cgit v1.3