It looks like we're 90% there, hah.
:py:`FORMATTED_METADATA: List[str]` Which meatadata fields should be formatted
in documentation pages. By default only
the ``summary`` field is.
+:py:`PLUGINS: List[str]` List of `plugins <{filename}/plugins.rst>`_
+ to use. See `Plugins`_ for more
+ information.
+:py:`PLUGIN_PATHS: List[str]` Additional plugin search paths. Relative
+ paths are relative to :py:`INPUT`.
: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
expected to be formatted as :abbr:`reST <reStructuredText>` and exposed as
HTML, otherwise as a plain text.
+`Plugins`_
+==========
+
+The :abbr:`reST <reStructuredText>` content is not limited to just the builtin
+functionality and it's possible to extend it via plugins eiter
+`from m.css itself <{filename}/plugins.rst>`_ or 3rd party ones. See
+documentation of each plugin to see its usage; the
+`m.htmlsanity <{filename}/plugins/htmlsanity.rst>`_ plugin is used
+unconditionally while all others are optional. For example, enabling the common
+m.css plugins might look like this:
+
+.. code:: py
+
+ PLUGINS = ['m.code', 'm.components', 'm.dox']
+
+`Implementing custom plugins`_
+------------------------------
+
+Other plugins can be loaded from paths specified in :py:`PLUGIN_PATHS`. Custom
+plugins need to implement a registration function named :py:`register_mcss()`.
+It gets passed the following named arguments and the plugin might or might not
+use them.
+
+.. class:: m-table
+
+=========================== ===================================================
+Keyword argument Content
+=========================== ===================================================
+:py:`mcss_settings` Dict containing all m.css settings
+:py:`jinja_environment` Jinja2 environment. Useful for adding new filters
+ etc.
+=========================== ===================================================
+
+Registration function for a plugin that needs to query the :py:`OUTPUT` setting
+might look like this --- the remaining keyword arguments will collapse into
+the :py:`**kwargs` parameter. See code of various m.css plugins for actual
+examples.
+
+.. code:: py
+
+ output_dir = None
+
+ …
+
+ def register_mcss(mcss_settings, **kwargs):
+ global output_dir
+ output_dir = mcss_settings['OUTPUT']
+
`pybind11 compatibility`_
=========================
:gh:`m.alias <mosra/m.css$master/plugins/m/alias.py>`
- :gh:`m.metadata <mosra/m.css$master/plugins/m/metadata.py>`
-All plugins that make sense in the context of the
-`Doxygen theme <{filename}/documentation/doxygen.rst>`_ are implicitly exposed
-to it, without needing to explicitly enable them.
+For the `Python doc theme <{filename}/documentation/python.rst>`_ it's enough
+to simply list them in :py:`PLUGINS`. For the `Doxygen theme <{filename}/documentation/doxygen.rst>`_,
+all plugins that make sense in its context are implicitly exposed to it,
+without needing to explicitly enable them.
Note that particular plugins can have additional dependencies, see
documentation of each of them to see more. Click on the headings below to get
PLUGINS += ['m.htmlsanity', 'm.components']
+`Python doc theme`_
+-------------------
+
+Simply list the plugin in your :py:`PLUGINS`. The `m.htmlsanity`_ plugin is
+available always, no need to mention it explicitly:
+
+.. code:: py
+
+ PLUGINS += ['m.components']
+
`Doxygen theme`_
----------------
pip3 install Pyphen
+`Python doc theme`_
+-------------------
+
+The ``m.htmlsanity`` plugin is available always, no need to mention it
+explicitly. However, the options aren't, so you might want to supply them.
+The same dependencies as for `Pelican`_ apply here.
+
+.. code:: py
+
+ M_HTMLSANITY_SMART_QUOTES = True
+ M_HTMLSANITY_HYPHENATION = True
+
`Doxygen theme`_
----------------
pip3 install Pillow
+`Python doc theme`_
+-------------------
+
+Simply list the plugin in your :py:`PLUGINS`. The `m.htmlsanity`_ plugin is
+available always, no need to mention it explicitly. The same dependencies as
+for `Pelican`_ apply here.
+
+.. code:: py
+
+ PLUGINS += ['m.images']
+ M_IMAGES_REQUIRE_ALT_TEXT = False
+
`Doxygen theme`_
----------------
For Pelican, download the `m/link.py <{filename}/plugins.rst>`_ file, put it
including the ``m/`` directory into one of your :py:`PLUGIN_PATHS` and add
-:py:`m.link` package to your :py:`PLUGINS` in ``pelicanconf.py``:
+:py:`m.link` package to your :py:`PLUGINS` in ``pelicanconf.py``. For the
+Python doc theme, it's enough to just list it in :py:`PLUGINS`:
.. code:: python
For Pelican, download the `m/gh.py <{filename}/plugins.rst>`_ file, put it
including the ``m/`` directory into one of your :py:`PLUGIN_PATHS` and add
-:py:`m.gh` package to your :py:`PLUGINS` in ``pelicanconf.py``:
+:py:`m.gh` package to your :py:`PLUGINS` in ``pelicanconf.py``. For the Python
+doc theme, it's enough to just list it in :py:`PLUGINS`:
.. code:: python
For Pelican, download the `m/gl.py <{filename}/plugins.rst>`_ file, put it
including the ``m/`` directory into one of your :py:`PLUGIN_PATHS` and add
-:py:`m.gl` package to your :py:`PLUGINS` in ``pelicanconf.py``:
+:py:`m.gl` package to your :py:`PLUGINS` in ``pelicanconf.py``. For the Python
+doc theme, it's enough to just list it in :py:`PLUGINS`:
.. code:: python
For Pelican, download the `m/vk.py <{filename}/plugins.rst>`_ file, put it
including the ``m/`` directory into one of your :py:`PLUGIN_PATHS` and add
-:py:`m.vk` package to your :py:`PLUGINS` in ``pelicanconf.py``:
+:py:`m.vk` package to your :py:`PLUGINS` in ``pelicanconf.py``. For the Python
+doc theme, it's enough to just list it in :py:`PLUGINS`:
.. code:: python
('doxygen/corrade.tag', 'https://doc.magnum.graphics/corrade/', ['Corrade::']),
('doxygen/magnum.tag', 'https://doc.magnum.graphics/magnum/', ['Magnum::'])]
+For the Python doc theme, the configuration is the same. Tag file paths are
+relative to the configuration file location or to :py:`PATH`, if specified.
+
Use the :rst:`:dox:` interpreted text role for linking to documented symbols.
All link targets understood by Doxygen's ``@ref`` or ``@link`` commands are
understood by this plugin as well, in addition it's possible to link to the
M_MATH_RENDER_AS_CODE = False
M_MATH_CACHE_FILE = 'm.math.cache'
+For the Python doc theme, it's enough to mention it in :py:`PLUGINS`. The
+`m.htmlsanity`_ plugin is available always, no need to mention it explicitly:
+
+.. code:: py
+
+ PLUGINS += ['m.code']
+
For the Doxygen theme, this feature is builtin. Use either the ``@f[`` command
for block-level math or the ``@f$`` command for inline math. It's possible to
add extra CSS classes by placing ``@m_class`` in a paragraph before the actual
PLUGINS += ['m-htmlsanity', 'm.code']
+For the Python doc theme, it's enough to mention it in :py:`PLUGINS`. The
+`m.htmlsanity`_ plugin is available always, no need to mention it explicitly:
+
+.. code:: py
+
+ PLUGINS += ['m.code']
+
For the Doxygen theme, this feature is builtin. Use the ``@code{.ext}`` command
either in a block or inline, the various ``@include`` and ``@snippet`` commands
support it as well. Language detection is done from the value of ``.ext`` in
For Pelican, download the `m/plots.py <{filename}/plugins.rst>`_ file, put it
including the ``m/`` directory into one of your :py:`PLUGIN_PATHS` and add
-``m.plots`` package to your :py:`PLUGINS` in ``pelicanconf.py``.
+``m.plots`` package to your :py:`PLUGINS` in ``pelicanconf.py``. For the
+Python doc theme, it's enough to just list it in :py:`PLUGINS`:
.. code:: python
'FINE_PRINT': '[default]',
'FORMATTED_METADATA': ['summary'],
+ 'PLUGINS': [],
+ 'PLUGIN_PATHS': [],
+
'CLASS_INDEX_EXPAND_LEVELS': 1,
'CLASS_INDEX_EXPAND_INNER': False,
env.filters['path_to_url'] = path_to_url
env.filters['urljoin'] = urljoin
- # Set up the plugins
- m.htmlsanity.register_mcss(config, env)
-
# Populate the INPUT, if not specified, make it absolute
if config['INPUT'] is None: config['INPUT'] = basedir
else: config['INPUT'] = os.path.join(basedir, config['INPUT'])
state = State(config)
+ # Set up extra plugin paths. The one for m.css plugins was added above.
+ for path in config['PLUGIN_PATHS']:
+ if path not in sys.path: sys.path.append(os.path.join(config['INPUT'], path))
+
+ # Import plugins
+ for plugin in ['m.htmlsanity'] + config['PLUGINS']:
+ module = importlib.import_module(plugin)
+ module.register_mcss(mcss_settings=config, jinja_environment=env)
+
for module in config['INPUT_MODULES']:
if isinstance(module, str):
module_name = module
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>Dot | 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>
+ Dot
+ </h1>
+<div class="m-graph m-warning">
+<svg style="width: 3.875rem; height: 11.875rem;" viewBox="0.00 0.00 62.00 190.31">
+<g transform="scale(1 1) rotate(0) translate(4 186.309)">
+<g class="m-node m-flat">
+<title>a</title>
+<ellipse cx="27" cy="-163.924" rx="27" ry="18.2703"/>
+<text text-anchor="middle" x="27" y="-160.124">a</text>
+</g>
+<g class="m-node m-flat">
+<title>b</title>
+<ellipse cx="27" cy="-91.1543" rx="27" ry="18.2703"/>
+<text text-anchor="middle" x="27" y="-87.3543">b</text>
+</g>
+<g class="m-edge">
+<title>a->b</title>
+<path d="M27,-145.43C27,-137.748 27,-128.539 27,-119.955"/>
+<polygon points="30.5001,-119.938 27,-109.938 23.5001,-119.938 30.5001,-119.938"/>
+</g>
+<g class="m-node m-flat">
+<title>c</title>
+<ellipse cx="27" cy="-18.3848" rx="27" ry="18.2703"/>
+<text text-anchor="middle" x="27" y="-14.5848">c</text>
+</g>
+<g class="m-edge">
+<title>b->c</title>
+<path d="M27,-72.6607C27,-64.9784 27,-55.7693 27,-47.185"/>
+<polygon points="30.5001,-47.1687 27,-37.1687 23.5001,-47.1687 30.5001,-47.1687"/>
+</g>
+</g>
+</svg>
+</div>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>Dot | 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>
+ Dot
+ </h1>
+<div class="m-graph m-warning">
+<svg style="width: 3.875rem; height: 11.875rem;" viewBox="0.00 0.00 62.00 190.31">
+<g transform="scale(1 1) rotate(0) translate(4 186.3087)">
+<g class="m-node m-flat">
+<title>a</title>
+<ellipse cx="27" cy="-163.9239" rx="27" ry="18.2703"/>
+<text text-anchor="middle" x="27" y="-160.1239">a</text>
+</g>
+<g class="m-node m-flat">
+<title>b</title>
+<ellipse cx="27" cy="-91.1543" rx="27" ry="18.2703"/>
+<text text-anchor="middle" x="27" y="-87.3543">b</text>
+</g>
+<g class="m-edge">
+<title>a->b</title>
+<path d="M27,-145.1839C27,-137.3999 27,-128.1942 27,-119.6079"/>
+<polygon points="30.5001,-119.5788 27,-109.5789 23.5001,-119.5789 30.5001,-119.5788"/>
+</g>
+<g class="m-node m-flat">
+<title>c</title>
+<ellipse cx="27" cy="-18.3848" rx="27" ry="18.2703"/>
+<text text-anchor="middle" x="27" y="-14.5848">c</text>
+</g>
+<g class="m-edge">
+<title>b->c</title>
+<path d="M27,-72.4144C27,-64.6303 27,-55.4246 27,-46.8383"/>
+<polygon points="30.5001,-46.8093 27,-36.8093 23.5001,-46.8094 30.5001,-46.8093"/>
+</g>
+</g>
+</svg>
+</div>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+Dot
+###
+
+.. digraph::
+ :class: m-warning
+
+ a -> b -> c
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>This shows some plugins | 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>
+ This shows some plugins
+ </h1>
+<p>This project is <a href="https://github.com/mosra/m.css">on GitHub</a> and</p>
+<table class="m-table">
+<thead>
+<tr><th>The</th>
+<th>tables</th>
+</tr>
+</thead>
+<tbody>
+<tr><td>render</td>
+<td>nicer now.</td>
+</tr>
+</tbody>
+</table>
+<aside class="m-note m-success">
+Yup!</aside>
+<p>See, <a href="https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glDrawElements.xhtml">glDrawElements()</a> is the grandpa of <a href="https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdDraw.html">vkCmdDraw()</a>. But
+<a class="m-flat" href="javascript:alert("boo!")">not everything is as it “seems” to be</a>
+— however the typography makes that bearable. Python bindings for
+<a href="https://doc.magnum.graphics/corrade/namespaceCorrade_1_1Containers.html">Corrade::Containers</a> and Magnum are nice too:</p>
+<pre class="m-code"><span class="gp">>>> </span><span class="kn">from</span> <span class="nn">magnum</span> <span class="kn">import</span> <span class="o">*</span>
+<span class="gp">>>> </span><span class="n">a</span> <span class="o">=</span> <span class="n">Vector3</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">)</span>
+<span class="gp">>>> </span><span class="n">a</span><span class="o">.</span><span class="n">dot</span><span class="p">()</span>
+<span class="go">14.0</span></pre>
+<p class="m-transition">~~~ Custom plugins! ~~~</p>
+<p>And now something totally different:</p>
+<style>
+div.m-plot svg { font-family: DejaVu Sans; }
+</style><div class="m-plot">
+<svg viewBox="0 0 576 113.76">
+ <defs>
+ <style type="text/css">
+*{stroke-linecap:butt;stroke-linejoin:round;}
+ </style>
+ </defs>
+ <g id="figure_1">
+ <g id="axes_1">
+ <g id="patch_1">
+ <path d="M 59.365156 69.588125 L 560.62 69.588125 L 560.62 27.757969 L 59.365156 27.757969 z" class="m-background"/>
+ </g>
+ <g id="plot1-value0"><title>15.0 meters, i guess?</title>
+ <path clip-path="url(#p2abd2612b1)" d="M 59.365156 29.659339 L 298.057939 29.659339 L 298.057939 46.560413 L 59.365156 46.560413 z" class="m-bar m-success"/>
+ </g>
+ <g id="plot1-value1"><title>30.0 meters, i guess?</title>
+ <path clip-path="url(#p2abd2612b1)" d="M 59.365156 50.785681 L 536.750722 50.785681 L 536.750722 67.686754 L 59.365156 67.686754 z" class="m-bar m-success"/>
+ </g>
+ <g id="matplotlib.axis_1">
+ <g id="xtick_1">
+ <g id="line2d_1">
+ <defs>
+ <path d="M 0 0 L 0 3.5" id="m15f0a304df" class="m-line"/>
+ </defs>
+ <g>
+ <use x="59.365156" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_1">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 59.365156, 84.946406)" x="59.365156" y="84.946406">0</text>
+ </g>
+ </g>
+ <g id="xtick_2">
+ <g id="line2d_2">
+ <g>
+ <use x="138.929417" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_2">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 138.929417, 84.946406)" x="138.929417" y="84.946406">5</text>
+ </g>
+ </g>
+ <g id="xtick_3">
+ <g id="line2d_3">
+ <g>
+ <use x="218.493678" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_3">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 218.493678, 84.946406)" x="218.493678" y="84.946406">10</text>
+ </g>
+ </g>
+ <g id="xtick_4">
+ <g id="line2d_4">
+ <g>
+ <use x="298.057939" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_4">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 298.057939, 84.946406)" x="298.057939" y="84.946406">15</text>
+ </g>
+ </g>
+ <g id="xtick_5">
+ <g id="line2d_5">
+ <g>
+ <use x="377.6222" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_5">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 377.6222, 84.946406)" x="377.6222" y="84.946406">20</text>
+ </g>
+ </g>
+ <g id="xtick_6">
+ <g id="line2d_6">
+ <g>
+ <use x="457.186461" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_6">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 457.186461, 84.946406)" x="457.186461" y="84.946406">25</text>
+ </g>
+ </g>
+ <g id="xtick_7">
+ <g id="line2d_7">
+ <g>
+ <use x="536.750722" xlink:href="#m15f0a304df" y="69.588125"/>
+ </g>
+ </g>
+ <g id="text_7">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 536.750722, 84.946406)" x="536.750722" y="84.946406">30</text>
+ </g>
+ </g>
+ <g id="text_8">
+ <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 309.992578, 99.592344)" x="309.992578" y="99.592344">meters, i guess?</text>
+ </g>
+ </g>
+ <g id="matplotlib.axis_2">
+ <g id="ytick_1">
+ <g id="line2d_8">
+ <defs>
+ <path d="M 0 0 L -3.5 0" id="mba4ce04b6c" class="m-line"/>
+ </defs>
+ <g>
+ <use x="59.365156" xlink:href="#mba4ce04b6c" y="38.109876"/>
+ </g>
+ </g>
+ <g id="text_9">
+ <text class="m-label" style="text-anchor:end;" transform="rotate(-0, 52.365156, 42.289017)" x="52.365156" y="42.289017">First</text>
+ </g>
+ </g>
+ <g id="ytick_2">
+ <g id="line2d_9">
+ <g>
+ <use x="59.365156" xlink:href="#mba4ce04b6c" y="59.236218"/>
+ </g>
+ </g>
+ <g id="text_10">
+ <text class="m-label" style="text-anchor:end;" transform="rotate(-0, 52.365156, 63.415358)" x="52.365156" y="63.415358">Second</text>
+ </g>
+ </g>
+ </g>
+ <g id="text_11">
+ <text class="m-title" style="text-anchor:middle;" transform="rotate(-0, 309.992578, 21.757969)" x="309.992578" y="21.757969">A plot with a single color</text>
+ </g>
+ </g>
+ </g>
+ <defs>
+ <clipPath id="p2abd2612b1">
+ <rect height="41.830156" width="501.254844" x="59.365156" y="27.757969"/>
+ </clipPath>
+ </defs>
+</svg>
+</div>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+This shows some plugins
+#######################
+
+This project is :gh:`on GitHub <mosra/m.css>` and
+
+.. class:: m-table
+
+====== ======
+The tables
+====== ======
+render nicer now.
+====== ======
+
+.. note-success::
+
+ Yup!
+
+.. role:: link-flat(link)
+ :class: m-flat
+
+See, :glfn:`DrawElements` is the grandpa of :vkfn:`CmdDraw`. But
+:link-flat:`not everything is as it "seems" to be <javascript:alert("boo!")>`
+--- however the typography makes that bearable. Python bindings for
+:dox:`Corrade::Containers` and Magnum are nice too:
+
+.. code:: pycon
+
+ >>> from magnum import *
+ >>> a = Vector3(1.0, 2.0, 3.0)
+ >>> a.dot()
+ 14.0
+
+.. fancy-line:: Custom plugins!
+
+And now something totally different:
+
+.. raw:: html
+
+ <style>
+ div.m-plot svg { font-family: DejaVu Sans; }
+ </style>
+
+.. plot:: A plot with a single color
+ :type: barh
+ :labels:
+ First
+ Second
+ :units: meters, i guess?
+ :values: 15 30
+ :colors: success
--- /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 docutils.parsers import rst
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.roles import set_classes
+from docutils import nodes
+
+class FancyLine(rst.Directive):
+ final_argument_whitespace = True
+ has_content = False
+ required_arguments = 1
+
+ def run(self):
+ text = '~~~ {} ~~~'.format(self.arguments[0])
+ title_nodes, _ = self.state.inline_text(text, self.lineno)
+ node = nodes.paragraph('', '', *title_nodes)
+ node['classes'] += ['m-transition']
+ return [node]
+
+def register_mcss(**kwargs):
+ rst.directives.register_directive('fancy-line', FancyLine)
# DEALINGS IN THE SOFTWARE.
#
+import os
+import re
+import subprocess
+
+from distutils.version import LooseVersion
+
from . import BaseTestCase
+def dot_version():
+ return re.match(".*version (?P<version>\d+\.\d+\.\d+).*", subprocess.check_output(['dot', '-V'], stderr=subprocess.STDOUT).decode('utf-8').strip()).group('version')
+
class Page(BaseTestCase):
def __init__(self, *args, **kwargs):
super().__init__(__file__, '', *args, **kwargs)
})
# The same output as Page, just the file is taken from elsewhere
self.assertEqual(*self.actual_expected_contents('index.html', '../page/index.html'))
+
+class Plugins(BaseTestCase):
+ def __init__(self, *args, **kwargs):
+ super().__init__(__file__, 'plugins', *args, **kwargs)
+
+ def test(self):
+ self.run_python({
+ # Test all of them to check the registration works well
+ 'PLUGINS': [
+ 'm.abbr',
+ 'm.code',
+ 'm.components',
+ 'm.dot',
+ 'm.dox',
+ 'm.gh',
+ 'm.gl',
+ 'm.link',
+ 'm.plots',
+ 'm.vk',
+ 'fancyline'
+ ],
+ 'PLUGIN_PATHS': ['plugins'],
+ 'INPUT_PAGES': ['index.rst', 'dot.rst'],
+ 'M_HTMLSANITY_SMART_QUOTES': True,
+ 'M_DOT_FONT': 'DejaVu Sans',
+ 'M_PLOTS_FONT': 'DejaVu Sans',
+ 'M_DOX_TAGFILES': [
+ (os.path.join(self.path, '../../../doc/documentation/corrade.tag'), 'https://doc.magnum.graphics/corrade/')
+ ]
+ })
+ self.assertEqual(*self.actual_expected_contents('index.html'))
+ # The output is different for older Graphviz
+ self.assertEqual(*self.actual_expected_contents('dot.html', 'dot.html' if LooseVersion(dot_version()) >= LooseVersion("2.40.1") else 'dot-238.html'))
return [nodes.abbreviation(title, title, **options)], []
return [nodes.abbreviation(abbr, abbr, title=title, **options)], []
-def register():
+def register_mcss(**kwargs):
rst.roles.register_local_role('abbr', abbr)
+
+register = register_mcss # for Pelican
with open(alias_file, 'w') as f:
f.write("""<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url={}" /></head></html>\n""".format(alias_target))
-def get_generators(generators): return AliasGenerator
+def register_mcss(**kwargs):
+ assert not "This plugin is Pelican-only" # pragma: no cover
-def register():
- # TODO: why `lambda generators: AliasGenerator` doesn't work?
- signals.get_generators.connect(get_generators)
+def _pelican_get_generators(generators): return AliasGenerator
+
+def register(): # for Pelican
+ signals.get_generators.connect(_pelican_get_generators)
code.options = {'class': directives.class_option,
'language': directives.unchanged}
-def register():
+def register_mcss(**kwargs):
rst.directives.register_directive('code', Code)
rst.directives.register_directive('include', Include)
rst.roles.register_canonical_role('code', code)
+
+register = register_mcss # for Pelican
def label_flat_dim(name, rawtext, text, lineno, inliner, options={}, content=[]):
return label(['m-flat', 'm-dim'], name, rawtext, text, lineno, inliner, options, content)
-def register():
+def register_mcss(**kwargs):
rst.directives.register_directive('transition', Transition)
rst.directives.register_directive('note-default', DefaultNote)
rst.roles.register_canonical_role('label-flat-danger', label_flat_danger)
rst.roles.register_canonical_role('label-flat-info', label_flat_info)
rst.roles.register_canonical_role('label-flat-dim', label_flat_dim)
+
+register = register_mcss # for Pelican
self.arguments[0] if self.arguments else '',
'\n'.join(self.content)))
-def configure(pelicanobj):
+def register_mcss(mcss_settings, **kwargs):
dot2svg.configure(
- pelicanobj.settings.get('M_DOT_FONT', 'Source Sans Pro'),
- pelicanobj.settings.get('M_DOT_FONT_SIZE', 16.0))
-
-def register():
- pelican.signals.initialized.connect(configure)
+ mcss_settings.get('M_DOT_FONT', 'Source Sans Pro'),
+ mcss_settings.get('M_DOT_FONT_SIZE', 16.0))
rst.directives.register_directive('digraph', Digraph)
rst.directives.register_directive('strict-digraph', StrictDigraph)
rst.directives.register_directive('graph', Graph)
rst.directives.register_directive('strict-graph', StrictGraph)
+
+def _pelican_configure(pelicanobj):
+ register_mcss(mcss_settings=pelicanobj.settings)
+
+def register(): # for Pelican
+ pelican.signals.initialized.connect(_pelican_configure)
return title, link, hash
-def init(pelicanobj):
- global symbol_mapping, symbol_prefixes, tagfile_basenames
+def init(tagfiles, input):
+ rst.roles.register_local_role('dox', dox)
- tagfiles = pelicanobj.settings.get('M_DOX_TAGFILES', [])
+ global symbol_mapping, symbol_prefixes, tagfile_basenames
# Pre-round to populate subclasses. Clear everything in case we init'd
# before already.
tagfile_basenames += [(os.path.splitext(os.path.basename(tagfile))[0], path, css_classes)]
symbol_prefixes += prefixes
- tree = ET.parse(tagfile)
+ tree = ET.parse(os.path.join(input, tagfile))
root = tree.getroot()
for child in root:
if child.tag == 'compound' and 'kind' in child.attrib:
node = nodes.literal(rawtext, target, **_options)
return [node], []
-def register():
- signals.initialized.connect(init)
+def register_mcss(mcss_settings, **kwargs):
+ init(input=mcss_settings['INPUT'],
+ tagfiles=mcss_settings.get('M_DOX_TAGFILES', []))
- rst.roles.register_local_role('dox', dox)
+def _pelican_configure(pelicanobj):
+ settings = {
+ # For backwards compatibility, the input directory is pelican's CWD
+ 'INPUT': os.getcwd(),
+ }
+ for key in ['M_DOX_TAGFILES']:
+ if key in pelicanobj.settings: settings[key] = pelicanobj.settings[key]
+
+ register_mcss(mcss_settings=settings)
+
+def register(): # for Pelican
+ signals.initialized.connect(_pelican_configure)
settings = {}
-def init(pelicanobj):
- settings['path'] = pelicanobj.settings.get('PATH', 'content')
-
def filesize(name, rawtext, text, lineno, inliner, options={}, content=[]):
# Support both {filename} (3.7.1) and {static} (3.8) placeholders
- file = os.path.join(os.getcwd(), settings['path'])
- size = os.path.getsize(text.format(filename=file, static=file))
+ size = os.path.getsize(text.format(filename=settings['INPUT'], static=settings['INPUT']))
for unit in ['','k','M','G','T']:
if abs(size) < 1024.0:
def filesize_gz(name, rawtext, text, lineno, inliner, options={}, content=[]):
# Support both {filename} (3.7.1) and {static} (3.8) placeholders
- file = os.path.join(os.getcwd(), settings['path'])
- with open(text.format(filename=file, static=file), mode='rb') as f:
+ with open(text.format(filename=settings['INPUT'], static=settings['INPUT']), mode='rb') as f:
size = len(gzip.compress(f.read()))
for unit in ['','k','M','G','T']:
set_classes(options)
return [nodes.inline(size_string, size_string, **options)], []
-def register():
- signals.initialized.connect(init)
-
+def register_mcss(mcss_settings, **kwargs):
+ global settings
+ settings['INPUT'] = mcss_settings['INPUT']
rst.roles.register_local_role('filesize', filesize)
rst.roles.register_local_role('filesize-gz', filesize_gz)
+
+def _pelican_configure(pelicanobj):
+ settings = {
+ 'INPUT': os.path.join(os.getcwd(), pelicanobj.settings['PATH'])
+ }
+ register_mcss(mcss_settings=settings)
+
+def register(): # for Pelican
+ signals.initialized.connect(_pelican_configure)
node = nodes.reference(rawtext, title, refuri=url, **options)
return [node], []
-def register():
+def register_mcss(**kwargs):
rst.roles.register_local_role('gh', gh)
+
+register = register_mcss # for Pelican
node = nodes.reference(rawtext, "gl" + title + prefix + "()", refuri=url, **options)
return [node], []
-def register():
+def register_mcss(**kwargs):
rst.roles.register_local_role('glext', glext)
rst.roles.register_local_role('webglext', webglext)
rst.roles.register_local_role('glfn', glfn)
rst.roles.register_local_role('glfnext', glfnext)
+
+register = register_mcss # for Pelican
# DEALINGS IN THE SOFTWARE.
#
+import copy
import os
from docutils.parsers import rst
from docutils.parsers.rst import Directive
except ImportError:
PIL = None
-settings = {}
+default_settings = {
+ 'INPUT': None,
+ 'M_IMAGES_REQUIRE_ALT_TEXT': False
+}
+
+settings = None
class Image(Directive):
"""Image directive
width = None
height = None
# If scaling requested, open the files and calculate the scaled size
- # Support both {filename} (3.7.1) and {static} (3.8) placeholders. In
- # all cases use only width and not both so the max-width can correctly
+ # Support both {filename} (3.7.1) and {static} (3.8) placeholders,
+ # also prepend the absolute path in case we're not Pelican. In all
+ # cases use only width and not both so the max-width can correctly
# scale the image down on smaller screen sizes.
# TODO: implement ratio-preserving scaling to avoid jumps on load using
# the margin-bottom hack
if 'scale' in self.options:
- file = os.path.join(os.getcwd(), settings['PATH'])
- absuri = reference.format(filename=file, static=file)
+ file = os.path.join(os.getcwd(), settings['INPUT'])
+ absuri = os.path.join(file, reference.format(filename=file, static=file))
im = PIL.Image.open(absuri)
width = "{}px".format(int(im.width*self.options['scale']/100.0))
elif 'width' in self.options:
uri, _, caption = uri_caption.partition(' ')
# Open the files and calculate the overall width
- # Support both {filename} (3.7.1) and {static} (3.8) placeholders
- file = os.path.join(os.getcwd(), settings['PATH'])
- absuri = uri.format(filename=file, static=file)
+ # Support both {filename} (3.7.1) and {static} (3.8) placeholders,
+ # also prepend the absolute path in case we're not Pelican
+ file = os.path.join(os.getcwd(), settings['INPUT'])
+ absuri = os.path.join(file, uri.format(filename=file, static=file))
im = PIL.Image.open(absuri)
# If no caption provided, get EXIF info, if it's there
return [grid_node]
-def configure(pelicanobj):
- settings['PATH'] = pelicanobj.settings.get('PATH', 'content')
- settings['M_IMAGES_REQUIRE_ALT_TEXT'] = pelicanobj.settings.get('M_IMAGES_REQUIRE_ALT_TEXT', False)
-
-def register():
- signals.initialized.connect(configure)
+def register_mcss(mcss_settings, **kwargs):
+ global default_settings, settings
+ settings = copy.deepcopy(default_settings)
+ for key in settings.keys():
+ if key in mcss_settings: settings[key] = mcss_settings[key]
rst.directives.register_directive('image', Image)
rst.directives.register_directive('figure', Figure)
rst.directives.register_directive('image-grid', ImageGrid)
+
+def _pelican_configure(pelicanobj):
+ settings = {
+ 'INPUT': pelicanobj.settings['PATH'],
+ }
+ for key in 'M_IMAGES_REQUIRE_ALT_TEXT':
+ if key in pelicanobj.settings: settings[key] = pelicanobj.settings[key]
+
+ register_mcss(mcss_settings=settings)
+
+def register(): # for Pelican
+ signals.initialized.connect(_pelican_configure)
node = nodes.reference(rawtext, title, refuri=url, **options)
return [node], []
-def register():
+def register_mcss(**kwargs):
rst.roles.register_local_role('link', link)
+
+register = register_mcss # for Pelican
# DEALINGS IN THE SOFTWARE.
#
+import copy
import html
import os
import re
import latex2svg
import latex2svgextra
-render_as_code = False
+default_settings = {
+ 'INPUT': '',
+ 'M_MATH_RENDER_AS_CODE': False,
+ 'M_MATH_CACHE_FILE': 'm.math.cache'
+}
+
+settings = None
def _is_math_figure(parent):
# The parent has to be a figure, marked as m-figure
parent = self.state.parent
# Fallback rendering as code requested
- if render_as_code:
+ if settings['M_MATH_RENDER_AS_CODE']:
# If this is a math figure, replace the figure CSS class to have a
# matching border
if _is_math_figure(parent):
text = rawtext.split('`')[1]
# Fallback rendering as code requested
- if render_as_code:
+ if settings['M_MATH_RENDER_AS_CODE']:
set_classes(options)
classes = []
if 'classes' in options:
node = nodes.raw(rawtext, latex2svgextra.patch(text, svg, depth, attribs), format='html', **options)
return [node], []
-def configure_pelican(pelicanobj):
- global render_as_code
- render_as_code = pelicanobj.settings.get('M_MATH_RENDER_AS_CODE', False)
- cache_file = pelicanobj.settings.get('M_MATH_CACHE_FILE', 'm.math.cache')
- if cache_file and os.path.exists(cache_file):
- latex2svgextra.unpickle_cache(cache_file)
- else:
- latex2svgextra.unpickle_cache(None)
-
def save_cache(pelicanobj):
- cache_file = pelicanobj.settings.get('M_MATH_CACHE_FILE', 'm.math.cache')
- if cache_file: latex2svgextra.pickle_cache(cache_file)
+ if settings['M_MATH_CACHE_FILE']:
+ latex2svgextra.pickle_cache(settings['M_MATH_CACHE_FILE'])
+
+def register_mcss(mcss_settings, **kwargs):
+ global default_settings, settings
+ settings = copy.deepcopy(default_settings)
+ for key in settings.keys():
+ if key in mcss_settings: settings[key] = mcss_settings[key]
+
+ if settings['M_MATH_CACHE_FILE']:
+ settings['M_MATH_CACHE_FILE'] = os.path.join(settings['INPUT'], settings['M_MATH_CACHE_FILE'])
+
+ if os.path.exists(settings['M_MATH_CACHE_FILE']):
+ latex2svgextra.unpickle_cache(settings['M_MATH_CACHE_FILE'])
+ else:
+ latex2svgextra.unpickle_cache(None)
+
+ rst.directives.register_directive('math', Math)
+ rst.roles.register_canonical_role('math', math)
+
+def _configure_pelican(pelicanobj):
+ register_mcss(mcss_settings=pelicanobj.settings)
def register():
- pelican.signals.initialized.connect(configure_pelican)
+ pelican.signals.initialized.connect(_configure_pelican)
pelican.signals.finalized.connect(save_cache)
pelican.signals.content_object_init.connect(new_page)
- rst.directives.register_directive('math', Math)
- rst.roles.register_canonical_role('math', math)
if hasattr(page, 'title'):
article.category.badge_title = page.title
-def register():
+def register_mcss(**kwargs):
+ assert not "This plugin is Pelican-only" # pragma: no cover
+
+def register(): # for Pelican
signals.article_generator_finalized.connect(populate_metadata)
def new_page(content):
mpl.rcParams['svg.hashsalt'] = 0
-def configure(pelicanobj):
- font = pelicanobj.settings.get('M_PLOTS_FONT', 'Source Sans Pro')
+def register_mcss(mcss_settings, **kwargs):
+ font = mcss_settings.get('M_PLOTS_FONT', 'Source Sans Pro')
for i in range(len(_class_mapping)):
src, dst = _class_mapping[i]
_class_mapping[i] = (src.format(font=font), dst)
mpl.rcParams['font.family'] = font
-def register():
- pelican.signals.initialized.connect(configure)
- pelican.signals.content_object_init.connect(new_page)
rst.directives.register_directive('plot', Plot)
+
+def _pelican_configure(pelicanobj):
+ register_mcss(mcss_settings=pelicanobj.settings)
+
+def register(): # for Pelican
+ pelican.signals.initialized.connect(_pelican_configure)
+ pelican.signals.content_object_init.connect(new_page)
node = nodes.reference(rawtext, title, refuri=url, **options)
return [node], []
-def register():
+def register_mcss(**kwargs):
rst.roles.register_local_role('vkext', vkext)
rst.roles.register_local_role('vkfn', vkfn)
rst.roles.register_local_role('vktype', vktype)
+
+register = register_mcss # for Pelican