lepture/mistune

Plugin design does not play nice with new renderers

Closed this issue · 5 comments

In your current design plugins do also implement the respective renderer functionality.
This leads to a conflict between implementing new renderers and plugins.

For example, when at the moment I would supply my own renderer I don't see a way to use plugins without needing to change your code base.
With a custom renderer I'm therefore unable to use the existing plugins.

I have no clear solution for that.
One way might be to redesign the plugin to a class like structure similar to renderer.
This then would at least allow to overwrite and implement necessary methods by inheriting from the existing plugin classes and than just supply the overwritten plugin classes to markdown = Markdown(plugins=[MyPlugin]).

I definitely feel like you should think about redesigning your current plugin implementation.
Maybe analogical to the Directive implementation.

I would like to propose a first version of a Plugin class. For an application see: #267 (comment)

@lepture What do you think? I can open up a pull request (also adjusting the existing plugins).

import re
from abc import ABC, abstractmethod
from re import Match, Pattern
from typing import Any, Callable, Dict

from mistune.block_parser import BlockParser
from mistune.inline_parser import InlineParser
from mistune.markdown import Markdown
from mistune.renderers import BaseRenderer
from mistune.scanner import ScannerParser


class Plugin(ABC):

    def __call__(self, markdown: Markdown):
        self.register(markdown)

    @property
    @abstractmethod
    def name(self) -> str:
        pass

    @property
    @abstractmethod
    def pattern(self) -> Pattern:
        pass

    @property
    def renderers(self) -> Dict[str, Callable]:
        return {
            'html': self.render_html,
            'ast': self.render_ast,
        }

    def _get_render_method(self, renderer_name: str) -> Callable:
        if renderer_name not in self.renderers:
            raise NotImplementedError(
                f"Plugin {self.name} does not implement a render method for {renderer_name}")

        return self.renderers[renderer_name]

    def _register_parser(self, parser: ScannerParser):
        parser.register_rule(self.name, self.pattern, self.parse)
        parser.rules.append(self.name)

    def _register_renderer(self, renderer: BaseRenderer):
        renderer.register(self.name, self._get_render_method(renderer.NAME))

    @abstractmethod
    def register(self, markdown: Markdown) -> None:
        pass

    @abstractmethod
    def parse(self, parser: ScannerParser, match: Match, state: Dict[str, Any]) -> Dict[str, Any]:
        pass

    @abstractmethod
    def render_html(self, *args) -> str:
        pass

    @abstractmethod
    def render_ast(self, *args) -> Dict[str, Any]:
        pass


class InlinePlugin(Plugin, ABC):

    @abstractmethod
    def parse(self, parser: InlineParser, match: Match, state: Dict[str, Any]) -> Dict[str, Any]:
        pass

    def register(self, markdown: Markdown) -> None:
        self._register_parser(markdown.inline)
        self._register_renderer(markdown.renderer)


class BlockPlugin(Plugin, ABC):

    @abstractmethod
    def parse(self, parser: BlockParser, match: Match, state: Dict[str, Any]) -> Dict[str, Any]:
        pass

    def register(self, markdown: Markdown) -> None:
        self._register_parser(markdown.block)
        self._register_renderer(markdown.renderer)

The problem here is that many plugins are not pure inline or block. And they may have serval renderers. Checkout table plugin.

With a custom renderer I'm therefore unable to use the existing plugins.

What do you mean? Can you give me an example?

@Spenhouet to use plugins with a custom renderer, you just need to provide the methods associated with the plugin you want to use to your renderer.
I guess those methods are separated from the renderer and added when a plugin is loaded in order to limit the leakage of code dedicated to plugins in the renderers, but that's about all.

no updates from the author, close it for now.