diff --git a/Classes/Controller/ApiController.php b/Classes/Controller/ApiController.php index 3815800d..c20a6c15 100644 --- a/Classes/Controller/ApiController.php +++ b/Classes/Controller/ApiController.php @@ -16,12 +16,11 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ActionController; use Neos\Flow\ResourceManagement\ResourceManager; +use Sitegeist\Monocle\Domain\Fusion\PrototypeRepository; use Sitegeist\Monocle\Fusion\FusionService; -use Sitegeist\Monocle\Fusion\FusionView; -use Sitegeist\Monocle\Fusion\ReverseFusionParser; use Sitegeist\Monocle\Service\PackageKeyTrait; -use Symfony\Component\Yaml\Yaml; use Sitegeist\Monocle\Service\ConfigurationService; +use Sitegeist\Monocle\Domain\PrototypeDetails\PrototypeDetailsFactory; /** * Class ApiController @@ -60,6 +59,18 @@ class ApiController extends ActionController */ protected $configurationService; + /** + * @Flow\Inject + * @var PrototypeRepository + */ + protected $prototypeRepository; + + /** + * @Flow\Inject + * @var PrototypeDetailsFactory + */ + protected $prototypeDetailsFactory; + /** * Get all configurations for this site package * @@ -93,30 +104,15 @@ public function configurationAction($sitePackageKey = null) */ public function prototypeDetailsAction($sitePackageKey, $prototypeName) { - $sitePackageKey = $sitePackageKey ?: $this->getDefaultSitePackageKey(); - - $prototypePreviewRenderPath = FusionService::RENDERPATH_DISCRIMINATOR . str_replace(['.', ':'], ['_', '__'], $prototypeName); - - // render html - $fusionView = new FusionView(); - $fusionView->setControllerContext($this->getControllerContext()); - $fusionView->setFusionPath($prototypePreviewRenderPath); - $fusionView->setPackageKey($sitePackageKey); - - // render fusion source - $fusionObjectTree = $this->fusionService->getMergedFusionObjectTreeForSitePackage($sitePackageKey); - $fusionAst = $fusionObjectTree['__prototypes'][$prototypeName]; - $fusionCode = ReverseFusionParser::restorePrototypeCode($prototypeName, $fusionAst); - - $result = [ - 'prototypeName' => $prototypeName, - 'renderedCode' => $fusionCode, - 'parsedCode' => Yaml::dump($fusionAst, 99), - 'fusionAst' => $fusionAst, - 'anatomy' => $this->fusionService->getAnatomicalPrototypeTreeFromAstExcerpt($fusionAst) - ]; - - $this->view->assign('value', $result); + $prototype = $this->prototypeRepository + ->findOneByPrototypeNameInSitePackage( + $prototypeName, + $sitePackageKey + ); + $prototypeDetails = $this->prototypeDetailsFactory + ->forPrototype($prototype); + + $this->view->assign('value', $prototypeDetails); } /** diff --git a/Classes/Domain/Fusion/Prototype.php b/Classes/Domain/Fusion/Prototype.php new file mode 100644 index 00000000..b3b657dd --- /dev/null +++ b/Classes/Domain/Fusion/Prototype.php @@ -0,0 +1,141 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Neos\Utility\Arrays; +use Neos\Fusion\Core\Runtime as FusionRuntime; + +/** + * @Flow\Proxy(false) + */ +final class Prototype +{ + /** + * @var PrototypeName + */ + private $name; + + /** + * @var array + */ + private $ast; + + /** + * @var FusionRuntime + */ + private $runtime; + + /** + * @param PrototypeName $name + * @param array $ast + * @param FusionRuntime $runtime + */ + public function __construct( + PrototypeName $name, + array $ast, + FusionRuntime $runtime + ) { + $this->name = $name; + $this->ast = $ast; + $this->runtime = $runtime; + } + + /** + * @return PrototypeName + */ + public function getName(): PrototypeName + { + return $this->name; + } + + /** + * @return array + */ + public function getAst(): array + { + return $this->ast; + } + + /** + * @return boolean + */ + public function isComponent(): bool + { + return $this->extends( + PrototypeName::fromString('Neos.Fusion:Component') + ); + } + + /** + * @param PrototypeName $ancestorPrototypeName + * @return boolean + */ + public function extends(PrototypeName $ancestorPrototypeName): bool + { + if (isset($this->ast['__prototypeChain'])) { + return in_array( + (string) $ancestorPrototypeName, + $this->ast['__prototypeChain'] + ); + } + + return false; + } + + /** + * @param null|string $path + * @return array|string[] + */ + public function getKeys(?string $path = null): array + { + if ($path !== null) { + $ast = Arrays::getValueByPath($this->ast, $path); + if (!is_array($ast)) { + $ast = []; + } + } else { + $ast = $this->ast; + } + + return array_filter(array_keys($ast), function (string $key): bool { + return substr($key, 0, 2) !== '__'; + }); + } + + /** + * @param string $path + * @param array $context + * @return mixed + */ + public function evaluate(string $path, array $context = []) + { + if ($path[0] !== '/') { + throw new \InvalidArgumentException( + '$path must start with "/".' + ); + } + + $currentContext = $this->runtime->getCurrentContext(); + $this->runtime->pushContextArray(array_merge($currentContext ?: [], $context)); + + $result = $this->runtime->evaluate( + sprintf('/<%s>%s', $this->name, $path) + ); + + $this->runtime->popContext(); + + return $result; + } +} diff --git a/Classes/Domain/Fusion/PrototypeName.php b/Classes/Domain/Fusion/PrototypeName.php new file mode 100644 index 00000000..c207bb29 --- /dev/null +++ b/Classes/Domain/Fusion/PrototypeName.php @@ -0,0 +1,60 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class PrototypeName implements \JsonSerializable +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + private function __construct(string $value) + { + $this->value = $value; + } + + /** + * @param string $string + * @return self + */ + public static function fromString(string $string): self + { + return new self($string); + } + + /** + * @return string + */ + public function jsonSerialize() + { + return $this->value; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->value; + } +} diff --git a/Classes/Domain/Fusion/PrototypeRepository.php b/Classes/Domain/Fusion/PrototypeRepository.php new file mode 100644 index 00000000..afcaa261 --- /dev/null +++ b/Classes/Domain/Fusion/PrototypeRepository.php @@ -0,0 +1,57 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Neos\Fusion\Core\Runtime as FusionRuntime; +use Neos\Fusion\Core\RuntimeFactory as FusionRuntimeFactory; +use Sitegeist\Monocle\Fusion\FusionService; + +/** + * @Flow\Scope("singleton") + */ +final class PrototypeRepository +{ + /** + * @Flow\Inject + * @var FusionRuntimeFactory + */ + protected $fusionRuntimeFactory; + + /** + * @Flow\Inject + * @var FusionService + */ + protected $fusionService; + + public function findOneByPrototypeNameInSitePackage( + string $prototypeName, + string $sitePackageKey + ): ?Prototype { + $fusionObjectTree = $this->fusionService->getMergedFusionObjectTreeForSitePackage($sitePackageKey); + + if (isset($fusionObjectTree['__prototypes'][$prototypeName])) { + $fusionAst = $fusionObjectTree['__prototypes'][$prototypeName]; + $fusionRuntime = $this->fusionRuntimeFactory->create($fusionObjectTree); + + return new Prototype( + PrototypeName::fromString($prototypeName), + $fusionAst, + $fusionRuntime + ); + } + + return null; + } +} diff --git a/Classes/Domain/PrototypeDetails/Anatomy.php b/Classes/Domain/PrototypeDetails/Anatomy.php new file mode 100644 index 00000000..7107da27 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Anatomy.php @@ -0,0 +1,72 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\PrototypeName; + +/** + * @Flow\Proxy(false) + */ +final class Anatomy implements \JsonSerializable +{ + /** + * @var PrototypeName + */ + private $prototypeName; + + /** + * @var array|AnatomyInterface[] + */ + private $children; + + /** + * @param PrototypeName $prototypeName + * @param array $children + */ + public function __construct( + PrototypeName $prototypeName, + array $children + ) { + $this->prototypeName = $prototypeName; + $this->children = $children; + } + + /** + * @return PrototypeName + */ + public function getPrototypeName(): PrototypeName + { + return $this->prototypeName; + } + + /** + * @return array|AnatomyInterface[] + */ + public function getChildren(): array + { + return $this->children; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return [ + 'prototypeName' => $this->prototypeName, + 'children' => $this->children + ]; + } +} diff --git a/Classes/Domain/PrototypeDetails/AnatomyFactory.php b/Classes/Domain/PrototypeDetails/AnatomyFactory.php new file mode 100644 index 00000000..646efaa8 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/AnatomyFactory.php @@ -0,0 +1,67 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; + +/** + * @Flow\Scope("singleton") + */ +final class AnatomyFactory +{ + /** + * @param Prototype $prototype + * @return Anatomy + */ + public function fromPrototypeForPrototypeDetails( + Prototype $prototype + ): Anatomy { + $children = []; + $referencedFusionObjects = FusionPrototypeAst::fromArray( + $prototype->getAst() + )->getAllReferencedFusionObjects(); + + foreach ($referencedFusionObjects as $fusionObjectAst) { + $children[] = $this->fromFusionObjectAstForPrototypeDetails( + $fusionObjectAst + ); + } + + return new Anatomy($prototype->getName(), $children); + } + + /** + * @param FusionObjectAst $fusionObjectAst + * @return Anatomy + */ + public function fromFusionObjectAstForPrototypeDetails( + FusionObjectAst $fusionObjectAst + ): Anatomy { + $children = []; + $referencedFusionObjects = $fusionObjectAst + ->getAllReferencedFusionObjects(); + + foreach ($referencedFusionObjects as $childFusionObjectAst) { + $children[] = $this->fromFusionObjectAstForPrototypeDetails( + $childFusionObjectAst + ); + } + + return new Anatomy( + $fusionObjectAst->getPrototypeName(), + $children + ); + } +} diff --git a/Classes/Domain/PrototypeDetails/FusionObjectAst.php b/Classes/Domain/PrototypeDetails/FusionObjectAst.php new file mode 100644 index 00000000..b031f887 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/FusionObjectAst.php @@ -0,0 +1,83 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\PrototypeName; + +/** + * @Flow\Proxy(false) + */ +final class FusionObjectAst +{ + /** + * @var array + */ + private $value; + + /** + * @param array $value + */ + private function __construct(array $value) + { + if (!isset($value['__objectType'])) { + throw new \UnexpectedValueException('__objectType key must be set.'); + } + + $this->value = $value; + } + + public function getPrototypeName(): PrototypeName + { + return PrototypeName::fromString($this->value['__objectType']); + } + + /** + * @return \Iterator + */ + public function getAllReferencedFusionObjects(): \Iterator + { + $findReferencedFusionPrototypeAstsRecursively = function ( + array $astExcerpt + ) use (&$findReferencedFusionPrototypeAstsRecursively): \Iterator { + if (isset($astExcerpt['__objectType'])) { + yield self::fromArray($astExcerpt); + } else { + foreach ($astExcerpt as $key => $value) { + if (substr($key, 0, 2) === '__') { + continue; + } + + if (is_array($value)) { + yield from $findReferencedFusionPrototypeAstsRecursively($value); + } + } + } + }; + + $value = $this->value; + unset($value['__objectType']); + + yield from $findReferencedFusionPrototypeAstsRecursively($value); + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self($array); + } +} diff --git a/Classes/Domain/PrototypeDetails/FusionPrototypeAst.php b/Classes/Domain/PrototypeDetails/FusionPrototypeAst.php new file mode 100644 index 00000000..1b5ff35d --- /dev/null +++ b/Classes/Domain/PrototypeDetails/FusionPrototypeAst.php @@ -0,0 +1,82 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class FusionPrototypeAst implements \JsonSerializable +{ + /** + * @var array + */ + private $value; + + /** + * @param array $value + */ + private function __construct(array $value) + { + if (!isset($value['__prototypeObjectName'])) { + throw new \UnexpectedValueException('__prototypeObjectName key must be set.'); + } + + $this->value = $value; + } + + /** + * @return \Iterator + */ + public function getAllReferencedFusionObjects(): \Iterator + { + $findReferencedFusionPrototypeAstsRecursively = function ( + array $astExcerpt + ) use (&$findReferencedFusionPrototypeAstsRecursively): \Iterator { + if (isset($astExcerpt['__objectType'])) { + yield FusionObjectAst::fromArray($astExcerpt); + } else { + foreach ($astExcerpt as $key => $value) { + if (substr($key, 0, 2) === '__') { + continue; + } + + if (is_array($value)) { + yield from $findReferencedFusionPrototypeAstsRecursively($value); + } + } + } + }; + + yield from $findReferencedFusionPrototypeAstsRecursively($this->value); + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self($array); + } + + /** + * @return array + */ + public function jsonSerialize() + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/ParsedCode.php b/Classes/Domain/PrototypeDetails/ParsedCode.php new file mode 100644 index 00000000..7ab6afc9 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/ParsedCode.php @@ -0,0 +1,52 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class ParsedCode implements \JsonSerializable +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + private function __construct(string $value) + { + $this->value = $value; + } + + /** + * @param string $string + * @return self + */ + public static function fromString(string $string): self + { + return new self($string); + } + + /** + * @return string + */ + public function jsonSerialize() + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/PropSets/PropSet.php b/Classes/Domain/PrototypeDetails/PropSets/PropSet.php new file mode 100644 index 00000000..9565c83b --- /dev/null +++ b/Classes/Domain/PrototypeDetails/PropSets/PropSet.php @@ -0,0 +1,55 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class PropSet implements \JsonSerializable +{ + /** + * @var PropSetName + */ + private $name; + + /** + * @var array + */ + private $overrides; + + /** + * @param PropSetName $name + * @param array $overrides + */ + public function __construct( + PropSetName $name, + array $overrides + ) { + $this->name = $name; + $this->overrides = $overrides; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return [ + 'name' => $this->name, + 'overrides' => $this->overrides + ]; + } +} diff --git a/Classes/Domain/PrototypeDetails/PropSets/PropSetCollection.php b/Classes/Domain/PrototypeDetails/PropSets/PropSetCollection.php new file mode 100644 index 00000000..3b8559f8 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/PropSets/PropSetCollection.php @@ -0,0 +1,83 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; +use Sitegeist\Monocle\Domain\PrototypeDetails\Props\PropValue; + +/** + * @Flow\Proxy(false) + */ +final class PropSetCollection implements \JsonSerializable +{ + /** + * @var array|PropSet[] + */ + private $propSets; + + /** + * @param PropSet ...$propSets + */ + private function __construct(PropSet ...$propSets) + { + $this->propSets = $propSets; + } + + /** + * @param Prototype $prototype + * @return self + */ + public static function fromPrototype(Prototype $prototype): self + { + $ast = $prototype->getAst(); + $propSets = []; + + if (isset($ast['__meta']['styleguide']['propSets'])) { + $propSetNames = array_keys( + $ast['__meta']['styleguide']['propSets'] + ); + + foreach ($propSetNames as $propSetNameAsString) { + $propSetName = PropSetName::fromString($propSetNameAsString); + $values = $prototype->evaluate(sprintf( + '/__meta/styleguide/propSets/%s', + $propSetName + )); + $overrides = []; + + if (is_array($values)) { + foreach ($values as $name => $value) { + if (PropValue::isValid($value)) { + $overrides[(string) $name] = + PropValue::fromAny($value); + } + } + } + + $propSets[] = new PropSet($propSetName, $overrides); + } + } + + return new self(...$propSets); + } + + /** + * @return array|PropSet[] + */ + public function jsonSerialize() + { + return $this->propSets; + } +} diff --git a/Classes/Domain/PrototypeDetails/PropSets/PropSetName.php b/Classes/Domain/PrototypeDetails/PropSets/PropSetName.php new file mode 100644 index 00000000..1433ed44 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/PropSets/PropSetName.php @@ -0,0 +1,60 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class PropSetName implements \JsonSerializable +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + private function __construct(string $value) + { + $this->value = $value; + } + + /** + * @param string $string + * @return self + */ + public static function fromString(string $string): self + { + return new self($string); + } + + /** + * @return string + */ + public function jsonSerialize() + { + return $this->value; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/Editor.php b/Classes/Domain/PrototypeDetails/Props/Editor.php new file mode 100644 index 00000000..754806dc --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/Editor.php @@ -0,0 +1,71 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class Editor implements EditorInterface +{ + /** + * @var EditorIdentifier + */ + private $identifier; + + /** + * @var EditorOptions + */ + private $options; + + /** + * @param EditorIdentifier $identifier + * @param EditorOptions $options + */ + public function __construct( + EditorIdentifier $identifier, + EditorOptions $options + ) { + $this->identifier = $identifier; + $this->options = $options; + } + + /** + * @return EditorIdentifier + */ + public function getIdentifier(): EditorIdentifier + { + return $this->identifier; + } + + /** + * @return EditorOptions + */ + public function getOptions(): EditorOptions + { + return $this->options; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return [ + 'identifier' => $this->identifier, + 'options' => $this->options + ]; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/EditorFactory.php b/Classes/Domain/PrototypeDetails/Props/EditorFactory.php new file mode 100644 index 00000000..4d3efcb7 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/EditorFactory.php @@ -0,0 +1,245 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; + +/** + * @Flow\Proxy(false) + */ +final class EditorFactory +{ + /** + * Get an editor that fits the given prop of the given prototype + * + * @param Prototype $prototype + * @param PropName $propName + * @return EditorInterface|null + */ + public function for( + Prototype $prototype, + PropName $propName + ): ?EditorInterface { + if ($manualConfiguration = $prototype->evaluate( + sprintf( + '/__meta/styleguide/options/propEditors/%s', + $propName + ) + )) { + try { + return $this->fromManualConfiguration($manualConfiguration); + } catch (\UnexpectedValueException $e) { + throw new \DomainException( + sprintf( + 'Invalid editor configuration for prop "%s" of "%s": %s', + $propName, + $prototype->getName(), + $e->getMessage() + ) + ); + } + } + + if ($propValue = PropValue::of($prototype, $propName)) { + return $this->forPropValue($propValue); + } + + return null; + } + + /** + * Provides a fitting editor for any given prop value + * + * @param PropValue $propValue + * @return null|EditorInterface + */ + public function forPropValue(PropValue $propValue): ?EditorInterface + { + switch (true) { + case $propValue->isBoolean(): + return $this->checkBox(); + case $propValue->isNumber(): + return $this->text(); + case $propValue->isString(): + if ($propValue->getLength() <= 80) { + return $this->text(); + } else { + return $this->textArea(); + } + break; + default: + return null; + } + } + + /** + * @param array $manualConfiguration + * @return EditorInterface + */ + public function fromManualConfiguration(array $manualConfiguration): EditorInterface + { + if (!isset($manualConfiguration['editor'])) { + throw new \UnexpectedValueException( + 'Path "editor" must be defined.' + ); + } + + if (!is_string($manualConfiguration['editor'])) { + throw new \UnexpectedValueException( + 'Path "editor" must evaluate to a string.' + ); + } + + if (!isset($manualConfiguration['editorOptions'])) { + $manualConfiguration['editorOptions'] = []; + } + + if (!is_array($manualConfiguration['editorOptions'])) { + throw new \UnexpectedValueException( + 'Path "editorOptions" must evaluate to an array.' + ); + } + + switch ($manualConfiguration['editor']) { + case 'Sitegeist.Monocle/Props/Editors/Checkbox': + return $this->checkbox(); + case 'Sitegeist.Monocle/Props/Editors/Text': + return $this->text(); + case 'Sitegeist.Monocle/Props/Editors/TextArea': + return $this->textArea(); + case 'Sitegeist.Monocle/Props/Editors/SelectBox': + return $this->selectBox($manualConfiguration['editorOptions']); + default: + throw new \UnexpectedValueException( + sprintf( + 'Unknown editor "%s".', + $manualConfiguration['editor'] + ) + ); + } + } + + /** + * Provides a CheckBox editor + * + * @return EditorInterface + */ + public function checkbox(): EditorInterface + { + return new Editor( + EditorIdentifier::fromString( + 'Sitegeist.Monocle/Props/Editors/Checkbox' + ), + EditorOptions::empty() + ); + } + + /** + * Provides a Text editor + * + * @return EditorInterface + */ + public function text(): EditorInterface + { + return new Editor( + EditorIdentifier::fromString( + 'Sitegeist.Monocle/Props/Editors/Text' + ), + EditorOptions::empty() + ); + } + + /** + * Provides a TextArea editor + * + * @return EditorInterface + */ + public function textArea(): EditorInterface + { + return new Editor( + EditorIdentifier::fromString( + 'Sitegeist.Monocle/Props/Editors/TextArea' + ), + EditorOptions::empty() + ); + } + + /** + * Provides a SelectBox Editor + * + * @param array $options + * @return EditorInterface + */ + public function selectBox(array $options): EditorInterface + { + if (!isset($options['options'])) { + $options['options'] = []; + } + + if (!is_array($options['options'])) { + throw new \UnexpectedValueException( + sprintf( + 'SelectBox options must be an array. Got "%" instead.', + gettype($options['options']) + ) + ); + } else { + $options['options'] = array_values($options['options']); + } + + foreach ($options['options'] as $option) { + if (!isset($option['label'])) { + throw new \UnexpectedValueException( + 'All SelectBox options must have a label.' + ); + } + + if (!is_string($option['label'])) { + throw new \UnexpectedValueException( + sprintf( + 'All SelectBox option labels must be of type string. Got "%s" instead.', + gettype($option['label']) + ) + ); + } + + if (!isset($option['value'])) { + throw new \UnexpectedValueException( + sprintf( + 'All SelectBox options must have a value. Found option "%s" without one.', + $option['label'] + ) + ); + } + + if (!is_string($option['value']) && !is_int($option['value']) && !is_float($option['value'])) { + throw new \UnexpectedValueException( + sprintf( + 'All SelectBox option labels must be either of type string, integer or float. Got "%s" for option "%s" instead.', + gettype($option['value']), + $option['label'] + ) + ); + } + } + + return new Editor( + EditorIdentifier::fromString( + 'Sitegeist.Monocle/Props/Editors/SelectBox' + ), + EditorOptions::fromArray($options) + ); + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/EditorIdentifier.php b/Classes/Domain/PrototypeDetails/Props/EditorIdentifier.php new file mode 100644 index 00000000..1dd85d54 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/EditorIdentifier.php @@ -0,0 +1,52 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class EditorIdentifier implements \JsonSerializable +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + private function __construct(string $value) + { + $this->value = $value; + } + + /** + * @param string $string + * @return self + */ + public static function fromString(string $string): self + { + return new self($string); + } + + /** + * @return string + */ + public function jsonSerialize() + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/EditorInterface.php b/Classes/Domain/PrototypeDetails/Props/EditorInterface.php new file mode 100644 index 00000000..b5085d79 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/EditorInterface.php @@ -0,0 +1,27 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +interface EditorInterface extends \JsonSerializable +{ + /** + * @return EditorIdentifier + */ + public function getIdentifier(): EditorIdentifier; + + /** + * @return EditorOptions + */ + public function getOptions(): EditorOptions; +} diff --git a/Classes/Domain/PrototypeDetails/Props/EditorOptions.php b/Classes/Domain/PrototypeDetails/Props/EditorOptions.php new file mode 100644 index 00000000..bb37546f --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/EditorOptions.php @@ -0,0 +1,83 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use UnexpectedValueException; + +/** + * @Flow\Proxy(false) + */ +final class EditorOptions implements \JsonSerializable +{ + /** + * @var array + */ + private $value; + + /** + * @param array $value + */ + private function __construct(array $value) + { + $this->value = $value; + } + + /** + * @return self + */ + public static function empty(): self + { + return new self([]); + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self($array); + } + + /** + * @param \JsonSerializable $jsonSerializable + * @return self + */ + public static function fromJsonSerializable( + \JsonSerializable $jsonSerializable + ): self { + $jsonSerializeResult = $jsonSerializable->jsonSerialize(); + + if (!is_array($jsonSerializeResult)) { + throw new UnexpectedValueException( + sprintf( + '$jsonSerializable->jsonSerialize() must return an ' . + 'array. Got "%" instead.', + gettype($jsonSerializeResult) + ) + ); + } + + return new self($jsonSerializeResult); + } + + /** + * @return array + */ + public function jsonSerialize() + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/Prop.php b/Classes/Domain/PrototypeDetails/Props/Prop.php new file mode 100644 index 00000000..ae9aedd9 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/Prop.php @@ -0,0 +1,85 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class Prop implements PropInterface +{ + /** + * @var PropName + */ + private $name; + + /** + * @var PropValue + */ + private $value; + + /** + * @var EditorInterface + */ + private $editor; + + /** + * @param PropName $name + * @param PropValue $value + * @param EditorInterface $editor + */ + public function __construct( + PropName $name, + PropValue $value, + EditorInterface $editor + ) { + $this->name = $name; + $this->value = $value; + $this->editor = $editor; + } + + /** + * @return PropName + */ + public function getName(): PropName + { + return $this->name; + } + + /** + * @return PropValue + */ + public function getValue(): PropValue + { + return $this->value; + } + + /** + * @return EditorInterface + */ + public function getEditor(): EditorInterface + { + return $this->editor; + } + + public function jsonSerialize() + { + return [ + 'name' => $this->name, + 'value' => $this->value, + 'editor' => $this->editor + ]; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropInterface.php b/Classes/Domain/PrototypeDetails/Props/PropInterface.php new file mode 100644 index 00000000..333a358b --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropInterface.php @@ -0,0 +1,32 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +interface PropInterface extends \JsonSerializable +{ + /** + * @return PropName + */ + public function getName(): PropName; + + /** + * @return PropValue + */ + public function getValue(): PropValue; + + /** + * @return EditorInterface + */ + public function getEditor(): EditorInterface; +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropName.php b/Classes/Domain/PrototypeDetails/Props/PropName.php new file mode 100644 index 00000000..67d502d5 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropName.php @@ -0,0 +1,85 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; + +/** + * @Flow\Proxy(false) + */ +final class PropName implements \JsonSerializable +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + private function __construct(string $value) + { + $this->value = $value; + } + + /** + * @param Prototype $prototype + * @return array|PropName[] + */ + public static function fromPrototype(Prototype $prototype): array + { + $propNames = $prototype->getKeys('__meta.styleguide.props'); + if ($prototype->isComponent()) { + $propNames = array_unique( + array_merge( + array_filter( + $prototype->getKeys(), + function (string $key): bool { + return $key !== 'renderer'; + } + ), + $propNames + ) + ); + } + + return array_map([self::class, 'fromString'], $propNames); + } + + /** + * @param string $string + * @return self + */ + public static function fromString(string $string): self + { + return new self($string); + } + + /** + * @return string + */ + public function jsonSerialize() + { + return $this->value; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropValue.php b/Classes/Domain/PrototypeDetails/Props/PropValue.php new file mode 100644 index 00000000..88afdcd4 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropValue.php @@ -0,0 +1,186 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; +use Sitegeist\Monocle\Domain\Fusion\PrototypeName; + +/** + * @Flow\Proxy(false) + */ +final class PropValue implements \JsonSerializable +{ + /** + * @var mixed + */ + private $value; + + /** + * @param mixed $value + */ + private function __construct($value) + { + if (!self::isValid($value)) { + throw new \UnexpectedValueException( + 'PropValue must be a primitive, an array or an object ' . + 'of type \\stdClass. Got "%s" instead.', + is_object($value) ? get_class($value) : gettype($value) + ); + } + + $this->value = $value; + } + + /** + * @param Prototype $prototype + * @param PropName $propName + * @return null|self + */ + public static function of(Prototype $prototype, PropName $propName): ?self + { + $value = $prototype + ->evaluate('/__meta/styleguide/props/' . $propName); + + if ($prototype->extends(PrototypeName::fromString('Neos.Fusion:Component'))) { + $value = $value ?? $prototype->evaluate('/' . $propName); + } + + if (PropValue::isValid($value)) { + return PropValue::fromAny($value); + } + + return null; + } + + /** + * @param mixed $value + * @return self + */ + public static function fromAny($any): self + { + return new self($any); + } + + /** + * @param mixed $value + * @return boolean + */ + public static function isValid($value): bool + { + if (is_array($value)) { + foreach ($value as $childValue) { + if (!self::isValid($childValue)) { + return false; + } + } + + return true; + } + + return ( + is_bool($value) + || is_int($value) + || is_float($value) + || is_string($value) + || $value instanceof \stdClass + ); + } + + /** + * @return integer + */ + public function getLength(): int + { + switch (true) { + case $this->isString(): + return strlen($this->value); + default: + throw new \DomainException( + sprintf( + 'PropValue of type "%s" has no length.', + gettype($this->value) + ) + ); + } + } + + /** + * @return boolean + */ + public function isBoolean(): bool + { + return is_bool($this->value); + } + + /** + * @return boolean + */ + public function isNumber(): bool + { + return is_int($this->value) || is_float($this->value); + } + + /** + * @return boolean + */ + public function isString(): bool + { + return is_string($this->value); + } + + /** + * @return boolean + */ + public function isList(): bool + { + if (is_array($this->value)) { + foreach ($this->value as $key => $value) { + if ($key === 0) { + return true; + } + } + } + + return false; + } + + /** + * @return boolean + */ + public function isDictionary(): bool + { + if ($this->value instanceof \stdClass) { + return true; + } + + if (is_array($this->value)) { + foreach ($this->value as $key => $value) { + if ($key !== 0) { + return true; + } + } + } + + return false; + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + return $this->value; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropsCollection.php b/Classes/Domain/PrototypeDetails/Props/PropsCollection.php new file mode 100644 index 00000000..9f3dee49 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropsCollection.php @@ -0,0 +1,51 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class PropsCollection implements PropsCollectionInterface +{ + /** + * @var array|PropInterface[] + */ + private $props; + + /** + * @param PropInterface ...$props + */ + public function __construct(PropInterface ...$props) + { + $this->props = $props; + } + + /** + * @return iterable + */ + public function getProps(): iterable + { + return $this->props; + } + + /** + * @return array|PropInterface[] + */ + public function jsonSerialize() + { + return $this->props; + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropsCollectionBuilder.php b/Classes/Domain/PrototypeDetails/Props/PropsCollectionBuilder.php new file mode 100644 index 00000000..6fe95620 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropsCollectionBuilder.php @@ -0,0 +1,56 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy("false") + */ +final class PropsCollectionBuilder +{ + /** + * @var array + */ + private $props = []; + + /** + * @param PropInterface $prop + * @return self + */ + public function addProp(PropInterface $prop): self + { + if (isset($this->props[(string) $prop->getName()])) { + throw new \DomainException( + sprintf( + 'Tried to add duplicate prop "%s". Props must be unique ' . + 'within a PropCollection.', + $prop->getName() + ) + ); + } + + $this->props[(string) $prop->getName()] = $prop; + + return $this; + } + + /** + * @return PropsCollection + */ + public function build(): PropsCollection + { + return new PropsCollection(...array_values($this->props)); + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropsCollectionFactory.php b/Classes/Domain/PrototypeDetails/Props/PropsCollectionFactory.php new file mode 100644 index 00000000..40a86702 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropsCollectionFactory.php @@ -0,0 +1,52 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; +use Sitegeist\Monocle\Domain\Fusion\PrototypeName; + +/** + * @Flow\Scope("singleton") + */ +final class PropsCollectionFactory implements PropsCollectionFactoryInterface +{ + /** + * @Flow\Inject + * @var EditorFactory + */ + protected $editorFactory; + + /** + * @param Prototype $fusionPrototypeAst + * @return PropsCollectionInterface + */ + public function fromPrototypeForPrototypeDetails( + Prototype $prototype + ): PropsCollectionInterface { + $propsCollectionBuilder = new PropsCollectionBuilder(); + + foreach (PropName::fromPrototype($prototype) as $propName) { + if ($propValue = PropValue::of($prototype, $propName)) { + if ($editor = $this->editorFactory->for($prototype, $propName)) { + $propsCollectionBuilder->addProp( + new Prop($propName, $propValue, $editor) + ); + } + } + } + + return $propsCollectionBuilder->build(); + } +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropsCollectionFactoryInterface.php b/Classes/Domain/PrototypeDetails/Props/PropsCollectionFactoryInterface.php new file mode 100644 index 00000000..225d2981 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropsCollectionFactoryInterface.php @@ -0,0 +1,27 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Sitegeist\Monocle\Domain\Fusion\Prototype; + +interface PropsCollectionFactoryInterface +{ + /** + * @param Prototype $prototype + * @return PropsCollectionInterface + */ + public function fromPrototypeForPrototypeDetails( + Prototype $prototype + ): PropsCollectionInterface; +} diff --git a/Classes/Domain/PrototypeDetails/Props/PropsCollectionInterface.php b/Classes/Domain/PrototypeDetails/Props/PropsCollectionInterface.php new file mode 100644 index 00000000..19405b8f --- /dev/null +++ b/Classes/Domain/PrototypeDetails/Props/PropsCollectionInterface.php @@ -0,0 +1,22 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +interface PropsCollectionInterface extends \JsonSerializable +{ + /** + * @return iterable + */ + public function getProps(): iterable; +} diff --git a/Classes/Domain/PrototypeDetails/PrototypeDetails.php b/Classes/Domain/PrototypeDetails/PrototypeDetails.php new file mode 100644 index 00000000..c07f3e45 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/PrototypeDetails.php @@ -0,0 +1,159 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\PrototypeName; +use Sitegeist\Monocle\Domain\PrototypeDetails\Props\PropsCollectionInterface; +use Sitegeist\Monocle\Domain\PrototypeDetails\PropSets\PropSetCollection; + +/** + * @Flow\Proxy(false) + */ +final class PrototypeDetails implements PrototypeDetailsInterface +{ + /** + * @var PrototypeName + */ + private $prototypeName; + + /** + * @var RenderedCode + */ + private $renderedCode; + + /** + * @var ParsedCode + */ + private $parsedCode; + + /** + * @var FusionPrototypeAst + */ + private $fusionAst; + + /** + * @var Anatomy + */ + private $anatomy; + + /** + * @var PropsCollectionInterface + */ + private $props; + + /** + * @var PropSetCollection + */ + private $propSets; + + /** + * @param PrototypeName $prototypeName + * @param RenderedCode $renderedCode + * @param ParsedCode $parsedCode + * @param FusionPrototypeAst $fusionAst + * @param Anatomy $anatomy + * @param PropsCollectionInterface $props + * @param PropSetCollection $propSets + */ + public function __construct( + PrototypeName $prototypeName, + RenderedCode $renderedCode, + ParsedCode $parsedCode, + FusionPrototypeAst $fusionAst, + Anatomy $anatomy, + PropsCollectionInterface $props, + PropSetCollection $propSets + ) { + $this->prototypeName = $prototypeName; + $this->renderedCode = $renderedCode; + $this->parsedCode = $parsedCode; + $this->fusionAst = $fusionAst; + $this->anatomy = $anatomy; + $this->props = $props; + $this->propSets = $propSets; + } + + /** + * @return PrototypeName + */ + public function getPrototypeName(): PrototypeName + { + return $this->prototypeName; + } + + /** + * @return RenderedCode + */ + public function getRenderedCode(): RenderedCode + { + return $this->renderedCode; + } + + /** + * @return ParsedCode + */ + public function getParsedCode(): ParsedCode + { + return $this->parsedCode; + } + + /** + * @return FusionPrototypeAst + */ + public function getFusionAst(): FusionPrototypeAst + { + return $this->fusionAst; + } + + /** + * @return Anatomy + */ + public function getAnatomy(): Anatomy + { + return $this->anatomy; + } + + /** + * @return PropsCollectionInterface + */ + public function getProps(): PropsCollectionInterface + { + return $this->props; + } + + /** + * @return PropSetCollection + */ + public function getPropSets(): PropSetCollection + { + return $this->propSets; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return [ + 'prototypeName' => $this->prototypeName, + 'renderedCode' => $this->renderedCode, + 'parsedCode' => $this->parsedCode, + 'fusionAst' => $this->fusionAst, + 'anatomy' => $this->anatomy, + 'props' => $this->props, + 'propSets' => $this->propSets + ]; + } +} diff --git a/Classes/Domain/PrototypeDetails/PrototypeDetailsFactory.php b/Classes/Domain/PrototypeDetails/PrototypeDetailsFactory.php new file mode 100644 index 00000000..2a6c0bdd --- /dev/null +++ b/Classes/Domain/PrototypeDetails/PrototypeDetailsFactory.php @@ -0,0 +1,66 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\Prototype; +use Sitegeist\Monocle\Domain\PrototypeDetails\Props\PropsCollectionFactoryInterface; +use Sitegeist\Monocle\Domain\PrototypeDetails\PropSets\PropSetCollection; +use Sitegeist\Monocle\Fusion\ReverseFusionParser; +use Symfony\Component\Yaml\Yaml; + +/** + * @Flow\Scope("singleton") + */ +final class PrototypeDetailsFactory +{ + /** + * @Flow\Inject + * @var AnatomyFactory + */ + protected $anatomyFactory; + + /** + * @Flow\Inject + * @var PropsCollectionFactoryInterface + */ + protected $propsCollectionFactory; + + /** + * @param string $prototypeNameString + * @param string $sitePackageKey + * @return PrototypeDetailsInterface + */ + public function forPrototype(Prototype $prototype): PrototypeDetailsInterface + { + return new PrototypeDetails( + $prototype->getName(), + RenderedCode::fromString( + ReverseFusionParser::restorePrototypeCode( + (string) $prototype->getName(), + $prototype->getAst() + ) + ), + ParsedCode::fromString( + Yaml::dump($prototype->getAst(), 99) + ), + FusionPrototypeAst::fromArray($prototype->getAst()), + $this->anatomyFactory + ->fromPrototypeForPrototypeDetails($prototype), + $this->propsCollectionFactory + ->fromPrototypeForPrototypeDetails($prototype), + PropSetCollection::fromPrototype($prototype) + ); + } +} diff --git a/Classes/Domain/PrototypeDetails/PrototypeDetailsInterface.php b/Classes/Domain/PrototypeDetails/PrototypeDetailsInterface.php new file mode 100644 index 00000000..197c6732 --- /dev/null +++ b/Classes/Domain/PrototypeDetails/PrototypeDetailsInterface.php @@ -0,0 +1,60 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; +use Sitegeist\Monocle\Domain\Fusion\PrototypeName; +use Sitegeist\Monocle\Domain\PrototypeDetails\Props\PropsCollectionInterface; +use Sitegeist\Monocle\Domain\PrototypeDetails\PropSets\PropSetCollection; + +/** + * @Flow\Proxy(false) + */ +interface PrototypeDetailsInterface extends \JsonSerializable +{ + /** + * @return PrototypeName + */ + public function getPrototypeName(): PrototypeName; + + /** + * @return RenderedCode + */ + public function getRenderedCode(): RenderedCode; + + /** + * @return ParsedCode + */ + public function getParsedCode(): ParsedCode; + + /** + * @return FusionPrototypeAst + */ + public function getFusionAst(): FusionPrototypeAst; + + /** + * @return Anatomy + */ + public function getAnatomy(): Anatomy; + + /** + * @return PropsCollectionInterface + */ + public function getProps(): PropsCollectionInterface; + + /** + * @return PropSetCollection + */ + public function getPropSets(): PropSetCollection; +} diff --git a/Classes/Domain/PrototypeDetails/RenderedCode.php b/Classes/Domain/PrototypeDetails/RenderedCode.php new file mode 100644 index 00000000..47f3ca4a --- /dev/null +++ b/Classes/Domain/PrototypeDetails/RenderedCode.php @@ -0,0 +1,52 @@ + + * Wilhelm Behncke + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +use Neos\Flow\Annotations as Flow; + +/** + * @Flow\Proxy(false) + */ +final class RenderedCode implements \JsonSerializable +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value + */ + private function __construct(string $value) + { + $this->value = $value; + } + + /** + * @param string $string + * @return self + */ + public static function fromString(string $string): self + { + return new self($string); + } + + /** + * @return string + */ + public function jsonSerialize() + { + return $this->value; + } +} diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index 127d0614..f341d855 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -11,4 +11,7 @@ Sitegeist\Monocle\Aspects\FusionCachingAspect: factoryMethodName: getCache arguments: 1: - value: Sitegeist_Monocle_Fusion \ No newline at end of file + value: Sitegeist_Monocle_Fusion + +Sitegeist\Monocle\Domain\PrototypeDetails\Props\PropsCollectionFactoryInterface: + className: Sitegeist\Monocle\Domain\PrototypeDetails\Props\PropsCollectionFactory diff --git a/Documentation/PropEditors.md b/Documentation/PropEditors.md new file mode 100644 index 00000000..9e8eb5b3 --- /dev/null +++ b/Documentation/PropEditors.md @@ -0,0 +1,104 @@ +# PropEditors + +## Sitegeist.Monocle/Props/Editors/TextField + +**Configuration Schema** + +-None- + +**Example** +``` +prototype(Vendor.Package:MyComponent) < prototype(Neos.Fusion:Component) { + @styleguide { + options { + propEditors { + titleText { + editor = 'Sitegeist.Monocle/Props/Editors/TextField' + } + } + } + } +} +``` + +## Sitegeist.Monocle/Props/Editors/TextArea + +**Configuration Schema** + +-None- + +**Example** +``` +prototype(Vendor.Package:MyComponent) < prototype(Neos.Fusion:Component) { + @styleguide { + options { + propEditors { + descriptionText { + editor = 'Sitegeist.Monocle/Props/Editors/TextArea' + } + } + } + } +} +``` + +## Sitegeist.Monocle/Props/Editors/Checkbox + +**Configuration Schema** + +-None- + +**Example** +``` +prototype(Vendor.Package:MyComponent) < prototype(Neos.Fusion:Component) { + @styleguide { + options { + propEditors { + isVisible { + editor = 'Sitegeist.Monocle/Props/Editors/Checkbox' + } + } + } + } +} +``` + +## Sitegeist.Monocle/Props/Editors/SelectBox + +**Configuration Schema** +|Property|Type|Description| +|-|-|-| +|options|`list` or `dictionary`|List of available options for the select box| +|options[].label|`string`|Label that should be displayed for the option| +|options[].value|`string` or `int` or `float`|Value of the option| + +**Example** +``` +prototype(Vendor.Package:MyComponent) < prototype(Neos.Fusion:Component) { + @styleguide { + options { + propEditors { + headlineType { + editor = 'Sitegeist.Monocle/Props/Editors/SelectBox' + editorOptions { + options { + h1 { + label = 'H1' + value = 'h1' + } + h2 { + label = 'H2' + value = 'h2' + } + h3 { + label = 'H3' + value = 'h3' + } + } + } + } + } + } + } +} +``` diff --git a/README.md b/README.md index 545d38a4..d0604428 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ To render a prototype as a styleguide-item it simply has to be annotated: ``` prototype(Vendor.Package:Components.Headline) < prototype(Neos.Fusion:Component) { - # + # # styleguide annotation to define title, description and props for the styleguide - # + # @styleguide { title = 'My Custom Prototype' description = 'A Prototype ....' @@ -56,12 +56,12 @@ prototype(Vendor.Package:Components.Headline) < prototype(Neos.Fusion:Component) headline-2 { tagName = 'h2' content = 'Alternate styleguide content for h2' - } + } } } - # - # normal fusion props and renderer + # + # normal fusion props and renderer # tagName = 'h1' content = '' @@ -83,7 +83,7 @@ To map an actual content-node on a component-prototype use a separate fusion pro ``` prototype(Vendor.Package:Content.Headline) < prototype(Neos.Neos:ContentComponent){ content = ${q(node).property('title')} - + renderer = Vendor.Package:Components.Headline { @apply.props = ${props} } @@ -425,6 +425,61 @@ prototype(Vendor.Package:Component.SearchExample) < prototype(Neos.Fusion:Compon } ``` +### Props & Prop Editors + +While previewing Fusion prototypes the Monocle UI offers a mechanism to override certain properties of that prototype in an ad-hoc fashion. This allows you to quickly examine whether your prototype works in certain unforseen configurations (longer or shorter text for instance). + +Monocle will try to reproduce the API of your prototype from multiple sources and will offer all props as editable that can be plausibly associated with a specific editor configuration. By default, Monocle will scan the `@styleguide.props` path in your fusion code for prop values. If your prototype happens to be a `Neos.Fusion:Component` (which in most cases it'll likely be), Monocle also scans all default values of your Component props. + +Given a prop value, Monocle will check its type and provide a fitting editor configuration. Below is a table for all standard cases: + +| Value Type | Editor | +|-|-| +| `string` (with less than 81 characters) | TextField | +| `string` (with more than 80 characters) | TextArea | +| `int` or `float` | TextField | +| `boolean` | CheckBox | + +Additionally, if you need more control over which editor is used you may include a custom configuration under the `@styleguide.options.propEditors` path in your fusion code: + +``` +prototype(Vendor.Package:MyAlertComponent) < prototype(Neos.Fusion:Component) { + @styleguide { + options { + propEditors { + severity { + editor = 'Sitegeist.Monocle/Props/Editors/SelectBox' + editorOptions { + options { + info { + label = 'Info' + value = 'info' + } + success { + label = 'Success' + value = 'success' + } + warning { + label = 'Warning' + value = 'warning' + } + error { + label = 'Error' + value = 'error' + } + } + } + } + } + } + } +} +``` + +An overview of available editors can be found under [[PropEditors](./Documentation/PropEditors.md)]. + +If you are using [`PackageFactory.AtomicFusion.PropTypes`](https://github.com/PackageFactory/atomic-fusion-proptypes) then check out [`Sitegeist.Monocle.PropTypes`](https://github.com/sitegeist/Sitegeist.Monocle.PropTypes). This package automatically generates editor configurations that that fit your PropTypes. + ### Fusion Object Tree Caching Monocle will cache the fusion code for every site package. To invalidate this cache diff --git a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/index.tsx b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/index.tsx index 615ff2d1..8a8967f1 100644 --- a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/index.tsx +++ b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/index.tsx @@ -12,22 +12,27 @@ import { PropsItem } from "./props-item"; import style from "./style.css"; interface InspectorProps { - fusionAst: { - __meta: { - styleguide: { - props: { - [key: string]: any - } - propSets: { - [key: string]: any - } + prototypeDetails: null | { + props: { + name: string + value: any + editor: { + identifier: string + options: any } - } + }[] + propSets: { + name: string + overrides: Record + }[] } overriddenProps: { [key: string]: any } - selectedPropSet: string + selectedPropSet: { + name: string + overrides: Record + } isVisible: boolean selectPropSet: (propSetName: string) => void overrideProp: (name: string, value: any) => void @@ -47,14 +52,11 @@ class InspectorC extends PureComponent { }; render() { - const {fusionAst, overriddenProps, selectedPropSet, isVisible} = this.props; - if (!fusionAst) { + const {prototypeDetails, overriddenProps, selectedPropSet, isVisible} = this.props; + if (!prototypeDetails) { return null; } - const {props, propSets} = fusionAst.__meta.styleguide; - const currentProps = (propSets && selectedPropSet in propSets) ? Object.assign({}, props, propSets[selectedPropSet]) : props; - return (
{ [style.isVisible]: isVisible })} > - {propSets && ( - + {Boolean(prototypeDetails.propSets.length) && ( +
+ +
+ )} + {prototypeDetails.props && ( +
+ {prototypeDetails.props.map(prop => ( + + ))} +
)} - {currentProps && Object.keys(currentProps).map(name => ( - - ))}
); } @@ -88,7 +103,7 @@ export const Inspector = connect((state: State) => { const currentlyRenderedPrototype = selectors.prototypes.currentlyRendered(state); return { - ...currentlyRenderedPrototype, + prototypeDetails: currentlyRenderedPrototype, overriddenProps: selectors.prototypes.overriddenProps(state), selectedPropSet: selectors.prototypes.selectedPropSet(state) }; diff --git a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/index.tsx b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/index.tsx index ad1cfdd8..74fe88e3 100644 --- a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/index.tsx +++ b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/index.tsx @@ -3,6 +3,7 @@ import { PureComponent } from "react"; import Button from "@neos-project/react-ui-components/lib-esm/Button"; import Icon from "@neos-project/react-ui-components/lib-esm/Icon"; +import DropDown from "@neos-project/react-ui-components/lib-esm/DropDown"; import { PropSetList } from "./prop-set-list"; @@ -12,8 +13,9 @@ interface PropSetSelectorProps { label: string enable: boolean propSets: { - [key: string]: any - } + name: string + overrides: Record + }[] onSelectPropSet: (propSetName: string) => void } diff --git a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/prop-set-list/index.tsx b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/prop-set-list/index.tsx index 9e6c8c84..7e22ae7f 100644 --- a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/prop-set-list/index.tsx +++ b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/prop-set-list/index.tsx @@ -9,8 +9,9 @@ import style from "./style.css"; interface PropSetListProps { propSets: { - [key: string]: any - } + name: string + overrides: Record + }[] onSelectPropSet: (propSetName: string) => void } @@ -31,12 +32,12 @@ class PropSetListC extends PureComponent { label={'Default'} onClick={this.handleSelectPropSet} /> - {Object.keys(propSets).map( - propSetName => ( + {propSets.map( + propSet => ( ) diff --git a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/style.css b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/style.css index 4cdeb8f6..3138f642 100644 --- a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/style.css +++ b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/prop-set-selector/style.css @@ -4,12 +4,10 @@ border: 1px solid var(--brandColorsContrastDark); justify-content: space-between; align-items: center; - margin-bottom: 1rem; } .container { width: 100%; max-width: 800px; - margin: 0 auto; position: relative; label { diff --git a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/props-item/index.tsx b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/props-item/index.tsx index 988f6944..8fee93fb 100644 --- a/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/props-item/index.tsx +++ b/Resources/Private/JavaScript/containers/styleguide/header/toolbox/props-inspector/inspector/props-item/index.tsx @@ -4,41 +4,79 @@ import { PureComponent } from "react"; import TextInput from "@neos-project/react-ui-components/lib-esm/TextInput"; import TextArea from "@neos-project/react-ui-components/lib-esm/TextArea"; import CheckBox from "@neos-project/react-ui-components/lib-esm/CheckBox"; +import SelectBox from "@neos-project/react-ui-components/lib-esm/SelectBox"; import style from "./style.css"; interface PropsItemProps { - name: string - type: string - value: any + prop: { + name: string + value: any + editor: { + identifier: string + options: any + } + } + overriddenValue: any onChange: (name: string, value: any) => void } export class PropsItem extends PureComponent { handleChange = (value: any) => { - const {onChange, name} = this.props; + const { onChange, prop } = this.props; if (onChange) { - onChange(name, value); + onChange(prop.name, value); } }; - renderField = (name: string, value: any, type: string, onChange: (name: string, value: any) => void) => { - switch (type) { - case 'string': { - const isLarge = value.length > 80; - if (isLarge) { - return ( -