fix: blank lines split implicit verses into separate sections (Closes #29)

The ChordPro parser previously skipped all blank lines unconditionally,
causing songs without explicit {start_of_verse}/{end_of_verse} directives
to be parsed as a single section. This adds an explicitSection flag to
track whether a section was started by an explicit directive or implicitly.
Blank lines now flush implicit sections, allowing the next lyric line to
start a new implicit VERSE section. Explicit sections still ignore blank
lines as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-17 15:35:15 +01:00
parent 032387c02d
commit f64e5e2818
2 changed files with 153 additions and 2 deletions

View File

@@ -24,6 +24,7 @@ object ChordProParser {
var currentType: SectionType? = null
var currentLabel: String? = null
var currentLines = mutableListOf<SongLine>()
var explicitSection = false
// Notes block state
var inNotesBlock = false
@@ -42,6 +43,7 @@ object ChordProParser {
currentType = null
currentLabel = null
currentLines = mutableListOf()
explicitSection = false
}
}
@@ -72,8 +74,13 @@ object ChordProParser {
// Skip comments
if (line.trimStart().startsWith("#")) continue
// Skip empty lines
if (line.isBlank()) continue
// Blank line: flush implicit sections, skip otherwise
if (line.isBlank()) {
if (currentType != null && !explicitSection) {
flushSection()
}
continue
}
// Directive line
if (line.trimStart().startsWith("{") && line.trimEnd().endsWith("}")) {
@@ -109,6 +116,7 @@ object ChordProParser {
flushSection()
currentType = SectionType.VERSE
currentLabel = value
explicitSection = true
}
"end_of_verse", "eov" -> {
flushSection()
@@ -117,6 +125,7 @@ object ChordProParser {
flushSection()
currentType = SectionType.CHORUS
currentLabel = value
explicitSection = true
}
"end_of_chorus", "eoc" -> {
flushSection()
@@ -125,6 +134,7 @@ object ChordProParser {
flushSection()
currentType = SectionType.REPEAT
currentLabel = value
explicitSection = true
}
"end_of_repeat", "eor" -> {
flushSection()

View File

@@ -628,4 +628,145 @@ class ChordProParserTest {
song.sections[0].lines[1].segments[0].text shouldBe "Some text"
song.sections[0].lines[2].imagePath shouldBe "img2.png"
}
@Test
fun `blank line splits implicit verses into separate sections`() {
val input = """
{title: Am Brunnen vor dem Tore}
Am [D]Brunnen vor dem Tore
Ich [D]träumt in seinem Schatten
Ich musst auch heute wandern
Da hab ich noch im Dunkeln
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 2
song.sections[0].type shouldBe SectionType.VERSE
song.sections[0].lines shouldHaveSize 2
song.sections[0].lines[0].segments shouldHaveSize 2
song.sections[0].lines[0].segments[0].chord.shouldBeNull()
song.sections[0].lines[0].segments[0].text shouldBe "Am "
song.sections[0].lines[0].segments[1].chord shouldBe "D"
song.sections[0].lines[0].segments[1].text shouldBe "Brunnen vor dem Tore"
song.sections[1].type shouldBe SectionType.VERSE
song.sections[1].lines shouldHaveSize 2
song.sections[1].lines[0].segments[0].text shouldBe "Ich musst auch heute wandern"
}
@Test
fun `blank lines within explicit sections are ignored`() {
val input = """
{title: Song}
{start_of_verse: Verse 1}
Line one
Line two
{end_of_verse}
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 1
song.sections[0].type shouldBe SectionType.VERSE
song.sections[0].label shouldBe "Verse 1"
song.sections[0].lines shouldHaveSize 2
}
@Test
fun `blank lines between metadata do not create empty sections`() {
val input = """
{title: Song}
{lyricist: Someone}
{composer: Someone Else}
[Am]Hello world
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 1
song.sections[0].type shouldBe SectionType.VERSE
song.sections[0].lines shouldHaveSize 1
}
@Test
fun `mixed explicit and implicit sections with blank lines`() {
val input = """
{title: Song}
{start_of_chorus}
[C]Chorus line
Still in chorus
{end_of_chorus}
Implicit verse one
Implicit verse two
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 3
song.sections[0].type shouldBe SectionType.CHORUS
song.sections[0].lines shouldHaveSize 2
song.sections[1].type shouldBe SectionType.VERSE
song.sections[1].lines shouldHaveSize 1
song.sections[1].lines[0].segments[0].text shouldBe "Implicit verse one"
song.sections[2].type shouldBe SectionType.VERSE
song.sections[2].lines shouldHaveSize 1
song.sections[2].lines[0].segments[0].text shouldBe "Implicit verse two"
}
@Test
fun `multiple blank lines between implicit verses`() {
val input = """
{title: Song}
First verse line
Second verse line
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 2
song.sections[0].type shouldBe SectionType.VERSE
song.sections[0].lines shouldHaveSize 1
song.sections[1].type shouldBe SectionType.VERSE
song.sections[1].lines shouldHaveSize 1
}
@Test
fun `three implicit verses separated by blank lines`() {
val input = """
{title: Song}
[Am]Verse one line one
Verse one line two
[C]Verse two line one
Verse two line two
[G]Verse three line one
Verse three line two
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 3
song.sections.forEach { section ->
section.type shouldBe SectionType.VERSE
section.lines shouldHaveSize 2
}
}
@Test
fun `blank lines within explicit chorus are ignored`() {
val input = """
{title: Song}
{start_of_chorus}
Line one
Line two
Line three
{end_of_chorus}
""".trimIndent()
val song = ChordProParser.parse(input)
song.sections shouldHaveSize 1
song.sections[0].type shouldBe SectionType.CHORUS
song.sections[0].lines shouldHaveSize 3
}
}