utilikit-1.0.0/utilikit.module

utilikit.module
<?php

/**
 * @file
 * Main module file for UtiliKit - Dynamic Utility-First CSS for Drupal.
 *
 * This module provides utility-first CSS functionality for Drupal sites,
 * offering dynamic CSS generation, multiple rendering modes (inline/static),
 * content scanning for utility class discovery, and comprehensive caching.
 *
 * Key features include:
 * - Dynamic utility class CSS generation and injection
 * - Static and inline rendering modes for performance optimization
 * - Automatic content scanning for utility class discovery
 * - Entity-based auto-updates for real-time CSS synchronization
 * - Comprehensive caching and performance optimizations
 * - Admin interface for configuration and monitoring
 * - Development tools and debugging capabilities
 */

declare(strict_types=1);

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\node\NodeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\utilikit\Service\UtilikitConstants;

/**
 * Implements hook_help().
 *
 * Provides help information for the UtiliKit module by rendering the
 * README.md file when accessed through the help system.
 */
function utilikit_help(string $route_name, RouteMatchInterface $route_match): ?string {
  if ($route_name === 'help.page.utilikit') {
    return _utilikit_helper_render_readme();
  }
  return NULL;
}

/**
 * Helper function to render README.md for the help system.
 *
 * Attempts to render the module's README.md file using the Markdown filter
 * if available, otherwise falls back to plain text display with HTML
 * escaping for security.
 *
 * @return string
 *   The rendered content of README.md, either as processed Markdown or
 *   as escaped HTML in a <pre> tag.
 */
function _utilikit_helper_render_readme(): string {
  $readme_path = __DIR__ . '/README.md';
  $text = file_get_contents($readme_path);

  if ($text === FALSE) {
    return t('README.md file not found.');
  }

  if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
    return '<pre>' . htmlspecialchars($text) . '</pre>';
  }

  // Use the Markdown filter to render the README.
  $filter_manager = \Drupal::service('plugin.manager.filter');
  $settings = \Drupal::config('markdown.settings')->getRawData();
  $filter = $filter_manager->createInstance('markdown', ['settings' => $settings]);
  return $filter->process($text, 'en')->getProcessedText();
}

/**
 * Implements hook_page_attachments().
 *
 * Attaches UtiliKit libraries, settings, and CSS to pages based on the
 * current configuration and scope settings. Handles both static and inline
 * rendering modes, attaches appropriate JavaScript libraries, and sets up
 * drupalSettings for client-side functionality.
 *
 * The function performs several key operations:
 * - Validates scope and applies UtiliKit only where configured
 * - Handles static CSS file generation and linking
 * - Configures JavaScript settings for client-side utilities
 * - Attaches development and debugging libraries when enabled
 * - Sets up cache contexts and tags for proper invalidation
 */
function utilikit_page_attachments(array &$attachments): void {
  if (!\Drupal::config('utilikit.settings')->get('rendering_mode')) {
    return;
  }

  $config = \Drupal::service('utilikit.service')->getSettings();

  $settings = [
    'scope_global' => (bool) $config->get('scope_global'),
    'disable_admin' => (bool) $config->get('disable_admin'),
    'scope_content_types' => (bool) $config->get('scope_content_types'),
    'enabled_content_types' => $config->get('enabled_content_types') ?? [],
  ];

  if (!utilikit_should_apply($settings)) {
    return;
  }

  $activeBreakpoints = array_keys(array_filter($config->get('active_breakpoints') ?? UtilikitConstants::DEFAULT_BREAKPOINTS));
  $rendering_mode = $config->get('rendering_mode') ?? 'inline';

  if ($rendering_mode === 'static') {
    $fileManager = \Drupal::service('utilikit.file_manager');
    $fileManager->ensureStaticCssFile();

    $css_url = $fileManager->getStaticCssUrl();
    if (!$css_url) {
      \Drupal::logger('utilikit')->warning('Static CSS file not found. Falling back to inline mode.');
      $rendering_mode = 'inline';
    }
  }
  elseif ($rendering_mode === 'head') {
    $stateManager = \Drupal::service('utilikit.state_manager');
    $knownClasses = $stateManager->getKnownClasses();

    if (!empty($knownClasses)) {
      $css = $stateManager->getGeneratedCss();

      if (empty($css)) {
        $cssGenerator = \Drupal::service('utilikit.css_generator');
        $css = $cssGenerator->generateCssFromClasses($knownClasses);
        $stateManager->setGeneratedCss($css);
      }

      if (!empty($css)) {
        // Apply optimization if enabled (same as static mode)
        if ($config->get('optimize_css')) {
          $fileManager = \Drupal::service('utilikit.file_manager');
          $css = $fileManager->minifyCss($css);
        }

        $attachments['#attached']['html_head'][] = [
          [
            '#type' => 'html_tag',
            '#tag' => 'style',
            '#value' => $css,
            '#attributes' => ['id' => 'utilikit-head-mode'],
            '#weight' => -100,
          ],
          'utilikit_head_css',
        ];
      }
    }
  }

  $stateManager = \Drupal::service('utilikit.state_manager');

  $attachments['#attached']['drupalSettings']['utilikit'] = [
    'devMode' => (bool) $config->get('dev_mode'),
    'showPageErrors' => (bool) $config->get('show_page_errors'),
    'enableTransitions' => (bool) $config->get('enable_transitions'),
    'debounce' => (int) ($config->get('debounce') ?? 50),
    'logLevel' => $config->get('log_level') ?? 'warnings',
    'adminPreview' => (bool) $config->get('admin_preview'),
    'activeBreakpoints' => $activeBreakpoints,
    'renderingMode' => $rendering_mode,
    'csrfToken' => \Drupal::csrfToken()->get('utilikit-update-css'),
    'cssTimestamp' => $stateManager->getCssTimestamp(),
    'breakpoints' => UtilikitConstants::BREAKPOINT_VALUES,
  ];

  if ($rendering_mode === 'static') {
    $css_url = \Drupal::service('utilikit.file_manager')->getStaticCssUrl();

    $attachments['#attached']['library'][] = 'utilikit/utilikit.static';

    $attachments['#attached']['html_head'][] = [
      [
        '#type' => 'html_tag',
        '#tag' => 'link',
        '#attributes' => [
          'href' => $css_url,
          'rel' => 'stylesheet',
          'id' => 'utilikit-static-css',
          'media' => 'all',
        ],
        '#weight' => -100,
      ],
      'utilikit_static_css',
    ];
  }
  elseif ($rendering_mode === 'head') {
    $attachments['#attached']['library'][] = 'utilikit/utilikit.static';
  }
  else {
    $attachments['#attached']['library'][] = 'utilikit/utilikit.engine';
  }

  if ($config->get('show_page_errors')) {
    $attachments['#attached']['library'][] = 'utilikit/utilikit.engine.message';
  }

  if ($config->get('dev_mode')) {
    $attachments['#attached']['library'][] = 'utilikit/utilikit.debug';
  }

  if (\Drupal::currentUser()->hasPermission('use utilikit update button')
    && in_array($rendering_mode, ['static', 'head'], TRUE)) {
    $attachments['#attached']['library'][] = 'utilikit/utilikit.update.button';

    $attachments['#attached']['drupalSettings']['utilikit']['updateButton'] = [
      'enabled' => TRUE,
      'autoUpdate' => [
        'node' => (bool) $config->get('update_on_node_save'),
        'block' => (bool) $config->get('update_on_block_save'),
        'paragraph' => (bool) $config->get('update_on_paragraph_save'),
      ],
    ];
  }

  // CSP nonce only needed for inline mode (dynamic JS injection)
  if ($rendering_mode === 'inline') {
    $nonce = Crypt::randomBytesBase64(16);
    $attachments['#attached']['drupalSettings']['utilikit']['cspNonce'] = $nonce;
  }

  $attachments['#cache']['tags'][] = UtilikitConstants::CACHE_TAG_CONFIG;
  $attachments['#cache']['tags'][] = UtilikitConstants::CACHE_TAG_CSS;
  $attachments['#cache']['contexts'][] = 'user.permissions';
  $attachments['#cache']['contexts'][] = 'route';
  $attachments['#cache']['tags'][] = 'utilikit:mode:' . $rendering_mode;
}

/**
 * Implements hook_entity_presave().
 *
 * Automatically scans entities for utility classes and updates CSS when
 * auto-update is enabled for the entity type. Handles both static and
 * inline rendering modes with appropriate locking for static mode to
 * prevent concurrent CSS file operations.
 *
 * For static mode, uses a lock-and-queue system to handle concurrent
 * updates gracefully. If a lock cannot be acquired, the update is queued
 * for later processing to avoid blocking entity saves.
 */
function utilikit_entity_presave($entity): void {
  $config = \Drupal::config('utilikit.settings');
  $entity_type = $entity->getEntityTypeId();

  if (!isset(UtilikitConstants::AUTO_UPDATE_ENTITY_TYPES[$entity_type])) {
    return;
  }

  $setting_key = UtilikitConstants::AUTO_UPDATE_ENTITY_TYPES[$entity_type];
  $auto_update_enabled = (bool) $config->get($setting_key);

  if (!$auto_update_enabled) {
    return;
  }

  if (!$entity instanceof FieldableEntityInterface) {
    return;
  }

  $serviceProvider = \Drupal::service('utilikit.service_provider');
  $classes = $serviceProvider->getContentScanner()->scanEntity($entity);

  if (empty($classes)) {
    return;
  }

  $mode = $config->get('rendering_mode') ?? 'inline';

  if (in_array($mode, ['static', 'head'], TRUE)) {
    $lock = \Drupal::lock();
    $locked = $lock->acquire(UtilikitConstants::LOCK_CSS_UPDATE, UtilikitConstants::CSS_UPDATE_LOCK_WAIT);

    if (!$locked) {
      $queue = \Drupal::queue(UtilikitConstants::QUEUE_CSS_PROCESSOR);
      $queue->createItem([
        'classes' => $classes,
        'entity_type' => $entity->getEntityTypeId(),
        'entity_id' => $entity->id(),
        'timestamp' => time(),
      ]);

      \Drupal::messenger()->addWarning(t(
        'UtiliKit CSS update is in progress. Your content was saved, but styles will be updated shortly.'
      ));

      return;
    }

    try {
      utilikit_update_classes($classes);
    } finally {
      $lock->release(UtilikitConstants::LOCK_CSS_UPDATE);
    }
  }
  else {
    utilikit_update_classes($classes);
  }
}

/**
 * Determines whether UtiliKit should be applied to the current page.
 *
 * Evaluates scope configuration settings to determine if UtiliKit should
 * be active on the current page. Handles global scope, admin route
 * exclusions, content type restrictions, and special module routes.
 *
 * @param array $config
 *   Configuration array containing:
 *   - 'scope_global': Whether global scope is enabled
 *   - 'disable_admin': Whether to disable on admin routes
 *   - 'scope_content_types': Whether content type scoping is enabled
 *   - 'enabled_content_types': Array of enabled content type machine names.
 *
 * @return bool
 *   TRUE if UtiliKit should be applied to the current page, FALSE otherwise.
 */
function utilikit_should_apply(array $config): bool {
  $route_name = \Drupal::routeMatch()->getRouteName();
  $utilikit_submodule_routes = [
    'utilikit_playground.page',
    'utilikit_examples.page',
    'utilikit_test.suite',
  ];

  if (in_array($route_name, $utilikit_submodule_routes, TRUE)) {
    return FALSE;
  }

  if ($config['scope_global']) {
    if ($config['disable_admin'] && \Drupal::service('router.admin_context')->isAdminRoute()) {
      return FALSE;
    }
    return TRUE;
  }

  if ($config['scope_content_types']) {
    $route_match = \Drupal::routeMatch();
    $node = $route_match->getParameter('node');

    if ($node instanceof NodeInterface) {
      $enabled_types = $config['enabled_content_types'] ?? [];
      return in_array($node->bundle(), $enabled_types, TRUE);
    }
  }

  return FALSE;
}

/**
 * Cleans up static CSS files and clears all related caches.
 *
 * Utility function that removes all UtiliKit static CSS files from the
 * file system and clears all caches to ensure a clean state. Used during
 * mode switches and troubleshooting operations.
 */
function utilikit_cleanup_static_files(): void {
  \Drupal::service('utilikit.file_manager')->cleanupStaticFiles();
  \Drupal::service('utilikit.cache_manager')->clearAllCaches();
}

/**
 * Invalidates CSS-related caches without affecting other cache types.
 *
 * Targeted cache invalidation function that clears only CSS-related caches,
 * leaving other caches intact for better performance during CSS updates.
 */
function utilikit_invalidate_caches(): void {
  \Drupal::service('utilikit.cache_manager')->clearCssCaches();
}

/**
 * Updates UtiliKit CSS with new utility classes.
 *
 * Central function for processing utility classes and updating the CSS
 * generation. Includes logic to prevent recursive calls when invoked
 * from entity presave operations.
 *
 * @param array $classes
 *   Array of utility class names to process and add to the CSS.
 * @param bool $force
 *   If TRUE, bypasses the recursive call check. Defaults to FALSE.
 */
function utilikit_update_classes(array $classes, bool $force = FALSE): void {
  if (!$force) {
    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
    foreach ($backtrace as $call) {
      if (isset($call['function']) && $call['function'] === 'utilikit_entity_presave') {
        return;
      }
    }
  }

  $serviceProvider = \Drupal::service('utilikit.service_provider');
  $serviceProvider->updateCssAndFile($classes);
}

/**
 * Implements hook_theme().
 *
 * Defines theme templates provided by the UtiliKit module.
 */
function utilikit_theme() {
  return [
    'utilikit_update_button' => [
      'variables' => [],
      'template' => 'utilikit-update-button',
    ],
  ];
}

/**
 * Implements hook_utilikit_cleanup_entity_types_alter().
 *
 * Allows other modules to alter the list of entity types that should be
 * processed during cleanup operations. This is primarily used for
 * excluding specific entity types from automatic scanning and processing.
 *
 *   Array of entity type machine names to be processed during cleanup.
 *   Modules can add or remove entity types by modifying this array.
 */
function utilikit_utilikit_cleanup_entity_types_alter(&$entity_types) {
}

/**
 * Determines whether an entity should be scanned for utility classes.
 *
 * Evaluates whether a given entity should be included in content scanning
 * operations. Excludes test entities and example content by default, and
 * allows other modules to alter the exclusion logic through a hook.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity to evaluate for scanning inclusion.
 *
 * @return bool
 *   TRUE if the entity should be scanned, FALSE if it should be excluded.
 */
function utilikit_should_scan_entity(EntityInterface $entity) {
  if (str_starts_with($entity->getEntityTypeId(), 'utilikit_test')) {
    return FALSE;
  }

  if ($entity->getEntityTypeId() === 'node' && $entity->bundle() === 'utilikit_example') {
    return FALSE;
  }

  $exclude = FALSE;
  \Drupal::moduleHandler()->alter('utilikit_cleanup_exclude_entity', $exclude, $entity);

  return !$exclude;
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc