Skip to content

Instantly share code, notes, and snippets.

@mykeels
Created October 21, 2025 11:56
Show Gist options
  • Select an option

  • Save mykeels/1c059fdf8d17ef06abd6602f329ae450 to your computer and use it in GitHub Desktop.

Select an option

Save mykeels/1c059fdf8d17ef06abd6602f329ae450 to your computer and use it in GitHub Desktop.
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;
}
}
}
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