Skip to content

Allow themes to extend ImageDirective and FigureDirective (currently final) #1303

@CybotTM

Description

@CybotTM

Feature request

Problem

ImageDirective and FigureDirective are declared final, which prevents downstream themes from extending them to customize behavior. The only option is to fork the entire class and replace the upstream definition via a DI compiler pass (removeDefinition()).

This is a significant maintenance burden: the forked code must be manually diffed and updated on every dependency upgrade.

Concrete use case (TYPO3 Documentation theme)

The TYPO3 Documentation rendering theme needs to:

  1. Detect deprecated CSS class names in :class: options (e.g., :class: float-left) and rewrite them to modern equivalents (float-start) before the node is constructed
  2. Emit deprecation warnings with accurate RST source file/line context (requires access to BlockContext)
  3. Strip float classes from inner images inside figures (float should apply to <figure>, not the inner <img>)

For FigureDirective, we already had a fork for zoom functionality, so the float handling was added on top. But for ImageDirective, we had to create a new fork solely because of final — copying processNode() and resolveLinkTarget() verbatim, just to add a process() override.

The forked ImageDirective is here: TYPO3-Documentation/render-guides PR #1179

What we currently have to do

// 1. Fork the entire class (copy processNode + resolveLinkTarget verbatim)
final class ImageDirective extends BaseDirective
{
    // ...copied verbatim from upstream v1.9.4...
    public function processNode(...): Node { /* exact copy */ }
    private function resolveLinkTarget(...): LinkInlineNode { /* exact copy */ }

    // The only addition we actually need:
    public function process(BlockContext $blockContext, Directive $directive): Node|null
    {
        // detect + rewrite deprecated classes
        // then delegate to parent::process()
    }
}

// 2. Remove upstream definition via compiler pass
public function process(ContainerBuilder $container): void
{
    if ($container->hasDefinition(BaseImageDirective::class)) {
        $container->removeDefinition(BaseImageDirective::class);
    }
}

Suggested solutions (in order of preference)

Option A: Remove final from ImageDirective and FigureDirective

The simplest change. Make processNode() protected instead of public, and allow themes to extend the class:

class ImageDirective extends BaseDirective  // no longer final
{
    protected function processNode(...): Node { ... }     // was public
    protected function resolveLinkTarget(...): LinkInlineNode { ... }  // was private
}

Downstream themes could then:

class ImageDirective extends BaseImageDirective
{
    public function process(BlockContext $blockContext, Directive $directive): Node|null
    {
        $this->rewriteClasses($directive);
        return parent::process($blockContext, $directive);
    }
}

No verbatim code copying, no drift risk.

Option B: Add a pre-processing hook/event

If you prefer to keep final, add an event or hook that fires before the node is constructed, allowing themes to transform directive options:

// In BaseDirective::process() or DirectiveRule
$this->eventDispatcher->dispatch(new PreProcessDirectiveEvent($blockContext, $directive));

This would be useful for any directive customization, not just images/figures.

Option C: Make BaseDirective::process() call a template method

Add an overridable method that runs before node construction:

abstract class BaseDirective
{
    public function process(BlockContext $blockContext, Directive $directive): Node|null
    {
        $this->preProcess($blockContext, $directive);  // hook point
        $node = $this->processNode($blockContext, $directive);
        // ...
    }

    protected function preProcess(BlockContext $blockContext, Directive $directive): void
    {
        // no-op by default, themes can override
    }
}

Context

This came up while modernizing from Bootstrap 4 float-left/float-right to Bootstrap 5 float-start/float-end in the TYPO3 docs theme. The :align: mapping is theme-specific (different themes use different CSS frameworks), but the ability to intercept and transform directive options before node construction is a general need.

I'd be happy to submit a PR for whichever approach you prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions