From cfa03e7b24d883a36341b03a84c22beb06dd113d Mon Sep 17 00:00:00 2001 From: Laurent Franceschetti Date: Tue, 20 Feb 2024 10:34:10 +0100 Subject: [PATCH] Correct management of opt-in - `render_macros` in the YAML header has the last word - update `opt-in` test case - update documentation --- CHANGELOG.md | 4 + mkdocs_macros/plugin.py | 76 +++++----- test/opt-in/docs/rendered/exception.md | 17 +++ test/opt-in/mkdocs.yml | 9 +- webdoc/docs/advanced.md | 10 ++ webdoc/docs/rendering.md | 195 ++++++++++++++++++++----- 6 files changed, 238 insertions(+), 73 deletions(-) create mode 100644 test/opt-in/docs/rendered/exception.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aa86c4..b8e8594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * Added: parameters `j2_comment_start_string` and `j2_comment_end_string` to plugin's parameters, to specify alternate markers for comments. +* Added the multiline parameter `force_render_paths` in the config file, + to specify directories or file patterns to be rendered for the case when `render_by_default = false` + (the `render_macros` parameter in the YAML header of the page + has the last word). ## 1.0.5, 2023-10-31 diff --git a/mkdocs_macros/plugin.py b/mkdocs_macros/plugin.py index 40938e3..6061054 100644 --- a/mkdocs_macros/plugin.py +++ b/mkdocs_macros/plugin.py @@ -251,7 +251,7 @@ def reverse(x): @property def page(self): """ - The page information + The current page's information """ try: return self._page @@ -262,7 +262,7 @@ def page(self): @property def markdown(self): """ - The markdown after interpretation + The markdown of the current page, after interpretation """ try: return self._markdown @@ -273,7 +273,7 @@ def markdown(self): @markdown.setter def markdown(self, value): """ - Used to set the raw markdown + Used to set the raw markdown of the current page """ if not isinstance(value, str): raise ValueError("Value provided to attribute markdown " @@ -494,15 +494,27 @@ def _load_modules(self): def render(self, markdown: str, force_rendering:bool=False): """ - Render a page through jinja2: it executes the macros. - It keeps account of the `render_macros` variable + 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: force the rendering anyway + - 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 ------- @@ -510,10 +522,7 @@ def render(self, markdown: str, force_rendering:bool=False): Notes ----- - - Must called by '_on_page_markdown()' - - If the YAML header of the page contains `ignore_macros: true` - then NO rendering will be done, and the markdown will be returned - as is. + - Must called by _on_page_markdown() """ # Process meta_variables @@ -527,30 +536,31 @@ def render(self, markdown: str, force_rendering:bool=False): # this is a premature rendering, no meta variables in the page meta_variables = {} - if force_rendering: - # [if force_render=True, it skips all the reasoning in the else] - pass - else: - # Warning this is ternary logic(True, False, None: nothing said) - ignore_macros = None # deprecated - render_macros = None - - if meta_variables: - # determine whether the page will be rendered or not - # the two formulations are accepted - ignore_macros = meta_variables.get('ignore_macros') - render_macros = meta_variables.get('render_macros') - - if self.config['render_by_default']: - # opt-out: force of a page NOT to be interpreted, - opt_out = ignore_macros == True or render_macros == False - if opt_out: - return markdown + + # Warning this is ternary logic(True, False, None: nothing said) + render_macros = None + + if 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: - # opt-in: you must force a page to be interpreted - opt_in = render_macros == True or ignore_macros == False - if not opt_in: - return markdown + return markdown # Update the page with meta variables # i.e. what's in the yaml header of the page diff --git a/test/opt-in/docs/rendered/exception.md b/test/opt-in/docs/rendered/exception.md new file mode 100644 index 0000000..b3233aa --- /dev/null +++ b/test/opt-in/docs/rendered/exception.md @@ -0,0 +1,17 @@ +--- +render_macros: false # opt-in +--- +# Opt-out by page header (priority) + +{# This page should not be rendered #} + +In theory, this page should be rendered because its source file +is in the `rendered/` directory. + +However, it can be NEVER be rendered, because the variable `render_macros` +is set to `false` in the YAML header. + +When it is set to `false`, it takes precedence over the directives +in the `force_render_paths` variable! + +{{ macros_info() }} \ No newline at end of file diff --git a/test/opt-in/mkdocs.yml b/test/opt-in/mkdocs.yml index 853d255..252f8dd 100644 --- a/test/opt-in/mkdocs.yml +++ b/test/opt-in/mkdocs.yml @@ -4,10 +4,10 @@ theme: mkdocs nav: - Home: index.md - Other: not_rendered/noname.md - - Rendered (by header): noname.md # opt-in by header - - Rendered (by dir): rendered/noname.md # opt-in by directory - - Rendered (by name): render_this_one.md # opt-in by file pattern - + - Render (by header): noname.md # opt-in by header + - Render (by dir): rendered/noname.md # opt-in by directory + - Render (by name): render_this_one.md # opt-in by file pattern + - Exception: rendered/exception.md # forced opt-out in header plugins: - search @@ -19,6 +19,7 @@ plugins: force_render_paths: | # this directory will be rendered: rendered/ + # this pattern of files will be rendered: render_*.md diff --git a/webdoc/docs/advanced.md b/webdoc/docs/advanced.md index d57141c..a124cae 100644 --- a/webdoc/docs/advanced.md +++ b/webdoc/docs/advanced.md @@ -249,7 +249,17 @@ of conflict, the plugin will attempt to privilege the latest branch. algorithm might not work as you expect. +Controlling the rendering of pages +-------------------------------------------- + +A frequent issue, when adding the Mkdocs-Macros plugin to an +**existing MkDocs project**, is that some pre-existing markdown pages +may not be rendered correctly, +or cause a syntax error, or some other error. +That is because Mkdocs-Macros might confuse snippets in those pages +with Jinja2 statements, try to render them and fail. +This issue (as well as its solutions) is described under the diff --git a/webdoc/docs/rendering.md b/webdoc/docs/rendering.md index a5839bf..3308e6c 100644 --- a/webdoc/docs/rendering.md +++ b/webdoc/docs/rendering.md @@ -1,11 +1,17 @@ -Controlling the rendering of pages -================================== +Controlling the rendering of macros +======================================== +What is meant here by _**rendering**_ is the translation +by Mkdocs-Macros of macros as well as [Jinja2 control structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures) +and [comments](https://jinja.palletsprojects.com/en/3.1.x/templates/#comments) into pure Markdown/HTML. -!!! Tip "Migrations to mkdocs-macros" - This page may be useful for **large mkdocs projects** - that have decided to adopt mkdocs-macros at a later - stage of their existence. +!!! Tip "Migrations to Mkdocs-Macros" + This page may be useful for **large MkDocs projects** + that have decided to: + + - adopt Mkdocs-Macros at a latet stage of their existence; + - include a **subproject** using Mkdocs-Macros into a main project + that doesn't. @@ -13,15 +19,15 @@ Controlling the rendering of pages ## Introduction ### Issue -The most frequent issue, when adding the mkdocs-macros plugin to an -existing mkdocs project, is some markdown pages +The most frequent issue, when adding the Mkdocs-Macros plugin to an +existing MkDocs project, is that some pre-existing markdown pages may not be rendered correctly, or cause a syntax error, or some other error. The reason is that, by default, when the Jinja2 template engine in the **macro plugin** encounters any text that has the standard markers (typically starting with `{%`} or `{{`) this will cause a conflict: -it will try to interpret that text as a macro +it will try to interpret that text as as Jinj2 directive or macro and fail to behave properly. The most likely places where this can occur are the following: @@ -40,7 +46,7 @@ The most likely places where this can occur are the following: 1. If the statement does not fit Jinja2 syntax, a syntax error will be displayed in the rendered page. - 2. If mkdocs-macros mistakenly tries to interprets a syntactically + 2. If Mkdocs-Macros mistakenly tries to interprets a syntactically valid Jinja2 statement containing a variable, the most likely result is the page will fail (you can change this behavior with the [`on_undefined` parameter in the config file](troubleshooting.md#what-happens-if-a-variable-is-undefined)). @@ -52,14 +58,14 @@ The most likely places where this can occur are the following: This question of accidental rendering is covered generally in the Jinja2 documentation as [escaping](https://jinja.palletsprojects.com/en/2.11.x/templates/?highlight=raw#escaping). - Here we need to help **mkdocs-macros** clearly distinguish + Here we need to help **Mkdocs-Macros** clearly distinguish between **two types of Jinja2 statements**: 1. **Documentation statements**, which must appear as-is in the final HTML pages, - and therefore **must not** be interpreted by mkdocs-macros. + and therefore **must not** be interpreted by Mkdocs-Macros. 2. **Actionable Jinja2 statements**: calls to variables or macros, etc., - which mkdocs-macros **must** replace by their equivalent. + which Mkdocs-Macros **must** replace by their equivalent. ### Special Cases @@ -127,7 +133,7 @@ For example, the following LaTeX snippet is used to draw a table: Here we are trying to solve a different problem: **how to avoid interpretation** of Jinja2 statements - **by mkdocs-macros**, + **by Mkdocs-Macros**, so that **they actually appear in the HTML output**? ## Solutions @@ -138,14 +144,12 @@ _From version 0.5.7_ !!! Tip "Quick fix" This solution is a quick fix, if you are "migrating" - a pre-existing mkdocs project under mkdocs-macros, and - some markdown pages fail, or do not display correctly. - - This will leave more time to implement the next solutions. + a pre-existing MkDocs project under Mkdocs-Macros, and + only a few Markdown pages fail, or do not display correctly. In the header of the markdown page, indicate that the markdown should -be used "as-is" (no rendering of mkdocs-macros), +be used "as-is" (no rendering of Mkdocs-Macros), by setting the `ignore_macros` meta-data key to the `true`value. @@ -156,13 +160,17 @@ render_macros: false --- ``` +!!! Important "" + That parameter takes priority over all other considerations. + It guarantees that Mkdocs-Macros will not attempt to render this page. + Any other value than `true` (or an absence of this key), will be interpreted as a `false` value. -!!! Warning "_From version 1.0.0_" +!!! Warning "_From version 1.1.0_" - This directive is also accepted, though it is now deprecated: + This directive is no longer accepted and will cause an error: --- # YAML header @@ -177,13 +185,20 @@ as a `false` value. _From version 1.0.0_ !!! Tip "Large preexisting projects" - If you already have a large mkdocs project and have several - problematic pages, or do not wish to control - the rendering of all pages, this solution may be for you. + If you already have a particularly large MkDocs project and have several + problematic pages, or do not wish write a YAML header for all of them, + this solution may be for you. The **opt-in** solution consists of changing the default behavior of -mkdocs-macros: no pages will be rendered (no macros interpreted) -unless this is specifically requested in the page's header. +Mkdocs-Macros: no pages will be rendered (no macros interpreted) +unless this is specifically requested. + +After that, there are two ways to specify which pages must be rendered: + +1. In the YAML header of each markdown page + (`render_macros: true`). +2. _From version 1.1.0_ In the configuration file, by setting the + `force_render_paths` parameter. To change the default behavior, set the `render_by_default` parameter to false in the config file (mkdocs.yml): @@ -195,6 +210,10 @@ plugins: render_by_default: false ``` + + +#### Opt-in with the markdown page's header + To render a specific page: ```yaml @@ -204,7 +223,107 @@ render_macros: true --- ``` -mkdocs-macros will _not_ attempt to render the other pages. +Mkdocs-Macros will _not_ attempt to render the other pages. + +!!! Warning "_From version 1.1.0_" + + The following directive is no longer accepted and will cause an error: + + --- + # YAML header + ignore_macros: false + --- + + +#### Opt-in through the config file + +_From version 1.1.0_ + +When `render_macros`is set to `false`, the parameter `force_render_paths` +can be used to specify a list of **exceptions** (**opt-in**) i.e. +relative paths of pages within the documents directory +(as well as file patterns) in which macros must be rendered. + + +!!! Note "Use case" + This feature was developed for very large MkDocs projects, typically when + a whole subproject is + later inserted (as a subdirectory) into a bigger project that doesn't. + + Default rendering of macros is out of question since it would + break the parent project; at the same time, adding a YAML header + in all pages of the child project would be tedious. + + Setting the subdirectory as an exception (opt-in) can solve the problem. + + + +The syntax follows more or less the [.gitignore pattern matching](https://git-scm.com/docs/gitignore#_pattern_format). + +For example: + +```yaml +plugins: + - search + - macros: + # do not render the pages by default + # requires an opt-in + render_by_default: false + # render this subdirectory of the documents directory: + force_render_paths: rendered/ +``` + +!!! Warning "The page header has the last word" + If `render_macros` is set to `false` in the YAML header of the page, + it will _**never**_ be rendered, even if it matches the specification in + `force_render_paths`. + + Similarly, if it is set to `true`, it will be rendered regardless of + `force_render_paths`. + + + +The syntax allows more than one instruction, with [examples provided in this page of the Pathlib library documentation](https://python-path-specification.readthedocs.io/en/stable/readme.html#tutorial), e.g.: + + +```yaml +plugins: + - search + - macros: + # do not render the pages by default + # requires an opt-in + render_by_default: false + # render those paths and patterns: + force_render_paths: | + # this directory will be rendered: + rendered/ + # this pattern of files will be rendered: + render_*.md +``` + +!!! Note "Syntax of the multiline parameter" + + `force_render_paths` + can be a YAML multiline literal string (note the pipe symbol). + Comments (starting with a `#`) are accepted _within_ the string + and are ignored. + + +It is also possible to specify exceptions with `!` operator, +(e.g. `!foo*.md` excludes all files starting with `foo` and with +the `md` extension from the list of candidates.) + +!!! Warning "Location of the root directory" + + Contrary to other parameters of the plugin, + which consider that the root directory is the Mkdocs project's directory + (where the config file resides), + the root directory here is the **documents** directory, generally + named `docs`. + Starting the relative path from that subdirectory is logical, + since the markdown pages are not supposed + to exist outside of it. + ### Solution 3: Snippets as jinja2 strings (one-liners) @@ -244,21 +363,25 @@ The same approach can also be used for inline definitions, e.g.: You can use the `raw` expression for inline definitions, for example: {% raw %} `{{{method}}}-{{{url}}}.json` {% endraw %} for escaping 3-bracket expressions often used in Handlebars. -### Solution 5: Altering the syntax of jinja2 for mkdocs-macros +### Solution 5: Altering the syntax of jinja2 for Mkdocs-Macros -Sometimes the introduction of mkdocs-macros comes late in the chain, and the -existing pages already contain a lot of Jinja2 statements that are -should appear in the final HTML pages: escaping all of them +Sometimes the introduction of Mkdocs-Macros comes late in the chain, and the +existing pages already contain a lot of Jinja2 statements +(or statements with a similar syntax) that +should appear as-is in the final HTML pages: escaping all of them would not really be an option. -Or else, you do not wish to bother the writers of markdown pages -with the obligation of escaping Jinja2 statements. +Or else, you are using some other plugin or Markdown extension that demands +a syntax that is too similar to that normally used by Jinja2 +(and you do not wish to implement an MkDocs-Macros [pluglet](pluglets.md) to replace it). + !!! Tip "Solution" Rather than refactoring all the existing markdown pages to fence - those Jinja2 statements, - it may be preferable to alter the **markers** for variables or blocks - used in mkdocs-macros. + those statements to protect them from rendering, + it may be preferable, as a last resort, + to alter the **markers** for variables or blocks + used in Mkdocs-Macros. The parameters to control those markers are described in the documentation of the [high-level API for Jinja2](https://jinja.palletsprojects.com/en/3.1.x/api/#high-level-api).