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.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user