Extending syntax with roles and directives¶
概覽¶
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
編寫擴充套件¶
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:
角色類別¶
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
指示詞類別¶
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.
什麼是 docutils 節點?¶
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.
另請參見
The docutils documentation on creating directives, and creating roles.
setup
函式¶
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.
使用擴充套件¶
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']
小訣竅
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.
延伸閱讀¶
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.