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
This commit is contained in:
commit
a0341e72d7
14 changed files with 2882 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*.o
|
||||
*.so
|
||||
*.a
|
||||
esv
|
373
COPYING
Normal file
373
COPYING
Normal file
|
@ -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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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.
|
||||
|
||||
<signature of Ty Coon>, 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.
|
45
Makefile
Normal file
45
Makefile
Normal file
|
@ -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
|
79
README.md
Normal file
79
README.md
Normal file
|
@ -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.
|
93
esv.1
Normal file
93
esv.1
Normal file
|
@ -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
|
67
esv.conf.5
Normal file
67
esv.conf.5
Normal file
|
@ -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
|
369
esv.d
Normal file
369
esv.d
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
23
import/dini/LICENSE
Normal file
23
import/dini/LICENSE
Normal file
|
@ -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.
|
30
import/dini/README
Normal file
30
import/dini/README
Normal file
|
@ -0,0 +1,30 @@
|
|||
This code is a modified version of dini, found here:
|
||||
<https://github.com/robik/dini>
|
||||
My changes can be found here:
|
||||
<https://github.com/jtbx/dini>
|
||||
|
||||
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.
|
3
import/dini/package.d
Normal file
3
import/dini/package.d
Normal file
|
@ -0,0 +1,3 @@
|
|||
module dini;
|
||||
|
||||
public import dini.parser;
|
681
import/dini/parser.d
Normal file
681
import/dini/parser.d
Normal file
|
@ -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`);
|
||||
}
|
786
import/dini/reader.d
Normal file
786
import/dini/reader.d
Normal file
|
@ -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());
|
||||
}
|
66
import/dini/utils.d
Normal file
66
import/dini/utils.d
Normal file
|
@ -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");
|
||||
}
|
263
main.d
Normal file
263
main.d
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue