Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/conf.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
=============
Configuration
=============

The extension provides the following configuration:

.. autoconfval:: data_define_directives

A dictionary ``dict[str, directive_def]`` for creating custom directives for
data definition.

The ``str`` key is the name of the directive to be created;
The ``directive_def`` value is a ``dict`` with the following keys:

- ``schema`` (dict): Schema definition, with keys:

- ``name`` (str, optional): The DSL for the name field. Defaults to
``'str, required, uniq, ref'``.
- ``attrs`` (dict, optional): A mapping of attribute names to DSL.
Defaults to ``{}``.
- ``content`` (str, optional): The DSL for content. Defaults to ``'str'``.

- ``template`` (dict): Template definition, with keys:

- ``text`` (str, required): The Jinja2 template text.
- ``on`` (str, optional): The render phase. Defaults to ``'parsing'``.
Available values: ``'parsing'``, ``'parsed'``, ``'resolving'``.
- ``debug`` (bool, optional): Enable debug output. Defaults to ``False``.

Example:

.. code-block:: python

data_define_directives = {
'custom': {
'schema': {
'name': 'str, required',
'attrs': {'type': 'str'},
},
'template': {
'on': 'parsing',
'text': 'Custom: {{ name }} (type: {{ attrs.type }})',
},
},
}

This creates a ``.. custom::`` directive that requires a name argument
and accepts a ``type`` option.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Contents
:caption: Contents

usage
conf
changelog

The Sphinx Notes Project
Expand Down
82 changes: 82 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,85 @@ Directives
.. data.render::

Sphinx has **{{ _sphinx.extensions | length }}** extensions loaded.


Defining Custom Directives
===========================

Instead of using :rst:dir:`data.define`, :rst:dir:`data.template`, and
:rst:dir:`data.schema` directives to define data in documents, you can define
custom directives in :file:`conf.py` using the :confval:`data_define_directives`
configuration option.

This is useful when you want to create a reusable directive with a fixed schema
and template across multiple documents.

Basic Usage
-----------

First, configure the directive in your :file:`conf.py`:

.. code-block:: python

extensions = ['sphinxnotes.data']

data_define_directives = {
'cat': {
'schema': {
'name': 'str, required',
'attrs': {'color': 'str', 'age': 'int'},
},
'template': {
'text': '{{ name }} is a {{ attrs.color }} cat, {{ attrs.age }} years old.',
},
},
}

Then use it in your document:

.. code-block:: rst

.. cat:: mimi
:color: black
:age: 3

This will render: **mimi is a black cat, 3 years old.**

Render Phase
------------

You can specify when the template is rendered using the ``on`` option:

.. code-block:: python

data_define_directives = {
'delayed': {
'schema': {'name': 'str'},
'template': {
'on': 'resolving',
'text': 'Document: {{ name }}',
},
},
}

See :external+render:ref:`phases` for more details about render phases.

Debug Output
------------

You can enable debug output for template rendering using the ``debug`` option:

.. code-block:: python

data_define_directives = {
'debug example': {
'schema': {'name': 'str'},
'template': {
'text': '{{ name }}',
'debug': True,
},
},
}

When enabled, the directive will display debug information about the template
rendering process in the generated documentation.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies = [

# CUSTOM DEPENDENCIES START
"sphinxnotes-render",
"schema", # python dict schema validation
# CUSTOM DEPENDENCIES END
]

Expand Down
15 changes: 3 additions & 12 deletions src/sphinxnotes/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,6 @@
from typing import TYPE_CHECKING

from . import meta
from .adhoc import (
TemplateDefineDirective,
SchemaDefineDirective,
FreeDataDefineDirective,
FreeDataDefineRoleDispatcher,
DataRenderDirective,
)

if TYPE_CHECKING:
from sphinx.application import Sphinx
Expand All @@ -36,11 +29,9 @@ def setup(app: Sphinx):

app.setup_extension('sphinxnotes.render')

app.add_directive('data.define', FreeDataDefineDirective)
app.add_directive('data.template', TemplateDefineDirective)
app.add_directive('data.schema', SchemaDefineDirective)
app.add_directive('data.render', DataRenderDirective)
from . import adhoc, derive

app.connect('source-read', FreeDataDefineRoleDispatcher().install)
adhoc.setup(app)
derive.setup(app)

return meta.post_setup(app)
11 changes: 10 additions & 1 deletion src/sphinxnotes/data/adhoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from types import ModuleType
from docutils.utils import Reporter
from sphinx.util.typing import RoleFunction
from sphinxnotes.render.ctx import PendingContext, ResolvedContext
from sphinxnotes.render import PendingContext, ResolvedContext


# Keys of env.temp_data.
Expand Down Expand Up @@ -201,3 +201,12 @@ def install(self, app: Sphinx, docname: str, source: list[str]) -> None:
automatically.
"""
self.enable()


def setup(app: Sphinx) -> None:
app.add_directive('data.define', FreeDataDefineDirective)
app.add_directive('data.template', TemplateDefineDirective)
app.add_directive('data.schema', SchemaDefineDirective)
app.add_directive('data.render', DataRenderDirective)

app.connect('source-read', FreeDataDefineRoleDispatcher().install)
69 changes: 69 additions & 0 deletions src/sphinxnotes/data/derive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
sphinxnotes.data.derive
~~~~~~~~~~~~~~~~~~~~~~~

:copyright: Copyright 2025~2026 by the Shengyu Zhang.
:license: BSD, see LICENSE for details.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from schema import Schema as DictSchema, SchemaError as DictSchemaError, Optional, Or

from sphinx.errors import ConfigError
from sphinxnotes.render import Schema, Template, Phase, StrictDataDefineDirective

if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.config import Config


DATA_DEFINE_DIRECTIVE = DictSchema(
{
'schema': {
Optional('name', default='str, required, uniq, ref'): Or(str, type(None)),
Optional('attrs', default={}): {str: str},
Optional('content', default='str'): Or(str, type(None)),
},
'template': {
Optional('on', default='parsing'): Or('parsing', 'parsed', 'resolving'),
'text': str,
Optional('debug', default=False): bool,
},
}
)


def _validate_directive_define(d: dict, config: Config) -> tuple[Schema, Template]:
validated = DATA_DEFINE_DIRECTIVE.validate(d)

schemadef = validated['schema']
schema = Schema.from_dsl(
schemadef['name'], schemadef['attrs'], schemadef['content']
)

tmpldef = validated['template']
phase = Phase[tmpldef['on'].title()]
template = Template(text=tmpldef['text'], phase=phase, debug=tmpldef['debug'])

return schema, template


def _config_inited(app: Sphinx, config: Config) -> None:
for name, objdef in app.config.data_define_directives.items():
try:
schema, tmpl = _validate_directive_define(objdef, config)
except (DictSchemaError, ValueError) as e:
raise ConfigError(
f'Validating data_define_directives[{repr(name)}]: {e}'
) from e

directive_cls = StrictDataDefineDirective.derive(name, schema, tmpl)
app.add_directive(name, directive_cls)


def setup(app: Sphinx) -> None:
app.add_config_value('data_define_directives', {}, 'env', types=dict)
app.connect('config-inited', _config_inited)
16 changes: 16 additions & 0 deletions tests/roots/test-derive/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
keep_warnings = True

extensions = ['sphinxnotes.data']

data_define_directives = {
'custom': {
'schema': {
'name': 'str, required',
'attrs': {'type': 'str'},
},
'template': {
'on': 'parsing',
'text': 'Custom: {{ name }} (type: {{ attrs.type }})',
},
},
}
8 changes: 8 additions & 0 deletions tests/roots/test-derive/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Test
====

.. custom:: myname
:type: mytype

.. custom:: myname
:unkown:
11 changes: 11 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ def test_data_template_and_define(app, status, warning, phase):
assert 'RenderedValue1' in html
assert 'RenderedValue2' in html
assert 'RenderedContent' in html


@pytest.mark.sphinx('html', testroot='derive')
def test_data_define_directives(app, status, warning):
"""Test that data_define_directives generates directives correctly."""
app.build()

html = (app.outdir / 'index.html').read_text(encoding='utf-8')

assert 'Custom: myname (type: mytype)' in html
assert 'Error in “custom” directive: unknown option: “unkown”.'
Loading