From f60604db6087e2371d3bdf24d26782c80d82e598 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:31:21 +0200 Subject: [PATCH 1/5] Use attribute `ReadonlyModelProperty` to control `Model::set()` write access --- .../src/Model/Attributes/ReadonlyModelProperty.php | 10 ++++++++++ formwork/src/Model/Model.php | 12 ++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 formwork/src/Model/Attributes/ReadonlyModelProperty.php diff --git a/formwork/src/Model/Attributes/ReadonlyModelProperty.php b/formwork/src/Model/Attributes/ReadonlyModelProperty.php new file mode 100644 index 000000000..13b1350d7 --- /dev/null +++ b/formwork/src/Model/Attributes/ReadonlyModelProperty.php @@ -0,0 +1,10 @@ +isPromoted()) { + if ($this->isReadonly($key)) { + throw new BadMethodCallException(sprintf('Cannot set readonly model property %s::$%s', static::class, $key)); + } + // If defined use a setter if (method_exists($this, $setter = 'set' . ucfirst($key))) { $this->{$setter}($value); @@ -140,4 +146,10 @@ public function data(): array { return $this->data; } + + private function isReadonly(string $property): bool + { + $attributes = (new ReflectionProperty($this, $property))->getAttributes(ReadonlyModelProperty::class, ReflectionAttribute::IS_INSTANCEOF); + return $attributes !== []; + } } From 57c7798976747313926b50dfc01a34e8d97789e1 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:39:20 +0200 Subject: [PATCH 2/5] Implement file metadata --- formwork/config/system.yaml | 1 + formwork/src/Files/File.php | 32 +++++++++++++++++++++++++++++++- formwork/src/Images/Image.php | 12 +++++++++--- formwork/src/Pages/Page.php | 9 +++++++-- panel/translations/de.yaml | 2 ++ panel/translations/en.yaml | 2 ++ panel/translations/es.yaml | 2 ++ panel/translations/fr.yaml | 2 ++ panel/translations/it.yaml | 2 ++ panel/translations/pt.yaml | 2 ++ panel/translations/ru.yaml | 2 ++ site/schemes/files/file.yaml | 1 + site/schemes/files/image.yaml | 15 +++++++++++++++ 13 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 site/schemes/files/file.yaml create mode 100644 site/schemes/files/image.yaml diff --git a/formwork/config/system.yaml b/formwork/config/system.yaml index 080db0d00..609b3abbf 100644 --- a/formwork/config/system.yaml +++ b/formwork/config/system.yaml @@ -43,6 +43,7 @@ fields: files: allowedExtensions: [] + metadataExtension: .meta.yaml images: jpegQuality: 85 diff --git a/formwork/src/Files/File.php b/formwork/src/Files/File.php index c30deedcc..7e80ac313 100644 --- a/formwork/src/Files/File.php +++ b/formwork/src/Files/File.php @@ -2,49 +2,64 @@ namespace Formwork\Files; +use Formwork\App; use Formwork\Data\Contracts\Arrayable; use Formwork\Files\Exceptions\FileUriGenerationException; +use Formwork\Model\Attributes\ReadonlyModelProperty; +use Formwork\Model\Model; +use Formwork\Parsers\Yaml; use Formwork\Utils\FileSystem; use Formwork\Utils\MimeType; use Formwork\Utils\Str; use RuntimeException; use Stringable; -class File implements Arrayable, Stringable +class File extends Model implements Arrayable, Stringable { + protected const MODEL_IDENTIFIER = 'file'; + + protected const SCHEME_IDENTIFIER = 'files.file'; + /** * File name */ + #[ReadonlyModelProperty] protected string $name; /** * File extension */ + #[ReadonlyModelProperty] protected string $extension; /** * File MIME type */ + #[ReadonlyModelProperty] protected string $mimeType; /** * File type in a human-readable format */ + #[ReadonlyModelProperty] protected ?string $type = null; /** * File size in a human-readable format */ + #[ReadonlyModelProperty] protected string $size; /** * File last modified time */ + #[ReadonlyModelProperty] protected int $lastModifiedTime; /** * File hash */ + #[ReadonlyModelProperty] protected string $hash; protected FileUriGenerator $uriGenerator; @@ -58,6 +73,7 @@ public function __construct(protected string $path) { $this->name = basename($path); $this->extension = FileSystem::extension($path); + $this->loadData(); } public function __toString(): string @@ -190,6 +206,20 @@ public function toArray(): array ]; } + private function loadData(): void + { + $app = App::instance(); + + $this->scheme = $app->schemes()->get(static::SCHEME_IDENTIFIER); + $this->fields = $this->scheme->fields(); + + $metadataFile = $this->path . $app->config()->get('system.files.metadataExtension'); + + $this->data = FileSystem::exists($metadataFile) ? Yaml::parseFile($metadataFile) : []; + + $this->fields->setValues($this->data); + } + /** * Match MIME type with an array of extensions * diff --git a/formwork/src/Images/Image.php b/formwork/src/Images/Image.php index 1f96e7dac..a1beb46db 100644 --- a/formwork/src/Images/Image.php +++ b/formwork/src/Images/Image.php @@ -31,6 +31,7 @@ use Formwork\Images\Transform\Sharpen; use Formwork\Images\Transform\Smoothen; use Formwork\Images\Transform\TransformCollection; +use Formwork\Model\Attributes\ReadonlyModelProperty; use Formwork\Utils\FileSystem; use Formwork\Utils\MimeType; use Formwork\Utils\Uri; @@ -38,16 +39,20 @@ class Image extends File { - protected string $path; + protected const MODEL_IDENTIFIER = 'image'; + protected const SCHEME_IDENTIFIER = 'files.image'; + + #[ReadonlyModelProperty] protected AbstractHandler $handler; + #[ReadonlyModelProperty] protected ImageInfo $info; + #[ReadonlyModelProperty] protected TransformCollection $transforms; - protected string $mimeType; - + #[ReadonlyModelProperty] protected ?string $type = 'image'; /** @@ -312,6 +317,7 @@ public function process(?string $mimeType = null, bool $forceCache = false): Ima } $image = new Image($path, $this->options); + $image->data = $this->data; $image->uriGenerator = $this->uriGenerator; $image->transforms = $this->transforms; $image->handler = $this->handler; diff --git a/formwork/src/Pages/Page.php b/formwork/src/Pages/Page.php index f1eeca316..3d7998e3a 100644 --- a/formwork/src/Pages/Page.php +++ b/formwork/src/Pages/Page.php @@ -568,8 +568,13 @@ protected function loadFiles(): void $languages[] = $language; } } - } elseif (in_array($extension, $config->get('system.files.allowedExtensions'), true)) { - $files[] = App::instance()->getService(FileFactory::class)->make(FileSystem::joinPaths($this->path, $file)); + } else { + if (Str::endsWith($file, $config->get('system.files.metadataExtension'))) { + continue; + } + if (in_array($extension, $config->get('system.files.allowedExtensions'), true)) { + $files[] = App::instance()->getService(FileFactory::class)->make(FileSystem::joinPaths($this->path, $file)); + } } } } diff --git a/panel/translations/de.yaml b/panel/translations/de.yaml index b88a94949..602585fd0 100644 --- a/panel/translations/de.yaml +++ b/panel/translations/de.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: Die Seite existiert nicht oder die Anfr panel.errors.error.notFound.heading: Oops, Seite nicht gefunden! panel.errors.error.notFound.status: Nicht gefunden panel.files.actions: Aktionen +panel.files.metadata: Metadaten +panel.files.metadata.alternativeText: Alternativtext panel.files.viewAsList: Als Liste anzeigen panel.files.viewAsThumbnails: Als Miniaturansichten anzeigen panel.login.attempt.failed: Anmeldeversuch fehlgeschlagen! Versuchen Sie es erneut. diff --git a/panel/translations/en.yaml b/panel/translations/en.yaml index d2476db9e..0fff3ad3f 100644 --- a/panel/translations/en.yaml +++ b/panel/translations/en.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: The page does not exist or the request panel.errors.error.notFound.heading: Oops, page not found! panel.errors.error.notFound.status: Not found panel.files.actions: Actions +panel.files.metadata: Metadata +panel.files.metadata.alternativeText: Alternative text panel.files.viewAsList: View as list panel.files.viewAsThumbnails: View as thumbnails panel.login.attempt.failed: Login attempt failed! Try again. diff --git a/panel/translations/es.yaml b/panel/translations/es.yaml index 6cb3a0aa7..0eb290cf2 100644 --- a/panel/translations/es.yaml +++ b/panel/translations/es.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: La página no existe o la solicitud no panel.errors.error.notFound.heading: ¡Ups, página no encontrada! panel.errors.error.notFound.status: No encontrado panel.files.actions: Acciones +panel.files.metadata: Metadatos +panel.files.metadata.alternativeText: Texto alternativo panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: ¡Intento de inicio de sesión fallido! Intenta de nuevo. diff --git a/panel/translations/fr.yaml b/panel/translations/fr.yaml index a34ff5f40..1de8336cc 100644 --- a/panel/translations/fr.yaml +++ b/panel/translations/fr.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: La page n’existe pas ou la demande n panel.errors.error.notFound.heading: Oups, page non trouvée! panel.errors.error.notFound.status: Pas trouvé panel.files.actions: Actions +panel.files.metadata: Métadonnées +panel.files.metadata.alternativeText: Texte alternatif panel.files.viewAsList: Afficher en liste panel.files.viewAsThumbnails: Afficher en vignettes panel.login.attempt.failed: La tentative de connexion a échoué! Réessayer. diff --git a/panel/translations/it.yaml b/panel/translations/it.yaml index a431bd700..d96953ab5 100644 --- a/panel/translations/it.yaml +++ b/panel/translations/it.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: La pagina non esiste o la richiesta non panel.errors.error.notFound.heading: Oops, pagina non trovata! panel.errors.error.notFound.status: Non trovato panel.files.actions: Azioni +panel.files.metadata: Metadati +panel.files.metadata.alternativeText: Testo alternativo panel.files.viewAsList: Visualizza come lista panel.files.viewAsThumbnails: Visualizza come miniature panel.login.attempt.failed: Tentativo di accesso fallito! Riprova. diff --git a/panel/translations/pt.yaml b/panel/translations/pt.yaml index 260334214..48ff0a997 100644 --- a/panel/translations/pt.yaml +++ b/panel/translations/pt.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: A página não existe ou a solicitaçã panel.errors.error.notFound.heading: Ops, página não encontrada! panel.errors.error.notFound.status: Não encontrado panel.files.actions: Ações +panel.files.metadata: Metadados +panel.files.metadata.alternativeText: Texto alternativo panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: Falha ao tentar efetuar login! Tente novamente. diff --git a/panel/translations/ru.yaml b/panel/translations/ru.yaml index cca773fbe..b6b150b52 100644 --- a/panel/translations/ru.yaml +++ b/panel/translations/ru.yaml @@ -33,6 +33,8 @@ panel.errors.error.notFound.description: Страница не существу panel.errors.error.notFound.heading: К сожалению, страница не найдена! panel.errors.error.notFound.status: Не обнаружена panel.files.actions: Действия +panel.files.metadata: Метаданные +panel.files.metadata.alternativeText: Альтернативный текст panel.files.viewAsList: Просмотр в виде списка panel.files.viewAsThumbnails: Просмотр в виде миниатюр panel.login.attempt.failed: Войти попытка не удалась! Попробуйте еще раз. diff --git a/site/schemes/files/file.yaml b/site/schemes/files/file.yaml new file mode 100644 index 000000000..44a777071 --- /dev/null +++ b/site/schemes/files/file.yaml @@ -0,0 +1 @@ +title: File diff --git a/site/schemes/files/image.yaml b/site/schemes/files/image.yaml new file mode 100644 index 000000000..44b1f0490 --- /dev/null +++ b/site/schemes/files/image.yaml @@ -0,0 +1,15 @@ +title: Image + +extend: files.file + +layout: + type: sections + sections: + metadata: + label: '{{panel.files.metadata}}' + fields: [alt] + +fields: + alt: + type: text + label: '{{panel.files.metadata.alternativeText}}' From ec9bc5c40c1d09f11783c52b3313ead70e7a97c3 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:42:06 +0200 Subject: [PATCH 3/5] Allow file metadata editing from panel --- .../src/Panel/Controllers/PagesController.php | 50 ++++++ panel/routes.php | 2 +- panel/translations/de.yaml | 1 + panel/translations/en.yaml | 1 + panel/translations/es.yaml | 1 + panel/translations/fr.yaml | 1 + panel/translations/it.yaml | 1 + panel/translations/pt.yaml | 1 + panel/translations/ru.yaml | 1 + panel/views/pages/file.php | 165 ++++++++++-------- 10 files changed, 146 insertions(+), 78 deletions(-) diff --git a/formwork/src/Panel/Controllers/PagesController.php b/formwork/src/Panel/Controllers/PagesController.php index 2d1c212d6..b68faac78 100644 --- a/formwork/src/Panel/Controllers/PagesController.php +++ b/formwork/src/Panel/Controllers/PagesController.php @@ -5,6 +5,7 @@ use Formwork\Exceptions\TranslatedException; use Formwork\Fields\Exceptions\ValidationException; use Formwork\Fields\FieldCollection; +use Formwork\Files\File; use Formwork\Files\FileUploader; use Formwork\Http\Files\UploadedFile; use Formwork\Http\JsonResponse; @@ -509,10 +510,32 @@ public function file(RouteParams $routeParams): Response $files = $page->files(); $file = $files->get($filename); + + switch ($this->request->method()) { + case RequestMethod::GET: + $data = $file->data(); + + $file->fields()->setValues($data); + + break; + + case RequestMethod::POST: + $data = $this->request->input(); + + $file->fields()->setValues($data)->validate(); + + $this->updateFileMetadata($file, $file->fields()); + + $this->panel()->notify($this->translate('panel.files.metadata.updated'), 'success'); + + return $this->redirect($this->generateRoute('panel.pages.file', ['page' => $page->route(), 'filename' => $filename])); + } + $fileIndex = $files->indexOf($file); $this->modal('renameFile'); $this->modal('deleteFile'); + $this->modal('changes'); return new Response($this->view('pages.file', [ 'title' => $file->name(), @@ -586,6 +609,33 @@ protected function createPage(FieldCollection $fieldCollection): Page return $this->site()->retrievePage($path); } + protected function updateFileMetadata(File $file, FieldCollection $fieldCollection): void + { + $data = $file->data(); + + $scheme = $file->scheme(); + + $defaults = $scheme->fields()->pluck('default'); + + foreach ($fieldCollection as $field) { + if ($field->isEmpty() || (Arr::has($defaults, $field->name()) && Arr::get($defaults, $field->name()) === $field->value())) { + unset($data[$field->name()]); + continue; + } + + $data[$field->name()] = $field->value(); + } + + $metaFile = $file->path() . $this->config->get('system.files.metadataExtension'); + + if ($data === [] && FileSystem::exists($metaFile)) { + FileSystem::delete($metaFile); + return; + } + + FileSystem::write($metaFile, Yaml::encode($data)); + } + /** * Update a page */ diff --git a/panel/routes.php b/panel/routes.php index d0ff9bc31..6c146e1cb 100644 --- a/panel/routes.php +++ b/panel/routes.php @@ -163,7 +163,7 @@ 'panel.pages.file' => [ 'path' => '/pages/{page}/file/{filename}/', 'action' => 'Formwork\Panel\Controllers\PagesController@file', - 'methods' => ['GET'], + 'methods' => ['GET', 'POST'], ], 'panel.pages.delete' => [ diff --git a/panel/translations/de.yaml b/panel/translations/de.yaml index 602585fd0..684832313 100644 --- a/panel/translations/de.yaml +++ b/panel/translations/de.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Nicht gefunden panel.files.actions: Aktionen panel.files.metadata: Metadaten panel.files.metadata.alternativeText: Alternativtext +panel.files.metadata.updated: Dateimetadaten aktualisiert panel.files.viewAsList: Als Liste anzeigen panel.files.viewAsThumbnails: Als Miniaturansichten anzeigen panel.login.attempt.failed: Anmeldeversuch fehlgeschlagen! Versuchen Sie es erneut. diff --git a/panel/translations/en.yaml b/panel/translations/en.yaml index 0fff3ad3f..31f20bd50 100644 --- a/panel/translations/en.yaml +++ b/panel/translations/en.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Not found panel.files.actions: Actions panel.files.metadata: Metadata panel.files.metadata.alternativeText: Alternative text +panel.files.metadata.updated: File metadata updated panel.files.viewAsList: View as list panel.files.viewAsThumbnails: View as thumbnails panel.login.attempt.failed: Login attempt failed! Try again. diff --git a/panel/translations/es.yaml b/panel/translations/es.yaml index 0eb290cf2..2fc5ab2ca 100644 --- a/panel/translations/es.yaml +++ b/panel/translations/es.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: No encontrado panel.files.actions: Acciones panel.files.metadata: Metadatos panel.files.metadata.alternativeText: Texto alternativo +panel.files.metadata.updated: Metadatos del archivo actualizados panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: ¡Intento de inicio de sesión fallido! Intenta de nuevo. diff --git a/panel/translations/fr.yaml b/panel/translations/fr.yaml index 1de8336cc..099e2728f 100644 --- a/panel/translations/fr.yaml +++ b/panel/translations/fr.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Pas trouvé panel.files.actions: Actions panel.files.metadata: Métadonnées panel.files.metadata.alternativeText: Texte alternatif +panel.files.metadata.updated: Métadonnées du fichier mises à jour panel.files.viewAsList: Afficher en liste panel.files.viewAsThumbnails: Afficher en vignettes panel.login.attempt.failed: La tentative de connexion a échoué! Réessayer. diff --git a/panel/translations/it.yaml b/panel/translations/it.yaml index d96953ab5..695e18abf 100644 --- a/panel/translations/it.yaml +++ b/panel/translations/it.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Non trovato panel.files.actions: Azioni panel.files.metadata: Metadati panel.files.metadata.alternativeText: Testo alternativo +panel.files.metadata.updated: Metadati del file aggiornati panel.files.viewAsList: Visualizza come lista panel.files.viewAsThumbnails: Visualizza come miniature panel.login.attempt.failed: Tentativo di accesso fallito! Riprova. diff --git a/panel/translations/pt.yaml b/panel/translations/pt.yaml index 48ff0a997..09bc8f371 100644 --- a/panel/translations/pt.yaml +++ b/panel/translations/pt.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Não encontrado panel.files.actions: Ações panel.files.metadata: Metadados panel.files.metadata.alternativeText: Texto alternativo +panel.files.metadata.updated: Metadados do ficheiro atualizados panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: Falha ao tentar efetuar login! Tente novamente. diff --git a/panel/translations/ru.yaml b/panel/translations/ru.yaml index b6b150b52..ed02c617e 100644 --- a/panel/translations/ru.yaml +++ b/panel/translations/ru.yaml @@ -35,6 +35,7 @@ panel.errors.error.notFound.status: Не обнаружена panel.files.actions: Действия panel.files.metadata: Метаданные panel.files.metadata.alternativeText: Альтернативный текст +panel.files.metadata.updated: Метаданные файла обновлены panel.files.viewAsList: Просмотр в виде списка panel.files.viewAsThumbnails: Просмотр в виде миниатюр panel.login.attempt.failed: Войти попытка не удалась! Попробуйте еще раз. diff --git a/panel/views/pages/file.php b/panel/views/pages/file.php index 9c92cda0c..5c78ea675 100644 --- a/panel/views/pages/file.php +++ b/panel/views/pages/file.php @@ -1,94 +1,105 @@ layout('panel') ?> -
-
-
icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?> name() ?>
-
icon('arrow-left-circle') ?>translate('panel.pages.file.backToPage') ?>
+
+
+
+
icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?> name() ?>
+ +
+
+ href="uri('/pages/' . trim($page->route(), '/') . '/file/' . ($previousFile->name()) . '/') ?>" title="translate('panel.pages.previousFile') ?>" aria-label="translate('panel.pages.previousFile') ?>">icon('chevron-left') ?> + href="uri('/pages/' . trim($page->route(), '/') . '/file/' . ($nextFile->name()) . '/') ?>" title="translate('panel.pages.nextFile') ?>" aria-label="translate('panel.pages.nextFile') ?>">icon('chevron-right') ?> + user()->permissions()->has('pages.renameFiles')) : ?> + + + user()->permissions()->has('pages.replaceFiles')) : ?> + + + user()->permissions()->has('pages.deleteFiles')) : ?> + + + fields()->isEmpty()): ?> + + +
- href="uri('/pages/' . trim($page->route(), '/') . '/file/' . ($previousFile->name()) . '/') ?>" title="translate('panel.pages.previousFile') ?>" aria-label="translate('panel.pages.previousFile') ?>">icon('chevron-left') ?> - href="uri('/pages/' . trim($page->route(), '/') . '/file/' . ($nextFile->name()) . '/') ?>" title="translate('panel.pages.nextFile') ?>" aria-label="translate('panel.pages.nextFile') ?>">icon('chevron-right') ?> - user()->permissions()->has('pages.renameFiles')) : ?> - - - user()->permissions()->has('pages.replaceFiles')) : ?> - - - user()->permissions()->has('pages.deleteFiles')) : ?> - + type() === 'image') : ?> +
+
+
+ translate('panel.pages.file.preview') ?> +
+
+ +
+
+
-
-type() === 'image') : ?> -
+ type() === 'video') : ?>
translate('panel.pages.file.preview') ?>
- +
-
- -type() === 'video') : ?> +
- translate('panel.pages.file.preview') ?> + translate('panel.pages.file.info') ?>
-
- -
-
- -
-
- translate('panel.pages.file.info') ?> -
-
-
-
-
translate('panel.pages.file.info.mimeType') ?>:
- mimeType() ?> +
+
+
+
translate('panel.pages.file.info.mimeType') ?>:
+ mimeType() ?> +
+
+
translate('panel.pages.file.info.size') ?>:
+ size() ?> +
+
+
translate('panel.pages.file.info.lastModifiedTime') ?>:
+ datetime($file->lastModifiedTime()) ?> +
+
+
translate('panel.pages.file.info.uri') ?>:
+ uri() ?> +
+ type() === 'image') : ?> + insert('_files/images/info/info', ['file' => $file]) ?> +
-
-
translate('panel.pages.file.info.size') ?>:
- size() ?> -
-
-
translate('panel.pages.file.info.lastModifiedTime') ?>:
- datetime($file->lastModifiedTime()) ?> -
-
-
translate('panel.pages.file.info.uri') ?>:
- uri() ?> -
- type() === 'image') : ?> - insert('_files/images/info/info', ['file' => $file]) ?> -
-
-
-type() === 'image') : ?> - hasExifData() && $file->getExifData()->hasPositionData()) : ?> -
-
- - translate('panel.pages.file.position') ?> -
-
- insert('_files/images/position/map', ['exif' => $file->getExifData()]) ?> -
-
+ + type() === 'image') : ?> + hasExifData() && $file->getExifData()->hasPositionData()) : ?> +
+
+ + translate('panel.pages.file.position') ?> +
+
+ insert('_files/images/position/map', ['exif' => $file->getExifData()]) ?> +
+
+ + hasExifData()) : ?> + + - hasExifData()) : ?> - + + fields()->isEmpty()): ?> + insert('fields', ['fields' => $file->fields()]) ?> - \ No newline at end of file + \ No newline at end of file From 44c946d992ca4bdeab831a421243f47ba735ba48 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:46:00 +0200 Subject: [PATCH 4/5] Rewrite markdown extension to add images alternative text by default --- ...aseExtension.php => FormworkExtension.php} | 6 +- .../CommonMark/ImageAltProcessor.php | 38 ++++++++ .../Extensions/CommonMark/ImageRenderer.php | 88 +++++++++++++++++++ .../CommonMark/LinkBaseProcessor.php | 4 +- formwork/src/Parsers/Markdown.php | 7 +- 5 files changed, 138 insertions(+), 5 deletions(-) rename formwork/src/Parsers/Extensions/CommonMark/{LinkBaseExtension.php => FormworkExtension.php} (70%) create mode 100644 formwork/src/Parsers/Extensions/CommonMark/ImageAltProcessor.php create mode 100644 formwork/src/Parsers/Extensions/CommonMark/ImageRenderer.php diff --git a/formwork/src/Parsers/Extensions/CommonMark/LinkBaseExtension.php b/formwork/src/Parsers/Extensions/CommonMark/FormworkExtension.php similarity index 70% rename from formwork/src/Parsers/Extensions/CommonMark/LinkBaseExtension.php rename to formwork/src/Parsers/Extensions/CommonMark/FormworkExtension.php index 5ec2dc892..3af72a03f 100644 --- a/formwork/src/Parsers/Extensions/CommonMark/LinkBaseExtension.php +++ b/formwork/src/Parsers/Extensions/CommonMark/FormworkExtension.php @@ -8,17 +8,19 @@ use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; -class LinkBaseExtension implements ConfigurableExtensionInterface +class FormworkExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $configurationBuilder): void { $configurationBuilder->addSchema('formwork', Expect::structure([ - 'baseRoute' => Expect::string('/'), + 'imageAltProperty' => Expect::string('alt'), + 'baseRoute' => Expect::string('/'), ])); } public function register(EnvironmentBuilderInterface $environmentBuilder): void { + $environmentBuilder->addEventListener(DocumentParsedEvent::class, new ImageAltProcessor($environmentBuilder->getConfiguration())); $environmentBuilder->addEventListener(DocumentParsedEvent::class, new LinkBaseProcessor($environmentBuilder->getConfiguration())); } } diff --git a/formwork/src/Parsers/Extensions/CommonMark/ImageAltProcessor.php b/formwork/src/Parsers/Extensions/CommonMark/ImageAltProcessor.php new file mode 100644 index 000000000..b50f35f3f --- /dev/null +++ b/formwork/src/Parsers/Extensions/CommonMark/ImageAltProcessor.php @@ -0,0 +1,38 @@ +getDocument()->iterator() as $node) { + if (!$node instanceof Image) { + continue; + } + + $baseRoute = $this->configuration->get('formwork/baseRoute'); + + $site = App::instance()->site(); + + $uri = $node->getUrl(); + + $key = $this->configuration->get('formwork/imageAltProperty'); + + $alt = $site->findPage($baseRoute)?->files()->get($uri)?->get($key); + + if ($alt !== null) { + $node->data->set('attributes/alt', $alt); + } + } + } +} diff --git a/formwork/src/Parsers/Extensions/CommonMark/ImageRenderer.php b/formwork/src/Parsers/Extensions/CommonMark/ImageRenderer.php new file mode 100644 index 000000000..b10377904 --- /dev/null +++ b/formwork/src/Parsers/Extensions/CommonMark/ImageRenderer.php @@ -0,0 +1,88 @@ +data->get('attributes'); + + $forbidUnsafeLinks = ! $this->configuration->get('allow_unsafe_links'); + if ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl())) { + $attrs['src'] = ''; + } else { + $attrs['src'] = $node->getUrl(); + } + + if (($alt = $this->getAltText($node)) !== '') { + $attrs['alt'] = $alt; + } + + if (($title = $node->getTitle()) !== null) { + $attrs['title'] = $title; + } + + return new HtmlElement('img', $attrs, '', true); + } + + public function setConfiguration(ConfigurationInterface $configuration): void + { + $this->configuration = $configuration; + } + + public function getXmlTagName(Node $node): string + { + return 'image'; + } + + /** + * @param Image $node + * + * @return array + */ + public function getXmlAttributes(Node $node): array + { + Image::assertInstanceOf($node); + + return [ + 'destination' => $node->getUrl(), + 'title' => $node->getTitle() ?? '', + ]; + } + + private function getAltText(Image $image): string + { + $altText = ''; + + foreach ((new NodeIterator($image)) as $n) { + if ($n instanceof StringContainerInterface) { + $altText .= $n->getLiteral(); + } elseif ($n instanceof Newline) { + $altText .= "\n"; + } + } + + return $altText; + } +} diff --git a/formwork/src/Parsers/Extensions/CommonMark/LinkBaseProcessor.php b/formwork/src/Parsers/Extensions/CommonMark/LinkBaseProcessor.php index 15b62adcf..7506043c6 100644 --- a/formwork/src/Parsers/Extensions/CommonMark/LinkBaseProcessor.php +++ b/formwork/src/Parsers/Extensions/CommonMark/LinkBaseProcessor.php @@ -24,12 +24,14 @@ public function __invoke(DocumentParsedEvent $documentParsedEvent): void $baseRoute = $this->configuration->get('formwork/baseRoute'); + $site = App::instance()->site(); + $uri = $node->getUrl(); // Process only if scheme is either null, 'http' or 'https' if (in_array(Uri::scheme($uri), [null, 'http', 'https'], true) && ((Uri::host($uri) === null || Uri::host($uri) === '') && $uri[0] !== '#')) { $relativeUri = Uri::resolveRelative($uri, $baseRoute); - $uri = App::instance()->site()->uri($relativeUri, includeLanguage: false); + $uri = $site->uri($relativeUri, includeLanguage: false); } $node->setUrl($uri); diff --git a/formwork/src/Parsers/Markdown.php b/formwork/src/Parsers/Markdown.php index ebc63a96a..7a6ebb635 100644 --- a/formwork/src/Parsers/Markdown.php +++ b/formwork/src/Parsers/Markdown.php @@ -3,10 +3,12 @@ namespace Formwork\Parsers; use Formwork\App; -use Formwork\Parsers\Extensions\CommonMark\LinkBaseExtension; +use Formwork\Parsers\Extensions\CommonMark\FormworkExtension; +use Formwork\Parsers\Extensions\CommonMark\ImageRenderer; use Formwork\Sanitizer\HtmlSanitizer; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; +use League\CommonMark\Extension\CommonMark\Node\Inline\Image; use League\CommonMark\Extension\Table\TableExtension; use League\CommonMark\MarkdownConverter; @@ -29,7 +31,8 @@ public static function parse(string $input, array $options = []): string $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new TableExtension()); - $environment->addExtension(new LinkBaseExtension()); + $environment->addExtension(new FormworkExtension()); + $environment->addRenderer(Image::class, new ImageRenderer()); $markdownConverter = new MarkdownConverter($environment); From b2295e65a45174f243a9354e4beab374d6692480 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:46:51 +0200 Subject: [PATCH 5/5] Add metadata to index images --- site/pages/index/formwork.png.meta.yaml | 1 + site/pages/index/panel.png.meta.yaml | 1 + 2 files changed, 2 insertions(+) create mode 100644 site/pages/index/formwork.png.meta.yaml create mode 100644 site/pages/index/panel.png.meta.yaml diff --git a/site/pages/index/formwork.png.meta.yaml b/site/pages/index/formwork.png.meta.yaml new file mode 100644 index 000000000..900c6c473 --- /dev/null +++ b/site/pages/index/formwork.png.meta.yaml @@ -0,0 +1 @@ +alt: 'Screenshot of Formwork home page with a link to the administration panel and a dashboard preview' diff --git a/site/pages/index/panel.png.meta.yaml b/site/pages/index/panel.png.meta.yaml new file mode 100644 index 000000000..7bdbba4e3 --- /dev/null +++ b/site/pages/index/panel.png.meta.yaml @@ -0,0 +1 @@ +alt: 'Screenshot of Formwork administration panel'