Unverified Commit 1c65c7ad authored by Alex Pott's avatar Alex Pott
Browse files

feat: #3366561 Support preloading of fonts in the libraries API

By: catch
By: vbouchet
By: rikki_iki
By: berdir
By: finnsky
By: grimreaper
parent 8d660fef
Loading
Loading
Loading
Loading
Loading
+1 −1
Changes for core/core.services.yml: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -1445,7 +1445,7 @@ services:
    arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager']
  html_response.attachments_processor:
    class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor
    arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager']
    arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager', '@file_url_generator']
  html_response.subscriber:
    class: Drupal\Core\EventSubscriber\HtmlResponseSubscriber
    arguments: ['@html_response.attachments_processor']
+36 −0
Changes for core/lib/Drupal/Core/Asset/AssetResolver.php: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -175,6 +175,42 @@ protected function filterLibrariesByType(array $libraries, string $asset_type):
    return $libraries;
  }

  /**
   * {@inheritdoc}
   */
  public function getFontAssets(AttachedAssetsInterface $assets, ?LanguageInterface $language = NULL): array {
    if (!$assets->getLibraries()) {
      return [];
    }
    // Get the complete list of libraries to load including dependencies.
    $libraries_to_load = $this->getLibrariesToLoad($assets, 'fonts');

    if (!$libraries_to_load) {
      return [];
    }
    if (!isset($language)) {
      $language = $this->languageManager->getCurrentLanguage();
    }
    // Add the active theme name to the cache key since active themes may
    // implement hook_library_info_alter().
    $active_theme = $this->themeManager->getActiveTheme()->getName();
    $cid = 'fonts:' . $active_theme . ':' . $language->getId() . Crypt::hashBase64(serialize($libraries_to_load));
    if ($cached = $this->cache->get($cid)) {
      return $cached->data;
    }
    $fonts = [];
    foreach ($libraries_to_load as $library) {
      [$extension, $name] = explode('/', $library, 2);
      $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
      foreach ($definition['fonts'] as $font) {
        $fonts[] = $font;
      }
    }
    $this->cache->set($cid, $fonts, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);

    return $fonts;
  }

  /**
   * {@inheritdoc}
   */
+13 −0
Changes for core/lib/Drupal/Core/Asset/AssetResolverInterface.php: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -87,4 +87,17 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua
   */
  public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?LanguageInterface $language = NULL);

  /**
   * Returns the fonts for the current response's libraries.
   *
   * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
   *   The assets attached to the current response.
   * @param \Drupal\Core\Language\LanguageInterface $language
   *   (optional) The interface language the assets will be rendered with.
   *
   * @return array
   *   An array of font assets.
   */
  public function getFontAssets(AttachedAssetsInterface $assets, ?LanguageInterface $language = NULL): array;

}
+2 −2
Changes for core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -157,7 +157,7 @@ public function buildByExtension($extension) {
      if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings']) && !isset($library['dependencies'])) {
        throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension));
      }
      $library += ['dependencies' => [], 'js' => [], 'css' => []];
      $library += ['dependencies' => [], 'js' => [], 'css' => [], 'fonts' => []];

      if (isset($library['header']) && !is_bool($library['header'])) {
        throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension));
@@ -188,7 +188,7 @@ public function buildByExtension($extension) {
        ];
      }

      foreach (['js', 'css'] as $type) {
      foreach (['js', 'css', 'fonts'] as $type) {
        // Prepare (flatten) the SMACSS-categorized definitions.
        // @todo After Asset(ic) changes, retain the definitions as-is and
        //   properly resolve dependencies for all (css) libraries per category,
+25 −20
Changes for core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php: 25 added lines, 20 removed lines.
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
@@ -40,26 +41,6 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
   */
  protected $config;

  /**
   * Constructs a HtmlResponseAttachmentsProcessor object.
   *
   * @param \Drupal\Core\Asset\AssetResolverInterface $assetResolver
   *   An asset resolver.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   A config factory for retrieving required config objects.
   * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $cssCollectionRenderer
   *   The CSS asset collection renderer.
   * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $jsCollectionRenderer
   *   The JS asset collection renderer.
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler service.
   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
   *   The language manager.
   */
  public function __construct(
    protected AssetResolverInterface $assetResolver,
    ConfigFactoryInterface $config_factory,
@@ -69,7 +50,12 @@ public function __construct(
    protected RendererInterface $renderer,
    protected ModuleHandlerInterface $moduleHandler,
    protected LanguageManagerInterface $languageManager,
    protected ?FileUrlGeneratorInterface $fileUrlGenerator = NULL,
  ) {
    if (!isset($fileUrlGenerator)) {
      $this->fileUrlGenerator = \Drupal::service('file_url_generator');
      @trigger_error('Constructing HtmlResponseAttachmentsProcessor without a file url generator is deprecated in drupal:11.4.0 and the argument will be required in drupal:12.0.0. See https://www.drupal.org/project/drupal/issues/3366561', E_USER_DEPRECATED);
    }
    $this->config = $config_factory->get('system.performance');
  }

@@ -139,6 +125,25 @@ public function processAttachments(AttachmentsInterface $response) {
      $attached['library'] = $assets->getLibraries();
      $attached['drupalSettings'] = $assets->getSettings();

      // Collect fonts from libraries, and if they include fonts to preload, add
      // them to #attached['html_head_link'].
      $fonts = $this->assetResolver->getFontAssets($assets, $this->languageManager->getCurrentLanguage());
      foreach ($fonts as $font) {
        if ($font['preload']) {
          $extension = pathinfo($font['data'], PATHINFO_EXTENSION);
          $attached['html_head_link'][] = [
            [
              'href' => $this->fileUrlGenerator->generate($font['data'])->toString(),
              'rel' => 'preload',
              'as' => 'font',
              'type' => 'font/' . $extension,
              'crossorigin' => 'anonymous',
            ],
            FALSE,
          ];
        }
      }

      // Since we can only replace content in the HTML head section if there's a
      // placeholder for it, we can safely avoid processing the render array if
      // it's not present.
Loading