Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 228 additions & 3 deletions includes/create-theme/theme-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,228 @@ public static function get_file_extension_from_url( $url ) {
return $extension;
}

public static function replace_namespace( $content, $old_slug, $new_slug, $old_name, $new_name ) {
$new_slug_underscore = str_replace( '-', '_', $new_slug ) . '_';
public static function replace_namespace( $content, $old_slug, $new_slug, $old_name, $new_name, $file_extension = '' ) {
$new_slug_underscore = self::get_php_identifier( $new_slug ) . '_';
$old_slug_underscore = str_replace( '-', '_', $old_slug ) . '_';

if ( false !== strpos( $content, '<?php' ) ) {
$content = self::replace_localized_javascript_identifier( $content, $old_slug, $new_slug, $old_name );
$updated_content = '';

foreach ( token_get_all( $content ) as $token ) {
if ( is_array( $token ) && self::is_php_identifier_token( $token[0] ) ) {
$updated_content .= self::replace_php_identifier( $token[1], $old_slug, $new_slug, $old_name );
} else {
$token_content = is_array( $token ) ? $token[1] : $token;
$updated_content .= self::replace_namespace_value(
$token_content,
$old_slug,
$new_slug,
$old_name,
$new_name,
$old_slug_underscore,
$new_slug_underscore
);
}
}

return $updated_content;
}

if ( 'js' === strtolower( $file_extension ) ) {
return self::replace_javascript_namespace(
$content,
$old_slug,
$new_slug,
$old_name,
$new_name,
$old_slug_underscore,
$new_slug_underscore
);
}

return self::replace_namespace_value(
$content,
$old_slug,
$new_slug,
$old_name,
$new_name,
$old_slug_underscore,
$new_slug_underscore
);
}

private static function get_php_identifier( $slug ) {
$identifier = preg_replace( '/[^a-z0-9_]/', '', str_replace( '-', '_', sanitize_key( $slug ) ) );

if ( '' === $identifier ) {
return 'theme';
}

if ( is_numeric( $identifier[0] ) ) {
return 'theme_' . $identifier;
}

return $identifier;
}

private static function get_php_namespace( $slug ) {
return implode( '_', array_map( 'ucfirst', explode( '_', self::get_php_identifier( $slug ) ) ) );
}

private static function get_javascript_identifier( $slug, $capitalize = false ) {
$parts = preg_split( '/[^a-z0-9]+/', strtolower( $slug ), -1, PREG_SPLIT_NO_EMPTY );

if ( empty( $parts ) ) {
$parts = array( 'theme' );
}

if ( is_numeric( $parts[0][0] ) ) {
array_unshift( $parts, 'theme' );
}

$identifier = array_shift( $parts );

foreach ( $parts as $part ) {
$identifier .= ucfirst( $part );
}

return $capitalize ? ucfirst( $identifier ) : $identifier;
}

private static function get_javascript_identifier_replacements( $old_slug, $new_slug, $old_name ) {
$old_identifier = self::get_javascript_identifier( $old_slug );
$old_capitalized = self::get_javascript_identifier( $old_slug, true );
$old_name_identifier = preg_replace( '/[^a-zA-Z0-9_$]/', '', $old_name );
$new_identifier = self::get_javascript_identifier( $new_slug );
$new_capitalized = self::get_javascript_identifier( $new_slug, true );
$replacements = array(
$old_identifier => $new_identifier,
$old_capitalized => $new_capitalized,
$old_name_identifier => $new_capitalized,
);
$filtered_replacements = array_filter( $replacements, 'strlen', ARRAY_FILTER_USE_KEY );

uksort(
$filtered_replacements,
function ( $first, $second ) {
return strlen( $second ) - strlen( $first );
}
);

return $filtered_replacements;
}

private static function replace_javascript_identifier( $identifier, $old_slug, $new_slug, $old_name, $preserve_plain_values = false ) {
$replacements = self::get_javascript_identifier_replacements( $old_slug, $new_slug, $old_name );

foreach ( $replacements as $old_identifier => $new_identifier ) {
if ( 0 !== strpos( $identifier, $old_identifier ) ) {
continue;
}

$suffix = substr( $identifier, strlen( $old_identifier ) );

if (
$preserve_plain_values &&
(
( '' === $suffix && ( $identifier === $old_slug || $identifier === $old_name ) ) ||
( '' !== $suffix && ! preg_match( '/^[A-Z0-9]/', $suffix ) )
)
) {
return $identifier;
}

return $new_identifier . $suffix;
}

return $identifier;
}

private static function replace_embedded_javascript_identifiers( $content, $old_slug, $new_slug, $old_name ) {
return preg_replace_callback(
'/[$A-Z_a-z][$0-9A-Z_a-z]*/',
function ( $matches ) use ( $old_slug, $new_slug, $old_name ) {
return self::replace_javascript_identifier( $matches[0], $old_slug, $new_slug, $old_name, true );
},
$content
);
}

private static function replace_localized_javascript_identifier( $content, $old_slug, $new_slug, $old_name ) {
$pattern = '/(wp_localize_script\s*\(\s*[^,]+,\s*)([\'\"])([$A-Z_a-z][$0-9A-Z_a-z]*)(\2)/';

return preg_replace_callback(
$pattern,
function ( $matches ) use ( $old_slug, $new_slug, $old_name ) {
$identifier = self::replace_javascript_identifier( $matches[3], $old_slug, $new_slug, $old_name );

return $matches[1] . $matches[2] . $identifier . $matches[4];
},
$content
);
}

private static function replace_javascript_namespace( $content, $old_slug, $new_slug, $old_name, $new_name, $old_slug_underscore, $new_slug_underscore ) {
$content = self::replace_embedded_javascript_identifiers( $content, $old_slug, $new_slug, $old_name );
$pattern = <<<'REGEX'
~(?P<string>"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')
|(?P<template>`(?:\\.|[^`\\])*`)
|(?P<line_comment>//[^\r\n]*)
|(?P<block_comment>/\*.*?\*/)
|(?P<identifier>[$A-Z_a-z][$0-9A-Z_a-z]*)~sx
REGEX;

return preg_replace_callback(
$pattern,
function ( $matches ) use ( $old_slug, $new_slug, $old_name, $new_name, $old_slug_underscore, $new_slug_underscore ) {
if ( ! empty( $matches['identifier'] ) ) {
return self::replace_javascript_identifier( $matches[0], $old_slug, $new_slug, $old_name );
}

if ( ! empty( $matches['template'] ) ) {
// Template literals may contain expressions, so leave them intact.
return $matches[0];
}

return self::replace_namespace_value(
$matches[0],
$old_slug,
$new_slug,
$old_name,
$new_name,
$old_slug_underscore,
$new_slug_underscore
);
},
$content
);
}

private static function is_php_identifier_token( $token_id ) {
return T_STRING === $token_id ||
T_VARIABLE === $token_id ||
( defined( 'T_NAME_QUALIFIED' ) && constant( 'T_NAME_QUALIFIED' ) === $token_id ) ||
( defined( 'T_NAME_FULLY_QUALIFIED' ) && constant( 'T_NAME_FULLY_QUALIFIED' ) === $token_id ) ||
( defined( 'T_NAME_RELATIVE' ) && constant( 'T_NAME_RELATIVE' ) === $token_id );
}

private static function replace_php_identifier( $content, $old_slug, $new_slug, $old_name ) {
$new_identifier = self::get_php_identifier( $new_slug );
$new_namespace = self::get_php_namespace( $new_slug );
$old_identifier = self::get_php_identifier( $old_slug );
$replacements = array(
$old_identifier . '_' => $new_identifier . '_',
$old_identifier => $new_identifier,
$old_slug => $new_identifier,
$old_name => $new_namespace,
);

return strtr( $content, array_filter( $replacements, 'strlen', ARRAY_FILTER_USE_KEY ) );
}

private static function replace_namespace_value( $content, $old_slug, $new_slug, $old_name, $new_name, $old_slug_underscore, $new_slug_underscore ) {

// Generate placeholders
$placeholder_slug = md5( $old_slug );
$placeholder_slug_underscore = md5( $old_slug_underscore );
Expand Down Expand Up @@ -92,7 +310,14 @@ public static function clone_theme_to_folder( $location, $new_slug, $new_name )
if ( preg_match( "/\.({$valid_extensions_regex})$/", $relative_path ) ) {
// Replace namespace values if provided
if ( $new_slug ) {
$contents = self::replace_namespace( $contents, $old_slug, $new_slug, $old_name, $new_name );
$contents = self::replace_namespace(
$contents,
$old_slug,
$new_slug,
$old_name,
$new_name,
pathinfo( $relative_path, PATHINFO_EXTENSION )
);
}
}

Expand Down
9 changes: 8 additions & 1 deletion includes/create-theme/theme-zip.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,14 @@ public static function copy_theme_to_zip( $zip, $new_slug, $new_name ) {

// Replace namespace values if provided
if ( $new_slug ) {
$contents = CBT_Theme_Utils::replace_namespace( $contents, $old_slug, $new_slug, $old_name, $new_name );
$contents = CBT_Theme_Utils::replace_namespace(
$contents,
$old_slug,
$new_slug,
$old_name,
$new_name,
pathinfo( $relative_path, PATHINFO_EXTENSION )
);
}

// Add current file to archive
Expand Down
119 changes: 119 additions & 0 deletions tests/test-theme-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,123 @@ function oldslug_support() {
$this->assertStringContainsString( 'function new_slug_support', $updated_code_string );
$this->assertStringContainsString( "function_exists( 'new_slug_support' )", $updated_code_string );
}

/**
* @dataProvider get_php_identifier_replacements
*/
public function test_replace_namespace_in_php_code_uses_valid_identifiers( $new_name, $new_slug, $expected_identifier, $expected_namespace ) {
$code_string = "<?php
/**
* This file adds functions to the Ollie WordPress theme.
*/
namespace Ollie;

class Ollie_Setup {
}

function ollie_setup() {
\$ollie_setting = true;
}

if ( ! function_exists( 'ollie_setup' ) ) {
}

use Ollie\Setup;
";

$updated_code_string = CBT_Theme_Utils::replace_namespace( $code_string, 'ollie', $new_slug, 'Ollie', $new_name );

$this->assertStringContainsString( "namespace {$expected_namespace};", $updated_code_string );
$this->assertStringContainsString( "class {$expected_namespace}_Setup", $updated_code_string );
$this->assertStringContainsString( "function {$expected_identifier}_setup", $updated_code_string );
$this->assertStringContainsString( "\${$expected_identifier}_setting", $updated_code_string );
$this->assertStringContainsString( "function_exists( '{$expected_identifier}_setup' )", $updated_code_string );
$this->assertStringContainsString( "use {$expected_namespace}\\Setup;", $updated_code_string );
$this->assertStringContainsString( "functions to the {$new_name} WordPress theme", $updated_code_string );
}

/**
* @dataProvider get_javascript_identifier_replacements
*/
public function test_replace_namespace_in_javascript_uses_valid_identifiers( $new_name, $new_slug, $expected_identifier, $expected_global ) {
$code_string = <<<'JS'
const extendable = window.ExtendableAnimations;
window.extendableOpenAnimationModal = () => true;
window.addEventListener( 'extendableAnimationSettingsChanged', () => true );
const textDomain = 'extendable';
const cssClass = 'extendable-animation-modal';
const displayName = 'Extendable';
JS;

$updated_code_string = CBT_Theme_Utils::replace_namespace(
$code_string,
'extendable',
$new_slug,
'Extendable',
$new_name,
'js'
);

$this->assertStringContainsString( "const {$expected_identifier} = window.{$expected_global}Animations;", $updated_code_string );
$this->assertStringContainsString( "window.{$expected_identifier}OpenAnimationModal = () => true;", $updated_code_string );
$this->assertStringContainsString( "'{$expected_identifier}AnimationSettingsChanged'", $updated_code_string );
$this->assertStringContainsString( "const textDomain = '{$new_slug}';", $updated_code_string );
$this->assertStringContainsString( "const cssClass = '{$new_slug}-animation-modal';", $updated_code_string );
$this->assertStringContainsString( "const displayName = '{$new_name}';", $updated_code_string );
$this->assertStringNotContainsString( "window.{$new_slug}", $updated_code_string );
$this->assertStringNotContainsString( "window.{$new_name}", $updated_code_string );
}

public function test_replace_namespace_in_javascript_recognizes_camel_case_source_slug() {
$code_string = "const spectraOne = window.SpectraOneSettings; const textDomain = 'spectra-one';";
$updated_code_string = CBT_Theme_Utils::replace_namespace(
$code_string,
'spectra-one',
'my-theme',
'Spectra One',
'My Theme',
'js'
);

$this->assertStringContainsString( 'const myTheme = window.MyThemeSettings;', $updated_code_string );
$this->assertStringContainsString( "const textDomain = 'my-theme';", $updated_code_string );
}

public function test_replace_namespace_updates_javascript_identifier_references_in_php_strings() {
$code_string = <<<'PHP'
<?php
wp_localize_script( 'extendable-animations', 'ExtendableAnimations', array() );
PHP;

$updated_code_string = CBT_Theme_Utils::replace_namespace(
$code_string,
'extendable',
'my-theme',
'Extendable',
'My Theme',
'php'
);

$this->assertStringContainsString(
"wp_localize_script( 'my-theme-animations', 'MyThemeAnimations', array() );",
$updated_code_string
);
}

public function get_php_identifier_replacements() {
return array(
'theme name with spaces' => array( 'My Theme', 'my-theme', 'my_theme', 'My_Theme' ),
'theme name with hyphen' => array( 'my-theme', 'my-theme', 'my_theme', 'My_Theme' ),
'theme name with symbol' => array( 'FE & Ollie', 'fe-ollie', 'fe_ollie', 'Fe_Ollie' ),
'theme name with number' => array( '2024 Clone', '2024-clone', 'theme_2024_clone', 'Theme_2024_Clone' ),
);
}

public function get_javascript_identifier_replacements() {
return array(
'theme name with spaces' => array( 'My Theme', 'my-theme', 'myTheme', 'MyTheme' ),
'theme name with symbol' => array( 'FE & Ollie', 'fe-ollie', 'feOllie', 'FeOllie' ),
'theme name with number' => array( '2024 Clone', '2024-clone', 'theme2024Clone', 'Theme2024Clone' ),
);
}
}
Loading