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.
58 lines
2.4 KiB
C#
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();
|
|
});
|
|
}
|