wp/wp-includes/class-wp-theme-json-resolver.php
changeset 18 be944660c56a
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
       
     1 <?php
       
     2 /**
       
     3  * WP_Theme_JSON_Resolver class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage Theme
       
     7  * @since 5.8.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Class that abstracts the processing of the different data sources
       
    12  * for site-level config and offers an API to work with them.
       
    13  *
       
    14  * @access private
       
    15  */
       
    16 class WP_Theme_JSON_Resolver {
       
    17 
       
    18 	/**
       
    19 	 * Container for data coming from core.
       
    20 	 *
       
    21 	 * @since 5.8.0
       
    22 	 * @var WP_Theme_JSON
       
    23 	 */
       
    24 	private static $core = null;
       
    25 
       
    26 	/**
       
    27 	 * Container for data coming from the theme.
       
    28 	 *
       
    29 	 * @since 5.8.0
       
    30 	 * @var WP_Theme_JSON
       
    31 	 */
       
    32 	private static $theme = null;
       
    33 
       
    34 	/**
       
    35 	 * Whether or not the theme supports theme.json.
       
    36 	 *
       
    37 	 * @since 5.8.0
       
    38 	 * @var bool
       
    39 	 */
       
    40 	private static $theme_has_support = null;
       
    41 
       
    42 	/**
       
    43 	 * Structure to hold i18n metadata.
       
    44 	 *
       
    45 	 * @since 5.8.0
       
    46 	 * @var array
       
    47 	 */
       
    48 	private static $theme_json_i18n = null;
       
    49 
       
    50 	/**
       
    51 	 * Processes a file that adheres to the theme.json schema
       
    52 	 * and returns an array with its contents, or a void array if none found.
       
    53 	 *
       
    54 	 * @since 5.8.0
       
    55 	 *
       
    56 	 * @param string $file_path Path to file. Empty if no file.
       
    57 	 * @return array Contents that adhere to the theme.json schema.
       
    58 	 */
       
    59 	private static function read_json_file( $file_path ) {
       
    60 		$config = array();
       
    61 		if ( $file_path ) {
       
    62 			$decoded_file = json_decode(
       
    63 				file_get_contents( $file_path ),
       
    64 				true
       
    65 			);
       
    66 
       
    67 			$json_decoding_error = json_last_error();
       
    68 			if ( JSON_ERROR_NONE !== $json_decoding_error ) {
       
    69 				trigger_error( "Error when decoding a theme.json schema at path $file_path " . json_last_error_msg() );
       
    70 				return $config;
       
    71 			}
       
    72 
       
    73 			if ( is_array( $decoded_file ) ) {
       
    74 				$config = $decoded_file;
       
    75 			}
       
    76 		}
       
    77 		return $config;
       
    78 	}
       
    79 
       
    80 	/**
       
    81 	 * Converts a tree as in i18n-theme.json into a linear array
       
    82 	 * containing metadata to translate a theme.json file.
       
    83 	 *
       
    84 	 * For example, given this input:
       
    85 	 *
       
    86 	 *     {
       
    87 	 *       "settings": {
       
    88 	 *         "*": {
       
    89 	 *           "typography": {
       
    90 	 *             "fontSizes": [ { "name": "Font size name" } ],
       
    91 	 *             "fontStyles": [ { "name": "Font size name" } ]
       
    92 	 *           }
       
    93 	 *         }
       
    94 	 *       }
       
    95 	 *     }
       
    96 	 *
       
    97 	 * will return this output:
       
    98 	 *
       
    99 	 *     [
       
   100 	 *       0 => [
       
   101 	 *         'path'    => [ 'settings', '*', 'typography', 'fontSizes' ],
       
   102 	 *         'key'     => 'name',
       
   103 	 *         'context' => 'Font size name'
       
   104 	 *       ],
       
   105 	 *       1 => [
       
   106 	 *         'path'    => [ 'settings', '*', 'typography', 'fontStyles' ],
       
   107 	 *         'key'     => 'name',
       
   108 	 *         'context' => 'Font style name'
       
   109 	 *       ]
       
   110 	 *     ]
       
   111 	 *
       
   112 	 * @since 5.8.0
       
   113 	 *
       
   114 	 * @param array $i18n_partial A tree that follows the format of i18n-theme.json.
       
   115 	 * @param array $current_path Optional. Keeps track of the path as we walk down the given tree.
       
   116 	 *                            Default empty array.
       
   117 	 * @return array A linear array containing the paths to translate.
       
   118 	 */
       
   119 	private static function extract_paths_to_translate( $i18n_partial, $current_path = array() ) {
       
   120 		$result = array();
       
   121 		foreach ( $i18n_partial as $property => $partial_child ) {
       
   122 			if ( is_numeric( $property ) ) {
       
   123 				foreach ( $partial_child as $key => $context ) {
       
   124 					$result[] = array(
       
   125 						'path'    => $current_path,
       
   126 						'key'     => $key,
       
   127 						'context' => $context,
       
   128 					);
       
   129 				}
       
   130 				return $result;
       
   131 			}
       
   132 			$result = array_merge(
       
   133 				$result,
       
   134 				self::extract_paths_to_translate( $partial_child, array_merge( $current_path, array( $property ) ) )
       
   135 			);
       
   136 		}
       
   137 		return $result;
       
   138 	}
       
   139 
       
   140 	/**
       
   141 	 * Returns a data structure used in theme.json translation.
       
   142 	 *
       
   143 	 * @since 5.8.0
       
   144 	 *
       
   145 	 * @return array An array of theme.json fields that are translatable and the keys that are translatable.
       
   146 	 */
       
   147 	public static function get_fields_to_translate() {
       
   148 		if ( null === self::$theme_json_i18n ) {
       
   149 			$file_structure        = self::read_json_file( __DIR__ . '/theme-i18n.json' );
       
   150 			self::$theme_json_i18n = self::extract_paths_to_translate( $file_structure );
       
   151 		}
       
   152 		return self::$theme_json_i18n;
       
   153 	}
       
   154 
       
   155 	/**
       
   156 	 * Translates a chunk of the loaded theme.json structure.
       
   157 	 *
       
   158 	 * @since 5.8.0
       
   159 	 *
       
   160 	 * @param array  $array_to_translate The chunk of theme.json to translate.
       
   161 	 * @param string $key                The key of the field that contains the string to translate.
       
   162 	 * @param string $context            The context to apply in the translation call.
       
   163 	 * @param string $domain             Text domain. Unique identifier for retrieving translated strings.
       
   164 	 * @return array Returns the modified $theme_json chunk.
       
   165 	 */
       
   166 	private static function translate_theme_json_chunk( array $array_to_translate, $key, $context, $domain ) {
       
   167 		foreach ( $array_to_translate as $item_key => $item_to_translate ) {
       
   168 			if ( empty( $item_to_translate[ $key ] ) ) {
       
   169 				continue;
       
   170 			}
       
   171 
       
   172 			// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain
       
   173 			$array_to_translate[ $item_key ][ $key ] = translate_with_gettext_context( $array_to_translate[ $item_key ][ $key ], $context, $domain );
       
   174 		}
       
   175 
       
   176 		return $array_to_translate;
       
   177 	}
       
   178 
       
   179 	/**
       
   180 	 * Given a theme.json structure modifies it in place to update certain values
       
   181 	 * by its translated strings according to the language set by the user.
       
   182 	 *
       
   183 	 * @since 5.8.0
       
   184 	 *
       
   185 	 * @param array  $theme_json The theme.json to translate.
       
   186 	 * @param string $domain     Optional. Text domain. Unique identifier for retrieving translated strings.
       
   187 	 *                           Default 'default'.
       
   188 	 * @return array Returns the modified $theme_json_structure.
       
   189 	 */
       
   190 	private static function translate( $theme_json, $domain = 'default' ) {
       
   191 		$fields = self::get_fields_to_translate();
       
   192 		foreach ( $fields as $field ) {
       
   193 			$path    = $field['path'];
       
   194 			$key     = $field['key'];
       
   195 			$context = $field['context'];
       
   196 
       
   197 			/*
       
   198 			 * We need to process the paths that include '*' separately.
       
   199 			 * One example of such a path would be:
       
   200 			 * [ 'settings', 'blocks', '*', 'color', 'palette' ]
       
   201 			 */
       
   202 			$nodes_to_iterate = array_keys( $path, '*', true );
       
   203 			if ( ! empty( $nodes_to_iterate ) ) {
       
   204 				/*
       
   205 				 * At the moment, we only need to support one '*' in the path, so take it directly.
       
   206 				 * - base will be [ 'settings', 'blocks' ]
       
   207 				 * - data will be [ 'color', 'palette' ]
       
   208 				 */
       
   209 				$base_path = array_slice( $path, 0, $nodes_to_iterate[0] );
       
   210 				$data_path = array_slice( $path, $nodes_to_iterate[0] + 1 );
       
   211 				$base_tree = _wp_array_get( $theme_json, $base_path, array() );
       
   212 				foreach ( $base_tree as $node_name => $node_data ) {
       
   213 					$array_to_translate = _wp_array_get( $node_data, $data_path, null );
       
   214 					if ( is_null( $array_to_translate ) ) {
       
   215 						continue;
       
   216 					}
       
   217 
       
   218 					// Whole path will be [ 'settings', 'blocks', 'core/paragraph', 'color', 'palette' ].
       
   219 					$whole_path       = array_merge( $base_path, array( $node_name ), $data_path );
       
   220 					$translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain );
       
   221 					_wp_array_set( $theme_json, $whole_path, $translated_array );
       
   222 				}
       
   223 			} else {
       
   224 				$array_to_translate = _wp_array_get( $theme_json, $path, null );
       
   225 				if ( is_null( $array_to_translate ) ) {
       
   226 					continue;
       
   227 				}
       
   228 
       
   229 				$translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain );
       
   230 				_wp_array_set( $theme_json, $path, $translated_array );
       
   231 			}
       
   232 		}
       
   233 
       
   234 		return $theme_json;
       
   235 	}
       
   236 
       
   237 	/**
       
   238 	 * Return core's origin config.
       
   239 	 *
       
   240 	 * @since 5.8.0
       
   241 	 *
       
   242 	 * @return WP_Theme_JSON Entity that holds core data.
       
   243 	 */
       
   244 	public static function get_core_data() {
       
   245 		if ( null !== self::$core ) {
       
   246 			return self::$core;
       
   247 		}
       
   248 
       
   249 		$config     = self::read_json_file( __DIR__ . '/theme.json' );
       
   250 		$config     = self::translate( $config );
       
   251 		self::$core = new WP_Theme_JSON( $config, 'core' );
       
   252 
       
   253 		return self::$core;
       
   254 	}
       
   255 
       
   256 	/**
       
   257 	 * Returns the theme's data.
       
   258 	 *
       
   259 	 * Data from theme.json can be augmented via the $theme_support_data variable.
       
   260 	 * This is useful, for example, to backfill the gaps in theme.json that a theme
       
   261 	 * has declared via add_theme_supports.
       
   262 	 *
       
   263 	 * Note that if the same data is present in theme.json and in $theme_support_data,
       
   264 	 * the theme.json's is not overwritten.
       
   265 	 *
       
   266 	 * @since 5.8.0
       
   267 	 *
       
   268 	 * @param array $theme_support_data Optional. Theme support data in theme.json format.
       
   269 	 *                                  Default empty array.
       
   270 	 * @return WP_Theme_JSON Entity that holds theme data.
       
   271 	 */
       
   272 	public static function get_theme_data( $theme_support_data = array() ) {
       
   273 		if ( null === self::$theme ) {
       
   274 			$theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json' ) );
       
   275 			$theme_json_data = self::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
       
   276 			self::$theme     = new WP_Theme_JSON( $theme_json_data );
       
   277 		}
       
   278 
       
   279 		if ( empty( $theme_support_data ) ) {
       
   280 			return self::$theme;
       
   281 		}
       
   282 
       
   283 		/*
       
   284 		 * We want the presets and settings declared in theme.json
       
   285 		 * to override the ones declared via add_theme_support.
       
   286 		 */
       
   287 		$with_theme_supports = new WP_Theme_JSON( $theme_support_data );
       
   288 		$with_theme_supports->merge( self::$theme );
       
   289 
       
   290 		return $with_theme_supports;
       
   291 	}
       
   292 
       
   293 	/**
       
   294 	 * There are different sources of data for a site: core and theme.
       
   295 	 *
       
   296 	 * While the getters {@link get_core_data}, {@link get_theme_data} return the raw data
       
   297 	 * from the respective origins, this method merges them all together.
       
   298 	 *
       
   299 	 * If the same piece of data is declared in different origins (core and theme),
       
   300 	 * the last origin overrides the previous. For example, if core disables custom colors
       
   301 	 * but a theme enables them, the theme config wins.
       
   302 	 *
       
   303 	 * @since 5.8.0
       
   304 	 *
       
   305 	 * @param array $settings Optional. Existing block editor settings. Default empty array.
       
   306 	 * @return WP_Theme_JSON
       
   307 	 */
       
   308 	public static function get_merged_data( $settings = array() ) {
       
   309 		$theme_support_data = WP_Theme_JSON::get_from_editor_settings( $settings );
       
   310 
       
   311 		$result = new WP_Theme_JSON();
       
   312 		$result->merge( self::get_core_data() );
       
   313 		$result->merge( self::get_theme_data( $theme_support_data ) );
       
   314 
       
   315 		return $result;
       
   316 	}
       
   317 
       
   318 	/**
       
   319 	 * Whether the current theme has a theme.json file.
       
   320 	 *
       
   321 	 * @since 5.8.0
       
   322 	 *
       
   323 	 * @return bool
       
   324 	 */
       
   325 	public static function theme_has_support() {
       
   326 		if ( ! isset( self::$theme_has_support ) ) {
       
   327 			self::$theme_has_support = (bool) self::get_file_path_from_theme( 'theme.json' );
       
   328 		}
       
   329 
       
   330 		return self::$theme_has_support;
       
   331 	}
       
   332 
       
   333 	/**
       
   334 	 * Builds the path to the given file and checks that it is readable.
       
   335 	 *
       
   336 	 * If it isn't, returns an empty string, otherwise returns the whole file path.
       
   337 	 *
       
   338 	 * @since 5.8.0
       
   339 	 *
       
   340 	 * @param string $file_name Name of the file.
       
   341 	 * @return string The whole file path or empty if the file doesn't exist.
       
   342 	 */
       
   343 	private static function get_file_path_from_theme( $file_name ) {
       
   344 		/*
       
   345 		 * This used to be a locate_template call. However, that method proved problematic
       
   346 		 * due to its use of constants (STYLESHEETPATH) that threw errors in some scenarios.
       
   347 		 *
       
   348 		 * When the theme.json merge algorithm properly supports child themes,
       
   349 		 * this should also fall back to the template path, as locate_template did.
       
   350 		 */
       
   351 		$located   = '';
       
   352 		$candidate = get_stylesheet_directory() . '/' . $file_name;
       
   353 		if ( is_readable( $candidate ) ) {
       
   354 			$located = $candidate;
       
   355 		}
       
   356 		return $located;
       
   357 	}
       
   358 
       
   359 	/**
       
   360 	 * Cleans the cached data so it can be recalculated.
       
   361 	 *
       
   362 	 * @since 5.8.0
       
   363 	 */
       
   364 	public static function clean_cached_data() {
       
   365 		self::$core              = null;
       
   366 		self::$theme             = null;
       
   367 		self::$theme_has_support = null;
       
   368 		self::$theme_json_i18n   = null;
       
   369 	}
       
   370 
       
   371 }