# Nim CLI for retrieving Biblical passages # © 2023 Jonathan Bernard ## Simple command-line tool for retrieving Biblical passages. import std/[json, logging, os, re, strutils, wordwrap] import cliutils, docopt, zero_functional import ./api_bible import ./esv proc formatMarkdown(raw, translation: string): string = var reference = "" var inVerse = false var verseLines = newSeq[string]() for line in raw.splitLines: if reference.len == 0: reference = line.strip if inVerse: if line.startsWith("Footnotes"): inVerse = false elif line.isEmptyOrWhitespace and verseLines[^1] != "": verseLines.add("") elif not line.match(re"^\s+[^\s]"): continue elif line.match(re"$(.*)\(ESV\)$"): verseLines.add(line[0 ..< ^5]) else: verseLines.add(line) elif line.match(re"^\s+\[\d+\]"): inVerse = true verseLines.add(line) let wrapped = (verseLines --> map(if it.len > 90: it.strip else: it & " "). map(it.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "**$1**")])). map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p") result = (wrapped.splitLines --> map("> " & it)). join("\p") & "\p> -- *" & reference & " (" & translation.toUpperAscii & ")*" proc formatPlain( raw, translation: string, keepVerseNumbers = true): string = var reference = "" var inVerse = false var verseLines = newSeq[string]() for line in raw.splitLines: if reference.len == 0: reference = line.strip if inVerse: if line.startsWith("Footnotes"): inVerse = false elif line.isEmptyOrWhitespace and verseLines[^1] != "": verseLines.add("") elif not line.match(re"^\s+[^\s]"): continue elif line.match(re"$(.*)\(ESV\)$"): verseLines.add(line[0 ..< ^5]) else: verseLines.add(line) elif line.match(re"^\s+\[\d+\]"): inVerse = true verseLines.add(line) let wrapped = (verseLines --> map(if it.len > 90: it.strip else: it & " "). map( if keepVerseNumbers: it.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "$1")]) else: it.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "")])). map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p") result = (wrapped.splitLines --> map(it)). join("\p") & "\p– " & reference & " (" & translation.toUpperAscii & ")" proc fetchPassages(reference, translation: string, cfg: CombinedConfig): seq[string] = case translation of "esv": esv.fetchPassages( reference, cfg.getVal("esv-api-token"), cfg.getVal("esv-api-root", "https://api.esv.org")) of "amp", "nkjv", "niv": api_bible.fetchPassages( reference, translation, cfg.getVal("api-bible-api-key"), cfg.getVal("api-bible-root", api_bible.apiBibleRoot), cfg.getVal( "api-bible-" & translation & "-bible-id", api_bible.defaultBibleId(translation))) else: raise newException(ValueError, "unsupported translation '" & translation & "'; supported translations: amp, esv, nkjv, niv") when isMainModule: const USAGE = """Usage: bibleref [options] Options: --debug Log debug information. --echo-args Echo back the arguments that were passed on the command line for debugging purposes. -f, --output-format Select a specific output format. Valid values are 'raw', 'markdown', 'plain', 'reading'. -t, --translation Select a specific translation. Supported values are 'amp', 'esv', 'nkjv', and 'niv'. Defaults to 'esv'. --esv-api-token Provide the API token on the command line. By default this will be read either from the .bibleref.cfg.json file or the ESV_API_TOKEN envionment variable. --api-bible-api-key Provide the API.Bible API key for translations backed by api.bible. --api-bible-root Override the API.Bible API root. Defaults to https://rest.api.bible/v1. --api-bible-amp-bible-id Override the API.Bible Bible ID for AMP. --api-bible-niv-bible-id Override the API.Bible Bible ID for NIV. --api-bible-nkjv-bible-id Override the API.Bible Bible ID for NKJV. """ let consoleLogger = newConsoleLogger( levelThreshold=lvlInfo, fmtStr="bibleref - $levelname: ") logging.addHandler(consoleLogger) try: # Parse arguments let args = docopt(USAGE, version = "1.0.0") if args["--debug"]: consoleLogger.levelThreshold = lvlDebug if args["--echo-args"]: stderr.writeLine($args) let cfgFilePath = getEnv("HOME") / ".bibleref.cfg.json" var cfgFileJson = newJObject() if fileExists(cfgFilePath): debug "Loading config from " & cfgFilePath cfgFileJson = parseFile(cfgFilePath) let cfg = CombinedConfig(docopt: args, json: cfgFileJson) let translation = cfg.getVal("translation", "esv").strip.toLowerAscii let reference = $args[""] 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)) echo formattedPassages.join("\p\p") except CatchableError: fatal getCurrentExceptionMsg() debug getCurrentException().getStackTrace() quit(QuitFailure)