feat(app): Anki's bracket furigana becomes readings where its templates show them

A field a note type shows through Anki's furigana filters
({{furigana:Reading}}, kanji:, kana:) holds 漢字[かんじ]; the import now
turns it into readings over the text before the brackets, as Anki's own
filters read it (back to a space, which goes). Other fields stay as
written; a sound stays a sound. The note types' templates and field
names are read from both package formats (Anki's unicase collation
registered so their tables can be read at all). New fixtures exported
by Anki itself (tool/anki/make_fixtures.py) test both formats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-09 13:26:37 +02:00
commit dd36424917
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 208 additions and 15 deletions

View file

@ -202,6 +202,31 @@ void main() {
},
]);
});
test("Anki's bracket furigana becomes readings: the text before the "
'brackets back to a space, the space dropped; a sound stays', () {
expect(
ankiBracketsToRuby('日本語[にほんご]を 勉強[べんきょう]する'),
'<ruby>日本語<rt>にほんご</rt></ruby>を<ruby>勉強<rt>べんきょう</rt></ruby>する',
);
expect(
ankiBracketsToRuby('<b>猫[ねこ]</b>'),
'<b><ruby>猫<rt>ねこ</rt></ruby></b>',
);
expect(ankiBracketsToRuby('犬[sound:inu.mp3]'), '犬[sound:inu.mp3]');
});
test('the fields a note type shows through a furigana filter, whatever '
'filters come with it', () {
expect(
ankiFuriganaFieldNames([
'{{Expression}}',
'{{FrontSide}}<hr id=answer>{{furigana:Reading}}<br>{{kanji:Word}}',
'{{text:kana:Sentence}}{{#Meaning}}{{Meaning}}{{/Meaning}}',
]),
{'Reading', 'Word', 'Sentence'},
);
});
}
extension on Map<String, String> {

View file

@ -193,6 +193,38 @@ void main() {
);
});
for (final name in ['anki_furigana.apkg', 'anki_furigana_legacy.apkg']) {
test("a field the note type shows with Anki's furigana gets readings; "
'the same text in another field stays as written ($name)', () async {
final furigana = readAnkiPackage(fixture(name), temp: temp);
expect(furigana.collection.notes.single.furiganaFields, {1});
await importer.import(
furigana,
studysetId: japanese,
mode: AnkiImportMode.flatten,
keepProgress: false,
includeMedia: false,
);
final card = (await cards.cardsOf(japanese)).single;
expect(card.content.front.plainText.trim(), '日本語[にほんご]');
final document =
card.content.back.editorJson['document']! as Map<String, Object?>;
final readings = [
for (final block in document['children']! as List<Object?>)
if (block case {'data': {'delta': final List<Object?> delta}})
for (final op in delta)
if (op case {
'insert': final String text,
'attributes': {'ruby': final String ruby},
})
(text, ruby),
];
expect(readings, [('日本語', 'にほんご'), ('勉強', 'べんきょう')]);
});
}
test('makes a studyset of each deck below the top one', () async {
await import(mode: AnkiImportMode.subStudysets);