16 private const string CurrentGeneratorName =
"AnkiIO/1.0";
25 private static readonly JsonSerializerOptions Options =
new()
27 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
29 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
46 ArgumentNullException.ThrowIfNull(deck);
48 return JsonSerializer.Serialize(ToDocument(deck), Options) +
"\n";
70 ArgumentNullException.ThrowIfNull(json);
71 var document = JsonSerializer.Deserialize<NativeDocument>(json, Options) ??
throw new JsonException(
"The JSON document was empty.");
72 return FromDocument(document);
91 public static async Task
WriteAsync(
AnkiDeck deck, Stream destination, CancellationToken cancellationToken =
default)
93 ArgumentNullException.ThrowIfNull(destination);
95 await JsonSerializer.SerializeAsync(destination, ToDocument(deck), Options, cancellationToken).ConfigureAwait(
false);
120 public static async Task<AnkiDeck>
ReadAsync(Stream source, CancellationToken cancellationToken =
default)
122 ArgumentNullException.ThrowIfNull(source);
123 var document = await JsonSerializer.DeserializeAsync<NativeDocument>(source, Options, cancellationToken).ConfigureAwait(
false) ??
throw new JsonException(
"The JSON document was empty.");
124 return FromDocument(document);
127 private static AnkiDeck FromDocument(NativeDocument document)
131 throw new JsonException($
"Unsupported AnkiIO JSON format version {document.FormatVersion}; expected {CurrentFormatVersion}.");
134 var serializedTypes = document.NoteTypes ??
throw new JsonException(
"The noteTypes collection cannot be null.");
135 var types = serializedTypes.ToDictionary(
136 value => (value ??
throw new JsonException(
"The noteTypes collection contains a null entry.")).Id,
137 value => FromDto(value!));
138 var deck = FromDto(document.Deck ??
throw new JsonException(
"The root deck is missing."), types);
143 private static NativeDocument ToDocument(AnkiDeck deck)
145 var types = deck.Traverse().SelectMany(value => value.Notes).Select(note => note.NoteType).GroupBy(type => type.Id).Select(group => group.First()).OrderBy(type => type.Id).Select(ToDto).ToList();
146 return new NativeDocument { FormatVersion =
CurrentFormatVersion, Generator = CurrentGeneratorName, NoteTypes = types, Deck = ToDto(deck) };
149 private static NoteTypeDto ToDto(AnkiNoteType value) =>
new()
155 Fields = value.Fields.ToList(),
156 Templates = value.Templates.ToList(),
159 private static DeckDto ToDto(AnkiDeck value) =>
new()
163 Description = value.Description,
164 Metadata =
new SortedDictionary<string, string>(value.Metadata, StringComparer.Ordinal),
165 Notes = value.Notes.OrderBy(note => note.Id).Select(ToDto).ToList(),
166 Subdecks = value.Subdecks.OrderBy(deck => deck.Name, StringComparer.Ordinal).ThenBy(deck => deck.Id).Select(ToDto).ToList(),
167 ExtensionData = value.UnknownData.Count == 0 ? null :
new SortedDictionary<string, JsonElement>(value.UnknownData, StringComparer.Ordinal),
170 private static NoteDto ToDto(AnkiNote value) =>
new()
174 NoteTypeId = value.NoteType.Id,
175 Fields = value.NoteType.Fields.Select(field => value.Fields[field.Name]).ToList(),
176 Tags = value.Tags.Order(StringComparer.Ordinal).ToList(),
177 Cards = value.Cards.OrderBy(card => card.TemplateOrdinal).Select(card =>
new CardDto
180 DeckId = card.DeckId,
181 TemplateOrdinal = card.TemplateOrdinal,
183 Scheduling = card.Scheduling,
184 ReviewHistory = card.ReviewHistory.OrderBy(review => review.Id).ToList(),
188 private static AnkiNoteType FromDto(NoteTypeDto value)
190 var type =
new AnkiNoteType(value.Name, value.Kind, value.Id) { Css = value.Css };
191 foreach (var field
in value.Fields ??
throw new JsonException($
"Note type {value.Id} has a null fields collection."))
193 type.AddConfiguredField(field ??
throw new JsonException($
"Note type {value.Id} contains a null field definition."));
196 foreach (var
template in value.Templates ??
throw new JsonException($
"Note type {value.Id} has a null templates collection."))
198 type.AddConfiguredTemplate(
template ??
throw new JsonException($
"Note type {value.Id} contains a null template definition."));
204 private static AnkiDeck FromDto(DeckDto value, IReadOnlyDictionary<long, AnkiNoteType> noteTypes)
206 var deck =
new AnkiDeck(value.Name, value.Id) { Description = value.Description };
207 foreach (var pair
in value.Metadata ??
throw new JsonException($
"Deck {value.Id} has a null metadata object."))
209 deck.Metadata.Add(pair.Key, pair.Value ??
throw new JsonException($
"Deck {value.Id} metadata '{pair.Key}' has a null value."));
212 if (value.ExtensionData is not
null)
214 foreach (var pair
in value.ExtensionData)
216 deck.UnknownData[pair.Key] = pair.Value.Clone();
220 foreach (var valueNote
in value.Notes ??
throw new JsonException($
"Deck {value.Id} has a null notes collection."))
222 if (valueNote is
null)
224 throw new JsonException($
"Deck {value.Id} contains a null note.");
227 if (!noteTypes.TryGetValue(valueNote.NoteTypeId, out var type))
229 throw new JsonException($
"Note {valueNote.Id} references missing note type {valueNote.NoteTypeId}.");
232 var serializedFields = valueNote.Fields ??
throw new JsonException($
"Note {valueNote.Id} has a null fields collection.");
233 if (serializedFields.Count != type.Fields.Count)
235 throw new JsonException($
"Note {valueNote.Id} has {serializedFields.Count} fields; note type {type.Id} requires {type.Fields.Count}.");
238 var fields = type.Fields.Select((field, index) => (field.Name, Value: serializedFields[index] ??
throw new JsonException($
"Note {valueNote.Id} contains a null value for field '{field.Name}'."))).ToDictionary(pair => pair.Name, pair => pair.Value, StringComparer.Ordinal);
239 var tags = valueNote.Tags ??
throw new JsonException($
"Note {valueNote.Id} has a null tags collection.");
240 if (tags.Any(tag => tag is
null))
242 throw new JsonException($
"Note {valueNote.Id} contains a null tag.");
245 var note = deck.AddNote(type, fields, tags, valueNote.Guid, valueNote.Id);
246 var cards = valueNote.Cards ??
throw new JsonException($
"Note {valueNote.Id} has a null cards collection.");
247 note.RestoreCards(cards.Select(cardValue =>
249 var card = cardValue ?? throw new JsonException($
"Note {valueNote.Id} contains a null card.");
250 var scheduling = card.Scheduling ?? throw new JsonException($
"Card {card.Id} has a null scheduling object.");
251 var restored = new AnkiCard(card.Id, valueNote.Id, card.DeckId, card.TemplateOrdinal, scheduling) { Flag = card.Flag };
252 foreach (var review in card.ReviewHistory ??
throw new JsonException($
"Card {card.Id} has a null reviewHistory collection."))
254 restored.ReviewHistory.Add(review ??
throw new JsonException($
"Card {card.Id} contains a null review-history entry."));
261 foreach (var child
in value.Subdecks ??
throw new JsonException($
"Deck {value.Id} has a null subdecks collection."))
263 deck.AddExistingSubdeck(FromDto(child ??
throw new JsonException($
"Deck {value.Id} contains a null subdeck."), noteTypes));
269 private static void EnsureValid(
AnkiDeck deck)