From 83690db7e9bb4524347bdcd97815fd534c92e0e4 Mon Sep 17 00:00:00 2001 From: "Quintino A. G. Souza" Date: Tue, 24 Mar 2026 09:55:13 -0300 Subject: [PATCH] Adiciona token [node:content-page-ancestors] para Pathauto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registra token do tipo array que resolve o caminho hierárquico completo de um content_page: domínio (alias de user/term/etc.) + slugs dos nós pai, do mais distante ao mais próximo. Permite padrão Pathauto como "[node:content-page-ancestors:join-path]/[node:title]". Co-Authored-By: Claude Sonnet 4.6 --- structural_pages.module | 133 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/structural_pages.module b/structural_pages.module index cab2f9a..2fc22ab 100644 --- a/structural_pages.module +++ b/structural_pages.module @@ -158,6 +158,12 @@ function structural_pages_token_info(): array { 'description' => t('The hierarchical path of the site section taxonomy (e.g., undergraduate/courses).'), ]; + $info['tokens']['node']['content-page-ancestors'] = [ + 'name' => t('Content Page Ancestors Path'), + 'description' => t('Array of ancestor path segments for a content_page node (domain + parent pages). Use [node:content-page-ancestors:join-path] in Pathauto patterns to get a slash-separated path (e.g., user/joao/pesquisa).'), + 'type' => 'array', + ]; + $info['tokens']['term']['hierarchy-path'] = [ 'name' => t('Hierarchy Path'), 'description' => t('The hierarchical path of the term including ancestors (e.g., institutional/news).'), @@ -178,6 +184,30 @@ function structural_pages_tokens(string $type, array $tokens, array $data, array if ($name === 'site-section-path') { $replacements[$original] = _structural_pages_get_section_path($node); } + if ($name === 'content-page-ancestors') { + $path = _structural_pages_get_content_page_ancestors($node); + $replacements[$original] = !empty($path) + ? array_values(array_filter(explode('/', $path))) + : ''; + } + } + + // Handle [node:content-page-ancestors:join-path] and other chained tokens. + // Pathauto's [array:join-path] cleans each segment individually and joins + // with '/', preserving slashes. The token ends in ':join-path]' which is + // in Pathauto's safe_tokens list, so the result is NOT re-cleaned. + if ($ancestors_tokens = \Drupal::token()->findWithPrefix($tokens, 'content-page-ancestors')) { + $path = _structural_pages_get_content_page_ancestors($node); + if (!empty($path)) { + $segments = array_values(array_filter(explode('/', $path))); + $replacements += \Drupal::token()->generate( + 'array', + $ancestors_tokens, + ['array' => $segments], + $options, + $bubbleable_metadata + ); + } } } @@ -234,6 +264,109 @@ function _structural_pages_get_section_path(NodeInterface $node): string { return implode('/', $path_parts); } +/** + * Gets the full ancestor path for a content_page node. + * + * Traverses the field_parent_page chain to the root, then prepends the domain + * entity's URL alias (from field_site_section). Intended as a Pathauto token + * prefix: configure the pattern as "[node:content-page-ancestors]/[node:title]". + * + * Examples: + * - Root page under user "joao" → "user/joao" + * - Child of "Pesquisa" under user "joao" → "user/joao/pesquisa" + * - Root page under term "graduacao/disciplinas" → "graduacao/disciplinas" + * + * @param \Drupal\node\NodeInterface $node + * The content_page node. + * + * @return string + * The ancestor path without leading/trailing slashes, or empty string. + */ +function _structural_pages_get_content_page_ancestors(NodeInterface $node): string { + if ($node->bundle() !== 'content_page') { + return ''; + } + + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + $alias_cleaner = \Drupal::service('pathauto.alias_cleaner'); + $path_alias_manager = \Drupal::service('path_alias.manager'); + + // Walk up the field_parent_page chain collecting parent titles. + $parent_slugs = []; + $visited = []; + $current = $node; + $max = 50; + + while ($max-- > 0) { + if (isset($visited[$current->id()])) { + break; + } + $visited[$current->id()] = TRUE; + + if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) { + // Reached the root content_page: resolve domain from field_site_section. + if (!$current->hasField('field_site_section') || $current->get('field_site_section')->isEmpty()) { + break; + } + + $fss = $current->get('field_site_section')->first(); + $domain_type = $fss->target_type ?? 'taxonomy_term'; + $domain_id = $fss->target_id; + $domain = \Drupal::entityTypeManager()->getStorage($domain_type)->load($domain_id); + + if (!$domain || !$domain->hasLinkTemplate('canonical')) { + break; + } + + // Build the system path for the domain entity. + // Constructed directly for known entity types to avoid URL generator + // context issues that can occur inside hook_tokens(). + $domain_system_path = match ($domain_type) { + 'user' => '/user/' . $domain_id, + 'taxonomy_term' => '/taxonomy/term/' . $domain_id, + default => (static function () use ($domain): string { + try { + return '/' . $domain->toUrl('canonical')->getInternalPath(); + } + catch (\Exception $e) { + return ''; + } + })(), + }; + + if (empty($domain_system_path)) { + break; + } + + // Resolve to alias if one exists. + $domain_alias = $path_alias_manager->getAliasByPath($domain_system_path); + $domain_path = ltrim($domain_alias, '/'); + + // Build: domain/parent2/parent1 (parents were collected innermost-first). + $parts = array_merge([$domain_path], array_reverse($parent_slugs)); + return implode('/', array_filter($parts)); + } + + // Collect this node's parent's title (we traverse before adding current). + $parent_field = $current->get('field_parent_page')->first(); + $parent_id = $parent_field->target_id ?? NULL; + if (!$parent_id) { + break; + } + + $parent = $node_storage->load($parent_id); + if (!$parent) { + break; + } + + // Add the parent's slug to the list (innermost first, reversed later). + $parent_slugs[] = $alias_cleaner->cleanString($parent->getTitle()); + $current = $parent; + } + + return ''; +} + /** * Gets the hierarchical path of a taxonomy term. *