Custom Directives

Create your own directive handlers

1 min read 254 words

Create directive handlers to extend Markdown syntax.

DirectiveHandler Protocol

Implement theDirectiveHandlerprotocol:

from patitas.directives import DirectiveHandler
from patitas.nodes import Block, Directive
from patitas.location import SourceLocation
from patitas.stringbuilder import StringBuilder

class MyDirective:
    """Custom directive handler."""

    name = "my-directive"

    def parse(
        self,
        *,
        title: str,
        options: dict[str, str],
        content: str,
        location: SourceLocation | None = None,
    ) -> Block | None:
        """Parse directive into AST node."""
        return Directive(
            name=self.name,
            title=title,
            options=options,
            raw_content=content,
            children=(),
            location=location,
        )

    def render(
        self,
        node: Directive,
        out: StringBuilder,
        render_children: callable,
    ) -> None:
        """Render directive to HTML."""
        out.append(f'<div class="my-directive">')
        out.append(f'<h4>{node.title}</h4>')
        out.append(f'<p>{node.raw_content}</p>')
        out.append('</div>')

Registering Directives

UseDirectiveRegistry:

from patitas.directives import DirectiveRegistry, DirectiveRegistryBuilder

# Build a registry
builder = DirectiveRegistryBuilder()
builder.register(MyDirective())

registry = builder.build()

Options Parsing

Use typed options classes:

from patitas.directives import DirectiveOptions

class MyOptions(DirectiveOptions):
    color: str = "blue"
    size: int = 12
    enabled: bool = True

class MyDirective:
    name = "styled"
    options_class = MyOptions

    def parse(self, *, options: dict[str, str], **kwargs):
        parsed = MyOptions.from_dict(options)
        # parsed.color, parsed.size, parsed.enabled are typed

Contracts

Enforce nesting rules with contracts:

from patitas.directives import DirectiveContract

TAB_SET_CONTRACT = DirectiveContract(
    required_children=["tab-item"],
    allowed_children=["tab-item"],
)

class TabSetDirective:
    name = "tab-set"
    contract = TAB_SET_CONTRACT

Parse-Only and Render-Only

For simpler cases:

from patitas.directives import DirectiveParseOnly, DirectiveRenderOnly

class ParseOnlyDirective(DirectiveParseOnly):
    name = "parse-only"

    def parse(self, **kwargs) -> Block | None:
        # Only parsing, use default renderer
        pass

class RenderOnlyDirective(DirectiveRenderOnly):
    name = "render-only"

    def render(self, node, out, render_children) -> None:
        # Only rendering, use default parser
        pass