summaryrefslogtreecommitdiffstats
path: root/doc/development
diff options
context:
space:
mode:
Diffstat (limited to 'doc/development')
-rw-r--r--doc/development/builders.rst34
-rw-r--r--doc/development/index.rst24
-rw-r--r--doc/development/overview.rst32
-rw-r--r--doc/development/templating.rst478
-rw-r--r--doc/development/theming.rst342
-rw-r--r--doc/development/tutorials/autodoc_ext.rst141
-rw-r--r--doc/development/tutorials/examples/README.rst11
-rw-r--r--doc/development/tutorials/examples/autodoc_intenum.py58
-rw-r--r--doc/development/tutorials/examples/helloworld.py19
-rw-r--r--doc/development/tutorials/examples/recipe.py159
-rw-r--r--doc/development/tutorials/examples/todo.py135
-rw-r--r--doc/development/tutorials/helloworld.rst189
-rw-r--r--doc/development/tutorials/index.rst17
-rw-r--r--doc/development/tutorials/recipe.rst227
-rw-r--r--doc/development/tutorials/todo.rst367
15 files changed, 2233 insertions, 0 deletions
diff --git a/doc/development/builders.rst b/doc/development/builders.rst
new file mode 100644
index 0000000..bb67770
--- /dev/null
+++ b/doc/development/builders.rst
@@ -0,0 +1,34 @@
+Configuring builders
+====================
+
+Discover builders by entry point
+--------------------------------
+
+.. versionadded:: 1.6
+
+:term:`builder` extensions can be discovered by means of `entry points`_ so
+that they do not have to be listed in the :confval:`extensions` configuration
+value.
+
+Builder extensions should define an entry point in the ``sphinx.builders``
+group. The name of the entry point needs to match your builder's
+:attr:`~.Builder.name` attribute, which is the name passed to the
+:option:`sphinx-build -b` option. The entry point value should equal the
+dotted name of the extension module. Here is an example of how an entry point
+for 'mybuilder' can be defined in the extension's ``setup.py``
+
+.. code-block:: python
+
+ setup(
+ # ...
+ entry_points={
+ 'sphinx.builders': [
+ 'mybuilder = my.extension.module',
+ ],
+ }
+ )
+
+Note that it is still necessary to register the builder using
+:meth:`~.Sphinx.add_builder` in the extension's :func:`setup` function.
+
+.. _entry points: https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
diff --git a/doc/development/index.rst b/doc/development/index.rst
new file mode 100644
index 0000000..55a31a0
--- /dev/null
+++ b/doc/development/index.rst
@@ -0,0 +1,24 @@
+=========================
+Writing Sphinx Extensions
+=========================
+
+This guide is aimed at giving a quick introduction for those wishing to
+develop their own extensions for Sphinx. Sphinx possesses significant
+extensibility capabilities including the ability to hook into almost every
+point of the build process. If you simply wish to use Sphinx with existing
+extensions, refer to :doc:`/usage/index`. For a more detailed discussion of
+the extension interface see :doc:`/extdev/index`.
+
+.. toctree::
+ :maxdepth: 2
+
+ overview
+ tutorials/index
+ builders
+
+.. toctree::
+ :caption: Theming
+ :maxdepth: 2
+
+ templating
+ theming
diff --git a/doc/development/overview.rst b/doc/development/overview.rst
new file mode 100644
index 0000000..df8f5bb
--- /dev/null
+++ b/doc/development/overview.rst
@@ -0,0 +1,32 @@
+Developing extensions overview
+==============================
+
+This page contains general information about developing Sphinx extensions.
+
+Make an extension depend on another extension
+---------------------------------------------
+
+Sometimes your extension depends on the functionality of another
+Sphinx extension. Most Sphinx extensions are activated in a
+project's :file:`conf.py` file, but this is not available to you as an
+extension developer.
+
+.. module:: sphinx.application
+ :no-index:
+
+To ensure that another extension is activated as a part of your own extension,
+use the :meth:`sphinx.application.Sphinx.setup_extension` method. This will
+activate another extension at run-time, ensuring that you have access to its
+functionality.
+
+For example, the following code activates the ``recommonmark`` extension:
+
+.. code-block:: python
+
+ def setup(app):
+ app.setup_extension("recommonmark")
+
+.. note::
+
+ Since your extension will depend on another, make sure to include
+ it as a part of your extension's installation requirements.
diff --git a/doc/development/templating.rst b/doc/development/templating.rst
new file mode 100644
index 0000000..73ae6e5
--- /dev/null
+++ b/doc/development/templating.rst
@@ -0,0 +1,478 @@
+.. highlight:: html+jinja
+
+.. _templating:
+
+==========
+Templating
+==========
+
+Sphinx uses the `Jinja <https://jinja.palletsprojects.com/>`_ templating engine
+for its HTML templates. Jinja is a text-based engine, inspired by Django
+templates, so anyone having used Django will already be familiar with it. It
+also has excellent documentation for those who need to make themselves familiar
+with it.
+
+
+Do I need to use Sphinx's templates to produce HTML?
+----------------------------------------------------
+
+No. You have several other options:
+
+* You can write a :class:`~sphinx.application.TemplateBridge` subclass that
+ calls your template engine of choice, and set the :confval:`template_bridge`
+ configuration value accordingly.
+
+* You can :ref:`write a custom builder <writing-builders>` that derives from
+ :class:`~sphinx.builders.html.StandaloneHTMLBuilder` and calls your template
+ engine of choice.
+
+* You can use the :class:`~sphinxcontrib.serializinghtml.PickleHTMLBuilder` that
+ produces pickle files with the page contents, and postprocess them using a
+ custom tool, or use them in your Web application.
+
+
+Jinja/Sphinx Templating Primer
+------------------------------
+
+The default templating language in Sphinx is Jinja. It's Django/Smarty inspired
+and easy to understand. The most important concept in Jinja is :dfn:`template
+inheritance`, which means that you can overwrite only specific blocks within a
+template, customizing it while also keeping the changes at a minimum.
+
+To customize the output of your documentation you can override all the templates
+(both the layout templates and the child templates) by adding files with the
+same name as the original filename into the template directory of the structure
+the Sphinx quickstart generated for you.
+
+Sphinx will look for templates in the folders of :confval:`templates_path`
+first, and if it can't find the template it's looking for there, it falls back
+to the selected theme's templates.
+
+A template contains **variables**, which are replaced with values when the
+template is evaluated, **tags**, which control the logic of the template and
+**blocks** which are used for template inheritance.
+
+Sphinx's *basic* theme provides base templates with a couple of blocks it will
+fill with data. These are located in the :file:`themes/basic` subdirectory of
+the Sphinx installation directory, and used by all builtin Sphinx themes.
+Templates with the same name in the :confval:`templates_path` override templates
+supplied by the selected theme.
+
+For example, to add a new link to the template area containing related links all
+you have to do is to add a new template called ``layout.html`` with the
+following contents::
+
+ {% extends "!layout.html" %}
+ {% block rootrellink %}
+ <li><a href="https://project.invalid/">Project Homepage</a> &raquo;</li>
+ {{ super() }}
+ {% endblock %}
+
+By prefixing the name of the overridden template with an exclamation mark,
+Sphinx will load the layout template from the underlying HTML theme.
+
+.. important::
+ If you override a block, call ``{{ super() }}`` somewhere to render the
+ block's original content in the extended template -- unless you don't want
+ that content to show up.
+
+
+Working with the builtin templates
+----------------------------------
+
+The builtin **basic** theme supplies the templates that all builtin Sphinx
+themes are based on. It has the following elements you can override or use:
+
+Blocks
+~~~~~~
+
+The following blocks exist in the ``layout.html`` template:
+
+``doctype``
+ The doctype of the output format. By default this is XHTML 1.0 Transitional
+ as this is the closest to what Sphinx and Docutils generate and it's a good
+ idea not to change it unless you want to switch to HTML 5 or a different but
+ compatible XHTML doctype.
+
+``linktags``
+ This block adds a couple of ``<link>`` tags to the head section of the
+ template.
+
+``extrahead``
+ This block is empty by default and can be used to add extra contents into
+ the ``<head>`` tag of the generated HTML file. This is the right place to
+ add references to JavaScript or extra CSS files.
+
+``relbar1``, ``relbar2``
+ This block contains the *relation bar*, the list of related links (the
+ parent documents on the left, and the links to index, modules etc. on the
+ right). ``relbar1`` appears before the document, ``relbar2`` after the
+ document. By default, both blocks are filled; to show the relbar only
+ before the document, you would override `relbar2` like this::
+
+ {% block relbar2 %}{% endblock %}
+
+``rootrellink``, ``relbaritems``
+ Inside the relbar there are three sections: The ``rootrellink``, the links
+ from the documentation and the custom ``relbaritems``. The ``rootrellink``
+ is a block that by default contains a list item pointing to the root
+ document by default, the ``relbaritems`` is an empty block. If you
+ override them to add extra links into the bar make sure that they are list
+ items and end with the :data:`reldelim1`.
+
+``document``
+ The contents of the document itself. It contains the block "body" where
+ the individual content is put by subtemplates like ``page.html``.
+
+ .. note::
+ In order for the built-in JavaScript search to show a page preview on
+ the results page, the document or body content should be wrapped in an
+ HTML element containing the ``role="main"`` attribute. For example::
+
+ <div role="main">
+ {% block document %}{% endblock %}
+ </div>
+
+``sidebar1``, ``sidebar2``
+ A possible location for a sidebar. ``sidebar1`` appears before the document
+ and is empty by default, ``sidebar2`` after the document and contains the
+ default sidebar. If you want to swap the sidebar location override this and
+ call the ``sidebar`` helper::
+
+ {% block sidebar1 %}{{ sidebar() }}{% endblock %}
+ {% block sidebar2 %}{% endblock %}
+
+ (The ``sidebar2`` location for the sidebar is needed by the ``sphinxdoc.css``
+ stylesheet, for example.)
+
+``sidebarlogo``
+ The logo location within the sidebar. Override this if you want to place
+ some content at the top of the sidebar.
+
+``footer``
+ The block for the footer div. If you want a custom footer or markup before
+ or after it, override this one.
+
+The following four blocks are *only* used for pages that do not have assigned a
+list of custom sidebars in the :confval:`html_sidebars` config value. Their use
+is deprecated in favor of separate sidebar templates, which can be included via
+:confval:`html_sidebars`.
+
+``sidebartoc``
+ The table of contents within the sidebar.
+
+ .. deprecated:: 1.0
+
+``sidebarrel``
+ The relation links (previous, next document) within the sidebar.
+
+ .. deprecated:: 1.0
+
+``sidebarsourcelink``
+ The "Show source" link within the sidebar (normally only shown if this is
+ enabled by :confval:`html_show_sourcelink`).
+
+ .. deprecated:: 1.0
+
+``sidebarsearch``
+ The search box within the sidebar. Override this if you want to place some
+ content at the bottom of the sidebar.
+
+ .. deprecated:: 1.0
+
+
+Configuration Variables
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Inside templates you can set a couple of variables used by the layout template
+using the ``{% set %}`` tag:
+
+.. data:: reldelim1
+
+ The delimiter for the items on the left side of the related bar. This
+ defaults to ``' &raquo;'`` Each item in the related bar ends with the value
+ of this variable.
+
+.. data:: reldelim2
+
+ The delimiter for the items on the right side of the related bar. This
+ defaults to ``' |'``. Each item except of the last one in the related bar
+ ends with the value of this variable.
+
+Overriding works like this::
+
+ {% extends "!layout.html" %}
+ {% set reldelim1 = ' &gt;' %}
+
+.. data:: script_files
+
+ Add additional script files here, like this::
+
+ {% set script_files = script_files + ["_static/myscript.js"] %}
+
+ .. deprecated:: 1.8.0
+
+ Please use ``.Sphinx.add_js_file()`` instead.
+
+Helper Functions
+~~~~~~~~~~~~~~~~
+
+Sphinx provides various Jinja functions as helpers in the template. You can use
+them to generate links or output multiply used elements.
+
+.. function:: pathto(document)
+
+ Return the path to a Sphinx document as a URL. Use this to refer to built
+ documents.
+
+.. function:: pathto(file, 1)
+ :no-index:
+
+ Return the path to a *file* which is a filename relative to the root of the
+ generated output. Use this to refer to static files.
+
+.. function:: hasdoc(document)
+
+ Check if a document with the name *document* exists.
+
+.. function:: sidebar()
+
+ Return the rendered sidebar.
+
+.. function:: relbar()
+
+ Return the rendered relation bar.
+
+.. function:: warning(message)
+
+ Emit a warning message.
+
+Global Variables
+~~~~~~~~~~~~~~~~
+
+These global variables are available in every template and are safe to use.
+There are more, but most of them are an implementation detail and might change
+in the future.
+
+.. data:: builder
+
+ The name of the builder (e.g. ``html`` or ``htmlhelp``).
+
+.. data:: copyright
+
+ The value of :confval:`copyright`.
+
+.. data:: docstitle
+
+ The title of the documentation (the value of :confval:`html_title`), except
+ when the "single-file" builder is used, when it is set to ``None``.
+
+.. data:: embedded
+
+ True if the built HTML is meant to be embedded in some viewing application
+ that handles navigation, not the web browser, such as for HTML help or Qt
+ help formats. In this case, the sidebar is not included.
+
+.. data:: favicon_url
+
+ The relative path to the HTML favicon image from the current document, or
+ URL to the favicon, or ``''``.
+
+ .. versionadded:: 4.0
+
+.. data:: file_suffix
+
+ The value of the builder's :attr:`~.SerializingHTMLBuilder.out_suffix`
+ attribute, i.e. the file name extension that the output files will get. For
+ a standard HTML builder, this is usually ``.html``.
+
+.. data:: has_source
+
+ True if the reST document sources are copied (if :confval:`html_copy_source`
+ is ``True``).
+
+.. data:: language
+
+ The value of :confval:`language`.
+
+.. data:: last_updated
+
+ The build date.
+
+.. data:: logo_url
+
+ The relative path to the HTML logo image from the current document, or URL
+ to the logo, or ``''``.
+
+ .. versionadded:: 4.0
+
+.. data:: master_doc
+
+ Same as :data:`root_doc`.
+
+ .. versionchanged:: 4.0
+
+ Renamed to ``root_doc``.
+
+.. data:: root_doc
+
+ The value of :confval:`root_doc`, for usage with :func:`pathto`.
+
+ .. versionchanged:: 4.0
+
+ Renamed from ``master_doc``.
+
+.. data:: pagename
+
+ The "page name" of the current file, i.e. either the document name if the
+ file is generated from a reST source, or the equivalent hierarchical name
+ relative to the output directory
+ (``[directory/]filename_without_extension``).
+
+.. data:: project
+
+ The value of :confval:`project`.
+
+.. data:: release
+
+ The value of :confval:`release`.
+
+.. data:: rellinks
+
+ A list of links to put at the left side of the relbar, next to "next" and
+ "prev". This usually contains links to the general index and other indices,
+ such as the Python module index. If you add something yourself, it must be a
+ tuple ``(pagename, link title, accesskey, link text)``.
+
+.. data:: shorttitle
+
+ The value of :confval:`html_short_title`.
+
+.. data:: show_source
+
+ True if :confval:`html_show_sourcelink` is ``True``.
+
+.. data:: sphinx_version
+
+ The version of Sphinx used to build represented as a string for example "3.5.1".
+
+.. data:: sphinx_version_tuple
+
+ The version of Sphinx used to build represented as a tuple of five elements.
+ For Sphinx version 3.5.1 beta 3 this would be ``(3, 5, 1, 'beta', 3)``.
+ The fourth element can be one of: ``alpha``, ``beta``, ``rc``, ``final``.
+ ``final`` always has 0 as the last element.
+
+ .. versionadded:: 4.2
+
+.. data:: docutils_version_info
+
+ The version of Docutils used to build represented as a tuple of five elements.
+ For Docutils version 0.16.1 beta 2 this would be ``(0, 16, 1, 'beta', 2)``.
+ The fourth element can be one of: ``alpha``, ``beta``, ``candidate``, ``final``.
+ ``final`` always has 0 as the last element.
+
+ .. versionadded:: 5.0.2
+
+.. data:: styles
+
+ A list of the names of the main stylesheets as given by the theme or
+ :confval:`html_style`.
+
+ .. versionadded:: 5.1
+
+.. data:: title
+
+ The title of the current document, as used in the ``<title>`` tag.
+
+.. data:: use_opensearch
+
+ The value of :confval:`html_use_opensearch`.
+
+.. data:: version
+
+ The value of :confval:`version`.
+
+
+In addition to these values, there are also all **theme options** available
+(prefixed by ``theme_``), as well as the values given by the user in
+:confval:`html_context`.
+
+In documents that are created from source files (as opposed to
+automatically-generated files like the module index, or documents that already
+are in HTML form), these variables are also available:
+
+.. data:: body
+
+ A string containing the content of the page in HTML form as produced by the
+ HTML builder, before the theme is applied.
+
+.. data:: display_toc
+
+ A boolean that is True if the toc contains more than one entry.
+
+.. data:: meta
+
+ Document metadata (a dictionary), see :ref:`metadata`.
+
+.. data:: metatags
+
+ A string containing the page's HTML :dudir:`meta` tags.
+
+.. data:: next
+
+ The next document for the navigation. This variable is either false or has
+ two attributes `link` and `title`. The title contains HTML markup. For
+ example, to generate a link to the next page, you can use this snippet::
+
+ {% if next %}
+ <a href="{{ next.link|e }}">{{ next.title }}</a>
+ {% endif %}
+
+.. data:: page_source_suffix
+
+ The suffix of the file that was rendered. Since we support a list of
+ :confval:`source_suffix`, this will allow you to properly link to the
+ original source file.
+
+.. data:: parents
+
+ A list of parent documents for navigation, structured like the :data:`next`
+ item.
+
+.. data:: prev
+
+ Like :data:`next`, but for the previous page.
+
+.. data:: sourcename
+
+ The name of the copied source file for the current document. This is only
+ nonempty if the :confval:`html_copy_source` value is ``True``.
+ This has empty value on creating automatically-generated files.
+
+.. data:: toc
+
+ The local table of contents for the current page, rendered as HTML bullet
+ lists.
+
+.. data:: toctree
+
+ A callable yielding the global TOC tree containing the current page, rendered
+ as HTML bullet lists. Optional keyword arguments:
+
+ ``collapse``
+ If true, all TOC entries that are not ancestors of the current page are
+ collapsed.
+ ``True`` by default.
+
+ ``maxdepth``
+ The maximum depth of the tree. Set it to ``-1`` to allow unlimited depth.
+ Defaults to the max depth selected in the toctree directive.
+
+ ``titles_only``
+ If true, put only top-level document titles in the tree.
+ ``False`` by default.
+
+ ``includehidden``
+ If true, the ToC tree will also contain hidden entries.
+ ``False`` by default.
diff --git a/doc/development/theming.rst b/doc/development/theming.rst
new file mode 100644
index 0000000..538dcaf
--- /dev/null
+++ b/doc/development/theming.rst
@@ -0,0 +1,342 @@
+HTML theme development
+======================
+
+.. versionadded:: 0.6
+
+.. note::
+
+ This document provides information about creating your own theme. If you
+ simply wish to use a pre-existing HTML themes, refer to
+ :doc:`/usage/theming`.
+
+Sphinx supports changing the appearance of its HTML output via *themes*. A
+theme is a collection of HTML templates, stylesheet(s) and other static files.
+Additionally, it has a configuration file which specifies from which theme to
+inherit, which highlighting style to use, and what options exist for customizing
+the theme's look and feel.
+
+Themes are meant to be project-unaware, so they can be used for different
+projects without change.
+
+.. note::
+
+ See :ref:`dev-extensions` for more information that may
+ be helpful in developing themes.
+
+
+Creating themes
+---------------
+
+Themes take the form of either a directory or a zipfile (whose name is the
+theme name), containing the following:
+
+* A :file:`theme.conf` file.
+* HTML templates, if needed.
+* A ``static/`` directory containing any static files that will be copied to the
+ output static directory on build. These can be images, styles, script files.
+
+The :file:`theme.conf` file is in INI format [1]_ (readable by the standard
+Python :mod:`configparser` module) and has the following structure:
+
+.. sourcecode:: ini
+
+ [theme]
+ inherit = base theme
+ stylesheet = main CSS name
+ pygments_style = stylename
+ sidebars = localtoc.html, relations.html, sourcelink.html, searchbox.html
+
+ [options]
+ variable = default value
+
+* The **inherit** setting gives the name of a "base theme", or ``none``. The
+ base theme will be used to locate missing templates (most themes will not have
+ to supply most templates if they use ``basic`` as the base theme), its options
+ will be inherited, and all of its static files will be used as well. If you
+ want to also inherit the stylesheet, include it via CSS' ``@import`` in your
+ own.
+
+* The **stylesheet** setting gives a list of CSS filenames separated commas which
+ will be referenced in the HTML header. You can also use CSS' ``@import``
+ technique to include one from the other, or use a custom HTML template that
+ adds ``<link rel="stylesheet">`` tags as necessary. Setting the
+ :confval:`html_style` config value will override this setting.
+
+* The **pygments_style** setting gives the name of a Pygments style to use for
+ highlighting. This can be overridden by the user in the
+ :confval:`pygments_style` config value.
+
+* The **pygments_dark_style** setting gives the name of a Pygments style to use
+ for highlighting when the CSS media query ``(prefers-color-scheme: dark)``
+ evaluates to true. It is injected into the page using
+ :meth:`~sphinx.application.Sphinx.add_css_file()`.
+
+* The **sidebars** setting gives the comma separated list of sidebar templates
+ for constructing sidebars. This can be overridden by the user in the
+ :confval:`html_sidebars` config value.
+
+* The **options** section contains pairs of variable names and default values.
+ These options can be overridden by the user in :confval:`html_theme_options`
+ and are accessible from all templates as ``theme_<name>``.
+
+.. versionadded:: 1.7
+ sidebar settings
+
+.. versionchanged:: 5.1
+
+ The stylesheet setting accepts multiple CSS filenames
+
+.. _distribute-your-theme:
+
+Distribute your theme as a Python package
+-----------------------------------------
+
+As a way to distribute your theme, you can use a Python package. This makes it
+easier for users to set up your theme.
+
+To distribute your theme as a Python package, please define an entry point
+called ``sphinx.html_themes`` in your ``setup.py`` file, and write a ``setup()``
+function to register your themes using ``add_html_theme()`` API in it::
+
+ # 'setup.py'
+ setup(
+ ...
+ entry_points = {
+ 'sphinx.html_themes': [
+ 'name_of_theme = your_package',
+ ]
+ },
+ ...
+ )
+
+ # 'your_package.py'
+ from os import path
+
+ def setup(app):
+ app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__)))
+
+If your theme package contains two or more themes, please call
+``add_html_theme()`` twice or more.
+
+.. versionadded:: 1.2
+ 'sphinx_themes' entry_points feature.
+
+.. deprecated:: 1.6
+ ``sphinx_themes`` entry_points has been deprecated.
+
+.. versionadded:: 1.6
+ ``sphinx.html_themes`` entry_points feature.
+
+
+Templating
+----------
+
+The :doc:`guide to templating <templating>` is helpful if you want to write your
+own templates. What is important to keep in mind is the order in which Sphinx
+searches for templates:
+
+* First, in the user's ``templates_path`` directories.
+* Then, in the selected theme.
+* Then, in its base theme, its base's base theme, etc.
+
+When extending a template in the base theme with the same name, use the theme
+name as an explicit directory: ``{% extends "basic/layout.html" %}``. From a
+user ``templates_path`` template, you can still use the "exclamation mark"
+syntax as described in the templating document.
+
+
+.. _theming-static-templates:
+
+Static templates
+~~~~~~~~~~~~~~~~
+
+Since theme options are meant for the user to configure a theme more easily,
+without having to write a custom stylesheet, it is necessary to be able to
+template static files as well as HTML files. Therefore, Sphinx supports
+so-called "static templates", like this:
+
+If the name of a file in the ``static/`` directory of a theme (or in the user's
+static path, for that matter) ends with ``_t``, it will be processed by the
+template engine. The ``_t`` will be left from the final file name. For
+example, the *classic* theme has a file ``static/classic.css_t`` which uses
+templating to put the color options into the stylesheet. When a documentation
+project is built with the classic theme, the output directory will contain a
+``_static/classic.css`` file where all template tags have been processed.
+
+
+Use custom page metadata in HTML templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Any key / value pairs in :doc:`field lists </usage/restructuredtext/field-lists>`
+that are placed *before* the page's title will be available to the Jinja
+template when building the page within the :data:`meta` attribute. For example,
+if a page had the following text before its first title:
+
+.. code-block:: rst
+
+ :mykey: My value
+
+ My first title
+ --------------
+
+Then it could be accessed within a Jinja template like so:
+
+.. code-block:: jinja
+
+ {%- if meta is mapping %}
+ {{ meta.get("mykey") }}
+ {%- endif %}
+
+Note the check that ``meta`` is a dictionary ("mapping" in Jinja
+terminology) to ensure that using it in this way is valid.
+
+
+Defining custom template functions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes it is useful to define your own function in Python that you wish to
+then use in a template. For example, if you'd like to insert a template value
+with logic that depends on the user's configuration in the project, or if you'd
+like to include non-trivial checks and provide friendly error messages for
+incorrect configuration in the template.
+
+To define your own template function, you'll need to define two functions
+inside your module:
+
+* A **page context event handler** (or **registration**) function. This is
+ connected to the :class:`.Sphinx` application via an event callback.
+* A **template function** that you will use in your Jinja template.
+
+First, define the registration function, which accepts the arguments for
+:event:`html-page-context`.
+
+Within the registration function, define the template function that you'd like to
+use within Jinja. The template function should return a string or Python objects
+(lists, dictionaries) with strings inside that Jinja uses in the templating process
+
+.. note::
+
+ The template function will have access to all of the variables that
+ are passed to the registration function.
+
+At the end of the registration function, add the template function to the
+Sphinx application's context with ``context['template_func'] = template_func``.
+
+Finally, in your extension's ``setup()`` function, add your registration
+function as a callback for :event:`html-page-context`.
+
+.. code-block:: python
+
+ # The registration function
+ def setup_my_func(app, pagename, templatename, context, doctree):
+ # The template function
+ def my_func(mystring):
+ return "Your string is %s" % mystring
+ # Add it to the page's context
+ context['my_func'] = my_func
+
+ # Your extension's setup function
+ def setup(app):
+ app.connect("html-page-context", setup_my_func)
+
+Now, you will have access to this function in jinja like so:
+
+.. code-block:: jinja
+
+ <div>
+ {{ my_func("some string") }}
+ </div>
+
+
+Add your own static files to the build assets
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default, Sphinx copies static files on the ``static/`` directory of the template
+directory. However, if your package needs to place static files outside of the
+``static/`` directory for some reasons, you need to copy them to the ``_static/``
+directory of HTML outputs manually at the build via an event hook. Here is an
+example of code to accomplish this:
+
+.. code-block:: python
+
+ from os import path
+ from sphinx.util.fileutil import copy_asset_file
+
+ def copy_custom_files(app, exc):
+ if app.builder.format == 'html' and not exc:
+ staticdir = path.join(app.builder.outdir, '_static')
+ copy_asset_file('path/to/myextension/_static/myjsfile.js', staticdir)
+
+ def setup(app):
+ app.connect('build-finished', copy_custom_files)
+
+
+Inject JavaScript based on user configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If your extension makes use of JavaScript, it can be useful to allow users
+to control its behavior using their Sphinx configuration. However, this can
+be difficult to do if your JavaScript comes in the form of a static library
+(which will not be built with Jinja).
+
+There are two ways to inject variables into the JavaScript space based on user
+configuration.
+
+First, you may append ``_t`` to the end of any static files included with your
+extension. This will cause Sphinx to process these files with the templating
+engine, allowing you to embed variables and control behavior.
+
+For example, the following JavaScript structure:
+
+.. code-block:: none
+
+ mymodule/
+ ├── _static
+ │   └── myjsfile.js_t
+ └── mymodule.py
+
+Will result in the following static file placed in your HTML's build output:
+
+.. code-block:: none
+
+ _build/
+ └── html
+ └── _static
+    └── myjsfile.js
+
+See :ref:`theming-static-templates` for more information.
+
+Second, you may use the :meth:`.Sphinx.add_js_file` method without pointing it
+to a file. Normally, this method is used to insert a new JavaScript file
+into your site. However, if you do *not* pass a file path, but instead pass
+a string to the "body" argument, then this text will be inserted as JavaScript
+into your site's head. This allows you to insert variables into your project's
+JavaScript from Python.
+
+For example, the following code will read in a user-configured value and then
+insert this value as a JavaScript variable, which your extension's JavaScript
+code may use:
+
+.. code-block:: python
+
+ # This function reads in a variable and inserts it into JavaScript
+ def add_js_variable(app):
+ # This is a configuration that you've specified for users in `conf.py`
+ js_variable = app.config['my_javascript_variable']
+ js_text = "var my_variable = '%s';" % js_variable
+ app.add_js_file(None, body=js_text)
+ # We connect this function to the step after the builder is initialized
+ def setup(app):
+ # Tell Sphinx about this configuration variable
+ app.add_config_value('my_javascript_variable', 0, 'html')
+ # Run the function after the builder is initialized
+ app.connect('builder-inited', add_js_variable)
+
+As a result, in your theme you can use code that depends on the presence of
+this variable. Users can control the variable's value by defining it in their
+:file:`conf.py` file.
+
+
+.. [1] It is not an executable Python file, as opposed to :file:`conf.py`,
+ because that would pose an unnecessary security risk if themes are
+ shared.
diff --git a/doc/development/tutorials/autodoc_ext.rst b/doc/development/tutorials/autodoc_ext.rst
new file mode 100644
index 0000000..cfd23e7
--- /dev/null
+++ b/doc/development/tutorials/autodoc_ext.rst
@@ -0,0 +1,141 @@
+.. _autodoc_ext_tutorial:
+
+Developing autodoc extension for IntEnum
+========================================
+
+The objective of this tutorial is to create an extension that adds
+support for new type for autodoc. This autodoc extension will format
+the ``IntEnum`` class from Python standard library. (module ``enum``)
+
+Overview
+--------
+
+We want the extension that will create auto-documentation for IntEnum.
+``IntEnum`` is the integer enum class from standard library ``enum`` module.
+
+Currently this class has no special auto documentation behavior.
+
+We want to add following to autodoc:
+
+* A new ``autointenum`` directive that will document the ``IntEnum`` class.
+* The generated documentation will have all the enum possible values
+ with names.
+* The ``autointenum`` directive will have an option ``:hex:`` which will
+ cause the integers be printed in hexadecimal form.
+
+
+Prerequisites
+-------------
+
+We need the same setup as in :doc:`the previous extensions <todo>`. This time,
+we will be putting out extension in a file called :file:`autodoc_intenum.py`.
+The :file:`my_enums.py` will contain the sample enums we will document.
+
+Here is an example of the folder structure you might obtain:
+
+.. code-block:: text
+
+ └── source
+    ├── _ext
+ │   └── autodoc_intenum.py
+    ├── conf.py
+    ├── index.rst
+    └── my_enums.py
+
+
+Writing the extension
+---------------------
+
+Start with ``setup`` function for the extension.
+
+.. literalinclude:: examples/autodoc_intenum.py
+ :language: python
+ :linenos:
+ :pyobject: setup
+
+
+The :meth:`~sphinx.application.Sphinx.setup_extension` method will pull the
+autodoc extension because our new extension depends on autodoc.
+:meth:`~sphinx.application.Sphinx.add_autodocumenter` is the method that
+registers our new auto documenter class.
+
+We want to import certain objects from the autodoc extension:
+
+.. literalinclude:: examples/autodoc_intenum.py
+ :language: python
+ :linenos:
+ :lines: 1-7
+
+
+There are several different documenter classes such as ``MethodDocumenter``
+or ``AttributeDocumenter`` available in the autodoc extension but
+our new class is the subclass of ``ClassDocumenter`` which a
+documenter class used by autodoc to document classes.
+
+This is the definition of our new the auto-documenter class:
+
+.. literalinclude:: examples/autodoc_intenum.py
+ :language: python
+ :linenos:
+ :pyobject: IntEnumDocumenter
+
+
+Important attributes of the new class:
+
+**objtype**
+ This attribute determines the ``auto`` directive name. In
+ this case the auto directive will be ``autointenum``.
+
+**directivetype**
+ This attribute sets the generated directive name. In
+ this example the generated directive will be ``.. :py:class::``.
+
+**priority**
+ the larger the number the higher is the priority. We want our
+ documenter be higher priority than the parent.
+
+**option_spec**
+ option specifications. We copy the parent class options and
+ add a new option *hex*.
+
+
+Overridden members:
+
+**can_document_member**
+ This member is important to override. It should
+ return *True* when the passed object can be documented by this class.
+
+**add_directive_header**
+ This method generates the directive header. We add
+ **:final:** directive option. Remember to call **super** or no directive
+ will be generated.
+
+**add_content**
+ This method generates the body of the class documentation.
+ After calling the super method we generate lines for enum description.
+
+
+Using the extension
+-------------------
+
+You can now use the new autodoc directive to document any ``IntEnum``.
+
+For example, you have the following ``IntEnum``:
+
+.. code-block:: python
+ :caption: my_enums.py
+
+ class Colors(IntEnum):
+ """Colors enumerator"""
+ NONE = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+
+This will be the documentation file with auto-documentation directive:
+
+.. code-block:: rst
+ :caption: index.rst
+
+ .. autointenum:: my_enums.Colors
diff --git a/doc/development/tutorials/examples/README.rst b/doc/development/tutorials/examples/README.rst
new file mode 100644
index 0000000..2b9c01b
--- /dev/null
+++ b/doc/development/tutorials/examples/README.rst
@@ -0,0 +1,11 @@
+:orphan:
+
+Tutorial examples
+=================
+
+This directory contains a number of examples used in the tutorials. These are
+intended to be increasingly complex to demonstrate the various features of
+Sphinx, but should aim to be as complicated as necessary but no more.
+Individual sections are referenced by line numbers, meaning if you make changes
+to the source files, you should update the references in the documentation
+accordingly.
diff --git a/doc/development/tutorials/examples/autodoc_intenum.py b/doc/development/tutorials/examples/autodoc_intenum.py
new file mode 100644
index 0000000..75fa204
--- /dev/null
+++ b/doc/development/tutorials/examples/autodoc_intenum.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from enum import IntEnum
+from typing import TYPE_CHECKING, Any
+
+from sphinx.ext.autodoc import ClassDocumenter, bool_option
+
+if TYPE_CHECKING:
+ from docutils.statemachine import StringList
+
+ from sphinx.application import Sphinx
+
+
+class IntEnumDocumenter(ClassDocumenter):
+ objtype = 'intenum'
+ directivetype = ClassDocumenter.objtype
+ priority = 10 + ClassDocumenter.priority
+ option_spec = dict(ClassDocumenter.option_spec)
+ option_spec['hex'] = bool_option
+
+ @classmethod
+ def can_document_member(cls,
+ member: Any, membername: str,
+ isattr: bool, parent: Any) -> bool:
+ try:
+ return issubclass(member, IntEnum)
+ except TypeError:
+ return False
+
+ def add_directive_header(self, sig: str) -> None:
+ super().add_directive_header(sig)
+ self.add_line(' :final:', self.get_sourcename())
+
+ def add_content(self,
+ more_content: StringList | None,
+ no_docstring: bool = False,
+ ) -> None:
+
+ super().add_content(more_content, no_docstring)
+
+ source_name = self.get_sourcename()
+ enum_object: IntEnum = self.object
+ use_hex = self.options.hex
+ self.add_line('', source_name)
+
+ for the_member_name, enum_member in enum_object.__members__.items():
+ the_member_value = enum_member.value
+ if use_hex:
+ the_member_value = hex(the_member_value)
+
+ self.add_line(
+ f"**{the_member_name}**: {the_member_value}", source_name)
+ self.add_line('', source_name)
+
+
+def setup(app: Sphinx) -> None:
+ app.setup_extension('sphinx.ext.autodoc') # Require autodoc extension
+ app.add_autodocumenter(IntEnumDocumenter)
diff --git a/doc/development/tutorials/examples/helloworld.py b/doc/development/tutorials/examples/helloworld.py
new file mode 100644
index 0000000..d6d81fd
--- /dev/null
+++ b/doc/development/tutorials/examples/helloworld.py
@@ -0,0 +1,19 @@
+from docutils import nodes
+from docutils.parsers.rst import Directive
+
+
+class HelloWorld(Directive):
+
+ def run(self):
+ paragraph_node = nodes.paragraph(text='Hello World!')
+ return [paragraph_node]
+
+
+def setup(app):
+ app.add_directive("helloworld", HelloWorld)
+
+ return {
+ 'version': '0.1',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/doc/development/tutorials/examples/recipe.py b/doc/development/tutorials/examples/recipe.py
new file mode 100644
index 0000000..c7ebf2a
--- /dev/null
+++ b/doc/development/tutorials/examples/recipe.py
@@ -0,0 +1,159 @@
+from collections import defaultdict
+
+from docutils.parsers.rst import directives
+
+from sphinx import addnodes
+from sphinx.directives import ObjectDescription
+from sphinx.domains import Domain, Index
+from sphinx.roles import XRefRole
+from sphinx.util.nodes import make_refnode
+
+
+class RecipeDirective(ObjectDescription):
+ """A custom directive that describes a recipe."""
+
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ 'contains': directives.unchanged_required,
+ }
+
+ def handle_signature(self, sig, signode):
+ signode += addnodes.desc_name(text=sig)
+ return sig
+
+ def add_target_and_index(self, name_cls, sig, signode):
+ signode['ids'].append('recipe' + '-' + sig)
+ if 'contains' in self.options:
+ ingredients = [
+ x.strip() for x in self.options.get('contains').split(',')]
+
+ recipes = self.env.get_domain('recipe')
+ recipes.add_recipe(sig, ingredients)
+
+
+class IngredientIndex(Index):
+ """A custom index that creates an ingredient matrix."""
+
+ name = 'ingredient'
+ localname = 'Ingredient Index'
+ shortname = 'Ingredient'
+
+ def generate(self, docnames=None):
+ content = defaultdict(list)
+
+ recipes = {name: (dispname, typ, docname, anchor)
+ for name, dispname, typ, docname, anchor, _
+ in self.domain.get_objects()}
+ recipe_ingredients = self.domain.data['recipe_ingredients']
+ ingredient_recipes = defaultdict(list)
+
+ # flip from recipe_ingredients to ingredient_recipes
+ for recipe_name, ingredients in recipe_ingredients.items():
+ for ingredient in ingredients:
+ ingredient_recipes[ingredient].append(recipe_name)
+
+ # convert the mapping of ingredient to recipes to produce the expected
+ # output, shown below, using the ingredient name as a key to group
+ #
+ # name, subtype, docname, anchor, extra, qualifier, description
+ for ingredient, recipe_names in ingredient_recipes.items():
+ for recipe_name in recipe_names:
+ dispname, typ, docname, anchor = recipes[recipe_name]
+ content[ingredient].append(
+ (dispname, 0, docname, anchor, docname, '', typ))
+
+ # convert the dict to the sorted list of tuples expected
+ content = sorted(content.items())
+
+ return content, True
+
+
+class RecipeIndex(Index):
+ """A custom index that creates an recipe matrix."""
+
+ name = 'recipe'
+ localname = 'Recipe Index'
+ shortname = 'Recipe'
+
+ def generate(self, docnames=None):
+ content = defaultdict(list)
+
+ # sort the list of recipes in alphabetical order
+ recipes = self.domain.get_objects()
+ recipes = sorted(recipes, key=lambda recipe: recipe[0])
+
+ # generate the expected output, shown below, from the above using the
+ # first letter of the recipe as a key to group thing
+ #
+ # name, subtype, docname, anchor, extra, qualifier, description
+ for _name, dispname, typ, docname, anchor, _priority in recipes:
+ content[dispname[0].lower()].append(
+ (dispname, 0, docname, anchor, docname, '', typ))
+
+ # convert the dict to the sorted list of tuples expected
+ content = sorted(content.items())
+
+ return content, True
+
+
+class RecipeDomain(Domain):
+
+ name = 'recipe'
+ label = 'Recipe Sample'
+ roles = {
+ 'ref': XRefRole(),
+ }
+ directives = {
+ 'recipe': RecipeDirective,
+ }
+ indices = {
+ RecipeIndex,
+ IngredientIndex,
+ }
+ initial_data = {
+ 'recipes': [], # object list
+ 'recipe_ingredients': {}, # name -> object
+ }
+
+ def get_full_qualified_name(self, node):
+ return f'recipe.{node.arguments[0]}'
+
+ def get_objects(self):
+ yield from self.data['recipes']
+
+ def resolve_xref(self, env, fromdocname, builder, typ, target, node,
+ contnode):
+ match = [(docname, anchor)
+ for name, sig, typ, docname, anchor, prio
+ in self.get_objects() if sig == target]
+
+ if len(match) > 0:
+ todocname = match[0][0]
+ targ = match[0][1]
+
+ return make_refnode(builder, fromdocname, todocname, targ,
+ contnode, targ)
+ else:
+ print('Awww, found nothing')
+ return None
+
+ def add_recipe(self, signature, ingredients):
+ """Add a new recipe to the domain."""
+ name = f'recipe.{signature}'
+ anchor = f'recipe-{signature}'
+
+ self.data['recipe_ingredients'][name] = ingredients
+ # name, dispname, type, docname, anchor, priority
+ self.data['recipes'].append(
+ (name, signature, 'Recipe', self.env.docname, anchor, 0))
+
+
+def setup(app):
+ app.add_domain(RecipeDomain)
+
+ return {
+ 'version': '0.1',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/doc/development/tutorials/examples/todo.py b/doc/development/tutorials/examples/todo.py
new file mode 100644
index 0000000..15368f4
--- /dev/null
+++ b/doc/development/tutorials/examples/todo.py
@@ -0,0 +1,135 @@
+from docutils import nodes
+from docutils.parsers.rst import Directive
+
+from sphinx.locale import _
+from sphinx.util.docutils import SphinxDirective
+
+
+class todo(nodes.Admonition, nodes.Element):
+ pass
+
+
+class todolist(nodes.General, nodes.Element):
+ pass
+
+
+def visit_todo_node(self, node):
+ self.visit_admonition(node)
+
+
+def depart_todo_node(self, node):
+ self.depart_admonition(node)
+
+
+class TodolistDirective(Directive):
+
+ def run(self):
+ return [todolist('')]
+
+
+class TodoDirective(SphinxDirective):
+
+ # this enables content in the directive
+ has_content = True
+
+ def run(self):
+ targetid = 'todo-%d' % self.env.new_serialno('todo')
+ targetnode = nodes.target('', '', ids=[targetid])
+
+ todo_node = todo('\n'.join(self.content))
+ todo_node += nodes.title(_('Todo'), _('Todo'))
+ self.state.nested_parse(self.content, self.content_offset, todo_node)
+
+ if not hasattr(self.env, 'todo_all_todos'):
+ self.env.todo_all_todos = []
+
+ self.env.todo_all_todos.append({
+ 'docname': self.env.docname,
+ 'lineno': self.lineno,
+ 'todo': todo_node.deepcopy(),
+ 'target': targetnode,
+ })
+
+ return [targetnode, todo_node]
+
+
+def purge_todos(app, env, docname):
+ if not hasattr(env, 'todo_all_todos'):
+ return
+
+ env.todo_all_todos = [todo for todo in env.todo_all_todos
+ if todo['docname'] != docname]
+
+
+def merge_todos(app, env, docnames, other):
+ if not hasattr(env, 'todo_all_todos'):
+ env.todo_all_todos = []
+ if hasattr(other, 'todo_all_todos'):
+ env.todo_all_todos.extend(other.todo_all_todos)
+
+
+def process_todo_nodes(app, doctree, fromdocname):
+ if not app.config.todo_include_todos:
+ for node in doctree.findall(todo):
+ node.parent.remove(node)
+
+ # Replace all todolist nodes with a list of the collected todos.
+ # Augment each todo with a backlink to the original location.
+ env = app.builder.env
+
+ if not hasattr(env, 'todo_all_todos'):
+ env.todo_all_todos = []
+
+ for node in doctree.findall(todolist):
+ if not app.config.todo_include_todos:
+ node.replace_self([])
+ continue
+
+ content = []
+
+ for todo_info in env.todo_all_todos:
+ para = nodes.paragraph()
+ filename = env.doc2path(todo_info['docname'], base=None)
+ description = (
+ _('(The original entry is located in %s, line %d and can be found ') %
+ (filename, todo_info['lineno']))
+ para += nodes.Text(description)
+
+ # Create a reference
+ newnode = nodes.reference('', '')
+ innernode = nodes.emphasis(_('here'), _('here'))
+ newnode['refdocname'] = todo_info['docname']
+ newnode['refuri'] = app.builder.get_relative_uri(
+ fromdocname, todo_info['docname'])
+ newnode['refuri'] += '#' + todo_info['target']['refid']
+ newnode.append(innernode)
+ para += newnode
+ para += nodes.Text('.)')
+
+ # Insert into the todolist
+ content.append(todo_info['todo'])
+ content.append(para)
+
+ node.replace_self(content)
+
+
+def setup(app):
+ app.add_config_value('todo_include_todos', False, 'html')
+
+ app.add_node(todolist)
+ app.add_node(todo,
+ html=(visit_todo_node, depart_todo_node),
+ latex=(visit_todo_node, depart_todo_node),
+ text=(visit_todo_node, depart_todo_node))
+
+ app.add_directive('todo', TodoDirective)
+ app.add_directive('todolist', TodolistDirective)
+ app.connect('doctree-resolved', process_todo_nodes)
+ app.connect('env-purge-doc', purge_todos)
+ app.connect('env-merge-info', merge_todos)
+
+ return {
+ 'version': '0.1',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/doc/development/tutorials/helloworld.rst b/doc/development/tutorials/helloworld.rst
new file mode 100644
index 0000000..8940e3d
--- /dev/null
+++ b/doc/development/tutorials/helloworld.rst
@@ -0,0 +1,189 @@
+Developing a "Hello world" extension
+====================================
+
+The objective of this tutorial is to create a very basic extension that adds a
+new directive. This directive will output a paragraph containing "hello world".
+
+Only basic information is provided in this tutorial. For more information, refer
+to the :doc:`other tutorials <index>` that go into more details.
+
+.. warning::
+
+ For this extension, you will need some basic understanding of docutils_
+ and Python.
+
+
+Overview
+--------
+
+We want the extension to add the following to Sphinx:
+
+* A ``helloworld`` directive, that will simply output the text "hello world".
+
+
+Prerequisites
+-------------
+
+We will not be distributing this plugin via `PyPI`_ and will instead include it
+as part of an existing project. This means you will need to use an existing
+project or create a new one using :program:`sphinx-quickstart`.
+
+We assume you are using separate source (:file:`source`) and build
+(:file:`build`) folders. Your extension file could be in any folder of your
+project. In our case, let's do the following:
+
+#. Create an :file:`_ext` folder in :file:`source`
+#. Create a new Python file in the :file:`_ext` folder called
+ :file:`helloworld.py`
+
+Here is an example of the folder structure you might obtain:
+
+.. code-block:: text
+
+ └── source
+    ├── _ext
+ │   └── helloworld.py
+    ├── _static
+    ├── conf.py
+    ├── somefolder
+    ├── index.rst
+    ├── somefile.rst
+    └── someotherfile.rst
+
+
+Writing the extension
+---------------------
+
+Open :file:`helloworld.py` and paste the following code in it:
+
+.. literalinclude:: examples/helloworld.py
+ :language: python
+ :linenos:
+
+Some essential things are happening in this example, and you will see them for
+all directives.
+
+.. rubric:: The directive class
+
+Our new directive is declared in the ``HelloWorld`` class.
+
+.. literalinclude:: examples/helloworld.py
+ :language: python
+ :linenos:
+ :lines: 5-9
+
+This class extends the docutils_' ``Directive`` class. All extensions that
+create directives should extend this class.
+
+.. seealso::
+
+ `The docutils documentation on creating directives <docutils directives_>`_
+
+This class contains a ``run`` method. This method is a requirement and it is
+part of every directive. It contains the main logic of the directive and it
+returns a list of docutils nodes to be processed by Sphinx. These nodes are
+docutils' way of representing the content of a document. There are many types of
+nodes available: text, paragraph, reference, table, etc.
+
+.. seealso::
+
+ `The docutils documentation on nodes <docutils nodes_>`_
+
+The ``nodes.paragraph`` class creates a new paragraph node. A paragraph
+node typically contains some text that we can set during instantiation using
+the ``text`` parameter.
+
+.. rubric:: The ``setup`` function
+
+.. currentmodule:: sphinx.application
+
+This function is a requirement. We use it to plug our new directive into
+Sphinx.
+
+.. literalinclude:: examples/helloworld.py
+ :language: python
+ :linenos:
+ :lines: 12-
+
+The simplest thing you can do is to call the :meth:`~Sphinx.add_directive` method,
+which is what we've done here. For this particular call, the first argument is
+the name of the directive itself as used in a reST file. In this case, we would
+use ``helloworld``. For example:
+
+.. code-block:: rst
+
+ Some intro text here...
+
+ .. helloworld::
+
+ Some more text here...
+
+We also return the :ref:`extension metadata <ext-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 :file:`conf.py` file to make Sphinx
+aware of it. There are two steps necessary here:
+
+#. Add the :file:`_ext` directory to the `Python path`_ using
+ ``sys.path.append``. This should be placed at the top of the file.
+
+#. Update or create the :confval:`extensions` list and add the extension file
+ name to the list
+
+For example:
+
+.. code-block:: python
+
+ import os
+ import sys
+
+ sys.path.append(os.path.abspath("./_ext"))
+
+ extensions = ['helloworld']
+
+.. tip::
+
+ We're not distributing this 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:
+
+.. code-block:: rst
+
+ Some intro text here...
+
+ .. helloworld::
+
+ Some more text here...
+
+The sample above would generate:
+
+.. code-block:: text
+
+ Some intro text here...
+
+ Hello World!
+
+ Some more text here...
+
+
+Further reading
+---------------
+
+This is the very basic principle of an extension that creates a new directive.
+
+For a more advanced example, refer to :doc:`todo`.
+
+
+.. _docutils: https://docutils.sourceforge.io/
+.. _docutils directives: https://docutils.sourceforge.io/docs/howto/rst-directives.html
+.. _docutils nodes: https://docutils.sourceforge.io/docs/ref/doctree.html
+.. _PyPI: https://pypi.org/
+.. _Python package: https://packaging.python.org/
+.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
diff --git a/doc/development/tutorials/index.rst b/doc/development/tutorials/index.rst
new file mode 100644
index 0000000..a7eee48
--- /dev/null
+++ b/doc/development/tutorials/index.rst
@@ -0,0 +1,17 @@
+.. _extension-tutorials-index:
+
+Extension tutorials
+===================
+
+Refer to the following tutorials to get started with extension development.
+
+
+.. toctree::
+ :caption: Directive tutorials
+ :maxdepth: 1
+
+ helloworld
+ todo
+ recipe
+ autodoc_ext
+
diff --git a/doc/development/tutorials/recipe.rst b/doc/development/tutorials/recipe.rst
new file mode 100644
index 0000000..1ed428a
--- /dev/null
+++ b/doc/development/tutorials/recipe.rst
@@ -0,0 +1,227 @@
+Developing a "recipe" extension
+===============================
+
+The objective of this tutorial is to illustrate roles, directives and domains.
+Once complete, we will be able to use this extension to describe a recipe and
+reference that recipe from elsewhere in our documentation.
+
+.. note::
+
+ This tutorial is based on a guide first published on `opensource.com`_ and
+ is provided here with the original author's permission.
+
+ .. _opensource.com: https://opensource.com/article/18/11/building-custom-workflows-sphinx
+
+
+Overview
+--------
+
+We want the extension to add the following to Sphinx:
+
+* A ``recipe`` :term:`directive`, containing some content describing the recipe
+ steps, along with a ``:contains:`` option highlighting the main ingredients
+ of the recipe.
+
+* A ``ref`` :term:`role`, which provides a cross-reference to the recipe
+ itself.
+
+* A ``recipe`` :term:`domain`, which allows us to tie together the above role
+ and domain, along with things like indices.
+
+For that, we will need to add the following elements to Sphinx:
+
+* A new directive called ``recipe``
+
+* New indexes to allow us to reference ingredient and recipes
+
+* A new domain called ``recipe``, which will contain the ``recipe`` directive
+ and ``ref`` role
+
+
+Prerequisites
+-------------
+
+We need the same setup as in :doc:`the previous extensions <todo>`. This time,
+we will be putting out extension in a file called :file:`recipe.py`.
+
+Here is an example of the folder structure you might obtain:
+
+.. code-block:: text
+
+ └── source
+    ├── _ext
+ │   └── recipe.py
+    ├── conf.py
+    └── index.rst
+
+
+Writing the extension
+---------------------
+
+Open :file:`recipe.py` and paste the following code in it, all of which we will
+explain in detail shortly:
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+
+Let's look at each piece of this extension step-by-step to explain what's going
+on.
+
+.. rubric:: The directive class
+
+The first thing to examine is the ``RecipeDirective`` directive:
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+ :pyobject: RecipeDirective
+
+Unlike :doc:`helloworld` and :doc:`todo`, this directive doesn't derive from
+:class:`docutils.parsers.rst.Directive` and doesn't define a ``run`` method.
+Instead, it derives from :class:`sphinx.directives.ObjectDescription` and
+defines ``handle_signature`` and ``add_target_and_index`` methods. This is
+because ``ObjectDescription`` is a special-purpose directive that's intended
+for describing things like classes, functions, or, in our case, recipes. More
+specifically, ``handle_signature`` implements parsing the signature of the
+directive and passes on the object's name and type to its superclass, while
+``add_target_and_index`` adds a target (to link to) and an entry to the index
+for this node.
+
+We also see that this directive defines ``has_content``, ``required_arguments``
+and ``option_spec``. Unlike the ``TodoDirective`` directive added in the
+:doc:`previous tutorial <todo>`, this directive takes a single argument, the
+recipe name, and an option, ``contains``, in addition to the nested
+reStructuredText in the body.
+
+.. rubric:: The index classes
+
+.. currentmodule:: sphinx.domains
+
+.. todo:: Add brief overview of indices
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+ :pyobject: IngredientIndex
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+ :pyobject: RecipeIndex
+
+Both ``IngredientIndex`` and ``RecipeIndex`` are derived from :class:`Index`.
+They implement custom logic to generate a tuple of values that define the
+index. Note that ``RecipeIndex`` is a simple index that has only one entry.
+Extending it to cover more object types is not yet part of the code.
+
+Both indices use the method :meth:`Index.generate` to do their work. This
+method combines the information from our domain, sorts it, and returns it in a
+list structure that will be accepted by Sphinx. This might look complicated but
+all it really is is a list of tuples like ``('tomato', 'TomatoSoup', 'test',
+'rec-TomatoSoup',...)``. Refer to the :doc:`domain API guide
+</extdev/domainapi>` for more information on this API.
+
+These index pages can be referenced with the :rst:role:`ref` role by combining
+the domain name and the index ``name`` value. For example, ``RecipeIndex`` can be
+referenced with ``:ref:`recipe-recipe``` and ``IngredientIndex`` can be referenced
+with ``:ref:`recipe-ingredient```.
+
+.. rubric:: The domain
+
+A Sphinx domain is a specialized container that ties together roles,
+directives, and indices, among other things. Let's look at the domain we're
+creating here.
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+ :pyobject: RecipeDomain
+
+There are some interesting things to note about this ``recipe`` domain and domains
+in general. Firstly, we actually register our directives, roles and indices
+here, via the ``directives``, ``roles`` and ``indices`` attributes, rather than
+via calls later on in ``setup``. We can also note that we aren't actually
+defining a custom role and are instead reusing the
+:class:`sphinx.roles.XRefRole` role and defining the
+:class:`sphinx.domains.Domain.resolve_xref` method. This method takes two
+arguments, ``typ`` and ``target``, which refer to the cross-reference type and
+its target name. We'll use ``target`` to resolve our destination from our
+domain's ``recipes`` because we currently have only one type of node.
+
+Moving on, we can see that we've defined ``initial_data``. The values defined in
+``initial_data`` will be copied to ``env.domaindata[domain_name]`` as the
+initial data of the domain, and domain instances can access it via
+``self.data``. We see that we have defined two items in ``initial_data``:
+``recipes`` and ``recipe_ingredients``. Each contains a list of all objects
+defined (i.e. all recipes) and a hash that maps a canonical ingredient name to
+the list of objects. The way we name objects is common across our extension and
+is defined in the ``get_full_qualified_name`` method. For each object created,
+the canonical name is ``recipe.<recipename>``, where ``<recipename>`` is the
+name the documentation writer gives the object (a recipe). This enables the
+extension to use different object types that share the same name. Having a
+canonical name and central place for our objects is a huge advantage. Both our
+indices and our cross-referencing code use this feature.
+
+.. rubric:: The ``setup`` function
+
+.. currentmodule:: sphinx.application
+
+:doc:`As always <todo>`, the ``setup`` function is a requirement and is used to
+hook the various parts of our extension into Sphinx. Let's look at the
+``setup`` function for this extension.
+
+.. literalinclude:: examples/recipe.py
+ :language: python
+ :linenos:
+ :pyobject: setup
+
+This looks a little different to what we're used to seeing. There are no calls
+to :meth:`~Sphinx.add_directive` or even :meth:`~Sphinx.add_role`. Instead, we
+have a single call to :meth:`~Sphinx.add_domain` followed by some
+initialization of the :ref:`standard domain <domains-std>`. This is because we
+had already registered our directives, roles and indexes as part of the
+directive itself.
+
+
+Using the extension
+-------------------
+
+You can now use the extension throughout your project. For example:
+
+.. code-block:: rst
+ :caption: index.rst
+
+ Joe's Recipes
+ =============
+
+ Below are a collection of my favourite recipes. I highly recommend the
+ :recipe:ref:`TomatoSoup` recipe in particular!
+
+ .. toctree::
+
+ tomato-soup
+
+.. code-block:: rst
+ :caption: tomato-soup.rst
+
+ The recipe contains `tomato` and `cilantro`.
+
+ .. recipe:recipe:: TomatoSoup
+ :contains: tomato, cilantro, salt, pepper
+
+ This recipe is a tasty tomato soup, combine all ingredients
+ and cook.
+
+The important things to note are the use of the ``:recipe:ref:`` role to
+cross-reference the recipe actually defined elsewhere (using the
+``:recipe:recipe:`` directive).
+
+
+Further reading
+---------------
+
+For more information, refer to the `docutils`_ documentation and
+:doc:`/extdev/index`.
+
+.. _docutils: https://docutils.sourceforge.io/docs/
diff --git a/doc/development/tutorials/todo.rst b/doc/development/tutorials/todo.rst
new file mode 100644
index 0000000..f23d8ad
--- /dev/null
+++ b/doc/development/tutorials/todo.rst
@@ -0,0 +1,367 @@
+Developing a "TODO" extension
+=============================
+
+The objective of this tutorial is to create a more comprehensive extension than
+that created in :doc:`helloworld`. Whereas that guide just covered writing a
+custom :term:`directive`, this guide adds multiple directives, along with custom
+nodes, additional config values and custom event handlers. To this end, we will
+cover a ``todo`` extension that adds capabilities to include todo entries in the
+documentation, and to collect these in a central place. This is similar the
+``sphinxext.todo`` extension distributed with Sphinx.
+
+
+Overview
+--------
+
+.. note::
+ To understand the design of this extension, refer to
+ :ref:`important-objects` and :ref:`build-phases`.
+
+We want the extension to add the following to Sphinx:
+
+* A ``todo`` directive, containing some content that is marked with "TODO" and
+ only shown in the output if a new config value is set. Todo entries should not
+ be in the output by default.
+
+* A ``todolist`` directive that creates a list of all todo entries throughout
+ the documentation.
+
+For that, we will need to add the following elements to Sphinx:
+
+* New directives, called ``todo`` and ``todolist``.
+
+* New document tree nodes to represent these directives, conventionally also
+ called ``todo`` and ``todolist``. We wouldn't need new nodes if the new
+ directives only produced some content representable by existing nodes.
+
+* A new config value ``todo_include_todos`` (config value names should start
+ with the extension name, in order to stay unique) that controls whether todo
+ entries make it into the output.
+
+* New event handlers: one for the :event:`doctree-resolved` event, to
+ replace the todo and todolist nodes, one for :event:`env-merge-info`
+ to merge intermediate results from parallel builds, and one for
+ :event:`env-purge-doc` (the reason for that will be covered later).
+
+
+Prerequisites
+-------------
+
+As with :doc:`helloworld`, we will not be distributing this plugin via PyPI so
+once again we need a Sphinx project to call this from. You can use an existing
+project or create a new one using :program:`sphinx-quickstart`.
+
+We assume you are using separate source (:file:`source`) and build
+(:file:`build`) folders. Your extension file could be in any folder of your
+project. In our case, let's do the following:
+
+#. Create an :file:`_ext` folder in :file:`source`
+#. Create a new Python file in the :file:`_ext` folder called :file:`todo.py`
+
+Here is an example of the folder structure you might obtain:
+
+.. code-block:: text
+
+ └── source
+    ├── _ext
+ │   └── todo.py
+    ├── _static
+    ├── conf.py
+    ├── somefolder
+    ├── index.rst
+    ├── somefile.rst
+    └── someotherfile.rst
+
+
+Writing the extension
+---------------------
+
+Open :file:`todo.py` and paste the following code in it, all of which we will
+explain in detail shortly:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+
+This is far more extensive extension than the one detailed in :doc:`helloworld`,
+however, we will will look at each piece step-by-step to explain what's
+happening.
+
+.. rubric:: The node classes
+
+Let's start with the node classes:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 8-21
+
+Node classes usually don't have to do anything except inherit from the standard
+docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from
+``Admonition`` because it should be handled like a note or warning, ``todolist``
+is just a "general" node.
+
+.. note::
+
+ Many extensions will not have to create their own node classes and work fine
+ with the nodes already provided by `docutils
+ <https://docutils.sourceforge.io/docs/ref/doctree.html>`__ and :ref:`Sphinx
+ <nodes>`.
+
+.. attention::
+
+ It is important to know that while you can extend Sphinx without
+ leaving your ``conf.py``, if you declare an inherited node right
+ there, you'll hit an unobvious :py:class:`~pickle.PickleError`. So if
+ something goes wrong, please make sure that you put inherited nodes
+ into a separate Python module.
+
+ For more details, see:
+
+ - https://github.com/sphinx-doc/sphinx/issues/6751
+ - https://github.com/sphinx-doc/sphinx/issues/1493
+ - https://github.com/sphinx-doc/sphinx/issues/1424
+
+.. rubric:: The directive classes
+
+A directive class is a class deriving usually from
+:class:`docutils.parsers.rst.Directive`. The directive interface is also
+covered in detail in the `docutils documentation`_; the important thing is that
+the class should have attributes that configure the allowed markup, and a
+``run`` method that returns a list of nodes.
+
+Looking first at the ``TodolistDirective`` directive:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 24-27
+
+It's very simple, creating and returning an instance of our ``todolist`` node
+class. The ``TodolistDirective`` directive itself has neither content nor
+arguments that need to be handled. That brings us to the ``TodoDirective``
+directive:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 30-53
+
+Several important things are covered here. First, as you can see, we're now
+subclassing the :class:`~sphinx.util.docutils.SphinxDirective` helper class
+instead of the usual :class:`~docutils.parsers.rst.Directive` class. This
+gives us access to the :ref:`build environment instance <important-objects>`
+using the ``self.env`` property. Without this, we'd have to use the rather
+convoluted ``self.state.document.settings.env``. Then, to act as a link target
+(from ``TodolistDirective``), the ``TodoDirective`` directive needs to return a
+target node in addition to the ``todo`` node. The target ID (in HTML, this will
+be the anchor name) is generated by using ``env.new_serialno`` which returns a
+new unique integer on each call and therefore leads to unique target names. The
+target node is instantiated without any text (the first two arguments).
+
+On creating admonition node, the content body of the directive are parsed using
+``self.state.nested_parse``. 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 ``todo`` node. Following this, the ``todo``
+node is added to the environment. This is needed to be able to create a list of
+all todo entries throughout the documentation, in the place where the author
+puts a ``todolist`` directive. For this case, the environment attribute
+``todo_all_todos`` is used (again, the name should be unique, so it is prefixed
+by the extension name). It does not exist when a new environment is created, so
+the directive must check and create it if necessary. Various information about
+the todo entry's location are stored along with a copy of the node.
+
+In the last line, the nodes that should be put into the doctree are returned:
+the target node and the admonition node.
+
+The node structure that the directive returns looks like this::
+
+ +--------------------+
+ | target node |
+ +--------------------+
+ +--------------------+
+ | todo node |
+ +--------------------+
+ \__+--------------------+
+ | admonition title |
+ +--------------------+
+ | paragraph |
+ +--------------------+
+ | ... |
+ +--------------------+
+
+.. rubric:: The event handlers
+
+Event handlers are one of Sphinx's most powerful features, providing a way to
+do hook into any part of the documentation process. There are many events
+provided by Sphinx itself, as detailed in :ref:`the API guide <events>`, and
+we're going to use a subset of them here.
+
+Let's look at the event handlers used in the above example. First, the one for
+the :event:`env-purge-doc` event:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 56-61
+
+Since we store information from source files in the environment, which is
+persistent, it may become out of date when the source file changes. Therefore,
+before each source file is read, the environment's records of it are cleared,
+and the :event:`env-purge-doc` event gives extensions a chance to do the same.
+Here we clear out all todos whose docname matches the given one from the
+``todo_all_todos`` list. If there are todos left in the document, they will be
+added again during parsing.
+
+The next handler, for the :event:`env-merge-info` event, is used
+during parallel builds. As during parallel builds all threads have
+their own ``env``, there's multiple ``todo_all_todos`` lists that need
+to be merged:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 64-68
+
+
+The other handler belongs to the :event:`doctree-resolved` event:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 71-113
+
+The :event:`doctree-resolved` event is emitted at the end of :ref:`phase 3
+(resolving) <build-phases>` and allows custom resolving to be done. The handler
+we have written for this event is a bit more involved. If the
+``todo_include_todos`` config value (which we'll describe shortly) is false,
+all ``todo`` and ``todolist`` nodes are removed from the documents. If not,
+``todo`` nodes just stay where and how they are. ``todolist`` nodes are
+replaced by a list of todo entries, complete with backlinks to the location
+where they come from. The list items are composed of the nodes from the
+``todo`` entry and docutils nodes created on the fly: a paragraph for each
+entry, containing text that gives the location, and a link (reference node
+containing an italic node) with the backreference. The reference URI is built
+by :meth:`sphinx.builders.Builder.get_relative_uri` which creates a suitable
+URI depending on the used builder, and appending the todo node's (the target's)
+ID as the anchor name.
+
+.. rubric:: The ``setup`` function
+
+.. currentmodule:: sphinx.application
+
+As noted :doc:`previously <helloworld>`, the ``setup`` function is a requirement
+and is used to plug directives into Sphinx. However, we also use it to hook up
+the other parts of our extension. Let's look at our ``setup`` function:
+
+.. literalinclude:: examples/todo.py
+ :language: python
+ :linenos:
+ :lines: 116-
+
+The calls in this function refer to the classes and functions we added earlier.
+What the individual calls do is the following:
+
+* :meth:`~Sphinx.add_config_value` lets Sphinx know that it should recognize the
+ new *config value* ``todo_include_todos``, whose default value should be
+ ``False`` (this also tells Sphinx that it is a boolean value).
+
+ If the third argument was ``'html'``, HTML documents would be full rebuild if the
+ config value changed its value. This is needed for config values that
+ influence reading (build :ref:`phase 1 (reading) <build-phases>`).
+
+* :meth:`~Sphinx.add_node` adds a new *node class* to the build system. It also
+ can specify visitor functions for each supported output format. These visitor
+ functions are needed when the new nodes stay until :ref:`phase 4 (writing)
+ <build-phases>`. Since the ``todolist`` node is always replaced in
+ :ref:`phase 3 (resolving) <build-phases>`, it doesn't need any.
+
+* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class.
+
+* Finally, :meth:`~Sphinx.connect` adds an *event handler* to the event whose
+ name is given by the first argument. The event handler function is called
+ with several arguments which are documented with the event.
+
+With this, our extension is complete.
+
+
+Using the extension
+-------------------
+
+As before, we need to enable the extension by declaring it in our
+:file:`conf.py` file. There are two steps necessary here:
+
+#. Add the :file:`_ext` directory to the `Python path`_ using
+ ``sys.path.append``. This should be placed at the top of the file.
+
+#. Update or create the :confval:`extensions` list and add the extension file
+ name to the list
+
+In addition, we may wish to set the ``todo_include_todos`` config value. As
+noted above, this defaults to ``False`` but we can set it explicitly.
+
+For example:
+
+.. code-block:: python
+
+ import os
+ import sys
+
+ sys.path.append(os.path.abspath("./_ext"))
+
+ extensions = ['todo']
+
+ todo_include_todos = False
+
+You can now use the extension throughout your project. For example:
+
+.. code-block:: rst
+ :caption: index.rst
+
+ Hello, world
+ ============
+
+ .. toctree::
+ somefile.rst
+ someotherfile.rst
+
+ Hello world. Below is the list of TODOs.
+
+ .. todolist::
+
+.. code-block:: rst
+ :caption: somefile.rst
+
+ foo
+ ===
+
+ Some intro text here...
+
+ .. todo:: Fix this
+
+.. code-block:: rst
+ :caption: someotherfile.rst
+
+ bar
+ ===
+
+ Some more text here...
+
+ .. todo:: Fix that
+
+Because we have configured ``todo_include_todos`` to ``False``, we won't
+actually see anything rendered for the ``todo`` and ``todolist`` directives.
+However, if we toggle this to true, we will see the output described
+previously.
+
+
+Further reading
+---------------
+
+For more information, refer to the `docutils`_ documentation and
+:doc:`/extdev/index`.
+
+
+.. _docutils: https://docutils.sourceforge.io/docs/
+.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
+.. _docutils documentation: https://docutils.sourceforge.io/docs/ref/rst/directives.html