diff --git a/esv.d b/esv.d index e8dd861..8fa95d7 100644 --- a/esv.d +++ b/esv.d @@ -46,6 +46,7 @@ int lFlag; /* line length */ bool nFlag, NFlag; /* verse numbers */ bool PFlag; /* disable pager */ bool rFlag, RFlag; /* passage references */ +string sFlag; /* search passages */ bool VFlag; /* show version */ int @@ -89,21 +90,18 @@ run(string[] args) /* Parse command-line options */ try { - args.getopt( + getopt(args, getoptConfig.bundling, getoptConfig.caseSensitive, "a", &aFlag, "C", &CFlag, - "F", &FFlag, - "f", &fFlag, - "H", &HFlag, - "h", &hFlag, + "F", &FFlag, "f", &fFlag, + "H", &HFlag, "h", &hFlag, "l", &lFlag, - "N", &NFlag, - "n", &nFlag, + "N", &NFlag, "n", &nFlag, "P", &PFlag, - "R", &RFlag, - "r", &rFlag, + "R", &RFlag, "r", &rFlag, + "s", &sFlag, "V", &VFlag, ); } catch (GetOptException e) { @@ -115,7 +113,7 @@ run(string[] args) throw new Exception(e.msg); /* catch-all */ } catch (ConvException e) { throw new Exception( - "illegal argument to -l option -- integer required"); + "illegal argument to -l option -- must be integer"); } if (VFlag) { @@ -123,8 +121,13 @@ run(string[] args) return true; } + if (sFlag != "") { + /* skip argument validation */ + goto config; + } + if (args.length < 3) { - stderr.writefln("usage: %s [-C config] [-l length] [-aFfHhNnPRrV] book verses", args[0].baseName()); + stderr.writefln("usage: %s [-aFfHhNnPRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); return false; } @@ -136,6 +139,7 @@ run(string[] args) /* determine configuration file * Options have first priority, then environment variables, * then the default path */ +config: configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH) .expandTilde(); try { @@ -162,8 +166,8 @@ key = %s #[passage] #footnotes = false #headings = false -#passage_references = false -#verse_numbers = false +#passage-references = false +#verse-numbers = false "(DEFAULT_APIKEY)); } } @@ -182,16 +186,21 @@ key = %s esv = new ESVApi(apiKey); + enforce(!(aFlag && sFlag), "cannot specify both -a and -s options"); if (aFlag) { string tmpf, mpegPlayer; - /* check for mpg123 */ - enforce(executeShell( - format!"command -v %s >/dev/null 2>&1"(DEFAULT_MPEGPLAYER)).status == 0, - DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); - - tmpf = esv.getAudioVerses(args[1], args[2]); + tmpf = esv.getAudioPassage(args[1], args[2]); mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); + + /* check for an audio player */ + enforce( + executeShell( + format!"command -v %s >/dev/null 2>&1"(mpegPlayer) + ).status == 0, + format!"%s is required for audio mode; cannot continue"(mpegPlayer) + ); + /* esv has built-in support for mpg123 and mpv. * other players will work, just recompile with * the DEFAULT_MPEGPLAYER enum set differently @@ -203,6 +212,10 @@ key = %s executeShell(mpegPlayer ~ tmpf); return true; } + if (sFlag) { + writeln(esv.searchFormat(sFlag)); + return true; + } esv.extraParameters = iniData["api"].getKey("parameters"); @@ -213,9 +226,9 @@ key = %s } /* Get [passage] keys */ - foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) { + foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) { try { - esv.opts.b["include_" ~ key] = + esv.opts.b["include-" ~ key] = returnValid("true", iniData["passage"].getKey(key)).to!bool(); } catch (ConvException e) { throw new Exception(format! @@ -224,27 +237,30 @@ key = %s ); } catch (IniException e) {} // just do nothing; use the default settings } - /* Get line_length ([passage]) */ - try esv.opts.i["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + /* Get line-length ([passage]) */ + try { + esv.opts.i["line-length"] = + returnValid("0", iniData["passage"].getKey("line-length")).to!int(); + } catch (ConvException e) { throw new Exception( format!"%s: illegal value '%s' -- must be an integer"( configPath, - iniData["passage"].getKey("line_length")) + iniData["passage"].getKey("line-length")) ); } catch (IniException e) {} // just do nothing; use the default setting - if (fFlag) esv.opts.b["include_footnotes"] = true; - if (hFlag) esv.opts.b["include_headings"] = true; - if (nFlag) esv.opts.b["include_verse_numbers"] = true; - if (rFlag) esv.opts.b["include_passage_references"] = true; - if (FFlag) esv.opts.b["include_footnotes"] = false; - if (HFlag) esv.opts.b["include_headings"] = false; - if (NFlag) esv.opts.b["include_verse_numbers"] = false; - if (RFlag) esv.opts.b["include_passage_references"] = false; - if (lFlag != 0) esv.opts.i["line_length"] = lFlag; + if (fFlag) esv.opts.b["include-footnotes"] = true; + if (hFlag) esv.opts.b["include-headings"] = true; + if (nFlag) esv.opts.b["include-verse-numbers"] = true; + if (rFlag) esv.opts.b["include-passage-references"] = true; + if (FFlag) esv.opts.b["include-footnotes"] = false; + if (HFlag) esv.opts.b["include-headings"] = false; + if (NFlag) esv.opts.b["include-verse-numbers"] = false; + if (RFlag) esv.opts.b["include-passage-references"] = false; + if (lFlag != 0) esv.opts.i["line-length"] = lFlag; - verses = esv.getVerses(args[1].parseBook(), args[2]); + verses = esv.getPassage(args[1].parseBook(), args[2]); foreach (string line; verses.splitLines()) ++lines; diff --git a/esvapi.d b/esvapi.d index ec14723..e6ca771 100644 --- a/esvapi.d +++ b/esvapi.d @@ -27,7 +27,7 @@ import std.format : format; import std.json : JSONValue, parseJSON; import std.regex : regex, matchAll; import std.stdio : File; -import std.string : capitalize, tr; +import std.string : capitalize, tr, wrap; import std.net.curl : HTTP; enum ESVIndent @@ -184,7 +184,11 @@ class ESVApi _url = ESVAPI_URL; opts = ESVApiOptions(true); extraParameters = ""; - onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; }; + onProgress = (size_t dlTotal, size_t dlNow, + size_t ulTotal, size_t ulNow) + { + return 0; + }; } /* @@ -222,15 +226,15 @@ class ESVApi _url = url; } /* - * Requests the verse(s) in text format from the API and returns it. + * Requests the passage in text format from the API and returns it. * The (case-insensitive) name of the book being searched are * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * Example: getVerses("John", "3:16-21") + * Example: getPassage("John", "3:16-21") */ string - getVerses(in char[] book, in char[] verse) const + getPassage(in char[] book, in char[] verse) in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { @@ -242,60 +246,37 @@ class ESVApi { void *o; string[] parambuf; - foreach (string opt; ESVAPI_PARAMETERS) { - switch (opt) { - case "indent-using": - o = cast(void *)opts.indent_using; - break; - case "indent-poetry": - case "include-passage-references": - case "include-verse-numbers": - case "include-first-verse-numbers": - case "include-footnotes": - case "include-footnote-body": - case "include-headings": - case "include-short-copyright": - case "include-copyright": - case "include-passage-horizontal-lines": - case "include-heading-horizontal-lines": - case "include-selahs": - o = cast(void *)opts.b[opt]; - break; - case "line-length": - case "horizontal-line-length": - case "indent-paragraphs": - case "indent-poetry-lines": - case "indent-declares": - case "indent-psalm-doxology": - o = cast(void *)opts.i[opt]; - break; - default: break; - } - parambuf[parambuf.length] = format!"&%s=%s"( - opt, - opt == "indent-using" ? - opts.indent_using == ESVIndent.TAB ? "tab" : "space" - : o.to!string() - ); + + void + addParams(R)(R item) + { + parambuf[parambuf.length] = + format!"&%s=%s"(item.key, item.value); + } + + /* integers booleans indent_using */ + parambuf = new string[opts.i.length + opts.b.length + 1]; + + foreach (item; opts.i.byKeyValue()) + addParams(item); + foreach (item; opts.b.byKeyValue()) + addParams(item); + + parambuf[parambuf.length] = + format!"&indent-using=%s"( + opts.indent_using == ESVIndent.TAB ? "tab" : "space"); + + /* assemble string from string buffer */ + foreach (string param; parambuf) { + params ~= param; } } - request = HTTP(format!"%s/text/?q=%s+%s%s%s"( - _url, + response = makeRequest(format!"text/?q=%s+%s"( book .capitalize() .tr(" ", "+"), - verse, params, extraParameters) - ); - request.onProgress = onProgress; - request.onReceive = - (ubyte[] data) - { - response ~= data; - return data.length; - }; - request.addRequestHeader("Authorization", "Token " ~ _key); - request.perform(); + verse) ~ params ~ extraParameters); return response.parseJSON()["passages"][0].str; } /* @@ -305,23 +286,81 @@ class ESVApi * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * Example: getAudioVerses("John", "3:16-21") + * Example: getAudioPassage("John", "3:16-21") */ string - getAudioVerses(in char[] book, in char[] verse) const + getAudioPassage(in char[] book, in char[] verse) in (bookValid(book), "Invalid book") in (verseValid(verse), "Invalid verse format") { char[] response; - File tmpFile; + File tmpFile; + HTTP request; - auto request = HTTP(format!"%s/audio/?q=%s+%s"( - _url, + response = makeRequest(format!"audio/?q=%s+%s"( book .capitalize() .tr(" ", "+"), verse) ); + tmpFile = File(_tmp, "w"); + tmpFile.write(response); + return _tmp; + } + /* + * Requests a passage search for the given query. + * Returns a string containing JSON data representing + * the results of the search. + * + * Example: search("It is finished") + */ + char[] + search(in string query) + { + char[] response; + HTTP request; + JSONValue json; + + response = makeRequest("search/?q=" ~ query.tr(" ", "+")); + return response; + } + /* + * Calls search() and formats the results nicely as plain text. + */ + char[] + searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") + (in string query, int lineLength = 0) /* 0 means default */ + { + JSONValue resp; + char[] layout; + + resp = parseJSON(search(query)); + layout = []; + + enforce!ESVException(resp["total_results"].integer != 0, + "No results for search"); + + lineLength = lineLength == 0 ? 80 : lineLength; + + foreach (JSONValue item; resp["results"].array) { + layout ~= format!fmt( + item["reference"].str, + item["content"].str + .wrap(lineLength) + ); + } + + return layout; + } + + protected char[] + makeRequest(in char[] query) + { + char[] response; + HTTP request; + + response = []; + request = HTTP(_url ~ "/" ~ query); request.onProgress = onProgress; request.onReceive = (ubyte[] data) @@ -331,55 +370,8 @@ class ESVApi }; request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); - tmpFile = File(_tmp, "w"); - tmpFile.write(response); - return _tmp; - } - /* - * Requests a passage search for the given query. - * If raw is false, formats the passage search as - * plain text, otherwise returns JSON from the API. - * - * Example: search("It is finished") - */ - char[] - search(in string query, in bool raw = true) - { - ulong i; - char[] response; - char[] layout; - HTTP request; - JSONValue json; - request = HTTP(format!"%s/search/?q=%s"( - _url, query.tr(" ", "+")) - ); - request.onProgress = onProgress; - request.onReceive = - (ubyte[] data) - { - response ~= cast(char[])data; - return data.length; - }; - request.addRequestHeader("Authorization", "Token " ~ _key); - request.perform(); - - if (raw) - return response; - - json = response.parseJSON(); - layout = cast(char[])""; - - enforce!ESVException(json["total_results"].integer == 0, "No results for search"); - - foreach (ulong i, JSONValue result; json["results"]) { - layout ~= format!"%s\n %s\n"( - result["reference"].str, - result["content"].str - ); - } - - return layout; + return response; } }