Compare commits
3 Commits
dbc39480f7
...
4fcf5d3bed
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fcf5d3bed | |||
| 6972a4e531 | |||
| 98253920ca |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Simple Nim CLI for retrieving Biblical passages"
|
description = "Simple Nim CLI for retrieving Biblical passages"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+2
-1
@@ -138,6 +138,7 @@ Options:
|
|||||||
--api-bible-nkjv-bible-id <id>
|
--api-bible-nkjv-bible-id <id>
|
||||||
Override the API.Bible Bible ID for NKJV.
|
Override the API.Bible Bible ID for NKJV.
|
||||||
"""
|
"""
|
||||||
|
const VERSION = "1.1.1"
|
||||||
|
|
||||||
let consoleLogger = newConsoleLogger(
|
let consoleLogger = newConsoleLogger(
|
||||||
levelThreshold=lvlInfo,
|
levelThreshold=lvlInfo,
|
||||||
@@ -146,7 +147,7 @@ Options:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
let args = docopt(USAGE, version = "1.1.0")
|
let args = docopt(USAGE, version = VERSION)
|
||||||
|
|
||||||
if args["--debug"]:
|
if args["--debug"]:
|
||||||
consoleLogger.levelThreshold = lvlDebug
|
consoleLogger.levelThreshold = lvlDebug
|
||||||
|
|||||||
+157
-30
@@ -2,8 +2,24 @@ import std/[strutils, tables]
|
|||||||
|
|
||||||
import ./reference_parser
|
import ./reference_parser
|
||||||
|
|
||||||
type BibleIndex = object
|
type
|
||||||
verses: Table[string, string]
|
SegmentBreak = enum
|
||||||
|
sbSame = "same"
|
||||||
|
sbLine = "line"
|
||||||
|
sbParagraph = "paragraph"
|
||||||
|
|
||||||
|
BibleSegment = object
|
||||||
|
code: string
|
||||||
|
chapter: int
|
||||||
|
verse: int
|
||||||
|
segment: int
|
||||||
|
breakKind: SegmentBreak
|
||||||
|
indent: int
|
||||||
|
text: string
|
||||||
|
|
||||||
|
BibleIndex = object
|
||||||
|
segments: seq[BibleSegment]
|
||||||
|
verses: Table[string, bool]
|
||||||
lastVerseByChapter: Table[string, int]
|
lastVerseByChapter: Table[string, int]
|
||||||
lastChapterByBook: Table[string, int]
|
lastChapterByBook: Table[string, int]
|
||||||
translationName: string
|
translationName: string
|
||||||
@@ -14,6 +30,38 @@ proc verseKey(code: string, chapter, verse: int): string =
|
|||||||
proc chapterKey(code: string, chapter: int): string =
|
proc chapterKey(code: string, chapter: int): string =
|
||||||
code & "\t" & $chapter
|
code & "\t" & $chapter
|
||||||
|
|
||||||
|
proc legacySegment(text: string): tuple[breakKind: SegmentBreak, text: string] =
|
||||||
|
const pilcrow = "\xC2\xB6"
|
||||||
|
let stripped = text.strip(leading = true, trailing = false)
|
||||||
|
|
||||||
|
if stripped.startsWith(pilcrow):
|
||||||
|
result.breakKind = sbParagraph
|
||||||
|
result.text = stripped[pilcrow.len .. ^1].strip(leading = true, trailing = false)
|
||||||
|
else:
|
||||||
|
result.breakKind = sbSame
|
||||||
|
result.text = text
|
||||||
|
|
||||||
|
proc parseSegmentBreak(s: string): SegmentBreak =
|
||||||
|
case s
|
||||||
|
of "same": sbSame
|
||||||
|
of "line": sbLine
|
||||||
|
of "paragraph": sbParagraph
|
||||||
|
else:
|
||||||
|
raise newException(ValueError, "invalid embedded Bible segment break: " & s)
|
||||||
|
|
||||||
|
proc addSegment(index: var BibleIndex, segment: BibleSegment) =
|
||||||
|
index.segments.add(segment)
|
||||||
|
index.verses[verseKey(segment.code, segment.chapter, segment.verse)] = true
|
||||||
|
|
||||||
|
let cKey = chapterKey(segment.code, segment.chapter)
|
||||||
|
if not index.lastVerseByChapter.hasKey(cKey) or
|
||||||
|
segment.verse > index.lastVerseByChapter[cKey]:
|
||||||
|
index.lastVerseByChapter[cKey] = segment.verse
|
||||||
|
|
||||||
|
if not index.lastChapterByBook.hasKey(segment.code) or
|
||||||
|
segment.chapter > index.lastChapterByBook[segment.code]:
|
||||||
|
index.lastChapterByBook[segment.code] = segment.chapter
|
||||||
|
|
||||||
proc loadBibleIndex(rows, translationName: string): BibleIndex =
|
proc loadBibleIndex(rows, translationName: string): BibleIndex =
|
||||||
result.translationName = translationName
|
result.translationName = translationName
|
||||||
|
|
||||||
@@ -21,26 +69,30 @@ proc loadBibleIndex(rows, translationName: string): BibleIndex =
|
|||||||
if line.strip.len == 0:
|
if line.strip.len == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
let parts = line.split('\t', maxsplit = 3)
|
let parts = line.split('\t', maxsplit = 6)
|
||||||
if parts.len != 4:
|
if parts.len != 4 and parts.len != 7:
|
||||||
raise newException(ValueError,
|
raise newException(ValueError,
|
||||||
"invalid embedded " & translationName & " row: " & line)
|
"invalid embedded " & translationName & " row: " & line)
|
||||||
|
|
||||||
let code = parts[0]
|
if parts.len == 4:
|
||||||
let chapter = parseInt(parts[1])
|
let legacy = legacySegment(parts[3])
|
||||||
let verse = parseInt(parts[2])
|
result.addSegment(BibleSegment(
|
||||||
let text = parts[3]
|
code: parts[0],
|
||||||
|
chapter: parseInt(parts[1]),
|
||||||
result.verses[verseKey(code, chapter, verse)] = text
|
verse: parseInt(parts[2]),
|
||||||
|
segment: 0,
|
||||||
let cKey = chapterKey(code, chapter)
|
breakKind: legacy.breakKind,
|
||||||
if not result.lastVerseByChapter.hasKey(cKey) or
|
indent: 0,
|
||||||
verse > result.lastVerseByChapter[cKey]:
|
text: legacy.text))
|
||||||
result.lastVerseByChapter[cKey] = verse
|
else:
|
||||||
|
result.addSegment(BibleSegment(
|
||||||
if not result.lastChapterByBook.hasKey(code) or
|
code: parts[0],
|
||||||
chapter > result.lastChapterByBook[code]:
|
chapter: parseInt(parts[1]),
|
||||||
result.lastChapterByBook[code] = chapter
|
verse: parseInt(parts[2]),
|
||||||
|
segment: parseInt(parts[3]),
|
||||||
|
breakKind: parseSegmentBreak(parts[4]),
|
||||||
|
indent: parseInt(parts[5]),
|
||||||
|
text: parts[6]))
|
||||||
|
|
||||||
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):
|
||||||
@@ -62,13 +114,10 @@ proc requireVerse(index: BibleIndex, code: string, chapter, verse: int): string
|
|||||||
raise newException(ValueError,
|
raise newException(ValueError,
|
||||||
"no embedded " & index.translationName & " data for " &
|
"no embedded " & index.translationName & " data for " &
|
||||||
bookInfo(code).name & " " & $chapter & ":" & $verse)
|
bookInfo(code).name & " " & $chapter & ":" & $verse)
|
||||||
index.verses[vKey]
|
""
|
||||||
|
|
||||||
proc addVerseLines(
|
proc selectedVerses(index: BibleIndex, reference: PassageReference, range: RefRange):
|
||||||
lines: var seq[string],
|
Table[string, bool] =
|
||||||
index: BibleIndex,
|
|
||||||
reference: PassageReference,
|
|
||||||
range: RefRange) =
|
|
||||||
|
|
||||||
let code = reference.book.code
|
let code = reference.book.code
|
||||||
discard index.requireLastChapter(code)
|
discard index.requireLastChapter(code)
|
||||||
@@ -90,18 +139,96 @@ proc addVerseLines(
|
|||||||
raise newException(ValueError, "reference range starts after it ends")
|
raise newException(ValueError, "reference range starts after it ends")
|
||||||
|
|
||||||
for verse in startVerse .. endVerse:
|
for verse in startVerse .. endVerse:
|
||||||
lines.add(" [" & $verse & "] " & index.requireVerse(code, chapter, verse))
|
discard index.requireVerse(code, chapter, verse)
|
||||||
|
result[verseKey(code, chapter, verse)] = true
|
||||||
|
|
||||||
|
proc addFormattedSegment(
|
||||||
|
lines: var seq[string],
|
||||||
|
segment: BibleSegment,
|
||||||
|
emittedVerses: var Table[string, bool]) =
|
||||||
|
|
||||||
|
let vKey = verseKey(segment.code, segment.chapter, segment.verse)
|
||||||
|
let marker =
|
||||||
|
if emittedVerses.hasKey(vKey): ""
|
||||||
|
else:
|
||||||
|
emittedVerses[vKey] = true
|
||||||
|
"[" & $segment.verse & "] "
|
||||||
|
|
||||||
|
let prefix = repeat(" ", segment.indent + 1)
|
||||||
|
let content = marker & segment.text
|
||||||
|
let hasPassageLine = lines.len > 1 and lines[^1].len > 0
|
||||||
|
|
||||||
|
if segment.breakKind == sbSame and hasPassageLine:
|
||||||
|
if lines[^1].len > 0 and not lines[^1][^1].isSpaceAscii:
|
||||||
|
lines[^1].add(" ")
|
||||||
|
lines[^1].add(content)
|
||||||
|
else:
|
||||||
|
let isMarkerlessParagraphContinuation =
|
||||||
|
segment.breakKind == sbParagraph and marker.len == 0 and hasPassageLine
|
||||||
|
|
||||||
|
if isMarkerlessParagraphContinuation:
|
||||||
|
if lines[^1].len > 0 and not lines[^1][^1].isSpaceAscii:
|
||||||
|
lines[^1].add(" ")
|
||||||
|
lines[^1].add(content)
|
||||||
|
else:
|
||||||
|
if segment.breakKind == sbParagraph and hasPassageLine:
|
||||||
|
lines.add("")
|
||||||
|
lines.add(prefix & content)
|
||||||
|
|
||||||
|
proc normalizeFirstSelectedSegment(segment: BibleSegment): BibleSegment =
|
||||||
|
result = segment
|
||||||
|
if result.breakKind == sbSame:
|
||||||
|
result.breakKind = sbLine
|
||||||
|
elif result.breakKind == sbParagraph and result.segment == 0:
|
||||||
|
result.breakKind = sbLine
|
||||||
|
|
||||||
|
proc addAllSegments(lines: var seq[string], index: BibleIndex, code: string) =
|
||||||
|
var emittedVerses: Table[string, bool]
|
||||||
|
var firstSelected = true
|
||||||
|
|
||||||
|
discard index.requireLastChapter(code)
|
||||||
|
for segment in index.segments:
|
||||||
|
if segment.code == code:
|
||||||
|
var adjusted = segment
|
||||||
|
if firstSelected:
|
||||||
|
adjusted = normalizeFirstSelectedSegment(adjusted)
|
||||||
|
lines.addFormattedSegment(adjusted, emittedVerses)
|
||||||
|
firstSelected = false
|
||||||
|
|
||||||
|
proc addRangeSeparator(lines: var seq[string]) =
|
||||||
|
if lines.len > 1 and lines[^1].len > 0:
|
||||||
|
lines.add("")
|
||||||
|
|
||||||
|
proc addVerseLines(
|
||||||
|
lines: var seq[string],
|
||||||
|
index: BibleIndex,
|
||||||
|
reference: PassageReference,
|
||||||
|
range: RefRange) =
|
||||||
|
|
||||||
|
let code = reference.book.code
|
||||||
|
let selected = index.selectedVerses(reference, range)
|
||||||
|
var emittedVerses: Table[string, bool]
|
||||||
|
var firstSelected = true
|
||||||
|
|
||||||
|
for segment in index.segments:
|
||||||
|
if segment.code == code and
|
||||||
|
selected.hasKey(verseKey(segment.code, segment.chapter, segment.verse)):
|
||||||
|
var adjusted = segment
|
||||||
|
if firstSelected:
|
||||||
|
adjusted = normalizeFirstSelectedSegment(adjusted)
|
||||||
|
lines.addFormattedSegment(adjusted, emittedVerses)
|
||||||
|
firstSelected = false
|
||||||
|
|
||||||
proc fetchReference(index: BibleIndex, reference: PassageReference): string =
|
proc fetchReference(index: BibleIndex, reference: PassageReference): string =
|
||||||
var lines = @[$reference]
|
var lines = @[$reference]
|
||||||
let code = reference.book.code
|
let code = reference.book.code
|
||||||
|
|
||||||
if reference.ranges.len == 0:
|
if reference.ranges.len == 0:
|
||||||
for chapter in 1 .. index.requireLastChapter(code):
|
lines.addAllSegments(index, code)
|
||||||
for verse in 1 .. index.requireLastVerse(code, chapter):
|
|
||||||
lines.add(" [" & $verse & "] " & index.requireVerse(code, chapter, verse))
|
|
||||||
else:
|
else:
|
||||||
for range in reference.ranges:
|
for idx, range in reference.ranges:
|
||||||
|
if idx > 0:
|
||||||
|
lines.addRangeSeparator()
|
||||||
lines.addVerseLines(index, reference, range)
|
lines.addVerseLines(index, reference, range)
|
||||||
|
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import std/[strutils, unittest]
|
import std/[strutils, unittest]
|
||||||
|
|
||||||
|
import ../src/embedded_bible
|
||||||
import ../src/kjv
|
import ../src/kjv
|
||||||
import ../src/reference_parser
|
import ../src/reference_parser
|
||||||
|
|
||||||
@@ -82,3 +83,74 @@ suite "offline KJV backend":
|
|||||||
check passages.len == 1
|
check passages.len == 1
|
||||||
check passages[0].startsWith("Jude 3\n")
|
check passages[0].startsWith("Jude 3\n")
|
||||||
check passages[0].contains(" [3] ")
|
check passages[0].contains(" [3] ")
|
||||||
|
|
||||||
|
suite "embedded Bible layout":
|
||||||
|
const layoutRows = """
|
||||||
|
JHN 8 31 0 paragraph 0 Then Jesus said,
|
||||||
|
JHN 8 31 1 same 0 "If you remain in My word,
|
||||||
|
JHN 8 32 0 same 0 you shall know the truth."
|
||||||
|
JHN 8 33 0 paragraph 0 They answered Him.
|
||||||
|
JHN 8 39 0 paragraph 0 They answered Him, "Abraham is our father."
|
||||||
|
JHN 8 39 1 paragraph 0 Jesus said to them, "If you were Abraham's children,
|
||||||
|
JHN 8 40 0 same 0 you would do the works of Abraham."
|
||||||
|
HEB 4 7 0 paragraph 0 again He establishes a certain day:
|
||||||
|
HEB 4 7 1 line 0 "Today, if you will hear His voice,
|
||||||
|
HEB 4 7 2 line 1 do not harden your hearts."
|
||||||
|
"""
|
||||||
|
|
||||||
|
test "preserves inline verse markers within a paragraph":
|
||||||
|
let passages = embedded_bible.fetchPassages(layoutRows, "John 8:31-32", "TEST")
|
||||||
|
|
||||||
|
check passages.len == 1
|
||||||
|
check passages[0] == "John 8:31-32\n" &
|
||||||
|
" [31] Then Jesus said, \"If you remain in My word, [32] you shall know the truth.\""
|
||||||
|
|
||||||
|
test "preserves paragraph breaks":
|
||||||
|
let passages = embedded_bible.fetchPassages(layoutRows, "John 8:31-33", "TEST")
|
||||||
|
|
||||||
|
check passages[0] == "John 8:31-33\n" &
|
||||||
|
" [31] Then Jesus said, \"If you remain in My word, [32] you shall know the truth.\"\n" &
|
||||||
|
"\n" &
|
||||||
|
" [33] They answered Him."
|
||||||
|
|
||||||
|
test "preserves indented quote lines":
|
||||||
|
let passages = embedded_bible.fetchPassages(layoutRows, "Hebrews 4:7", "TEST")
|
||||||
|
|
||||||
|
check passages[0] == "Hebrews 4:7\n" &
|
||||||
|
" [7] again He establishes a certain day:\n" &
|
||||||
|
" \"Today, if you will hear His voice,\n" &
|
||||||
|
" do not harden your hearts.\""
|
||||||
|
|
||||||
|
test "starts mid-paragraph ranges on a fresh line":
|
||||||
|
let passages = embedded_bible.fetchPassages(layoutRows, "John 8:32-33", "TEST")
|
||||||
|
|
||||||
|
check passages[0] == "John 8:32-33\n" &
|
||||||
|
" [32] you shall know the truth.\"\n" &
|
||||||
|
"\n" &
|
||||||
|
" [33] They answered Him."
|
||||||
|
|
||||||
|
test "joins markerless paragraph continuations":
|
||||||
|
let passages = embedded_bible.fetchPassages(layoutRows, "John 8:39-40", "TEST")
|
||||||
|
|
||||||
|
check passages[0] == "John 8:39-40\n" &
|
||||||
|
" [39] They answered Him, \"Abraham is our father.\" " &
|
||||||
|
"Jesus said to them, \"If you were Abraham's children, " &
|
||||||
|
"[40] you would do the works of Abraham.\""
|
||||||
|
|
||||||
|
test "supports legacy verse rows":
|
||||||
|
const legacyRows = "JHN\t3\t16\tFor God so loved the world.\n"
|
||||||
|
let passages = embedded_bible.fetchPassages(legacyRows, "John 3:16", "TEST")
|
||||||
|
|
||||||
|
check passages[0] == "John 3:16\n [16] For God so loved the world."
|
||||||
|
|
||||||
|
test "groups legacy rows into paragraphs using pilcrows":
|
||||||
|
const legacyRows =
|
||||||
|
"JHN\t8\t31\tThen Jesus said.\n" &
|
||||||
|
"JHN\t8\t32\tYou shall know the truth.\n" &
|
||||||
|
"JHN\t8\t33\t\xC2\xB6 They answered Him.\n"
|
||||||
|
let passages = embedded_bible.fetchPassages(legacyRows, "John 8:31-33", "TEST")
|
||||||
|
|
||||||
|
check passages[0] == "John 8:31-33\n" &
|
||||||
|
" [31] Then Jesus said. [32] You shall know the truth.\n" &
|
||||||
|
"\n" &
|
||||||
|
" [33] They answered Him."
|
||||||
|
|||||||
+52
-13
@@ -84,6 +84,22 @@ proc parseVerseLine(line: string): tuple[verse: int, text: string] =
|
|||||||
result.verse = parseInt(rest[0 ..< numberEnd])
|
result.verse = parseInt(rest[0 ..< numberEnd])
|
||||||
result.text = stripUsfmMarkup(rest[numberEnd + 1 .. ^1])
|
result.text = stripUsfmMarkup(rest[numberEnd + 1 .. ^1])
|
||||||
|
|
||||||
|
proc usfmBlock(line: string): tuple[matched: bool, segmentBreak: string, indent: int] =
|
||||||
|
if line.startsWith("\\p") or line.startsWith("\\m"):
|
||||||
|
return (true, "paragraph", 0)
|
||||||
|
|
||||||
|
if line.startsWith("\\q"):
|
||||||
|
var idx = 2
|
||||||
|
var digits = ""
|
||||||
|
while idx < line.len and line[idx].isDigit:
|
||||||
|
digits.add(line[idx])
|
||||||
|
inc idx
|
||||||
|
|
||||||
|
let indent =
|
||||||
|
if digits.len > 0: parseInt(digits)
|
||||||
|
else: 1
|
||||||
|
return (true, "line", indent)
|
||||||
|
|
||||||
proc findCanonFiles(inputDir: string): Table[string, string] =
|
proc findCanonFiles(inputDir: string): Table[string, string] =
|
||||||
for path in walkFiles(inputDir / "*eng-kjv.usfm"):
|
for path in walkFiles(inputDir / "*eng-kjv.usfm"):
|
||||||
let name = path.extractFilename
|
let name = path.extractFilename
|
||||||
@@ -104,35 +120,58 @@ proc generate(inputDir, outputPath: string) =
|
|||||||
|
|
||||||
var chapter = 0
|
var chapter = 0
|
||||||
var verse = 0
|
var verse = 0
|
||||||
var verseText = ""
|
var segment = 0
|
||||||
|
var segmentText = ""
|
||||||
|
var segmentBreak = "line"
|
||||||
|
var indent = 0
|
||||||
|
|
||||||
proc flushVerse() =
|
proc flushSegment() =
|
||||||
if chapter > 0 and verse > 0:
|
if chapter > 0 and verse > 0:
|
||||||
let text = normalizeWhitespace(verseText).replace("\t", " ")
|
let text = normalizeWhitespace(segmentText).replace("\t", " ")
|
||||||
if text.len > 0:
|
if text.len > 0:
|
||||||
rows.add([code, $chapter, $verse, text].join("\t"))
|
rows.add([
|
||||||
verse = 0
|
code,
|
||||||
verseText = ""
|
$chapter,
|
||||||
|
$verse,
|
||||||
|
$segment,
|
||||||
|
segmentBreak,
|
||||||
|
$indent,
|
||||||
|
text
|
||||||
|
].join("\t"))
|
||||||
|
inc segment
|
||||||
|
segmentBreak = "same"
|
||||||
|
segmentText = ""
|
||||||
|
|
||||||
|
proc startBlock(nextBreak: string, nextIndent: int) =
|
||||||
|
flushSegment()
|
||||||
|
segmentBreak = nextBreak
|
||||||
|
indent = nextIndent
|
||||||
|
|
||||||
for rawLine in canonFiles[code].lines:
|
for rawLine in canonFiles[code].lines:
|
||||||
let line = rawLine.strip
|
let line = rawLine.strip
|
||||||
|
|
||||||
if line.startsWith("\\c "):
|
if line.startsWith("\\c "):
|
||||||
flushVerse()
|
flushSegment()
|
||||||
chapter = parseInt(line[3..^1].strip)
|
chapter = parseInt(line[3..^1].strip)
|
||||||
|
verse = 0
|
||||||
elif line.startsWith("\\v "):
|
elif line.startsWith("\\v "):
|
||||||
flushVerse()
|
flushSegment()
|
||||||
let parsed = parseVerseLine(line)
|
let parsed = parseVerseLine(line)
|
||||||
verse = parsed.verse
|
verse = parsed.verse
|
||||||
verseText = parsed.text
|
segment = 0
|
||||||
|
segmentText = parsed.text
|
||||||
elif verse > 0:
|
elif verse > 0:
|
||||||
|
let block = usfmBlock(line)
|
||||||
|
if block.matched:
|
||||||
|
startBlock(block.segmentBreak, block.indent)
|
||||||
|
|
||||||
let continued = stripUsfmMarkup(line)
|
let continued = stripUsfmMarkup(line)
|
||||||
if continued.len > 0:
|
if continued.len > 0:
|
||||||
if verseText.len > 0:
|
if segmentText.len > 0:
|
||||||
verseText.add(' ')
|
segmentText.add(' ')
|
||||||
verseText.add(continued)
|
segmentText.add(continued)
|
||||||
|
|
||||||
flushVerse()
|
flushSegment()
|
||||||
|
|
||||||
createDir(outputPath.parentDir)
|
createDir(outputPath.parentDir)
|
||||||
writeFile(outputPath, rows.join("\n") & "\n")
|
writeFile(outputPath, rows.join("\n") & "\n")
|
||||||
|
|||||||
+56
-17
@@ -25,7 +25,10 @@ type
|
|||||||
code: string
|
code: string
|
||||||
chapter: int
|
chapter: int
|
||||||
verse: int
|
verse: int
|
||||||
verseText: string
|
segment: int
|
||||||
|
segmentText: string
|
||||||
|
segmentBreak: string
|
||||||
|
indent: int
|
||||||
rows: seq[string]
|
rows: seq[string]
|
||||||
|
|
||||||
proc normalizeWhitespace(s: string): string =
|
proc normalizeWhitespace(s: string): string =
|
||||||
@@ -194,9 +197,12 @@ proc hasHref(node: XmlNode): bool =
|
|||||||
if hasHref(child):
|
if hasHref(child):
|
||||||
return true
|
return true
|
||||||
|
|
||||||
proc isBlockElement(node: XmlNode): bool =
|
proc isParagraphElement(node: XmlNode): bool =
|
||||||
node.kind == xnElement and
|
node.kind == xnElement and node.tag == "p" and
|
||||||
node.tag in ["blockquote", "br", "div", "h1", "h2", "h3", "li", "p"]
|
(node.hasClass("calibre_18") or
|
||||||
|
node.hasClass("calibre_20") or
|
||||||
|
node.hasClass("calibre_23") or
|
||||||
|
node.hasClass("calibre_34"))
|
||||||
|
|
||||||
proc chapterMarker(node: XmlNode): int =
|
proc chapterMarker(node: XmlNode): int =
|
||||||
if node.kind == xnElement and node.tag == "span" and node.hasClass("calibre1"):
|
if node.kind == xnElement and node.tag == "span" and node.hasClass("calibre1"):
|
||||||
@@ -243,13 +249,27 @@ proc leadingVerseText(s: string): tuple[verse: int, rest: string] =
|
|||||||
if idx < text.len:
|
if idx < text.len:
|
||||||
result.rest = text[idx .. ^1]
|
result.rest = text[idx .. ^1]
|
||||||
|
|
||||||
proc flushVerse(state: var ParseState) =
|
proc flushSegment(state: var ParseState) =
|
||||||
if state.chapter > 0 and state.verse > 0:
|
if state.chapter > 0 and state.verse > 0:
|
||||||
let text = normalizeWhitespace(state.verseText).replace("\t", " ")
|
let text = normalizeWhitespace(state.segmentText).replace("\t", " ")
|
||||||
if text.len > 0:
|
if text.len > 0:
|
||||||
state.rows.add([state.code, $state.chapter, $state.verse, text].join("\t"))
|
state.rows.add([
|
||||||
|
state.code,
|
||||||
|
$state.chapter,
|
||||||
|
$state.verse,
|
||||||
|
$state.segment,
|
||||||
|
state.segmentBreak,
|
||||||
|
$state.indent,
|
||||||
|
text
|
||||||
|
].join("\t"))
|
||||||
|
inc state.segment
|
||||||
|
state.segmentBreak = "same"
|
||||||
|
|
||||||
state.verseText = ""
|
state.segmentText = ""
|
||||||
|
|
||||||
|
proc startBlock(state: var ParseState, segmentBreak: string) =
|
||||||
|
state.flushSegment()
|
||||||
|
state.segmentBreak = segmentBreak
|
||||||
|
|
||||||
proc walkPassageText(node: XmlNode, state: var ParseState) =
|
proc walkPassageText(node: XmlNode, state: var ParseState) =
|
||||||
case node.kind
|
case node.kind
|
||||||
@@ -259,13 +279,14 @@ proc walkPassageText(node: XmlNode, state: var ParseState) =
|
|||||||
let leading = leadingVerseText(node.text)
|
let leading = leadingVerseText(node.text)
|
||||||
if leading.verse > 0:
|
if leading.verse > 0:
|
||||||
state.verse = leading.verse
|
state.verse = leading.verse
|
||||||
state.verseText.add(leading.rest)
|
state.segment = 0
|
||||||
|
state.segmentText.add(leading.rest)
|
||||||
elif state.verse > 0:
|
elif state.verse > 0:
|
||||||
state.verseText.add(node.text)
|
state.segmentText.add(node.text)
|
||||||
of xnElement:
|
of xnElement:
|
||||||
let headingChapter = headingChapterMarker(node, state.code)
|
let headingChapter = headingChapterMarker(node, state.code)
|
||||||
if headingChapter > 0:
|
if headingChapter > 0:
|
||||||
state.flushVerse()
|
state.flushSegment()
|
||||||
state.chapter = headingChapter
|
state.chapter = headingChapter
|
||||||
state.verse = 0
|
state.verse = 0
|
||||||
return
|
return
|
||||||
@@ -275,25 +296,43 @@ proc walkPassageText(node: XmlNode, state: var ParseState) =
|
|||||||
|
|
||||||
let chapter = chapterMarker(node)
|
let chapter = chapterMarker(node)
|
||||||
if chapter > 0:
|
if chapter > 0:
|
||||||
state.flushVerse()
|
state.flushSegment()
|
||||||
state.chapter = chapter
|
state.chapter = chapter
|
||||||
state.verse = 1
|
state.verse = 1
|
||||||
|
state.segment = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
let verse = verseMarker(node)
|
let verse = verseMarker(node)
|
||||||
if verse > 0:
|
if verse > 0:
|
||||||
state.flushVerse()
|
state.flushSegment()
|
||||||
state.verse = verse
|
state.verse = verse
|
||||||
|
state.segment = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
if node.tag == "sup":
|
if node.tag == "sup":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if node.tag == "p":
|
||||||
|
let segmentBreak =
|
||||||
|
if node.isParagraphElement: "paragraph"
|
||||||
|
else: "line"
|
||||||
|
state.startBlock(segmentBreak)
|
||||||
|
for child in node.items:
|
||||||
|
walkPassageText(child, state)
|
||||||
|
state.flushSegment()
|
||||||
|
elif node.tag == "blockquote":
|
||||||
|
let oldIndent = state.indent
|
||||||
|
state.indent = oldIndent + 1
|
||||||
|
state.startBlock("line")
|
||||||
|
for child in node.items:
|
||||||
|
walkPassageText(child, state)
|
||||||
|
state.flushSegment()
|
||||||
|
state.indent = oldIndent
|
||||||
|
elif node.tag == "br":
|
||||||
|
state.startBlock("line")
|
||||||
|
else:
|
||||||
for child in node.items:
|
for child in node.items:
|
||||||
walkPassageText(child, state)
|
walkPassageText(child, state)
|
||||||
|
|
||||||
if node.isBlockElement and state.chapter > 0 and state.verse > 0:
|
|
||||||
state.verseText.add(' ')
|
|
||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
@@ -310,7 +349,7 @@ proc parseBook(epubPath: string, source: BookSource): seq[string] =
|
|||||||
let doc = parseHtml(newStringStream(html))
|
let doc = parseHtml(newStringStream(html))
|
||||||
walkPassageText(doc, state)
|
walkPassageText(doc, state)
|
||||||
|
|
||||||
state.flushVerse()
|
state.flushSegment()
|
||||||
state.rows
|
state.rows
|
||||||
|
|
||||||
proc generate(epubPath, outputPath: string) =
|
proc generate(epubPath, outputPath: string) =
|
||||||
|
|||||||
Reference in New Issue
Block a user