From 1fb2472bd1c128b610dff5cdd7a7415125e33c1d Mon Sep 17 00:00:00 2001 From: Marco Wildermuth Date: Wed, 25 Aug 2021 10:47:35 +0200 Subject: [PATCH 1/3] Render the fluid in fixtures recursively --- .../Component/ExampleViewHelper.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Classes/ViewHelpers/Component/ExampleViewHelper.php b/Classes/ViewHelpers/Component/ExampleViewHelper.php index 3c272ea..ec77ac9 100644 --- a/Classes/ViewHelpers/Component/ExampleViewHelper.php +++ b/Classes/ViewHelpers/Component/ExampleViewHelper.php @@ -167,19 +167,21 @@ function () { /** * Renders inline fluid code in a fixture array that will be provided as example data to a component * - * @param array $data + * @param mixed $data * @param RenderingContextInterface $renderingContext - * @return void + * @return mixed */ - public static function renderFluidInExampleData(array $data, RenderingContextInterface $renderingContext) + public static function renderFluidInExampleData($data, RenderingContextInterface $renderingContext) { - return array_map(function ($value) use ($renderingContext) { - if (is_string($value)) { - return $renderingContext->getTemplateParser()->parse($value)->render($renderingContext); - } else { - return $value; - } - }, $data); + if (is_string($data)) { + return $renderingContext->getTemplateParser()->parse($data)->render($renderingContext); + } elseif (is_array($data)) { + return array_map(function ($value) use ($renderingContext) { + return self::renderFluidInExampleData($value, $renderingContext); + }, $data); + } else { + return $data; + } } /** From 240b19b5423ec4c91778549895caae7113cea230 Mon Sep 17 00:00:00 2001 From: Marco Wildermuth Date: Wed, 25 Aug 2021 19:01:37 +0200 Subject: [PATCH 2/3] Add ViewHelper to render styleguide component with fixture --- .../Component/RenderFixtureViewHelper.php | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 Classes/ViewHelpers/Component/RenderFixtureViewHelper.php diff --git a/Classes/ViewHelpers/Component/RenderFixtureViewHelper.php b/Classes/ViewHelpers/Component/RenderFixtureViewHelper.php new file mode 100644 index 0000000..9bf93a9 --- /dev/null +++ b/Classes/ViewHelpers/Component/RenderFixtureViewHelper.php @@ -0,0 +1,247 @@ +registerArgument('component', 'string', 'Name of the component that should be rendered', true); + $this->registerArgument('fixtureName', 'string', 'Name of the fixture that the component should be rendered with', false, 'default'); + $this->registerArgument('fixtureData', 'array', 'Additional dynamic fixture data that should be used'); + } + + /** + * Renders fluid example code for the specified component + * + * @param array $arguments + * @param \Closure $renderChildrenClosure + * @param RenderingContextInterface $renderingContext + * @return string + */ + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ): string { + + $componentIdentifier = self::sanitizeComponentIdentifier($arguments['component'] ?? ''); + + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + $componentRepository = $objectManager->get(ComponentRepository::class); + + $component = $componentRepository->findWithFixturesByIdentifier($componentIdentifier); + if (!$component) { + return sprintf('Component %s not found', $componentIdentifier); + } + + if (!isset($arguments['fixtureName']) && !isset($arguments['fixtureData'])) { + throw new \InvalidArgumentException(sprintf( + 'A fixture name or fixture data has to be specified to render the component %s.', + $arguments['component'] + ), 1566377563); + } + + $fixtureData = $arguments['fixtureData'] ?? []; + + $fixtureName = self::sanitizeFixtureName($arguments['fixtureName'] ?? 'default'); + + if (isset($fixtureName)) { + $componentFixture = $component->getFixture($fixtureName); + if (!$componentFixture) { + throw new \InvalidArgumentException(sprintf( + 'Invalid fixture name "%s" specified for component %s.', + $fixtureName, + $componentIdentifier + ), 1566377564); + } + + // Merge static fixture data with manually edited data + $fixtureData = array_replace($componentFixture->getData(), $fixtureData); + } + + $renderingContext->getViewHelperResolver()->addNamespace('fsv', 'Sitegeist\FluidStyleguide\ViewHelpers'); + + // Parse fluid code in fixtures + $fixtureData = self::renderFluidInExampleData($fixtureData, $renderingContext); + + $styleguideConfigurationManager = $objectManager->get(StyleguideConfigurationManager::class); + $componentContext = $styleguideConfigurationManager->getComponentContext(); + + $componentMarkup = self::renderComponent( + $component, + $fixtureData, + $renderingContext + ); + + $componentWithContext = self::applyComponentContext( + $componentMarkup, + $componentContext, + $renderingContext, + array_replace( + $component->getDefaultValues(), + $fixtureData + ) + ); + + return $componentWithContext; + } + + /** + * Make sure that the component identifier doesn't include any malicious characters + * + * @param string $componentIdentifier + * @return string + */ + protected static function sanitizeComponentIdentifier(string $componentIdentifier): string + { + return trim(preg_replace('#[^a-z0-9_\\\\]#i', '', $componentIdentifier), '\\'); + } + + /** + * Make sure that the fixture name doesn't include any malicious characters + * + * @param string $fixtureName + * @return string + */ + protected static function sanitizeFixtureName(string $fixtureName): string + { + return preg_replace('#[^a-z0-9_]#i', '', $fixtureName); + } + + /** + * Calls a component with the supplied example data + * + * @param Component $component + * @param array $data + * @param RenderingContextInterface $renderingContext + * @return string + */ + public static function renderComponent( + Component $component, + array $data, + RenderingContextInterface $renderingContext + ): string { + // Check if all required arguments were supplied to the component + foreach ($component->getArguments() as $expectedArgument) { + if ($expectedArgument->isRequired() && !isset($data[$expectedArgument->getName()])) { + throw new RequiredComponentArgumentException(sprintf( + 'Required argument "%s" was not supplied for component %s.', + $expectedArgument->getName(), + $component->getName()->getIdentifier() + ), 1566636254); + } + } + + return ComponentRenderer::renderComponent( + $data, + function () { + return ''; + }, + $renderingContext, + $component->getName()->getIdentifier() + ); + } + + /** + * Renders inline fluid code in a fixture array that will be provided as example data to a component + * + * @param mixed $data + * @param RenderingContextInterface $renderingContext + * @return mixed + */ + public static function renderFluidInExampleData($data, RenderingContextInterface $renderingContext) + { + if (is_string($data)) { + return $renderingContext->getTemplateParser()->parse($data)->render($renderingContext); + } elseif (is_array($data)) { + return array_map(function ($value) use ($renderingContext) { + return self::renderFluidInExampleData($value, $renderingContext); + }, $data); + } else { + return $data; + } + } + + /** + * Wraps component markup in the specified component context (HTML markup) + * The component markup will replace all pipe characters (|) in the context string + * Optionally, a renderingContext and template data can be provided, in which case + * the context markup will be treated as fluid markup + * + * @param string $componentMarkup + * @param string $context + * @param RenderingContextInterface $renderingContext + * @param array $data + * @return string + */ + public static function applyComponentContext( + string $componentMarkup, + string $context, + RenderingContextInterface $renderingContext = null, + array $data = [] + ): string { + // Check if the context should be fetched from a file + $context = self::checkObtainComponentContextFromFile($context); + + if (isset($renderingContext)) { + // Use unique value as component markup marker + $marker = '###COMPONENT_MARKUP_' . mt_rand() . '###'; + $context = str_replace('|', $marker, $context); + + // Parse fluid tags in context string + $originalVariableContainer = $renderingContext->getVariableProvider(); + $renderingContext->setVariableProvider(new StandardVariableProvider($data)); + $context = $renderingContext->getTemplateParser()->parse($context)->render($renderingContext); + $renderingContext->setVariableProvider($originalVariableContainer); + + // Wrap component markup + return str_replace($marker, $componentMarkup, $context); + } else { + return str_replace('|', $componentMarkup, $context); + } + } + + /** + * Checks if the provided component context is a file path and returns its contents; + * falls back to the specified context string. + * + * @param string $context + * @return string + */ + protected static function checkObtainComponentContextFromFile(string $context): string + { + // Probably not a file path + if (strpos($context, '|') !== false) { + return $context; + } + + // Check if the value is a valid file + $path = GeneralUtility::getFileAbsFileName($context); + if (!file_exists($path)) { + return $context; + } + + return file_get_contents($path); + } +} From d83882bbdcaba8b03d407c5f7868129663f682e1 Mon Sep 17 00:00:00 2001 From: Philipp Kitzberger Date: Wed, 1 Sep 2021 14:20:50 +0200 Subject: [PATCH 3/3] Added viewhelper to import data from another fixture --- .../Component/IncludeFixtureViewHelper.php | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 Classes/ViewHelpers/Component/IncludeFixtureViewHelper.php diff --git a/Classes/ViewHelpers/Component/IncludeFixtureViewHelper.php b/Classes/ViewHelpers/Component/IncludeFixtureViewHelper.php new file mode 100644 index 0000000..37bb1d6 --- /dev/null +++ b/Classes/ViewHelpers/Component/IncludeFixtureViewHelper.php @@ -0,0 +1,228 @@ +registerArgument('component', 'string', 'Name of the component that should be rendered', true); + $this->registerArgument('fixtureName', 'string', 'Name of the fixture that the component should be rendered with', false, 'default'); + $this->registerArgument('fixtureData', 'array', 'Additional dynamic fixture data that should be used'); + } + + /** + * Renders fluid example code for the specified component + * + * @param array $arguments + * @param \Closure $renderChildrenClosure + * @param RenderingContextInterface $renderingContext + * @return array + */ + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ): array { + + $componentIdentifier = self::sanitizeComponentIdentifier($arguments['component'] ?? ''); + + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + $componentRepository = $objectManager->get(ComponentRepository::class); + + $component = $componentRepository->findWithFixturesByIdentifier($componentIdentifier); + if (!$component) { + return sprintf('Component %s not found', $componentIdentifier); + } + + if (!isset($arguments['fixtureName']) && !isset($arguments['fixtureData'])) { + throw new \InvalidArgumentException(sprintf( + 'A fixture name or fixture data has to be specified to render the component %s.', + $arguments['component'] + ), 1566377563); + } + + $fixtureData = $arguments['fixtureData'] ?? []; + + $fixtureName = self::sanitizeFixtureName($arguments['fixtureName'] ?? 'default'); + + if (isset($fixtureName)) { + $componentFixture = $component->getFixture($fixtureName); + if (!$componentFixture) { + throw new \InvalidArgumentException(sprintf( + 'Invalid fixture name "%s" specified for component %s.', + $fixtureName, + $componentIdentifier + ), 1566377564); + } + + // Merge static fixture data with manually edited data + $fixtureData = array_replace($componentFixture->getData(), $fixtureData); + } + + $renderingContext->getViewHelperResolver()->addNamespace('fsv', 'Sitegeist\FluidStyleguide\ViewHelpers'); + + // Parse fluid code in fixtures + $fixtureData = self::renderFluidInExampleData($fixtureData, $renderingContext); + + return $fixtureData; + } + + /** + * Make sure that the component identifier doesn't include any malicious characters + * + * @param string $componentIdentifier + * @return string + */ + protected static function sanitizeComponentIdentifier(string $componentIdentifier): string + { + return trim(preg_replace('#[^a-z0-9_\\\\]#i', '', $componentIdentifier), '\\'); + } + + /** + * Make sure that the fixture name doesn't include any malicious characters + * + * @param string $fixtureName + * @return string + */ + protected static function sanitizeFixtureName(string $fixtureName): string + { + return preg_replace('#[^a-z0-9_]#i', '', $fixtureName); + } + + /** + * Calls a component with the supplied example data + * + * @param Component $component + * @param array $data + * @param RenderingContextInterface $renderingContext + * @return string + */ + public static function renderComponent( + Component $component, + array $data, + RenderingContextInterface $renderingContext + ): string { + // Check if all required arguments were supplied to the component + foreach ($component->getArguments() as $expectedArgument) { + if ($expectedArgument->isRequired() && !isset($data[$expectedArgument->getName()])) { + throw new RequiredComponentArgumentException(sprintf( + 'Required argument "%s" was not supplied for component %s.', + $expectedArgument->getName(), + $component->getName()->getIdentifier() + ), 1566636254); + } + } + + return ComponentRenderer::renderComponent( + $data, + function () { + return ''; + }, + $renderingContext, + $component->getName()->getIdentifier() + ); + } + + /** + * Renders inline fluid code in a fixture array that will be provided as example data to a component + * + * @param mixed $data + * @param RenderingContextInterface $renderingContext + * @return mixed + */ + public static function renderFluidInExampleData($data, RenderingContextInterface $renderingContext) + { + if (is_string($data)) { + return $renderingContext->getTemplateParser()->parse($data)->render($renderingContext); + } elseif (is_array($data)) { + return array_map(function ($value) use ($renderingContext) { + return self::renderFluidInExampleData($value, $renderingContext); + }, $data); + } else { + return $data; + } + } + + /** + * Wraps component markup in the specified component context (HTML markup) + * The component markup will replace all pipe characters (|) in the context string + * Optionally, a renderingContext and template data can be provided, in which case + * the context markup will be treated as fluid markup + * + * @param string $componentMarkup + * @param string $context + * @param RenderingContextInterface $renderingContext + * @param array $data + * @return string + */ + public static function applyComponentContext( + string $componentMarkup, + string $context, + RenderingContextInterface $renderingContext = null, + array $data = [] + ): string { + // Check if the context should be fetched from a file + $context = self::checkObtainComponentContextFromFile($context); + + if (isset($renderingContext)) { + // Use unique value as component markup marker + $marker = '###COMPONENT_MARKUP_' . mt_rand() . '###'; + $context = str_replace('|', $marker, $context); + + // Parse fluid tags in context string + $originalVariableContainer = $renderingContext->getVariableProvider(); + $renderingContext->setVariableProvider(new StandardVariableProvider($data)); + $context = $renderingContext->getTemplateParser()->parse($context)->render($renderingContext); + $renderingContext->setVariableProvider($originalVariableContainer); + + // Wrap component markup + return str_replace($marker, $componentMarkup, $context); + } else { + return str_replace('|', $componentMarkup, $context); + } + } + + /** + * Checks if the provided component context is a file path and returns its contents; + * falls back to the specified context string. + * + * @param string $context + * @return string + */ + protected static function checkObtainComponentContextFromFile(string $context): string + { + // Probably not a file path + if (strpos($context, '|') !== false) { + return $context; + } + + // Check if the value is a valid file + $path = GeneralUtility::getFileAbsFileName($context); + if (!file_exists($path)) { + return $context; + } + + return file_get_contents($path); + } +}