core: чанкер markdown по заголовкам с перекрытием
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package memo.core
|
||||
|
||||
data class Chunk(
|
||||
val heading: String,
|
||||
val line: Int,
|
||||
val text: String,
|
||||
val ord: Int,
|
||||
)
|
||||
|
||||
private const val CHUNK_SIZE = 4000
|
||||
private const val CHUNK_OVERLAP = 600
|
||||
|
||||
private val HEADING_REGEX = Regex("^(#{1,3}) (.*)$")
|
||||
|
||||
fun chunkMarkdown(text: String): List<Chunk> {
|
||||
val sections = mutableListOf<Triple<String, Int, String>>()
|
||||
|
||||
var currentHeading = ""
|
||||
var currentLine = 1
|
||||
val currentBody = StringBuilder()
|
||||
|
||||
val preamble = StringBuilder()
|
||||
var foundHeading = false
|
||||
|
||||
fun flush(heading: String, line: Int, body: String) {
|
||||
val trimmed = body.trim()
|
||||
if (trimmed.isNotEmpty()) {
|
||||
sections.add(Triple(heading, line, trimmed))
|
||||
}
|
||||
}
|
||||
|
||||
val lines = text.split('\n')
|
||||
for ((idx, raw) in lines.withIndex()) {
|
||||
val lineNo = idx + 1
|
||||
val match = HEADING_REGEX.matchEntire(raw)
|
||||
if (match != null) {
|
||||
if (!foundHeading) {
|
||||
flush("", 1, preamble.toString())
|
||||
foundHeading = true
|
||||
} else {
|
||||
flush(currentHeading, currentLine, currentBody.toString())
|
||||
}
|
||||
currentHeading = match.groupValues[2].trim()
|
||||
currentLine = lineNo
|
||||
currentBody.setLength(0)
|
||||
} else {
|
||||
if (foundHeading) {
|
||||
currentBody.append(raw).append('\n')
|
||||
} else {
|
||||
preamble.append(raw).append('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundHeading) {
|
||||
flush(currentHeading, currentLine, currentBody.toString())
|
||||
} else {
|
||||
flush("", 1, preamble.toString())
|
||||
}
|
||||
|
||||
val result = mutableListOf<Chunk>()
|
||||
var ord = 0
|
||||
for ((heading, line, body) in sections) {
|
||||
if (body.length <= CHUNK_SIZE) {
|
||||
result.add(Chunk(heading, line, body, ord++))
|
||||
} else {
|
||||
val step = CHUNK_SIZE - CHUNK_OVERLAP
|
||||
var i = 0
|
||||
while (i < body.length) {
|
||||
val end = minOf(i + CHUNK_SIZE, body.length)
|
||||
result.add(Chunk(heading, line, body.substring(i, end), ord++))
|
||||
if (end == body.length) break
|
||||
i += step
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package memo.core
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ChunkerTest {
|
||||
|
||||
@Test
|
||||
fun splitsByHeadingLevels() {
|
||||
val input = "# A\nbodyA\n## B\nbodyB\n### C\nbodyC"
|
||||
val chunks = chunkMarkdown(input)
|
||||
assertEquals(3, chunks.size)
|
||||
assertEquals(listOf("A", "B", "C"), chunks.map { it.heading })
|
||||
assertEquals(listOf(1, 3, 5), chunks.map { it.line })
|
||||
assertEquals(listOf(0, 1, 2), chunks.map { it.ord })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textBeforeFirstHeadingBecomesChunkWithEmptyHeading() {
|
||||
val input = "преамбула\n# Заголовок\nтело"
|
||||
val chunks = chunkMarkdown(input)
|
||||
assertEquals(2, chunks.size)
|
||||
assertEquals("", chunks[0].heading)
|
||||
assertEquals(1, chunks[0].line)
|
||||
assertEquals("Заголовок", chunks[1].heading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fourHashesIsNotAHeading() {
|
||||
val input = "# A\nbody\n#### не заголовок\nmore"
|
||||
val chunks = chunkMarkdown(input)
|
||||
assertEquals(1, chunks.size)
|
||||
assertEquals("A", chunks[0].heading)
|
||||
assertTrue(chunks[0].text.contains("#### не заголовок"))
|
||||
assertTrue(chunks[0].text.contains("more"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptySectionsAreDropped() {
|
||||
val input = "# A\nsome text\n## B\n## C"
|
||||
val chunks = chunkMarkdown(input)
|
||||
assertEquals(1, chunks.size)
|
||||
assertEquals("A", chunks[0].heading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longSectionIsSplitWithOverlap() {
|
||||
val body = "а".repeat(6000)
|
||||
val input = "# A\n$body"
|
||||
val chunks = chunkMarkdown(input)
|
||||
assertEquals(2, chunks.size)
|
||||
assertEquals(4000, chunks[0].text.length)
|
||||
assertEquals(2600, chunks[1].text.length)
|
||||
assertEquals(
|
||||
chunks[0].text.substring(4000 - 600),
|
||||
chunks[1].text.substring(0, 600),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user