diff --git a/modules/ui_patterns_library/src/StoriesSyntaxConverter.php b/modules/ui_patterns_library/src/StoriesSyntaxConverter.php index 7c13c4392967b07f30c1e3b304cd289c31568381..4be791ebb1ab86cb4c35adcba57323842c96d3a4 100644 --- a/modules/ui_patterns_library/src/StoriesSyntaxConverter.php +++ b/modules/ui_patterns_library/src/StoriesSyntaxConverter.php @@ -11,88 +11,224 @@ namespace Drupal\ui_patterns_library; * Let's put them back. * * Before: ["type" => "component", "component" => "example:card"] - * After: ["#type" => "component", "#component" => "example:card"] + * After: ["#type" => "component", "#component" => "example:card"] */ class StoriesSyntaxConverter { /** * An array with one (and only one) of those keys may be a render array. */ - const RENDER_KEYS = ["theme", "type", "markup", "plain_text", "#theme", "#type", "#markup", "#plain_text"]; + public const RENDER_KEYS = [ + 'markup', + 'plain_text', + 'theme', + 'type', + '#markup', + '#plain_text', + '#theme', + '#type', + ]; + + public const KNOWN_PROPERTIES = [ + 'type' => [ + 'html_tag' => [ + 'attached', + 'attributes', + 'tag', + 'type', + 'value', + ], + ], + 'theme' => [ + 'layout' => [ + 'attached', + 'attributes', + 'theme', + 'settings', + ], + ], + ]; /** - * Process stories slots. + * List of render properties which should have been children instead. */ - public function convertSlots(array $slots): array { - foreach ($slots as $slot_id => $slot) { - if (!is_array($slot)) { - continue; - } - $slots[$slot_id] = $this->convertArray($slot); - } - return $slots; - } + public const PROPERTIES_INSTEAD_OF_CHILDREN = [ + 'type' => [ + 'component' => [ + 'slots', + ], + ], + 'theme' => [ + 'status_messages' => [ + 'message_list', + ], + 'table' => [ + 'header', + 'rows', + 'footer', + 'empty', + 'caption', + ], + ], + ]; /** - * Convert an array. + * Process stories slots. */ - protected function convertArray(array $array): array { + public function convertSlots(array $array): array { if ($this->isRenderArray($array)) { return $this->convertRenderArray($array); } foreach ($array as $index => $value) { - if (!is_array($value)) { + if (!\is_array($value)) { continue; } - $array[$index] = $this->convertArray($value); + $array[$index] = $this->convertSlots($value); } return $array; } /** * Convert a render array. + * + * @param array $renderable + * The render array being processed. + * + * @return array + * The processed render array. */ protected function convertRenderArray(array $renderable): array { - foreach ($renderable as $property => $value) { - if (is_array($value)) { - $renderable[$property] = $this->convertArray($value); + // Weird detection. + if (isset($renderable['type'], self::PROPERTIES_INSTEAD_OF_CHILDREN['type'][$renderable['type']])) { + return $this->convertWeirdRenderArray($renderable, self::PROPERTIES_INSTEAD_OF_CHILDREN['type'][$renderable['type']]); + } + if (isset($renderable['theme'])) { + $baseThemeHook = \explode('__', $renderable['theme'])[0]; + if (isset(self::PROPERTIES_INSTEAD_OF_CHILDREN['theme'][$baseThemeHook])) { + return $this->convertWeirdRenderArray($renderable, self::PROPERTIES_INSTEAD_OF_CHILDREN['theme'][$baseThemeHook]); } } - $in_html_tag = (isset($renderable["type"]) && $renderable["type"] === "html_tag"); - $html_tag_allowed_render_keys = ["type", "attributes", "tag", "value", "attached"]; + + // Normal with special case detection. + if (isset($renderable['type'], self::KNOWN_PROPERTIES['type'][$renderable['type']])) { + return $this->convertNormalRenderArray($renderable, self::KNOWN_PROPERTIES['type'][$renderable['type']]); + } + if (isset($renderable['theme'])) { + $baseThemeHook = \explode('__', $renderable['theme'])[0]; + if (isset(self::KNOWN_PROPERTIES['theme'][$baseThemeHook])) { + return $this->convertNormalRenderArray($renderable, self::KNOWN_PROPERTIES['theme'][$baseThemeHook]); + } + } + + return $this->convertNormalRenderArray($renderable, []); + } + + /** + * Add property prefix. + * + * @param array $renderable + * The renderable array. + * @param mixed $property + * The property. + * + * @return array + * The array with prefixed property. + */ + protected function convertProperty(array $renderable, mixed $property): array { + if (!\is_string($property)) { + return $renderable; + } + if (\str_starts_with($property, '#')) { + return $renderable; + } + $renderable['#' . $property] = $renderable[$property]; + unset($renderable[$property]); + return $renderable; + } + + /** + * To convert "normal" render array. + * + * A "normal" render arrays is an array: + * - where properties (key starts with a '#') are not renderables + * - children (key does not start with a '#') are only renderables. + * + * Examples: + * - html_tag which is forbidding renderables in #value + * - layout where every region is a child. + * + * @param array $renderable + * The renderable array. + * @param array $knownProperties + * The list of know properties. + * + * @return array + * The converted array. + */ + protected function convertNormalRenderArray(array $renderable, array $knownProperties): array { foreach ($renderable as $property => $value) { - if (!is_string($property)) { - continue; + if (empty($knownProperties) && \is_string($property)) { + // Default to add prefix to every entries. + $renderable = $this->convertProperty($renderable, $property); } - // html_tag is special. - if ($in_html_tag && !in_array($property, $html_tag_allowed_render_keys)) { - continue; + elseif (\in_array($property, $knownProperties, TRUE)) { + // We add # prefix only to known properties. + $renderable = $this->convertProperty($renderable, $property); } - if (str_starts_with($property, "#")) { - continue; + elseif (\is_array($value)) { + // Other keys may have children so let's drill. + $renderable[$property] = $this->convertSlots($value); + } + } + return $renderable; + } + + /** + * The "weird" render arrays, where renderables are found only in properties. + * + * Examples: component with #slots, table with #rows... + * + * @param array $renderable + * The renderable array. + * @param array $propertiesWithRenderables + * The list of properties to look for. + * + * @return array + * The updated renderable array. + */ + protected function convertWeirdRenderArray(array $renderable, array $propertiesWithRenderables): array { + foreach ($renderable as $property => $value) { + if (\in_array($property, $propertiesWithRenderables, TRUE) && \is_array($value)) { + $renderable[$property] = $this->convertSlots($value); } - $renderable["#" . $property] = $value; - unset($renderable[$property]); + // There are no children, so we add a # everywhere. + $renderable = $this->convertProperty($renderable, $property); } return $renderable; } /** * Is the array a render array? + * + * @param array $array + * The array being processed. + * + * @return bool + * True if a render array. */ protected function isRenderArray(array $array): bool { - if (array_is_list($array)) { + if (\array_is_list($array)) { return FALSE; } // An array needs one, and only one, of those properties to be a render // array. - $intersect = array_intersect(array_keys($array), self::RENDER_KEYS); - if (count($intersect) != 1) { + $intersect = \array_intersect(\array_keys($array), self::RENDER_KEYS); + if (\count($intersect) != 1) { return FALSE; } // This property has to be a string value. - $property = $intersect[array_key_first($intersect)]; - if (!is_string($array[$property])) { + $property = $intersect[\array_key_first($intersect)]; + if (!\is_string($array[$property])) { return FALSE; } return TRUE; diff --git a/modules/ui_patterns_library/tests/src/Unit/StoriesSyntaxConversionTest.php b/modules/ui_patterns_library/tests/src/Unit/StoriesSyntaxConversionTest.php index ffb21518e3d16cf8652b75da94338f09bc2887b5..aa6e28876d2745328456d25c28f9eb105b0f64f5 100644 --- a/modules/ui_patterns_library/tests/src/Unit/StoriesSyntaxConversionTest.php +++ b/modules/ui_patterns_library/tests/src/Unit/StoriesSyntaxConversionTest.php @@ -8,9 +8,10 @@ use Drupal\Tests\UnitTestCase; use Drupal\ui_patterns_library\StoriesSyntaxConverter; /** - * @coversDefaultClass \Drupal\ui_patterns\Plugin\UiPatterns\PropType\LinksPropType + * @coversDefaultClass \Drupal\ui_patterns_library\StoriesSyntaxConverter * * @group ui_patterns + * @group ui_patterns_library */ final class StoriesSyntaxConversionTest extends UnitTestCase { @@ -22,7 +23,7 @@ final class StoriesSyntaxConversionTest extends UnitTestCase { public function testConvertSlot(array $value, array $expected): void { $converter = new StoriesSyntaxConverter(); $converted = $converter->convertSlots($value); - self::assertEquals($converted, $expected); + self::assertEquals($expected, $converted); } /** @@ -34,6 +35,7 @@ final class StoriesSyntaxConversionTest extends UnitTestCase { "Bootstrap accordion" => self::bootstrapAccordion(), "Bootstrap carousel" => self::bootstrapCarousel(), "Daisy Grid Row 4" => self::daisyGridRow4(), + "Props special words" => self::componentSpecialProps(), ]; foreach ($data as $label => $test) { yield $label => [ @@ -296,4 +298,48 @@ final class StoriesSyntaxConversionTest extends UnitTestCase { ]; } + /** + * Fake component with props with reserved words. + */ + protected static function componentSpecialProps(): array { + $slots = [ + 'component' => [ + 'type' => 'component', + 'component' => 'foo:bar', + 'props' => [ + 'attributes' => [ + 'class' => ['bar'], + 'type' => 'bar', + ], + 'tag' => 'bar', + 'type' => 'bar', + 'value' => 'bar', + 'attached' => 'bar', + ], + ], + ]; + $expected = [ + 'component' => [ + '#type' => 'component', + '#component' => 'foo:bar', + '#props' => [ + 'attributes' => [ + 'class' => [ + 0 => 'bar', + ], + 'type' => 'bar', + ], + 'tag' => 'bar', + 'type' => 'bar', + 'value' => 'bar', + 'attached' => 'bar', + ], + ], + ]; + return [ + 'value' => $slots, + 'expected' => $expected, + ]; + } + }