Additional Samples

Various examples of styling applied to Sphinx constructs. You can view the source of this page to see the specific reStructuredText used to create these examples.

Subpages

Suppages get bread crumbs when they are not at the top level.

Headings

This is a first level heading ( h1 ).

Sub-Heading

This is a second level heading ( h2 ).

Sub-Sub-Heading

This is a third level heading ( h3 ).

Code

The theme uses pygments for inline code text and

multiline
code text

Here’s an included example with line numbers.

  1"""Sphinx Material theme."""
  2
  3import hashlib
  4import inspect
  5import os
  6import re
  7import sys
  8from multiprocessing import Manager
  9from pathlib import Path
 10from typing import List, Optional
 11from xml.etree import ElementTree
 12
 13import bs4
 14import slugify
 15from bs4 import BeautifulSoup
 16from sphinx.util import console, logging
 17
 18from ._version import get_versions
 19
 20__version__ = get_versions()["version"]
 21del get_versions
 22
 23ROOT_SUFFIX = "--page-root"
 24
 25USER_TABLE_CLASSES = []
 26USER_TABLE_NO_STRIP_CLASSES = ["no-sphinx-material-strip"]
 27
 28
 29def setup(app):
 30    """Setup connects events to the sitemap builder"""
 31    app.connect("html-page-context", add_html_link)
 32    app.connect("build-finished", create_sitemap)
 33    app.connect("build-finished", reformat_pages)
 34    app.connect("build-finished", minify_css)
 35    app.connect("builder-inited", update_html_context)
 36    app.connect("config-inited", update_table_classes)
 37    manager = Manager()
 38    site_pages = manager.list()
 39    sitemap_links = manager.list()
 40    app.multiprocess_manager = manager
 41    app.sitemap_links = sitemap_links
 42    app.site_pages = site_pages
 43    app.add_html_theme(
 44        "sphinx_material", os.path.join(html_theme_path()[0], "sphinx_material")
 45    )
 46    return {
 47        "version": __version__,
 48        "parallel_read_safe": True,
 49        "parallel_write_safe": True,
 50    }
 51
 52
 53def add_html_link(app, pagename, templatename, context, doctree):
 54    """As each page is built, collect page names for the sitemap"""
 55    base_url = app.config["html_theme_options"].get("base_url", "")
 56    if base_url:
 57        app.sitemap_links.append(base_url + pagename + ".html")
 58    minify = app.config["html_theme_options"].get("html_minify", False)
 59    prettify = app.config["html_theme_options"].get("html_prettify", False)
 60    if minify and prettify:
 61        raise ValueError("html_minify and html_prettify cannot both be True")
 62    if minify or prettify:
 63        app.site_pages.append(os.path.join(app.outdir, pagename + ".html"))
 64
 65
 66def create_sitemap(app, exception):
 67    """Generates the sitemap.xml from the collected HTML page links"""
 68    if (
 69        not app.config["html_theme_options"].get("base_url", "")
 70        or exception is not None
 71        or not app.sitemap_links
 72    ):
 73        return
 74
 75    outdir = app.outdir if isinstance(app.outdir, Path) else str(app.outdir)
 76    filename = str(Path(outdir) / "sitemap.xml")
 77
 78    print(
 79        "Generating sitemap for {0} pages in "
 80        "{1}".format(len(app.sitemap_links), console.colorize("blue", filename))
 81    )
 82
 83    root = ElementTree.Element("urlset")
 84    root.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
 85
 86    for link in app.sitemap_links:
 87        url = ElementTree.SubElement(root, "url")
 88        ElementTree.SubElement(url, "loc").text = link
 89    app.sitemap_links[:] = []
 90
 91    ElementTree.ElementTree(root).write(filename)
 92
 93
 94def reformat_pages(app, exception):
 95    if exception is not None or not app.site_pages:
 96        return
 97    minify = app.config["html_theme_options"].get("html_minify", False)
 98    last = -1
 99    npages = len(app.site_pages)
100    transform = "Minifying" if minify else "Prettifying"
101    print("{0} {1} files".format(transform, npages))
102    transform = transform.lower()
103    # TODO: Consider using parallel execution
104    for i, page in enumerate(app.site_pages):
105        if int(100 * (i / npages)) - last >= 1:
106            last = int(100 * (i / npages))
107            color_page = console.colorize("blue", page)
108            msg = "{0} files... [{1}%] {2}".format(transform, last, color_page)
109            sys.stdout.write("\033[K" + msg + "\r")
110        with open(page, "r", encoding="utf-8") as content:
111            if minify:
112                from css_html_js_minify.html_minifier import html_minify
113
114                html = html_minify(content.read())
115            else:
116                soup = BeautifulSoup(content.read(), features="lxml")
117                html = soup.prettify()
118        with open(page, "w", encoding="utf-8") as content:
119            content.write(html)
120    app.site_pages[:] = []
121    print()
122
123
124def minify_css(app, exception):
125    if exception is not None or not app.config["html_theme_options"].get(
126        "css_minify", False
127    ):
128        app.multiprocess_manager.shutdown()
129        return
130    import glob
131
132    from css_html_js_minify.css_minifier import css_minify
133
134    css_files = glob.glob(os.path.join(app.outdir, "**", "*.css"), recursive=True)
135    print("Minifying {0} css files".format(len(css_files)))
136    for css_file in css_files:
137        colorized = console.colorize("blue", css_file)
138        msg = "minifying css file {0}".format(colorized)
139        sys.stdout.write("\033[K" + msg + "\r")
140        with open(css_file, "r", encoding="utf-8") as content:
141            css = css_minify(content.read())
142        with open(css_file, "w", encoding="utf-8") as content:
143            content.write(css)
144    print()
145    app.multiprocess_manager.shutdown()
146
147
148def update_html_context(app):
149    config = app.config
150    config.html_context = {**get_html_context(), **config.html_context}
151
152
153def update_table_classes(app, config):
154    table_classes = config.html_theme_options.get("table_classes")
155    if table_classes:
156        USER_TABLE_CLASSES.extend(table_classes)
157
158    table_no_strip_classes = config.html_theme_options.get("table_no_strip")
159    if table_no_strip_classes:
160        USER_TABLE_NO_STRIP_CLASSES.extend(table_no_strip_classes)
161
162
163def html_theme_path():
164    return [os.path.dirname(os.path.abspath(__file__))]
165
166
167def ul_to_list(node: bs4.element.Tag, fix_root: bool, page_name: str) -> List[dict]:
168    out = []
169    for child in node.find_all("li", recursive=False):
170        if callable(child.isspace) and child.isspace():
171            continue
172        formatted = {}
173        if child.a is not None:
174            formatted["href"] = child.a["href"]
175            formatted["contents"] = "".join(map(str, child.a.contents))
176            if fix_root and formatted["href"] == "#" and child.a.contents:
177                slug = slugify.slugify(page_name) + ROOT_SUFFIX
178                formatted["href"] = "#" + slug
179            formatted["current"] = "current" in child.a.get("class", [])
180        if child.ul is not None:
181            formatted["children"] = ul_to_list(child.ul, fix_root, page_name)
182        else:
183            formatted["children"] = []
184        out.append(formatted)
185    return out
186
187
188class CaptionList(list):
189    _caption = ""
190
191    def __init__(self, caption=""):
192        super().__init__()
193        self._caption = caption
194
195    @property
196    def caption(self):
197        return self._caption
198
199    @caption.setter
200    def caption(self, value):
201        self._caption = value
202
203
204def derender_toc(
205    toc_text, fix_root=True, page_name: str = "md-page-root--link"
206) -> List[dict]:
207    nodes = []
208    try:
209        toc = BeautifulSoup(toc_text, features="html.parser")
210        for child in toc.children:
211            if callable(child.isspace) and child.isspace():
212                continue
213            if child.name == "p":
214                nodes.append({"caption": "".join(map(str, child.contents))})
215            elif child.name == "ul":
216                nodes.extend(ul_to_list(child, fix_root, page_name))
217            else:
218                raise NotImplementedError
219    except Exception as exc:
220        logger = logging.getLogger(__name__)
221        logger.warning(
222            "Failed to process toctree_text\n" + str(exc) + "\n" + str(toc_text)
223        )
224
225    return nodes
226
227
228def walk_contents(tags):
229    out = []
230    for tag in tags.contents:
231        if hasattr(tag, "contents"):
232            out.append(walk_contents(tag))
233        else:
234            out.append(str(tag))
235    return "".join(out)
236
237
238def table_fix(body_text, page_name="md-page-root--link"):
239    # This is a hack to skip certain classes of tables
240    ignore_table_classes = {"highlighttable", "longtable", "dataframe"} | set(
241        USER_TABLE_NO_STRIP_CLASSES
242    )
243    try:
244        body = BeautifulSoup(body_text, features="html.parser")
245        for table in body.select("table"):
246            classes = set(table.get("class", tuple()))
247            if classes.intersection(ignore_table_classes):
248                continue
249            classes = [tc for tc in classes if tc in USER_TABLE_CLASSES]
250            if classes:
251                table["class"] = classes
252            else:
253                del table["class"]
254        first_h1: Optional[bs4.element.Tag] = body.find("h1")
255        headers = body.find_all(re.compile("^h[1-6]$"))
256        for i, header in enumerate(headers):
257            for a in header.select("a"):
258                if "headerlink" in a.get("class", ""):
259                    header["id"] = a["href"][1:]
260        if first_h1 is not None:
261            slug = slugify.slugify(page_name) + ROOT_SUFFIX
262            first_h1["id"] = slug
263            for a in first_h1.select("a"):
264                a["href"] = "#" + slug
265
266        divs = body.find_all("div", {"class": "section"})
267        for div in divs:
268            div.unwrap()
269
270        return str(body)
271    except Exception as exc:
272        logger = logging.getLogger(__name__)
273        logger.warning("Failed to process body_text\n" + str(exc))
274        return body_text
275
276
277# These final lines exist to give sphinx a stable str representation of
278# these two functions across runs, and to ensure that the str changes
279# if the source does.
280#
281# Note that this would be better down with a metaclass factory
282table_fix_src = inspect.getsource(table_fix)
283table_fix_hash = hashlib.sha512(table_fix_src.encode()).hexdigest()
284derender_toc_src = inspect.getsource(derender_toc)
285derender_toc_hash = hashlib.sha512(derender_toc_src.encode()).hexdigest()
286
287
288class TableFixMeta(type):
289    def __repr__(self):
290        return f"table_fix, hash: {table_fix_hash}"
291
292    def __str__(self):
293        return f"table_fix, hash: {table_fix_hash}"
294
295
296class TableFix(object, metaclass=TableFixMeta):
297    def __new__(cls, *args, **kwargs):
298        return table_fix(*args, **kwargs)
299
300
301class DerenderTocMeta(type):
302    def __repr__(self):
303        return f"derender_toc, hash: {derender_toc_hash}"
304
305    def __str__(self):
306        return f"derender_toc, hash: {derender_toc_hash}"
307
308
309class DerenderToc(object, metaclass=DerenderTocMeta):
310    def __new__(cls, *args, **kwargs):
311        return derender_toc(*args, **kwargs)
312
313
314def get_html_context():
315    return {"table_fix": TableFix, "derender_toc": DerenderToc}

It also works with existing Sphinx highlighting:

<html>
  <body>Hello World</body>
</html>
def hello():
    """Greet."""
    return "Hello World"
/**
 * Greet.
 */
function hello(): {
  return "Hello World";
}

Admonitions

The theme uses the admonition classes for Sphinx admonitions.

Note

Note

This is a note .

Todo

Todo

It is essential to complete todo items.

Warning

Warning

This is a warning .

Danger

Danger

This is danger -ous.

Attention

Attention

Do I have your attention ?

Caution

Caution

Use caution !

Error

Error

You have made a grave error .

Hint

Hint

Can you take a hint ?

Important

Important

It is important to correctly use admonitions.

Tip

Tip

Please tip your waiter.

Custom Admonitions

Custom

You can create your own admonitions with the default style.

Footnotes

I have footnoted a first item [ 1 ] and second item [ 2 ] . This also references the second item [ 2 ] .

Footnotes

Icons

The following template HTML:

<span style="font-size: 2rem;" class="md-icon">&#xe869;</span>

translates to a the site’s icon:

The material icon font provides hundreds to choose from. You can use the <i> tag or the <span> tag.

Tables

Here are some examples of Sphinx tables . The Sphinx Material all classes and only applies the default style to classless tables. If you want to use a custom table class, you will need to do two thing. First, apply it using .. cssclass:: custom-class and then add it to your configuration’s table_classes variable.

Grid

A grid table:

Header1

Header2

Header3

Header4

row1, cell1

cell2

cell3

cell4

row2 …

Simple

A simple table:

H1

H2

H3

cell1

cell2

cell3

User-styled Table

Note

table_classes is set to [“plain”] in the site’s configuration. Only plain remains as the class of the table. Other standard classes applied by Sphinx are removed.

This is feature demonstration. There is no css for the plain class, and so this is completely unstyled.

User

Styled

Table

cell1

cell2

cell3

List Tables

A List Table

Column 1

Column 2

Item 1

Item 2

Alignment

Warning

Alignment is not currently working as expected.

Center Aligned

Column 1

Column 2

Item 1

Item 2

Right Aligned

Treat

Quantity

Description

Albatross

2.99

On a stick!

Crunchy Frog

1.49

If we took the bones out, it wouldn’t be crunchy, now would it?

Gannet Ripple

1.99

On a stick!

Code Documentation

An example Python function.

format_exception ( etype , value , tb [ , limit=None ] )

Format the exception with a traceback.

Parameters :
  • etype – exception type

  • value – exception value

  • tb – traceback object

  • limit ( integer or None ) – maximum number of stack frames to show

Return type :

list of strings

An example JavaScript function.

class MyAnimal ( name [ , age ] )
Arguments :
  • name ( string() ) – The name of the animal

  • age ( number() ) – an optional age for the animal

Glossaries

environment

A structure where information about all documents under the root is saved, and used for cross-referencing. The environment is pickled after the parsing stage, so that successive runs only need to read and parse new and changed documents.

source directory

The directory which, including its subdirectories, contains all source files for one Sphinx project.

Math

\[ \begin{align}\begin{aligned}(a + b)^2 = a^2 + 2ab + b^2\\(a - b)^2 = a^2 - 2ab + b^2\end{aligned}\end{align} \]
\[\begin{split}(a + b)^2 &= (a + b)(a + b) \\ &= a^2 + 2ab + b^2\end{split}\]
\begin{eqnarray} y & = & ax^2 + bx + c \\ f(x) & = & x^2 + 2xy + y^2 \end{eqnarray}

Production Lists

try_stmt  ::=  try1_stmt | try2_stmt
try1_stmt ::=  "try" ":" suite
               ("except" [expression ["," target]] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try2_stmt ::=  "try" ":" suite
               "finally" ":" suite