bs_base-8.x-1.x-dev/bs_base.bs_base.install

bs_base.bs_base.install
<?php

use Drupal\bs_lib\ThemeTools;
use Drush\Drush;
use Psr\Log\LogLevel;

/**
 * @file
 * Update hooks for bs_base base themes.
 */

/**
 * Set bs_base.maintenance_image configuration value.
 */
function bs_base_bs_update_8000($target_theme_name) {
  echo "To set the default maintenance image, run drush vset $target_theme_name.settings bs_base.maintenance_image.path themes/contrib/bs_base/images/maintenance.svg\n";
}

/**
 * Update node js packages, remove gulp-rucksack and gulp-sourcemaps.
 */
function bs_base_bs_update_8001($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      '"autoprefixer": ".*"' => '"autoprefixer": "^9.7.3"',
      '"deepmerge": ".*"' => '"deepmerge": "^3.3.0"',
      '"del": ".*"' => '"del": "^4.1.1"',
      '"eslint": ".*"' => '"eslint": "^5.16.0"',
      '"gulp": ".*"' => '"gulp": "^4.0.2"',
      '"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "^1.6.0"',
      // Remove gulp-rucksack.
      '^\s*"gulp-rucksack": ".*",\n' => '',
      '"gulp-sass-glob": ".*"' => '"gulp-sass-glob": "^1.1.0"',
      // Remove gulp-sourcemaps.
      '^\s*"gulp-sourcemaps": ".*",\n' => '',
      '"js-yaml": ".*"' => '"js-yaml": "^3.13.1"',
      '"postcss-scss": ".*"' => '"postcss-scss": "^1.0.6"',
      '"pump": ".*"' => '"pump": "^2.0.1"',
      '"stylelint-config-standard": ".*"' => '"stylelint-config-standard": "^18.3.0"',
      '"stylelint-order": ".*"' => '"stylelint-order": "^2.2.1"',
      '"stylelint-scss": ".*"' => '"stylelint-scss": "^3.13.0"',
      '"stylelint-selector-bem-pattern": ".*"' => '"stylelint-selector-bem-pattern": "^2.1.0"'
    ]);
  }
}

/**
 * Add gulp-debug and through2 packages.
 */
function bs_base_bs_update_8002($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      '"devDependencies": {' => "\"devDependencies\": {\n    \"gulp-debug\": \"^4.0.0\",\n    \"through2\": \"^3.0.1\",",
    ]);
  }
}

/**
 * Add build-css package.json script.
 */
function bs_base_bs_update_8003($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      // Add a script part.
      '^(?=.*devDependencies)[^{]+{[^}]+}' => "$0," . "\n". '  "scripts": {'. "\n". '    "build-css": "((type pnpm && pnpm install) || npm install) && npx gulp clean:css && npx gulp sass"' . "\n" . '  }',
    ]);
  }
}

/**
 * Drupal 9 compatibility update.
 */
function bs_base_bs_update_8004($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/' . $target_theme_name . '.info.yml', [
      'core: 8.x' => 'core_version_requirement: ^8.7.7 || ^9',
    ]);
  }
}

/**
 * Update node libs.
 */
function bs_base_bs_update_8005($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/.browserslistrc', [
      'last 2 version' => 'defaults',
      "Safari >= 8\n" => '',
      "not bb < 10 # means Support only latest Blackberry 10\n" => '',
    ]);

    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulpfile.js', [
      'yaml\.safeLoad' => 'yaml.load',
    ]);

    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      '"autoprefixer": ".*"' => '"autoprefixer": "^10.2.6"',
      '"deepmerge": ".*"' => '"deepmerge": "^4.2.2"',
      '"del": ".*"' => '"del": "^6.0.0"',
      '"eslint": ".*"' => '"eslint": "^7.28.0"',
      '"gulp-changed": ".*"' => '"gulp-changed": "^4.0.3"',
      '"gulp-if": ".*"' => '"gulp-if": "^3.0.0"',
      '"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "^2.0.7"',
      '"gulp-notify": ".*"' => '"gulp-notify": "^4.0.0"',
      '"gulp-postcss": ".*"' => '"gulp-postcss": "^9.0.0"',
      '"gulp-sass": ".*"' => '"gulp-sass": "^4.1.0"',
      '"js-yaml": ".*"' => '"js-yaml": "^4.1.0"',
      '"postcss-reporter"' => '"postcss": "^8.3.4",' . "\n" . '    "postcss-reporter"',
      '"postcss-reporter": ".*"' => '"postcss-reporter": "^7.0.2"',
      '"postcss-scss": ".*"' => '"postcss-scss": "^4.0.0"',
      '"pump": ".*"' => '"pump": "^3.0.0"',
      '"stylelint": ".*"' => '"stylelint": "^13.13.1"',
      '"stylelint-config-standard": ".*"' => '"stylelint-config-standard": "^22.0.0"',
      '"stylelint-order": ".*"' => '"stylelint-order": "^4.1.0"',
      '"stylelint-scss": ".*"' => '"stylelint-scss": "^3.19.0"',
      // Remove through2 - replaced with native Node.js Transform streams
      '^\s*"through2": ".*",?\n' => ''
    ]);
  }
}

/**
 * Check and fix bad update of bs_bootstrap to version 4.6.0.
 */
function bs_base_bs_update_8006($target_theme_name) {
  // Detect if target theme is updated to non-existent 'bs_bootstrap: 8005'
  // while actual bootstrap version used is 4.3.1.
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    $yml_content = file_get_contents($themes_info[$target_theme_name]->pathname);
    if ($yml_content && (strpos($yml_content, 'bs_bootstrap: 8004') || strpos($yml_content, 'bs_bootstrap: 8005'))) {
      $package_content = file_get_contents($themes_info[$target_theme_name]->subpath . '/package.json');
      if ($package_content && strpos($package_content, '"bootstrap": "~4.3.1",')) {
        // Fix update version info.
        $yml_content = str_replace('bs_bootstrap: 8004', 'bs_bootstrap: 8003', $yml_content);
        $yml_content = str_replace('bs_bootstrap: 8005', 'bs_bootstrap: 8003', $yml_content);
        file_put_contents($themes_info[$target_theme_name]->pathname, $yml_content);

        // Notify user that he needs to execute update one more time.
        drush_log("Bad update is fixed, please run `drush bs-tu $target_theme_name` one more time to update theme correctly.", LogLevel::ALERT);
      }
    }
  }
}

/**
 * Remove node plugins that we do not use.
 */
function bs_base_bs_update_8007($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      "^.*\"eslint.*\n" => '',
      "^.*\"gulp-cached.*\n" => '',
      "^.*\"gulp-changed.*\n" => '',
      "^.*\"gulp-if.*\n" => '',
      "^.*\"gulp-notify.*\n" => '',
      "^.*\"gulp-sass-glob.*\n" => '',
      "^.*\"gulp-svgstore.*\n" => '',
      "^.*\"gulp-uglify.*\n" => '',
      "^.*\"node-sass-import-once.*\n" => '',
      "^.*\"postcss-scss.*\n" => '',
      "^.*\"pump.*\n" => '',
    ]);
  }
}

/**
 * Update node js packages.
 */
function bs_base_bs_update_8008($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      '"autoprefixer": ".*"' => '"autoprefixer": "^10.4.5"',
      '"gulp-postcss": ".*"' => '"gulp-postcss": "^9.0.1"',
      '"gulp-sass": ".*"' => '"gulp-sass": "^4.1.1"',
      '"postcss": ".*"' => '"postcss": "^8.4.13"',
      '"postcss-reporter": ".*"' => '"postcss-reporter": "^7.0.5"',
      '"stylelint-scss": ".*"' => '"stylelint-scss": "^3.21.0"',
      '"stylelint-selector-bem-pattern": ".*"' => '"stylelint-selector-bem-pattern": "^2.1.1"',
    ]);
  }
}

/**
 * Remove IE11 support.
 */
function bs_base_bs_update_8009($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/.browserslistrc', [
      '^#?ie >= 11\n' => '',
    ]);
  }
}

/**
 * Update node js packages.
 */
function bs_base_bs_update_8010($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    // Dependency update.
    $updates = [
      '"autoprefixer": ".*"' => "\"@hail2u/css-mqpacker\": \"github:hail2u/node-css-mqpacker#v9.0.1\",\n    \"autoprefixer\": \"^10.4.13\"",
      // Remove css-mqpacker in favor on new @hail2u/css-mqpacker.
      '^\s*"css-mqpacker": ".*",\n' => '',
      '"bootstrap": ".*"' => '"bootstrap": "~4.6.2"',
      '"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "^2.0.8"',
      '"gulp-sass": ".*"' => '"gulp-sass": "^5.1.0"',
      '"postcss": ".*"' => "\"node-sass\": \"^8.0.0\",\n    \"postcss\": \"^8.4.21\"",
      '"stylelint": ".*"' => '"stylelint": "^14.16.1"',
      '"stylelint-config-standard": ".*"' => '"stylelint-config-standard": "^29.0.0"',
      '"stylelint-order": ".*"' => '"stylelint-order": "^6.0.1"',
      '"stylelint-scss": ".*"' => '"stylelint-scss": "^4.3.0"',
    ];
    // If node is already defined just update to version 18.
    if (_bs_base_regexp_exist($themes_info[$target_theme_name]->subpath . '/package.json', '"node": ".*"')) {
      $updates['"node": ".*"'] = '"node": "18"';
    }
    else {
      $updates['"license": "GPL",'] = "\"license\": \"GPL\",\n  \"engines\": {\n    \"node\": \"18\"\n  },";
    }
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', $updates);

    // gulp-sass 5 breaking change.
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulp-tasks.js', [
      '\(gulp, plugins, options\)' => '(gulp, sass, plugins, options)',
    ]);
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulpfile.js', [
      'merge = require\(\'deepmerge\'\);' => "merge = require('deepmerge');\nconst sass = require('gulp-sass')(require('node-sass'));",
      '\(gulp, plugins, options\)' => '(gulp, sass, plugins, options)'
    ]);

    // Copy new .npmrc and .nvmrc from bs_bootstrap theme.
    $options = [
      'parent_machine_name' => 'bs_bootstrap',
      'parent_path' => _bs_base_drupal_get_theme_path('bs_bootstrap'),
      'child_machine_name' => $target_theme_name,
      'child_path' => _bs_base_drupal_get_theme_path($target_theme_name),
    ];
    foreach (['.npmrc', '.nvmrc'] as $file) {
      _bs_base_copy_file($file, $options);
    }

    // Remove node_modules so, we are sure we install/rebuild everything clean.
    system('rm -rf ' . escapeshellarg($themes_info[$target_theme_name]->subpath . '/node_modules'));
  }
}

/**
 * Add caniuse-lite update into theme build process.
 */
function bs_base_bs_update_8011($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      'npx gulp clean:css' => "npx browserslist@latest --update-db && npx gulp clean:css",
    ]);
  }
}

/**
 * Remove gulp-plumber, styleint and gulp-sass-lint, update other node libs.
 */
function bs_base_bs_update_8012($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    // Dependency updates.
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      // Packages updates.
      '"autoprefixer": ".*"' => '"autoprefixer": "^10.4.14"',
      '"deepmerge": ".*"' => '"deepmerge": "^4.3.0"',
      '"del": ".*"' => '"del": "^6.1.1"',
      // Packages removal.
      '^\s*"gulp-plumber": ".*",\n' => '',
      '^\s*"gulp-sass-lint": ".*",\n' => '',
      '^\s*"stylelint": ".*",\n' => '',
      '^\s*"stylelint-config-standard": ".*",\n' => '',
      '^\s*"stylelint-order": ".*",\n' => '',
      '^\s*"stylelint-scss": ".*",\n' => '',
      '^\s*"stylelint-selector-bem-pattern": ".*",\n' => '',
    ]);

    // Remove stylelint configuration.
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulp-options.yml', [
      '# Stylelint config.*\n.*\n.*\.stylelintrc\.yml\'' => '',
    ]);

    // Update other node dependencies in a package-lock.json or pnpm-lock.yaml.
    $target_path = _bs_base_drupal_get_theme_path($target_theme_name);
    exec('cd ' . $target_path . ' && ((type pnpm && pnpm update) || npm update)');
  }
}

/**
 * Remove pnpm support.
 */
function bs_base_bs_update_8013($target_theme_name) {
  $themes_info = _bs_base_drupal_theme_list_info();
  if (isset($themes_info[$target_theme_name])) {
    _bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
      // Packages updates.
      '\(\(type pnpm \&\& pnpm install\) \|\| npm install\)' => 'npm install',
    ]);

    system('rm -f ' . escapeshellarg($themes_info[$target_theme_name]->subpath . '/pnpm-lock.yaml'));

    // Remove node_modules so, we are sure we install/rebuild everything clean.
    system('rm -rf ' . escapeshellarg($themes_info[$target_theme_name]->subpath . '/node_modules'));
  }
}

/**
 * Convert theme-options.yml vars to SASS vars and remove theme-options.yml.
 */
function bs_base_bs_update_200001($target_theme_name) {
  $themes_info = ThemeTools::drupalThemeListInfo();
  if (isset($themes_info[$target_theme_name])) {
    // Convert theme-options.yml SASS vars to native SASS vars.
    $theme_path = $themes_info[$target_theme_name]->subpath;
    $theme_options_file = $theme_path . '/theme-options.yml';
    if (file_exists($theme_options_file)) {
      $sass_content = _bs_base_yaml_variables_to_sass($theme_options_file);

      // Check if sass section exists with injectVariables
      if (!empty($sass_content)) {
        $file_content = "// Converted from theme-options.yml\n\n" . $sass_content;

        // Create variables directory if it doesn't exist
        $variables_dir = $theme_path . '/sass/variables';
        if (!is_dir($variables_dir)) {
          mkdir($variables_dir, 0755, TRUE);
        }

        // Write SASS variables to file.
        $sass_file = $variables_dir . '/_' . $target_theme_name . '.scss';
        if (file_exists($sass_file)) {
          // If SASS vars file already exist join converted SASS code with old
          // code.
          $existing_content = file_get_contents($sass_file);
          $file_content .= "\n\n\n// Original content.\n\n" . $existing_content;
        }

        file_put_contents($sass_file, $file_content);
        Drush::logger()->info("Successfully converted YAML variables to SASS for $target_theme_name theme.");
      }
      else {
        Drush::logger()->warning("No variables section found in theme-options.yml for $target_theme_name theme.");
      }

      // Delete theme-options.yml file after conversion.
      unlink($theme_options_file);
    }
    else {
      Drush::logger()->warning("No theme-options.yml file found for $target_theme_name theme.");
    }

    // Remove gulp-sass-inject package.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/package.json', [
      '\s*"gulp-sass-inject": ".*",' => '',
    ]);

    // Remove the theme-options.yml JS code block from gulpfile.js.
    $theme_gulp_file = $theme_path . '/gulpfile.js';
    $js = file_get_contents($theme_gulp_file);
    $replace_count = 0;
    $js = preg_replace(
      '#\n// Load theme options if theme-options\.yml file exist and merge it with.*?\n}\n#s',
      '',
      $js,
      -1,
      $replace_count
    );
    if ($replace_count === 1) {
      file_put_contents($theme_gulp_file, $js);
    }
    else {
      Drush::logger()->warning("Failed to remove theme-options.yml JS code in $theme_gulp_file file.");
    }
  }
}

/**
 * Converts a YAML file's `variables:` section into SASS variables.
 *
 * - Supports nested maps (up to 3 levels deep).
 * - Preserves comments and blank lines.
 * - Skips YAML lists (not supported).
 *
 * @param string $yamlPath
 *   Path to YAML file.
 * @return string
 *   Converted YAML vars to SASS vars.
 */
function _bs_base_yaml_variables_to_sass(string $yamlPath): string {
  $lines = file($yamlPath);
  $inVariables = FALSE;
  $variablesIndent = NULL;
  $sass = [];
  $stack = [];

  foreach ($lines as $line) {
    // Detect start of 'variables:' section.
    if (!$inVariables && preg_match('/^\s*variables:\s*$/', $line)) {
      $inVariables = TRUE;
      $variablesIndent = strlen($line) - strlen(ltrim($line)) + 2;
      continue;
    }

    if ($inVariables) {
      $indent = strlen($line) - strlen(ltrim($line));
      $trimmed = trim($line);

      // Check empty lines and line comments.
      if ($trimmed === '' || str_starts_with($trimmed, '#')) {
        if (str_starts_with($trimmed, '#')) {
          // Replace leading # with //
          $sass[] = preg_replace('/^#/', '$1//', $trimmed);
        }
        else {
          // Close maps if needed.
          while (count($stack) > 0) {
            array_pop($stack);
            $sass[] = str_repeat('  ', count($stack)) . (count($stack) > 0 ? '),' : ');');
          }

          $sass[] = '';
        }
        continue;
      }

      // Check for a key without value (start of a map).
      if (preg_match('/^(\s*)([^\s:]+):\s*$/', $line, $m)) {
        $key = $m[2];
        $depth = ($indent - $variablesIndent) / 2;

        // Close map if needed.
        while (count($stack) > $depth) {
          array_pop($stack);
          $sass[] = str_repeat('  ', count($stack)) . (count($stack) > 0 ? '),' : ');');
        }

        // Add new map opening.
        if (count($stack) === 0) {
          $sass[] = str_repeat('  ', $depth) . "\$$key: (";
        }
        else {
          $sass[] = str_repeat('  ', $depth) . "$key: (";
        }

        $stack[] = $key;
        continue;
      }

      // Handle key-value with optional inline comment.
      if (preg_match('/^\s*([^\s:]+):\s+([\'"]?.+?[\'"]?)\s*(#.*)?$/', $line, $m)) {
        $key = $m[1];
        $val = trim($m[2], "'\""); // Remove YAML quotes
        $comment = $m[3] ?? '';
        $depth = ($indent - $variablesIndent) / 2;

        $lineOut = str_repeat('  ', $depth);
        if (empty($stack)) {
          $lineOut .= "\$$key: $val;";
        } else {
          $lineOut .= "$key: $val,";
        }

        if ($comment) {
          $comment = preg_replace('/^#/', '$1//', $comment);
          $lineOut .= " $comment";
        }

        $sass[] = $lineOut;
        continue;
      }

      // Unknown line structure, just keep it.
      $sass[] = $line;
    }
  }

  // Close any remaining open maps.
  while (!empty($stack)) {
    array_pop($stack);
    $sass[] = str_repeat('  ', count($stack)) . (count($stack) > 0 ? '),' : ');');
  }

  return implode("\n", $sass);
}

/**
 * Deprecate bs_bootstrap.
 */
function bs_base_bs_update_200002($target_theme_name) {
  $themes_info = ThemeTools::drupalThemeListInfo();
  if (isset($themes_info[$target_theme_name])) {
    // Theme info file.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/' . $target_theme_name . '.info.yml', [
      // Remove bs_bootstrap update version.
      '\s*bs_bootstrap: 8009' => '',
      // /themes/contrib/bs_base/themes/bs_bootstrap => /themes/contrib/bs_base
      '/' . $themes_info['bs_bootstrap']->subpath . '/css' => '/' . $themes_info['bs_base']->subpath . '/css',
      // All other bs_bootstrap to bs_base.
      'bs_bootstrap' => 'bs_base',
    ]);

    // README.md, gulpfile.js: bs_bootstrap => bs_base.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/README.md', [
      'bs_bootstrap' => 'bs_base',
    ]);
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/gulpfile.js', [
      'bs_bootstrap' => 'bs_base',
    ]);

    // Config install bs_bootstrap: => bs_base:.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/config/install/' . $target_theme_name . '.settings.yml', [
      'bs_bootstrap:' => 'bs_base:',
    ]);

    // Config scheme bs_bootstrap.settings => bs_base.settings.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/config/schema/' . $target_theme_name . '.schema.yml', [
      'bs_bootstrap\.settings' => 'bs_base.settings',
    ]);

    // Theme file bs_bootstrap => bs_base.
    $theme_file = $themes_info[$target_theme_name]->subpath . '/' . $target_theme_name . '.theme';
    if (file_exists($theme_file)) {
      ThemeTools::regexpFile($theme_file, [
        'bs_bootstrap' => 'bs_base',
      ]);
    }

    // All SASS files bs_bootstrap => bs_base.
    $iterator = new \RecursiveIteratorIterator(
      new \RecursiveDirectoryIterator(DRUPAL_ROOT . '/' . $themes_info[$target_theme_name]->subpath . '/sass')
    );
    foreach ($iterator as $file) {
      if ($file->isFile() && $file->getExtension() === 'scss') {
        ThemeTools::regexpFile($file->getPathname(), [
          'bs_bootstrap' => 'bs_base',
        ]);
      }
    }

    // gulp-options.yml remove bs_bootstrap section from parentTheme.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/gulp-options.yml', [
      "\s*-\s*name: 'bs_bootstrap'(?:\n\s*#.*)?\n.*" => '',
    ]);

    // Templates folder - extending bs_bootstrap and attaching bs_bootstrap libs.
    $iterator = new \RecursiveIteratorIterator(
      new \RecursiveDirectoryIterator(DRUPAL_ROOT . '/' . $themes_info[$target_theme_name]->subpath . '/templates')
    );
    foreach ($iterator as $file) {
      if ($file->isFile()) {
        ThemeTools::regexpFile($file->getPathname(), [
          'bs_bootstrap' => 'bs_base',
        ]);
      }
    }

    // We changed parent theme definition from bs_bootstrap to bs_base so we
    // need to refresh theme info data.
    ThemeTools::drupalThemeListInfo(TRUE);

    Drush::logger()->warning('Make sure to clear Drupal cache after this update!');
  }
}

/**
 * Switch to icons font auto hashing.
 */
function bs_base_bs_update_200003($target_theme_name) {
  $themes_info = ThemeTools::drupalThemeListInfo();
  if (isset($themes_info[$target_theme_name])) {
    // fantasticon/scss.hbs append SASS var $bs-icon-font-hash to the end.
    $file_contents = file_get_contents($themes_info[$target_theme_name]->subpath . '/fantasticon/scss.hbs');
    if ($file_contents !== FALSE) {
      $file_contents .= "\n" . '$bs-icon-font-hash: "__ICON_FONT_HASH__" !default;' . "\n";
      file_put_contents($themes_info[$target_theme_name]->subpath . '/fantasticon/scss.hbs', $file_contents);
    }

    // package.json update build-icons step.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/package.json', [
      '"build-icons": "fantasticon"' => '"build-icons": "node build-icons.js"',
    ]);

    // Copy build-icons.js from bs_base to target theme.
    $options = [
      'parent_machine_name' => 'bs_base',
      'parent_path' => ThemeTools::drupalGetThemePath('bs_base'),
      'child_machine_name' => $target_theme_name,
      'child_path' => ThemeTools::drupalGetThemePath($target_theme_name),
    ];
    ThemeTools::copyFile('build-icons.js', $options);

    // Check if $bs-icon-font-hash was used in target theme and if yes remove
    // it definition.
    $iterator = new \RecursiveIteratorIterator(
      new \RecursiveDirectoryIterator(DRUPAL_ROOT . '/' . $themes_info[$target_theme_name]->subpath . '/sass/variables')
    );
    foreach ($iterator as $file) {
      if ($file->isFile() && $file->getExtension() === 'scss') {
        ThemeTools::regexpFile($file->getPathname(), [
          '\$bs-icon-font-hash:.*\n' => '',
        ]);
      }
    }
  }
}

/**
 * Do not automatically update caniuse-lite db anymore.
 */
function bs_base_bs_update_200004($target_theme_name) {
  $themes_info = ThemeTools::drupalThemeListInfo();
  if (isset($themes_info[$target_theme_name])) {
    // package.json update steps changes.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/package.json', [
      // Remove: "build-browsers": "npx browserslist@latest --update-db",
      '^\s+"build-browsers": "npx browserslist@latest --update-db",\n' => '',
      // Remove 'npm run build-browsers' from build step and add a new
      // 'update-browserslist' step after it.
      '"build": "npm run build-install & npm run build-icons &\s*npm run build-browsers & npm run build-css"' => '"build": "npm run build-install & npm run build-icons & npm run build-css",'. "\n" . '    "update-browserslist": "npx update-browserslist-db@latest"',
    ]);

    // Show an example how to execute browserlist-db update in the future
    // from update function when needed.
    ThemeTools::updateBrowserList($target_theme_name);
  }
}

/**
 * Optimize CSS @font-face loading.
 */
function bs_base_bs_update_200005($target_theme_name) {
  $themes_info = ThemeTools::drupalThemeListInfo();
  if (isset($themes_info[$target_theme_name])) {
    // Remove: `@import "bs_base/sass/theme/partials/fonts";` line with or
    // without comment on the start of the line.
    ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/sass/theme/theme.scss', [
      '^(?:\/\/\s+)?@import\s+"bs_base\/sass\/theme\/partials\/fonts";\n' => '',
    ]);
  }
}

/**
 * Migrate to Bootstrap 5.3.8 and update dependencies.
 */
function bs_base_bs_update_200006($target_theme_name) {
  $themes_info = ThemeTools::drupalThemeListInfo();
  if (empty($themes_info[$target_theme_name])) {
    return;
  }

  $theme_path = $themes_info[$target_theme_name]->subpath;

  // Update Bootstrap and related dependencies.
  ThemeTools::regexpFile($theme_path . '/package.json', [
    // Update Node.js version requirement.
    '"node": "18"' => '"node": "20"',

    // Add Popper.js for Bootstrap 5, update to Bootstrap 5.3.8, update other
    // dependencies and switch to exact versions to prevent accidental updates to
    //insecure npm versions.
    '"@twbs/fantasticon": ".*"' => '"@popperjs/core": "2.11.8",' . "\n" . '    "@twbs/fantasticon": "3.1.0"',
    '"bootstrap": ".*"' => '"bootstrap": "5.3.8"',
    '"autoprefixer": ".*"' => '"autoprefixer": "10.4.21"',
    '"deepmerge": ".*"' => '"deepmerge": "4.3.1"',
    '"del": ".*"' => '"del": "6.1.1"',
    '"gulp": ".*"' => '"gulp": "4.0.2"',
    '"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "2.0.8"',
    '"gulp-postcss": ".*"' => '"gulp-postcss": "10.0.0"',
    '"gulp-sass": ".*"' => '"gulp-sass": "5.1.0"',
    '"gulp-strip-css-comments": ".*"' => '"gulp-strip-css-comments": "2.0.0"',
    '"js-yaml": ".*"' => '"js-yaml": "4.1.0"',
    '"postcss": ".*"' => '"postcss": "8.5.6"',
    '"postcss-reporter": ".*"' => '"postcss-reporter": "7.1.0"',
    '"node-sass": ".*"' => '"node-sass": "9.0.0"',

    // Remove gulp-debug - replaced with native Node.js Transform streams.
    '^\s*"gulp-debug": ".*",?\n' => '',

    // Remove deprecated/unused packages.
    '^\s*"@hail2u/css-mqpacker": ".*",\n' => '',
    '^\s*"font-awesome": ".*",\n' => '',
    '^\s*"gulp-concat": ".*",\n' => '',
    '^\s*"picocolors": ".*",\n' => '',
    // Make sure through2 is removed no matter is it last or in the middle.
    '^\s*"through2": ".*",\n' => '',
    ',\n^\s*"through2": ".*"\n' => '',

    // Fix build script to use sequential execution.
    '"build": "npm run build-install & npm run build-icons & npm run build-css"' => '"build": "npm run build-install && npm run build-icons && npm run build-css"',
  ]);

  // Remove css-mqpacker from gulp-options.yml.
  ThemeTools::regexpFile($theme_path . '/gulp-options.yml', [
    '^\s*- "@hail2u/css-mqpacker"\n' => '',
  ]);

  // Remove css-mqpacker from gulp-tasks.js if it exists.
  $gulp_tasks_file = $theme_path . '/gulp-tasks.js';
  if (file_exists($gulp_tasks_file)) {
    ThemeTools::regexpFile($gulp_tasks_file, [
      ',\s*plugins\.hail2u\.cssMqpacker\(\{sort:\s*true\}\)' => '',
    ]);
  }

  // Rename bs_lib/messages to bs_lib/alert. for twig and yaml files.
  // No need to do replacement in theme.info.yml file, ti will be done by
  // generator update when it detects missing library and a new library in
  // parent theme.
  ThemeTools::regexpFiles($theme_path, '*.twig', [
    'bs_lib/messages' => 'bs_lib/alert',
  ]);

  ThemeTools::regexpFile($theme_path . '/' . $target_theme_name . '.libraries.yml', [
    // navigation.css is removed and parts are mvoed to nav and navbar libs.
    '\s*css/components/navigation.css: {}' => '',
    // pagination.css is moved from global styling to pagination lib.
    '\s*css/components/pagination.css: {}' => '',
  ]);
  // Notify s that sass/components/navigation.scss is removed, and they
  // needs to manually delete this file and move any custom SASS imports to
  // either nav or navbar.scss.
  Drush::logger()->warning("Notice that {$theme_path}/sass/components/navigation.scss is removed. Please remove this file and move any custom SASS imports to sass/components/nav.scss or sass/components/navbar.scss.");

  // Update Bootstrap 5 data attributes in template files.
  $template_files = glob($theme_path . '/{**/*,*}.twig');
  foreach ($template_files as $file) {
    $content = file_get_contents($file);
    $original_content = $content;

    // Update data attributes
    $content = str_replace('data-toggle=', 'data-bs-toggle=', $content);
    $content = str_replace('data-target=', 'data-bs-target=', $content);
    $content = str_replace('data-dismiss=', 'data-bs-dismiss=', $content);

    // Update screen reader classes
    $content = str_replace('class="sr-only"', 'class="visually-hidden"', $content);
    $content = str_replace("class='sr-only'", "class='visually-hidden'", $content);

    // Update close button classes
    $content = str_replace('class="close"', 'class="btn-close"', $content);
    $content = str_replace("class='close'", "class='btn-close'", $content);

    // Update dropdown positioning classes
    $content = str_replace('dropdown-menu-right', 'dropdown-menu-end', $content);
    $content = str_replace('dropdown-menu-left', 'dropdown-menu-start', $content);

    // Remove deprecated input-group wrapper classes (simplified for safety)
    $content = str_replace('input-group-prepend', '', $content);
    $content = str_replace('input-group-append', '', $content);

    if ($content !== $original_content) {
      file_put_contents($file, $content);
    }
  }
  // Update JavaScript files too with data attrib changes.
  $js_files = glob($theme_path . '/js/*.js');
  foreach ($js_files as $file) {
    $content = file_get_contents($file);
    $original_content = $content;

    // Update data attributes in JS
    $content = str_replace('data-dismiss', 'data-bs-dismiss', $content);
    $content = str_replace('data-toggle', 'data-bs-toggle', $content);
    $content = str_replace('data-target', 'data-bs-target', $content);

    // Update close button class and remove inner span
    $content = preg_replace('/class="close"/', 'class="btn-close"', $content);
    $content = preg_replace('/<span aria-hidden="true">&times;<\/span>\s*<\/button>/', '</button>', $content);

    // Update sr-only class
    $content = str_replace('sr-only', 'visually-hidden', $content);

    if ($content !== $original_content) {
      file_put_contents($file, $content);
    }
  }

  // Inject import functions into SASS init.
  $sass_import_functions = ThemeTools::getThemeSASSImportFunctions($target_theme_name);
  if (!empty($sass_import_functions)) {
    // Inject on first empty line after first comments.
    if (ThemeTools::regexpExist($theme_path . '/sass/_init.scss', '\A((?://[^\n]*\n)+)(\n)')) {
      ThemeTools::regexpFile($theme_path . '/sass/_init.scss', [
        '\A((?://[^\n]*\n)+)(\n)' => '$1' . "\n" . $sass_import_functions . "\n\n",
      ], 1);
    }
    // Or if this empty line does not exist inject on the start.
    else {
      ThemeTools::regexpFile($theme_path . '/sass/_init.scss', [
        '\A' => $sass_import_functions . "\n\n",
      ], 1);
    }
  }

  ThemeTools::regexpFile($theme_path . '/config/install/' . $target_theme_name . '.settings.yml', [
    // Remove custom_forms setting.
    '\s*custom_forms: .*' => '',
  ]);

  // Bootstrap 5 SASS breaking changes - imports, functions, and variables
  // changes.

  // Update variable names, classes, etc in SASS.
  ThemeTools::regexpFiles($theme_path . '/sass', '*.scss', [
    // Remove Bootstrap imports that no longer exist in Bootstrap 5.
    '^\s*@import\s*"bootstrap/scss/code";\s*\n' => '',
    '^\s*@import\s*"bootstrap/scss/custom-forms";\s*\n' => '',
    '^\s*@import\s*"bootstrap/scss/print";\s*\n' => '',
    '^\s*@import\s*"bootstrap/scss/utilities/text";\s*\n' => '',
    '^\s*@import\s*"bootstrap/scss/utilities/background";\s*\n' => '',
    '^\s*@import\s*"bootstrap/scss/utilities/sizing";\s*\n' => '',

    // Fix Bootstrap 5 import path changes
    '@import\s*"bootstrap/scss/utilities/screenreaders(\.scss)?"' => '@import "bootstrap/scss/helpers/visually-hidden"',
    '@import\s*"bootstrap/scss/utilities/stretched-link"' => '@import "bootstrap/scss/helpers/stretched-link"',
    '@import\s*"bootstrap/scss/input-group"' => '@import "bootstrap/scss/forms/input-group"',

    // Heading vars.
    '\$font-size-h1' => '$h1-font-size',
    '\$font-size-h2' => '$h2-font-size',
    '\$font-size-h3' => '$h3-font-size',
    '\$font-size-h4' => '$h4-font-size',
    '\$font-size-h5' => '$h5-font-size',
    '\$font-size-h6' => '$h6-font-size',

    // Forms.
    '\$form-group-margin-bottom' => '$form-item-margin-y',
    '\$custom-control-indicator-bg' => '$form-check-input-bg',
    '\$custom-control-label-disabled-color' => '$form-check-input-disabled-color',
    '\$custom-select-bg' => '$form-select-bg',
    '\$custom-select-border-radius' => '$form-select-border-radius',
    '\$custom-select-disabled-color' => '$form-select-disabled-color',
    '\$custom-select-focus-box-shadow' => '$form-select-focus-box-shadow',
    '\$custom-select-indicator-color' => '$form-select-indicator-color',
    '\$custom-select-bg-size' => '$form-select-bg-size',
    '\$custom-select-indicator' => '$form-select-indicator',
    '\$custom-select-indicator-padding' => '$form-select-indicator-padding',
    '\.custom-control-label' => '.form-check-label',
    '\.custom-control-input' => '.form-check-input',
    '\.custom-select' => '.form-select',
    '\.custom-file-control' => '.form-control',
    '\.form-group' => '.form-item',
    // $custom-input-button key border-hover-color is renamed.
    '\'border-hover-color\'' => '\'hover-border-color\'',

    // Spacing utilities.
    '\.ml-' => '.ms-',
    '\.mr-' => '.me-',
    '\.pl-' => '.ps-',
    '\.pr-' => '.pe-',

    // Text alignment.
    '\.text-left' => '.text-start',
    '\.text-right' => '.text-end',

    // Float utilities.
    '\.float-left' => '.float-start',
    '\.float-right' => '.float-end',

    // Border utilities.
    '\.border-left' => '.border-start',
    '\.border-right' => '.border-end',

    // Table utilities.
    '\$table-cell-padding;' => '$table-cell-padding-x;',

    // Text utilities.
    '\.text-monospace' => '.font-monospace',
    '\.font-weight-' => '.fw-',
    '\.font-italic' => '.fst-italic',

    // Button utilities.
    '\.btn-block' => '.d-grid',

    // Replace deprecated SASS mixins and function with pure CSS, Bootstrap 5 or
    // more supported SASS versions.
    '@include hover\(\)' => '&:hover',
    '@include hover-focus\(\)' => '&:hover, &:focus',
    '@include plain-hover-focus\(\)' => '&:hover, &:focus',
    '@include hover-focus-active\(\)' => '&:hover, &:focus, &:active',
    '@include hover {' => '&:hover {',
    '@include hover-focus {' => '&:hover, &:focus {',
    'color-yiq\(' => 'color-contrast(',
    // darken(color, amt) → adjust-color(color, $lightness: -amt)
    '(?<![\w-])darken\(\s*((?:[^(),]++|(?<par>\((?:[^()]++|(?&par))*\)))++)\s*,\s*((?:[^()]++|(?&par))++)\s*\)' => 'adjust-color($1, \$lightness: -$3)',
    // lighten(color, amt) → adjust-color(color, $lightness: amt)
    '(?<![\w-])lighten\(\s*((?:[^(),]++|(?<par>\((?:[^()]++|(?&par))*\)))++)\s*,\s*((?:[^()]++|(?&par))++)\s*\)' => 'adjust-color($1, \$lightness: $3)',
    '@elseif' => '@else if',
    '@include sr-only' => '@include visually-hidden',
    '@include bs-icon-var\(bs-icon-content\(([\'"][^\'\"]+[\'"])\)\);' => '@include bs-icon-create-content($1);',
    '@include fa-icon\(' => '@include bs-icon-font(',

    // Replace theme-color-level function with shift-color
    'theme-color-level\(' => 'shift-color(',
    '\$alert-bg-level' => '$alert-bg-scale',
    'border-left-radius\(' => 'border-start-radius(',
    'border-right-radius\(' => 'border-end-radius(',

    // Replace all division operator `/` with `divide` BS5 function.
    // This pattern:
    // 0. Skips lines starting with `//` or containing aspect-ratio/grid-row/grid-column
    // 1. Uses negative lookbehind (?<![\/\w]) to avoid URLs
    // 2. Uses negative lookahead (?![\/\w]) to avoid file paths
    // 3. Matches full left-hand expressions (variables, numbers, parens, + - * ops)
    // 4. Restricts right-hand side to a single primary (var/number/paren), preserving operator order
    // 5. Handles Bootstrap’s shorthand cases like `0 2/3*$spacer-x`
    //
    // EXAMPLES IT HANDLES:
    // - $spacer / 2                   → divide($spacer, 2)
    // - $grid-gutter-width / 2        → divide($grid-gutter-width, 2)
    // - $font-size-base * 1.25 / 2    → divide($font-size-base * 1.25, 2)
    // - ($spacer * 3) / 4             → divide(($spacer * 3), 4)
    // - $spacer / ($ratio + 1)        → divide($spacer, ($ratio + 1))
    // - 0 2/3*$spacer-x               → 0 divide(2, 3)*$spacer-x
    //
    // NOTE - this is not perfect, some more complex combinations will either
    // not be converted or converted with error which will require manual
    // intervention/fix.
    '/(?m)^[ \t]*\/\/.*$(*SKIP)(*F)|^[ \t]*(?:aspect-ratio|grid-(?:column|row))\s*:[^\n]*$(*SKIP)(*F)|(?<![\/\w])((?:\$[\w-]+|\d+(?:\.\d+)?|\([^)]+\))(?:\s*[\+\-\*]\s*(?:\$[\w-]+|\d+(?:\.\d+)?|\([^)]+\)))*?)\s*\/\s*(-?\s*(?:\$[\w-]+|\d+(?:\.\d+)?|\([^)]+\)))(?![\/\w])/' => 'divide($1, $2)',
  ]);

  // Check for deprecated classes that need manual removal.
  $deprecated_classes = ['text-hide', 'rounded-sm', 'rounded-lg'];
  $warnings = [];
  foreach ($deprecated_classes as $class) {
    $escaped_class = '\.' . $class;

    // Check SASS files.
    $matching_sass_files = ThemeTools::regexpExistFiles($theme_path, '*.scss', $escaped_class);
    if (!empty($matching_sass_files)) {
      $warnings[] = "SASS: Found deprecated class '.$class' - manual removal required";
    }

    // Check templates too.
    $template_files = glob($theme_path . '/**/*.twig');
    foreach ($template_files as $file) {
      $content = file_get_contents($file);
      if (preg_match('/([\'"]\s*[^\'"]*)' . preg_quote($class, '/') . '([\s\'"])/i', $content)) {
        $relative_file = str_replace(DRUPAL_ROOT . '/', '', $file);
        $warnings[] = "TEMPLATE: Found deprecated class '$class' in $relative_file - manual removal required";
        break;
      }
    }
  }

  // Check for removed form variables that still need manual attention.
  $removed_vars = ['$custom-control-', '$custom-select-', '$custom-file-'];
  foreach ($removed_vars as $var) {
    $escaped_var = str_replace('$', '\$', $var);

    // Check SASS files.
    $matching_sass_files = ThemeTools::regexpExistFiles($theme_path, '*.scss', $escaped_var);
    if (!empty($matching_sass_files)) {
      $warnings[] = "SASS: Found removed Bootstrap 4 variable starting with: $var - manual migration required";
    }
  }

  if (!empty($warnings)) {
    Drush::logger()->warning("$target_theme_name contains deprecated classes/variables requiring manual attention:");
    foreach ($warnings as $warning) {
      Drush::logger()->warning($warning);
    }
    Drush::logger()->warning('See @link for guidance on manual migration.', ['@link' => 'https://www.drupal.org/docs/8/themes/bs-base/misc/upgrading-from-1x-to-20x-version']);
  }
}

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

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