1using System.Collections.ObjectModel;
2using System.Globalization;
32 private readonly List<AnkiDeck> subdecks = [];
33 private readonly List<AnkiNote> notes = [];
34 private readonly ReadOnlyCollection<AnkiDeck> subdecksView;
35 private readonly ReadOnlyCollection<AnkiNote> notesView;
36 private ConventionalNoteTypeCache conventionalNoteTypes;
51 : this(name, id, new ConventionalNoteTypeCache())
55 private AnkiDeck(
string name,
long?
id, ConventionalNoteTypeCache conventionalNoteTypes)
57 ArgumentException.ThrowIfNullOrWhiteSpace(name);
58 if (name.Contains(
"::", StringComparison.Ordinal))
60 throw new ArgumentException(
"A deck segment cannot contain '::'. Build hierarchy with AddSubdeck().", nameof(name));
64 Id =
id ?? AnkiId.New();
65 Media =
new AnkiMediaCollection();
66 subdecksView = subdecks.AsReadOnly();
67 notesView = notes.AsReadOnly();
68 this.conventionalNoteTypes = conventionalNoteTypes;
77 public long Id {
get; }
81 public string Name {
get; }
96 public IDictionary<string, string>
Metadata {
get; } =
new Dictionary<string, string>(StringComparer.Ordinal);
103 public IDictionary<string, JsonElement>
UnknownData {
get; } =
new Dictionary<string, JsonElement>(StringComparer.Ordinal);
115 public IReadOnlyList<AnkiDeck>
Subdecks => subdecksView;
120 public IReadOnlyList<AnkiNote>
Notes => notesView;
139 if (subdecks.Any(deck =>
string.Equals(deck.Name, name, StringComparison.OrdinalIgnoreCase)))
141 throw new ArgumentException($
"Subdeck '{name}' already exists.", nameof(name));
144 var deck =
new AnkiDeck(name,
id, conventionalNoteTypes);
171 public AnkiNote AddNote(
AnkiNoteType noteType, IReadOnlyDictionary<string, string> fields, IEnumerable<string>? tags =
null,
string? guid =
null,
long?
id =
null)
173 ArgumentNullException.ThrowIfNull(noteType);
174 ArgumentNullException.ThrowIfNull(fields);
175 var note =
new AnkiNote(noteType, fields, tags,
id, guid);
176 note.GenerateCards(
Id, nameof(fields));
178 conventionalNoteTypes.Observe(noteType);
191 internal void AddExistingSubdeck(
AnkiDeck deck)
193 deck.UseConventionalNoteTypes(conventionalNoteTypes);
197 internal void AddExistingNote(
AnkiNote note)
199 note.AttachToDeck(
Id);
201 conventionalNoteTypes.Observe(note.
NoteType);
204 private void UseConventionalNoteTypes(ConventionalNoteTypeCache cache)
206 conventionalNoteTypes.CopyMissingTo(cache);
207 conventionalNoteTypes = cache;
208 foreach (var note
in notes)
210 cache.Observe(note.NoteType);
213 foreach (var subdeck
in subdecks)
215 subdeck.UseConventionalNoteTypes(cache);
228 foreach (var child
in subdecks)
230 foreach (var descendant
in child.Traverse())
232 yield
return descendant;
264 IEnumerable<string>? tags =
null,
268 ArgumentNullException.ThrowIfNull(front);
269 ArgumentNullException.ThrowIfNull(back);
273 new Dictionary<string, string>(StringComparer.Ordinal)
310 IEnumerable<string>? tags =
null,
314 ArgumentNullException.ThrowIfNull(front);
315 ArgumentNullException.ThrowIfNull(back);
319 new Dictionary<string, string>(StringComparer.Ordinal)
367 IEnumerable<string>? tags =
null,
371 ArgumentNullException.ThrowIfNull(text);
372 ArgumentNullException.ThrowIfNull(extra);
373 ValidateSimpleClozeText(text);
377 new Dictionary<string, string>(StringComparer.Ordinal)
387 private static void ValidateSimpleClozeText(
string text)
389 const string marker =
"{{c";
391 var foundDeletion =
false;
393 while (text.IndexOf(marker, searchIndex, StringComparison.Ordinal) is var markerIndex && markerIndex >= 0)
395 var indexStart = markerIndex + marker.Length;
396 if (indexStart >= text.Length || text[indexStart] is <
'0' or >
'9')
398 throw new ArgumentException(
399 "Cloze markers must place a positive numeric index after '{{c', such as '{{c1::answer}}'.",
403 var contentSeparator = indexStart;
404 while (contentSeparator < text.Length && text[contentSeparator] is >=
'0' and <=
'9')
409 if (contentSeparator + 1 >= text.Length
410 || text[contentSeparator] !=
':'
411 || text[contentSeparator + 1] !=
':')
413 throw new ArgumentException(
"Cloze indexes must be followed by the '::' content separator.", nameof(text));
416 var indexText = text.AsSpan(indexStart, contentSeparator - indexStart);
417 if (indexText[0] ==
'0'
418 || !
int.TryParse(indexText, NumberStyles.None, CultureInfo.InvariantCulture, out var index)
421 throw new ArgumentException(
"Cloze indexes must be positive integers representable by System.Int32.", nameof(text));
424 var contentStart = contentSeparator + 2;
425 var closingDelimiter = text.IndexOf(
"}}", contentStart, StringComparison.Ordinal);
426 if (closingDelimiter < 0)
428 throw new ArgumentException(
"Cloze deletions must end with '}}'.", nameof(text));
431 var content = text[contentStart..closingDelimiter];
432 if (content.Contains(
"{{", StringComparison.Ordinal))
434 throw new ArgumentException(
"Nested cloze or template markup requires the low-level AddNote API.", nameof(text));
437 var hintSeparator = content.IndexOf(
"::", StringComparison.Ordinal);
438 var answer = hintSeparator < 0 ? content : content[..hintSeparator];
439 if (answer.Length == 0)
441 throw new ArgumentException(
"Cloze answer text cannot be empty.", nameof(text));
444 if (hintSeparator >= 0
445 && content.IndexOf(
"::", hintSeparator + 2, StringComparison.Ordinal) >= 0)
447 throw new ArgumentException(
"Simple cloze markup can contain at most one hint separator.", nameof(text));
450 foundDeletion =
true;
451 searchIndex = closingDelimiter + 2;
456 throw new ArgumentException(
"Cloze text must contain at least one positive deletion such as '{{c1::answer}}'.", nameof(text));
AnkiNote AddClozeNote(string text, string extra="", IEnumerable< string >? tags=null, string? guid=null, long? id=null)
Adds a conventional Cloze note and generates one card for each distinct positive cloze index.
IReadOnlyList< AnkiDeck > Subdecks
Gets only the direct children created below this deck.
AnkiDeck(string name, long? id=null)
Initializes a top-level deck with an empty note and subdeck collection.
IEnumerable< AnkiDeck > Traverse()
Enumerates the complete hierarchy in the same deterministic order used by writers.
AnkiMediaCollection Media
Gets media filenames and payloads contributed by this deck during package export.
long Id
Gets the persisted identity used by cards and package metadata to refer to this deck.
IReadOnlyList< AnkiNote > Notes
Gets notes assigned directly to this deck.
IDictionary< string, string > Metadata
Gets application-defined string metadata for AnkiIO native-JSON round trips.
AnkiNote AddBasicAndReversedNote(string front, string back, IEnumerable< string >? tags=null, string? guid=null, long? id=null)
Adds a conventional Basic (and reversed) note that generates front-to-back and back-to-front cards.
AnkiNote AddNote(AnkiNoteType noteType, IReadOnlyDictionary< string, string > fields, IEnumerable< string >? tags=null, string? guid=null, long? id=null)
Adds a note and generates its cards using safe new-card scheduling.
bool RemoveNote(AnkiNote note)
Removes a note and its generated cards from this deck.
AnkiDeck AddSubdeck(string name, long? id=null)
Creates and adds a direct child deck.
IDictionary< string, JsonElement > UnknownData
Gets unknown native-JSON deck properties retained without interpretation.
string Name
Gets this deck's local display-name segment, not its full hierarchy path.
string Description
Gets or sets the description Anki may show on the deck overview screen.
AnkiNote AddBasicNote(string front, string back, IEnumerable< string >? tags=null, string? guid=null, long? id=null)
Adds a conventional Basic note that generates one front-to-back card.
Defines the reusable schema and rendering rules shared by a family of Anki notes.
Creates fresh conventional Basic, reversed, and Cloze definitions for callers that need direct contro...
static AnkiNoteType CreateCloze()
Creates a conventional Cloze definition with Text and Extra fields.
static AnkiNoteType CreateBasicAndReversed()
Creates a conventional Basic (and reversed) definition that generates two study directions.
static AnkiNoteType CreateBasic()
Creates a conventional Basic definition that generates one front-to-back card.
Stores one fact or item of knowledge and owns the cards generated from it.
AnkiNoteType NoteType
Gets the shared model that defines field order, rendering templates, CSS, and card generation.