Skip to content
Merged
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
23 changes: 23 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,26 @@
# CUSTOM CONFIGURATION

intersphinx_mapping['render'] = ('https://sphinx.silverrainz.me/render/', None)

# [example config start]
data_define_directives = {
'cat': {
'schema': {
'name': 'str, required',
'attrs': {
'color': 'str',
},
'content': 'str, required'
},
'template': {
'debug': False,
'on': 'parsing',
'text': '\n'.join([
'Hi human! I am a cat named {{ name }}, I have {{ color }} fur.',
'',
'{{ content }}.',
]),
},
},
}
# [example config end]
30 changes: 30 additions & 0 deletions docs/conf.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
=============
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, works same as the
:rst:dir:`data.schema` directive, which has the following keys:

- ``name`` (str, optional): same as the directive argument
- ``attr`` (dict, can be empty): same as the directive options
- ``content`` (str, optional): same as the directive content

- ``template`` (dict): Template definition, works same as the
:rst:dir:`data.template` directive, which has the following keys:

- ``text`` (str): the Jinja2 template text.
- ``on`` (str, optional): same as :rst:dir:`data.template:on`
- ``debug`` (bool, optional): same as :rst:dir:`data.template:debug`

See :ref:`custom-dir` for example.

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
38 changes: 38 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,41 @@ Directives
.. data.render::

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


.. _custom-dir:

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.

First, add ``'sphinxnotes.data'`` to your extension list like what we do in
:doc:`Getting Started <index>`.

Then add the following code to your :file:`conf.py`:

.. literalinclude:: conf.py
:language: python
:start-after: [example config start]
:end-before: [example config end]

This creates a ``.. cat::`` directive that requires a name argument and accepts
a ``color`` options and a content. Use it in your document:

.. example::
:style: grid

.. cat:: mimi
:color: black and brown

I like fish!

For more details please refer to the :confval:`data_define_directives`
configuration value.
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