From 176fa46816955d26c4e2234198dd939849589603 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sun, 14 Jun 2026 08:46:34 -0500 Subject: [PATCH] Add translation-aware passage queries --- .gitignore | 1 + src/bibleref.nim | 39 ++++++++++++---------- src/passage_query.nim | 64 ++++++++++++++++++++++++++++++++++++ tests/test_passage_query.nim | 44 +++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 src/passage_query.nim create mode 100644 tests/test_passage_query.nim diff --git a/.gitignore b/.gitignore index 037aa5a..dca4609 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ esv_api bibleref tests/test_offline_kjv +tests/test_passage_query data/private/ *.sw? diff --git a/src/bibleref.nim b/src/bibleref.nim index a77e59b..13ee9e8 100644 --- a/src/bibleref.nim +++ b/src/bibleref.nim @@ -10,6 +10,7 @@ import ./api_bible import ./esv import ./kjv import ./mev +import ./passage_query proc formatMarkdown(raw, translation: string): string = var reference = "" @@ -95,7 +96,7 @@ proc fetchPassages(reference, translation: string, cfg: CombinedConfig): seq[str else: raise newException(ValueError, "unsupported translation '" & translation & - "'; supported translations: akjv, amp, esv, kjv, mev, nkjv, niv") + "'; supported translations: " & supportedTranslationsList()) when isMainModule: const USAGE = """Usage: @@ -115,6 +116,9 @@ Options: Select a specific translation. Supported values are 'akjv', 'amp', 'esv', 'kjv', 'mev', 'nkjv', and 'niv'. Defaults to 'esv'. + Individual references may override this with a + trailing marker, for example: + 'John 3:16 (KJV); John 3:16 (ESV)'. --esv-api-token Provide the API token on the command line. By default this will be read either from the @@ -156,24 +160,25 @@ Options: cfgFileJson = parseFile(cfgFilePath) let cfg = CombinedConfig(docopt: args, json: cfgFileJson) - let translation = cfg.getVal("translation", "esv").strip.toLowerAscii + let defaultTranslation = cfg.getVal("translation", "esv") let reference = $args[""] + let queries = parsePassageQueries(reference, defaultTranslation) - let passages = fetchPassages(reference, translation, cfg) - - let formattedPassages = - case $args["--output-format"]: - of "plain": - passages --> map(formatPlain(it, translation)) - of "reading": - passages --> - map(formatPlain(it, translation, keepVerseNumbers = false)) - of "text": - passages --> - map(it.multiReplace([(re"\[(\d+)\]", "$1")])) - of "raw": passages - else: - passages --> map(formatMarkdown(it, translation)) + var formattedPassages: seq[string] = @[] + for query in queries: + for passage in fetchPassages(query.referenceText, query.translation, cfg): + formattedPassages.add( + case $args["--output-format"]: + of "plain": + formatPlain(passage, query.translation) + of "reading": + formatPlain(passage, query.translation, keepVerseNumbers = false) + of "text": + passage.multiReplace([(re"\[(\d+)\]", "$1")]) + of "raw": + passage + else: + formatMarkdown(passage, query.translation)) echo formattedPassages.join("\p\p") diff --git a/src/passage_query.nim b/src/passage_query.nim new file mode 100644 index 0000000..2dceaf9 --- /dev/null +++ b/src/passage_query.nim @@ -0,0 +1,64 @@ +import std/strutils + +import ./reference_parser + +type PassageQuery* = object + reference*: PassageReference + translation*: string + +const SupportedTranslations* = [ + "akjv", "amp", "esv", "kjv", "mev", "niv", "nkjv" +] + +proc supportedTranslationsList*(): string = + SupportedTranslations.join(", ") + +proc normalizeTranslation*(translation: string): string = + result = translation.strip.toLowerAscii + + for supported in SupportedTranslations: + if result == supported: + return + + raise newException(ValueError, + "unsupported translation '" & translation & + "'; supported translations: " & supportedTranslationsList()) + +proc splitTrailingTranslationMarker( + input: string): tuple[referenceText: string, translation: string] = + + let text = input.strip + if not text.endsWith(")"): + return (text, "") + + let openIdx = text.rfind("(") + if openIdx < 0: + return (text, "") + + let referenceText = text[0 ..< openIdx].strip + let translation = text[openIdx + 1 ..< text.len - 1].strip + if referenceText.len == 0 or translation.len == 0: + return (text, "") + + (referenceText, translation) + +proc parsePassageQuery*(input, defaultTranslation: string): PassageQuery = + let parsed = splitTrailingTranslationMarker(input) + result.reference = parseReference(parsed.referenceText) + result.translation = + if parsed.translation.len > 0: + normalizeTranslation(parsed.translation) + else: + normalizeTranslation(defaultTranslation) + +proc parsePassageQueries*(input, defaultTranslation: string): seq[PassageQuery] = + for rawRef in input.split(';'): + let refText = rawRef.strip + if refText.len > 0: + result.add(parsePassageQuery(refText, defaultTranslation)) + + if result.len == 0: + raise newException(ValueError, "empty Bible reference") + +proc referenceText*(query: PassageQuery): string = + $query.reference diff --git a/tests/test_passage_query.nim b/tests/test_passage_query.nim new file mode 100644 index 0000000..5b74fb0 --- /dev/null +++ b/tests/test_passage_query.nim @@ -0,0 +1,44 @@ +import std/unittest + +import ../src/passage_query + +suite "passage query parser": + test "uses the default translation when no marker is present": + let queries = parsePassageQueries("John 3:16", "kjv") + + check queries.len == 1 + check queries[0].referenceText == "John 3:16" + check queries[0].translation == "kjv" + + test "uses a trailing translation marker": + let queries = parsePassageQueries("2 John 5 (KJV)", "esv") + + check queries.len == 1 + check queries[0].referenceText == "2 John 5" + check queries[0].translation == "kjv" + + test "parses mixed translation queries": + let queries = parsePassageQueries("2 John 5 (KJV); 2 John 5 (ESV)", "mev") + + check queries.len == 2 + check queries[0].referenceText == "2 John 5" + check queries[0].translation == "kjv" + check queries[1].referenceText == "2 John 5" + check queries[1].translation == "esv" + + test "uses the default translation per unmarked reference": + let queries = parsePassageQueries("John 3:16; Psalm 23 (MEV)", "nkjv") + + check queries.len == 2 + check queries[0].referenceText == "John 3:16" + check queries[0].translation == "nkjv" + check queries[1].referenceText == "Psalms 23" + check queries[1].translation == "mev" + + test "rejects unknown translation markers": + expect ValueError: + discard parsePassageQueries("John 3:16 (XYZ)", "esv") + + test "rejects unknown default translations": + expect ValueError: + discard parsePassageQueries("John 3:16", "xyz")