From bc2b3ac0b44a211985fa06ce66c46aa67ab2f880 Mon Sep 17 00:00:00 2001 From: Dendy Faist Date: Sun, 28 Sep 2025 09:48:17 +0200 Subject: [PATCH] feat: Add korean cards & korean production card creation --- src/Command/CreateKoreanProductionCommand.php | 72 ++++++++++ src/Entity/KoreanProductionNote.php | 75 +++++++++++ src/Entity/KoreanSentenceNote.php | 123 ++++++++++++++++++ src/Entity/Note.php | 45 ++++++- src/Entity/SentenceListeningNote.php | 55 +------- src/Entity/SentenceNote.php | 45 +------ src/Entity/Term.php | 16 ++- src/Entity/UnicodeNote.php | 2 +- src/Service/AnkiService.php | 26 +++- 9 files changed, 360 insertions(+), 99 deletions(-) create mode 100644 src/Command/CreateKoreanProductionCommand.php create mode 100644 src/Entity/KoreanProductionNote.php create mode 100644 src/Entity/KoreanSentenceNote.php diff --git a/src/Command/CreateKoreanProductionCommand.php b/src/Command/CreateKoreanProductionCommand.php new file mode 100644 index 0000000..ccc1b5e --- /dev/null +++ b/src/Command/CreateKoreanProductionCommand.php @@ -0,0 +1,72 @@ +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( + << $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 $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 --- + +} diff --git a/src/Entity/KoreanSentenceNote.php b/src/Entity/KoreanSentenceNote.php new file mode 100644 index 0000000..a577e88 --- /dev/null +++ b/src/Entity/KoreanSentenceNote.php @@ -0,0 +1,123 @@ + */ + private array $terms = []; + + // -------------------------------------------------- Getters & setters --- + + /** @return list */ + public function getTerms(): array + { + return $this->terms; + } + /** @param list $terms */ + public function setTerms(array $terms): static + { + $this->terms = $terms; + return $this; + } + + + // ------------------------------------------------------- Anki-related --- + + /** @param array $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("
\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; + } +} diff --git a/src/Entity/Note.php b/src/Entity/Note.php index db87505..9eb4396 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -98,7 +98,7 @@ class Note // -------------------------------------------------- Utility functions --- - protected static function stringHighlight(string $haystack, string $needle) + protected static function stringHighlight(string $haystack, string $needle): string { $replace = sprintf( '%s', @@ -108,4 +108,47 @@ class Note 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( + '/(?[0-9A-Za-z\-_]+)(_S)?(?\d*) EP(?\d*) \((?