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!"
}
Source
.. data.render::
   :extra: cat

   {{ load_extra('cat').name }}
Result

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


Source
.. data.render::

   {{ "Hello world" | catify }}
Result

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

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:

Now use the directive in your document:

Source
.. mimi::
Result

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 method current_raw_data()).

  • The generated RawData are parsed to ParsedData according to the Schema returned from current_schema() method.

    Tip

    Internally, the ParsedData is returned by current_context, so we do not need to implement it.

  • The the fields of schema are generated from Field Description Language which restricted the color must be an space-separated list, and birth must be a integer.

  • The current_template still returns a Jinja template, but it uses more fancy syntax.

Use the directive in your document:

Source
.. cat2:: mimi
   :color: black and brown
   :birth: 2025

   I like fish!
Result

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 from Schema.name

  • option_spec - derived from Schema.attrs

  • has_content - derived from Schema.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:

Source
.. cat3:: mimi
   :color: black and brown
   :birth: 2025

   I like fish!
Result

Hi human! I am a cat named mimi, I have black and brown fur. I am 1 years old.

I like fish!.