././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/0000755000076500000240000000000014707203644015330 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1708423407.0 mkdocs_macros_plugin-1.3.7/LICENSE.md0000644000076500000240000000215614565074357016751 0ustar00laurentstaff# 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722150399.0 mkdocs_macros_plugin-1.3.7/MANIFEST.in0000644000076500000240000000013714651366777017106 0ustar00laurentstaffinclude README.md include LICENSE.md include mkdocs_macros/*.md include mkdocs_macros/py.typed ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/PKG-INFO0000644000076500000240000001772614707203644016442 0ustar00laurentstaffMetadata-Version: 2.1 Name: mkdocs-macros-plugin Version: 1.3.7 Summary: Unleash the power of MkDocs with macros and variables Author-email: 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 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"
![Mkdocs-Macros](logo.png) # Unleash the power of MkDocs with variables and macros [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Language](https://img.shields.io/github/languages/top/fralau/mkdocs_macros_plugin) ![PyPI](https://img.shields.io/pypi/v/mkdocs-macros-plugin) ![Github](https://img.shields.io/github/v/tag/fralau/mkdocs_macros_plugin?label=github%20tag) ![macros](https://img.shields.io/pypi/dm/mkdocs-macros-plugin) :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: ![macros_info()](macros_info.png) 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). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728067427.0 mkdocs_macros_plugin-1.3.7/README.md0000644000076500000240000001534514700033543016607 0ustar00laurentstaff
![Mkdocs-Macros](logo.png) # Unleash the power of MkDocs with variables and macros [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Language](https://img.shields.io/github/languages/top/fralau/mkdocs_macros_plugin) ![PyPI](https://img.shields.io/pypi/v/mkdocs-macros-plugin) ![Github](https://img.shields.io/github/v/tag/fralau/mkdocs_macros_plugin?label=github%20tag) ![macros](https://img.shields.io/pypi/dm/mkdocs-macros-plugin) :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: ![macros_info()](macros_info.png) 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). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/0000755000076500000240000000000014707203643020153 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728118538.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/__init__.py0000644000076500000240000000036714700177412022267 0ustar00laurentstaff# ------------------- # 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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727106727.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/context.py0000644000076500000240000003103014674307247022216 0ustar00laurentstaff""" 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 %} {% endfor %} {% for row in rows %} {% for item in row %} {% endfor %} {% endfor %}
{{ item }}
{{ item }}
""") 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658502004.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/errors.py0000644000076500000240000000206114266535564022052 0ustar00laurentstaffimport 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722176079.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/macros_info.md0000644000076500000240000000310314651451117022770 0ustar00laurentstaff{# 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 }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729101644.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/plugin.py0000644000076500000240000010442314703777514022040 0ustar00laurentstaff# -------------------------------------------- # 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 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 ( install_package, parse_package, trace, debug, update, import_local_module, format_chatter, LOG, get_log_level, setup_directory, CustomEncoder, # 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 # ------------------------------------------------ @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_ost_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: try: # if absent, install (from pypi) trace("Module '%s' not found, installing (source: '%s')" % (module_name, source_name)) install_package(source_name) # install package raises NameError module = importlib.import_module(module_name) except (NameError, ModuleNotFoundError): raise ModuleNotFoundError("Could not import installed " "module '%s' (missing?)" % module_name, name=module_name) 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 # ---------------------------------- 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 # ---------------------------------- # 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. It is put here, in case some plugin hooks into the config, after the execution of the `on_config()` of this plugin. """ trace("Config variables:", list(self.variables.keys())) debug("Config variables:\n", payload=json.dumps(self.variables, cls=CustomEncoder)) if self.macros: trace("Config macros:", list(self.macros.keys())) debug("Config macros:", payload=json.dumps(self.macros, cls=CustomEncoder)) if self.filters: trace("Config filters:", list(self.filters.keys())) debug("Config filters:", payload=json.dumps(self.filters, cls=CustomEncoder)) 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 "{" in 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722150399.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/py.typed0000644000076500000240000000000014651366777021660 0ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729281269.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros/util.py0000644000076500000240000002135214704536365021514 0ustar00laurentstaff#!/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 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 from collections import UserDict class CustomEncoder(json.JSONEncoder): """ Custom encoder for JSON serialization. Used for debugging purposes. """ def default(self, obj: Any) -> Any: if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, UserDict): # for objects used by MkDocs (config, plugin, etc.s) return dict(obj) elif inspect.isfunction(obj): return f"Function: %s %s" % (inspect.signature(obj), obj.__doc__) try: return super().default(obj) except TypeError: debug(f"json: cannot encode {obj.__class__}") try: return str(obj) except Exception: # in case something happens along the line return f"!Non printable object: {obj.__class__}" # ------------------------------------------ # Packages and modules # ------------------------------------------ def parse_package(package:str): """ Parse a package name if it is in the forme 'foo:bar' then 'foo' is the source, and '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 install_package(package:str): """ Install a package from pip """ try: subprocess.check_call(["pip3", "install", package]) except subprocess.CalledProcessError: raise NameError("Could not install package '%s'" % package) 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/0000755000076500000240000000000014707203643023223 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/PKG-INFO0000644000076500000240000001772614707203643024335 0ustar00laurentstaffMetadata-Version: 2.1 Name: mkdocs-macros-plugin Version: 1.3.7 Summary: Unleash the power of MkDocs with macros and variables Author-email: 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 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"
![Mkdocs-Macros](logo.png) # Unleash the power of MkDocs with variables and macros [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Language](https://img.shields.io/github/languages/top/fralau/mkdocs_macros_plugin) ![PyPI](https://img.shields.io/pypi/v/mkdocs-macros-plugin) ![Github](https://img.shields.io/github/v/tag/fralau/mkdocs_macros_plugin?label=github%20tag) ![macros](https://img.shields.io/pypi/dm/mkdocs-macros-plugin) :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: ![macros_info()](macros_info.png) 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). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/SOURCES.txt0000644000076500000240000000220314707203643025104 0ustar00laurentstaffLICENSE.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/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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/dependency_links.txt0000644000076500000240000000000114707203643027271 0ustar00laurentstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/entry_points.txt0000644000076500000240000000007414707203643026522 0ustar00laurentstaff[mkdocs.plugins] macros = mkdocs_macros.plugin:MacrosPlugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/requires.txt0000644000076500000240000000031414707203643025621 0ustar00laurentstaffhjson jinja2 mkdocs>=0.17 packaging pathspec python-dateutil pyyaml super-collections termcolor [test] mkdocs-include-markdown-plugin mkdocs-macros-test mkdocs-material>=6.2 mkdocs-test mkdocs-d2-plugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/mkdocs_macros_plugin.egg-info/top_level.txt0000644000076500000240000000003714707203643025755 0ustar00laurentstaffdist mkdocs_macros test webdoc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955704.0 mkdocs_macros_plugin-1.3.7/pyproject.toml0000644000076500000240000000255514707203570020251 0ustar00laurentstaff[project] name = "mkdocs-macros-plugin" # This version number is the REFERENCE for the rest of the project, # particularly for update_pypi.sh version = "1.3.7" description = "Unleash the power of MkDocs with macros and variables" readme = "README.md" license = { text = "MIT" } requires-python = ">=3.8" authors = [ { name = "Laurent Franceschetti", email = "info@settlenext.com" }, ] 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", "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" ] [project.entry-points."mkdocs.plugins"] macros = "mkdocs_macros.plugin:MacrosPlugin" [project.urls] Homepage = "https://github.com/fralau/mkdocs_macros_plugin" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/setup.cfg0000644000076500000240000000004614707203644017151 0ustar00laurentstaff[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729138606.0 mkdocs_macros_plugin-1.3.7/setup.py0000644000076500000240000000040114704107656017040 0ustar00laurentstaff""" 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', )././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/0000755000076500000240000000000014707203643016306 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727203983.0 mkdocs_macros_plugin-1.3.7/test/__init__.py0000644000076500000240000000000014674605217020413 0ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728498295.0 mkdocs_macros_plugin-1.3.7/test/fixture.py0000644000076500000240000000626714701545167020365 0ustar00laurentstaff""" 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('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: entry = self.find_entry("config variables", source='macros', severity='debug') if entry and entry.payload: self._variables = SuperDict(json.loads(entry.payload)) else: print(entry) raise ValueError("Cannot find 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") 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") return self._filters ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1582441066.0 mkdocs_macros_plugin-1.3.7/test/main_sample.py0000644000076500000240000000237513624421152021146 0ustar00laurentstaff# -------------------------------------------- # 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/module/0000755000076500000240000000000014707203643017573 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728479129.0 mkdocs_macros_plugin-1.3.7/test/module/__init__.py0000644000076500000240000000012414701477631021705 0ustar00laurentstaff""" This __init__.py file is indispensable for pytest to recognize its packages. """././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727270751.0 mkdocs_macros_plugin-1.3.7/test/module/main.py0000644000076500000240000000404614675007537021105 0ustar00laurentstaffimport 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728479032.0 mkdocs_macros_plugin-1.3.7/test/module/test_site.py0000644000076500000240000000721114701477470022156 0ustar00laurentstaff""" 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(meta.user, header='Installed', header_level=4) assert page.find(meta.announcement, header='Accessing meta') assert page.find(meta.bottles.lemonade, header='Dot notation') assert not page.find(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(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('Home', header='Post-macro') # ---------------- # Environment page # ---------------- page = project.get_page('environment') # read a few things that are in the tables assert page.find('unit_price = 50', header='General list') # there are two headers containing 'Macros': assert page.find('say_hello', header='Macros$') # test the `include_file()` method (used for the mkdocs.yaml file) HEADER = r"^mkdocs.*portion" assert page.find('site_name:', header=HEADER) assert page.find('name: material', header=HEADER) assert not page.find('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(latex, header='Offending Latex') # Footer is processed (but not rendered) assert page.find(r'now()', header='Pre-macro') assert page.find('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!" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/module_dir/0000755000076500000240000000000014707203643020431 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/module_dir/mymodule/0000755000076500000240000000000014707203643022264 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1624090327.0 mkdocs_macros_plugin-1.3.7/test/module_dir/mymodule/__init__.py0000644000076500000240000000125514063323327024375 0ustar00laurentstaffimport 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']././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/new_syntax/0000755000076500000240000000000014707203643020505 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1584876391.0 mkdocs_macros_plugin-1.3.7/test/new_syntax/main.py0000644000076500000240000000021213635645547022012 0ustar00laurentstaffimport os def define_env(env): """ This is the hook for the functions (new form) """ env.variables['cwd'] = os.getcwd() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/no_module/0000755000076500000240000000000014707203643020267 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701149609.0 mkdocs_macros_plugin-1.3.7/test/no_module/main.py0000644000076500000240000000022214531275651021564 0ustar00laurentstaff# 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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/null/0000755000076500000240000000000014707203644017261 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727505832.0 mkdocs_macros_plugin-1.3.7/test/null/__init__.py0000644000076500000240000000012414675722650021376 0ustar00laurentstaff""" This __init__.py file is indispensable for pytest to recognize its packages. """././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728464580.0 mkdocs_macros_plugin-1.3.7/test/null/test_site.py0000644000076500000240000000217114701443304021630 0ustar00laurentstaff""" 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" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/opt_in/0000755000076500000240000000000014707203644017577 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727536302.0 mkdocs_macros_plugin-1.3.7/test/opt_in/__init__.py0000644000076500000240000000012414676016256021713 0ustar00laurentstaff""" This __init__.py file is indispensable for pytest to recognize its packages. """././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/opt_in/__pycache__/0000755000076500000240000000000014707203643022006 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/opt_in/__pycache__/new_syntax/0000755000076500000240000000000014707203644024206 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576234638.0 mkdocs_macros_plugin-1.3.7/test/opt_in/__pycache__/new_syntax/main.py0000644000076500000240000000021213574667216025511 0ustar00laurentstaffimport os def define_env(env): """ This is the hook for the functions (new form) """ env.variables['cwd'] = os.getcwd() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728478934.0 mkdocs_macros_plugin-1.3.7/test/opt_in/test_site.py0000644000076500000240000000246214701477326022164 0ustar00laurentstaff""" 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(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("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('macros_info') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/opt_out/0000755000076500000240000000000014707203644020000 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727549724.0 mkdocs_macros_plugin-1.3.7/test/opt_out/__init__.py0000644000076500000240000000012414676050434022110 0ustar00laurentstaff""" This __init__.py file is indispensable for pytest to recognize its packages. """././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955747.0 mkdocs_macros_plugin-1.3.7/test/opt_out/__pycache__/0000755000076500000240000000000014707203643022207 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/opt_out/__pycache__/new_syntax/0000755000076500000240000000000014707203644024407 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576234638.0 mkdocs_macros_plugin-1.3.7/test/opt_out/__pycache__/new_syntax/main.py0000644000076500000240000000021213574667216025712 0ustar00laurentstaffimport os def define_env(env): """ This is the hook for the functions (new form) """ env.variables['cwd'] = os.getcwd() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728477956.0 mkdocs_macros_plugin-1.3.7/test/opt_out/test_site.py0000644000076500000240000000206414701475404022356 0ustar00laurentstaff""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/plugin_d2/0000755000076500000240000000000014707203644020172 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729279127.0 mkdocs_macros_plugin-1.3.7/test/plugin_d2/__init__.py0000644000076500000240000000012414704532227022277 0ustar00laurentstaff""" This __init__.py file is indispensable for pytest to recognize its packages. """././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729280245.0 mkdocs_macros_plugin-1.3.7/test/plugin_d2/test_t2.py0000644000076500000240000000213514704534365022135 0ustar00laurentstaff""" 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" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/register_macros/0000755000076500000240000000000014707203644021477 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728498450.0 mkdocs_macros_plugin-1.3.7/test/register_macros/__init__.py0000644000076500000240000000000014701545422023573 0ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728534534.0 mkdocs_macros_plugin-1.3.7/test/register_macros/hooks.py0000644000076500000240000000242514701654006023173 0ustar00laurentstaffdef 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!") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728498530.0 mkdocs_macros_plugin-1.3.7/test/register_macros/test_doc.py0000644000076500000240000000357414701545542023666 0ustar00laurentstaff""" 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 def test_pages(): 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})" # check the presence of variables in the environment print("Variables:", list(project.variables.keys())) for variable in MY_VARIABLES: assert variable in project.variables print(f"{variable}: {project.variables[variable]}") 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]}") # ---------------- # 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(value, header="Variables") # macro print("Check macro: bar") assert page.find(bar(2, 5), header="Macros") # filter message = page.meta.message result = scramble(message) print(f"Check filter: scramble('{message}') --> '{result}'") assert page.find(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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1729955748.0 mkdocs_macros_plugin-1.3.7/test/simple/0000755000076500000240000000000014707203644017600 5ustar00laurentstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727244983.0 mkdocs_macros_plugin-1.3.7/test/simple/__init__.py0000644000076500000240000000012414674725267021723 0ustar00laurentstaff""" This __init__.py file is indispensable for pytest to recognize its packages. """././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600200460.0 mkdocs_macros_plugin-1.3.7/test/simple/main_old.py0000644000076500000240000000021213730217414021722 0ustar00laurentstaffimport os def define_env(env): """ This is the hook for the functions (new form) """ env.variables['cwd'] = os.getcwd() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728492356.0 mkdocs_macros_plugin-1.3.7/test/simple/test_site.py0000644000076500000240000000303014701531504022142 0ustar00laurentstaff""" 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 # ---------------- # 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('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)