Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ internal class SefariaBookPayloadReader(
if (depth == 0 || (leafPrimitive != null && leafPrimitive.isString)) {
val content = leafPrimitive?.takeIf { it.isString }?.content
if (!content.isNullOrEmpty()) {
val cleaned = cleanSefariaLine(content)
val cleaned = SefariaDashlessDibburim.separate(bookHeTitle, cleanSefariaLine(content))
if (cleaned.isNotEmpty()) {
output += linePrefix + cleaned
if (cleanShifts != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.github.kdroidfilter.seforimlibrary.sefariasqlite

import co.touchlab.kermit.Logger
import io.github.kdroidfilter.seforimlibrary.common.dh.DhExtractor
import java.util.concurrent.ConcurrentHashMap

/**
* Sefaria's Talmud commentaries separate the dibbur hamatchil from the comment
* with a spaced dash (`דיבור – פירוש`). The Tosafot volumes listed here end it
* with a period instead, so neither the reader nor the `line_dh` index can
* tell the dibbur from the first sentence. Rewrites the first `. ` of such a
* line to ` – ` so these volumes read and index like the rest.
*/
internal object SefariaDashlessDibburim {

/** Sefaria `heTitle`s whose lines end the dibbur with a period (≥ 88% of content lines fit). */
val bookHeTitles: Set<String> = setOf(
"תוספות על בבא בתרא",
"תוספות על מנחות",
"תוספות על נדה",
"תוספות על שבועות",
"תוספות על סוכה",
"תוספות על ראש השנה",
"תוספות על מועד קטן",
"תוספות על ביצה",
"תוספות על חגיגה",
"תוספות על תענית",
"תוספות על מכות",
"תוספות על הוריות",
)

/** Longer first sentences are commentary, not a quoted dibbur. */
private const val MAX_DIBBUR_WORDS = 12

private const val SEPARATOR = " – "

private val SPACED_DASH = Regex("""\s[-–—]\s""")
private val WHITESPACE = Regex("""\s+""")

private val separatedByBook = ConcurrentHashMap<String, Int>()

/** Returns [line] with its dibbur separated by a dash, or unchanged when the book or line does not fit. */
fun separate(bookHeTitle: String, line: String): String {
if (bookHeTitle !in bookHeTitles) return line
if (line.startsWith("<h", ignoreCase = true) || SPACED_DASH.containsMatchIn(line)) return line
val cut = line.indexOf(". ")
if (cut <= 0) return line
val dibbur = line.substring(0, cut)
if ('<' in dibbur) return line
val words = WHITESPACE.split(dibbur.trim()).count { it.isNotEmpty() }
if (words !in 1..MAX_DIBBUR_WORDS) return line
val comment = line.substring(cut + 2)
if (comment.isBlank()) return line
val separated = dibbur + SEPARATOR + comment
// Keep this source repair aligned with the downstream index. In
// particular, structural markers such as `מתני'` and `(הג"ה` must not
// be rewritten merely because they happen to end with a period.
if (DhExtractor.extract(separated, DhExtractor.Format.DASH) == null) return line
separatedByBook.merge(bookHeTitle, 1, Int::plus)
return separated
}

/** Starts a fresh per-import summary; this object also serves reusable readers in the same JVM. */
fun resetSummary() = separatedByBook.clear()

fun logSummary(logger: Logger) {
for (title in bookHeTitles) {
val n = separatedByBook[title] ?: 0
logger.i { "Dashless dibburim: '$title' — $n lines separated" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class SefariaDirectImporter(
private set

suspend fun import() = coroutineScope {
SefariaDashlessDibburim.resetSummary()
val dbRoot = findDatabaseExportRoot(exportRoot)
val jsonDir = dbRoot.resolve("json")
val schemaDir = dbRoot.resolve("schemas")
Expand Down Expand Up @@ -448,6 +449,7 @@ class SefariaDirectImporter(
}

logger.i { "Inserted all books and lines" }
SefariaDashlessDibburim.logSummary(logger)

// Build the title→bookId index in two global phases (all primaries, then
// all aliases) so a primary title always beats any alias regardless of
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.github.kdroidfilter.seforimlibrary.sefariasqlite

import kotlin.test.Test
import kotlin.test.assertEquals

class SefariaDashlessDibburimTest {

private val book = "תוספות על סוכה"

@Test
fun `first sentence becomes the dibbur, separated by a spaced en dash`() {
// Verbatim from Tosafot on Sukkah 2a in seforim.db.
assertEquals(
"מאי שנא גבי סוכה דתני פסולה ומאי שנא גבי מבוי דתני תקנתא – והא דלא פריך מאי שנא גבי הדס",
SefariaDashlessDibburim.separate(
book,
"מאי שנא גבי סוכה דתני פסולה ומאי שנא גבי מבוי דתני תקנתא. והא דלא פריך מאי שנא גבי הדס",
),
)
}

@Test
fun `only the first period is touched`() {
assertEquals(
"אמר רבה – קצת קשה. ותירץ דאין הכי נמי.",
SefariaDashlessDibburim.separate(book, "אמר רבה. קצת קשה. ותירץ דאין הכי נמי."),
)
}

@Test
fun `a line that already has a dash is left alone`() {
val line = "סוכה דאורייתא תני פסולה – פרש\"י דשייך למיתני בה לשון פסול. ועוד"
assertEquals(line, SefariaDashlessDibburim.separate(book, line))
}

@Test
fun `a long first sentence is commentary, not a dibbur`() {
val line = "ואם תאמר מאי שנא גבי סוכה דתני פסולה ומאי שנא גבי מבוי דתני תקנתא והא דלא פריך. ויש לומר"
assertEquals(line, SefariaDashlessDibburim.separate(book, line))
}

@Test
fun `headings, tagged prefixes and lines without a sentence break are left alone`() {
assertEquals("<h2>דף ב.</h2>", SefariaDashlessDibburim.separate(book, "<h2>דף ב.</h2>"))
assertEquals("<b>תוספות</b>. ביאור", SefariaDashlessDibburim.separate(book, "<b>תוספות</b>. ביאור"))
assertEquals("תוספות", SefariaDashlessDibburim.separate(book, "תוספות"))
assertEquals("אמר רבה. ", SefariaDashlessDibburim.separate(book, "אמר רבה. "))
}

@Test
fun `structural markers from the real corpus are not rewritten as dibburim`() {
// Verbatim from Tosafot on Bava Batra 139b and 163b in seforim.db.
val mishnah = "מתני'. והבנות יזונו. מה שהזכיר רשב\"ם"
val gloss = "(הג\"ה. שיטה ומחצה. נראה ליישב כגון שחתומים עדים"
assertEquals(mishnah, SefariaDashlessDibburim.separate("תוספות על בבא בתרא", mishnah))
assertEquals(gloss, SefariaDashlessDibburim.separate("תוספות על בבא בתרא", gloss))
}

@Test
fun `books outside the list are never changed`() {
val line = "מאימתי קורין. משעה שהכהנים נכנסין לאכול"
assertEquals(line, SefariaDashlessDibburim.separate("רש\"י על ברכות", line))
}
}