22
33namespace Drupal \silverback_gutenberg \GutenbergValidation ;
44
5+ use Drupal \Component \Utility \Html ;
56use Drupal \Core \StringTranslation \TranslatableMarkup ;
67
78/**
@@ -73,31 +74,39 @@ public function validateCardinality(array $block, array $expected_children): arr
7374 return $ this ->validateEmptyInnerBlocks ($ expected_children );
7475 }
7576
76- // Count blocks, then check if the quantity for each is correct .
77+ // Count blocks and keep references for additional validations .
7778 $ countInnerBlockInstances = [];
79+ $ innerBlocksByName = [];
7880 foreach ($ block ['innerBlocks ' ] as $ innerBlock ) {
79- if (!isset ($ countInnerBlockInstances [$ innerBlock ['blockName ' ]])) {
80- $ countInnerBlockInstances [$ innerBlock ['blockName ' ]] = 0 ;
81+ $ blockName = $ innerBlock ['blockName ' ] ?? NULL ;
82+ if ($ blockName === NULL ) {
83+ continue ;
8184 }
82- $ countInnerBlockInstances [$ innerBlock ['blockName ' ]]++;
85+ if (!isset ($ countInnerBlockInstances [$ blockName ])) {
86+ $ countInnerBlockInstances [$ blockName ] = 0 ;
87+ }
88+ $ countInnerBlockInstances [$ blockName ]++;
89+ $ innerBlocksByName [$ blockName ][] = $ innerBlock ;
8390 }
8491
8592 foreach ($ expected_children as $ child ) {
86- if (!isset ($ countInnerBlockInstances [$ child ['blockName ' ]]) && $ child ['min ' ] > 0 ) {
93+ $ blockName = $ child ['blockName ' ];
94+ $ childBlocks = $ innerBlocksByName [$ blockName ] ?? [];
95+
96+ if (!isset ($ countInnerBlockInstances [$ blockName ]) && $ child ['min ' ] > 0 ) {
8797 $ message = $ this ->getExpectedQuantityErrorMessage ($ child );
8898 return [
8999 'is_valid ' => FALSE ,
90100 'message ' => $ message ,
91101 ];
92102 }
93103 // Minimum is set to 0, so we don't care if the block is not present.
94- if (!isset ($ countInnerBlockInstances [$ child ['blockName ' ]]) && $ child ['min ' ] === 0 ) {
95- return [
96- 'is_valid ' => TRUE ,
97- 'message ' => '' ,
98- ];
104+ if (!isset ($ countInnerBlockInstances [$ blockName ]) && $ child ['min ' ] === 0 ) {
105+ continue ;
99106 }
100- if ($ countInnerBlockInstances [$ child ['blockName ' ]] < $ child ['min ' ]) {
107+
108+ $ blockCount = $ countInnerBlockInstances [$ blockName ] ?? 0 ;
109+ if ($ blockCount < $ child ['min ' ]) {
101110 return [
102111 'is_valid ' => FALSE ,
103112 'message ' => \Drupal::translation ()->formatPlural ($ child ['min ' ],
@@ -109,7 +118,7 @@ public function validateCardinality(array $block, array $expected_children): arr
109118 ]),
110119 ];
111120 }
112- if ($ child ['max ' ] !== GutenbergCardinalityValidatorInterface::CARDINALITY_UNLIMITED && $ countInnerBlockInstances [ $ child [ ' blockName ' ]] > $ child ['max ' ]) {
121+ if ($ child ['max ' ] !== GutenbergCardinalityValidatorInterface::CARDINALITY_UNLIMITED && $ blockCount > $ child ['max ' ]) {
113122 return [
114123 'is_valid ' => FALSE ,
115124 'message ' => \Drupal::translation ()->formatPlural ($ child ['max ' ],
@@ -121,6 +130,13 @@ public function validateCardinality(array $block, array $expected_children): arr
121130 ]),
122131 ];
123132 }
133+
134+ if (!empty ($ childBlocks ) && !$ this ->hasPopulatedBlock ($ childBlocks )) {
135+ return [
136+ 'is_valid ' => FALSE ,
137+ 'message ' => $ this ->getMissingContentErrorMessage ($ child ),
138+ ];
139+ }
124140 }
125141
126142 return [
@@ -172,7 +188,18 @@ private function validateEmptyInnerBlocks (array $expected_children): array {
172188 private function validateAnyInnerBlocks (array $ inner_blocks , array $ expected_children ): array {
173189 $ min = $ expected_children ['min ' ];
174190 $ max = $ expected_children ['max ' ];
175- $ count = count ($ inner_blocks ['innerBlocks ' ]);
191+ $ innerBlockList = $ inner_blocks ['innerBlocks ' ] ?? [];
192+ $ count = count ($ innerBlockList );
193+ if (
194+ $ count > 0 &&
195+ !$ this ->hasPopulatedBlock ($ innerBlockList ) &&
196+ !$ this ->isBlockPopulated ($ inner_blocks )
197+ ) {
198+ return [
199+ 'is_valid ' => FALSE ,
200+ 'message ' => $ this ->getMissingContentErrorMessage (NULL ),
201+ ];
202+ }
176203 if ($ count < $ min ) {
177204 return [
178205 'is_valid ' => FALSE ,
@@ -218,4 +245,132 @@ private function getExpectedQuantityErrorMessage(array $child_block): string|Tra
218245 return $ result ;
219246 }
220247
248+ private function blockHasMeaningfulHtml (array $ block ): bool {
249+ $ innerHTML = $ block ['innerHTML ' ] ?? '' ;
250+ if (is_string ($ innerHTML ) && $ this ->stringContainsContent ($ innerHTML )) {
251+ return TRUE ;
252+ }
253+
254+ if (!empty ($ block ['innerContent ' ]) && is_array ($ block ['innerContent ' ])) {
255+ foreach ($ block ['innerContent ' ] as $ chunk ) {
256+ if (is_string ($ chunk ) && $ this ->stringContainsContent ($ chunk )) {
257+ return TRUE ;
258+ }
259+ }
260+ }
261+
262+ return FALSE ;
263+ }
264+
265+ private function stringContainsContent (string $ value ): bool {
266+ $ decoded = Html::decodeEntities ($ value );
267+ $ stripped = trim (strip_tags ($ decoded ));
268+ if ($ stripped !== '' ) {
269+ return TRUE ;
270+ }
271+
272+ return (bool ) preg_match ('/<(img|video|audio|iframe|svg|figure|source|embed|object|picture)\b/i ' , $ value );
273+ }
274+
275+ private function blockHasMeaningfulAttributes (array $ block ): bool {
276+ $ attrs = $ block ['attrs ' ] ?? [];
277+ if (empty ($ attrs )) {
278+ return FALSE ;
279+ }
280+
281+ foreach ($ attrs as $ value ) {
282+ if ($ this ->isMeaningfulValue ($ value )) {
283+ return TRUE ;
284+ }
285+ }
286+
287+ return FALSE ;
288+ }
289+
290+ private function isMeaningfulValue (mixed $ value ): bool {
291+ if ($ value === NULL ) {
292+ return FALSE ;
293+ }
294+ if (is_string ($ value )) {
295+ return trim ($ value ) !== '' ;
296+ }
297+ if (is_bool ($ value )) {
298+ return $ value ;
299+ }
300+ if (is_numeric ($ value )) {
301+ return TRUE ;
302+ }
303+ if (is_array ($ value )) {
304+ foreach ($ value as $ item ) {
305+ if ($ this ->isMeaningfulValue ($ item )) {
306+ return TRUE ;
307+ }
308+ }
309+ return FALSE ;
310+ }
311+
312+ return TRUE ;
313+ }
314+
315+ /**
316+ * Checks if any block in the supplied list is populated.
317+ */
318+ private function hasPopulatedBlock (array $ blocks ): bool {
319+ foreach ($ blocks as $ block ) {
320+ if (is_array ($ block ) && $ this ->isBlockPopulated ($ block )) {
321+ return TRUE ;
322+ }
323+ }
324+ return FALSE ;
325+ }
326+
327+ private function isBlockPopulated (array $ block ): bool {
328+ $ evaluated = FALSE ;
329+
330+ if (array_key_exists ('innerHTML ' , $ block ) || array_key_exists ('innerContent ' , $ block )) {
331+ $ evaluated = TRUE ;
332+ if ($ this ->blockHasMeaningfulHtml ($ block )) {
333+ return TRUE ;
334+ }
335+ }
336+
337+ if (array_key_exists ('attrs ' , $ block )) {
338+ $ evaluated = TRUE ;
339+ if ($ this ->blockHasMeaningfulAttributes ($ block )) {
340+ return TRUE ;
341+ }
342+ }
343+
344+ if (!empty ($ block ['innerBlocks ' ]) && is_array ($ block ['innerBlocks ' ])) {
345+ $ evaluated = TRUE ;
346+ foreach ($ block ['innerBlocks ' ] as $ innerBlock ) {
347+ if (is_array ($ innerBlock ) && $ this ->isBlockPopulated ($ innerBlock )) {
348+ return TRUE ;
349+ }
350+ }
351+ }
352+
353+ if (!$ evaluated ) {
354+ return TRUE ;
355+ }
356+
357+ return FALSE ;
358+ }
359+
360+ private function getMissingContentErrorMessage (?array $ child_block ): string |TranslatableMarkup {
361+ $ messageSuffix = t ('content or attributes. ' );
362+
363+ if (!empty ($ child_block )) {
364+ $ messageParams = [
365+ '%label ' => $ child_block ['blockLabel ' ],
366+ '@message_suffix ' => $ messageSuffix ,
367+ ];
368+ return t ('%label: block must contain @message_suffix ' , $ messageParams );
369+ }
370+
371+ return t ('Block must contain @message_suffix ' , [
372+ '@message_suffix ' => $ messageSuffix ,
373+ ]);
374+ }
375+
221376}
0 commit comments