fralau / mkdocs-macros-plugin

Create richer and more beautiful pages in MkDocs, by using variables and calls to macros in the markdown code.
https://mkdocs-macros-plugin.readthedocs.io
Other
318 stars 50 forks source link

Modifying page markdown before macro interpretation #158

Closed hexus closed 1 year ago

hexus commented 1 year ago

Hello there!

First of all, thanks for such an excellent plugin for MkDocs. It makes it a breeze to use templating, and it's nice to have all of the features of Jinja available.

One thing that tripped me up today, however, was trying to achieve auto-appended macro-interpreted Markdown for every page. I certainly don't consider this a bug or anything, but I thought you might like to know about it as a potential use case.

I'm using Material for MkDocs, and I wanted to use a data-driven glossary from data that I include using your excellent include_yaml feature.

plugins:
  - macros:
      include_dir: includes
      include_yaml:
        - includes/glossary.yml

First approach

Originally, I'd tried to use the macros within a snippet via pymdownx.snippets's auto_append configuration, but these are simply included after your plugin runs (for "MkDocs reasons" unbeknownst to me):

markdown_extensions:
  - pymdownx.snippets:
      auto_append:
        - glossary.md
      base_path:
        - includes

We can assume here that includes/glossary.md is a Markdown file that contains Jinja templating, intended for interpretation by mkdocs-macro-plugin.

Second approach

After that, I looked into mkdocs-macros-plugin modules, with the context of the Advanced usage guide, and it seemed simple enough.

So I created a main.py and tried modifying the page markdown before macros are interpreted with the on_pre_page_macros() hook.

def define_env(env):
    "Adds a macro-interpreted footer to all pages"

def on_pre_page_macros(env):
    env.page.markdown += "\n\n{% include('glossary.md') %}"

This didn't work as I might have expected; the macro was not interpreted, or included on the page at all. 🤔

Final approach

Having observed the source code of the plugin, I understood why and realised I could work around it.

footer = ''

def define_env(env):
    "Adds a macro-interpreted footer to all pages"

def on_post_page_macros(env):
    global footer

    if not footer:
        footer = env.render(markdown="\n\n{% include('glossary.md') %}")

    env.raw_markdown += footer

The above works, but it does feel a little hacky. 🤓

Potential improvements for mkdocs-macros-plugin

I can see that my confusion was a due to my mismatched expectation from the Content and availability of env.page documentation and the following approach in the plugin:

# execute the pre-macro functions in the various modules
for func in self.pre_macro_functions:
    func(self)
# render the macros
self._raw_markdown = self.render(markdown)
# execute the post-macro functions in the various modules
for func in self.post_macro_functions:
    func(self)
return self.raw_markdown

So, perhaps it could take into account the resulting env.page.markdown value, or just set env.markdown before the calls to the pre_macro_functions, and then use its resulting value for the calls to the post_macro_functions.

Something like this:

# execute the pre-macro functions in the various modules
self.markdown = markdown;
for func in self.pre_macro_functions:
    func(self)
# render the macros
self._raw_markdown = self.render(self.markdown)
# execute the post-macro functions in the various modules
for func in self.post_macro_functions:
    func(self)
return self.raw_markdown

Alternatively, a nice feature would be the ability to auto append or auto prepend files to each page, much like pymdownx.snippets's auto_append configuration.

The ideal configuration scenario for me would be to avoid maintaining my own Python entirely and just configure a number of auto-appended or auto-prepended Markdown files from the include directory.

For example:

plugins:
  - macros:
      auto_prepend:
        - header.md
      auto_append:
        - footer.md
      include_dir: includes
      include_yaml:
        - includes/glossary.yml

I hope this was insightful, and I hope I explained clearly enough.

Cheers! 🍻

github-actions[bot] commented 1 year ago

Welcome to this project and thank you!' first issue

fralau commented 1 year ago

That's very interesting.

fralau commented 1 year ago

You don't have to use the hack! That works to add a 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-rendering)', 'Name of the page is _%s_' % env.page.title])
    env.raw_markdown += footer

Keep in mind that Python code is executed when it is called, so it is doing the conversion.

hexus commented 1 year ago

In my case, I did have to use the workaround due to the need for macros in a footer: {% include('glossary.md') %}

I at least used a global variable to memoize the rendered HTML, making sure that the macro was only resolved once per build, rather than once per page per build. Convenient that Markdown can contain any HTML!

fralau commented 1 year ago

I implemented your suggestion and that should work now:

def on_pre_page_macros(env):
    """
    Actions to be done before macro interpretation,
    """ 
    footer = "\n\n{% include('glossary.md') %}"
    env.markdown += footer

Could I ask you to test?

hexus commented 1 year ago

Yep, this works a treat. Super simple main.py now.

def on_pre_page_macros(env):
    env.markdown += "\n\n{% include('glossary.md') %}"

Thanks, @fralau! :beers:

Perhaps some day I'll prepare a PR for append and prepend configs. 🤓

fralau commented 1 year ago

@hexus Good idea for the PR, thanks! 👍

First open an issue with a concise statement of the problem, and the solution you intend to implement. In that way, you could then go into the development with the assurance that it will work. And we will have documentation in the future, for reference.