commit a0341e72d7819120146a290215b4f7907483ecb1 Author: Jeremy Baxter Date: Thu Mar 23 15:59:09 2023 +1300 Add source code Added: - esv.d: reusable D interface to the ESV web API - main.d: the main program - Makefile - README.md - modified version of dini (dependency) - man pages - licenses diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4d9507 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.o +*.so +*.a +esv diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0ed4056 --- /dev/null +++ b/COPYING @@ -0,0 +1,373 @@ +All files except esv.d are licensed under the +GNU General Public License, version 3. + +The file esv.d is licensed under the +BSD 3-Clause License, which is as follows: + +The BSD 3-Clause License (BSD3) + +Copyright (c) 2023 Jeremy Baxter. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder the nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The GNU General Public License is as follows: + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7603231 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +### If you fail to build, run 'make deps'! ### + +PROG = esv +IMPORT = import +PREFIX = /usr/local +MANPREFIX = /usr/share/man + +DC = ldc2 +CFLAGS = -Os -I${IMPORT} +OBJS = main.o esv.o ini.o + +ifeq (${DEBUG},) + CFLAGS += -release +endif + +ifneq (${WI},) + CFLAGS += -wi +else + CFLAGS += -w +endif + +all: esv + +esv: ${OBJS} + ${DC} ${CFLAGS} -of=${PROG} ${OBJS} + +# main executable +main.o: main.d esv.o + ${DC} -c ${CFLAGS} main.d -of=main.o + +esv.o: esv.d + ${DC} -c -i ${CFLAGS} esv.d -of=esv.o + +ini.o: ${IMPORT}/dini/*.d + ${DC} -c -i ${CFLAGS} ${IMPORT}/dini/*.d -of=ini.o + +clean: + rm -f ${PROG} ${OBJS} + +install: esv + install -m755 esv ${DESTDIR}${PREFIX}/bin/esv + cp -f esv.1 ${DESTDIR}${MANPREFIX}/man1 + cp -f esv.conf.5 ${DESTDIR}${MANPREFIX}/man5 + +.PHONY: clean install diff --git a/README.md b/README.md new file mode 100644 index 0000000..dafb069 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# esv + +*Read the Bible from your terminal* + +`esv` is a utility that displays passages of the English Standard Bible on your terminal. +It connects to the ESV web API to retrieve the passages, +and allows configuration through command-line options and the configuration file. + +Example usage: + +``` +$ esv Psalm 23 +Psalm 23 + +The LORD Is My Shepherd + +A Psalm of David. + + The LORD is my shepherd; I shall not want. + He makes me lie down in green pastures.... +``` + +If the requested passage is over 32 lines long, `esv` will pipe it through a pager +(default less). The pager being used can be changed through the `ESV_PAGER` +environment variable or just disabled altogether by passing the -P option. + +The names of Bible books are not case sensitive, so John, john, JOHN and jOhN +are all accepted. + +## Audio + +`esv` supports playing audio passages through the -a option. +The `mpg123` audio/video player is utilised here and so it is therefore required +if you want to use audio mode. + +Audio usage is the same as normal text usage. `esv -a Matthew 5-7` will play +an audio passage of Matthew 5-7. + +## Installation + +To install `esv`, first make sure you have the [LLVM D compiler (ldc)](https://github.com/ldc-developers/ldc#installation) installed on your system. +There are no other external dependencies. + +First clone the source code repository: + +``` +$ git clone https://codeberg.org/jtbx/esv +$ cd esv +``` + +Now, compile and install: + +``` +$ make +# make install +``` + +By default the Makefile guesses that the ldc executable is named `ldc2`. If it is installed +under `ldc` instead, you can override the default D compiler executable by adding `DC=ldc` +to the end of the `make` line. + +## Documentation + +All documentation is contained in the manual pages. To access them, you can run +`man esv` and `man esv.conf` for the `esv` utility and the configuration file respectively. + +## Copying + +Copying, modifying and redistributing all files part of this software except esv.d is permitted +as long as your changed conform to the GNU General Public License version 2. + +The file esv.d is a reusable library. It is covered under the BSD 3-Clause License. + +In both cases, the licenses are contained in the file COPYING. + +This software uses a modified version of a library named "dini". This is released under +the Boost Software License version 1.0, which can be found in import/dini/LICENSE. +dini can be found at https://github.com/robik/dini. +My changes can be found at https://github.com/jtbx/dini. diff --git a/esv.1 b/esv.1 new file mode 100644 index 0000000..0dea3c7 --- /dev/null +++ b/esv.1 @@ -0,0 +1,93 @@ +.Dd $Mdocdate: March 23 2023 $ +.Dt ESV 1 +.Os +.Sh NAME +.Nm esv +.Nd read the Bible from your terminal +.Sh SYNOPSIS +.Nm esv +.Bk -words +.Op Fl C Ar config +.Op Fl l Ar length +.Op Fl aFfHhNnRrV +.Ar book verses +.Ek +.Sh DESCRIPTION +.Nm +is a program that displays passages of the Bible on your terminal. +It can also play recorded audio tracks of certain passages, +through integration with the +.Xr mpg123 1 +utility. +If a text passage is too long for standard display on a terminal, +.Nm +will put it through a text pager (default less) in order for you to be able to +scroll through the text with ease. This behaviour can be disabled by passing +the +.Fl P +flag. +.Pp +The options are as follows: +.Bl -tag -width keyword +.It Fl a +Instead of displaying text passages, play a recorded audio track. +.It Fl C Ar config +Use +.Ar config +as the configuration file path. This overrides the +.Ev ESV_CONFIG +environment variable (see section +.Sx ENVIRONMENT ) . +.It Fl F +Exclude footnotes. +.It Fl f +Include footnotes (the default). +.It Fl H +Exclude headings. +.It Fl h +Include headings (the default). +.It Fl l Ar length +Use +.Ar length +as the maximum line length. +.It Fl N +Exclude verse numbers. +.It Fl n +Include verse numbers (the default). +.It Fl P +If the passage is over 32 lines long, don't +pipe it into a pager. +.It Fl R +Exclude passage references. +.It Fl r +Include passage references (the default). +.It Fl V +Print the version number and exit. +.Sh ENVIRONMENT +.Bl -tag -width ESV_CONFIG +.It Ev ESV_CONFIG +Where to read the configuration file, rather than using the default location (see section +.Sx FILES ) . +.It Ev ESV_PAGER +What pager to use when the passage is over 32 lines long, rather than using +the +.Ic less +utility. +.Sh FILES +.Bl -tag -width ~/.config/esv.conf +.It Pa ~/.config/esv.conf +default configuration file location +.El + +.Sh EXAMPLES +Read Psalm 23: +.Pp +.Dl esv Psalm 23 +.Pp +Listen to a recorded audio track of Matthew 5-7: +.Pp +.Dl esv -a Matthew 5-7 +.Pp + +.Sh SEE ALSO +.Xr esv.conf 5 diff --git a/esv.conf.5 b/esv.conf.5 new file mode 100644 index 0000000..ad98cdc --- /dev/null +++ b/esv.conf.5 @@ -0,0 +1,67 @@ +.Dd $Mdocdate: March 23 2023 $ +.Dt ESV.CONF 5 +.Os +.Sh NAME +.Nm esv.conf +.Nd configuration file for esv +.Sh DESCRIPTION +The +.Xr esv 1 +utility uses a configuration file to customize its behaviour. +This file uses the standard Unix configuration file format, with +section-based key-value pairs. An example is listed below: +.Pp +.Dl [section] +.Dl key = value +.Pp +Comments can be used by putting a pound +.Dq # +symbol at the beginning of a line. +.Pp +The available configuration options are as follows: +.Bl -tag -width keyword +.It Sy [passage] +The +.Sy [passage] +section contains settings that modify the way passages are displayed. +.Bl -tag -width keyword +.It Em footnotes +Boolean value that determines whether or not footnotes are displayed +under the text. +.It Em headings +Boolean value that determines whether or not headings are displayed. +.It Em passage_references +Boolean value that determines whether or not passage references are +displayed before the text. +.It Em verse_numbers +Boolean value that determines whether or not verse numbers are displayed. +.It Em line_length +Integer value that determines the maximum length for each line of +the passage. +.El +.It Sy [api] +The +.Sy [api] +section contains settings that determine information +passed to the ESV Bible API. +.Bl -tag -width -keyword +.It Em key +Your API key, available from +.Lk http://api.esv.org +.Pp +This key is required, and is automatically filled in. +.It Em parameters +Optional HTTP parameters passed to the API. +If you are using this, make sure it starts with an ampersand symbol +.Dq & . +.El +.El + +.Sh FILES +.Bl -tag -width ~/.config/esv.conf +.It Pa ~/.config/esv.conf +default configuration file location +.El + +.Sh SEE ALSO +.Xr esv 1 diff --git a/esv.d b/esv.d new file mode 100644 index 0000000..53303eb --- /dev/null +++ b/esv.d @@ -0,0 +1,369 @@ +/* + * esv.d: a reusable interface to the ESV HTTP API + * licensed under the BSD 3-Clause License: + * + * The BSD 3-Clause License (BSD3) + * + * Copyright (c) 2023 Jeremy Baxter. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder the nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import std.algorithm : filter, map; +import std.array : appender; +import std.ascii : isAlphaNum; +import std.base64 : Base64; +import std.conv : to; +import std.file : mkdirRecurse, tempDir, write; +import std.format : format; +import std.json : JSONValue, parseJSON; +import std.random : rndGen; +import std.range : take; +import std.regex : matchAll, replaceAll, regex; +import std.string : capitalize; +import std.utf : toUTF8; +import std.net.curl; + +const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; +const string[] BIBLE_BOOKS = [ + // Old Testament + "Genesis", + "Exodus", + "Leviticus", + "Numbers", + "Deuteronomy", + "Joshua", + "Judges", + "Ruth", + "1_Samuel", + "2_Samuel", + "1_Kings", + "2_Kings", + "1_Chronicles", + "2_Chronicles", + "Ezra", + "Nehemiah", + "Esther", + "Job", + "Psalm", // <- + "Psalms", // <- both are valid + "Proverbs", + "Ecclesiastes", + "Song_of_Solomon", + "Isaiah", + "Jeremiah", + "Lamentations", + "Ezekiel", + "Daniel", + "Hosea", + "Joel", + "Amos", + "Obadiah", + "Jonah", + "Micah", + "Nahum", + "Habakkuk", + "Zephaniah", + "Haggai", + "Zechariah", + "Malachi", + // New Testament + "Matthew", + "Mark", + "Luke", + "John", + "Acts", + "Romans", + "1_Corinthians", + "2_Corinthians", + "Galatians", + "Ephesians", + "Philippians", + "Colossians", + "1_Thessalonians", + "2_Thessalonians", + "1_Timothy", + "2_Timothy", + "Titus", + "Philemon", + "Hebrews", + "James", + "1_Peter", + "2_Peter", + "1_John", + "2_John", + "3_John", + "Jude", + "Revelation" +]; + +class EsvAPI +{ + private string _key; + private string _url; + private string _mode; + EsvAPIOptions opts; + string extraParameters; + int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress; + string tmpDir; + this(const string key) + { + this._url = ESVAPI_URL; + this._key = key; + this._mode = "text"; + this.opts.setDefaults(); + this.extraParameters = ""; + this.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {return 0;}; + this.tmpDir = tempDir() ~ "esvapi"; + } + /* + * Returns the API URL currently in use. + */ + final string getURL() const nothrow + { + return _url; + } + /* + * If the url argument is a valid HTTP URL, sets the API URL currently in use + * to the given url argument. Otherwise, throws a UrlException. + */ + final void setURL(const string url) + { + auto matches = url.matchAll("^https?://.+\\..+(/.+)?"); + if (matches.empty) + throw new UrlException("Invalid URL format"); + else + this._url = url; + } + /* + * Returns the API authentication key that was given when the API object was instantiated. + * This authentication key cannot be changed after instantiation. + */ + final string getKey() const nothrow + { + return _key; + } + /* + * Returns the API authentication key currently in use. + */ + final string getMode() const nothrow + { + return _mode; + } + /* + * If the mode argument is either "text" or "html", + * sets the text API mode to the given mode argument. + * If the mode argument is not one of those, + * then this function will do nothing. + */ + final void setMode(const string mode) nothrow + { + foreach (string m; ["text", "html"] ) + { + if (mode == m) + { + this._mode = mode; + return; + } + } + } + /* + * Returns true if the argument book is a valid book of the Bible. + * Otherwise, returns false. + */ + final bool validateBook(const string book) const nothrow + { + foreach (string b; BIBLE_BOOKS) + { + if (book.capitalize() == b.capitalize()) + return true; + } + return false; + } + /* + * Returns true if the argument book is a valid verse format. + * Otherwise, returns false. + */ + final bool validateVerse(const string verse) const + { + bool attemptRegex(const string re) const + { + auto matches = verse.matchAll(re); + return !matches.empty; + } + if (attemptRegex("^\\d{1,3}$") || + attemptRegex("^\\d{1,3}-\\d{1,3}$") || + attemptRegex("^\\d{1,3}:\\d{1,3}$") || + attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) + { + return true; + } + else + { + return false; + } + } + /* + * Requests the verse(s) 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") + */ + final string getVerses(const string book, const string verse) const + { + if (!this.validateBook(book)) + throw new EsvPassageException("Invalid book"); + if (!this.validateVerse(verse)) + throw new EsvPassageException("Invalid verse format"); + + string apiURL = format!"%s/%s/?q=%s+%s%s%s"(this._url, this._mode, + book.capitalize().replaceAll(regex("_"), "+"), verse, this.assembleParameters(), this.extraParameters); + auto request = HTTP(apiURL); + string response; + request.onProgress = this.onProgress; + request.onReceive = (ubyte[] data) + { + response = cast(string)data; + return data.length; + }; + request.addRequestHeader("Authorization", "Token " ~ this._key); + request.perform(); + return response.parseJSON()["passages"][0].str; + } + /* + * Requests an audio track of the verse(s) from the API and + * returns a file path containing an MP3 sound track. + * 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") + */ + final string getAudioVerses(const string book, const string verse) + { + if (!this.validateBook(book)) + throw new EsvPassageException("Invalid book"); + if (!this.validateVerse(verse)) + throw new EsvPassageException("Invalid verse format"); + + string apiURL = format!"%s/audio/?q=%s+%s"(this._url, book.capitalize().replaceAll(regex("_"), "+"), verse); + auto request = HTTP(apiURL); + ubyte[] response; + request.onProgress = this.onProgress; + request.onReceive = (ubyte[] data) + { + response = response ~= data; + return data.length; + }; + request.addRequestHeader("Authorization", "Token " ~ this._key); + request.perform(); + string tmpFile = tempFile(); + tmpFile.write(response); + return tmpFile; + } + private string assembleParameters() const + { + string params = ""; + string addParam(string param, string value) const + { + return format!"%s&%s=%s"(params, param, value); + } + params = addParam("include-passage-references", this.opts.boolOpts["include_passage_references"].to!string); + params = addParam("include-verse-numbers", this.opts.boolOpts["include_verse_numbers"].to!string); + params = addParam("include-first-verse-numbers", this.opts.boolOpts["include_first_verse_numbers"].to!string); + params = addParam("include-footnotes", this.opts.boolOpts["include_footnotes"].to!string); + params = addParam("include-footnote-body", this.opts.boolOpts["include_footnote_body"].to!string); + params = addParam("include-headings", this.opts.boolOpts["include_headings"].to!string); + params = addParam("include-short-copyright", this.opts.boolOpts["include_short_copyright"].to!string); + params = addParam("include-copyright", this.opts.boolOpts["include_copyright"].to!string); + params = addParam("include-passage-horizontal-lines", this.opts.boolOpts["include_passage_horizontal_lines"].to!string); + params = addParam("include-heading-horizontal-lines", this.opts.boolOpts["include_heading_horizontal_lines"].to!string); + params = addParam("include-selahs", this.opts.boolOpts["include_selahs"].to!string); + params = addParam("indent-poetry", this.opts.boolOpts["indent_poetry"].to!string); + params = addParam("horizontal-line-length", this.opts.intOpts ["horizontal_line_length"].to!string); + params = addParam("indent-paragraphs", this.opts.intOpts ["indent_paragraphs"].to!string); + params = addParam("indent-poetry-lines", this.opts.intOpts ["indent_poetry_lines"].to!string); + params = addParam("indent-declares", this.opts.intOpts ["indent_declares"].to!string); + params = addParam("indent-psalm-doxology", this.opts.intOpts ["indent_psalm_doxology"].to!string); + params = addParam("line-length", this.opts.intOpts ["line_length"].to!string); + params = addParam("indent-using", this.opts.indent_using.to!string); + return params; + } + private string tempFile() const + { + auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); + auto result = appender!string(); + Base64.encode(rndNums, result); + this.tmpDir.mkdirRecurse(); + string f = this.tmpDir ~ "/" ~ result.data.filter!isAlphaNum().to!string(); + f.write(""); + return f; + } +} + +struct EsvAPIOptions +{ + bool[string] boolOpts; + int[string] intOpts; + string indent_using; + void setDefaults() nothrow + { + this.boolOpts["include_passage_references"] = true; + this.boolOpts["include_verse_numbers"] = true; + this.boolOpts["include_first_verse_numbers"] = true; + this.boolOpts["include_footnotes"] = true; + this.boolOpts["include_footnote_body"] = true; + this.boolOpts["include_headings"] = true; + this.boolOpts["include_short_copyright"] = true; + this.boolOpts["include_copyright"] = false; + this.boolOpts["include_passage_horizontal_lines"] = false; + this.boolOpts["include_heading_horizontal_lines"] = false; + this.boolOpts["include_selahs"] = true; + this.boolOpts["indent_poetry"] = true; + this.intOpts["horizontal_line_length"] = 55; + this.intOpts["indent_paragraphs"] = 2; + this.intOpts["indent_poetry_lines"] = 4; + this.intOpts["indent_declares"] = 40; + this.intOpts["indent_psalm_doxology"] = 30; + this.intOpts["line_length"] = 0; + this.indent_using = "space"; + } +} + +class UrlException : Exception +{ + this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure + { + super(msg, file, line); + } +} + +class EsvPassageException : Exception +{ + this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure + { + super(msg, file, line); + } +} diff --git a/import/dini/LICENSE b/import/dini/LICENSE new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/import/dini/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/import/dini/README b/import/dini/README new file mode 100644 index 0000000..6d53e26 --- /dev/null +++ b/import/dini/README @@ -0,0 +1,30 @@ +This code is a modified version of dini, found here: + +My changes can be found here: + + +Licensed under the Boost Software License - Version 1.0: + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/import/dini/package.d b/import/dini/package.d new file mode 100644 index 0000000..309d84e --- /dev/null +++ b/import/dini/package.d @@ -0,0 +1,3 @@ +module dini; + +public import dini.parser; \ No newline at end of file diff --git a/import/dini/parser.d b/import/dini/parser.d new file mode 100644 index 0000000..66ba3fb --- /dev/null +++ b/import/dini/parser.d @@ -0,0 +1,681 @@ +/** + * INI parsing functionality. + * + * Examples: + * --- + * auto ini = Ini.ParseString("test = bar") + * writeln("Value of test is ", ini["test"]); + * --- + */ +module dini.parser; + +import std.algorithm : min, max, countUntil; +import std.array : split, replaceInPlace, join; +import std.file : readText; +import std.stdio : File; +import std.string : strip, splitLines; +import std.traits : isSomeString; +import std.range : ElementType; +import std.conv : to; +import dini.reader : UniversalINIReader, INIException, INIToken; + + +/** + * Represents ini section + * + * Example: + * --- + * Ini ini = Ini.Parse("path/to/your.conf"); + * string value = ini.getKey("a"); + * --- + */ +struct IniSection +{ + /// Section name + protected string _name = "root"; + + /// Parent + /// Null if none + protected IniSection* _parent; + + /// Childs + protected IniSection[] _sections; + + /// Keys + protected string[string] _keys; + + + + /** + * Creates new IniSection instance + * + * Params: + * name = Section name + */ + public this(string name) + { + _name = name; + _parent = null; + } + + + /** + * Creates new IniSection instance + * + * Params: + * name = Section name + * parent = Section parent + */ + public this(string name, IniSection* parent) + { + _name = name; + _parent = parent; + } + + /** + * Sets section key + * + * Params: + * name = Key name + * value = Value to set + */ + public void setKey(string name, string value) + { + _keys[name] = value; + } + + /** + * Checks if specified key exists + * + * Params: + * name = Key name + * + * Returns: + * True if exists, false otherwise + */ + public bool hasKey(string name) @safe nothrow @nogc + { + return (name in _keys) !is null; + } + + /** + * Gets key value + * + * Params: + * name = Key name + * + * Returns: + * Key value + */ + public string getKey(string name) + { + if(!hasKey(name)) { + return ""; + } + + return _keys[name]; + } + + + /// ditto + alias getKey opCall; + + /** + * Gets key value or defaultValue if key does not exist + * + * Params: + * name = Key name + * defaultValue = Default value + * + * Returns: + * Key value or defaultValue + * + */ + public string getKey(string name, string defaultValue) @safe nothrow + { + return hasKey(name) ? _keys[name] : defaultValue; + } + + /** + * Removes key + * + * Params: + * name = Key name + */ + public void removeKey(string name) + { + _keys.remove(name); + } + + /** + * Adds section + * + * Params: + * section = Section to add + */ + public void addSection(ref IniSection section) + { + _sections ~= section; + } + + /** + * Checks if specified section exists + * + * Params: + * name = Section name + * + * Returns: + * True if exists, false otherwise + */ + public bool hasSection(string name) + { + foreach(ref section; _sections) + { + if(section.name() == name) + return true; + } + + return false; + } + + /** + * Returns reference to section + * + * Params: + * Section name + * + * Returns: + * Section with specified name + */ + public ref IniSection getSection(string name) + { + foreach(ref section; _sections) + { + if(section.name() == name) + return section; + } + + throw new IniException("Section '"~name~"' does not exist"); + } + + + /// ditto + public alias getSection opIndex; + + /** + * Removes section + * + * Params: + * name = Section name + */ + public void removeSection(string name) + { + IniSection[] childs; + + foreach(section; _sections) + { + if(section.name != name) + childs ~= section; + } + + _sections = childs; + } + + /** + * Section name + * + * Returns: + * Section name + */ + public string name() @property + { + return _name; + } + + /** + * Array of keys + * + * Returns: + * Associative array of keys + */ + public string[string] keys() @property + { + return _keys; + } + + /** + * Array of sections + * + * Returns: + * Array of sections + */ + public IniSection[] sections() @property + { + return _sections; + } + + /** + * Root section + */ + public IniSection root() @property + { + IniSection s = this; + + while(s.getParent() != null) + s = *(s.getParent()); + + return s; + } + + /** + * Section parent + * + * Returns: + * Pointer to parent, or null if parent does not exists + */ + public IniSection* getParent() + { + return _parent; + } + + /** + * Checks if current section has parent + * + * Returns: + * True if section has parent, false otherwise + */ + public bool hasParent() + { + return _parent != null; + } + + /** + * Moves current section to another one + * + * Params: + * New parent + */ + public void setParent(ref IniSection parent) + { + _parent.removeSection(this.name); + _parent = &parent; + parent.addSection(this); + } + + + /** + * Parses filename + * + * Params: + * filename = Configuration filename + * doLookups = Should variable lookups be resolved after parsing? + */ + public void parse(string filename, bool doLookups = true) + { + parseString(readText(filename), doLookups); + } + + public void parse(File* file, bool doLookups = true) + { + string data = file.byLine().join().to!string; + parseString(data, doLookups); + } + + public void parseWith(Reader)(string filename, bool doLookups = true) + { + parseStringWith!Reader(readText(filename), doLookups); + } + + public void parseWith(Reader)(File* file, bool doLookups = true) + { + string data = file.byLine().join().to!string; + parseStringWith!Reader(data, doLookups); + } + + public void parseString(string data, bool doLookups = true) + { + parseStringWith!UniversalINIReader(data, doLookups); + } + + public void parseStringWith(Reader)(string data, bool doLookups = true) + { + IniSection* section = &this; + + auto reader = Reader(data); + alias KeyType = reader.KeyType; + while (reader.next()) switch (reader.type) with (INIToken) { + case SECTION: + section = &this; + string name = reader.value.get!string; + auto parts = name.split(":"); + + // [section : parent] + if (parts.length > 1) + name = parts[0].strip; + + IniSection child = IniSection(name, section); + + if (parts.length > 1) { + string parent = parts[1].strip; + child.inherit(section.getSectionEx(parent)); + } + section.addSection(child); + section = §ion.getSection(name); + break; + + case KEY: + section.setKey(reader.value.get!KeyType.name, reader.value.get!KeyType.value); + break; + + default: + break; + } + + if(doLookups == true) + parseLookups(); + } + + /** + * Parses lookups + */ + public void parseLookups() + { + foreach (name, ref value; _keys) + { + ptrdiff_t start = -1; + char[] buf; + + foreach (i, c; value) { + if (c == '%') { + if (start != -1) { + IniSection sect; + string newValue; + char[][] parts; + + if (buf[0] == '.') { + parts = buf[1..$].split("."); + sect = this.root; + } + else { + parts = buf.split("."); + sect = this; + } + + newValue = sect.getSectionEx(parts[0..$-1].join(".").idup).getKey(parts[$-1].idup); + + value.replaceInPlace(start, i+1, newValue); + start = -1; + buf = []; + } + else { + start = i; + } + } + else if (start != -1) { + buf ~= c; + } + } + } + + foreach(child; _sections) { + child.parseLookups(); + } + } + + /** + * Returns section by name in inheriting(names connected by dot) + * + * Params: + * name = Section name + * + * Returns: + * Section + */ + public IniSection getSectionEx(string name) + { + IniSection* root = &this; + auto parts = name.split("."); + + foreach(part; parts) { + root = (&root.getSection(part)); + } + + return *root; + } + + /** + * Inherits keys from section + * + * Params: + * Section to inherit + */ + public void inherit(IniSection sect) + { + this._keys = sect.keys().dup; + } + + + public void save(string filename) + { + import std.file; + + if (exists(filename)) + remove(filename); + + File file = File(filename, "w"); + + foreach (section; _sections) { + file.writeln("[" ~ section.name() ~ "]"); + + string[string] propertiesInSection = section.keys(); + foreach (key; propertiesInSection.keys) { + file.writeln(key ~ " = " ~ propertiesInSection[key]); + } + + file.writeln(); + } + + file.close(); + } + + + /** + * Parses Ini file + * + * Params: + * filename = Path to ini file + * + * Returns: + * IniSection root + */ + static Ini Parse(string filename, bool parseLookups = true) + { + Ini i; + i.parse(filename, parseLookups); + return i; + } + + + /** + * Parses Ini file with specified reader + * + * Params: + * filename = Path to ini file + * + * Returns: + * IniSection root + */ + static Ini ParseWith(Reader)(string filename, bool parseLookups = true) + { + Ini i; + i.parseWith!Reader(filename, parseLookups); + return i; + } + + static Ini ParseString(string data, bool parseLookups = true) + { + Ini i; + i.parseString(data, parseLookups); + return i; + } + + static Ini ParseStringWith(Reader)(string data, bool parseLookups = true) + { + Ini i; + i.parseStringWith!Reader(data, parseLookups); + return i; + } +} + +// Compat +alias INIException IniException; + +/// ditto +alias IniSection Ini; + + +/// +Struct siphon(Struct)(Ini ini) +{ + import std.traits; + Struct ans; + if(ini.hasSection(Struct.stringof)) + foreach(ti, Name; FieldNameTuple!(Struct)) + { + alias ToType = typeof(ans.tupleof[ti]); + if(ini[Struct.stringof].hasKey(Name)) + ans.tupleof[ti] = to!ToType(ini[Struct.stringof].getKey(Name)); + } + return ans; +} + +unittest { + struct Section { + int var; + } + + auto ini = Ini.ParseString("[Section]\nvar=3"); + auto m = ini.siphon!Section; + assert(m.var == 3); +} + + +unittest { + auto data = q"( +key1 = value + +# comment + +test = bar ; comment + +[section 1] +key1 = new key +num = 151 +empty + + +[ various ] +"quoted key"= VALUE 123 + +quote_multiline = """ + this is value +""" + +escape_sequences = "yay\nboo" +escaped_newlines = abcd \ +efg +)"; + + auto ini = Ini.ParseString(data); + assert(ini.getKey("key1") == "value"); + assert(ini.getKey("test") == "bar ; comment"); + + assert(ini.hasSection("section 1")); + with (ini["section 1"]) { + assert(getKey("key1") == "new key"); + assert(getKey("num") == "151"); + assert(getKey("empty") == ""); + } + + assert(ini.hasSection("various")); + with (ini["various"]) { + assert(getKey("quoted key") == "VALUE 123"); + assert(getKey("quote_multiline") == "\n this is value\n"); + assert(getKey("escape_sequences") == "yay\nboo"); + assert(getKey("escaped_newlines") == "abcd efg"); + } +} + +unittest { + auto data = q"EOF +key1 = value + +# comment + +test = bar ; comment + +[section 1] +key1 = new key +num = 151 +empty + +EOF"; + + auto ini = Ini.ParseString(data); + assert(ini.getKey("key1") == "value"); + assert(ini.getKey("test") == "bar ; comment"); + assert(ini.hasSection("section 1")); + assert(ini["section 1"]("key1") == "new key"); + assert(ini["section 1"]("num") == "151"); + assert(ini["section 1"]("empty") == ""); +} + +unittest { + auto data = q"EOF +[def] +name1=value1 +name2=value2 + +[foo : def] +name1=Name1 from foo. Lookup for def.name2: %name2% +EOF"; + + // Parse file + auto ini = Ini.ParseString(data, true); + + assert(ini["foo"].getKey("name1") + == "Name1 from foo. Lookup for def.name2: value2"); +} + +unittest { + auto data = q"EOF +[section] +name=%value% +EOF"; + + // Create ini struct instance + Ini ini; + Ini iniSec = IniSection("section"); + ini.addSection(iniSec); + + // Set key value + ini["section"].setKey("value", "verify"); + + // Now, you can use value in ini file + ini.parseString(data); + + assert(ini["section"].getKey("name") == "verify"); +} + + +unittest { + import dini.reader; + + alias MyReader = INIReader!( + UniversalINIFormat, + UniversalINIReader.CurrentFlags & ~INIFlags.ProcessEscapes, + UniversalINIReader.CurrentBoxer + ); + auto ini = Ini.ParseStringWith!MyReader(`path=C:\Path`); + assert(ini("path") == `C:\Path`); +} \ No newline at end of file diff --git a/import/dini/reader.d b/import/dini/reader.d new file mode 100644 index 0000000..6a03199 --- /dev/null +++ b/import/dini/reader.d @@ -0,0 +1,786 @@ +/** + * Implements INI reader. + * + * `INIReader` is fairly low-level, configurable reader for reading INI data, + * which you can use to build your own object-model. + * + * High level interface is available in `dini.parser`. + * + * + * Unless you need to change `INIReader` behaviour, you should use one of provided + * preconfigured readers: + * + * - `StrictINIReader` + * + * Lower compatibility, may be bit faster. + * + * + * - `UniversalINIReader` + * + * Higher compatibility, may be slighly slower. + */ +module dini.reader; + +import std.algorithm : countUntil, canFind, map; +import std.array : array; +import std.functional : unaryFun; +import std.string : representation, assumeUTF, strip, + stripLeft, stripRight, split, join, format; +import std.range : ElementType, replace; +import std.uni : isWhite, isSpace; +import std.variant : Algebraic; +import dini.utils : isBoxer, BoxerType, parseEscapeSequences; + + +/** + * Represents type of current token used by INIReader. + */ +enum INIToken +{ + BLANK, /// + SECTION, /// + KEY, /// + COMMENT /// +} + + +/** + * Represents a block definition. + * + * Block definitions are used to define new quote and comment sequences + * to be accepted by INIReader. + * + * BlockDefs can be either single line or multiline. To define new single + * line block `INIBlockDef.mutliline` must be set to `false` AND `closing` + * must be set to newline string(`"\n"`). + */ +struct INIBlockDef +{ + /** + * Opening character sequence + */ + string opening; + + /** + * Closing character sequence + */ + string closing; + + /** + * Should newline characters be allowed? + */ + bool multiline; +} + + +/** + * INIReader behaviour flags. + * + * These flags can be used to modify INIReader behaviour. + */ +enum INIFlags : uint +{ + /** + * Should escape sequences be translated? + */ + ProcessEscapes = 1 << 0, + + + /** + * Section names will be trimmed. + */ + TrimSections = 1 << 4, + + /** + * Key names will be trimmed. + */ + TrimKeys = 1 << 5, + + /** + * Values will be trimmed. + */ + TrimValues = 1 << 6, + + /** + * Section names, keys and values will be trimmed. + */ + TrimAll = TrimSections | TrimKeys | TrimValues +} + + +/** + * Defines INI format. + * + * This struct defines INI comments and quotes sequences. + * + * `INIReader` adds no default quotes or comment definitions, + * and thus when defining custom format make sure to include default + * definitions to increase compatibility. + */ +struct INIFormatDescriptor +{ + /** + * List of comment definitions to support. + */ + INIBlockDef[] comments; + + /** + * List of quote definitions to support. + */ + INIBlockDef[] quotes; +} + + +/** + * Strict INI format. + * + * This format is used by `MinimalINIReader`. + * + * This format defines only `;` as comment character and `"` as only quote. + * For more universal format consider using `UniversalINIFormat`. + */ +const INIFormatDescriptor StrictINIFormat = INIFormatDescriptor( + [INIBlockDef(";", "\n", false)], + [INIBlockDef(`"`, `"`, false)] +); + + +/** + * Universal INI format. + * + * This format extends `StrictINIFormat` with hash-comments (`#`) and multiline + * triple-quotes (`"""`). + */ +const INIFormatDescriptor UniversalINIFormat = INIFormatDescriptor( + [INIBlockDef(";", "\n", false), INIBlockDef("#", "\n", false)], + [INIBlockDef(`"""`, `"""`, true), INIBlockDef(`"`, `"`, false)] +); + + +/** + * Thrown when an parsing error occurred. + */ +class INIException : Exception +{ + this(string msg = null, Throwable next = null) { super(msg, next); } + this(string msg, string file, size_t line, Throwable next = null) { + super(msg, file, line, next); + } +} + + +/** + * Represents parsed INI key. + * + * Prefer using `YOUR_READER.KeyType` alias. + */ +struct INIReaderKey(ValueType) +{ + /** + * Key name + */ + string name; + + /** + * Key value (may be boxed) + */ + ValueType value; +} + + +/** + * Splits source into tokens. + * + * This struct requires token delimeters to be ASCII-characters, + * Unicode is not supported **only** for token delimeters. + * + * Unless you want to modify `INIReader` behaviour prefer using one of available + * preconfigured variants: + * + * - `StrictINIReader` + * - `UniversalINIReader` + * + * + * `INIReader` expects three template arguments: + * + * - `Format` + * + * Instance of `INIFormatDescriptor`, defines quote and comment sequences. + * + * + * - `Flags` + * + * `INIReaderFlags` (can be OR-ed) + * + * + * - `Boxer` + * + * Name of a function that takes `(string value, INIReader reader)` and returns a value. + * By default all readers just proxy values, doing nothing, but this can be used to e.g. + * store token values as JSONValue or other Algebraic-like type. + * + * `INIReader.BoxType` is always return type of boxer function. So if you passed a boxer that + * returns `SomeAlgebraic` then `typeof(reader.key.value)` is `SomeAlgebraic`. + * + * + * Params: + * Format - `INIFormatDescriptor` to use. + * Flags - Reader behaviour flags. + * Boxer - Function name that can optionally box values. + * + * + * Examples: + * --- + * auto reader = UniversalINIReader("key=value\n"); + * + * while (reader.next) { + * writeln(reader.value); + * } + * --- + */ +struct INIReader(INIFormatDescriptor Format, ubyte Flags = 0x00, alias Boxer) + if (isBoxer!Boxer) +{ + /** + * Reader's format descriptor. + */ + alias CurrentFormat = Format; + + /** + * Reader's flags. + */ + alias CurrentFlags = Flags; + + /** + * Reader's boxer. + */ + alias CurrentBoxer = Boxer; + + /** + * Reader's Box type (boxer return type). + */ + alias BoxType = BoxerType!Boxer; + + + /** + * Alias for INIReaderKey!(BoxType). + */ + alias KeyType = INIReaderKey!BoxType; + + /** + * Type of `value` property. + */ + alias TokenValue = Algebraic!(string, KeyType); + + + /** + * INI source bytes. + */ + immutable(ubyte)[] source; + + /** + * INI source offset in bytes. + */ + size_t sourceOffset; + + /** + * Type of current token. + */ + INIToken type; + + /** + * Indicates whenever source has been exhausted. + */ + bool empty; + + /** + * Used only with Key tokens. + * + * Indicates whenever current value has been quoted. + * This information can be used by Boxers to skip boxing of quoted values. + */ + bool isQuoted; + + /** + * Current token's value. + */ + TokenValue value; + + + /** + * Creates new instance of `INIReader` from `source`. + * + * If passed source does not end with newline it is added (and thus allocates). + * To prevent allocation make sure `source` ends with new line. + * + * Params: + * source - INI source. + */ + this(string source) + { + // Make source end with newline + if (source[$-1] != '\n') + this.source = (source ~ "\n").representation; + else + this.source = source.representation; + } + + /** + * Returns key token. + * + * Use this only if you know current token is KEY. + */ + KeyType key() @property { + return value.get!KeyType; + } + + /** + * Returns section name. + * + * Use this only if you know current token is SECTION. + */ + string sectionName() @property { + return value.get!string; + } + + /** + * Reads next token. + * + * Returns: + * True if more tokens are available, false otherwise. + */ + bool next() + { + isQuoted = false; + skipWhitespaces(); + + if (current.length == 0) { + empty = true; + return false; + } + + int pairIndex = -1; + while(source.length - sourceOffset > 0) + { + if (findPair!`comments`(pairIndex)) { + readComment(pairIndex); + break; + } + else if (current[0] == '[') { + readSection(); + break; + } + else if (isWhite(current[0])) { + skipWhitespaces(); + } + else { + readEntry(); + break; + } + } + + return true; + } + + bool findPair(string fieldName)(out int pairIndex) + { + if (source.length - sourceOffset > 0 && sourceOffset > 0 && source[sourceOffset - 1] == '\\') return false; + + alias MemberType = typeof(__traits(getMember, Format, fieldName)); + foreach (size_t i, ElementType!MemberType pairs; __traits(getMember, Format, fieldName)) { + string opening = pairs.tupleof[0]; + + if (source.length - sourceOffset < opening.length) + continue; + + if (current[0..opening.length] == opening) { + pairIndex = cast(int)i; + return true; + } + } + + return false; + } + + void readSection() + { + type = INIToken.SECTION; + auto index = current.countUntil(']'); + if (index == -1) + throw new INIException("Section not closed"); + + value = current[1 .. index].assumeUTF; + + static if (Flags & INIFlags.TrimSections) + value = value.get!string.strip; + + sourceOffset += index + 1; + } + + void readComment(int pairIndex) + { + type = INIToken.COMMENT; + INIBlockDef commentDef = Format.comments[pairIndex]; + sourceOffset += commentDef.opening.length; + + auto index = current.countUntil(commentDef.closing); + if (index == -1) + throw new INIException("Comment not closed"); + + value = current[0.. index].assumeUTF; + + if (commentDef.multiline == false && value.get!string.canFind('\n')) + throw new INIException("Comment not closed (multiline)"); + + sourceOffset += index + commentDef.closing.length; + } + + void readEntry() + { + type = INIToken.KEY; + KeyType key; + + readKey(key); + if (current[0] == '=') { + sourceOffset += 1; + key.value = readValue(); + } + + value = key; + } + + void readKey(out KeyType key) + { + if (tryReadQuote(key.name)) { + isQuoted = true; + return; + } + + auto newLineOffset = current.countUntil('\n'); + if (newLineOffset > 0) { // read untill newline/some assign sequence + auto offset = current[0..newLineOffset].countUntil('='); + + if (offset == -1) + key.name = current[0 .. newLineOffset].assumeUTF; + else + key.name = current[0 .. offset].assumeUTF; + + sourceOffset += key.name.length; + key.name = key.name.stripRight; + + static if (Flags & INIFlags.TrimKeys) + key.name = key.name.stripLeft; + } + } + + + BoxType readValue() + { + auto firstNonSpaceIndex = current.countUntil!(a => !isSpace(a)); + if (firstNonSpaceIndex > 0) + sourceOffset += firstNonSpaceIndex; + + string result = ""; + auto indexBeforeQuotes = sourceOffset; + + isQuoted = tryReadQuote(result); + auto newlineOffset = current.countUntil('\n'); + string remains = current[0..newlineOffset].assumeUTF; + + if (isQuoted && newlineOffset > 0) { + sourceOffset = indexBeforeQuotes; + isQuoted = false; + } + + if (!isQuoted) { + bool escaped = false; + int[] newlineOffsets = []; + auto localOffset = 0; + for (; source.length - sourceOffset > 0; ++localOffset) { + if (source[sourceOffset + localOffset] == '\\') { + escaped = !escaped; + continue; + } + + else if(escaped && source[sourceOffset + localOffset] == '\r') + continue; + + else if(escaped && source[sourceOffset + localOffset] == '\n') + newlineOffsets ~= localOffset; + + else if (!escaped && source[sourceOffset + localOffset] == '\n') + break; + + escaped = false; + } + + result = current[0..localOffset].assumeUTF.split("\n").map!((line) { + line = line.stripRight; + if (line[$-1] == '\\') return line[0..$-1].stripLeft; + return line.stripLeft; + }).array.join(); + sourceOffset += localOffset; + } + + static if (Flags & INIFlags.TrimValues) + if (!isQuoted) + result = result.strip; + + static if (Flags & INIFlags.ProcessEscapes) + result = parseEscapeSequences(result); + + return Boxer(result); + } + + bool tryReadQuote(out string result) + { + int pairIndex; + + if (findPair!`quotes`(pairIndex)) { + auto quote = Format.quotes[pairIndex]; + sourceOffset += quote.opening.length; + + auto closeIndex = current.countUntil(quote.closing); + if (closeIndex == -1) + throw new INIException("Unterminated string literal"); + + result = current[0..closeIndex].assumeUTF; + sourceOffset += result.length + quote.closing.length; + + if (result.canFind('\n') && quote.multiline == false) + throw new INIException("Unterminated string literal which spans multiple lines (invalid quotes used?)"); + + return true; + } + + return false; + } + + void skipWhitespaces() + { + while (current.length && isWhite(current[0])) + sourceOffset += 1; + } + + private immutable(ubyte)[] current() @property { + return source[sourceOffset..$]; + } +} + + +/** + * Universal `INIReader` variant. + * + * Use this variant if you want to have more compatible parser. + * + * Specifics: + * - Uses `UniversalINIFormat`. + * - Trims section names, keys and values. + * - Processes escapes in values (e.g. `\n`). + */ +alias UniversalINIReader = INIReader!(UniversalINIFormat, INIFlags.TrimAll | INIFlags.ProcessEscapes, (string a) => a); + + +/** + * Strict `INIReader` variant. + * + * Use this variant if you want to have more strict (and bit faster) parser. + * + * Specifics: + * - Uses `StrictINIFormat` + * - Only Keys are trimmed. + * - No escape sequences are resolved. + */ +alias StrictINIReader = INIReader!(StrictINIFormat, INIFlags.TrimKeys, (string a) => a); + + +unittest { + auto source = ` +; comment + +multiline = """ + this is +""" + +numeric=-100000 +numeric2=09843 +[section (name)] +@=bar +`; + + + auto reader = UniversalINIReader(source); + alias Key = reader.KeyType; + + assert(reader.next()); + assert(reader.type == INIToken.COMMENT); + assert(reader.sectionName == " comment"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.key.name == "multiline"); + assert(reader.key.value == "\n this is\n"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "numeric"); + assert(reader.value.get!Key.value == "-100000"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "numeric2"); + assert(reader.value.get!Key.value == "09843"); + + assert(reader.next()); + assert(reader.type == INIToken.SECTION); + assert(reader.value.get!string == "section (name)"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "@"); + assert(reader.value.get!Key.value == `bar`); + + assert(!reader.next()); +} + + +unittest { + auto source = ` +####### TEST ######## + +numeric value=15 +ThisIsMultilineValue=thisis\ + verylong # comment +"Floating=Value"=1.51 + +[] # comment works +JustAKey +`; + + auto reader = UniversalINIReader(source); + alias Key = reader.KeyType; + + assert(reader.next()); + assert(reader.type == INIToken.COMMENT); + assert(reader.value.get!string == "###### TEST ########"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "numeric value"); + assert(reader.value.get!Key.value == `15`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "ThisIsMultilineValue"); + assert(reader.value.get!Key.value == `thisisverylong # comment`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "Floating=Value"); + assert(reader.value.get!Key.value == `1.51`); + + assert(reader.next()); + assert(reader.type == INIToken.SECTION); + assert(reader.value.get!string == ""); + + assert(reader.next()); + assert(reader.type == INIToken.COMMENT); + assert(reader.value.get!string == " comment works"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "JustAKey"); + assert(reader.value.get!Key.value == null); + + assert(!reader.next()); +} + +unittest { + string source = ` + [ Debug ] +sNumString=10Test +QuotedNum="10" +QuotedFloat="10.1" +Num=10 +Float=10.1 +`; + + auto reader = UniversalINIReader(source); + alias Key = reader.KeyType; + + assert(reader.next()); + assert(reader.type == INIToken.SECTION); + assert(reader.value.get!string == "Debug"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "sNumString"); + assert(reader.value.get!Key.value == `10Test`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "QuotedNum"); + assert(reader.value.get!Key.value == `10`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "QuotedFloat"); + assert(reader.value.get!Key.value == `10.1`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "Num"); + assert(reader.value.get!Key.value == "10"); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "Float"); + assert(reader.value.get!Key.value == "10.1"); + + assert(!reader.next()); +} + +unittest { + string source = ` + [ Debug ] +sNumString=10Test +QuotedNum="10" +QuotedFloat="10.1" +Num=10 +Float=10.1 +`; + + auto reader = StrictINIReader(source); + alias Key = reader.KeyType; + + assert(reader.next()); + assert(reader.type == INIToken.SECTION); + assert(reader.value.get!string == " Debug "); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "sNumString"); + assert(reader.value.get!Key.value == `10Test`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "QuotedNum"); + assert(reader.value.get!Key.value == `10`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "QuotedFloat"); + assert(reader.value.get!Key.value == `10.1`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "Num"); + assert(reader.value.get!Key.value == `10`); + + assert(reader.next()); + assert(reader.type == INIToken.KEY); + assert(reader.value.get!Key.name == "Float"); + assert(reader.value.get!Key.value == `10.1`); + + assert(!reader.next()); +} \ No newline at end of file diff --git a/import/dini/utils.d b/import/dini/utils.d new file mode 100644 index 0000000..9dc9cf2 --- /dev/null +++ b/import/dini/utils.d @@ -0,0 +1,66 @@ +module dini.utils; + +import std.format : format, formatElement, FormatSpec, FormatException, formattedRead; +import std.traits : arity, isCallable, Parameters, ReturnType; + + +enum bool isBoxer(alias boxer) = isCallable!boxer + && arity!boxer == 1 + && is(Parameters!boxer[0] == string); + +alias BoxerType(alias boxer) = ReturnType!boxer; + + +static char[char] escapeSequences; +static this() { + escapeSequences = [ + 'n': '\n', 'r': '\r', 't': '\t', 'b': '\b', '\\': '\\', + '#': '#', ';': ';', '=': '=', ':': ':', '"': '"', '\'': '\'' + ]; +} + +string parseEscapeSequences(string input) +{ + bool inEscape; + const(char)[] result = []; + result.reserve(input.length); + + for(auto i = 0; i < input.length; i++) { + char c = input[i]; + + if (inEscape) { + if (c in escapeSequences) + result ~= escapeSequences[c]; + else if (c == 'x') { + ubyte n; + if (i + 3 > input.length) + throw new FormatException("Invalid escape sequence (\\x)"); + string s = input[i+1..i+3]; + if (formattedRead(s, "%x", &n) < 1) + throw new FormatException("Invalid escape sequence (\\x)"); + result ~= cast(char)n; + i += 2; + } + else { + throw new FormatException("Invalid escape sequence (\\%s..)".format(c)); + } + } + else if (!inEscape && c == '\\') { + inEscape = true; + continue; + } + else result ~= c; + + inEscape = false; + } + + return cast(string)result; +} + +unittest { + assert(parseEscapeSequences("abc wef ' n r ;a") == "abc wef ' n r ;a"); + assert(parseEscapeSequences(`\\n \\\\\\\\\\r`) == `\n \\\\\r`); + assert(parseEscapeSequences(`hello\nworld`) == "hello\nworld"); + assert(parseEscapeSequences(`multi\r\nline \#notacomment`) == "multi\r\nline #notacomment"); + assert(parseEscapeSequences(`welp \x5A\x41\x7a`) == "welp ZAz"); +} diff --git a/main.d b/main.d new file mode 100644 index 0000000..71c7cdc --- /dev/null +++ b/main.d @@ -0,0 +1,263 @@ +/* + * esv: read the Bible from your terminal + * + * The GPLv2 License (GPLv2) + * Copyright (c) 2023 Jeremy Baxter + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import std.conv : to, ConvException; +import std.file : exists, write, FileException; +import std.getopt : getopt, GetOptException, config; +import std.path : baseName, expandTilde, isValidPath; +import std.process : environment, executeShell; +import std.regex : regex, matchFirst, replaceFirst; +import std.stdio : writef, writeln, writefln, stderr; +import std.string : splitLines; + +import esv; +import dini; + +enum VERSION = "0.1.0"; + +enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; +enum DEFAULT_PAGER = "less"; + +enum ENV_CONFIG = "ESV_CONFIG"; +enum ENV_PAGER = "ESV_PAGER"; + +bool optAudio; +string optConfigPath; +bool optFootnotes; +bool optNoFootnotes; +bool optHeadings; +bool optNoHeadings; +int optLineLength = 0; +bool optVerseNumbers; +bool optNoVerseNumbers; +bool optNoPager; +bool optPassageReferences; +bool optNoPassageReferences; +bool optVersion; + +int main(string[] args) +{ + void msg(string s) + { + stderr.writefln("%s: %s", args[0].baseName(), s); + } + + void panic(string s) + { + import core.runtime : Runtime; + import core.stdc.stdlib : exit; + + msg(s); + scope(exit) { + Runtime.terminate(); + exit(1); + } + } + + // Parse command-line options + try { + args.getopt( + config.bundling, + config.caseSensitive, + "a", &optAudio, + "C", &optConfigPath, + "F", &optNoFootnotes, + "f", &optFootnotes, + "H", &optNoHeadings, + "h", &optHeadings, + "l", &optLineLength, + "N", &optNoVerseNumbers, + "n", &optVerseNumbers, + "P", &optNoPager, + "R", &optNoPassageReferences, + "r", &optNoPassageReferences, + "V", &optVersion, + ); + } catch (GetOptException e) { + if (!e.msg.matchFirst(regex("^Unrecognized option")).empty) + panic("unknown option " ~ e.extractOpt()); + else if (!e.msg.matchFirst(regex("^Missing value for argument")).empty) + panic("missing argument for option " ~ e.extractOpt()); + } catch (ConvException e) + panic("value provided by option -l is not convertible to an integer value; must be a non-decimal number"); + + if (optVersion) { + writeln("esv version " ~ VERSION); + return 0; + } + + if (args.length < 3) { + stderr.writefln("usage: %s [-C config] [-l length] [-aFfHhNnPRrV] book verses", args[0].baseName()); + return 1; + } + + // Determine configuration file + // Options have first priority, then environment variables, then the default path + string configPath; + string configEnvVar = environment.get(ENV_CONFIG); + Ini iniData; + try { + if (optConfigPath != "") { + if (optConfigPath.isValidPath()) + configPath = optConfigPath.expandTilde(); + else + panic(optConfigPath ~ ": invalid file path"); + } else if (configEnvVar !is null) { + if (configEnvVar.isValidPath()) + configPath = configEnvVar.expandTilde(); + else + panic(configEnvVar ~ ": invalid file path"); + } else { + configPath = DEFAULT_CONFIGPATH.expandTilde(); + if (!configPath.exists()) { + configPath.write( +"# Default esv configuration file. + +# An API key is required to access the ESV Bible API. +[api] +#key = My API key here +# If you really need to, you can specify +# custom API parameters using `parameters`: +#parameters = &my-parameter=value + +# Some other settings that modify how the passages are displayed: +#[passage] +#footnotes = false +#headings = false +#passage_references = false +#verse_numbers = false +"); + } + } + iniData = Ini.Parse(configPath); + } catch (FileException e) { + // filesystem syscall errors + if (!e.msg.matchFirst(regex("^" ~ configPath ~ ": [Ii]s a directory")).empty || + !e.msg.matchFirst(regex("^" ~ configPath ~ ": [Nn]o such file or directory")).empty || + !e.msg.matchFirst(regex("^" ~ configPath ~ ": [Pp]ermission denied")).empty) + panic(e.msg); + } + string apiKey; + try apiKey = iniData["api"].getKey("key"); + catch (IniException e) + panic("API key not present in configuration file; cannot proceed"); + if (apiKey == "") + panic("API key not present in configuration file; cannot proceed"); + + // Initialise API object and validate the book and verse + EsvAPI esv = new EsvAPI(apiKey); + if (!esv.validateBook(args[1])) + panic("book '" ~ args[1] ~ "' does not exist"); + if (!esv.validateVerse(args[2])) + panic("invalid verse format '" ~ args[2] ~ "'"); + + if (optAudio) { + // check for mpg123 + if (executeShell("which mpg123 >/dev/null 2>&1").status > 0) { + panic("mpg123 is required for audio mode; cannot continue"); + return 1; + } else { + string tmpf = esv.getAudioVerses(args[1], args[2]); + // spawn mpg123 + executeShell("mpg123 -q " ~ tmpf); + return 0; + } + } + + esv.extraParameters = iniData["api"].getKey("parameters"); + + string returnValid(string def, string val) + { + if (val == "") + return def; + else + return val; + } + + // Get [passage] keys + foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) { + try { + esv.opts.boolOpts["include_" ~ key] = + returnValid("true", iniData["passage"].getKey(key)).catchConvException( + (ConvException ex, string str) + { + panic(configPath ~ ": value '" ~ str ~ + "' is not convertible to a boolean value; must be either 'true' or 'false'"); + } + ); + } catch (IniException e) {} // just do nothing; use the default settings + } + // Get line_length ([passage]) + try esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + catch (ConvException e) { + panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length") + ~ "' is not convertible to an integer value; must be a non-decimal number"); + } catch (IniException e) {} // just do nothing; use the default setting + + if (optNoFootnotes) esv.opts.boolOpts["include_footnotes"] = false; + if (optNoHeadings) esv.opts.boolOpts["include_headings"] = false; + if (optNoVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = false; + if (optNoPassageReferences) esv.opts.boolOpts["include_passage_references"] = false; + if (optLineLength != 0) esv.opts.intOpts ["line_length"] = optLineLength; + + string verses = esv.getVerses(args[1], args[2]); + int lines; + foreach (string line; verses.splitLines()) + ++lines; + + // If the passage is very long, pipe it into a pager + if (lines > 32 && !optNoPager) { + import std.process : pipeProcess, Redirect, wait, ProcessException; + string pager = environment.get(ENV_PAGER, DEFAULT_PAGER); + try { + auto pipe = pipeProcess(pager, Redirect.stdin); + pipe.stdin.writeln(verses); + pipe.stdin.flush(); + pipe.stdin.close(); + pipe.pid.wait(); + } + catch (ProcessException e) { + if (!e.msg.matchFirst(regex("^Executable file not found")).empty) { + panic(e.msg + .matchFirst(": (.+)$")[0] + .replaceFirst(regex("^: "), "") + ~ ": command not found" + ); + } + } + } else + writeln(verses); + + return 0; +} + +string extractOpt(GetOptException e) +{ + return e.msg.matchFirst("-.")[0]; +} + +bool catchConvException(string sb, void delegate(ConvException ex, string str) catchNet) +{ + try return sb.to!bool(); + catch (ConvException e) { + catchNet(e, sb); + return false; + } +}