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_symbiflow_theme", os.path.join(html_theme_path()[0], "sphinx_symbiflow_theme")
 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,
164               parent_id: str) -> List[dict]:
165    out = []
166    idx = 0
167    for child in node.find_all("li", recursive=False):
168        if callable(child.isspace) and child.isspace():
169            continue
170        formatted = {}
171        formatted["nested"] = False
172        if child.a is not None:
173            formatted["href"] = child.a["href"]
174            formatted["contents"] = "".join(map(str, child.a.contents))
175            if fix_root and formatted["href"] == "#" and child.a.contents:
176                slug = slugify.slugify(page_name) + ROOT_SUFFIX
177                formatted["href"] = "#" + slug
178            formatted["current"] = "current" in child.a.get("class", [])
179        if child.ul is not None:
180            formatted["nested"] = True
181            formatted["id"] = "{}-{}".format(parent_id, idx)
182            formatted["children"] = ul_to_list(child.ul, fix_root, page_name,
183                                               formatted["id"])
184        else:
185            formatted["children"] = []
186        out.append(formatted)
187        idx += 1
188    return out
189
190
191class CaptionList(list):
192    _caption = ""
193
194    def __init__(self, caption=""):
195        super().__init__()
196        self._caption = caption
197
198    @property
199    def caption(self):
200        return self._caption
201
202    @caption.setter
203    def caption(self, value):
204        self._caption = value
205
206
207def derender_toc(
208    toc_text, fix_root=True, page_name: str = "md-page-root--link"
209) -> List[dict]:
210    nodes = []
211    try:
212        toc = BeautifulSoup(toc_text, features="html.parser")
213        idx = 0
214        for child in toc.children:
215            if callable(child.isspace) and child.isspace():
216                continue
217            if child.name == "p":
218                nodes.append({"caption": "".join(map(str, child.contents))})
219            elif child.name == "ul":
220                nodes.extend(ul_to_list(child, fix_root, page_name, idx))
221            else:
222                raise NotImplementedError
223            idx += 1
224    except Exception as exc:
225        logger = logging.getLogger(__name__)
226        logger.warning(
227            "Failed to process toctree_text\n" + str(exc) + "\n" + str(toc_text)
228        )
229
230    return nodes
231
232
233def walk_contents(tags):
234    out = []
235    for tag in tags.contents:
236        if hasattr(tag, "contents"):
237            out.append(walk_contents(tag))
238        else:
239            out.append(str(tag))
240    return "".join(out)
241
242
243def table_fix(body_text, page_name="md-page-root--link"):
244    # This is a hack to skip certain classes of tables
245    ignore_table_classes = {"highlighttable", "longtable", "dataframe"} | set(
246        USER_TABLE_NO_STRIP_CLASSES
247    )
248    try:
249        body = BeautifulSoup(body_text, features="html.parser")
250        for table in body.select("table"):
251            classes = set(table.get("class", tuple()))
252            if classes.intersection(ignore_table_classes):
253                continue
254            classes = [tc for tc in classes if tc in USER_TABLE_CLASSES]
255            if classes:
256                table["class"] = classes
257            else:
258                del table["class"]
259        first_h1: Optional[bs4.element.Tag] = body.find("h1")
260        headers = body.find_all(re.compile("^h[1-6]$"))
261        for i, header in enumerate(headers):
262            for a in header.select("a"):
263                if "headerlink" in a.get("class", ""):
264                    header["id"] = a["href"][1:]
265        if first_h1 is not None:
266            slug = slugify.slugify(page_name) + ROOT_SUFFIX
267            first_h1["id"] = slug
268            for a in first_h1.select("a"):
269                a["href"] = "#" + slug
270
271        divs = body.find_all("div", {"class": "section"})
272        for div in divs:
273            div.unwrap()
274
275        return str(body)
276    except Exception as exc:
277        logger = logging.getLogger(__name__)
278        logger.warning("Failed to process body_text\n" + str(exc))
279        return body_text
280
281
282# These final lines exist to give sphinx a stable str representation of
283# these two functions across runs, and to ensure that the str changes
284# if the source does.
285#
286# Note that this would be better down with a metaclass factory
287table_fix_src = inspect.getsource(table_fix)
288table_fix_hash = hashlib.sha512(table_fix_src.encode()).hexdigest()
289derender_toc_src = inspect.getsource(derender_toc)
290derender_toc_hash = hashlib.sha512(derender_toc_src.encode()).hexdigest()
291
292
293class TableFixMeta(type):
294    def __repr__(self):
295        return f"table_fix, hash: {table_fix_hash}"
296
297    def __str__(self):
298        return f"table_fix, hash: {table_fix_hash}"
299
300
301class TableFix(object, metaclass=TableFixMeta):
302    def __new__(cls, *args, **kwargs):
303        return table_fix(*args, **kwargs)
304
305
306class DerenderTocMeta(type):
307    def __repr__(self):
308        return f"derender_toc, hash: {derender_toc_hash}"
309
310    def __str__(self):
311        return f"derender_toc, hash: {derender_toc_hash}"
312
313
314class DerenderToc(object, metaclass=DerenderTocMeta):
315    def __new__(cls, *args, **kwargs):
316        return derender_toc(*args, **kwargs)
317
318
319def get_html_context():
320    return {"table_fix": TableFix, "derender_toc": DerenderToc}
321
322from . import _version
323__version__ = _version.get_versions()['version']

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