diff --git a/mkdocs_macros/plugin.py b/mkdocs_macros/plugin.py index b7e841c..f9eea42 100644 --- a/mkdocs_macros/plugin.py +++ b/mkdocs_macros/plugin.py @@ -43,8 +43,7 @@ # The default name of the Python module: DEFAULT_MODULE_NAME = 'main' # main.py -# the directory where the rendered macros must go -RENDERED_MACROS_DIRNAME = '__docs_macros_rendered' + @@ -295,20 +294,7 @@ def reverse(x): - @property - def rendered_macros_dir(self): - """ - The directory, beside the docs_dir, that contains - the rendered pages from the macros. - """ - try: - r = self._rendered_macros_dir - except AttributeError: - raise AttributeError("Rendered macros directory is undefined") - if not os.path.isdir(self._rendered_macros_dir): - raise FileNotFoundError("Rendered macros directory is defined " - "but does not exists") - return r + # ------------------------------------------------ @@ -378,6 +364,7 @@ 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 @@ -393,6 +380,7 @@ 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) @@ -407,6 +395,7 @@ 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 @@ -723,6 +712,7 @@ def on_config(self, config): From the configuration file, builds a Jinj2 environment with variables, functions and filters. """ + trace("Configuring the macros environment...") # WARNING: this is not the config argument: trace("Macros arguments\n", self.config) # define the variables and macros as dictionaries @@ -770,18 +760,7 @@ def on_config(self, config): register_items('macro' , self.macros , self._add_macros ) register_items('filter' , self.filters , self._add_filters ) - # Provide information: - 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)) + # if len(extra): # trace("Extra variables (config file):", list(extra.keys())) # debug("Content of extra variables (config file):\n", dict(extra)) @@ -856,16 +835,26 @@ def on_config(self, config): # update environment with the custom filters: self.env.filters.update(self.filters) - # ------------------- - # Setup the markdown (rendered) directory - # ------------------- - docs_dir = config['docs_dir'] - abs_docs_dir = os.path.abspath(docs_dir) - # recreate only if debug (otherewise delete): - recreate = get_log_level('DEBUG') - self._rendered_macros_dir = setup_directory(abs_docs_dir, - RENDERED_MACROS_DIRNAME, - recreate=recreate) + trace("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): """ diff --git a/mkdocs_macros/util.py b/mkdocs_macros/util.py index 1ef5a01..09f48dc 100644 --- a/mkdocs_macros/util.py +++ b/mkdocs_macros/util.py @@ -136,7 +136,7 @@ def default(self, obj: Any) -> Any: try: return super().default(obj) except TypeError: - print(f"CANNOT INTERPRET {obj.__class__}") + debug(f"json: cannot encode {obj.__class__}") return str(obj) diff --git a/test/fixture.py b/test/fixture.py index f07d241..42b355f 100644 --- a/test/fixture.py +++ b/test/fixture.py @@ -5,6 +5,11 @@ """ import warnings +import json +import subprocess + + +from super_collections import SuperDict from mkdocs_test import DocProject, MkDocsPage @@ -25,18 +30,75 @@ def is_rendered(self): 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 variables(self): - "Alias for config.extra" - return self.config.extra + @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 diff --git a/test/register_macros/__init__.py b/test/register_macros/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/register_macros/docs/index.md b/test/register_macros/docs/index.md new file mode 100644 index 0000000..e156373 --- /dev/null +++ b/test/register_macros/docs/index.md @@ -0,0 +1,20 @@ +--- +message: Vive Zorglub +--- + +# Main Page + +This project contains a registered macro + +## Variables +I want to write 'foo': {{ foo }} + +x2 is also present: {{ x2 }} + +## Macros + +Calculation: {{ bar(2, 5) }} + +## Filters + +I want to scramble '{{ message }}': {{ message | scramble }} \ No newline at end of file diff --git a/test/register_macros/docs/second.md b/test/register_macros/docs/second.md new file mode 100644 index 0000000..b157c52 --- /dev/null +++ b/test/register_macros/docs/second.md @@ -0,0 +1,8 @@ +# Second page + +This page shows the information on variables, +to understand, how the functions, variables and filters +have been registered. + + +{{ macros_info() }} diff --git a/test/register_macros/hooks.py b/test/register_macros/hooks.py new file mode 100644 index 0000000..0c8378f --- /dev/null +++ b/test/register_macros/hooks.py @@ -0,0 +1,44 @@ +def foo(x:int, y:str): + "First macro" + return f"{x} and {y}" + +def bar(x:int, y:int): + "Second macro" + return x + y + +def scramble(s:str, length:int=None): + """ + Dummy filter to reverse the string and swap the case of each character. + + Usage in Markdown page: + + {{ "Hello world" | scramble }} -> Dlrow Olleh + {{ "Hello world" | scramble(6) }} -> Dlrow + """ + # Split the phrase into words + words = s.split() + # Reverse each word and then reverse the order of the words + reversed_words = [word[::-1].capitalize() for word in words][::-1] + # Join the reversed words to form the new phrase + new_phrase = ' '.join(reversed_words) + if length: + new_phrase = new_phrase[length] + return new_phrase + + +MY_FUNCTIONS = {"foo": foo, "bar": bar} +MY_VARIABLES = {"x1": 5, "x2": 'hello world'} +MY_FILTERS = {"scramble": scramble} + + +def on_config(config, **kwargs): + "Add the functions variables and filters to the mix" + # get MkdocsMacros plugin, but only if present + macros_plugin = config.plugins.get("macros") + if macros_plugin: + macros_plugin.register_macros(MY_FUNCTIONS) + macros_plugin.register_variables(MY_VARIABLES) + macros_plugin.register_filters(MY_FILTERS) + else: + raise SystemError("Cannot find macros plugin!") + diff --git a/test/register_macros/mkdocs.yml b/test/register_macros/mkdocs.yml new file mode 100644 index 0000000..850c193 --- /dev/null +++ b/test/register_macros/mkdocs.yml @@ -0,0 +1,19 @@ +site_name: Testing the hooks +theme: readthedocs + +nav: + - Home: index.md + - Next page: second.md + +hooks: + # Mkdocs hook for testing the Mkdocs-Macros hook + - hooks.py + +plugins: + - search + - macros + - test + +extra: + greeting: Hello World! + diff --git a/test/register_macros/test_doc.py b/test/register_macros/test_doc.py new file mode 100644 index 0000000..9ec2c77 --- /dev/null +++ b/test/register_macros/test_doc.py @@ -0,0 +1,70 @@ +""" +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() + diff --git a/test/simple/test_site.py b/test/simple/test_site.py index c1bfbe6..c4b9cf7 100644 --- a/test/simple/test_site.py +++ b/test/simple/test_site.py @@ -29,7 +29,7 @@ def test_pages(): assert VARIABLE_NAME in project.config.extra page = project.get_page('index') - assert page.is_markdown_rendered + assert page.is_markdown_rendered() # check that the `greeting` variable (defined under 'extra') is rendered: variables = project.config.extra @@ -43,7 +43,7 @@ def test_pages(): # 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.is_markdown_rendered() assert page.find('Macro Rendering Error') def test_strict(): diff --git a/webdoc/docs/macros.md b/webdoc/docs/macros.md index 5ef442b..7b13215 100644 --- a/webdoc/docs/macros.md +++ b/webdoc/docs/macros.md @@ -344,9 +344,9 @@ def define_env(env): ``` +## Notes on Modules - -## A caution about security +### A caution about security !!! Warning @@ -368,7 +368,7 @@ web pages** (for business applications). -## What you can and can't do with `define_env()` +### What you can and can't do with `define_env()` The fact is that you **cannot** actually access page information in the `define_env()` function, since @@ -403,3 +403,38 @@ At that point, **you don't have access to specific pages** Whatever you do in that way, is likely to be branded **black magic**. + +### Hook scripts (standard) versus MkDocs-Macros Modules + +#### Similarities +**Hook scripts** (offered as standard by MkDocs) +and **[modules](macros.md)** provided by MkDocs-Macros are python programs +that operate on the same principle : they use **hooks**, +which are special predefined +functions that will be called at specific point +of the code, so that you can customize the behavior of the software +according to your needs. + +Those scripts take a special object as an argument: + +1. `config` for MkDocs hook scripts +2. `env` for Mkdocs-Macros modules + + +#### Differences +Their purpose is, however, very different. To explain this +in a simple way: + +1. You can think of a standard hook script + as built over a **barebone plugin** + that doesn't do anything more than MkDocs, + except providing those hooks. + +2. An MkDocs-Macros module is exploiting the + full power of the **MkDocs-Macros plugin**, which relies + on its Jinja2 templating engine and the manipulation + of macros, variables and filters. + +A standard hook script is usually called `hooks.py`, while +an Mkdocs-Macros module is called `main.py` by default, though you can +change these names if you wish. \ No newline at end of file diff --git a/webdoc/docs/registration.md b/webdoc/docs/registration.md index 30f5b1c..9fb9e60 100644 --- a/webdoc/docs/registration.md +++ b/webdoc/docs/registration.md @@ -114,6 +114,8 @@ exported by the `MacrosPlugin` class (itself based on `BasePlugin`). ### Practice +#### Using a plugin + You want to register those macros/filters/variables at the `on_config()` method of your plugin, providing the MkDocs-Macros plugin is declared. @@ -167,5 +169,58 @@ class MyPlugin(BasePlugin): macros_plugin.register_filters(MY_VARIABLES) ``` +#### Using an MkDocs script + +_As of MkDocs version 1.4_ + +Writing a **plugin** requires a lot of work. +If the purpose is to solve a specific +problem for a specific documentation project, +then it may not be worth the effort. + + +Fortunately, we now have another solution for implementing +these event-processing +functions (`on_config()`, `on_page_markdown()`, etc.). + +This can be done through Python **hook scripts** written for the specific +documentation project, by using +MkDocs [hook](https://www.mkdocs.org/user-guide/configuration/#hooks) facility. Those hooks are simply functions that implement +these events. + +The first thing is to declare the script in the config file, +relative to the source directory of the project (here `hooks.py`): + +```yaml +site_name: Testing the hooks + +nav: + - ... + +hooks: + # Mkdocs hook for testing the Mkdocs-Macros hook + - hooks.py + +plugins: + - search + - macros +``` + +The only difference with the script above, is that `on_config()` +is declared as a function, instead as a method of the plugin. + +Otherwise, the code is identical: + +```python +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") + macros_plugin.register_macros(MY_FUNCTIONS) + macros_plugin.register_variables(MY_VARIABLES) + macros_plugin.register_filters(MY_FILTERS) +``` +!!! Tip "Difference between MkDocs hook scripts and MkDocs modules?" + See the [explanation](macros.md#hook-scripts-standard-versus-mkdocs-macros-modules). \ No newline at end of file