'articles', 'posts_per_page' => -1, 'post_status' => 'publish', 'meta_query' => array( array( 'key' => 'related_revue', 'value' => $revueID, 'compare' => '=', ), ), )); $authors = array(); foreach ($revueRelatedArticles->posts as $article) { $currentArticleAuthors = get_field('authors', $article->ID); if (empty($currentArticleAuthors) || !is_array($currentArticleAuthors)) continue; foreach ($currentArticleAuthors as $authorID) { $authors[] = $authorID; } } return array_unique($authors); } function get_author_articles_amount($authorID) { if (empty($authorID)) return 0; $articles = count_user_articles($authorID, 'articles'); return $articles; } /** * Récupère tous les termes uniques d'une taxonomie pour une revue * * Cette fonction collecte tous les termes d'une taxonomie spécifique * associés aux articles d'une revue. Elle parcourt tous les articles * liés à la revue et retourne les termes uniques de la taxonomie donnée. * * @param int $revueID L'ID de la revue * @param string $taxonomy Le nom de la taxonomie (ex: 'etiquettes', 'category') * @return array Tableau d'objets de termes uniques */ function get_revue_terms($revueID, $taxonomy) { $revueRelatedArticles = new WP_Query(array( 'post_type' => 'articles', 'posts_per_page' => -1, 'post_status' => 'publish', 'meta_query' => array( array( 'key' => 'related_revue', 'value' => $revueID, 'compare' => '=', ), ), )); $terms = array(); foreach ($revueRelatedArticles->posts as $article) { $currentArticleTerms = get_the_terms($article->ID, $taxonomy); if (empty($currentArticleTerms) || !is_array($currentArticleTerms)) continue; foreach ($currentArticleTerms as $term) { $terms[] = $term->term_id; } } $uniquesTermsArray = array(); foreach (array_unique($terms) as $term) { $termObject = get_term($term, $taxonomy); $uniquesTermsArray[] = $termObject; } return $uniquesTermsArray; } /** * Compte le nombre d'articles d'un utilisateur pour un type de post donné * * Cette fonction compte combien d'articles d'un type spécifique * sont associés à un utilisateur donné via le champ custom 'authors'. * * @param int $userID L'ID de l'utilisateur * @param string $postType Le type de post à compter (ex: 'articles') * @return int Le nombre d'articles trouvés */ function count_user_articles($userID, $postType) { $args = array( 'post_type' => $postType, 'meta_query' => array( array( 'key' => 'authors', 'value' => '"' . $userID . '"', 'compare' => 'LIKE', ), ), ); $query = new WP_Query($args); return $query->found_posts; } /** * Génère un sommaire automatique à partir du contenu d'un post * * Cette fonction analyse les blocs Gutenberg d'un post pour extraire * tous les titres de niveau 2 (h2) et créer un index de navigation. * Elle génère des ancres automatiques pour chaque titre. * * @param int $postID L'ID du post à analyser * @return array Tableau associatif contenant les éléments du sommaire * Chaque élément contient: title, anchor, level */ function build_sommaire_from_content($postID) { $blocks = parse_blocks(get_the_content($postID)); $titleBlocks = array_filter( $blocks, function ($block) { // Vérifier si c'est un bloc heading if ($block['blockName'] !== 'core/heading') { return false; } // Extraire le niveau depuis le HTML si les attributs sont vides if (empty($block['attrs']['level'])) { // Chercher seulement h2 dans le HTML if (preg_match('/]*>/i', $block['innerHTML'], $matches)) { return true; } return false; } // Utiliser le niveau des attributs s'il existe return $block['attrs']['level'] === 2; } ); $outputIndex = []; foreach ($titleBlocks as $block) { $title = strip_tags($block['innerHTML']); // Extraire le niveau depuis le HTML ou les attributs $level = $block['attrs']['level'] ?? null; if (!$level && preg_match('/]*>/i', $block['innerHTML'], $matches)) { $level = 2; } if ($level !== 2) continue; $anchor = $block['attrs']['idName'] ?? sanitize_title($title); // Ajouter un préfixe si l'ancre commence par un chiffre if (!empty($anchor) && preg_match('/^[0-9]/', $anchor)) { $anchor = 'anchor-' . $anchor; } $outputIndex[] = [ 'title' => $title, 'anchor' => $anchor, 'level' => $level, ]; } return $outputIndex; } /** * Construit un index des notes de bas de page à partir du contenu HTML * * Cette fonction analyse le contenu HTML pour détecter les liens de notes * de bas de page (avec classe 'footnote-reference') et extrait leur contenu * pour créer un index numéroté des notes. * * @param string $content Le contenu HTML à analyser * @return array Tableau des notes de bas de page avec clé, ancre et contenu */ function build_footnotes_index_from_content($content) { if (empty($content)) { return []; } $footnotes = []; // Trouve les balises avec classe footnote-reference ET attribut data-footnote-content (ordre flexible) $pattern = '/]*class="[^"]*footnote-reference[^"]*"[^>]*data-footnote-content="([^"]*)"[^>]*>|]*data-footnote-content="([^"]*)"[^>]*class="[^"]*footnote-reference[^"]*"[^>]*>/i'; $has_footnotes = preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); if ($has_footnotes) { foreach ($matches as $index => $match) { // Le contenu peut être dans match[1] ou match[2] selon l'ordre des attributs $footnote_content = !empty($match[1]) ? $match[1] : $match[2]; if (!empty($footnote_content)) { $footnotes[] = array( 'key' => $index + 1, 'anchorID' => $index + 1, 'content' => $footnote_content ); } } } return $footnotes; } /** * Ajoute des identifiants uniques aux liens de notes de bas de page et supprime le texte [NOTE] * * Cette fonction parcourt le contenu HTML et : * 1. Ajoute un attribut id unique à chaque lien ayant la classe "footnote-reference" * 2. Supprime le texte [NOTE] à l'intérieur des balises de footnotes * Cela permet de créer des ancres de navigation et d'afficher uniquement le numéro via CSS. * * @param string $content Le contenu HTML à traiter * @return string Le contenu modifié avec les IDs ajoutés et le texte [NOTE] supprimé */ function apply_footnotes_urls_to_content($content) { $post_type = get_post_type(); if ($post_type !== 'articles' && !is_admin()) return $content; // Compter les références de notes de bas de page $footnote_count = 0; // Utiliser preg_replace_callback pour traiter chaque occurrence individuellement $content = preg_replace_callback( '/]*class="[^"]*footnote-reference[^"]*"[^>]*)>/i', function ($matches) use (&$footnote_count) { $footnote_count++; return ''; }, $content ); // Supprimer tout le contenu textuel à l'intérieur des balises avec classe footnote-reference $content = preg_replace( '/]*class="[^"]*footnote-reference[^"]*"[^>]*)>.*?<\/a>/i', '', $content ); return $content; } add_filter('the_content', 'apply_footnotes_urls_to_content', 10); /** * Récupère le nombre de likes d'un post * * Cette fonction utilitaire récupère le compteur de likes stocké * dans les métadonnées d'un post. Retourne 0 si aucun like n'existe. * * @param int $post_id L'ID du post * @return int Le nombre de likes (0 si aucun) */ function get_post_likes_count($post_id) { $likes_count = get_post_meta($post_id, 'likes_count', true); return $likes_count ? intval($likes_count) : 0; } /** * Affiche le nombre de likes d'un post avec formatage * * Cette fonction utilitaire formate l'affichage du compteur de likes * avec une icône optionnelle et la gestion du pluriel. * * @param int $post_id L'ID du post * @param bool $show_icon Afficher l'icône cœur (défaut: true) * @return string Le texte formaté (ex: "❤️ 5 likes" ou "3 like") */ function display_likes_count($post_id, $show_icon = true) { $likes_count = get_post_likes_count($post_id); $icon = $show_icon ? '❤️ ' : ''; return $icon . $likes_count . ' like' . ($likes_count > 1 ? 's' : ''); } /** * Construit les URL de partage pour un post * * Cette fonction génère les URL de partage pour un post spécifique. * Elle retourne un tableau associatif contenant les URL de partage pour Facebook, Twitter-X et LinkedIn. * * @return array Tableau associatif contenant les URL de partage */ function build_share_urls() { $post_id = get_the_ID(); $postUrl = get_permalink($post_id); $postTitle = get_the_title($post_id); $facebookUrl = 'https://www.facebook.com/sharer.php?u=' . $postUrl; $twitterUrl = 'https://twitter.com/intent/tweet?text=' . $postTitle . '&url=' . get_the_permalink(get_the_id()); $linkedInUrl = 'https://www.linkedin.com/feed/?shareActive=true&text=' . $postTitle . ' ' . $postUrl; return array( 'Facebook' => $facebookUrl, 'Twitter-X' => $twitterUrl, 'Linkedin' => $linkedInUrl ); } /** * Génère les métadonnées Open Graph pour le partage sur les réseaux sociaux */ function generate_og_meta_tags() { global $post; // URL canonique $og_url = is_home() ? home_url() : get_permalink(); // Titre if (is_home() || is_front_page()) { $og_title = get_bloginfo('name'); $og_description = get_bloginfo('description'); } elseif (is_single() || is_page()) { $og_title = get_the_title(); $og_description = get_the_excerpt(); if (empty($og_description)) { $og_description = wp_trim_words(strip_tags(get_the_content()), 30); } } elseif (is_category()) { $og_title = single_cat_title('', false) . ' - ' . get_bloginfo('name'); $og_description = category_description(); } elseif (is_tag()) { $og_title = single_tag_title('', false) . ' - ' . get_bloginfo('name'); $og_description = tag_description(); } elseif (is_archive()) { $og_title = get_the_archive_title() . ' - ' . get_bloginfo('name'); $og_description = get_the_archive_description(); } else { $og_title = wp_get_document_title(); $og_description = get_bloginfo('description'); } // Nettoyer la description $og_description = wp_strip_all_tags($og_description); $og_description = str_replace(array("\r", "\n", "\t"), ' ', $og_description); $og_description = wp_trim_words($og_description, 30); // Image Open Graph fixe $og_image = get_stylesheet_directory_uri() . '/resources/img/og/dynamiques-og.png'; // Type de contenu $og_type = is_single() ? 'article' : 'website'; // Nom du site $site_name = get_bloginfo('name'); // Auteur (pour les articles) $og_author = ''; if (is_single()) { $og_author = get_the_author_meta('display_name'); } // Date de publication (pour les articles) $published_time = ''; $modified_time = ''; if (is_single()) { $published_time = get_the_date('c'); $modified_time = get_the_modified_date('c'); } // Générer les balises meta $meta_tags = array(); // Open Graph $meta_tags[] = ''; $meta_tags[] = ''; $meta_tags[] = ''; $meta_tags[] = ''; $meta_tags[] = ''; if (!empty($og_image)) { $meta_tags[] = ''; $meta_tags[] = ''; } if (!empty($og_author)) { $meta_tags[] = ''; } if (!empty($published_time)) { $meta_tags[] = ''; } if (!empty($modified_time)) { $meta_tags[] = ''; } // Twitter Cards $meta_tags[] = ''; $meta_tags[] = ''; $meta_tags[] = ''; if (!empty($og_image)) { $meta_tags[] = ''; } // Meta description standard $meta_tags[] = ''; // Canonical URL $meta_tags[] = ''; return implode("\n\t", $meta_tags); } /** * Génère un extrait de recherche avec le terme surligné * * Cette fonction cherche le terme de recherche dans le contenu du post * et retourne un extrait avec le terme mis en évidence. * * @param int $post_id L'ID du post * @param string $search_term Le terme à rechercher et surligner * @param int $context_length Nombre de caractères de contexte autour du terme (défaut: 150) * @return string L'extrait avec le terme surligné en HTML */ function get_search_snippet($post_id, $search_term, $context_length = 150) { if (empty($search_term)) { return wp_trim_words(get_the_excerpt($post_id), 30); } // Récupérer le contenu complet $content = get_post_field('post_content', $post_id); $title = get_the_title($post_id); // Nettoyer le contenu des balises HTML et shortcodes $content = wp_strip_all_tags($content); $content = strip_shortcodes($content); // Recherche insensible à la casse $search_term_escaped = preg_quote($search_term, '/'); $position = stripos($content, $search_term); // Si le terme n'est pas trouvé dans le contenu, chercher dans le titre if ($position === false) { $position_in_title = stripos($title, $search_term); if ($position_in_title !== false) { // Si trouvé dans le titre, retourner juste le début du contenu $snippet = substr($content, 0, $context_length * 2); $snippet = wp_trim_words($snippet, 30); // Surligner dans le titre et le snippet $highlighted_title = preg_replace( '/(' . $search_term_escaped . ')/i', '$1', $title ); return '' . $highlighted_title . ': ' . $snippet; } // Si vraiment pas trouvé, retourner l'excerpt normal return wp_trim_words(get_the_excerpt($post_id), 30); } // Calculer les positions de début et fin de l'extrait $start = max(0, $position - $context_length); $end = min(strlen($content), $position + strlen($search_term) + $context_length); // Extraire le snippet $snippet = substr($content, $start, $end - $start); // Ajouter des points de suspension si nécessaire if ($start > 0) { $snippet = '...' . $snippet; } if ($end < strlen($content)) { $snippet = $snippet . '...'; } // Surligner tous les termes de recherche (supporte les recherches multiples) $search_terms = explode(' ', $search_term); foreach ($search_terms as $term) { if (strlen(trim($term)) > 2) { // Ignorer les mots trop courts $term_escaped = preg_quote(trim($term), '/'); $snippet = preg_replace( '/(' . $term_escaped . ')/i', '$1', $snippet ); } } return $snippet; } /** * Helper sécurisé pour récupérer le focal point (avec fallback si plugin désactivé) * * @param int|null $post_id ID du post (null pour le post courant) * @return array Tableau avec 'x' et 'y' (valeurs entre 0 et 1) */ function safe_get_thumbnail_focal_point($post_id = null) { // Vérifier si la fonction du plugin existe if (function_exists('get_thumbnail_focal_point')) { return get_thumbnail_focal_point($post_id); } // Fallback : centre de l'image par défaut return ['x' => 0.5, 'y' => 0.5]; } /** * Helper sécurisé pour récupérer le focal point au format CSS (avec fallback si plugin désactivé) * * @param int|null $post_id ID du post (null pour le post courant) * @return string Format "50% 50%" pour object-position ou background-position */ function safe_get_thumbnail_focal_point_css($post_id = null) { // Vérifier si la fonction du plugin existe if (function_exists('get_thumbnail_focal_point_css')) { return get_thumbnail_focal_point_css($post_id); } // Fallback : centre de l'image par défaut return '50% 50%'; }