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 %}
+
+
+
/strg/prgm/web/anker/src/Controller/AnkiController.php
/strg/prgm/web/anker/templates/anki/index.html.twig