chore: Halkyon Learn moves into apps/learn

Its server, client, app and curator, and the tools that write its
assets (maps, Anki fixtures, icons), as they are; paths are fixed in
the commits that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 20:08:06 +02:00
commit 649499be23
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,125 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:halkyon_learn_curator/src/sources/kanjidic.dart';
import 'package:test/test.dart';
void main() {
late List<Map<String, Object?>> lines;
setUpAll(() async {
final jlpt = await readKanjiJlpt(
File('test/fixtures/kanji-data-sample.json'),
);
lines = [
for (final line in kanjidicDictionary(
await File('test/fixtures/kanjidic2-sample.xml').readAsString(),
jlpt: jlpt,
))
jsonDecode(line) as Map<String, Object?>,
];
});
Map<String, Object?> entry(String character) =>
lines.skip(1).firstWhere((line) => line['id'] == character);
Map<String, Object?> fields(String character) =>
entry(character)['entry']! as Map<String, Object?>;
test('the header says the file holds characters, and whose they are', () {
final header = lines.first;
expect(header['source'], 'kanjidic');
expect(header['kind'], 'characters');
expect(header['sourceVersion'], '2026-282');
expect((header['language'], header['glossLanguage']), ('ja', 'en'));
expect(header['license'], 'CC-BY-SA-4.0');
expect(
[
for (final credit in header['credits']! as List)
(credit as Map)['name'],
],
['KANJIDIC2', 'JLPT kanji lists'],
);
});
test('a kanji: its readings, English meanings only, grade, strokes, '
'frequency and JLPT level', () {
final eat = entry('食');
expect(eat['headword'], '食');
expect(eat['reading'], isNull);
expect(eat['labels'], ['jlpt:n5', 'grade:2', 'radical:184']);
expect(fields('食'), {
'meanings': ['eat', 'food'],
'on': ['ショク', 'ジキ'],
'kun': ['く.う', 'く.らう', 'た.べる', 'は.む'],
'grade': 2,
'strokes': 9,
'frequency': 328,
'jlpt': 5,
});
expect(fields('一')['meanings'], isNot(contains('un')));
});
test('a search finds a kanji by its on readings in either kana and in '
'romaji, by whole kun readings and by its meanings; the kanji part '
'of a kun reading only loosely', () {
final eat = entry('食');
expect(
eat['keys'],
containsAll([
'食',
'ショク',
'しょく',
'shoku',
'たべる',
'taberu',
'くらう',
'eat',
'food',
]),
);
expect(eat['keys'], isNot(contains('た')));
expect(eat['looseKeys'], containsAll(['た', 'ta', 'く', 'ku']));
// ひと- and ひと.つ: the suffix's dash goes, its kanji part is whole.
expect(entry('一')['keys'], containsAll(['ひと', 'ひとつ', 'hitotsu']));
expect(entry('一')['looseKeys'], isNot(contains('ひと')));
});
test('levels, grades and radicals label the kanji; level and grade rank '
'them: JLPT level, then school grade, then frequency', () {
expect(entry('亜')['labels'], ['jlpt:n1', 'grade:secondary', 'radical:7']);
// KANJIDIC2's classical radical, not Nelson's.
expect(entry('唖')['labels'], ['radical:30']);
final ranks = [
for (final kanji in ['一', '食', '亜', '唖']) entry(kanji)['rank']! as int,
];
expect(ranks, orderedEquals([...ranks]..sort()));
});
test('a character with no Japanese reading and no English meaning is '
'left out', () {
expect(lines.skip(1).where((line) => line['id'] == '僲'), isEmpty);
});
test('hiragana and katakana follow, the kana table each, before every '
'kanji, found by their romaji', () {
final kana = [
for (final line in lines.skip(1))
if ((line['labels']! as List).any(['hiragana', 'katakana'].contains))
line,
];
expect(kana, hasLength(142));
expect(kana.first['id'], 'あ');
expect(kana[71]['id'], 'ア');
expect(kana.last['id'], 'ポ');
expect(entry('ア')['labels'], ['katakana']);
expect(entry('ア')['keys'], containsAll(['ア', 'あ', 'a']));
expect(fields('ぢ'), {'romaji': 'ji', 'script': 'hiragana'});
// を is "wo" in the table, found by "o" too.
expect(fields('を')['romaji'], 'wo');
expect(entry('を')['keys'], containsAll(['wo', 'o']));
final lastKana = kana
.map((line) => line['rank']! as int)
.reduce((a, b) => a > b ? a : b);
expect(entry('食')['rank']! as int, greaterThan(lastKana));
});
}