Create a new directive
Sphinx can be extended with new directives. Similar to defining new roles, such can be defined in Python modules, which are attached to the Sphinx application.
Docutils offers a manual on how to create RST directives, and also Sphinx offers some tutorials on the subject. Though this How-To guide bases on my actual experiences and attempts to be as thorough and hands-on as possible.
Understand the concept of directives
It is important to understand what directives actually represent in a Sphinx application. Directives actually are an implementation in docutils, which Sphinx extends with additional capabilities.
Docutils itself translates the reStructuredText syntax in XML, which, similar to HTML, is a markup language, which consists of nodes. These nodes stand in a particular relation to each other, forming the Docutils Document Tree. Docutils provides a docutils.nodes.py Python module, which implements the tree data structure.
The important thing to understand here is, that each piece of content inside a
*.rst file is translated to a class of type Node or an attribute of it.
Basically, each Node is represented by a class of type Element, whereas
concrete element classes (detected by docutil’s NodeVisitor instance) inherit
from them. In addition, there are so-called element category classes, which the
concrete element class inherit from. The graph below shows the inheritance graph
of a small fraction of available element and category classes.
classDiagram
Node <-- Text
Node <-- Element
Element <-- TextElement
Element <-- document
TextElement <-- title
TextElement <-- subtitle
TextElement <-- rubric
Structural <-- document
Root <-- document
Titlebar <-- title
Body <-- Admonition
Element <-- admonition
Admonition <-- admonition
style Text fill: #85ff83ff
style Element fill: #85ff83ff
style TextElement fill: #85ff83ff
style Admonition fill: #6fb8ffff
style Structural fill: #6fb8ffff
style Root fill: #6fb8ffff
style Titlebar fill: #6fb8ffff
As a naming convention, concrete element classes are named in camelcase. To differentiate, parent element classes are colored in green, category classes are colored in blue.
When creating a new directive, we have to create two things:
the directive class, so that the directive is detected as such in an RST text (as opposed to regular paragraph text). In other word, docutils must know about the directive.
the corresponding node class into which the detected directive instance (which is created for any match of that directive) is translated into. As said above, docutils creates a doctree document out of each RST document, which can then be translated into HTML or other format by Sphinx.
Example: Create a custom admonition directive
It is highly advised to make use of existing elements, and extending those, instead of creating entire new directives from scratch, thereby leveraging code that is already included in docutils. In this example, we will create a waiting admonition, which is used like this:
.. waiting:: 30 min Just you wait ...
to be rendered into
30 min
Just you wait …
Define the admonition
For that, we create a new file inside source/_ext/ named waiting_admonition.py.
In here, we import the necessary base classes for both our custom admonition directive and our custom admonition node:
1from docutils.parsers.rst.directives.admonitions import BaseAdmonition 2from docutils.nodes import Admonition, Element 3from sphinx.application import Sphinx 4from sphinx.util.typing import ExtensionMetadata
Note, that the imports from line 3 and 4 will be used for type hinting only later on.
The BaseAdmonition will be used as a parent of our WaitingAdmonition
directive class, where both Admonition and Element will be the parents
of our waiting node class:
7class waiting(Admonition, Element): 8 pass 9 10 11class WaitingAdmonition(BaseAdmonition): 12 pass
To find out, which classes are best suited to inherit from for both the custom node class and the directive or role element class, check out these modules
Nodes: docutils.nodes.py
Directives: docutils.parsers.rst.directives (choose the respective module)
The waiting class is already done. Usually, it doesn’t have anything to do
except inheriting from the standard docutils class(es) (defined in docutils.nodes).
Let’s continue with the implementation of the WaitingAdmonition class:
11class WaitingAdmonition(BaseAdmonition): 12 required_arguments = 1 13 final_argument_whitespace = True 14 has_content = True 15 16 node_class = waiting 17 18 def run(self): 19 title = " ".join(self.arguments) 20 waiting_node = waiting(title=title) 21 self.state.nested_parse(self.content, self.content_offset, 22 waiting_node) 23 return [waiting_node]
Each class inheriting from docutils.parsers.rst.Directive (which
BaseAdmonition does) may define a list of attributes, which specify how the
WaitingAdmonition is supposed to be used.
- line 12-13:
required_arguments = 1specifies that the directive expects exactly one argument. Thefinal_argument_whitespace = Trueattribute declares that the last (in this case, the last is also the first, asoptional_argumentsis not defined, therefore is considered as0) argument is expected to contain whitespace. Both attributes allow for such syntax:.. waiting:: 30 min
Here,
30 minis considered as one argument, as only one is allowed, though it may contain whitespaces. Note, that this declaration is optional, as the default value is alreadyTrue.- line 14:
has_content = Trueallows for providing content inside the admonition. Providing content, while this is set toFalsewill throw an error during parsing the directive. It allows for the following syntax:.. waiting:: 30 min Please wait patiently, a good time to get a coffee.
- line 16:
This line is a special attribute added by th
BaseAdmonitionclass, which specifies the admonition node class, the WaitingAdmonition element is set to in the doctree. This will be ourwaitingclass, which will be defined next.- line 18:
Defining the
run()method is mandatory, as it is called to transform the associated directive into one or more nodes.Additional arguments are defined in the
rst.Directivedocstring:>>> from docutils.parsers import rst >>> print rst.Directive.__doc__
- line 19:
All arguments values, separated by space, are mapped into a list. The
titlevariable is created by concatenating this list of all input arguments. So, for.. waiting:: 30 min or more
the
titlewill be30 min or more.- line 20:
Creating the node that the waiting admonition is supposed to be translated to. Here, it passes the
titlevariable as title, so that the node can make use of it.- line 21-22:
Calling the
self.state.nested_parse()method, the content body of the directive is parsed. The first argument gives the content body, and the second one gives content offset. The third argument gives the parent node of parsed result, in our case the waiting node. Following this, the waiting node is added to the environment. This is needed to be able to insert the waiting directive’s content into the directive entries.- line 23:
Return the node to be put into the doctree (here, the waiting node). As multiple nodes may be returned for a directive, a list of one or more nodes is expected.
As a next step, the custom waiting node must receive methods on how the waiting
doctree node is translated into a HTML node, specifically sphinx.writers.html5.HTML5Translator
(other writer, such as for latex, each require separate methods. A method for
visiting the node and departing from the node is needed. The former defines what
HTML nodes to insert before any further nested nodes (see line 1 below), the latter
defines what nodes to insert after any further nested nodes (see line 3 blow):
<div my_custom_node> // further nested contents </div>
In the easiest case, the visit_waiting_node() and depart_waiting_node()
methods may simply call the predefined Sphinx sphinx.writers.html5.HTML5Translator
methods for visiting and departing from admonition nodes:
def visit_waiting_node(self, node): self.visit_admonition(node) def depart_waiting_node(self, node): self.depart_admonition(node)Note
The
selfargument is the writer instance, in case of HTML, thesphinx.writers.html5.HTML5Translatorinstance. It must be declared like that even though the functions are defined outside of a class.
In our case, this is not sufficient, because passing only the node into the
predefined methods, will create an admonition without title. Calling it like
self.visit_admonition(node, 'waiting') will lead to an error, as the
admonition label 'waiting' is not known to sphinx. We could pass in a
known label, such as self.visit_admonition(node, 'warning'), but this
will treat our custom waiting node exactly like a built-in warning node,
which obscures the existence of this custom admonition (we could have used a
warning admonition, in that case).
For this reason, we will define a custom body for the waiting node:
26def visit_waiting_node(self, node): 27 self.body.append("<div class=\"admonition waiting\">") 28 self.body.append("<p class=\"admonition-title\">") 29 self.body.append(f"{node.get('title')}") 30 self.body.append("</p>") 31 32 33def depart_waiting_node(self, node): 34 self.body.append("</div>")Note
Besides the
selfargument, both methods require thenodeargument, even if it is not used (as is the case fordepart_waiting_node()).
- line 27:
Append a
<div>element to the HTML writerbody, with theclass="admonition waiting"attribute. Theadmonitionclass is present for all admonition nodes, whereas thewaitingclass is a new class, allowing for customized styling.Note, that the
<div>element is not closed here (will be done in thedepart_waiting_node()method in line 34.Hint
The same is achieved by adding the
:class: waitingoption to a admonition directives:.. admonition:: :class: waiting
- line 28:
Appends the admonition title bar into the admonition.
Hint
Even though this element is contained in the
<div>admonition element, there is no need to indent the string. The rendering browser will handle this automatically.- line 29:
The admonition-title is appended. This
titlevalue was defined and inserted into the waiting node in theWaitingAdmonition.run()method. Node attributes can be retrieved by theElement.get()method.- line 30:
This adds a closing line for the admonition-title paragraph node. This line can actually be omitted, as a closing statement for this element is automatically set by the rendering browser before the next element starts. This is because the following node will be another paragraph (
<p>) which will contain the contents of the waiting admonition. Nesting one paragraph into another is not possible in HTML, so the paragraph is closed, before another one is opened.
Last but not least, both the defined waiting admonition and the waiting node must be registered in Sphinx, so they are known to the Sphinx builder:
37def setup(app: Sphinx): 38 app.add_node(waiting, html=(visit_waiting_node, depart_waiting_node)) 39 app.add_directive('waiting', WaitingAdmonition) 40 41 return {'version': '0.1'}
- line 37:
The
setup()method is a required method to plug new directives into Sphinx. It requires the Sphinx application instance as an argument.- line 38:
The
add_node()method registers the waiting node. For thehtmlbuilder, it expects a tuple, consisting of the visiting and the departing method for the node. For this, we pass the previously defined respective methods.- line 39:
The
add_directivemethod register the ‘waiting* directive. It requires the name value of the directive (here:'waiting') and the admonition, this name is connected to (here:WaitingAdmonition).- line 41:
This return statement is optional. It may return a dictionary, which is treated as the extensions metadata. For more info, see Extension metadata.
This concludes the definition of the waiting admonition.
This is the entire content of the waiting_admonition.py module:
1from docutils.parsers.rst.directives.admonitions import BaseAdmonition
2from docutils.nodes import Admonition, Element
3from sphinx.application import Sphinx
4
5try:
6 from sphinx.util.typing import ExtensionMetadata
7except ImportError:
8 ExtensionMetadata = 'ExtensionMetadataType' # Sphinx <7.3
9
10
11class waiting(Admonition, Element):
12 pass
13
14
15class WaitingAdmonition(BaseAdmonition):
16 required_arguments = 1
17 final_argument_whitespace = True
18 has_content = True
19
20 node_class = waiting
21
22 def run(self):
23 title = " ".join(self.arguments)
24 waiting_node = waiting(title=title)
25 self.state.nested_parse(self.content, self.content_offset,
26 waiting_node)
27 return [waiting_node]
28
29
30def visit_waiting_node(self, node):
31 self.body.append("<div class=\"admonition waiting\">")
32 self.body.append("<p class=\"admonition-title\">")
33 self.body.append(f"{node.get('title')}")
34 self.body.append("</p>")
35
36
37def depart_waiting_node(self, node):
38 self.body.append("</div>")
39
40
41def setup(app: Sphinx) -> ExtensionMetadata:
42 app.add_node(waiting, html=(visit_waiting_node, depart_waiting_node))
43 app.add_directive('waiting', WaitingAdmonition)
44
45 return {'version': '0.1'}
Add the admonition to the config
The waiting admonition can now be added the documentation’s conf.py file.
Under the extensions=[...] list, add the module name of the extension:
extensions = [ # other extensions 'waiting_admonition' ]
Make sure, the source/_ext/ directory is added to the interpreter’s
PYTHONPATH. Add the following line near the top of the conf.py file:
import os import sys sys.path.insert(0, os.path.abspath('.') + '/_ext')
On the next build, the extension will allow for using waiting admonitions:
.. waiting:: 30 min You just missed to the train. Have to catch the next one.
Style the admonition
For the rendered HTML output, the style of the waiting admonition will be defined by the used theme. And those themes naturally don’t provide a custom style for nodes that are not part of basic Sphinx. Hence, it is the responsibility of the extension to provide a decent style.
Note
A customized style might only work for a particular theme, as each may use different HTML templates. Effectively, CSS rules customized for a particular theme’s HTML template, are likely not considered for those of other themes.
As a general rule, when starting to add custom CSS rules, be certain to stay on a particular theme for the documentation project. It is not a solution to make CSS rules more general, making it match for templates on various themes, as the rules likely have low specificity, that they are overridden by the theme’s own, more specific, CSS rules.
As a workaround, you may define a custom.css file for each used theme:
if html_theme == 'sphinx_rtd_theme':
html_css_files = [
'css/custom_rtd.css',
]
elif html_theme == 'nature':
html_css_files = [
"css/custom_nature.css"
]
Make sure, the conf.py contains the variable definition to support custom styling:
html_static_path = ['_static'] html_css_files = [ 'css/custom.css', ]
In your source/_static/css/custom.css file, add the following content:
.admonition.waiting { background: #e7ffc2ff; } .admonition.waiting p.admonition-title { background: #7eab48ff; } .admonition.waiting p.admonition-title::before { content: "\2615 "; }
The style is applied after at the next sphinx-build run.