Compare commits
No commits in common. "master" and "esv-0.1.0" have entirely different histories.
25 changed files with 2397 additions and 1835 deletions
|
@ -1,10 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
indent_style = tab
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[*.{nix,yml}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,10 +1,4 @@
|
||||||
*.o
|
*.o
|
||||||
*.so
|
*.so
|
||||||
*.a
|
*.a
|
||||||
|
|
||||||
di/
|
|
||||||
esv
|
esv
|
||||||
esvsearch
|
|
||||||
result
|
|
||||||
|
|
||||||
config.mk
|
|
53
COPYING
53
COPYING
|
@ -1,35 +1,36 @@
|
||||||
The file initial.d is licensed under the Boost Software License,
|
All files except esv.d are licensed under the
|
||||||
Version 1.0. The contents of that license follows:
|
GNU General Public License, version 3.
|
||||||
|
|
||||||
Copyright (c) 2024 Jeremy Baxter. All rights reserved.
|
The file esv.d is licensed under the
|
||||||
|
BSD 3-Clause License, which is as follows:
|
||||||
|
|
||||||
Boost Software License - Version 1.0 - August 17th, 2003
|
The BSD 3-Clause License (BSD3)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person or organization
|
Copyright (c) 2023 Jeremy Baxter. All rights reserved.
|
||||||
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
|
Redistribution and use in source and binary forms, with or without
|
||||||
the above license grant, this restriction and the following disclaimer,
|
modification, are permitted provided that the following conditions are met:
|
||||||
must be included in all copies of the Software, in whole or in part, and
|
1. Redistributions of source code must retain the above copyright
|
||||||
all derivative works of the Software, unless such copies or derivative
|
notice, this list of conditions and the following disclaimer.
|
||||||
works are solely in the form of machine-executable object code generated by
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
a source language processor.
|
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.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
DEALINGS IN THE SOFTWARE.
|
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:
|
||||||
This software (esv) is licensed under the GNU General Public License,
|
|
||||||
version 2. The contents of that license follows:
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 2, June 1991
|
Version 2, June 1991
|
||||||
|
|
54
Makefile
54
Makefile
|
@ -1,33 +1,45 @@
|
||||||
|
### If you fail to build, run 'make deps'! ###
|
||||||
|
|
||||||
|
PROG = esv
|
||||||
IMPORT = import
|
IMPORT = import
|
||||||
PREFIX = /usr/local
|
PREFIX = /usr/local
|
||||||
MANPREFIX = ${PREFIX}/share/man
|
MANPREFIX = /usr/share/man
|
||||||
|
|
||||||
DC = ${_DC}
|
DC = ldc2
|
||||||
CFLAGS = ${_CFLAGS}
|
CFLAGS = -Os -I${IMPORT}
|
||||||
OBJS = ${_OBJS}
|
OBJS = main.o esv.o ini.o
|
||||||
|
|
||||||
all: esv esvsearch
|
ifeq (${DEBUG},)
|
||||||
|
CFLAGS += -release
|
||||||
|
endif
|
||||||
|
|
||||||
include config.mk
|
ifneq (${WI},)
|
||||||
|
CFLAGS += -wi
|
||||||
|
else
|
||||||
|
CFLAGS += -w
|
||||||
|
endif
|
||||||
|
|
||||||
esv: esv.o ${OBJS}
|
all: esv
|
||||||
${DC} ${_LDFLAGS} -of=$@ esv.o ${OBJS}
|
|
||||||
esvsearch: esvsearch.o ${OBJS}
|
|
||||||
${DC} ${_LDFLAGS} -of=$@ esvsearch.o ${OBJS}
|
|
||||||
|
|
||||||
.SUFFIXES: .d .o
|
esv: ${OBJS}
|
||||||
|
${DC} ${CFLAGS} -of=${PROG} ${OBJS}
|
||||||
|
|
||||||
.d.o:
|
# main executable
|
||||||
${DC} ${CFLAGS} -c $<
|
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:
|
clean:
|
||||||
rm -f esv esvsearch esv.o esvsearch.o ${OBJS}
|
rm -f ${PROG} ${OBJS}
|
||||||
|
|
||||||
install: esv esvsearch
|
install: esv
|
||||||
install -Dm755 esv ${DESTDIR}${PREFIX}/bin/esv
|
install -m755 esv ${DESTDIR}${PREFIX}/bin/esv
|
||||||
install -Dm755 esvsearch ${DESTDIR}${PREFIX}/bin/esvsearch
|
cp -f esv.1 ${DESTDIR}${MANPREFIX}/man1
|
||||||
install -Dm644 esv.1 ${DESTDIR}${MANPREFIX}/man1/esv.1
|
cp -f esv.conf.5 ${DESTDIR}${MANPREFIX}/man5
|
||||||
install -Dm644 esvsearch.1 ${DESTDIR}${MANPREFIX}/man1/esvsearch.1
|
|
||||||
install -Dm644 esv.conf.5 ${DESTDIR}${MANPREFIX}/man5/esv.conf.5
|
|
||||||
|
|
||||||
.PHONY: all clean install
|
.PHONY: clean install
|
||||||
|
|
13
README
13
README
|
@ -1,13 +0,0 @@
|
||||||
This is esv 0.3.0, a program that displays Bible passages on the terminal.
|
|
||||||
|
|
||||||
To build, first install the LDC compiler and libcurl.
|
|
||||||
Configure the build environment, compile and optionally install:
|
|
||||||
|
|
||||||
$ ./configure
|
|
||||||
$ make
|
|
||||||
# make install
|
|
||||||
|
|
||||||
Documentation can be found in the man page esv(1). A quick start guide
|
|
||||||
is provided at the project's website <https://reformers.dev/esv>.
|
|
||||||
|
|
||||||
Written by Jeremy Baxter <jeremy@baxters.nz>
|
|
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.
|
12
config.di
12
config.di
|
@ -1,12 +0,0 @@
|
||||||
module config;
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
enum esvVersion = "0.3.0";
|
|
||||||
|
|
||||||
enum apiKey = "abfb7456fa52ec4292c79e435890cfa3df14dc2b";
|
|
||||||
enum configPath = "~/.config/esv.conf";
|
|
||||||
enum mp3Player = "mpg123";
|
|
||||||
|
|
||||||
enum configEnv = "ESV_CONFIG";
|
|
||||||
enum playerEnv = "ESV_PLAYER";
|
|
165
configure
vendored
165
configure
vendored
|
@ -1,165 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
# simple and flexible configure script for people who don't like to waste time
|
|
||||||
# licensed to the public domain
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
mkf=config.mk
|
|
||||||
di='di'
|
|
||||||
imports="$di"
|
|
||||||
objs='esvapi.o util.o initial.o'
|
|
||||||
srcs='esvapi.d util.d initial.d'
|
|
||||||
|
|
||||||
# utility functions
|
|
||||||
|
|
||||||
present () {
|
|
||||||
command -v "$1" 1>/dev/null 2>/dev/null
|
|
||||||
}
|
|
||||||
throw () {
|
|
||||||
>&2 printf "%s: %s\n" "$(basename "$0")" "$1"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
using () {
|
|
||||||
>&2 printf "using %s\n" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# generators
|
|
||||||
|
|
||||||
## D compiler
|
|
||||||
gen_DC () {
|
|
||||||
if [ -n "$dc" ]; then
|
|
||||||
using "$dc"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if present ldc2; then
|
|
||||||
dc=ldc2
|
|
||||||
dcname="$dc"
|
|
||||||
elif present dmd; then
|
|
||||||
dc=dmd
|
|
||||||
dcname="$dc"
|
|
||||||
else
|
|
||||||
throw "D compiler not found; install LDC or DMD"
|
|
||||||
fi
|
|
||||||
|
|
||||||
using "$dc"
|
|
||||||
}
|
|
||||||
|
|
||||||
## flags used in the compilation step
|
|
||||||
gen_CFLAGS () {
|
|
||||||
if [ -z "$debug" ]; then
|
|
||||||
case "$dcname" in
|
|
||||||
ldc2) cflags="-Oz";;
|
|
||||||
dmd) cflags="-O";;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
cflags="-g"
|
|
||||||
case "$dcname" in
|
|
||||||
ldc2) cflags="$cflags -O0 -d-debug";;
|
|
||||||
dmd) cflags="$cflags -debug";;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
[ -n "$unittest" ] && cflags="$cflags -unittest -main"
|
|
||||||
|
|
||||||
for flag in $cflags; do
|
|
||||||
using "$flag"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
## flags used in the linking step
|
|
||||||
gen_LDFLAGS () {
|
|
||||||
if [ "$dcname" = ldc2 ]; then
|
|
||||||
if present ld.lld; then
|
|
||||||
ldflags="-linker=lld"
|
|
||||||
elif present ld.gold; then
|
|
||||||
ldflags="-linker=gold"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -z "$debug" ]; then
|
|
||||||
if [ -n "$ldflags" ]; then ldflags="$ldflags "; fi
|
|
||||||
ldflags="$ldflags-L--gc-sections"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for flag in $ldflags; do
|
|
||||||
using "$flag"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# command line interface
|
|
||||||
|
|
||||||
while getopts c:dhrt ch; do
|
|
||||||
case "$ch" in
|
|
||||||
c)
|
|
||||||
dcname="$(basename "$OPTARG")"
|
|
||||||
case "$dcname" in
|
|
||||||
ldc2) dc="$OPTARG" ;;
|
|
||||||
dmd) dc="$OPTARG" ;;
|
|
||||||
*) throw "unknown D compiler '$dcname' specified (valid options: ldc2, dmd)" ;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
d) debug=1 ;;
|
|
||||||
r) unset debug ;;
|
|
||||||
t) unittest=1; debug=1 ;;
|
|
||||||
h)
|
|
||||||
cat <<EOF
|
|
||||||
configure: create an optimised makefile for the current environment
|
|
||||||
|
|
||||||
options:
|
|
||||||
-c: force use of a particular compiler (dmd or ldc2)
|
|
||||||
-d: build in debug mode, with debug symbols and statements enabled
|
|
||||||
-r: build in release mode with optimisation flags enabled (default)
|
|
||||||
-t: build an executable to run unit tests (implies -d)
|
|
||||||
-h: show this help message
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
'?') exit 1 ;;
|
|
||||||
:) exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# creating the makefile
|
|
||||||
|
|
||||||
gen_DC
|
|
||||||
gen_CFLAGS
|
|
||||||
gen_LDFLAGS
|
|
||||||
|
|
||||||
for directory in $imports; do
|
|
||||||
mkdir -p "$directory"
|
|
||||||
Iflags="$(printf '%s -I%s' "$Iflags" "$directory" | xargs)"
|
|
||||||
done
|
|
||||||
|
|
||||||
for src in $srcs; do
|
|
||||||
! (echo "$src" | grep -Eq '\.d$') \
|
|
||||||
&& throw "$src: invalid source file extension"
|
|
||||||
[ -t 2 ] && printf ' (DI) %s \r' "$src" 1>&2
|
|
||||||
"$dc" -o- -op -H -Hd="$di" "$src"
|
|
||||||
done
|
|
||||||
|
|
||||||
rm -f "$mkf"
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '# begin generated definitions
|
|
||||||
_DC = %s
|
|
||||||
_CFLAGS = %s
|
|
||||||
_LDFLAGS = %s
|
|
||||||
' "$dc" "$(echo "$cflags $Iflags" | xargs)" "$ldflags"
|
|
||||||
|
|
||||||
## generate obj list
|
|
||||||
printf '_OBJS ='
|
|
||||||
for obj in $objs; do
|
|
||||||
printf ' %s' "$obj"
|
|
||||||
done
|
|
||||||
printf '\n# end generated definitions\n'
|
|
||||||
|
|
||||||
## generate dependency list
|
|
||||||
printf '\n# begin generated dependencies\n'
|
|
||||||
i=1
|
|
||||||
for obj in $objs; do
|
|
||||||
src="$(printf '%s' "$srcs" | awk '{print $'"$i"'}')"
|
|
||||||
[ -t 2 ] && printf ' (MK) %s \r' "$src" 1>&2
|
|
||||||
"$dc" -o- -makedeps "$src"
|
|
||||||
i="$((i + 1))"
|
|
||||||
done
|
|
||||||
unset i
|
|
||||||
printf '# end generated dependencies\n'
|
|
||||||
} >>"$mkf"
|
|
134
esv.1
134
esv.1
|
@ -1,57 +1,40 @@
|
||||||
.Dd $Mdocdate: May 01 2025 $
|
.Dd $Mdocdate: March 23 2023 $
|
||||||
.Dt ESV 1
|
.Dt ESV 1
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
.Nm esv
|
.Nm esv
|
||||||
.Nd read the Bible
|
.Nd read the Bible from your terminal
|
||||||
.Sh SYNOPSIS
|
.Sh SYNOPSIS
|
||||||
.Nm esv
|
.Nm esv
|
||||||
.Bk -words
|
.Bk -words
|
||||||
.Op Fl aFfHhNnRrV
|
.Op Fl C Ar config
|
||||||
.Op Fl c Ar config
|
|
||||||
.Op Fl l Ar length
|
.Op Fl l Ar length
|
||||||
|
.Op Fl aFfHhNnRrV
|
||||||
.Ar book verses
|
.Ar book verses
|
||||||
.Ek
|
.Ek
|
||||||
.Sh DESCRIPTION
|
.Sh DESCRIPTION
|
||||||
.Nm
|
.Nm
|
||||||
displays Bible passages on your terminal.
|
is a program that displays passages of the Bible on your terminal.
|
||||||
It can also play audio passages.
|
It can also play recorded audio tracks of certain passages,
|
||||||
.Pp
|
through integration with the
|
||||||
See the section
|
|
||||||
.Sx EXAMPLES
|
|
||||||
below for some basic usage examples.
|
|
||||||
Verses can be provided in the format of
|
|
||||||
.Em chapter ,
|
|
||||||
.Em chapter-chapter ,
|
|
||||||
.Em chapter:verse ,
|
|
||||||
and
|
|
||||||
.Em chapter:verse-verse .
|
|
||||||
If the name of your desired book has spaces in it, e.g.
|
|
||||||
.Dq "1 Corinthians" ,
|
|
||||||
you can provide the book name with hyphens or underscores in place of
|
|
||||||
the spaces, or you can pass the original book name.
|
|
||||||
Thus, both
|
|
||||||
.Dq 1-Corinthians
|
|
||||||
and
|
|
||||||
.Dq 1_Corinthians
|
|
||||||
are also valid book names.
|
|
||||||
.Pp
|
|
||||||
By default,
|
|
||||||
.Xr mpg123 1
|
.Xr mpg123 1
|
||||||
is used as the player for audio passages.
|
utility.
|
||||||
This can be overridden however;
|
If a text passage is too long for standard display on a terminal,
|
||||||
see the
|
.Nm
|
||||||
.Sx ENVIRONMENT
|
will put it through a text pager (default less) in order for you to be able to
|
||||||
section for more information.
|
scroll through the text with ease. This behaviour can be disabled by passing
|
||||||
|
the
|
||||||
|
.Fl P
|
||||||
|
flag.
|
||||||
.Pp
|
.Pp
|
||||||
The options are as follows:
|
The options are as follows:
|
||||||
.Bl -tag -width 123456
|
.Bl -tag -width keyword
|
||||||
.It Fl a
|
.It Fl a
|
||||||
Play an audio passage instead of printing a text passage.
|
Instead of displaying text passages, play a recorded audio track.
|
||||||
.It Fl c Ar config
|
.It Fl C Ar config
|
||||||
Read the configuration from the path
|
Use
|
||||||
.Ar config .
|
.Ar config
|
||||||
This overrides the
|
as the configuration file path. This overrides the
|
||||||
.Ev ESV_CONFIG
|
.Ev ESV_CONFIG
|
||||||
environment variable (see section
|
environment variable (see section
|
||||||
.Sx ENVIRONMENT ) .
|
.Sx ENVIRONMENT ) .
|
||||||
|
@ -64,88 +47,47 @@ Exclude headings.
|
||||||
.It Fl h
|
.It Fl h
|
||||||
Include headings (the default).
|
Include headings (the default).
|
||||||
.It Fl l Ar length
|
.It Fl l Ar length
|
||||||
Limit the length of lines in passages at
|
Use
|
||||||
.Ar length
|
.Ar length
|
||||||
characters. If
|
as the maximum line length.
|
||||||
.Ar length
|
|
||||||
is 0, do not set a limit on line length.
|
|
||||||
.Pp
|
|
||||||
If this option is not given,
|
|
||||||
the line length limit will fall back on the value provided in
|
|
||||||
the configuration file;
|
|
||||||
see
|
|
||||||
.Xr esv.conf 5 .
|
|
||||||
.Pp
|
|
||||||
If there is no value present in the configuration file,
|
|
||||||
.Nm
|
|
||||||
will get the width of the terminal,
|
|
||||||
and will use this as the line length limit,
|
|
||||||
unless the terminal's width is greater than 78,
|
|
||||||
in which case the line length limit will be set to 78.
|
|
||||||
This heuristic exists for readability purposes,
|
|
||||||
and you can always override it by specifying your preference in
|
|
||||||
.Xr esv.conf 5 .
|
|
||||||
.It Fl N
|
.It Fl N
|
||||||
Exclude verse numbers.
|
Exclude verse numbers.
|
||||||
.It Fl n
|
.It Fl n
|
||||||
Include verse numbers (the default).
|
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
|
.It Fl R
|
||||||
Exclude passage references.
|
Exclude passage references.
|
||||||
.It Fl r
|
.It Fl r
|
||||||
Include passage references (the default).
|
Include passage references (the default).
|
||||||
.It Fl V
|
.It Fl V
|
||||||
Print the version number and exit.
|
Print the version number and exit.
|
||||||
.El
|
|
||||||
.Pp
|
|
||||||
The options
|
|
||||||
.Fl FfHhlNnRr
|
|
||||||
only apply when reading text passages,
|
|
||||||
that is, when
|
|
||||||
.Fl a
|
|
||||||
or
|
|
||||||
.Fl s
|
|
||||||
is not given.
|
|
||||||
.Sh ENVIRONMENT
|
.Sh ENVIRONMENT
|
||||||
.Bl -tag -width ESV_CONFIG
|
.Bl -tag -width ESV_CONFIG
|
||||||
.It Ev ESV_CONFIG
|
.It Ev ESV_CONFIG
|
||||||
Where to read the configuration file,
|
Where to read the configuration file, rather than using the default location (see section
|
||||||
rather than using the default location (see section
|
|
||||||
.Sx FILES ) .
|
.Sx FILES ) .
|
||||||
.It Ev ESV_PLAYER
|
.It Ev ESV_PAGER
|
||||||
The name of the audio player to use when playing audio passages.
|
What pager to use when the passage is over 32 lines long, rather than using
|
||||||
The program specified must support playing MP3 audio.
|
the
|
||||||
If this is not set,
|
.Ic less
|
||||||
.Nm
|
utility.
|
||||||
will look for
|
|
||||||
.Xr mpg123 1
|
|
||||||
and start it.
|
|
||||||
.El
|
|
||||||
.Sh FILES
|
.Sh FILES
|
||||||
.Bl -tag -width ~/.config/esv.conf
|
.Bl -tag -width ~/.config/esv.conf
|
||||||
.It Pa ~/.config/esv.conf
|
.It Pa ~/.config/esv.conf
|
||||||
default configuration file location
|
default configuration file location
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Sh EXAMPLES
|
.Sh EXAMPLES
|
||||||
Read John 1:29-31:
|
Read Psalm 23:
|
||||||
.Pp
|
.Pp
|
||||||
.Dl esv John 1:29-31
|
.Dl esv Psalm 23
|
||||||
.Pp
|
.Pp
|
||||||
Listen to a recorded audio track of Psalm 128:
|
Listen to a recorded audio track of Matthew 5-7:
|
||||||
.Pp
|
.Pp
|
||||||
.Dl esv -a Psalm 139
|
.Dl esv -a Matthew 5-7
|
||||||
.Pp
|
.Pp
|
||||||
|
|
||||||
.Sh SEE ALSO
|
.Sh SEE ALSO
|
||||||
.Xr esvsearch 1
|
|
||||||
.Xr esv.conf 5
|
.Xr esv.conf 5
|
||||||
.Sh AUTHORS
|
|
||||||
.An Jeremy Baxter Aq Mt jeremy@baxters.nz
|
|
||||||
.Pp
|
|
||||||
Part of the
|
|
||||||
.Sy esv
|
|
||||||
distribution found at
|
|
||||||
.Lk https://reformers.dev/esv
|
|
||||||
.Sh BUGS
|
|
||||||
Currently there are no known bugs in
|
|
||||||
.Nm .
|
|
||||||
If you think you've found a potential bug,
|
|
||||||
please report it to my email address above.
|
|
||||||
|
|
54
esv.conf.5
54
esv.conf.5
|
@ -1,4 +1,4 @@
|
||||||
.Dd $Mdocdate: May 01 2025 $
|
.Dd $Mdocdate: March 23 2023 $
|
||||||
.Dt ESV.CONF 5
|
.Dt ESV.CONF 5
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
|
@ -7,20 +7,16 @@
|
||||||
.Sh DESCRIPTION
|
.Sh DESCRIPTION
|
||||||
The
|
The
|
||||||
.Xr esv 1
|
.Xr esv 1
|
||||||
program uses a configuration file to customize its behaviour.
|
utility uses a configuration file to customize its behaviour.
|
||||||
This file uses a basic plain text format,
|
This file uses the standard Unix configuration file format, with
|
||||||
with section-based key-value pairs.
|
section-based key-value pairs. An example is listed below:
|
||||||
An example is listed below:
|
|
||||||
.Pp
|
.Pp
|
||||||
.Dl [section]
|
.Dl [section]
|
||||||
.Dl key = value
|
.Dl key = value
|
||||||
.Pp
|
.Pp
|
||||||
A line beginning with a hashtag
|
Comments can be used by putting a pound
|
||||||
.Dq #
|
.Dq #
|
||||||
will be considered a
|
symbol at the beginning of a line.
|
||||||
.Dq comment
|
|
||||||
and will be ignored by
|
|
||||||
.Xr esv 1 .
|
|
||||||
.Pp
|
.Pp
|
||||||
The available configuration options are as follows:
|
The available configuration options are as follows:
|
||||||
.Bl -tag -width keyword
|
.Bl -tag -width keyword
|
||||||
|
@ -30,29 +26,18 @@ The
|
||||||
section contains settings that modify the way passages are displayed.
|
section contains settings that modify the way passages are displayed.
|
||||||
.Bl -tag -width keyword
|
.Bl -tag -width keyword
|
||||||
.It Em footnotes
|
.It Em footnotes
|
||||||
True/false value that determines whether
|
Boolean value that determines whether or not footnotes are displayed
|
||||||
footnotes are displayed under the text.
|
under the text.
|
||||||
.It Em headings
|
.It Em headings
|
||||||
True/false value that determines whether headings are displayed.
|
Boolean value that determines whether or not headings are displayed.
|
||||||
.It Em passage-references
|
.It Em passage_references
|
||||||
True/false value that determines whether
|
Boolean value that determines whether or not passage references are
|
||||||
passage references are displayed before the text.
|
displayed before the text.
|
||||||
.It Em verse-numbers
|
.It Em verse_numbers
|
||||||
True/false value that determines whether verse numbers are displayed
|
Boolean value that determines whether or not verse numbers are displayed.
|
||||||
in the text.
|
.It Em line_length
|
||||||
.It Em line-length
|
Integer value that determines the maximum length for each line of
|
||||||
Integer value that determines the line length limit.
|
the passage.
|
||||||
This is related to esv's
|
|
||||||
.Fl l
|
|
||||||
option;
|
|
||||||
it can be specified as 0 for unlimited line lengths,
|
|
||||||
or it can be left out entirely to choose an appropriate value
|
|
||||||
based on your terminal's width.
|
|
||||||
See the documentation for
|
|
||||||
.Fl l
|
|
||||||
in
|
|
||||||
.Xr esv 1
|
|
||||||
for a thorough description of this heuristic.
|
|
||||||
.El
|
.El
|
||||||
.It Sy [api]
|
.It Sy [api]
|
||||||
The
|
The
|
||||||
|
@ -64,18 +49,19 @@ passed to the ESV Bible API.
|
||||||
Your API key, available from
|
Your API key, available from
|
||||||
.Lk http://api.esv.org
|
.Lk http://api.esv.org
|
||||||
.Pp
|
.Pp
|
||||||
This key is required,
|
This key is required, and is automatically filled in.
|
||||||
and is automatically filled in since esv 0.2.0.
|
|
||||||
.It Em parameters
|
.It Em parameters
|
||||||
Optional HTTP parameters passed to the API.
|
Optional HTTP parameters passed to the API.
|
||||||
If you are using this, make sure it starts with an ampersand symbol
|
If you are using this, make sure it starts with an ampersand symbol
|
||||||
.Dq & .
|
.Dq & .
|
||||||
.El
|
.El
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Sh FILES
|
.Sh FILES
|
||||||
.Bl -tag -width ~/.config/esv.conf
|
.Bl -tag -width ~/.config/esv.conf
|
||||||
.It Pa ~/.config/esv.conf
|
.It Pa ~/.config/esv.conf
|
||||||
default configuration file location
|
default configuration file location
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Sh SEE ALSO
|
.Sh SEE ALSO
|
||||||
.Xr esv 1
|
.Xr esv 1
|
||||||
|
|
532
esv.d
532
esv.d
|
@ -1,209 +1,369 @@
|
||||||
/*
|
/*
|
||||||
* esv: read the Bible from your terminal
|
* esv.d: a reusable interface to the ESV HTTP API
|
||||||
|
* licensed under the BSD 3-Clause License:
|
||||||
*
|
*
|
||||||
* The GPLv2 License (GPLv2)
|
* The BSD 3-Clause License (BSD3)
|
||||||
* Copyright (c) 2023-2025 Jeremy Baxter
|
|
||||||
*
|
*
|
||||||
* esv is free software: you can redistribute it and/or modify
|
* Copyright (c) 2023 Jeremy Baxter. All rights reserved.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* esv is distributed in the hope that it will be useful,
|
* Redistribution and use in source and binary forms, with or without
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* modification, are permitted provided that the following conditions are met:
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* 1. Redistributions of source code must retain the above copyright
|
||||||
* GNU General Public License for more details.
|
* 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.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
|
||||||
* along with esv. If not, see <http://www.gnu.org/licenses/>.
|
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module esv;
|
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;
|
||||||
|
|
||||||
import std.conv : to, ConvException;
|
const enum ESVAPI_URL = "https://api.esv.org/v3/passage";
|
||||||
import std.file : exists, mkdirRecurse, write, FileException;
|
const string[] BIBLE_BOOKS = [
|
||||||
import std.format : format;
|
// Old Testament
|
||||||
import std.getopt : getopt, GetOptException;
|
"Genesis",
|
||||||
import std.path : baseName, dirName, expandTilde;
|
"Exodus",
|
||||||
import std.process : environment, executeShell;
|
"Leviticus",
|
||||||
import std.stdio : writeln, writefln;
|
"Numbers",
|
||||||
import std.string : splitLines;
|
"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"
|
||||||
|
];
|
||||||
|
|
||||||
import esvapi;
|
class EsvAPI
|
||||||
import initial;
|
|
||||||
import util;
|
|
||||||
|
|
||||||
import cf = config;
|
|
||||||
|
|
||||||
@safe:
|
|
||||||
|
|
||||||
bool aFlag; /* audio */
|
|
||||||
string cFlag; /* config path */
|
|
||||||
bool fFlag, FFlag; /* footnotes */
|
|
||||||
bool hFlag, HFlag; /* headings */
|
|
||||||
int lFlag; /* line length */
|
|
||||||
bool lFlagSpecified;
|
|
||||||
bool nFlag, NFlag; /* verse numbers */
|
|
||||||
bool rFlag, RFlag; /* passage references */
|
|
||||||
bool VFlag; /* show version */
|
|
||||||
|
|
||||||
int
|
|
||||||
main(string[] args)
|
|
||||||
{
|
{
|
||||||
INIUnit ini;
|
private string _key;
|
||||||
ESVApi esv;
|
private string _url;
|
||||||
string apiKey;
|
private string _mode;
|
||||||
string configPath;
|
EsvAPIOptions opts;
|
||||||
|
string extraParameters;
|
||||||
sharedInit(args);
|
int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress;
|
||||||
|
string tmpDir;
|
||||||
cFlag = null;
|
this(const string key)
|
||||||
|
{
|
||||||
/* Parse command-line options */
|
this._url = ESVAPI_URL;
|
||||||
try {
|
this._key = key;
|
||||||
import std.getopt : config;
|
this._mode = "text";
|
||||||
getopt(args,
|
this.opts.setDefaults();
|
||||||
config.bundling,
|
this.extraParameters = "";
|
||||||
config.caseSensitive,
|
this.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {return 0;};
|
||||||
"a", &aFlag,
|
this.tmpDir = tempDir() ~ "esvapi";
|
||||||
"c", &cFlag,
|
|
||||||
"F", &FFlag, "f", &fFlag,
|
|
||||||
"H", &HFlag, "h", &hFlag,
|
|
||||||
"l", &onLineLength,
|
|
||||||
"N", &NFlag, "n", &nFlag,
|
|
||||||
"R", &RFlag, "r", &rFlag,
|
|
||||||
"V", &VFlag,
|
|
||||||
);
|
|
||||||
} catch (GetOptException e) {
|
|
||||||
handleOptError(e.msg);
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
if (VFlag) {
|
* Returns the API URL currently in use.
|
||||||
writeln("esv " ~ cf.esvVersion);
|
*/
|
||||||
return 0;
|
final string getURL() const nothrow
|
||||||
|
{
|
||||||
|
return _url;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
if (args.length < 3) {
|
* If the url argument is a valid HTTP URL, sets the API URL currently in use
|
||||||
stderr.writefln(
|
* to the given url argument. Otherwise, throws a UrlException.
|
||||||
"usage: %s [-aFfHhNnRrV] [-c config] [-l length] book verses",
|
*/
|
||||||
baseName(args[0]));
|
final void setURL(const string url)
|
||||||
return 1;
|
{
|
||||||
|
auto matches = url.matchAll("^https?://.+\\..+(/.+)?");
|
||||||
|
if (matches.empty)
|
||||||
|
throw new UrlException("Invalid URL format");
|
||||||
|
else
|
||||||
|
this._url = url;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
enforceDie(bookValid(parseBook(args[1])),
|
* Returns the API authentication key that was given when the API object was instantiated.
|
||||||
"book '%s' does not exist", args[1]);
|
* This authentication key cannot be changed after instantiation.
|
||||||
enforceDie(verseValid(args[2]),
|
*/
|
||||||
"invalid verse format '%s'", args[2]);
|
final string getKey() const nothrow
|
||||||
|
{
|
||||||
/* determine configuration file: options take first priority,
|
return _key;
|
||||||
* then environment variables, and then the default path */
|
}
|
||||||
config:
|
/*
|
||||||
configPath = environment.get(cf.configEnv, cf.configPath)
|
* Returns the API authentication key currently in use.
|
||||||
.expandTilde();
|
*/
|
||||||
try {
|
final string getMode() const nothrow
|
||||||
if (cFlag)
|
{
|
||||||
configPath = cFlag.expandTilde();
|
return _mode;
|
||||||
|
}
|
||||||
if (!configPath.exists()) {
|
/*
|
||||||
mkdirRecurse(dirName(configPath));
|
* If the mode argument is either "text" or "html",
|
||||||
configPath.write(format!
|
* sets the text API mode to the given mode argument.
|
||||||
"## Configuration file for esv.
|
* If the mode argument is not one of those,
|
||||||
|
* then this function will do nothing.
|
||||||
# An API key is required to access the ESV Bible API.
|
*/
|
||||||
[api]
|
final void setMode(const string mode) nothrow
|
||||||
key = %s
|
{
|
||||||
|
foreach (string m; ["text", "html"] )
|
||||||
# Settings that modify how passages are displayed:
|
{
|
||||||
[passage]
|
if (mode == m)
|
||||||
#footnotes = false
|
{
|
||||||
#headings = false
|
this._mode = mode;
|
||||||
#passage-references = false
|
return;
|
||||||
#verse-numbers = false
|
}
|
||||||
"(cf.apiKey));
|
|
||||||
}
|
}
|
||||||
readINIFile(ini, configPath);
|
|
||||||
} catch (FileException e) {
|
|
||||||
/* filesystem syscall errors */
|
|
||||||
die(e.msg);
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
apiKey = ini["api"].key("key");
|
* Returns true if the argument book is a valid book of the Bible.
|
||||||
enforceDie(apiKey != null,
|
* Otherwise, returns false.
|
||||||
"API key not present in configuration file; cannot proceed");
|
*/
|
||||||
|
final bool validateBook(const string book) const nothrow
|
||||||
esv = ESVApi(apiKey);
|
{
|
||||||
|
foreach (string b; BIBLE_BOOKS)
|
||||||
if (aFlag) {
|
{
|
||||||
string tmpf, player;
|
if (book.capitalize() == b.capitalize())
|
||||||
|
return true;
|
||||||
try
|
}
|
||||||
tmpf = esv.getAudioPassage(parseBook(args[1]), args[2]);
|
return false;
|
||||||
catch (CurlException e)
|
|
||||||
die(e.msg);
|
|
||||||
player = environment.get(cf.playerEnv, cf.mp3Player);
|
|
||||||
|
|
||||||
/* check for an audio player */
|
|
||||||
enforceDie(
|
|
||||||
executeShell(
|
|
||||||
format!"command -v %s >/dev/null 2>&1"(player)
|
|
||||||
).status == 0,
|
|
||||||
player ~ " is required for audio mode; cannot continue");
|
|
||||||
|
|
||||||
/* esv has built-in support for mpg123 and mpv.
|
|
||||||
* Other players will work, just set ESV_PLAYER */
|
|
||||||
player ~=
|
|
||||||
player == "mpg123" ? " -q " :
|
|
||||||
player == "mpv" ? " --msg-level=all=no " : " ";
|
|
||||||
/* spawn the player */
|
|
||||||
executeShell(player ~ tmpf);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
esv.extraParameters = ini["api"].key("parameters", "");
|
* Returns true if the argument book is a valid verse format.
|
||||||
|
* Otherwise, returns false.
|
||||||
/* Get [passage] keys */
|
*/
|
||||||
foreach (string key; [
|
final bool validateVerse(const string verse) const
|
||||||
"footnotes",
|
{
|
||||||
"headings",
|
bool attemptRegex(const string re) const
|
||||||
"passage-references",
|
{
|
||||||
"verse-numbers"
|
auto matches = verse.matchAll(re);
|
||||||
]) {
|
return !matches.empty;
|
||||||
try
|
}
|
||||||
esv.opts.b["include-" ~ key] =
|
if (attemptRegex("^\\d{1,3}$") ||
|
||||||
ini["passage"].keyAs!bool(key, true);
|
attemptRegex("^\\d{1,3}-\\d{1,3}$") ||
|
||||||
catch (INITypeException e)
|
attemptRegex("^\\d{1,3}:\\d{1,3}$") ||
|
||||||
die(configPath ~ ": " ~ e.msg);
|
attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Get line-length ([passage]) */
|
/*
|
||||||
try
|
* Requests the verse(s) from the API and returns it.
|
||||||
esv.opts.i["line-length"] =
|
* The (case-insensitive) name of the book being searched are
|
||||||
lFlagSpecified ? lFlag :
|
* contained in the argument book. The verse(s) being looked up are
|
||||||
ini["passage"].keyAs!int("line-length", terminalColumns());
|
* contained in the argument verses.
|
||||||
catch (INITypeException e)
|
*
|
||||||
die(configPath ~ ": " ~ e.msg);
|
* 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");
|
||||||
|
|
||||||
if (fFlag) esv.opts.b["include-footnotes"] = true;
|
string apiURL = format!"%s/%s/?q=%s+%s%s%s"(this._url, this._mode,
|
||||||
if (hFlag) esv.opts.b["include-headings"] = true;
|
book.capitalize().replaceAll(regex("_"), "+"), verse, this.assembleParameters(), this.extraParameters);
|
||||||
if (nFlag) esv.opts.b["include-verse-numbers"] = true;
|
auto request = HTTP(apiURL);
|
||||||
if (rFlag) esv.opts.b["include-passage-references"] = true;
|
string response;
|
||||||
if (FFlag) esv.opts.b["include-footnotes"] = false;
|
request.onProgress = this.onProgress;
|
||||||
if (HFlag) esv.opts.b["include-headings"] = false;
|
request.onReceive = (ubyte[] data)
|
||||||
if (NFlag) esv.opts.b["include-verse-numbers"] = false;
|
{
|
||||||
if (RFlag) esv.opts.b["include-passage-references"] = false;
|
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");
|
||||||
|
|
||||||
try
|
string apiURL = format!"%s/audio/?q=%s+%s"(this._url, book.capitalize().replaceAll(regex("_"), "+"), verse);
|
||||||
writeln(esv.getPassage(parseBook(args[1]), args[2]));
|
auto request = HTTP(apiURL);
|
||||||
catch (CurlException e)
|
ubyte[] response;
|
||||||
die(e.msg);
|
request.onProgress = this.onProgress;
|
||||||
return 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void
|
struct EsvAPIOptions
|
||||||
onLineLength(string flag, string value)
|
|
||||||
{
|
{
|
||||||
lFlagSpecified = true;
|
bool[string] boolOpts;
|
||||||
try
|
int[string] intOpts;
|
||||||
lFlag = value.to!int();
|
string indent_using;
|
||||||
catch (ConvException e)
|
void setDefaults() nothrow
|
||||||
die("illegal argument to -l option -- must be a positive integer");
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
423
esvapi.d
423
esvapi.d
|
@ -1,423 +0,0 @@
|
||||||
/*
|
|
||||||
* esvapi.d: a reusable interface to the ESV HTTP API
|
|
||||||
*
|
|
||||||
* The GPLv2 License (GPLv2)
|
|
||||||
* Copyright (c) 2023-2025 Jeremy Baxter
|
|
||||||
*
|
|
||||||
* esv 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.
|
|
||||||
*
|
|
||||||
* esv 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 esv. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module esvapi;
|
|
||||||
|
|
||||||
import std.conv : to;
|
|
||||||
import std.exception : basicExceptionCtors, enforce;
|
|
||||||
import std.file : tempDir, write;
|
|
||||||
import std.format : format;
|
|
||||||
import std.json : JSONValue, parseJSON;
|
|
||||||
import std.regex : regex, matchAll;
|
|
||||||
import std.stdio : File;
|
|
||||||
import std.string : capitalize, tr, wrap;
|
|
||||||
import std.net.curl : HTTP;
|
|
||||||
|
|
||||||
public import std.net.curl : CurlException;
|
|
||||||
|
|
||||||
@safe:
|
|
||||||
|
|
||||||
/++ Indentation style to use when formatting passages. +/
|
|
||||||
enum ESVIndent
|
|
||||||
{
|
|
||||||
SPACE,
|
|
||||||
TAB
|
|
||||||
}
|
|
||||||
|
|
||||||
/++ Constant array of all books in the Bible. +/
|
|
||||||
immutable string[] bibleBooks = [
|
|
||||||
/* 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",
|
|
||||||
"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"
|
|
||||||
];
|
|
||||||
|
|
||||||
/++ All allowed API parameters (for text passages). +/
|
|
||||||
immutable string[] esvapiParameters = [
|
|
||||||
"include-passage-references",
|
|
||||||
"include-verse-numbers",
|
|
||||||
"include-first-verse-numbers",
|
|
||||||
"include-footnotes",
|
|
||||||
"include-footnote-body",
|
|
||||||
"include-headings",
|
|
||||||
"include-short-copyright",
|
|
||||||
"include-copyright",
|
|
||||||
"include-passage-horizontal-lines",
|
|
||||||
"include-heading-horizontal-lines",
|
|
||||||
"include-selahs",
|
|
||||||
"indent-poetry",
|
|
||||||
"horizontal-line-length",
|
|
||||||
"indent-paragraphs",
|
|
||||||
"indent-poetry-lines",
|
|
||||||
"indent-declares",
|
|
||||||
"indent-psalm-doxology",
|
|
||||||
"line-length",
|
|
||||||
"indent-using",
|
|
||||||
];
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Returns true if the argument book is a valid book of the Bible.
|
|
||||||
+ Otherwise, returns false.
|
|
||||||
+/
|
|
||||||
bool
|
|
||||||
bookValid(in char[] book) nothrow
|
|
||||||
{
|
|
||||||
foreach (string b; bibleBooks) {
|
|
||||||
if (book.capitalize() == b.capitalize())
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Returns true if the argument verse is a valid verse format.
|
|
||||||
+ Otherwise, returns false.
|
|
||||||
+/
|
|
||||||
bool
|
|
||||||
verseValid(in char[] verse)
|
|
||||||
{
|
|
||||||
foreach (string re; [
|
|
||||||
"^\\d{1,3}$",
|
|
||||||
"^\\d{1,3}-\\d{1,3}$",
|
|
||||||
"^\\d{1,3}:\\d{1,3}$",
|
|
||||||
"^\\d{1,3}:\\d{1,3}-\\d{1,3}$"
|
|
||||||
]) {
|
|
||||||
if (!verse.matchAll(regex(re)).empty)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@safe unittest
|
|
||||||
{
|
|
||||||
assert(verseValid("1"));
|
|
||||||
assert(verseValid("5-7"));
|
|
||||||
assert(verseValid("15:13"));
|
|
||||||
assert(verseValid("15:12-17"));
|
|
||||||
}
|
|
||||||
|
|
||||||
string
|
|
||||||
defaultSearchFmt(string reference, string content) pure
|
|
||||||
{
|
|
||||||
return format!"\033[1m%s\033[0m\n %s\n"(reference, content.wrap(80));
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Structure containing the authentication key, API URL,
|
|
||||||
+ any parameters to use when making a request as well as the
|
|
||||||
+ temporary directory to use when fetching audio passages.
|
|
||||||
+/
|
|
||||||
struct ESVApi
|
|
||||||
{
|
|
||||||
ESVApiOptions opts;
|
|
||||||
string key; /++ API key +/
|
|
||||||
string tmp; /++ Tempfile directory +/
|
|
||||||
string url; /++ API URL +/
|
|
||||||
string extraParameters; /++ Additional request parameters +/
|
|
||||||
|
|
||||||
this(string apiKey)
|
|
||||||
{
|
|
||||||
key = apiKey;
|
|
||||||
tmp = tempDir() ~ "esv";
|
|
||||||
url = "https://api.esv.org/v3/passage";
|
|
||||||
opts = ESVApiOptions(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Requests the passage in text format from the API and returns it.
|
|
||||||
+ The (case-insensitive) name of the book being searched is
|
|
||||||
+ contained in the argument book. The verses being looked up are
|
|
||||||
+ contained in the argument verses.
|
|
||||||
+
|
|
||||||
+ Example: getPassage("John", "3:16-21")
|
|
||||||
+/
|
|
||||||
string
|
|
||||||
getPassage(in char[] book, in char[] verse)
|
|
||||||
in (bookValid(book), "Invalid book")
|
|
||||||
in (verseValid(verse), "Invalid verse format")
|
|
||||||
{
|
|
||||||
char[] params, response;
|
|
||||||
|
|
||||||
params = [];
|
|
||||||
|
|
||||||
{
|
|
||||||
string[] parambuf;
|
|
||||||
|
|
||||||
void
|
|
||||||
addParams(R)(R item)
|
|
||||||
{
|
|
||||||
parambuf.length++;
|
|
||||||
parambuf[parambuf.length - 1] =
|
|
||||||
format!"&%s=%s"(item.key, item.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
parambuf = new string[opts.i.length + opts.b.length + 1];
|
|
||||||
|
|
||||||
foreach (item; opts.i.byKeyValue())
|
|
||||||
addParams(item);
|
|
||||||
foreach (item; opts.b.byKeyValue())
|
|
||||||
addParams(item);
|
|
||||||
|
|
||||||
parambuf[parambuf.length - 1] =
|
|
||||||
format!"&indent-using=%s"(
|
|
||||||
opts.indent_using == ESVIndent.TAB ? "tab" : "space");
|
|
||||||
|
|
||||||
/* assemble string from string buffer */
|
|
||||||
foreach (string param; parambuf) {
|
|
||||||
params ~= param;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response = makeRequest(format!"text/?q=%s+%s"(
|
|
||||||
book.capitalize().tr(" ", "+"), verse)
|
|
||||||
~ params ~ extraParameters);
|
|
||||||
|
|
||||||
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: getAudioPassage("John", "3:16-21")
|
|
||||||
+/
|
|
||||||
string
|
|
||||||
getAudioPassage(in char[] book, in char[] verse)
|
|
||||||
in (bookValid(book), "Invalid book")
|
|
||||||
in (verseValid(verse), "Invalid verse format")
|
|
||||||
{
|
|
||||||
File tmpFile;
|
|
||||||
|
|
||||||
tmpFile = File(tmp, "w");
|
|
||||||
tmpFile.write(makeRequest(format!"audio/?q=%s+%s"(
|
|
||||||
book.capitalize().tr(" ", "+"), verse)));
|
|
||||||
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Requests a passage search for the given query.
|
|
||||||
+ Returns a string containing JSON data representing
|
|
||||||
+ the results of the search.
|
|
||||||
+
|
|
||||||
+ Example: search("It is finished")
|
|
||||||
+/
|
|
||||||
string
|
|
||||||
search(in string query) @trusted
|
|
||||||
{
|
|
||||||
JSONValue[] pages, results;
|
|
||||||
JSONValue result;
|
|
||||||
|
|
||||||
JSONValue
|
|
||||||
makeQuery(long page)
|
|
||||||
{
|
|
||||||
return parseJSON(makeRequest("search/?page-size=100"
|
|
||||||
~ "&page=" ~ page.to!string()
|
|
||||||
~ "&q=" ~ query.tr(" ", "+")));
|
|
||||||
}
|
|
||||||
|
|
||||||
pages ~= makeQuery(1);
|
|
||||||
if (pages[0]["total_pages"].integer == 1) {
|
|
||||||
result = JSONValue([
|
|
||||||
"results": pages[0]["results"],
|
|
||||||
"total": JSONValue(pages[0]["results"].array.length)
|
|
||||||
]);
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (long i; 2 .. pages[0]["total_pages"].integer + 1) {
|
|
||||||
pages ~= makeQuery(i);
|
|
||||||
}
|
|
||||||
foreach (JSONValue page; pages) {
|
|
||||||
results ~= page["results"].array;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = JSONValue([
|
|
||||||
"results": JSONValue(results),
|
|
||||||
"total": JSONValue(results.length)
|
|
||||||
]);
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Calls search() and formats the results nicely as plain text,
|
|
||||||
+ unless a custom function is provided.
|
|
||||||
+/
|
|
||||||
string
|
|
||||||
searchFormat(in string query,
|
|
||||||
string function(string, string) fmt = &defaultSearchFmt)
|
|
||||||
{
|
|
||||||
char[] layout;
|
|
||||||
JSONValue resp;
|
|
||||||
|
|
||||||
resp = parseJSON(search(query));
|
|
||||||
layout = [];
|
|
||||||
|
|
||||||
enforce!ESVException(resp["total"].integer != 0,
|
|
||||||
"No results for search");
|
|
||||||
|
|
||||||
() @trusted {
|
|
||||||
foreach (JSONValue item; resp["results"].array) {
|
|
||||||
layout ~= fmt(item["reference"].str, item["content"].str);
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
return layout.idup();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected char[]
|
|
||||||
makeRequest(in char[] query) @trusted
|
|
||||||
{
|
|
||||||
char[] response;
|
|
||||||
HTTP request;
|
|
||||||
|
|
||||||
response = [];
|
|
||||||
request = HTTP(url ~ "/" ~ query);
|
|
||||||
request.onReceive =
|
|
||||||
(ubyte[] data)
|
|
||||||
{
|
|
||||||
response ~= data;
|
|
||||||
return data.length;
|
|
||||||
};
|
|
||||||
request.addRequestHeader("Authorization", "Token " ~ key);
|
|
||||||
request.perform();
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Structure containing parameters passed to the ESV API.
|
|
||||||
+/
|
|
||||||
struct ESVApiOptions
|
|
||||||
{
|
|
||||||
bool[string] b;
|
|
||||||
int[string] i;
|
|
||||||
ESVIndent indent_using;
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ If initialise is true, initialise an ESVApiOptions
|
|
||||||
+ structure with the default values.
|
|
||||||
+/
|
|
||||||
this(bool initialise) nothrow
|
|
||||||
{
|
|
||||||
if (!initialise)
|
|
||||||
return;
|
|
||||||
|
|
||||||
b["include-passage-references"] = true;
|
|
||||||
b["include-verse-numbers"] = true;
|
|
||||||
b["include-first-verse-numbers"] = true;
|
|
||||||
b["include-footnotes"] = true;
|
|
||||||
b["include-footnote-body"] = true;
|
|
||||||
b["include-headings"] = true;
|
|
||||||
b["include-short-copyright"] = true;
|
|
||||||
b["include-copyright"] = false;
|
|
||||||
b["include-passage-horizontal-lines"] = false;
|
|
||||||
b["include-heading-horizontal-lines"] = false;
|
|
||||||
b["include-selahs"] = true;
|
|
||||||
b["indent-poetry"] = true;
|
|
||||||
i["horizontal-line-length"] = 55;
|
|
||||||
i["indent-paragraphs"] = 2;
|
|
||||||
i["indent-poetry-lines"] = 4;
|
|
||||||
i["indent-declares"] = 40;
|
|
||||||
i["indent-psalm-doxology"] = 30;
|
|
||||||
i["line-length"] = 0;
|
|
||||||
indent_using = ESVIndent.SPACE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Exception thrown on API errors.
|
|
||||||
+
|
|
||||||
+ Currently only used when there is no search results
|
|
||||||
+ following a call of searchFormat.
|
|
||||||
+/
|
|
||||||
class ESVException : Exception
|
|
||||||
{
|
|
||||||
mixin basicExceptionCtors;
|
|
||||||
}
|
|
77
esvsearch.1
77
esvsearch.1
|
@ -1,77 +0,0 @@
|
||||||
.Dd $Mdocdate: May 01 2025 $
|
|
||||||
.Dt ESVSEARCH 1
|
|
||||||
.Os
|
|
||||||
.Sh NAME
|
|
||||||
.Nm esvsearch
|
|
||||||
.Nd search the Bible
|
|
||||||
.Sh SYNOPSIS
|
|
||||||
.Nm esvsearch
|
|
||||||
.Bk -words
|
|
||||||
.Op Fl emV
|
|
||||||
.Op Fl c Ar config
|
|
||||||
.Ar query
|
|
||||||
.Ek
|
|
||||||
.Sh DESCRIPTION
|
|
||||||
.Nm
|
|
||||||
searches the Bible for the given
|
|
||||||
.Ar query .
|
|
||||||
.Pp
|
|
||||||
Like
|
|
||||||
.Xr esv 1 ,
|
|
||||||
.Nm
|
|
||||||
requires a configuration file to operate.
|
|
||||||
This is required to know the API key to use
|
|
||||||
when accessing the ESV Bible online.
|
|
||||||
.Nm
|
|
||||||
uses the same configuration file as esv,
|
|
||||||
and it uses the same heuristics in determining the configuration file as well,
|
|
||||||
except that it does not
|
|
||||||
create a configuration file automatically
|
|
||||||
if one does not already exist;
|
|
||||||
you will need to invoke
|
|
||||||
.Xr esv 1
|
|
||||||
for the first time before running
|
|
||||||
.Nm .
|
|
||||||
.Bl -tag -width 123456
|
|
||||||
.It Fl c Ar config
|
|
||||||
Read the configuration from the path
|
|
||||||
.Ar config .
|
|
||||||
See the documentation on
|
|
||||||
.Fl c
|
|
||||||
in
|
|
||||||
.Xr esv 1
|
|
||||||
for more information.
|
|
||||||
.It Fl e
|
|
||||||
Instead of showing loose matches for
|
|
||||||
.Ar query ,
|
|
||||||
only show exact matches.
|
|
||||||
.It Fl m
|
|
||||||
Print matches in a machine-readable format,
|
|
||||||
where each result takes up just one line.
|
|
||||||
Any spaces in the book name are replaced with underscores
|
|
||||||
and a slash character separates the passage reference
|
|
||||||
from the passage content.
|
|
||||||
.It Fl V
|
|
||||||
Print the version number and exit.
|
|
||||||
.El
|
|
||||||
.Sh EXAMPLES
|
|
||||||
Search the Bible for verses containing
|
|
||||||
.Dq rabble :
|
|
||||||
.Pp
|
|
||||||
.Dl esvsearch rabble
|
|
||||||
.Pp
|
|
||||||
.Sh SEE ALSO
|
|
||||||
.Xr esv 1 ,
|
|
||||||
.Xr esv.conf 5
|
|
||||||
.Sh AUTHORS
|
|
||||||
.An Jeremy Baxter Aq Mt jeremy@baxters.nz
|
|
||||||
.Pp
|
|
||||||
Part of the
|
|
||||||
.Sy esv
|
|
||||||
distribution found at
|
|
||||||
.Lk https://reformers.dev/esv
|
|
||||||
.Sh BUGS
|
|
||||||
Currently there are no known bugs in
|
|
||||||
.Nm .
|
|
||||||
If you think you've found a potential bug,
|
|
||||||
please report it to my email address above.
|
|
145
esvsearch.d
145
esvsearch.d
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
* esvsearch: search the Bible from your terminal
|
|
||||||
*
|
|
||||||
* The GPLv2 License (GPLv2)
|
|
||||||
* Copyright (c) 2023-2025 Jeremy Baxter
|
|
||||||
*
|
|
||||||
* esv 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.
|
|
||||||
*
|
|
||||||
* esv 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 esv. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module esvsearch;
|
|
||||||
|
|
||||||
import std.file : FileException;
|
|
||||||
import std.getopt : getopt, GetOptException;
|
|
||||||
import std.path : baseName, expandTilde;
|
|
||||||
import std.process : environment;
|
|
||||||
import std.regex : regex, matchAll, replaceFirst;
|
|
||||||
import std.stdio : writeln, writefln;
|
|
||||||
import std.string : tr;
|
|
||||||
|
|
||||||
import esvapi;
|
|
||||||
import initial;
|
|
||||||
import util;
|
|
||||||
|
|
||||||
import cf = config;
|
|
||||||
|
|
||||||
@safe:
|
|
||||||
|
|
||||||
string cFlag; /* config path */
|
|
||||||
bool eFlag; /* exact matches */
|
|
||||||
bool mFlag; /* machine readable */
|
|
||||||
bool VFlag; /* show version */
|
|
||||||
|
|
||||||
string
|
|
||||||
machineReadableFmt(string reference, string content)
|
|
||||||
{
|
|
||||||
/* match the start of the reference against bibleBooks
|
|
||||||
* to identify what book it's from, so we can replace
|
|
||||||
* spaces in the book name with underscores :-) */
|
|
||||||
foreach (string book; bibleBooks) {
|
|
||||||
auto match = reference.matchAll(regex("^(" ~ book ~ ") \\d"));
|
|
||||||
if (!match.empty) {
|
|
||||||
assert(match.captures[1] == book
|
|
||||||
&& bookValid(match.captures[1]));
|
|
||||||
reference = reference.replaceFirst(
|
|
||||||
regex('^' ~ book), book.tr(" ", "_"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reference ~ " / " ~ content ~ "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
main(string[] args)
|
|
||||||
{
|
|
||||||
INIUnit ini;
|
|
||||||
ESVApi esv;
|
|
||||||
string apiKey;
|
|
||||||
string configPath;
|
|
||||||
string query;
|
|
||||||
|
|
||||||
sharedInit(args);
|
|
||||||
|
|
||||||
cFlag = null;
|
|
||||||
|
|
||||||
/* Parse command-line options */
|
|
||||||
try {
|
|
||||||
import std.getopt : config;
|
|
||||||
getopt(args,
|
|
||||||
config.bundling,
|
|
||||||
config.caseSensitive,
|
|
||||||
"c", &cFlag,
|
|
||||||
"e", &eFlag,
|
|
||||||
"m", &mFlag,
|
|
||||||
"V", &VFlag,
|
|
||||||
);
|
|
||||||
} catch (GetOptException e) {
|
|
||||||
handleOptError(e.msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (VFlag) {
|
|
||||||
writeln("esvsearch from esv " ~ cf.esvVersion);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length < 2) {
|
|
||||||
stderr.writefln("usage: %s [-emV] [-c config] query",
|
|
||||||
baseName(args[0]));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
query = args[1].dup();
|
|
||||||
query = eFlag ? `"` ~ query ~ `"` : query;
|
|
||||||
|
|
||||||
/* determine configuration file: options take first priority,
|
|
||||||
* then environment variables, and then the default path */
|
|
||||||
configPath = environment.get(cf.configEnv, cf.configPath)
|
|
||||||
.expandTilde();
|
|
||||||
try {
|
|
||||||
if (cFlag)
|
|
||||||
configPath = cFlag.expandTilde();
|
|
||||||
|
|
||||||
readINIFile(ini, configPath);
|
|
||||||
} catch (FileException e) {
|
|
||||||
/* filesystem syscall errors */
|
|
||||||
import core.stdc.errno : ENOENT;
|
|
||||||
|
|
||||||
if (e.errno != ENOENT)
|
|
||||||
die(e.msg);
|
|
||||||
|
|
||||||
warn(configPath ~ ": no such file or directory");
|
|
||||||
warn("Invoke esv to create an initial configuration file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKey = ini["api"].key("key");
|
|
||||||
enforceDie(apiKey != null,
|
|
||||||
"API key not present in configuration file; cannot proceed");
|
|
||||||
|
|
||||||
esv = ESVApi(apiKey);
|
|
||||||
|
|
||||||
foreach (char ch; args[1]) {
|
|
||||||
enforceDie(ch != '"', "query is invalid; remove any double quotes");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
writeln(mFlag
|
|
||||||
? esv.searchFormat(query, &machineReadableFmt)
|
|
||||||
: esv.searchFormat(query));
|
|
||||||
catch (ESVException)
|
|
||||||
die("no results");
|
|
||||||
catch (CurlException e)
|
|
||||||
die(e.msg);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
27
flake.lock
generated
27
flake.lock
generated
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1707743206,
|
|
||||||
"narHash": "sha256-AehgH64b28yKobC/DAWYZWkJBxL/vP83vkY+ag2Hhy4=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "2d627a2a704708673e56346fcb13d25344b8eaf3",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
35
flake.nix
35
flake.nix
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
description = "read the ESV Bible from your terminal";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = github:NixOS/nixpkgs/nixpkgs-unstable;
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
|
||||||
with nixpkgs.lib;
|
|
||||||
let
|
|
||||||
forAllSystems = fn:
|
|
||||||
genAttrs platforms.unix (system:
|
|
||||||
fn (import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = forAllSystems (pkgs: {
|
|
||||||
default = pkgs.stdenv.mkDerivation (with pkgs; {
|
|
||||||
name = "esv";
|
|
||||||
src = ./.;
|
|
||||||
|
|
||||||
nativeBuildInputs = [ ldc ];
|
|
||||||
buildInputs = [ curl ];
|
|
||||||
|
|
||||||
dontAddPrefix = true;
|
|
||||||
installFlags = [
|
|
||||||
"DESTDIR=$(out)"
|
|
||||||
"PREFIX=/"
|
|
||||||
];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
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");
|
||||||
|
}
|
455
initial.d
455
initial.d
|
@ -1,455 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Jeremy Baxter. All rights reserved.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ INI is a simple, concise configuration or data interchange
|
|
||||||
+ format. It consists of keys and values listed inside a section.
|
|
||||||
+ An INI document might look something like this:
|
|
||||||
+ ---
|
|
||||||
+ [user]
|
|
||||||
+ name = Kevin
|
|
||||||
+ age = 26
|
|
||||||
+ emailAddress = kevin@example.net
|
|
||||||
+
|
|
||||||
+ [config]
|
|
||||||
+ useBundledMalloc = false
|
|
||||||
+ forceDefaultInit = true
|
|
||||||
+ ---
|
|
||||||
+
|
|
||||||
+ $(B initial) is a library for parsing INI documents into an `INIUnit`,
|
|
||||||
+ a data structure representing an INI document. The function `readINI`
|
|
||||||
+ contains the parser; it parses the INI given to it and deserialises
|
|
||||||
+ it into the given `INIUnit`. The `readINIFile` function can be used
|
|
||||||
+ to read the INI from a file rather than a string.
|
|
||||||
+
|
|
||||||
+ $(B initial) is fully @safe, and can be used in @safe code.
|
|
||||||
+
|
|
||||||
+ License: [Boost License 1.0](https://www.boost.org/LICENSE_1_0.txt).
|
|
||||||
+ Authors: Jeremy Baxter
|
|
||||||
+/
|
|
||||||
module initial;
|
|
||||||
|
|
||||||
import std.conv : to, ConvException;
|
|
||||||
import std.exception : basicExceptionCtors, enforce;
|
|
||||||
import std.format : format;
|
|
||||||
import std.traits : isSomeChar;
|
|
||||||
|
|
||||||
private alias parserEnforce = enforce!INIParseException;
|
|
||||||
|
|
||||||
@safe:
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Structure that represents an INI file.
|
|
||||||
+
|
|
||||||
+ Contains `INISection`s in an associative array that can be
|
|
||||||
+ directly accessed if needs be, or the index operator can be
|
|
||||||
+ used to get an `INISection` and automatically initialise it
|
|
||||||
+ if needed.
|
|
||||||
+
|
|
||||||
+ The `serialise` method can be used to serialise the entire
|
|
||||||
+ structure into an INI document that can be then read again
|
|
||||||
+ by the parser and modified by humans.
|
|
||||||
+
|
|
||||||
+ Example:
|
|
||||||
+ ---
|
|
||||||
+ INIUnit ini;
|
|
||||||
+
|
|
||||||
+ /* write INI data */
|
|
||||||
+ ini["section"]["key"] = "value";
|
|
||||||
+ ini["section"]["num"] = "4.8";
|
|
||||||
+
|
|
||||||
+ /* read INI data */
|
|
||||||
+ readINI(ini, `
|
|
||||||
+ [section]
|
|
||||||
+ num = 5.3
|
|
||||||
+ `);
|
|
||||||
+
|
|
||||||
+ /* read INI from file */
|
|
||||||
+ readINIFile(ini, "config.ini");
|
|
||||||
+
|
|
||||||
+ /* write INI to file */
|
|
||||||
+ import std.file : write;
|
|
||||||
+ write("config.ini", ini.serialise());
|
|
||||||
+ ---
|
|
||||||
+/
|
|
||||||
struct INIUnit
|
|
||||||
{
|
|
||||||
INISection[string] sections; /++ Hashmap of INISections in this unit. +/
|
|
||||||
string defaultSection = "main"; /++ Name of the default section. +/
|
|
||||||
|
|
||||||
nothrow:
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Returns the value of *k* in the default section
|
|
||||||
+ or *defaultValue* if *k* is not present.
|
|
||||||
+/
|
|
||||||
string
|
|
||||||
key(string k, string defaultValue = null)
|
|
||||||
{
|
|
||||||
return this[defaultSection].key(k, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Serialises this `INIUnit` into an INI document.
|
|
||||||
+
|
|
||||||
+ If *defaultSectionHeading* is true, includes the
|
|
||||||
+ section heading of the default section.
|
|
||||||
+ Example:
|
|
||||||
+ ---
|
|
||||||
+ INIUnit ini;
|
|
||||||
+
|
|
||||||
+ ini["sections"] = "ages suburbs";
|
|
||||||
+ ini["ages"]["John"] = "37";
|
|
||||||
+ ini["ages"]["Mary"] = "29";
|
|
||||||
+ ini["suburbs"]["John"] = "Gordonton";
|
|
||||||
+ ini["suburbs"]["Mary"] = "Ashmore";
|
|
||||||
+
|
|
||||||
+ writeln(ini.serialise());
|
|
||||||
+ /*
|
|
||||||
+ * sections = ages suburbs
|
|
||||||
+ * [ages]
|
|
||||||
+ * John = 37
|
|
||||||
+ * Mary = 29
|
|
||||||
+ * [suburbs]
|
|
||||||
+ * John = Gordonton
|
|
||||||
+ * Mary = Ashmore
|
|
||||||
+ */
|
|
||||||
+ ---
|
|
||||||
+/
|
|
||||||
char[]
|
|
||||||
serialise(bool defaultSectionHeading = false)
|
|
||||||
{
|
|
||||||
char[] buf;
|
|
||||||
|
|
||||||
buf = this[defaultSection].serialise(defaultSectionHeading);
|
|
||||||
foreach (string sect; sections.byKey()) {
|
|
||||||
if (sect == defaultSection)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
buf ~= this[sect].serialise();
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Returns the `INISection` associated with the name *sect*.
|
|
||||||
+ Initialises it if it doesn't exist.
|
|
||||||
+/
|
|
||||||
ref INISection
|
|
||||||
opIndex(string sect)
|
|
||||||
{
|
|
||||||
if (!(sect in sections))
|
|
||||||
sections[sect] = INISection(sect);
|
|
||||||
|
|
||||||
return sections[sect];
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Sets the value of the key *k* in the default section to *v*.
|
|
||||||
+ Example:
|
|
||||||
+ ---
|
|
||||||
+ ini["key"] = "value";
|
|
||||||
+ ---
|
|
||||||
+/
|
|
||||||
void
|
|
||||||
opIndexAssign(string v, string k)
|
|
||||||
{
|
|
||||||
sections[defaultSection][k] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Sets the value of the `INISection` named *sect*.
|
|
||||||
+/
|
|
||||||
void
|
|
||||||
opIndexAssign(INISection v, string sect)
|
|
||||||
{
|
|
||||||
sections[sect] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Represents an INI section.
|
|
||||||
+
|
|
||||||
+ Holds keys in the form of an associative array.
|
|
||||||
+ Index the `keys` property to get this data raw, or to avoid
|
|
||||||
+ range errors, use the `key` or `keyAs` functions.
|
|
||||||
+
|
|
||||||
+ `alias this` is applied to the `keys` associative array,
|
|
||||||
+ meaning you can index the structure to get or set keys' values.
|
|
||||||
+/
|
|
||||||
struct INISection
|
|
||||||
{
|
|
||||||
string[string] keys; /++ Hashmap of keys belonging to this section. +/
|
|
||||||
string name; /++ Name of this section. +/
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Construct a new `INISection` with the given name.
|
|
||||||
+ Called by `readINI` and `INIUnit.opIndex`.
|
|
||||||
+/
|
|
||||||
this(string name) nothrow
|
|
||||||
{
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Returns the value of `k` in this section or
|
|
||||||
+ `defaultValue` if the key is not present.
|
|
||||||
+/
|
|
||||||
string
|
|
||||||
key(string k, string defaultValue = null) nothrow
|
|
||||||
{
|
|
||||||
return k in keys ? keys[k] : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Using `std.conv`, converts the value of *k* to *T*.
|
|
||||||
+ On conversion failure throws an `INITypeException`
|
|
||||||
+ with a message containing location information.
|
|
||||||
+/
|
|
||||||
T
|
|
||||||
keyAs(T)(string k, T defaultValue)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
return k in keys ? keys[k].to!T() : defaultValue;
|
|
||||||
catch (ConvException)
|
|
||||||
throw new INITypeException(
|
|
||||||
format!"unable to convert [%s].%s to %s"(name, k, T.stringof));
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Serialise this section into an INI document.
|
|
||||||
+
|
|
||||||
+ If `sectionHeading` is true, includes the section heading at the top.
|
|
||||||
+/
|
|
||||||
char[]
|
|
||||||
serialise(bool sectionHeading = true) nothrow
|
|
||||||
{
|
|
||||||
char[] buf;
|
|
||||||
|
|
||||||
if (sectionHeading)
|
|
||||||
buf ~= "[" ~ name.to!(char[]) ~ "]\n";
|
|
||||||
|
|
||||||
foreach (string key; keys.byKey()) {
|
|
||||||
buf ~= key ~ " = " ~ keys[key] ~ "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
alias keys this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Exception thrown when parsing failure occurs.
|
|
||||||
+/
|
|
||||||
class INIParseException : Exception
|
|
||||||
{
|
|
||||||
mixin basicExceptionCtors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Exception thrown when conversion failure occurs.
|
|
||||||
+/
|
|
||||||
class INITypeException : Exception
|
|
||||||
{
|
|
||||||
mixin basicExceptionCtors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string
|
|
||||||
locate(size_t l, size_t c)
|
|
||||||
{
|
|
||||||
return format!"%d:%d"(l, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Parse the given INI data into an `INIUnit`.
|
|
||||||
+
|
|
||||||
+ To parse a file's contents, use readINIFile instead.
|
|
||||||
+
|
|
||||||
+ Whitespace is first stripped from the beginning and end of
|
|
||||||
+ the line before parsing it.
|
|
||||||
+
|
|
||||||
+ The following rules apply when parsing:
|
|
||||||
+ $(UL
|
|
||||||
+ $(LI if a hash character `#` is found at the beginning of a line,
|
|
||||||
+ the rest of the line is ignored)
|
|
||||||
+ $(LI if a left square bracket character `[` is found at
|
|
||||||
+ the beginning of a line, the line is considered a
|
|
||||||
+ section heading)
|
|
||||||
+ $(LI inside a section heading, any character can be used
|
|
||||||
+ inside the section heading except the right square bracket
|
|
||||||
+ character)
|
|
||||||
+ $(LI once the right square bracket character `]` is found,
|
|
||||||
+ the section heading is considered finished. Any trailing
|
|
||||||
+ characters are garbage and will trigger a parse error.)
|
|
||||||
+ $(LI if a character except the left square bracket is found
|
|
||||||
+ at the beginning of a line, the line is considered an
|
|
||||||
+ assignment in the current section)
|
|
||||||
+ $(LI once an equals character `=` is found after the first
|
|
||||||
+ part of an assignment, the rest of the line is considered
|
|
||||||
+ the "value" while the first part is the "key")
|
|
||||||
+ $(LI if whitespace is found in the first part of an assignment,
|
|
||||||
+ it is ignored and the next equals sign is looked for
|
|
||||||
+ immediately)
|
|
||||||
+ $(LI if whitespace is found after an equals character `=`
|
|
||||||
+ in an assignment, it is ignored until the first
|
|
||||||
+ non-whitespace character is found)
|
|
||||||
+ $(LI once a non-whitespace character is found after an equals
|
|
||||||
+ character `=` in an assignment, it is counted part of the
|
|
||||||
+ "value")
|
|
||||||
+/
|
|
||||||
void
|
|
||||||
readINI(ref INIUnit ini, string data)
|
|
||||||
{
|
|
||||||
import std.string : splitLines, strip;
|
|
||||||
|
|
||||||
string section; /* name of current section */
|
|
||||||
string text; /* current key, value, section, whatever */
|
|
||||||
string key; /* name of key in section */
|
|
||||||
string value; /* value */
|
|
||||||
bool inAssign; /* in key assignment? */
|
|
||||||
bool inHeading; /* in section heading? */
|
|
||||||
bool needEqls; /* require equals sign immediately? */
|
|
||||||
bool pastEqls; /* past equals sign in assignment? */
|
|
||||||
bool trailing; /* trailing garbage after heading? */
|
|
||||||
|
|
||||||
section = ini.defaultSection;
|
|
||||||
|
|
||||||
nextline:
|
|
||||||
foreach (size_t ln, string line; data.splitLines()) {
|
|
||||||
line = line.strip();
|
|
||||||
if (line.length == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
inAssign = inHeading = needEqls = pastEqls = trailing = false;
|
|
||||||
text = key = value = "";
|
|
||||||
foreach (size_t cn, char ch; line) {
|
|
||||||
switch (ch) {
|
|
||||||
/* comment? */
|
|
||||||
case '#':
|
|
||||||
if (cn == 0)
|
|
||||||
continue nextline;
|
|
||||||
|
|
||||||
text ~= ch;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* beginning of a section heading? */
|
|
||||||
case '[':
|
|
||||||
if (cn == 0) /* beginning of line? */
|
|
||||||
inHeading = true;
|
|
||||||
else
|
|
||||||
text ~= ch;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* end of a section heading? */
|
|
||||||
case ']':
|
|
||||||
parserEnforce(inHeading || inAssign,
|
|
||||||
locate(ln, cn) ~ ": unexpected character ']'");
|
|
||||||
|
|
||||||
if (inHeading) {
|
|
||||||
section = text;
|
|
||||||
text = "";
|
|
||||||
inHeading = false;
|
|
||||||
trailing = true;
|
|
||||||
} else if (inAssign) {
|
|
||||||
text ~= ch;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* middle of an assignment? */
|
|
||||||
case '=':
|
|
||||||
if (inAssign) {
|
|
||||||
if (pastEqls) {
|
|
||||||
/* count it as part of the value */
|
|
||||||
text ~= ch;
|
|
||||||
} else {
|
|
||||||
key = text;
|
|
||||||
text = "";
|
|
||||||
pastEqls = true;
|
|
||||||
needEqls = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
goto default;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* whitespace */
|
|
||||||
case ' ':
|
|
||||||
case '\t':
|
|
||||||
case '\v':
|
|
||||||
case '\r':
|
|
||||||
case '\n':
|
|
||||||
case '\f':
|
|
||||||
if (inAssign) {
|
|
||||||
if (!pastEqls)
|
|
||||||
needEqls = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (cn == 0) /* beginning of line? */
|
|
||||||
inAssign = true;
|
|
||||||
|
|
||||||
parserEnforce((inAssign || inHeading) && !trailing,
|
|
||||||
locate(ln, cn) ~ ": unexpected character '" ~ ch ~ "'");
|
|
||||||
|
|
||||||
if (inAssign)
|
|
||||||
parserEnforce(!needEqls,
|
|
||||||
locate(ln, cn) ~ ": expected '=', not '" ~ ch ~ "'");
|
|
||||||
|
|
||||||
text ~= ch;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* once the line has finished being looped over... */
|
|
||||||
if (inAssign) {
|
|
||||||
parserEnforce(key.length != 0,
|
|
||||||
locate(ln, line.length - 1) ~ ": key cannot be empty");
|
|
||||||
value = text;
|
|
||||||
text = "";
|
|
||||||
|
|
||||||
if (!(section in ini.sections))
|
|
||||||
ini[section] = INISection(section);
|
|
||||||
ini[section][key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Parse INI from a file located at `fileName`.
|
|
||||||
+/
|
|
||||||
void
|
|
||||||
readINIFile(ref INIUnit ini, string fileName)
|
|
||||||
{
|
|
||||||
import std.file : readText;
|
|
||||||
|
|
||||||
readINI(ini, readText(fileName));
|
|
||||||
}
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
102
util.d
102
util.d
|
@ -1,102 +0,0 @@
|
||||||
module util;
|
|
||||||
|
|
||||||
import std.stdio : File;
|
|
||||||
|
|
||||||
public @safe:
|
|
||||||
|
|
||||||
File stderr;
|
|
||||||
|
|
||||||
private {
|
|
||||||
string[] mainArgs;
|
|
||||||
|
|
||||||
version (OpenBSD) {
|
|
||||||
immutable(char) *promises;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+ Common initialisation function shared between esv and esvsearch
|
|
||||||
+/
|
|
||||||
void
|
|
||||||
sharedInit(string[] args)
|
|
||||||
{
|
|
||||||
mainArgs = args;
|
|
||||||
stderr = File("/dev/stderr", "w");
|
|
||||||
|
|
||||||
version (OpenBSD) () @trusted {
|
|
||||||
import core.sys.openbsd.unistd : pledge;
|
|
||||||
import std.string : toStringz;
|
|
||||||
|
|
||||||
promises = toStringz("stdio rpath wpath cpath inet dns tty proc exec prot_exec");
|
|
||||||
pledge(promises, null);
|
|
||||||
}();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
enforceDie(A...)(bool cond, string fmt, A a)
|
|
||||||
{
|
|
||||||
import std.format : format;
|
|
||||||
|
|
||||||
if (!cond)
|
|
||||||
die(format(fmt, a));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
handleOptError(in string msg)
|
|
||||||
{
|
|
||||||
import std.algorithm : startsWith;
|
|
||||||
import std.regex : matchFirst;
|
|
||||||
|
|
||||||
string opt = msg.matchFirst("-.")[0];
|
|
||||||
|
|
||||||
enforceDie(!msg.startsWith("Unrecognized option"),
|
|
||||||
"unknown option " ~ opt);
|
|
||||||
enforceDie(!msg.startsWith("Missing value for argument"),
|
|
||||||
"missing argument for option " ~ opt);
|
|
||||||
|
|
||||||
die(msg); /* catch-all */
|
|
||||||
}
|
|
||||||
|
|
||||||
string
|
|
||||||
parseBook(in string book)
|
|
||||||
{
|
|
||||||
import std.string : tr;
|
|
||||||
|
|
||||||
return book.tr("-_", " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
@safe unittest
|
|
||||||
{
|
|
||||||
assert(parseBook("1-Corinthians") == "1 Corinthians");
|
|
||||||
assert(parseBook("1_Corinthians") == "1 Corinthians");
|
|
||||||
}
|
|
||||||
|
|
||||||
ushort
|
|
||||||
terminalColumns() @trusted
|
|
||||||
{
|
|
||||||
import core.sys.posix.sys.ioctl;
|
|
||||||
|
|
||||||
winsize w;
|
|
||||||
|
|
||||||
ioctl(1, TIOCGWINSZ, &w);
|
|
||||||
return w.ws_col > 72 ? 72 : w.ws_col;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
warn(string mesg)
|
|
||||||
{
|
|
||||||
import std.path : baseName;
|
|
||||||
|
|
||||||
stderr.writeln(baseName(mainArgs[0]) ~ ": " ~ mesg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
die(string mesg) @trusted
|
|
||||||
{
|
|
||||||
import core.runtime : Runtime;
|
|
||||||
import core.stdc.stdlib : exit;
|
|
||||||
|
|
||||||
warn(mesg);
|
|
||||||
Runtime.terminate();
|
|
||||||
exit(1);
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue