Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da6ba66e1c | |||
| 17f953882f | |||
| ffe3118ddf | |||
| 49594610dc | |||
| 8f83c07693 | |||
| 8422199d7b | |||
| a0c17bcad9 |
@@ -1,2 +1,3 @@
|
|||||||
esv_api
|
esv_api
|
||||||
|
bibleref
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.2.2"
|
version = "1.0.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Simple Nim CLI wrapper around the ESV API (api.esv.org)"
|
description = "Simple Nim CLI for retrieving Biblical passages"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
bin = @["esv_api"]
|
bin = @["bibleref"]
|
||||||
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires "nim >= 1.6.10"
|
requires "nim >= 1.6.10"
|
||||||
requires @["docopt", "zero_functional"]
|
requires @["docopt", "nimquery", "zero_functional"]
|
||||||
|
|
||||||
# dependencies from git.jdb-software.com/jdb/nim-packages.git
|
# dependencies from git.jdb-software.com/jdb/nim-packages.git
|
||||||
requires @["cliutils"]
|
requires @["cliutils"]
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import std/[httpclient, json, logging, strutils, uri]
|
||||||
|
|
||||||
|
const apiBibleRoot* = "https://rest.api.bible/v1"
|
||||||
|
|
||||||
|
proc configBibleIdKey(translation: string): string =
|
||||||
|
let normalizedTranslation = translation.toLowerAscii
|
||||||
|
"apiBible" & normalizedTranslation[0].toUpperAscii &
|
||||||
|
normalizedTranslation[1..^1] & "BibleId"
|
||||||
|
|
||||||
|
proc defaultBibleId*(translation: string): string =
|
||||||
|
case translation.toLowerAscii
|
||||||
|
of "niv": "78a9f6124f344018-01"
|
||||||
|
else: ""
|
||||||
|
|
||||||
|
proc apiGet(apiRoot, path, query, apiKey: string): JsonNode =
|
||||||
|
var root = apiRoot
|
||||||
|
while root.endsWith("/"): root.setLen(root.len - 1)
|
||||||
|
|
||||||
|
var urlPath = root & path
|
||||||
|
if query.len > 0:
|
||||||
|
urlPath &= "?" & query
|
||||||
|
|
||||||
|
debug "requesting " & urlPath
|
||||||
|
|
||||||
|
let http = newHttpClient()
|
||||||
|
http.headers = newHttpHeaders({"api-key": apiKey})
|
||||||
|
parseJson(http.getContent(urlPath))
|
||||||
|
|
||||||
|
proc resolveBibleId(translation, apiKey, apiRoot, configuredBibleId: string): string =
|
||||||
|
let normalizedTranslation = translation.toLowerAscii
|
||||||
|
|
||||||
|
if configuredBibleId.strip.len > 0:
|
||||||
|
return configuredBibleId.strip
|
||||||
|
|
||||||
|
let defaultId = defaultBibleId(normalizedTranslation)
|
||||||
|
if defaultId.len > 0:
|
||||||
|
return defaultId
|
||||||
|
|
||||||
|
let translationCode = normalizedTranslation.toUpperAscii
|
||||||
|
let respJson = apiGet(
|
||||||
|
apiRoot,
|
||||||
|
"/bibles",
|
||||||
|
"language=eng&abbreviation=" & encodeUrl(translationCode) &
|
||||||
|
"&include-full-details=false",
|
||||||
|
apiKey)
|
||||||
|
|
||||||
|
for bible in respJson["data"].getElems:
|
||||||
|
let abbreviation =
|
||||||
|
if bible.hasKey("abbreviation"): bible["abbreviation"].getStr else: ""
|
||||||
|
let abbreviationLocal =
|
||||||
|
if bible.hasKey("abbreviationLocal"): bible["abbreviationLocal"].getStr else: ""
|
||||||
|
|
||||||
|
if abbreviation.toLowerAscii == normalizedTranslation or
|
||||||
|
abbreviationLocal.toLowerAscii == normalizedTranslation:
|
||||||
|
return bible["id"].getStr
|
||||||
|
|
||||||
|
if respJson["data"].getElems.len > 0:
|
||||||
|
return respJson["data"].getElems[0]["id"].getStr
|
||||||
|
|
||||||
|
raise newException(ValueError,
|
||||||
|
"could not find an API.Bible Bible ID for '" & translation &
|
||||||
|
"'; configure " & configBibleIdKey(normalizedTranslation))
|
||||||
|
|
||||||
|
proc resolvePassageId(reference, bibleId, apiKey, apiRoot: string): string =
|
||||||
|
let respJson = apiGet(
|
||||||
|
apiRoot,
|
||||||
|
"/bibles/" & encodeUrl(bibleId) & "/search",
|
||||||
|
"query=" & encodeUrl(reference) & "&limit=1&sort=canonical",
|
||||||
|
apiKey)
|
||||||
|
|
||||||
|
if respJson["data"].hasKey("passages"):
|
||||||
|
let passages = respJson["data"]["passages"].getElems
|
||||||
|
if passages.len > 0:
|
||||||
|
return passages[0]["id"].getStr
|
||||||
|
|
||||||
|
if respJson["data"].hasKey("verses"):
|
||||||
|
let verses = respJson["data"]["verses"].getElems
|
||||||
|
if verses.len == 1:
|
||||||
|
return verses[0]["id"].getStr
|
||||||
|
|
||||||
|
raise newException(ValueError,
|
||||||
|
"could not resolve passage reference '" & reference & "' using API.Bible")
|
||||||
|
|
||||||
|
proc fetchPassages*(
|
||||||
|
reference,
|
||||||
|
translation,
|
||||||
|
apiKey,
|
||||||
|
apiRoot,
|
||||||
|
configuredBibleId: string): seq[string] =
|
||||||
|
|
||||||
|
let bibleId = resolveBibleId(translation, apiKey, apiRoot, configuredBibleId)
|
||||||
|
let passageId = resolvePassageId(reference, bibleId, apiKey, apiRoot)
|
||||||
|
|
||||||
|
let respJson = apiGet(
|
||||||
|
apiRoot,
|
||||||
|
"/bibles/" & encodeUrl(bibleId) & "/passages/" & encodeUrl(passageId),
|
||||||
|
"content-type=text&include-notes=false&include-titles=true" &
|
||||||
|
"&include-chapter-numbers=false&include-verse-numbers=true" &
|
||||||
|
"&include-verse-spans=false",
|
||||||
|
apiKey)
|
||||||
|
|
||||||
|
let passage = respJson["data"]
|
||||||
|
@[passage["reference"].getStr & "\n" & passage["content"].getStr]
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# 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 <reference> [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 <format> Select a specific output format. Valid values
|
||||||
|
are 'raw', 'markdown', 'plain', 'reading'.
|
||||||
|
|
||||||
|
-t, --translation <translation>
|
||||||
|
Select a specific translation. Supported values
|
||||||
|
are 'amp', 'esv', 'nkjv', and 'niv'. Defaults
|
||||||
|
to 'esv'.
|
||||||
|
|
||||||
|
--esv-api-token <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 <key> Provide the API.Bible API key for translations
|
||||||
|
backed by api.bible.
|
||||||
|
|
||||||
|
--api-bible-root <url> Override the API.Bible API root. Defaults to
|
||||||
|
https://rest.api.bible/v1.
|
||||||
|
|
||||||
|
--api-bible-amp-bible-id <id> Override the API.Bible Bible ID for AMP.
|
||||||
|
|
||||||
|
--api-bible-niv-bible-id <id> Override the API.Bible Bible ID for NIV.
|
||||||
|
|
||||||
|
--api-bible-nkjv-bible-id <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["<reference>"]
|
||||||
|
|
||||||
|
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)
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import std/[httpclient, json, logging, uri]
|
||||||
|
|
||||||
|
proc fetchPassages*(reference, apiToken, apiRoot: string): seq[string] =
|
||||||
|
let http = newHttpClient()
|
||||||
|
http.headers = newHttpHeaders({"Authorization": "Token " & apiToken})
|
||||||
|
|
||||||
|
let urlPath = apiRoot & "/v3/passage/text/?q=" & encodeUrl(reference)
|
||||||
|
debug "requesting " & urlPath
|
||||||
|
|
||||||
|
let respJson = parseJson(http.getContent(urlPath))
|
||||||
|
result = @[]
|
||||||
|
for passage in respJson["passages"].getElems:
|
||||||
|
result.add(passage.getStr)
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Nim CLI Wrapper for the ESV API
|
|
||||||
# © 2023 Jonathan Bernard
|
|
||||||
|
|
||||||
## Simple command-line wrapper around the ESV API.
|
|
||||||
|
|
||||||
import std/[httpclient, json, logging, os, re, strutils, uri, wordwrap]
|
|
||||||
import cliutils, docopt, zero_functional
|
|
||||||
|
|
||||||
proc formatMarkdown(raw: string): string =
|
|
||||||
let rawLines = raw.splitLines
|
|
||||||
let wrapped = (raw.splitLines -->
|
|
||||||
filter(match(it, re"^\s+(\[\d+\]|\w).*")).
|
|
||||||
map(it.strip.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "**$1**")])).
|
|
||||||
map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).
|
|
||||||
join("\p")
|
|
||||||
|
|
||||||
result = (wrapped.splitLines --> map("> " & it)).join("\p") &
|
|
||||||
"\p>\p> -- *" & rawLines[0].strip & " (ESV)*"
|
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
const USAGE = """Usage:
|
|
||||||
esv_api <reference> [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 <format> Select a specific output format. Valid values
|
|
||||||
are 'raw', 'markdown', 'plain'.
|
|
||||||
|
|
||||||
-t, --esv-api-token <token> Provide the API token on the command line. By
|
|
||||||
default this will be read either from the
|
|
||||||
.esv_api.cfg.json file or the ESV_API_TOKEN
|
|
||||||
envionment variable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
let consoleLogger = newConsoleLogger(
|
|
||||||
levelThreshold=lvlInfo,
|
|
||||||
fmtStr="esv_api - $levelname: ")
|
|
||||||
logging.addHandler(consoleLogger)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Parse arguments
|
|
||||||
let args = docopt(USAGE, version = "0.2.2")
|
|
||||||
|
|
||||||
if args["--debug"]:
|
|
||||||
consoleLogger.levelThreshold = lvlDebug
|
|
||||||
|
|
||||||
if args["--echo-args"]: stderr.writeLine($args)
|
|
||||||
|
|
||||||
let cfgFilePath = getEnv("HOME") / ".esv_api.cfg.json"
|
|
||||||
var cfgFileJson = newJObject()
|
|
||||||
if fileExists(cfgFilePath):
|
|
||||||
debug "Loading config from " & cfgFilePath
|
|
||||||
cfgFileJson = parseFile(cfgFilePath)
|
|
||||||
|
|
||||||
let cfg = CombinedConfig(docopt: args, json: cfgFileJson)
|
|
||||||
let apiToken = cfg.getVal("esv-api-token")
|
|
||||||
let apiRoot = cfg.getVal("esv-api-root", "https://api.esv.org")
|
|
||||||
let reference = $args["<reference>"]
|
|
||||||
|
|
||||||
let http = newHttpClient()
|
|
||||||
http.headers = newHttpHeaders({"Authorization": "Token " & apiToken})
|
|
||||||
|
|
||||||
let urlPath = apiRoot & "/v3/passage/text/?q=" & encodeUrl(reference)
|
|
||||||
debug "requesting " & urlPath
|
|
||||||
let respJson = parseJson(http.getContent(urlPath))
|
|
||||||
|
|
||||||
let formattedPassages =
|
|
||||||
case $args["--output-format"]:
|
|
||||||
of "text":
|
|
||||||
respJson["passages"].getElems -->
|
|
||||||
map(it.getStr.multiReplace([(re"\[(\d+)\]", "$1")]))
|
|
||||||
of "raw": respJson["passages"].getElems --> map(it.getStr)
|
|
||||||
else:
|
|
||||||
respJson["passages"].getElems --> map(formatMarkdown(it.getStr))
|
|
||||||
|
|
||||||
echo formattedPassages.join("\p\p")
|
|
||||||
|
|
||||||
except CatchableError:
|
|
||||||
fatal getCurrentExceptionMsg()
|
|
||||||
debug getCurrentException().getStackTrace()
|
|
||||||
quit(QuitFailure)
|
|
||||||
Reference in New Issue
Block a user