Skip to content

Commit

Permalink
refactor: Reuse Markdown configuration as declared in mkdocs.yml
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Apr 18, 2023
1 parent 40e6d90 commit afe091c
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 13 deletions.
29 changes: 23 additions & 6 deletions docs/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:**
Expand Down Expand Up @@ -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:**
Expand All @@ -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:**
Expand All @@ -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** <small>(best used with actual session syntax like
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/
6 changes: 5 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions src/markdown_exec/mkdocs_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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", {})
Expand All @@ -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
Expand All @@ -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()
69 changes: 65 additions & 4 deletions src/markdown_exec/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,3 +29,10 @@ def test_rendering_nested_blocks(md: Markdown) -> None:
),
)
assert html == "<p><strong>Bold!</strong></p>"


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()

0 comments on commit afe091c

Please sign in to comment.