@amazeelabs/silverback-gutenberg 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +983 -0
- package/drupal/silverback_gutenberg/README.md +439 -0
- package/drupal/silverback_gutenberg/composer.json +20 -0
- package/drupal/silverback_gutenberg/config/install/silverback_gutenberg.settings.yml +1 -0
- package/drupal/silverback_gutenberg/config/schema/silverback_gutenberg.schema.yml +13 -0
- package/drupal/silverback_gutenberg/css/gutenberg-tweaks.css +46 -0
- package/drupal/silverback_gutenberg/directives.gql +40 -0
- package/drupal/silverback_gutenberg/directives.graphql +46 -0
- package/drupal/silverback_gutenberg/js/base.js +24 -0
- package/drupal/silverback_gutenberg/js/gutenberg-tweaks.js +154 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.api.php +76 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.info.yml +8 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.libraries.yml +14 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.module +97 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.services.yml +29 -0
- package/drupal/silverback_gutenberg/src/Annotation/GutenbergBlockMutator.php +39 -0
- package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidator.php +37 -0
- package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidatorRule.php +37 -0
- package/drupal/silverback_gutenberg/src/Attribute/GutenbergBlockMutator.php +29 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorBase.php +24 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorInterface.php +41 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManager.php +114 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManagerInterface.php +30 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/EntityBlockMutatorBase.php +189 -0
- package/drupal/silverback_gutenberg/src/BlockSerializer.php +84 -0
- package/drupal/silverback_gutenberg/src/Controller/LinkitAutocomplete.php +84 -0
- package/drupal/silverback_gutenberg/src/Directives.php +74 -0
- package/drupal/silverback_gutenberg/src/EditorBlocksProcessor.php +53 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorInterface.php +19 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorTrait.php +221 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorBase.php +24 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorInterface.php +65 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorManager.php +37 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleInterface.php +20 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleManager.php +37 -0
- package/drupal/silverback_gutenberg/src/LinkProcessor.php +405 -0
- package/drupal/silverback_gutenberg/src/LinkedContentExtractor.php +35 -0
- package/drupal/silverback_gutenberg/src/Normalizer/GutenbergContentEntityNormalizer.php +123 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergContentTrackTrait.php +51 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergLinkedContent.php +96 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergMediaEmbed.php +63 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergReferencedContent.php +101 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockAttribute.php +42 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockChildren.php +32 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockHtml.php +30 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockMedia.php +159 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockType.php +29 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlocks.php +127 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockAttribute.php +29 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockChildren.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMarkup.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMedia.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockType.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlocks.php +36 -0
- package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/MediaBlockMutator.php +30 -0
- package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/NodeBlockMutator.php +25 -0
- package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/TermReferenceBlockMutator.php +104 -0
- package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMatcherTrait.php +69 -0
- package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMediaMatcher.php +53 -0
- package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackNodeMatcher.php +19 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/Gutenberg.php +15 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/GutenbergValidator.php +210 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Email.php +28 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Required.php +29 -0
- package/drupal/silverback_gutenberg/src/ReferencedContentExtractor.php +67 -0
- package/drupal/silverback_gutenberg/src/Routing/RouteSubscriber.php +17 -0
- package/drupal/silverback_gutenberg/src/Service/MediaService.php +27 -0
- package/drupal/silverback_gutenberg/src/SilverbackGutenbergServiceProvider.php +28 -0
- package/drupal/silverback_gutenberg/src/Utils.php +30 -0
- package/drupal/silverback_gutenberg/src/WebformMessageManager.php +29 -0
- package/drupal/silverback_gutenberg/tests/graphql/.graphqlrc.json +5 -0
- package/drupal/silverback_gutenberg/tests/graphql/queries/editor.gql +30 -0
- package/drupal/silverback_gutenberg/tests/graphql/schema.graphqls +37 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/silverback_gutenberg_test_validator.info.yml +9 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/ColumnValidator.php +42 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/GroupValidator.php +50 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/LinkValidator.php +43 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/BlockValidationRuleTest.php +194 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/EditorDirectivesTest.php +255 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergLinkedContentEUTrackTest.php +133 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergReferencedContentEUTrackTest.php +225 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/LinkProcessorTest.php +284 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/MediaNormalizerTest.php +174 -0
- package/drupal/silverback_gutenberg/tests/src/Traits/SampleAssetTrait.php +15 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/BlockSerializerTest.php +27 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/BlockValidatorCardinalityTest.php +1537 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/EditorBlocksProcessorTest.php +159 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/LinkedContentExtractorTest.php +65 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/ReferencedContentExtractorTest.php +248 -0
- package/drupal/silverback_gutenberg/tests/src/assets/media/data.json +4 -0
- package/drupal/silverback_gutenberg/tests/src/assets/media/source.html +71 -0
- package/drupal/silverback_gutenberg/tests/src/assets/media/target.html +71 -0
- package/package.json +16 -0
- package/turbo.json +15 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GraphQL\Directive;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Plugin\PluginBase;
|
|
6
|
+
use Drupal\graphql\GraphQL\Resolver\ResolverInterface;
|
|
7
|
+
use Drupal\graphql\GraphQL\ResolverBuilder;
|
|
8
|
+
use Drupal\graphql_directives\DirectiveInterface;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @Directive(
|
|
12
|
+
* id = "resolveEditorBlockAttribute",
|
|
13
|
+
* description = "Retrieve an editor block attribute.",
|
|
14
|
+
* arguments = {
|
|
15
|
+
* "key" = "String!",
|
|
16
|
+
* "plainText" = "Boolean"
|
|
17
|
+
* }
|
|
18
|
+
* )
|
|
19
|
+
*/
|
|
20
|
+
class EditorBlockAttribute extends PluginBase implements DirectiveInterface {
|
|
21
|
+
|
|
22
|
+
public function buildResolver(ResolverBuilder $builder, array $arguments): ResolverInterface {
|
|
23
|
+
return $builder->produce('editor_block_attribute')
|
|
24
|
+
->map('block', $builder->fromParent())
|
|
25
|
+
->map('name', $builder->fromValue($arguments['key']))
|
|
26
|
+
->map('plainText', $builder->fromValue($arguments['plainText'] ?? true));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GraphQL\Directive;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Plugin\PluginBase;
|
|
6
|
+
use Drupal\graphql\GraphQL\Resolver\ResolverInterface;
|
|
7
|
+
use Drupal\graphql\GraphQL\ResolverBuilder;
|
|
8
|
+
use Drupal\graphql_directives\DirectiveInterface;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @Directive(
|
|
12
|
+
* id = "resolveEditorBlockChildren"
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
class EditorBlockChildren extends PluginBase implements DirectiveInterface {
|
|
16
|
+
|
|
17
|
+
public function buildResolver(ResolverBuilder $builder, array $arguments): ResolverInterface {
|
|
18
|
+
return $builder->produce('editor_block_children')->map('block', $builder->fromParent());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GraphQL\Directive;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Plugin\PluginBase;
|
|
6
|
+
use Drupal\graphql\GraphQL\Resolver\ResolverInterface;
|
|
7
|
+
use Drupal\graphql\GraphQL\ResolverBuilder;
|
|
8
|
+
use Drupal\graphql_directives\DirectiveInterface;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @Directive(
|
|
12
|
+
* id = "resolveEditorBlockMarkup"
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
class EditorBlockMarkup extends PluginBase implements DirectiveInterface {
|
|
16
|
+
|
|
17
|
+
public function buildResolver(ResolverBuilder $builder, array $arguments): ResolverInterface {
|
|
18
|
+
return $builder->produce('editor_block_html')->map('block', $builder->fromParent());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GraphQL\Directive;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Plugin\PluginBase;
|
|
6
|
+
use Drupal\graphql\GraphQL\Resolver\ResolverInterface;
|
|
7
|
+
use Drupal\graphql\GraphQL\ResolverBuilder;
|
|
8
|
+
use Drupal\graphql_directives\DirectiveInterface;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @Directive(
|
|
12
|
+
* id = "resolveEditorBlockMedia"
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
class EditorBlockMedia extends PluginBase implements DirectiveInterface {
|
|
16
|
+
|
|
17
|
+
public function buildResolver(ResolverBuilder $builder, array $arguments): ResolverInterface {
|
|
18
|
+
return $builder->produce('editor_block_media')->map('block', $builder->fromParent());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GraphQL\Directive;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Plugin\PluginBase;
|
|
6
|
+
use Drupal\graphql\GraphQL\Resolver\ResolverInterface;
|
|
7
|
+
use Drupal\graphql\GraphQL\ResolverBuilder;
|
|
8
|
+
use Drupal\graphql_directives\DirectiveInterface;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @Directive(
|
|
12
|
+
* id = "resolveEditorBlockType"
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
class EditorBlockType extends PluginBase implements DirectiveInterface {
|
|
16
|
+
|
|
17
|
+
public function buildResolver(ResolverBuilder $builder, array $arguments): ResolverInterface {
|
|
18
|
+
return $builder->produce('editor_block_type')->map('block', $builder->fromParent());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GraphQL\Directive;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Entity\EntityInterface;
|
|
6
|
+
use Drupal\Core\Plugin\PluginBase;
|
|
7
|
+
use Drupal\graphql\GraphQL\Resolver\ResolverInterface;
|
|
8
|
+
use Drupal\graphql\GraphQL\ResolverBuilder;
|
|
9
|
+
use Drupal\graphql_directives\DirectiveInterface;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @Directive(
|
|
13
|
+
* id = "resolveEditorBlocks",
|
|
14
|
+
* description = "Parse a gutenberg document into block data.",
|
|
15
|
+
* arguments = {
|
|
16
|
+
* "path" = "String!",
|
|
17
|
+
* "ignored" = "[String!]",
|
|
18
|
+
* "aggregated" = "[String!]"
|
|
19
|
+
* }
|
|
20
|
+
* )
|
|
21
|
+
*/
|
|
22
|
+
class EditorBlocks extends PluginBase implements DirectiveInterface {
|
|
23
|
+
|
|
24
|
+
public function buildResolver(ResolverBuilder $builder, array $arguments): ResolverInterface {
|
|
25
|
+
return $builder->produce('editor_blocks', [
|
|
26
|
+
'path' => $builder->fromValue($arguments['path']),
|
|
27
|
+
'entity' => $builder->fromParent(),
|
|
28
|
+
'type' => $builder->callback(
|
|
29
|
+
fn(EntityInterface $entity) => $entity->getTypedData()->getDataDefinition()->getDataType()
|
|
30
|
+
),
|
|
31
|
+
'ignored' => $builder->fromValue($arguments['ignored'] ?? []),
|
|
32
|
+
'aggregated' => $builder->fromValue($arguments['aggregated'] ?? ['core/paragraph'])
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GutenbergBlockMutator;
|
|
4
|
+
|
|
5
|
+
use Drupal\silverback_gutenberg\Attribute\GutenbergBlockMutator;
|
|
6
|
+
use Drupal\silverback_gutenberg\BlockMutator\EntityBlockMutatorBase;
|
|
7
|
+
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
|
8
|
+
|
|
9
|
+
#[GutenbergBlockMutator(
|
|
10
|
+
id: "media_block_mutator",
|
|
11
|
+
label: new TranslatableMarkup("Media IDs to UUIDs and viceversa."),
|
|
12
|
+
)]
|
|
13
|
+
class MediaBlockMutator extends EntityBlockMutatorBase {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* {@inheritDoc}
|
|
17
|
+
*/
|
|
18
|
+
public bool $isMultiple = TRUE;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* {@inheritDoc}
|
|
22
|
+
*/
|
|
23
|
+
public string $gutenbergAttribute = 'mediaEntityIds';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* {@inheritDoc}
|
|
27
|
+
*/
|
|
28
|
+
public string $entityTypeId = 'media';
|
|
29
|
+
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GutenbergBlockMutator;
|
|
4
|
+
|
|
5
|
+
use Drupal\silverback_gutenberg\Attribute\GutenbergBlockMutator;
|
|
6
|
+
use Drupal\silverback_gutenberg\BlockMutator\EntityBlockMutatorBase;
|
|
7
|
+
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
|
8
|
+
|
|
9
|
+
#[GutenbergBlockMutator(
|
|
10
|
+
id: "node_block_mutator",
|
|
11
|
+
label: new TranslatableMarkup("Node ID to UUID and viceversa."),
|
|
12
|
+
)]
|
|
13
|
+
class NodeBlockMutator extends EntityBlockMutatorBase {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* {@inheritDoc}
|
|
17
|
+
*/
|
|
18
|
+
public string $gutenbergAttribute = 'nodeId';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* {@inheritDoc}
|
|
22
|
+
*/
|
|
23
|
+
public string $entityTypeId = 'node';
|
|
24
|
+
|
|
25
|
+
}
|
package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/TermReferenceBlockMutator.php
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\GutenbergBlockMutator;
|
|
4
|
+
|
|
5
|
+
use Drupal\silverback_gutenberg\Attribute\GutenbergBlockMutator;
|
|
6
|
+
use Drupal\silverback_gutenberg\BlockMutator\EntityBlockMutatorBase;
|
|
7
|
+
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
|
8
|
+
|
|
9
|
+
#[GutenbergBlockMutator(
|
|
10
|
+
id: "term_reference_block_mutator",
|
|
11
|
+
label: new TranslatableMarkup("Term References to UUIDs and vice versa."),
|
|
12
|
+
)]
|
|
13
|
+
class TermReferenceBlockMutator extends EntityBlockMutatorBase {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* {@inheritDoc}
|
|
17
|
+
*/
|
|
18
|
+
public string $entityTypeId = 'taxonomy_term';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* {@inheritDoc}
|
|
22
|
+
*/
|
|
23
|
+
public function applies(array $block): bool {
|
|
24
|
+
// Skip the parent applies() check since we want to be more flexible
|
|
25
|
+
if (empty($block['attrs'])) {
|
|
26
|
+
return FALSE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if any attribute is registered as a term reference
|
|
30
|
+
foreach ($block['attrs'] as $attrName => $value) {
|
|
31
|
+
// Skip empty values
|
|
32
|
+
if (empty($value)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if attribute is marked as a term reference
|
|
37
|
+
if ($this->isTermReferenceAttribute($attrName, $block, $value)) {
|
|
38
|
+
// Store the current attribute being processed
|
|
39
|
+
$this->gutenbergAttribute = $attrName;
|
|
40
|
+
|
|
41
|
+
// Auto-detect if multiple values
|
|
42
|
+
$this->isMultiple = is_array($value);
|
|
43
|
+
|
|
44
|
+
return TRUE;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return FALSE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Determine if an attribute is a term reference.
|
|
53
|
+
*
|
|
54
|
+
* @param string $attrName
|
|
55
|
+
* The attribute name to check.
|
|
56
|
+
* @param array $block
|
|
57
|
+
* The block data.
|
|
58
|
+
* @param mixed $value
|
|
59
|
+
* The attribute value.
|
|
60
|
+
*
|
|
61
|
+
* @return bool
|
|
62
|
+
* TRUE if the attribute is a term reference.
|
|
63
|
+
*/
|
|
64
|
+
protected function isTermReferenceAttribute(string $attrName, array $block, $value): bool {
|
|
65
|
+
if (preg_match('/(Term|Terms|TermId)$/i', $attrName)) {
|
|
66
|
+
// For single values
|
|
67
|
+
if (!is_array($value)) {
|
|
68
|
+
return $this->isValidTermIdentifier($value);
|
|
69
|
+
}
|
|
70
|
+
// For multiple values, check if all values are valid identifiers
|
|
71
|
+
elseif (is_array($value) && !empty($value)) {
|
|
72
|
+
return array_reduce($value, function ($carry, $item) {
|
|
73
|
+
return $carry && $this->isValidTermIdentifier($item);
|
|
74
|
+
}, TRUE);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add other checks here if needed
|
|
79
|
+
return FALSE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if a value is a valid term identifier (numeric ID or UUID).
|
|
84
|
+
*
|
|
85
|
+
* @param mixed $value
|
|
86
|
+
* The value to check.
|
|
87
|
+
*
|
|
88
|
+
* @return bool
|
|
89
|
+
* TRUE if the value is a valid term identifier.
|
|
90
|
+
*/
|
|
91
|
+
protected function isValidTermIdentifier($value): bool {
|
|
92
|
+
// Check if it's a numeric term ID
|
|
93
|
+
if (is_numeric($value)) {
|
|
94
|
+
return TRUE;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if it's a UUID format
|
|
98
|
+
if (is_string($value) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value)) {
|
|
99
|
+
return TRUE;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return FALSE;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\Linkit\Matcher;
|
|
4
|
+
|
|
5
|
+
use Drupal\Component\Utility\Html;
|
|
6
|
+
use Drupal\Core\Entity\EntityInterface;
|
|
7
|
+
use Drupal\Core\Entity\TranslatableInterface;
|
|
8
|
+
use Drupal\linkit\Suggestion\SuggestionCollection;
|
|
9
|
+
|
|
10
|
+
trait SilverbackMatcherTrait {
|
|
11
|
+
|
|
12
|
+
protected string $searchStringLowercase = '';
|
|
13
|
+
|
|
14
|
+
protected array $labelsToSortLowercase = [];
|
|
15
|
+
|
|
16
|
+
public function execute($string) {
|
|
17
|
+
$this->searchStringLowercase = mb_strtolower($string);
|
|
18
|
+
|
|
19
|
+
$suggestions = parent::execute($string)->getSuggestions();
|
|
20
|
+
|
|
21
|
+
// Sort suggestions by the position of the search string in the label.
|
|
22
|
+
uasort($this->labelsToSortLowercase, function ($a, $b) {
|
|
23
|
+
$aPos = strpos($a, $this->searchStringLowercase);
|
|
24
|
+
$bPos = strpos($b, $this->searchStringLowercase);
|
|
25
|
+
if ($aPos === $bPos) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
return ($aPos < $bPos) ? -1 : 1;
|
|
29
|
+
});
|
|
30
|
+
// Reorder suggestions according to the sorted labels.
|
|
31
|
+
$suggestions = array_values(array_replace($this->labelsToSortLowercase, $suggestions));
|
|
32
|
+
|
|
33
|
+
$new = new SuggestionCollection();
|
|
34
|
+
foreach ($suggestions as $suggestion) {
|
|
35
|
+
$new->addSuggestion($suggestion);
|
|
36
|
+
}
|
|
37
|
+
return $new;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected function buildLabel(EntityInterface $entity) {
|
|
41
|
+
// Entity query searches through all node translations, but the linkit node
|
|
42
|
+
// matcher prints only one translation (current language) in the
|
|
43
|
+
// suggestions. This can lead to a confusion because a suggestion can
|
|
44
|
+
// contain no input string.
|
|
45
|
+
// For such cases we redo the suggestion label to the following form:
|
|
46
|
+
// "<label in current language> (<translation that matches the input string>)"
|
|
47
|
+
// Additionally, we collect labels matching the input string to use for
|
|
48
|
+
// sorting later.
|
|
49
|
+
|
|
50
|
+
$label = (string) $entity->label();
|
|
51
|
+
if (str_contains(mb_strtolower($label), $this->searchStringLowercase)) {
|
|
52
|
+
$this->labelsToSortLowercase[] = mb_strtolower($label);
|
|
53
|
+
return Html::escape($label);
|
|
54
|
+
}
|
|
55
|
+
if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) {
|
|
56
|
+
foreach ($entity->getTranslationLanguages() as $language) {
|
|
57
|
+
$translation = $entity->getTranslation($language->getId());
|
|
58
|
+
$translatedLabel = (string) $translation->label();
|
|
59
|
+
if (str_contains(mb_strtolower($translatedLabel), $this->searchStringLowercase)) {
|
|
60
|
+
$this->labelsToSortLowercase[] = mb_strtolower($translatedLabel);
|
|
61
|
+
return Html::escape("{$label} ({$translatedLabel})");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
$this->labelsToSortLowercase[] = mb_strtolower($label);
|
|
66
|
+
return Html::escape($label);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\Linkit\Matcher;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Form\FormStateInterface;
|
|
6
|
+
use Drupal\linkit\Plugin\Linkit\Matcher\EntityMatcher;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @Matcher(
|
|
10
|
+
* id = "silverback:entity:media",
|
|
11
|
+
* label = @Translation("Silverback: Media"),
|
|
12
|
+
* target_entity = "media",
|
|
13
|
+
* provider = "media"
|
|
14
|
+
* )
|
|
15
|
+
*/
|
|
16
|
+
class SilverbackMediaMatcher extends EntityMatcher {
|
|
17
|
+
|
|
18
|
+
use SilverbackMatcherTrait;
|
|
19
|
+
|
|
20
|
+
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
|
|
21
|
+
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
|
22
|
+
$this->targetType = 'media';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public function buildConfigurationForm(
|
|
26
|
+
array $form,
|
|
27
|
+
FormStateInterface $form_state
|
|
28
|
+
) {
|
|
29
|
+
$form = parent::buildConfigurationForm(
|
|
30
|
+
$form,
|
|
31
|
+
$form_state
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Silverback Gutenberg does not take this setting into account anyway.
|
|
35
|
+
unset($form['substitution']);
|
|
36
|
+
|
|
37
|
+
return $form;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
|
|
41
|
+
parent::submitConfigurationForm($form, $form_state);
|
|
42
|
+
// Because we unset the substitution fieldset in ::buildConfigurationForm(),
|
|
43
|
+
// we need to force it to 'canonical' because
|
|
44
|
+
// Drupal\silverback_gutenberg\Controller\LinkitAutocomplete() controller
|
|
45
|
+
// needs that info. If the substitution type is not set, then the
|
|
46
|
+
// autocomplete controller will just leave the /media/mid urls that are
|
|
47
|
+
// generated by the original linkit autocomplete service intact, which means
|
|
48
|
+
// they won't be replaced, on saving, with their corresponding /media/uuid
|
|
49
|
+
// counterparts.
|
|
50
|
+
$this->configuration['substitution_type'] = 'canonical';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\Linkit\Matcher;
|
|
4
|
+
|
|
5
|
+
use Drupal\linkit\Plugin\Linkit\Matcher\NodeMatcher;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @Matcher(
|
|
9
|
+
* id = "silverback:entity:node",
|
|
10
|
+
* label = @Translation("Silverback: Content"),
|
|
11
|
+
* target_entity = "node",
|
|
12
|
+
* provider = "node"
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
class SilverbackNodeMatcher extends NodeMatcher {
|
|
16
|
+
|
|
17
|
+
use SilverbackMatcherTrait;
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\Validation\Constraint;
|
|
4
|
+
|
|
5
|
+
use Symfony\Component\Validator\Constraint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @Constraint(
|
|
9
|
+
* id = "Gutenberg",
|
|
10
|
+
* label = @Translation("Gutenberg", context = "Validation")
|
|
11
|
+
* )
|
|
12
|
+
*/
|
|
13
|
+
class Gutenberg extends Constraint {
|
|
14
|
+
|
|
15
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\Validation\Constraint;
|
|
4
|
+
|
|
5
|
+
use Drupal\silverback_gutenberg\GutenbergValidation\GutenbergValidatorInterface;
|
|
6
|
+
use Drupal\silverback_gutenberg\GutenbergValidation\GutenbergValidatorRuleManager;
|
|
7
|
+
use Drupal\Component\Plugin\Exception\PluginException;
|
|
8
|
+
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
|
|
9
|
+
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
|
10
|
+
use Drupal\Core\Render\Markup;
|
|
11
|
+
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
|
12
|
+
use Drupal\gutenberg\Parser\BlockParser;
|
|
13
|
+
use Symfony\Component\DependencyInjection\ContainerInterface;
|
|
14
|
+
use Symfony\Component\Validator\Constraint;
|
|
15
|
+
use Symfony\Component\Validator\ConstraintValidator;
|
|
16
|
+
use Drupal\silverback_gutenberg\GutenbergValidation\GutenbergValidatorManager;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validator class for the Gutenberg required field.
|
|
20
|
+
*/
|
|
21
|
+
class GutenbergValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
|
22
|
+
|
|
23
|
+
use StringTranslationTrait;
|
|
24
|
+
|
|
25
|
+
protected $violations = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Flat representation of the blocks to track validation errors.
|
|
29
|
+
*
|
|
30
|
+
* @var array $blocksList
|
|
31
|
+
*/
|
|
32
|
+
protected $blocksList = [];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validator manager service plugin.
|
|
36
|
+
*
|
|
37
|
+
* @var \Drupal\silverback_gutenberg\GutenbergValidation\GutenbergValidatorManager
|
|
38
|
+
*/
|
|
39
|
+
protected $validatorManager;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validator rule manager service plugin.
|
|
43
|
+
*
|
|
44
|
+
* @var \Drupal\silverback_gutenberg\GutenbergValidation\GutenbergValidatorRuleManager
|
|
45
|
+
*/
|
|
46
|
+
protected $validatorRuleManager;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Constructs a GutenbergValidator object
|
|
50
|
+
* @param \Drupal\silverback_gutenberg\GutenbergValidation\GutenbergValidatorManager $validator_manager
|
|
51
|
+
*/
|
|
52
|
+
public function __construct(
|
|
53
|
+
GutenbergValidatorManager $validator_manager,
|
|
54
|
+
GutenbergValidatorRuleManager $validator_rule_manager
|
|
55
|
+
) {
|
|
56
|
+
$this->validatorManager = $validator_manager;
|
|
57
|
+
$this->validatorRuleManager = $validator_rule_manager;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* {@inheritDoc}
|
|
62
|
+
*/
|
|
63
|
+
public static function create(ContainerInterface $container) {
|
|
64
|
+
return new static(
|
|
65
|
+
$container->get('plugin.manager.gutenberg_validator'),
|
|
66
|
+
$container->get('plugin.manager.gutenberg_validator_rule')
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* {@inheritDoc}
|
|
72
|
+
*/
|
|
73
|
+
public function validate($value, Constraint $constraint) {
|
|
74
|
+
// If the field is empty, we don't need to validate it.
|
|
75
|
+
// Delegate to Drupal.
|
|
76
|
+
if (empty($value->getValue())) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
$content = $value->getValue()[0]['value'];
|
|
80
|
+
$parser = new BlockParser();
|
|
81
|
+
$blocks = $parser->parse($content);
|
|
82
|
+
$plugins = [];
|
|
83
|
+
foreach ($this->validatorManager->getDefinitions() as $definition) {
|
|
84
|
+
try {
|
|
85
|
+
$plugins[] = $this->validatorManager->createInstance($definition['id'], []);
|
|
86
|
+
} catch (PluginNotFoundException | PluginException $e) {
|
|
87
|
+
// Do nothing if the plugin could not be instantiated, although we
|
|
88
|
+
// should never get here, as before we just had a call to
|
|
89
|
+
// getDefinitions().
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!empty($plugins)) {
|
|
94
|
+
$this->validateBlocks($blocks, $plugins);
|
|
95
|
+
}
|
|
96
|
+
// If we have any violations after running the blocks validation, we
|
|
97
|
+
// aggregate all of them in one message and fire a validation for the
|
|
98
|
+
// Gutenberg constraint.
|
|
99
|
+
if (!empty($this->violations)) {
|
|
100
|
+
$messages = [];
|
|
101
|
+
foreach ($this->violations as $violation) {
|
|
102
|
+
if (is_array($violation['message'])) {
|
|
103
|
+
$messages = array_merge($messages, $violation['message']);
|
|
104
|
+
} else {
|
|
105
|
+
$messages[] = $violation['message'];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
$this->context->addViolation($this->t('Invalid content: <ul><li>@violations</li></ul>', ['@violations' => Markup::create(implode('</li><li>', $messages))]));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validates a set of Gutenberg blocks (and their inner blocks) against a set
|
|
114
|
+
* of validator plugins.
|
|
115
|
+
*
|
|
116
|
+
* @param array $blocks
|
|
117
|
+
* @param array $plugins
|
|
118
|
+
*/
|
|
119
|
+
public function validateBlocks(array &$blocks, array $plugins, array $breadcrumbs = []) {
|
|
120
|
+
// @todo: we have here a pretty big nesting level, would be nice to find a
|
|
121
|
+
// solution to reduce it.
|
|
122
|
+
array_walk($blocks, function(&$block) use ($plugins, $breadcrumbs) {
|
|
123
|
+
array_walk($plugins, function(GutenbergValidatorInterface $plugin) use (&$block, $plugins, $breadcrumbs) {
|
|
124
|
+
// There are several recursion levels, so make sure to get a flat list
|
|
125
|
+
// of blocks, and mark the blocks that are already processed, so we don't
|
|
126
|
+
// end up with duplicates.
|
|
127
|
+
// This marker can then be reused to know which block instance
|
|
128
|
+
// is not valid.
|
|
129
|
+
if (empty($block['validator_id'])) {
|
|
130
|
+
$block['validator_id'] = uniqid();
|
|
131
|
+
$this->blocksList[$block['validator_id']] = $block['blockName'];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if the block has inner blocks, and validate them as well.
|
|
135
|
+
if (!empty($block['innerBlocks'])) {
|
|
136
|
+
$breadcrumbs[] = $block['blockName'];
|
|
137
|
+
$this->validateBlocks($block['innerBlocks'], [$plugin], $breadcrumbs);
|
|
138
|
+
}
|
|
139
|
+
if (!$plugin->applies($block)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
$validatedFields = $plugin->validatedFields($block);
|
|
143
|
+
if (!empty($validatedFields)) {
|
|
144
|
+
array_walk($validatedFields, function($validatedField, $attrName) use ($block, $breadcrumbs) {
|
|
145
|
+
if (empty($validatedField['rules'])) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
$attrValue = $block['attrs'][$attrName] ?? NULL;
|
|
149
|
+
array_walk($validatedField['rules'], function($validationRulePluginId) use ($attrValue, $attrName, $block, $validatedField, $breadcrumbs) {
|
|
150
|
+
try {
|
|
151
|
+
$rulePlugin = $this->validatorRuleManager->createInstance($validationRulePluginId);
|
|
152
|
+
} catch (PluginNotFoundException | PluginException $e) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
$validationMessage = $rulePlugin->validate($attrValue, $validatedField['field_label'] ?? $attrName);
|
|
156
|
+
// If the returned value is the boolean TRUE, then it means the
|
|
157
|
+
// field is valid, so we can just return.
|
|
158
|
+
if ($validationMessage === TRUE) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
$breadcrumbs[] = $block['blockName'];
|
|
162
|
+
$breadcrumbLabels = array_map(function($blockName) {
|
|
163
|
+
$blockNameParts = explode('/', $blockName);
|
|
164
|
+
return ucfirst(str_replace('-', ' ', end($blockNameParts)));
|
|
165
|
+
}, $breadcrumbs);
|
|
166
|
+
$breadcrumbsLabel = implode(' > ', $breadcrumbLabels);
|
|
167
|
+
$instanceId = $this->getInvalidInstanceId($block['validator_id'], $block['blockName']);
|
|
168
|
+
$message = '<span class="block-validation-error" data-block-instance="'. $instanceId .'" data-block-type="'. $block['blockName'] .'">';
|
|
169
|
+
$message .= $breadcrumbsLabel . ': ' . $validationMessage . '</span>';
|
|
170
|
+
$this->violations[] = [
|
|
171
|
+
'attribute' => $attrName,
|
|
172
|
+
'blockName' => $block['blockName'],
|
|
173
|
+
'rule' => $validationRulePluginId,
|
|
174
|
+
'message' => $message,
|
|
175
|
+
];
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// Last, call the validateContent method, in case there is a custom
|
|
180
|
+
// validation logic in the validator plugin itself.
|
|
181
|
+
$validateContent = $plugin->validateContent($block);
|
|
182
|
+
if (!empty($validateContent) && $validateContent['is_valid'] !== TRUE) {
|
|
183
|
+
$instanceId = $this->getInvalidInstanceId($block['validator_id'], $block['blockName']);
|
|
184
|
+
$message = '<span class="block-validation-error" data-block-instance="'. $instanceId .'" data-block-type="'. $block['blockName'] .'">';
|
|
185
|
+
$message .= $validateContent['message'] . '</span>';
|
|
186
|
+
$this->violations[] = [
|
|
187
|
+
'attribute' => 'block_content',
|
|
188
|
+
'blockName' => $block['blockName'],
|
|
189
|
+
'rule' => 'block_content',
|
|
190
|
+
'message' => $message,
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private function getInvalidInstanceId($validator_id, $block_name) {
|
|
198
|
+
$result = 0;
|
|
199
|
+
foreach($this->blocksList as $key => $value) {
|
|
200
|
+
if ($value === $block_name) {
|
|
201
|
+
$result++;
|
|
202
|
+
}
|
|
203
|
+
if ($key === $validator_id) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return $result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
}
|