feat: A lot of things that I don't remember but probs everything

This commit is contained in:
Dendy 2025-02-06 14:47:35 +09:00
parent 4fa52ec7cd
commit 4d58b2d299
5 changed files with 384 additions and 0 deletions

View File

@ -0,0 +1,26 @@
<?php
namespace App\Controller;
use App\Service\AnkiService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class AnkiController extends AbstractController
{
#[Route('/anki', name: 'app_anki')]
public function index(AnkiService $ankiService): Response
{
$note = $ankiService->getLatestNote();
$ankiService->updateNote($note);
dd($note);
dd($note->toAnki());
return $this->render('anki/index.html.twig', [
'controller_name' => $latestId,
]);
}
}

244
src/Entity/Note.php Normal file
View File

@ -0,0 +1,244 @@
<?php
namespace App\Entity;
//use App\Repository\NoteRepository;
//use Doctrine\ORM\Mapping as ORM;
//#[ORM\Entity(repositoryClass: NoteRepository::class)]
class Note
{
//#[ORM\Id]
//#[ORM\GeneratedValue]
//#[ORM\Column]
private int $id;
private array $terms = [];
const HIGHLIGHT_PATTERN = '/<span\s+([^>]*)>(.*?)<\/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("<br>\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('|<br ?/?>|', $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 = '<span ' . Note::HIGHLIGHT_ATTR_KANJI . '>' . $this->kanji;
$ret .= match ([null !== $this->definitionJp, null !== $this->definitionEn]) {
[false, false] => '</span>_',
[false, true] => ':</span> ' . $this->definitionEn,
[true, false] => '</span>' . $this->definitionJp,
[true, true] => '</span>' . $this->definitionJp . '<span style="color: #aacebe;">(' . $this->definitionEn . ')</span>',
};
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);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Note;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Note>
*/
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()
// ;
// }
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Service;
use App\Entity\Note;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AnkiService
{
function __construct(
private HttpClientInterface $client,
) {}
private function request(string $action, array $params): mixed
{
$res = $this->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()]);
}
}

View File

@ -0,0 +1,20 @@
{% extends 'base.html.twig' %}
{% block title %}Hello AnkiController!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code>/strg/prgm/web/anker/src/Controller/AnkiController.php</code></li>
<li>Your template at <code>/strg/prgm/web/anker/templates/anki/index.html.twig</code></li>
</ul>
</div>
{% endblock %}