Make WordPress Core

Changeset 52062


Ignore:
Timestamp:
11/08/2021 11:09:53 PM (2 years ago)
Author:
noisysocks
Message:

Editor: Add block theme infrastructure

Adds the required infrastructure to render block-based themes. This is sourced
from the Gutenberg plugin.

Fixes #54335.
Props bernhard-reiter, youknowriad, ntsekouras, hellofromtonya.

Location:
trunk
Files:
5 added
15 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-template-utils.php

    r51300 r52062  
    11<?php
    22/**
    3  * Utilities used to fetch and create templates.
     3 * Utilities used to fetch and create templates and template parts.
    44 *
    55 * @package WordPress
     
    77 */
    88
     9// Define constants for supported wp_template_part_area taxonomy.
     10if ( ! defined( 'WP_TEMPLATE_PART_AREA_HEADER' ) ) {
     11    define( 'WP_TEMPLATE_PART_AREA_HEADER', 'header' );
     12}
     13if ( ! defined( 'WP_TEMPLATE_PART_AREA_FOOTER' ) ) {
     14    define( 'WP_TEMPLATE_PART_AREA_FOOTER', 'footer' );
     15}
     16if ( ! defined( 'WP_TEMPLATE_PART_AREA_SIDEBAR' ) ) {
     17    define( 'WP_TEMPLATE_PART_AREA_SIDEBAR', 'sidebar' );
     18}
     19if ( ! defined( 'WP_TEMPLATE_PART_AREA_UNCATEGORIZED' ) ) {
     20    define( 'WP_TEMPLATE_PART_AREA_UNCATEGORIZED', 'uncategorized' );
     21}
     22
     23/**
     24 * Returns a filtered list of allowed area values for template parts.
     25 *
     26 * @since 5.9.0
     27 *
     28 * @return array The supported template part area values.
     29 */
     30function get_allowed_block_template_part_areas() {
     31    $default_area_definitions = array(
     32        array(
     33            'area'        => WP_TEMPLATE_PART_AREA_UNCATEGORIZED,
     34            'label'       => __( 'General' ),
     35            'description' => __(
     36                'General templates often perform a specific role like displaying post content, and are not tied to any particular area.'
     37            ),
     38            'icon'        => 'layout',
     39            'area_tag'    => 'div',
     40        ),
     41        array(
     42            'area'        => WP_TEMPLATE_PART_AREA_HEADER,
     43            'label'       => __( 'Header' ),
     44            'description' => __(
     45                'The Header template defines a page area that typically contains a title, logo, and main navigation.'
     46            ),
     47            'icon'        => 'header',
     48            'area_tag'    => 'header',
     49        ),
     50        array(
     51            'area'        => WP_TEMPLATE_PART_AREA_FOOTER,
     52            'label'       => __( 'Footer' ),
     53            'description' => __(
     54                'The Footer template defines a page area that typically contains site credits, social links, or any other combination of blocks.'
     55            ),
     56            'icon'        => 'footer',
     57            'area_tag'    => 'footer',
     58        ),
     59    );
     60
     61    /**
     62     * Filters the list of allowed template part area values.
     63     *
     64     * @since 5.9.0
     65     *
     66     * @param array $default_areas An array of supported area objects.
     67     */
     68    return apply_filters( 'default_wp_template_part_areas', $default_area_definitions );
     69}
     70
     71
     72/**
     73 * Returns a filtered list of default template types, containing their
     74 * localized titles and descriptions.
     75 *
     76 * @since 5.9.0
     77 *
     78 * @return array The default template types.
     79 */
     80function get_default_block_template_types() {
     81    $default_template_types = array(
     82        'index'          => array(
     83            'title'       => _x( 'Index', 'Template name' ),
     84            'description' => __( 'The default template used when no other template is available. This is a required template in WordPress.' ),
     85        ),
     86        'home'           => array(
     87            'title'       => _x( 'Home', 'Template name' ),
     88            'description' => __( 'Template used for the main page that displays blog posts. This is the front page by default in WordPress. If a static front page is set, this is the template used for the page that contains the latest blog posts.' ),
     89        ),
     90        'front-page'     => array(
     91            'title'       => _x( 'Front Page', 'Template name' ),
     92            'description' => __( 'Template used to render the front page of the site, whether it displays blog posts or a static page. The front page template takes precedence over the "Home" template.' ),
     93        ),
     94        'singular'       => array(
     95            'title'       => _x( 'Singular', 'Template name' ),
     96            'description' => __( 'Template used for displaying single views of the content. This template is a fallback for the Single, Post, and Page templates, which take precedence when they exist.' ),
     97        ),
     98        'single'         => array(
     99            'title'       => _x( 'Single Post', 'Template name' ),
     100            'description' => __( 'Template used to display a single blog post.' ),
     101        ),
     102        'page'           => array(
     103            'title'       => _x( 'Page', 'Template name' ),
     104            'description' => __( 'Template used to display individual pages.' ),
     105        ),
     106        'archive'        => array(
     107            'title'       => _x( 'Archive', 'Template name' ),
     108            'description' => __( 'The archive template displays multiple entries at once. It is used as a fallback for the Category, Author, and Date templates, which take precedence when they are available.' ),
     109        ),
     110        'author'         => array(
     111            'title'       => _x( 'Author', 'Template name' ),
     112            'description' => __( 'Archive template used to display a list of posts from a single author.' ),
     113        ),
     114        'category'       => array(
     115            'title'       => _x( 'Category', 'Template name' ),
     116            'description' => __( 'Archive template used to display a list of posts from the same category.' ),
     117        ),
     118        'taxonomy'       => array(
     119            'title'       => _x( 'Taxonomy', 'Template name' ),
     120            'description' => __( 'Archive template used to display a list of posts from the same taxonomy.' ),
     121        ),
     122        'date'           => array(
     123            'title'       => _x( 'Date', 'Template name' ),
     124            'description' => __( 'Archive template used to display a list of posts from a specific date.' ),
     125        ),
     126        'tag'            => array(
     127            'title'       => _x( 'Tag', 'Template name' ),
     128            'description' => __( 'Archive template used to display a list of posts with a given tag.' ),
     129        ),
     130        'attachment'     => array(
     131            'title'       => __( 'Media' ),
     132            'description' => __( 'Template used to display individual media items or attachments.' ),
     133        ),
     134        'search'         => array(
     135            'title'       => _x( 'Search', 'Template name' ),
     136            'description' => __( 'Template used to display search results.' ),
     137        ),
     138        'privacy-policy' => array(
     139            'title'       => __( 'Privacy Policy' ),
     140            'description' => '',
     141        ),
     142        '404'            => array(
     143            'title'       => _x( '404', 'Template name' ),
     144            'description' => __( 'Template shown when no content is found.' ),
     145        ),
     146    );
     147
     148    /**
     149     * Filters the list of template types.
     150     *
     151     * @since 5.9.0
     152     *
     153     * @param array $default_template_types An array of template types, formatted as [ slug => [ title, description ] ].
     154     */
     155    return apply_filters( 'default_template_types', $default_template_types );
     156}
     157
     158/**
     159 * Checks whether the input 'area' is a supported value.
     160 * Returns the input if supported, otherwise returns the 'uncategorized' value.
     161 *
     162 * @access private
     163 * @since 5.9.0
     164 *
     165 * @param string $type Template part area name.
     166 *
     167 * @return string Input if supported, else the uncategorized value.
     168 */
     169function _filter_block_template_part_area( $type ) {
     170    $allowed_areas = array_map(
     171        static function ( $item ) {
     172            return $item['area'];
     173        },
     174        get_allowed_block_template_part_areas()
     175    );
     176    if ( in_array( $type, $allowed_areas, true ) ) {
     177        return $type;
     178    }
     179
     180    $warning_message = sprintf(
     181        /* translators: %1$s: Template area type, %2$s: the uncategorized template area value. */
     182        __( '"%1$s" is not a supported wp_template_part area value and has been added as "%2$s".' ),
     183        $type,
     184        WP_TEMPLATE_PART_AREA_UNCATEGORIZED
     185    );
     186    trigger_error( $warning_message, E_USER_NOTICE );
     187    return WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
     188}
     189
     190/**
     191 * Finds all nested template part file paths in a theme's directory.
     192 *
     193 * @access private
     194 * @since 5.9.0
     195 *
     196 * @param string $base_directory The theme's file path.
     197 * @return array $path_list A list of paths to all template part files.
     198 */
     199function _get_block_templates_paths( $base_directory ) {
     200    $path_list = array();
     201    if ( file_exists( $base_directory ) ) {
     202        $nested_files      = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) );
     203        $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH );
     204        foreach ( $nested_html_files as $path => $file ) {
     205            $path_list[] = $path;
     206        }
     207    }
     208    return $path_list;
     209}
     210
     211/**
     212 * Retrieves the template file from the theme for a given slug.
     213 *
     214 * @access private
     215 * @since 5.9.0
     216 *
     217 * @param string $template_type wp_template or wp_template_part.
     218 * @param string $slug template slug.
     219 *
     220 * @return array|null Template.
     221 */
     222function _get_block_template_file( $template_type, $slug ) {
     223    if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) {
     224        return null;
     225    }
     226
     227    $template_base_paths = array(
     228        'wp_template'      => 'block-templates',
     229        'wp_template_part' => 'block-template-parts',
     230    );
     231    $themes              = array(
     232        get_stylesheet() => get_stylesheet_directory(),
     233        get_template()   => get_template_directory(),
     234    );
     235    foreach ( $themes as $theme_slug => $theme_dir ) {
     236        $file_path = $theme_dir . '/' . $template_base_paths[ $template_type ] . '/' . $slug . '.html';
     237        if ( file_exists( $file_path ) ) {
     238            $new_template_item = array(
     239                'slug'  => $slug,
     240                'path'  => $file_path,
     241                'theme' => $theme_slug,
     242                'type'  => $template_type,
     243            );
     244
     245            if ( 'wp_template_part' === $template_type ) {
     246                return _add_block_template_part_area_info( $new_template_item );
     247            }
     248
     249            if ( 'wp_template' === $template_type ) {
     250                return _add_block_template_info( $new_template_item );
     251            }
     252
     253            return $new_template_item;
     254        }
     255    }
     256
     257    return null;
     258}
     259
     260/**
     261 * Retrieves the template files from  the theme.
     262 *
     263 * @access private
     264 * @since 5.9.0
     265 *
     266 * @param string $template_type wp_template or wp_template_part.
     267 *
     268 * @return array Template.
     269 */
     270function _get_block_templates_files( $template_type ) {
     271    if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) {
     272        return null;
     273    }
     274
     275    $template_base_paths = array(
     276        'wp_template'      => 'block-templates',
     277        'wp_template_part' => 'block-template-parts',
     278    );
     279    $themes              = array(
     280        get_stylesheet() => get_stylesheet_directory(),
     281        get_template()   => get_template_directory(),
     282    );
     283
     284    $template_files = array();
     285    foreach ( $themes as $theme_slug => $theme_dir ) {
     286        $theme_template_files = _get_block_templates_paths( $theme_dir . '/' . $template_base_paths[ $template_type ] );
     287        foreach ( $theme_template_files as $template_file ) {
     288            $template_base_path = $template_base_paths[ $template_type ];
     289            $template_slug      = substr(
     290                $template_file,
     291                // Starting position of slug.
     292                strpos( $template_file, $template_base_path . DIRECTORY_SEPARATOR ) + 1 + strlen( $template_base_path ),
     293                // Subtract ending '.html'.
     294                -5
     295            );
     296            $new_template_item = array(
     297                'slug'  => $template_slug,
     298                'path'  => $template_file,
     299                'theme' => $theme_slug,
     300                'type'  => $template_type,
     301            );
     302
     303            if ( 'wp_template_part' === $template_type ) {
     304                $template_files[] = _add_block_template_part_area_info( $new_template_item );
     305            }
     306
     307            if ( 'wp_template' === $template_type ) {
     308                $template_files[] = _add_block_template_info( $new_template_item );
     309            }
     310        }
     311    }
     312
     313    return $template_files;
     314}
     315
     316/**
     317 * Attempts to add custom template information to the template item.
     318 *
     319 * @access private
     320 * @since 5.9.0
     321 *
     322 * @param array $template_item Template to add information to (requires 'slug' field).
     323 * @return array Template
     324 */
     325function _add_block_template_info( $template_item ) {
     326    if ( ! WP_Theme_JSON_Resolver::theme_has_support() ) {
     327        return $template_item;
     328    }
     329
     330    $theme_data = WP_Theme_JSON_Resolver::get_theme_data()->get_custom_templates();
     331    if ( isset( $theme_data[ $template_item['slug'] ] ) ) {
     332        $template_item['title']     = $theme_data[ $template_item['slug'] ]['title'];
     333        $template_item['postTypes'] = $theme_data[ $template_item['slug'] ]['postTypes'];
     334    }
     335
     336    return $template_item;
     337}
     338
     339/**
     340 * Attempts to add the template part's area information to the input template.
     341 *
     342 * @access private
     343 * @since 5.9.0
     344 *
     345 * @param array $template_info Template to add information to (requires 'type' and 'slug' fields).
     346 *
     347 * @return array Template.
     348 */
     349function _add_block_template_part_area_info( $template_info ) {
     350    if ( WP_Theme_JSON_Resolver::theme_has_support() ) {
     351        $theme_data = WP_Theme_JSON_Resolver::get_theme_data()->get_template_parts();
     352    }
     353
     354    if ( isset( $theme_data[ $template_info['slug'] ]['area'] ) ) {
     355        $template_info['title'] = $theme_data[ $template_info['slug'] ]['title'];
     356        $template_info['area']  = _filter_block_template_part_area( $theme_data[ $template_info['slug'] ]['area'] );
     357    } else {
     358        $template_info['area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
     359    }
     360
     361    return $template_info;
     362}
     363
     364/**
     365 * Returns an array containing the references of
     366 * the passed blocks and their inner blocks.
     367 *
     368 * @access private
     369 * @since 5.9.0
     370 *
     371 * @param array $blocks array of blocks.
     372 *
     373 * @return array block references to the passed blocks and their inner blocks.
     374 */
     375function _flatten_blocks( &$blocks ) {
     376    $all_blocks = array();
     377    $queue      = array();
     378    foreach ( $blocks as &$block ) {
     379        $queue[] = &$block;
     380    }
     381
     382    while ( count( $queue ) > 0 ) {
     383        $block = &$queue[0];
     384        array_shift( $queue );
     385        $all_blocks[] = &$block;
     386
     387        if ( ! empty( $block['innerBlocks'] ) ) {
     388            foreach ( $block['innerBlocks'] as &$inner_block ) {
     389                $queue[] = &$inner_block;
     390            }
     391        }
     392    }
     393
     394    return $all_blocks;
     395}
     396
     397/**
     398 * Parses wp_template content and injects the current theme's
     399 * stylesheet as a theme attribute into each wp_template_part
     400 *
     401 * @access private
     402 * @since 5.9.0
     403 *
     404 * @param string $template_content serialized wp_template content.
     405 *
     406 * @return string Updated wp_template content.
     407 */
     408function _inject_theme_attribute_in_block_template_content( $template_content ) {
     409    $has_updated_content = false;
     410    $new_content         = '';
     411    $template_blocks     = parse_blocks( $template_content );
     412
     413    $blocks = _flatten_blocks( $template_blocks );
     414    foreach ( $blocks as &$block ) {
     415        if (
     416            'core/template-part' === $block['blockName'] &&
     417            ! isset( $block['attrs']['theme'] )
     418        ) {
     419            $block['attrs']['theme'] = wp_get_theme()->get_stylesheet();
     420            $has_updated_content     = true;
     421        }
     422    }
     423
     424    if ( $has_updated_content ) {
     425        foreach ( $template_blocks as &$block ) {
     426            $new_content .= serialize_block( $block );
     427        }
     428
     429        return $new_content;
     430    }
     431
     432    return $template_content;
     433}
     434
     435/**
     436 * Build a unified template object based on a theme file.
     437 *
     438 * @access private
     439 * @since 5.9.0
     440 *
     441 * @param array $template_file Theme file.
     442 * @param array $template_type wp_template or wp_template_part.
     443 *
     444 * @return WP_Block_Template Template.
     445 */
     446function _build_block_template_result_from_file( $template_file, $template_type ) {
     447    $default_template_types = get_default_block_template_types();
     448    $template_content       = file_get_contents( $template_file['path'] );
     449    $theme                  = wp_get_theme()->get_stylesheet();
     450
     451    $template                 = new WP_Block_Template();
     452    $template->id             = $theme . '//' . $template_file['slug'];
     453    $template->theme          = $theme;
     454    $template->content        = _inject_theme_attribute_in_block_template_content( $template_content );
     455    $template->slug           = $template_file['slug'];
     456    $template->source         = 'theme';
     457    $template->type           = $template_type;
     458    $template->title          = ! empty( $template_file['title'] ) ? $template_file['title'] : $template_file['slug'];
     459    $template->status         = 'publish';
     460    $template->has_theme_file = true;
     461    $template->is_custom      = true;
     462
     463    if ( 'wp_template' === $template_type && isset( $default_template_types[ $template_file['slug'] ] ) ) {
     464        $template->description = $default_template_types[ $template_file['slug'] ]['description'];
     465        $template->title       = $default_template_types[ $template_file['slug'] ]['title'];
     466        $template->is_custom   = false;
     467    }
     468
     469    if ( 'wp_template' === $template_type && isset( $template_file['postTypes'] ) ) {
     470        $template->post_types = $template_file['postTypes'];
     471    }
     472
     473    if ( 'wp_template_part' === $template_type && isset( $template_file['area'] ) ) {
     474        $template->area = $template_file['area'];
     475    }
     476
     477    return $template;
     478}
     479
    9480/**
    10481 * Build a unified template object based a post Object.
    11482 *
    12483 * @access private
    13  * @since 5.8.0
     484 * @since 5.9.0
    14485 *
    15486 * @param WP_Post $post Template post.
     
    17488 * @return WP_Block_Template|WP_Error Template.
    18489 */
    19 function _build_template_result_from_post( $post ) {
    20     $terms = get_the_terms( $post, 'wp_theme' );
     490function _build_block_template_result_from_post( $post ) {
     491    $default_template_types = get_default_block_template_types();
     492    $terms                  = get_the_terms( $post, 'wp_theme' );
    21493
    22494    if ( is_wp_error( $terms ) ) {
     
    28500    }
    29501
    30     $theme = $terms[0]->name;
     502    $theme          = $terms[0]->name;
     503    $has_theme_file = wp_get_theme()->get_stylesheet() === $theme &&
     504        null !== _get_block_template_file( $post->post_type, $post->post_name );
    31505
    32506    $template                 = new WP_Block_Template();
     
    41515    $template->title          = $post->post_title;
    42516    $template->status         = $post->post_status;
    43     $template->has_theme_file = false;
     517    $template->has_theme_file = $has_theme_file;
     518    $template->is_custom      = true;
     519
     520    if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
     521        $template->is_custom = false;
     522    }
     523
     524    if ( 'wp_template_part' === $post->post_type ) {
     525        $type_terms = get_the_terms( $post, 'wp_template_part_area' );
     526        if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
     527            $template->area = $type_terms[0]->name;
     528        }
     529    }
    44530
    45531    return $template;
     
    54540 *     Optional. Arguments to retrieve templates.
    55541 *
    56  *     @type array  $slug__in List of slugs to include.
    57  *     @type int    $wp_id Post ID of customized template.
     542 *     @type array  $slug__in  List of slugs to include.
     543 *     @type int    $wp_id     Post ID of customized template.
     544 *     @type string $area      A 'wp_template_part_area' taxonomy value to filter by (for wp_template_part template type only).
     545 *     @type string $post_type Post type to get the templates for.
    58546 * }
    59  * @param string $template_type Optional. The template type (post type). Default 'wp_template'.
    60  * @return WP_Block_Template[] Block template objects.
     547 * @param array $template_type wp_template or wp_template_part.
     548 *
     549 * @return array Templates.
    61550 */
    62551function get_block_templates( $query = array(), $template_type = 'wp_template' ) {
     552    /**
     553     * Filters the block templates array before the query takes place.
     554     *
     555     * Return a non-null value to bypass the WordPress queries.
     556     *
     557     * @since 5.9
     558     *
     559     * @param WP_Block_Template[]|null $block_templates Return an array of block templates to short-circuit the default query,
     560     *                                                  or null to allow WP to run it's normal queries.
     561     * @param array $query {
     562     *     Optional. Arguments to retrieve templates.
     563     *
     564     *     @type array  $slug__in List of slugs to include.
     565     *     @type int    $wp_id Post ID of customized template.
     566     *     @type string $post_type Post type to get the templates for.
     567     * }
     568     * @param array $template_type wp_template or wp_template_part.
     569     */
     570    $templates = apply_filters( 'pre_get_block_templates', null, $query, $template_type );
     571    if ( ! is_null( $templates ) ) {
     572        return $templates;
     573    }
     574
     575    $post_type     = isset( $query['post_type'] ) ? $query['post_type'] : '';
    63576    $wp_query_args = array(
    64577        'post_status'    => array( 'auto-draft', 'draft', 'publish' ),
     
    75588    );
    76589
     590    if ( 'wp_template_part' === $template_type && isset( $query['area'] ) ) {
     591        $wp_query_args['tax_query'][]           = array(
     592            'taxonomy' => 'wp_template_part_area',
     593            'field'    => 'name',
     594            'terms'    => $query['area'],
     595        );
     596        $wp_query_args['tax_query']['relation'] = 'AND';
     597    }
     598
    77599    if ( isset( $query['slug__in'] ) ) {
    78600        $wp_query_args['post_name__in'] = $query['slug__in'];
    79601    }
    80602
    81     // This is only needed for the regular templates CPT listing and editor.
     603    // This is only needed for the regular templates/template parts CPT listing and editor.
    82604    if ( isset( $query['wp_id'] ) ) {
    83605        $wp_query_args['p'] = $query['wp_id'];
     
    89611    $query_result   = array();
    90612    foreach ( $template_query->posts as $post ) {
    91         $template = _build_template_result_from_post( $post );
    92 
    93         if ( ! is_wp_error( $template ) ) {
    94             $query_result[] = $template;
    95         }
    96     }
    97 
    98     return $query_result;
     613        $template = _build_block_template_result_from_post( $post );
     614
     615        if ( is_wp_error( $template ) ) {
     616            continue;
     617        }
     618
     619        if ( $post_type && ! $template->is_custom ) {
     620            continue;
     621        }
     622
     623        $query_result[] = $template;
     624    }
     625
     626    if ( ! isset( $query['wp_id'] ) ) {
     627        $template_files = _get_block_templates_files( $template_type );
     628        foreach ( $template_files as $template_file ) {
     629            $template = _build_block_template_result_from_file( $template_file, $template_type );
     630
     631            if ( $post_type && ! $template->is_custom ) {
     632                continue;
     633            }
     634
     635            if ( $post_type &&
     636                isset( $template->post_types ) &&
     637                ! in_array( $post_type, $template->post_types, true )
     638            ) {
     639                continue;
     640            }
     641
     642            $is_not_custom   = false === array_search(
     643                wp_get_theme()->get_stylesheet() . '//' . $template_file['slug'],
     644                array_column( $query_result, 'id' ),
     645                true
     646            );
     647            $fits_slug_query =
     648                ! isset( $query['slug__in'] ) || in_array( $template_file['slug'], $query['slug__in'], true );
     649            $fits_area_query =
     650                ! isset( $query['area'] ) || $template_file['area'] === $query['area'];
     651            $should_include  = $is_not_custom && $fits_slug_query && $fits_area_query;
     652            if ( $should_include ) {
     653                $query_result[] = $template;
     654            }
     655        }
     656    }
     657
     658    /**
     659     * Filters the array of queried block templates array after they've been fetched.
     660     *
     661     * @since 5.9
     662     *
     663     * @param WP_Block_Template[] $query_result Array of found block templates.
     664     * @param array $query {
     665     *     Optional. Arguments to retrieve templates.
     666     *
     667     *     @type array  $slug__in List of slugs to include.
     668     *     @type int    $wp_id Post ID of customized template.
     669     * }
     670     * @param array $template_type wp_template or wp_template_part.
     671     */
     672    return apply_filters( 'get_block_templates', $query_result, $query, $template_type );
    99673}
    100674
     
    105679 *
    106680 * @param string $id            Template unique identifier (example: theme_slug//template_slug).
    107  * @param string $template_type Optional. The template type (post type). Default 'wp_template'.
     681 * @param array  $template_type Optional. Template type: `'wp_template'` or '`wp_template_part'`.
     682 *                              Default `'wp_template'`.
     683 *
    108684 * @return WP_Block_Template|null Template.
    109685 */
    110686function get_block_template( $id, $template_type = 'wp_template' ) {
     687    /**
     688     * Filters the block templates array before the query takes place.
     689     *
     690     * Return a non-null value to bypass the WordPress queries.
     691     *
     692     * @since 5.9.0
     693     *
     694     * @param WP_Block_Template|null $block_template Return block template object to short-circuit the default query,
     695     *                                               or null to allow WP to run its normal queries.
     696     * @param string $id                             Template unique identifier (example: theme_slug//template_slug).
     697     * @param array  $template_type                  Template type: `'wp_template'` or '`wp_template_part'`.
     698     */
     699    $block_template = apply_filters( 'pre_get_block_template', null, $id, $template_type );
     700    if ( ! is_null( $block_template ) ) {
     701        return $block_template;
     702    }
     703
    111704    $parts = explode( '//', $id, 2 );
    112705    if ( count( $parts ) < 2 ) {
     
    132725
    133726    if ( count( $posts ) > 0 ) {
    134         $template = _build_template_result_from_post( $posts[0] );
     727        $template = _build_block_template_result_from_post( $posts[0] );
    135728
    136729        if ( ! is_wp_error( $template ) ) {
     
    139732    }
    140733
    141     return null;
    142 }
     734    $block_template = get_block_file_template( $id, $template_type );
     735
     736    /**
     737     * Filters the array of queried block templates array after they've been fetched.
     738     *
     739     * @since 5.9
     740     *
     741     * @param WP_Block_Template $block_template The found block template.
     742     * @param string            $id             Template unique identifier (example: theme_slug//template_slug).
     743     * @param array             $template_type  Template type: `'wp_template'` or '`wp_template_part'`.
     744     */
     745    return apply_filters( 'get_block_template', $block_template, $id, $template_type );
     746}
     747
     748/**
     749 * Retrieves a single unified template object using its id.
     750 *
     751 * @since 5.9.0
     752 *
     753 * @param string $id            Template unique identifier (example: theme_slug//template_slug).
     754 * @param array  $template_type Optional. Template type: `'wp_template'` or '`wp_template_part'`.
     755 *                              Default `'wp_template'`.
     756 */
     757function get_block_file_template( $id, $template_type = 'wp_template' ) {
     758    /**
     759     * Filters the block templates array before the query takes place.
     760     *
     761     * Return a non-null value to bypass the WordPress queries.
     762     *
     763     *
     764     * @since 5.9.0
     765     *
     766     * @param WP_Block_Template|null $block_template Return block template object to short-circuit the default query,
     767     *                                               or null to allow WP to run its normal queries.
     768     * @param string $id                             Template unique identifier (example: theme_slug//template_slug).
     769     * @param array  $template_type                  Template type: `'wp_template'` or '`wp_template_part'`.
     770     */
     771    $block_template = apply_filters( 'pre_get_block_file_template', null, $id, $template_type );
     772    if ( ! is_null( $block_template ) ) {
     773        return $block_template;
     774    }
     775
     776    $parts = explode( '//', $id, 2 );
     777    if ( count( $parts ) < 2 ) {
     778        /** This filter is documented at the end of this function */
     779        return apply_filters( 'get_block_file_template', null, $id, $template_type );
     780    }
     781    list( $theme, $slug ) = $parts;
     782
     783    if ( wp_get_theme()->get_stylesheet() !== $theme ) {
     784        /** This filter is documented at the end of this function */
     785        return apply_filters( 'get_block_file_template', null, $id, $template_type );
     786    }
     787
     788    $template_file = _get_block_template_file( $template_type, $slug );
     789    if ( null === $template_file ) {
     790        /** This filter is documented at the end of this function */
     791        return apply_filters( 'get_block_file_template', null, $id, $template_type );
     792    }
     793
     794    $block_template = _build_block_template_result_from_file( $template_file, $template_type );
     795
     796    /**
     797     * Filters the array of queried block templates array after they've been fetched.
     798     *
     799     * @since 5.9.0
     800     *
     801     * @param WP_Block_Template $block_template The found block template.
     802     * @param string            $id             Template unique identifier (example: theme_slug//template_slug).
     803     * @param array             $template_type  Template type: `'wp_template'` or '`wp_template_part'`.
     804     */
     805    return apply_filters( 'get_block_file_template', $block_template, $id, $template_type );
     806}
     807
     808/**
     809 * Print a template-part.
     810 *
     811 * @since 5.9.0
     812 *
     813 * @param string $part The template-part to print. Use "header" or "footer".
     814 */
     815function block_template_part( $part ) {
     816    $template_part = get_block_template( get_stylesheet() . '//' . $part, 'wp_template_part' );
     817    if ( ! $template_part || empty( $template_part->content ) ) {
     818        return;
     819    }
     820    echo do_blocks( $template_part->content );
     821}
     822
     823/**
     824 * Print the header template-part.
     825 *
     826 * @since 5.9.0
     827 */
     828function block_header_area() {
     829    block_template_part( 'header' );
     830}
     831
     832/**
     833 * Print the footer template-part.
     834 *
     835 * @since 5.9.0
     836 */
     837function block_footer_area() {
     838    block_template_part( 'footer' );
     839}
  • trunk/src/wp-includes/blocks.php

    r52057 r52062  
    11551155 * It's used in QueryPaginationNext and QueryPaginationPrevious blocks.
    11561156 *
     1157 * @since 5.9.0
     1158 *
    11571159 * @param WP_Block $block   Block instance.
    11581160 * @param boolean  $is_next Flag for hanlding `next/previous` blocks.
  • trunk/src/wp-includes/blocks/index.php

    r51176 r52062  
    3838require ABSPATH . WPINC . '/blocks/social-link.php';
    3939require ABSPATH . WPINC . '/blocks/tag-cloud.php';
     40require ABSPATH . WPINC . '/blocks/template-part.php';
    4041
    4142/**
  • trunk/src/wp-includes/class-wp-block-template.php

    r51003 r52062  
    101101     */
    102102    public $has_theme_file;
     103
     104    /**
     105     * Whether a template is a custom template.
     106     *
     107     * @since 5.9.0
     108     *
     109     * @var boolean
     110     */
     111    public $is_custom = true;
    103112}
  • trunk/src/wp-includes/class-wp-theme-json.php

    r52049 r52062  
    602602    public function get_custom_templates() {
    603603        $custom_templates = array();
    604         if ( ! isset( $this->theme_json['customTemplates'] ) ) {
     604        if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
    605605            return $custom_templates;
    606606        }
     
    626626    public function get_template_parts() {
    627627        $template_parts = array();
    628         if ( ! isset( $this->theme_json['templateParts'] ) ) {
     628        if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
    629629            return $template_parts;
    630630        }
  • trunk/src/wp-includes/default-filters.php

    r52049 r52062  
    665665add_filter( 'render_block_context', '_block_template_render_without_post_block_context' );
    666666add_filter( 'pre_wp_unique_post_slug', 'wp_filter_wp_template_unique_post_slug', 10, 5 );
     667add_action( 'save_post_wp_template_part', 'wp_set_unique_slug_on_create_template_part' );
    667668add_action( 'wp_footer', 'the_block_template_skip_link' );
    668669add_action( 'setup_theme', 'wp_enable_block_templates' );
  • trunk/src/wp-includes/post.php

    r52045 r52062  
    356356            ),
    357357            'map_meta_cap'          => true,
     358            'supports'              => array(
     359                'title',
     360                'slug',
     361                'excerpt',
     362                'editor',
     363                'revisions',
     364            ),
     365        )
     366    );
     367
     368    register_post_type(
     369        'wp_template_part',
     370        array(
     371            'labels'                => array(
     372                'name'                  => __( 'Template Parts' ),
     373                'singular_name'         => __( 'Template Part' ),
     374                'add_new'               => _x( 'Add New', 'Template Part' ),
     375                'add_new_item'          => __( 'Add New Template Part' ),
     376                'new_item'              => __( 'New Template Part' ),
     377                'edit_item'             => __( 'Edit Template Part' ),
     378                'view_item'             => __( 'View Template Part' ),
     379                'all_items'             => __( 'All Template Parts' ),
     380                'search_items'          => __( 'Search Template Parts' ),
     381                'parent_item_colon'     => __( 'Parent Template Part:' ),
     382                'not_found'             => __( 'No template parts found.' ),
     383                'not_found_in_trash'    => __( 'No template parts found in Trash.' ),
     384                'archives'              => __( 'Template part archives' ),
     385                'insert_into_item'      => __( 'Insert into template part' ),
     386                'uploaded_to_this_item' => __( 'Uploaded to this template part' ),
     387                'filter_items_list'     => __( 'Filter template parts list' ),
     388                'items_list_navigation' => __( 'Template parts list navigation' ),
     389                'items_list'            => __( 'Template parts list' ),
     390            ),
     391            'description'           => __( 'Template parts to include in your templates.' ),
     392            'public'                => false,
     393            '_builtin'              => true, /* internal use only. don't use this when registering your own post type. */
     394            'has_archive'           => false,
     395            'show_ui'               => false,
     396            'show_in_menu'          => false,
     397            'show_in_rest'          => true,
     398            'rewrite'               => false,
     399            'rest_base'             => 'template-parts',
     400            'rest_controller_class' => 'WP_REST_Templates_Controller',
     401            'map_meta_cap'          => true,
     402            'capabilities'          => array(
     403                'create_posts'           => 'edit_theme_options',
     404                'delete_posts'           => 'edit_theme_options',
     405                'delete_others_posts'    => 'edit_theme_options',
     406                'delete_private_posts'   => 'edit_theme_options',
     407                'delete_published_posts' => 'edit_theme_options',
     408                'edit_posts'             => 'edit_theme_options',
     409                'edit_others_posts'      => 'edit_theme_options',
     410                'edit_private_posts'     => 'edit_theme_options',
     411                'edit_published_posts'   => 'edit_theme_options',
     412                'publish_posts'          => 'edit_theme_options',
     413                'read'                   => 'edit_theme_options',
     414                'read_private_posts'     => 'edit_theme_options',
     415            ),
    358416            'supports'              => array(
    359417                'title',
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php

    r51962 r52062  
    157157            $query['area'] = $request['area'];
    158158        }
     159        if ( isset( $request['post_type'] ) ) {
     160            $query['post_type'] = $request['post_type'];
     161        }
    159162
    160163        $templates = array();
     
    188191     */
    189192    public function get_item( $request ) {
    190         $template = get_block_template( $request['id'], $this->post_type );
     193        if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
     194            $template = get_block_file_template( $request['id'], $this->post_type );
     195        } else {
     196            $template = get_block_template( $request['id'], $this->post_type );
     197        }
    191198
    192199        if ( ! $template ) {
     
    221228        if ( ! $template ) {
    222229            return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
     230        }
     231
     232        if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
     233            wp_delete_post( $template->wp_id, true );
     234            return $this->prepare_item_for_response( get_block_file_template( $request['id'], $this->post_type ), $request );
    223235        }
    224236
     
    396408        }
    397409
     410        if ( 'wp_template_part' === $this->post_type ) {
     411            if ( isset( $request['area'] ) ) {
     412                $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] );
     413            } elseif ( null !== $template && 'custom' !== $template->source && $template->area ) {
     414                $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area );
     415            } elseif ( ! $template->area ) {
     416                $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
     417            }
     418        }
     419
    398420        return $changes;
    399421    }
     
    503525     *
    504526     * @since 5.8.0
     527     * @since 5.9.0 Added `'area'` and `'post_type'`.
    505528     *
    506529     * @return array Collection parameters.
     
    508531    public function get_collection_params() {
    509532        return array(
    510             'context' => $this->get_context_param(),
    511             'wp_id'   => array(
     533            'context'   => $this->get_context_param(),
     534            'wp_id'     => array(
    512535                'description' => __( 'Limit to the specified post id.' ),
    513536                'type'        => 'integer',
    514537            ),
     538            'area'      => array(
     539                'description' => __( 'Limit to the specified template part area.' ),
     540                'type'        => 'string',
     541            ),
     542            'post_type' => array(
     543                'description' => __( 'Post type to get the templates for.' ),
     544                'type'        => 'string',
     545            ),
    515546        );
    516547    }
     
    520551     *
    521552     * @since 5.8.0
     553     * @since 5.9.0 Added `'area'`.
    522554     *
    523555     * @return array Item schema data.
     
    597629        );
    598630
     631        if ( 'wp_template_part' === $this->post_type ) {
     632            $schema['properties']['area'] = array(
     633                'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ),
     634                'type'        => 'string',
     635                'context'     => array( 'embed', 'view', 'edit' ),
     636            );
     637        }
     638
    599639        $this->schema = $schema;
    600640
  • trunk/src/wp-includes/taxonomy.php

    r52041 r52062  
    1919 *
    2020 * @since 2.8.0
     21 * @since 5.9.0 Added `'wp_template_part_area'` taxonomy.
    2122 *
    2223 * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
     
    176177    register_taxonomy(
    177178        'wp_theme',
    178         array( 'wp_template', 'wp_global_styles' ),
     179        array( 'wp_template', 'wp_template_part', 'wp_global_styles' ),
    179180        array(
    180181            'public'            => false,
     
    183184                'name'          => __( 'Themes' ),
    184185                'singular_name' => __( 'Theme' ),
     186            ),
     187            'query_var'         => false,
     188            'rewrite'           => false,
     189            'show_ui'           => false,
     190            '_builtin'          => true,
     191            'show_in_nav_menus' => false,
     192            'show_in_rest'      => false,
     193        )
     194    );
     195
     196    register_taxonomy(
     197        'wp_template_part_area',
     198        array( 'wp_template_part' ),
     199        array(
     200            'public'            => false,
     201            'hierarchical'      => false,
     202            'labels'            => array(
     203                'name'          => __( 'Template Part Areas' ),
     204                'singular_name' => __( 'Template Part Area' ),
    185205            ),
    186206            'query_var'         => false,
  • trunk/src/wp-includes/theme-templates.php

    r51199 r52062  
    11<?php
     2
     3/**
     4 * Sets a custom slug when creating auto-draft template parts.
     5 *
     6 * This is only needed for auto-drafts created by the regular WP editor.
     7 * If this page is to be removed, this won't be necessary.
     8 *
     9 * @since 5.9.0
     10 *
     11 */
     12function wp_set_unique_slug_on_create_template_part( $post_id ) {
     13    $post = get_post( $post_id );
     14    if ( 'auto-draft' !== $post->post_status ) {
     15        return;
     16    }
     17
     18    if ( ! $post->post_name ) {
     19        wp_update_post(
     20            array(
     21                'ID'        => $post_id,
     22                'post_name' => 'custom_slug_' . uniqid(),
     23            )
     24        );
     25    }
     26
     27    $terms = get_the_terms( $post_id, 'wp_theme' );
     28    if ( ! is_array( $terms ) || ! count( $terms ) ) {
     29        wp_set_post_terms( $post_id, wp_get_theme()->get_stylesheet(), 'wp_theme' );
     30    }
     31}
    232
    333/**
     
    1545 */
    1646function wp_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) {
    17     if ( 'wp_template' !== $post_type ) {
     47    if ( 'wp_template' !== $post_type && 'wp_template_part' !== $post_type ) {
    1848        return $override_slug;
    1949    }
  • trunk/tests/phpunit/includes/functions.php

    r51105 r52062  
    338338    remove_action( 'init', 'register_core_block_types_from_metadata' );
    339339    remove_action( 'init', 'register_block_core_legacy_widget' );
     340    remove_action( 'init', 'register_block_core_template_part' );
    340341}
    341342tests_add_filter( 'init', '_unhook_block_registration', 1000 );
  • trunk/tests/phpunit/tests/block-template-utils.php

    r52010 r52062  
    77
    88/**
    9  * Tests for the Block Template Loader abstraction layer.
     9 * Tests for the Block Templates abstraction layer.
    1010 */
    1111class Block_Template_Utils_Test extends WP_UnitTestCase {
    1212    private static $post;
     13    private static $template_part_post;
    1314
    1415    public static function wpSetUpBeforeClass() {
     16        // We may need a block theme.
     17        // switch_theme( 'tt1-blocks' );
     18
     19        // Set up a template post corresponding to a different theme.
     20        // We do this to ensure resolution and slug creation works as expected,
     21        // even with another post of that same name present for another theme.
     22        $args       = array(
     23            'post_type'    => 'wp_template',
     24            'post_name'    => 'my_template',
     25            'post_title'   => 'My Template',
     26            'post_content' => 'Content',
     27            'post_excerpt' => 'Description of my template',
     28            'tax_input'    => array(
     29                'wp_theme' => array(
     30                    'this-theme-should-not-resolve',
     31                ),
     32            ),
     33        );
     34        self::$post = self::factory()->post->create_and_get( $args );
     35        wp_set_post_terms( self::$post->ID, 'this-theme-should-not-resolve', 'wp_theme' );
     36
    1537        // Set up template post.
    1638        $args       = array(
     
    2850        self::$post = self::factory()->post->create_and_get( $args );
    2951        wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' );
     52
     53        // Set up template part post.
     54        $template_part_args       = array(
     55            'post_type'    => 'wp_template_part',
     56            'post_name'    => 'my_template_part',
     57            'post_title'   => 'My Template Part',
     58            'post_content' => 'Content',
     59            'post_excerpt' => 'Description of my template part',
     60            'tax_input'    => array(
     61                'wp_theme'              => array(
     62                    get_stylesheet(),
     63                ),
     64                'wp_template_part_area' => array(
     65                    WP_TEMPLATE_PART_AREA_HEADER,
     66                ),
     67            ),
     68        );
     69        self::$template_part_post = self::factory()->post->create_and_get( $template_part_args );
     70        wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' );
     71        wp_set_post_terms( self::$template_part_post->ID, get_stylesheet(), 'wp_theme' );
    3072    }
    3173
     
    3577
    3678    public function test_build_template_result_from_post() {
    37         $template = _build_template_result_from_post(
     79        $template = _build_block_template_result_from_post(
    3880            self::$post,
    3981            'wp_template'
     
    4183
    4284        $this->assertNotWPError( $template );
    43         $this->assertSame( get_stylesheet() . '//my_template', $template->id );
    44         $this->assertSame( get_stylesheet(), $template->theme );
    45         $this->assertSame( 'my_template', $template->slug );
    46         $this->assertSame( 'publish', $template->status );
    47         $this->assertSame( 'custom', $template->source );
    48         $this->assertSame( 'My Template', $template->title );
    49         $this->assertSame( 'Description of my template', $template->description );
    50         $this->assertSame( 'wp_template', $template->type );
     85        $this->assertEquals( get_stylesheet() . '//my_template', $template->id );
     86        $this->assertEquals( get_stylesheet(), $template->theme );
     87        $this->assertEquals( 'my_template', $template->slug );
     88        $this->assertEquals( 'publish', $template->status );
     89        $this->assertEquals( 'custom', $template->source );
     90        $this->assertEquals( 'My Template', $template->title );
     91        $this->assertEquals( 'Description of my template', $template->description );
     92        $this->assertEquals( 'wp_template', $template->type );
     93
     94        // Test template parts.
     95        $template_part = _build_block_template_result_from_post(
     96            self::$template_part_post,
     97            'wp_template_part'
     98        );
     99        $this->assertNotWPError( $template_part );
     100        $this->assertEquals( get_stylesheet() . '//my_template_part', $template_part->id );
     101        $this->assertEquals( get_stylesheet(), $template_part->theme );
     102        $this->assertEquals( 'my_template_part', $template_part->slug );
     103        $this->assertEquals( 'publish', $template_part->status );
     104        $this->assertEquals( 'custom', $template_part->source );
     105        $this->assertEquals( 'My Template Part', $template_part->title );
     106        $this->assertEquals( 'Description of my template part', $template_part->description );
     107        $this->assertEquals( 'wp_template_part', $template_part->type );
     108        $this->assertEquals( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area );
     109    }
     110
     111    function test_build_block_template_result_from_file() {
     112        $template = _build_block_template_result_from_file(
     113            array(
     114                'slug' => 'single',
     115                'path' => __DIR__ . '/../data/templates/template.html',
     116            ),
     117            'wp_template'
     118        );
     119
     120        $this->assertEquals( get_stylesheet() . '//single', $template->id );
     121        $this->assertEquals( get_stylesheet(), $template->theme );
     122        $this->assertEquals( 'single', $template->slug );
     123        $this->assertEquals( 'publish', $template->status );
     124        $this->assertEquals( 'theme', $template->source );
     125        $this->assertEquals( 'Single Post', $template->title );
     126        $this->assertEquals( 'Template used to display a single blog post.', $template->description );
     127        $this->assertEquals( 'wp_template', $template->type );
     128
     129        // Test template parts.
     130        $template_part = _build_block_template_result_from_file(
     131            array(
     132                'slug' => 'header',
     133                'path' => __DIR__ . '/../data/templates/template.html',
     134                'area' => WP_TEMPLATE_PART_AREA_HEADER,
     135            ),
     136            'wp_template_part'
     137        );
     138        $this->assertEquals( get_stylesheet() . '//header', $template_part->id );
     139        $this->assertEquals( get_stylesheet(), $template_part->theme );
     140        $this->assertEquals( 'header', $template_part->slug );
     141        $this->assertEquals( 'publish', $template_part->status );
     142        $this->assertEquals( 'theme', $template_part->source );
     143        $this->assertEquals( 'header', $template_part->title );
     144        $this->assertEquals( '', $template_part->description );
     145        $this->assertEquals( 'wp_template_part', $template_part->type );
     146        $this->assertEquals( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area );
     147    }
     148
     149    function test_inject_theme_attribute_in_block_template_content() {
     150        $theme                           = get_stylesheet();
     151        $content_without_theme_attribute = '<!-- wp:template-part {"slug":"header","align":"full", "tagName":"header","className":"site-header"} /-->';
     152        $template_content                = _inject_theme_attribute_in_block_template_content(
     153            $content_without_theme_attribute,
     154            $theme
     155        );
     156        $expected                        = sprintf(
     157            '<!-- wp:template-part {"slug":"header","align":"full","tagName":"header","className":"site-header","theme":"%s"} /-->',
     158            get_stylesheet()
     159        );
     160        $this->assertEquals( $expected, $template_content );
     161
     162        $content_without_theme_attribute_nested = '<!-- wp:group --><!-- wp:template-part {"slug":"header","align":"full", "tagName":"header","className":"site-header"} /--><!-- /wp:group -->';
     163        $template_content                       = _inject_theme_attribute_in_block_template_content(
     164            $content_without_theme_attribute_nested,
     165            $theme
     166        );
     167        $expected                               = sprintf(
     168            '<!-- wp:group --><!-- wp:template-part {"slug":"header","align":"full","tagName":"header","className":"site-header","theme":"%s"} /--><!-- /wp:group -->',
     169            get_stylesheet()
     170        );
     171        $this->assertEquals( $expected, $template_content );
     172
     173        // Does not inject theme when there is an existing theme attribute.
     174        $content_with_existing_theme_attribute = '<!-- wp:template-part {"slug":"header","theme":"fake-theme","align":"full", "tagName":"header","className":"site-header"} /-->';
     175        $template_content                      = _inject_theme_attribute_in_block_template_content(
     176            $content_with_existing_theme_attribute,
     177            $theme
     178        );
     179        $this->assertEquals( $content_with_existing_theme_attribute, $template_content );
     180
     181        // Does not inject theme when there is no template part.
     182        $content_with_no_template_part = '<!-- wp:post-content /-->';
     183        $template_content              = _inject_theme_attribute_in_block_template_content(
     184            $content_with_no_template_part,
     185            $theme
     186        );
     187        $this->assertEquals( $content_with_no_template_part, $template_content );
     188    }
     189
     190    /**
     191     * Should retrieve the template from the theme files.
     192     */
     193    function test_get_block_template_from_file() {
     194        $this->markTestIncomplete();
     195        // Requires switching to a block theme.
     196        /* $id       = get_stylesheet() . '//' . 'index';
     197        $template = get_block_template( $id, 'wp_template' );
     198        $this->assertEquals( $id, $template->id );
     199        $this->assertEquals( get_stylesheet(), $template->theme );
     200        $this->assertEquals( 'index', $template->slug );
     201        $this->assertEquals( 'publish', $template->status );
     202        $this->assertEquals( 'theme', $template->source );
     203        $this->assertEquals( 'wp_template', $template->type );
     204
     205        // Test template parts.
     206        $id       = get_stylesheet() . '//' . 'header';
     207        $template = get_block_template( $id, 'wp_template_part' );
     208        $this->assertEquals( $id, $template->id );
     209        $this->assertEquals( get_stylesheet(), $template->theme );
     210        $this->assertEquals( 'header', $template->slug );
     211        $this->assertEquals( 'publish', $template->status );
     212        $this->assertEquals( 'theme', $template->source );
     213        $this->assertEquals( 'wp_template_part', $template->type );
     214        $this->assertEquals( WP_TEMPLATE_PART_AREA_HEADER, $template->area );
     215        */
    51216    }
    52217
     
    57222        $id       = get_stylesheet() . '//' . 'my_template';
    58223        $template = get_block_template( $id, 'wp_template' );
    59         $this->assertSame( $id, $template->id );
    60         $this->assertSame( get_stylesheet(), $template->theme );
    61         $this->assertSame( 'my_template', $template->slug );
    62         $this->assertSame( 'publish', $template->status );
    63         $this->assertSame( 'custom', $template->source );
    64         $this->assertSame( 'wp_template', $template->type );
     224        $this->assertEquals( $id, $template->id );
     225        $this->assertEquals( get_stylesheet(), $template->theme );
     226        $this->assertEquals( 'my_template', $template->slug );
     227        $this->assertEquals( 'publish', $template->status );
     228        $this->assertEquals( 'custom', $template->source );
     229        $this->assertEquals( 'wp_template', $template->type );
     230
     231        // Test template parts.
     232        $id       = get_stylesheet() . '//' . 'my_template_part';
     233        $template = get_block_template( $id, 'wp_template_part' );
     234        $this->assertEquals( $id, $template->id );
     235        $this->assertEquals( get_stylesheet(), $template->theme );
     236        $this->assertEquals( 'my_template_part', $template->slug );
     237        $this->assertEquals( 'publish', $template->status );
     238        $this->assertEquals( 'custom', $template->source );
     239        $this->assertEquals( 'wp_template_part', $template->type );
     240        $this->assertEquals( WP_TEMPLATE_PART_AREA_HEADER, $template->area );
    65241    }
    66242
    67243    /**
    68      * Should retrieve block templates.
     244     * Should retrieve block templates (file and CPT)
    69245     */
    70246    public function test_get_block_templates() {
     
    85261        $this->assertContains( get_stylesheet() . '//' . 'my_template', $template_ids );
    86262
     263        // The result might change in a block theme.
     264        // $this->assertContains( get_stylesheet() . '//' . 'index', $template_ids );
     265
    87266        // Filter by slug.
    88267        $templates    = get_block_templates( array( 'slug__in' => array( 'my_template' ) ), 'wp_template' );
    89268        $template_ids = get_template_ids( $templates );
    90         $this->assertSame( array( get_stylesheet() . '//' . 'my_template' ), $template_ids );
     269        $this->assertEquals( array( get_stylesheet() . '//' . 'my_template' ), $template_ids );
    91270
    92271        // Filter by CPT ID.
    93272        $templates    = get_block_templates( array( 'wp_id' => self::$post->ID ), 'wp_template' );
    94273        $template_ids = get_template_ids( $templates );
    95         $this->assertSame( array( get_stylesheet() . '//' . 'my_template' ), $template_ids );
     274        $this->assertEquals( array( get_stylesheet() . '//' . 'my_template' ), $template_ids );
     275
     276        // Filter template part by area.
     277        // Requires a block theme.
     278        /*$templates    = get_block_templates( array( 'area' => WP_TEMPLATE_PART_AREA_HEADER ), 'wp_template_part' );
     279        $template_ids = get_template_ids( $templates );
     280        $this->assertEquals(
     281            array(
     282                get_stylesheet() . '//' . 'my_template_part',
     283                get_stylesheet() . '//' . 'header',
     284            ),
     285            $template_ids
     286        );
     287        */
     288    }
     289
     290    /**
     291     * Should flatten nested blocks
     292     */
     293    function test_flatten_blocks() {
     294        $content_template_part_inside_group = '<!-- wp:group --><!-- wp:template-part {"slug":"header"} /--><!-- /wp:group -->';
     295        $blocks                             = parse_blocks( $content_template_part_inside_group );
     296        $actual                             = _flatten_blocks( $blocks );
     297        $expected                           = array( $blocks[0], $blocks[0]['innerBlocks'][0] );
     298        $this->assertEquals( $expected, $actual );
     299
     300        $content_template_part_inside_group_inside_group = '<!-- wp:group --><!-- wp:group --><!-- wp:template-part {"slug":"header"} /--><!-- /wp:group --><!-- /wp:group -->';
     301        $blocks   = parse_blocks( $content_template_part_inside_group_inside_group );
     302        $actual   = _flatten_blocks( $blocks );
     303        $expected = array( $blocks[0], $blocks[0]['innerBlocks'][0], $blocks[0]['innerBlocks'][0]['innerBlocks'][0] );
     304        $this->assertEquals( $expected, $actual );
     305
     306        $content_without_inner_blocks = '<!-- wp:group /-->';
     307        $blocks                       = parse_blocks( $content_without_inner_blocks );
     308        $actual                       = _flatten_blocks( $blocks );
     309        $expected                     = array( $blocks[0] );
     310        $this->assertEquals( $expected, $actual );
    96311    }
    97312}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r52051 r52062  
    133133            '/wp/v2/block-types/(?P<namespace>[a-zA-Z0-9_-]+)/(?P<name>[a-zA-Z0-9_-]+)',
    134134            '/wp/v2/settings',
     135            '/wp/v2/template-parts',
     136            '/wp/v2/template-parts/(?P<id>[\/\w-]+)',
     137            '/wp/v2/template-parts/(?P<id>[\d]+)/autosaves',
     138            '/wp/v2/template-parts/(?P<parent>[\d]+)/autosaves/(?P<id>[\d]+)',
     139            '/wp/v2/template-parts/(?P<parent>[\d]+)/revisions',
     140            '/wp/v2/template-parts/(?P<parent>[\d]+)/revisions/(?P<id>[\d]+)',
    135141            '/wp/v2/templates',
    136142            '/wp/v2/templates/(?P<id>[\/\w-]+)',
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r52051 r52062  
    41654165                            "type": "integer",
    41664166                            "required": false
     4167                        },
     4168                        "area": {
     4169                            "description": "Limit to the specified template part area.",
     4170                            "type": "string",
     4171                            "required": false
     4172                        },
     4173                        "post_type": {
     4174                            "description": "Post type to get the templates for.",
     4175                            "type": "string",
     4176                            "required": false
    41674177                        }
    41684178                    }
     
    45514561        },
    45524562        "/wp/v2/templates/(?P<parent>[\\d]+)/autosaves/(?P<id>[\\d]+)": {
     4563            "namespace": "wp/v2",
     4564            "methods": [
     4565                "GET"
     4566            ],
     4567            "endpoints": [
     4568                {
     4569                    "methods": [
     4570                        "GET"
     4571                    ],
     4572                    "args": {
     4573                        "parent": {
     4574                            "description": "The ID for the parent of the autosave.",
     4575                            "type": "integer",
     4576                            "required": false
     4577                        },
     4578                        "id": {
     4579                            "description": "The ID for the autosave.",
     4580                            "type": "integer",
     4581                            "required": false
     4582                        },
     4583                        "context": {
     4584                            "description": "Scope under which the request is made; determines fields present in response.",
     4585                            "type": "string",
     4586                            "enum": [
     4587                                "view",
     4588                                "embed",
     4589                                "edit"
     4590                            ],
     4591                            "default": "view",
     4592                            "required": false
     4593                        }
     4594                    }
     4595                }
     4596            ]
     4597        },
     4598        "/wp/v2/template-parts": {
     4599            "namespace": "wp/v2",
     4600            "methods": [
     4601                "GET",
     4602                "POST"
     4603            ],
     4604            "endpoints": [
     4605                {
     4606                    "methods": [
     4607                        "GET"
     4608                    ],
     4609                    "args": {
     4610                        "context": {
     4611                            "description": "Scope under which the request is made; determines fields present in response.",
     4612                            "type": "string",
     4613                            "enum": [
     4614                                "view",
     4615                                "embed",
     4616                                "edit"
     4617                            ],
     4618                            "required": false
     4619                        },
     4620                        "wp_id": {
     4621                            "description": "Limit to the specified post id.",
     4622                            "type": "integer",
     4623                            "required": false
     4624                        },
     4625                        "area": {
     4626                            "description": "Limit to the specified template part area.",
     4627                            "type": "string",
     4628                            "required": false
     4629                        },
     4630                        "post_type": {
     4631                            "description": "Post type to get the templates for.",
     4632                            "type": "string",
     4633                            "required": false
     4634                        }
     4635                    }
     4636                },
     4637                {
     4638                    "methods": [
     4639                        "POST"
     4640                    ],
     4641                    "args": {
     4642                        "slug": {
     4643                            "description": "Unique slug identifying the template.",
     4644                            "type": "string",
     4645                            "minLength": 1,
     4646                            "pattern": "[a-zA-Z_\\-]+",
     4647                            "required": true
     4648                        },
     4649                        "theme": {
     4650                            "description": "Theme identifier for the template.",
     4651                            "type": "string",
     4652                            "required": false
     4653                        },
     4654                        "content": {
     4655                            "default": "",
     4656                            "description": "Content of template.",
     4657                            "type": [
     4658                                "object",
     4659                                "string"
     4660                            ],
     4661                            "required": false
     4662                        },
     4663                        "title": {
     4664                            "default": "",
     4665                            "description": "Title of template.",
     4666                            "type": [
     4667                                "object",
     4668                                "string"
     4669                            ],
     4670                            "required": false
     4671                        },
     4672                        "description": {
     4673                            "default": "",
     4674                            "description": "Description of template.",
     4675                            "type": "string",
     4676                            "required": false
     4677                        },
     4678                        "status": {
     4679                            "default": "publish",
     4680                            "description": "Status of template.",
     4681                            "type": "string",
     4682                            "required": false
     4683                        },
     4684                        "area": {
     4685                            "description": "Where the template part is intended for use (header, footer, etc.)",
     4686                            "type": "string",
     4687                            "required": false
     4688                        }
     4689                    }
     4690                }
     4691            ],
     4692            "_links": {
     4693                "self": [
     4694                    {
     4695                        "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts"
     4696                    }
     4697                ]
     4698            }
     4699        },
     4700        "/wp/v2/template-parts/(?P<id>[\\/\\w-]+)": {
     4701            "namespace": "wp/v2",
     4702            "methods": [
     4703                "GET",
     4704                "POST",
     4705                "PUT",
     4706                "PATCH",
     4707                "DELETE"
     4708            ],
     4709            "endpoints": [
     4710                {
     4711                    "methods": [
     4712                        "GET"
     4713                    ],
     4714                    "args": {
     4715                        "id": {
     4716                            "description": "The id of a template",
     4717                            "type": "string",
     4718                            "required": false
     4719                        }
     4720                    }
     4721                },
     4722                {
     4723                    "methods": [
     4724                        "POST",
     4725                        "PUT",
     4726                        "PATCH"
     4727                    ],
     4728                    "args": {
     4729                        "slug": {
     4730                            "description": "Unique slug identifying the template.",
     4731                            "type": "string",
     4732                            "minLength": 1,
     4733                            "pattern": "[a-zA-Z_\\-]+",
     4734                            "required": false
     4735                        },
     4736                        "theme": {
     4737                            "description": "Theme identifier for the template.",
     4738                            "type": "string",
     4739                            "required": false
     4740                        },
     4741                        "content": {
     4742                            "description": "Content of template.",
     4743                            "type": [
     4744                                "object",
     4745                                "string"
     4746                            ],
     4747                            "required": false
     4748                        },
     4749                        "title": {
     4750                            "description": "Title of template.",
     4751                            "type": [
     4752                                "object",
     4753                                "string"
     4754                            ],
     4755                            "required": false
     4756                        },
     4757                        "description": {
     4758                            "description": "Description of template.",
     4759                            "type": "string",
     4760                            "required": false
     4761                        },
     4762                        "status": {
     4763                            "description": "Status of template.",
     4764                            "type": "string",
     4765                            "required": false
     4766                        },
     4767                        "area": {
     4768                            "description": "Where the template part is intended for use (header, footer, etc.)",
     4769                            "type": "string",
     4770                            "required": false
     4771                        }
     4772                    }
     4773                },
     4774                {
     4775                    "methods": [
     4776                        "DELETE"
     4777                    ],
     4778                    "args": {
     4779                        "force": {
     4780                            "type": "boolean",
     4781                            "default": false,
     4782                            "description": "Whether to bypass Trash and force deletion.",
     4783                            "required": false
     4784                        }
     4785                    }
     4786                }
     4787            ]
     4788        },
     4789        "/wp/v2/template-parts/(?P<parent>[\\d]+)/revisions": {
     4790            "namespace": "wp/v2",
     4791            "methods": [
     4792                "GET"
     4793            ],
     4794            "endpoints": [
     4795                {
     4796                    "methods": [
     4797                        "GET"
     4798                    ],
     4799                    "args": {
     4800                        "parent": {
     4801                            "description": "The ID for the parent of the revision.",
     4802                            "type": "integer",
     4803                            "required": false
     4804                        },
     4805                        "context": {
     4806                            "description": "Scope under which the request is made; determines fields present in response.",
     4807                            "type": "string",
     4808                            "enum": [
     4809                                "view",
     4810                                "embed",
     4811                                "edit"
     4812                            ],
     4813                            "default": "view",
     4814                            "required": false
     4815                        },
     4816                        "page": {
     4817                            "description": "Current page of the collection.",
     4818                            "type": "integer",
     4819                            "default": 1,
     4820                            "minimum": 1,
     4821                            "required": false
     4822                        },
     4823                        "per_page": {
     4824                            "description": "Maximum number of items to be returned in result set.",
     4825                            "type": "integer",
     4826                            "minimum": 1,
     4827                            "maximum": 100,
     4828                            "required": false
     4829                        },
     4830                        "search": {
     4831                            "description": "Limit results to those matching a string.",
     4832                            "type": "string",
     4833                            "required": false
     4834                        },
     4835                        "exclude": {
     4836                            "description": "Ensure result set excludes specific IDs.",
     4837                            "type": "array",
     4838                            "items": {
     4839                                "type": "integer"
     4840                            },
     4841                            "default": [],
     4842                            "required": false
     4843                        },
     4844                        "include": {
     4845                            "description": "Limit result set to specific IDs.",
     4846                            "type": "array",
     4847                            "items": {
     4848                                "type": "integer"
     4849                            },
     4850                            "default": [],
     4851                            "required": false
     4852                        },
     4853                        "offset": {
     4854                            "description": "Offset the result set by a specific number of items.",
     4855                            "type": "integer",
     4856                            "required": false
     4857                        },
     4858                        "order": {
     4859                            "description": "Order sort attribute ascending or descending.",
     4860                            "type": "string",
     4861                            "default": "desc",
     4862                            "enum": [
     4863                                "asc",
     4864                                "desc"
     4865                            ],
     4866                            "required": false
     4867                        },
     4868                        "orderby": {
     4869                            "description": "Sort collection by object attribute.",
     4870                            "type": "string",
     4871                            "default": "date",
     4872                            "enum": [
     4873                                "date",
     4874                                "id",
     4875                                "include",
     4876                                "relevance",
     4877                                "slug",
     4878                                "include_slugs",
     4879                                "title"
     4880                            ],
     4881                            "required": false
     4882                        }
     4883                    }
     4884                }
     4885            ]
     4886        },
     4887        "/wp/v2/template-parts/(?P<parent>[\\d]+)/revisions/(?P<id>[\\d]+)": {
     4888            "namespace": "wp/v2",
     4889            "methods": [
     4890                "GET",
     4891                "DELETE"
     4892            ],
     4893            "endpoints": [
     4894                {
     4895                    "methods": [
     4896                        "GET"
     4897                    ],
     4898                    "args": {
     4899                        "parent": {
     4900                            "description": "The ID for the parent of the revision.",
     4901                            "type": "integer",
     4902                            "required": false
     4903                        },
     4904                        "id": {
     4905                            "description": "Unique identifier for the revision.",
     4906                            "type": "integer",
     4907                            "required": false
     4908                        },
     4909                        "context": {
     4910                            "description": "Scope under which the request is made; determines fields present in response.",
     4911                            "type": "string",
     4912                            "enum": [
     4913                                "view",
     4914                                "embed",
     4915                                "edit"
     4916                            ],
     4917                            "default": "view",
     4918                            "required": false
     4919                        }
     4920                    }
     4921                },
     4922                {
     4923                    "methods": [
     4924                        "DELETE"
     4925                    ],
     4926                    "args": {
     4927                        "parent": {
     4928                            "description": "The ID for the parent of the revision.",
     4929                            "type": "integer",
     4930                            "required": false
     4931                        },
     4932                        "id": {
     4933                            "description": "Unique identifier for the revision.",
     4934                            "type": "integer",
     4935                            "required": false
     4936                        },
     4937                        "force": {
     4938                            "type": "boolean",
     4939                            "default": false,
     4940                            "description": "Required to be true, as revisions do not support trashing.",
     4941                            "required": false
     4942                        }
     4943                    }
     4944                }
     4945            ]
     4946        },
     4947        "/wp/v2/template-parts/(?P<id>[\\d]+)/autosaves": {
     4948            "namespace": "wp/v2",
     4949            "methods": [
     4950                "GET",
     4951                "POST"
     4952            ],
     4953            "endpoints": [
     4954                {
     4955                    "methods": [
     4956                        "GET"
     4957                    ],
     4958                    "args": {
     4959                        "parent": {
     4960                            "description": "The ID for the parent of the autosave.",
     4961                            "type": "integer",
     4962                            "required": false
     4963                        },
     4964                        "context": {
     4965                            "description": "Scope under which the request is made; determines fields present in response.",
     4966                            "type": "string",
     4967                            "enum": [
     4968                                "view",
     4969                                "embed",
     4970                                "edit"
     4971                            ],
     4972                            "default": "view",
     4973                            "required": false
     4974                        }
     4975                    }
     4976                },
     4977                {
     4978                    "methods": [
     4979                        "POST"
     4980                    ],
     4981                    "args": {
     4982                        "parent": {
     4983                            "description": "The ID for the parent of the autosave.",
     4984                            "type": "integer",
     4985                            "required": false
     4986                        },
     4987                        "slug": {
     4988                            "description": "Unique slug identifying the template.",
     4989                            "type": "string",
     4990                            "minLength": 1,
     4991                            "pattern": "[a-zA-Z_\\-]+",
     4992                            "required": false
     4993                        },
     4994                        "theme": {
     4995                            "description": "Theme identifier for the template.",
     4996                            "type": "string",
     4997                            "required": false
     4998                        },
     4999                        "content": {
     5000                            "description": "Content of template.",
     5001                            "type": [
     5002                                "object",
     5003                                "string"
     5004                            ],
     5005                            "required": false
     5006                        },
     5007                        "title": {
     5008                            "description": "Title of template.",
     5009                            "type": [
     5010                                "object",
     5011                                "string"
     5012                            ],
     5013                            "required": false
     5014                        },
     5015                        "description": {
     5016                            "description": "Description of template.",
     5017                            "type": "string",
     5018                            "required": false
     5019                        },
     5020                        "status": {
     5021                            "description": "Status of template.",
     5022                            "type": "string",
     5023                            "required": false
     5024                        },
     5025                        "area": {
     5026                            "description": "Where the template part is intended for use (header, footer, etc.)",
     5027                            "type": "string",
     5028                            "required": false
     5029                        }
     5030                    }
     5031                }
     5032            ]
     5033        },
     5034        "/wp/v2/template-parts/(?P<parent>[\\d]+)/autosaves/(?P<id>[\\d]+)": {
    45535035            "namespace": "wp/v2",
    45545036            "methods": [
     
    86119093            ]
    86129094        }
     9095    },
     9096    "wp_template_part": {
     9097        "description": "Template parts to include in your templates.",
     9098        "hierarchical": false,
     9099        "name": "Template Parts",
     9100        "slug": "wp_template_part",
     9101        "taxonomies": [],
     9102        "rest_base": "template-parts",
     9103        "rest_namespace": "wp/v2",
     9104        "_links": {
     9105            "collection": [
     9106                {
     9107                    "href": "http://example.org/index.php?rest_route=/wp/v2/types"
     9108                }
     9109            ],
     9110            "wp:items": [
     9111                {
     9112                    "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts"
     9113                }
     9114            ],
     9115            "curies": [
     9116                {
     9117                    "name": "wp",
     9118                    "href": "https://api.w.org/{rel}",
     9119                    "templated": true
     9120                }
     9121            ]
     9122        }
    86139123    }
    86149124};
  • trunk/tools/webpack/blocks.js

    r52011 r52062  
    5454        'social-link',
    5555        'tag-cloud',
     56        'template-part',
    5657    ];
    5758    const blockFolders = [
Note: See TracChangeset for help on using the changeset viewer.