Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p>` Tags around Shortcodes

By default, the `RemoveWrappingParagraphElementsEventHandler` contained in this bundle will be used to remove `<p>...</p>` tags around shortcodes, if the shortcode is the only text content in that paragraph.
Expand Down
10 changes: 10 additions & 0 deletions src/DependencyInjection/Compiler/ShortcodeCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 = [];
Expand Down
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 </p>...<p> inside shortcodes with text content (using opening and closing shortcode markup)')
->defaultFalse()
->end()
->arrayNode('shortcodes')
->normalizeKeys(false)
->arrayPrototype()
Expand Down
6 changes: 6 additions & 0 deletions src/DependencyInjection/WebfactoryShortcodeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Webfactory\ShortcodeBundle\Handler\RemovePendingParagraphElementsInsideShortcodeEventHandler;

/**
* Loads the bundle configuration.
Expand All @@ -27,6 +28,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']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Webfactory\ShortcodeBundle\Handler;

use Thunder\Shortcode\Event\ReplaceShortcodesEvent;

/**
* Removes "</p>...<p>" directly inside a shortcode.
*
* This may happen when using shortcodes with content like the following, where the outer "<p>...</p>" will be removed
* by `RemoveWrappingParagraphElementsEventHandler`.
*
* <p>[shortcode]</p><p>Inner content</p><p>[/shortcode]</p>
*/
final class RemovePendingParagraphElementsInsideShortcodeEventHandler
{
public function __invoke(ReplaceShortcodesEvent $event): void
{
if (!$event->getShortcode()) {
return;
}

$text = $event->getText();

if (
preg_match('~^\s*</p>\s*~', $text, $prefixMatch)
&& preg_match('~\s*<p>\s*$~', $text, $postfixMatch)
) {
$event->setResult(mb_substr($text, mb_strlen($prefixMatch[0], 'utf-8'), -mb_strlen($postfixMatch[0], 'utf-8')));
}
}
}
13 changes: 7 additions & 6 deletions src/Resources/config/shortcodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down
137 changes: 50 additions & 87 deletions tests/DependencyInjection/Compiler/ShortcodeCompilerPassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,132 +3,95 @@
namespace Webfactory\ShortcodeBundle\Tests\DependencyInjection\Compiler;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use stdClass;
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));
}
}
1 change: 1 addition & 0 deletions tests/Fixtures/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [
Expand Down
9 changes: 9 additions & 0 deletions tests/Functional/EndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<p>the answer is 42</p>',
$this->renderTwig("{{ '<p>[placeholder value=42]</p><p>the answer is %value%</p><p>[/placeholder]</p>' | shortcodes }}")
);
}

#[Test]
public function content_without_shortcodes_wont_be_changed(): void
{
Expand Down