diff --git a/Chord Provider/ChordProParser/ChordPro.swift b/Chord Provider/ChordProParser/ChordPro.swift index a0a0e1e..433da3f 100644 --- a/Chord Provider/ChordProParser/ChordPro.swift +++ b/Chord Provider/ChordProParser/ChordPro.swift @@ -29,7 +29,7 @@ enum ChordPro { var currentSection = Song.Section(id: song.sections.count + 1) /// Parse each line of the text: for text in text.components(separatedBy: .newlines) { - switch text.prefix(1) { + switch text.trimmingCharacters(in: .whitespaces).prefix(1) { case "{": /// Directive processDirective(text: text, song: &song, currentSection: ¤tSection) @@ -129,14 +129,25 @@ enum ChordPro { case .c, .comment: if let label { - processSection( - label: label, - type: Environment.comment, - song: &song, - currentSection: ¤tSection - ) - song.sections.append(currentSection) - currentSection = Song.Section(id: song.sections.count + 1) + /// Start with a new line + var line = Song.Section.Line(id: currentSection.lines.count + 1) + line.comment = label + switch currentSection.type { + case .none: + /// A comment in its own section + processSection( + label: Environment.comment.rawValue, + type: Environment.comment, + song: &song, + currentSection: ¤tSection + ) + currentSection.lines.append(line) + song.sections.append(currentSection) + currentSection = Song.Section(id: song.sections.count + 1) + default: + /// An inline comment, e.g. inside a verse or chorus + currentSection.lines.append(line) + } } // MARK: Environment directives diff --git a/Chord Provider/Export/ExportSong+render.swift b/Chord Provider/Export/ExportSong+render.swift index 2b3f71b..3dac03e 100644 --- a/Chord Provider/Export/ExportSong+render.swift +++ b/Chord Provider/Export/ExportSong+render.swift @@ -90,19 +90,19 @@ extension ExportSong { for section in song.sections { switch section.type { case .verse, .bridge: - part = renderPart(view: Song.Render.VerseView(section: section, options: options, chords: song.chords)) + part = renderPart(view: Song.Render.VerseSectionView(section: section, options: options, chords: song.chords)) case .chorus: - part = renderPart(view: Song.Render.ChorusView(section: section, options: options, chords: song.chords)) + part = renderPart(view: Song.Render.ChorusSectionView(section: section, options: options, chords: song.chords)) case .repeatChorus: part = renderPart(view: Song.Render.RepeatChorusView(section: section, options: options)) case .tab: - part = renderPart(view: Song.Render.TabView(section: section, options: options)) + part = renderPart(view: Song.Render.TabSectionView(section: section, options: options)) case .grid: - part = renderPart(view: Song.Render.GridView(section: section, options: options, chords: song.chords)) + part = renderPart(view: Song.Render.GridSectionView(section: section, options: options, chords: song.chords)) case .comment: - part = renderPart(view: Song.Render.CommentView(section: section, options: options)) + part = renderPart(view: Song.Render.CommentSectionView(section: section, options: options)) default: - part = renderPart(view: Song.Render.PlainView(section: section, options: options)) + part = renderPart(view: Song.Render.PlainSectionView(section: section, options: options)) } if let part { parts.append(part) diff --git a/Chord Provider/SongModel/Song+Render/Song+Render.swift b/Chord Provider/SongModel/Song+Render/Song+Render.swift index 930cee6..8708375 100644 --- a/Chord Provider/SongModel/Song+Render/Song+Render.swift +++ b/Chord Provider/SongModel/Song+Render/Song+Render.swift @@ -57,19 +57,19 @@ extension Song { ForEach(song.sections) { section in switch section.type { case .verse, .bridge: - VerseView(section: section, options: options, chords: song.chords) + VerseSectionView(section: section, options: options, chords: song.chords) case .chorus: - ChorusView(section: section, options: options, chords: song.chords) + ChorusSectionView(section: section, options: options, chords: song.chords) case .repeatChorus: RepeatChorusView(section: section, options: options) case .tab: - TabView(section: section, options: options) + TabSectionView(section: section, options: options) case .grid: - GridView(section: section, options: options, chords: song.chords) + GridSectionView(section: section, options: options, chords: song.chords) case .comment: - CommentView(section: section, options: options) + CommentSectionView(section: section, options: options) default: - PlainView(section: section, options: options) + PlainSectionView(section: section, options: options) } } } @@ -131,18 +131,24 @@ extension Song.Render { } } - /// SwiftUI `View` for plain text - struct PlainView: View { + // MARK: Verse + + /// SwiftUI `View` for a verse section + struct VerseSectionView: View { /// The `section` of the song let section: Song.Section /// The display options let options: Song.DisplayOptions + /// The chords of the song + let chords: [ChordDefinition] /// The body of the `View` var body: some View { VStack(alignment: .leading) { ForEach(section.lines) { line in - ForEach(line.parts) { part in - Text(part.text.trimmingCharacters(in: .whitespacesAndNewlines)) + if line.comment.isEmpty { + PartsView(options: options, sectionID: section.id, parts: line.parts, chords: chords) + } else { + CommentLabelView(comment: line.comment, options: options) } } } @@ -150,8 +156,10 @@ extension Song.Render { } } - /// SwiftUI `View` for a grid - struct GridView: View { + // MARK: Chorus + + /// SwiftUI `View` for a chorus section + struct ChorusSectionView: View { /// The `section` of the song let section: Song.Section /// The display options @@ -160,50 +168,63 @@ extension Song.Render { let chords: [ChordDefinition] /// The body of the `View` var body: some View { - VStack(alignment: .leading, spacing: 0) { - Grid(alignment: .leading) { - ForEach(section.lines) { line in - GridRow { - ForEach(line.grid) { grid in - Text("|") - ForEach(grid.parts) { part in - if let chord = chords.first(where: { $0.id == part.chord }) { - ChordView(options: options, sectionID: section.id, partID: part.id, chord: chord) - } else { - Text(part.text) - } - } - } - } + VStack(alignment: .leading) { + ForEach(section.lines) { line in + if line.comment.isEmpty { + PartsView(options: options, sectionID: section.id, parts: line.parts, chords: chords) + } else { + CommentLabelView(comment: line.comment, options: options) } } } - .padding(.vertical, options.scale) - .modifier(SectionView(options: options, label: section.label)) + .modifier(SectionView(options: options, label: section.label ?? "Chorus", prominent: true)) } } - /// SwiftUI `View` for a verse - struct VerseView: View { + /// SwiftUI `View` for a chorus repeat + struct RepeatChorusView: View { + /// The `section` of the song + let section: Song.Section + /// The display options + let options: Song.DisplayOptions + /// The body of the `View` + var body: some View { + ProminentLabel(options: options, label: section.label ?? "Repeat Chorus", icon: "arrow.triangle.2.circlepath") + .modifier(SectionView(options: options)) + } + } + + // MARK: Tab + + /// SwiftUI `View` for a tab section + struct TabSectionView: View { /// The `section` of the song let section: Song.Section /// The display options let options: Song.DisplayOptions - /// The chords of the song - let chords: [ChordDefinition] /// The body of the `View` var body: some View { VStack(alignment: .leading) { ForEach(section.lines) { line in - PartsView(options: options, sectionID: section.id, parts: line.parts, chords: chords) + if line.comment.isEmpty { + Text(line.tab) + .lineLimit(1) + .minimumScaleFactor(0.01) + .monospaced() + } else { + CommentLabelView(comment: line.comment, options: options) + } } } + .padding(.vertical, options.scale) .modifier(SectionView(options: options, label: section.label)) } } - /// SwiftUI `View` for a chorus - struct ChorusView: View { + // MARK: Grid + + /// SwiftUI `View` for a grid section + struct GridSectionView: View { /// The `section` of the song let section: Song.Section /// The display options @@ -212,63 +233,84 @@ extension Song.Render { let chords: [ChordDefinition] /// The body of the `View` var body: some View { - VStack(alignment: .leading) { - ForEach(section.lines) { line in - PartsView(options: options, sectionID: section.id, parts: line.parts, chords: chords) + VStack(alignment: .leading, spacing: 0) { + Grid(alignment: .leading) { + ForEach(section.lines) { line in + if line.comment.isEmpty { + GridRow { + ForEach(line.grid) { grid in + Text("|") + ForEach(grid.parts) { part in + if let chord = chords.first(where: { $0.id == part.chord }) { + ChordView(options: options, sectionID: section.id, partID: part.id, chord: chord) + } else { + Text(part.text) + } + } + } + } + } else { + CommentLabelView(comment: line.comment, options: options) + } + } } } - .modifier(SectionView(options: options, label: section.label ?? "Chorus", prominent: true)) + .padding(.vertical, options.scale) + .modifier(SectionView(options: options, label: section.label)) } } - /// SwiftUI `View` for a chorus repeat - struct RepeatChorusView: View { + // MARK: Comment + + /// SwiftUI `View` for a comment in its own section + struct CommentSectionView: View { /// The `section` of the song let section: Song.Section /// The display options let options: Song.DisplayOptions /// The body of the `View` var body: some View { - ProminentLabel(options: options, label: section.label ?? "Repeat Chorus", icon: "arrow.triangle.2.circlepath") + CommentLabelView(comment: section.lines.first?.comment ?? "", options: options) .modifier(SectionView(options: options)) } } - /// SwiftUI `View` for a tab - struct TabView: View { - /// The `section` of the song - let section: Song.Section + /// SwiftUI `View` for a comment label + struct CommentLabelView: View { + /// The comment + let comment: String /// The display options let options: Song.DisplayOptions /// The body of the `View` var body: some View { - VStack(alignment: .leading) { - ForEach(section.lines) { line in - Text(line.tab) - } - } - .lineLimit(1) - .minimumScaleFactor(0.01) - .monospaced() - .padding(.vertical, options.scale) - .modifier(SectionView(options: options, label: section.label)) + ProminentLabel(options: options, label: comment, icon: "bubble.right", color: .yellow) + .italic() } } - /// SwiftUI `View` for a comment - struct CommentView: View { + // MARK: Plain + + /// SwiftUI `View` for a plain text section + struct PlainSectionView: View { /// The `section` of the song let section: Song.Section /// The display options let options: Song.DisplayOptions /// The body of the `View` var body: some View { - ProminentLabel(options: options, label: section.label ?? "", icon: "bubble.right", color: .yellow) - .italic() - .modifier(SectionView(options: options)) + VStack(alignment: .leading) { + ForEach(section.lines) { line in + ForEach(line.parts) { part in + Text(part.text.trimmingCharacters(in: .whitespacesAndNewlines)) + } + } + } + .modifier(SectionView(options: options, label: section.label)) } } + // MARK: Parts + /// SwiftUI `View` for parts of a line struct PartsView: View { /// The display options diff --git a/Chord Provider/SongModel/Song+Section+Line.swift b/Chord Provider/SongModel/Song+Section+Line.swift index d55acba..ba39775 100644 --- a/Chord Provider/SongModel/Song+Section+Line.swift +++ b/Chord Provider/SongModel/Song+Section+Line.swift @@ -19,5 +19,7 @@ extension Song.Section { var grid = [Grid]() /// The optional tab in the line var tab: String = "" + /// The optional comment in the line + var comment: String = "" } }