feat: A lot of things that I don't remember but probs everything
This commit is contained in:
parent
4fa52ec7cd
commit
4d58b2d299
|
@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
|
@ -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()]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 %}
|
Loading…
Reference in New Issue