From 1862f138db88366db718cbe608cc4a718e05e51f Mon Sep 17 00:00:00 2001
From: Matthias Pigulla
Date: Wed, 27 May 2026 18:28:55 +0200
Subject: [PATCH 1/4] Remove dangling `` tags inside shortcodes with text
content
---
README.md | 11 +++++++
.../Compiler/ShortcodeCompilerPass.php | 10 ++++++
src/DependencyInjection/Configuration.php | 4 +++
.../WebfactoryShortcodeExtension.php | 7 ++++
...aphElementsInsideShortcodeEventHandler.php | 32 +++++++++++++++++++
src/Resources/config/shortcodes.php | 13 ++++----
6 files changed, 71 insertions(+), 6 deletions(-)
create mode 100644 src/Handler/RemovePendingParagraphElementsInsideShortcodeEventHandler.php
diff --git a/README.md b/README.md
index 1514718..92e0f1d 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,17 @@ services:
- { name: 'webfactory.shortcode', shortcode: 'my-shortcode-name' }
```
+### Registering Event Listeners
+
+The [thunderer/Shortcode](https://github.com/thunderer/Shortcode) package uses an `EventContainer` to dispatch events during shortcode processing. You can register your own event listeners by tagging services with `webfactory.shortcode.event_listener` and specifying the `event` attribute:
+
+```yaml
+services:
+ My\Shortcode\EventListener:
+ tags:
+ - { name: 'webfactory.shortcode.event_listener', event: 'event.replace-shortcodes' }
+```
+
### Removing `
` Tags around Shortcodes
By default, the `RemoveWrappingParagraphElementsEventHandler` contained in this bundle will be used to remove `
...
` tags around shortcodes, if the shortcode is the only text content in that paragraph.
diff --git a/src/DependencyInjection/Compiler/ShortcodeCompilerPass.php b/src/DependencyInjection/Compiler/ShortcodeCompilerPass.php
index 43d1f70..9f53fed 100644
--- a/src/DependencyInjection/Compiler/ShortcodeCompilerPass.php
+++ b/src/DependencyInjection/Compiler/ShortcodeCompilerPass.php
@@ -5,6 +5,7 @@
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
+use Thunder\Shortcode\EventContainer\EventContainer;
use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Webfactory\ShortcodeBundle\Controller\GuideController;
@@ -25,6 +26,15 @@ public function process(ContainerBuilder $container): void
}
}
+ // add services tagged with webfactory.shortcode.event_listener as listeners to the event container
+ $eventListenerServices = $container->findTaggedServiceIds('webfactory.shortcode.event_listener');
+ $eventContainer = $container->findDefinition(EventContainer::class);
+ foreach ($eventListenerServices as $id => $tags) {
+ foreach ($tags as $tag) {
+ $eventContainer->addMethodCall('addListener', [$tag['event'], new Reference($id)]);
+ }
+ }
+
// prepare the GuideController if it's configuration is imported
if ($container->has(GuideController::class)) {
$allShortcodeTags = [];
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 58365ab..8385acb 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -29,6 +29,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->info('Limit the number of iterations when resolving shortcodes')
->defaultValue(null)
->end()
+ ->booleanNode('remove_pending_inner_paragraph_elements')
+ ->info('Remove
... inside shortcodes with text content (using opening and closing shortcode markup)')
+ ->defaultFalse()
+ ->end()
->arrayNode('shortcodes')
->normalizeKeys(false)
->arrayPrototype()
diff --git a/src/DependencyInjection/WebfactoryShortcodeExtension.php b/src/DependencyInjection/WebfactoryShortcodeExtension.php
index 685c114..65df0d5 100644
--- a/src/DependencyInjection/WebfactoryShortcodeExtension.php
+++ b/src/DependencyInjection/WebfactoryShortcodeExtension.php
@@ -7,6 +7,8 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
+use Thunder\Shortcode\Events;
+use Webfactory\ShortcodeBundle\Handler\RemovePendingParagraphElementsInsideShortcodeEventHandler;
/**
* Loads the bundle configuration.
@@ -27,6 +29,11 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('webfactory_shortcode.recursion_depth', $config['recursion_depth']);
$container->setParameter('webfactory_shortcode.max_iterations', $config['max_iterations']);
+ if ($config['remove_pending_inner_paragraph_elements']) {
+ $container->getDefinition(RemovePendingParagraphElementsInsideShortcodeEventHandler::class)
+ ->clearTag('webfactory.shortcode.event_listener');
+ }
+
foreach ($config['shortcodes'] as $shortcodeName => $shortcodeDefinition) {
$definition = new ChildDefinition('Webfactory\ShortcodeBundle\Handler\EmbeddedShortcodeHandler.'.$shortcodeDefinition['method']);
$definition->replaceArgument(1, $shortcodeDefinition['controller']);
diff --git a/src/Handler/RemovePendingParagraphElementsInsideShortcodeEventHandler.php b/src/Handler/RemovePendingParagraphElementsInsideShortcodeEventHandler.php
new file mode 100644
index 0000000..b2eb8f1
--- /dev/null
+++ b/src/Handler/RemovePendingParagraphElementsInsideShortcodeEventHandler.php
@@ -0,0 +1,32 @@
+...
" directly inside a shortcode.
+ *
+ * This may happen when using shortcodes with content like the following, where the outer "
...
" will be removed
+ * by `RemoveWrappingParagraphElementsEventHandler`.
+ *
+ * [shortcode]
Inner content
[/shortcode]
+ */
+final class RemovePendingParagraphElementsInsideShortcodeEventHandler
+{
+ public function __invoke(ReplaceShortcodesEvent $event): void
+ {
+ if (!$event->getShortcode()) {
+ return;
+ }
+
+ $text = $event->getText();
+
+ if (
+ preg_match('~^\s*\s*~', $text, $prefixMatch)
+ && preg_match('~\s*\s*$~', $text, $postfixMatch)
+ ) {
+ $event->setResult(mb_substr($text, mb_strlen($prefixMatch[0], 'utf-8'), -mb_strlen($postfixMatch[0], 'utf-8')));
+ }
+ }
+}
diff --git a/src/Resources/config/shortcodes.php b/src/Resources/config/shortcodes.php
index 2f80088..c463638 100644
--- a/src/Resources/config/shortcodes.php
+++ b/src/Resources/config/shortcodes.php
@@ -2,7 +2,6 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
-use function Symfony\Component\DependencyInjection\Loader\Configurator\inline_service;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
return static function(ContainerConfigurator $container) {
@@ -31,11 +30,13 @@
->public()
->factory([service(\Webfactory\ShortcodeBundle\Factory\ProcessorFactory::class), 'create']);
- $services->set(\Thunder\Shortcode\EventContainer\EventContainer::class)
- ->call('addListener', [
- \Thunder\Shortcode\Events::REPLACE_SHORTCODES,
- inline_service(\Webfactory\ShortcodeBundle\Handler\RemoveWrappingParagraphElementsEventHandler::class),
- ]);
+ $services->set(\Thunder\Shortcode\EventContainer\EventContainer::class);
+
+ $services->set(\Webfactory\ShortcodeBundle\Handler\RemoveWrappingParagraphElementsEventHandler::class)
+ ->tag('webfactory.shortcode.event_listener', ['event' => \Thunder\Shortcode\Events::REPLACE_SHORTCODES]);
+
+ $services->set(\Webfactory\ShortcodeBundle\Handler\RemovePendingParagraphElementsInsideShortcodeEventHandler::class)
+ ->tag('webfactory.shortcode.event_listener', ['event' => \Thunder\Shortcode\Events::REPLACE_SHORTCODES]);
$services->set('Webfactory\ShortcodeBundle\Handler\EmbeddedShortcodeHandler.esi', \Webfactory\ShortcodeBundle\Handler\EmbeddedShortcodeHandler::class)
->abstract()
From 8b161eb0add4a7779236fdebb978ec28c2ea0436 Mon Sep 17 00:00:00 2001
From: Matthias Pigulla
Date: Wed, 27 May 2026 18:40:29 +0200
Subject: [PATCH 2/4] Add a test
---
src/DependencyInjection/WebfactoryShortcodeExtension.php | 2 +-
tests/Fixtures/TestKernel.php | 1 +
tests/Functional/EndToEndTest.php | 9 +++++++++
3 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/DependencyInjection/WebfactoryShortcodeExtension.php b/src/DependencyInjection/WebfactoryShortcodeExtension.php
index 65df0d5..2c1511e 100644
--- a/src/DependencyInjection/WebfactoryShortcodeExtension.php
+++ b/src/DependencyInjection/WebfactoryShortcodeExtension.php
@@ -29,7 +29,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('webfactory_shortcode.recursion_depth', $config['recursion_depth']);
$container->setParameter('webfactory_shortcode.max_iterations', $config['max_iterations']);
- if ($config['remove_pending_inner_paragraph_elements']) {
+ if (! $config['remove_pending_inner_paragraph_elements']) {
$container->getDefinition(RemovePendingParagraphElementsInsideShortcodeEventHandler::class)
->clearTag('webfactory.shortcode.event_listener');
}
diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php
index 4382d08..8fb86a2 100644
--- a/tests/Fixtures/TestKernel.php
+++ b/tests/Fixtures/TestKernel.php
@@ -42,6 +42,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
$testControllerAction = ShortcodeTestController::class.'::test';
$container->loadFromExtension('webfactory_shortcode', [
+ 'remove_pending_inner_paragraph_elements' => true,
'shortcodes' => [
'test-config-inline' => $testControllerAction,
'test-config-esi' => [
diff --git a/tests/Functional/EndToEndTest.php b/tests/Functional/EndToEndTest.php
index 947280d..42552c9 100644
--- a/tests/Functional/EndToEndTest.php
+++ b/tests/Functional/EndToEndTest.php
@@ -24,6 +24,15 @@ public function paragraphs_wrapping_shortcodes_get_removed(): void
);
}
+ #[Test]
+ public function paragraphs_inside_shortcode_with_content_get_removed(): void
+ {
+ $this->assertEquals(
+ 'the answer is 42
',
+ $this->renderTwig("{{ '[placeholder value=42]
the answer is %value%
[/placeholder]
' | shortcodes }}")
+ );
+ }
+
#[Test]
public function content_without_shortcodes_wont_be_changed(): void
{
From 0a49f019befc54f3e0bdcf7415c94c749d544748 Mon Sep 17 00:00:00 2001
From: Matthias Pigulla
Date: Wed, 27 May 2026 18:52:10 +0200
Subject: [PATCH 3/4] Avoid mocking Symfony classes in functional tests
---
.../Compiler/ShortcodeCompilerPassTest.php | 136 +++++++-----------
1 file changed, 49 insertions(+), 87 deletions(-)
diff --git a/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php b/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
index 1ce6046..cd3f2d1 100644
--- a/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
+++ b/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
@@ -3,132 +3,94 @@
namespace Webfactory\ShortcodeBundle\Tests\DependencyInjection\Compiler;
use PHPUnit\Framework\Attributes\Test;
-use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
+use Thunder\Shortcode\EventContainer\EventContainer;
use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Webfactory\ShortcodeBundle\Controller\GuideController;
use Webfactory\ShortcodeBundle\DependencyInjection\Compiler\ShortcodeCompilerPass;
final class ShortcodeCompilerPassTest extends TestCase
{
- /**
- * System under test.
- *
- * @var ShortcodeCompilerPass
- */
- private $compilerPass;
-
- /** @var ContainerBuilder|MockObject */
- private $containerBuilder;
+ private ShortcodeCompilerPass $compilerPass;
+ private ContainerBuilder $containerBuilder;
protected function setUp(): void
{
$this->compilerPass = new ShortcodeCompilerPass();
- $this->containerBuilder = $this->getMockBuilder(ContainerBuilder::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->containerBuilder = new ContainerBuilder();
+
+ // Register the service definitions that the compiler pass always looks up
+ $this->containerBuilder->setDefinition(HandlerContainer::class, new Definition(HandlerContainer::class));
+ $this->containerBuilder->setDefinition(EventContainer::class, new Definition(EventContainer::class));
}
#[Test]
public function tagged_services_are_added_as_handlers_to_handler_container(): void
{
- $this->containerBuilder->expects($this->once())
- ->method('findTaggedServiceIds')
- ->willReturn([
- 'service_id1' => [
- ['shortcode' => 'shortcode1'],
- ],
- 'service_id2' => [
- ['shortcode' => 'shortcode2'],
- ],
- ]);
-
- $mockedShortcodeHandlerContainer = $this->createMock(Definition::class);
- $mockedShortcodeHandlerContainer->expects($this->exactly(2))
- ->method('addMethodCall')
- ->with('add', $this->callback(function (array $argument) {
- static $count = 0;
- ++$count;
-
- return 'shortcode'.$count === $argument[0] && $argument[1] instanceof Reference;
- }));
-
- $this->containerBuilder->expects($this->once())
- ->method('findDefinition')
- ->with(HandlerContainer::class)
- ->willReturn($mockedShortcodeHandlerContainer);
+ $this->containerBuilder->register('service_id1', \stdClass::class)
+ ->addTag('webfactory.shortcode', ['shortcode' => 'shortcode1']);
+ $this->containerBuilder->register('service_id2', \stdClass::class)
+ ->addTag('webfactory.shortcode', ['shortcode' => 'shortcode2']);
$this->compilerPass->process($this->containerBuilder);
+
+ $methodCalls = $this->containerBuilder->getDefinition(HandlerContainer::class)->getMethodCalls();
+
+ $this->assertCount(2, $methodCalls);
+
+ [$method1, $args1] = $methodCalls[0];
+ $this->assertSame('add', $method1);
+ $this->assertSame('shortcode1', $args1[0]);
+ $this->assertInstanceOf(Reference::class, $args1[1]);
+ $this->assertSame('service_id1', (string) $args1[1]);
+
+ [$method2, $args2] = $methodCalls[1];
+ $this->assertSame('add', $method2);
+ $this->assertSame('shortcode2', $args2[0]);
+ $this->assertInstanceOf(Reference::class, $args2[1]);
+ $this->assertSame('service_id2', (string) $args2[1]);
}
#[Test]
public function no_tagged_services_do_no_harm(): void
{
- $this->containerBuilder->expects($this->once())
- ->method('findTaggedServiceIds')
- ->willReturn([]);
-
$this->compilerPass->process($this->containerBuilder);
+
+ $this->assertCount(
+ 0,
+ $this->containerBuilder->getDefinition(HandlerContainer::class)->getMethodCalls()
+ );
}
#[Test]
public function shortcode_guide_service_gets_configured_if_set(): void
{
- $this->containerBuilder->expects($this->once())
- ->method('findTaggedServiceIds')
- ->willReturn([
- 'service_id1' => [
- ['shortcode' => 'shortcode1'],
- ],
- 'service_id2' => [
- ['shortcode' => 'shortcode2'],
- ],
- ]);
-
- $this->containerBuilder->expects($this->once())
- ->method('findDefinition')
- ->with(HandlerContainer::class)
- ->willReturn($this->createMock(Definition::class));
-
- $this->containerBuilder->expects($this->once())
- ->method('has')
- ->with(GuideController::class)
- ->willReturn(true);
-
- $mockedShortcodeGuideServiceDefinition = $this->createMock(Definition::class);
- $mockedShortcodeGuideServiceDefinition->expects($this->once())
- ->method('setArgument')
- ->with(
- 0,
- [
- ['shortcode' => 'shortcode1'],
- ['shortcode' => 'shortcode2'],
- ]
- );
-
- $this->containerBuilder->expects($this->once())
- ->method('getDefinition')
- ->with(GuideController::class)
- ->willReturn($mockedShortcodeGuideServiceDefinition);
+ $this->containerBuilder->register('service_id1', \stdClass::class)
+ ->addTag('webfactory.shortcode', ['shortcode' => 'shortcode1']);
+ $this->containerBuilder->register('service_id2', \stdClass::class)
+ ->addTag('webfactory.shortcode', ['shortcode' => 'shortcode2']);
+
+ $this->containerBuilder->setDefinition(GuideController::class, new Definition(GuideController::class));
$this->compilerPass->process($this->containerBuilder);
+
+ $this->assertSame(
+ [
+ ['shortcode' => 'shortcode1'],
+ ['shortcode' => 'shortcode2'],
+ ],
+ $this->containerBuilder->getDefinition(GuideController::class)->getArgument(0)
+ );
}
#[Test]
public function missing_shortcode_guide_service_does_no_harm(): void
{
- $this->containerBuilder->expects($this->once())
- ->method('findTaggedServiceIds')
- ->willReturn([]);
-
- $this->containerBuilder->expects($this->once())
- ->method('has')
- ->with(GuideController::class)
- ->willReturn(false);
-
$this->compilerPass->process($this->containerBuilder);
+
+ $this->assertFalse($this->containerBuilder->has(GuideController::class));
}
}
From b8349d1f1419209b620d26f47f840c6cd79c01ec Mon Sep 17 00:00:00 2001
From: mpdude <1202333+mpdude@users.noreply.github.com>
Date: Wed, 27 May 2026 16:52:40 +0000
Subject: [PATCH 4/4] Fix CS with PHP-CS-Fixer
---
src/DependencyInjection/WebfactoryShortcodeExtension.php | 3 +--
.../Compiler/ShortcodeCompilerPassTest.php | 9 +++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/DependencyInjection/WebfactoryShortcodeExtension.php b/src/DependencyInjection/WebfactoryShortcodeExtension.php
index 2c1511e..12f9d56 100644
--- a/src/DependencyInjection/WebfactoryShortcodeExtension.php
+++ b/src/DependencyInjection/WebfactoryShortcodeExtension.php
@@ -7,7 +7,6 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
-use Thunder\Shortcode\Events;
use Webfactory\ShortcodeBundle\Handler\RemovePendingParagraphElementsInsideShortcodeEventHandler;
/**
@@ -29,7 +28,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('webfactory_shortcode.recursion_depth', $config['recursion_depth']);
$container->setParameter('webfactory_shortcode.max_iterations', $config['max_iterations']);
- if (! $config['remove_pending_inner_paragraph_elements']) {
+ if (!$config['remove_pending_inner_paragraph_elements']) {
$container->getDefinition(RemovePendingParagraphElementsInsideShortcodeEventHandler::class)
->clearTag('webfactory.shortcode.event_listener');
}
diff --git a/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php b/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
index cd3f2d1..0e6e679 100644
--- a/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
+++ b/tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
@@ -4,6 +4,7 @@
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
+use stdClass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
@@ -30,9 +31,9 @@ protected function setUp(): void
#[Test]
public function tagged_services_are_added_as_handlers_to_handler_container(): void
{
- $this->containerBuilder->register('service_id1', \stdClass::class)
+ $this->containerBuilder->register('service_id1', stdClass::class)
->addTag('webfactory.shortcode', ['shortcode' => 'shortcode1']);
- $this->containerBuilder->register('service_id2', \stdClass::class)
+ $this->containerBuilder->register('service_id2', stdClass::class)
->addTag('webfactory.shortcode', ['shortcode' => 'shortcode2']);
$this->compilerPass->process($this->containerBuilder);
@@ -68,9 +69,9 @@ public function no_tagged_services_do_no_harm(): void
#[Test]
public function shortcode_guide_service_gets_configured_if_set(): void
{
- $this->containerBuilder->register('service_id1', \stdClass::class)
+ $this->containerBuilder->register('service_id1', stdClass::class)
->addTag('webfactory.shortcode', ['shortcode' => 'shortcode1']);
- $this->containerBuilder->register('service_id2', \stdClass::class)
+ $this->containerBuilder->register('service_id2', stdClass::class)
->addTag('webfactory.shortcode', ['shortcode' => 'shortcode2']);
$this->containerBuilder->setDefinition(GuideController::class, new Definition(GuideController::class));