PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.1
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
ablocks / includes / classes / atomic-container-base.php

atomic-container-base.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.13.1, at includes/classes/atomic-container-base.php

171 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Classes;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * Shared base for the atomic CONTAINER blocks — div, flex and grid.
10 *
11 * Everything about style compilation still comes from AtomicBlockBase. What
12 * this layer adds is one front-end-only rewrite: a container carrying a Link
13 * URL is saved as an `<a>` element (see makeContainerSave in
14 * atomic-shared/container.js), and an `<a>` may not contain another `<a>`.
15 *
16 * That is not a validation nicety, it is a parser rule with teeth. Feed a
17 * browser `<a class="…flex container"><div>…</div><div><a href>…</a></div>…</a>`
18 * — a linked container holding a child that is itself linked, an image with a
19 * link being the everyday way to get one — and the HTML5 adoption agency
20 * algorithm rebuilds the tree: the outer anchor is closed at the nested one, an
21 * EMPTY clone of it is left behind inside the child, and every sibling after
22 * that point is lifted out of the container altogether. Parsed with a real
23 * HTML5 parser, four children of a flex row came back as
24 *
25 * <a class="…"><div>1</div><div>2</div></a>
26 * <div><a class="…"></a><a href>3</a></div>
27 * <div>4</div>
28 *
29 * which is exactly the reported break: the first children still in their row
30 * inside the bordered box, an empty bordered box after it (that clone), and the
31 * remaining children spilled down the page at their natural size because they
32 * are no longer in the flex container at all. The editor never showed it — it
33 * renders the chosen htmlTag and no anchor whatsoever — so the two disagreed
34 * only once published.
35 *
36 * No stylesheet can reach this: by the time CSS runs, the DOM is already the
37 * wrong shape. So the anchor stops being the box. The container is rendered
38 * back as its own tag, carrying every class, id, data-attribute and therefore
39 * every compiled style and breakpoint it had, and the link becomes a
40 * transparent overlay child stretched across it (`.ablocks-atomic-link`, sized
41 * by the rules in each container's style.css). The whole box stays clickable,
42 * links inside it stay clickable and are no longer torn out of the layout, and
43 * the markup the browser is handed is valid, so the tree it builds is the tree
44 * the editor drew.
45 *
46 * Done here at render rather than in save() on purpose: the saved markup of
47 * every page already published carries the nested anchors, and a save-side fix
48 * — even with a deprecation to keep those posts valid in the editor — would
49 * still serve that stored markup on the front end until someone opened and
50 * re-saved each one. Rewriting on output fixes the pages that are already
51 * broken, today, and leaves the editor and the stored content untouched.
52 */
53 abstract class AtomicContainerBase extends AtomicBlockBase {
54
55 /**
56 * The tags the HTML-tag control offers — see TAGS in
57 * atomic-shared/container.js. Anything else falls back to a div rather
58 * than being echoed into the page as a tag name.
59 */
60 protected static $allowed_tags = [ 'div', 'section', 'article', 'aside', 'header', 'footer', 'main', 'nav' ];
61
62 public function render_callback( $attributes, $content, $block_instance ) {
63 // Before the base class wraps or filters anything, so it only ever sees
64 // the shape it would have seen for an unlinked container.
65 $content = self::unnest_container_link( $content, $attributes );
66 return parent::render_callback( $attributes, $content, $block_instance );
67 }
68
69 /**
70 * Turn `<a class="container" href>…</a>` back into `<tag class="container">
71 * <a class="ablocks-atomic-link" href></a>…</tag>`.
72 *
73 * Deliberately conservative: it only acts on markup shaped the way
74 * makeContainerSave writes it — a linked container whose first tag is the
75 * anchor and whose last tag closes it — and returns the content untouched
76 * on anything else, so a container that has already been rewritten, or one
77 * whose markup came from somewhere unexpected, is left exactly as it is.
78 *
79 * @param string $content The block's saved markup, inner blocks rendered.
80 * @param array $attributes The block's attributes.
81 * @return string The rewritten markup, or $content unchanged.
82 */
83 public static function unnest_container_link( $content, $attributes ) {
84 if ( ! is_string( $content ) || '' === $content ) {
85 return $content;
86 }
87 if ( empty( $attributes['link']['url'] ) ) {
88 return $content;
89 }
90 // The container's own anchor opens the markup and closes it; anything
91 // else is not the shape this rewrite understands.
92 if ( ! preg_match( '#^(\s*)<a\s([^>]*)>#i', $content, $open ) ) {
93 return $content;
94 }
95 if ( ! preg_match( '#</a>(\s*)$#i', $content, $close ) ) {
96 return $content;
97 }
98
99 $element_attrs = $open[2];
100
101 // The link attributes move to the overlay verbatim — they were escaped
102 // when the block was saved, and re-encoding them here would be one more
103 // place for that to go wrong.
104 $link_attrs = '';
105 foreach ( [ 'href', 'target', 'rel' ] as $name ) {
106 if ( preg_match( '#(^|\s)' . $name . '="([^"]*)"#i', $element_attrs, $found ) ) {
107 $link_attrs .= ' ' . $name . '="' . $found[2] . '"';
108 $element_attrs = str_replace( $found[0], $found[1], $element_attrs );
109 }
110 }
111 if ( '' === $link_attrs ) {
112 return $content;
113 }
114
115 $tag = isset( $attributes['htmlTag'] ) ? strtolower( (string) $attributes['htmlTag'] ) : 'div';
116 if ( ! in_array( $tag, self::$allowed_tags, true ) ) {
117 $tag = 'div';
118 }
119
120 // The hook the overlay is positioned against.
121 if ( preg_match( '#(^|\s)class="([^"]*)"#i', $element_attrs, $found ) ) {
122 $element_attrs = str_replace(
123 $found[0],
124 $found[1] . 'class="' . $found[2] . ' ablocks-has-link"',
125 $element_attrs
126 );
127 } else {
128 $element_attrs = 'class="ablocks-has-link" ' . $element_attrs;
129 }
130 // One space where the link attributes were lifted out.
131 $element_attrs = trim( preg_replace( '/\s{2,}/', ' ', $element_attrs ) );
132
133 $inner = substr(
134 $content,
135 strlen( $open[0] ),
136 strlen( $content ) - strlen( $open[0] ) - strlen( $close[0] )
137 );
138
139 return $open[1]
140 . '<' . $tag . ( '' !== $element_attrs ? ' ' . $element_attrs : '' ) . '>'
141 . '<a class="ablocks-atomic-link"' . $link_attrs . self::overlay_label( $inner ) . '></a>'
142 . $inner
143 . '</' . $tag . '>'
144 . $close[1];
145 }
146
147 /**
148 * An accessible name for the overlay.
149 *
150 * The anchor used to BE the container, so it was named by everything inside
151 * it; emptied out, it would announce as a bare URL and read as an unlabelled
152 * link. This gives back roughly what a screen reader heard before — the
153 * container's own text, or failing that the first alt text in it, which for
154 * the common linked-image card is the only text there is. Capped, because
155 * the name is a label and not the content.
156 *
157 * @param string $inner The container's inner markup.
158 * @return string An ` aria-label="…"` attribute, or an empty string.
159 */
160 private static function overlay_label( $inner ) {
161 $label = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( $inner ) ) );
162 if ( '' === $label && preg_match( '#\salt="([^"]+)"#i', $inner, $found ) ) {
163 $label = trim( $found[1] );
164 }
165 if ( '' === $label ) {
166 return '';
167 }
168 return ' aria-label="' . esc_attr( mb_substr( $label, 0, 100 ) ) . '"';
169 }
170 }
171