diff --git a/parser/src/main/kotlin/de/pfadfinder/songbook/parser/ChordProParser.kt b/parser/src/main/kotlin/de/pfadfinder/songbook/parser/ChordProParser.kt index 83c84da..021db13 100644 --- a/parser/src/main/kotlin/de/pfadfinder/songbook/parser/ChordProParser.kt +++ b/parser/src/main/kotlin/de/pfadfinder/songbook/parser/ChordProParser.kt @@ -24,6 +24,7 @@ object ChordProParser { var currentType: SectionType? = null var currentLabel: String? = null var currentLines = mutableListOf() + 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() diff --git a/parser/src/test/kotlin/de/pfadfinder/songbook/parser/ChordProParserTest.kt b/parser/src/test/kotlin/de/pfadfinder/songbook/parser/ChordProParserTest.kt index d50c8c1..ee7b8e8 100644 --- a/parser/src/test/kotlin/de/pfadfinder/songbook/parser/ChordProParserTest.kt +++ b/parser/src/test/kotlin/de/pfadfinder/songbook/parser/ChordProParserTest.kt @@ -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 + } }