Compare commits

..

4 Commits

Author SHA1 Message Date
9bd2079cd4 Fix footnote alignment and verse sorting
- Fix footnote spacing to account for multi-character superscript numbers
- Transform introductions from chapter 0 to verse 0 of their respective chapters
- Sort TSV by book, chapter, verse with verses before footnotes
- Update AWK to handle verse 0 introductions instead of chapter 0
- Print newline before introductions and start them from left margin
2025-12-18 21:46:12 +01:00
7ef782dda2 Add JSON output format for programmatic access
Implements structured JSON output with hierarchical schema including
book metadata, chapters, verses, and footnotes. All existing flags
(-F, -g, -L) are respected in JSON mode.
2025-12-16 18:49:32 +01:00
6432d37df3 add -w flag for custom widths 2025-12-16 18:29:45 +01:00
7300ba933c update README to current state of project 2025-12-16 18:16:16 +01:00
4 changed files with 23140 additions and 22783 deletions

140
README.md
View File

@@ -10,7 +10,12 @@ Code largely based off of [https://github.com/bontibon/kjv.git](https://github.c
usage: ./allioli [flags] [reference...]
-l list books
-w <n> set custom terminal width
-W no line wrap
-F no footnotes
-g show only German (no Latin)
-L show only Latin (no German)
-j output as JSON
-h show help
Reference types:
@@ -35,6 +40,72 @@ Code largely based off of [https://github.com/bontibon/kjv.git](https://github.c
All verses in a chapter of a book that match a pattern
```
## Features
### Bilingual Latin-German Display
allioli displays the original Latin Vulgate text alongside the German Allioli translation in a side-by-side format. This allows for easy comparison between the original and translated text.
- By default, both Latin and German are displayed side-by-side in two columns
- Use `-g` flag to display only the German translation
- Use `-L` flag to display only the Latin text
### Footnotes
The text includes footnotes marked with Unicode superscript numbers (e.g., ¹, ², ³). Footnotes are displayed below their corresponding verses with proper indentation.
- Use `-F` flag to disable footnote display if desired
### JSON Output
allioli supports structured JSON output for programmatic access to bible verses and footnotes. The JSON schema provides a hierarchical structure with book information, chapters, verses, and associated footnotes.
- Use `-j` flag to output in JSON format
- All other flags (`-F`, `-g`, `-L`) are respected in JSON mode
- The JSON structure follows this schema:
```json
{
"book": {
"name": "Johannes",
"abbreviation": "Joh",
"number": 51
},
"chapter": 1,
"verses": [
{
"verse": 1,
"text": {
"latin": "...",
"german": "..."
},
"footnotes": [
{
"number": 1,
"text": "..."
}
]
}
]
}
```
**Examples:**
```bash
# Get John 1:1 as JSON
./allioli -j "Johannes 1:1"
# Get verses without footnotes
./allioli -j -F "Johannes 1:1-3"
# Get only Latin text (with footnotes)
./allioli -j -L "Johannes 1:1"
# Get only German text (with footnotes)
./allioli -j -g "Johannes 1:1-5"
```
## Install
### From Source
@@ -57,10 +128,73 @@ so for example using yay, it's a simple
```
## Current state of project
## Example
You might notice the two books, 'Ester' and 'xEster'.
The significance of this mark left over from the original XML this project has started from will be invistigated in the coming months.
Here's for example how the beginning of John looks. The first part being an introduction to the chapter and then some verses with footnotes.
```
Johannes
Prolog (V. 18): Das Gott wesensgleiche Wort tat sich durch die Schöpfung und die
übernatürliche Offenbarung kund und ward dennoch nicht von den sündigen Menschen erkannt. (V. 5)
Selbst als es von seinem Vorläufer angekündigt in das Seinige kam, ward es von den Seinen nicht
aufgenommen, denen aber, die es aufnahmen, gab es die höchste Würde. (V. 13) Dennoch ward das Wort
Fleisch und offenbarte seine Herrlichkeit. (V. 18) I. 1-12,50 1. Das Wort wird von denen, die guten
Willens sind, aufgenommen, aber nicht von allen mit ausreichendem Glauben. a. Mit vollkommenem
Glauben von dem heil. Johannes dem Täufer, der ihn vor den Abgesandten des hohen Rates (V. 28) und
vor seinen Jüngern bekennt (V. 34), von den ersten Jüngern des Herrn, nach einem zweiten Zeugnis des
Johannes (V. 42) und der Offenbarung seiner Allwissenheit seitens des Herrn.
1:1 In principio erat verbum, et verbum erat apud | Im Anfange¹ war² das Wort,³ und das Wort war
Deum, et Deus erat verbum. | bei⁴ Gott,⁵ und Gott⁶ war das Wort.
¹Ehe etwas ward. [Gen 1,1, Spr 8,23] Mittelbar folgt hieraus nach dem
Sprachgebrauche der heil. Schrift die Ewigkeit des Wortes.
²Gegensatz zu [Gen 1,1]: Im Anfange schuf Gott. Durch die Form des
Zeitwortes war wird das Sein des Wortes als anfangs- und endlos bezeichnet.
³Es war steht vier Mal. Was du immer ausdenken magst, der Sohn war. (Ambr.);
du wirst keinen Zeitraum finden, in dem er nicht war. Die Offenbarung vom Sohne Gottes war
auch den Israeliten zuteil geworden, wie [Spr 8,22-31, Weish 7, Weish 8, Sir 24, Bar 3,94,4]
zeigen. Dieses Wort ist offenbar eine Person, denn später werden von ihm Dinge gesagt,
welche nur von Personen ausgesagt werden können; und zwar eine göttliche Person. (V. 1, 14)
Der Gedanke, dass das Wort Gottes persönlich, Sohn Gottes sei, war den Juden in der der
Menschwerdung unseres Herrn unmittelbar vorhergehenden Zeit geläufig und hatte in der
Schrift ihren Halt, z. B. [Weish 18,15, Weish 10,15]
⁴Von dem Vater unterschieden und doch mit ihm in innigster
Lebensgemeinschaft stehend.
⁵Dem Vater.
⁶Die Weglassung des Artikels im Griechischen deutet an, dass das Wort Gottes
im zweiten Falle nicht auf eine Person bezogen wird, wie in der ersten Hälfte des Verses
(Orig., Euseb.)
1:10 In mundo erat, et mundus per ipsum factus | Er war in der Welt,²⁷ und die Welt ist durch
est, et mundus eum non cognovit. | dasselbe gemacht worden, und die Welt²⁸ hat
| ihn nicht erkannt.
²⁷Vor der Menschwerdung (Chrys., Aug., Bed.).
²⁸Die Menschen, welche der Welt anhängen und das Irdische suchen (Chrys., Aug.).
1:11 In propria venit, et sui eum non receperunt. | Er kam²⁹ in sein Eigentum,³⁰ und die Seinigen
| nahmen ihn nicht auf.³¹
²⁹In der Menschwerdung zu allen Menschen (Chrys., Euth.), vorzüglich den
Juden. (Aug., Bed.) Steigerung der Verkündigung.
³⁰V. 9 wurde das Wort Licht genannt, V. 10 wird das Wirken des Wortes als
Licht bei den Heiden, V. 11 besonders bei den Juden geschildert.
³¹Vergl. [Sir 24,1].
1:12 Quotquot autem receperunt eum, dedit eis | Wie viele ihn aber aufnahmen, denen³² gab er
potestatem filios Dei fieri, his, qui credunt | Macht,³³ Kinder Gottes zu werden, denen
in nomine ejus: | nämlich, die an seinen Namen glauben,³⁴
³²Eine Ausnahme, wohl besonders die Heiden (Cyr.).
³³Durch den Glauben wird der Mensch auf die Taufe vorbereitet, in der er ein
Kind Gottes wird. (Thom.) Der Evangelist bemerkt vorweg, wie die, welche die ihnen gegebene
Macht benutzen, Kinder Gottes werden.
³⁴Vergl. [Mt 5,45].
1:13 Qui non ex sanguinibus, neque ex voluntate | welche nicht aus dem Geblüte, auch nicht aus
carnis, neque ex voluntate viri, sed ex Deo | dem Willen des Fleisches, noch aus dem Willen
nati sunt. | des Mannes,³⁵ sondern aus Gott geboren
| sind.³⁶
³⁵Das Geblüt ist gleichsam der Stoff, der Wille des Fleisches die sinnliche
wirksame Ursache, der Wille des Mannes die vernünftige wirkende Ursache. Ein Kind Gottes
wird man nicht, wie die Juden meinten, lediglich durch leibliche Abstammung.
³⁶Der Evangelist schildert die hohe Würde der Kindschaft, um die Gläubigen
zur Bewahrung dieses herrlichen Vorzuges anzustacheln (Chrys., Euth. Theoph.).
```
## Similar projects

View File

@@ -7,12 +7,10 @@ BEGIN {
# $6 Verse
FS = "\t"
MAX_WIDTH = 100
MAX_WIDTH = 120
if (ENVIRON["ALLIOLI_MAX_WIDTH"] ~ /^[0-9]+$/) {
if (int(ENVIRON["ALLIOLI_MAX_WIDTH"]) < MAX_WIDTH) {
MAX_WIDTH = int(ENVIRON["ALLIOLI_MAX_WIDTH"])
}
}
if (cmd == "ref") {
mode = parseref(ref, p)
@@ -51,8 +49,8 @@ function parseref(ref, arr) {
return "unknown"
}
if (match(ref, "^:?[1-9]+[0-9]*")) {
# 2, 3, 3a, 4, 5, 6, 9
if (match(ref, "^:?[0-9]+")) {
# 2, 3, 3a, 4, 5, 6, 9 (including chapter 0)
if (sub("^:", "", ref)) {
arr["chapter"] = int(substr(ref, 1, RLENGTH - 1))
ref = substr(ref, RLENGTH)
@@ -71,11 +69,11 @@ function parseref(ref, arr) {
return "unknown"
}
if (match(ref, "^:[1-9]+[0-9]*")) {
if (match(ref, "^:[0-9]+")) {
# 3, 3a, 5, 6
arr["verse"] = int(substr(ref, 2, RLENGTH - 1))
ref = substr(ref, RLENGTH + 1)
} else if (match(ref, "^-[1-9]+[0-9]*$")) {
} else if (match(ref, "^-[0-9]+$")) {
# 4
arr["chapter_end"] = int(substr(ref, 2))
return "range"
@@ -90,25 +88,25 @@ function parseref(ref, arr) {
return "unknown"
}
if (match(ref, "^-[1-9]+[0-9]*$")) {
if (match(ref, "^-[0-9]+$")) {
# 5
arr["verse_end"] = int(substr(ref, 2))
return "range"
} else if (match(ref, "-[1-9]+[0-9]*")) {
} else if (match(ref, "-[0-9]+")) {
# 6
arr["chapter_end"] = int(substr(ref, 2, RLENGTH - 1))
ref = substr(ref, RLENGTH + 1)
} else if (ref == "") {
# 3
return "exact"
} else if (match(ref, "^,[1-9]+[0-9]*")) {
} else if (match(ref, "^,[0-9]+")) {
# 3a
arr["verse", arr["verse"]] = 1
delete arr["verse"]
do {
arr["verse", substr(ref, 2, RLENGTH - 1)] = 1
ref = substr(ref, RLENGTH + 1)
} while (match(ref, "^,[1-9]+[0-9]*"))
} while (match(ref, "^,[0-9]+"))
if (ref != "") {
return "unknown"
@@ -119,7 +117,7 @@ function parseref(ref, arr) {
return "unknown"
}
if (match(ref, "^:[1-9]+[0-9]*$")) {
if (match(ref, "^:[0-9]+$")) {
# 6
arr["verse_end"] = int(substr(ref, 2))
return "range_ext"
@@ -304,7 +302,7 @@ function printfootnote(footnote_num, footnote, word_count, characters_printed
}
if( length(footnote) < MAX_WIDTH - 17){
for ( i=1; i <= MAX_WIDTH - length(footnote) - 1; i++){
for ( i=1; i <= MAX_WIDTH - length(footnote) - length(sup_num); i++){
printf(" ")
}
printf("%s%s\n", sup_num, footnote)
@@ -331,7 +329,49 @@ function printfootnote(footnote_num, footnote, word_count, characters_printed
}
function processline() {
if (printed_intrudction && $4 != 0){
# JSON mode: collect data instead of printing
if (ENVIRON["ALLIOLI_JSON_OUTPUT"] != "" && ENVIRON["ALLIOLI_JSON_OUTPUT"] != "0") {
book_key = $3 # Use book number as key
# Store book info (track multiple books for dump mode)
if (!json_book_seen[book_key]) {
json_book_seen[book_key] = 1
json_books[++json_book_total] = book_key
json_book_name[book_key] = $1
json_book_abbr[book_key] = $2
json_book_num[book_key] = $3
}
# Check if this is a footnote
if ($6 == "" && $7 ~ /^[0-9]+$/ && NF >= 8) {
json_footnotes[book_key, $4, $5, $7] = $8
json_footnote_nums[book_key, $4, $5, ++json_footnote_count[book_key, $4, $5]] = $7
}
# Verse with content (including chapter 0 introductions)
else if ($6 != "" || ($7 != "" && $7 !~ /^[0-9]+$/)) {
# Store verse data
json_latin[book_key, $4, $5] = $6
json_german[book_key, $4, $5] = $7
# Track unique verses per chapter
if (!json_verse_seen[book_key, $4, $5]) {
json_verse_seen[book_key, $4, $5] = 1
json_verses[book_key, $4, ++json_verse_count[book_key, $4]] = $5
}
# Track chapters per book
if (!json_chapter_seen[book_key, $4]) {
json_chapter_seen[book_key, $4] = 1
json_chapters[book_key, ++json_chapter_total[book_key]] = $4
}
}
outputted_records++
return
}
# Normal text output mode
if (printed_intrudction && $5 != 0){
printf("\n\n")
printed_intrudction=0
}
@@ -347,9 +387,9 @@ function processline() {
if ($6 == "" && $7 ~ /^[0-9]+$/ && NF >= 8) {
printfootnote($7, $8)
}
# Check if this is an introduction (chapter 0, column 6 empty, column 7 is text)
else if ($4 == 0 && $6 == ""){
printf("\t")
# Check if this is an introduction (verse 0, column 6 empty, column 7 is text)
else if ($5 == 0 && $6 == ""){
printf("\n")
printintroductionpar($7)
}
# Bilingual verse (both column 6 and 7 have text)
@@ -382,6 +422,10 @@ function processline() {
outputted_records++
}
cmd == "dump" {
processline()
}
cmd == "ref" && mode == "exact" && bookmatches($1, $2, p["book"]) && (p["chapter"] == "" || $4 == p["chapter"]) && (p["verse"] == "" || $5 == p["verse"]) {
processline()
}
@@ -403,7 +447,171 @@ cmd == "ref" && mode == "search" && (p["book"] == "" || bookmatches($1, $2, p["b
}
END {
# JSON output mode
if ((cmd == "ref" || cmd == "dump") && ENVIRON["ALLIOLI_JSON_OUTPUT"] != "" && ENVIRON["ALLIOLI_JSON_OUTPUT"] != "0") {
if (outputted_records == 0) {
if (cmd == "ref") {
print "Unknown reference: " ref
exit 1
}
}
# Determine language flags
only_latin = (ENVIRON["ALLIOLI_ONLY_LATIN"] != "" && ENVIRON["ALLIOLI_ONLY_LATIN"] != "0")
only_german = (ENVIRON["ALLIOLI_ONLY_GERMAN"] != "" && ENVIRON["ALLIOLI_ONLY_GERMAN"] != "0")
no_footnotes = (ENVIRON["ALLIOLI_NOFOOTNOTES"] != "" && ENVIRON["ALLIOLI_NOFOOTNOTES"] != "0")
# If we have multiple books (dump mode), output as array
if (json_book_total > 1) {
print "["
}
# Output each book
for (b_idx = 1; b_idx <= json_book_total; b_idx++) {
book_key = json_books[b_idx]
# Start book object
if (json_book_total > 1) {
printf(" {\n")
} else {
print "{"
}
# Book metadata
if (json_book_total > 1) {
printf(" \"book\": {\n")
printf(" \"name\": \"%s\",\n", json_book_name[book_key])
printf(" \"abbreviation\": \"%s\",\n", json_book_abbr[book_key])
printf(" \"number\": %d\n", json_book_num[book_key])
printf(" },\n")
} else {
printf(" \"book\": {\n")
printf(" \"name\": \"%s\",\n", json_book_name[book_key])
printf(" \"abbreviation\": \"%s\",\n", json_book_abbr[book_key])
printf(" \"number\": %d\n", json_book_num[book_key])
printf(" },\n")
}
# Output chapters (including chapter 0 for introduction)
indent = json_book_total > 1 ? " " : " "
for (c_idx = 1; c_idx <= json_chapter_total[book_key]; c_idx++) {
chapter = json_chapters[book_key, c_idx]
# Output chapter number and verses (works for chapter 0 and regular chapters)
printf("%s\"chapter\": %d,\n", indent, chapter)
printf("%s\"verses\": [\n", indent)
# Sort verses numerically before output
delete sorted_verses
for (v_idx = 1; v_idx <= json_verse_count[book_key, chapter]; v_idx++) {
sorted_verses[v_idx] = json_verses[book_key, chapter, v_idx]
}
# Simple bubble sort for numeric ordering
for (i = 1; i <= json_verse_count[book_key, chapter]; i++) {
for (j = i + 1; j <= json_verse_count[book_key, chapter]; j++) {
if (sorted_verses[i] + 0 > sorted_verses[j] + 0) {
temp = sorted_verses[i]
sorted_verses[i] = sorted_verses[j]
sorted_verses[j] = temp
}
}
}
# Output verses in sorted order
for (v_idx = 1; v_idx <= json_verse_count[book_key, chapter]; v_idx++) {
verse_num = sorted_verses[v_idx]
printf("%s {\n", indent)
printf("%s \"verse\": %d,\n", indent, verse_num)
# Text object
printf("%s \"text\": {", indent)
# Output text based on language flags
if (only_latin) {
printf("\n%s \"latin\": \"%s\"\n", indent, json_latin[book_key, chapter, verse_num])
} else if (only_german) {
# Remove superscript markers if footnotes disabled
german_text = json_german[book_key, chapter, verse_num]
if (no_footnotes) {
gsub(/[⁰¹²³⁴⁵⁶⁷⁸⁹]+/, "", german_text)
}
printf("\n%s \"german\": \"%s\"\n", indent, german_text)
} else {
# Both languages
german_text = json_german[book_key, chapter, verse_num]
if (no_footnotes) {
gsub(/[⁰¹²³⁴⁵⁶⁷⁸⁹]+/, "", german_text)
}
if (json_latin[book_key, chapter, verse_num] != "") {
printf("\n%s \"latin\": \"%s\",\n", indent, json_latin[book_key, chapter, verse_num])
}
if (german_text != "") {
printf("%s \"german\": \"%s\"\n", indent, german_text)
}
}
printf("%s }", indent)
# Footnotes array (if not disabled)
if (!no_footnotes && json_footnote_count[book_key, chapter, verse_num] > 0) {
printf(",\n%s \"footnotes\": [\n", indent)
for (f_idx = 1; f_idx <= json_footnote_count[book_key, chapter, verse_num]; f_idx++) {
fn_num = json_footnote_nums[book_key, chapter, verse_num, f_idx]
fn_text = json_footnotes[book_key, chapter, verse_num, fn_num]
printf("%s {\n", indent)
printf("%s \"number\": %d,\n", indent, fn_num)
printf("%s \"text\": \"%s\"\n", indent, fn_text)
if (f_idx < json_footnote_count[book_key, chapter, verse_num]) {
printf("%s },\n", indent)
} else {
printf("%s }\n", indent)
}
}
printf("%s ]\n", indent)
} else {
printf("\n")
}
# Close verse object
if (v_idx < json_verse_count[book_key, chapter]) {
printf("%s },\n", indent)
} else {
printf("%s }\n", indent)
}
}
printf("%s]\n", indent)
# Close chapter - add comma if not last chapter
if (c_idx < json_chapter_total[book_key]) {
printf(",\n")
}
}
# Close book object
if (json_book_total > 1) {
if (b_idx < json_book_total) {
printf(" },\n")
} else {
printf(" }\n")
}
} else {
print "}"
}
}
# Close array if multiple books
if (json_book_total > 1) {
print "]"
}
exit
}
# Normal text mode
if (cmd == "ref" && outputted_records == 0) {
print "Unknown reference: " ref
}
# dump mode doesn't need error handling - it outputs everything
}

View File

@@ -21,10 +21,12 @@ show_help() {
echo "usage: $(basename "$0") [flags] [reference...]"
echo
echo " -l list books"
echo " -w <n> set custom terminal width"
echo " -W no line wrap"
echo " -F no footnotes"
echo " -g show only German (no Latin)"
echo " -L show only Latin (no German)"
echo " -j output as JSON"
echo " -h show help"
echo
echo " Reference types:"
@@ -64,6 +66,14 @@ while [ $# -gt 0 ]; do
# List all book names with their abbreviations
get_data allioli.tsv | awk -v cmd=list "$(get_data allioli.awk)"
exit
elif [ "$1" = "-w" ]; then
shift
if [ $# -eq 0 ] || [ -z "$1" ]; then
echo "error: -w requires a width argument" >&2
exit 1
fi
export ALLIOLI_MAX_WIDTH="$1"
shift
elif [ "$1" = "-W" ]; then
export ALLIOLI_NOLINEWRAP=1
shift
@@ -76,6 +86,9 @@ while [ $# -gt 0 ]; do
elif [ "$1" = "-L" ]; then
export ALLIOLI_ONLY_LATIN=1
shift
elif [ "$1" = "-j" ]; then
export ALLIOLI_JSON_OUTPUT=1
shift
elif [ "$1" = "-h" ] || [ "$isFlag" -eq 1 ]; then
show_help
else
@@ -83,10 +96,12 @@ while [ $# -gt 0 ]; do
fi
done
if [ -z "$ALLIOLI_MAX_WIDTH" ]; then
cols=$(tput cols 2>/dev/null)
if [ $? -eq 0 ]; then
export ALLIOLI_MAX_WIDTH="$cols"
fi
fi
if [ $# -eq 0 ]; then
if [ ! -t 0 ]; then

45518
allioli.tsv

File diff suppressed because one or more lines are too long