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

This commit was merged in pull request #30.
This commit is contained in:
2026-03-17 15:35:38 +01:00
parent 032387c02d
commit 543fe66a44
2 changed files with 153 additions and 2 deletions

View File

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

View File

@@ -628,4 +628,145 @@ class ChordProParserTest {
song.sections[0].lines[1].segments[0].text shouldBe "Some text" song.sections[0].lines[1].segments[0].text shouldBe "Some text"
song.sections[0].lines[2].imagePath shouldBe "img2.png" 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
}
} }