parse function
Implementation
Kara? parse(String raw) {
if (!raw.startsWith("KARA")) {
throw NotKaraException;
}
late String title;
late String artist;
late int year;
late String album;
late List<String> languages;
Map<int, String> singers = {};
KaraSection currentSection = KaraSection.header;
List<bool>? currentSingers;
String? currentLyric;
Duration? currentStart;
Duration? currentEnd;
Map<String, String> currentTranslation = {};
List<KaraLine> lines = [];
List<KaraChunk> time = [];
for (final line in raw.split('\n').skip(1)) {
if (line.startsWith("#") || line.isEmpty) continue;
// Section
// e.g `[Singers]`, `[Intro]`, `[Chorus]`
if (line.startsWith("[")) {
final sectionName = line.substring(1, line.length - 1);
currentSection = switch (sectionName) {
"Singers" => KaraSection.singers,
"Intro" => KaraSection.intro,
"Verse" => KaraSection.verse,
"Pre-Chorus" => KaraSection.preChorus,
"Chorus" => KaraSection.chorus,
"Post-Chorus" => KaraSection.postChorus,
"Bridge" => KaraSection.bridge,
"Post-Bridge" => KaraSection.postBridge,
"Outro" => KaraSection.outro,
_ => KaraSection.header,
};
continue;
if (currentSection.isSongStructure && time.isNotEmpty) {}
}
if (currentSection == KaraSection.header) {
final parsed = _parseKeyValue(line);
if (parsed == null) {
continue;
}
final (:key, :value) = parsed;
switch (key) {
case "Title":
title = value;
case "Artist":
artist = value;
case "Year":
year = int.tryParse(value) ?? 0;
case "Album":
album = value;
case "Languages":
languages = value.split(",").map((e) => e.trim()).toList();
}
}
if (currentSection == KaraSection.singers) {
final parsed = _parseKeyValue(line);
if (parsed == null) {
continue;
}
final (:key, :value) = parsed;
singers.update(
int.tryParse(key) ?? 0,
(_) => value,
ifAbsent: () => value,
);
}
if (currentSection.isSongStructure) {
if (_parseTimestamp(line) case KaraChunk parsedTimestamp) {
if (currentStart == null ||
currentEnd == null ||
parsedTimestamp.start < currentStart) {
currentStart = parsedTimestamp.start;
currentEnd = parsedTimestamp.end;
}
if (parsedTimestamp.end > currentEnd) {
currentEnd = parsedTimestamp.end;
}
time.add(parsedTimestamp);
continue;
}
if (time.isNotEmpty) {
if (currentLyric == null ||
currentStart == null ||
currentEnd == null) {
continue;
}
lines.add(KaraLine(
section: currentSection,
singers: currentSingers,
lyric: currentLyric,
start: currentStart,
end: currentEnd,
translations: currentTranslation.isEmpty ? null : currentTranslation,
time: time,
));
currentLyric = null;
currentStart = null;
currentEnd = null;
currentTranslation = {};
time = [];
}
final parsed = _parseKeyValue(line);
if (parsed != null) {
currentSingers =
parsed.value.split(",").map((e) => int.tryParse(e.trim())).fold(
List<bool>.generate(
singers.length,
(index) => false,
growable: false,
), (singers, parsedSingerIndex) {
if (parsedSingerIndex == null) {
return singers;
}
singers?[parsedSingerIndex - 1] = true;
return singers;
});
continue;
}
if (currentLyric != null) {
final translation = line.split(" ");
currentTranslation.update(
translation.first, (_) => translation.skip(1).join(' '),
ifAbsent: () => translation.skip(1).join(" "));
continue;
}
currentLyric = line;
}
}
if (currentLyric != null && currentStart != null && currentEnd != null) {
lines.add(KaraLine(
section: currentSection,
singers: currentSingers,
lyric: currentLyric,
start: currentStart,
end: currentEnd,
translations: currentTranslation.isEmpty ? null : currentTranslation,
time: time,
));
}
return Kara(
title: title,
artist: artist,
year: year,
album: album,
languages: languages,
singers: singers.values.toList(), // FIXME: doesn't allow reordering
lines: lines,
);
}