` element (see makeContainerSave in
* atomic-shared/container.js), and an `` may not contain another ``.
*
* That is not a validation nicety, it is a parser rule with teeth. Feed a
* browser `…
…`
* — a linked container holding a child that is itself linked, an image with a
* link being the everyday way to get one — and the HTML5 adoption agency
* algorithm rebuilds the tree: the outer anchor is closed at the nested one, an
* EMPTY clone of it is left behind inside the child, and every sibling after
* that point is lifted out of the container altogether. Parsed with a real
* HTML5 parser, four children of a flex row came back as
*
* 1
2
*
* 4
*
* which is exactly the reported break: the first children still in their row
* inside the bordered box, an empty bordered box after it (that clone), and the
* remaining children spilled down the page at their natural size because they
* are no longer in the flex container at all. The editor never showed it — it
* renders the chosen htmlTag and no anchor whatsoever — so the two disagreed
* only once published.
*
* No stylesheet can reach this: by the time CSS runs, the DOM is already the
* wrong shape. So the anchor stops being the box. The container is rendered
* back as its own tag, carrying every class, id, data-attribute and therefore
* every compiled style and breakpoint it had, and the link becomes a
* transparent overlay child stretched across it (`.ablocks-atomic-link`, sized
* by the rules in each container's style.css). The whole box stays clickable,
* links inside it stay clickable and are no longer torn out of the layout, and
* the markup the browser is handed is valid, so the tree it builds is the tree
* the editor drew.
*
* Done here at render rather than in save() on purpose: the saved markup of
* every page already published carries the nested anchors, and a save-side fix
* — even with a deprecation to keep those posts valid in the editor — would
* still serve that stored markup on the front end until someone opened and
* re-saved each one. Rewriting on output fixes the pages that are already
* broken, today, and leaves the editor and the stored content untouched.
*/
abstract class AtomicContainerBase extends AtomicBlockBase {
/**
* The tags the HTML-tag control offers — see TAGS in
* atomic-shared/container.js. Anything else falls back to a div rather
* than being echoed into the page as a tag name.
*/
protected static $allowed_tags = [ 'div', 'section', 'article', 'aside', 'header', 'footer', 'main', 'nav' ];
public function render_callback( $attributes, $content, $block_instance ) {
// Before the base class wraps or filters anything, so it only ever sees
// the shape it would have seen for an unlinked container.
$content = self::unnest_container_link( $content, $attributes );
return parent::render_callback( $attributes, $content, $block_instance );
}
/**
* Turn `…` back into `
* …`.
*
* Deliberately conservative: it only acts on markup shaped the way
* makeContainerSave writes it — a linked container whose first tag is the
* anchor and whose last tag closes it — and returns the content untouched
* on anything else, so a container that has already been rewritten, or one
* whose markup came from somewhere unexpected, is left exactly as it is.
*
* @param string $content The block's saved markup, inner blocks rendered.
* @param array $attributes The block's attributes.
* @return string The rewritten markup, or $content unchanged.
*/
public static function unnest_container_link( $content, $attributes ) {
if ( ! is_string( $content ) || '' === $content ) {
return $content;
}
if ( empty( $attributes['link']['url'] ) ) {
return $content;
}
// The container's own anchor opens the markup and closes it; anything
// else is not the shape this rewrite understands.
if ( ! preg_match( '#^(\s*)]*)>#i', $content, $open ) ) {
return $content;
}
if ( ! preg_match( '#(\s*)$#i', $content, $close ) ) {
return $content;
}
$element_attrs = $open[2];
// The link attributes move to the overlay verbatim — they were escaped
// when the block was saved, and re-encoding them here would be one more
// place for that to go wrong.
$link_attrs = '';
foreach ( [ 'href', 'target', 'rel' ] as $name ) {
if ( preg_match( '#(^|\s)' . $name . '="([^"]*)"#i', $element_attrs, $found ) ) {
$link_attrs .= ' ' . $name . '="' . $found[2] . '"';
$element_attrs = str_replace( $found[0], $found[1], $element_attrs );
}
}
if ( '' === $link_attrs ) {
return $content;
}
$tag = isset( $attributes['htmlTag'] ) ? strtolower( (string) $attributes['htmlTag'] ) : 'div';
if ( ! in_array( $tag, self::$allowed_tags, true ) ) {
$tag = 'div';
}
// The hook the overlay is positioned against.
if ( preg_match( '#(^|\s)class="([^"]*)"#i', $element_attrs, $found ) ) {
$element_attrs = str_replace(
$found[0],
$found[1] . 'class="' . $found[2] . ' ablocks-has-link"',
$element_attrs
);
} else {
$element_attrs = 'class="ablocks-has-link" ' . $element_attrs;
}
// One space where the link attributes were lifted out.
$element_attrs = trim( preg_replace( '/\s{2,}/', ' ', $element_attrs ) );
$inner = substr(
$content,
strlen( $open[0] ),
strlen( $content ) - strlen( $open[0] ) - strlen( $close[0] )
);
return $open[1]
. '<' . $tag . ( '' !== $element_attrs ? ' ' . $element_attrs : '' ) . '>'
. ''
. $inner
. '' . $tag . '>'
. $close[1];
}
/**
* An accessible name for the overlay.
*
* The anchor used to BE the container, so it was named by everything inside
* it; emptied out, it would announce as a bare URL and read as an unlabelled
* link. This gives back roughly what a screen reader heard before — the
* container's own text, or failing that the first alt text in it, which for
* the common linked-image card is the only text there is. Capped, because
* the name is a label and not the content.
*
* @param string $inner The container's inner markup.
* @return string An ` aria-label="…"` attribute, or an empty string.
*/
private static function overlay_label( $inner ) {
$label = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( $inner ) ) );
if ( '' === $label && preg_match( '#\salt="([^"]+)"#i', $inner, $found ) ) {
$label = trim( $found[1] );
}
if ( '' === $label ) {
return '';
}
return ' aria-label="' . esc_attr( mb_substr( $label, 0, 100 ) ) . '"';
}
}