Render embedded plain footnotes
This commit is contained in:
+23
-2
@@ -98,6 +98,21 @@ proc fetchPassages(reference, translation: string, cfg: CombinedConfig): seq[str
|
||||
"unsupported translation '" & translation &
|
||||
"'; supported translations: " & supportedTranslationsList())
|
||||
|
||||
proc fetchPlainPassages(
|
||||
reference,
|
||||
translation: string,
|
||||
keepVerseNumbers: bool,
|
||||
cfg: CombinedConfig): seq[string] =
|
||||
|
||||
case translation
|
||||
of "akjv", "kjv":
|
||||
result = kjv.fetchPlainPassages(reference, keepVerseNumbers)
|
||||
of "mev":
|
||||
result = mev.fetchPlainPassages(reference, keepVerseNumbers)
|
||||
else:
|
||||
for passage in fetchPassages(reference, translation, cfg):
|
||||
result.add(formatPlain(passage, translation, keepVerseNumbers))
|
||||
|
||||
when isMainModule:
|
||||
const USAGE = """Usage:
|
||||
bibleref <reference> [options]
|
||||
@@ -167,11 +182,17 @@ Options:
|
||||
|
||||
var formattedPassages: seq[string] = @[]
|
||||
for query in queries:
|
||||
case $args["--output-format"]
|
||||
of "plain":
|
||||
formattedPassages.add(fetchPlainPassages(
|
||||
query.referenceText,
|
||||
query.translation,
|
||||
keepVerseNumbers = true,
|
||||
cfg))
|
||||
else:
|
||||
for passage in fetchPassages(query.referenceText, query.translation, cfg):
|
||||
formattedPassages.add(
|
||||
case $args["--output-format"]:
|
||||
of "plain":
|
||||
formatPlain(passage, query.translation)
|
||||
of "reading":
|
||||
formatPlain(passage, query.translation, keepVerseNumbers = false)
|
||||
of "text":
|
||||
|
||||
+207
-2
@@ -1,4 +1,4 @@
|
||||
import std/[strutils, tables]
|
||||
import std/[algorithm, json, strutils, tables, wordwrap]
|
||||
|
||||
import ./reference_parser
|
||||
|
||||
@@ -16,6 +16,12 @@ type
|
||||
breakKind: SegmentBreak
|
||||
indent: int
|
||||
text: string
|
||||
annotations: seq[BibleAnnotation]
|
||||
|
||||
BibleAnnotation = object
|
||||
kind: string
|
||||
offset: int
|
||||
targetText: string
|
||||
|
||||
BibleIndex = object
|
||||
segments: seq[BibleSegment]
|
||||
@@ -49,6 +55,31 @@ proc parseSegmentBreak(s: string): SegmentBreak =
|
||||
else:
|
||||
raise newException(ValueError, "invalid embedded Bible segment break: " & s)
|
||||
|
||||
proc parseAnnotations(metadata: string): seq[BibleAnnotation] =
|
||||
if metadata.strip.len == 0:
|
||||
return
|
||||
|
||||
let node = parseJson(metadata)
|
||||
if node.kind != JObject or not node.hasKey("annotations"):
|
||||
return
|
||||
|
||||
for item in node["annotations"].getElems:
|
||||
let targetText =
|
||||
if item.hasKey("targetText"): item["targetText"].getStr
|
||||
else: ""
|
||||
|
||||
if targetText.strip.len == 0:
|
||||
continue
|
||||
|
||||
result.add(BibleAnnotation(
|
||||
kind:
|
||||
if item.hasKey("kind"): item["kind"].getStr
|
||||
else: "",
|
||||
offset:
|
||||
if item.hasKey("offset"): item["offset"].getInt
|
||||
else: 0,
|
||||
targetText: targetText))
|
||||
|
||||
proc addSegment(index: var BibleIndex, segment: BibleSegment) =
|
||||
index.segments.add(segment)
|
||||
index.verses[verseKey(segment.code, segment.chapter, segment.verse)] = true
|
||||
@@ -92,7 +123,10 @@ proc loadBibleIndex(rows, translationName: string): BibleIndex =
|
||||
segment: parseInt(parts[3]),
|
||||
breakKind: parseSegmentBreak(parts[4]),
|
||||
indent: parseInt(parts[5]),
|
||||
text: parts[6]))
|
||||
text: parts[6],
|
||||
annotations:
|
||||
if parts.len == 8: parseAnnotations(parts[7])
|
||||
else: @[]))
|
||||
|
||||
proc requireLastChapter(index: BibleIndex, code: string): int =
|
||||
if not index.lastChapterByBook.hasKey(code):
|
||||
@@ -245,3 +279,174 @@ proc fetchPassages*(rows, reference, translationName: string): seq[string] =
|
||||
let index = loadBibleIndex(rows, translationName)
|
||||
for parsedReference in parseReferences(reference):
|
||||
result.add(fetchReference(index, parsedReference))
|
||||
|
||||
proc footnoteLabel(index: int): string =
|
||||
var n = index
|
||||
while true:
|
||||
result.insert($char(ord('a') + (n mod 26)), 0)
|
||||
n = (n div 26) - 1
|
||||
if n < 0:
|
||||
break
|
||||
|
||||
proc addSegmentWithFootnotes(
|
||||
line: var string,
|
||||
segment: BibleSegment,
|
||||
keepVerseNumbers: bool,
|
||||
notes: var seq[string]) =
|
||||
|
||||
var text = segment.text
|
||||
var annotations = segment.annotations
|
||||
annotations.sort(proc(a, b: BibleAnnotation): int = cmp(a.offset, b.offset))
|
||||
|
||||
var inserted = 0
|
||||
for annotation in annotations:
|
||||
let label = footnoteLabel(notes.len)
|
||||
notes.add(annotation.targetText)
|
||||
|
||||
let insertAt = max(0, min(text.len, annotation.offset + inserted))
|
||||
let marker = "[" & label & "]"
|
||||
text.insert(marker, insertAt)
|
||||
inserted += marker.len
|
||||
|
||||
let marker =
|
||||
if keepVerseNumbers: $segment.verse & " "
|
||||
else: ""
|
||||
|
||||
let content = marker & text
|
||||
if line.len == 0:
|
||||
line.add(repeat(" ", segment.indent) & content)
|
||||
elif segment.breakKind == sbSame:
|
||||
line.appendSameLine(content)
|
||||
else:
|
||||
line.add("\n" & repeat(" ", segment.indent) & content)
|
||||
|
||||
proc addFootnotes(lines: var seq[string], notes: seq[string]) =
|
||||
if notes.len == 0:
|
||||
return
|
||||
|
||||
lines.add("")
|
||||
for idx, note in notes:
|
||||
lines.add(footnoteLabel(idx) & ". " & note)
|
||||
|
||||
proc selectedChapters(index: BibleIndex, reference: PassageReference):
|
||||
seq[tuple[chapter: int, verses: Table[string, bool]]] =
|
||||
|
||||
let code = reference.book.code
|
||||
|
||||
if reference.ranges.len == 0:
|
||||
for chapter in 1 .. index.requireLastChapter(code):
|
||||
var verses: Table[string, bool]
|
||||
for verse in 1 .. index.requireLastVerse(code, chapter):
|
||||
discard index.requireVerse(code, chapter, verse)
|
||||
verses[verseKey(code, chapter, verse)] = true
|
||||
result.add((chapter, verses))
|
||||
else:
|
||||
var chapterPositions: Table[int, int]
|
||||
for range in reference.ranges:
|
||||
let selected = index.selectedVerses(reference, range)
|
||||
for key in selected.keys:
|
||||
let parts = key.split('\t')
|
||||
let chapter = parseInt(parts[1])
|
||||
if not chapterPositions.hasKey(chapter):
|
||||
chapterPositions[chapter] = result.len
|
||||
var verses: Table[string, bool]
|
||||
result.add((chapter, verses))
|
||||
result[chapterPositions[chapter]].verses[key] = true
|
||||
|
||||
proc plainChapter(
|
||||
index: BibleIndex,
|
||||
code: string,
|
||||
selected: Table[string, bool],
|
||||
keepVerseNumbers: bool): string =
|
||||
|
||||
var lines: seq[string]
|
||||
var currentLine = ""
|
||||
var notes: seq[string]
|
||||
var emittedVerses: Table[string, bool]
|
||||
var firstSelected = true
|
||||
|
||||
proc flushLine() =
|
||||
if currentLine.len > 0:
|
||||
lines.add(currentLine)
|
||||
currentLine = ""
|
||||
|
||||
for segment in index.segments:
|
||||
if segment.code != code:
|
||||
continue
|
||||
|
||||
let key = verseKey(segment.code, segment.chapter, segment.verse)
|
||||
if not selected.hasKey(key):
|
||||
continue
|
||||
|
||||
var adjusted = segment
|
||||
if firstSelected:
|
||||
adjusted = normalizeFirstSelectedSegment(adjusted)
|
||||
|
||||
let startsNewParagraph =
|
||||
adjusted.breakKind == sbParagraph and
|
||||
not firstSelected and
|
||||
(not emittedVerses.hasKey(key))
|
||||
|
||||
if startsNewParagraph:
|
||||
flushLine()
|
||||
lines.add("")
|
||||
elif adjusted.breakKind == sbLine:
|
||||
flushLine()
|
||||
|
||||
let marker =
|
||||
if emittedVerses.hasKey(key): false
|
||||
else:
|
||||
emittedVerses[key] = true
|
||||
true
|
||||
|
||||
var renderSegment = adjusted
|
||||
if not marker:
|
||||
renderSegment.verse = 0
|
||||
|
||||
if marker:
|
||||
currentLine.addSegmentWithFootnotes(renderSegment, keepVerseNumbers, notes)
|
||||
else:
|
||||
var noVerseSegment = renderSegment
|
||||
noVerseSegment.text = renderSegment.text
|
||||
noVerseSegment.breakKind = sbSame
|
||||
currentLine.addSegmentWithFootnotes(noVerseSegment, false, notes)
|
||||
|
||||
firstSelected = false
|
||||
|
||||
flushLine()
|
||||
lines.addFootnotes(notes)
|
||||
|
||||
var wrapped: seq[string]
|
||||
for line in lines:
|
||||
if line.len == 0:
|
||||
wrapped.add("")
|
||||
continue
|
||||
|
||||
let prepared =
|
||||
if line.len > 90: line.strip
|
||||
else: line & " "
|
||||
wrapped.add(wrapWords(prepared, maxLineWidth = 74, newLine = "\p"))
|
||||
wrapped.join("\p")
|
||||
|
||||
proc fetchPlainPassages*(
|
||||
rows,
|
||||
reference,
|
||||
translationName: string,
|
||||
keepVerseNumbers = true): seq[string] =
|
||||
|
||||
let index = loadBibleIndex(rows, translationName)
|
||||
for parsedReference in parseReferences(reference):
|
||||
var parts: seq[string]
|
||||
for chapter in index.selectedChapters(parsedReference):
|
||||
parts.add(index.plainChapter(
|
||||
parsedReference.book.code,
|
||||
chapter.verses,
|
||||
keepVerseNumbers))
|
||||
|
||||
let body = parts.join("\p\p")
|
||||
let referenceSeparator =
|
||||
if body.contains("\pa. "): "\p\p"
|
||||
else: "\p"
|
||||
|
||||
result.add(body & referenceSeparator & "– " & $parsedReference & " (" &
|
||||
translationName.toUpperAscii & ")")
|
||||
|
||||
@@ -5,3 +5,6 @@ const kjvRows = embeddedTranslationData("kjv")
|
||||
|
||||
proc fetchPassages*(reference: string): seq[string] =
|
||||
embedded_bible.fetchPassages(kjvRows, reference, "KJV")
|
||||
|
||||
proc fetchPlainPassages*(reference: string, keepVerseNumbers = true): seq[string] =
|
||||
embedded_bible.fetchPlainPassages(kjvRows, reference, "KJV", keepVerseNumbers)
|
||||
|
||||
@@ -11,3 +11,10 @@ proc fetchPassages*(reference: string): seq[string] =
|
||||
else:
|
||||
raise newException(ValueError,
|
||||
"MEV data is not embedded; generate data/private/mev.tsv and rebuild")
|
||||
|
||||
proc fetchPlainPassages*(reference: string, keepVerseNumbers = true): seq[string] =
|
||||
when hasEmbeddedTranslationData("mev"):
|
||||
embedded_bible.fetchPlainPassages(mevRows, reference, "MEV", keepVerseNumbers)
|
||||
else:
|
||||
raise newException(ValueError,
|
||||
"MEV data is not embedded; generate data/private/mev.tsv and rebuild")
|
||||
|
||||
@@ -170,3 +170,37 @@ HEB 4 7 2 line 1 do not harden your hearts."
|
||||
" [31] Then Jesus said. [32] You shall know the truth.\n" &
|
||||
"\n" &
|
||||
" [33] They answered Him."
|
||||
|
||||
test "plain output renders annotation footnotes":
|
||||
const rows =
|
||||
"JHN\t8\t1\t0\tparagraph\t0\tBut Jesus went to the Mount of Olives.\t" &
|
||||
"""{"annotations":[{"kind":"crossReference","marker":"a","offset":40,"targetText":"Mt 21:1"}]}""" & "\n" &
|
||||
"JHN\t8\t2\t0\tsame\t0\tEarly in the morning He returned to the temple.\t" &
|
||||
"""{"annotations":[{"kind":"crossReference","marker":"a","offset":49,"targetText":"Mt 26:55; Lk 4:20"}]}""" & "\n"
|
||||
let passages = embedded_bible.fetchPlainPassages(rows, "John 8:1-2", "TEST")
|
||||
|
||||
check passages[0] == "1 But Jesus went to the Mount of Olives.[a] 2 Early in the morning He\n" &
|
||||
"returned to the temple.[b]\n" &
|
||||
"\n" &
|
||||
"a. Mt 21:1\n" &
|
||||
"b. Mt 26:55; Lk 4:20\n" &
|
||||
"\n" &
|
||||
"– John 8:1-2 (TEST)"
|
||||
|
||||
test "plain output resets footnote labels per chapter":
|
||||
const rows =
|
||||
"JHN\t8\t59\t0\tparagraph\t0\tThey took up stones.\t" &
|
||||
"""{"annotations":[{"kind":"crossReference","marker":"a","offset":21,"targetText":"Lev 24:16"}]}""" & "\n" &
|
||||
"JHN\t9\t1\t0\tparagraph\t0\tAs Jesus passed by.\t" &
|
||||
"""{"annotations":[{"kind":"crossReference","marker":"a","offset":20,"targetText":"Jn 8:12"}]}""" & "\n"
|
||||
let passages = embedded_bible.fetchPlainPassages(rows, "John 8:59-9:1", "TEST")
|
||||
|
||||
check passages[0] == "59 They took up stones.[a]\n" &
|
||||
"\n" &
|
||||
"a. Lev 24:16\n" &
|
||||
"\n" &
|
||||
"1 As Jesus passed by.[a]\n" &
|
||||
"\n" &
|
||||
"a. Jn 8:12\n" &
|
||||
"\n" &
|
||||
"– John 8:59-9:1 (TEST)"
|
||||
|
||||
@@ -273,15 +273,24 @@ proc annotationKind(marker: string): string =
|
||||
if marker.strip.allCharsInSet({'0'..'9'}): "note"
|
||||
else: "crossReference"
|
||||
|
||||
proc cleanTargetText(targetText: string): string =
|
||||
var parts = targetText.splitWhitespace
|
||||
if parts.len >= 3 and parts[0].allCharsInSet({'0'..'9'}):
|
||||
parts.delete(0)
|
||||
parts.delete(0)
|
||||
return parts.join(" ")
|
||||
|
||||
targetText
|
||||
|
||||
proc annotationNode(marker, href, targetText: string, offset: int): JsonNode =
|
||||
let kind = annotationKind(marker)
|
||||
let cleanedTargetText = cleanTargetText(targetText)
|
||||
result = %*{
|
||||
"kind": annotationKind(marker),
|
||||
"marker": marker,
|
||||
"href": href,
|
||||
"kind": kind,
|
||||
"offset": offset
|
||||
}
|
||||
if targetText.len > 0:
|
||||
result["targetText"] = newJString(targetText)
|
||||
if cleanedTargetText.len > 0:
|
||||
result["targetText"] = newJString(cleanedTargetText)
|
||||
|
||||
proc segmentMetadata(state: ParseState): JsonNode =
|
||||
result = newJObject()
|
||||
|
||||
Reference in New Issue
Block a user