From afe091caa33ed54fd65e25e4f90b8b60786ba3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 18 Apr 2023 14:57:51 +0200 Subject: [PATCH] refactor: Reuse Markdown configuration as declared in mkdocs.yml --- docs/usage/index.md | 29 ++++++++++--- mkdocs.yml | 6 ++- src/markdown_exec/mkdocs_plugin.py | 21 ++++++++- src/markdown_exec/rendering.py | 69 ++++++++++++++++++++++++++++-- tests/test_converter.py | 9 ++++ 5 files changed, 121 insertions(+), 13 deletions(-) diff --git a/docs/usage/index.md b/docs/usage/index.md index f5e78c2..7f78cce 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -26,12 +26,11 @@ linking to their related documentation: - [`session`](#sessions): Execute code blocks within a named session, reusing previously defined variables, etc.. - [`source`](#render-the-source-code-as-well): Render the source as well as the output. - [`tabs`](#change-the-titles-of-tabs): When rendering the source using tabs, choose the tabs titles. -- [`title`](#additional-options): Title is a [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) option. +- [`title`](#additional-options): Title is a [Material for MkDocs][material] option. - [`updatetoc`](#generated-headings-in-table-of-contents): Whether to update the Table of Contents with generated headings. ## HTML vs. Markdown - By default, Markdown Exec will render what you print as Markdown. If you want to skip rendering, to inject HTML directly, you can set the `html` option to true. @@ -81,9 +80,13 @@ with one of the following values: - `above`: The source code will be rendered above the result. - `below`: The source code will be rendered below the result. -- `material-block`: The source code and result will be wrapped in a nice-looking block (only works with Material for MkDocs). -- `tabbed-left`: The source code and result will be rendered in tabs, in that order (remember to enable the `pymdownx.tabbed` extension). -- `tabbed-right`: The result and source code will be rendered in tabs, in that order (remember to enable the `pymdownx.tabbed` extension). +- `material-block`: The source code and result will be wrapped in a nice-looking block + (only works with [Material for MkDocs][material], + and requires the [`md_in_html`][md_in_html] extension) +- `tabbed-left`: The source code and result will be rendered in tabs, in that order + (requires the [`pymdownx.tabbed`][pymdownx.tabbed] extension). +- `tabbed-right`: The result and source code will be rendered in tabs, in that order + (requires the [`pymdownx.tabbed`][pymdownx.tabbed] extension). - `console`: The source and result are concatenated in a single code block, like an interactive console session. **Source above:** @@ -114,6 +117,9 @@ with one of the following values: ``` ```` +NOTE: **Important** +The `material-block` source option requires that you enable the [`md_in_html`][md_in_html] Markdown extension. + --- **Tabbed on the left:** @@ -124,6 +130,9 @@ with one of the following values: ``` ```` +NOTE: **Important** +The `tabbed-left` source option requires that you enable the [`pymdownx.tabbed`][pymdownx.tabbed] Markdown extension. + --- **Tabbed on the right:** @@ -134,6 +143,9 @@ with one of the following values: ``` ```` +NOTE: **Important** +The `tabbed-left` source option requires that you enable the [`pymdownx.tabbed`][pymdownx.tabbed] Markdown extension. + --- **Console** (best used with actual session syntax like @@ -145,6 +157,9 @@ with one of the following values: ``` ```` +[md_in_html]: https://python-markdown.github.io/extensions/md_in_html/ +[pymdownx.tabbed]: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ + ## Hiding lines from the source Every line that contains the string `markdown-exec: hide` will be hidden from the @@ -207,7 +222,7 @@ Wrapping the result is not possible when HTML output is enabled. ## Additional options -If you are using [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/), +If you are using [Material for MkDocs][material], you are probably familiar with the `title` option on code blocks: ````md @@ -349,3 +364,5 @@ That makes for a very meta-markdown markup: > (click on "Raw" to see the code blocks execution options). Of course "executing" Markdown (or rather, making it "literate") only makes sense when the source is shown as well. + +[material]: https://squidfunk.github.io/mkdocs-material/ \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index ea35511..8735077 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,9 +58,13 @@ extra_css: markdown_extensions: - admonition +- attr_list - callouts: strip_period: false -- pymdownx.emoji +- md_in_html +- pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg - pymdownx.magiclink - pymdownx.snippets: check_paths: true diff --git a/src/markdown_exec/mkdocs_plugin.py b/src/markdown_exec/mkdocs_plugin.py index 06f2c37..cb57aaf 100644 --- a/src/markdown_exec/mkdocs_plugin.py +++ b/src/markdown_exec/mkdocs_plugin.py @@ -13,7 +13,7 @@ from markdown_exec import formatter, formatters, validator from markdown_exec.logger import patch_loggers -from markdown_exec.rendering import MarkdownConverter +from markdown_exec.rendering import MarkdownConverter, markdown_config if TYPE_CHECKING: from jinja2 import Environment @@ -49,7 +49,22 @@ class MarkdownExecPlugin(BasePlugin): config_scheme = (("languages", config_options.Type(list, default=list(formatters.keys()))),) - def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002,D102 + def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 + """Configure the plugin. + + Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). + In this hook, we add custom fences for all the supported languages. + + We also save the Markdown extensions configuration + into [`markdown_config`][markdown_exec.rendering.markdown_config]. + + Arguments: + config: The MkDocs config object. + **kwargs: Additional arguments passed by MkDocs. + + Returns: + The modified config. + """ self.languages = self.config["languages"] mdx_configs = config.setdefault("mdx_configs", {}) superfences = mdx_configs.setdefault("pymdownx.superfences", {}) @@ -63,6 +78,7 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002,D1 "format": formatter, }, ) + markdown_config.save(config["markdown_extensions"], config["mdx_configs"]) return config def on_env(self, env: Environment, *, config: Config, files: Files) -> Environment | None: # noqa: ARG002,D102 @@ -74,3 +90,4 @@ def on_env(self, env: Environment, *, config: Config, files: Files) -> Environme def on_post_build(self, *, config: Config) -> None: # noqa: ARG002,D102 MarkdownConverter.counter = 0 + markdown_config.reset() diff --git a/src/markdown_exec/rendering.py b/src/markdown_exec/rendering.py index 8128388..112a0cf 100644 --- a/src/markdown_exec/rendering.py +++ b/src/markdown_exec/rendering.py @@ -3,9 +3,8 @@ from __future__ import annotations from functools import lru_cache -from itertools import chain from textwrap import indent -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from markdown import Markdown from markupsafe import Markup @@ -108,6 +107,67 @@ def add_source( raise ValueError(f"unsupported location for sources: {location}") +class MarkdownConfig: + """This class returns a singleton used to store Markdown extensions configuration. + + You don't have to instantiate the singleton yourself: + we provide it as [`markdown_config`][markdown_exec.rendering.markdown_config]. + """ + + _singleton: MarkdownConfig | None = None + + def __new__(cls) -> MarkdownConfig: # noqa: D102 + if cls._singleton is None: + cls._singleton = super().__new__(cls) + return cls._singleton + + def __init__(self) -> None: # noqa: D107 + self.exts: list[str | Extension] | None = None + self.exts_config: dict[str, dict[str, Any]] | None = None + + def save(self, exts: list[str | Extension], exts_config: dict[str, dict[str, Any]]) -> None: + """Save Markdown extensions and their configuration. + + Parameters: + exts: The Markdown extensions. + exts_config: The extensions configuration. + """ + self.exts = exts + self.exts_config = exts_config + + def reset(self) -> None: + """Reset Markdown extensions and their configuration.""" + self.exts = None + self.exts_config = None + + +markdown_config = MarkdownConfig() +"""This object can be used to save the configuration of your Markdown extensions. + +For example, since we provide a MkDocs plugin, we use it to store the configuration +that was read from `mkdocs.yml`: + +```python +from markdown_exec.rendering import markdown_config + +# ...in relevant events/hooks, access and modify extensions and their configs, then: +markdown_config.save(extensions, extensions_config) +``` + +See the actual event hook: [`on_config`][markdown_exec.mkdocs_plugin.MarkdownExecPlugin.on_config]. +See the [`save`][markdown_exec.rendering.MarkdownConfig.save] +and [`reset`][markdown_exec.rendering.MarkdownConfig.reset] methods. + +Without it, Markdown Exec will rely on the `registeredExtensions` attribute +of the original Markdown instance, which does not forward everything +that was configured, notably extensions like `tables`. Other extensions +such as `attr_list` are visible, but fail to register properly when +reusing their instances. It means that the rendered HTML might differ +from what you expect (tables not rendered, attribute lists not injected, +emojis not working, etc.). +""" + + @lru_cache(maxsize=None) def _register_headings_processors(md: Markdown) -> None: md.treeprocessors.register( @@ -124,8 +184,9 @@ def _register_headings_processors(md: Markdown) -> None: def _mimic(md: Markdown, headings: list[Element], *, update_toc: bool = True) -> Markdown: new_md = Markdown() - extensions: list[Extension | str] = list(chain(md.registeredExtensions, ["tables", "md_in_html"])) - new_md.registerExtensions(extensions, {}) + extensions: list[Extension | str] = markdown_config.exts or md.registeredExtensions # type: ignore[assignment] + extensions_config: dict[str, dict[str, Any]] = markdown_config.exts_config or {} + new_md.registerExtensions(extensions, extensions_config) new_md.treeprocessors.register( IdPrependingTreeprocessor(md, ""), IdPrependingTreeprocessor.name, diff --git a/tests/test_converter.py b/tests/test_converter.py index 26eba5c..1ea165f 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -5,6 +5,8 @@ from textwrap import dedent from typing import TYPE_CHECKING +from markdown_exec.rendering import MarkdownConfig, markdown_config + if TYPE_CHECKING: from markdown import Markdown @@ -27,3 +29,10 @@ def test_rendering_nested_blocks(md: Markdown) -> None: ), ) assert html == "

Bold!

" + + +def test_instantiating_config_singleton() -> None: + """Assert that the Markdown config instances act as a singleton.""" + assert MarkdownConfig() is markdown_config + markdown_config.save([], {}) + markdown_config.reset()