Compare commits
19 Commits
feature/do
...
feature/ch
Author | SHA1 | Date | |
---|---|---|---|
933dc424c3 | |||
29df284237 | |||
0dc7555ed1 | |||
2152468bdc | |||
74060da6e5 | |||
a1ecaf0189 | |||
074c1d8570 | |||
8226ac24d8 | |||
94bf0075f0 | |||
f7282ae31a | |||
e40e889c14 | |||
8ec4b2167a | |||
bf7d1efb43 | |||
7bdf9683b4 | |||
f338d791a7 | |||
e26c2a64b8 | |||
33bb4e77e5 | |||
978c075a3e | |||
a741ee102d |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{compose.yaml,compose.*.yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
6
.env
6
.env
@@ -24,6 +24,10 @@ APP_SECRET=
|
|||||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
#
|
#
|
||||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=mariadb-10.9.5&charset=utf8mb4"
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.13-MariaDB&charset=utf8mb4"
|
||||||
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
|
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
|
||||||
###< doctrine/doctrine-bundle ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> nelmio/cors-bundle ###
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
###< nelmio/cors-bundle ###
|
||||||
|
@@ -8,18 +8,21 @@
|
|||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"doctrine/doctrine-bundle": "^2.9",
|
"doctrine/doctrine-bundle": "^2.9",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||||
"doctrine/orm": "^2.14",
|
"doctrine/orm": "^3.4",
|
||||||
"league/commonmark": "^2.6",
|
"league/commonmark": "^2.6",
|
||||||
"league/pipeline": "^1.0",
|
"league/pipeline": "^1.0",
|
||||||
|
"nelmio/cors-bundle": "^2.5",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.1",
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
|
"symfony/asset": "*",
|
||||||
"symfony/console": "*",
|
"symfony/console": "*",
|
||||||
"symfony/dotenv": "*",
|
"symfony/dotenv": "*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/form": "*",
|
"symfony/form": "*",
|
||||||
"symfony/framework-bundle": "*",
|
"symfony/framework-bundle": "*",
|
||||||
"symfony/monolog-bundle": "^3.0",
|
"symfony/monolog-bundle": "^3.0",
|
||||||
|
"symfony/object-mapper": "7.3.*",
|
||||||
"symfony/property-access": "*",
|
"symfony/property-access": "*",
|
||||||
"symfony/property-info": "*",
|
"symfony/property-info": "*",
|
||||||
"symfony/runtime": "*",
|
"symfony/runtime": "*",
|
||||||
@@ -29,6 +32,7 @@
|
|||||||
"symfony/uid": "*",
|
"symfony/uid": "*",
|
||||||
"symfony/validator": "*",
|
"symfony/validator": "*",
|
||||||
"symfony/yaml": "*",
|
"symfony/yaml": "*",
|
||||||
|
"tempest/highlight": "^2.11",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^3.0"
|
"twig/twig": "^3.0"
|
||||||
},
|
},
|
||||||
@@ -82,7 +86,7 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"symfony": {
|
"symfony": {
|
||||||
"allow-contrib": false,
|
"allow-contrib": false,
|
||||||
"require": "7.2.*"
|
"require": "7.3.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1266
composer.lock
generated
1266
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,4 +11,5 @@ return [
|
|||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||||
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
@@ -11,7 +11,7 @@ doctrine:
|
|||||||
orm:
|
orm:
|
||||||
report_fields_where_declared: true
|
report_fields_where_declared: true
|
||||||
auto_generate_proxy_classes: true
|
auto_generate_proxy_classes: true
|
||||||
enable_lazy_ghost_objects: true
|
enable_native_lazy_objects: true
|
||||||
validate_xml_mapping: true
|
validate_xml_mapping: true
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
identity_generation_preferences:
|
identity_generation_preferences:
|
||||||
|
15
config/packages/nelmio_cors.yaml
Normal file
15
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
nelmio_cors:
|
||||||
|
defaults:
|
||||||
|
origin_regex: true
|
||||||
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
|
expose_headers: ['Link']
|
||||||
|
max_age: 3600
|
||||||
|
paths:
|
||||||
|
'^/api/':
|
||||||
|
allow_origin: ['*']
|
||||||
|
allow_headers: ['*']
|
||||||
|
allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
|
||||||
|
max_age: 3600
|
||||||
|
'^/': null
|
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
property_info:
|
||||||
|
with_constructor_extractor: true
|
@@ -8,4 +8,6 @@ when@dev:
|
|||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
framework:
|
framework:
|
||||||
profiler: { collect: false }
|
profiler:
|
||||||
|
collect: false
|
||||||
|
collect_serializer_data: true
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
when@dev:
|
when@dev:
|
||||||
_errors:
|
_errors:
|
||||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||||
prefix: /_error
|
prefix: /_error
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
when@dev:
|
when@dev:
|
||||||
web_profiler_wdt:
|
web_profiler_wdt:
|
||||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||||
prefix: /_wdt
|
prefix: /_wdt
|
||||||
|
|
||||||
web_profiler_profiler:
|
web_profiler_profiler:
|
||||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||||
prefix: /_profiler
|
prefix: /_profiler
|
||||||
|
@@ -15,7 +15,6 @@ services:
|
|||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
App\:
|
App\:
|
||||||
resource: '../src/'
|
resource: '../src/'
|
||||||
exclude:
|
|
||||||
- '../src/DependencyInjection/'
|
# add more service definitions when explicit configuration is needed
|
||||||
- '../src/Entity/'
|
# please note that last definitions always *replace* previous ones
|
||||||
- '../src/Kernel.php'
|
|
||||||
|
87
public/github-light-default.css
Normal file
87
public/github-light-default.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
pre, code {
|
||||||
|
color: #1f2328;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-keyword {
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-property {
|
||||||
|
color: #8250df;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-attribute {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-type {
|
||||||
|
color: #EA4334;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-generic {
|
||||||
|
color: #9d3af6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-value {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-literal {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-number {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-variable {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-comment {
|
||||||
|
color: #6e7781;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-blur {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-addition {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100%;
|
||||||
|
background-color: #00FF0022;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-deletion {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100%;
|
||||||
|
background-color: #FF000011;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-gutter {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #555;
|
||||||
|
padding: 0 1ch;
|
||||||
|
margin-right: 1ch;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-gutter-addition {
|
||||||
|
background-color: #34A853;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hl-gutter-deletion {
|
||||||
|
background-color: #EA4334;
|
||||||
|
color: #fff;
|
||||||
|
}
|
@@ -26,17 +26,16 @@ class SnipUpdateContentCommand extends Command
|
|||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
// $io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
$qb = $this->snipContentRepository->createQueryBuilder('s');
|
$qb = $this->snipContentRepository->createQueryBuilder('s');
|
||||||
$qb->where('s.text IS NOT NULL');
|
$qb->where('s.text IS NOT NULL');
|
||||||
|
|
||||||
$c = 0;
|
|
||||||
/** @var SnipContent $snipContent */
|
/** @var SnipContent $snipContent */
|
||||||
foreach ($qb->getQuery()->getResult() as $snipContent) {
|
foreach ($qb->getQuery()->getResult() as $snipContent) {
|
||||||
$text = $snipContent->getText();
|
$text = $snipContent->text;
|
||||||
$text = Lexer::reconstruct(Lexer::tokenize($text));
|
$text = Lexer::reconstruct(Lexer::tokenize($text));
|
||||||
$snipContent->setText($text);
|
$snipContent->text = $text;
|
||||||
$this->snipContentRepository->save($snipContent);
|
$this->snipContentRepository->save($snipContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ use App\Security\Voter\SnipVoter;
|
|||||||
use App\Service\SnipContent\SnipContentService;
|
use App\Service\SnipContent\SnipContentService;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||||
|
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
class ApiController extends AbstractApiController
|
class ApiController extends AbstractApiController
|
||||||
@@ -34,11 +35,11 @@ class ApiController extends AbstractApiController
|
|||||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||||
|
|
||||||
return $this->successResponse([
|
return $this->successResponse([
|
||||||
'id' => $snip->getId(),
|
'id' => $snip->id,
|
||||||
'content' => $snip->getActiveText(),
|
'content' => $snip->getActiveText(),
|
||||||
'createdBy' => [
|
'createdBy' => [
|
||||||
'id' => $snip->getCreatedBy()->getId(),
|
'id' => $snip->createdBy->getId(),
|
||||||
'name' => $snip->getCreatedBy()->getName(),
|
'name' => $snip->createdBy->getName(),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -49,27 +50,28 @@ class ApiController extends AbstractApiController
|
|||||||
#[MapRequestPayload] SnipPostRequest $request,
|
#[MapRequestPayload] SnipPostRequest $request,
|
||||||
SnipContentService $cs,
|
SnipContentService $cs,
|
||||||
SnipRepository $repo,
|
SnipRepository $repo,
|
||||||
|
ObjectMapperInterface $mapper,
|
||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||||
|
|
||||||
if (!($snip->getActiveVersion() === $snip->getLatestVersion())) {
|
if (!($snip->activeVersion === $snip->getLatestVersion())) {
|
||||||
return $this->errorResponse('Snip is not the latest version');
|
return $this->errorResponse('Snip is not the latest version');
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->pushToSnip($snip);
|
$mapper->map($request, $snip);
|
||||||
$repo->save($snip);
|
$repo->save($snip);
|
||||||
if ($request->content !== null) {
|
if ($request->content !== null) {
|
||||||
$cs->update($snip, $request->content);
|
$cs->update($snip, $request->content, $request->contentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->successResponse([
|
return $this->successResponse([
|
||||||
'id' => $snip->getId(),
|
'id' => $snip->id,
|
||||||
'name' => $snip->getName(),
|
'name' => $snip->name,
|
||||||
'content' => $snip->getActiveText(),
|
'content' => $snip->getActiveText(),
|
||||||
'createdBy' => [
|
'createdBy' => [
|
||||||
'id' => $snip->getCreatedBy()->getId(),
|
'id' => $snip->createdBy->getId(),
|
||||||
'name' => $snip->getCreatedBy()->getName(),
|
'name' => $snip->createdBy->getName(),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
42
src/Controller/Api/ExtensionController.php
Normal file
42
src/Controller/Api/ExtensionController.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Api;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
|
||||||
|
class ExtensionController extends AbstractApiController
|
||||||
|
{
|
||||||
|
#[Route('/content', methods: ['POST'])]
|
||||||
|
public function setContent(
|
||||||
|
Request $request,
|
||||||
|
CacheInterface $cache,
|
||||||
|
): Response {
|
||||||
|
// Parse JSON payload
|
||||||
|
$data = json_decode($request->getContent(), true);
|
||||||
|
if (!$data || !isset($data['html'])) {
|
||||||
|
return $this->errorResponse('Invalid JSON payload: Missing html parameter');
|
||||||
|
}
|
||||||
|
$html = $data['html'];
|
||||||
|
|
||||||
|
// Store HTML in cache with unique key
|
||||||
|
$userId = $this->getUser()?->getId();
|
||||||
|
$cacheKey = 'html_' . $userId . '_' . bin2hex(random_bytes(8));
|
||||||
|
$cache->get($cacheKey, function () use ($html) {
|
||||||
|
// value to cache
|
||||||
|
return $html;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build URL to redirect user to a form page later
|
||||||
|
$formUrl = $this->generateUrl(
|
||||||
|
'snip_new',
|
||||||
|
['cacheKey' => $cacheKey],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->successResponse(['url' => $formUrl]);
|
||||||
|
}
|
||||||
|
}
|
@@ -18,10 +18,10 @@ class SnipContentController extends AbstractController
|
|||||||
#[Route('/compare/{to}/{from}', name: '_compare')]
|
#[Route('/compare/{to}/{from}', name: '_compare')]
|
||||||
public function compare(SnipContent $to, ?SnipContent $from = null): Response
|
public function compare(SnipContent $to, ?SnipContent $from = null): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $to->getSnip());
|
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $to->snip);
|
||||||
|
|
||||||
if ($from === null) {
|
if ($from === null) {
|
||||||
$from = $to->getParent();
|
$from = $to->parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = MyersDiff::buildDiffLines(
|
$diff = MyersDiff::buildDiffLines(
|
||||||
@@ -30,7 +30,7 @@ class SnipContentController extends AbstractController
|
|||||||
);
|
);
|
||||||
|
|
||||||
return $this->render('content/compare.html.twig', [
|
return $this->render('content/compare.html.twig', [
|
||||||
'snip' => $to->getSnip(),
|
'snip' => $to->snip,
|
||||||
'diff' => $diff,
|
'diff' => $diff,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,8 @@ namespace App\Controller;
|
|||||||
use App\Controller\Attribute\MapQueryCached;
|
use App\Controller\Attribute\MapQueryCached;
|
||||||
use App\Dto\SnipFilterRequest;
|
use App\Dto\SnipFilterRequest;
|
||||||
use App\Entity\Snip;
|
use App\Entity\Snip;
|
||||||
|
use App\Entity\SnipContent;
|
||||||
|
use App\Entity\Tag;
|
||||||
use App\Form\ConfirmationType;
|
use App\Form\ConfirmationType;
|
||||||
use App\Form\SnipType;
|
use App\Form\SnipType;
|
||||||
use App\Repository\SnipRepository;
|
use App\Repository\SnipRepository;
|
||||||
@@ -16,6 +18,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
|
||||||
#[Route('/snip', name: 'snip')]
|
#[Route('/snip', name: 'snip')]
|
||||||
class SnipController extends AbstractController
|
class SnipController extends AbstractController
|
||||||
@@ -85,22 +88,20 @@ class SnipController extends AbstractController
|
|||||||
* Temporary solution to prevent editing of old versions
|
* Temporary solution to prevent editing of old versions
|
||||||
* It technically fully works, but rendering the version history needs an update first
|
* It technically fully works, but rendering the version history needs an update first
|
||||||
*/
|
*/
|
||||||
$isLatest = $snip->getActiveVersion() === $snip->getLatestVersion();
|
$isLatest = $snip->activeVersion === $snip->getLatestVersion();
|
||||||
if (!$isLatest) {
|
if (!$isLatest) {
|
||||||
$this->addFlash('error', 'Snip is not the latest version, changes will not be saved.');
|
$this->addFlash('error', 'Snip is not the latest version, changes will not be saved.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->createForm(SnipType::class, $snip);
|
$form = $this->createForm(SnipType::class, $snip)
|
||||||
$form->add('Save', SubmitType::class);
|
->add('Save', SubmitType::class);
|
||||||
if ($snip->getId()) {
|
$form->get('content')->setData($snip->getActiveText());
|
||||||
$form->get('content')->setData($snip->getActiveText());
|
|
||||||
}
|
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
if (!$isLatest) {
|
if (!$isLatest) {
|
||||||
return $this->redirectToRoute('snip_single', [
|
return $this->redirectToRoute('snip_single', [
|
||||||
'snip' => $snip->getId(),
|
'snip' => $snip->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
$this->repository->save($snip);
|
$this->repository->save($snip);
|
||||||
@@ -113,7 +114,7 @@ class SnipController extends AbstractController
|
|||||||
$this->addFlash('success', sprintf('Snip "%s" saved', $snip));
|
$this->addFlash('success', sprintf('Snip "%s" saved', $snip));
|
||||||
|
|
||||||
return $this->redirectToRoute('snip_single', [
|
return $this->redirectToRoute('snip_single', [
|
||||||
'snip' => $snip->getId(),
|
'snip' => $snip->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,15 +124,47 @@ class SnipController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/new', name: '_new')]
|
#[Route('/new/{cacheKey}', name: '_new')]
|
||||||
public function new(Request $request, SnipContentService $contentService): Response
|
public function new(
|
||||||
|
Request $request,
|
||||||
|
SnipContentService $contentService,
|
||||||
|
CacheInterface $cache,
|
||||||
|
?string $cacheKey = null,
|
||||||
|
): Response
|
||||||
{
|
{
|
||||||
$snip = new Snip();
|
$snip = new Snip();
|
||||||
$snip->setCreatedAtNow()
|
$snip->setCreatedAtNow();
|
||||||
->setCreatedBy($this->getUser())
|
$snip->createdBy = $this->getUser();
|
||||||
;
|
|
||||||
|
|
||||||
return $this->edit($snip, $request, $contentService);
|
$form = $this->createForm(SnipType::class, $snip);
|
||||||
|
$form->add('Create', SubmitType::class);
|
||||||
|
|
||||||
|
if ($cacheKey) {
|
||||||
|
$content = $cache->get($cacheKey, fn() => null);
|
||||||
|
$form->get('content')->setData($content);
|
||||||
|
$form->get('parser')->setData('unsafe');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->repository->save($snip);
|
||||||
|
$contentService->update(
|
||||||
|
$snip,
|
||||||
|
$form->get('content')->getData(),
|
||||||
|
$form->get('contentName')->getData()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addFlash('success', sprintf('Snip "%s" created', $snip));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('snip_single', [
|
||||||
|
'snip' => $snip->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('snip/create.html.twig', [
|
||||||
|
'snip' => $snip,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/delete/{snip}', name: '_delete')]
|
#[Route('/delete/{snip}', name: '_delete')]
|
||||||
@@ -142,7 +175,7 @@ class SnipController extends AbstractController
|
|||||||
$form = $this->createForm(ConfirmationType::class);
|
$form = $this->createForm(ConfirmationType::class);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$snip->setActiveVersion(null);
|
$snip->activeVersion = null;
|
||||||
$this->repository->save($snip);
|
$this->repository->save($snip);
|
||||||
$this->repository->remove($snip);
|
$this->repository->remove($snip);
|
||||||
$this->addFlash('success', sprintf('Snip "%s" deleted', $snip));
|
$this->addFlash('success', sprintf('Snip "%s" deleted', $snip));
|
||||||
@@ -159,14 +192,14 @@ class SnipController extends AbstractController
|
|||||||
public function archive(Snip $snip): Response
|
public function archive(Snip $snip): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||||
$snip->setArchived(!$snip->isArchived());
|
$snip->archived = !$snip->archived;
|
||||||
$this->repository->save($snip);
|
$this->repository->save($snip);
|
||||||
if ($snip->isArchived()) {
|
if ($snip->archived) {
|
||||||
$this->addFlash('success', sprintf('Snip "%s" archived', $snip));
|
$this->addFlash('success', sprintf('Snip "%s" archived', $snip));
|
||||||
} else {
|
} else {
|
||||||
$this->addFlash('success', sprintf('Snip "%s" unarchived', $snip));
|
$this->addFlash('success', sprintf('Snip "%s" unarchived', $snip));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute('snip_edit', ['snip' => $snip->getId()]);
|
return $this->redirectToRoute('snip_edit', ['snip' => $snip->id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -33,7 +33,7 @@ class VersionController extends AbstractController
|
|||||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||||
|
|
||||||
$this->contentService->setVersion($snip, $version);
|
$this->contentService->setVersion($snip, $version);
|
||||||
$this->addFlash('success', 'Snip version set to ' . $version->getId());
|
$this->addFlash('success', 'Snip version set to ' . $version->id);
|
||||||
return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]);
|
return $this->redirectToRoute('snip_single', ['snip' => $snip->id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
14
src/Dto/Condition/ConditionNotNull.php
Normal file
14
src/Dto/Condition/ConditionNotNull.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Dto\Condition;
|
||||||
|
|
||||||
|
use Symfony\Component\ObjectMapper\ConditionCallableInterface;
|
||||||
|
|
||||||
|
class ConditionNotNull implements ConditionCallableInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __invoke(mixed $value, object $source, ?object $target): bool
|
||||||
|
{
|
||||||
|
return null !== $value;
|
||||||
|
}
|
||||||
|
}
|
@@ -20,13 +20,4 @@ readonly class SnipFilterRequest implements CachableDtoInterface
|
|||||||
public ?string $sort = self::SORT_NAME,
|
public ?string $sort = self::SORT_NAME,
|
||||||
public ?string $tag = self::TAG_ALL,
|
public ?string $tag = self::TAG_ALL,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'visibility' => $this->visibility,
|
|
||||||
'sort' => $this->sort,
|
|
||||||
'tag' => $this->tag,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -2,27 +2,21 @@
|
|||||||
|
|
||||||
namespace App\Dto;
|
namespace App\Dto;
|
||||||
|
|
||||||
|
use App\Dto\Condition\ConditionNotNull;
|
||||||
use App\Entity\Snip;
|
use App\Entity\Snip;
|
||||||
|
use Symfony\Component\ObjectMapper\Attribute\Map;
|
||||||
|
|
||||||
|
#[Map(target: Snip::class)]
|
||||||
class SnipPostRequest
|
class SnipPostRequest
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
#[Map(if: new ConditionNotNull())]
|
||||||
public ?string $name = null,
|
public ?string $name = null,
|
||||||
public ?string $content = null,
|
public ?string $content = null,
|
||||||
|
#[Map(if: new ConditionNotNull())]
|
||||||
public ?bool $public = null,
|
public ?bool $public = null,
|
||||||
|
#[Map(if: new ConditionNotNull())]
|
||||||
public ?bool $visible = null,
|
public ?bool $visible = null,
|
||||||
|
public ?string $contentName = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function pushToSnip(Snip $snip): void
|
|
||||||
{
|
|
||||||
if ($this->name !== null) {
|
|
||||||
$snip->setName($this->name);
|
|
||||||
}
|
|
||||||
if ($this->public !== null) {
|
|
||||||
$snip->setPublic($this->public);
|
|
||||||
}
|
|
||||||
if ($this->visible !== null) {
|
|
||||||
$snip->setVisible($this->visible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -10,46 +10,22 @@ trait TrackedTrait
|
|||||||
{
|
{
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private ?DateTime $createdAt = null;
|
public ?DateTime $createdAt = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private ?User $createdBy = null;
|
public ?User $createdBy = null;
|
||||||
|
|
||||||
public function getCreatedBy(): ?User
|
|
||||||
{
|
|
||||||
return $this->createdBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCreatedBy(?User $createdBy): self
|
|
||||||
{
|
|
||||||
$this->createdBy = $createdBy;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCreatedAt(): ?DateTime
|
|
||||||
{
|
|
||||||
return $this->createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCreatedAt(DateTime $createdAt): self
|
|
||||||
{
|
|
||||||
$this->createdAt = $createdAt;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCreatedAtNowNoSeconds(): self
|
public function setCreatedAtNowNoSeconds(): self
|
||||||
{
|
{
|
||||||
$this->setCreatedAt(DateTime::createFromFormat('Y-m-d H:i', date('Y-m-d H:i')));
|
$this->createdAt = DateTime::createFromFormat('Y-m-d H:i', date('Y-m-d H:i'));
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCreatedAtNow(): self
|
public function setCreatedAtNow(): self
|
||||||
{
|
{
|
||||||
$this->setCreatedAt(new DateTime());
|
$this->createdAt = new DateTime();
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@@ -17,34 +17,34 @@ class Snip
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
private ?string $name = null;
|
public ?string $name = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private bool $public = false;
|
public bool $public = false;
|
||||||
|
|
||||||
#[ORM\OneToMany(mappedBy: 'snip', targetEntity: SnipContent::class, orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: SnipContent::class, mappedBy: 'snip', orphanRemoval: true)]
|
||||||
private Collection $snipContents;
|
public Collection $snipContents;
|
||||||
|
|
||||||
#[ORM\OneToOne]
|
#[ORM\OneToOne]
|
||||||
private ?SnipContent $activeVersion = null;
|
public ?SnipContent $activeVersion = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
private ?string $parser = null;
|
public ?string $parser = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private bool $visible = true;
|
public bool $visible = true;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private bool $archived = false;
|
public bool $archived = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Tag>
|
* @var Collection<int, Tag>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Tag::class, mappedBy: 'snips')]
|
#[ORM\ManyToMany(targetEntity: Tag::class, mappedBy: 'snips')]
|
||||||
private Collection $tags;
|
public Collection $tags;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -59,51 +59,14 @@ class Snip
|
|||||||
|
|
||||||
public function getActiveText(): string
|
public function getActiveText(): string
|
||||||
{
|
{
|
||||||
return SnipContentService::rebuildText($this->getActiveVersion());
|
return SnipContentService::rebuildText($this->activeVersion);
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): ?string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(string $name): self
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isPublic(): ?bool
|
|
||||||
{
|
|
||||||
return $this->public;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setPublic(bool $public): self
|
|
||||||
{
|
|
||||||
$this->public = $public;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, SnipContent>
|
|
||||||
*/
|
|
||||||
public function getSnipContents(): Collection
|
|
||||||
{
|
|
||||||
return $this->snipContents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addSnipContent(SnipContent $snipContent): self
|
public function addSnipContent(SnipContent $snipContent): self
|
||||||
{
|
{
|
||||||
if (!$this->snipContents->contains($snipContent)) {
|
if (!$this->snipContents->contains($snipContent)) {
|
||||||
$this->snipContents->add($snipContent);
|
$this->snipContents->add($snipContent);
|
||||||
$snipContent->setSnip($this);
|
$snipContent->snip = $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -113,8 +76,8 @@ class Snip
|
|||||||
{
|
{
|
||||||
if ($this->snipContents->removeElement($snipContent)) {
|
if ($this->snipContents->removeElement($snipContent)) {
|
||||||
// set the owning side to null (unless already changed)
|
// set the owning side to null (unless already changed)
|
||||||
if ($snipContent->getSnip() === $this) {
|
if ($snipContent->snip === $this) {
|
||||||
$snipContent->setSnip(null);
|
$snipContent->snip = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,62 +89,6 @@ class Snip
|
|||||||
return $this->snipContents->last() ?: null;
|
return $this->snipContents->last() ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getActiveVersion(): ?SnipContent
|
|
||||||
{
|
|
||||||
return $this->activeVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setActiveVersion(?SnipContent $activeVersion): static
|
|
||||||
{
|
|
||||||
$this->activeVersion = $activeVersion;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getParser(): ?string
|
|
||||||
{
|
|
||||||
return $this->parser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setParser(string $parser): static
|
|
||||||
{
|
|
||||||
$this->parser = $parser;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isVisible(): ?bool
|
|
||||||
{
|
|
||||||
return $this->visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setVisible(bool $visible): static
|
|
||||||
{
|
|
||||||
$this->visible = $visible;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isArchived(): ?bool
|
|
||||||
{
|
|
||||||
return $this->archived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setArchived(bool $archived): static
|
|
||||||
{
|
|
||||||
$this->archived = $archived;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, Tag>
|
|
||||||
*/
|
|
||||||
public function getTags(): Collection
|
|
||||||
{
|
|
||||||
return $this->tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addTag(Tag $tag): static
|
public function addTag(Tag $tag): static
|
||||||
{
|
{
|
||||||
if (!$this->tags->contains($tag)) {
|
if (!$this->tags->contains($tag)) {
|
||||||
|
@@ -17,124 +17,30 @@ class SnipContent
|
|||||||
#[ORM\Column(type: UlidType::NAME, unique: true)]
|
#[ORM\Column(type: UlidType::NAME, unique: true)]
|
||||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
#[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
|
#[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
|
||||||
private ?Ulid $id = null;
|
public ?Ulid $id = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'snipContents')]
|
#[ORM\ManyToOne(inversedBy: 'snipContents')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private ?Snip $snip = null;
|
public ?Snip $snip = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||||
private ?self $parent = null;
|
public ?self $parent = null;
|
||||||
|
|
||||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
|
/** @var Collection<int, self> */
|
||||||
private Collection $children;
|
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
||||||
|
public Collection $children;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private ?string $text = null;
|
public ?string $text = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?array $diff = null;
|
public ?array $diff = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $name = null;
|
public ?string $name = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->children = new ArrayCollection();
|
$this->children = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?Ulid
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSnip(): ?Snip
|
|
||||||
{
|
|
||||||
return $this->snip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setSnip(?Snip $snip): self
|
|
||||||
{
|
|
||||||
$this->snip = $snip;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getParent(): ?self
|
|
||||||
{
|
|
||||||
return $this->parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setParent(?self $parent): self
|
|
||||||
{
|
|
||||||
$this->parent = $parent;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, self>
|
|
||||||
*/
|
|
||||||
public function getChildren(): Collection
|
|
||||||
{
|
|
||||||
return $this->children;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addChild(self $child): self
|
|
||||||
{
|
|
||||||
if (!$this->children->contains($child)) {
|
|
||||||
$this->children->add($child);
|
|
||||||
$child->setParent($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeChild(self $child): self
|
|
||||||
{
|
|
||||||
if ($this->children->removeElement($child)) {
|
|
||||||
// set the owning side to null (unless already changed)
|
|
||||||
if ($child->getParent() === $this) {
|
|
||||||
$child->setParent(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getText(): ?string
|
|
||||||
{
|
|
||||||
return $this->text;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setText(?string $text): self
|
|
||||||
{
|
|
||||||
$this->text = $text;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDiff(): ?array
|
|
||||||
{
|
|
||||||
return $this->diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setDiff(?array $diff): static
|
|
||||||
{
|
|
||||||
$this->diff = $diff;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): ?string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(?string $name): static
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -16,22 +16,22 @@ class Tag
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
#[Assert\NotEqualTo(SnipFilterRequest::TAG_ALL)]
|
#[Assert\NotEqualTo(SnipFilterRequest::TAG_ALL)]
|
||||||
#[Assert\NotEqualTo(SnipFilterRequest::TAG_NONE)]
|
#[Assert\NotEqualTo(SnipFilterRequest::TAG_NONE)]
|
||||||
private ?string $name = null;
|
public ?string $name = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private ?User $user = null;
|
public ?User $user = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Snip>
|
* @var Collection<int, Snip>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Snip::class, inversedBy: 'tags')]
|
#[ORM\ManyToMany(targetEntity: Snip::class, inversedBy: 'tags')]
|
||||||
private Collection $snips;
|
public Collection $snips;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -43,43 +43,6 @@ class Tag
|
|||||||
return $this->name ?? '';
|
return $this->name ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): ?string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(string $name): static
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUser(): ?User
|
|
||||||
{
|
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setUser(?User $user): static
|
|
||||||
{
|
|
||||||
$this->user = $user;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, Snip>
|
|
||||||
*/
|
|
||||||
public function getSnips(): Collection
|
|
||||||
{
|
|
||||||
return $this->snips;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addSnip(Snip $snip): static
|
public function addSnip(Snip $snip): static
|
||||||
{
|
{
|
||||||
if (!$this->snips->contains($snip)) {
|
if (!$this->snips->contains($snip)) {
|
||||||
|
@@ -34,7 +34,7 @@ class TagsType extends AbstractType implements DataTransformerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
$tags = array_map(fn(Tag $tag) => $tag->getName(), $value);
|
$tags = array_map(fn(Tag $tag) => $tag->name, $value);
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,8 @@ class TagsType extends AbstractType implements DataTransformerInterface
|
|||||||
$tagEntity = $this->repository->findOneBy(['name' => $tag, 'user' => $user]);
|
$tagEntity = $this->repository->findOneBy(['name' => $tag, 'user' => $user]);
|
||||||
if ($tagEntity === null) {
|
if ($tagEntity === null) {
|
||||||
$tagEntity = new Tag();
|
$tagEntity = new Tag();
|
||||||
$tagEntity->setName($tag)->setUser($user);
|
$tagEntity->name = $tag;
|
||||||
|
$tagEntity->user = $user;
|
||||||
|
|
||||||
// Validate the new Tag entity
|
// Validate the new Tag entity
|
||||||
$errors = $this->validator->validate($tagEntity);
|
$errors = $this->validator->validate($tagEntity);
|
||||||
|
@@ -4,13 +4,14 @@ namespace App\Security\Voter;
|
|||||||
|
|
||||||
use App\Entity\Snip;
|
use App\Entity\Snip;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
class SnipVoter extends Voter
|
class SnipVoter extends Voter
|
||||||
{
|
{
|
||||||
public const EDIT = 'edit';
|
public const string EDIT = 'edit';
|
||||||
public const VIEW = 'view';
|
public const string VIEW = 'view';
|
||||||
|
|
||||||
protected function supports(string $attribute, mixed $subject): bool
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
{
|
{
|
||||||
@@ -20,7 +21,7 @@ class SnipVoter extends Voter
|
|||||||
&& $subject instanceof Snip;
|
&& $subject instanceof Snip;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token,/* , ?Vote $vote = null */): bool
|
||||||
{
|
{
|
||||||
/** @var Snip $subject */
|
/** @var Snip $subject */
|
||||||
|
|
||||||
@@ -28,14 +29,14 @@ class SnipVoter extends Voter
|
|||||||
|
|
||||||
switch ($attribute) {
|
switch ($attribute) {
|
||||||
case self::VIEW:
|
case self::VIEW:
|
||||||
if ($subject->isPublic()) {
|
if ($subject->public) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case self::EDIT:
|
case self::EDIT:
|
||||||
if (!$user instanceof UserInterface) {
|
if (!$user instanceof UserInterface) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($subject->getCreatedBy() === $user) {
|
if ($subject->createdBy === $user) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@@ -15,27 +15,26 @@ readonly class SnipContentService
|
|||||||
|
|
||||||
public function update(Snip $snip, string $contents, ?string $contentName): void
|
public function update(Snip $snip, string $contents, ?string $contentName): void
|
||||||
{
|
{
|
||||||
$parentContent = $snip->getActiveVersion();
|
$parentContent = $snip->activeVersion;
|
||||||
if (self::rebuildText($parentContent) === $contents) {
|
if (self::rebuildText($parentContent) === $contents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new snipContent entity with previous one as parent
|
// Create new snipContent entity with previous one as parent
|
||||||
$content = new SnipContent();
|
$content = new SnipContent();
|
||||||
$content
|
$content->text = $contents;
|
||||||
->setText($contents)
|
$content->snip = $snip;
|
||||||
->setSnip($snip)
|
$content->name = $contentName;
|
||||||
->setName($contentName)
|
|
||||||
;
|
|
||||||
if ($parentContent !== null) {
|
if ($parentContent !== null) {
|
||||||
$content->setParent($parentContent);
|
$content->parent = $parentContent;
|
||||||
$this->contentToRelative($parentContent);
|
$this->contentToRelative($parentContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->em->persist($content);
|
$this->em->persist($content);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
$snip->setActiveVersion($content);
|
$snip->activeVersion = $content;
|
||||||
$this->em->persist($snip);
|
$this->em->persist($snip);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
@@ -45,52 +44,52 @@ readonly class SnipContentService
|
|||||||
if ($snipContent === null) {
|
if ($snipContent === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if ($snipContent->getText()) {
|
if ($snipContent->text) {
|
||||||
return $snipContent->getText();
|
return $snipContent->text;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parentContent = $snipContent->getParent();
|
$parentContent = $snipContent->parent;
|
||||||
if ($parentContent === null && $snipContent->getDiff() === null) {
|
if ($parentContent === null && $snipContent->diff === null) {
|
||||||
return '---Something went very wrong, cant rebuild the text---';
|
return '---Something went very wrong, cant rebuild the text---';
|
||||||
}
|
}
|
||||||
|
|
||||||
return MyersDiff::rebuildBFromCompact(
|
return MyersDiff::rebuildBFromCompact(
|
||||||
self::rebuildText($parentContent), $snipContent->getDiff()
|
self::rebuildText($parentContent), $snipContent->diff
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setVersion(Snip $snip, SnipContent $version): void
|
public function setVersion(Snip $snip, SnipContent $version): void
|
||||||
{
|
{
|
||||||
$activeVersion = $snip->getActiveVersion();
|
$activeVersion = $snip->activeVersion;
|
||||||
$this->contentToAbsolute($version);
|
$this->contentToAbsolute($version);
|
||||||
$this->contentToRelative($activeVersion);
|
$this->contentToRelative($activeVersion);
|
||||||
|
|
||||||
$snip->setActiveVersion($version);
|
$snip->activeVersion = $version;
|
||||||
$this->em->persist($snip);
|
$this->em->persist($snip);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function contentToRelative(SnipContent $content): void
|
public function contentToRelative(SnipContent $content): void
|
||||||
{
|
{
|
||||||
if ($content->getText() === null || $content->getParent() === null) {
|
if ($content->text === null || $content->parent === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$contentText = $content->getText();
|
$contentText = $content->text;
|
||||||
$parentText = self::rebuildText($content->getParent());
|
$parentText = self::rebuildText($content->parent);
|
||||||
$diff = MyersDiff::calculate($parentText, $contentText);
|
$diff = MyersDiff::calculate($parentText, $contentText);
|
||||||
$content->setDiff($diff);
|
$content->diff = $diff;
|
||||||
$content->setText(null);
|
$content->text = null;
|
||||||
$this->em->persist($content);
|
$this->em->persist($content);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function contentToAbsolute(SnipContent $content): void
|
public function contentToAbsolute(SnipContent $content): void
|
||||||
{
|
{
|
||||||
if ($content->getDiff() === null) {
|
if ($content->diff === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$content->setText(self::rebuildText($content));
|
$content->text = self::rebuildText($content);
|
||||||
$content->setDiff(null);
|
$content->diff = null;
|
||||||
$this->em->persist($content);
|
$this->em->persist($content);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ abstract class AbstractParser implements ParserInterface
|
|||||||
try {
|
try {
|
||||||
return $this->safeParseView($content);
|
return $this->safeParseView($content);
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($exception->getMessage()));
|
return sprintf('<pre><code>%s</code></pre>', htmlspecialchars($exception->getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,9 +17,9 @@ class GenericParser extends AbstractParser
|
|||||||
$builder = new PipelineBuilder();
|
$builder = new PipelineBuilder();
|
||||||
$pipeline = $builder
|
$pipeline = $builder
|
||||||
->add(new HtmlEscapeStage())
|
->add(new HtmlEscapeStage())
|
||||||
|
// ->add(new ReplaceBlocksStage('<pre>', '</pre>', '```'))
|
||||||
|
// ->add(new ReplaceBlocksStage('<code>', '</code>', '``'))
|
||||||
->add(new ReplaceStage(PHP_EOL, '<br>'))
|
->add(new ReplaceStage(PHP_EOL, '<br>'))
|
||||||
->add(new ReplaceBlocksStage('<pre><code class="hljs">', '</code></pre>', '```'))
|
|
||||||
->add(new ReplaceBlocksStage('<code class="hljs">', '</code>', '``'))
|
|
||||||
->add($this->referenceStage)
|
->add($this->referenceStage)
|
||||||
->add($this->includeStage)
|
->add($this->includeStage)
|
||||||
->build()
|
->build()
|
||||||
@@ -27,13 +27,4 @@ class GenericParser extends AbstractParser
|
|||||||
|
|
||||||
return $pipeline->process($content);
|
return $pipeline->process($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parseRaw(string $content): string
|
|
||||||
{
|
|
||||||
return str_replace(
|
|
||||||
['```', '``'],
|
|
||||||
'',
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -37,11 +37,11 @@ class IncludeReferenceStage implements StageInterface
|
|||||||
$content = null;
|
$content = null;
|
||||||
}
|
}
|
||||||
if ($content) {
|
if ($content) {
|
||||||
$snip = $content->getSnip();
|
$snip = $content->snip;
|
||||||
} else {
|
} else {
|
||||||
$snip = $this->snipRepository->find($id);
|
$snip = $this->snipRepository->find($id);
|
||||||
if ($snip) {
|
if ($snip) {
|
||||||
$content = $this->snipContentRepository->find($snip->getActiveVersion());
|
$content = $this->snipContentRepository->find($snip->activeVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($content === null) {
|
if ($content === null) {
|
||||||
|
@@ -4,13 +4,14 @@ namespace App\Service\SnipParser\Generic;
|
|||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use League\Pipeline\StageInterface;
|
use League\Pipeline\StageInterface;
|
||||||
|
use Tempest\Highlight\Highlighter;
|
||||||
|
|
||||||
class ReplaceBlocksStage implements StageInterface
|
readonly class ReplaceBlocksStage implements StageInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $openTag = '<pre><code>',
|
public string $openTag = '<pre><code>',
|
||||||
public readonly string $closeTag = '</code></pre>',
|
public string $closeTag = '</code></pre>',
|
||||||
public readonly string $delimiter = '```'
|
public string $delimiter = '```'
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke(mixed $payload): string
|
public function __invoke(mixed $payload): string
|
||||||
@@ -26,8 +27,9 @@ class ReplaceBlocksStage implements StageInterface
|
|||||||
{
|
{
|
||||||
$pattern = sprintf('/%s(.+?)%s/s', preg_quote($this->delimiter), preg_quote($this->delimiter));
|
$pattern = sprintf('/%s(.+?)%s/s', preg_quote($this->delimiter), preg_quote($this->delimiter));
|
||||||
|
|
||||||
return preg_replace_callback($pattern, function ($matches) {
|
$highlighter = new Highlighter()->withGutter();
|
||||||
return $this->openTag . trim($matches[1]) . $this->closeTag;
|
return preg_replace_callback($pattern, function ($matches) use ($highlighter) {
|
||||||
|
return $this->openTag . $highlighter->parse(trim($matches[1]), 'php') . $this->closeTag;
|
||||||
}, $text);
|
}, $text);
|
||||||
}
|
}
|
||||||
}
|
}
|
13
src/Service/SnipParser/Generic/UnsafeParser.php
Normal file
13
src/Service/SnipParser/Generic/UnsafeParser.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\SnipParser\Generic;
|
||||||
|
|
||||||
|
use App\Service\SnipParser\AbstractParser;
|
||||||
|
|
||||||
|
class UnsafeParser extends AbstractParser
|
||||||
|
{
|
||||||
|
public function safeParseView(string $content): string
|
||||||
|
{
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
@@ -36,7 +36,7 @@ readonly class UrlReferenceStage implements StageInterface
|
|||||||
return sprintf('<span title="access denied">%s</span>', $matches[0]);
|
return sprintf('<span title="access denied">%s</span>', $matches[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = $this->router->generate('snip_single', ['snip' => $snip->getId()]);
|
$url = $this->router->generate('snip_single', ['snip' => $snip->id]);
|
||||||
return sprintf('<a href="%s">%s</a>', $url, $snip);
|
return sprintf('<a href="%s">%s</a>', $url, $snip);
|
||||||
}, $payload);
|
}, $payload);
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,14 @@
|
|||||||
namespace App\Service\SnipParser\Html;
|
namespace App\Service\SnipParser\Html;
|
||||||
|
|
||||||
use App\Service\SnipParser\AbstractParser;
|
use App\Service\SnipParser\AbstractParser;
|
||||||
|
use Tempest\Highlight\Highlighter;
|
||||||
|
|
||||||
class HtmlParser extends AbstractParser
|
class HtmlParser extends AbstractParser
|
||||||
{
|
{
|
||||||
public function safeParseView(string $content): string
|
public function safeParseView(string $content): string
|
||||||
{
|
{
|
||||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($content));
|
$highlighter = new Highlighter()->withGutter();
|
||||||
|
|
||||||
|
return '<pre data-lang="html" class="notranslate">' . $highlighter->parse($content, 'html') . '</pre>';
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,11 +6,16 @@ use App\Repository\SnipRepository;
|
|||||||
use App\Service\SnipParser\AbstractParser;
|
use App\Service\SnipParser\AbstractParser;
|
||||||
use League\CommonMark\Event\DocumentParsedEvent;
|
use League\CommonMark\Event\DocumentParsedEvent;
|
||||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||||
|
use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
|
||||||
|
use League\CommonMark\Extension\Footnote\FootnoteExtension;
|
||||||
|
use League\CommonMark\Extension\Table\Table;
|
||||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||||
use League\CommonMark\Node\Inline\Text;
|
use League\CommonMark\Node\Inline\Text;
|
||||||
use League\CommonMark\Node\Query;
|
use League\CommonMark\Node\Query;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Routing\RouterInterface;
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Tempest\Highlight\CommonMark\HighlightExtension;
|
||||||
|
use Tempest\Highlight\Highlighter;
|
||||||
|
|
||||||
class MarkdownParser extends AbstractParser
|
class MarkdownParser extends AbstractParser
|
||||||
{
|
{
|
||||||
@@ -21,8 +26,25 @@ class MarkdownParser extends AbstractParser
|
|||||||
|
|
||||||
public function safeParseView(string $content): string
|
public function safeParseView(string $content): string
|
||||||
{
|
{
|
||||||
$converter = new GithubFlavoredMarkdownConverter();
|
$config = [
|
||||||
$converter->getEnvironment()->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...));
|
'default_attributes' => [
|
||||||
|
Table::class => [
|
||||||
|
'class' => 'table table-hover',
|
||||||
|
],
|
||||||
|
Link::class => [
|
||||||
|
'class' => 'btn btn-sm btn-secondary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$converter = new GithubFlavoredMarkdownConverter($config);
|
||||||
|
$converter
|
||||||
|
->getEnvironment()
|
||||||
|
->addExtension(new HighlightExtension(new Highlighter()->withGutter()))
|
||||||
|
->addExtension(new FootnoteExtension())
|
||||||
|
->addExtension(new DefaultAttributesExtension())
|
||||||
|
->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...))
|
||||||
|
;
|
||||||
return $converter->convert($content);
|
return $converter->convert($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,11 +54,17 @@ class MarkdownParser extends AbstractParser
|
|||||||
|
|
||||||
$linkNodes = new Query()
|
$linkNodes = new Query()
|
||||||
->where(Query::type(Link::class))
|
->where(Query::type(Link::class))
|
||||||
->findAll($document);
|
->findAll($document)
|
||||||
|
;
|
||||||
|
|
||||||
|
/** @var Link $linkNode */
|
||||||
foreach ($linkNodes as $linkNode) {
|
foreach ($linkNodes as $linkNode) {
|
||||||
$url = $linkNode->getUrl();
|
$url = $linkNode->getUrl();
|
||||||
|
|
||||||
|
if (!is_numeric($url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$snip = $this->snipRepo->find($url);
|
$snip = $this->snipRepo->find($url);
|
||||||
if ($snip === null) {
|
if ($snip === null) {
|
||||||
continue;
|
continue;
|
||||||
|
@@ -29,9 +29,9 @@ readonly class ParserFactory
|
|||||||
|
|
||||||
public function getBySnip(Snip $snip): ParserInterface
|
public function getBySnip(Snip $snip): ParserInterface
|
||||||
{
|
{
|
||||||
$parser = $snip->getParser();
|
$parser = $snip->parser;
|
||||||
if (null === $parser) {
|
if (null === $parser) {
|
||||||
throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->getParser()));
|
throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->parser));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->get($parser);
|
return $this->get($parser);
|
||||||
|
@@ -25,7 +25,7 @@ class SnipLoader implements LoaderInterface
|
|||||||
|
|
||||||
public function getCacheKey(string $name): string
|
public function getCacheKey(string $name): string
|
||||||
{
|
{
|
||||||
return $this->getFromKey($name)->getActiveVersion()->getId();
|
return $this->getFromKey($name)->activeVersion->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isFresh(string $name, int $time): bool
|
public function isFresh(string $name, int $time): bool
|
||||||
|
@@ -56,8 +56,8 @@ class SnipTwigExtension extends AbstractExtension
|
|||||||
$request = new SnipFilterRequest(SnipFilterRequest::VISIBILITY_ALL, tag: $tag);
|
$request = new SnipFilterRequest(SnipFilterRequest::VISIBILITY_ALL, tag: $tag);
|
||||||
$snips = $this->snipRepo->findByRequest($user, $request);
|
$snips = $this->snipRepo->findByRequest($user, $request);
|
||||||
return array_map(fn(Snip $snip) => [
|
return array_map(fn(Snip $snip) => [
|
||||||
'id' => $snip->getId(),
|
'id' => $snip->id,
|
||||||
'name' => $snip->getName(),
|
'name' => $snip->name,
|
||||||
], $snips);
|
], $snips);
|
||||||
}
|
}
|
||||||
}
|
}
|
46
symfony.lock
46
symfony.lock
@@ -1,4 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"doctrine/deprecations": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"doctrine/doctrine-bundle": {
|
"doctrine/doctrine-bundle": {
|
||||||
"version": "2.14",
|
"version": "2.14",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -26,6 +35,18 @@
|
|||||||
"migrations/.gitignore"
|
"migrations/.gitignore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"nelmio/cors-bundle": {
|
||||||
|
"version": "2.5",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.5",
|
||||||
|
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/nelmio_cors.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/console": {
|
"symfony/console": {
|
||||||
"version": "7.2",
|
"version": "7.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -76,14 +97,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/framework-bundle": {
|
"symfony/framework-bundle": {
|
||||||
"version": "7.2",
|
"version": "7.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
"repo": "github.com/symfony/recipes",
|
"repo": "github.com/symfony/recipes",
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"version": "7.2",
|
"version": "7.3",
|
||||||
"ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89"
|
"ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
".editorconfig",
|
||||||
"config/packages/cache.yaml",
|
"config/packages/cache.yaml",
|
||||||
"config/packages/framework.yaml",
|
"config/packages/framework.yaml",
|
||||||
"config/preload.php",
|
"config/preload.php",
|
||||||
@@ -115,6 +137,18 @@
|
|||||||
"config/packages/monolog.yaml"
|
"config/packages/monolog.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/property_info.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/routing": {
|
"symfony/routing": {
|
||||||
"version": "7.2",
|
"version": "7.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -177,12 +211,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/web-profiler-bundle": {
|
"symfony/web-profiler-bundle": {
|
||||||
"version": "7.2",
|
"version": "7.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
"repo": "github.com/symfony/recipes",
|
"repo": "github.com/symfony/recipes",
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"version": "6.1",
|
"version": "7.3",
|
||||||
"ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7"
|
"ref": "a363460c1b0b4a4d0242f2ce1a843ca0f6ac9026"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"config/packages/web_profiler.yaml",
|
"config/packages/web_profiler.yaml",
|
||||||
|
@@ -54,21 +54,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ parent() }}
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const codeBlocks = document.querySelectorAll('code.hljs');
|
|
||||||
|
|
||||||
codeBlocks.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
7
templates/snip/create.html.twig
Normal file
7
templates/snip/create.html.twig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'base/one.column.html.twig' %}
|
||||||
|
|
||||||
|
{% set title = 'Create Snip' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{{ form(form) }}
|
||||||
|
{% endblock %}
|
@@ -1,10 +1,6 @@
|
|||||||
{% extends 'snip/base.html.twig' %}
|
{% extends 'snip/base.html.twig' %}
|
||||||
|
|
||||||
{% if snip.id %}
|
{% set title %}{{ snip }} - Edit{% endset %}
|
||||||
{% set title %}{{ snip }} - Edit{% endset %}
|
|
||||||
{% else %}
|
|
||||||
{% set title = 'Create Snip' %}
|
|
||||||
{% endif %}
|
|
||||||
{% set active = 'edit' %}
|
{% set active = 'edit' %}
|
||||||
|
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
|
@@ -15,18 +15,5 @@
|
|||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet" href="{{ asset('github-light-default.css') }}">
|
||||||
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const codeBlocks = document.querySelectorAll('code.hljs');
|
|
||||||
|
|
||||||
codeBlocks.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in New Issue
Block a user