Files
step3/api/QuestionsApi/Services/Flashcards/FlashcardClozeParser.cs
T
hz 48a8c8361d Add flashcards feature with spaced repetition scheduling
Adds a full flashcards system: FSRS-based scheduling, cloze/basic
card types, CSV import/export, media storage, and study UI, wired
into navigation and the dashboard alongside existing question flows.
2026-07-21 15:08:52 -05:00

58 lines
2.4 KiB
C#

using System.Text.RegularExpressions;
namespace QuestionsApi.Services.Flashcards;
public sealed record ClozeDeletion(int Number, string Answer, string? Hint);
// {{c1::answer}} or {{c1::answer::hint}}. Numbers need not be contiguous or start at 1;
// each distinct number becomes its own independently scheduled card (a "sibling").
public static partial class FlashcardClozeParser
{
[GeneratedRegex(@"\{\{c(\d+)::((?:(?!\}\}).)*)\}\}", RegexOptions.Singleline)]
private static partial Regex ClozeRegex();
public static List<ClozeDeletion> Parse(string text)
{
if (string.IsNullOrEmpty(text)) return [];
var results = new List<ClozeDeletion>();
foreach (Match match in ClozeRegex().Matches(text))
{
if (!int.TryParse(match.Groups[1].Value, out var number) || number < 1)
continue;
var inner = match.Groups[2].Value;
var parts = inner.Split("::", 2);
var answer = parts[0].Trim();
var hint = parts.Length > 1 ? parts[1].Trim() : null;
if (answer.Length == 0) continue;
results.Add(new ClozeDeletion(number, answer, string.IsNullOrEmpty(hint) ? null : hint));
}
return results;
}
public static List<int> DistinctClozeNumbers(string text) =>
Parse(text).Select(v => v.Number).Distinct().OrderBy(v => v).ToList();
public static bool HasValidCloze(string text) => DistinctClozeNumbers(text).Count > 0;
// Renders the front for a given cloze number: that number's answer is hidden (replaced
// by its hint or "..."), all other cloze numbers are revealed as their answer text.
public static string RenderFront(string text, int clozeNumber) =>
ClozeRegex().Replace(text, match =>
{
if (!int.TryParse(match.Groups[1].Value, out var number)) return match.Value;
var inner = match.Groups[2].Value;
var parts = inner.Split("::", 2);
if (number != clozeNumber) return parts[0].Trim();
var hint = parts.Length > 1 ? parts[1].Trim() : "";
return hint.Length > 0 ? $"[{hint}]" : "[...]";
});
public static string RenderBack(string text, int clozeNumber) =>
ClozeRegex().Replace(text, match =>
{
var inner = match.Groups[2].Value;
var parts = inner.Split("::", 2);
return parts[0].Trim();
});
}