Directives are block-level content elements that extend Markdown with custom rendering. Bengal uses MyST-style syntax (:::{name}) and provides a base class for creating your own directives.
Directive Basics
Directives in Bengal:
- Parse content using the MyST fenced syntax
- Support typed options with automatic coercion
- Can define parent-child nesting relationships
- Register automatically when added to the directive factory
Creating a Custom Directive
SubclassBengalDirectiveand implement the required methods:
from bengal.directives import BengalDirective, DirectiveToken
class AlertDirective(BengalDirective):
"""Custom alert box directive."""
# Required: Names that trigger this directive
NAMES = ["alert", "callout"]
# Required: Token type for the AST
TOKEN_TYPE = "alert"
def parse_directive(self, title, options, content, children, state):
"""Parse the directive into a token."""
return DirectiveToken(
type=self.TOKEN_TYPE,
attrs={
"level": title or "info",
"class": options.get("class", ""),
},
children=children,
)
def render(self, renderer, text, **attrs):
"""Render the token to HTML."""
level = attrs.get("level", "info")
css_class = attrs.get("class", "")
classes = f"alert alert-{level} {css_class}".strip()
return f'<div class="{classes}">{text}</div>'
Usage in Markdown:
:::{alert} warning
:class: my-custom-class
This is a warning message.
:::
Class Attributes
NAMES (Required)
List of directive names that trigger this directive:
NAMES = ["dropdown", "details", "collapsible"]
All names map to the same directive class.
TOKEN_TYPE (Required)
String identifier for the AST node:
TOKEN_TYPE = "dropdown"
Used internally to match parse and render phases.
OPTIONS_CLASS (Optional)
Typed dataclass for parsing directive options:
from bengal.directives import DirectiveOptions
from dataclasses import dataclass
@dataclass
class DropdownOptions(DirectiveOptions):
open: bool = False
icon: str | None = None
class DropdownDirective(BengalDirective):
NAMES = ["dropdown"]
TOKEN_TYPE = "dropdown"
OPTIONS_CLASS = DropdownOptions
def parse_directive(self, title, options, content, children, state):
# options is now a DropdownOptions instance
return DirectiveToken(
type=self.TOKEN_TYPE,
attrs={
"title": title or "Details",
"open": options.open, # Typed access
"icon": options.icon,
},
children=children,
)
CONTRACT (Optional)
Define valid parent-child nesting relationships:
from bengal.directives import DirectiveContract
class StepDirective(BengalDirective):
NAMES = ["step"]
TOKEN_TYPE = "step"
CONTRACT = DirectiveContract(requires_parent=("steps",))
This ensures:::{step}can only appear inside:::{steps}.
Methods to Implement
parse_directive
Build the AST token from parsed components:
def parse_directive(
self,
title: str, # Text after directive name
options: Options, # Parsed options (or DirectiveOptions instance)
content: str, # Raw content inside directive
children: list, # Parsed child tokens
state: Any, # Parser state (for advanced use)
) -> DirectiveToken:
return DirectiveToken(
type=self.TOKEN_TYPE,
attrs={"title": title},
children=children,
)
render
Convert the token to HTML:
def render(
self,
renderer: Any, # Patitas renderer (for rendering children)
text: str, # Pre-rendered HTML of children
**attrs: Any, # Attributes from parse_directive
) -> str:
title = attrs.get("title", "")
return f"<details><summary>{title}</summary>{text}</details>"
Built-in Utilities
The package provides HTML generation helpers:
from bengal.directives import (
escape_html, # Escape HTML entities
build_class_string, # Build CSS class string from list
class_attr, # Generate class="..." attribute
data_attrs, # Generate data-* attributes
bool_attr, # Generate boolean attributes (open, disabled)
)
def render(self, renderer, text, **attrs):
classes = build_class_string("alert", attrs.get("class", ""))
data = data_attrs(level=attrs.get("level"))
return f'<div class="{classes}" {data}>{text}</div>'
Example: Embed Directive
A directive for embedding external content:
from dataclasses import dataclass
from bengal.directives import (
BengalDirective,
DirectiveOptions,
DirectiveToken,
escape_html,
)
@dataclass
class EmbedOptions(DirectiveOptions):
width: str = "100%"
height: str = "400px"
title: str = ""
class EmbedDirective(BengalDirective):
"""Embed external content via iframe."""
NAMES = ["embed", "iframe"]
TOKEN_TYPE = "embed"
OPTIONS_CLASS = EmbedOptions
def parse_directive(self, title, options, content, children, state):
# Title contains the URL
url = title.strip() if title else ""
return DirectiveToken(
type=self.TOKEN_TYPE,
attrs={
"url": url,
"width": options.width,
"height": options.height,
"title": options.title or "Embedded content",
},
)
def render(self, renderer, text, **attrs):
url = escape_html(attrs.get("url", ""))
width = escape_html(attrs.get("width", "100%"))
height = escape_html(attrs.get("height", "400px"))
title = escape_html(attrs.get("title", ""))
return (
f'<iframe src="{url}" '
f'width="{width}" height="{height}" '
f'title="{title}" frameborder="0" '
f'allowfullscreen loading="lazy"></iframe>'
)
Usage:
:::{embed} https://www.youtube.com/embed/dQw4w9WgXcQ
:width: 560px
:height: 315px
:title: Video Tutorial
:::
Registration
Custom directives must be registered with the Patitas parser. The standard approach is to add them to the directive registry inbengal/directives/registry.py:
# In bengal/directives/registry.py, add to _DIRECTIVE_MAP:
_DIRECTIVE_MAP: dict[str, str] = {
# ... existing directives ...
"alert": "your_module.alert_directive",
"callout": "your_module.alert_directive",
}
Note
Currently, custom directives require modification to the Bengal codebase or a fork. A plugin system for external directive registration is on the roadmap.
Testing Directives
Test both parse and render phases by instantiating your directive class directly:
import pytest
from your_directives import AlertDirective # Your custom directive
def test_alert_directive_parse():
directive = AlertDirective()
# Test parse_directive
token = directive.parse_directive(
title="warning",
options={},
content="Test content",
children=[],
state=None,
)
assert token.attrs["level"] == "warning"
def test_alert_directive_render():
directive = AlertDirective()
html = directive.render(
renderer=None,
text="<p>Test content</p>",
level="warning",
)
assert 'class="alert alert-warning"' in html
assert "<p>Test content</p>" in html
For built-in directives, useget_directive()to retrieve registered classes:
from bengal.directives import get_directive
def test_dropdown_directive():
DropdownDirective = get_directive("dropdown")
directive = DropdownDirective()
# ... test implementation
Related
- Directives Reference for built-in directives
- Build Pipeline for understanding when directives are processed