Skip to content
Snippets Groups Projects
Commit fc10d3af authored by Pierre Dureau's avatar Pierre Dureau
Browse files

Issue #3505217 by grimreaper, pdureau, jmaxant: Support render arrays using...

Issue #3505217 by grimreaper, pdureau, jmaxant: Support render arrays using render properties as render children
parent 4c440880
No related branches found
No related tags found
1 merge request!21Issue #3505217 by pdureau: Support Single directory components
Pipeline #497924 passed
......@@ -11,7 +11,7 @@ namespace Drupal\ui_examples;
* Let's put them back.
*
* Before: ["type" => "component", "component" => "example:card"]
* After: ["#type" => "component", "#component" => "example:card"]
* After: ["#type" => "component", "#component" => "example:card"]
*/
class ExampleSyntaxConverter implements ExampleSyntaxConverterInterface {
......@@ -29,6 +29,65 @@ class ExampleSyntaxConverter implements ExampleSyntaxConverterInterface {
'#type',
];
public const KNOWN_PROPERTIES = [
'type' => [
'html_tag' => [
'attached',
'attributes',
'tag',
'type',
'value',
'#attached',
'#attributes',
'#tag',
'#type',
'#value',
],
],
'theme' => [
'layout' => [
'attached',
'attributes',
'theme',
'settings',
'#attached',
'#attributes',
'#theme',
'#settings',
],
],
];
/**
* List of render properties which should have been children instead.
*/
public const PROPERTIES_INSTEAD_OF_CHILDREN = [
'type' => [
'component' => [
'slots',
'#slots',
],
],
'theme' => [
'status_messages' => [
'message_list',
'#message_list',
],
'table' => [
'header',
'rows',
'footer',
'empty',
'caption',
'#header',
'#rows',
'#footer',
'#empty',
'#caption',
],
],
];
/**
* {@inheritdoc}
*/
......@@ -55,43 +114,113 @@ class ExampleSyntaxConverter implements ExampleSyntaxConverterInterface {
* The processed render array.
*/
protected function convertRenderArray(array $renderable): array {
foreach ($renderable as $property => $value) {
if (\is_array($value)) {
$renderable[$property] = $this->convertArray($value);
$renderable = $this->prepareRenderArray($renderable);
// 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]);
}
}
// 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]);
}
}
$in_html_tag = (isset($renderable['type']) && $renderable['type'] === 'html_tag');
$html_tag_allowed_render_keys = [
'attached',
'attributes',
'tag',
'type',
'value',
];
$in_layout = (isset($renderable['theme']) && \explode('__', $renderable['theme'])[0] === 'layout');
$layout_allowed_render_keys = [
'attached',
'attributes',
'theme',
'settings',
];
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 the prefix to every entry.
$renderable = $this->convertProperty($renderable, $property);
}
// html_tag is special.
if ($in_html_tag && !\in_array($property, $html_tag_allowed_render_keys, TRUE)) {
continue;
elseif (\in_array($property, $knownProperties, TRUE)) {
// We add # prefix only to known properties.
$renderable = $this->convertProperty($renderable, $property);
}
// Layouts are special.
if ($in_layout && !\in_array($property, $layout_allowed_render_keys, TRUE)) {
continue;
elseif (\is_array($value)) {
// Other keys may have children, so let's drill.
$renderable[$property] = $this->convertArray($value);
}
if (\str_starts_with($property, '#')) {
continue;
}
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->convertArray($value);
}
$renderable['#' . $property] = $value;
unset($renderable[$property]);
// There are no children, so we add a # everywhere.
$renderable = $this->convertProperty($renderable, $property);
}
return $renderable;
}
......@@ -123,4 +252,27 @@ class ExampleSyntaxConverter implements ExampleSyntaxConverterInterface {
return TRUE;
}
/**
* Prepare a render array.
*
* Remove # on type and theme, will re-add it later.
*
* @param array $renderable
* The render array.
*
* @return array
* The prepared render array.
*/
protected function prepareRenderArray(array $renderable): array {
if (isset($renderable['#type']) && !isset($renderable['type'])) {
$renderable['type'] = $renderable['#type'];
unset($renderable['#type']);
}
if (isset($renderable['#theme']) && !isset($renderable['theme'])) {
$renderable['theme'] = $renderable['#theme'];
unset($renderable['#theme']);
}
return $renderable;
}
}
......@@ -22,7 +22,7 @@ final class ExampleSyntaxConverterTest extends UnitTestCase {
public function testConvertArray(array $value, array $expected): void {
$converter = new ExampleSyntaxConverter();
$converted = $converter->convertArray($value);
$this->assertEquals($converted, $expected);
$this->assertEquals($expected, $converted);
}
/**
......@@ -32,8 +32,10 @@ final class ExampleSyntaxConverterTest extends UnitTestCase {
$data = [
'Not convertible' => self::notConvertible(),
'Status messages' => self::statusMessages(),
'Table' => self::table(),
'Theme image' => self::themeImage(),
'Theme layout' => self::themeLayout(),
'Props special words' => self::componentSpecialProps(),
];
foreach ($data as $label => $test) {
yield $label => [
......@@ -85,97 +87,373 @@ final class ExampleSyntaxConverterTest extends UnitTestCase {
*/
protected static function statusMessages(): array {
$value = [
'theme' => 'status_messages',
'message_list' => [
'error' => [
'Error message:',
[
'markup' => 'With link: ',
0 => [
'type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
[
'theme' => 'status_messages',
'message_list' => [
'error' => [
'Error message:',
[
[
'markup' => 'With link: ',
],
[
'type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
],
],
],
'warning' => [
'Warning message:',
[
[
'markup' => 'With link: ',
],
[
'type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
],
],
],
'status' => [
'Status message:',
[
[
'markup' => 'With link: ',
],
[
'type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
],
],
],
],
'warning' => [
'Warning message:',
[
'markup' => 'With link: ',
0 => [
'type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
[
'theme' => 'status_messages',
'#message_list' => [
'error' => [
'Error message:',
[
[
'markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
],
],
],
'warning' => [
'Warning message:',
[
[
'markup' => 'With link: ',
],
[
'type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
],
],
],
'status' => [
'Status message:',
[
[
'markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
],
'status' => [
'Status message:',
[
'markup' => 'With link: ',
0 => [
'type' => 'html_tag',
'tag' => 'a',
'value' => 'Examples page',
'attributes' => [
'href' => '/examples',
],
];
$expected = [
[
'#theme' => 'status_messages',
'#message_list' => [
'error' => [
'Error message:',
[
[
'#markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
'warning' => [
'Warning message:',
[
[
'#markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
'status' => [
'Status message:',
[
[
'#markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
],
],
[
'#theme' => 'status_messages',
'#message_list' => [
'error' => [
'Error message:',
[
[
'#markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
'warning' => [
'Warning message:',
[
[
'#markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
'status' => [
'Status message:',
[
[
'#markup' => 'With link: ',
],
[
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
],
],
],
],
],
];
return [
'value' => $value,
'expected' => $expected,
];
}
/**
* Table.
*/
protected static function table(): array {
$value = [
[
'theme' => 'table',
'header' => [
'foo' => [
'data' => [
'markup' => 'foo',
],
],
'bar' => [
'data' => [
'markup' => 'bar',
],
],
],
'rows' => [
[
'data' => [
'markup' => 'baz',
],
],
],
'footer' => [
[
'data' => [
'markup' => 'baz',
],
],
],
'empty' => [
'markup' => 'baz',
],
'caption' => [
'markup' => 'baz',
],
],
[
'theme' => 'table',
'#header' => [
'foo' => [
'data' => [
'markup' => 'foo',
],
],
'bar' => [
'data' => [
'#markup' => 'bar',
],
],
],
'rows' => [
[
'data' => [
'markup' => 'baz',
],
],
],
'#footer' => [
[
'data' => [
'#markup' => 'baz',
],
],
],
'empty' => [
'#markup' => 'baz',
],
'#caption' => [
'markup' => 'baz',
],
],
];
$expected = [
'#theme' => 'status_messages',
'#message_list' => [
'error' => [
'Error message:',
[
'#theme' => 'table',
'#header' => [
'foo' => [
'data' => [
'#markup' => 'foo',
],
],
'bar' => [
'data' => [
'#markup' => 'bar',
],
],
],
'#rows' => [
[
'#markup' => 'With link: ',
0 => [
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
'data' => [
'#markup' => 'baz',
],
],
],
'warning' => [
'Warning message:',
'#footer' => [
[
'#markup' => 'With link: ',
0 => [
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
'data' => [
'#markup' => 'baz',
],
],
],
'status' => [
'Status message:',
'#empty' => [
'#markup' => 'baz',
],
'#caption' => [
'#markup' => 'baz',
],
],
[
'#theme' => 'table',
'#header' => [
'foo' => [
'data' => [
'#markup' => 'foo',
],
],
'bar' => [
'data' => [
'#markup' => 'bar',
],
],
],
'#rows' => [
[
'#markup' => 'With link: ',
0 => [
'#type' => 'html_tag',
'#tag' => 'a',
'#value' => 'Examples page',
'#attributes' => [
'href' => '/examples',
],
'data' => [
'#markup' => 'baz',
],
],
],
'#footer' => [
[
'data' => [
'#markup' => 'baz',
],
],
],
'#empty' => [
'#markup' => 'baz',
],
'#caption' => [
'#markup' => 'baz',
],
],
];
return [
......@@ -189,19 +467,39 @@ final class ExampleSyntaxConverterTest extends UnitTestCase {
*/
protected static function themeImage(): array {
$value = [
'theme' => 'image',
'uri' => 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iYmQtcGxhY2Vob2xkZXItaW1nIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtaGlkZGVuPSJ0cnVlIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBzbGljZSIgZm9jdXNhYmxlPSJmYWxzZSI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzc3NyI+PC9yZWN0Pjwvc3ZnPgo=',
'attributes' => [
'width' => '100%',
'height' => '100%',
[
'theme' => 'image',
'uri' => 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iYmQtcGxhY2Vob2xkZXItaW1nIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtaGlkZGVuPSJ0cnVlIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBzbGljZSIgZm9jdXNhYmxlPSJmYWxzZSI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzc3NyI+PC9yZWN0Pjwvc3ZnPgo=',
'attributes' => [
'width' => '100%',
'height' => '100%',
],
],
[
'theme' => 'image',
'#uri' => 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iYmQtcGxhY2Vob2xkZXItaW1nIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtaGlkZGVuPSJ0cnVlIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBzbGljZSIgZm9jdXNhYmxlPSJmYWxzZSI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzc3NyI+PC9yZWN0Pjwvc3ZnPgo=',
'attributes' => [
'width' => '100%',
'height' => '100%',
],
],
];
$expected = [
'#theme' => 'image',
'#uri' => 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iYmQtcGxhY2Vob2xkZXItaW1nIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtaGlkZGVuPSJ0cnVlIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBzbGljZSIgZm9jdXNhYmxlPSJmYWxzZSI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzc3NyI+PC9yZWN0Pjwvc3ZnPgo=',
'#attributes' => [
'width' => '100%',
'height' => '100%',
[
'#theme' => 'image',
'#uri' => 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iYmQtcGxhY2Vob2xkZXItaW1nIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtaGlkZGVuPSJ0cnVlIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBzbGljZSIgZm9jdXNhYmxlPSJmYWxzZSI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzc3NyI+PC9yZWN0Pjwvc3ZnPgo=',
'#attributes' => [
'width' => '100%',
'height' => '100%',
],
],
[
'#theme' => 'image',
'#uri' => 'data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iYmQtcGxhY2Vob2xkZXItaW1nIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtaGlkZGVuPSJ0cnVlIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBzbGljZSIgZm9jdXNhYmxlPSJmYWxzZSI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iIzc3NyI+PC9yZWN0Pjwvc3ZnPgo=',
'#attributes' => [
'width' => '100%',
'height' => '100%',
],
],
];
return [
......@@ -237,6 +535,28 @@ final class ExampleSyntaxConverterTest extends UnitTestCase {
],
],
],
[
'#theme' => 'layout',
'my_region' => [
'markup' => 'my markup',
],
'attributes' => [
'class' => [
'my-class',
],
],
],
[
'theme' => 'layout__my_theme',
'my_region' => [
'markup' => 'my markup',
],
'#attributes' => [
'class' => [
'my-class',
],
],
],
];
$expected = [
[
......@@ -261,6 +581,28 @@ final class ExampleSyntaxConverterTest extends UnitTestCase {
],
],
],
[
'#theme' => 'layout',
'my_region' => [
'#markup' => 'my markup',
],
'#attributes' => [
'class' => [
'my-class',
],
],
],
[
'#theme' => 'layout__my_theme',
'my_region' => [
'#markup' => 'my markup',
],
'#attributes' => [
'class' => [
'my-class',
],
],
],
];
return [
'value' => $value,
......@@ -268,4 +610,78 @@ final class ExampleSyntaxConverterTest 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',
],
],
'component_mixed' => [
'#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',
],
],
'component_mixed' => [
'#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,
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment