Compare commits
7 Commits
c49e62d052
...
c7c1070f74
Author | SHA1 | Date |
---|---|---|
|
c7c1070f74 | |
|
5469ce7fec | |
|
bc2b3ac0b4 | |
|
8f24f23130 | |
|
1f40c781cd | |
|
0da46316f2 | |
|
14cde2ee70 |
|
@ -1,8 +1,125 @@
|
||||||
巹
|
刄
|
||||||
|
Just an 異体字 of 刃... Dunno, doesn't make much sense to me
|
||||||
|
|
||||||
|
吨
|
||||||
|
For phonetic purposes, used to say "ton" as in the unit of weight... that's it.
|
||||||
|
|
||||||
|
刕
|
||||||
|
Kanji used for the names of weapons and stuff like that I guess just cuz it
|
||||||
|
looks kinda cool.
|
||||||
|
|
||||||
|
吣
|
||||||
|
Puking of cats and dogs, also foul language. This is the simplification of 唚.
|
||||||
|
Doesn't seem to be all that used. Can't find examples of any words with this.
|
||||||
|
|
||||||
|
吡
|
||||||
|
Only used in transliteration from other languages for "bi", that's kinda it.
|
||||||
|
It kinda makes sense doe.
|
||||||
|
|
||||||
|
叽
|
||||||
|
Just a variaton on 叫 that isn't all that used it seems. Not worth it, it seems
|
||||||
|
from my little research.
|
||||||
|
|
||||||
|
叚
|
||||||
|
Variant form of 假 which kinda seems to mean borrowing or something. Crazy.
|
||||||
|
|
||||||
|
叕
|
||||||
|
No information on it. Probably old version of something that turned into
|
||||||
|
something with radicals or whatever.
|
||||||
|
|
||||||
|
厾
|
||||||
|
To touch lightly, to poke (with a stick, or with whatever). Maybe this pops
|
||||||
|
up in the future, revive in such a case but it's not really seeming like a very
|
||||||
|
popular word from my search
|
||||||
|
|
||||||
|
厤
|
||||||
|
Seems to be a historial version of 歴, not seen anywhere but dictionaries of
|
||||||
|
ancient stuff. Not really worth it
|
||||||
|
|
||||||
|
厣
|
||||||
|
A covering flap in animals, such as a gill cover.
|
||||||
|
Not very interesting for me to learn
|
||||||
|
|
||||||
|
厝
|
||||||
|
Not really used much. Whetstone, used in some expressions but failed to find
|
||||||
|
usages of them. Seems to be also a variant of 錯, which is interesting.
|
||||||
|
|
||||||
|
厓
|
||||||
|
Old version of 崖 and 涯. I guess it used to mean kinda both and the radicals
|
||||||
|
were added later
|
||||||
|
|
||||||
|
剅
|
||||||
|
Seems like it used only in the name of a specific place and in dialects. I can't
|
||||||
|
find youtube videos that use it even, really.
|
||||||
|
|
||||||
|
刿
|
||||||
|
Not really used for anything. Simplification of 劌 which isn't really all that
|
||||||
|
used anyway.
|
||||||
|
|
||||||
|
刽
|
||||||
|
Simplified variant of a kanji that isn't really used. Only one 熟語 in the
|
||||||
|
chinese dictionary, and the two in the Wiktionary are literary and archaic.
|
||||||
|
|
||||||
|
刱
|
||||||
|
Really I should've learnt this one but oh well. A variant of 剏, but this is
|
||||||
|
accepted for the kanken.
|
||||||
|
|
||||||
|
刧
|
||||||
|
Variant form of 劫, not really all that interesting.
|
||||||
|
|
||||||
|
刬
|
||||||
|
Same as 剗, it's just a simplified version:
|
||||||
|
> Another variant of 鏟... how? I must be missing something.
|
||||||
|
|
||||||
|
厎
|
||||||
|
Not used at all lol. It seems to be an 異体字, means whetstone. Seems like
|
||||||
|
to be treated kinda like 磨 in japanese or something
|
||||||
|
|
||||||
|
厍
|
||||||
|
Only used in names, it's not the simplified version of 庫
|
||||||
|
|
||||||
|
剟
|
||||||
|
To delete or to cut into blocks... not much else. There's no reason in learning
|
||||||
|
this. In japanese yet another kezuru lol.
|
||||||
|
|
||||||
|
剜
|
||||||
|
There's no info online about it. It just means "to scoop" as in "えぐる"
|
||||||
|
(yet another one). Doesn't make much sense to learn.
|
||||||
|
(it's not cutting an arm as a punishment)
|
||||||
|
|
||||||
|
剗
|
||||||
|
Another variant of 鏟... how? I must be missing something.
|
||||||
|
|
||||||
|
剐
|
||||||
|
Unorthodox variant of 剮. Doesn't seem to be all that used, not worth it.
|
||||||
|
Seems to mean to cut flesh from bone
|
||||||
|
|
||||||
|
剡
|
||||||
|
Chinese character, used in names of stuff, not much else. Doesn't seem to be
|
||||||
|
worth it.
|
||||||
|
|
||||||
|
剷
|
||||||
|
Variant form of 鏟, apparently. It's only used in Taiwan and it kinda means
|
||||||
|
shoveling and leveling. Not worth the effort.
|
||||||
|
|
||||||
|
劁
|
||||||
|
To cut, used in the word for castrating livestock. That's it. Can't find
|
||||||
|
usages or even modern usages.
|
||||||
|
|
||||||
|
劐
|
||||||
|
Can't find really much info or relevancy. Can't find usage of it either.
|
||||||
|
|
||||||
|
劦
|
||||||
|
Unused really. Changed over to other kanji like 協 and 捏
|
||||||
|
https://en.wiktionary.org/wiki/%E5%8A%A6
|
||||||
|
|
||||||
|
劢
|
||||||
|
Simplified form of 勱, as far as I can tell it kinda means exherting oneself
|
||||||
|
and also as a simplification for 励む. There's no real value in it.
|
||||||
|
|
||||||
|
卺
|
||||||
ladle for holding wine made from dried gourd (匏), used in ancient marriage
|
ladle for holding wine made from dried gourd (匏), used in ancient marriage
|
||||||
rituals.
|
rituals. Not used at all
|
||||||
Not very interesting but also it's the simplified version of 巹... Idk, a little
|
|
||||||
stretched.
|
|
||||||
|
|
||||||
卬
|
卬
|
||||||
It's mostly only used as a component, the origins are unclear but it's known
|
It's mostly only used as a component, the origins are unclear but it's known
|
||||||
|
@ -68,11 +185,6 @@ Old variant/origin? of 陶, which is 常用漢字 / 名前に使える漢字 and
|
||||||
to kinda mean "pottery" , "clay" . It's used in 陶器[とうき].
|
to kinda mean "pottery" , "clay" . It's used in 陶器[とうき].
|
||||||
Not interesting on its own
|
Not interesting on its own
|
||||||
|
|
||||||
匜
|
|
||||||
Old vase for holding water/wine. Not used aside from that.
|
|
||||||
https://en.wikipedia.org/wiki/Yi_(vessel)
|
|
||||||
REVISIT
|
|
||||||
|
|
||||||
匦
|
匦
|
||||||
Simplified form of 匭. Not of interest at all. Small box or something.
|
Simplified form of 匭. Not of interest at all. Small box or something.
|
||||||
|
|
||||||
|
|
|
@ -26878,5 +26878,6 @@
|
||||||
"䨻": "y/23050",
|
"䨻": "y/23050",
|
||||||
"𬚩": "y/28415",
|
"𬚩": "y/28415",
|
||||||
"𠔻": "y/28412",
|
"𠔻": "y/28412",
|
||||||
"𪚥": "y/28413"
|
"𪚥": "y/28413",
|
||||||
|
"办": "y/13156"
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,6 +189,10 @@
|
||||||
鄆潞碣媓嗉瓱嚕畤縉怵锻輗劓輟玷煆帕蒺箛吭笒偆晹偰毉昺啤冓罏秖節禎閶搨辤凞邗彀嚈噠
|
鄆潞碣媓嗉瓱嚕畤縉怵锻輗劓輟玷煆帕蒺箛吭笒偆晹偰毉昺啤冓罏秖節禎閶搨辤凞邗彀嚈噠
|
||||||
瑭潢昪煜嬀㬎芾忢鍳溉袗鸊鷉鱖陏鏧鰧讌糝箟髁糙譛离㷔壩芮斝蘄虢穌鼂瑇瑒耊找凳柰縧麈
|
瑭潢昪煜嬀㬎芾忢鍳溉袗鸊鷉鱖陏鏧鰧讌糝箟髁糙譛离㷔壩芮斝蘄虢穌鼂瑇瑒耊找凳柰縧麈
|
||||||
盔撾鮊鱔黿攢驊騮驌駃騠騊駼顖綉扆拄炁拽憨鰣殮鄷壳猙扒倮璇戩瘖瘂髒豭瑗癯踽偬鐇滎鼇
|
盔撾鮊鱔黿攢驊騮驌駃騠騊駼顖綉扆拄炁拽憨鰣殮鄷壳猙扒倮璇戩瘖瘂髒豭瑗癯踽偬鐇滎鼇
|
||||||
盦惲翬琿龢芩暻犹洳溏沺呴棃筷豬駙珓崁⻆歃賾澂拕泆瀅氂轀鬭斁鐲懟阨頥忼擕隳熛鄄渮纊
|
盦惲翬琿龢芩暻犹洳溏沺呴棃筷豬駙珓崁⻆歃賾澂絋鶇幞揀颫犖鞢韴韈嵓齩髃𥇥𥆩䑛虗殱𤄃
|
||||||
鶇濰帮紓幞蜇阴竽傈轌蕋駉盻禘鄹愀纆絏杇棖臧鍇騂魋忞葸悾黻勉忮踧踖絺綌紾飪阼喭僎訒
|
姱裼蟫蚜炅佟綦虁剕甗絺塤綌璦菹貙詡偀蝪篶覬覦餤踹摔鷖躺腁霪睟摽焄鷆听哎珱昰鯳魵鮴
|
||||||
慝蕢柙櫝肸訕耰蓧鼗璩靛坷煮糲揀
|
鞁莇筴黻倢伃栝蕡䗪惷蔛蕢鱉尃豨蓚塋郅堋竓纊騭栫祅喼抳犾罧揵檑餺飥邙刕醮羡愡敕兹滚
|
||||||
|
瘕軺蔲汛幗紱嘽膄崫砡槝糫薏鵟癭菔怔忡㾮鉀梍凞鈺蓪枘掫匾傕卬臧轀沚椴嵒轣噐搞您櫤誐
|
||||||
|
儗撇儵呕槫鳫裵澑猬杔膛鶴騸菇琇鐲靖杦畆鰩划呿秂瞪弸蕫皤唏凬帮摒棙岼湶砿飃拕泆瀅氂
|
||||||
|
鬭斁懟阨頥忼擕隳熛鄄渮濰紓蜇阴竽傈轌蕋駉盻禘鄹愀纆絏杇棖鍇騂魋忞葸悾勉忮踧踖紾飪
|
||||||
|
阼喭僎訒慝柙櫝肸訕耰蓧鼗璩靛坷煮糲
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\KoreanProductionNote;
|
||||||
|
use App\Entity\KoreanSentenceNote;
|
||||||
|
use App\Service\AnkiService;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand('app:create:korean:production', 'Create new listening Anki Cards')]
|
||||||
|
class CreateKoreanProductionCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AnkiService $ankiService,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
//$this->addArgument(
|
||||||
|
// 'count',
|
||||||
|
// InputArgument::REQUIRED,
|
||||||
|
// 'Amount of cards to make',
|
||||||
|
//);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$ksns = $this->ankiService->getAllFromClass(KoreanSentenceNote::class);
|
||||||
|
$kpns = $this->ankiService->getAllFromClass(KoreanProductionNote::class);
|
||||||
|
|
||||||
|
$existentTerms = [];
|
||||||
|
foreach ($kpns as $productionNote) {
|
||||||
|
$existentTerms[$productionNote->getTerm()->getKanji()] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//$newNotesCount = intval($input->getArgument('count'));
|
||||||
|
|
||||||
|
$newProductionNotes = [];
|
||||||
|
foreach ($ksns as $sentenceNote) {
|
||||||
|
foreach ($sentenceNote->getTerms() as $term) {
|
||||||
|
$termStr = $term->getKanji();
|
||||||
|
|
||||||
|
if (key_exists($termStr, $existentTerms)) continue;
|
||||||
|
|
||||||
|
$existentTerms[$term->getKanji()] = null;
|
||||||
|
$newProductionNotes[] = KoreanProductionNote::fromNote($sentenceNote, $term);
|
||||||
|
//if (count($newProductionNotes) >= $newNotesCount) break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($newProductionNotes as $newNote) {
|
||||||
|
$this->ankiService->addNote($newNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
<<<FMNT
|
||||||
|
max usage: %0.2f MiB
|
||||||
|
current usage: %0.2f MiB\n
|
||||||
|
FMNT,
|
||||||
|
memory_get_peak_usage() / 1024 / 1024,
|
||||||
|
memory_get_usage() / 1024 / 1024,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,7 +84,7 @@ class CreateProductionCommand extends Command
|
||||||
$sNote = $this->ankiService->getNote($noteIds[array_key_last($noteIds)]);
|
$sNote = $this->ankiService->getNote($noteIds[array_key_last($noteIds)]);
|
||||||
|
|
||||||
$newSlNote = SentenceListeningNote::fromNote($sNote, $term);
|
$newSlNote = SentenceListeningNote::fromNote($sNote, $term);
|
||||||
if (!$this->ankiService->addNote($newSlNote, 'production')) {
|
if (!$this->ankiService->addNote($newSlNote)) {
|
||||||
throw new \Exception('Failed to add note!');
|
throw new \Exception('Failed to add note!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ class KanjiController extends AbstractController
|
||||||
|
|
||||||
if (!$force and file_exists($cacheFile)) return require $cacheFile;
|
if (!$force and file_exists($cacheFile)) return require $cacheFile;
|
||||||
|
|
||||||
$ret = [];
|
$ret = ['lists' => []];
|
||||||
|
|
||||||
$lists = [
|
$lists = [
|
||||||
'sn' => $this->anki->getKnownSnKanjiCounts(),
|
'sn' => $this->anki->getKnownSnKanjiCounts(),
|
||||||
|
@ -70,6 +70,8 @@ class KanjiController extends AbstractController
|
||||||
'ignored' => $this->getIgnoredList(),
|
'ignored' => $this->getIgnoredList(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$ret['lists'] = $lists;
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
$storedLists = [
|
$storedLists = [
|
||||||
'taiwan', 'bushu', 'kyoyou', 'kyuujitai', 'kanken', 'hsk', 'zhcn',
|
'taiwan', 'bushu', 'kyoyou', 'kyuujitai', 'kanken', 'hsk', 'zhcn',
|
||||||
|
@ -77,13 +79,19 @@ class KanjiController extends AbstractController
|
||||||
];
|
];
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
foreach ($storedLists as $storedName) {
|
foreach ($storedLists as $storedName) {
|
||||||
|
$ret['lists'][$storedName] = [];
|
||||||
$storedList = $this->charList->getList($storedName);
|
$storedList = $this->charList->getList($storedName);
|
||||||
|
|
||||||
// Simple list
|
// Simple list
|
||||||
if (key_exists('chars', $storedList)) {
|
if (key_exists('chars', $storedList)) {
|
||||||
|
$ret['lists'][$storedName] = $storedList['chars'];
|
||||||
$lists[$storedName] = $storedList['chars'];
|
$lists[$storedName] = $storedList['chars'];
|
||||||
} else {
|
} else {
|
||||||
foreach ($storedList['sublists'] as $subname => $sublist) {
|
foreach ($storedList['sublists'] as $subname => $sublist) {
|
||||||
|
$ret['lists'][$storedName] = array_merge(
|
||||||
|
$ret['lists'][$storedName],
|
||||||
|
$sublist['chars'],
|
||||||
|
);
|
||||||
$lists["$storedName-$subname"] = $sublist['chars'];
|
$lists["$storedName-$subname"] = $sublist['chars'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,6 +142,7 @@ class KanjiController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render(self::tmpl('grid'), [
|
return $this->render(self::tmpl('grid'), [
|
||||||
|
'char_info' => $charInfo,
|
||||||
'characters' => $chars,
|
'characters' => $chars,
|
||||||
'completed' => $completedRows,
|
'completed' => $completedRows,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
class KoreanProductionNote extends Note
|
||||||
|
{
|
||||||
|
const MODEL_NAME = 'Korean production';
|
||||||
|
const DECK = '한국어::받아쓰기';
|
||||||
|
|
||||||
|
private ?array $mediaInfo = [];
|
||||||
|
private ?Term $term = null;
|
||||||
|
|
||||||
|
public static function fromNote(Note $origNote, Term $term): self
|
||||||
|
{
|
||||||
|
$slNote = new self();
|
||||||
|
foreach (get_object_vars($origNote) as $prop => $value) {
|
||||||
|
$slNote->$prop = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Related fields are updated using the setter
|
||||||
|
$slNote->setTerm($term);
|
||||||
|
// Reset relations and basic data
|
||||||
|
$slNote->id = null;
|
||||||
|
$slNote->model = self::MODEL_NAME;
|
||||||
|
$slNote->cardIds = [];
|
||||||
|
|
||||||
|
return $slNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------- Getters & setters ---
|
||||||
|
|
||||||
|
public function getTerm(): Term
|
||||||
|
{
|
||||||
|
return $this->term;
|
||||||
|
}
|
||||||
|
public function setTerm(Term $term): static
|
||||||
|
{
|
||||||
|
$this->fields['Vocab'] = $term->getKanji();
|
||||||
|
$this->fields['VocabDef'] = $term->toAnkiVocabDef();
|
||||||
|
$this->fields['VocabAudio'] = $term->audio;
|
||||||
|
$this->fields['Sent'] = Note::stringHighlight(
|
||||||
|
$this->fields['Sent'],
|
||||||
|
$term->getKanji(),
|
||||||
|
);
|
||||||
|
$this->term = $term;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------- Anki-related ---
|
||||||
|
|
||||||
|
/** @param array<string, string> $noteInfo */
|
||||||
|
public static function fromAnki(array $noteInfo): static
|
||||||
|
{
|
||||||
|
$note = parent::fromAnki($noteInfo);
|
||||||
|
|
||||||
|
if ($note->getModel() !== self::MODEL_NAME) {
|
||||||
|
throw new \Exception('Trying to parse wrong model');
|
||||||
|
}
|
||||||
|
|
||||||
|
$note->mediaInfo = Note::parseMediaInfo($note->fields['Notes']);
|
||||||
|
|
||||||
|
// Set VocabKanji field
|
||||||
|
$note->term = Term::fromNoteFields($note->fields)[0] ?? null;
|
||||||
|
if ($note->term === null) {
|
||||||
|
throw new \Exception("Couldn't get term for Listening card");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $note;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------- Derived methods ---
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
class KoreanSentenceNote extends Note
|
||||||
|
{
|
||||||
|
const MODEL_NAME = 'Korean sentences';
|
||||||
|
|
||||||
|
private ?array $mediaInfo = [];
|
||||||
|
/** @var list<Term> */
|
||||||
|
private array $terms = [];
|
||||||
|
|
||||||
|
// -------------------------------------------------- Getters & setters ---
|
||||||
|
|
||||||
|
/** @return list<Term> */
|
||||||
|
public function getTerms(): array
|
||||||
|
{
|
||||||
|
return $this->terms;
|
||||||
|
}
|
||||||
|
/** @param list<Term> $terms */
|
||||||
|
public function setTerms(array $terms): static
|
||||||
|
{
|
||||||
|
$this->terms = $terms;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------- Anki-related ---
|
||||||
|
|
||||||
|
/** @param array<string, string> $noteInfo */
|
||||||
|
public static function fromAnki(array $noteInfo): static
|
||||||
|
{
|
||||||
|
$note = parent::fromAnki($noteInfo);
|
||||||
|
|
||||||
|
if ($note->getModel() !== self::MODEL_NAME) {
|
||||||
|
throw new \Exception('Trying to parse wrong model');
|
||||||
|
}
|
||||||
|
|
||||||
|
$note->mediaInfo = Note::parseMediaInfo($note->fields['Notes']);
|
||||||
|
|
||||||
|
// Set VocabKanji field
|
||||||
|
$note->terms = Term::fromNoteFields($note->fields);
|
||||||
|
|
||||||
|
// If unable to, create them from the highlighted parts in the sentence
|
||||||
|
if (empty($note->terms)) {
|
||||||
|
foreach ($note->getHighlightedKanji() as $highlighedKanji) {
|
||||||
|
$term = new Term();
|
||||||
|
$term->kanji = $highlighedKanji;
|
||||||
|
$term->definitionEn = null;
|
||||||
|
$term->definitionJp = null;
|
||||||
|
$note->terms[] = $term;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toAnki(): array
|
||||||
|
{
|
||||||
|
return array_merge(parent::toAnki(), [
|
||||||
|
'fields' => [
|
||||||
|
'VocabKanji' => join('|', array_map(
|
||||||
|
fn(Term $x) => $x->getKanji(),
|
||||||
|
$this->terms,
|
||||||
|
)),
|
||||||
|
'VocabFurigana' => join('|', array_map(
|
||||||
|
fn(Term $x) => $x->getReading() ?? '_',
|
||||||
|
$this->terms,
|
||||||
|
)),
|
||||||
|
'VocabDef' => join("<br>\n", array_map(
|
||||||
|
fn(Term $x) => $x->toAnkiVocabDef(),
|
||||||
|
$this->terms,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------- Derived methods ---
|
||||||
|
|
||||||
|
public function hasTerm(string $kanji): bool
|
||||||
|
{
|
||||||
|
foreach ($this->terms as $term) {
|
||||||
|
assert($term instanceof Term);
|
||||||
|
|
||||||
|
if ($term->kanji == $kanji) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSentKanjiHighlighted(): bool
|
||||||
|
{
|
||||||
|
return str_contains(
|
||||||
|
$this->fields['SentKanji'],
|
||||||
|
self::HIGHLIGHT_ATTR_KANJI,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return an array of strings with the highlighted kanji in the SentKanji */
|
||||||
|
public function getHighlightedKanji(): array
|
||||||
|
{
|
||||||
|
$ret = [];
|
||||||
|
$matches = [];
|
||||||
|
|
||||||
|
// 1. Get all spans in the text
|
||||||
|
preg_match_all(
|
||||||
|
self::HIGHLIGHT_PATTERN,
|
||||||
|
$this->fields['SentKanji'],
|
||||||
|
$matches,
|
||||||
|
PREG_SET_ORDER,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Check the ones that match with the kanji color
|
||||||
|
foreach ($matches as $match) {
|
||||||
|
if ($match[1] === self::HIGHLIGHT_ATTR_KANJI) {
|
||||||
|
$ret[] = mb_trim($match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,7 +98,7 @@ class Note
|
||||||
|
|
||||||
// -------------------------------------------------- Utility functions ---
|
// -------------------------------------------------- Utility functions ---
|
||||||
|
|
||||||
protected static function stringHighlight(string $haystack, string $needle)
|
protected static function stringHighlight(string $haystack, string $needle): string
|
||||||
{
|
{
|
||||||
$replace = sprintf(
|
$replace = sprintf(
|
||||||
'<span %s>%s</span>',
|
'<span %s>%s</span>',
|
||||||
|
@ -108,4 +108,47 @@ class Note
|
||||||
|
|
||||||
return str_replace($needle, $replace, strip_tags($haystack));
|
return str_replace($needle, $replace, strip_tags($haystack));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function parseMediaInfo(string $notes): ?array
|
||||||
|
{
|
||||||
|
$matches = null;
|
||||||
|
|
||||||
|
// Parse the notes fields. It can be in the form of
|
||||||
|
// series-name_S01 EP07 (11h22m33s44ms)
|
||||||
|
// or
|
||||||
|
// movie-name EP (11h22m33s44ms)
|
||||||
|
if (1 !== preg_match(
|
||||||
|
'/(?<name>[0-9A-Za-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
|
||||||
|
$notes,
|
||||||
|
$matches,
|
||||||
|
)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove number-indexed matches, cast numbers to integers
|
||||||
|
$matches = [
|
||||||
|
'name' => $matches['name'],
|
||||||
|
'time' => $matches['time'],
|
||||||
|
// NOTE: intval returns 0 if not a number, which is false-like
|
||||||
|
'season' => intval($matches['season']) ?: null,
|
||||||
|
'episode' => intval($matches['episode']) ?: null,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parse time into a DateInterval and replace it in the matches array
|
||||||
|
$time = new \DateInterval('PT0S');
|
||||||
|
|
||||||
|
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
|
||||||
|
preg_match('/(\d+)s/', $matches['time'], $seconds);
|
||||||
|
preg_match('/(\d+)m/', $matches['time'], $minutes);
|
||||||
|
preg_match('/(\d+)h/', $matches['time'], $hours);
|
||||||
|
|
||||||
|
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
|
||||||
|
if ($seconds[1] ?? false) $time->s = $seconds[1];
|
||||||
|
if ($minutes[1] ?? false) $time->i = $minutes[1];
|
||||||
|
if ($hours[1] ?? false) $time->h = $hours[1];
|
||||||
|
|
||||||
|
$matches['time'] = $time;
|
||||||
|
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Entity;
|
||||||
class SentenceListeningNote extends Note
|
class SentenceListeningNote extends Note
|
||||||
{
|
{
|
||||||
const MODEL_NAME = 'Japanese sentences listening';
|
const MODEL_NAME = 'Japanese sentences listening';
|
||||||
|
const DECK = '日本語::漢字';
|
||||||
|
|
||||||
private ?array $mediaInfo = [];
|
private ?array $mediaInfo = [];
|
||||||
private ?Term $term = null;
|
private ?Term $term = null;
|
||||||
|
@ -20,7 +21,6 @@ class SentenceListeningNote extends Note
|
||||||
$this->fields['VocabKanji'] = $term->getKanji();
|
$this->fields['VocabKanji'] = $term->getKanji();
|
||||||
$this->fields['VocabFurigana'] = $term->getReading();
|
$this->fields['VocabFurigana'] = $term->getReading();
|
||||||
$this->fields['VocabDef'] = $term->toAnkiVocabDef();
|
$this->fields['VocabDef'] = $term->toAnkiVocabDef();
|
||||||
$this->fields['SentFurigana'] = ''; // We don't want to keep this
|
|
||||||
$this->fields['SentKanji'] = $this->stringHighlight(
|
$this->fields['SentKanji'] = $this->stringHighlight(
|
||||||
$this->fields['SentKanji'],
|
$this->fields['SentKanji'],
|
||||||
$term->getKanji(),
|
$term->getKanji(),
|
||||||
|
@ -40,7 +40,7 @@ class SentenceListeningNote extends Note
|
||||||
throw new \Exception('Trying to parse wrong model');
|
throw new \Exception('Trying to parse wrong model');
|
||||||
}
|
}
|
||||||
|
|
||||||
$note->mediaInfo = self::parseMediaInfo($note->fields['Notes']);
|
$note->mediaInfo = Note::parseMediaInfo($note->fields['Notes']);
|
||||||
|
|
||||||
// Set VocabKanji field
|
// Set VocabKanji field
|
||||||
$note->term = Term::fromNoteFields($note->fields)[0] ?? null;
|
$note->term = Term::fromNoteFields($note->fields)[0] ?? null;
|
||||||
|
@ -51,9 +51,9 @@ class SentenceListeningNote extends Note
|
||||||
return $note;
|
return $note;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromNote(Note $origNote, Term $term): static
|
public static function fromNote(Note $origNote, Term $term): self
|
||||||
{
|
{
|
||||||
$slNote = new static();
|
$slNote = new self();
|
||||||
foreach (get_object_vars($origNote) as $prop => $value) {
|
foreach (get_object_vars($origNote) as $prop => $value) {
|
||||||
$slNote->$prop = $value;
|
$slNote->$prop = $value;
|
||||||
}
|
}
|
||||||
|
@ -83,47 +83,4 @@ class SentenceListeningNote extends Note
|
||||||
self::HIGHLIGHT_ATTR_KANJI,
|
self::HIGHLIGHT_ATTR_KANJI,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function parseMediaInfo(string $notes): ?array
|
|
||||||
{
|
|
||||||
$matches = null;
|
|
||||||
|
|
||||||
// Parse the notes fields. It can be in the form of
|
|
||||||
// series-name_S01 EP07 (11h22m33s44ms)
|
|
||||||
// or
|
|
||||||
// movie-name EP (11h22m33s44ms)
|
|
||||||
if (1 !== preg_match(
|
|
||||||
'/(?<name>[0-9A-Za-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
|
|
||||||
$notes,
|
|
||||||
$matches,
|
|
||||||
)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove number-indexed matches, cast numbers to integers
|
|
||||||
$matches = [
|
|
||||||
'name' => $matches['name'],
|
|
||||||
'time' => $matches['time'],
|
|
||||||
// NOTE: intval returns 0 if not a number, which is false-like
|
|
||||||
'season' => intval($matches['season']) ?: null,
|
|
||||||
'episode' => intval($matches['episode']) ?: null,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Parse time into a DateInterval and replace it in the matches array
|
|
||||||
$time = new \DateInterval('PT0S');
|
|
||||||
|
|
||||||
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
|
|
||||||
preg_match('/(\d+)s/', $matches['time'], $seconds);
|
|
||||||
preg_match('/(\d+)m/', $matches['time'], $minutes);
|
|
||||||
preg_match('/(\d+)h/', $matches['time'], $hours);
|
|
||||||
|
|
||||||
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
|
|
||||||
if ($seconds[1] ?? false) $time->s = $seconds[1];
|
|
||||||
if ($minutes[1] ?? false) $time->i = $minutes[1];
|
|
||||||
if ($hours[1] ?? false) $time->h = $hours[1];
|
|
||||||
|
|
||||||
$matches['time'] = $time;
|
|
||||||
|
|
||||||
return $matches;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class SentenceNote extends Note
|
||||||
throw new \Exception('Trying to parse wrong model');
|
throw new \Exception('Trying to parse wrong model');
|
||||||
}
|
}
|
||||||
|
|
||||||
$note->mediaInfo = self::parseMediaInfo($note->fields['Notes']);
|
$note->mediaInfo = Note::parseMediaInfo($note->fields['Notes']);
|
||||||
|
|
||||||
// Set VocabKanji field
|
// Set VocabKanji field
|
||||||
$note->terms = Term::fromNoteFields($note->fields);
|
$note->terms = Term::fromNoteFields($note->fields);
|
||||||
|
@ -133,47 +133,4 @@ class SentenceNote extends Note
|
||||||
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function parseMediaInfo(string $notes): ?array
|
|
||||||
{
|
|
||||||
$matches = null;
|
|
||||||
|
|
||||||
// Parse the notes fields. It can be in the form of
|
|
||||||
// series-name_S01 EP07 (11h22m33s44ms)
|
|
||||||
// or
|
|
||||||
// movie-name EP (11h22m33s44ms)
|
|
||||||
if (1 !== preg_match(
|
|
||||||
'/(?<name>[0-9A-Za-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
|
|
||||||
$notes,
|
|
||||||
$matches,
|
|
||||||
)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove number-indexed matches, cast numbers to integers
|
|
||||||
$matches = [
|
|
||||||
'name' => $matches['name'],
|
|
||||||
'time' => $matches['time'],
|
|
||||||
// NOTE: intval returns 0 if not a number, which is false-like
|
|
||||||
'season' => intval($matches['season']) ?: null,
|
|
||||||
'episode' => intval($matches['episode']) ?: null,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Parse time into a DateInterval and replace it in the matches array
|
|
||||||
$time = new \DateInterval('PT0S');
|
|
||||||
|
|
||||||
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
|
|
||||||
preg_match('/(\d+)s/', $matches['time'], $seconds);
|
|
||||||
preg_match('/(\d+)m/', $matches['time'], $minutes);
|
|
||||||
preg_match('/(\d+)h/', $matches['time'], $hours);
|
|
||||||
|
|
||||||
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
|
|
||||||
if ($seconds[1] ?? false) $time->s = $seconds[1];
|
|
||||||
if ($minutes[1] ?? false) $time->i = $minutes[1];
|
|
||||||
if ($hours[1] ?? false) $time->h = $hours[1];
|
|
||||||
|
|
||||||
$matches['time'] = $time;
|
|
||||||
|
|
||||||
return $matches;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Term
|
||||||
public ?string $kanji;
|
public ?string $kanji;
|
||||||
public ?string $definitionJp;
|
public ?string $definitionJp;
|
||||||
public ?string $definitionEn;
|
public ?string $definitionEn;
|
||||||
|
public ?string $audio;
|
||||||
|
|
||||||
public function getReading(): ?string
|
public function getReading(): ?string
|
||||||
{
|
{
|
||||||
|
@ -20,7 +21,7 @@ class Term
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the kanji version & the reading for a given term
|
* Get the kanji version & the reading for a given term
|
||||||
*
|
*
|
||||||
* TODO: Make this smarter & handle mixing of kanji & hiradana
|
* TODO: Make this smarter & handle mixing of kanji & hiradana
|
||||||
*
|
*
|
||||||
* @return array{'kanji': string, 'reading': null|string}
|
* @return array{'kanji': string, 'reading': null|string}
|
||||||
|
@ -148,6 +149,8 @@ class Term
|
||||||
*/
|
*/
|
||||||
public static function fromNoteFields(array $fields): array
|
public static function fromNoteFields(array $fields): array
|
||||||
{
|
{
|
||||||
|
$audios = explode('|', $fields['VocabAudio'] ?? '');
|
||||||
|
|
||||||
// -------------------- Trying to extract it with the modern syntax ---
|
// -------------------- Trying to extract it with the modern syntax ---
|
||||||
// 言葉: word
|
// 言葉: word
|
||||||
// 上げる:上に動くこと。
|
// 上げる:上に動くこと。
|
||||||
|
@ -157,13 +160,22 @@ class Term
|
||||||
foreach (preg_split('|<br ?/?>|', $fields['VocabDef']) as $line) {
|
foreach (preg_split('|<br ?/?>|', $fields['VocabDef']) as $line) {
|
||||||
$terms[] = self::fromVocabDefLine(strip_tags($line));
|
$terms[] = self::fromVocabDefLine(strip_tags($line));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Assign audio
|
||||||
|
if (count($audios) === count($terms) and $terms[0] !== null) {
|
||||||
|
foreach (array_keys($audios) as $key) {
|
||||||
|
if ($terms[$key] === null) dd($fields);
|
||||||
|
$terms[$key]->audio = mb_trim(strip_tags($audios[$key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If there's no nulls, everything went good
|
// If there's no nulls, everything went good
|
||||||
if (!in_array(null, $terms, true)) return $terms;
|
if (!in_array(null, $terms, true)) return $terms;
|
||||||
|
|
||||||
|
|
||||||
// ------------ Extracting failed, try to infer from other syntaxes ---
|
// ------------ Extracting failed, try to infer from other syntaxes ---
|
||||||
|
|
||||||
$kanjis = explode('|', $fields['VocabKanji']);
|
$kanjis = explode('|', $fields['VocabKanji'] ?? $fields['Vocab'] ?? '');
|
||||||
$defs = explode('|', $fields['VocabDef']);
|
$defs = explode('|', $fields['VocabDef']);
|
||||||
// Number of legacy definitions is different from number of kanji
|
// Number of legacy definitions is different from number of kanji
|
||||||
if (count($kanjis) !== count($defs)) return [];
|
if (count($kanjis) !== count($defs)) return [];
|
||||||
|
|
|
@ -7,7 +7,7 @@ use App\Utils\Number;
|
||||||
class UnicodeNote extends Note
|
class UnicodeNote extends Note
|
||||||
{
|
{
|
||||||
const string MODEL_NAME = 'Unicode';
|
const string MODEL_NAME = 'Unicode';
|
||||||
const string DECK = 'unicode';
|
const string DECK = '日本語::unicode';
|
||||||
|
|
||||||
private ?int $codepoint = null;
|
private ?int $codepoint = null;
|
||||||
/** @var Term[] */
|
/** @var Term[] */
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\KoreanProductionNote;
|
||||||
|
use App\Entity\KoreanSentenceNote;
|
||||||
use App\Entity\Note;
|
use App\Entity\Note;
|
||||||
use App\Entity\SentenceListeningNote;
|
use App\Entity\SentenceListeningNote;
|
||||||
use App\Entity\SentenceNote;
|
use App\Entity\SentenceNote;
|
||||||
|
@ -38,10 +40,12 @@ class AnkiService
|
||||||
/** The note's id is updated on success.
|
/** The note's id is updated on success.
|
||||||
* @return bool True on success
|
* @return bool True on success
|
||||||
*/
|
*/
|
||||||
public function addNote(Note &$note, string $deckName): bool
|
public function addNote(Note &$note, ?string $deckName = null): bool
|
||||||
{
|
{
|
||||||
|
$deckName ??= constant(get_class($note) . '::DECK');
|
||||||
|
|
||||||
$note->setId($this->request('addNote', ['note' => [
|
$note->setId($this->request('addNote', ['note' => [
|
||||||
'deckName' => $deckName,
|
'deckName' => $deckName,
|
||||||
'modelName' => $note->getModel(),
|
'modelName' => $note->getModel(),
|
||||||
'fields' => $note->getFields(),
|
'fields' => $note->getFields(),
|
||||||
'options' => ['allowDuplicate' => false],
|
'options' => ['allowDuplicate' => false],
|
||||||
|
@ -72,6 +76,24 @@ class AnkiService
|
||||||
return $this->request('findNotes', ['query' => $query]);
|
return $this->request('findNotes', ['query' => $query]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
public function getAllIdsFromClass(string $class): array
|
||||||
|
{
|
||||||
|
$query = sprintf('"note:%s"', constant("$class::MODEL_NAME"));
|
||||||
|
return $this->request('findNotes', ['query' => $query]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of object
|
||||||
|
* @param class-string<T> $class
|
||||||
|
* @return list<T>
|
||||||
|
*/
|
||||||
|
public function getAllFromClass(string $class): array
|
||||||
|
{
|
||||||
|
$ids = $this->getAllIdsFromClass($class);
|
||||||
|
return $this->getNotes($ids);
|
||||||
|
}
|
||||||
|
|
||||||
public function getAllSentenceNoteIds(): array
|
public function getAllSentenceNoteIds(): array
|
||||||
{
|
{
|
||||||
return $this->request(
|
return $this->request(
|
||||||
|
@ -119,6 +141,8 @@ class AnkiService
|
||||||
UnicodeNote::MODEL_NAME => UnicodeNote::fromAnki($noteInfo),
|
UnicodeNote::MODEL_NAME => UnicodeNote::fromAnki($noteInfo),
|
||||||
SentenceNote::MODEL_NAME => SentenceNote::fromAnki($noteInfo),
|
SentenceNote::MODEL_NAME => SentenceNote::fromAnki($noteInfo),
|
||||||
SentenceListeningNote::MODEL_NAME => SentenceListeningNote::fromAnki($noteInfo),
|
SentenceListeningNote::MODEL_NAME => SentenceListeningNote::fromAnki($noteInfo),
|
||||||
|
KoreanSentenceNote::MODEL_NAME => KoreanSentenceNote::fromAnki($noteInfo),
|
||||||
|
KoreanProductionNote::MODEL_NAME => KoreanProductionNote::fromAnki($noteInfo),
|
||||||
default => throw new \Exception(sprintf(
|
default => throw new \Exception(sprintf(
|
||||||
'Unrecognized Note "%s" of type "%s"',
|
'Unrecognized Note "%s" of type "%s"',
|
||||||
$noteInfo['noteId'],
|
$noteInfo['noteId'],
|
||||||
|
|
|
@ -18,6 +18,7 @@ class AppExtension extends AbstractExtension
|
||||||
// Reference: https://twig.symfony.com/doc/3.x/advanced.html#automatic-escaping
|
// Reference: https://twig.symfony.com/doc/3.x/advanced.html#automatic-escaping
|
||||||
new TwigFilter('basename', basename(...)),
|
new TwigFilter('basename', basename(...)),
|
||||||
new TwigFilter('ruby', self::ankiRubyToHtml(...)),
|
new TwigFilter('ruby', self::ankiRubyToHtml(...)),
|
||||||
|
new TwigFilter('intersect_key', array_intersect_key(...))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,5 +141,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-body border-top p-2 sticky-bottom">
|
||||||
|
{% set learnt_kanken = char_info.lists.unicode|intersect_key(char_info.lists.kanken)|length %}
|
||||||
|
{% set total_kanken = char_info.lists.kanken|length %}
|
||||||
|
Kanken {{ learnt_kanken }}/{{ total_kanken }} ({{ ((learnt_kanken/total_kanken) * 100)|number_format(2) }}%)
|
||||||
|
|
|
||||||
|
{% set sn_kanken = char_info.lists.sn|intersect_key(char_info.lists.kanken)|length %}
|
||||||
|
{{ sn_kanken }}/{{ total_kanken }} ({{ ((sn_kanken/total_kanken) * 100)|number_format(2) }}%)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -39,6 +39,26 @@
|
||||||
Wiktionary JA
|
Wiktionary JA
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-primary"
|
||||||
|
href="{{ 'https://ja.wikipedia.org/w/index.php?' ~ {
|
||||||
|
search: char
|
||||||
|
}|url_encode }}"
|
||||||
|
>
|
||||||
|
Wikipedia JA
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-primary"
|
||||||
|
href="{{ 'https://zh.wikipedia.org/w/index.php?' ~ {
|
||||||
|
search: char
|
||||||
|
}|url_encode }}"
|
||||||
|
>
|
||||||
|
Wikipedia ZH
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
|
|
Loading…
Reference in New Issue