././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4534357
mkdocs_macros_plugin-1.4.0/ 0000755 0000765 0000024 00000000000 15064255234 015321 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1708423407.0
mkdocs_macros_plugin-1.4.0/LICENSE.md 0000644 0000765 0000024 00000002156 14565074357 016743 0 ustar 00laurent staff # MIT License
Copyright (C) 2018-2024 Laurent Franceschetti
(see contributors for their respective portions)
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.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1722150399.0
mkdocs_macros_plugin-1.4.0/MANIFEST.in 0000644 0000765 0000024 00000000137 14651366777 017100 0 ustar 00laurent staff include README.md
include LICENSE.md
include mkdocs_macros/*.md
include mkdocs_macros/py.typed
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4532437
mkdocs_macros_plugin-1.4.0/PKG-INFO 0000644 0000765 0000024 00000020041 15064255234 016413 0 ustar 00laurent staff Metadata-Version: 2.4
Name: mkdocs-macros-plugin
Version: 1.4.0
Summary: Unleash the power of MkDocs with macros and variables
Author: Laurent Franceschetti
License: MIT
Project-URL: Homepage, https://github.com/fralau/mkdocs_macros_plugin
Keywords: macros,markdown,mkdocs,python
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.5
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: hjson
Requires-Dist: jinja2
Requires-Dist: mkdocs>=0.17
Requires-Dist: packaging
Requires-Dist: pathspec
Requires-Dist: python-dateutil
Requires-Dist: pyyaml
Requires-Dist: super-collections>=0.5.7
Requires-Dist: termcolor
Provides-Extra: test
Requires-Dist: mkdocs-include-markdown-plugin; extra == "test"
Requires-Dist: mkdocs-macros-test; extra == "test"
Requires-Dist: mkdocs-material>=6.2; extra == "test"
Requires-Dist: mkdocs-test; extra == "test"
Requires-Dist: mkdocs-d2-plugin; extra == "test"
Provides-Extra: doc
Requires-Dist: mkdocs-mermaid2-plugin; extra == "doc"
Dynamic: license-file

# Unleash the power of MkDocs with variables and macros
[](https://opensource.org/licenses/MIT)




:open_file_folder: [Used by > 2K repositories on Github](https://github.com/fralau/mkdocs_macros_plugin/network/dependents)
🥇 Listed as [High-Quality Plugin](https://github.com/mkdocs/catalog#-code-execution-variables--templating)
**mkdocs-macros-plugin** is a general-purpose plugin for [MkDocs](https://www.mkdocs.org/)
that uses **variables** and **macros** (functions) to automate tasks, and produce richer and more beautiful pages.
```markdown
The unit price of product A is {{ unit_price }} EUR.
Taking the standard discount into account,
the sale price of 50 units is {{ price(unit_price, 50) }} EUR.
```
View the [mkdocs-macro documentation](https://mkdocs-macros-plugin.readthedocs.io/) on Read the Docs.
## Overview
**mkdocs-macros-plugin** is a plugin that makes it easier for contributors
of an [MkDocs](https://www.mkdocs.org/) website to produce richer and more beautiful pages. It transforms the markdown pages
into [jinja2](https://jinja.palletsprojects.com/en/2.10.x/) templates
that use **variables**, calls to **macros** and custom **filters**.
> **You can also partially replace MkDocs plugins with mkdocs-macros modules,
> and [pluglets](https://mkdocs-macros-plugin.readthedocs.io/en/latest/pluglets/)
> (pre-installed modules).**
### Using variables
You can leverage the power of Python in markdown thanks to jinja2
by writing this :
```markdown
The unit price of product A is {{ unit_price }} EUR.
Taking the standard discount into account,
the sale price of 50 units is {{ price(unit_price, 50) }} EUR.
```
If you defined a `price()` function, this could translate into:
```
The unit price of product A is 10.00 EUR.
Taking the standard discount into account,
the sale price of 50 units is 450.00 EUR.
```
> The result of a macro can be **HTML code**:
this makes macros especially useful
to make custom extensions to the syntax of markdown, such as buttons,
calls to email, embedding YouTube videos, etc.
It is possible to use the wide range of facilities provided by
[Jinja2 templates](http://jinja.pocoo.org/docs/2.10/templates/) such
as conditions (`{% if ... %}`) and loops (`{% for ... %}`).
### Defining variables
Regular **variables** can be defined in five ways:
| No | Validity | For whom | Description |
| --- | --- | --- | ---- |
| 1. | global | designer of the website | in the `mkdocs.yml` file, under the `extra` heading |
| 2. | global | contributor | in external yaml definition files |
| 3. | global | programmer | in a `main.py` file (Python), by adding them to a dictionary |
| 4. | local (page) | writer | in the YAML header of each Markdown page |
| 5. | local (page) | writer | with a `{%set variable = value %}` statement |
In addition, predefined objects are provided (local and global), typically
for the environment, project, page, git information, etc.
### Macros and filters
Similarly programmers can define their own **macros** and **filters**,
as Python functions in the `main.py` file,
which the users will then be able to
use without much difficulty, as jinja2 directives in the markdown page.
## Installation
### Prerequisites
- Python version > 3.7
- MkDocs version >= 1.0
(compatible with post 1.5 versions)
### Standard installation
```
pip install mkdocs-macros-plugin
```
### "Manual installation"
To install the package, download it and run:
```
pip install .
# or...
python setup.py install
```
### Development/test installation
To install the extra dependencies required for testing the package, run:
```
pip install "mkdocs-macros-plugin[test]"
```
### Declaration of plugin
Declare the plugin in the file `mkdocs.yml`:
```yaml
plugins:
- search
- macros
```
> **Note:** If you have no `plugins` entry in your config file yet,
you should also add the `search` plugin.
If no `plugins` entry is set, MkDocs enables `search` by default; but
if you use it, then you have to declare it explicitly.
By default, undefined variables are printed to the page as-is. If you
wish for a page to fail on undefined variables, you should use the
below configuration instead:
```yaml
plugins:
- search
- macros
on_undefined: strict
```
For details and more options, see the [documentation](
https://mkdocs-macros-plugin.readthedocs.io/en/latest/troubleshooting/#what-happens-if-a-variable-is-undefined).
### Check that it works
The recommended way to check that the plugin works properly is to add the
following command in one of the pages of your site (let's say `info.md`):
```
{{ macros_info() }}
```
In the terminal, restart the environment:
```
> mkdocs serve
````
You will notice that additional information now appears in the terminal:
```
INFO - Building documentation...
[macros] Macros arguments: {'module_name': 'main', 'include_yaml': [], 'j2_block_start_string': '', 'j2_block_end_string': '', 'j2_variable_start_string': '', 'j2_variable_end_string': ''}
```
Within the browser (e.g. http://127.0.0.1:8000/info), you should
see a description of the plugin's environment:

If you see it that information, you should be all set.
Give a good look at the General List, since it gives you an overview
of what you can do out of the box with the macros plugin.
The other parts give you more detailed information.
## Using pluglets
### What are pluglets?
**Pluglets** are small, easy-to-write programs
that use mkdocs-macro's foundation
to offer services to mkdocs projects, which would normally
be offered by plugins.
Pluglets are Python packages, which can be hosted on github, and
distributed through [PyPI](https://pypi.org/).
### How to add a pluglet to an mkdocs project?
Install it:
```shell
pip install
```
Declare it in the project's config (`mkdocs.yml`) file:
```yaml
plugins:
- search
- macros:
modules:
-
```
### How to write a pluglet?
[See instructions in the documentation](https://mkdocs-macros-plugin.readthedocs.io/en/latest/pluglets/).
A sample pluglet can be found in [mkdocs-test (github)](https://github.com/fralau/mkdocs-macros-test).
### List of existing pluglets
[See the wiki page on Github](https://github.com/fralau/mkdocs-macros-plugin/wiki/Mkdocs%E2%80%90Macros-Pluglets).
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1728067427.0
mkdocs_macros_plugin-1.4.0/README.md 0000644 0000765 0000024 00000015345 14700033543 016601 0 ustar 00laurent staff

# Unleash the power of MkDocs with variables and macros
[](https://opensource.org/licenses/MIT)




:open_file_folder: [Used by > 2K repositories on Github](https://github.com/fralau/mkdocs_macros_plugin/network/dependents)
🥇 Listed as [High-Quality Plugin](https://github.com/mkdocs/catalog#-code-execution-variables--templating)
**mkdocs-macros-plugin** is a general-purpose plugin for [MkDocs](https://www.mkdocs.org/)
that uses **variables** and **macros** (functions) to automate tasks, and produce richer and more beautiful pages.
```markdown
The unit price of product A is {{ unit_price }} EUR.
Taking the standard discount into account,
the sale price of 50 units is {{ price(unit_price, 50) }} EUR.
```
View the [mkdocs-macro documentation](https://mkdocs-macros-plugin.readthedocs.io/) on Read the Docs.
## Overview
**mkdocs-macros-plugin** is a plugin that makes it easier for contributors
of an [MkDocs](https://www.mkdocs.org/) website to produce richer and more beautiful pages. It transforms the markdown pages
into [jinja2](https://jinja.palletsprojects.com/en/2.10.x/) templates
that use **variables**, calls to **macros** and custom **filters**.
> **You can also partially replace MkDocs plugins with mkdocs-macros modules,
> and [pluglets](https://mkdocs-macros-plugin.readthedocs.io/en/latest/pluglets/)
> (pre-installed modules).**
### Using variables
You can leverage the power of Python in markdown thanks to jinja2
by writing this :
```markdown
The unit price of product A is {{ unit_price }} EUR.
Taking the standard discount into account,
the sale price of 50 units is {{ price(unit_price, 50) }} EUR.
```
If you defined a `price()` function, this could translate into:
```
The unit price of product A is 10.00 EUR.
Taking the standard discount into account,
the sale price of 50 units is 450.00 EUR.
```
> The result of a macro can be **HTML code**:
this makes macros especially useful
to make custom extensions to the syntax of markdown, such as buttons,
calls to email, embedding YouTube videos, etc.
It is possible to use the wide range of facilities provided by
[Jinja2 templates](http://jinja.pocoo.org/docs/2.10/templates/) such
as conditions (`{% if ... %}`) and loops (`{% for ... %}`).
### Defining variables
Regular **variables** can be defined in five ways:
| No | Validity | For whom | Description |
| --- | --- | --- | ---- |
| 1. | global | designer of the website | in the `mkdocs.yml` file, under the `extra` heading |
| 2. | global | contributor | in external yaml definition files |
| 3. | global | programmer | in a `main.py` file (Python), by adding them to a dictionary |
| 4. | local (page) | writer | in the YAML header of each Markdown page |
| 5. | local (page) | writer | with a `{%set variable = value %}` statement |
In addition, predefined objects are provided (local and global), typically
for the environment, project, page, git information, etc.
### Macros and filters
Similarly programmers can define their own **macros** and **filters**,
as Python functions in the `main.py` file,
which the users will then be able to
use without much difficulty, as jinja2 directives in the markdown page.
## Installation
### Prerequisites
- Python version > 3.7
- MkDocs version >= 1.0
(compatible with post 1.5 versions)
### Standard installation
```
pip install mkdocs-macros-plugin
```
### "Manual installation"
To install the package, download it and run:
```
pip install .
# or...
python setup.py install
```
### Development/test installation
To install the extra dependencies required for testing the package, run:
```
pip install "mkdocs-macros-plugin[test]"
```
### Declaration of plugin
Declare the plugin in the file `mkdocs.yml`:
```yaml
plugins:
- search
- macros
```
> **Note:** If you have no `plugins` entry in your config file yet,
you should also add the `search` plugin.
If no `plugins` entry is set, MkDocs enables `search` by default; but
if you use it, then you have to declare it explicitly.
By default, undefined variables are printed to the page as-is. If you
wish for a page to fail on undefined variables, you should use the
below configuration instead:
```yaml
plugins:
- search
- macros
on_undefined: strict
```
For details and more options, see the [documentation](
https://mkdocs-macros-plugin.readthedocs.io/en/latest/troubleshooting/#what-happens-if-a-variable-is-undefined).
### Check that it works
The recommended way to check that the plugin works properly is to add the
following command in one of the pages of your site (let's say `info.md`):
```
{{ macros_info() }}
```
In the terminal, restart the environment:
```
> mkdocs serve
````
You will notice that additional information now appears in the terminal:
```
INFO - Building documentation...
[macros] Macros arguments: {'module_name': 'main', 'include_yaml': [], 'j2_block_start_string': '', 'j2_block_end_string': '', 'j2_variable_start_string': '', 'j2_variable_end_string': ''}
```
Within the browser (e.g. http://127.0.0.1:8000/info), you should
see a description of the plugin's environment:

If you see it that information, you should be all set.
Give a good look at the General List, since it gives you an overview
of what you can do out of the box with the macros plugin.
The other parts give you more detailed information.
## Using pluglets
### What are pluglets?
**Pluglets** are small, easy-to-write programs
that use mkdocs-macro's foundation
to offer services to mkdocs projects, which would normally
be offered by plugins.
Pluglets are Python packages, which can be hosted on github, and
distributed through [PyPI](https://pypi.org/).
### How to add a pluglet to an mkdocs project?
Install it:
```shell
pip install
```
Declare it in the project's config (`mkdocs.yml`) file:
```yaml
plugins:
- search
- macros:
modules:
-
```
### How to write a pluglet?
[See instructions in the documentation](https://mkdocs-macros-plugin.readthedocs.io/en/latest/pluglets/).
A sample pluglet can be found in [mkdocs-test (github)](https://github.com/fralau/mkdocs-macros-test).
### List of existing pluglets
[See the wiki page on Github](https://github.com/fralau/mkdocs-macros-plugin/wiki/Mkdocs%E2%80%90Macros-Pluglets).
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4402223
mkdocs_macros_plugin-1.4.0/mkdocs_macros/ 0000755 0000765 0000024 00000000000 15064255234 020145 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1728118538.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/__init__.py 0000644 0000765 0000024 00000000367 14700177412 022261 0 ustar 00laurent staff # -------------------
# These can be imported in macro code
# -------------------
# from .plugin import MacrosPlugin
# for fixing URLS in macros
from .context import fix_url, is_relative as is_relative_url
# from .util import SuperDict, SuperList ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1727106727.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/context.py 0000644 0000765 0000024 00000031030 14674307247 022210 0 ustar 00laurent staff """
Basic context for the jinja2 templates.
"Batteries included": It defines standard variables, macros and filters
that a template designer is likely to need.
It contains in particular documentation functions.
Laurent Franceschetti (c) 2020
"""
from urllib.parse import urlparse
import os
import sys
import subprocess
import platform
import traceback
from importlib.metadata import version as package_version
import datetime
from dateutil.parser import parse as date_parse
from functools import partial
import mkdocs
from mkdocs.structure.nav import get_navigation
from mkdocs.structure.files import File
from mkdocs.utils import normalize_url
import jinja2
from jinja2 import Template
from markdown import markdown
# ---------------------------------
# Initialization
# ---------------------------------
# Local directory
SOURCE_DIR = os.path.dirname(os.path.abspath(__file__))
# Name of the package (for version)
PACKAGE_NAME = 'mkdocs-macros-plugin'
# ---------------------------------
# Documentation utilities
# ---------------------------------
def list_items(obj):
"""
Returns a list of key,value pairs for the content of an object
Creates an abstraction layer so that we do not have to worry.
"""
try:
return obj.items()
except AttributeError:
# it's an object
return obj.__dict__.items()
except TypeError:
# it's a list: enumerate
return enumerate(list(obj))
def get_first_para(s) -> str:
"Get the first para of a docstring"
first_lines = []
for row in s.strip().splitlines():
if not row:
break
else:
first_lines.append(row)
r = ' '.join(first_lines).strip()
# fix last character that ends with a semi-colon
if r.endswith(':'):
r = r[:-1] + '.'
return r
def format_value(value):
"Properly format the value, to make it descriptive"
# those classes will be processed as "dictionary type"
# NOTE: using the name does nto force us to import them
LISTED_CLASSES = 'Config', 'File', 'Section'
# those types will be printed without question
SHORT_TYPES = int, float, str, list
if callable(value):
# for functions
docstring = get_first_para(value.__doc__)
# we interpret the markdown in the docstring,
# since both jinja2 and ourselves use markdown,
# and we need to produce a HTML table:
docstring = markdown(docstring)
try:
varnames = ', '.join(value.__code__.co_varnames)
return "(%s)
%s" % (varnames, docstring)
except AttributeError:
# code not available
return docstring
elif (isinstance(value, dict) or
type(value).__name__ in LISTED_CLASSES):
# print("Processing:", type(value).__name__, isinstance(value, SHORT_TYPES))
r_list = []
for key, value in list_items(value):
if isinstance(value, SHORT_TYPES):
r_list.append("%s = %s" % (key, repr(value)))
else:
# object or dict: write minimal info:
r_list.append("%s [%s]" %
(key, type(value).__name__))
return ', '.join(r_list)
else:
return repr(value)
def make_html(rows, header=[], tb_class='macros-tb'):
"Produce an HTML table"
font_color = "#000000" # black
back_color = "#F0FFFF" # light blue
grid_color = "#DCDCDC"
padding = "5px"
style = f"color:{font_color}; border:1px solid {grid_color}; padding: {padding}"
templ = Template("""
{% for item in header %}
| {{ item }} |
{% endfor %}
{% for row in rows %}
{% for item in row %}
| {{ item }} |
{% endfor %}
{% endfor %}
""")
return templ.render(locals())
def get_git_info():
"""
Get the abbreviated commit version (not provided by get_git_info())
Returns a dictionary
"""
LAST_COMMIT = ['git', 'log', '-1']
COMMANDS = {
'short_commit': ['git', 'rev-parse', '--short', 'HEAD'],
'commit': ['git', 'rev-parse', 'HEAD'],
'tag': ['git', 'describe', '--tags'],
# With --abbrev set to 0, git will find the closest tagname without any suffix
'short_tag': ['git', 'describe', '--tags', '--abbrev=0'],
'author': LAST_COMMIT + ["--pretty=format:%an"],
'author_email': LAST_COMMIT + ["--pretty=format:%ae"],
'committer': LAST_COMMIT + ["--pretty=format:%cn"],
'committer_email': LAST_COMMIT + ["--pretty=format:%ce"],
# %cd is the commit date
'date_ISO': LAST_COMMIT + ['--pretty=format:%cd'],
'message': LAST_COMMIT + ["--pretty=format:%B"],
'raw': LAST_COMMIT,
'root_dir': ['git', 'rev-parse', '--show-toplevel']
}
# always return a date, even in case of failure
r = {'status': False, 'date': None}
try:
for var, command in COMMANDS.items():
# NOTE: The 'text' argument is clearer,
# but for Python < 3.7, only `universal_newlines`
# is accepted
try:
r[var] = subprocess.check_output(command,
universal_newlines=True,
stderr=subprocess.DEVNULL).strip()
if var == 'date_ISO':
r['date'] = date_parse(r[var])
r['status'] = True
except subprocess.CalledProcessError as e:
if e.returncode == 128:
# generally means "unexpected error"
# git status (no repo),
# git tag (no tag)
r[var] = ''
else:
# should be 1, type whatever that is
r[var] = "# Cannot execute '%s': %s" % (command, e)
except Exception as e:
# any other error, it's probably meaningless at this point
r[var] = "# Unexpected error '%s': %s" % (command, e)
# convert
return r
except FileNotFoundError as e:
# not git command
return r.update(
{'status': False,
'diagnosis': 'Git command not found',
'error': str(e)})
def python_version():
"Get the python version"
try:
return sys.version.split('(')[0].rstrip()
except (AttributeError, IndexError) as e:
return str(e)
def system_name():
"Get the system name"
r = platform.system()
if not r:
# you never know
return ""
# print("Found:", r)
CONV = {'Win': 'Windows', 'Darwin': 'MacOs'}
return CONV.get(r, r)
def system_version():
"Get the system version"
try:
return platform.mac_ver()[0] or platform.release()
except (AttributeError, IndexError) as e:
return str(e)
# for the navigation
class Files(object):
"This helper class is needed to rebuild the navigation"
def __init__(self, config):
self.config = config
self._filenames = []
@property
def filenames(self):
"The list of filenames (not used at the moment"
return self._filenames
def get_file_from_path(self, path):
"Build the filenames"
self._filenames.append(path)
file = File(os.path.basename(path),
os.path.dirname(path),
os.path.dirname(path), True)
return file
def documentation_pages(self):
return []
# ---------------------------------
# Urls
# ---------------------------------
def is_relative(url):
"""
Check whether a url is relative
>>> urlparse("http://www.google.com")
ParseResult(scheme='http', netloc='www.google.com', path='', params='', query='', fragment='')
>>> urlparse("../foo")
ParseResult(scheme='', netloc='', path='../foo', params='', query='', fragment='')
"""
p = urlparse(url)
return (not p.scheme) and p.path
def fix_url(url):
"""
If url is relative, fix it so that it points to the docs directory.
This is necessary because relative links in markdown must be adapted
in html ('img/foo.png' => '../img/img.png').
"""
if is_relative(url):
r = "../" + url
else:
r = url
return r
# ---------------------------------
# Exports to the environment
# ---------------------------------
def define_env(env):
"""
This is the hook for declaring variables, macros and filters
"""
# Get data on the environment (versions)
try:
environment = {
'system': system_name(),
'system_version': system_version(),
'python_version': python_version(),
'mkdocs_version': mkdocs.__version__,
'macros_plugin_version': package_version(PACKAGE_NAME),
'jinja2_version': jinja2.__version__,
# 'site_git_version': site_git_version(),
}
except Exception as e:
# Avoid breaking the system if error in reading the system info:
environment = ("Cannot read system info! %s: %s" %
(type(e).__name__, str(e)))
env.variables['environment'] = environment
# configuration of the plugin, in the yaml file:
env.variables['plugin'] = env.config
# git information:
env.variables['git'] = get_git_info()
def render_file(filename):
"""
Render an external page (filename) containing jinja2 code
Do not declare as macro, as this is pointless.
"""
SOURCE_FILE = os.path.join(SOURCE_DIR, filename)
with open(SOURCE_FILE) as f:
s = f.read()
# now we need to render the jinja2 directives,
# always rendering (to skip reasoning about page header)
return env.render(s, force_rendering=True)
@env.macro
def context(obj:dict=None):
"""
*Default Mkdocs-Macro*: List an object
(by default the variables)
"""
if not obj:
obj = env.variables
try:
return [(var, type(value).__name__, format_value(value))
for var, value in list_items(obj)]
except jinja2.exceptions.UndefinedError as e:
return [("Error!", type(e).__name__, str(e))]
except AttributeError:
# Not an object or dictionary (int, str, etc.)
return [(obj, type(obj).__name__, repr(obj))]
@env.filter
def pretty(var_list):
"""
*Default Mkdocs-Macro*: Prettify a dictionary or object
(used for environment documentation, or debugging).
Note: it will work only on the product of the `context()` macro
To prettify any object `obj`, thus use: `context(obj) | pretty`
"""
if not var_list:
return ''
else:
try:
rows = [("%s" % var, "%s" % var_type,
content.replace('\n', '
'))
for var, var_type, content in var_list]
header = ['Variable', 'Type', 'Content']
return make_html(rows, header)
except Exception as e:
# dont make the whole page fail:
return "#%s: %s\n%s" % (type(e).__name__, e,
traceback.format_exc())
@env.macro
def macros_info():
"""
*Test/debug function*:
list useful documentation on the mkdocs_macro environment.
"""
# NOTE: this is template
return render_file('macros_info.md')
@env.macro
def now():
"""
*Default Mkdocs-Macro*:
Get the current time (at the moment of the project build).
It returns a datetime object.
Used alone, it provides a timestamp.
To get the year use `now().year`, for the month number
`now().month`, etc.
"""
return datetime.datetime.now()
# add fix url function as macro
env.macro(fix_url)
# add the normal mkdocs url function
# env.filter(normalize_url)
@env.filter
def relative_url(path: str):
"""
*Default Mkdocs-Macro*:
convert the path of any page according to MkDoc's internal logic,
into a URL relative to the current page
(implements the `normalize_url()` function from `mkdocs.util`).
Typically used to manage custom navigation:
`{{ page.url | relative_url }}`.
"""
return normalize_url(path=path, page=env.page) ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1658502004.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/errors.py 0000644 0000765 0000024 00000002061 14266535564 022044 0 ustar 00laurent staff import textwrap
import traceback
from functools import singledispatch
from jinja2 import TemplateSyntaxError
from mkdocs.structure.pages import Page
@singledispatch
def format_error(error: Exception, markdown: str, page: Page) -> str:
"""Default error message for a generic exception."""
error_type = type(error).__name__
return textwrap.dedent(
f'''
# _Macro Rendering Error_
_File_: `{page.file.src_path}`
_{error_type}_: {error}
```
%s
```
''',
).strip() % traceback.format_exc()
@format_error.register
def _format_template_syntax_error(
error: TemplateSyntaxError,
markdown: str,
page: Page,
) -> str:
"""Template rendering failed."""
line = markdown.splitlines()[error.lineno - 1]
return textwrap.dedent(
f'''
# _Macro Syntax Error_
_File_: `{page.file.src_path}`
_Line {error.lineno} in Markdown file:_ **{error.message}**
```markdown
{line}
```
'''
).strip()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1722176079.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/macros_info.md 0000644 0000765 0000024 00000003103 14651451117 022762 0 ustar 00laurent staff {#
Template for the macro_info() command
(C) Laurent Franceschetti 2019
#}
## Macros Plugin Environment
### General List
All available variables and filters within the macros plugin:
{{ context() | pretty }}
### Config Information
Standard MkDocs configuration information. Do not try to modify.
e.g. {{ "`{{ config.docs_dir }}`" }}
See also the [MkDocs documentation on the config object](https://www.MkDocs.org/user-guide/custom-themes/#config).
{{ context(config)| pretty }}
### Macros
These macros have been defined programmatically for this environment
(module or pluglets).
{{ context(macros)| pretty }}
### Git Information
Information available on the last commit and the git repository containing the
documentation project:
e.g. {{ "`{{ git.message }}`" }}
{{ context(git)| pretty }}
### Page Attributes
Provided by MkDocs. These attributes change for every page
(the attributes shown are for this page).
e.g. {{ "`{{ page.title }}`" }}
See also the [MkDocs documentation on the page object](https://www.MkDocs.org/user-guide/custom-themes/#page).
{{ context(page)| pretty }}
To have all titles of all pages, use:
```
{% raw %}
{% for page in navigation.pages %}
- {{ page.title }}
{% endfor %}
{% endraw %}
```
### Plugin Filters
These filters are provided as a standard by the macros plugin.
{{ context(filters)| pretty }}
### Builtin Jinja2 Filters
These filters are provided by Jinja2 as a standard.
See also the [Jinja2 documentation on builtin filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filterss).
{{ context(filters_builtin) | pretty }}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/plugin.py 0000644 0000765 0000024 00000105671 15064255224 022026 0 ustar 00laurent staff # --------------------------------------------
# Main part of the plugin
# Defines the MacrosPlugin class
#
# Laurent Franceschetti (c) 2018
# MIT License
# --------------------------------------------
import importlib
import os
from copy import copy
import pathspec
import json
from datetime import datetime
import yaml
from jinja2 import (
Environment, FileSystemLoader, Undefined, DebugUndefined, StrictUndefined,
)
from super_collections import SuperDict, yaml_support
yaml_support()
from mkdocs.config import config_options
from mkdocs.config.config_options import Type as PluginType
from mkdocs.plugins import BasePlugin
from mkdocs.structure.pages import Page
from mkdocs_macros.errors import format_error
from mkdocs_macros.context import define_env
from mkdocs_macros.util import (
is_on_pypi, parse_package, trace, debug,
update, import_local_module, format_chatter, LOG, get_log_level,
setup_directory
# SuperDict,
)
# ------------------------------------------
# Initialization
# ------------------------------------------
# The subsets of the YAML file that will be used for the variables:
YAML_VARIABLES = 'extra'
# The default name of the Python module:
DEFAULT_MODULE_NAME = 'main' # main.py
# ------------------------------------------
# Debug
# ------------------------------------------
# message for the front matter of markdown pages saved after rendering:
YAML_HEADER_WARNING = (
"# IMPORTANT NOTE:"
"\n# This page was automatically generated by MkDocs-Macros "
"for debug purposes,"
"\n# after rendering the macros as plain text."
f"\n# ({datetime.now():%Y-%m-%d %H:%M:%S})"
)
# Possible behavior in case of ignored variables or macros (first is default)
class LaxUndefined(Undefined):
"Pass anything wrong as blank"
def _fail_with_undefined_error(self, *args, **kwargs):
return ''
UNDEFINED_BEHAVIOR = {'keep': DebugUndefined,
'silent': Undefined,
'strict': StrictUndefined,
# lax will even pass unknown objects:
'lax': LaxUndefined}
# By default undefined jinja2 variables AND macros will be left as-is
# see https://stackoverflow.com/a/53134416
DEFAULT_UNDEFINED_BEHAVIOR = 'keep'
# Return codes in case of error
ERROR_MACRO = 100
# ------------------------------------------
# Plugin
# ------------------------------------------
# little utility for updating a dictionary from another
def register_items(category:str, ref:dict, additional:dict):
"""
Register outside items (additional) into a ref dictionary.
Fail with KeyError the key already exists.
E.g: register_items('macro', self.macros, items)
"""
for key, value in additional.items():
if key in ref:
raise KeyError("Registration error: "
"%s %s already exists" % (category, key))
ref[key] = value
class MacrosPlugin(BasePlugin):
"""
Inject config 'extra' variables into the markdown
plus macros / variables defined in external module.
The python code is located in 'main.py' or in a 'main' package
in the root directory of the website
(unless you want to redefine that name in the 'python_module' value
in the mkdocs.yml file)
"""
# what is under the 'macros' namespace (will go into the config property):
J2_STRING = PluginType(str, default='')
config_scheme = (
# main python module:
('module_name', PluginType(str,
default=DEFAULT_MODULE_NAME)),
('modules', PluginType(list,
default=[])),
# How to render pages by default: yes (opt-out), no (opt-in)
('render_by_default', PluginType(bool, default=True)),
# Force the rendering of those directories and files
# Use Pathspec syntax (similar to gitignore)
# see: https://python-path-specification.readthedocs.io/en/stable/readme.html#tutorial
# this is relative to doc_dir
('force_render_paths', J2_STRING),
# Include directory for external files
# also works for {% include ....%}) and {% import ....%}):
('include_dir', J2_STRING),
# list of additional yaml files:
('include_yaml', PluginType(list, default=[])),
# for altering the j2 markers, in case of need:
# https://jinja.palletsprojects.com/en/latest/api/
('j2_block_start_string', J2_STRING),
('j2_block_end_string', J2_STRING),
('j2_variable_start_string', J2_STRING),
('j2_variable_end_string', J2_STRING),
('j2_comment_start_string', J2_STRING),
('j2_comment_end_string', J2_STRING),
# for behavior of unknown macro (e.g. other plugin):
('on_undefined', PluginType(str, default=DEFAULT_UNDEFINED_BEHAVIOR)),
# for CD/CI set that parameter to true
('on_error_fail', PluginType(bool, default=False)),
('verbose', PluginType(bool, default=False))
)
# these are lists of external items (loaded last)
# in case they are declared before on_config is run
# (i.e. other plugin is running before this one)
_add_macros = {}
_add_filters = {}
_add_variables = {}
def start_chatting(self, prefix: str, color: str = 'yellow'):
"Generate a chatter function (trace for macros)"
def chatter(*args):
"""
Defines a tracer for the Verbose mode, to be used in macros.
If `verbose: true` in the YAML config file (under macros plugin),
it will start "chattering"
(talking a lot and in a friendly way,
about mostly unimportant things).
Otherwise, it will remain silent.
If you change the `verbose` while the local server is activated,
(`mkdocs server`) this should be instantly reflected.
Usage:
-----
chatter = env.make_chatter('MY_MODULE_NAME')
chatter("This is a dull debug message.")
Will result in:
INFO - [macros - Simple module] - This is a dull info message.
"""
if self.config['verbose']:
LOG.info(format_chatter(*args, prefix=prefix, color=color))
return chatter
# ------------------------------------------------
# These properties are available in the env object
# in macros
# ------------------------------------------------
@property
def conf(self):
"""
Dictionary containing of the whole config file (by default: mkdocs.yml)
This property may be useful if the code in the module needs to access
general configuration information.
NOTE: this property is called 'conf', because there is already
a 'config' property in a BasePlugin object,
which is the data connected to the macros plugin
(in the yaml file)
"""
try:
return self._conf
except AttributeError:
raise AttributeError("Conf property of macros plugin "
"was called before it was initialized!")
@property
def variables(self):
"The cumulative list of variables, initialized by on_config()"
try:
return self._variables
except AttributeError:
raise AttributeError("Property called before on_config()")
@property
def macros(self):
"The cumulative list of macros, initialized by on_config()"
try:
return self._macros
except AttributeError:
raise AttributeError("Property called before on_config()")
@property
def filters(self):
"The list of filters defined in the module, initialized by on_config()"
try:
return self._filters
except AttributeError:
self._filters = {}
return self._filters
@property
def project_dir(self) -> str:
"The directory of project"
# we calculate it from the configuration file
CONFIG_FILE = self.conf['config_file_path']
return os.path.dirname(os.path.abspath(CONFIG_FILE))
def macro(self, v, name=''):
"""
Registers a variable as a macro in the template,
i.e. in the variables dictionary:
env.macro(myfunc)
Optionally, you can assign a different name:
env.macro(myfunc, 'funcname')
You can also use it as a decorator:
@env.macro
def foo(a):
return a ** 2
More info:
https://stackoverflow.com/questions/6036082/call-a-python-function-from-jinja2
"""
name = name or v.__name__
self.macros[name] = v
return v
def filter(self, v, name=''):
"""
Register a filter in the template,
i.e. in the filters dictionary:
env.filter(myfunc)
Optionally, you can assign a different name:
env.filter(myfunc, 'filtername')
You can also use it as a decorator:
@env.filter
def reverse(x):
"Reverse a string (and uppercase)"
return x.upper().[::-1]
See: https://jinja.palletsprojects.com/en/2.10.x/api/#custom-filters
"""
name = name or v.__name__
self.filters[name] = v
return v
# ------------------------------------------------
# Property of the current page for on_page_markdown()
# ------------------------------------------------
@property
def page(self) -> Page:
"""
The current page's information
"""
try:
return self._page
except AttributeError:
raise AttributeError("Too early: page information is not available"
"at this stage!")
@property
def markdown(self) -> str:
"""
The markdown of the current page, after interpretation
"""
try:
return self._markdown
except AttributeError:
raise AttributeError("Too early: raw markdown is not available"
"at this stage!")
@markdown.setter
def markdown(self, value):
"""
Used to set the raw markdown of the current page.
[Especially used in the `on_pre_page_macros()` and
`on_post_page_macros()` hooks.]
"""
if not isinstance(value, str):
raise ValueError("Value provided to attribute markdown "
"should be a string")
# check whether attribute is accessible:
self.markdown
self._markdown = value
@property
def raw_markdown(self) -> str:
"""
Cancelled attribute
"""
trace("Property env.raw_markdown is removed "
"as of 1.1.0; use env.markdown instead!")
return self.markdown(self)
@markdown.setter
def raw_markdown(self, value):
"""
Used to set the raw markdown
"""
trace("Property env.raw_markdown is removed "
"as of 1.1.0; use env.markdown instead!")
self.markdown = value
# ----------------------------------
# Hooks for other applications
# ----------------------------------
def register_macros(self, items:dict):
"""
Register macros (hook for other plugins).
These will be added last, and raise an exception if already present.
"""
trace(f"Registering external macros: {list(items)}")
try:
# after on_config
self._macros
register_items('macro', self.macros, items)
self.variables["macros"].update(self.macros)
self.env.globals.update(self.macros)
except AttributeError:
# before on_config: store for later
self._add_macros.update(items)
def register_filters(self, items:dict):
"""
Register filters (hook for other plugins).
These will be added last, and raise an exception if already present.
"""
trace(f"Registering external filters: {list(items)}")
try:
self._filters
register_items('filter', self.filters, items)
self.variables["filters"].update(self.filters)
self.env.filters.update(self.filters)
except AttributeError:
# before on_config: store for later
self._add_filters.update(items)
def register_variables(self, items:dict):
"""
Register variables (hook for other plugins).
These will be added last, and raise an exception if already present.
"""
trace(f"Registering external variables: {list(items)}")
try:
# after on_config
self._variables
register_items('variables', self.variables, items)
except AttributeError:
# before on_config: store for later
self._add_variables.update(items)
# ----------------------------------
# Function lists, for later events
# ----------------------------------
@property
def pre_macro_functions(self):
"""
List of pre-macro functions contained in modules.
These are deferred to the on_page_markdown() event.
"""
try:
return self._pre_macro_functions
except AttributeError:
raise AttributeError("You called the pre_macro_functions property "
"too early. Does not exist yet !")
@property
def post_macro_functions(self):
"""
List of post-macro functions contained in modules.
These are deferred to the on_page_markdown() event.
"""
try:
return self._post_macro_functions
except AttributeError:
raise AttributeError("You called the post_macro_functions property "
"too early. Does not exist yet !")
@property
def post_build_functions(self):
"""
List of post build functions contained in modules.
These are deferred to the on_post_build() event.
"""
try:
return self._post_build_functions
except AttributeError:
raise AttributeError("You called post_build_functions property "
"too early. Does not exist yet !")
def force_page_rendering(self, filename:str)->bool:
"""
Predicate: it defines whether the rendering of this page
filename must be forced
(because it is in the `force_render_paths` parameters).
That parameterer is parsed in on_config() and used to define
`render_paths_spec`.
"""
try:
return self._render_paths_spec.match_file(filename)
except AttributeError:
raise AttributeError("You called the force_render() method "
"too early. Not initialized yet !")
# -----------------------s-----------
# load elements
# ----------------------------------
def _load_yaml(self):
"Load the the external yaml files"
for el in self.config['include_yaml']:
# el is either a filename or {key: filename} single-entry dict
try:
[[key, filename]] = el.items()
except AttributeError:
key = None
filename = el
# Paths are be relative to the project root.
filename = os.path.join(self.project_dir, filename)
if os.path.isfile(filename):
with open(filename, encoding="utf-8") as f:
# load the yaml file
# NOTE: for the SafeLoader argument, see: https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation
content = yaml.load(f, Loader=yaml.SafeLoader)
trace("Loading yaml file:", filename)
if key is not None:
content = {key: content}
update(self.variables, content)
else:
trace("WARNING: YAML configuration file was not found!",
filename)
def _load_module(self, module, module_name):
"""
Load a single module
Add variables and functions to the config dictionary,
via the python module
(located in the same directory as the Yaml config file).
This function enriches the variables dictionary
The python module must contain the following hook:
define_env(env):
"Declare environment for jinja2 templates for markdown"
env.variables['a'] = 5
@env.macro
def bar(x):
...
@env.macro
def baz(x):
...
@env.filter
def foobar(x):
...
"""
if not module:
return
trace("Found external Python module '%s' in:" % module_name,
self.project_dir)
# execute the hook for the macros
function_found = False
if hasattr(module, 'define_env'):
module.define_env(self)
function_found = True
# DECLARE additional event functions
# NOTE: each of these functions requires self (the environment).
STANDARD_FUNCTIONS = ['define_env']
def add_function(funcname: str, funclist: list):
"Add another standard function to the module"
STANDARD_FUNCTIONS.append(funcname)
if hasattr(module, funcname):
nonlocal function_found
func = getattr(module, funcname)
funclist.append(func)
function_found = True
add_function('on_pre_page_macros', self.pre_macro_functions)
add_function('on_post_page_macros', self.post_macro_functions)
add_function('on_post_build', self.post_build_functions)
if function_found:
trace("Functions found:", ','.join(STANDARD_FUNCTIONS))
else:
raise NameError("None of the standard functions was found "
"in module '%s':\n%s" %
(module_name, STANDARD_FUNCTIONS))
def _load_modules(self):
"Load all modules"
self._pre_macro_functions = []
self._post_macro_functions = []
self._post_build_functions = []
# pluglets installed modules (as in pip list)
modules = self.config['modules']
if modules:
trace("Preinstalled modules: ", ','.join(modules))
for m in modules:
# split the name of package in source (pypi) and module name
source_name, module_name = parse_package(m)
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError:
if is_on_pypi(source_name, fail_silently=True):
err_msg = (f"Counld not import pluglet '{source_name}'. "
f"Please install it from Pypi:\n\n pip install {source_name}")
raise ModuleNotFoundError(err_msg, name=module_name)
else:
raise ModuleNotFoundError(f"Could not import "
"module '{module_name}' (missing?)")
self._load_module(module, module_name)
# local module (file or dir)
local_module_name = self.config['module_name']
debug("Project dir '%s'" % self.project_dir)
module = import_local_module(self.project_dir, local_module_name)
if module:
trace("Found local Python module '%s' in:" % local_module_name,
self.project_dir)
self._load_module(module, local_module_name)
else:
if local_module_name == DEFAULT_MODULE_NAME:
# do not do anything if there is no main module
trace("No default module `%s` found" % DEFAULT_MODULE_NAME)
else:
raise ImportError("Macro plugin could not find custom '%s' "
"module in '%s'." %
(local_module_name, self.project_dir))
# ----------------------------------
# output elements
# ----------------------------------
@property
def env(self) -> Environment:
"""
The templating environment for Jinja2.
It is a core component of the macros engine.
It is defined in `on_config`.
NOTE: Do NOT confuse with the env argument in a module.
"""
try:
return self._env
except AttributeError:
raise AttributeError("Jinja2 environment is not defined yet!")
def render(self, markdown: str, force_rendering:bool=False) -> str:
"""
Render a page through jinja2: it reads the code and
executes the macros.
It tests the `render_macros` metavariable
in the page's header to decide whether to actually
render or not (but you can force it).
PRINCIPLE OF PRECAUTION:
If the YAML header of the page contains `render_macros: false`:
that takes priority:
NO rendering will be done, and the markdown will be returned
as is (even if `force_rendering` is set to true).
Arguments
---------
- markdown: the markdown/HTML page (with the jinja2 macros)
- force_rendering: if True, it forces the rendering,
even if the page header doesn't say so
(used in the case when `render_by_default` is set to false
in the config file)
Returns
-------
A pure markdown/HTML page.
Notes
-----
- Must called by _on_page_markdown()
"""
# Process meta_variables
# ----------------------
# copy the page variables and update with the meta data
# in the YAML header:
page_variables = copy(self.variables)
try:
meta_variables = self.variables['page'].meta
except KeyError as e:
# this is a premature rendering, no meta variables in the page
meta_variables = {}
# Warning this is ternary logic(True, False, None: nothing said)
render_macros = None
if meta_variables:
# file_path = self.variables.page.file.src_path
file_path = self.page.file.src_path
debug(f"Metadata in page '{file_path}'",
payload=meta_variables)
# determine whether the page will be rendered or not
# the two formulations are accepted
render_macros = meta_variables.get('render_macros')
# ignore_macros should be phased out
if meta_variables.get('ignore_macros'):
raise ValueError("The metavariable `ignore_macros` "
"is now FORBIDDEN "
"in the header of markdown pages, "
"use `render_macros` instead.")
# this takes precedence over any other consideration:
if render_macros == False:
return markdown
if self.config['render_by_default'] == False:
# opt-in
if force_rendering or render_macros == True:
pass # opt-in
else:
return markdown
# Update the page with meta variables
# i.e. what's in the yaml header of the page
page_variables.update(meta_variables)
# Rendering
# ----------------------
# expand the template
on_error_fail = self.config['on_error_fail']
try:
md_template = self.env.from_string(markdown)
# Execute the jinja2 template and return
return md_template.render(**page_variables)
except Exception as error:
error_message = format_error(
error,
markdown=markdown,
page=self.page,
)
trace('ERROR', error_message, level='warning')
if on_error_fail:
exit(ERROR_MACRO)
else:
return error_message
def has_j2(self, s:str) -> bool:
"""
Defines whether a string might contain j2 code.
The criterion is: does it contain any start strings,
such as, e.g., `{{`?
It takes into account the j2_..._start_string
parameters of the config file.
"""
env = self.env
CANDIDATES = [env.variable_start_string,
env.block_start_string,
env.comment_start_string]
return any(item in s for item in CANDIDATES)
# ----------------------------------
# Standard Hooks for a mkdocs plugin
# ----------------------------------
def on_config(self, config):
"""
Called once (initialization)
From the configuration file, builds a Jinj2 environment
with variables, functions and filters.
"""
debug("Configuring the macros environment...")
# WARNING: this is not the config argument:
debug("Macros arguments\n", self.config)
# define the variables and macros as dictionaries
# (for update function to work):
self._variables = SuperDict()
self._macros = SuperDict()
# load the extra variables
extra = dict(config.get(YAML_VARIABLES))
# make a copy for documentation:
self.variables['extra'] = extra
# actual variables (top level will be loaded later)
# export the whole data passed as argument, in case of need:
self._conf = config
# add a copy to the template variables
# that copy may be manipulated
self.variables['config'] = copy(config)
assert self.variables['config'] is not config
# load other yaml files
self._load_yaml()
# load the standard plugin context
define_env(self)
# at this point load the actual variables from extra (YAML file)
self.variables.update(extra)
# add variables, functions and filters from the Python module:
# by design, this MUST be the last step, so that programmers have
# full control on what happened in the configuration files
self._load_modules()
# place where variables/macros/filters are registered
# if they they were declared before Mkdocs-Macros in the config file.
# self._add_variables['foo'] = 5
# def bar(x):
# "Dummy function"
# return x + 5
# self._add_macros['bar'] = bar
# self._add_filters['baz'] = lambda s: s.upper()
register_items('variable', self.variables, self._add_variables)
register_items('macro' , self.macros , self._add_macros )
register_items('filter' , self.filters , self._add_filters )
# if len(extra):
# trace("Extra variables (config file):", list(extra.keys()))
# debug("Content of extra variables (config file):\n", dict(extra))
# Define the spec for the file paths whose rendering must be forced.
# It will be used by the force_page_rendering() predicate:
force_render_paths = self.config['force_render_paths']
self._render_paths_spec = pathspec.PathSpec.from_lines(
'gitwildmatch',
force_render_paths.splitlines())
# -------------------
# Create the jinja2 environment:
# -------------------
DOCS_DIR = config.get('docs_dir')
debug("Docs directory:", DOCS_DIR)
# define the include directory:
# NOTE: using DOCS_DIR as default is not ideal,
# because those files get rendered as well, which is incorrect
# since they are partials; but we do not want to break existing installs
include_dir = self.config['include_dir'] or DOCS_DIR
if not os.path.isdir(include_dir):
raise FileNotFoundError("MACROS ERROR: Include directory '%s' "
"does not exist!" %
include_dir)
if self.config['include_dir']:
trace("Includes directory:", include_dir)
else:
debug("Includes directory:", include_dir)
# get the behavior in case of unknown variable (default: keep)
on_undefined = self.config['on_undefined']
if on_undefined not in UNDEFINED_BEHAVIOR:
raise ValueError("Illegal value for undefined macro parameter '%s'" % on_undefined)
undefined = UNDEFINED_BEHAVIOR[on_undefined]
debug("Undefined behavior:", undefined)
env_config = {
'loader': FileSystemLoader(include_dir),
'undefined': undefined
}
# read the config variables for jinja2:
for key, value in self.config.items():
# take definitions in config_scheme where key starts with 'j2_'
# (if value is not empty)
# and forward them to jinja2
# this is used for the markers
if key.startswith('j2_') and value:
variable_name = key.split('_', 1)[1] # remove prefix
trace("Found j2 variable '%s': '%s'" %
(variable_name, value))
env_config[variable_name] = value
# finally build the environment:
self._env = Environment(**env_config)
# -------------------
# Process macros
# -------------------
# reference all macros
self.variables['macros'] = copy(self.macros)
# add the macros to the environment's global (not to the template!)
self.env.globals.update(self.macros)
# -------------------
# Process filters
# -------------------
# reference all filters, for doc [these are copies, so no black magic]
# NOTE: self.variables is reflected in the list of variables
# in the jinja2 environment (same object)
self.variables['filters'] = copy(self.filters)
self.variables['filters_builtin'] = copy(self.env.filters)
# update environment with the custom filters:
self.env.filters.update(self.filters)
debug("End of environment config")
def on_pre_build(self, *, config):
"""
Provide information on the variables, so that mkdocs-test
can capture the trace (for testing)
It is put here, in case some plugin hooks into the config
to add some variables, macros or filters, after the execution
of the `on_config()` of this plugin.
"""
trace("Config variables:", list(self.variables.keys()))
debug("Config variables:", payload=SuperDict(self.variables).to_json())
if self.macros:
trace("Config macros:", list(self.macros.keys()))
debug("Config macros:", payload=SuperDict(self.macros).to_json())
if self.filters:
trace("Config filters:", list(self.filters.keys()))
debug("Config filters:", payload=SuperDict(self.filters).to_json())
def on_nav(self, nav, config, files):
"""
Called after the site navigation is created.
Capture the nav and files objects so they can be used by
templates.
"""
# nav has useful properties like 'pages' and 'items'
# see: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/nav.py
self.variables['navigation'] = nav
# files has collection of files discovered in docs_dir
# see: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/files.py
# NOTE: useful for writing macros that check for the existence of files; e.g., a macro to mark a link as disabled, if its target doesn't exist
self.variables['files'] = files
def on_serve(self, server, config, **kwargs):
"""
Called when the serve command is used during development.
This is to add files or directories to the list of "watched"
files for auto-reloading.
"""
# define directories to add, keep non nulls
additional = [self.config['include_dir'] # markdown includes
]
additional = [el for el in additional if el]
if additional:
trace("We will also watch:", additional)
# necessary because of a bug in mkdocs:
# more information in:
# https://github.com/mkdocs/mkdocs/issues/1952))
try:
builder = list(server.watcher._tasks.values())[0]["func"]
except AttributeError:
# change in mkdocs 1.2, see: https://www.mkdocs.org/about/release-notes/#backward-incompatible-changes-in-12
# this parameter is now optional
builder = None
# go ahead and watch
for el in additional:
if el:
server.watch(el, builder)
def on_page_markdown(self, markdown, page:Page,
config, **kwargs):
"""
Pre-rendering for each page of the website.
It uses the jinja2 directives, together with
variables, macros and filters, to create pure markdown code.
"""
self._page = page
if not self.variables:
self.markdown = markdown
else:
debug("Rendering source page:", page.file.src_path)
# Update the page info in the document
# page is an object with a number of properties (title, url, ...)
# see: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/pages.py
self.variables["page"] = copy(page)
# Define whether we must force the rendering of this page,
# based on filename (relative to docs_dir directory)
filename = page.file.src_path
force_rendering = self.force_page_rendering(filename)
# set the markdown (for the first time)
self._markdown = markdown
# execute the pre-macro functions in the various modules
for func in self.pre_macro_functions:
func(self)
# render the macros
self.markdown = self.render(
markdown=self.markdown,
force_rendering=force_rendering
)
# Convert macros in the title from render (if exists)
# to answer 144
# There is a bizarre issue #215 where setting the title
# prevents interpretation of icons with pymdownx.emoji
debug("Page title:",page.title)
if self.has_j2(page.title):
page.title = self.render(markdown=page.title,
force_rendering=force_rendering)
debug("Page title after macro rendering:",page.title)
# execute the post-macro functions in the various modules
for func in self.post_macro_functions:
func(self)
return self.markdown
def on_post_build(self, config: config_options.Config):
"""
Hook for post build actions, typically adding
raw files to the setup.
"""
# execute the functions in the various modules
for func in self.post_build_functions:
func(self)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1722150399.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/py.typed 0000644 0000765 0000024 00000000000 14651366777 021652 0 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros/util.py 0000644 0000765 0000024 00000020670 15064255224 021500 0 ustar 00laurent staff #!/usr/bin/env python3
"""
Utilities for mkdocs-macros
"""
import subprocess
from copy import deepcopy
import os, sys, importlib.util, shutil
from typing import Literal
from packaging.version import Version
import json
import inspect
import requests
from datetime import datetime
from typing import Any
from termcolor import colored
import mkdocs
import hjson
# ------------------------------------------
# Trace and debug
# ------------------------------------------
TRACE_COLOR = 'green'
TRACE_PREFIX = 'macros'
import logging
LOG = logging.getLogger("mkdocs.plugins." + __name__)
MKDOCS_LOG_VERSION = '1.2'
if Version(mkdocs.__version__) < Version(MKDOCS_LOG_VERSION):
# filter doesn't do anything since that version
from mkdocs.utils import warning_filter
LOG.addFilter(warning_filter)
def format_trace(*args, payload:str=''):
"""
General purpose print function, as trace,
for the mkdocs-macros framework;
it will appear if --verbose option is activated
The payload is simply some text that will be added after a newline.
"""
first = args[0]
rest = [str(el) for el in args[1:]]
if payload:
rest.append(f"\n{payload}")
text = "[%s] - %s" % (TRACE_PREFIX, first)
emphasized = colored(text, TRACE_COLOR)
return ' '.join([emphasized] + rest)
TRACE_LEVELS = {
'debug' : logging.DEBUG,
'info' : logging.INFO,
'warning' : logging.WARNING,
'error' : logging.ERROR,
'critical': logging.CRITICAL
}
def trace(*args, payload:str='', level:str='info'):
"""
General purpose print function, as trace,
for the mkdocs-macros framework;
it will appear unless --quiet option is activated.
Payload is an information that goes to the next lines
(typically a json dump)
The level is 'debug', 'info', 'warning', 'error' or 'critical'.
"""
msg = format_trace(*args, payload=payload)
try:
LOG.log(TRACE_LEVELS[level], msg)
except KeyError:
raise ValueError("Unknown level '%s' %s" % (level,
tuple(TRACE_LEVELS.keys())
)
)
return msg
# LOG.info(msg)
def debug(*args, payload:str=''):
"""
General purpose print function, as trace,
for the mkdocs-macros framework;
it will appear if --verbose option is activated
"""
msg = format_trace(*args, payload=payload)
LOG.debug(msg)
def get_log_level(level_name:str) -> bool:
"Get the log level (INFO, DEBUG, etc.)"
level = getattr(logging, level_name.upper(), None)
return LOG.isEnabledFor(level)
def format_chatter(*args, prefix:str, color:str=TRACE_COLOR):
"""
Format information for env.chatter() in macros.
(This is specific for macros)
"""
full_prefix = colored('[%s - %s] -' % (TRACE_PREFIX, prefix),
color)
args = [full_prefix] + [str(arg) for arg in args]
msg = ' '.join(args)
return msg
# ------------------------------------------
# Packages and modules
# ------------------------------------------
def parse_package(package:str):
"""
Parse a package name
if it is in the forme 'foo:bar' then it is a pluglet:
- 'foo' is the source,
- 'bar' is the (import) package name.
Returns the source name (for pip install) and the package name (for import)
"""
l = package.split(':')
if len(l) == 1:
source_name = package_name = l[0]
else:
source_name, package_name = l[:2]
return source_name, package_name
def is_on_pypi(source_name: str, fail_silently: bool = False) -> bool:
"""
Check if a package is available on PyPI.
Parameters:
- source_name: the name of the package to check
- fail_silently: if True, return False on network error; if False, raise the error
Returns:
- True if the package exists on PyPI
- False if not found.
(will raise a RunTime error on network error,
unless fail_silently=True: will report False)
"""
url = f"https://pypi.org/pypi/{source_name}/json"
try:
response = requests.get(url, timeout=3)
return response.status_code == 200
except requests.exceptions.RequestException as e:
if fail_silently:
return False
raise RuntimeError(f"Unable to reach PyPI to check for '{source_name}': {e}")
def import_local_module(project_dir, module_name):
"""
Import a module from a pathname.
"""
# get the full path
if not os.path.isdir(project_dir):
raise FileNotFoundError("Project dir does not exist: %s" % project_dir)
# there are 2 possibilities: dir or file
pathname_dir = os.path.join(project_dir, module_name)
pathname_file = pathname_dir + '.py'
if os.path.isfile(pathname_file):
spec = importlib.util.spec_from_file_location(module_name,
pathname_file)
module = importlib.util.module_from_spec(spec)
# execute the module
spec.loader.exec_module(module)
return module
elif os.path.isdir(pathname_dir):
# directory
sys.path.insert(0, project_dir)
# If the import is relative, then the package name must be given,
# so that Python always knows how to call it.
try:
return importlib.import_module(module_name, package='main')
except ImportError as e:
# BUT Python will NOT allow an import past the root of the project;
# this will fail when the module will actually be loaded.
# the only way, is to insert the directory into the path
sys.path.insert(0, module_name)
module_name = os.path.basename(module_name)
return importlib.import_module(module_name, package='main')
else:
return None
# ------------------------------------------
# Arithmetic
# ------------------------------------------
def update(d1, d2):
"""
Update object d1, with object d2, recursively
It has a simple behaviour:
- if these are dictionaries, attempt to merge keys
(recursively).
- otherwise simply makes a deep copy.
"""
BASIC_TYPES = (int, float, str, bool, complex)
if isinstance(d1, dict) and isinstance(d2, dict):
for key, value in d2.items():
# print(key, value)
if key in d1:
# key exists
if isinstance(d1[key], BASIC_TYPES):
d1[key] = value
else:
update(d1[key], value)
else:
d1[key] = deepcopy(value)
else:
# if it is any kind of object
d1 = deepcopy(d2)
# ------------------------------------------
# File system
# ------------------------------------------
def setup_directory(reference_dir: str, dir_name: str,
recreate:bool=True) -> str:
"""
Create a new directory beside the specified one.
Parameters:
- reference_dir (str): The path of the current (reference) directory.
- dir_name (str): The name of the new directory to be created beside the current directory.
Returns
- the directory
"""
# Find the parent directory and define new path:
parent_dir = os.path.dirname(reference_dir)
new_dir = os.path.join(parent_dir, dir_name)
# Safety: prevent deletion of current_dir
if new_dir == parent_dir:
raise FileExistsError("Cannot recreate the current dir!")
# Safety: check if the new directory exists
if os.path.exists(new_dir):
# If it exists, empty its contents
shutil.rmtree(new_dir)
# Recreate the new directory
if recreate:
os.makedirs(new_dir)
return new_dir
if __name__ == '__main__':
# test merging of dictionaries
a = {'foo': 4, 'bar': 5}
b = {'foo': 5, 'baz': 6}
update(a, b)
print(a)
assert a['foo'] == 5
assert a['baz'] == 6
a = {'foo': 4, 'bar': 5}
b = {'foo': 5, 'baz': ['hello', 'world']}
update(a, b)
print(a)
assert a['baz'] == ['hello', 'world']
a = {'foo': 4, 'bar': {'first': 1, 'second': 2}}
b = {'foo': 5, 'bar': {'first': 2, 'third': 3}}
update(a, b)
print(a)
assert a['bar'] == {'first': 2, 'second': 2, 'third': 3}
NEW = {'hello': 5}
c = {'bar': {'third': NEW}}
update(a, c)
print(a)
assert a['bar']['third'] == NEW
NEW = {'first': 2, 'third': 3}
a = {'foo': 4}
b = {'bar': NEW}
update(a, b)
print(a)
assert a['bar'] == NEW
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.452782
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/ 0000755 0000765 0000024 00000000000 15064255234 023215 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550684.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/PKG-INFO 0000644 0000765 0000024 00000020041 15064255234 024307 0 ustar 00laurent staff Metadata-Version: 2.4
Name: mkdocs-macros-plugin
Version: 1.4.0
Summary: Unleash the power of MkDocs with macros and variables
Author: Laurent Franceschetti
License: MIT
Project-URL: Homepage, https://github.com/fralau/mkdocs_macros_plugin
Keywords: macros,markdown,mkdocs,python
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.5
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: hjson
Requires-Dist: jinja2
Requires-Dist: mkdocs>=0.17
Requires-Dist: packaging
Requires-Dist: pathspec
Requires-Dist: python-dateutil
Requires-Dist: pyyaml
Requires-Dist: super-collections>=0.5.7
Requires-Dist: termcolor
Provides-Extra: test
Requires-Dist: mkdocs-include-markdown-plugin; extra == "test"
Requires-Dist: mkdocs-macros-test; extra == "test"
Requires-Dist: mkdocs-material>=6.2; extra == "test"
Requires-Dist: mkdocs-test; extra == "test"
Requires-Dist: mkdocs-d2-plugin; extra == "test"
Provides-Extra: doc
Requires-Dist: mkdocs-mermaid2-plugin; extra == "doc"
Dynamic: license-file

# Unleash the power of MkDocs with variables and macros
[](https://opensource.org/licenses/MIT)




:open_file_folder: [Used by > 2K repositories on Github](https://github.com/fralau/mkdocs_macros_plugin/network/dependents)
🥇 Listed as [High-Quality Plugin](https://github.com/mkdocs/catalog#-code-execution-variables--templating)
**mkdocs-macros-plugin** is a general-purpose plugin for [MkDocs](https://www.mkdocs.org/)
that uses **variables** and **macros** (functions) to automate tasks, and produce richer and more beautiful pages.
```markdown
The unit price of product A is {{ unit_price }} EUR.
Taking the standard discount into account,
the sale price of 50 units is {{ price(unit_price, 50) }} EUR.
```
View the [mkdocs-macro documentation](https://mkdocs-macros-plugin.readthedocs.io/) on Read the Docs.
## Overview
**mkdocs-macros-plugin** is a plugin that makes it easier for contributors
of an [MkDocs](https://www.mkdocs.org/) website to produce richer and more beautiful pages. It transforms the markdown pages
into [jinja2](https://jinja.palletsprojects.com/en/2.10.x/) templates
that use **variables**, calls to **macros** and custom **filters**.
> **You can also partially replace MkDocs plugins with mkdocs-macros modules,
> and [pluglets](https://mkdocs-macros-plugin.readthedocs.io/en/latest/pluglets/)
> (pre-installed modules).**
### Using variables
You can leverage the power of Python in markdown thanks to jinja2
by writing this :
```markdown
The unit price of product A is {{ unit_price }} EUR.
Taking the standard discount into account,
the sale price of 50 units is {{ price(unit_price, 50) }} EUR.
```
If you defined a `price()` function, this could translate into:
```
The unit price of product A is 10.00 EUR.
Taking the standard discount into account,
the sale price of 50 units is 450.00 EUR.
```
> The result of a macro can be **HTML code**:
this makes macros especially useful
to make custom extensions to the syntax of markdown, such as buttons,
calls to email, embedding YouTube videos, etc.
It is possible to use the wide range of facilities provided by
[Jinja2 templates](http://jinja.pocoo.org/docs/2.10/templates/) such
as conditions (`{% if ... %}`) and loops (`{% for ... %}`).
### Defining variables
Regular **variables** can be defined in five ways:
| No | Validity | For whom | Description |
| --- | --- | --- | ---- |
| 1. | global | designer of the website | in the `mkdocs.yml` file, under the `extra` heading |
| 2. | global | contributor | in external yaml definition files |
| 3. | global | programmer | in a `main.py` file (Python), by adding them to a dictionary |
| 4. | local (page) | writer | in the YAML header of each Markdown page |
| 5. | local (page) | writer | with a `{%set variable = value %}` statement |
In addition, predefined objects are provided (local and global), typically
for the environment, project, page, git information, etc.
### Macros and filters
Similarly programmers can define their own **macros** and **filters**,
as Python functions in the `main.py` file,
which the users will then be able to
use without much difficulty, as jinja2 directives in the markdown page.
## Installation
### Prerequisites
- Python version > 3.7
- MkDocs version >= 1.0
(compatible with post 1.5 versions)
### Standard installation
```
pip install mkdocs-macros-plugin
```
### "Manual installation"
To install the package, download it and run:
```
pip install .
# or...
python setup.py install
```
### Development/test installation
To install the extra dependencies required for testing the package, run:
```
pip install "mkdocs-macros-plugin[test]"
```
### Declaration of plugin
Declare the plugin in the file `mkdocs.yml`:
```yaml
plugins:
- search
- macros
```
> **Note:** If you have no `plugins` entry in your config file yet,
you should also add the `search` plugin.
If no `plugins` entry is set, MkDocs enables `search` by default; but
if you use it, then you have to declare it explicitly.
By default, undefined variables are printed to the page as-is. If you
wish for a page to fail on undefined variables, you should use the
below configuration instead:
```yaml
plugins:
- search
- macros
on_undefined: strict
```
For details and more options, see the [documentation](
https://mkdocs-macros-plugin.readthedocs.io/en/latest/troubleshooting/#what-happens-if-a-variable-is-undefined).
### Check that it works
The recommended way to check that the plugin works properly is to add the
following command in one of the pages of your site (let's say `info.md`):
```
{{ macros_info() }}
```
In the terminal, restart the environment:
```
> mkdocs serve
````
You will notice that additional information now appears in the terminal:
```
INFO - Building documentation...
[macros] Macros arguments: {'module_name': 'main', 'include_yaml': [], 'j2_block_start_string': '', 'j2_block_end_string': '', 'j2_variable_start_string': '', 'j2_variable_end_string': ''}
```
Within the browser (e.g. http://127.0.0.1:8000/info), you should
see a description of the plugin's environment:

If you see it that information, you should be all set.
Give a good look at the General List, since it gives you an overview
of what you can do out of the box with the macros plugin.
The other parts give you more detailed information.
## Using pluglets
### What are pluglets?
**Pluglets** are small, easy-to-write programs
that use mkdocs-macro's foundation
to offer services to mkdocs projects, which would normally
be offered by plugins.
Pluglets are Python packages, which can be hosted on github, and
distributed through [PyPI](https://pypi.org/).
### How to add a pluglet to an mkdocs project?
Install it:
```shell
pip install
```
Declare it in the project's config (`mkdocs.yml`) file:
```yaml
plugins:
- search
- macros:
modules:
-
```
### How to write a pluglet?
[See instructions in the documentation](https://mkdocs-macros-plugin.readthedocs.io/en/latest/pluglets/).
A sample pluglet can be found in [mkdocs-test (github)](https://github.com/fralau/mkdocs-macros-test).
### List of existing pluglets
[See the wiki page on Github](https://github.com/fralau/mkdocs-macros-plugin/wiki/Mkdocs%E2%80%90Macros-Pluglets).
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550684.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/SOURCES.txt 0000644 0000765 0000024 00000002331 15064255234 025100 0 ustar 00laurent staff LICENSE.md
MANIFEST.in
README.md
pyproject.toml
setup.py
mkdocs_macros/__init__.py
mkdocs_macros/context.py
mkdocs_macros/errors.py
mkdocs_macros/macros_info.md
mkdocs_macros/plugin.py
mkdocs_macros/py.typed
mkdocs_macros/util.py
mkdocs_macros_plugin.egg-info/PKG-INFO
mkdocs_macros_plugin.egg-info/SOURCES.txt
mkdocs_macros_plugin.egg-info/dependency_links.txt
mkdocs_macros_plugin.egg-info/entry_points.txt
mkdocs_macros_plugin.egg-info/requires.txt
mkdocs_macros_plugin.egg-info/top_level.txt
test/__init__.py
test/fixture.py
test/main_sample.py
test/test_various.py
test/missing_macros/__init__.py
test/missing_macros/test_site.py
test/module/__init__.py
test/module/main.py
test/module/test_site.py
test/module_dir/mymodule/__init__.py
test/new_syntax/main.py
test/no_module/main.py
test/null/__init__.py
test/null/test_site.py
test/opt_in/__init__.py
test/opt_in/test_site.py
test/opt_in/__pycache__/new_syntax/main.py
test/opt_out/__init__.py
test/opt_out/test_site.py
test/opt_out/__pycache__/new_syntax/main.py
test/plugin_d2/__init__.py
test/plugin_d2/test_t2.py
test/register_macros/__init__.py
test/register_macros/hooks.py
test/register_macros/test_doc.py
test/simple/__init__.py
test/simple/main_old.py
test/simple/test_site.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550684.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/dependency_links.txt 0000644 0000765 0000024 00000000001 15064255234 027263 0 ustar 00laurent staff
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550684.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/entry_points.txt 0000644 0000765 0000024 00000000074 15064255234 026514 0 ustar 00laurent staff [mkdocs.plugins]
macros = mkdocs_macros.plugin:MacrosPlugin
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550684.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/requires.txt 0000644 0000765 0000024 00000000361 15064255234 025615 0 ustar 00laurent staff hjson
jinja2
mkdocs>=0.17
packaging
pathspec
python-dateutil
pyyaml
super-collections>=0.5.7
termcolor
[doc]
mkdocs-mermaid2-plugin
[test]
mkdocs-include-markdown-plugin
mkdocs-macros-test
mkdocs-material>=6.2
mkdocs-test
mkdocs-d2-plugin
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550684.0
mkdocs_macros_plugin-1.4.0/mkdocs_macros_plugin.egg-info/top_level.txt 0000644 0000765 0000024 00000000050 15064255234 025742 0 ustar 00laurent staff dist
journals
mkdocs_macros
test
webdoc
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/pyproject.toml 0000644 0000765 0000024 00000002610 15064255224 020233 0 ustar 00laurent staff [project]
name = "mkdocs-macros-plugin"
# This version number is the REFERENCE for the rest of the project,
# particularly for update_pypi.sh
version = "1.4.0"
description = "Unleash the power of MkDocs with macros and variables"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.8"
authors = [{ name = "Laurent Franceschetti" }]
keywords = ["macros", "markdown", "mkdocs", "python"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.5",
]
dependencies = [
"hjson",
"jinja2",
"mkdocs>=0.17",
"packaging",
"pathspec",
"python-dateutil",
"pyyaml",
"super-collections >= 0.5.7",
"termcolor",
]
[tool.setuptools]
packages = { find = { exclude = ["*.tests"] } }
[project.optional-dependencies]
test = [
"mkdocs-include-markdown-plugin",
"mkdocs-macros-test",
"mkdocs-material>=6.2",
"mkdocs-test",
"mkdocs-d2-plugin",
]
# for the MkDocs documentation (webdoc/)
doc = ["mkdocs-mermaid2-plugin"]
[project.entry-points."mkdocs.plugins"]
macros = "mkdocs_macros.plugin:MacrosPlugin"
[project.urls]
Homepage = "https://github.com/fralau/mkdocs_macros_plugin"
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.453472
mkdocs_macros_plugin-1.4.0/setup.cfg 0000644 0000765 0000024 00000000046 15064255234 017142 0 ustar 00laurent staff [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1729138606.0
mkdocs_macros_plugin-1.4.0/setup.py 0000644 0000765 0000024 00000000401 14704107656 017032 0 ustar 00laurent staff """
Installation using setup.py is no longer supported.
Use `python -m pip install .` instead.
"""
from setuptools import setup
# Fake reference so GitHub still considers it a real package for statistics purposes.
setup(
name='mkdocs-macros-plugin',
) ././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.441632
mkdocs_macros_plugin-1.4.0/test/ 0000755 0000765 0000024 00000000000 15064255234 016300 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1727203983.0
mkdocs_macros_plugin-1.4.0/test/__init__.py 0000644 0000765 0000024 00000000000 14674605217 020405 0 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/test/fixture.py 0000644 0000765 0000024 00000007412 15064255224 020343 0 ustar 00laurent staff """
Specific for MkDocs Projects
(C) Laurent Franceschetti 2024
"""
import warnings
import json
import subprocess
from super_collections import SuperDict
from mkdocs_test import DocProject, MkDocsPage
class MacrosPage(MkDocsPage):
"Specific for MkDocs-Macros"
def has_error(self:MkDocsPage):
"Predicate: check whether the page has an error"
return self.find_text('Macro Rendering Error')
@property
def is_rendered(self):
"Accomodate earlier formulation"
warnings.warn("The page property `.is_rendered` is DEPRECATED "
"use `.is_markdown_rendered()` instead.",
UserWarning, stacklevel=2)
return self.is_markdown_rendered()
class MacrosDocProject(DocProject):
"Specific for MkDocs-Macros"
def build(self, strict:bool=False) -> subprocess.CompletedProcess:
"""
Build the documentation, to perform the tests
Verbose is forced to True, to get the variables, functions and filters
"""
super().build(strict=strict, verbose=True)
@property
def pages(self) -> dict[MacrosPage]:
"List of pages"
pages = super().pages
return {key: MacrosPage(value) for key, value in pages.items()}
@property
def macros_plugin(self):
"Information on the plugin"
return self.get_plugin('macros')
# ------------------------------------
# Get information through the payload
# ------------------------------------
@property
def variables(self):
"Return the variables"
try:
return self._variables
except AttributeError:
print("ENTRIES:", self.find_entries("config variables",
source='',
severity='debug'))
print("ENTRIES:", self.find_entries("config variables",
source='macros'))
entry = self.find_entry("config variables",
source='macros',
severity='debug')
if entry and entry.payload:
payload = json.loads(entry.payload)
self._variables = SuperDict(payload)
else:
# print(entry)
# raise ValueError("Cannot find variables")
self._variables = {}
return self._variables
@property
def macros(self):
"Return the macros"
try:
return self._macros
except AttributeError:
entry = self.find_entry("config macros",
source='macros',
severity='debug')
if entry and entry.payload:
self._macros = SuperDict(json.loads(entry.payload))
else:
# print(entry)
# raise ValueError("Cannot find macros")
self._macros = {}
return self._macros
@property
def filters(self):
"Return the filters"
try:
return self._filters
except AttributeError:
entry = self.find_entry("config filters",
source='macros',
severity='debug')
if entry and entry.payload:
self._filters = SuperDict(json.loads(entry.payload))
else:
# print(entry)
# raise ValueError("Cannot find filters")
self._filters = {}
return self._filters
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1582441066.0
mkdocs_macros_plugin-1.4.0/test/main_sample.py 0000644 0000765 0000024 00000002375 13624421152 021140 0 ustar 00laurent staff # --------------------------------------------
# This is a test file and example of how functions file should be defined
# By default it should be called 'main.py'
# Or in a file of a properly defined python package called 'main'.
# It is actually used by module_reader.py
# --------------------------------------------
def define_env(env):
"""
This is the hook for declaring variables, macros and filters (new form)
"""
env.variables['baz'] = "John Doe"
@env.macro
def bar(x):
return (2.3 * x) + 7
# If you wish, you can declare a macro with a different name:
def f(x):
return x * x
f = env.macro(f, 'barbaz')
# define a filter
@env.filter
def reverse(x):
"Reverse a string (and uppercase)"
return x.upper()[::-1]
def declare_variables(variables, macro):
"""
This is the hook for the functions (OLD FORM)
Prefer define_env
- variables: the dictionary that contains the variables
- macro: a decorator function, to declare a macro.
"""
variables['baz'] = "John Doe"
@macro
def bar(x):
return (2.3 * x) + 7
# If you wish, you can declare a macro with a different name:
def f(x):
return x * x
f = macro(f, 'barbaz')
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4418387
mkdocs_macros_plugin-1.4.0/test/missing_macros/ 0000755 0000765 0000024 00000000000 15064255234 021315 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/test/missing_macros/__init__.py 0000644 0000765 0000024 00000000124 15064255224 023422 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/test/missing_macros/test_site.py 0000644 0000765 0000024 00000001006 15064255224 023666 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
from mkdocs_test import DocProject
def test_build():
project = DocProject(".")
# did not fail
print("building website...")
build_result = project.build(strict=True)
result = build_result.stderr
print("Result:", result)
# fails, declaring that the pluglet exists and must be installed.
assert build_result.returncode != 0 # failure
assert "pluglet" in result
assert "pip install" in result
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4432063
mkdocs_macros_plugin-1.4.0/test/module/ 0000755 0000765 0000024 00000000000 15064255234 017565 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1728479129.0
mkdocs_macros_plugin-1.4.0/test/module/__init__.py 0000644 0000765 0000024 00000000124 14701477631 021677 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1754926128.0
mkdocs_macros_plugin-1.4.0/test/module/main.py 0000644 0000765 0000024 00000004046 15046406060 021062 0 ustar 00laurent staff import os
SIGNATURE = 'MAIN'
def define_env(env):
"""
This is the hook for the functions (new form)
"""
# activate trace
chatter = env.start_chatting(SIGNATURE)
env.macros.cwd = os.getcwd()
# use dot notat ion for adding
env.macros.baz = env.macros.fix_url('foo')
@env.macro
def include_file(filename, start_line=0, end_line=None):
"""
Include a file, optionally indicating start_line and end_line
(start counting from 0)
The path is relative to the top directory of the documentation
project.
"""
chatter("Including:", filename)
full_filename = os.path.join(env.project_dir, filename)
with open(full_filename, 'r') as f:
lines = f.readlines()
line_range = lines[start_line:end_line]
return '\n'.join(line_range)
@env.macro
def doc_env():
"Document the environment"
return {name: getattr(env, name)
for name in dir(env) if not
(name.startswith('_') or name.startswith('register'))}
# Optional: a special function for making relative urls point to root
fix_url = env.macros.fix_url
@env.macro
def button(label, url):
"Add a button"
chatter("Display a button:", label, url)
url = fix_url(url)
HTML = """%s"""
return HTML % (url, label)
env.variables.special_docs_dir = env.variables.config['docs_dir']
def on_pre_page_macros(env):
"Before macros are executed"
footer = "\n##Added Footer (Pre-macro)\nBuild hour is {{ now() }}"
env.markdown += footer
def on_post_page_macros(env):
"After macros were executed"
# This will add a (Markdown or HTML) footer
footer = '\n'.join(
['', '##Added Footer (Post-macro)', 'Name of the page is _%s_' % env.page.title])
env.markdown += footer
def on_post_build(env):
"Post build action"
# activate trace
chatter = env.start_chatting(SIGNATURE)
chatter("This means `on_post_build(env)` works")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1754926128.0
mkdocs_macros_plugin-1.4.0/test/module/test_site.py 0000644 0000765 0000024 00000007317 15046406060 022145 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
import re
from mkdocs_test.common import find_after
from test.fixture import MacrosDocProject
CURRENT_project = '.'
def test_pages():
project = MacrosDocProject(CURRENT_project)
project.build()
# did not fail
assert not project.build_result.returncode
# ----------------
# Check that the chatter works
# ----------------
entries = project.find_entries(source='main')
assert len(entries) > 0
# the post-built worked:
assert project.find_entry(source='main', title='post_build')
# ----------------
# First page
# ----------------
page = project.get_page('index')
assert page.is_markdown_rendered()
VARIABLE_NAME = 'unit_price'
# it is defined in the config file (extra)
assert VARIABLE_NAME in project.config.extra
price = project.config.extra.unit_price
# check the page meta
# those meta are not in the config file
meta = page.meta
assert 'user' in meta
assert 'bottles' in meta
assert 'announcement' in meta
assert meta.user == 'Joe'
assert page.find_text(meta.user, header='Installed', header_level=4)
assert page.find_text(meta.announcement, header='Accessing meta')
assert page.find_text(meta.bottles.lemonade, header='Dot notation')
assert not page.find_text(meta.user * 2, header='Macro') # negative test
assert 'bottles' not in project.config.extra
assert 'bottles' not in project.variables
# check that the `greeting` variable is rendered:
assert VARIABLE_NAME in project.variables
assert f"{price} euros" in page.markdown
assert f"{project.macros_plugin.include_dir}" in page.markdown
# check that both on_pre/post_page_macro() worked
assert "Added Footer (Pre-macro)" in page.markdown, f"Not in {page.markdown}"
assert page.find_text(r'is \d{4}-\d{2}-\d{2}', header='Pre-macro')
assert "Added Footer (Post-macro)" in page.markdown
assert find_after(page.plain_text, 'name of the page', 'home')
assert page.find_text('Home', header='Post-macro')
# ----------------
# Environment page
# ----------------
page = project.get_page('environment')
# read a few things that are in the tables
assert page.find_text('unit_price = 50', header='General list')
# there are two headers containing 'Macros':
assert page.find_text('say_hello', header='Macros$')
# test the `include_file()` method (used for the mkdocs.yaml file)
HEADER = r"^mkdocs.*portion"
assert page.find_text('site_name:', header=HEADER)
assert page.find_text('name: material', header=HEADER)
assert not page.find_text('foobar 417', header=HEADER) # negative control
# ----------------
# Literal page
# ----------------
page = project.get_page('literal')
# instruction not to render:
assert page.meta.render_macros == False
assert page.is_markdown_rendered() == False, f"Target: {page.markdown}, \nSource:{page.source_page.markdown}"
# Latex is not interpreted:
latex = re.escape(r"\begin{tabular}{|ccc|}")
assert page.find_text(latex, header='Offending Latex')
# Footer is processed (but not rendered)
assert page.find_text(r'now()', header='Pre-macro')
assert page.find_text('Not interpreted', header='Post-macro')
def test_strict():
"This project must fail"
project = MacrosDocProject(CURRENT_project)
# it must fail with the --strict option,
# because the second page contains an error
project.build(strict=True)
assert not project.build_result.returncode
warning = project.find_entry("Macro Rendering",
severity='warning')
assert not warning, "Warning found, shouldn't!"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4358535
mkdocs_macros_plugin-1.4.0/test/module_dir/ 0000755 0000765 0000024 00000000000 15064255234 020423 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4436154
mkdocs_macros_plugin-1.4.0/test/module_dir/mymodule/ 0000755 0000765 0000024 00000000000 15064255234 022256 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1624090327.0
mkdocs_macros_plugin-1.4.0/test/module_dir/mymodule/__init__.py 0000644 0000765 0000024 00000001255 14063323327 024367 0 ustar 00laurent staff import os
def define_env(env):
"""
This is the hook for the functions (new form)
"""
env.macros.cwd = os.getcwd()
# use dot notation for adding
env.macros.baz = env.macros.fix_url('foo')
# Optional: a special function for making relative urls point to root
fix_url = env.macros.fix_url
@env.macro
def button(label, url):
"Add a button"
url = fix_url(url)
HTML = """%s"""
return HTML % (url, label)
env.variables.special_docs_dir = env.variables.config['docs_dir']
@env.macro
def show_nav():
"Show the navigation"
return env.conf['nav'] ././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.444165
mkdocs_macros_plugin-1.4.0/test/new_syntax/ 0000755 0000765 0000024 00000000000 15064255234 020477 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1584876391.0
mkdocs_macros_plugin-1.4.0/test/new_syntax/main.py 0000644 0000765 0000024 00000000212 13635645547 022004 0 ustar 00laurent staff import os
def define_env(env):
"""
This is the hook for the functions (new form)
"""
env.variables['cwd'] = os.getcwd()
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.444536
mkdocs_macros_plugin-1.4.0/test/no_module/ 0000755 0000765 0000024 00000000000 15064255234 020261 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701149609.0
mkdocs_macros_plugin-1.4.0/test/no_module/main.py 0000644 0000765 0000024 00000000222 14531275651 021556 0 ustar 00laurent staff # there is no standard function here
# the build will fail and spit the list of possible functions that must be here
def foo(x):
return x + 5 ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4454806
mkdocs_macros_plugin-1.4.0/test/null/ 0000755 0000765 0000024 00000000000 15064255234 017252 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1727505832.0
mkdocs_macros_plugin-1.4.0/test/null/__init__.py 0000644 0000765 0000024 00000000124 14675722650 021370 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1754926128.0
mkdocs_macros_plugin-1.4.0/test/null/test_site.py 0000644 0000765 0000024 00000002171 15046406060 021623 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
import test
from test.fixture import MacrosDocProject
def test_pages():
PROJECT = MacrosDocProject()
build_result = PROJECT.build(strict=False)
# did not fail
return_code = PROJECT.build_result.returncode
assert not return_code, "Failed when it should not"
# ----------------
# First page
# ----------------
page = PROJECT.get_page('index')
print("Has error:", page.has_error)
assert not page.has_error()
ERROR_MSG = f"Is rendered!:\n{page.markdown}\n---SOURCE:\n{page.source.markdown}\n---"
assert not page.is_markdown_rendered(), ERROR_MSG
# ----------------
# Second page
# ----------------
# there is intentionally an error (`foo` does not exist)
page = PROJECT.get_page('second')
assert not page.is_markdown_rendered()
def test_strict():
"This project must fail"
PROJECT = MacrosDocProject()
# it must not fail with the --strict option,
PROJECT.build(strict=True)
assert not PROJECT.build_result.returncode, "Failed when it should not"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4464214
mkdocs_macros_plugin-1.4.0/test/opt_in/ 0000755 0000765 0000024 00000000000 15064255234 017570 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1727536302.0
mkdocs_macros_plugin-1.4.0/test/opt_in/__init__.py 0000644 0000765 0000024 00000000124 14676016256 021705 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4361997
mkdocs_macros_plugin-1.4.0/test/opt_in/__pycache__/ 0000755 0000765 0000024 00000000000 15064255234 022000 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4468963
mkdocs_macros_plugin-1.4.0/test/opt_in/__pycache__/new_syntax/ 0000755 0000765 0000024 00000000000 15064255234 024177 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1576234638.0
mkdocs_macros_plugin-1.4.0/test/opt_in/__pycache__/new_syntax/main.py 0000644 0000765 0000024 00000000212 13574667216 025503 0 ustar 00laurent staff import os
def define_env(env):
"""
This is the hook for the functions (new form)
"""
env.variables['cwd'] = os.getcwd()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1730141314.0
mkdocs_macros_plugin-1.4.0/test/opt_in/test_site.py 0000644 0000765 0000024 00000002501 14707756202 022147 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
from test.fixture import MacrosDocProject
CURRENT_project = '.'
def test_opt_in():
project = MacrosDocProject(CURRENT_project)
project.build()
# did not fail
assert not project.build_result.returncode
# ---------------------------
# which pages are rendered?
# ---------------------------
# test the config:
macros = project.macros_plugin
assert macros.render_by_default == False
page = project.get_page('render_this_one')
assert page.title == "Render (by name)"
assert page.is_markdown_rendered()
assert page.find_text(page.meta.signal), f"Did not find signal '{page.meta.signal}'"
print([page.source.markdown for page in project.pages.values()])
page2 = project.get_page('rendered/noname')
assert page2.file.src_uri == 'rendered/noname.md', f"is: {page2.file.src_uri}"
assert page2.find_text("0: Hello world")
assert page2.is_markdown_rendered()
assert not project.get_page('not_rendered/noname').is_markdown_rendered()
# exception in the meta:
exception_page = project.get_page('rendered/exception')
assert exception_page.meta.render_macros == False
assert not exception_page.is_markdown_rendered()
assert exception_page.find_text('macros_info')
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4479165
mkdocs_macros_plugin-1.4.0/test/opt_out/ 0000755 0000765 0000024 00000000000 15064255234 017771 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1727549724.0
mkdocs_macros_plugin-1.4.0/test/opt_out/__init__.py 0000644 0000765 0000024 00000000124 14676050434 022102 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.436351
mkdocs_macros_plugin-1.4.0/test/opt_out/__pycache__/ 0000755 0000765 0000024 00000000000 15064255234 022201 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.448469
mkdocs_macros_plugin-1.4.0/test/opt_out/__pycache__/new_syntax/ 0000755 0000765 0000024 00000000000 15064255234 024400 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1576234638.0
mkdocs_macros_plugin-1.4.0/test/opt_out/__pycache__/new_syntax/main.py 0000644 0000765 0000024 00000000212 13574667216 025704 0 ustar 00laurent staff import os
def define_env(env):
"""
This is the hook for the functions (new form)
"""
env.variables['cwd'] = os.getcwd()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1728477956.0
mkdocs_macros_plugin-1.4.0/test/opt_out/test_site.py 0000644 0000765 0000024 00000002064 14701475404 022350 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
from mkdocs_test.common import h1
from test.fixture import MacrosDocProject
def test_opt_in():
project = MacrosDocProject('.')
project.build()
# did not fail
assert not project.build_result.returncode
# ---------------------------
# which pages are rendered?
# ---------------------------
# test the config (this is the default anyway)
macros = project.macros_plugin
assert macros.render_by_default == True
h1("Pages")
print("Pages:", len(project.pages))
for page in project.pages.values():
print(page.file.src_uri, page.title)
print("---")
# opt-out:
page = project.get_page('index')
assert page.meta.render_macros == False
assert not page.is_markdown_rendered()
assert "macros_info" in page.markdown
# Normal:
page = project.get_page('rendered')
assert page
assert "render_macros" not in page.meta
assert page.is_markdown_rendered()
assert page.meta.signal in page.markdown
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.449671
mkdocs_macros_plugin-1.4.0/test/plugin_d2/ 0000755 0000765 0000024 00000000000 15064255234 020163 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1729279127.0
mkdocs_macros_plugin-1.4.0/test/plugin_d2/__init__.py 0000644 0000765 0000024 00000000124 14704532227 022271 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1729280245.0
mkdocs_macros_plugin-1.4.0/test/plugin_d2/test_t2.py 0000644 0000765 0000024 00000002135 14704534365 022127 0 ustar 00laurent staff """
Testing the d2 project
There was an incompatibility:
Error: The current file is not set for the '!relative' tag. It cannot be used in this context; the intended usage is within `markdown_extensions`.
see https://github.com/fralau/mkdocs-macros-plugin/issues/249
Requires d2
(C) Laurent Franceschetti 2024
"""
REQUIRED = "d2"
import pytest
import subprocess
def is_d2_installed():
try:
subprocess.run(["brew", "list", REQUIRED], check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return True
except subprocess.CalledProcessError:
return False
import test
from test.fixture import MacrosDocProject
@pytest.mark.skipif(not is_d2_installed(), reason="d2 is not installed")
def test_d2():
"""
This test will run only if d2 library is installed;
otherwise the d2 plugin will not run
https://d2lang.com/tour/install/
"""
project = MacrosDocProject()
project.build(strict=False)
# did not fail
print(project.build_result.stderr)
assert not project.build_result.returncode, "Failed when it should not" ././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1758550684.450639
mkdocs_macros_plugin-1.4.0/test/register_macros/ 0000755 0000765 0000024 00000000000 15064255234 021470 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1728498450.0
mkdocs_macros_plugin-1.4.0/test/register_macros/__init__.py 0000644 0000765 0000024 00000000000 14701545422 023565 0 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1728534534.0
mkdocs_macros_plugin-1.4.0/test/register_macros/hooks.py 0000644 0000765 0000024 00000002425 14701654006 023165 0 ustar 00laurent staff def foo(x:int, y:str):
"First macro"
return f"{x} and {y}"
def bar(x:int, y:int):
"Second macro"
return x + y
def scramble(s:str, length:int=None):
"""
Dummy filter to reverse the string and swap the case of each character.
Usage in Markdown page:
{{ "Hello world" | scramble }} -> Dlrow Olleh
{{ "Hello world" | scramble(6) }} -> Dlrow
"""
# Split the phrase into words
words = s.split()
# Reverse each word and then reverse the order of the words
reversed_words = [word[::-1].capitalize() for word in words][::-1]
# Join the reversed words to form the new phrase
new_phrase = ' '.join(reversed_words)
if length:
new_phrase = new_phrase[length]
return new_phrase
MY_FUNCTIONS = {"foo": foo, "bar": bar}
MY_VARIABLES = {"x1": 5, "x2": 'hello world'}
MY_FILTERS = {"scramble": scramble}
def on_config(config, **kwargs):
"Add the functions variables and filters to the mix"
# get MkdocsMacros plugin, but only if present
macros_plugin = config.plugins.get("macros")
if macros_plugin:
macros_plugin.register_macros(MY_FUNCTIONS)
macros_plugin.register_variables(MY_VARIABLES)
macros_plugin.register_filters(MY_FILTERS)
else:
raise SystemError("Cannot find macros plugin!")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/test/register_macros/test_doc.py 0000644 0000765 0000024 00000004321 15064255224 023645 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
from test.fixture import MacrosDocProject
from .hooks import MY_VARIABLES, MY_FUNCTIONS, MY_FILTERS, bar, scramble
project = None
def test_build_project():
global project
project = MacrosDocProject(".")
build_result = project.build(strict=True)
# did not fail
return_code = project.build_result.returncode
assert not return_code, f"Build returned with {return_code} {build_result.args})"
print("Build successful")
print("Variables?")
# entry = project.find_entries("config variables", severity="debug")
# print(entry)
def test_variables():
# check the presence of variables in the environment
print("Variables:", list(project.variables.keys()))
for variable in MY_VARIABLES:
print(f"{variable}...")
assert variable in project.variables
print(f"...{project.variables[variable]}")
def test_macros_and_filters():
print("Macros:", project.macros)
print("Macros:", list(project.macros.keys()))
for macro in MY_FUNCTIONS:
assert macro in project.macros
print(f"{macro}: {project.macros[macro]}")
print("Filters:", list(project.filters.keys()))
for filter in MY_FILTERS:
assert filter in project.filters
print(f"{filter}: {project.filters[filter]}")
def test_pages():
# ----------------
# First page
# ----------------
page = project.get_page('index')
assert page.is_markdown_rendered()
# variable
value = MY_VARIABLES['x2']
print(f"Check if x2 ('{value}') is present")
assert page.find_text(value, header="Variables")
# macro
print("Check macro: bar")
assert page.find_text(bar(2, 5), header="Macros")
# filter
message = page.meta.message
result = scramble(message)
print(f"Check filter: scramble('{message}') --> '{result}'")
assert page.find_text(result, header="Filters")
# ----------------
# Second page
# ----------------
# there is intentionally an error (`foo` does not exist)
page = project.get_page('second')
assert 'foo' not in project.config.extra
assert page.is_markdown_rendered()
assert not page.has_error()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1758550684.4523685
mkdocs_macros_plugin-1.4.0/test/simple/ 0000755 0000765 0000024 00000000000 15064255234 017571 5 ustar 00laurent staff ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1727244983.0
mkdocs_macros_plugin-1.4.0/test/simple/__init__.py 0000644 0000765 0000024 00000000124 14674725267 021715 0 ustar 00laurent staff """
This __init__.py file is indispensable for pytest to
recognize its packages.
""" ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1576234638.0
mkdocs_macros_plugin-1.4.0/test/simple/main_old.py 0000644 0000765 0000024 00000000212 13574667216 021733 0 ustar 00laurent staff import os
def define_env(env):
"""
This is the hook for the functions (new form)
"""
env.variables['cwd'] = os.getcwd()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1730978124.0
mkdocs_macros_plugin-1.4.0/test/simple/test_site.py 0000644 0000765 0000024 00000003512 14713120514 022137 0 ustar 00laurent staff """
Testing the project
(C) Laurent Franceschetti 2024
"""
import pytest
from mkdocs_test import DocProject
def test_pages():
project = DocProject(".")
build_result = project.build(strict=False)
# did not fail
return_code = project.build_result.returncode
assert not return_code, f"Build returned with {return_code} {build_result.args})"
# ----------------
# First page
# ----------------
VARIABLE_NAME = 'greeting'
# it is defined in the config file (extra)
assert VARIABLE_NAME in project.config.extra
page = project.get_page('index')
assert page.is_markdown_rendered()
# check that the `greeting` variable (defined under 'extra') is rendered:
variables = project.config.extra
assert VARIABLE_NAME in variables
assert variables.greeting in page.markdown
# test built-in filters (#253)
header = 'built-in filters'
assert page.find_text("result", header)
assert page.find_text("result is: 17.5", header) # abs
assert page.find_text("saying: HELLO WORLD", header) # upper
assert page.find_text("length is: 12", header) # length
# ----------------
# Second page
# ----------------
# there is intentionally an error (`foo` does not exist)
page = project.get_page('second')
assert 'foo' not in project.config.extra
assert page.is_markdown_rendered()
assert page.find_text('Macro Rendering Error')
def test_strict():
"This project must fail"
project = DocProject(".")
# it must fail with the --strict option,
# because the second page contains an error
project.build(strict=True)
assert project.build_result.returncode
warning = project.find_entry("Macro Rendering",
severity='warning')
assert warning, "No warning found"
print(warning)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1758550676.0
mkdocs_macros_plugin-1.4.0/test/test_various.py 0000644 0000765 0000024 00000001571 15064255224 021404 0 ustar 00laurent staff """
Various tests
"""
import pytest
# ----------------------
# is_on_pypi
# ----------------------
from mkdocs_macros.util import is_on_pypi # Replace with actual import path
def test_known_package_exists():
# requires connection
assert is_on_pypi("requests", fail_silently=True) is True
def test_nonexistent_package():
assert is_on_pypi("this_package_does_not_exist_123456", fail_silently=True) is False
def test_network_failure(monkeypatch):
# Simulate network failure by patching requests.get to raise a RequestException
import requests
def mock_get(*args, **kwargs):
raise requests.exceptions.ConnectionError("Simulated network failure")
monkeypatch.setattr(requests, "get", mock_get)
assert is_on_pypi("requests", fail_silently=True) is False
with pytest.raises(RuntimeError):
is_on_pypi("requests", fail_silently=False)