Render embedded plain footnotes
This commit is contained in:
+34
-13
@@ -98,6 +98,21 @@ proc fetchPassages(reference, translation: string, cfg: CombinedConfig): seq[str
|
|||||||
"unsupported translation '" & translation &
|
"unsupported translation '" & translation &
|
||||||
"'; supported translations: " & supportedTranslationsList())
|
"'; 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:
|
when isMainModule:
|
||||||
const USAGE = """Usage:
|
const USAGE = """Usage:
|
||||||
bibleref <reference> [options]
|
bibleref <reference> [options]
|
||||||
@@ -167,19 +182,25 @@ Options:
|
|||||||
|
|
||||||
var formattedPassages: seq[string] = @[]
|
var formattedPassages: seq[string] = @[]
|
||||||
for query in queries:
|
for query in queries:
|
||||||
for passage in fetchPassages(query.referenceText, query.translation, cfg):
|
case $args["--output-format"]
|
||||||
formattedPassages.add(
|
of "plain":
|
||||||
case $args["--output-format"]:
|
formattedPassages.add(fetchPlainPassages(
|
||||||
of "plain":
|
query.referenceText,
|
||||||
formatPlain(passage, query.translation)
|
query.translation,
|
||||||
of "reading":
|
keepVerseNumbers = true,
|
||||||
formatPlain(passage, query.translation, keepVerseNumbers = false)
|
cfg))
|
||||||
of "text":
|
else:
|
||||||
passage.multiReplace([(re"\[(\d+)\]", "$1")])
|
for passage in fetchPassages(query.referenceText, query.translation, cfg):
|
||||||
of "raw":
|
formattedPassages.add(
|
||||||
passage
|
case $args["--output-format"]:
|
||||||
else:
|
of "reading":
|
||||||
formatMarkdown(passage, query.translation))
|
formatPlain(passage, query.translation, keepVerseNumbers = false)
|
||||||
|
of "text":
|
||||||
|
passage.multiReplace([(re"\[(\d+)\]", "$1")])
|
||||||
|
of "raw":
|
||||||
|
passage
|
||||||
|
else:
|
||||||
|
formatMarkdown(passage, query.translation))
|
||||||
|
|
||||||
echo formattedPassages.join("\p\p")
|
echo formattedPassages.join("\p\p")
|
||||||
|
|
||||||
|
|||||||
+207
-2
@@ -1,4 +1,4 @@
|
|||||||
import std/[strutils, tables]
|
import std/[algorithm, json, strutils, tables, wordwrap]
|
||||||
|
|
||||||
import ./reference_parser
|
import ./reference_parser
|
||||||
|
|
||||||
@@ -16,6 +16,12 @@ type
|
|||||||
breakKind: SegmentBreak
|
breakKind: SegmentBreak
|
||||||
indent: int
|
indent: int
|
||||||
text: string
|
text: string
|
||||||
|
annotations: seq[BibleAnnotation]
|
||||||
|
|
||||||
|
BibleAnnotation = object
|
||||||
|
kind: string
|
||||||
|
offset: int
|
||||||
|
targetText: string
|
||||||
|
|
||||||
BibleIndex = object
|
BibleIndex = object
|
||||||
segments: seq[BibleSegment]
|
segments: seq[BibleSegment]
|
||||||
@@ -49,6 +55,31 @@ proc parseSegmentBreak(s: string): SegmentBreak =
|
|||||||
else:
|
else:
|
||||||
raise newException(ValueError, "invalid embedded Bible segment break: " & s)
|
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) =
|
proc addSegment(index: var BibleIndex, segment: BibleSegment) =
|
||||||
index.segments.add(segment)
|
index.segments.add(segment)
|
||||||
index.verses[verseKey(segment.code, segment.chapter, segment.verse)] = true
|
index.verses[verseKey(segment.code, segment.chapter, segment.verse)] = true
|
||||||
@@ -92,7 +123,10 @@ proc loadBibleIndex(rows, translationName: string): BibleIndex =
|
|||||||
segment: parseInt(parts[3]),
|
segment: parseInt(parts[3]),
|
||||||
breakKind: parseSegmentBreak(parts[4]),
|
breakKind: parseSegmentBreak(parts[4]),
|
||||||
indent: parseInt(parts[5]),
|
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 =
|
proc requireLastChapter(index: BibleIndex, code: string): int =
|
||||||
if not index.lastChapterByBook.hasKey(code):
|
if not index.lastChapterByBook.hasKey(code):
|
||||||
@@ -245,3 +279,174 @@ proc fetchPassages*(rows, reference, translationName: string): seq[string] =
|
|||||||
let index = loadBibleIndex(rows, translationName)
|
let index = loadBibleIndex(rows, translationName)
|
||||||
for parsedReference in parseReferences(reference):
|
for parsedReference in parseReferences(reference):
|
||||||
result.add(fetchReference(index, parsedReference))
|
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] =
|
proc fetchPassages*(reference: string): seq[string] =
|
||||||
embedded_bible.fetchPassages(kjvRows, reference, "KJV")
|
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:
|
else:
|
||||||
raise newException(ValueError,
|
raise newException(ValueError,
|
||||||
"MEV data is not embedded; generate data/private/mev.tsv and rebuild")
|
"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" &
|
" [31] Then Jesus said. [32] You shall know the truth.\n" &
|
||||||
"\n" &
|
"\n" &
|
||||||
" [33] They answered Him."
|
" [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"
|
if marker.strip.allCharsInSet({'0'..'9'}): "note"
|
||||||
else: "crossReference"
|
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 =
|
proc annotationNode(marker, href, targetText: string, offset: int): JsonNode =
|
||||||
|
let kind = annotationKind(marker)
|
||||||
|
let cleanedTargetText = cleanTargetText(targetText)
|
||||||
result = %*{
|
result = %*{
|
||||||
"kind": annotationKind(marker),
|
"kind": kind,
|
||||||
"marker": marker,
|
|
||||||
"href": href,
|
|
||||||
"offset": offset
|
"offset": offset
|
||||||
}
|
}
|
||||||
if targetText.len > 0:
|
if cleanedTargetText.len > 0:
|
||||||
result["targetText"] = newJString(targetText)
|
result["targetText"] = newJString(cleanedTargetText)
|
||||||
|
|
||||||
proc segmentMetadata(state: ParseState): JsonNode =
|
proc segmentMetadata(state: ParseState): JsonNode =
|
||||||
result = newJObject()
|
result = newJObject()
|
||||||
|
|||||||
Reference in New Issue
Block a user