Skip to content

Commit

Permalink
EZP-29301: Provided support for custom classes and data attributes (#39)
Browse files Browse the repository at this point in the history
* [Symfony] Created SA-aware configuration for custom attributes

* [SiteAccess] Mapped configuration for custom attributes

* Implemented validation of custom classes and attributes settings

* Improved merging custom classes and attributes settings from scopes

* Implemented ez_online_editor_attributes translation extractor

* Set const visibility in RichText SiteAccess config parser

* Changed "multiple" setting for attributes to be false by default

* Disabled normalization of attribute names and improved validation

* Refactored XSLT for list items to be uniform accross formats

Changed the way transformation from DocBook to the other formats
is done, to process listitem instead of listitem/para
so ezattribute element is a direct child of listitem

Formats:
* DocBook
* XHTML5EDIT
* Output

* Added custom data attributes support for ezembed(inline) elements

* Added custom HTML class support for ezembed(inline) elements
  • Loading branch information
alongosz authored Jun 17, 2019
1 parent aed1630 commit 8a66616
Show file tree
Hide file tree
Showing 29 changed files with 699 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Parser\AbstractFieldTypeParser;
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\SiteAccessAware\ContextualizerInterface;
use InvalidArgumentException;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition;

Expand All @@ -18,6 +19,24 @@
*/
class RichText extends AbstractFieldTypeParser
{
public const CLASSES_SA_SETTINGS_ID = 'fieldtypes.ezrichtext.classes';
private const CLASSES_NODE_KEY = 'classes';

public const ATTRIBUTES_SA_SETTINGS_ID = 'fieldtypes.ezrichtext.attributes';
private const ATTRIBUTES_NODE_KEY = 'attributes';
private const ATTRIBUTE_TYPE_NODE_KEY = 'type';
private const ATTRIBUTE_TYPE_CHOICE = 'choice';
private const ATTRIBUTE_TYPE_BOOLEAN = 'boolean';
private const ATTRIBUTE_TYPE_STRING = 'string';
private const ATTRIBUTE_TYPE_NUMBER = 'number';

// constants common for OE custom classes and data attributes configuration
private const ELEMENT_NODE_KEY = 'element';
private const DEFAULT_VALUE_NODE_KEY = 'default_value';
private const CHOICES_NODE_KEY = 'choices';
private const REQUIRED_NODE_KEY = 'required';
private const MULTIPLE_NODE_KEY = 'multiple';

/**
* Returns the fieldType identifier the config parser works for.
* This is to create the right configuration node under system.<siteaccess_name>.fieldtypes.
Expand Down Expand Up @@ -171,6 +190,8 @@ public function addFieldTypeSemanticConfig(NodeBuilder $nodeBuilder)
->info('List of RichText Custom Styles enabled for the current scope. The Custom Styles must be defined in ezpublish.ezrichtext.custom_styles Node.')
->scalarPrototype()->end()
->end();

$this->buildOnlineEditorConfiguration($nodeBuilder);
}

/**
Expand Down Expand Up @@ -239,6 +260,17 @@ public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerIn
);
}
}

$onlineEditorSettingsMap = [
self::CLASSES_NODE_KEY => self::CLASSES_SA_SETTINGS_ID,
self::ATTRIBUTES_NODE_KEY => self::ATTRIBUTES_SA_SETTINGS_ID,
];
foreach ($onlineEditorSettingsMap as $key => $settingsId) {
if (isset($scopeSettings['fieldtypes']['ezrichtext'][$key])) {
$scopeSettings[$settingsId] = $scopeSettings['fieldtypes']['ezrichtext'][$key];
unset($scopeSettings['fieldtypes']['ezrichtext'][$key]);
}
}
}
}

Expand All @@ -249,6 +281,13 @@ public function postMap(array $config, ContextualizerInterface $contextualizer)
$contextualizer->mapConfigArray('fieldtypes.ezrichtext.output_custom_xsl', $config);
$contextualizer->mapConfigArray('fieldtypes.ezrichtext.edit_custom_xsl', $config);
$contextualizer->mapConfigArray('fieldtypes.ezrichtext.input_custom_xsl', $config);
$contextualizer->mapConfigArray(self::CLASSES_SA_SETTINGS_ID, $config);
// merge attributes of the same element from different scopes
$contextualizer->mapConfigArray(
self::ATTRIBUTES_SA_SETTINGS_ID,
$config,
ContextualizerInterface::MERGE_FROM_SECOND_LEVEL
);
}

/**
Expand Down Expand Up @@ -353,4 +392,128 @@ private function setupDeprecatedConfiguration(NodeBuilder $nodeBuilder)
->end()
;
}

/**
* Build configuration nodes strictly related to Online Editor.
*
* @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $nodeBuilder
*/
private function buildOnlineEditorConfiguration(NodeBuilder $nodeBuilder): void
{
$invalidChoiceCallback = function (array $v) {
$message = sprintf(
'Default value must be one of the possible choices: %s, but "%s" given',
implode(', ', $v[self::CHOICES_NODE_KEY]),
$v[self::DEFAULT_VALUE_NODE_KEY]
);

throw new InvalidArgumentException($message, 1);
};

$nodeBuilder
->arrayNode(self::CLASSES_NODE_KEY)
->useAttributeAsKey(self::ELEMENT_NODE_KEY)
->arrayPrototype()
->validate()
->ifTrue(function (array $v) {
return !empty($v[self::DEFAULT_VALUE_NODE_KEY])
&& !in_array($v[self::DEFAULT_VALUE_NODE_KEY], $v[self::CHOICES_NODE_KEY]);
})
->then($invalidChoiceCallback)
->end()
->children()
->arrayNode(self::CHOICES_NODE_KEY)
->scalarPrototype()->end()
->isRequired()
->end()
->booleanNode(self::REQUIRED_NODE_KEY)
->defaultFalse()
->end()
->scalarNode(self::DEFAULT_VALUE_NODE_KEY)
->end()
->booleanNode(self::MULTIPLE_NODE_KEY)
->defaultTrue()
->end()
->end()
->end()
->end()
->arrayNode(self::ATTRIBUTES_NODE_KEY)
->useAttributeAsKey(self::ELEMENT_NODE_KEY)
->arrayPrototype()
// allow dashes in data attribute name
->normalizeKeys(false)
->arrayPrototype()
->validate()
->always($this->getAttributesValidatorCallback($invalidChoiceCallback))
->end()
->children()
->enumNode(self::ATTRIBUTE_TYPE_NODE_KEY)
->isRequired()
->values(
[
self::ATTRIBUTE_TYPE_CHOICE,
self::ATTRIBUTE_TYPE_BOOLEAN,
self::ATTRIBUTE_TYPE_STRING,
self::ATTRIBUTE_TYPE_NUMBER,
]
)
->end()
->arrayNode(self::CHOICES_NODE_KEY)
->validate()
->ifEmpty()->thenUnset()
->end()
->scalarPrototype()
->end()
->end()
->booleanNode(self::MULTIPLE_NODE_KEY)->defaultFalse()->end()
->booleanNode(self::REQUIRED_NODE_KEY)->defaultFalse()->end()
->scalarNode(self::DEFAULT_VALUE_NODE_KEY)->end()
->end()
->end()
->end()
->end();
}

/**
* Return validation callback which will validate custom data attributes semantic config.
*
* The validation validates the following rules:
* - if a custom data attribute is not of `choice` type, it must not define `choices` list,
* - a `default_value` of custom data attribute must be the one from `choices` list,
* - a custom data attribute of `boolean` type must not define `required` setting.
*
* @param callable $invalidChoiceCallback
*
* @return callable
*/
private function getAttributesValidatorCallback(callable $invalidChoiceCallback): callable
{
return function (array $v) use ($invalidChoiceCallback) {
if ($v[self::ATTRIBUTE_TYPE_NODE_KEY] === self::ATTRIBUTE_TYPE_CHOICE
&& !empty($v[self::DEFAULT_VALUE_NODE_KEY])
&& !in_array($v[self::DEFAULT_VALUE_NODE_KEY], $v[self::CHOICES_NODE_KEY])
) {
$invalidChoiceCallback($v);
} elseif ($v[self::ATTRIBUTE_TYPE_NODE_KEY] === self::ATTRIBUTE_TYPE_BOOLEAN && $v[self::REQUIRED_NODE_KEY]) {
throw new InvalidArgumentException(
sprintf('Boolean type does not support "%s" setting', self::REQUIRED_NODE_KEY)
);
} elseif ($v[self::ATTRIBUTE_TYPE_NODE_KEY] !== self::ATTRIBUTE_TYPE_CHOICE && !empty($v[self::CHOICES_NODE_KEY])) {
throw new InvalidArgumentException(
sprintf(
'%s type does not support "%s" setting',
ucfirst($v[self::ATTRIBUTE_TYPE_NODE_KEY]),
self::CHOICES_NODE_KEY
)
);
}

// at this point, for non-choice types, unset choice type-related settings
if ($v[self::ATTRIBUTE_TYPE_NODE_KEY] !== self::ATTRIBUTE_TYPE_CHOICE) {
unset($v[self::CHOICES_NODE_KEY], $v[self::MULTIPLE_NODE_KEY]);
}

return $v;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('rest.yml');
$loader->load('templating.yml');
$loader->load('form.yml');
$loader->load('translation.yml');

// load Kernel BC layer
$loader->load('bc/aliases.yml');
Expand Down
11 changes: 11 additions & 0 deletions src/bundle/Resources/config/translation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
_defaults:
autowire: true
autoconfigure: true
public: false

EzSystems\EzPlatformRichText\Translation\Extractor\OnlineEditorCustomAttributesExtractor:
arguments:
$siteAccessList: '%ezpublish.siteaccess.list%'
tags:
- { name: jms_translation.extractor, alias: ez_online_editor_attributes }
10 changes: 9 additions & 1 deletion src/bundle/Resources/views/RichText/embed/content.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@
{% if embedParams.link is defined %}
{% set params = params|merge( { "linkParameters": embedParams.link } ) %}
{% endif %}
{% if embedParams.dataAttributes is defined %}
{# Note: intentionally using here new convention for parameter names #}
{% set data_attributes_str = ' ' ~ embedParams.dataAttributes|ez_data_attributes_serialize %}
{# Note: passing data attributes as param for 3rd party overridden embed views #}
{% set params = params|merge( { "data_attributes": embedParams.dataAttributes } ) %}
{% else %}
{% set data_attributes_str = '' %}
{% endif %}

<div {% if embedParams.anchor is defined %}id="{{ embedParams.anchor }}"{% endif %} class="{% if embedParams.align is defined %}align-{{ embedParams.align }}{% endif %}{% if embedParams.class is defined %} {{ embedParams.class }}{% endif %}">
<div {% if embedParams.anchor is defined %}id="{{ embedParams.anchor }}"{% endif %} class="{% if embedParams.align is defined %}align-{{ embedParams.align }}{% endif %}{% if embedParams.class is defined %} {{ embedParams.class }}{% endif %}"{{ data_attributes_str|raw }}>
{{ fos_httpcache_tag('relation-' ~ embedParams.id) }}
{{
render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
{% if embedParams.link is defined %}
{% set params = params|merge( { "linkParameters": embedParams.link } ) %}
{% endif %}
{% if embedParams.dataAttributes is defined %}
{# Note: intentionally using here new convention for parameter names #}
{% set params = params|merge( { "data_attributes": embedParams.dataAttributes } ) %}
{% endif %}
{% if embedParams.class is defined %}
{% set params = params|merge( { "class": embedParams.class } ) %}
{% endif %}

{{ fos_httpcache_tag('relation-' ~ embedParams.id) }}
{{
render(
Expand Down
7 changes: 7 additions & 0 deletions src/bundle/Resources/views/RichText/embed/location.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
{% if embedParams.link is defined %}
{% set params = params|merge( { "linkParameters": embedParams.link } ) %}
{% endif %}
{% if embedParams.dataAttributes is defined %}
{# Note: intentionally using here new convention for parameter names #}
{% set params = params|merge( { "data_attributes": embedParams.dataAttributes } ) %}
{% endif %}
{% if embedParams.class is defined %}
{% set params = params|merge( { "class": embedParams.class } ) %}
{% endif %}

<div {% if embedParams.anchor is defined %}id="{{ embedParams.anchor }}"{% endif %} class="{% if embedParams.align is defined %}align-{{ embedParams.align }}{% endif %}{% if embedParams.class is defined %} {{ embedParams.class }}{% endif %}">
{{ fos_httpcache_tag('relation-location-' ~ embedParams.id) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
{% if embedParams.link is defined %}
{% set params = params|merge( { "linkParameters": embedParams.link } ) %}
{% endif %}
{% if embedParams.dataAttributes is defined %}
{# Note: intentionally using here new convention for parameter names #}
{% set params = params|merge( { "data_attributes": embedParams.dataAttributes } ) %}
{% endif %}
{% if embedParams.class is defined %}
{% set params = params|merge( { "class": embedParams.class } ) %}
{% endif %}

{{ fos_httpcache_tag('relation-location-' ~ embedParams.id) }}
{{
render(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace EzSystems\EzPlatformRichText\Translation\Extractor;

use eZ\Publish\Core\MVC\ConfigResolverInterface;
use EzSystems\EzPlatformRichTextBundle\DependencyInjection\Configuration\Parser\FieldType\RichText;
use JMS\TranslationBundle\Model\MessageCatalogue;
use JMS\TranslationBundle\Model\Message\XliffMessage;
use JMS\TranslationBundle\Translation\ExtractorInterface;

/**
* Generates translation strings for limitation types.
*/
final class OnlineEditorCustomAttributesExtractor implements ExtractorInterface
{
private const MESSAGE_DOMAIN = 'online_editor';
private const ATTRIBUTES_MESSAGE_ID_PREFIX = 'ezrichtext.attributes';
private const CLASS_LABEL_MESSAGE_ID = 'ezrichtext.classes.class.label';

/**
* @var \eZ\Publish\Core\MVC\ConfigResolverInterface
*/
private $configResolver;

/**
* @var string[]
*/
private $siteAccessList;

/**
* @param \eZ\Publish\Core\MVC\ConfigResolverInterface $configResolver
* @param string[] $siteAccessList
*/
public function __construct(ConfigResolverInterface $configResolver, array $siteAccessList)
{
$this->configResolver = $configResolver;
$this->siteAccessList = $siteAccessList;
}

/**
* Iterate over each scope and extract custom attributes label names.
*
* @return \JMS\TranslationBundle\Model\MessageCatalogue
*/
public function extract(): MessageCatalogue
{
$catalogue = new MessageCatalogue();

$catalogue->add($this->createMessage(self::CLASS_LABEL_MESSAGE_ID, 'Class'));

foreach ($this->siteAccessList as $scope) {
if (!$this->configResolver->hasParameter(RichText::ATTRIBUTES_SA_SETTINGS_ID)) {
continue;
}
$this->extractMessagesForScope($catalogue, $scope);
}

return $catalogue;
}

/**
* @param string $id
* @param string $desc
*
* @return \JMS\TranslationBundle\Model\Message\XliffMessage
*/
private function createMessage(string $id, string $desc): XliffMessage
{
$message = new XliffMessage($id, self::MESSAGE_DOMAIN);
$message->setNew(false);
$message->setMeaning($desc);
$message->setDesc($desc);
$message->setLocaleString($desc);
$message->addNote('key: ' . $id);

return $message;
}

/**
* Extract messages from the given scope into the catalogue.
*
* @param \JMS\TranslationBundle\Model\MessageCatalogue $catalogue
* @param string $scope
*/
private function extractMessagesForScope(MessageCatalogue $catalogue, string $scope): void
{
$attributes = $this->configResolver->getParameter(
RichText::ATTRIBUTES_SA_SETTINGS_ID,
null,
$scope
);
foreach ($attributes as $elementName => $attributesConfig) {
foreach (array_keys($attributesConfig) as $attributeName) {
$messageId = sprintf(
'%s.%s.%s.label',
self::ATTRIBUTES_MESSAGE_ID_PREFIX,
$elementName,
$attributeName
);
// by default let's use attribute name
$catalogue->add(
$this->createMessage($messageId, $attributeName)
);
}
}
}
}
Loading

0 comments on commit 8a66616

Please sign in to comment.