Compare commits
2
Commits
612896d2c6
...
c19b5899e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c19b5899e4 | ||
|
|
914f5fd9d1 |
@@ -6,6 +6,7 @@ dependencies:
|
||||
- field.field.node.projet.body
|
||||
- field.field.node.projet.field_artiste_s_invite_s
|
||||
- field.field.node.projet.field_dates
|
||||
- field.field.node.projet.field_fichiers
|
||||
- field.field.node.projet.field_images
|
||||
- field.field.node.projet.field_invite_accompagne_curate_p
|
||||
- field.field.node.projet.field_partenaire_s
|
||||
@@ -18,6 +19,7 @@ dependencies:
|
||||
- autocomplete_deluxe
|
||||
- datetime_range
|
||||
- field_group
|
||||
- file
|
||||
- image
|
||||
- path
|
||||
- text
|
||||
@@ -101,6 +103,7 @@ third_party_settings:
|
||||
group_media:
|
||||
children:
|
||||
- field_images
|
||||
- field_fichiers
|
||||
label: Media
|
||||
region: content
|
||||
parent_name: group_tabs
|
||||
@@ -161,6 +164,13 @@ content:
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
field_fichiers:
|
||||
type: file_generic
|
||||
weight: 8
|
||||
region: content
|
||||
settings:
|
||||
progress_indicator: throbber
|
||||
third_party_settings: { }
|
||||
field_images:
|
||||
type: image_image
|
||||
weight: 7
|
||||
|
||||
@@ -6,6 +6,7 @@ dependencies:
|
||||
- field.field.node.projet.body
|
||||
- field.field.node.projet.field_artiste_s_invite_s
|
||||
- field.field.node.projet.field_dates
|
||||
- field.field.node.projet.field_fichiers
|
||||
- field.field.node.projet.field_images
|
||||
- field.field.node.projet.field_invite_accompagne_curate_p
|
||||
- field.field.node.projet.field_partenaire_s
|
||||
@@ -15,6 +16,7 @@ dependencies:
|
||||
- node.type.projet
|
||||
module:
|
||||
- datetime_range
|
||||
- file
|
||||
- image
|
||||
- options
|
||||
- text
|
||||
@@ -50,6 +52,14 @@ content:
|
||||
third_party_settings: { }
|
||||
weight: 102
|
||||
region: content
|
||||
field_fichiers:
|
||||
type: file_default
|
||||
label: above
|
||||
settings:
|
||||
use_description_as_link_text: true
|
||||
third_party_settings: { }
|
||||
weight: 110
|
||||
region: content
|
||||
field_images:
|
||||
type: image
|
||||
label: above
|
||||
|
||||
@@ -7,6 +7,7 @@ dependencies:
|
||||
- field.field.node.projet.body
|
||||
- field.field.node.projet.field_artiste_s_invite_s
|
||||
- field.field.node.projet.field_dates
|
||||
- field.field.node.projet.field_fichiers
|
||||
- field.field.node.projet.field_images
|
||||
- field.field.node.projet.field_invite_accompagne_curate_p
|
||||
- field.field.node.projet.field_partenaire_s
|
||||
@@ -56,6 +57,7 @@ content:
|
||||
region: content
|
||||
hidden:
|
||||
field_dates: true
|
||||
field_fichiers: true
|
||||
field_invite_accompagne_curate_p: true
|
||||
field_partenaire_s: true
|
||||
field_portage: true
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
uuid: 01cf5142-1617-4b0a-afbe-46bed19f3a75
|
||||
langcode: fr
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_fichiers
|
||||
- node.type.projet
|
||||
module:
|
||||
- file
|
||||
id: node.projet.field_fichiers
|
||||
field_name: field_fichiers
|
||||
entity_type: node
|
||||
bundle: projet
|
||||
label: Fichiers
|
||||
description: ''
|
||||
required: false
|
||||
translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'default:file'
|
||||
handler_settings: { }
|
||||
file_directory: '[date:custom:Y]-[date:custom:m]'
|
||||
file_extensions: 'pdf doc docx odt zip txt'
|
||||
max_filesize: ''
|
||||
description_field: true
|
||||
field_type: file
|
||||
@@ -0,0 +1,23 @@
|
||||
uuid: 9a9ebb54-2e2f-46bd-9586-60ebedfc95d8
|
||||
langcode: fr
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- file
|
||||
- node
|
||||
id: node.field_fichiers
|
||||
field_name: field_fichiers
|
||||
entity_type: node
|
||||
type: file
|
||||
settings:
|
||||
target_type: file
|
||||
display_field: true
|
||||
display_default: false
|
||||
uri_scheme: public
|
||||
module: file
|
||||
locked: false
|
||||
cardinality: -1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
@@ -84,6 +84,17 @@ process:
|
||||
image_field_caption/format:
|
||||
plugin: default_value
|
||||
default_value: wysiwyg
|
||||
field_fichiers:
|
||||
plugin: sub_process
|
||||
source: pdfs
|
||||
process:
|
||||
target_id:
|
||||
plugin: file_import
|
||||
source: uri
|
||||
destination: destination
|
||||
reuse: true
|
||||
skip_on_missing_source: true
|
||||
description: description
|
||||
field_dates/value:
|
||||
plugin: leshed_date
|
||||
source: date_start
|
||||
|
||||
@@ -4,7 +4,7 @@ langcode: fr
|
||||
uuid: e53d1b41-902a-4030-a476-ad2d5f3e5070
|
||||
name: 'Le Shed'
|
||||
mail: dev@figureslibres.io
|
||||
slogan: ''
|
||||
slogan: "Centre d'art en Normandie"
|
||||
page:
|
||||
403: ''
|
||||
404: ''
|
||||
|
||||
+11
@@ -98,6 +98,17 @@ process:
|
||||
image_field_caption/format:
|
||||
plugin: default_value
|
||||
default_value: wysiwyg
|
||||
field_fichiers:
|
||||
plugin: sub_process
|
||||
source: pdfs
|
||||
process:
|
||||
target_id:
|
||||
plugin: file_import
|
||||
source: uri
|
||||
destination: destination
|
||||
reuse: true
|
||||
skip_on_missing_source: true
|
||||
description: description
|
||||
field_dates/value:
|
||||
plugin: leshed_date
|
||||
source: date_start
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\migrate_leshed\Plugin\migrate\process;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\File\FileExists;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\migrate\Attribute\MigrateProcess;
|
||||
use Drupal\migrate\MigrateExecutableInterface;
|
||||
use Drupal\migrate\MigrateSkipRowException;
|
||||
use Drupal\migrate\ProcessPluginBase;
|
||||
use Drupal\migrate\Row;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Copies a local file into the file system as a managed File entity.
|
||||
*
|
||||
* The source value is a readable file path/URI. The file is copied into the
|
||||
* destination directory and registered as a managed file; the file id is
|
||||
* returned.
|
||||
*
|
||||
* Configuration:
|
||||
* - destination: (required) destination directory URI, e.g. 'public://2026-08/'.
|
||||
* - uid: owner uid of the created file (default 0).
|
||||
* - reuse: reuse an existing managed file at the destination URI (default TRUE).
|
||||
* - skip_on_missing_source: skip the value when the source is missing
|
||||
* (default TRUE) instead of failing.
|
||||
*
|
||||
* @code
|
||||
* field_fichiers/target_id:
|
||||
* plugin: file_import
|
||||
* source: uri
|
||||
* destination: destination
|
||||
* @endcode
|
||||
*/
|
||||
#[MigrateProcess(id: 'file_import')]
|
||||
final class FileImport extends ProcessPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Per-run cache of already resolved destination URIs to file ids.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $fidCache = [];
|
||||
|
||||
public function __construct(
|
||||
array $configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
private readonly FileSystemInterface $fileSystem,
|
||||
private readonly EntityTypeManagerInterface $entityTypeManager,
|
||||
) {
|
||||
$configuration += [
|
||||
'uid' => 0,
|
||||
'reuse' => TRUE,
|
||||
'skip_on_missing_source' => TRUE,
|
||||
];
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
|
||||
return new self(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('file_system'),
|
||||
$container->get('entity_type.manager'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): ?int {
|
||||
$source = is_string($value) ? trim($value) : '';
|
||||
if ($source === '') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!is_file($source) && !is_file($this->fileSystem->realpath($source) ?: $source)) {
|
||||
return $this->skipFile($migrate_executable, "Source file '$source' is missing.");
|
||||
}
|
||||
|
||||
$directory = $this->getPropertyValue($this->configuration['destination'], $row);
|
||||
if (!is_string($directory) || $directory === '') {
|
||||
return $this->skipFile($migrate_executable, "Missing 'destination' for file '$source'.");
|
||||
}
|
||||
$directory = rtrim($directory, '/') . '/';
|
||||
$destination = $directory . $this->fileSystem->basename($source);
|
||||
|
||||
// Reuse an already imported file to keep the migration idempotent.
|
||||
if ($this->configuration['reuse']) {
|
||||
if (isset($this->fidCache[$destination])) {
|
||||
return $this->fidCache[$destination];
|
||||
}
|
||||
$existing = $this->entityTypeManager->getStorage('file')
|
||||
->loadByProperties(['uri' => $destination]);
|
||||
if ($existing) {
|
||||
return $this->fidCache[$destination] = (int) reset($existing)->id();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
|
||||
return $this->skipFile($migrate_executable, "Cannot prepare directory '$directory'.");
|
||||
}
|
||||
$this->fileSystem->copy($source, $destination, FileExists::Replace);
|
||||
|
||||
$file = $this->entityTypeManager->getStorage('file')->create([
|
||||
'uri' => $destination,
|
||||
'uid' => (int) $this->getPropertyValue($this->configuration['uid'], $row),
|
||||
'status' => 1,
|
||||
]);
|
||||
$file->save();
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
return $this->skipFile($migrate_executable, "Import of file '$source' failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->fidCache[$destination] = (int) $file->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message and skips the current file item.
|
||||
*
|
||||
* @throws \Drupal\migrate\MigrateSkipRowException
|
||||
*/
|
||||
private function skipFile(MigrateExecutableInterface $migrate_executable, string $message): never {
|
||||
$migrate_executable->saveMessage($message);
|
||||
throw new MigrateSkipRowException($message, FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a configuration value, supporting destination (@) references.
|
||||
*/
|
||||
private function getPropertyValue(mixed $key, Row $row): mixed {
|
||||
if (is_string($key) && str_starts_with($key, '@')) {
|
||||
return $row->get(substr($key, 1));
|
||||
}
|
||||
if (is_string($key) && $row->hasSourceProperty($key)) {
|
||||
return $row->getSourceProperty($key);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Drupal\migrate_leshed\Plugin\migrate\source;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Utility\Token;
|
||||
use Drupal\migrate\Attribute\MigrateSource;
|
||||
use Drupal\migrate\Plugin\MigrationInterface;
|
||||
use Drupal\migrate\Row;
|
||||
@@ -46,6 +48,11 @@ final class ProjetNode extends Url {
|
||||
*/
|
||||
private const IMAGE_EXTENSIONS = ['png', 'gif', 'jpg', 'jpeg', 'webp'];
|
||||
|
||||
/**
|
||||
* File extensions to import into field_fichiers.
|
||||
*/
|
||||
private const FILE_EXTENSIONS = ['pdf'];
|
||||
|
||||
/**
|
||||
* Default destination directory when no constant is configured.
|
||||
*/
|
||||
@@ -63,6 +70,11 @@ final class ProjetNode extends Url {
|
||||
*/
|
||||
private ?array $folderMap = NULL;
|
||||
|
||||
/**
|
||||
* Memoized destination directory for field_fichiers (from field settings).
|
||||
*/
|
||||
private ?string $filesDestination = NULL;
|
||||
|
||||
public function __construct(
|
||||
array $configuration,
|
||||
$plugin_id,
|
||||
@@ -70,6 +82,8 @@ final class ProjetNode extends Url {
|
||||
MigrationInterface $migration,
|
||||
DataParserPluginManager $parserPluginManager,
|
||||
private readonly FileSystemInterface $fileSystem,
|
||||
private readonly EntityFieldManagerInterface $entityFieldManager,
|
||||
private readonly Token $token,
|
||||
) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $parserPluginManager);
|
||||
}
|
||||
@@ -85,6 +99,8 @@ final class ProjetNode extends Url {
|
||||
$migration,
|
||||
$container->get('plugin.manager.migrate_plus.data_parser'),
|
||||
$container->get('file_system'),
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('token'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,6 +111,7 @@ final class ProjetNode extends Url {
|
||||
return parent::fields() + [
|
||||
'body_html' => $this->t('Project body (HTML converted from Markdown)'),
|
||||
'images' => $this->t('Project images with captions'),
|
||||
'pdfs' => $this->t('Project PDF files'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -123,12 +140,14 @@ final class ProjetNode extends Url {
|
||||
// an undefined source key, even when no drive folder exists yet.
|
||||
$row->setSourceProperty('body_html', NULL);
|
||||
$row->setSourceProperty('images', []);
|
||||
$row->setSourceProperty('pdfs', []);
|
||||
|
||||
$uid = trim((string) $row->getSourceProperty('id'));
|
||||
$folder = $uid !== '' ? $this->findProjectFolder($uid) : NULL;
|
||||
if ($folder !== NULL) {
|
||||
$row->setSourceProperty('body_html', $this->readBody($folder, $uid));
|
||||
$row->setSourceProperty('images', $this->readImages($folder, $uid, (string) $row->getSourceProperty('title')));
|
||||
$row->setSourceProperty('pdfs', $this->readPdfs($folder, $uid));
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
@@ -188,8 +207,7 @@ final class ProjetNode extends Url {
|
||||
sort($entries, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
$markdown = NULL;
|
||||
foreach ($entries as $entry) {
|
||||
// Ignore ".back.md" backups of the pre-clean source text.
|
||||
if (!is_file($dir . '/' . $entry) || !preg_match('/\.md$/i', $entry) || preg_match('/\.back\.md$/i', $entry)) {
|
||||
if (!is_file($dir . '/' . $entry) || !preg_match('/\.md$/i', $entry)) {
|
||||
continue;
|
||||
}
|
||||
if (preg_match($preferred, $entry)) {
|
||||
@@ -206,10 +224,44 @@ final class ProjetNode extends Url {
|
||||
if ($contents === FALSE || trim($contents) === '') {
|
||||
return NULL;
|
||||
}
|
||||
$contents = $this->cleanBodyMarkdown($contents);
|
||||
if ($contents === '') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return (string) $this->getMarkdownConverter()->convert($contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the raw source Markdown before conversion.
|
||||
*
|
||||
* Removes redundant content already present elsewhere: raw HTML wrappers,
|
||||
* invisible characters, and the leading title block (H1 title + H2/H3
|
||||
* artists/subtitle, redundant with the sheet). Headings located *inside* the
|
||||
* body (e.g. artist/curator bios) are preserved. Idempotent: running it on an
|
||||
* already-cleaned text has no effect.
|
||||
*/
|
||||
private function cleanBodyMarkdown(string $md): string {
|
||||
// 1. Strip HTML tags (<div>, <br>, </div>, ...).
|
||||
$md = (string) preg_replace('/<[^>]+>/', '', $md);
|
||||
// 2. Strip zero-width/BOM characters and turn NBSP into a normal space.
|
||||
$md = (string) preg_replace('/[\x{200B}\x{200E}\x{200F}\x{FEFF}]/u', '', $md);
|
||||
$md = (string) preg_replace('/\x{00A0}/u', ' ', $md);
|
||||
// 3. Drop only the leading heading block: skip leading blank and heading
|
||||
// lines until the first body line.
|
||||
$lines = preg_split('/\R/u', $md) ?: [];
|
||||
$i = 0;
|
||||
$count = count($lines);
|
||||
while ($i < $count && (trim($lines[$i]) === '' || preg_match('/^\s*#{1,6}\s/u', $lines[$i]))) {
|
||||
$i++;
|
||||
}
|
||||
$md = implode("\n", array_slice($lines, $i));
|
||||
// 4. Collapse blank lines and trim.
|
||||
$md = (string) preg_replace('/\n{3,}/', "\n\n", $md);
|
||||
|
||||
return trim($md);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the list of images (URI + alt) for the project.
|
||||
*
|
||||
@@ -272,6 +324,90 @@ final class ProjetNode extends Url {
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the list of PDF files for the project.
|
||||
*
|
||||
* Collects PDFs both at the project folder root and inside the "{UID}_pdf"
|
||||
* subfolder. Each item carries the destination directory (from the
|
||||
* field_fichiers settings) and a human-readable description.
|
||||
*
|
||||
* @return array<int, array{uri: string, destination: string, description: string}>
|
||||
* The files to import.
|
||||
*/
|
||||
private function readPdfs(string $folderUri, string $uid): array {
|
||||
$dir = $this->fileSystem->realpath($folderUri);
|
||||
if ($dir === FALSE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Directories to scan: the project folder root and its "{UID}_pdf" subfolder.
|
||||
$sources = [$folderUri];
|
||||
$pattern = '/^' . preg_quote($uid, '/') . '_pdf$/i';
|
||||
foreach (scandir($dir) ?: [] as $entry) {
|
||||
if (preg_match($pattern, $entry) && is_dir($dir . '/' . $entry)) {
|
||||
$sources[] = $folderUri . '/' . $entry;
|
||||
}
|
||||
}
|
||||
|
||||
$destination = $this->filesDestination();
|
||||
$pdfs = [];
|
||||
foreach ($sources as $sourceUri) {
|
||||
$sourceDir = $this->fileSystem->realpath($sourceUri);
|
||||
if ($sourceDir === FALSE) {
|
||||
continue;
|
||||
}
|
||||
$files = scandir($sourceDir) ?: [];
|
||||
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
foreach ($files as $file) {
|
||||
if (!is_file($sourceDir . '/' . $file)) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), self::FILE_EXTENSIONS, TRUE)) {
|
||||
continue;
|
||||
}
|
||||
$pdfs[] = [
|
||||
'uri' => $sourceUri . '/' . $file,
|
||||
'destination' => $destination,
|
||||
'description' => $this->humanizeFilename($file),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $pdfs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves (once) the field_fichiers destination directory from its settings.
|
||||
*/
|
||||
private function filesDestination(): string {
|
||||
if ($this->filesDestination !== NULL) {
|
||||
return $this->filesDestination;
|
||||
}
|
||||
$scheme = 'public';
|
||||
$directory = '';
|
||||
$definitions = $this->entityFieldManager->getFieldDefinitions('node', 'projet');
|
||||
$field = $definitions['field_fichiers'] ?? NULL;
|
||||
if ($field !== NULL) {
|
||||
$scheme = $field->getFieldStorageDefinition()->getSetting('uri_scheme') ?: 'public';
|
||||
$directory = (string) $this->token->replace((string) $field->getSetting('file_directory'));
|
||||
}
|
||||
|
||||
return $this->filesDestination = $scheme . '://' . trim($directory, '/') . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a file name into a readable label (drops UID prefix and extension).
|
||||
*/
|
||||
private function humanizeFilename(string $file): string {
|
||||
$name = pathinfo($file, PATHINFO_FILENAME);
|
||||
// Drop a leading "{UID}_" numeric prefix.
|
||||
$name = preg_replace('/^\d+[_-]/', '', $name) ?? $name;
|
||||
// Separators to spaces, collapse, trim.
|
||||
$name = (string) preg_replace('/[_\-]+/u', ' ', $name);
|
||||
|
||||
return trim((string) preg_replace('/\s+/u', ' ', $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw contents of a caption (legende) file, or '' when missing.
|
||||
*/
|
||||
|
||||
@@ -59,46 +59,119 @@ import Splitting from "splitting";
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
function initTitles(){
|
||||
let titles = document.querySelectorAll('#block-leshed-identitedusite>a, article.node-type-projet>h2');
|
||||
// Rendu typographique des titres. Police Epilogue = axe variable `wght`
|
||||
// (100–900) uniquement ; l'italique est un fichier séparé (font-style).
|
||||
const TITLE_STYLE = {
|
||||
// 3 groupes de graisse : thin = majorité (défaut), medium = quelques,
|
||||
// bold = les pics. Les fines ne sont pas comptées (c'est le reste).
|
||||
wghtThin: { min: 100, max: 200 }, // majorité (défaut)
|
||||
wghtMedium: { min: 200, max: 400 }, // quelques
|
||||
wghtBold: { min: 800, max: 900 }, // pics
|
||||
boldRatio: 0.28, // proportion de pics gras par mot
|
||||
mediumRatio: 0.12, // proportion de medium par mot
|
||||
italicOnBold: 0.6, // proba d'italique sur un pic
|
||||
italicOnMedium: 0.4, // proba d'italique sur un medium
|
||||
italicOnThin: 0.30, // proba d'italique sur une fine
|
||||
};
|
||||
|
||||
function initTitles() {
|
||||
const titles = document.querySelectorAll('#block-leshed-identitedusite>a, article.node-type-projet>header>h2');
|
||||
for (const txt of titles) {
|
||||
console.log(txt.innerText, '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -');
|
||||
let splitted = Splitting({ target: txt, by: 'chars'});
|
||||
console.log("splitted", splitted);
|
||||
|
||||
const splitted = Splitting({ target: txt, by: 'chars' });
|
||||
for (const word of splitted[0].words) {
|
||||
let chars = word.getElementsByClassName('char');
|
||||
console.log("chars", chars);
|
||||
styleWordChars(Array.from(word.getElementsByClassName('char')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const char of chars) {
|
||||
console.log(char.innerText);
|
||||
/**
|
||||
* Tire k positions réparties régulièrement dans [0, n[ : le mot est découpé
|
||||
* en k cases contiguës, une position au hasard par case (aléa contrôlé, pas
|
||||
* de paquets). Les positions déjà prises par un autre groupe sont évitées.
|
||||
*/
|
||||
function spreadPositions(n, k, exclude = new Set()) {
|
||||
const set = new Set();
|
||||
for (let i = 0; i < k; i++) {
|
||||
const start = Math.floor((i * n) / k);
|
||||
const end = Math.floor(((i + 1) * n) / k); // borne exclue
|
||||
const span = Math.max(1, end - start);
|
||||
let pos = randomInt(start, start + span);
|
||||
let guard = 0;
|
||||
while ((exclude.has(pos) || set.has(pos)) && guard < span) {
|
||||
pos = start + ((pos - start + 1) % span);
|
||||
guard++;
|
||||
}
|
||||
set.add(pos);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
// Weight
|
||||
let wght = randomInt(150, 600);//200 + Math.random() * 800;
|
||||
/**
|
||||
* Applique graisse + italique caractère par caractère.
|
||||
*
|
||||
* Majorité de fines ; quelques medium et quelques pics gras, chacun réparti
|
||||
* régulièrement dans le mot. L'italique est corrélé à la graisse.
|
||||
*/
|
||||
function styleWordChars(chars) {
|
||||
const n = chars.length;
|
||||
if (!n) return;
|
||||
|
||||
// Ital
|
||||
let ital = Math.random() > 0.7 ? 1 : 0;
|
||||
// char.style.fontVariationSettings = `'wght' ${wght}, 'wdth' ${wdth}, 'ital' ${ital}`;
|
||||
char.style.fontVariationSettings = `'wght' ${wght}`;
|
||||
char.style.fontStyle = ital ? 'italic' : 'normal';
|
||||
// Accents répartis régulièrement : d'abord les pics gras, puis les medium
|
||||
// (en évitant les positions déjà grasses). Le reste sera fin.
|
||||
const bold = spreadPositions(n, Math.round(n * TITLE_STYLE.boldRatio));
|
||||
const medium = spreadPositions(n, Math.round(n * TITLE_STYLE.mediumRatio), bold);
|
||||
|
||||
// // Letter spacing
|
||||
// let wdth_wght_ratio = wdth*wght / 10000;
|
||||
// console.log('wdth_wght_ratio',wdth_wght_ratio);
|
||||
// 1) Décide graisse + italique pour chaque caractère.
|
||||
const decisions = chars.map((char, i) => {
|
||||
let range, pItal;
|
||||
if (bold.has(i)) {
|
||||
range = TITLE_STYLE.wghtBold;
|
||||
pItal = TITLE_STYLE.italicOnBold;
|
||||
}
|
||||
else if (medium.has(i)) {
|
||||
range = TITLE_STYLE.wghtMedium;
|
||||
pItal = TITLE_STYLE.italicOnMedium;
|
||||
}
|
||||
else {
|
||||
range = TITLE_STYLE.wghtThin;
|
||||
pItal = TITLE_STYLE.italicOnThin;
|
||||
}
|
||||
return {
|
||||
char,
|
||||
wght: randomInt(range.min, range.max + 1),
|
||||
ital: Math.random() < pItal,
|
||||
};
|
||||
});
|
||||
|
||||
// char.style.letterSpacing = wdth_wght_ratio < 1 ? "0.5em" : ital ? "0.05em" : 0;
|
||||
// if (ital) {
|
||||
// let prev_char = char.previousSibling;
|
||||
// if (prev_char) {
|
||||
// char.style.letterSpacing = prev_char.style.letterSpacing = "0.03em";
|
||||
// }
|
||||
// }
|
||||
// 2) Au moins une italique par mot : si aucune, on en force une, de
|
||||
// préférence sur un pic gras ou un medium, sinon sur une lettre au hasard.
|
||||
if (!decisions.some((d) => d.ital)) {
|
||||
const accents = [...bold, ...medium];
|
||||
const idx = accents.length ? accents[randomInt(0, accents.length)] : randomInt(0, n);
|
||||
decisions[idx].ital = true;
|
||||
}
|
||||
|
||||
// 3) Pas plus de 2 italiques consécutives : on casse les séries de 3+.
|
||||
let run = 0;
|
||||
for (const d of decisions) {
|
||||
if (d.ital) {
|
||||
run += 1;
|
||||
if (run > 2) {
|
||||
d.ital = false;
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 4) Applique.
|
||||
for (const { char, wght, ital } of decisions) {
|
||||
char.style.fontVariationSettings = `'wght' ${wght}`;
|
||||
char.style.fontStyle = ital ? 'italic' : 'normal';
|
||||
char.classList.toggle('italic', ital);
|
||||
char.classList.toggle('normal', !ital);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ header[role="banner"]{
|
||||
display: flex;
|
||||
justify-content:space-between;
|
||||
align-items: center;
|
||||
padding: 0.2em 2em;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
#block-leshed-identitedusite{
|
||||
@@ -170,3 +170,76 @@ header[role="banner"]{
|
||||
}
|
||||
} // end of header[role="banner"]{
|
||||
|
||||
|
||||
// _____ _____ __ ____ ___ _ ____
|
||||
// |_ _/ _ \ \ / / / ___/ _ \| | / ___|
|
||||
// | || | | \ \ /\ / / | | | | | | | \___ \
|
||||
// | || |_| |\ V V / | |__| |_| | |___ ___) |
|
||||
// |_| \___/ \_/\_/ \____\___/|_____|____/
|
||||
|
||||
div.layout.layout--twocol-section--50-50{
|
||||
flex-wrap: nowrap;
|
||||
column-gap: 1em;
|
||||
div.layout__region{
|
||||
.views-row{
|
||||
// border: 1px solid red;
|
||||
// box-sizing: border-box;
|
||||
margin-bottom: 1.5em;
|
||||
article.node-type-projet{
|
||||
>header{
|
||||
>h2{
|
||||
margin: 0;
|
||||
font-size: 5em;
|
||||
text-transform:lowercase;
|
||||
filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px #fff);
|
||||
span.word{
|
||||
hyphens: auto;
|
||||
|
||||
// $bgcolor:rgba(255,255,255,0.8);
|
||||
// // background-color: $bgcolor;
|
||||
// border-radius: 0.8em;
|
||||
// box-shadow: 0 0 20px #fff;
|
||||
|
||||
span.char{
|
||||
letter-spacing:-0.2em;
|
||||
}
|
||||
}
|
||||
// span.whitespace{
|
||||
// letter-spacing: 0.5em;
|
||||
// }
|
||||
}
|
||||
}
|
||||
>div{
|
||||
// margin-top: 3em;
|
||||
}
|
||||
// only with images, title overlap the image
|
||||
&.has-image{
|
||||
>header{
|
||||
>h2{
|
||||
position: relative;
|
||||
max-height: 1em;
|
||||
overflow: visible;
|
||||
z-index: 10;
|
||||
// text-shadow: 0 0 10px #fff;
|
||||
// box-shadow: 0 0 15px #f00;
|
||||
// filter: drop-shadow(0 0 5px #fff);
|
||||
}
|
||||
div.field_images{
|
||||
position:relative;
|
||||
// width: 100%;
|
||||
z-index: 5;
|
||||
a{
|
||||
display:block;
|
||||
img{
|
||||
width:100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,15 +74,25 @@ function parse_menu_item(&$items, $key){
|
||||
* Implements hook_preprocess_HOOK() for node.html.twig.
|
||||
*/
|
||||
function leshed_preprocess_node(&$variables) {
|
||||
$node_type = $variables['node']->getType();
|
||||
/** @var \Drupal\node\Entity\Node $node */
|
||||
$node = $variables['node'];
|
||||
$node_type = $node->getType();
|
||||
if (!isset($variables["attributes"]['class'])) {
|
||||
$variables["attributes"]['class'] = [];
|
||||
}
|
||||
$variables["attributes"]['class'][] = "node-type-{$node_type}";
|
||||
|
||||
if ($variables['view_mode'] === "teaser") {
|
||||
/** @var \Drupal\file\Plugin\\Field\FieldType\FileFieldItemList $field_images */
|
||||
$field_images = $node->get('field_images');
|
||||
if (!$field_images->isEmpty()) {
|
||||
$variables["attributes"]['class'][] = "has-image";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function leshed_preprocess_contanier(&$variables) {
|
||||
function leshed_preprocess_container(&$variables) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
{#
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to display a node.
|
||||
*
|
||||
* Available variables:
|
||||
* - node: The node entity with limited access to object properties and methods.
|
||||
* Only method names starting with "get", "has", or "is" and a few common
|
||||
* methods such as "id", "label", and "bundle" are available. For example:
|
||||
* - node.getCreatedTime() will return the node creation timestamp.
|
||||
* - node.hasField('field_example') returns TRUE if the node bundle includes
|
||||
* field_example. (This does not indicate the presence of a value in this
|
||||
* field.)
|
||||
* - node.isPublished() will return whether the node is published or not.
|
||||
* Calling other methods, such as node.delete(), will result in an exception.
|
||||
* See \Drupal\node\Entity\Node for a full list of public properties and
|
||||
* methods for the node object.
|
||||
* - label: (optional) The title of the node.
|
||||
* - content: All node items. Use {{ content }} to print them all,
|
||||
* or print a subset such as {{ content.field_example }}. Use
|
||||
* {{ content|without('field_example') }} to temporarily suppress the printing
|
||||
* of a given child element.
|
||||
* - author_picture: The node author user entity, rendered using the "compact"
|
||||
* view mode.
|
||||
* - metadata: Metadata for this node.
|
||||
* - date: (optional) Themed creation date field.
|
||||
* - author_name: (optional) Themed author name field.
|
||||
* - url: Direct URL of the current node.
|
||||
* - display_submitted: Whether submission information should be displayed.
|
||||
* - attributes: HTML attributes for the containing element.
|
||||
* The attributes.class element may contain one or more of the following
|
||||
* classes:
|
||||
* - node: The current template type (also known as a "theming hook").
|
||||
* - node--type-[type]: The current node type. For example, if the node is an
|
||||
* "Article" it would result in "node--type-article". Note that the machine
|
||||
* name will often be in a short form of the human readable label.
|
||||
* - node--view-mode-[view_mode]: The View Mode of the node; for example, a
|
||||
* teaser would result in: "node--view-mode-teaser", and
|
||||
* full: "node--view-mode-full".
|
||||
* The following are controlled through the node publishing options.
|
||||
* - node--promoted: Appears on nodes promoted to the front page.
|
||||
* - node--sticky: Appears on nodes ordered above other non-sticky nodes in
|
||||
* teaser listings.
|
||||
* - node--unpublished: Appears on unpublished nodes visible only to site
|
||||
* admins.
|
||||
* - title_attributes: Same as attributes, except applied to the main title
|
||||
* tag that appears in the template.
|
||||
* - content_attributes: Same as attributes, except applied to the main
|
||||
* content tag that appears in the template.
|
||||
* - author_attributes: Same as attributes, except applied to the author of
|
||||
* the node tag that appears in the template.
|
||||
* - title_prefix: Additional output populated by modules, intended to be
|
||||
* displayed in front of the main title tag that appears in the template.
|
||||
* - title_suffix: Additional output populated by modules, intended to be
|
||||
* displayed after the main title tag that appears in the template.
|
||||
* - view_mode: View mode; for example, "teaser" or "full".
|
||||
* - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
|
||||
* - page: Flag for the full page state. Will be true if view_mode is 'full'.
|
||||
*
|
||||
* @see template_preprocess_node()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
#}
|
||||
<article{{ attributes }}>
|
||||
|
||||
{{ title_prefix }}
|
||||
{% if label and not page %}
|
||||
<h2{{ title_attributes }}>
|
||||
<a href="{{ url }}" rel="bookmark">{{ label }}</a>
|
||||
</h2>
|
||||
{% endif %}
|
||||
{{ title_suffix }}
|
||||
|
||||
{% if display_submitted %}
|
||||
<footer>
|
||||
{{ author_picture }}
|
||||
<div{{ author_attributes }}>
|
||||
{% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %}
|
||||
{{ metadata }}
|
||||
</div>
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
<div{{ content_attributes }}>
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
{#
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to display a node.
|
||||
*
|
||||
* Available variables:
|
||||
* - node: The node entity with limited access to object properties and methods.
|
||||
* Only method names starting with "get", "has", or "is" and a few common
|
||||
* methods such as "id", "label", and "bundle" are available. For example:
|
||||
* - node.getCreatedTime() will return the node creation timestamp.
|
||||
* - node.hasField('field_example') returns TRUE if the node bundle includes
|
||||
* field_example. (This does not indicate the presence of a value in this
|
||||
* field.)
|
||||
* - node.isPublished() will return whether the node is published or not.
|
||||
* Calling other methods, such as node.delete(), will result in an exception.
|
||||
* See \Drupal\node\Entity\Node for a full list of public properties and
|
||||
* methods for the node object.
|
||||
* - label: (optional) The title of the node.
|
||||
* - content: All node items. Use {{ content }} to print them all,
|
||||
* or print a subset such as {{ content.field_example }}. Use
|
||||
* {{ content|without('field_example') }} to temporarily suppress the printing
|
||||
* of a given child element.
|
||||
* - author_picture: The node author user entity, rendered using the "compact"
|
||||
* view mode.
|
||||
* - metadata: Metadata for this node.
|
||||
* - date: (optional) Themed creation date field.
|
||||
* - author_name: (optional) Themed author name field.
|
||||
* - url: Direct URL of the current node.
|
||||
* - display_submitted: Whether submission information should be displayed.
|
||||
* - attributes: HTML attributes for the containing element.
|
||||
* The attributes.class element may contain one or more of the following
|
||||
* classes:
|
||||
* - node: The current template type (also known as a "theming hook").
|
||||
* - node--type-[type]: The current node type. For example, if the node is an
|
||||
* "Article" it would result in "node--type-article". Note that the machine
|
||||
* name will often be in a short form of the human readable label.
|
||||
* - node--view-mode-[view_mode]: The View Mode of the node; for example, a
|
||||
* teaser would result in: "node--view-mode-teaser", and
|
||||
* full: "node--view-mode-full".
|
||||
* The following are controlled through the node publishing options.
|
||||
* - node--promoted: Appears on nodes promoted to the front page.
|
||||
* - node--sticky: Appears on nodes ordered above other non-sticky nodes in
|
||||
* teaser listings.
|
||||
* - node--unpublished: Appears on unpublished nodes visible only to site
|
||||
* admins.
|
||||
* - title_attributes: Same as attributes, except applied to the main title
|
||||
* tag that appears in the template.
|
||||
* - content_attributes: Same as attributes, except applied to the main
|
||||
* content tag that appears in the template.
|
||||
* - author_attributes: Same as attributes, except applied to the author of
|
||||
* the node tag that appears in the template.
|
||||
* - title_prefix: Additional output populated by modules, intended to be
|
||||
* displayed in front of the main title tag that appears in the template.
|
||||
* - title_suffix: Additional output populated by modules, intended to be
|
||||
* displayed after the main title tag that appears in the template.
|
||||
* - view_mode: View mode; for example, "teaser" or "full".
|
||||
* - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'.
|
||||
* - page: Flag for the full page state. Will be true if view_mode is 'full'.
|
||||
*
|
||||
* @see template_preprocess_node()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
#}
|
||||
|
||||
{#
|
||||
{% if content.field_images.getvalue|length %}
|
||||
{% set attributes = attributes.addClass('has-image') %}
|
||||
{% endif %} #}
|
||||
|
||||
<article{{ attributes }}>
|
||||
|
||||
{# {{ title_prefix }} #}
|
||||
{# {% if label and not page %} #}
|
||||
<header>
|
||||
<h2{{ title_attributes }}>
|
||||
<a href="{{ url }}" rel="bookmark">{{ label }}</a>
|
||||
</h2>
|
||||
{{ content.field_images }}
|
||||
</header>
|
||||
{# {% endif %} #}
|
||||
{# {{ title_suffix }} #}
|
||||
|
||||
|
||||
<div{{ content_attributes }}>
|
||||
{{ content|without('field_images') }}
|
||||
</div>
|
||||
|
||||
</article>
|
||||
Reference in New Issue
Block a user