From a4ec3e96718c9522aaaa68e102a3fd962d1c7c1b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 5 May 2019 14:14:23 +0200 Subject: [PATCH] documentation/python: initial support for documentation pages. --- doc/documentation/python.rst | 73 ++++++++++++++--- documentation/python.py | 91 +++++++++++++++++++-- documentation/templates/python/page.html | 6 ++ documentation/test_python/page/another.html | 32 ++++++++ documentation/test_python/page/another.rst | 6 ++ documentation/test_python/page/index.html | 31 +++++++ documentation/test_python/page/index.rst | 4 + documentation/test_python/page/pages.html | 47 +++++++++++ documentation/test_python/test_page.py | 37 +++++++++ 9 files changed, 310 insertions(+), 17 deletions(-) create mode 100644 documentation/test_python/page/another.html create mode 100644 documentation/test_python/page/another.rst create mode 100644 documentation/test_python/page/index.html create mode 100644 documentation/test_python/page/index.rst create mode 100644 documentation/test_python/page/pages.html create mode 100644 documentation/test_python/test_page.py diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 0c13aed9..ceed523b 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -145,7 +145,11 @@ Variable Description `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 ` + 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. @@ -191,6 +195,9 @@ Variable Description 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 @@ -352,8 +359,8 @@ search to a subdomain: 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) @@ -523,6 +530,43 @@ non-obvious way to document enum values as well. 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 ` (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 ` and exposed as +HTML, otherwise as a plain text. + `pybind11 compatibility`_ ========================= @@ -644,8 +688,8 @@ and ``m-dark.documentation.compiled.css`` files are produced like this: ./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: @@ -654,10 +698,9 @@ 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`_ @@ -685,9 +728,11 @@ Property Description :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 @@ -711,7 +756,8 @@ Property Description 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 @@ -744,6 +790,11 @@ Property Description 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`_ ```````````````````` diff --git a/documentation/python.py b/documentation/python.py index 59a7aecc..ec14d007 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -26,6 +26,7 @@ import argparse import copy +import docutils import enum import urllib.parse import html @@ -56,6 +57,7 @@ default_config = { 'PROJECT_SUBTITLE': None, 'MAIN_PROJECT_URL': None, 'INPUT_MODULES': [], + 'INPUT_PAGES': [], 'OUTPUT': 'output', 'THEME_COLOR': '#22272e', 'FAVICON': 'favicon-dark.png', @@ -68,11 +70,16 @@ default_config = { ('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 @@ -830,6 +837,73 @@ def render_class(state: State, path, class_, env): 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
tags around the content, + # also explicitly disable the

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( @@ -869,6 +943,9 @@ def run(basedir, config, templates): 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]): @@ -895,12 +972,14 @@ def run(basedir, config, templates): # 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']): diff --git a/documentation/templates/python/page.html b/documentation/templates/python/page.html index 41bc6970..f3be8bad 100644 --- a/documentation/templates/python/page.html +++ b/documentation/templates/python/page.html @@ -6,4 +6,10 @@

{%+ for name, target in page.breadcrumb[:-1] %}{{ name }}.{% endfor %}{{ page.breadcrumb[-1][0] }}

+ {% if page.summary %} +

{{ page.summary }}

+ {% endif %} + {% if page.content %} +{{ page.content }} + {% endif %} {% endblock %} diff --git a/documentation/test_python/page/another.html b/documentation/test_python/page/another.html new file mode 100644 index 00000000..5501daf9 --- /dev/null +++ b/documentation/test_python/page/another.html @@ -0,0 +1,32 @@ + + + + + Another page | My Python Project + + + + + +
+
+
+
+
+

+ Another page +

+

Here's some summary. It's formated as well.

+

And the text.

+
+
+
+
+ + diff --git a/documentation/test_python/page/another.rst b/documentation/test_python/page/another.rst new file mode 100644 index 00000000..a6508a09 --- /dev/null +++ b/documentation/test_python/page/another.rst @@ -0,0 +1,6 @@ +Another page +############ + +:summary: Here's some summary. **It's formated as well.** + +And the *text*. diff --git a/documentation/test_python/page/index.html b/documentation/test_python/page/index.html new file mode 100644 index 00000000..845beadc --- /dev/null +++ b/documentation/test_python/page/index.html @@ -0,0 +1,31 @@ + + + + + Main Page | My Python Project + + + + + +
+
+
+
+
+

+ Main Page +

+

This page is not shown in the page tree.

+
+
+
+
+ + diff --git a/documentation/test_python/page/index.rst b/documentation/test_python/page/index.rst new file mode 100644 index 00000000..a2c051a2 --- /dev/null +++ b/documentation/test_python/page/index.rst @@ -0,0 +1,4 @@ +Main Page +######### + +This page is not shown in the page tree. diff --git a/documentation/test_python/page/pages.html b/documentation/test_python/page/pages.html new file mode 100644 index 00000000..6caa460f --- /dev/null +++ b/documentation/test_python/page/pages.html @@ -0,0 +1,47 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Pages

+
    +
  • Another page Here's some summary. It's formated as well.
  • +
+ +
+
+
+
+ + diff --git a/documentation/test_python/test_page.py b/documentation/test_python/test_page.py new file mode 100644 index 00000000..b61415c8 --- /dev/null +++ b/documentation/test_python/test_page.py @@ -0,0 +1,37 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# 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')) -- 2.30.2