feat: support rich multi-paragraph notes with formatting (Closes #7)
Add {start_of_notes}/{end_of_notes} (and short forms {son}/{eon}) block
directives to ChordProParser for multi-paragraph note content. Blank lines
within the block separate paragraphs. The renderer now word-wraps note
paragraphs to fit within the content width. MeasurementEngine estimates
wrapped line count for more accurate height calculations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #11.
This commit is contained in:
@@ -25,6 +25,17 @@ object ChordProParser {
|
||||
var currentLabel: String? = null
|
||||
var currentLines = mutableListOf<SongLine>()
|
||||
|
||||
// Notes block state
|
||||
var inNotesBlock = false
|
||||
var currentNoteParagraph = StringBuilder()
|
||||
|
||||
fun flushNoteParagraph() {
|
||||
if (currentNoteParagraph.isNotEmpty()) {
|
||||
notes.add(currentNoteParagraph.toString().trim())
|
||||
currentNoteParagraph = StringBuilder()
|
||||
}
|
||||
}
|
||||
|
||||
fun flushSection() {
|
||||
if (currentType != null) {
|
||||
sections.add(SongSection(type = currentType!!, label = currentLabel, lines = currentLines.toList()))
|
||||
@@ -37,6 +48,27 @@ object ChordProParser {
|
||||
for (rawLine in lines) {
|
||||
val line = rawLine.trimEnd()
|
||||
|
||||
// Inside a notes block: collect lines as paragraphs
|
||||
if (inNotesBlock) {
|
||||
if (line.trimStart().startsWith("{") && line.trimEnd().endsWith("}")) {
|
||||
val inner = line.trim().removePrefix("{").removeSuffix("}").trim().lowercase()
|
||||
if (inner == "end_of_notes" || inner == "eon") {
|
||||
flushNoteParagraph()
|
||||
inNotesBlock = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (line.isBlank()) {
|
||||
flushNoteParagraph()
|
||||
} else {
|
||||
if (currentNoteParagraph.isNotEmpty()) {
|
||||
currentNoteParagraph.append(" ")
|
||||
}
|
||||
currentNoteParagraph.append(line.trim())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip comments
|
||||
if (line.trimStart().startsWith("#")) continue
|
||||
|
||||
@@ -97,6 +129,14 @@ object ChordProParser {
|
||||
"end_of_repeat", "eor" -> {
|
||||
flushSection()
|
||||
}
|
||||
"start_of_notes", "son" -> {
|
||||
inNotesBlock = true
|
||||
}
|
||||
"end_of_notes", "eon" -> {
|
||||
// Should have been handled in the notes block above
|
||||
flushNoteParagraph()
|
||||
inNotesBlock = false
|
||||
}
|
||||
"chorus" -> {
|
||||
flushSection()
|
||||
sections.add(SongSection(type = SectionType.CHORUS))
|
||||
|
||||
@@ -485,4 +485,96 @@ class ChordProParserTest {
|
||||
line.segments[2].chord shouldBe "G"
|
||||
line.segments[2].text shouldBe "End"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse notes block with multiple paragraphs`() {
|
||||
val input = """
|
||||
{title: Song}
|
||||
{start_of_notes}
|
||||
First paragraph of the notes.
|
||||
It continues on the next line.
|
||||
|
||||
Second paragraph with different content.
|
||||
{end_of_notes}
|
||||
{start_of_verse}
|
||||
text
|
||||
{end_of_verse}
|
||||
""".trimIndent()
|
||||
val song = ChordProParser.parse(input)
|
||||
song.notes shouldHaveSize 2
|
||||
song.notes[0] shouldBe "First paragraph of the notes. It continues on the next line."
|
||||
song.notes[1] shouldBe "Second paragraph with different content."
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse notes block with single paragraph`() {
|
||||
val input = """
|
||||
{title: Song}
|
||||
{start_of_notes}
|
||||
A single note paragraph.
|
||||
{end_of_notes}
|
||||
{start_of_verse}
|
||||
text
|
||||
{end_of_verse}
|
||||
""".trimIndent()
|
||||
val song = ChordProParser.parse(input)
|
||||
song.notes shouldHaveSize 1
|
||||
song.notes[0] shouldBe "A single note paragraph."
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse notes block with short directives son eon`() {
|
||||
val input = """
|
||||
{title: Song}
|
||||
{son}
|
||||
Short form notes.
|
||||
{eon}
|
||||
{start_of_verse}
|
||||
text
|
||||
{end_of_verse}
|
||||
""".trimIndent()
|
||||
val song = ChordProParser.parse(input)
|
||||
song.notes shouldHaveSize 1
|
||||
song.notes[0] shouldBe "Short form notes."
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notes block and single note directives combine`() {
|
||||
val input = """
|
||||
{title: Song}
|
||||
{note: Single line note}
|
||||
{start_of_notes}
|
||||
Block note paragraph.
|
||||
{end_of_notes}
|
||||
{start_of_verse}
|
||||
text
|
||||
{end_of_verse}
|
||||
""".trimIndent()
|
||||
val song = ChordProParser.parse(input)
|
||||
song.notes shouldHaveSize 2
|
||||
song.notes[0] shouldBe "Single line note"
|
||||
song.notes[1] shouldBe "Block note paragraph."
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse notes block with three paragraphs`() {
|
||||
val input = """
|
||||
{title: Song}
|
||||
{start_of_notes}
|
||||
Paragraph one.
|
||||
|
||||
Paragraph two.
|
||||
|
||||
Paragraph three.
|
||||
{end_of_notes}
|
||||
{start_of_verse}
|
||||
text
|
||||
{end_of_verse}
|
||||
""".trimIndent()
|
||||
val song = ChordProParser.parse(input)
|
||||
song.notes shouldHaveSize 3
|
||||
song.notes[0] shouldBe "Paragraph one."
|
||||
song.notes[1] shouldBe "Paragraph two."
|
||||
song.notes[2] shouldBe "Paragraph three."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user