When building the authoring experience in Drupal, its nice to add in a token browser for text fields and let users replace tokens with other fields from the content or site.

The token browser provides the ability to filter down to entity types, users, site and other things, but it shows ALL the tokens for those. I wanted to restrict the tokens shown from the node type to a specific subset to reduce complexity and make it less daunting for users using the token browser, which has a huge number of options, even on a brand new site.

Standard token browser

 

After some research (ok, Googling), I couldn't find a built in way, so had to look further into how the browser is built for opportunities to enhance the token selection. 

The method I went with is to provide a custom TreeBuilder, extended from the base Drupal one, then replace it in the service container, which is documented very well here: https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/altering-existing-services-providing-dynamic-services.

Using this technique allowed presentation of a much smaller, more specific set of tokens for the target users.

If you want to try this out, this is how you do it with a custom module and form_alter.

Create a new module directory and add an .info.yml file

my_module.info.yml

name: My Module
type: module 
description: Provide an alternative tree builder with filtering.
core_version_requirement: ^10 
dependencies: { }

Create the my_module.services.yml file which will replace the original tree builder with the new one.

my_module.services.yml

services:
  token.tree_builder:
    class: Drupal\my_module\Service\MyTreeBuilder
    arguments: ['@token', '@token.entity_mapper', '@cache.data', '@language_manager']

Create the class that extends the existing tree builder and adds to the existing functionality by building a list of things to be filtered, using the parent class buildRenderable function and then filtering out the extra unneeded parts.

This replaces the core functionality, so should be backwards compatible, or will break things in other places.

my_module/src/Service/MyTreeBuilder.php​​​

<?php  

declare(strict_types=1);  

namespace Drupal\my_module\Service;  

use Drupal\token\TreeBuilder;  

/**
* Provide our own Token TreeBuilder.
*
* Allow fields for nodes with 'node%field_prefix'
* Remove fields with 'node!field_prefix'
*/
class MyTreeBuilder extends TreeBuilder {  

  /**
   * Filter token output to only provide a subset of tokens.
   *
   * {@inheritdoc}
   */
  public function buildRenderable(array $token_types, array $options = []) {
    $new_token_types = $filters = $removals = [];  

    // Build the list of token types for the parent.
    foreach ($token_types as $token_type) {
      if (str_contains($token_type, '%')) {
        [$token_type, $filter] = explode('%', $token_type);
        $filters[$token_type][] = $filter;
      }  

      if (str_contains($token_type, '!')) {
        [$token_type, $filter] = explode('!', $token_type);
        $removals[$token_type][] = $filter;
      }  

      $new_token_types[$token_type] = $token_type;
    }  

    // Get the full list of tokens.
    $token_types = $new_token_types;
    $tree = parent::buildRenderable($token_types, $options);  

    // Filter to only the allowed things now.
    foreach ($filters as $type => $list) {
      foreach ($tree['#token_tree'][$type]['tokens'] as $token => $details) {
        foreach ($list as $filter) {
          if (str_contains($token, $filter)) {
            continue 2;
          }
        }
        unset($tree['#token_tree'][$type]['tokens'][$token]);
      }
    }  

    // Finally take out any specific removals.
    foreach ($removals as $type => $list) {
      foreach ($tree['#token_tree'][$type]['tokens'] as $token => $details) {
        foreach ($list as $filter) {
          if (str_contains($token, $filter)) {
            unset($tree['#token_tree'][$type]['tokens'][$token]);
          }
        }
      }
    }  

    return $tree;
  }

}

The last step is to add the token browser to a field. This is example usage in a form_alter to add the filtered token tree builder which allows fields stating with `field_calc_`, `field_syn_` and then removes fields starting with `field_calc_syn_`.

// Add helpful token tree, even for template. 
$form['field_token_help']['token_help'] = [ 
  '#type' => 'container', 
  'token_link' => [ 
    '#token_types' => [ 
      'user',
      'site',
      'node%field_calc_', 
      'node%field_job_', 
      'node!field_calc_job_', 
    ],
    '#theme' => 'token_tree_link', 
    '#show_restricted' => FALSE, 
    '#show_nested' => FALSE, 
    '#global_types' => FALSE, 
    '#click_insert' => TRUE, 
    '#recursion_limit' => 1, 
    '#weight' => 90,
  ], 
];

References:
https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/altering-existing-services-providing-dynamic-services