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
129    from css_html_js_minify.css_minifier import css_minify
130
131    css_files = glob.glob(os.path.join(app.outdir, "**", "*.css"), recursive=True)
132    print("Minifying {0} css files".format(len(css_files)))
133    for css_file in css_files:
134        colorized = console.colorize("blue", css_file)
135        msg = "minifying css file {0}".format(colorized)
136        sys.stdout.write("\033[K" + msg + "\r")
137        with open(css_file, "r", encoding="utf-8") as content:
138            css = css_minify(content.read())
139        with open(css_file, "w", encoding="utf-8") as content:
140            content.write(css)
141    print()
142    app.multiprocess_manager.shutdown()
143
144
145def update_html_context(app):
146    config = app.config
147    config.html_context = {**get_html_context(), **config.html_context}
148
149
150def update_table_classes(app, config):
151    table_classes = config.html_theme_options.get("table_classes")
152    if table_classes:
153        USER_TABLE_CLASSES.extend(table_classes)
154
155    table_no_strip_classes = config.html_theme_options.get("table_no_strip")
156    if table_no_strip_classes:
157        USER_TABLE_NO_STRIP_CLASSES.extend(table_no_strip_classes)
158
159
160def html_theme_path():
161    return [os.path.dirname(os.path.abspath(__file__))]
162
163
164def ul_to_list(node: bs4.element.Tag, fix_root: bool, page_name: str) -> List[dict]:
165    out = []
166    for child in node.find_all("li", recursive=False):
167        if callable(child.isspace) and child.isspace():
168            continue
169        formatted = {}
170        if child.a is not None:
171            formatted["href"] = child.a["href"]
172            formatted["contents"] = "".join(map(str, child.a.contents))
173            if fix_root and formatted["href"] == "#" and child.a.contents:
174                slug = slugify.slugify(page_name) + ROOT_SUFFIX
175                formatted["href"] = "#" + slug
176            formatted["current"] = "current" in child.a.get("class", [])
177        if child.ul is not None:
178            formatted["children"] = ul_to_list(child.ul, fix_root, page_name)
179        else:
180            formatted["children"] = []
181        out.append(formatted)
182    return out
183
184
185class CaptionList(list):
186    _caption = ""
187
188    def __init__(self, caption=""):
189        super().__init__()
190        self._caption = caption
191
192    @property
193    def caption(self):
194        return self._caption
195
196    @caption.setter
197    def caption(self, value):
198        self._caption = value
199
200
201def derender_toc(
202    toc_text, fix_root=True, page_name: str = "md-page-root--link"
203) -> List[dict]:
204    nodes = []
205    try:
206        toc = BeautifulSoup(toc_text, features="html.parser")
207        for child in toc.children:
208            if callable(child.isspace) and child.isspace():
209                continue
210            if child.name == "p":
211                nodes.append({"caption": "".join(map(str, child.contents))})
212            elif child.name == "ul":
213                nodes.extend(ul_to_list(child, fix_root, page_name))
214            else:
215                raise NotImplementedError
216    except Exception as exc:
217        logger = logging.getLogger(__name__)
218        logger.warning(
219            "Failed to process toctree_text\n" + str(exc) + "\n" + str(toc_text)
220        )
221
222    return nodes
223
224
225def walk_contents(tags):
226    out = []
227    for tag in tags.contents:
228        if hasattr(tag, "contents"):
229            out.append(walk_contents(tag))
230        else:
231            out.append(str(tag))
232    return "".join(out)
233
234
235def table_fix(body_text, page_name="md-page-root--link"):
236    # This is a hack to skip certain classes of tables
237    ignore_table_classes = {"highlighttable", "longtable", "dataframe"} | set(
238        USER_TABLE_NO_STRIP_CLASSES
239    )
240    try:
241        body = BeautifulSoup(body_text, features="html.parser")
242        for table in body.select("table"):
243            classes = set(table.get("class", tuple()))
244            if classes.intersection(ignore_table_classes):
245                continue
246            classes = [tc for tc in classes if tc in USER_TABLE_CLASSES]
247            if classes:
248                table["class"] = classes
249            else:
250                del table["class"]
251        first_h1: Optional[bs4.element.Tag] = body.find("h1")
252        headers = body.find_all(re.compile("^h[1-6]$"))
253        for i, header in enumerate(headers):
254            for a in header.select("a"):
255                if "headerlink" in a.get("class", ""):
256                    header["id"] = a["href"][1:]
257        if first_h1 is not None:
258            slug = slugify.slugify(page_name) + ROOT_SUFFIX
259            first_h1["id"] = slug
260            for a in first_h1.select("a"):
261                a["href"] = "#" + slug
262
263        divs = body.find_all("div", {"class": "section"})
264        for div in divs:
265            div.unwrap()
266
267        return str(body)
268    except Exception as exc:
269        logger = logging.getLogger(__name__)
270        logger.warning("Failed to process body_text\n" + str(exc))
271        return body_text
272
273
274# These final lines exist to give sphinx a stable str representation of
275# these two functions across runs, and to ensure that the str changes
276# if the source does.
277#
278# Note that this would be better down with a metaclass factory
279table_fix_src = inspect.getsource(table_fix)
280table_fix_hash = hashlib.sha512(table_fix_src.encode()).hexdigest()
281derender_toc_src = inspect.getsource(derender_toc)
282derender_toc_hash = hashlib.sha512(derender_toc_src.encode()).hexdigest()
283
284
285class TableFixMeta(type):
286    def __repr__(self):
287        return f"table_fix, hash: {table_fix_hash}"
288
289    def __str__(self):
290        return f"table_fix, hash: {table_fix_hash}"
291
292
293class TableFix(object, metaclass=TableFixMeta):
294    def __new__(cls, *args, **kwargs):
295        return table_fix(*args, **kwargs)
296
297
298class DerenderTocMeta(type):
299    def __repr__(self):
300        return f"derender_toc, hash: {derender_toc_hash}"
301
302    def __str__(self):
303        return f"derender_toc, hash: {derender_toc_hash}"
304
305
306class DerenderToc(object, metaclass=DerenderTocMeta):
307    def __new__(cls, *args, **kwargs):
308        return derender_toc(*args, **kwargs)
309
310
311def get_html_context():
312    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