Skip to content

Commit

Permalink
Fix template texts localizing/escaping (#641)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Crist <jcrist@pbking.com>
  • Loading branch information
matiasbenedetto and pbking committed May 16, 2024
1 parent d3ffe65 commit ca08514
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 97 deletions.
1 change: 1 addition & 0 deletions admin/class-create-theme.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

require_once __DIR__ . '/resolver_additions.php';
require_once __DIR__ . '/create-theme/theme-locale.php';
require_once __DIR__ . '/create-theme/theme-tags.php';
require_once __DIR__ . '/create-theme/theme-zip.php';
require_once __DIR__ . '/create-theme/theme-media.php';
Expand Down
158 changes: 158 additions & 0 deletions admin/create-theme/theme-locale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
/*
* Locale related functionality
*/
class CBT_Theme_Locale {

/**
* Escape a string for localization.
*
* @param string $string The string to escape.
* @return string The escaped string.
*/
public static function escape_string( $string ) {
// Avoid escaping if the text is not a string.
if ( ! is_string( $string ) ) {
return $string;
}

// Check if the text is already escaped.
if ( str_starts_with( $string, '<?php echo' ) ) {
return $string;
}

$string = addcslashes( $string, "'" );
return "<?php echo __('" . $string . "', '" . wp_get_theme()->get( 'TextDomain' ) . "');?>";
}

/**
* Get a replacement pattern for escaping the text from the html content of a block.
*
* @param string $block_name The block name.
* @return array|null The regex patterns to match the content that needs to be escaped.
* Returns null if the block is not supported.
* Returns an array of regex patterns if the block has html elements that need to be escaped.
*/
private static function get_text_replacement_patterns_for_html( $block_name ) {
switch ( $block_name ) {
case 'core/paragraph':
return array( '/(<p[^>]*>)(.*?)(<\/p>)/' );
case 'core/heading':
return array( '/(<h[^>]*>)(.*?)(<\/h[^>]*>)/' );
case 'core/list-item':
return array( '/(<li[^>]*>)(.*?)(<\/li>)/' );
case 'core/verse':
return array( '/(<pre[^>]*>)(.*?)(<\/pre>)/' );
case 'core/button':
return array( '/(<a[^>]*>)(.*?)(<\/a>)/' );
case 'core/image':
case 'core/cover':
case 'core/media-text':
return array( '/alt="(.*?)"/' );
case 'core/quote':
case 'core/pullquote':
return array(
'/(<p[^>]*>)(.*?)(<\/p>)/',
'/(<cite[^>]*>)(.*?)(<\/cite>)/',
);
case 'core/table':
return array(
'/(<td[^>]*>)(.*?)(<\/td>)/',
'/(<th[^>]*>)(.*?)(<\/th>)/',
'/(<figcaption[^>]*>)(.*?)(<\/figcaption>)/',
);
default:
return null;
}
}

/*
* Localize text in text blocks.
*
* @param array $blocks The blocks to localize.
* @return array The localized blocks.
*/
public static function escape_text_content_of_blocks( $blocks ) {
foreach ( $blocks as &$block ) {

// Recursively escape the inner blocks.
if ( ! empty( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = self::escape_text_content_of_blocks( $block['innerBlocks'] );
}

/*
* Set the pattern based on the block type.
* The pattern is used to match the content that needs to be escaped.
* Patterns are defined in the get_text_replacement_patterns_for_html method.
*/
$patterns = self::get_text_replacement_patterns_for_html( $block['blockName'] );

// If the block does not have any patterns leave the block as is and continue to the next block.
if ( ! $patterns ) {
continue;
}

// Builds the replacement callback function based on the block type.
switch ( $block['blockName'] ) {
case 'core/paragraph':
case 'core/heading':
case 'core/list-item':
case 'core/verse':
case 'core/button':
case 'core/quote':
case 'core/pullquote':
case 'core/table':
$replace_content_callback = function ( $content, $pattern ) {
if ( empty( $content ) ) {
return;
}
return preg_replace_callback(
$pattern,
function( $matches ) {
return $matches[1] . self::escape_string( $matches[2] ) . $matches[3];
},
$content
);
};
break;
case 'core/image':
case 'core/cover':
case 'core/media-text':
$replace_content_callback = function ( $content, $pattern ) {
if ( empty( $content ) ) {
return;
}
return preg_replace_callback(
$pattern,
function( $matches ) {
return 'alt="' . self::escape_string( $matches[1] ) . '"';
},
$content
);
};
break;
default:
$replace_content_callback = null;
break;
}

// Apply the replacement patterns to the block content.
foreach ( $patterns as $pattern ) {
if (
! empty( $block['innerContent'] ) &&
is_callable( $replace_content_callback )
) {
$block['innerContent'] = is_array( $block['innerContent'] )
? array_map(
function( $content ) use ( $replace_content_callback, $pattern ) {
return $replace_content_callback( $content, $pattern );
},
$block['innerContent']
)
: $replace_content_callback( $block['innerContent'], $pattern );
}
}
}
return $blocks;
}
}
105 changes: 10 additions & 95 deletions admin/create-theme/theme-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,105 +268,20 @@ public static function add_templates_to_local( $export_type, $path = null, $slug
}
}

/**
* Escape text in template content.
*
* @param object $template The template to escape text content in.
* @return object The template with the content escaped.
*/
public static function escape_text_in_template( $template ) {

$template_blocks = parse_blocks( $template->content );
$text_to_localize = array();

// Gather up all the strings that need to be localized
foreach ( $template_blocks as &$block ) {
$text_to_localize = array_merge( $text_to_localize, self::get_text_to_localize_from_block( $block ) );
}
$text_to_localize = array_unique( $text_to_localize );

// Localize the strings
foreach ( $text_to_localize as $text ) {
$template->content = str_replace( $text, self::escape_text( $text ), $template->content );
}

$template_blocks = parse_blocks( $template->content );
$localized_blocks = CBT_Theme_Locale::escape_text_content_of_blocks( $template_blocks );
$updated_template_content = serialize_blocks( $localized_blocks );
$template->content = $updated_template_content;
return $template;
}

private static function get_text_to_localize_from_block( $block ) {

$text_to_localize = array();

// Text Blocks (paragraphs and headings)
if ( in_array( $block['blockName'], array( 'core/paragraph', 'core/heading', 'core/list-item', 'core/verse' ), true ) ) {
$markup = $block['innerContent'][0];
// remove the tags from the beginning and end of the markup
$markup = substr( $markup, strpos( $markup, '>' ) + 1 );
$markup = substr( $markup, 0, strrpos( $markup, '<' ) );
$text_to_localize[] = $markup;
}

// Quote Blocks
if ( in_array( $block['blockName'], array( 'core/quote', 'core/pullquote' ), true ) ) {
$markup = serialize_blocks( array( $block ) );
// Grab paragraph tag content
if ( preg_match( '/<p[^>]*>(.*?)<\/p>/', $markup, $matches ) ) {
$text_to_localize[] = $matches[1];
}
// Grab cite tag content
if ( preg_match( '/<cite[^>]*>(.*?)<\/cite>/', $markup, $matches ) ) {
$text_to_localize[] = $matches[1];
}
}

// Button Blocks
if ( in_array( $block['blockName'], array( 'core/button' ), true ) ) {
$markup = $block['innerContent'][0];
if ( preg_match( '/<a[^>]*>(.*?)<\/a>/', $markup, $matches ) ) {
$text_to_localize[] = $matches[1];
}
}

// Alt text in Image and Cover Blocks
if ( in_array( $block['blockName'], array( 'core/image', 'core/cover', 'core/media-text' ), true ) ) {
$markup = $block['innerContent'][0];
if ( preg_match( '/alt="(.*?)"/', $markup, $matches ) ) {
$text_to_localize[] = $matches[1];
}
if ( array_key_exists( 'alt', $block['attrs'] ) ) {
$text_to_localize[] = $block['attrs']['alt'];
}
}

// Table Blocks
if ( in_array( $block['blockName'], array( 'core/table' ), true ) ) {
$markup = serialize_blocks( array( $block ) );
// Grab table cell content
if ( preg_match_all( '/<td[^>]*>(.*?)<\/td>/', $markup, $matches ) ) {
$text_to_localize = array_merge( $text_to_localize, $matches[1] );
}
// Grab table header content
if ( preg_match_all( '/<th[^>]*>(.*?)<\/th>/', $markup, $matches ) ) {
$text_to_localize = array_merge( $text_to_localize, $matches[1] );
}
// Grab the caption
if ( preg_match_all( '/<figcaption[^>]*>(.*?)<\/figcaption>/', $markup, $matches ) ) {
$text_to_localize = array_merge( $text_to_localize, $matches[1] );
}
}

// process inner blocks
if ( ! empty( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $inner_block ) {
$text_to_localize = array_merge( $text_to_localize, self::get_text_to_localize_from_block( $inner_block ) );
}
}

return $text_to_localize;
}

public static function escape_text( $text ) {
if ( ! $text ) {
return $text;
}
$text = addcslashes( $text, "'" );
return "<?php echo __('" . $text . "', '" . wp_get_theme()->get( 'TextDomain' ) . "');?>";
}

private static function eliminate_environment_specific_content_from_block( $block, $options = null ) {

// remove theme attribute from template parts
Expand Down
51 changes: 51 additions & 0 deletions tests/CbtThemeLocale/base.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/**
* Base test case for Theme Locale tests.
*
* @package Create_Block_Theme
*/
abstract class CBT_Theme_Locale_UnitTestCase extends WP_UnitTestCase {

/**
* Stores the original active theme slug in order to restore it in tear down.
*
* @var string|null
*/
private $orig_active_theme_slug;

/**
* Stores the custom test theme directory.
*
* @var string|null;
*/
private $test_theme_dir;

/**
* Sets up tests.
*/
public function set_up() {
parent::set_up();

// Store the original active theme.
$this->orig_active_theme_slug = get_option( 'stylesheet' );

// Create a test theme directory.
$this->test_theme_dir = DIR_TESTDATA . '/themes/';

// Register test theme directory.
register_theme_directory( $this->test_theme_dir );

// Switch to the test theme.
switch_theme( 'test-theme-locale' );
}

/**
* Tears down tests.
*/
public function tear_down() {
parent::tear_down();

// Restore the original active theme.
switch_theme( $this->orig_active_theme_slug );
}
}
48 changes: 48 additions & 0 deletions tests/CbtThemeLocale/escapeString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Tests for the CBT_Theme_Locale::escape_string method.
*
* @package Create_Block_Theme
* @covers CBT_Theme_Locale::escape_string
* @group locale
*/
class CBT_Theme_Locale_EscapeString extends CBT_Theme_Locale_UnitTestCase {
public function test_escape_string() {
$string = 'This is a test text.';
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( "<?php echo __('This is a test text.', 'test-locale-theme');?>", $escaped_string );
}

public function test_escape_string_with_single_quote() {
$string = "This is a test text with a single quote '";
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( "<?php echo __('This is a test text with a single quote \\'', 'test-locale-theme');?>", $escaped_string );
}

public function test_escape_string_with_double_quote() {
$string = 'This is a test text with a double quote "';
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( "<?php echo __('This is a test text with a double quote \"', 'test-locale-theme');?>", $escaped_string );
}

public function test_escape_string_with_html() {
$string = '<p>This is a test text with HTML.</p>';
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( "<?php echo __('<p>This is a test text with HTML.</p>', 'test-locale-theme');?>", $escaped_string );
}

public function test_escape_string_with_already_escaped_string() {
$string = "<?php echo __('This is a test text.', 'test-locale-theme');?>";
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( $string, $escaped_string );
}

public function test_escape_string_with_non_string() {
$string = null;
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( $string, $escaped_string );
}
}
Loading

0 comments on commit ca08514

Please sign in to comment.