class-wpml-gutenberg-integration.php 9.69 KB
<?php

use WPML\FP\Fns;
use WPML\FP\Str;

/**
 * Class WPML_Gutenberg_Integration
 */
class WPML_Gutenberg_Integration implements \WPML\PB\Gutenberg\Integration {

	const PACKAGE_ID              = 'Gutenberg';
	const GUTENBERG_OPENING_START = '<!-- wp:';
	const GUTENBERG_CLOSING_START = '<!-- /wp:';
	const CLASSIC_BLOCK_NAME      = 'core/classic-block';

	/**
	 * @var WPML\PB\Gutenberg\StringsInBlock\StringsInBlock
	 */
	private $strings_in_blocks;

	/**
	 * @var WPML_Gutenberg_Config_Option
	 */
	private $config_option;

	/**
	 * @var WPML_Gutenberg_Strings_Registration $strings_registration
	 */
	private $strings_registration;

	public function __construct(
		WPML\PB\Gutenberg\StringsInBlock\StringsInBlock $strings_in_block,
		WPML_Gutenberg_Config_Option $config_option,
		WPML_Gutenberg_Strings_Registration $strings_registration
	) {
		$this->strings_in_blocks    = $strings_in_block;
		$this->config_option        = $config_option;
		$this->strings_registration = $strings_registration;
	}

	public function add_hooks() {
		add_filter( 'wpml_page_builder_support_required', array( $this, 'page_builder_support_required' ), 10, 1 );
		add_action( 'wpml_page_builder_register_strings', array( $this, 'register_strings' ), 10, 2 );
		add_action( 'wpml_page_builder_string_translated', array( $this, 'string_translated' ), 10, 5 );
		add_filter( 'wpml_config_array', array( $this, 'wpml_config_filter' ) );
		add_filter( 'wpml_pb_should_body_be_translated', array( $this, 'should_body_be_translated_filter' ), PHP_INT_MAX, 3 );
		add_filter( 'wpml_get_translatable_types', array( $this, 'remove_package_strings_type_filter' ), 11 );
	}

	/**
	 * @param array $plugins
	 *
	 * @return array
	 */
	function page_builder_support_required( $plugins ) {
		$plugins[] = self::PACKAGE_ID;

		return $plugins;
	}

	/**
	 * @param WP_Post $post
	 * @param array $package_data
	 */
	function register_strings( WP_Post $post, $package_data ) {

		if ( $this->is_gutenberg_post( $post ) && self::PACKAGE_ID === $package_data['kind'] ) {
			$this->strings_registration->register_strings( $post, $package_data );
		}
	}

	public function register_strings_from_widget( array $blocks, array $package_data ) {
		$this->strings_registration->register_strings_from_widget( $blocks, $package_data );
	}

	/**
	 * @param WP_Block_Parser_Block|array $block
	 *
	 * @return WP_Block_Parser_Block
	 */
	public static function sanitize_block( $block ) {
		if ( ! $block instanceof WP_Block_Parser_Block ) {

			if ( empty( $block['blockName'] ) ) {
				$block['blockName'] = self::CLASSIC_BLOCK_NAME;
			}

			$block = new WP_Block_Parser_Block( $block['blockName'], $block['attrs'], $block['innerBlocks'], $block['innerHTML'], $block['innerContent'] );
		}

		return $block;
	}

	/**
	 * @param string $package_kind
	 * @param int $translated_post_id
	 * @param WP_Post $original_post
	 * @param array $string_translations
	 * @param string $lang
	 */
	public function string_translated(
		$package_kind,
		$translated_post_id,
		$original_post,
		$string_translations,
		$lang
	) {

		if ( self::PACKAGE_ID === $package_kind ) {
			$content = $this->replace_strings_in_blocks( $original_post->post_content, $string_translations, $lang );
			wpml_update_escaped_post( [ 'ID' => $translated_post_id, 'post_content' => $content ], $lang );
		}

	}

	/**
	 * @param string $content
	 * @param array  $string_translations
	 * @param string $lang
	 *
	 * @return string
	 */
	public function replace_strings_in_blocks( $content, $string_translations, $lang ) {
		$blocks = self::parse_blocks( $content );
		$blocks = $this->update_block_translations( $blocks, $string_translations, $lang );

		return Fns::reduce( Str::concat(), '', Fns::map( [ $this, 'render_block' ], $blocks ) );
	}

	/**
	 * @param array $blocks
	 * @param array $string_translations
	 * @param string $lang
	 *
	 * @return array
	 */
	private function update_block_translations( $blocks, $string_translations, $lang ) {
		foreach ( $blocks as &$block ) {

			$block = self::sanitize_block( $block );
			$block = $this->strings_in_blocks->update( $block, $string_translations, $lang );

			if ( isset( $block->innerBlocks ) ) {
				$block->innerBlocks = $this->update_block_translations(
					$block->innerBlocks,
					$string_translations,
					$lang
				);
			}
		}

		return $blocks;
	}

	/**
	 * @param array|WP_Block_Parser_Block $block
	 *
	 * @return string
	 */
	public static function render_block( $block ) {
		$content = '';

		$block = self::sanitize_block( $block );

		if ( self::CLASSIC_BLOCK_NAME !== $block->blockName ) {
			$block_content = self::render_inner_HTML( $block );
			$block_type    = preg_replace( '/^core\//', '', $block->blockName );

			$block_attributes = '';
			if ( self::has_non_empty_attributes( $block ) ) {
				$block_attributes = ' ' . json_encode( $block->attrs, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
			}
			$content .= self::GUTENBERG_OPENING_START . $block_type . $block_attributes . ' ';

			if ( $block_content ) {
				$content .= '-->' . $block_content . self::GUTENBERG_CLOSING_START . $block_type . ' -->';
			} else {
				$content .= '/-->';
			}
		} else {
			$content = wpautop( $block->innerHTML );
		}

		return $content;

	}

	public static function has_non_empty_attributes( WP_Block_Parser_Block $block ) {
		return (bool) ( (array) $block->attrs );
	}

	/**
	 * @param WP_Block_Parser_Block $block
	 *
	 * @return string
	 */
	private static function render_inner_HTML( $block ) {

		if ( isset ( $block->innerBlocks ) && count( $block->innerBlocks ) ) {

			if ( isset( $block->innerContent ) ) {
				$content = self::render_inner_HTML_with_innerContent( $block );
			} else {
				$content = self::render_inner_HTML_with_guess_parts( $block );
			}

		} else {
			$content = $block->innerHTML;
		}

		return $content;

	}

	/**
	 * Since Gutenberg 4.2.0 and WP 5.0.0 we have a new
	 * property WP_Block_Parser_Block::$innerContent which
	 * provides the sequence of inner elements:
	 * strings or null if it's an inner block.
	 *
	 * @see WP_Block_Parser_Block::$innerContent
	 *
	 * @param WP_Block_Parser_Block $block
	 *
	 * @return string
	 */
	private static function render_inner_HTML_with_innerContent( $block ) {
		$content           = '';
		$inner_block_index = 0;

		foreach ( $block->innerContent as $inner_content ) {
			if ( is_string( $inner_content ) ) {
				$content .= $inner_content;
			} else {
				$content .= self::render_block( $block->innerBlocks[ $inner_block_index ] );
				$inner_block_index++;
			}
		}

		return $content;
	}

	/**
	 * @param WP_Block_Parser_Block $block
	 *
	 * @return string
	 */
	private static function render_inner_HTML_with_guess_parts( $block ) {
		$inner_html_parts = self::guess_inner_HTML_parts( $block );

		$content = $inner_html_parts[0];

		foreach ( $block->innerBlocks as $inner_block ) {
			$content .= self::render_block( $inner_block );
		}

		$content .= $inner_html_parts[1];

		return $content;
	}

	/**
	 * The gutenberg parser prior to version 4.2.0 (Gutenberg) and 5.0.0 (WP)
	 * doesn't handle inner blocks correctly.
	 * It should really return the HTML before and after the blocks
	 * We're just guessing what it is here
	 * The usual innerHTML would be: <div class="xxx"></div>
	 * The columns block also includes new lines: <div class="xxx">\n\n</div>
	 * So we try to split at ></ and also include white space and new lines between the tags
	 *
	 * @param WP_Block_Parser_Block $block
	 *
	 * @return array
	 */
	private static function guess_inner_HTML_parts( $block ) {
		$inner_HTML = $block->innerHTML;

		$parts = array( $inner_HTML, '' );

		switch( $block->blockName ) {
			case 'core/media-text':
				$html_to_find = '<div class="wp-block-media-text__content">';
				$pos          = mb_strpos( $inner_HTML, $html_to_find ) + mb_strlen( $html_to_find );
				$parts        = array(
					mb_substr( $inner_HTML, 0, $pos ),
					mb_substr( $inner_HTML, $pos )
				);
				break;

			default:
				preg_match( '#>\s*</#', $inner_HTML, $matches );

				if ( count( $matches ) === 1 ) {
					$parts = explode( $matches[0], $inner_HTML );
					if ( count( $parts ) === 2 ) {
						$match_mid_point = 1 + ( mb_strlen( $matches[0] ) - 3 ) / 2;
						// This is the first ">" char plus half the remaining between the tags

						$parts[0] .= mb_substr( $matches[0], 0, $match_mid_point );
						$parts[1] = mb_substr( $matches[0], $match_mid_point ) . $parts[1];
					}
				}
				break;
		}

		return $parts;
	}

	/**
	 * @param array $config_data
	 *
	 * @return array
	 */
	public function wpml_config_filter( $config_data ) {
		$this->config_option->update_from_config( $config_data );

		return $config_data;
	}

	/**
	 * @param bool    $translate
	 * @param WP_Post $post
	 * @param string  $context
	 *
	 * @return bool
	 */
	public function should_body_be_translated_filter( $translate, WP_Post $post, $context = '' ) {
		if ( 'translate_images_in_post_content' === $context && $this->is_gutenberg_post( $post ) ) {
			$translate = true;
		}

		return $translate;
	}

	/**
	 * @param WP_Post $post
	 *
	 * @return bool
	 */
	private function is_gutenberg_post( WP_Post $post ) {
		return (bool) preg_match( '/' . self::GUTENBERG_OPENING_START . '/', $post->post_content );
	}

	public static function parse_blocks( $content ) {
		global $wp_version;
		if ( version_compare( $wp_version, '5.0-beta1', '>=' ) ) {
			return parse_blocks( $content );
		} else {
			// @phpstan-ignore-next-line
			return gutenberg_parse_blocks( $content );
		}
	}

	/**
	 * Remove Gutenberg (string package) from translation dashboard filters
	 *
	 * @param array $types
	 *
	 * @return mixed
	 */
	public function remove_package_strings_type_filter( $types ) {

		if ( array_key_exists( 'gutenberg', $types ) ) {
			unset( $types['gutenberg'] );
		}

		return $types;
	}
}