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.