Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
1a04ac9
Dashboard: Use Howdy greeting for page title (#78740)
jameskoster May 29, 2026
47326be
Block Editor: Refactor Inserter to a function component (#78766)
Mamaduka May 29, 2026
1dae94d
Dashboard: Move layout settings to customize toolbar (#78738)
jameskoster May 29, 2026
46c5c66
chore(pipeline): bootstrap 77199-cover-bindings-id-url-internal
cbravobernal May 28, 2026
acddc45
docs(pipeline): add 1-spec/requirements.md for 77199-cover-bindings-i…
cbravobernal May 28, 2026
521b5a4
docs(pipeline): add 1-spec/spec.md for 77199-cover-bindings-id-url-in…
cbravobernal May 28, 2026
7a24468
docs(pipeline): add 1-spec/spec-review-1-rejected.md for 77199-cover-…
cbravobernal May 28, 2026
0ea3a03
docs(pipeline): revise 1-spec/spec.md for 77199-cover-bindings-id-url…
cbravobernal May 28, 2026
67fb513
docs(pipeline): add 1-spec/spec-review-2-rejected.md for 77199-cover-…
cbravobernal May 28, 2026
04cd848
docs(pipeline): revise 1-spec/spec.md for 77199-cover-bindings-id-url…
cbravobernal May 28, 2026
7ab2b45
docs(pipeline): add 1-spec/spec-review-approved.md for 77199-cover-bi…
cbravobernal May 28, 2026
58e31ac
docs(pipeline): add 2-design-doc/design-doc.md for 77199-cover-bindin…
cbravobernal May 28, 2026
980afab
docs(pipeline): add 2-design-doc/design-doc-review-1-rejected.md for …
cbravobernal May 28, 2026
0b85aab
docs(pipeline): revise 2-design-doc/design-doc.md for 77199-cover-bin…
cbravobernal May 28, 2026
ccb995f
docs(pipeline): add 2-design-doc/design-doc-review-2-rejected.md for …
cbravobernal May 28, 2026
9c94e09
docs(pipeline): revise 2-design-doc/design-doc.md for 77199-cover-bin…
cbravobernal May 28, 2026
29cf009
docs(pipeline): add 2-design-doc/design-doc-review-approved.md for 77…
cbravobernal May 28, 2026
4caef2a
docs(pipeline): add 3-plan/code-plan.md for 77199-cover-bindings-id-u…
cbravobernal May 28, 2026
9ba9c6a
docs(pipeline): add 3-plan/code-plan-review-1-rejected.md for 77199-c…
cbravobernal May 28, 2026
57bd145
docs(pipeline): revise 3-plan/code-plan.md for 77199-cover-bindings-i…
cbravobernal May 28, 2026
66041cd
docs(pipeline): add 3-plan/code-plan-review-approved.md for 77199-cov…
cbravobernal May 28, 2026
dc81f91
docs(pipeline): add 3-plan/doc-plan.md for 77199-cover-bindings-id-ur…
cbravobernal May 28, 2026
a08d8ed
docs(pipeline): add 3-plan/doc-plan-review-approved.md for 77199-cove…
cbravobernal May 28, 2026
769d08c
feat(block-library): mark cover id attribute as content role
cbravobernal May 28, 2026
fd3339f
feat(block-bindings): allow id and url on cover via 7.1 compat filter
cbravobernal May 28, 2026
450d8a3
feat(block-bindings): add gutenberg_cover_bindings_is_active helper
cbravobernal May 28, 2026
2b8be92
feat(block-bindings): neutralise useFeaturedImage on bound covers (re…
cbravobernal May 28, 2026
1caa4c7
feat(block-bindings): rewrite bound cover render output via render_bl…
cbravobernal May 28, 2026
0de5de6
feat(block-library): add useCoverBindingState hook for cover bindings
cbravobernal May 28, 2026
cd2421e
feat(block-library): add reactive observer + derived values in cover …
cbravobernal May 28, 2026
bf0e42f
feat(block-library): bound-cover placeholder + forced-img render in C…
cbravobernal May 28, 2026
a7f57f4
feat(block-library): hide cover controls conflicting with active bind…
cbravobernal May 28, 2026
066534f
test(block-library): add cover bindings server-render PHPUnit coverage
cbravobernal May 28, 2026
e630d1e
test(block-library): add Jest coverage for cover bindings hook + obse…
cbravobernal May 28, 2026
b71054e
test(block-library): add Pattern Overrides round-trip e2e test for co…
cbravobernal May 28, 2026
940cf22
docs(pipeline): add 4-code/code-review-approved.md for 77199-cover-bi…
cbravobernal May 28, 2026
2747dc0
docs(backport): add cover bindings backport-changelog stub for 7.1
cbravobernal May 28, 2026
ee75670
docs(block-library): changelog entry for Cover block bindings
cbravobernal May 28, 2026
43aaf25
docs(pipeline): add 5-docs/docs-review-approved.md for 77199-cover-bi…
cbravobernal May 28, 2026
44bea45
chore: drop .pipelines artifacts from branch
cbravobernal May 29, 2026
78b8908
refactor(block-bindings): inline __default expansion in is_active
cbravobernal May 29, 2026
a7b122c
refactor(block-library): drop unused returns from useCoverBindingState
cbravobernal May 29, 2026
40d8e60
test(block-library): trim cover bindings Jest coverage to critical cases
cbravobernal May 29, 2026
b9162cc
refactor(block-bindings): trim docblock noise in 7.1 compat file
cbravobernal May 29, 2026
c499047
refactor(block-bindings): inline single-caller helpers
cbravobernal May 29, 2026
6a67afa
test(block-library): consolidate cover bindings PHPUnit fixtures
cbravobernal May 29, 2026
6c5b9dd
refactor(block-library): trim CoverEdit binding wiring
cbravobernal May 29, 2026
678f2f8
refactor(block-library): tighten useCoverBindingState
cbravobernal May 29, 2026
c8a631a
test(block-library): tighten cover bindings e2e assertions
cbravobernal May 29, 2026
ebfb704
test(block-library): consolidate cover Jest mock setup
cbravobernal May 29, 2026
9b43b06
refactor(block-bindings): trim cover render filter prose
cbravobernal May 29, 2026
3306226
refactor(block-library): trim cover edit binding prose
cbravobernal May 29, 2026
ccc920e
Build: restore dot:true on fast-glob in transpilePackage
cbravobernal May 29, 2026
6309f4e
refactor(block-bindings): use strict equality for cover binding args
cbravobernal May 29, 2026
6831eb0
refactor(block-bindings): drop unused params on render_block_data cal…
cbravobernal May 29, 2026
8519ac1
refactor(block-bindings): consolidate cover saved-markup regex patterns
cbravobernal May 29, 2026
2cf08e1
fix(block-library): keep MediaReplaceFlow on bound covers
cbravobernal May 29, 2026
d3ed731
fix(block-library): drop same-source invariant, mirror core/image url…
cbravobernal May 29, 2026
928b2e6
fix(block-bindings): inject <img> when saved cover markup has none
cbravobernal May 29, 2026
495a9cb
fix(block-bindings): strip stored overlay color on bound covers
cbravobernal May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ packages/react-native-editor/src/setup-local.js
# Files related to applying patches
*.rej
*.orig
/.pipelines/
3 changes: 3 additions & 0 deletions backport-changelog/7.1/TODO-cover-bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/TODO

* https://github.com/WordPress/gutenberg/pull/TODO
298 changes: 298 additions & 0 deletions lib/compat/wordpress-7.1/block-bindings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
<?php
/**
* Block Bindings: Cover block support.
*
* Adds `id` and `url` to the server-side supported-attributes list for
* `core/cover` and rewrites the rendered output to honour active bindings.
*
* @since 7.1.0
* @package gutenberg
*/

/**
* Saved-markup regex for the parallax/repeat `<div class="wp-block-cover__image-background">`
* element. The WP HTML API cannot delete elements or change tag names, so
* `preg_match` is the only way to locate the element for byte-offset splicing.
*
* Mirrors the pattern already in `packages/block-library/src/cover/index.php`.
*/
const GUTENBERG_COVER_BINDINGS_DIV_PATTERN = '/<div\s+[^>]*\bwp-block-cover__image-background\b[^>]*><\/div>/U';

/**
* Saved-markup regex for the plain `<img class="wp-block-cover__image-background">`
* element used when neither parallax nor repeat is active. Used only by
* `strip_image` — `rewrite_image` mutates the `<img>` in place via Tag Processor.
*/
const GUTENBERG_COVER_BINDINGS_IMG_PATTERN = '/<img\s+[^>]*\bwp-block-cover__image-background\b[^>]*\/?\s*>/U';

if ( ! function_exists( 'gutenberg_cover_bindings_add_supported_attributes' ) ) {
/**
* Adds `id` and `url` to the bindings-supported attributes for `core/cover`.
*
* @since 7.1.0
*/
function gutenberg_cover_bindings_add_supported_attributes( $attributes, $block_type ) {
if ( 'core/cover' !== $block_type ) {
return $attributes;
}

if ( ! in_array( 'id', $attributes, true ) ) {
$attributes[] = 'id';
}
if ( ! in_array( 'url', $attributes, true ) ) {
$attributes[] = 'url';
}

return $attributes;
}
}

add_filter( 'block_bindings_supported_attributes', 'gutenberg_cover_bindings_add_supported_attributes', 10, 2 );

if ( ! function_exists( 'gutenberg_cover_bindings_is_active' ) ) {
/**
* Whether a parsed Cover has a `url` binding (with or without an `id`
* binding). A `__default` entry counts. Server-side mirror of
* `useCoverBindingState`'s `bindingActive`.
*
* @since 7.1.0
*/
function gutenberg_cover_bindings_is_active( array $attrs ): bool {
$bindings = $attrs['metadata']['bindings'] ?? null;
if ( empty( $bindings ) || ! is_array( $bindings ) ) {
return false;
}
return isset( $bindings['__default'] ) || isset( $bindings['url'] );
}
}

if ( ! function_exists( 'gutenberg_cover_bindings_prepare_block' ) ) {
/**
* Forces `useFeaturedImage` off on bound covers before `WP_Block::render()`.
*
* AC-18: an active `id`+`url` binding always wins over `useFeaturedImage`.
*
* @since 7.1.0
*/
function gutenberg_cover_bindings_prepare_block( $parsed_block ) {
if ( 'core/cover' !== ( $parsed_block['blockName'] ?? '' ) ) {
return $parsed_block;
}

$attrs = $parsed_block['attrs'] ?? array();

// AC-21: skip embed-video covers.
if (
( $attrs['backgroundType'] ?? '' ) === 'embed-video' ||
! gutenberg_cover_bindings_is_active( $attrs )
) {
return $parsed_block;
}

if ( ! empty( $attrs['useFeaturedImage'] ) ) {
$parsed_block['attrs']['useFeaturedImage'] = false;
}

return $parsed_block;
}
}

add_filter( 'render_block_data', 'gutenberg_cover_bindings_prepare_block', 10, 1 );

if ( ! function_exists( 'gutenberg_cover_bindings_strip_image' ) ) {
/**
* Removes the saved Cover image element (either `<img>` or parallax `<div>`).
*
* @since 7.1.0
* @access private
*/
function gutenberg_cover_bindings_strip_image( string $content ): string {
// Parallax/repeat <div> form is probed first; <img>-only regex would miss it.
foreach ( array( GUTENBERG_COVER_BINDINGS_DIV_PATTERN, GUTENBERG_COVER_BINDINGS_IMG_PATTERN ) as $pattern ) {
if ( 1 === preg_match( $pattern, $content, $m, PREG_OFFSET_CAPTURE ) ) {
return substr( $content, 0, $m[0][1] ) . substr( $content, $m[0][1] + strlen( $m[0][0] ) );
}
}
return $content;
}
}

if ( ! function_exists( 'gutenberg_cover_bindings_rewrite_image' ) ) {
/**
* Rewrites the saved Cover image element to point at the bound URL/ID.
*
* Handles both forms emitted by `save.js`: the parallax/repeat `<div>` is
* replaced wholesale with a freshly-built `<img>`; the plain `<img>` form is
* rewritten in place via Tag Processor. Idempotent on its own output.
*
* @since 7.1.0
* @access private
*/
function gutenberg_cover_bindings_rewrite_image( string $content, string $resolved_url, int $resolved_id, array $attrs ): string {
$alt = $resolved_id > 0

Check warning on line 132 in lib/compat/wordpress-7.1/block-bindings.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Equals sign not aligned with surrounding assignments; expected 10 spaces but found 11 spaces
? trim( strip_tags( (string) get_post_meta( $resolved_id, '_wp_attachment_image_alt', true ) ) )
: '';
$wp_image_cls = $resolved_id > 0 ? ' wp-image-' . $resolved_id : '';

Check warning on line 135 in lib/compat/wordpress-7.1/block-bindings.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Equals sign not aligned with surrounding assignments; expected 1 space but found 2 spaces
$size_slug = isset( $attrs['sizeSlug'] ) && '' !== $attrs['sizeSlug']

Check warning on line 136 in lib/compat/wordpress-7.1/block-bindings.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Equals sign not aligned with surrounding assignments; expected 4 spaces but found 5 spaces
? ' size-' . $attrs['sizeSlug']
: '';

$object_position = '';
if (
isset( $attrs['focalPoint']['x'], $attrs['focalPoint']['y'] ) &&
is_numeric( $attrs['focalPoint']['x'] ) &&
is_numeric( $attrs['focalPoint']['y'] )
) {
$object_position = sprintf(
'%s%% %s%%',
round( (float) $attrs['focalPoint']['x'] * 100 ),
round( (float) $attrs['focalPoint']['y'] * 100 )
);
}

// Parallax/repeat <div> form: rebuild as an <img>. Saved markup is
// the source of truth, NOT $attrs['hasParallax']/['isRepeated'].
if ( 1 === preg_match( GUTENBERG_COVER_BINDINGS_DIV_PATTERN, $content, $m, PREG_OFFSET_CAPTURE ) ) {
$object_position_attrs = '' === $object_position ? '' : sprintf(
' data-object-position="%s" style="object-position:%s;"',
esc_attr( $object_position ),
esc_attr( $object_position )
);

$rebuilt_img = sprintf(
'<img class="wp-block-cover__image-background%s%s" alt="%s" src="%s" data-object-fit="cover"%s />',
esc_attr( $wp_image_cls ),
esc_attr( $size_slug ),
esc_attr( $alt ),
esc_url( $resolved_url ),
$object_position_attrs
);

return substr( $content, 0, $m[0][1] ) . $rebuilt_img . substr( $content, $m[0][1] + strlen( $m[0][0] ) );
}

// Plain <img> form: rewrite attributes in place.
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag(
array(
'tag_name' => 'IMG',
'class_name' => 'wp-block-cover__image-background',
)
) ) {
// No image element saved (Cover authored without picking media,
// then bound). Inject a fresh `<img>` before the overlay `<span>`.
$object_position_attrs = '' === $object_position ? '' : sprintf(
' data-object-position="%s" style="object-position:%s;"',
esc_attr( $object_position ),
esc_attr( $object_position )
);
$injected_img = sprintf(
'<img class="wp-block-cover__image-background%s%s" alt="%s" src="%s" data-object-fit="cover"%s />',
esc_attr( $wp_image_cls ),
esc_attr( $size_slug ),
esc_attr( $alt ),
esc_url( $resolved_url ),
$object_position_attrs
);
if ( 1 === preg_match(
'/<span\s+[^>]*\bwp-block-cover__background\b[^>]*>/U',
$content,
$sm,
PREG_OFFSET_CAPTURE
) ) {
return substr( $content, 0, $sm[0][1] ) . $injected_img . substr( $content, $sm[0][1] );
}
return $content;
}

$processor->set_attribute( 'src', $resolved_url );
$processor->set_attribute( 'alt', $alt );

// Strip any saved wp-image-{old} before adding the resolved one.
// Collect first — remove_class while iterating the Generator is unsafe.
$class_list = $processor->class_list();
if ( null !== $class_list ) {
$to_remove = array();
foreach ( $class_list as $cls ) {
if ( 0 === strpos( $cls, 'wp-image-' ) ) {
$to_remove[] = $cls;
}
}
foreach ( $to_remove as $cls ) {
$processor->remove_class( $cls );
}
}
if ( $resolved_id > 0 ) {
$processor->add_class( 'wp-image-' . $resolved_id );
}

return $processor->get_updated_html();
}
}

if ( ! function_exists( 'gutenberg_cover_bindings_render_block' ) ) {
/**
* Rewrites a rendered Cover block to honour an active `id`+`url` binding.
*
* Registered at priority 9 — MUST run before the generic priority-10
* `gutenberg_block_bindings_render_block` in `wordpress-6.9/block-bindings.php`
* so the substitution runs on saved markup, not bindings-resolved markup.
*
* @since 7.1.0
*/
function gutenberg_cover_bindings_render_block( $block_content, $block, $instance ) {
if ( 'core/cover' !== ( $block['blockName'] ?? '' ) ) {
return $block_content;
}

$attrs = $instance->attributes ?? array();

// AC-21: skip embed-video covers.
if ( ( $attrs['backgroundType'] ?? '' ) === 'embed-video' ) {
return $block_content;
}

if ( ! gutenberg_cover_bindings_is_active( $attrs ) ) {
return $block_content;
}

$url = $attrs['url'] ?? null;
if ( empty( $url ) ) {
return gutenberg_cover_bindings_strip_image( $block_content );
}

// `id` is optional. When present and resolving to a non-attachment,
// strip — that's the explicit "unresolvable internal media" state.
$id = (int) ( $attrs['id'] ?? 0 );
if ( $id > 0 ) {
$attachment = get_post( $id );
if ( ! $attachment || 'attachment' !== $attachment->post_type ) {
return gutenberg_cover_bindings_strip_image( $block_content );
}
}

$block_content = gutenberg_cover_bindings_rewrite_image( $block_content, (string) $url, $id, $attrs );

// Relax stored dimRatio:100 + strip the saved overlay color: stored
// `customOverlayColor` was derived from whatever image the cover was
// authored with, so it no longer matches the bound media.
$processor = new WP_HTML_Tag_Processor( $block_content );
$relax_dim_class = 100 === (int) ( $attrs['dimRatio'] ?? 100 );
while ( $processor->next_tag(
array(
'tag_name' => 'SPAN',
'class_name' => 'wp-block-cover__background',
)
) ) {
if ( $relax_dim_class ) {
$processor->remove_class( 'has-background-dim-100' );
}
$processor->remove_attribute( 'style' );
}

return $processor->get_updated_html();
}
}

// Priority 9 — must run before the generic priority-10 filter.
add_filter( 'render_block', 'gutenberg_cover_bindings_render_block', 9, 3 );
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php';
require __DIR__ . '/compat/wordpress-7.1/rest-api.php';
require __DIR__ . '/compat/wordpress-7.1/collaboration.php';
require __DIR__ . '/compat/wordpress-7.1/block-bindings.php';

// Plugin specific code.
require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php';
Expand Down
Loading
Loading