Extending syntax with roles and directives¶
Overview¶
The syntax of both reStructuredText and MyST can be extended by creating new directives - for block-level elements - and roles - for inline elements.
In this tutorial we shall extend Sphinx to add:
A
hello
role, that will simply output the textHello {text}!
.A
hello
directive, that will simply output the textHello {text}!
, as a paragraph.
For this extension, you will need some basic understanding of Python, and we shall also introduce aspects of the docutils API.
Setting up the project¶
You can either use an existing Sphinx project or create a new one using sphinx-quickstart.
With this we will add the extension to the project,
within the source
folder:
Create an
_ext
folder insource
Create a new Python file in the
_ext
folder calledhelloworld.py
Here is an example of the folder structure you might obtain:
└── source
├── _ext
│ └── helloworld.py
├── conf.py
├── index.rst
Writing the extension¶
Open helloworld.py
and paste the following code in it:
1from __future__ import annotations
2
3from docutils import nodes
4
5from sphinx.application import Sphinx
6from sphinx.util.docutils import SphinxDirective, SphinxRole
7from sphinx.util.typing import ExtensionMetadata
8
9
10class HelloRole(SphinxRole):
11 """A role to say hello!"""
12
13 def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
14 node = nodes.inline(text=f'Hello {self.text}!')
15 return [node], []
16
17
18class HelloDirective(SphinxDirective):
19 """A directive to say hello!"""
20
21 required_arguments = 1
22
23 def run(self) -> list[nodes.Node]:
24 paragraph_node = nodes.paragraph(text=f'hello {self.arguments[0]}!')
25 return [paragraph_node]
26
27
28def setup(app: Sphinx) -> ExtensionMetadata:
29 app.add_role('hello', HelloRole())
30 app.add_directive('hello', HelloDirective)
31
32 return {
33 'version': '0.1',
34 'parallel_read_safe': True,
35 'parallel_write_safe': True,
36 }
Some essential things are happening in this example:
The role class¶
Our new role is declared in the HelloRole
class.
1class HelloRole(SphinxRole):
2 """A role to say hello!"""
3
4 def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
5 node = nodes.inline(text=f'Hello {self.text}!')
6 return [node], []
This class extends the SphinxRole
class.
The class contains a run
method,
which is a requirement for every role.
It contains the main logic of the role and it
returns a tuple containing:
a list of inline-level docutils nodes to be processed by Sphinx.
an (optional) list of system message nodes
The directive class¶
Our new directive is declared in the HelloDirective
class.
1class HelloDirective(SphinxDirective):
2 """A directive to say hello!"""
3
4 required_arguments = 1
5
6 def run(self) -> list[nodes.Node]:
7 paragraph_node = nodes.paragraph(text=f'hello {self.arguments[0]}!')
8 return [paragraph_node]
This class extends the SphinxDirective
class.
The class contains a run
method,
which is a requirement for every directive.
It contains the main logic of the directive and it
returns a list of block-level docutils nodes to be processed by Sphinx.
It also contains a required_arguments
attribute,
which tells Sphinx how many arguments are required for the directive.
What are docutils nodes?¶
When Sphinx parses a document, it creates an "Abstract Syntax Tree" (AST) of nodes that represent the content of the document in a structured way, that is generally independent of any one input (rST, MyST, etc) or output (HTML, LaTeX, etc) format. It is a tree because each node can have children nodes, and so on:
<document>
<paragraph>
<text>
Hello world!
The docutils package provides many built-in nodes, to represent different types of content such as text, paragraphs, references, tables, etc.
Each node type generally only accepts a specific set of direct child nodes,
for example the document
node should only contain "block-level" nodes,
such as paragraph
, section
, table
, etc,
whilst the paragraph
node should only contain "inline-level" nodes,
such as text
, emphasis
, strong
, etc.
See also
The docutils documentation on creating directives, and creating roles.
The setup
function¶
This function is a requirement. We use it to plug our new directive into Sphinx.
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_role('hello', HelloRole())
app.add_directive('hello', HelloDirective)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
The simplest thing you can do is to call the
Sphinx.add_role()
and Sphinx.add_directive()
methods,
which is what we've done here.
For this particular call, the first argument is the name of the role/directive itself
as used in a reStructuredText file.
In this case, we would use hello
. For example:
Some intro text here...
.. hello:: world
Some text with a :hello:`world` role.
We also return the extension metadata that indicates the version of our extension, along with the fact that it is safe to use the extension for both parallel reading and writing.
Using the extension¶
The extension has to be declared in your conf.py
file to make Sphinx
aware of it. There are two steps necessary here:
Add the
_ext
directory to the Python path usingsys.path.append
. This should be placed at the top of the file.Update or create the
extensions
list and add the extension file name to the list
For example:
import sys
from pathlib import Path
sys.path.append(str(Path('_ext').resolve()))
extensions = ['helloworld']
Tip
Because we haven't installed our extension as a Python package, we need to
modify the Python path so Sphinx can find our extension. This is why we
need the call to sys.path.append
.
You can now use the extension in a file. For example:
Some intro text here...
.. hello:: world
Some text with a :hello:`world` role.
The sample above would generate:
Some intro text here...
Hello world!
Some text with a hello world! role.
Further reading¶
This is the very basic principle of an extension that creates a new role and directive.
For a more advanced example, refer to Extending the build process.
If you wish to share your extension across multiple projects or with others, check out the 第三方插件 section.