`shown here <{filename}/css/page-layout.rst#link-back-to-main-site-from-a-subsite>`_.
:py:`INPUT_MODULES: List[Any]` List of modules to generate the docs from.
Values can be either strings or module
- objects.
+ objects. See `Module inspection`_ for more
+ information.
+:py:`INPUT_PAGES: List[str]` List of :abbr:`reST <reStructuredText>`
+ files for standalone pages. See `Pages`_
+ for more information.
:py:`OUTPUT: str` Where to save the output. Relative paths
relative are to the config file base dir;
if not set, ``output/`` is used.
put into the footer. If not set, a default
generic text is used. If empty, no footer
is rendered at all.
+:py:`FORMATTED_METADATA: List[str]` Which meatadata fields should be formatted
+ in documentation pages. By default only
+ the ``summary`` field is.
:py:`CLASS_INDEX_EXPAND_LEVELS` How many levels of the class index tree to
expand. :py:`0` means only the top-level
symbols are shown. If not set, :py:`1` is
SEARCH_EXTERNAL_URL = 'https://google.com/search?q=site:doc.magnum.graphics+{query}'
-`Content`_
-==========
+`Module inspection`_
+====================
By default, if a module contains the :py:`__all__` attribute, all names listed
there are exposed in the documentation. Otherwise, all module (and class)
The documentation output for enums includes enum value values and the class it
was derived from, so it's possible to know whether it's an enum or a flag.
+`Pages`_
+========
+
+In addition to documentation generated by inspecting particular module, it's
+possible to add dedicated documentation pages. Content is written in
+:abbr:`reST <reStructuredText>` (see
+`Writing reST content <{filename}/themes/writing-rst-content.rst>`_ for a short
+introduction) and taken from files specified in :py:`INPUT_PAGES`. Filenames
+are interpreted relative to configuration file path, output filename is input
+basename with extension replaced to ``.html``. In particular, content of
+a ``index.rst`` file is used for the documentation main page. Example:
+
+.. code:: py
+
+ INPUT_PAGES = ['pages/index.rst']
+
+.. code:: rst
+
+ My Python library
+ =================
+
+ :summary: Welcome on the main page!
+
+ This is a documentation of the mypythonlib module. You can use it like
+ this:
+
+ .. code:: py
+
+ import mypythonlib
+ mypythonlib.foo()
+
+Apart from :py:`:summary:`, the page can have any number of metadata, with all
+of them exposed as properties of ``page`` in the `output templates`_. Fields
+listed in :py:`FORMATTED_METADATA` (the :py:`:summary:` is among them) are
+expected to be formatted as :abbr:`reST <reStructuredText>` and exposed as
+HTML, otherwise as a plain text.
+
`pybind11 compatibility`_
=========================
./postprocess.py m-dark.css m-documentation.css -o m-dark+documentation.compiled.css
./postprocess.py m-dark.css m-documentation.css --no-import -o m-dark.documentation.compiled.css
-`Module documentation template`_
---------------------------------
+`Output templates`_
+-------------------
Each output file is rendered with one of these templates:
======================= =======================================================
Filename Use
======================= =======================================================
-``module.html`` Module documentation. See `Module properties`_ for more
- information.
-``class.html`` Class documentation. See `Class properties`_ for more
- information.
+``module.html`` Module documentation
+``class.html`` Class documentation
+``page.html`` Explicit documentation pages, including the main page
======================= =======================================================
Each template gets passed all configuration values from the `Configuration`_
:py:`page.url` File URL
:py:`page.breadcrumb` List of :py:`(title, URL)` tuples for
breadcrumb navigation.
+:py:`page.content` Detailed documentation, if any
======================================= =======================================
-Each module page has the following additional properties:
+Each module page, rendered with ``module.html``, has the following additional
+properties:
.. class:: m-table m-fullwidth
description block [2]_
======================================= =======================================
-Each class page has the following additional properties:
+Each class page, rendered with ``class.html``, has the following additional
+properties:
.. class:: m-table m-fullwidth
description block [2]_
======================================= =======================================
+Explicit documentation pages rendered with ``class.html`` have additional
+properties taken from input metadata. If given metadata is listed in
+:py:`FORMATTED_METADATA`, it's rendered into HTML, otherwise it's exposed as
+plain text.
+
`Module properties`_
````````````````````
import argparse
import copy
+import docutils
import enum
import urllib.parse
import html
'PROJECT_SUBTITLE': None,
'MAIN_PROJECT_URL': None,
'INPUT_MODULES': [],
+ 'INPUT_PAGES': [],
'OUTPUT': 'output',
'THEME_COLOR': '#22272e',
'FAVICON': 'favicon-dark.png',
('Modules', 'modules', []),
('Classes', 'classes', [])],
'LINKS_NAVBAR2': [],
+
'PAGE_HEADER': None,
'FINE_PRINT': '[default]',
+ 'FORMATTED_METADATA': ['summary'],
+
'CLASS_INDEX_EXPAND_LEVELS': 1,
'CLASS_INDEX_EXPAND_INNER': False,
+
'PYBIND11_COMPATIBILITY': False,
+
'SEARCH_DISABLED': False,
'SEARCH_DOWNLOAD_BINARY': False,
'SEARCH_HELP': """.. raw:: html
render(state.config, 'class.html', page, env)
return index_entry
+def publish_rst(source):
+ pub = docutils.core.Publisher(
+ writer=m.htmlsanity.SaneHtmlWriter(),
+ source_class=docutils.io.StringInput,
+ destination_class=docutils.io.StringOutput)
+ pub.set_components('standalone', 'restructuredtext', 'html')
+ #pub.writer.translator_class = m.htmlsanity.SaneHtmlTranslator
+ pub.process_programmatic_settings(None, m.htmlsanity.docutils_settings, None)
+ # Docutils uses a deprecated U mode for opening files, so instead of
+ # monkey-patching docutils.io.FileInput to not do that (like Pelican does),
+ # I just read the thing myself.
+ pub.set_source(source=source)
+ pub.publish()
+ return pub
+
+def render_page(state: State, path, filename, env):
+ logging.debug("generating %s.html", '.'.join(path))
+
+ # Render the file
+ with open(filename, 'r') as f: pub = publish_rst(f.read())
+
+ # Extract metadata from the page
+ metadata = {}
+ for docinfo in pub.document.traverse(docutils.nodes.docinfo):
+ for element in docinfo.children:
+ if element.tagname == 'field':
+ name_elem, body_elem = element.children
+ name = name_elem.astext()
+ if name in state.config['FORMATTED_METADATA']:
+ # If the metadata are formatted, format them. Use a special
+ # translator that doesn't add <dd> tags around the content,
+ # also explicitly disable the <p> around as we not need it
+ # always.
+ # TODO: uncrapify this a bit
+ visitor = m.htmlsanity._SaneFieldBodyTranslator(pub.document)
+ visitor.compact_field_list = True
+ body_elem.walkabout(visitor)
+ value = visitor.astext()
+ else:
+ value = body_elem.astext()
+ metadata[name.lower()] = value
+
+ # Breadcrumb, we don't do page hierarchy yet
+ assert len(path) == 1
+ breadcrumb = [(pub.writer.parts.get('title'), path[0] + '.html')]
+
+ page = Empty()
+ page.url = breadcrumb[-1][1]
+ page.breadcrumb = breadcrumb
+ page.prefix_wbr = path[0]
+
+ # Set page content and add extra metadata from there
+ page.content = pub.writer.parts.get('body').rstrip()
+ for key, value in metadata.items(): setattr(page, key, value)
+ if not hasattr(page, 'summary'): page.summary = ''
+
+ render(state.config, 'page.html', page, env)
+
+ # Index entry for this page, return only if it's not an index
+ if path == ['index']: return []
+ index_entry = IndexEntry()
+ index_entry.kind = 'page'
+ index_entry.name = breadcrumb[-1][0]
+ index_entry.url = page.url
+ index_entry.summary = page.summary
+ return [index_entry]
+
def run(basedir, config, templates):
# Prepare Jinja environment
env = jinja2.Environment(
state.class_index += [render_module(state, [module_name], module, env)]
+ for page in config['INPUT_PAGES']:
+ state.page_index += render_page(state, [os.path.splitext(os.path.basename(page))[0]], os.path.join(basedir, page), env)
+
# Recurse into the tree and mark every node that has nested modules with
# has_nestaable_children.
def mark_nested_modules(list: List[IndexEntry]):
# TODO could keep_trailing_newline fix this better?
f.write(b'\n')
- # Create index.html
- # TODO: use actual reST source and have this just as a fallback
- page = Empty()
- page.breadcrumb = [(config['PROJECT_TITLE'], 'index.html')]
- page.url = page.breadcrumb[-1][1]
- render(config, 'page.html', page, env)
+ # Create index.html if it was not provided by the user
+ if 'index.rst' not in [os.path.basename(i) for i in config['INPUT_PAGES']]:
+ logging.debug("writing index.html for an empty main page")
+
+ page = Empty()
+ page.breadcrumb = [(config['PROJECT_TITLE'], 'index.html')]
+ page.url = page.breadcrumb[-1][1]
+ render(config, 'page.html', page, env)
# Copy referenced files
for i in config['STYLESHEETS'] + config['EXTRA_FILES'] + ([config['FAVICON'][0]] if config['FAVICON'] else []) + ([] if config['SEARCH_DISABLED'] else ['search.js']):
<h1>
{%+ for name, target in page.breadcrumb[:-1] %}<span class="m-breadcrumb"><a href="{{ target }}">{{ name }}</a>.<wbr/></span>{% endfor %}{{ page.breadcrumb[-1][0] }}
</h1>
+ {% if page.summary %}
+ <p>{{ page.summary }}</p>
+ {% endif %}
+ {% if page.content %}
+{{ page.content }}
+ {% endif %}
{% endblock %}
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>Another page | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ Another page
+ </h1>
+ <p>Here's some summary. <strong>It's formated as well.</strong></p>
+<p>And the <em>text</em>.</p>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+Another page
+############
+
+:summary: Here's some summary. **It's formated as well.**
+
+And the *text*.
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>Main Page | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ Main Page
+ </h1>
+<p>This page is not shown in the page tree.</p>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+Main Page
+#########
+
+This page is not shown in the page tree.
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>Pages</h2>
+ <ul class="m-doc">
+ <li><a href="another.html" class="m-doc">Another page</a> <span class="m-doc">Here's some summary. <strong>It's formated as well.</strong></span></li>
+ </ul>
+ <script>
+ function toggle(e) {
+ e.parentElement.className = e.parentElement.className == 'm-doc-collapsible' ?
+ 'm-doc-expansible' : 'm-doc-collapsible';
+ return false;
+ }
+ /* Collapse all nodes marked as such. Doing it via JS instead of
+ directly in markup so disabling it doesn't harm usability. The list
+ is somehow regenerated on every iteration and shrinks as I change
+ the classes. It's not documented anywhere and I'm not sure if this
+ is the same across browsers, so I am going backwards in that list to
+ be sure. */
+ var collapsed = document.getElementsByClassName("collapsed");
+ for(var i = collapsed.length - 1; i >= 0; --i)
+ collapsed[i].className = 'm-doc-expansible';
+ </script>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+#
+# This file is part of m.css.
+#
+# Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+
+from . import BaseTestCase
+
+class Page(BaseTestCase):
+ def __init__(self, *args, **kwargs):
+ super().__init__(__file__, '', *args, **kwargs)
+
+ def test(self):
+ self.run_python({
+ 'INPUT_PAGES': ['index.rst', 'another.rst']
+ })
+ self.assertEqual(*self.actual_expected_contents('index.html'))
+ self.assertEqual(*self.actual_expected_contents('another.html'))
+ self.assertEqual(*self.actual_expected_contents('pages.html'))