Unverified Commit 75f04581 authored by Alex Pott's avatar Alex Pott
Browse files

fix: #3474070 Allow StarterKit forked themes with prefix

By: joelpittet
By: smustgrave
By: garphy
By: yannickoo
(cherry picked from commit 95b87751)
parent 69b779f7
Loading
Loading
Loading
Loading
Loading
+35 −2
Original line number Diff line number Diff line
@@ -148,22 +148,55 @@ protected function execute(InputInterface $input, OutputInterface $output): int
      'old' => self::namePatterns($starterkit->getName(), $starterkit->info['name']),
      'new' => self::namePatterns($destination_theme, $theme_label),
    ];

    // Generate unique placeholders for each pattern key in $patterns['old'] so
    // we can perform a two-pass replacement without collisions. This avoids
    // cases where a replacement introduces text that matches another pattern
    // (or where short labels like "Simple" overlap with class-name fragments),
    // which would otherwise cause accidental double replacements.
    $placeholders = [];
    foreach (array_keys($patterns['old']) as $key) {
      $placeholders[$key] = '__' . strtoupper($key) . '__';
    }

    $filesToEdit = self::createFilesFinder($tmpDir)
      ->contains(array_values($patterns['old']))
      ->notPath($starterkit_config['no_edit']);
    foreach ($filesToEdit as $file) {
      $contents = file_get_contents($file->getRealPath());
      $contents = str_replace($patterns['old'], $patterns['new'], $contents);
      // Step 1: Replace old patterns with placeholders in content.
      $contents = str_replace($patterns['old'], $placeholders, $contents);
      // Step 2: Replace placeholders with new patterns in content.
      $contents = str_replace($placeholders, $patterns['new'], $contents);

      // Normalize comment marker driven replacements. When the source theme
      // uses very short labels (for example "Simple") the generic
      // replacements above can map the text to the wrong placeholder because
      // both the label and class name fragments share the same literal value.
      // Unique, explicit marker lines make the intent unambiguous, so update
      // those lines after the generic pass.
      $markerReplacements = [
        '#@starterkit:machine_name' => $patterns['new']['machine_name'],
        '#@starterkit:label' => $patterns['new']['label'],
        '#@starterkit:machine_class_name' => $patterns['new']['machine_name_pascal'],
        '#@starterkit:label_class_name' => $patterns['new']['machine_name_pascal'],
      ];
      foreach ($markerReplacements as $marker => $replacement) {
        $pattern = '/(^\s*' . preg_quote($marker, '/') . '\s*\R)([^\r\n]*)/m';
        $contents = preg_replace($pattern, '$1' . $replacement, $contents);
      }
      file_put_contents($file->getRealPath(), $contents);
    }

    // Step 3: Repeat for file renaming.
    $filesToRename = self::createFilesFinder($tmpDir)
      ->name(array_map(static fn (string $pattern) => "*$pattern*", array_values($patterns['old'])))
      ->notPath($starterkit_config['no_rename']);
    foreach ($filesToRename as $file) {
      $filepath_segments = explode('/', $file->getRealPath());
      $filename = array_pop($filepath_segments);
      $filename = str_replace($patterns['old'], $patterns['new'], $filename);
      $filename = str_replace($patterns['old'], $placeholders, $filename);
      $filename = str_replace($placeholders, $patterns['new'], $filename);
      $filepath_segments[] = $filename;
      $filesystem->rename($file->getRealPath(), implode('/', $filepath_segments));
    }
+112 −8
Original line number Diff line number Diff line
@@ -45,6 +45,110 @@ protected function setUp(): void {
    chdir($this->getWorkingPath());
  }

  /**
   * Tests generating theme from a simple named Starterkit enabled theme.
   */
  public function testSimpleStarterkitTheme(): void {
    $starterkit_theme_path_relative = 'themes/simple';
    $starterkit_theme_path_absolute = $this->getWorkspaceDirectory() . '/' . $starterkit_theme_path_relative;
    mkdir($starterkit_theme_path_absolute);
    file_put_contents($starterkit_theme_path_absolute . '/simple.info.yml', Yaml::encode([
      'name' => 'Simple',
      'type' => 'theme',
      'base theme' => FALSE,
      'core_version_requirement' => '*',
    ]));
    file_put_contents($starterkit_theme_path_absolute . '/simple.starterkit.yml', Yaml::encode([
      'ignore' => ['/simple.starterkit.yml'],
      'no_edit' => [],
      'no_rename' => [],
      'info' => [
        'version' => '1.0.0',
      ],
    ]));
    mkdir($starterkit_theme_path_absolute . '/src');
    file_put_contents($starterkit_theme_path_absolute . '/src/SimpleUtility.php', <<<PHP
<?php

namespace Drupal\simple;

final class SimpleUtility {

  public const MACHINE_NAME = 'simple';
  public const MACHINE_CLASS_NAME = 'Simple';

}
PHP);
    file_put_contents($starterkit_theme_path_absolute . '/simple.theme', <<<PHP
<?php

/**
 * @file
 * Drupal theme functions for Simple.
 */

function simple_preprocess_html(array &\$variables): void {
  \$variables['#attached']['drupalSettings']['simple'] = 'simple';
}
PHP);
    $fixture = <<<FIXTURE
#@starterkit:machine_name
simple
#@starterkit:label
A Simple Theme
#@starterkit:machine_class_name
Simple
#@starterkit:label_class_name
Simple
FIXTURE;
    file_put_contents($starterkit_theme_path_absolute . '/README.md', $fixture);

    $this->assertFileExists($starterkit_theme_path_absolute . '/simple.info.yml');
    $this->assertThemeExists($starterkit_theme_path_relative);

    $tester = $this->runCommand([
      'machine-name' => 'simple_theme',
      '--name' => 'My Simple Theme',
      '--description' => 'Custom theme generated from a Simple Starterkit theme',
      '--starterkit' => 'simple',
    ]);

    $tester->assertCommandIsSuccessful();
    $theme_path_relative = 'themes/simple_theme';
    $this->assertThemeExists($theme_path_relative);

    $info = $this->assertThemeExists($theme_path_relative);
    self::assertEquals('My Simple Theme', $info['name']);
    $readme_file = $this->getWorkspaceDirectory() . "/$theme_path_relative/README.md";
    $this->assertFileExists($readme_file);
    $fixture = <<<FIXTURE
#@starterkit:machine_name
simple_theme
#@starterkit:label
My Simple Theme
#@starterkit:machine_class_name
SimpleTheme
#@starterkit:label_class_name
SimpleTheme
FIXTURE;
    $this->assertSame($fixture, file_get_contents($readme_file));

    // The .theme file should be renamed and contain updated machine and label
    // values.
    $dot_theme_path = $this->getWorkspaceDirectory() . "/$theme_path_relative/simple_theme.theme";
    $this->assertFileExists($dot_theme_path);
    $dot_theme_contents = file_get_contents($dot_theme_path);
    self::assertStringContainsString("\$variables['#attached']['drupalSettings']['simple_theme']", $dot_theme_contents);
    // Ensure content replacements respect the namespace and class fragments.
    $utility_file = $this->getWorkspaceDirectory() . "/$theme_path_relative/src/SimpleThemeUtility.php";
    $this->assertFileExists($utility_file);
    $utility_contents = file_get_contents($utility_file);
    self::assertStringContainsString('namespace Drupal\\simple_theme;', $utility_contents);
    self::assertStringContainsString('class SimpleThemeUtility', $utility_contents);
    self::assertStringContainsString("public const MACHINE_NAME = 'simple_theme'", $utility_contents);
    self::assertStringContainsString("public const MACHINE_CLASS_NAME = 'SimpleTheme'", $utility_contents);
  }

  /**
   * Generates PHP process to generate a theme from core's starterkit theme.
   *
@@ -478,13 +582,13 @@ public function testNoEdit(): void {
      ],
    ]);
    $fixture = <<<FIXTURE
# machine_name
#@starterkit:machine_name
starterkit_theme
# label
#@starterkit:label
Starterkit theme
# machine_class_name
#@starterkit:machine_class_name
StarterkitTheme
# label_class_name
#@starterkit:label_class_name
StarterkitTheme
FIXTURE;

@@ -523,13 +627,13 @@ class StarterkitThemePreRender implements TrustedCallbackInterface {
    self::assertEquals($fixture, file_get_contents($theme_path_absolute . '/no_edit_fixture.txt'));
    self::assertFileExists($theme_path_absolute . '/edit_fixture.txt');
    self::assertEquals(<<<EDITED
# machine_name
#@starterkit:machine_name
test_custom_theme
# label
#@starterkit:label
Test custom starterkit theme
# machine_class_name
#@starterkit:machine_class_name
TestCustomTheme
# label_class_name
#@starterkit:label_class_name
TestCustomTheme
EDITED, file_get_contents($theme_path_absolute . '/edit_fixture.txt'));