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

1

My first footnote.

2 ( 1 , 2 )

My second footnote.

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