Basic - although quite huge - infrastructure for rendering bundles
This commit is contained in:
parent
5b19490296
commit
58e6c07ab6
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface Bundles
|
||||
{
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class BundleRendererCannotBeFound extends Exception
|
||||
{
|
||||
//
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidInternalLink extends Exception
|
||||
{
|
||||
//
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PartnerCannotBeFound extends Exception
|
||||
{
|
||||
//
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BundleCreator;
|
||||
namespace App\Services\BundleCreators;
|
||||
|
||||
use App\Exceptions\BundleCreatorCannotBeFound;
|
||||
use Illuminate\Filesystem\FilesystemManager;
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BundleCreator\Contracts;
|
||||
namespace App\Services\BundleCreators\Contracts;
|
||||
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BundleCreator\Creators;
|
||||
namespace App\Services\BundleCreators\Creators;
|
||||
|
||||
use App\Classes\AttachmentsManager;
|
||||
use App\Classes\Bundle;
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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), ' $1');
|
||||
|
||||
// Perform the replacements and return the modified HTML
|
||||
return preg_replace($patterns, $replacements, $html);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
class Image extends BaseMediaComponent
|
||||
{
|
||||
protected string $view = 'components.image';
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
class Sound extends BaseMediaComponent
|
||||
{
|
||||
protected string $view = 'components.sound';
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
class Video extends BaseMediaComponent
|
||||
{
|
||||
protected string $view = 'components.video';
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'article' => \App\ImageFilters\Article::class,
|
||||
'listitem' => \App\ImageFilters\ListItem::class,
|
||||
'gallery' => \App\ImageFilters\Gallery::class,
|
||||
];
|
|
@ -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,
|
||||
],
|
||||
];
|
|
@ -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'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ARG PHP_VERSION=8.0
|
||||
FROM sail-${PHP_VERSION}/app as base
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
24
pint.json
24
pint.json
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
* {
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
import './bootstrap';
|
|
@ -1,4 +0,0 @@
|
|||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
<figure>
|
||||
<audio controls preload="{{ $preload }}">
|
||||
<source src="{{ $sndUrl }}" type="{{ $type }}">
|
||||
</audio>
|
||||
<figcaption>{!! $caption !!}</figcaption>
|
||||
</figure>
|
|
@ -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
|
@ -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', '.*');
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue