Created
October 21, 2025 11:56
-
-
Save mykeels/1c059fdf8d17ef06abd6602f329ae450 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Text.RegularExpressions; | |
| namespace Mykeels.Solfa | |
| { | |
| public class SolfaParser | |
| { | |
| public List<Line> Parse(string src) | |
| { | |
| var lines = src.Split('\n'); | |
| var result = new List<Line>(); | |
| var tonicSolfaLineRegex = new Regex("^(.*?>) (.+)$"); | |
| var lyricsLineRegex = new Regex("^ +(.+)$"); | |
| var noteRegex = new Regex("^(d|de|r|re|m|f|fe|s|se|l|le|t)((?:'+)|(?:,+))?$", RegexOptions.Compiled); | |
| foreach (var line in lines) | |
| { | |
| var tonicMatch = tonicSolfaLineRegex.Match(line); | |
| if (tonicMatch.Success) | |
| { | |
| var prefix = tonicMatch.Groups[1].Value; | |
| var content = tonicMatch.Groups[2].Value; | |
| var tokens = new List<Token>(); | |
| var noteOrSpace = new Regex("\\S+|\\s+", RegexOptions.Compiled); | |
| var matches = noteOrSpace.Matches(content); | |
| foreach (Match match in matches) | |
| { | |
| var token = match.Value; | |
| if (Regex.IsMatch(token, "^\\s+$")) | |
| { | |
| tokens.Add(new Text(token)); | |
| } | |
| else | |
| { | |
| var noteMatch = noteRegex.Match(token); | |
| if (noteMatch.Success) | |
| { | |
| var note = (Note)Enum.Parse(typeof(Note), noteMatch.Groups[1].Value); | |
| var modifierStr = noteMatch.Groups[2].Value ?? string.Empty; | |
| string noteContent = $"{note.ToString()}{modifierStr}"; | |
| NoteModifier? modifier = null; | |
| int modifierCount = 0; | |
| if (!string.IsNullOrEmpty(modifierStr)) | |
| { | |
| if (Regex.IsMatch(modifierStr, "^'+$")) | |
| { | |
| modifier = NoteModifier.OctaveAbove; | |
| modifierCount = modifierStr.Length; | |
| } | |
| else if (Regex.IsMatch(modifierStr, "^,+$")) | |
| { | |
| modifier = NoteModifier.OctaveBelow; | |
| modifierCount = modifierStr.Length; | |
| } | |
| else | |
| { | |
| tokens.Add(new Text(token)); | |
| continue; | |
| } | |
| } | |
| tokens.Add(new TonicSolfaNote( | |
| Raw: noteContent, | |
| Note: note, | |
| OctaveModifier: modifier.HasValue ? new NoteOctaveModifier(modifier.Value, modifierCount) : null | |
| )); | |
| } | |
| else | |
| { | |
| tokens.Add(new Text(token)); | |
| } | |
| } | |
| } | |
| result.Add(new TonicSolfaLine(content, prefix) { Tokens = tokens }); | |
| continue; | |
| } | |
| var lyricsMatch = lyricsLineRegex.Match(line); | |
| if (lyricsMatch.Success) | |
| { | |
| var content = lyricsMatch.Groups[1].Value; | |
| result.Add(new LyricsLine(content) { Tokens = new List<Text> { new Text(content) } }); | |
| continue; | |
| } | |
| result.Add(new CommentMetadataLine(line) { Tokens = new List<Text> { new Text(line) } }); | |
| } | |
| return result; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Text.Json.Serialization; | |
| using System.Text.RegularExpressions; | |
| namespace Mykeels.Solfa | |
| { | |
| [JsonConverter(typeof(JsonStringEnumConverter))] | |
| public enum Note | |
| { | |
| d, | |
| de, | |
| r, | |
| re, | |
| m, | |
| f, | |
| fe, | |
| s, | |
| se, | |
| l, | |
| le, | |
| t | |
| } | |
| public enum VoicePart | |
| { | |
| Treble, | |
| Alto, | |
| Tenor, | |
| Bass | |
| } | |
| [JsonConverter(typeof(JsonStringEnumConverter))] | |
| public enum NoteModifier | |
| { | |
| /// <summary> | |
| /// ' | |
| /// </summary> | |
| OctaveAbove, | |
| /// <summary> | |
| /// , | |
| /// </summary> | |
| OctaveBelow | |
| } | |
| public record NoteOctaveModifier(NoteModifier Modifier, int Count); | |
| [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] | |
| [JsonDerivedType(typeof(TonicSolfaNote), "tonic-solfa-note")] | |
| [JsonDerivedType(typeof(Text), "text")] | |
| public abstract record Token(string Raw) | |
| { | |
| public abstract string Type { get; } | |
| } | |
| public record TonicSolfaNote( | |
| string Raw, | |
| Note Note, | |
| NoteOctaveModifier? OctaveModifier | |
| ) : Token(Raw) | |
| { | |
| public override string Type => "tonic-solfa"; | |
| public const int BaseOctave = 4; | |
| public int Octave | |
| { | |
| get | |
| { | |
| if (OctaveModifier == null) | |
| { | |
| return BaseOctave; | |
| } | |
| bool isAbove = OctaveModifier.Modifier == NoteModifier.OctaveAbove; | |
| int octave = isAbove ? BaseOctave + OctaveModifier.Count : BaseOctave - OctaveModifier.Count; | |
| return octave; | |
| } | |
| } | |
| public string WhiteSpacing | |
| { | |
| get | |
| { | |
| return new string(' ', Math.Max(0, OctaveModifier?.Count ?? 0)); | |
| } | |
| } | |
| } | |
| public record Text(string Raw) : Token(Raw) | |
| { | |
| public override string Type => "text"; | |
| } | |
| [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] | |
| [JsonDerivedType(typeof(TonicSolfaLine), "tonic-solfa")] | |
| [JsonDerivedType(typeof(LyricsLine), "lyrics")] | |
| [JsonDerivedType(typeof(CommentMetadataLine), "comment-metadata")] | |
| public abstract record Line(string Raw) | |
| { | |
| public abstract string Type { get; } | |
| } | |
| public record TonicSolfaLine(string Raw, string Prefix) : Line(Raw) | |
| { | |
| public override string Type => "tonic-solfa"; | |
| public List<Token> Tokens { get; set; } = new List<Token>(); | |
| public string GetPrefix() => Prefix.TrimEnd('>').Trim(); | |
| public string GetVoicePart() | |
| { | |
| string prefix = Prefix.ToLower(); | |
| if (prefix.Contains("π,") || prefix.Contains("alto")) | |
| { | |
| return "π,"; | |
| } | |
| else if (prefix.Contains("π’'") || prefix.Contains("tenor")) | |
| { | |
| return "π’'"; | |
| } | |
| else if (prefix.Contains("π") || prefix.Contains("treble")) | |
| { | |
| return "π"; | |
| } | |
| else if (prefix.Contains("π’") || prefix.Contains("bass")) | |
| { | |
| return "π’"; | |
| } | |
| return "π"; // default to treble | |
| } | |
| public VoicePart VoicePart => GetVoicePart() switch | |
| { | |
| "π" => VoicePart.Treble, | |
| "π," => VoicePart.Alto, | |
| "π’'" => VoicePart.Tenor, | |
| "π’" => VoicePart.Bass, | |
| _ => VoicePart.Treble | |
| }; | |
| public string GetTextColor() | |
| { | |
| return VoicePart switch | |
| { | |
| VoicePart.Treble => "text-red-600 dark:text-green-300", | |
| VoicePart.Alto => "text-blue-600 dark:text-yellow-300", | |
| VoicePart.Tenor => "text-green-600 dark:text-pink-300", | |
| VoicePart.Bass => "text-yellow-600 dark:text-red-300", | |
| _ => "text-gray-600 dark:text-gray-300" | |
| }; | |
| } | |
| } | |
| public record LyricsLine(string Raw) : Line(Raw) | |
| { | |
| public override string Type => "lyrics"; | |
| public List<Text> Tokens { get; set; } = new List<Text>(); | |
| } | |
| public record CommentMetadataLine(string Raw) : Line(Raw) | |
| { | |
| public override string Type => "comment-metadata"; | |
| public List<Text> Tokens { get; set; } = new List<Text>(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment