From 943177bc085ba1daec67e68d69f3ad83b0174c52 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 23 Apr 2025 01:06:21 +0200 Subject: [PATCH] Implement custom parsers/renderers with autowiring for snip content --- migrations/Version20250422222542.php | 38 ++++++++++++++ src/Controller/SnipController.php | 10 ++-- src/Entity/Snip.php | 15 ++++++ src/Form/SnipType.php | 11 ++++ src/Service/SnipParser/AbstractParser.php | 16 ++++++ .../GenericParser.php} | 27 +++++----- .../{Stages => Generic}/HtmlEscapeStage.php | 2 +- .../IncludeReferenceStage.php | 7 ++- .../ReplaceBlocksStage.php | 2 +- .../{Stages => Generic}/ReplaceStage.php | 2 +- .../{Stages => Generic}/UrlReferenceStage.php | 2 +- src/Service/SnipParser/ParserFactory.php | 52 +++++++++++++++++++ src/Service/SnipParser/ParserInterface.php | 15 ++++++ src/Service/SnipParser/Website/HtmlParser.php | 18 +++++++ templates/base/navbar.html.twig | 3 ++ 15 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 migrations/Version20250422222542.php create mode 100644 src/Service/SnipParser/AbstractParser.php rename src/Service/SnipParser/{Pipeline.php => Generic/GenericParser.php} (51%) rename src/Service/SnipParser/{Stages => Generic}/HtmlEscapeStage.php (88%) rename src/Service/SnipParser/{Stages => Generic}/IncludeReferenceStage.php (89%) rename src/Service/SnipParser/{Stages => Generic}/ReplaceBlocksStage.php (95%) rename src/Service/SnipParser/{Stages => Generic}/ReplaceStage.php (86%) rename src/Service/SnipParser/{Stages => Generic}/UrlReferenceStage.php (94%) create mode 100644 src/Service/SnipParser/ParserFactory.php create mode 100644 src/Service/SnipParser/ParserInterface.php create mode 100644 src/Service/SnipParser/Website/HtmlParser.php diff --git a/migrations/Version20250422222542.php b/migrations/Version20250422222542.php new file mode 100644 index 0000000..1ca3290 --- /dev/null +++ b/migrations/Version20250422222542.php @@ -0,0 +1,38 @@ +addSql(<<<'SQL' + ALTER TABLE snip ADD parser VARCHAR(255) NOT NULL + SQL); + $this->addSql(<<<'SQL' + UPDATE snip SET parser = 'generic' + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE snip DROP parser + SQL); + } +} diff --git a/src/Controller/SnipController.php b/src/Controller/SnipController.php index bd57515..bc2774a 100644 --- a/src/Controller/SnipController.php +++ b/src/Controller/SnipController.php @@ -8,7 +8,7 @@ use App\Form\SnipType; use App\Repository\SnipRepository; use App\Security\Voter\SnipVoter; use App\Service\SnipContent\SnipContentService; -use App\Service\SnipParser\Pipeline; +use App\Service\SnipParser\ParserFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; @@ -42,23 +42,23 @@ class SnipController extends AbstractController } #[Route('/single/{snip}', name: '_single')] - public function single(Snip $snip, Pipeline $pl): Response + public function single(Snip $snip, ParserFactory $pf): Response { $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); return $this->render('snip/single.html.twig', [ 'snip' => $snip, - 'content' => $pl->parse($this->contentService->getActiveText($snip)), + 'content' => $pf->getBySnip($snip)->parseView($this->contentService->getActiveText($snip)), ]); } #[Route('/raw/{snip}', name: '_raw')] - public function raw(Snip $snip, Pipeline $pl, Request $request): Response + public function raw(Snip $snip, ParserFactory $pf, Request $request): Response { $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); $response = new Response( - $pl->clean($this->contentService->getActiveText($snip)), + $pf->getBySnip($snip)->parseRaw($this->contentService->getActiveText($snip)), Response::HTTP_OK, ['Content-Type' => 'text/html'] ); diff --git a/src/Entity/Snip.php b/src/Entity/Snip.php index bd4e99f..d0313c3 100644 --- a/src/Entity/Snip.php +++ b/src/Entity/Snip.php @@ -30,6 +30,9 @@ class Snip #[ORM\ManyToOne] private ?SnipContent $activeVersion = null; + #[ORM\Column(length: 255)] + private ?string $parser = null; + public function __construct() { $this->snipContents = new ArrayCollection(); @@ -115,4 +118,16 @@ class Snip return $this; } + + public function getParser(): ?string + { + return $this->parser; + } + + public function setParser(string $parser): static + { + $this->parser = $parser; + + return $this; + } } diff --git a/src/Form/SnipType.php b/src/Form/SnipType.php index 28b3e65..6471132 100644 --- a/src/Form/SnipType.php +++ b/src/Form/SnipType.php @@ -3,17 +3,28 @@ namespace App\Form; use App\Entity\Snip; +use App\Service\SnipParser\ParserFactory; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class SnipType extends AbstractType { + public function __construct( + private readonly ParserFactory $parserFactory, + ) {} + public function buildForm(FormBuilderInterface $builder, array $options): void { + dump(iterator_to_array($this->parserFactory->getChoices())); $builder ->add('name') + ->add('parser', ChoiceType::class, [ + 'choice_label' => fn(string $parser) => ucfirst($parser), + 'choices' => $this->parserFactory->getChoices(), + ]) ->add('content', TextareaType::class, [ 'attr' => ['rows' => 20], 'mapped' => false, diff --git a/src/Service/SnipParser/AbstractParser.php b/src/Service/SnipParser/AbstractParser.php new file mode 100644 index 0000000..9c0c53d --- /dev/null +++ b/src/Service/SnipParser/AbstractParser.php @@ -0,0 +1,16 @@ +build() ; - return $pipeline->process($payload); + return $pipeline->process($content); } - public function clean(string $payload): string + public function parseRaw(string $content): string { return str_replace( ['```', '``'], '', - $payload + $content ); } } \ No newline at end of file diff --git a/src/Service/SnipParser/Stages/HtmlEscapeStage.php b/src/Service/SnipParser/Generic/HtmlEscapeStage.php similarity index 88% rename from src/Service/SnipParser/Stages/HtmlEscapeStage.php rename to src/Service/SnipParser/Generic/HtmlEscapeStage.php index 521b784..31c9496 100644 --- a/src/Service/SnipParser/Stages/HtmlEscapeStage.php +++ b/src/Service/SnipParser/Generic/HtmlEscapeStage.php @@ -1,6 +1,6 @@ %s', $matches[0]); } - return $this->pipeline->parse( + return $this->pipeline->parseView( $this->snipContentService->rebuildText($content) ); }, $payload); diff --git a/src/Service/SnipParser/Stages/ReplaceBlocksStage.php b/src/Service/SnipParser/Generic/ReplaceBlocksStage.php similarity index 95% rename from src/Service/SnipParser/Stages/ReplaceBlocksStage.php rename to src/Service/SnipParser/Generic/ReplaceBlocksStage.php index 2350063..cc5907c 100644 --- a/src/Service/SnipParser/Stages/ReplaceBlocksStage.php +++ b/src/Service/SnipParser/Generic/ReplaceBlocksStage.php @@ -1,6 +1,6 @@ $id + * + * @return T + * @throws ServiceNotFoundException + */ + public function get(string $id): ParserInterface + { + return $this->locator->get($id); + } + + public function getBySnip(Snip $snip): ParserInterface + { + $parser = $snip->getParser(); + if (null === $parser) { + throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->getParser())); + } + + return $this->get($parser); + } + + /** + * @return iterable + */ + public function getAll(): iterable + { + return $this->locator->getIterator(); + } + + public function getChoices(): iterable + { + foreach ($this->getAll() as $parser) yield $parser::getName(); + } +} \ No newline at end of file diff --git a/src/Service/SnipParser/ParserInterface.php b/src/Service/SnipParser/ParserInterface.php new file mode 100644 index 0000000..404b34b --- /dev/null +++ b/src/Service/SnipParser/ParserInterface.php @@ -0,0 +1,15 @@ +%s', htmlspecialchars($content)); + } +} \ No newline at end of file diff --git a/templates/base/navbar.html.twig b/templates/base/navbar.html.twig index af6696c..86c5593 100644 --- a/templates/base/navbar.html.twig +++ b/templates/base/navbar.html.twig @@ -35,6 +35,9 @@ Logout {% else %} +