1
0
Fork 0

Basic - although quite huge - infrastructure for rendering bundles

This commit is contained in:
Richard Dern 2024-04-20 23:27:47 +02:00
parent 5b19490296
commit 58e6c07ab6
77 changed files with 3993 additions and 443 deletions

View File

@ -3,13 +3,20 @@
namespace App\Classes;
use App\Classes\Traits\ManagesMetadata;
use App\Classes\Traits\Repairs\RepairsImages;
use App\Classes\Traits\Repairs\RepairsSounds;
use App\Classes\Traits\Repairs\RepairsVideos;
use App\View\Components\Image;
use App\View\Components\Sound;
use App\View\Components\Video;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class AttachmentsManager
{
use ManagesMetadata;
use ManagesMetadata, RepairsImages, RepairsSounds, RepairsVideos;
/**
* Manages images
@ -34,11 +41,19 @@ class AttachmentsManager
private string $attachmentsDir = 'attachments';
public function __construct(protected string $kind, protected string $root, protected FilesystemAdapter $disk)
private FilesystemAdapter $disk;
private bool $isLoaded = false;
public function __construct(protected string $kind, protected Bundle $bundle)
{
$this->disk = $bundle->getDisk();
$root = $bundle->getDataDir();
$this->metadataFilePath = sprintf('%s%s.json', $root, $kind);
$this->targetForFiles = sprintf('%s%s/%s', $root, $this->attachmentsDir, $kind);
$this->manager = new MetadataManager($this->metadataFilePath, $disk);
$this->manager = new MetadataManager($this->metadataFilePath, $bundle);
}
/**
@ -114,9 +129,13 @@ public function exists()
/**
* Load data from disk
*/
public function load()
public function load(bool $reload = false)
{
$this->manager->load();
if (!$this->isLoaded || $reload) {
$this->manager->load();
$this->isLoaded = true;
}
}
/**
@ -127,6 +146,178 @@ public function save(): bool
return $this->manager->save();
}
/**
* Replace all attachments declared in specified markdown with appropriate
* Blade components
*/
public function replaceAttachmentsInMarkdown(string $markdown)
{
$pattern = '/<x-attachment\s+ref="([^"]+)"\s*\/>/';
preg_match_all($pattern, $markdown, $matches);
foreach ($matches[1] as $index => $ref) {
try {
$component = $this->getComponentByRef($ref, 'article');
} catch (Exception $ex) {
continue;
}
$replacement = Str::squish((string) $component->render());
$markdown = Str::replace($matches[0][$index], $replacement, $markdown);
}
return $markdown;
}
/**
* Return a Blade component for the attachment we passed the ref off
*/
public function getComponentByRef(string $ref, ?string $filter = null)
{
$attachment = $this->getAttachmentData($ref);
$variant = null;
if (!empty($filter)) {
$variant = $this->getVariantData($ref, $filter);
}
// We set the fullname to the full path of the file in the filesystem
// so the Blade component does not have to worry about it
$attachment['filename'] = $this->getAttachmentFullPath($ref);
if (!empty($filter)) {
$variant['filename'] = $this->getVariantFullPath($ref, $filter);
}
$component = $this->getBladeComponent($attachment, $variant);
return $component;
}
/**
* Remove an attachment from disk and any reference in this file
*/
public function deleteAttachment(string $ref)
{
$path = $this->getAttachmentFullPath($ref);
$parent = dirname($path);
$this->disk->delete($path);
if (empty($this->disk->listContents($parent, true))) {
$this->disk->delete($parent);
}
$this->manager->remove([
sprintf('files.%s', $ref),
sprintf('variants.%s', $ref),
sprintf('history.%s', $ref),
]);
}
/**
* Remove an attachment from disk and any reference in this file
*/
public function deleteVariant(string $originalRef, string $filter)
{
$path = $this->getVariantFullPath($originalRef, $filter);
$parent = dirname($path);
$this->disk->delete($path);
if (empty($this->disk->listContents($parent, true))) {
$this->disk->delete($parent);
}
$this->manager->remove([
sprintf('variants.%s.%s', $originalRef, $filter),
]);
}
/**
* Return full path of specified attachment
*/
public function getAttachmentFullPath(string $ref)
{
$data = $this->getAttachmentData($ref);
return sprintf('%s%s', $this->bundle->getDataDir(), $data['filename']);
}
/**
* Return attachment's relative path to bundle's data directory
*/
public function getAttachmentRelativePath(string $ref)
{
$fullPath = $this->getAttachmentFullPath($ref);
return Str::remove($this->bundle->getDataDir(), $fullPath);
}
/**
* Return full path of specified attachment's variant
*/
public function getVariantFullPath(string $originalRef, string $filter)
{
$originalPath = $this->getAttachmentFullPath($originalRef);
$parentDir = dirname($originalPath);
$extension = pathinfo($originalPath, PATHINFO_EXTENSION);
$variantExtension = $extension === 'gif' ? 'gif' : 'webp';
return sprintf('%s/%s.%s', $parentDir, $filter, $variantExtension);
}
/**
* Return attachment variant's relative path to bundle's data directory
*/
public function getVariantRelativePath(string $ref, string $filter)
{
$fullPath = $this->getVariantFullPath($ref, $filter);
return Str::remove($this->bundle->getDataDir(), $fullPath);
}
/**
* Return specified attachment data
*/
public function getAttachmentData(string $ref): array
{
$data = $this->manager->get(sprintf('files.%s', $ref));
if (empty($data)) {
throw new Exception(sprintf('Unknown attachment %s', $ref));
}
return $data;
}
/**
* Return specified variant data
*/
public function getVariantData(string $originalRef, string $filter): array
{
return $this->manager->get(sprintf('variants.%s.%s', $originalRef, $filter), []);
}
/**
* Repair attachments
*/
public function repair()
{
switch ($this->kind) {
case self::Images:
$this->repairImages();
break;
case self::Sounds:
$this->repairSounds();
break;
case self::Videos:
$this->repairVideos();
break;
}
}
/**
* Generate new, random reference for a file
*/
@ -146,4 +337,22 @@ private function getFormatedFilename(string $path): string
return sprintf('%s.%s', $slug, $extension);
}
/**
* Return a Blade component corresponding to the kind of data we are
* managing, and we pass it attachment data
*/
private function getBladeComponent(array $data, ?array $variant = [])
{
switch ($this->kind) {
case self::Images:
return new Image($data, $variant);
case self::Sounds:
return new Sound($data, $variant);
case self::Videos:
return new Video($data, $variant);
default:
throw new Exception(sprintf('Unknown Blade Component for attachment kind "%s"', $this->kind));
}
}
}

View File

@ -5,33 +5,90 @@
use App\Classes\Traits\ManagesAttachments;
use App\Classes\Traits\ManagesMarkdown;
use App\Classes\Traits\ManagesMetadata;
use App\Contracts\Bundles;
use App\Services\BundleRenderers\Facades\BundleRenderer;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Str;
use League\Flysystem\StorageAttributes;
class Bundle
class Bundle implements Bundles
{
use ManagesAttachments, ManagesMarkdown, ManagesMetadata;
use ManagesAttachments,
ManagesMarkdown,
ManagesMetadata;
protected string $dataDir;
public function getPath()
{
return $this->path;
}
protected int $currentPage = 1;
public function __construct(protected string $path, protected FilesystemAdapter $disk)
{
$this->path = Str::start(Str::finish($this->path, '/'), '/');
$this->path = $this->normalizeBundlePath($path);
$this->dataDir = $this->path . 'data/';
$this->registerDefaultManagers();
}
/**
* Return current page number
*/
public function getCurrentPage(): int
{
return $this->currentPage;
}
/**
* Return bundle's path
*/
public function getPath(): string
{
return $this->path;
}
/**
* Return bundle's data dir
*/
public function getDataDir(): string
{
return $this->dataDir;
}
/**
* Return bundle's filesystem instance
*/
public function getDisk(): FilesystemAdapter
{
return $this->disk;
}
/**
* Return the section where this bundle is located
*/
public function getSection(): string
{
$parts = preg_split('#/#', $this->path, -1, PREG_SPLIT_NO_EMPTY);
if (count($parts) > 0) {
return config(sprintf('sections.%s.title', $parts[0]), $parts[0]);
}
return null;
}
/**
* Return a boolean value indicating if there already is a bundle in
* specified path
*/
public function exists(): bool
{
return $this->markdown()->exists();
}
/**
* Load everything
*/
public function load()
public function load(): void
{
$this->loadAttachments();
$this->loadMetadata();
@ -41,7 +98,7 @@ public function load()
/**
* Store all files of the bundle
*/
public function save()
public function save(): void
{
$this->saveAttachments();
$this->saveMetadata();
@ -49,59 +106,27 @@ public function save()
}
/**
* Register default managers
* Repair bundle
*/
private function registerDefaultManagers()
public function repair(): void
{
$this->markdown();
$this->metadata();
$this->metadata('metadata');
$this->attachments(AttachmentsManager::Images);
$this->attachments(AttachmentsManager::Sounds);
$this->attachments(AttachmentsManager::Videos);
$this->load();
$this->lintMarkdown();
$this->repairAttachments();
$this->save();
}
public function render()
{
$renderer = BundleRenderer::getBundleRendererFor($this);
return $renderer->render();
}
/**
* Get a complete filename prefixed with bundle's path
* Return a list of bundles in the specified path
*/
private function getFilenameInBundle(string $filename, ?string $extension = null)
{
return $this->getFullpath($this->path, $filename, $extension);
}
/**
* Get a complete filename prefixed with bundle's data dir
*/
private function getFilenameInDataBundle(string $filename, ?string $extension = null)
{
return $this->getFullpath($this->dataDir, $filename, $extension);
}
/**
* Return full path of specified filename in specified root directory, and
* optionally add specified extension
*/
private function getFullpath(string $root, string $filename, ?string $extension = null)
{
$filename = Str::remove($root, $filename);
if (!empty($extension) && !Str::endsWith($filename, $extension)) {
$filename .= $extension;
}
return sprintf('%s%s', $root, $filename);
}
/**
* Return a boolean value indicating if there already is a bundle in
* specified path
*/
public function exists()
{
return $this->markdown()->exists();
}
public static function findBundles(FilesystemAdapter $disk, ?string $path = '/', bool $recursive = false)
public static function findBundles(FilesystemAdapter $disk, ?string $path = '/', bool $recursive = false): array
{
if ($recursive) {
return $disk
@ -124,4 +149,73 @@ public static function findBundles(FilesystemAdapter $disk, ?string $path = '/',
return $bundles;
}
}
/**
* Return a normalized representation of bundle's path
*/
private function normalizeBundlePath(string $path): string
{
$parts = preg_split('#/#', $path, -1, PREG_SPLIT_NO_EMPTY);
$count = count($parts);
if ($count > 2 && $parts[$count - 2] === 'page') {
// Requested URL is pagination
$page = array_pop($parts);
if (!is_numeric($page) || $page < 1) {
throw new Exception(sprintf('Invalid page number: %s', $page));
}
$this->currentPage = $page;
// Pop "page" out of the parts
array_pop($parts);
}
return Str::start(Str::finish(implode('/', $parts), '/'), '/');
}
/**
* Register default managers
*/
private function registerDefaultManagers(): void
{
$this->markdown();
$this->metadata();
$this->metadata('metadata');
$this->attachments(AttachmentsManager::Images);
$this->attachments(AttachmentsManager::Sounds);
$this->attachments(AttachmentsManager::Videos);
}
/**
* Get a complete filename prefixed with bundle's path
*/
private function getFilenameInBundle(string $filename, ?string $extension = null): string
{
return $this->getFullpath($this->path, $filename, $extension);
}
/**
* Get a complete filename prefixed with bundle's data dir
*/
private function getFilenameInDataBundle(string $filename, ?string $extension = null): string
{
return $this->getFullpath($this->dataDir, $filename, $extension);
}
/**
* Return full path of specified filename in specified root directory, and
* optionally add specified extension
*/
private function getFullpath(string $root, string $filename, ?string $extension = null): string
{
$filename = Str::remove($root, $filename);
if (!empty($extension) && !Str::endsWith($filename, $extension)) {
$filename .= $extension;
}
return sprintf('%s%s', $root, $filename);
}
}

312
app/Classes/Link.php Normal file
View File

@ -0,0 +1,312 @@
<?php
namespace App\Classes;
use App\Exceptions\PartnerCannotBeFound;
use App\Services\Partners\Contracts\Partner;
use App\Services\Partners\Facades\Partner as PartnerFactory;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use League\CommonMark\Util\HtmlElement;
class Link
{
private bool $isExternal;
private bool $isDead;
private string $reason;
private Carbon $checked;
private ?Partner $partner;
private string $finalUrl;
private string $title;
public function __construct(protected string $url, protected ?string $innerHtml = null)
{
}
/**
* Return a boolean value indicating if specified url is an anchor
*/
public function isAnchor(): bool
{
return Str::startsWith($this->url, '#');
}
/**
* Return a boolean value indicating if the URL is either internal or
* external to current website
*/
public function isExternal(): bool
{
if (!isset($this->isExternal)) {
$this->isExternal = Str::startsWith($this->url, [
'http://',
'https://',
]);
}
return $this->isExternal;
}
/**
* Return a boolean value indicating if URL can be reached
*/
public function isDead(): bool
{
if (!isset($this->isDead)) {
$this->isDead = $this->fetchIsDead()['isDead'];
}
return $this->isDead;
}
/**
* If URL cannot be reached, return the reason why
*/
public function reason(): ?string
{
if (!isset($this->reason)) {
$this->reason = $this->fetchIsDead()['reason'] ?? null;
}
return $this->reason;
}
/**
* Return the last date/time the URL was checked
*/
public function checked(): ?Carbon
{
if (!isset($this->checked)) {
$this->checked = $this->fetchIsDead()['checked'] ?? null;
}
return $this->checked;
}
/**
* Return a partner associated with URL, if any
*/
public function partner(): ?Partner
{
if (!isset($this->partner)) {
if (!$this->isExternal()) {
$this->partner = null;
} else {
try {
$this->partner = PartnerFactory::getPartner($this->url);
} catch (PartnerCannotBeFound $ex) {
$this->partner = null;
}
}
}
return $this->partner;
}
/**
* Return the final URL of the link, after any kind of modifications we can
* make
*/
public function finalUrl(): string
{
if (!isset($this->finalUrl)) {
$this->finalUrl = $this->fetchFinalUrl();
}
return $this->finalUrl;
}
/**
* Return a suitable title for the link
*/
public function title(): string
{
if (!isset($this->title)) {
$this->title = $this->fetchTitle();
}
return $this->title;
}
/**
* Return the link as a HtmlElement
*/
public function toHtmlElement(): HtmlElement
{
$rel = $this->fetchRel();
$classes = $this->fetchCssClasses();
return new HtmlElement('a', [
'href' => $this->finalUrl(),
'rel' => implode(' ', $rel),
'title' => $this->title(),
'class' => implode(' ', $classes),
], $this->innerHtml);
}
/**
* Tries HEAD and GET requests to find out if URL can be reached
*/
private function fetchIsDead()
{
$cacheKey = sprintf('link_status_%s', md5($this->url));
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$isDead = true;
$reason = null;
$checked = now();
foreach (['head', 'get'] as $method) {
try {
if (Http::throw()->{$method}($this->url)->ok()) {
$isDead = false;
break;
}
} catch (Exception $ex) {
$reason = $ex->getMessage();
}
}
$result = compact('isDead', 'reason', 'checked');
Cache::put($cacheKey, $result, now()->addMonth());
return $result;
}
/**
* Return the final URL
*/
private function fetchFinalUrl(): string
{
if ($this->isAnchor()) {
return $this->url;
}
if (!empty($this->partner())) {
return $this->partner()->getAffiliateLink();
}
return $this->url;
}
/**
* Return an array of appropriate rel attributes
*/
private function fetchRel(): array
{
$rel = [];
if (!empty($this->partner())) {
$rel[] = 'nofollow';
} elseif ($this->isExternal()) {
$rel = [
'nofollow',
'noreferrer',
'noopener',
];
}
return $rel;
}
/**
* Return applicable CSS classes
*/
private function fetchCssClasses(): array
{
$classes = [];
if ($this->isExternal()) {
$classes = [
config('markdown.external_link.html_class'),
];
if (!empty($this->partner())) {
$classes[] = 'affiliate';
}
if ($this->isDead()) {
$classes[] = 'dead';
}
}
return $classes;
}
/**
* Return a suitable title for this link
*/
private function fetchTitle(): string
{
if ($this->isAnchor()) {
return sprintf('Lien direct vers %s', $this->url);
}
if ($this->isExternal()) {
$title = 'Lien ';
if (!empty($this->partner())) {
$title .= 'affilié ';
} else {
$title .= 'externe ';
}
if ($this->isDead()) {
$title .= sprintf(
' (mort depuis le %s - %s) ',
$this->checked()->format('d/m/Y'),
$this->reason()
);
}
$title .= sprintf(' : %s', $this->url);
return $title;
} else {
return $this->fetchBundleTitle();
}
}
/**
* Build link title from internal bundle
*/
private function fetchBundleTitle(): string
{
$bundle = new Bundle($this->url, Storage::disk(env('CONTENT_DISK')));
$bundle->load();
$date = $bundle->metadata()->get('date');
$title = $bundle->metadata()->get('title');
$section = $bundle->getSection();
if (!empty($date)) {
return sprintf(
'Lien interne : [%s] %s | Publié le %s',
$section,
$title,
Carbon::parse($date)->format('d/m/Y')
);
} else {
return sprintf(
'Lien interne : [%s] %s',
$section,
$title,
);
}
}
}

View File

@ -2,6 +2,8 @@
namespace App\Classes;
use App\Services\Markdown\Formatter;
use App\Services\Markdown\Linter;
use Illuminate\Filesystem\FilesystemAdapter;
class MarkdownManager
@ -11,14 +13,20 @@ class MarkdownManager
*/
private ?string $originalContent = null;
private FilesystemAdapter $disk;
private bool $isLoaded = false;
/**
* Current markdown content
*/
private ?string $content = null;
public function __construct(protected string $filename, protected FilesystemAdapter $disk)
{
public function __construct(
protected string $filename,
protected Bundle $bundle
) {
$this->disk = $bundle->getDisk();
}
/**
@ -32,11 +40,15 @@ public function exists()
/**
* Load markdown file
*/
public function load()
public function load(bool $reload = false)
{
$content = $this->disk->get($this->filename);
if (!$this->isLoaded || $reload) {
$content = $this->disk->get($this->filename);
$this->set($content);
$this->originalContent = $content;
$this->content = $content;
$this->isLoaded = true;
}
}
/**
@ -47,6 +59,18 @@ public function get(): ?string
return $this->content;
}
/**
* Convert markdown to HTML
*/
public function render()
{
$content = $this->bundle->replaceAttachmentsInMarkdown($this->content ?? '');
$formatter = new Formatter($content);
$result = $formatter->render();
return $result;
}
/**
* Set current markdown content
*/
@ -64,6 +88,18 @@ public function isDirty(): bool
return $this->originalContent !== $this->content;
}
/**
* Lint markdown
*/
public function lint(): void
{
$linter = new Linter($this->content ?? '');
$this->content = $linter->format();
$this->save();
}
/**
* Store file on disk
*/

View File

@ -3,7 +3,6 @@
namespace App\Classes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Collection;
/**
* Class MetadataManager
@ -18,38 +17,35 @@ class MetadataManager
protected $content;
protected FilesystemAdapter $disk;
protected bool $isLoaded = false;
/**
* Constructor for the MetadataManager class.
*
* @param string $filename The filename where metadata is stored.
* @param disk $disk The disk abstraction provided by Laravel.
*/
public function __construct(protected string $filename, protected FilesystemAdapter $disk)
public function __construct(protected string $filename, protected Bundle $bundle)
{
$this->disk = $bundle->getDisk();
$this->load();
}
/**
* Loads metadata from disk using the disk, with caching to improve performance.
*/
public function load()
public function load(bool $reload = false)
{
$data = $this->readFromDisk();
if (!$this->isLoaded || $reload) {
$data = $this->readFromDisk();
$this->originalContent = new Collection($data);
$this->content = new Collection($data);
}
/**
* Reads metadata from disk. Caches the content to reduce file system reads.
*
* @return array The data read from the file.
*/
protected function readFromDisk()
{
$data = $this->disk->get($this->filename);
return json_decode($data, true) ?? [];
$this->originalContent = $data;
$this->content = $data;
$this->isLoaded = true;
}
}
/**
@ -59,7 +55,7 @@ protected function readFromDisk()
*/
public function isDirty(): bool
{
return $this->originalContent->toJson() != $this->content->toJson();
return collect($this->originalContent)->toJson() != collect($this->content)->toJson();
}
/**
@ -69,29 +65,17 @@ public function isDirty(): bool
*/
public function save(): bool
{
if (!$this->isDirty()) {
if (!$this->isDirty() || empty($this->content)) {
return false;
}
$this->writeToDisk($this->content->all());
$this->writeToDisk($this->content);
$this->originalContent = new Collection($this->content->all());
$this->originalContent = $this->content;
return true;
}
/**
* Writes the provided data to disk as JSON.
*
* @param array $data The data to write to disk.
*/
protected function writeToDisk(array $data)
{
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->disk->put($this->filename, $json);
}
/**
* Sets a metadata value for a specified key.
*
@ -100,11 +84,7 @@ protected function writeToDisk(array $data)
*/
public function set($key, $value)
{
$content = $this->content->dot();
$newValue = collect([$key => $value])->dot();
$newContent = collect($content)->replaceRecursive($newValue)->undot();
$this->content = $newContent;
data_set($this->content, $key, $value, true);
}
/**
@ -112,18 +92,16 @@ public function set($key, $value)
*/
public function setMany(array $array)
{
$content = $this->content->dot();
$newValue = collect($array)->dot();
$newContent = collect($content)->replaceRecursive($newValue)->undot();
$this->content = $newContent;
foreach ($array as $key => $value) {
$this->set($key, $value);
}
}
public function merge(array $array)
{
$content = $this->content->dot();
$content = collect($this->content)->dot();
$newValue = collect($array)->dot();
$newContent = collect($content)->mergeRecursive($newValue)->undot();
$newContent = collect($content)->mergeRecursive($newValue)->undot()->toArray();
$this->content = $newContent;
}
@ -137,7 +115,7 @@ public function merge(array $array)
*/
public function get($key, $default = null)
{
return collect($this->content)->dot()->get($key, $default);
return data_get($this->content, $key, $default);
}
/**
@ -147,7 +125,7 @@ public function get($key, $default = null)
*/
public function all()
{
return $this->content->all();
return $this->content;
}
/**
@ -157,6 +135,30 @@ public function all()
*/
public function remove($key)
{
$this->content->forget($key);
collect($this->content)->forget($key);
}
/**
* Reads metadata from disk. Caches the content to reduce file system reads.
*
* @return array The data read from the file.
*/
protected function readFromDisk()
{
$data = $this->disk->get($this->filename) ?? '[]';
return json_decode($data, true);
}
/**
* Writes the provided data to disk as JSON.
*
* @param array $data The data to write to disk.
*/
protected function writeToDisk(array $data)
{
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->disk->put($this->filename, $json);
}
}

View File

@ -8,13 +8,34 @@ trait ManagesAttachments
{
private array $attachmentsManagers = [];
/**
* Iterates over attached manager to replace attachments in specified
* markdown with appropriate Blade components
*/
public function replaceAttachmentsInMarkdown(string $markdown)
{
foreach ($this->attachmentsManagers as $manager) {
$markdown = $manager->replaceAttachmentsInMarkdown($markdown);
}
return $markdown;
}
/**
* Return an instance of attachments manager for specified kind of files
*/
public function attachments(string $kind): AttachmentsManager
{
return $this->registerAttachmentsManager($kind);
}
/**
* Register an attachments manager for specified filename
*/
private function registerAttachmentsManager(string $kind): AttachmentsManager
{
if (!array_key_exists($kind, $this->attachmentsManagers)) {
$this->attachmentsManagers[$kind] = new AttachmentsManager($kind, $this->dataDir, $this->disk);
$this->attachmentsManagers[$kind] = new AttachmentsManager($kind, $this);
}
return $this->attachmentsManagers[$kind];
@ -41,10 +62,12 @@ private function saveAttachments()
}
/**
* Return an instance of attachments manager for specified kind of files
* Repair all attachments files that needs to be
*/
public function attachments(string $kind): AttachmentsManager
private function repairAttachments()
{
return $this->registerAttachmentsManager($kind);
foreach ($this->attachmentsManagers as $manager) {
$manager->repair();
}
}
}

View File

@ -8,6 +8,14 @@ trait ManagesMarkdown
{
private array $markdownManagers = [];
/**
* Return an instance of markdown manager for specified filename
*/
public function markdown(?string $filename = 'index'): MarkdownManager
{
return $this->registerMarkdownManager($filename);
}
/**
* Register a markdown manager for specified filename
*/
@ -16,7 +24,7 @@ private function registerMarkdownManager(string $filename): MarkdownManager
$filename = $this->getFilenameInBundle($filename, '.md');
if (!array_key_exists($filename, $this->markdownManagers)) {
$this->markdownManagers[$filename] = new MarkdownManager($filename, $this->disk);
$this->markdownManagers[$filename] = new MarkdownManager($filename, $this);
}
return $this->markdownManagers[$filename];
@ -43,10 +51,12 @@ private function saveMarkdown()
}
/**
* Return an instance of markdown manager for specified filename
* Lint all markdowns in the bundle
*/
public function markdown(?string $filename = 'index'): MarkdownManager
private function lintMarkdown()
{
return $this->registerMarkdownManager($filename);
foreach ($this->markdownManagers as $manager) {
$manager->lint();
}
}
}

View File

@ -8,6 +8,14 @@ trait ManagesMetadata
{
private array $metadataManagers = [];
/**
* Return an instance of metadata manager for specified filename
*/
public function metadata(?string $filename = 'index'): MetadataManager
{
return $this->registerMetadataManager($filename);
}
/**
* Register a metadata manager for specified filename
*/
@ -16,7 +24,7 @@ private function registerMetadataManager(string $filename): MetadataManager
$filename = $this->getFilenameInDataBundle($filename, '.json');
if (!array_key_exists($filename, $this->metadataManagers)) {
$this->metadataManagers[$filename] = new MetadataManager($filename, $this->disk);
$this->metadataManagers[$filename] = new MetadataManager($filename, $this);
}
return $this->metadataManagers[$filename];
@ -41,12 +49,4 @@ private function saveMetadata()
$manager->save();
}
}
/**
* Return an instance of metadata manager for specified filename
*/
public function metadata(?string $filename = 'index'): MetadataManager
{
return $this->registerMetadataManager($filename);
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Classes\Traits\Repairs;
use Carbon\Carbon;
use Intervention\Image\Laravel\Facades\Image;
/**
* Trait for AttachmentsManager
*/
trait RepairsImages
{
/**
* Repair all attached images
*/
private function repairImages()
{
$this->load();
foreach ($this->manager->get('files', []) as $ref => $data) {
$this->repairKnownImage($ref, $data);
}
$this->save();
}
/**
* Repair a single image. Move it to appropriate directory and create or
* update variations.
*/
private function repairKnownImage(string $ref, array $data)
{
if (!empty($data['url'])) {
$data['filename'] = $data['url'];
unset($data['url']);
$this->manager->set(sprintf('files.%s', $ref), $data);
}
$extension = pathinfo($data['filename'], PATHINFO_EXTENSION);
$currentFullPath = sprintf('%s%s', $this->bundle->getDataDir(), $data['filename']);
$expectedFullPath = sprintf(
'%s%s/%s/%s/original.%s',
$this->bundle->getDataDir(),
$this->attachmentsDir,
$this->kind,
$ref,
$extension
);
if (!$this->disk->exists($currentFullPath)) {
$this->deleteAttachment($ref);
return;
}
if (empty($data['last_modified'])) {
$data['last_modified'] = Carbon::parse($this->disk->lastModified($currentFullPath))->toIso8601String();
}
if ($currentFullPath !== $expectedFullPath) {
$this->disk->move($currentFullPath, $expectedFullPath);
$data['last_modified'] = Carbon::parse($this->disk->lastModified($expectedFullPath))->toIso8601String();
$data['filename'] = sprintf(
'%s/%s/%s/original.%s',
$this->attachmentsDir,
$this->kind,
$ref,
$extension
);
$parentDir = dirname($currentFullPath);
if (empty($this->disk->listContents($parentDir, true))) {
$this->disk->delete($parentDir);
}
}
$this->manager->set(sprintf('files.%s', $ref), $data);
$this->syncImageVariants($ref);
}
/**
* Synchronize image variants (apply pre-defined filters)
*/
private function syncImageVariants(string $ref)
{
$currentVariants = $this->manager->get(sprintf('variants.%s', $ref), []);
$configVariants = array_keys(config('imagefilters'));
// Synchronize or delete currently declared variants
foreach ($currentVariants as $filter => $variantData) {
if (!in_array($filter, $configVariants)) {
$this->deleteVariant($ref, $filter);
continue;
}
$this->syncImageVariant($ref, $filter);
}
// Synchronize variants with the ones expected by configuration
foreach ($configVariants as $filter) {
$this->syncImageVariant($ref, $filter);
}
}
/**
* Synchronize specific image variant
*/
private function syncImageVariant(string $ref, string $filter)
{
$originalData = $this->getAttachmentData($ref);
$variantData = $this->getVariantData($ref, $filter);
$variantFilepath = $this->getVariantFullPath($ref, $filter);
if (!$this->disk->exists($variantFilepath)) {
$this->createImageVariant($ref, $filter);
} else {
$originalLastModified = Carbon::parse($originalData['last_modified']);
$variantLastModified = Carbon::parse($variantData['last_modified'] ?? $this->disk->lastModified($variantFilepath));
if ($originalLastModified->gt($variantLastModified)) {
$this->createImageVariant($ref, $filter);
}
}
}
/**
* Create the variant for specified image and filter
*/
private function createImageVariant(string $ref, string $filter)
{
$filterClass = config(sprintf('imagefilters.%s', $filter));
$original = $this->getAttachmentFullPath($ref);
$target = $this->getVariantFullPath($ref, $filter);
$contents = $this->disk->get($original);
$image = Image::read($contents);
$variantData = $this->getVariantData($ref, $filter);
$image->modify(new $filterClass());
if ($image->isAnimated()) {
$contents = (string) $image->toGif();
} else {
$contents = (string) $image->toWebp();
}
$this->disk->put($target, $contents);
$variantData['filename'] = $this->getVariantRelativePath($ref, $filter);
$variantData['last_modified'] = now();
$this->manager->set(sprintf('variants.%s.%s', $ref, $filter), $variantData);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Classes\Traits\Repairs;
use Carbon\Carbon;
/**
* Trait for AttachmentsManager
*/
trait RepairsSounds
{
/**
* Repair all attached sounds
*/
private function repairSounds()
{
$this->load();
foreach ($this->manager->get('files', []) as $ref => $data) {
$this->repairKnownSound($ref, $data);
}
$this->save();
}
/**
* Repair a single sound. Move it to appropriate directory
*/
private function repairKnownSound(string $ref, array $data)
{
if (!empty($data['url'])) {
$data['filename'] = $data['url'];
unset($data['url']);
$this->manager->set(sprintf('files.%s', $ref), $data);
}
$extension = pathinfo($data['filename'], PATHINFO_EXTENSION);
$currentFullPath = sprintf('%s%s', $this->bundle->getDataDir(), $data['filename']);
$expectedFullPath = sprintf(
'%s%s/%s/%s/original.%s',
$this->bundle->getDataDir(),
$this->attachmentsDir,
$this->kind,
$ref,
$extension
);
if (!$this->disk->exists($currentFullPath)) {
$this->deleteAttachment($ref);
return;
}
if (empty($data['last_modified'])) {
$data['last_modified'] = Carbon::parse($this->disk->lastModified($currentFullPath))->toIso8601String();
}
if ($currentFullPath !== $expectedFullPath) {
$this->disk->move($currentFullPath, $expectedFullPath);
$data['last_modified'] = Carbon::parse($this->disk->lastModified($expectedFullPath))->toIso8601String();
$data['filename'] = sprintf(
'%s/%s/%s/original.%s',
$this->attachmentsDir,
$this->kind,
$ref,
$extension
);
$parentDir = dirname($currentFullPath);
if (empty($this->disk->listContents($parentDir, true))) {
$this->disk->delete($parentDir);
}
}
$this->manager->set(sprintf('files.%s', $ref), $data);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Classes\Traits\Repairs;
use Carbon\Carbon;
/**
* Trait for AttachmentsManager
*/
trait RepairsVideos
{
/**
* Repair all attached videos
*/
private function repairVideos()
{
$this->load();
foreach ($this->manager->get('files', []) as $ref => $data) {
$this->repairKnownVideo($ref, $data);
}
$this->save();
}
/**
* Repair a single video. Move it to appropriate directory
*/
private function repairKnownVideo(string $ref, array $data)
{
if (!empty($data['url'])) {
$data['filename'] = $data['url'];
unset($data['url']);
$this->manager->set(sprintf('files.%s', $ref), $data);
}
$extension = pathinfo($data['filename'], PATHINFO_EXTENSION);
$currentFullPath = sprintf('%s%s', $this->bundle->getDataDir(), $data['filename']);
$expectedFullPath = sprintf(
'%s%s/%s/%s/original.%s',
$this->bundle->getDataDir(),
$this->attachmentsDir,
$this->kind,
$ref,
$extension
);
if (!$this->disk->exists($currentFullPath)) {
$this->deleteAttachment($ref);
return;
}
if (empty($data['last_modified'])) {
$data['last_modified'] = Carbon::parse($this->disk->lastModified($currentFullPath))->toIso8601String();
}
if ($currentFullPath !== $expectedFullPath) {
$this->disk->move($currentFullPath, $expectedFullPath);
$data['last_modified'] = Carbon::parse($this->disk->lastModified($expectedFullPath))->toIso8601String();
$data['filename'] = sprintf(
'%s/%s/%s/original.%s',
$this->attachmentsDir,
$this->kind,
$ref,
$extension
);
$parentDir = dirname($currentFullPath);
if (empty($this->disk->listContents($parentDir, true))) {
$this->disk->delete($parentDir);
}
}
$this->manager->set(sprintf('files.%s', $ref), $data);
}
}

View File

@ -1,8 +1,9 @@
<?php
namespace App\Console\Commands\Article;
namespace App\Console\Commands\Bundle;
use App\Services\BundleCreator\Contracts\CreatesBundle;
use App\Services\BundleCreators\Contracts\CreatesBundle;
use App\Services\BundleCreators\Facades\BundleCreator;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
@ -15,7 +16,7 @@ class Create extends Command implements PromptsForMissingInput
*
* @var string
*/
protected $signature = 'article:create { section : Specific section in which the article will be created }';
protected $signature = 'bundle:create { section : Specific section in which the article will be created }';
/**
* The console command description.
@ -24,26 +25,6 @@ class Create extends Command implements PromptsForMissingInput
*/
protected $description = 'Create new article';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array<string, string>
*/
protected function promptForMissingArgumentsUsing(): array
{
return [
'section' => fn () => select('Article section', $this->listSections()),
];
}
/**
* Return either an instance of the bundle creator or form specifications
*/
protected function getBundleCreator(string $section, ?array $data = []): array|CreatesBundle
{
return app()->make('bundleCreator.factory')->getBundleCreatorFor($section, $data);
}
/**
* Execute the console command.
*/
@ -70,6 +51,26 @@ public function handle()
$this->line($path);
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array<string, string>
*/
protected function promptForMissingArgumentsUsing(): array
{
return [
'section' => fn () => select('Article section', $this->listSections()),
];
}
/**
* Return either an instance of the bundle creator or form specifications
*/
protected function getBundleCreator(string $section, ?array $data = []): array|CreatesBundle
{
return BundleCreator::getBundleCreatorFor($section, $data);
}
/**
* List available sections as options for a select input
*/

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands\Bundle;
use App\Classes\Bundle;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class Repair extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bundle:repair { path? : Specific bundle to repair }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Repair articles';
/**
* Execute the console command.
*/
public function handle()
{
$path = $this->argument('path') ?? '/';
$bundles = Bundle::findBundles(Storage::disk(env('CONTENT_DISK')), $path, true);
foreach ($bundles as $bundle) {
$this->output->write(sprintf('Repairing %s... ', $bundle->getPath()));
$bundle->repair();
$this->info('OK');
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands\Bundle;
use App\Classes\Bundle;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class Upgrade extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bundle:upgrade { path? : Specific bundle to upgrade }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Upgrade bundles from previous CMS version';
/**
* Execute the console command.
*/
public function handle()
{
$disk = Storage::disk(env('CONTENT_DISK'));
$path = $this->argument('path') ?? '/';
$bundles = Bundle::findBundles($disk, $path, true);
dd('Nothing to do');
foreach ($bundles as $bundle) {
$this->output->write(sprintf('Upgrading %s... ', $bundle->getPath()));
$this->info('OK');
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands\Markdown;
use App\Services\Markdown\Linter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class Lint extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'markdown:lint { path : Path to the file to be formatted }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Format markdown';
/**
* Execute the console command.
*/
public function handle()
{
Log::debug(sprintf('Linting %s', $this->argument('path')));
$content = Storage::disk(env('CONTENT_DISK'))->get($this->argument('path'));
$lint = new Linter($content);
$result = $lint->format();
if ($result !== $content) {
Storage::disk(env('CONTENT_DISK'))->put($this->argument('path'), $result);
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Contracts;
interface Bundles
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class BundleRendererCannotBeFound extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidInternalLink extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class PartnerCannotBeFound extends Exception
{
//
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\ImageFilters;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
class Article implements ModifierInterface
{
public function apply(ImageInterface $image): ImageInterface
{
$image->scaleDown(width: 800, height: 600);
return $image;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\ImageFilters;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
class Gallery implements ModifierInterface
{
public function apply(ImageInterface $image): ImageInterface
{
$image->scaleDown(height: 300);
return $image;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\ImageFilters;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
class ListItem implements ModifierInterface
{
public function apply(ImageInterface $image): ImageInterface
{
$image->coverDown(720, 400);
return $image;
}
}

View File

@ -2,16 +2,16 @@
namespace App\Providers;
use App\Services\BundleCreator\BundleCreatorFactory;
use App\Services\BundleCreators\BundleCreatorFactory;
use Illuminate\Support\ServiceProvider;
class BundleCreatorServiceProvider extends ServiceProvider
{
protected array $bundleCreators = [
\App\Services\BundleCreator\Creators\BlogBundleCreator::class,
\App\Services\BundleCreator\Creators\CollectibleBundleCreator::class,
\App\Services\BundleCreator\Creators\CriticBundleCreator::class,
\App\Services\BundleCreator\Creators\LinkBundleCreator::class,
\App\Services\BundleCreators\Creators\BlogBundleCreator::class,
\App\Services\BundleCreators\Creators\CollectibleBundleCreator::class,
\App\Services\BundleCreators\Creators\CriticBundleCreator::class,
\App\Services\BundleCreators\Creators\LinkBundleCreator::class,
];
/**

View File

@ -0,0 +1,37 @@
<?php
namespace App\Providers;
use App\Services\BundleRenderers\BundleRendererFactory;
use Illuminate\Support\ServiceProvider;
class BundleRendererServiceProvider extends ServiceProvider
{
protected array $bundleRenderers = [
\App\Services\BundleRenderers\Renderers\BlogRenderer::class,
];
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton('bundleRenderer.factory', function ($app) {
$factory = new BundleRendererFactory();
foreach ($this->bundleRenderers as $class) {
$factory->registerBundleRenderer($class);
}
return $factory;
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Providers;
use App\Services\Partners\PartnersFactory;
use Illuminate\Support\ServiceProvider;
class PartnersServiceProvider extends ServiceProvider
{
protected array $partners = [
\App\Services\Partners\Partners\Amazon::class,
\App\Services\Partners\Partners\Omlet::class,
\App\Services\Partners\Partners\Lego::class,
];
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton('partners.factory', function ($app) {
$factory = new PartnersFactory();
foreach ($this->partners as $class) {
$factory->registerPartner($class);
}
return $factory;
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View File

@ -31,37 +31,9 @@ class Browser
private $description = null;
/**
* Create the RemoteWebDriver instance.
*/
protected function driver(): RemoteWebDriver
public function __construct(public string $url, public ?string $optionsSet = 'normal')
{
if (!isset($this->driver)) {
$options = $this->getDriverOptions();
$this->driver = RemoteWebDriver::create(
$_ENV['DUSK_DRIVER_URL'],
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
return $this->driver;
}
/**
* Return driver options
*/
protected function getDriverOptions()
{
$options = (new ChromeOptions)
->addArguments(
collect(config(sprintf('browser.%s', $this->optionsSet)))
->all()
);
return $options;
}
public function name()
@ -98,11 +70,6 @@ public function getDescription()
return $this->description;
}
public function __construct(public string $url, public ?string $optionsSet = 'normal')
{
}
/**
* Navigate to specified URL
*/
@ -145,6 +112,39 @@ public function go()
}
}
/**
* Create the RemoteWebDriver instance.
*/
protected function driver(): RemoteWebDriver
{
if (!isset($this->driver)) {
$options = $this->getDriverOptions();
$this->driver = RemoteWebDriver::create(
$_ENV['DUSK_DRIVER_URL'],
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
return $this->driver;
}
/**
* Return driver options
*/
protected function getDriverOptions()
{
$options = (new ChromeOptions)
->addArguments(
collect(config(sprintf('browser.%s', $this->optionsSet)))
->all()
);
return $options;
}
private function handleNavigation(DuskBrowser $browser)
{
$browser->resize(1920, 1080)

View File

@ -1,6 +1,6 @@
<?php
namespace App\Services\BundleCreator;
namespace App\Services\BundleCreators;
use App\Exceptions\BundleCreatorCannotBeFound;
use Illuminate\Filesystem\FilesystemManager;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Services\BundleCreator\Contracts;
namespace App\Services\BundleCreators\Contracts;
use Illuminate\Filesystem\FilesystemAdapter;

View File

@ -1,9 +1,9 @@
<?php
namespace App\Services\BundleCreator\Creators;
namespace App\Services\BundleCreators\Creators;
use App\Exceptions\NotImplemented;
use App\Services\BundleCreator\Contracts\CreatesBundle;
use App\Services\BundleCreators\Contracts\CreatesBundle;
use Illuminate\Filesystem\FilesystemAdapter;
abstract class BaseBundleCreator implements CreatesBundle

View File

@ -1,6 +1,6 @@
<?php
namespace App\Services\BundleCreator\Creators;
namespace App\Services\BundleCreators\Creators;
use App\Classes\Bundle;
use App\Exceptions\BundleAlreadyExists;
@ -16,7 +16,6 @@ class BlogBundleCreator extends BaseBundleCreator
public function __construct(protected ?array $data, protected FilesystemAdapter $disk)
{
}
/**

View File

@ -1,24 +1,23 @@
<?php
namespace App\Services\BundleCreator\Creators;
namespace App\Services\BundleCreators\Creators;
use App\Classes\Bundle;
use App\Services\BundleCreator\Creators\CollectibleCreators\LegoBundleCreator;
use App\Services\BundleCreators\Creators\CollectibleCreators\LegoBundleCreator;
use Illuminate\Filesystem\FilesystemAdapter;
use function Laravel\Prompts\select;
class CollectibleBundleCreator extends BaseBundleCreator
{
private static string $section = 'collections';
public static $bundleCreators = [
LegoBundleCreator::class,
];
private static string $section = 'collections';
public function __construct(protected ?array $data, protected FilesystemAdapter $disk)
{
}
/**
@ -45,6 +44,15 @@ public function formSpecs(): ?array
return $specs;
}
/**
* Return a boolean value indicating if this creator in particular can
* create bundles for specified section
*/
public static function handles(string $section, ?array $data = []): bool
{
return $section === static::$section;
}
private function listBrands()
{
$bundles = Bundle::findBundles($this->disk, static::$section);
@ -58,13 +66,4 @@ private function listBrands()
return $brands;
}
/**
* Return a boolean value indicating if this creator in particular can
* create bundles for specified section
*/
public static function handles(string $section, ?array $data = []): bool
{
return $section === static::$section;
}
}

View File

@ -1,11 +1,11 @@
<?php
namespace App\Services\BundleCreator\Creators\CollectibleCreators;
namespace App\Services\BundleCreators\Creators\CollectibleCreators;
use App\Classes\AttachmentsManager;
use App\Classes\Bundle;
use App\Exceptions\BundleAlreadyExists;
use App\Services\BundleCreator\Creators\BaseBundleCreator;
use App\Services\BundleCreators\Creators\BaseBundleCreator;
use App\Services\Rebrickable\RebrickableClient;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Str;
@ -21,7 +21,6 @@ class LegoBundleCreator extends BaseBundleCreator
public function __construct(protected ?array $data, protected FilesystemAdapter $disk)
{
}
/**

View File

@ -1,6 +1,6 @@
<?php
namespace App\Services\BundleCreator\Creators;
namespace App\Services\BundleCreators\Creators;
use App\Classes\Bundle;
use App\Exceptions\BundleAlreadyExists;
@ -123,6 +123,15 @@ public function formSpecs(): ?array
return $specs;
}
/**
* Return a boolean value indicating if this creator in particular can
* create bundles for specified section
*/
public static function handles(string $section, ?array $data = []): bool
{
return $section === static::$section;
}
private function listKinds()
{
$bundles = Bundle::findBundles($this->disk, static::$section);
@ -136,13 +145,4 @@ private function listKinds()
return $kinds;
}
/**
* Return a boolean value indicating if this creator in particular can
* create bundles for specified section
*/
public static function handles(string $section, ?array $data = []): bool
{
return $section === static::$section;
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Services\BundleCreator\Creators;
namespace App\Services\BundleCreators\Creators;
use App\Classes\AttachmentsManager;
use App\Classes\Bundle;

View File

@ -0,0 +1,13 @@
<?php
namespace App\Services\BundleCreators\Facades;
use Illuminate\Support\Facades\Facade;
class BundleCreator extends Facade
{
protected static function getFacadeAccessor()
{
return 'bundleCreator.factory';
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Services\BundleRenderers;
use App\Classes\Bundle;
use App\Exceptions\BundleRendererCannotBeFound;
class BundleRendererFactory
{
/**
* Registered bundle renderers
*/
protected static $bundleRenderers = [];
/**
* Return a bundle renderer instance for specified bundle, if available.
*/
public function getBundleRendererFor(Bundle $bundle)
{
foreach (self::$bundleRenderers as $bundleRenderer) {
if ($bundleRenderer::handles($bundle)) {
return $bundleRenderer::make($bundle);
}
}
throw new BundleRendererCannotBeFound();
}
/**
* Register a bundle renderer
*/
public static function registerBundleRenderer($bundleRenderer)
{
if (!in_array($bundleRenderer, self::$bundleRenderers)) {
self::$bundleRenderers[] = $bundleRenderer;
}
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Services\BundleRenderers\Contracts;
use App\Classes\Bundle;
interface RendersBundle
{
/**
* Renders a complete HTML view of the bundle
*/
public function render();
/**
* Return a boolean value indicating if this creator in particular can
* create bundles for specified section
*/
public static function handles(Bundle $bundle): bool;
/**
* Return an instance of the creator, using specified data as input
*/
public static function make(Bundle $bundle): RendersBundle;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Services\BundleRenderers\Facades;
use Illuminate\Support\Facades\Facade;
class BundleRenderer extends Facade
{
protected static function getFacadeAccessor()
{
return 'bundleRenderer.factory';
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Services\BundleRenderers\Renderers;
use App\Classes\Bundle;
use App\Services\BundleRenderers\Contracts\RendersBundle;
abstract class BaseRenderer implements RendersBundle
{
public function __construct(protected Bundle $bundle)
{
$bundle->load();
}
/**
* Return an instance of the creator, using specified data as input
*/
public static function make(Bundle $bundle): RendersBundle
{
return new static($bundle);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Services\BundleRenderers\Renderers;
use App\Classes\Bundle;
use Carbon\Carbon;
class BlogRenderer extends BaseRenderer
{
/**
* Renders a complete HTML view of the bundle
*/
public function render()
{
return view('article', [
'articleTitle' => $this->bundle->metadata()->get('title'),
'date' => Carbon::parse($this->bundle->metadata()->get('date'))->format('d/m/Y'),
'body' => $this->bundle->markdown()->render(),
]);
}
/**
* Return a boolean value indicating if this creator in particular can
* create bundles for specified section
*/
public static function handles(Bundle $bundle): bool
{
$parts = preg_split('#/#', $bundle->getPath(), -1, PREG_SPLIT_NO_EMPTY);
return $parts && $parts[0] === 'blog' && count($parts) === 5;
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Services\Markdown;
use App\Services\Markdown\Renderers\LinkRenderer;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
use League\CommonMark\Extension\Footnote\FootnoteExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
use Spatie\CommonMarkShikiHighlighter\HighlightCodeExtension;
/**
* Formats markdown content using a variety of CommonMark extensions
* and custom renderers, also incorporating Blade template rendering.
*/
class Formatter
{
/**
* Constructor.
*
* @param string $source The markdown source content.
*/
public function __construct(protected string $source)
{
}
/**
* Renders the markdown content with all enabled extensions and custom processing.
*
* @return string The rendered HTML content.
*/
public function render(): string
{
$hash = md5($this->source);
$cacheKey = sprintf('markdown_formatted_%s', $hash);
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
// First, process the source with Blade to handle any dynamic content
$result = Blade::render($this->source);
// Convert markdown to HTML using the configured environment and extensions
$converter = new MarkdownConverter($this->prepareEnvironment());
$converted = $converter->convert($result);
// Perform final adjustments like inserting non-breaking spaces
$result = $this->insertNonBreakingSpaces($converted->getContent());
Cache::put($cacheKey, $result, now()->addMonth());
return $result;
}
/**
* Prepares the markdown parser environment with extensions and custom renderers.
*
* @return Environment The configured markdown environment.
*/
protected function prepareEnvironment(): Environment
{
// Load markdown configuration and initialize the environment
$environment = new Environment(config('markdown'));
// Define all the extensions to be used
$extensions = [
new CommonMarkCoreExtension(),
new HighlightCodeExtension(theme: 'aurora-x'),
new FrontMatterExtension(),
new FootnoteExtension(),
new ExternalLinkExtension(),
new HeadingPermalinkExtension(),
new StrikethroughExtension(),
new TableExtension(),
new TableOfContentsExtension(),
new DisallowedRawHtmlExtension(),
new TaskListExtension(),
];
// Add each extension to the environment
foreach ($extensions as $extension) {
$environment->addExtension($extension);
}
// Use a custom renderer for links
$environment->addRenderer(Link::class, new LinkRenderer());
return $environment;
}
/**
* Inserts non-breaking spaces before certain punctuation marks in French typography.
*
* @param string $html The HTML content to process.
* @return string The processed HTML content with non-breaking spaces added.
*/
protected function insertNonBreakingSpaces(string $html): string
{
// Define patterns for spaces that should be replaced with non-breaking spaces
$patterns = [
'/ (\;)/',
'/ (\:)/',
'/ (\!)/',
'/ (\?)/',
];
// Corresponding replacements for each pattern
$replacements = array_fill(0, count($patterns), '&nbsp;$1');
// Perform the replacements and return the modified HTML
return preg_replace($patterns, $replacements, $html);
}
}

View File

@ -0,0 +1,315 @@
<?php
namespace App\Services\Markdown;
/**
* Linter class is responsible for formatting markdown content.
*/
class Linter
{
/**
* The markdown content to be formatted.
*
* @var string
*/
private $markdown;
/**
* Characters that mark the end of a sentence.
*
* @var array
*/
private $phraseEndingChars = ['.', '!', '?'];
/**
* Constructor takes markdown content and prepares it for formatting.
*
* @param string $markdown Markdown content to format.
*/
public function __construct(?string $markdown = '')
{
$this->markdown = mb_convert_encoding($markdown, 'UTF-8');
}
/**
* Format the markdown content by applying various formatting rules.
*
* @return string The formatted markdown.
*/
public function format(): string
{
if (empty($this->markdown)) {
return $this->markdown;
}
$blocks = $this->segmentMarkdown();
$processedBlocks = array_map(function ($block) {
$type = $this->determineBlockType($block);
return $this->formatBlock($block, $type);
}, $blocks);
return implode("\n\n", $processedBlocks);
}
/**
* Segment the markdown into blocks based on empty lines, respecting code blocks and multi-line HTML.
*
* @return array Array of blocks, each containing markdown content.
*/
private function segmentMarkdown(): array
{
$blocks = [];
$currentBlock = '';
$lines = explode("\n", $this->markdown);
$inCodeBlock = false;
foreach ($lines as $line) {
if (preg_match('/^```/', trim($line))) {
if ($inCodeBlock) {
// End of a code block
$currentBlock .= $line . "\n";
$blocks[] = $currentBlock;
$currentBlock = '';
$inCodeBlock = false;
} else {
// Start of a code block
if (!empty($currentBlock)) {
$blocks[] = $currentBlock;
$currentBlock = '';
}
$inCodeBlock = true;
$currentBlock .= $line . "\n";
}
} elseif ($inCodeBlock) {
// Inside a code block
$currentBlock .= $line . "\n";
} else {
// Normal line processing
if (trim($line) === '' && trim($currentBlock) !== '') {
$blocks[] = $currentBlock;
$currentBlock = '';
} else {
$currentBlock .= $line . "\n";
}
}
}
// Add the last block if not empty
if (!empty(trim($currentBlock))) {
$blocks[] = $currentBlock;
}
return $blocks;
}
/**
* Determine the type of a markdown block.
*
* @param string $block The markdown block to analyze.
* @return string The type of the block.
*/
private function determineBlockType(string $block): string
{
if (preg_match('/^\s*```/', trim($block))) {
return 'code';
}
if (preg_match('/^\s*<[^>]+>/', trim($block))) {
return 'html';
}
if (preg_match('/^\s*#/', trim($block))) {
return 'header';
}
if (preg_match('/^\s*\|/', trim($block))) {
return 'table';
}
if (preg_match('/^\s*>\s/', trim($block))) {
return 'blockquote';
}
if (
preg_match('/^\s*-\s/', trim($block))
|| preg_match('/^\s*\d+\.\s/', trim($block))
) {
return 'list';
}
if (preg_match('/^\s*\[\^[\w-]+\]:/', trim($block))) {
return 'footnote';
}
return 'paragraph'; // Default to paragraph if no other type matches
}
/**
* Apply formatting rules to a single markdown block based on its type.
*
* @param string $block The markdown block to format.
* @param string $type The type of the block.
* @return string The formatted block.
*/
private function formatBlock(string $block, string $type): string
{
$block = trim($block, "\n");
switch ($type) {
case 'code':
return $this->formatCodeBlock($block);
case 'html':
return $this->formatHtmlBlock($block);
case 'header':
return $this->formatHeaderBlock($block);
case 'table':
return $this->formatTableBlock($block);
case 'blockquote':
return $this->formatBlockquoteBlock($block);
case 'list':
return $this->formatListBlock($block);
case 'footnote':
return $this->formatFootnoteBlock($block);
default:
return $this->formatParagraphBlock($block);
}
}
private function formatCodeBlock(string $block): string
{
// Split the block into lines
$lines = explode("\n", $block);
// Clean the first line if it starts with ```
if (count($lines) > 0 && preg_match('/^```/', trim($lines[0]))) {
$lines[0] = preg_replace('/^(```\w*)\s*{.*?}$/', '$1', trim($lines[0]));
}
// Reassemble the block
return implode("\n", $lines);
}
private function formatHtmlBlock(string $block): string
{
// HTML-specific formatting
return $block;
}
private function formatFootnoteBlock(string $block): string
{
// HTML-specific formatting
return $block;
}
private function formatHeaderBlock(string $block): string
{
// Header-specific formatting
return $this->replaceUnderscoresWithAsterisks($block);
}
private function formatTableBlock(string $block): string
{
// HTML-specific formatting
return $block;
}
private function formatBlockquoteBlock(string $block): string
{
// Blockquote-specific formatting
return $block;
}
private function formatListBlock(string $block): string
{
// List-specific formatting
return $block;
}
/**
* Apply formatting rules to a paragraph block.
*
* @param string $block The paragraph block to format.
* @return string The formatted paragraph block.
*/
private function formatParagraphBlock(string $block): string
{
// Normalize three dots and variants to the ellipsis character
$block = preg_replace('/\.{3}(?!\.)/', '…', $block);
// Remove unnecessary new lines within the paragraph
$block = str_replace("\n", ' ', $block);
// Normalize spaces (replace multiple spaces with a single space)
$block = preg_replace('/\s+/', ' ', $block);
// Avoid adding space in markdown links by temporarily replacing them
preg_match_all('/\[[^\]]+\]\([^\)]+\)/', $block, $links);
foreach ($links[0] as $index => $link) {
$block = str_replace($link, "link_placeholder_{$index}", $block);
}
// Add space after punctuation
$block = preg_replace('/(\S)([.!?…])(\s|$)/', '$1$2 ', $block);
// Restore links
foreach ($links[0] as $index => $link) {
$block = str_replace("link_placeholder_{$index}", $link, $block);
}
$delimiter = sprintf('/(?<=[%s])\s+/u', implode('', array_map('preg_quote', $this->phraseEndingChars)));
$sentences = preg_split($delimiter, $block, -1, PREG_SPLIT_NO_EMPTY);
$sentences = array_map(function ($sentence) {
// Replace underscores by asterisks when they are used as pairs and not part of markdown links
$sentence = $this->replaceUnderscoresWithAsterisks($sentence);
return trim($sentence);
}, $sentences);
// Join sentences by new lines
$formattedParagraph = implode("\n", $sentences);
return $formattedParagraph;
}
/**
* Replace underscores with asterisks when used in pairs, not affecting markdown links.
*
* @param string $sentence The sentence to process.
* @return string The processed sentence.
*/
private function replaceUnderscoresWithAsterisks(string $sentence): string
{
// Temporarily remove Markdown links to avoid processing underscores within them
$patterns = [
'/\[[^\]]+\]\([^\)]+\)/', // Match links of the form [text](link)
'/<[^>]+>/', // Match links of the form <link>
'/\[\^[^\]]+\]/', // Match footnote references of the form [^footnote]
];
$links = [];
foreach ($patterns as $pattern) {
preg_match_all($pattern, $sentence, $matches);
foreach ($matches[0] as $index => $match) {
// Store the link with a unique placeholder
$placeholder = sprintf('link-placeholder-%d-%d', count($links), $index);
$links[$placeholder] = $match;
$sentence = str_replace($match, $placeholder, $sentence);
}
}
// Replace all non-link underscore pairs
$sentence = preg_replace_callback('/(_[^_]+_)/', function ($matches) {
// Replace underscores with asterisks, but keep the content
return str_replace('_', '*', $matches[0]);
}, $sentence);
// Restore the links
foreach ($links as $placeholder => $link) {
$sentence = str_replace($placeholder, $link, $sentence);
}
return $sentence;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Services\Markdown\Renderers;
use App\Classes\Link;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link as CommonMarkLink;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
class LinkRenderer implements NodeRendererInterface
{
/**
* Renders a link node into an HTML element.
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): HtmlElement
{
CommonMarkLink::assertInstanceOf($node);
$innerHtml = $childRenderer->renderNodes($node->children());
$url = $node->getUrl();
return (new Link($url, $innerHtml))->toHtmlElement();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Services\Partners\Contracts;
interface Partner
{
/**
* Return an affiliate link corresponding to original URL
*/
public function getAffiliateLink(): string;
/**
* Return a boolean value indicating if the partner can handle specified
* host extracted from a link
*/
public static function handles(string $host): bool;
/**
* Return an instance of a partner using specified url.
*/
public static function make(string $url): Partner;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Services\Partners\Facades;
use Illuminate\Support\Facades\Facade;
class Partner extends Facade
{
protected static function getFacadeAccessor()
{
return 'partners.factory';
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Services\Partners\Partners;
class Amazon extends BasePartner
{
protected static array $handledHosts = [
'amazon.fr',
'amazon.com',
];
/**
* Return an affiliate link corresponding to original URL
*/
public function getAffiliateLink(): string
{
return $this->getSimpleAffiliationLink('Amazon', 'tag', config('services.amazon.tracking_id'));
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Services\Partners\Partners;
use App\Services\Partners\Contracts\Partner;
use Exception;
use League\Uri\Uri;
abstract class BasePartner implements Partner
{
protected static array $handledHosts;
public function __construct(protected string $url)
{
}
/**
* Return an affiliate link corresponding to original URL
*/
abstract public function getAffiliateLink(): string;
/**
* Return a boolean value indicating if the partner can handle specified
* host extracted from a link
*/
public static function handles(string $host): bool
{
if (isset(static::$handledHosts)) {
return in_array($host, static::$handledHosts);
}
}
/**
* Return an instance of a partner using specified url.
*/
public static function make(string $url): Partner
{
return new static($url);
}
/**
* Return a simple, tag-based in-url tracking id affiliated link
*/
protected function getSimpleAffiliationLink(string $serviceName, string $urlParam, string $trackingId): string
{
if (empty($trackingId)) {
throw new Exception(sprintf('Empty %s tracking id', $serviceName));
}
if (strpos($this->url, $trackingId) !== false) {
return $this->url;
}
$uri = Uri::new($this->url)->withQuery(sprintf('%s=%s', $urlParam, $trackingId));
return (string) $uri;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Services\Partners\Partners;
class Lego extends BasePartner
{
protected static array $handledHosts = [
'www.lego.com',
];
/**
* Return an affiliate link corresponding to original URL
*/
public function getAffiliateLink(): string
{
$config = config('services.lego');
$platform = $config['platform'];
$instance = new $platform($config);
$url = $instance->getAffiliateLink('lego', $this->url);
return $url;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Services\Partners\Partners;
class Omlet extends BasePartner
{
protected static array $handledHosts = [
'www.omlet.fr',
];
/**
* Return an affiliate link corresponding to original URL
*/
public function getAffiliateLink(): string
{
return $this->getSimpleAffiliationLink('Omlet', 'aid', config('services.omlet.tracking_id'));
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Services\Partners;
use App\Exceptions\PartnerCannotBeFound;
class PartnersFactory
{
/**
* Registered partners
*/
protected static $partners = [];
/**
* Return a partner instance for specified url, if available
*/
public function getPartner(string $url)
{
$host = parse_url($url, PHP_URL_HOST);
foreach (self::$partners as $partner) {
if ($partner::handles($host)) {
return $partner::make($url);
}
}
throw new PartnerCannotBeFound();
}
/**
* Register a partner
*/
public static function registerPartner($partner)
{
if (!in_array($partner, self::$partners)) {
self::$partners[] = $partner;
}
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace App\Services\Partners\Platforms;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class Rakuten
{
public function __construct(protected array $partnerConfig)
{
}
/**
* Generates an affiliate link for a given partner and URL.
*
* @param string $partnerName The name of the partner.
* @param string $originalUrl The original URL to be converted into an affiliate link.
* @return string|null The generated affiliate link or null in case of failure.
*/
public function getAffiliateLink(string $partnerName, string $originalUrl): ?string
{
// Creating a unique cache key based on partner name and original URL
$cacheKey = sprintf('rakuten_%s_%s', $partnerName, base64_encode($originalUrl));
// Attempting to retrieve the affiliate link from cache to avoid unnecessary API calls
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
// Getting access token for the API
$accessToken = $this->getAccessToken();
if (empty($accessToken)) {
// Return null if we cannot obtain an access token
return null;
}
try {
// Making a POST request to the Rakuten API to generate the affiliate link
$response = Http::throw()
->withHeaders([
'Authorization' => sprintf('Bearer %s', $accessToken),
])
->post('https://api.linksynergy.com/v1/links/deep_links', [
'url' => $originalUrl,
'advertiser_id' => $this->partnerConfig['advertiser_id'],
]);
} catch (Exception $ex) {
// Logging the exception for debugging purposes
report($ex);
return null;
}
// Verifying the API response and extracting the affiliate link
if (!$response->ok() || empty($response->json()['advertiser']['deep_link']['deep_link_url'])) {
return null;
}
$url = $response->json()['advertiser']['deep_link']['deep_link_url'];
// Storing the generated affiliate link in cache for 24 hours to improve performance
Cache::put($cacheKey, $url, 60 * 60 * 24);
return $url;
}
/**
* Retrieves the access token required for API calls, with caching to optimize the process.
*
* @return string|null The access token or null if retrieval fails.
*/
private function getAccessToken(): ?string
{
$cacheKey = 'rakuten.access_token';
$token = Cache::get($cacheKey);
// Return cached token if available
if (!empty($token)) {
return $token;
}
try {
// Requesting a new access token from the Rakuten API
$response = Http::throw()
->asForm()
->withHeaders([
'Authorization' => sprintf('Bearer %s', config('services.rakuten.token_key')),
])
->post('https://api.linksynergy.com/token', [
'scope' => config('services.rakuten.sid'),
]);
} catch (Exception $ex) {
// Logging the exception for debugging purposes
report($ex);
return null;
}
// Verifying the API response and extracting the access token and its expiration time
if (!$response->ok() || empty($response->json()['access_token']) || empty($response->json()['expires_in'])) {
return null;
}
$accessToken = $response->json()['access_token'];
$expire = $response->json()['expires_in'];
// Caching the access token for the duration of its validity to reduce API calls
Cache::put($cacheKey, $accessToken, $expire);
return $accessToken;
}
}

View File

@ -20,6 +20,11 @@ class WikidataExtractor
protected $entities;
public function __construct(protected array $exclusions, protected array $inclusions)
{
}
public function included()
{
return $this->included;
@ -40,11 +45,6 @@ public function everythingElse()
return $this->everythingElse;
}
public function __construct(protected array $exclusions, protected array $inclusions)
{
}
/**
* Split data from specified array in three arrays containing explicitely
* included properties, explicitely excluded properties and unused

View File

@ -0,0 +1,66 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\Component;
abstract class BaseMediaComponent extends Component
{
protected string $view;
/**
* Create a new component instance.
*/
public function __construct(protected array $data, protected ?array $variant = [])
{
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
$originalUrl = $this->copyFile($this->data['filename']);
$variantUrl = $this->variant ? $this->copyFile($this->variant['filename']) : null;
return view($this->view, [
'originalUrl' => $originalUrl,
'variantUrl' => $variantUrl,
'originalData' => $this->data,
'variantData' => $this->variant,
]);
}
/**
* Copy a file to public disk and return relative url to use
*/
protected function copyFile(string $path)
{
$content = Storage::disk(env('CONTENT_DISK'))->get($path);
$md5 = md5($content);
$targetPath = $this->buildTargetFilePath($path, $md5);
if (!Storage::disk('public')->exists($targetPath)) {
Storage::disk('public')->put($targetPath, $content);
}
return Str::remove(env('APP_URL'), Storage::disk('public')->url($targetPath));
}
/**
* Return a path for the target file
*/
protected function buildTargetFilePath(string $originalPath, string $md5)
{
$extension = pathinfo($originalPath, PATHINFO_EXTENSION);
$pathParts = str_split($md5, 4);
$pathParts[] = sprintf('%s.%s', $md5, $extension);
$targetPath = implode('/', $pathParts);
return $targetPath;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\View\Components;
class Image extends BaseMediaComponent
{
protected string $view = 'components.image';
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\View\Components;
class Sound extends BaseMediaComponent
{
protected string $view = 'components.sound';
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\View\Components;
class Video extends BaseMediaComponent
{
protected string $view = 'components.video';
}

View File

@ -3,6 +3,8 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\BundleCreatorServiceProvider::class,
App\Providers\BundleRendererServiceProvider::class,
App\Providers\PartnersServiceProvider::class,
App\Providers\RebrickableServiceProvider::class,
App\Providers\WikidataServiceProvider::class,
];

View File

@ -8,7 +8,9 @@
"php": "^8.2",
"intervention/image-laravel": "^1.2",
"laravel/framework": "^11.0",
"laravel/tinker": "^2.9"
"laravel/tinker": "^2.9",
"league/uri": "^7.4",
"spatie/commonmark-shiki-highlighter": "^2.4"
},
"require-dev": {
"fakerphp/faker": "^1.23",

300
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9e55fbb085ad9b9101e865d303b71804",
"content-hash": "165079fe9575e489811c4f0c5da66a22",
"packages": [
{
"name": "brick/math",
@ -2039,6 +2039,180 @@
],
"time": "2024-01-28T23:22:08+00:00"
},
{
"name": "league/uri",
"version": "7.4.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
"reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4",
"reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4",
"shasum": ""
},
"require": {
"league/uri-interfaces": "^7.3",
"php": "^8.1"
},
"conflict": {
"league/uri-schemes": "^1.0"
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-fileinfo": "to create Data URI from file contennts",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
"league/uri-components": "Needed to easily manipulate URI objects components",
"php-64bit": "to improve IPV4 host parsing",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://nyamsprod.com"
}
],
"description": "URI manipulation library",
"homepage": "https://uri.thephpleague.com",
"keywords": [
"data-uri",
"file-uri",
"ftp",
"hostname",
"http",
"https",
"middleware",
"parse_str",
"parse_url",
"psr-7",
"query-string",
"querystring",
"rfc3986",
"rfc3987",
"rfc6570",
"uri",
"uri-template",
"url",
"ws"
],
"support": {
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri/tree/7.4.1"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"time": "2024-03-23T07:42:40+00:00"
},
{
"name": "league/uri-interfaces",
"version": "7.4.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
"reference": "8d43ef5c841032c87e2de015972c06f3865ef718"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718",
"reference": "8d43ef5c841032c87e2de015972c06f3865ef718",
"shasum": ""
},
"require": {
"ext-filter": "*",
"php": "^8.1",
"psr/http-factory": "^1",
"psr/http-message": "^1.1 || ^2.0"
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"php-64bit": "to improve IPV4 host parsing",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://nyamsprod.com"
}
],
"description": "Common interfaces and classes for URI representation and interaction",
"homepage": "https://uri.thephpleague.com",
"keywords": [
"data-uri",
"file-uri",
"ftp",
"hostname",
"http",
"https",
"parse_str",
"parse_url",
"psr-7",
"query-string",
"querystring",
"rfc3986",
"rfc3987",
"rfc6570",
"uri",
"url",
"ws"
],
"support": {
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"time": "2024-03-23T07:42:40+00:00"
},
{
"name": "monolog/monolog",
"version": "3.6.0",
@ -3331,6 +3505,130 @@
],
"time": "2023-11-08T05:53:05+00:00"
},
{
"name": "spatie/commonmark-shiki-highlighter",
"version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/commonmark-shiki-highlighter.git",
"reference": "3dd337649d87a9b264838320a07a89d22e75d41b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/commonmark-shiki-highlighter/zipball/3dd337649d87a9b264838320a07a89d22e75d41b",
"reference": "3dd337649d87a9b264838320a07a89d22e75d41b",
"shasum": ""
},
"require": {
"league/commonmark": "^2.4.2",
"php": "^8.0",
"spatie/shiki-php": "^2.0",
"symfony/process": "^6.0|^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.19|^v3.49.0",
"phpunit/phpunit": "^9.5",
"spatie/phpunit-snapshot-assertions": "^4.2.7",
"spatie/ray": "^1.28"
},
"type": "commonmark-extension",
"autoload": {
"psr-4": {
"Spatie\\CommonMarkShikiHighlighter\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Highlight code blocks with league/commonmark and Shiki",
"homepage": "https://github.com/spatie/commonmark-shiki-highlighter",
"keywords": [
"commonmark-shiki-highlighter",
"spatie"
],
"support": {
"source": "https://github.com/spatie/commonmark-shiki-highlighter/tree/2.4.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2024-04-11T12:12:10+00:00"
},
{
"name": "spatie/shiki-php",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/shiki-php.git",
"reference": "b4bd54222c40b44800aabce0a4382e0c463b5901"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/shiki-php/zipball/b4bd54222c40b44800aabce0a4382e0c463b5901",
"reference": "b4bd54222c40b44800aabce0a4382e0c463b5901",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.4|^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^v3.0",
"pestphp/pest": "^1.8",
"phpunit/phpunit": "^9.5",
"spatie/pest-plugin-snapshots": "^1.1",
"spatie/ray": "^1.10"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\ShikiPhp\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rias Van der Veken",
"email": "rias@spatie.be",
"role": "Developer"
},
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Highlight code using Shiki in PHP",
"homepage": "https://github.com/spatie/shiki-php",
"keywords": [
"shiki",
"spatie"
],
"support": {
"source": "https://github.com/spatie/shiki-php/tree/2.0.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2024-02-19T09:00:59+00:00"
},
{
"name": "symfony/clock",
"version": "v7.0.5",

7
config/imagefilters.php Normal file
View File

@ -0,0 +1,7 @@
<?php
return [
'article' => \App\ImageFilters\Article::class,
'listitem' => \App\ImageFilters\ListItem::class,
'gallery' => \App\ImageFilters\Gallery::class,
];

79
config/markdown.php Normal file
View File

@ -0,0 +1,79 @@
<?php
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkRenderer;
return [
'renderer' => [
'block_separator' => "\n",
'inner_separator' => "\n",
'soft_break' => "\n",
],
'commonmark' => [
'enable_em' => true,
'enable_strong' => true,
'use_asterisk' => true,
'use_underscore' => true,
'unordered_list_markers' => ['-', '*', '+'],
],
'html_input' => 'allow',
'allow_unsafe_links' => false,
'max_nesting_level' => PHP_INT_MAX,
'slug_normalizer' => [
'max_length' => 255,
],
'disallowed_raw_html' => [
'disallowed_tags' => ['title'],
],
'external_link' => [
'internal_hosts' => parse_url(env('APP_URL'), PHP_URL_HOST),
'open_in_new_window' => false,
'html_class' => 'external',
'nofollow' => 'external',
'noopener' => 'external',
'noreferrer' => 'external',
],
'footnote' => [
'backref_class' => 'footnote-backref',
'backref_symbol' => '↩',
'container_add_hr' => true,
'container_class' => 'footnotes',
'ref_class' => 'footnote-ref',
'ref_id_prefix' => 'fnref:',
'footnote_class' => 'footnote',
'footnote_id_prefix' => 'fn:',
],
'heading_permalink' => [
'html_class' => 'heading-permalink',
'id_prefix' => '',
'apply_id_to_heading' => false,
'heading_class' => '',
'fragment_prefix' => '',
'insert' => 'before',
'min_heading_level' => 2,
'max_heading_level' => 6,
'title' => 'Lien direct',
'symbol' => HeadingPermalinkRenderer::DEFAULT_SYMBOL,
'aria_hidden' => true,
],
'table' => [
'wrap' => [
'enabled' => false,
'tag' => 'div',
'attributes' => [],
],
'alignment_attributes' => [
'left' => ['align' => 'left'],
'center' => ['align' => 'center'],
'right' => ['align' => 'right'],
],
],
'table_of_contents' => [
'html_class' => 'table-of-contents',
'position' => 'top',
'style' => 'ordered',
'min_heading_level' => 1,
'max_heading_level' => 6,
'normalize' => 'relative',
'placeholder' => null,
],
];

View File

@ -2,18 +2,6 @@
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'rebrickable' => [
'baseUrl' => 'https://rebrickable.com/api/v3/lego',
'key' => env('REBRICKABLE_API_KEY'),
@ -23,21 +11,26 @@
'baseApiUrl' => 'https://www.wikidata.org/w/api.php',
],
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
'amazon' => [
'trackingId' => env('AMAZON_TRACKING_ID'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'omlet' => [
'trackingId' => env('OMLET_TRACKING_ID'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
'lego' => [
'advertiser_id' => 50641,
'platform' => \App\Services\Partners\Platforms\Rakuten::class,
],
'rakuten' => [
'sid' => env('RAKUTEN_SID'),
'app_name' => env('RAKUTEN_APP_NAME'),
'client_id' => env('RAKUTEN_CLIENT_ID'),
'secret' => env('RAKUTEN_SECRET'),
// base64 client_id:secret
'token_key' => env('RAKUTEN_TOKEN_KEY'),
],
];

View File

@ -1,11 +1,20 @@
services:
laravel.test:
base_laravel.test:
build:
context: ./vendor/laravel/sail/runtimes/8.3
context: ./vendor/laravel/sail/runtimes/${PHP_VERSION}
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
image: sail-8.3/app
image: sail-${PHP_VERSION}/app
command: /bin/true
laravel.test:
build:
context: ./docker
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
PHP_VERSION: ${PHP_VERSION}
image: custom-sail-${PHP_VERSION}/app
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
@ -27,6 +36,7 @@ services:
- redis
- memcached
- selenium
- base_laravel.test
pgsql:
image: 'postgres:15'
ports:

2
docker/Dockerfile Normal file
View File

@ -0,0 +1,2 @@
ARG PHP_VERSION=8.0
FROM sail-${PHP_VERSION}/app as base

961
package-lock.json generated Normal file
View File

@ -0,0 +1,961 @@
{
"name": "html",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@fontsource/quicksand": "^5.0.18",
"shiki": "^1.3.0"
},
"devDependencies": {
"axios": "^1.6.4",
"laravel-vite-plugin": "^1.0",
"vite": "^5.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@fontsource/quicksand": {
"version": "5.0.18",
"resolved": "https://registry.npmjs.org/@fontsource/quicksand/-/quicksand-5.0.18.tgz",
"integrity": "sha512-Dvv89zZG0vjZzmYwkUJTbFNV/uhNikqB9Qgl4KMtFw/gTaS5nA7vLIOjfEz/gjWlKPsYnuSIvVArLR0/xptxlA=="
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
"integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
"integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
"integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
"integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
"integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
"integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
"integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
"integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
"integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
"integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
"integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
"integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
"integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
"integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
"integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
"integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@shikijs/core": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.3.0.tgz",
"integrity": "sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA=="
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/laravel-vite-plugin": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.2.tgz",
"integrity": "sha512-Mcclml10khYzBVxDwJro8wnVDwD4i7XOSEMACQNnarvTnHjrjXLLL+B/Snif2wYAyElsOqagJZ7VAinb/2vF5g==",
"dev": true,
"dependencies": {
"picocolors": "^1.0.0",
"vite-plugin-full-reload": "^1.1.0"
},
"bin": {
"clean-orphaned-assets": "bin/clean.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/rollup": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
"integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.5"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.14.3",
"@rollup/rollup-android-arm64": "4.14.3",
"@rollup/rollup-darwin-arm64": "4.14.3",
"@rollup/rollup-darwin-x64": "4.14.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
"@rollup/rollup-linux-arm-musleabihf": "4.14.3",
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
"@rollup/rollup-linux-arm64-musl": "4.14.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
"@rollup/rollup-linux-riscv64-gnu": "4.14.3",
"@rollup/rollup-linux-s390x-gnu": "4.14.3",
"@rollup/rollup-linux-x64-gnu": "4.14.3",
"@rollup/rollup-linux-x64-musl": "4.14.3",
"@rollup/rollup-win32-arm64-msvc": "4.14.3",
"@rollup/rollup-win32-ia32-msvc": "4.14.3",
"@rollup/rollup-win32-x64-msvc": "4.14.3",
"fsevents": "~2.3.2"
}
},
"node_modules/shiki": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.3.0.tgz",
"integrity": "sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==",
"dependencies": {
"@shikijs/core": "1.3.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vite": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz",
"integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==",
"dev": true,
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.38",
"rollup": "^4.13.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
},
"node_modules/vite-plugin-full-reload": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.1.0.tgz",
"integrity": "sha512-3cObNDzX6DdfhD9E7kf6w2mNunFpD7drxyNgHLw+XwIYAgb+Xt16SEXo0Up4VH+TMf3n+DSVJZtW2POBGcBYAA==",
"dev": true,
"dependencies": {
"picocolors": "^1.0.0",
"picomatch": "^2.3.1"
}
}
}
}

View File

@ -9,5 +9,9 @@
"axios": "^1.6.4",
"laravel-vite-plugin": "^1.0",
"vite": "^5.0"
},
"dependencies": {
"@fontsource/quicksand": "^5.0.18",
"shiki": "^1.3.0"
}
}

View File

@ -7,6 +7,30 @@
"not_operator_with_successor_space": false,
"concat_space": {
"spacing": "one"
},
"visibility_required": {
"elements": [
"method",
"property"
]
},
"ordered_class_elements": {
"order": [
"use_trait",
"constant_public",
"constant_protected",
"constant_private",
"property_public",
"property_protected",
"property_private",
"construct",
"destruct",
"magic",
"phpunit",
"method_public",
"method_protected",
"method_private"
]
}
}
}

View File

@ -0,0 +1,2 @@
* {
}

View File

@ -1 +0,0 @@
import './bootstrap';

View File

@ -1,4 +0,0 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@vite('resources/css/app.css')
</head>
<body>
<h1>{!! $articleTitle !!}</h1>
<div class="article">{!! $body !!}</div>
</body>
</html>

View File

@ -0,0 +1,19 @@
<figure>
<a href="{{ $originalUrl }}" title="Voir l'image à taille réelle">
<img src="{{ $variantUrl }}" @if (!empty($class)) class="{{ $class }}" @endif />
</a>
@if (!empty($originalData['title']) || !empty($originalData['prompt']) || !empty($originalData['attribution']))
<figcaption>
@if (!empty($originalData['prompt']))
{!! (new \App\Services\Markdown\Formatter($originalData['prompt']))->render() !!}
@endif
@if (!empty($originalData['title']))
{!! (new \App\Services\Markdown\Formatter($originalData['title']))->render() !!}
@endif
@if (!empty($originalData['attribution']))
{!! (new \App\Services\Markdown\Formatter($originalData['attribution']))->render() !!}
@endif
</figcaption>
@endif
</figure>

View File

@ -0,0 +1,6 @@
<figure>
<audio controls preload="{{ $preload }}">
<source src="{{ $sndUrl }}" type="{{ $type }}">
</audio>
<figcaption>{!! $caption !!}</figcaption>
</figure>

View File

@ -0,0 +1,6 @@
<figure>
<video width="{{ $width }}" height="{{ $height }}" controls>
<source src="{{ $videoUrl }}" type="{{ $type }}">
Quelque chose ne fonctionne pas !
</video>
</figure>

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,11 @@
<?php
use App\Classes\Bundle;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
Route::get('/', function () {
return view('welcome');
});
Route::get('{any}', function () {
$bundle = new Bundle(request()->path(), Storage::disk('content'));
return $bundle->render();
})->where('any', '.*');

View File

@ -4,7 +4,7 @@ import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
input: ['resources/css/app.css'],
refresh: true,
}),
],