feat(dictionary): characters of Chinese, Korean and Greek, written by hand; pinyin typed
Parity with Japanese's kanji and kana: - The characters' tab is named by its language (Hanzi, Hangul & Hanja, Alphabet), its groups too: HSK levels and the characters in common use (Hong Kong's grades for traditional ones); Hangul and the Hanja taught in schools; Greek's letters. Radicals for Chinese and Korean in their own forms (讠 for 言 in simplified Chinese), none for Greek. - A character's readings are named as its language names them: On and Kun, Pinyin, Eum and Hun (음, 훈); a letter means its sound and name. - How characters are written comes by language: Chinese from Make Me a Hanzi (its medians in KanjiVG's square, Arphic Public License, a data file beside the app, never in cards), the Hangul letters drawn by the curator; a Korean word is written letter by letter (닭: ㄷ ㅏ ㄺ). Words of alphabets (German, Greek) aren't written by hand. - A Chinese word's pinyin is typed in a session, tone numbers turning into marks (ni3 → nǐ); KRDict's levels (초급, 중급, 고급) tag Korean words; a Korean word's pronunciation shows in brackets ([가치]). - Smaller: Look up in the composer keeps a noun's article; the Watch filter names its level list; hints and placeholders by language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7070cd6bd2
commit
5479b973fc
153 changed files with 2559 additions and 230 deletions
|
|
@ -286,6 +286,42 @@ box, in the order they're written) and the components KanjiVG first splits it
|
|||
into (`c`, when it has any). Then update the release named in the folder's
|
||||
`ATTRIBUTION.md`.
|
||||
|
||||
Chinese characters are written as Make Me a Hanzi draws them
|
||||
([skishore/makemeahanzi](https://github.com/skishore/makemeahanzi), decided
|
||||
2026-10-14 for parity with KanjiVG: its own shapes and the mainland's stroke
|
||||
order, 9,574 characters of both scripts; the strokes under the Arphic Public
|
||||
License, the components under the LGPL). Download its `graphics.txt` and
|
||||
`dictionary.txt`, then:
|
||||
|
||||
```sh
|
||||
dart run halkyon_learn_curator:curate makemeahanzi \
|
||||
--input graphics.txt --dictionary dictionary.txt \
|
||||
--output ../halkyon_learn_flutter/assets/makemeahanzi
|
||||
```
|
||||
|
||||
It writes the same files as for KanjiVG (99 files, 2.7 MB): each stroke is the
|
||||
line Make Me a Hanzi draws through the middle of its outline (its median),
|
||||
scaled from its 1024 square, whose y axis points up from 124 below the bottom,
|
||||
into KanjiVG's 109 square, y pointing down; the outlines are left out, as the
|
||||
app draws and checks strokes along their middle. A character's components are
|
||||
the first split of its decomposition (海: 氵, 每). Each file says how and when it
|
||||
was changed (`notice`), as the Arphic Public License asks of a changed file.
|
||||
They stay a data file beside the app, never in cards (the folder's
|
||||
`ATTRIBUTION.md`).
|
||||
|
||||
The Hangul letters are drawn by the curator itself (CC0), as no openly
|
||||
licensed data draws them with their strokes:
|
||||
|
||||
```sh
|
||||
dart run halkyon_learn_curator:curate hangul \
|
||||
--output ../halkyon_learn_flutter/assets/hangul
|
||||
```
|
||||
|
||||
writes `031.json.gz`: the 40 letters in the order Korean schools teach their
|
||||
strokes (`lib/src/sources/hangul_strokes.dart`), and the pairs of consonants
|
||||
(ㄲ, and the clusters closing syllables, 닭's ㄺ) as their two letters side by
|
||||
side, so a Korean word is written letter by letter.
|
||||
|
||||
## Adding a source
|
||||
|
||||
Write an adapter in `lib/src/sources/` that turns the source into the lines
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import 'dart:io';
|
|||
import 'package:archive/archive.dart';
|
||||
import 'package:args/args.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/cedict.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/hangul_strokes.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/jlpt.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/jmdict.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/jmdict_xml.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/kanjidic.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/kanjivg.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/krdict.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/makemeahanzi.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/tatoeba.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/unihan.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/wiktionary.dart';
|
||||
|
|
@ -30,6 +32,18 @@ import 'package:halkyon_learn_curator/src/wordfreq.dart';
|
|||
/// --input kanjivg-20260714.xml.gz \
|
||||
/// --output ../halkyon_learn_flutter/assets/kanjivg
|
||||
///
|
||||
/// The Hangul letters' strokes, the curator's own, into the app's stroke
|
||||
/// files:
|
||||
///
|
||||
/// dart run halkyon_learn_curator:curate hangul \
|
||||
/// --output ../halkyon_learn_flutter/assets/hangul
|
||||
///
|
||||
/// Make Me a Hanzi into the app's stroke files of Chinese characters:
|
||||
///
|
||||
/// dart run halkyon_learn_curator:curate makemeahanzi \
|
||||
/// --input graphics.txt --dictionary dictionary.txt \
|
||||
/// --output ../halkyon_learn_flutter/assets/makemeahanzi
|
||||
///
|
||||
/// Wiktionary, as kaikki.org's dump of it, into dictionary files of the
|
||||
/// languages named (German and Greek with English meanings, English with
|
||||
/// German ones):
|
||||
|
|
@ -143,6 +157,10 @@ Future<void> main(List<String> args) async {
|
|||
'unihan: CC-CEDICT, whose words tell traditional characters in use '
|
||||
'and rank the characters.',
|
||||
)
|
||||
..addOption(
|
||||
'dictionary',
|
||||
help: "makemeahanzi: its dictionary.txt, for the characters' components.",
|
||||
)
|
||||
..addOption(
|
||||
'korean',
|
||||
help:
|
||||
|
|
@ -382,6 +400,56 @@ Future<void> main(List<String> args) async {
|
|||
);
|
||||
return;
|
||||
}
|
||||
if (source == 'hangul') {
|
||||
final output = options.option('output');
|
||||
if (output == null) {
|
||||
_fail('Usage: curate hangul --output <folder>\n${parser.usage}');
|
||||
}
|
||||
final strokes = hangulStrokes();
|
||||
final folder = await Directory(output).create(recursive: true);
|
||||
final files = strokeFiles(strokes);
|
||||
for (final MapEntry(key: name, value: characters) in files.entries) {
|
||||
await File('${folder.path}/$name.json.gz')
|
||||
.writeAsBytes(gzip.encode(utf8.encode(jsonEncode(characters))));
|
||||
}
|
||||
stdout.writeln(
|
||||
'Wrote the strokes of ${strokes.length} Hangul letters to $output.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (source == 'makemeahanzi') {
|
||||
final input = options.option('input');
|
||||
final dictionary = options.option('dictionary');
|
||||
final output = options.option('output');
|
||||
if (input == null || output == null) {
|
||||
_fail(
|
||||
'Usage: curate makemeahanzi --input graphics.txt '
|
||||
'[--dictionary dictionary.txt] --output <folder>\n${parser.usage}',
|
||||
);
|
||||
}
|
||||
final strokes = makemeahanziStrokes(
|
||||
await File(input).readAsLines(),
|
||||
dictionary: dictionary == null
|
||||
? const []
|
||||
: await File(dictionary).readAsLines(),
|
||||
);
|
||||
final folder = await Directory(output).create(recursive: true);
|
||||
final files = strokeFiles(
|
||||
strokes,
|
||||
notice: makemeahanziNotice(
|
||||
DateTime.now().toUtc().toIso8601String().substring(0, 10),
|
||||
),
|
||||
);
|
||||
for (final MapEntry(key: name, value: characters) in files.entries) {
|
||||
await File('${folder.path}/$name.json.gz')
|
||||
.writeAsBytes(gzip.encode(utf8.encode(jsonEncode(characters))));
|
||||
}
|
||||
stdout.writeln(
|
||||
'Wrote the strokes of ${strokes.length} characters to '
|
||||
'${files.length} files in $output.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final input = options.option('input');
|
||||
final output = options.option('output');
|
||||
if (!['jmdict', 'kanjidic', 'kanjivg'].contains(source) ||
|
||||
|
|
|
|||
118
halkyon_learn_curator/lib/src/sources/hangul_strokes.dart
Normal file
118
halkyon_learn_curator/lib/src/sources/hangul_strokes.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:halkyon_learn_curator/src/sources/kanjivg.dart';
|
||||
|
||||
/// How the Hangul letters (jamo) are written, the curator's own (CC0): no
|
||||
/// openly licensed data draws them, so they are drawn here, each stroke a
|
||||
/// line through KanjiVG's 109 × 109 square, in the order Korean schools
|
||||
/// teach (ㄹ: ㄱ, the middle bar, ㄴ; ㅇ from the top, anticlockwise).
|
||||
/// Doubled consonants and the clusters that close syllables (ㄲ, ㄺ) are
|
||||
/// their two letters side by side, each narrowed to its half.
|
||||
Map<String, CharacterStrokes> hangulStrokes() => {
|
||||
for (final MapEntry(key: letter, value: strokes) in _letters.entries)
|
||||
letter: (strokes: strokes, components: const []),
|
||||
for (final (letter, left, right) in _pairs)
|
||||
letter: (
|
||||
strokes: [
|
||||
for (final stroke in _letters[left]!) _narrowed(stroke, 12, 53),
|
||||
for (final stroke in _letters[right]!) _narrowed(stroke, 56, 97),
|
||||
],
|
||||
components: [left, right],
|
||||
),
|
||||
};
|
||||
|
||||
/// The letters drawn on their own, each filling the square as a letter
|
||||
/// learnt alone is written.
|
||||
const _letters = {
|
||||
'ㄱ': ['M24,26 L80,26 L79,58 L66,88'],
|
||||
'ㄴ': ['M28,22 L28,82 L86,82'],
|
||||
'ㄷ': ['M26,24 L84,24', 'M28,24 L28,84 L86,84'],
|
||||
'ㄹ': ['M26,22 L80,22 L80,52', 'M28,52 L80,52', 'M28,52 L28,86 L84,86'],
|
||||
'ㅁ': ['M26,24 L26,86', 'M26,24 L82,24 L82,86', 'M26,86 L82,86'],
|
||||
'ㅂ': ['M28,20 L28,86', 'M80,20 L80,86', 'M28,52 L80,52', 'M28,86 L80,86'],
|
||||
'ㅅ': ['M56,20 L52,48 L24,86', 'M52,50 L86,86'],
|
||||
'ㅇ': [_ieung],
|
||||
'ㅈ': ['M24,26 L80,26 L50,56 L22,88', 'M52,54 L86,88'],
|
||||
'ㅊ': ['M54,12 L56,24', 'M24,34 L80,34 L50,62 L22,92', 'M52,60 L86,92'],
|
||||
'ㅋ': ['M24,26 L80,26 L79,58 L66,88', 'M26,54 L78,54'],
|
||||
'ㅌ': ['M28,22 L84,22', 'M28,52 L82,52', 'M28,22 L28,86 L86,86'],
|
||||
'ㅍ': ['M22,24 L86,24', 'M38,24 L40,84', 'M70,24 L68,84', 'M18,86 L90,86'],
|
||||
'ㅎ': ['M54,10 L54,22', 'M26,32 L82,32', _hieutCircle],
|
||||
'ㅏ': ['M46,12 L46,96', 'M46,52 L76,52'],
|
||||
'ㅑ': ['M46,12 L46,96', 'M46,40 L76,40', 'M46,64 L76,64'],
|
||||
'ㅓ': ['M30,52 L62,52', 'M62,12 L62,96'],
|
||||
'ㅕ': ['M30,40 L62,40', 'M30,64 L62,64', 'M62,12 L62,96'],
|
||||
'ㅗ': ['M54,38 L54,66', 'M14,68 L94,68'],
|
||||
'ㅛ': ['M40,40 L40,66', 'M68,40 L68,66', 'M14,68 L94,68'],
|
||||
'ㅜ': ['M14,42 L94,42', 'M54,44 L54,76'],
|
||||
'ㅠ': ['M14,42 L94,42', 'M40,44 L40,76', 'M68,44 L68,76'],
|
||||
'ㅡ': ['M14,54 L94,54'],
|
||||
'ㅣ': ['M54,12 L54,96'],
|
||||
'ㅐ': ['M34,12 L34,96', 'M34,52 L56,52', 'M66,12 L66,96'],
|
||||
'ㅒ': ['M34,12 L34,96', 'M34,40 L56,40', 'M34,64 L56,64', 'M66,12 L66,96'],
|
||||
'ㅔ': ['M22,52 L48,52', 'M48,12 L48,96', 'M72,12 L72,96'],
|
||||
'ㅖ': ['M22,40 L48,40', 'M22,64 L48,64', 'M48,12 L48,96', 'M72,12 L72,96'],
|
||||
'ㅘ': ['M36,48 L36,68', 'M10,70 L60,70', 'M72,10 L72,98', 'M72,54 L94,54'],
|
||||
'ㅙ': [
|
||||
'M30,48 L30,68',
|
||||
'M8,70 L50,70',
|
||||
'M62,10 L62,98',
|
||||
'M62,54 L78,54',
|
||||
'M86,10 L86,98',
|
||||
],
|
||||
'ㅚ': ['M38,48 L38,68', 'M10,70 L64,70', 'M78,10 L78,98'],
|
||||
'ㅝ': ['M8,46 L58,46', 'M34,48 L34,76', 'M56,58 L76,58', 'M76,10 L76,98'],
|
||||
'ㅞ': [
|
||||
'M6,46 L48,46',
|
||||
'M28,48 L28,76',
|
||||
'M50,58 L64,58',
|
||||
'M64,10 L64,98',
|
||||
'M86,10 L86,98',
|
||||
],
|
||||
'ㅟ': ['M8,46 L62,46', 'M36,48 L36,76', 'M78,10 L78,98'],
|
||||
'ㅢ': ['M10,58 L64,58', 'M78,10 L78,98'],
|
||||
};
|
||||
|
||||
/// ㅇ's circle, from its top anticlockwise.
|
||||
const _ieung =
|
||||
'M54.5,24.5 C38,24.5 24.5,38 24.5,54.5 C24.5,71 38,84.5 54.5,84.5 '
|
||||
'C71,84.5 84.5,71 84.5,54.5 C84.5,38 71,24.5 54.5,24.5';
|
||||
|
||||
/// ㅎ's circle, smaller below its bars.
|
||||
const _hieutCircle =
|
||||
'M54,42 C42,42 32,52 32,64 C32,76 42,86 54,86 C66,86 76,76 76,64 '
|
||||
'C76,52 66,42 54,42';
|
||||
|
||||
/// The letters of two consonants side by side: the doubled ones, and the
|
||||
/// clusters that close syllables (닭's ㄺ).
|
||||
const _pairs = [
|
||||
('ㄲ', 'ㄱ', 'ㄱ'),
|
||||
('ㄸ', 'ㄷ', 'ㄷ'),
|
||||
('ㅃ', 'ㅂ', 'ㅂ'),
|
||||
('ㅆ', 'ㅅ', 'ㅅ'),
|
||||
('ㅉ', 'ㅈ', 'ㅈ'),
|
||||
('ㄳ', 'ㄱ', 'ㅅ'),
|
||||
('ㄵ', 'ㄴ', 'ㅈ'),
|
||||
('ㄶ', 'ㄴ', 'ㅎ'),
|
||||
('ㄺ', 'ㄹ', 'ㄱ'),
|
||||
('ㄻ', 'ㄹ', 'ㅁ'),
|
||||
('ㄼ', 'ㄹ', 'ㅂ'),
|
||||
('ㄽ', 'ㄹ', 'ㅅ'),
|
||||
('ㄾ', 'ㄹ', 'ㅌ'),
|
||||
('ㄿ', 'ㄹ', 'ㅍ'),
|
||||
('ㅀ', 'ㄹ', 'ㅎ'),
|
||||
('ㅄ', 'ㅂ', 'ㅅ'),
|
||||
];
|
||||
|
||||
/// [stroke] with its x coordinates narrowed from the square's 10 to 99
|
||||
/// into [from] to [to], its y as it was.
|
||||
String _narrowed(String stroke, double from, double to) {
|
||||
String scaled(String x) {
|
||||
final value = from + (double.parse(x) - 10) * (to - from) / 89;
|
||||
return value.toStringAsFixed(1).replaceAll(RegExp(r'\.0$'), '');
|
||||
}
|
||||
|
||||
// Every point is `x,y` after a command letter or a space.
|
||||
return stroke.replaceAllMapped(
|
||||
RegExp(r'(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)'),
|
||||
(match) => '${scaled(match[1]!)},${match[2]}',
|
||||
);
|
||||
}
|
||||
|
|
@ -44,14 +44,20 @@ String strokeFileOf(String character) =>
|
|||
(character.runes.first >> 8).toRadixString(16).padLeft(3, '0');
|
||||
|
||||
/// [strokes] as the app's stroke files, by name: for each character its
|
||||
/// strokes (`s`) and, when it has any, its components (`c`).
|
||||
/// strokes (`s`) and, when it has any, its components (`c`); each file
|
||||
/// with [notice] (`notice`), where its licence asks a changed file to say
|
||||
/// how it was changed.
|
||||
Map<String, Map<String, Object>> strokeFiles(
|
||||
Map<String, CharacterStrokes> strokes,
|
||||
) {
|
||||
Map<String, CharacterStrokes> strokes, {
|
||||
String? notice,
|
||||
}) {
|
||||
final files = <String, Map<String, Object>>{};
|
||||
for (final MapEntry(key: character, value: (:strokes, :components))
|
||||
in strokes.entries) {
|
||||
files.putIfAbsent(strokeFileOf(character), () => {})[character] = {
|
||||
files.putIfAbsent(
|
||||
strokeFileOf(character),
|
||||
() => {'notice': ?notice},
|
||||
)[character] = {
|
||||
's': strokes,
|
||||
if (components.isNotEmpty) 'c': components,
|
||||
};
|
||||
|
|
|
|||
133
halkyon_learn_curator/lib/src/sources/makemeahanzi.dart
Normal file
133
halkyon_learn_curator/lib/src/sources/makemeahanzi.dart
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:halkyon_learn_curator/src/sources/kanjivg.dart';
|
||||
|
||||
/// How Chinese characters are written, from Make Me a Hanzi
|
||||
/// (<https://github.com/skishore/makemeahanzi>): its `graphics.txt`
|
||||
/// ([graphics], a JSON object per line, Arphic Public License) as each
|
||||
/// character's strokes in the order they are written, each the median
|
||||
/// Make Me a Hanzi draws through its outline as a line in KanjiVG's 109 ×
|
||||
/// 109 square (the outlines are left out: the app draws and checks strokes
|
||||
/// along their middle, as KanjiVG gives them); and its `dictionary.txt`
|
||||
/// ([dictionary], LGPL-3.0-or-later) as the characters each is built from,
|
||||
/// the first split of its decomposition (海: 氵, 每).
|
||||
Map<String, CharacterStrokes> makemeahanziStrokes(
|
||||
Iterable<String> graphics, {
|
||||
Iterable<String> dictionary = const [],
|
||||
}) {
|
||||
final components = <String, List<String>>{};
|
||||
for (final line in dictionary) {
|
||||
if (line.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final json = jsonDecode(line);
|
||||
if (json case {
|
||||
'character': final String character,
|
||||
'decomposition': final String decomposition,
|
||||
}) {
|
||||
components[character] = _firstSplit(decomposition);
|
||||
}
|
||||
}
|
||||
return {
|
||||
for (final line in graphics)
|
||||
if (line.trim().isNotEmpty)
|
||||
if (jsonDecode(line) case {
|
||||
'character': final String character,
|
||||
'medians': final List<Object?> medians,
|
||||
})
|
||||
character: (
|
||||
strokes: [
|
||||
for (final median in medians)
|
||||
if (median is List<Object?> && median.isNotEmpty)
|
||||
_pathOf(median),
|
||||
],
|
||||
components: components[character] ?? const [],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// A median, Make Me a Hanzi's points (`[[336, 704], …]`) in its 1024
|
||||
/// square whose y axis points up from 124 below the bottom, as SVG path
|
||||
/// data in KanjiVG's 109 square, y pointing down.
|
||||
String _pathOf(List<Object?> median) {
|
||||
String point(Object? xy) {
|
||||
final [x, y] = switch (xy) {
|
||||
[final num x, final num y] => [x, y],
|
||||
_ => const [0, 0],
|
||||
};
|
||||
String scaled(num value) =>
|
||||
(value * 109 / 1024).toStringAsFixed(1).replaceAll(RegExp(r'\.0$'), '');
|
||||
return '${scaled(x)},${scaled(900 - y)}';
|
||||
}
|
||||
|
||||
return [
|
||||
'M${point(median.first)}',
|
||||
for (final xy in median.skip(1)) 'L${point(xy)}',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
/// The characters an ideographic description sequence ([ids], `⿰氵每`)
|
||||
/// joins at its top: each part that is one character, none that is a
|
||||
/// sequence of its own or unknown (`?`).
|
||||
List<String> _firstSplit(String ids) {
|
||||
final characters = ids.runes.map(String.fromCharCode).toList();
|
||||
if (characters.isEmpty || !_operands.containsKey(characters.first)) {
|
||||
return const [];
|
||||
}
|
||||
final parts = <String>[];
|
||||
var at = 1;
|
||||
for (var part = 0; part < _operands[characters.first]!; part++) {
|
||||
if (at >= characters.length) {
|
||||
break;
|
||||
}
|
||||
final start = at;
|
||||
at = _skip(characters, at);
|
||||
if (at == start + 1 && characters[start] != '?') {
|
||||
parts.add(characters[start]);
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/// Where the part of [characters] starting at [at] ends.
|
||||
int _skip(List<String> characters, int at) {
|
||||
if (at >= characters.length) {
|
||||
return at;
|
||||
}
|
||||
final operands = _operands[characters[at]];
|
||||
if (operands == null) {
|
||||
return at + 1;
|
||||
}
|
||||
var next = at + 1;
|
||||
for (var i = 0; i < operands; i++) {
|
||||
next = _skip(characters, next);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/// The ideographic description characters, by how many parts each joins.
|
||||
const _operands = {
|
||||
'⿰': 2,
|
||||
'⿱': 2,
|
||||
'⿲': 3,
|
||||
'⿳': 3,
|
||||
'⿴': 2,
|
||||
'⿵': 2,
|
||||
'⿶': 2,
|
||||
'⿷': 2,
|
||||
'⿸': 2,
|
||||
'⿹': 2,
|
||||
'⿺': 2,
|
||||
'⿻': 2,
|
||||
};
|
||||
|
||||
/// What each stroke file made from Make Me a Hanzi says of it, as the
|
||||
/// Arphic Public License asks of a changed file: how and when ([date]) it
|
||||
/// was changed.
|
||||
String makemeahanziNotice(String date) =>
|
||||
"Changed from Make Me a Hanzi's graphics.txt (Arphic Public License) and "
|
||||
'dictionary.txt (LGPL-3.0-or-later) by halkyon_learn_curator on $date: '
|
||||
"each stroke's median scaled into a 109 by 109 square with its y axis "
|
||||
'pointing down, the outlines left out, the characters split up across '
|
||||
"files by code point; a character's components are the first split of "
|
||||
'its decomposition.';
|
||||
|
|
@ -70,3 +70,11 @@ three senses, their English and Japanese equivalents and two examples, without
|
|||
sound files or related words. 한국어기초사전 is by the National Institute of
|
||||
Korean Language (국립국어원), CC BY-SA 2.0 KR
|
||||
(<https://krdict.korean.go.kr/kboardPolicy/copyRightTermsInfo>).
|
||||
|
||||
`makemeahanzi-graphics-sample.txt` and `makemeahanzi-dictionary-sample.txt` hold
|
||||
the lines of Make Me a Hanzi's `graphics.txt` and `dictionary.txt` (commit
|
||||
`618dbab`, 2018-10-16, <https://github.com/skishore/makemeahanzi>) for 水 and 海.
|
||||
The strokes derive from the fonts Arphic PL KaitiM GB and Arphic PL UKai, © 1999
|
||||
Arphic Technology Co., Ltd., under the Arphic Public License (its text is in the
|
||||
app's `assets/makemeahanzi/ARPHICPL.TXT`); the dictionary's lines are under the
|
||||
LGPL, version 3 or later (`assets/makemeahanzi/LGPL.txt`).
|
||||
|
|
|
|||
2
halkyon_learn_curator/test/fixtures/makemeahanzi-dictionary-sample.txt
vendored
Normal file
2
halkyon_learn_curator/test/fixtures/makemeahanzi-dictionary-sample.txt
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"character":"水","definition":"water, liquid, lotion, juice","pinyin":["shuǐ"],"decomposition":"⿻亅?","etymology":{"type":"pictographic","hint":"A river running between two banks; compare 川"},"radical":"水","matches":[[0],null,null,null]}
|
||||
{"character":"海","definition":"sea, ocean; maritime","pinyin":["hǎi"],"decomposition":"⿰氵每","etymology":{"type":"pictophonetic","phonetic":"每","semantic":"氵","hint":"water"},"radical":"氵","matches":[[0],[0],[0],[1],[1],[1],[1],[1],[1],[1]]}
|
||||
2
halkyon_learn_curator/test/fixtures/makemeahanzi-graphics-sample.txt
vendored
Normal file
2
halkyon_learn_curator/test/fixtures/makemeahanzi-graphics-sample.txt
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"character":"水","strokes":["M 535 506 Q 538 699 560 762 Q 578 793 520 817 Q 486 836 465 830 Q 447 823 463 799 Q 485 771 486 736 Q 490 697 478 121 Q 477 97 463 88 Q 454 81 432 88 Q 407 94 382 99 Q 348 111 351 100 Q 352 93 373 78 Q 440 24 457 -5 Q 476 -41 493 -42 Q 508 -43 524 -7 Q 543 41 541 117 Q 531 294 534 470 L 535 506 Z","M 154 501 Q 141 501 139 492 Q 138 485 153 477 Q 199 452 227 461 Q 333 489 343 489 Q 359 486 347 456 Q 296 326 249 262 Q 201 190 114 119 Q 99 106 110 103 Q 120 102 141 113 Q 217 153 281 224 Q 342 288 419 454 Q 429 478 441 489 Q 456 501 447 511 Q 437 524 399 537 Q 378 549 336 530 Q 270 509 154 501 Z","M 590 446 Q 630 476 766 584 Q 787 603 814 615 Q 838 627 825 647 Q 809 666 779 681 Q 752 696 738 692 Q 723 691 729 675 Q 735 639 659 553 Q 620 508 577 459 C 557 436 566 428 590 446 Z","M 577 459 Q 555 484 535 506 C 515 528 516 494 534 470 Q 756 161 817 160 Q 898 169 967 175 Q 995 178 996 185 Q 997 192 964 205 Q 810 253 753 295 Q 690 346 590 446 L 577 459 Z"],"medians":[[[473,814],[500,795],[521,770],[508,455],[507,91],[485,42],[442,56],[371,91],[369,98],[358,98]],[[147,491],[209,482],[354,512],[381,505],[392,495],[384,459],[353,387],[280,261],[187,160],[113,110]],[[737,682],[750,671],[766,637],[721,583],[591,461],[590,452]],[[539,500],[550,465],[618,387],[729,271],[782,229],[820,207],[990,185]]]}
|
||||
{"character":"海","strokes":["M 246 711 Q 267 686 289 655 Q 302 639 320 636 Q 333 635 340 649 Q 349 665 343 700 Q 339 730 249 767 Q 233 773 224 771 Q 218 768 218 753 Q 219 740 246 711 Z","M 154 542 Q 191 493 228 471 Q 244 467 255 481 Q 262 494 260 512 Q 254 537 236 553 Q 188 586 150 586 Q 146 587 142 585 Q 135 584 137 570 Q 138 557 154 542 Z","M 123 190 Q 111 186 113 168 Q 131 89 165 72 Q 172 65 183 68 Q 190 69 191 95 Q 197 141 293 399 Q 300 415 299 422 Q 298 432 289 425 Q 273 415 163 232 Q 151 211 123 190 Z","M 520 686 Q 593 788 594 793 Q 593 796 593 798 Q 589 810 563 832 Q 538 848 520 849 Q 502 848 510 827 Q 525 793 458 683 Q 427 632 383 577 Q 373 567 371 561 Q 367 551 381 553 Q 420 559 500 661 L 520 686 Z","M 500 661 Q 548 640 599 650 Q 687 666 778 684 Q 820 694 826 699 Q 836 706 831 716 Q 824 728 794 736 Q 770 742 661 710 Q 579 692 520 686 C 490 683 472 672 500 661 Z","M 754 149 Q 809 139 863 120 Q 884 113 894 115 Q 904 119 898 137 Q 889 156 843 189 Q 825 205 762 196 L 694 195 Q 550 196 422 187 Q 409 186 413 196 Q 449 281 474 343 L 488 379 Q 506 428 517 460 Q 527 497 535 517 Q 538 524 538 529 C 541 538 541 538 521 557 Q 487 594 475 586 Q 468 577 467 568 Q 488 514 443 375 L 430 336 Q 388 227 345 190 Q 338 183 339 169 Q 343 151 360 132 Q 367 122 382 129 Q 419 147 493 154 Q 578 166 685 158 L 754 149 Z","M 685 158 Q 667 80 651 53 Q 641 37 619 45 Q 553 64 557 68 Q 526 80 548 50 Q 582 7 607 -37 Q 629 -68 649 -56 Q 728 -11 754 149 L 762 196 Q 775 311 780 365 L 785 413 Q 794 503 812 525 Q 831 549 819 561 Q 795 583 758 598 Q 740 607 718 599 Q 669 583 631 574 Q 589 564 521 557 C 491 554 508 528 538 529 Q 545 528 551 528 Q 572 528 688 560 Q 709 564 723 556 Q 739 544 726 405 L 722 365 Q 710 262 694 195 L 685 158 Z","M 579 484 Q 595 426 617 415 Q 630 411 641 424 Q 647 436 645 457 Q 635 488 592 507 Q 585 511 580 502 Q 576 495 579 484 Z","M 780 365 Q 859 368 941 362 Q 960 362 966 370 Q 972 380 956 395 Q 896 440 855 424 Q 822 420 785 413 L 726 405 Q 677 401 621 393 Q 557 387 488 379 L 443 375 Q 382 371 317 365 Q 296 364 311 347 Q 324 334 341 327 Q 360 321 377 325 Q 402 331 430 336 L 474 343 Q 568 362 722 365 L 780 365 Z","M 552 292 Q 567 253 590 231 Q 602 225 612 234 Q 619 243 622 256 Q 622 277 611 291 Q 599 306 571 319 Q 559 326 551 326 Q 545 326 545 316 Q 544 306 552 292 Z"],"medians":[[[229,762],[301,700],[321,656]],[[147,575],[210,530],[235,493]],[[177,78],[165,108],[161,162],[292,418]],[[519,836],[542,813],[549,793],[530,752],[463,648],[402,578],[379,561]],[[508,661],[530,670],[632,679],[754,707],[820,710]],[[480,577],[505,524],[492,462],[449,331],[385,194],[384,171],[416,164],[592,179],[775,172],[824,164],[888,126]],[[531,556],[548,544],[559,545],[715,580],[743,575],[772,543],[735,230],[710,107],[683,34],[668,14],[642,-4],[548,61]],[[590,495],[620,450],[623,432]],[[314,356],[368,346],[631,379],[883,397],[955,376]],[[554,317],[589,277],[599,247]]]}
|
||||
56
halkyon_learn_curator/test/stroke_sources_test.dart
Normal file
56
halkyon_learn_curator/test/stroke_sources_test.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:halkyon_learn_curator/src/sources/hangul_strokes.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/kanjivg.dart';
|
||||
import 'package:halkyon_learn_curator/src/sources/makemeahanzi.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test("Make Me a Hanzi's medians as lines in KanjiVG's square, y pointing "
|
||||
'down; the first split of the decomposition as components', () {
|
||||
final strokes = makemeahanziStrokes(
|
||||
File('test/fixtures/makemeahanzi-graphics-sample.txt').readAsLinesSync(),
|
||||
dictionary: File('test/fixtures/makemeahanzi-dictionary-sample.txt')
|
||||
.readAsLinesSync(),
|
||||
);
|
||||
|
||||
final water = strokes['水']!;
|
||||
expect(water.strokes, hasLength(4));
|
||||
// [473, 814] in its square whose y points up from 124 below the
|
||||
// bottom: x 473 × 109 / 1024, y (900 − 814) × 109 / 1024.
|
||||
expect(water.strokes.first, startsWith('M50.3,9.2 L53.2,11.2'));
|
||||
// 水's decomposition (⿻亅?) names one part; the unknown one is left out.
|
||||
expect(water.components, ['亅']);
|
||||
expect(strokes['海']!.components, ['氵', '每']);
|
||||
expect(strokes['海']!.strokes, hasLength(10));
|
||||
});
|
||||
|
||||
test('stroke files say how they were changed where the licence asks', () {
|
||||
final files = strokeFiles(
|
||||
makemeahanziStrokes(
|
||||
File('test/fixtures/makemeahanzi-graphics-sample.txt')
|
||||
.readAsLinesSync(),
|
||||
),
|
||||
notice: makemeahanziNotice('2026-10-14'),
|
||||
);
|
||||
|
||||
expect(files.keys, ['06c', '06d']);
|
||||
expect(files['06c']!['notice'], contains('on 2026-10-14'));
|
||||
expect(files['06c']!['水'], isA<Map<String, Object>>());
|
||||
});
|
||||
|
||||
test('the Hangul letters: each in the order schools teach, the pairs of '
|
||||
'consonants side by side, each in its half', () {
|
||||
final strokes = hangulStrokes();
|
||||
|
||||
expect(strokes, hasLength(51));
|
||||
expect(strokes['ㄹ']!.strokes, hasLength(3));
|
||||
expect(strokes['ㅇ']!.strokes.single, startsWith('M54.5,24.5 C38,24.5'));
|
||||
final cluster = strokes['ㄺ']!;
|
||||
expect(cluster.components, ['ㄹ', 'ㄱ']);
|
||||
expect(cluster.strokes, hasLength(4));
|
||||
// ㄹ narrowed into the left half, ㄱ into the right one.
|
||||
expect(cluster.strokes.first, startsWith('M19.4,22 L44.2,22'));
|
||||
expect(cluster.strokes.last, startsWith('M62.4,26'));
|
||||
});
|
||||
}
|
||||
BIN
halkyon_learn_flutter/assets/hangul/031.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/hangul/031.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/02e.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/02e.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/034.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/034.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/035.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/035.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/037.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/037.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/038.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/038.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/039.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/039.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/03b.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/03b.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/03c.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/03c.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/03f.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/03f.json.gz
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/makemeahanzi/043.json.gz
Normal file
BIN
halkyon_learn_flutter/assets/makemeahanzi/043.json.gz
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue