Extending¶
Extending the FDL¶
You can extend the Field Description Language by registering custom types, flags, and by-options
through the data attribute of
sphinxnotes.render.REGISTRY.
Adding Custom Types¶
Use add_type() method of
sphinxnotes.render.REGISTRY to add a new type:
>>> from sphinxnotes.render import REGISTRY, Field
>>>
>>> def parse_color(v: str):
... return tuple(int(x) for x in v.split(';'))
...
>>> def color_to_str(v):
... return ';'.join(str(x) for x in v)
...
>>> REGISTRY.data.add_type('color', tuple, parse_color, color_to_str)
>>> Field.from_dsl('color').parse('255;0;0')
(255, 0, 0)
Adding Custom Flags¶
Use add_flag() method of
sphinxnotes.render.REGISTRY to add a new flag:
>>> from sphinxnotes.render import REGISTRY, Field
>>> REGISTRY.data.add_flag('unique', default=False)
>>> field = Field.from_dsl('int, unique')
>>> field.unique
True
Adding Custom By-Options¶
Use add_by_option() method of
sphinxnotes.render.REGISTRY to add a new by-option:
>>> from sphinxnotes.render import REGISTRY, Field
>>> REGISTRY.data.add_by_option('group', str)
>>> field = Field.from_dsl('str, group by size')
>>> field.group
'size'
>>> REGISTRY.data.add_by_option('index', str, store='append')
>>> field = Field.from_dsl('str, index by month, index by year')
>>> field.index
['month', 'year']
Extending Extra Contexts¶
Extra contexts are registered by a
@sphinxnotes.render.extra_context class decorator.
The decorated class must be one of the following classes:
ParsingPhaseExtraContext,
ParsedPhaseExtraContext,
ResolvingPhaseExtraContext,
GlobalExtraContext.
from os import path
import json
from sphinx.environment import BuildEnvironment
from sphinxnotes.render import (
extra_context,
GlobalExtraContext,
)
@extra_context('cat')
class CatExtraContext(GlobalExtraContext):
def generate(self, env: BuildEnvironment):
with open(path.join(path.dirname(__file__), 'cat.json')) as f:
return json.loads(f.read())
cat.json
{
"name": "mimi",
"attrs": {
"color": "black and brown"
},
"content": "I like fish!"
}
.. data.render::
:extra: cat
{{ load_extra('cat').name }}
mimi
Extending ilters¶
Template filters are registered by a
@sphinxnotes.render.filter function decorator.
The decorated function takes a sphinx.environment.BuildEnvironment
as argument and returns a filter function.
Note
The decorator is used to decorate the filter function factory, NOT the filter function itself.
@filter('catify')
def catify(_: BuildEnvironment):
"""Speak in a cat-like tone"""
def _filter(value: str) -> str:
return value + ', meow~'
return _filter
.. data.render::
{{ "Hello world" | catify }}
Hello world, meow~
Extending Directives/Roles¶
Tip
Before reading this documentation, please refer to
Extending syntax with roles and directives.
See how to extend SphinxDirective and SphinxRole.
All of the classes listed in Roles and Directives are subclassed from the
internal sphinxnotes.render.Pipeline class, which is responsible to generate
the dedicated node that
carries a Main Context and a Template.
At the appropriate Render Phases, the node will be rendered into markup text, usually reStructuredText. The rendered text is then parsed again by Sphinx and inserted into the document.
See also
Templating for template variables, phases, and extra context
Field Description Language for the field description language used by
FieldandSchemaImplementations of
sphinxnotes-render.extandsphinxnotes-any.
Subclassing BaseContextDirective¶
Now we have a quick example to help you get Started.
Create a Sphinx documentation
with the following conf.py:
from sphinx.application import Sphinx
from sphinxnotes.render import ParsedData, BaseContextDirective, Template, Phase
class MimiDirective(BaseContextDirective):
def current_context(self):
return ParsedData(
name='mimi',
attrs={'color': 'black and brown'},
content='I like fish!',
)
def current_template(self):
return Template(
'Hi human! I am a cat named {{ name }}, I have {{ color }} fur.\n\n'
'{{ content }}.',
phase=Phase.Parsing,
)
def setup(app: Sphinx):
app.setup_extension('sphinxnotes.render')
app.add_directive('mimi', MimiDirective)
This is the smallest useful extension built on top of sphinxnotes.render:
it defines a mimi-dedicated directive by subclassing
BaseContextDirectiveit returns a
ResolvedContextobject fromcurrent_context()it returns a
Templatefromcurrent_template()the template is rendered in the default
Parsingphase
Now use the directive in your document:
.. mimi::
Hi human! I am a cat named mimi, I have black and brown fur.
I like fish!.
Subclassing BaseDataDefineDirective¶
BaseDataDefineDirective is higher level of API than BaseContextDirective.
You no longer need to implement the current_context methods; instead,
implement the current_schema()
method.
Here’s an example:
from datetime import datetime
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinxnotes.render import (
BaseDataDefineDirective,
Schema,
Field,
Template,
)
class CatDirective(BaseDataDefineDirective):
required_arguments = 1
option_spec = {
'color': directives.unchanged,
'birth': directives.unchanged,
}
has_content = True
def current_schema(self):
return Schema(
name=Field.from_dsl('str'),
attrs={
'color': Field.from_dsl('list of str'),
'birth': Field.from_dsl('int'),
},
content=Field.from_dsl('str'),
)
def current_template(self):
year = datetime.now().year
return Template(
'Hi human! I am a cat named {{ name }}, I have {{ "and".join(color) }} fur.\n'
f'I am {{{{ {year} - birth }}}} years old.\n\n'
'{{ content }}.'
)
def setup(app: Sphinx):
app.setup_extension('sphinxnotes.render')
app.add_directive('cat2', CatDirective)
Key differences from BaseContextDirective:
The directive automatically generates
RawData(from directive’s arguments, options, and content, by methodcurrent_raw_data()).The generated RawData are parsed to
ParsedDataaccording to theSchemareturned fromcurrent_schema()method.Tip
Internally, the
ParsedDatais returned bycurrent_context, so we do not need to implement it.The the fields of schema are generated from Field Description Language which restricted the
colormust be an space-separated list, andbirthmust be a integer.The
current_templatestill returns a Jinja template, but it uses more fancy syntax.
Use the directive in your document:
.. cat2:: mimi
:color: black and brown
:birth: 2025
I like fish!
Hi human! I am a cat named mimi, I have black and brown fur. I am 1 years old.
I like fish!.
Subclassing StrictDataDefineDirective¶
StrictDataDefineDirective is an even higher-level API built on top of
BaseDataDefineDirective. It automatically handles SphinxDirective’s members
from your Schema, so you don’t need to manually
set:
required_arguments/optional_arguments- derived fromSchema.nameoption_spec- derived fromSchema.attrshas_content- derived fromSchema.content
You no longer need to manually create subclasses, simply pass schema and
template to derive()
method:
from datetime import datetime
from sphinx.application import Sphinx
from sphinxnotes.render import (
StrictDataDefineDirective,
Schema,
Field,
Template,
)
schema = Schema(
name=Field.from_dsl('str'),
attrs={
'color': Field.from_dsl('list of str'),
'birth': Field.from_dsl('int'),
},
content=Field.from_dsl('str'),
)
template = Template(
'Hi human! I am a cat named {{ name }}, I have {{ "and".join(color) }} fur.\n'
f'I am {{{{ {datetime.now().year} - birth }}}} years old.\n\n'
'{{ content }}.'
)
CatDirective = StrictDataDefineDirective.derive('cat', schema, template)
def setup(app: Sphinx):
app.setup_extension('sphinxnotes.render')
app.add_directive('cat3', CatDirective)
Use the directive in your document:
.. cat3:: mimi
:color: black and brown
:birth: 2025
I like fish!
Hi human! I am a cat named mimi, I have black and brown fur. I am 1 years old.
I like fish!.