diff --git a/src/Controller/AnkiController.php b/src/Controller/AnkiController.php new file mode 100644 index 0000000..dcac3e4 --- /dev/null +++ b/src/Controller/AnkiController.php @@ -0,0 +1,26 @@ +getLatestNote(); + + $ankiService->updateNote($note); + + dd($note); + dd($note->toAnki()); + + return $this->render('anki/index.html.twig', [ + 'controller_name' => $latestId, + ]); + } +} diff --git a/src/Entity/Note.php b/src/Entity/Note.php new file mode 100644 index 0000000..a06c22a --- /dev/null +++ b/src/Entity/Note.php @@ -0,0 +1,244 @@ +]*)>(.*?)<\/span>/i'; + const HIGHLIGHT_ATTR_KANJI = 'style="color: rgb(255, 78, 8);"'; + + public function getId(): int + { + return $this->id; + } + + public function hasTerm(string $kanji): bool + { + foreach ($this->terms as $term) { + assert($term instanceof Term); + + if ($term->kanji == $kanji) return true; + } + + return false; + } + + public static function fromAnki(array $noteInfo): self + { + $note = new self(); + + $note->id = $noteInfo['noteId']; + + $fields = array_map(fn($x) => $x['value'], $noteInfo['fields']); + + // Set VocabKanji field + //$vocabKanji = explode('|', $fields['VocabKanji']); + $note->terms = self::parseVocabDef($fields['VocabDef']); + + // If not defined, find them from the highlighted parts in the sentence + if (empty($note->terms)) { + // 1. Get all spans in the text + preg_match_all( + self::HIGHLIGHT_PATTERN, + $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) { + $term = new Term(); + $term->kanji = mb_trim($match[2]); + $term->definitionEn = null; + $term->definitionJp = null; + $note->terms[] = $term; + } + } + } + + // Set to null whatever is null + $readings = array_map( + fn($x) => in_array($x, ['_', '_', '']) ? null : $x, + explode('|', $fields['VocabFurigana']), + ); + + // Set readings from furigana field + foreach ($note->terms as $key => &$term) { + if (null === $term->getReading()) { + if (null !== ($readings[$key] ?? null)) { + $term->kanji .= '[' . $readings[$key] . ']'; + } + } + } + + return $note; + } + + public function toAnki(): array + { + return [ + 'id' => $this->id, + '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, + )), + ], + ]; + } + + public static function parseVocabDef(string $vocabDef): array + { + if (mb_trim($vocabDef) == "") return []; + + $terms = []; + + foreach (preg_split('|
|', $vocabDef) as $line) { + $term = Term::fromVocabDefLine(strip_tags($line)); + if (null === $term) dd("error parsing term", $line); + $terms[] = $term; + }; + + return $terms; + } +} + +class Term +{ + public ?string $kanji; + public ?string $definitionJp; + public ?string $definitionEn; + + public function getReading(): ?string + { + return self::parseFurigana($this->kanji)['reading']; + } + + public function getKanji(): string + { + return self::parseFurigana($this->kanji)['kanji']; + } + + public static function parseFurigana(string $furigana): array + { + // 0: all, 1: (kanji/hiragana), 2: ([reading]): 3: (reading) + preg_match_all('/([^ \[]+)(\[([^\]]*)\])? ?/', $furigana, $matches, PREG_SET_ORDER); + + $matchedKanji = array_map(fn($x) => $x[1], $matches); + $matchedReading = array_map(fn($x) => $x[3] ?? $x[1], $matches); + + + return [ + 'kanji' => join('', $matchedKanji), + 'reading' => $matchedKanji == $matchedReading + ? null + : join('', $matchedReading), + ]; + } + + public function toAnkiVocabDef() + { + $ret = '' . $this->kanji; + + $ret .= match ([null !== $this->definitionJp, null !== $this->definitionEn]) { + [false, false] => ':_', + [false, true] => ': ' . $this->definitionEn, + [true, false] => ':' . $this->definitionJp, + [true, true] => ':' . $this->definitionJp . '(' . $this->definitionEn . ')', + }; + + return $ret; + } + + public static function fromVocabDefLine(string $vocabDefLine): ?Term + { + $term = new Term(); + + // ------------------------------------------------------ Get Kanji --- + + $jpStart = mb_strpos($vocabDefLine, ':'); + $enStart = mb_strpos($vocabDefLine, ':'); + + // Get the kanji, as it may not be in the same order for some reason + if (false !== $jpStart) { + $term->kanji = mb_substr($vocabDefLine, 0, $jpStart); + $def = mb_substr($vocabDefLine, $jpStart + 1, null); + $jpStart = 0; + } elseif (false !== $enStart) { + $term->kanji = mb_substr($vocabDefLine, 0, $enStart); + $def = mb_substr($vocabDefLine, $enStart + 1, null); + $enStart = 0; + } + // Convert 「this」 into [this] + $term->kanji = mb_trim(strtr($term->kanji, [ + '「' => '[', + '」' => ']', + ' ' => ' ', + ])); + $def = mb_trim($def); + if (!is_string($term->kanji)) { + return null; + } + + // -------------------------------------------------- No definition --- + + // Special case where there's no definitions + if ($def === '' or $def === '_' or $def === '_') { + $term->definitionJp = null; + $term->definitionEn = null; + return $term; + } + + // This means there's both en and jp + $parentStart = mb_strpos($def, '('); + + // -------------------------------------------------- Only Japanese --- + + if (false !== $jpStart and false === $parentStart) { + // It's all japanese, start to finish + $term->definitionJp = mb_trim(mb_substr($def, 0)); + $term->definitionEn = null; + return $term; + } + + // -------------------------------------- Both Japanese and English --- + + if (false !== $jpStart and false !== $parentStart) { + $term->definitionJp = mb_trim(mb_substr($def, 0, $parentStart)); + // -1 to remove the parenthesis end + $term->definitionEn = mb_trim(mb_substr($def, $parentStart + 1, -1)); + return $term; + } + + // --------------------------------------------------- Only english --- + + if (false !== $enStart) { + $term->definitionJp = null; + $term->definitionEn = mb_trim(mb_substr($def, 0)); + return $term; + } + + // ------------------------------------------------- Unvalid syntax --- + + dd("Unexpected error, couldn't parse definition line", $vocabDefLine); + } +} diff --git a/src/Repository/NoteRepository.php b/src/Repository/NoteRepository.php new file mode 100644 index 0000000..23b3d1e --- /dev/null +++ b/src/Repository/NoteRepository.php @@ -0,0 +1,43 @@ + + */ +class NoteRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Note::class); + } + +// /** +// * @return Note[] Returns an array of Note objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('n') +// ->andWhere('n.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('n.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Note +// { +// return $this->createQueryBuilder('n') +// ->andWhere('n.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Service/AnkiService.php b/src/Service/AnkiService.php new file mode 100644 index 0000000..0f559ea --- /dev/null +++ b/src/Service/AnkiService.php @@ -0,0 +1,51 @@ +client->request( + 'POST', + 'http://localhost:8765', + ['json' => [ + 'action' => $action, + 'version' => 6, + 'params' => $params, + ]] + )->toArray(); + + throw new \Exception('AnkiConnect returned error: ' . $res['error']); + + return $res['result']; + } + + public function getLatestNote(): Note + { + $latestId = max($this->request( + 'findNotes', + ['aquery' => 'added:1 "note:Japanese sentences"'] + )); + + $noteInfo = $this->request('notesInfo', ['notes' => [$latestId]])[0]; + + return Note::fromAnki($noteInfo); + } + + public function updateNote(Note $note) + { + $this->request('guiBrowse', ['query' => 'nid:1']); + + $this->request('updateNoteFields', ['note' => $note->toAnki()]); + + $this->request('guiBrowse', ['query' => 'nid:' . $note->getId()]); + } +} diff --git a/templates/anki/index.html.twig b/templates/anki/index.html.twig new file mode 100644 index 0000000..18fc9d2 --- /dev/null +++ b/templates/anki/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello AnkiController!{% endblock %} + +{% block body %} + + +
+

Hello {{ controller_name }}! ✅

+ + This friendly message is coming from: + +
+{% endblock %}