@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +983 -0
  2. package/drupal/silverback_gutenberg/README.md +439 -0
  3. package/drupal/silverback_gutenberg/composer.json +20 -0
  4. package/drupal/silverback_gutenberg/config/install/silverback_gutenberg.settings.yml +1 -0
  5. package/drupal/silverback_gutenberg/config/schema/silverback_gutenberg.schema.yml +13 -0
  6. package/drupal/silverback_gutenberg/css/gutenberg-tweaks.css +46 -0
  7. package/drupal/silverback_gutenberg/directives.gql +40 -0
  8. package/drupal/silverback_gutenberg/directives.graphql +46 -0
  9. package/drupal/silverback_gutenberg/js/base.js +24 -0
  10. package/drupal/silverback_gutenberg/js/gutenberg-tweaks.js +154 -0
  11. package/drupal/silverback_gutenberg/silverback_gutenberg.api.php +76 -0
  12. package/drupal/silverback_gutenberg/silverback_gutenberg.info.yml +8 -0
  13. package/drupal/silverback_gutenberg/silverback_gutenberg.libraries.yml +14 -0
  14. package/drupal/silverback_gutenberg/silverback_gutenberg.module +97 -0
  15. package/drupal/silverback_gutenberg/silverback_gutenberg.services.yml +29 -0
  16. package/drupal/silverback_gutenberg/src/Annotation/GutenbergBlockMutator.php +39 -0
  17. package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidator.php +37 -0
  18. package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidatorRule.php +37 -0
  19. package/drupal/silverback_gutenberg/src/Attribute/GutenbergBlockMutator.php +29 -0
  20. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorBase.php +24 -0
  21. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorInterface.php +41 -0
  22. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManager.php +114 -0
  23. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManagerInterface.php +30 -0
  24. package/drupal/silverback_gutenberg/src/BlockMutator/EntityBlockMutatorBase.php +189 -0
  25. package/drupal/silverback_gutenberg/src/BlockSerializer.php +84 -0
  26. package/drupal/silverback_gutenberg/src/Controller/LinkitAutocomplete.php +84 -0
  27. package/drupal/silverback_gutenberg/src/Directives.php +74 -0
  28. package/drupal/silverback_gutenberg/src/EditorBlocksProcessor.php +53 -0
  29. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorInterface.php +19 -0
  30. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorTrait.php +221 -0
  31. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorBase.php +24 -0
  32. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorInterface.php +65 -0
  33. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorManager.php +37 -0
  34. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleInterface.php +20 -0
  35. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleManager.php +37 -0
  36. package/drupal/silverback_gutenberg/src/LinkProcessor.php +405 -0
  37. package/drupal/silverback_gutenberg/src/LinkedContentExtractor.php +35 -0
  38. package/drupal/silverback_gutenberg/src/Normalizer/GutenbergContentEntityNormalizer.php +123 -0
  39. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergContentTrackTrait.php +51 -0
  40. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergLinkedContent.php +96 -0
  41. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergMediaEmbed.php +63 -0
  42. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergReferencedContent.php +101 -0
  43. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockAttribute.php +42 -0
  44. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockChildren.php +32 -0
  45. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockHtml.php +30 -0
  46. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockMedia.php +159 -0
  47. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockType.php +29 -0
  48. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlocks.php +127 -0
  49. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockAttribute.php +29 -0
  50. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockChildren.php +21 -0
  51. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMarkup.php +21 -0
  52. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMedia.php +21 -0
  53. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockType.php +21 -0
  54. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlocks.php +36 -0
  55. package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/MediaBlockMutator.php +30 -0
  56. package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/NodeBlockMutator.php +25 -0
  57. package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/TermReferenceBlockMutator.php +104 -0
  58. package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMatcherTrait.php +69 -0
  59. package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMediaMatcher.php +53 -0
  60. package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackNodeMatcher.php +19 -0
  61. package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/Gutenberg.php +15 -0
  62. package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/GutenbergValidator.php +210 -0
  63. package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Email.php +28 -0
  64. package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Required.php +29 -0
  65. package/drupal/silverback_gutenberg/src/ReferencedContentExtractor.php +67 -0
  66. package/drupal/silverback_gutenberg/src/Routing/RouteSubscriber.php +17 -0
  67. package/drupal/silverback_gutenberg/src/Service/MediaService.php +27 -0
  68. package/drupal/silverback_gutenberg/src/SilverbackGutenbergServiceProvider.php +28 -0
  69. package/drupal/silverback_gutenberg/src/Utils.php +30 -0
  70. package/drupal/silverback_gutenberg/src/WebformMessageManager.php +29 -0
  71. package/drupal/silverback_gutenberg/tests/graphql/.graphqlrc.json +5 -0
  72. package/drupal/silverback_gutenberg/tests/graphql/queries/editor.gql +30 -0
  73. package/drupal/silverback_gutenberg/tests/graphql/schema.graphqls +37 -0
  74. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/silverback_gutenberg_test_validator.info.yml +9 -0
  75. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/ColumnValidator.php +42 -0
  76. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/GroupValidator.php +50 -0
  77. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/LinkValidator.php +43 -0
  78. package/drupal/silverback_gutenberg/tests/src/Kernel/BlockValidationRuleTest.php +194 -0
  79. package/drupal/silverback_gutenberg/tests/src/Kernel/EditorDirectivesTest.php +255 -0
  80. package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergLinkedContentEUTrackTest.php +133 -0
  81. package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergReferencedContentEUTrackTest.php +225 -0
  82. package/drupal/silverback_gutenberg/tests/src/Kernel/LinkProcessorTest.php +284 -0
  83. package/drupal/silverback_gutenberg/tests/src/Kernel/MediaNormalizerTest.php +174 -0
  84. package/drupal/silverback_gutenberg/tests/src/Traits/SampleAssetTrait.php +15 -0
  85. package/drupal/silverback_gutenberg/tests/src/Unit/BlockSerializerTest.php +27 -0
  86. package/drupal/silverback_gutenberg/tests/src/Unit/BlockValidatorCardinalityTest.php +1537 -0
  87. package/drupal/silverback_gutenberg/tests/src/Unit/EditorBlocksProcessorTest.php +159 -0
  88. package/drupal/silverback_gutenberg/tests/src/Unit/LinkedContentExtractorTest.php +65 -0
  89. package/drupal/silverback_gutenberg/tests/src/Unit/ReferencedContentExtractorTest.php +248 -0
  90. package/drupal/silverback_gutenberg/tests/src/assets/media/data.json +4 -0
  91. package/drupal/silverback_gutenberg/tests/src/assets/media/source.html +71 -0
  92. package/drupal/silverback_gutenberg/tests/src/assets/media/target.html +71 -0
  93. package/package.json +16 -0
  94. 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
+ }
@@ -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
+ }