markdown-macros-extension 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markdown_macros_extension-0.1.0/LICENSE +21 -0
- markdown_macros_extension-0.1.0/PKG-INFO +91 -0
- markdown_macros_extension-0.1.0/README.md +58 -0
- markdown_macros_extension-0.1.0/markdown_macros/__init__.py +15 -0
- markdown_macros_extension-0.1.0/markdown_macros/front_matter.py +106 -0
- markdown_macros_extension-0.1.0/markdown_macros/macros.py +323 -0
- markdown_macros_extension-0.1.0/markdown_macros_extension.egg-info/PKG-INFO +91 -0
- markdown_macros_extension-0.1.0/markdown_macros_extension.egg-info/SOURCES.txt +14 -0
- markdown_macros_extension-0.1.0/markdown_macros_extension.egg-info/dependency_links.txt +1 -0
- markdown_macros_extension-0.1.0/markdown_macros_extension.egg-info/entry_points.txt +2 -0
- markdown_macros_extension-0.1.0/markdown_macros_extension.egg-info/requires.txt +10 -0
- markdown_macros_extension-0.1.0/markdown_macros_extension.egg-info/top_level.txt +1 -0
- markdown_macros_extension-0.1.0/pyproject.toml +51 -0
- markdown_macros_extension-0.1.0/setup.cfg +4 -0
- markdown_macros_extension-0.1.0/tests/test_front_matter.py +75 -0
- markdown_macros_extension-0.1.0/tests/test_macros.py +136 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BarCar (https://github.com/barcar)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markdown-macros-extension
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python-Markdown extension: MkDocs-Macros–style Jinja2 templating (variables, macros, filters) with YAML front matter for Zensical and MkDocs
|
|
5
|
+
Author: BarCar
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/barcar/markdown-macros#readme
|
|
8
|
+
Project-URL: Repository, https://github.com/barcar/markdown-macros
|
|
9
|
+
Keywords: markdown,frontmatter,yaml,zensical,mkdocs,macros
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: markdown>=3.4
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: jinja2>=3.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: zensical; extra == "docs"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Markdown Macros
|
|
35
|
+
|
|
36
|
+
[](https://github.com/barcar/markdown-macros/actions/workflows/pages.yml)
|
|
37
|
+
|
|
38
|
+
**Author:** [BarCar](https://github.com/barcar) · **Repository:** [github.com/barcar/markdown-macros](https://github.com/barcar/markdown-macros)
|
|
39
|
+
|
|
40
|
+
A **Python-Markdown extension** that brings [MkDocs Macros](https://mkdocs-macros-plugin.readthedocs.io/)–style **Jinja2 templating** to any Markdown pipeline: variables (config + YAML front matter), **macros**, and **filters**. Works with [Zensical](https://zensical.org), MkDocs, or plain Python—without the MkDocs plugin.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Variables** from config, YAML front matter, or `include_yaml`
|
|
45
|
+
- **Macros and filters** via `define_env(env)` in a Python module (same API as MkDocs Macros)
|
|
46
|
+
- **YAML front matter** parsed and exposed as `md.Meta` / `md.front_matter`
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install markdown-macros-extension
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires: `markdown>=3.4`, `pyyaml>=6.0`, `jinja2>=3.0`.
|
|
55
|
+
|
|
56
|
+
## Quick example
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import markdown
|
|
60
|
+
from markdown_macros import MacrosExtension
|
|
61
|
+
|
|
62
|
+
text = """---
|
|
63
|
+
title: My Page
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
# {{ title }}
|
|
67
|
+
|
|
68
|
+
The answer is {{ 6 * 7 }}.
|
|
69
|
+
"""
|
|
70
|
+
md = markdown.Markdown(extensions=[MacrosExtension()])
|
|
71
|
+
html = md.convert(text) # title and 42 rendered
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In Zensical or MkDocs, add `markdown_macros` to your Markdown extensions and optionally set `module_name`, `variables`, `include_yaml`, etc. See the [documentation](https://barcar.github.io/markdown-macros/) for configuration, usage, and the full API.
|
|
75
|
+
|
|
76
|
+
**Compatibility:** API-aligned with the [MkDocs Macros plugin](https://mkdocs-macros-plugin.readthedocs.io/) (variables, macros, filters, `define_env`); see [Compatibility](https://barcar.github.io/markdown-macros/compatibility/) in the docs.
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
**Online:** [Documentation](https://barcar.github.io/markdown-macros/) (GitHub Pages).
|
|
81
|
+
|
|
82
|
+
To build the docs locally:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install -e ".[docs]"
|
|
86
|
+
zensical serve
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Markdown Macros
|
|
2
|
+
|
|
3
|
+
[](https://github.com/barcar/markdown-macros/actions/workflows/pages.yml)
|
|
4
|
+
|
|
5
|
+
**Author:** [BarCar](https://github.com/barcar) · **Repository:** [github.com/barcar/markdown-macros](https://github.com/barcar/markdown-macros)
|
|
6
|
+
|
|
7
|
+
A **Python-Markdown extension** that brings [MkDocs Macros](https://mkdocs-macros-plugin.readthedocs.io/)–style **Jinja2 templating** to any Markdown pipeline: variables (config + YAML front matter), **macros**, and **filters**. Works with [Zensical](https://zensical.org), MkDocs, or plain Python—without the MkDocs plugin.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Variables** from config, YAML front matter, or `include_yaml`
|
|
12
|
+
- **Macros and filters** via `define_env(env)` in a Python module (same API as MkDocs Macros)
|
|
13
|
+
- **YAML front matter** parsed and exposed as `md.Meta` / `md.front_matter`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install markdown-macros-extension
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires: `markdown>=3.4`, `pyyaml>=6.0`, `jinja2>=3.0`.
|
|
22
|
+
|
|
23
|
+
## Quick example
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import markdown
|
|
27
|
+
from markdown_macros import MacrosExtension
|
|
28
|
+
|
|
29
|
+
text = """---
|
|
30
|
+
title: My Page
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# {{ title }}
|
|
34
|
+
|
|
35
|
+
The answer is {{ 6 * 7 }}.
|
|
36
|
+
"""
|
|
37
|
+
md = markdown.Markdown(extensions=[MacrosExtension()])
|
|
38
|
+
html = md.convert(text) # title and 42 rendered
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In Zensical or MkDocs, add `markdown_macros` to your Markdown extensions and optionally set `module_name`, `variables`, `include_yaml`, etc. See the [documentation](https://barcar.github.io/markdown-macros/) for configuration, usage, and the full API.
|
|
42
|
+
|
|
43
|
+
**Compatibility:** API-aligned with the [MkDocs Macros plugin](https://mkdocs-macros-plugin.readthedocs.io/) (variables, macros, filters, `define_env`); see [Compatibility](https://barcar.github.io/markdown-macros/compatibility/) in the docs.
|
|
44
|
+
|
|
45
|
+
## Documentation
|
|
46
|
+
|
|
47
|
+
**Online:** [Documentation](https://barcar.github.io/markdown-macros/) (GitHub Pages).
|
|
48
|
+
|
|
49
|
+
To build the docs locally:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e ".[docs]"
|
|
53
|
+
zensical serve
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown Macros: MkDocs-Macros–style extension for Python-Markdown.
|
|
3
|
+
|
|
4
|
+
Jinja2 templating with variables (config + YAML front matter), macros, and
|
|
5
|
+
filters. Parses --- YAML --- front matter and exposes md.Meta / md.front_matter.
|
|
6
|
+
Use define_env(env) in a module for macros/filters/variables.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from markdown_macros.front_matter import FrontMatterExtension
|
|
10
|
+
from markdown_macros.macros import MacrosExtension, make_extension
|
|
11
|
+
|
|
12
|
+
# Python-Markdown discovers extensions via makeExtension (camelCase)
|
|
13
|
+
makeExtension = make_extension
|
|
14
|
+
|
|
15
|
+
__all__ = ["FrontMatterExtension", "MacrosExtension", "make_extension", "makeExtension"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YAML Front Matter preprocessor for Python-Markdown.
|
|
3
|
+
|
|
4
|
+
Strips the leading --- YAML --- block from the source and stores parsed data
|
|
5
|
+
in md.Meta (Python-Markdown style: lowercase keys, list-of-strings values)
|
|
6
|
+
and md.front_matter (full parsed dict for nested structures).
|
|
7
|
+
|
|
8
|
+
Compatible with Zensical and MkDocs Macros: runs only at the Markdown layer;
|
|
9
|
+
no Jinja2 or plugin logic. When MkDocs Macros is used, it can consume
|
|
10
|
+
page-level metadata; this extension ensures front matter is available as md.Meta
|
|
11
|
+
for the rest of the pipeline.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from markdown import Extension
|
|
16
|
+
from markdown.preprocessors import Preprocessor
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import yaml
|
|
20
|
+
except ImportError:
|
|
21
|
+
yaml = None
|
|
22
|
+
|
|
23
|
+
# Match --- at start of document, then content until next --- (YAML style only for now)
|
|
24
|
+
FRONT_MATTER_RE = re.compile(
|
|
25
|
+
r"^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n",
|
|
26
|
+
re.MULTILINE,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _meta_from_dict(data):
|
|
31
|
+
"""Convert parsed YAML dict to Python-Markdown Meta format: lowercase keys, list values."""
|
|
32
|
+
meta = {}
|
|
33
|
+
if not isinstance(data, dict):
|
|
34
|
+
return meta
|
|
35
|
+
for key, value in data.items():
|
|
36
|
+
k = key.lower().strip()
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
meta[k] = [str(v) for v in value]
|
|
39
|
+
elif value is None:
|
|
40
|
+
meta[k] = [""]
|
|
41
|
+
else:
|
|
42
|
+
meta[k] = [str(value)]
|
|
43
|
+
return meta
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FrontMatterPreprocessor(Preprocessor):
|
|
47
|
+
"""Preprocessor that strips YAML front matter and stores it on the Markdown instance."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, md, config):
|
|
50
|
+
super().__init__(md)
|
|
51
|
+
self.config = config
|
|
52
|
+
|
|
53
|
+
def run(self, lines):
|
|
54
|
+
text = "\n".join(lines)
|
|
55
|
+
match = FRONT_MATTER_RE.match(text)
|
|
56
|
+
if not match:
|
|
57
|
+
# Ensure Meta and front_matter exist for downstream (e.g. themes, MkDocs)
|
|
58
|
+
if not hasattr(self.md, "Meta") or self.md.Meta is None:
|
|
59
|
+
self.md.Meta = {}
|
|
60
|
+
if not hasattr(self.md, "front_matter") or self.md.front_matter is None:
|
|
61
|
+
self.md.front_matter = {}
|
|
62
|
+
return lines
|
|
63
|
+
|
|
64
|
+
raw_block = match.group(1).strip()
|
|
65
|
+
if yaml is not None:
|
|
66
|
+
try:
|
|
67
|
+
data = yaml.safe_load(raw_block)
|
|
68
|
+
if data is None:
|
|
69
|
+
data = {}
|
|
70
|
+
if not isinstance(data, dict):
|
|
71
|
+
data = {"content": data}
|
|
72
|
+
except Exception:
|
|
73
|
+
data = {}
|
|
74
|
+
else:
|
|
75
|
+
data = {}
|
|
76
|
+
|
|
77
|
+
# Expose for Python-Markdown and downstream (e.g. MkDocs page.meta)
|
|
78
|
+
self.md.Meta = getattr(self.md, "Meta", None) or {}
|
|
79
|
+
self.md.Meta.update(_meta_from_dict(data))
|
|
80
|
+
# Full structure for themes/plugins that expect nested data
|
|
81
|
+
self.md.front_matter = data
|
|
82
|
+
|
|
83
|
+
# Return remaining markdown (without the front matter block)
|
|
84
|
+
rest = text[match.end() :]
|
|
85
|
+
return rest.split("\n")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FrontMatterExtension(Extension):
|
|
89
|
+
"""Python-Markdown extension that parses YAML front matter and exposes it as md.Meta and md.front_matter."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, **kwargs):
|
|
92
|
+
# No config options; declare empty config so base Extension.setConfig behaves correctly.
|
|
93
|
+
self.config = {}
|
|
94
|
+
super().__init__(**kwargs)
|
|
95
|
+
|
|
96
|
+
def extendMarkdown(self, md):
|
|
97
|
+
md.registerExtension(self)
|
|
98
|
+
md.preprocessors.register(
|
|
99
|
+
FrontMatterPreprocessor(md, self.getConfigs()),
|
|
100
|
+
"front_matter",
|
|
101
|
+
priority=25, # Run early, before most other preprocessors (higher = first)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def make_extension(**kwargs):
|
|
106
|
+
return FrontMatterExtension(**kwargs)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MkDocs-Macros–style extension for Python-Markdown.
|
|
3
|
+
|
|
4
|
+
Treats the document as a Jinja2 template: parses YAML front matter for
|
|
5
|
+
page-level variables, merges with config variables and optional module
|
|
6
|
+
(macros/filters/variables via define_env(env)), then renders the body.
|
|
7
|
+
Exposes md.Meta and md.front_matter for downstream use.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from markdown import Extension
|
|
16
|
+
from markdown.preprocessors import Preprocessor
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import jinja2
|
|
20
|
+
except ImportError:
|
|
21
|
+
jinja2 = None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import yaml
|
|
25
|
+
except ImportError:
|
|
26
|
+
yaml = None
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("markdown_macros")
|
|
29
|
+
|
|
30
|
+
FRONT_MATTER_RE = re.compile(
|
|
31
|
+
r"^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n",
|
|
32
|
+
re.MULTILINE,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _meta_from_dict(data):
|
|
37
|
+
"""Convert dict to Python-Markdown Meta: lowercase keys, list values."""
|
|
38
|
+
meta = {}
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
return meta
|
|
41
|
+
for key, value in data.items():
|
|
42
|
+
k = key.lower().strip()
|
|
43
|
+
if isinstance(value, list):
|
|
44
|
+
meta[k] = [str(v) for v in value]
|
|
45
|
+
elif value is None:
|
|
46
|
+
meta[k] = [""]
|
|
47
|
+
else:
|
|
48
|
+
meta[k] = [str(value)]
|
|
49
|
+
return meta
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MacroEnv:
|
|
53
|
+
"""Minimal env object for define_env(env) compatibility with MkDocs Macros."""
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.variables = {}
|
|
57
|
+
self._macros = {}
|
|
58
|
+
self._filters = {}
|
|
59
|
+
|
|
60
|
+
def macro(self, fn=None, name=None):
|
|
61
|
+
"""Register a macro. Use as @env.macro or env.macro(func) or env.macro(func, 'name')."""
|
|
62
|
+
if fn is None:
|
|
63
|
+
return lambda f: self.macro(f, name)
|
|
64
|
+
self._macros[name or fn.__name__] = fn
|
|
65
|
+
return fn
|
|
66
|
+
|
|
67
|
+
def filter(self, fn=None, name=None):
|
|
68
|
+
"""Register a filter. Use as @env.filter or env.filter(func)."""
|
|
69
|
+
if fn is None:
|
|
70
|
+
return lambda f: self.filter(f, name)
|
|
71
|
+
self._filters[name or fn.__name__] = fn
|
|
72
|
+
return fn
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_module(module_name, project_root=None):
|
|
76
|
+
"""Load a module by name (e.g. 'main') and return (variables, macros, filters) from define_env."""
|
|
77
|
+
root = Path(project_root or ".").resolve()
|
|
78
|
+
for candidate in [root / f"{module_name}.py", root / module_name / "__init__.py"]:
|
|
79
|
+
if candidate.exists():
|
|
80
|
+
spec = importlib.util.spec_from_file_location(module_name, candidate)
|
|
81
|
+
if spec and spec.loader:
|
|
82
|
+
mod = importlib.util.module_from_spec(spec)
|
|
83
|
+
spec.loader.exec_module(mod)
|
|
84
|
+
if hasattr(mod, "define_env"):
|
|
85
|
+
env = MacroEnv()
|
|
86
|
+
mod.define_env(env)
|
|
87
|
+
return env.variables, env._macros, env._filters
|
|
88
|
+
break
|
|
89
|
+
try:
|
|
90
|
+
mod = __import__(module_name)
|
|
91
|
+
if hasattr(mod, "define_env"):
|
|
92
|
+
env = MacroEnv()
|
|
93
|
+
mod.define_env(env)
|
|
94
|
+
return env.variables, env._macros, env._filters
|
|
95
|
+
except ImportError:
|
|
96
|
+
pass
|
|
97
|
+
return {}, {}, {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_pluglets(module_names):
|
|
101
|
+
"""Load pluglet modules by package name; return merged (variables, macros, filters)."""
|
|
102
|
+
all_vars = {}
|
|
103
|
+
all_macros = {}
|
|
104
|
+
all_filters = {}
|
|
105
|
+
for name in module_names or []:
|
|
106
|
+
name = (name or "").strip()
|
|
107
|
+
if not name:
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
mod = importlib.import_module(name)
|
|
111
|
+
if hasattr(mod, "define_env"):
|
|
112
|
+
env = MacroEnv()
|
|
113
|
+
mod.define_env(env)
|
|
114
|
+
all_vars.update(env.variables)
|
|
115
|
+
all_macros.update(env._macros)
|
|
116
|
+
all_filters.update(env._filters)
|
|
117
|
+
except ImportError as e:
|
|
118
|
+
logger.warning("markdown_macros: could not load pluglet %r: %s", name, e)
|
|
119
|
+
return all_vars, all_macros, all_filters
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _load_one_yaml(path, project_root=None):
|
|
123
|
+
"""Load a single YAML file; return dict or None."""
|
|
124
|
+
if not yaml:
|
|
125
|
+
return None
|
|
126
|
+
root = Path(project_root or ".").resolve()
|
|
127
|
+
p = root / path if not Path(path).is_absolute() else Path(path)
|
|
128
|
+
if not p.exists():
|
|
129
|
+
return None
|
|
130
|
+
try:
|
|
131
|
+
with open(p, encoding="utf-8") as f:
|
|
132
|
+
data = yaml.safe_load(f)
|
|
133
|
+
return data if isinstance(data, dict) else None
|
|
134
|
+
except Exception:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _merge_include_yaml(include_yaml, project_root, variables):
|
|
139
|
+
"""
|
|
140
|
+
Merge include_yaml into variables.
|
|
141
|
+
include_yaml can be: list of paths (flat merge) or dict of key -> path (merge under key).
|
|
142
|
+
"""
|
|
143
|
+
if not include_yaml:
|
|
144
|
+
return
|
|
145
|
+
if isinstance(include_yaml, dict):
|
|
146
|
+
for key, path in include_yaml.items():
|
|
147
|
+
data = _load_one_yaml(path, project_root)
|
|
148
|
+
if data is not None:
|
|
149
|
+
variables[key] = data
|
|
150
|
+
else:
|
|
151
|
+
for path in include_yaml:
|
|
152
|
+
data = _load_one_yaml(path, project_root)
|
|
153
|
+
if isinstance(data, dict):
|
|
154
|
+
variables.update(data)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class MacrosPreprocessor(Preprocessor):
|
|
158
|
+
"""Parse front matter, build Jinja2 context, render body."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, md, config):
|
|
161
|
+
super().__init__(md)
|
|
162
|
+
self.config = config
|
|
163
|
+
|
|
164
|
+
def run(self, lines):
|
|
165
|
+
text = "\n".join(lines)
|
|
166
|
+
project_root = self.config.get("project_root") or "."
|
|
167
|
+
verbose = self.config.get("verbose", False)
|
|
168
|
+
|
|
169
|
+
# 1) Parse and strip front matter
|
|
170
|
+
match = FRONT_MATTER_RE.match(text)
|
|
171
|
+
if match:
|
|
172
|
+
raw_block = match.group(1).strip()
|
|
173
|
+
if yaml:
|
|
174
|
+
try:
|
|
175
|
+
data = yaml.safe_load(raw_block)
|
|
176
|
+
if data is None:
|
|
177
|
+
data = {}
|
|
178
|
+
if not isinstance(data, dict):
|
|
179
|
+
data = {"content": data}
|
|
180
|
+
except Exception:
|
|
181
|
+
data = {}
|
|
182
|
+
else:
|
|
183
|
+
data = {}
|
|
184
|
+
self.md.Meta = getattr(self.md, "Meta", None) or {}
|
|
185
|
+
self.md.Meta.update(_meta_from_dict(data))
|
|
186
|
+
self.md.front_matter = data
|
|
187
|
+
body = text[match.end() :]
|
|
188
|
+
else:
|
|
189
|
+
self.md.Meta = getattr(self.md, "Meta", None) or {}
|
|
190
|
+
if not hasattr(self.md, "front_matter") or self.md.front_matter is None:
|
|
191
|
+
self.md.front_matter = {}
|
|
192
|
+
body = text
|
|
193
|
+
|
|
194
|
+
# render_by_default: if False, only render when front matter has render_macros: true
|
|
195
|
+
render_by_default = self.config.get("render_by_default", True)
|
|
196
|
+
page_render_macros = self.md.front_matter.get("render_macros")
|
|
197
|
+
if not render_by_default and not page_render_macros:
|
|
198
|
+
if verbose:
|
|
199
|
+
logger.debug("markdown_macros: skipping Jinja2 (render_by_default=False, no render_macros)")
|
|
200
|
+
return body.split("\n")
|
|
201
|
+
|
|
202
|
+
# 2) Build context: config variables + include_yaml + module + pluglets + front matter
|
|
203
|
+
variables = dict(self.config.get("variables") or {})
|
|
204
|
+
_merge_include_yaml(
|
|
205
|
+
self.config.get("include_yaml"),
|
|
206
|
+
project_root,
|
|
207
|
+
variables,
|
|
208
|
+
)
|
|
209
|
+
module_name = (self.config.get("module_name") or "").strip()
|
|
210
|
+
if module_name:
|
|
211
|
+
if verbose:
|
|
212
|
+
logger.debug("markdown_macros: loading module %r", module_name)
|
|
213
|
+
mod_vars, macros, filters = _load_module(module_name, project_root)
|
|
214
|
+
variables.update(mod_vars)
|
|
215
|
+
variables.update(macros)
|
|
216
|
+
else:
|
|
217
|
+
macros = {}
|
|
218
|
+
filters = {}
|
|
219
|
+
# Pluglets (preinstalled modules)
|
|
220
|
+
pluglet_names = self.config.get("modules") or []
|
|
221
|
+
if pluglet_names:
|
|
222
|
+
if verbose:
|
|
223
|
+
logger.debug("markdown_macros: loading pluglets %s", pluglet_names)
|
|
224
|
+
pv, pm, pf = _load_pluglets(pluglet_names)
|
|
225
|
+
variables.update(pv)
|
|
226
|
+
if not module_name:
|
|
227
|
+
macros = {}
|
|
228
|
+
filters = {}
|
|
229
|
+
macros.update(pm)
|
|
230
|
+
filters.update(pf)
|
|
231
|
+
if match:
|
|
232
|
+
variables.update(self.md.front_matter)
|
|
233
|
+
|
|
234
|
+
if not jinja2:
|
|
235
|
+
return body.split("\n")
|
|
236
|
+
|
|
237
|
+
# 3) Build Jinja2 environment
|
|
238
|
+
include_dir = self.config.get("include_dir") or ""
|
|
239
|
+
include_dir_path = (Path(project_root) / include_dir).resolve() if include_dir else None
|
|
240
|
+
env_kw = {
|
|
241
|
+
"block_start_string": self.config.get("j2_block_start_string") or "{%",
|
|
242
|
+
"block_end_string": self.config.get("j2_block_end_string") or "%}",
|
|
243
|
+
"variable_start_string": self.config.get("j2_variable_start_string") or "{{",
|
|
244
|
+
"variable_end_string": self.config.get("j2_variable_end_string") or "}}",
|
|
245
|
+
}
|
|
246
|
+
comment_start = self.config.get("j2_comment_start_string")
|
|
247
|
+
comment_end = self.config.get("j2_comment_end_string")
|
|
248
|
+
if comment_start is not None:
|
|
249
|
+
env_kw["comment_start_string"] = comment_start
|
|
250
|
+
if comment_end is not None:
|
|
251
|
+
env_kw["comment_end_string"] = comment_end
|
|
252
|
+
on_undefined = self.config.get("on_undefined", "keep")
|
|
253
|
+
if on_undefined == "strict":
|
|
254
|
+
env_kw["undefined"] = jinja2.StrictUndefined
|
|
255
|
+
j2_extensions = self.config.get("j2_extensions") or []
|
|
256
|
+
if j2_extensions:
|
|
257
|
+
env_kw["extensions"] = list(j2_extensions)
|
|
258
|
+
if include_dir_path and include_dir_path.exists():
|
|
259
|
+
env_kw["loader"] = jinja2.FileSystemLoader(str(include_dir_path))
|
|
260
|
+
env = jinja2.Environment(**env_kw)
|
|
261
|
+
env.filters.update(filters)
|
|
262
|
+
|
|
263
|
+
# MkDocs Macros compatibility: expand Jinja2 in the YAML "title" field (plugin exception since 1.0.2)
|
|
264
|
+
title_val = variables.get("title")
|
|
265
|
+
if isinstance(title_val, str) and ("{{" in title_val or "{%" in title_val):
|
|
266
|
+
try:
|
|
267
|
+
title_template = env.from_string(title_val)
|
|
268
|
+
variables["title"] = title_template.render(**variables)
|
|
269
|
+
self.md.front_matter["title"] = variables["title"]
|
|
270
|
+
self.md.Meta["title"] = [variables["title"]]
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
if verbose:
|
|
276
|
+
logger.debug("markdown_macros: rendering template")
|
|
277
|
+
template = env.from_string(body)
|
|
278
|
+
rendered = template.render(**variables)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
if self.config.get("on_error_fail", False):
|
|
281
|
+
raise
|
|
282
|
+
if verbose:
|
|
283
|
+
logger.debug("markdown_macros: render error (swallowing): %s", e)
|
|
284
|
+
rendered = body
|
|
285
|
+
return rendered.split("\n")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class MacrosExtension(Extension):
|
|
289
|
+
"""Python-Markdown extension: Jinja2 templating with variables, macros, and filters (MkDocs-Macros style)."""
|
|
290
|
+
|
|
291
|
+
def __init__(self, **kwargs):
|
|
292
|
+
self.config = {
|
|
293
|
+
"variables": [{}, "Global variables merged into Jinja2 context"],
|
|
294
|
+
"module_name": ["", "Python module name (e.g. main) with define_env(env)"],
|
|
295
|
+
"modules": [[], "List of pluglet package names (e.g. mkdocs_macros_plugin.include)"],
|
|
296
|
+
"include_yaml": [[], "Paths to YAML files (list) or key: path (dict) to merge into variables"],
|
|
297
|
+
"include_dir": ["", "Directory for {% include 'file' %} (relative to project_root)"],
|
|
298
|
+
"project_root": [".", "Project root for resolving module, YAML, and include_dir paths"],
|
|
299
|
+
"render_by_default": [True, "If False, only render when front matter has render_macros: true"],
|
|
300
|
+
"on_error_fail": [False, "If True, raise on Jinja2 error instead of returning unchanged body"],
|
|
301
|
+
"on_undefined": ["keep", "Undefined vars: 'keep' (leave/empty) or 'strict' (raise)"],
|
|
302
|
+
"verbose": [False, "Log debug messages"],
|
|
303
|
+
"j2_block_start_string": ["{%", "Jinja2 block start"],
|
|
304
|
+
"j2_block_end_string": ["%}", "Jinja2 block end"],
|
|
305
|
+
"j2_variable_start_string": ["{{", "Jinja2 variable start"],
|
|
306
|
+
"j2_variable_end_string": ["}}", "Jinja2 variable end"],
|
|
307
|
+
"j2_comment_start_string": [None, "Jinja2 comment start (default {#)"],
|
|
308
|
+
"j2_comment_end_string": [None, "Jinja2 comment end (default #})"],
|
|
309
|
+
"j2_extensions": [[], "List of Jinja2 extension names or classes"],
|
|
310
|
+
}
|
|
311
|
+
super().__init__(**kwargs)
|
|
312
|
+
|
|
313
|
+
def extendMarkdown(self, md):
|
|
314
|
+
md.registerExtension(self)
|
|
315
|
+
md.preprocessors.register(
|
|
316
|
+
MacrosPreprocessor(md, self.getConfigs()),
|
|
317
|
+
"macros",
|
|
318
|
+
priority=20,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def make_extension(**kwargs):
|
|
323
|
+
return MacrosExtension(**kwargs)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markdown-macros-extension
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python-Markdown extension: MkDocs-Macros–style Jinja2 templating (variables, macros, filters) with YAML front matter for Zensical and MkDocs
|
|
5
|
+
Author: BarCar
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/barcar/markdown-macros#readme
|
|
8
|
+
Project-URL: Repository, https://github.com/barcar/markdown-macros
|
|
9
|
+
Keywords: markdown,frontmatter,yaml,zensical,mkdocs,macros
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: markdown>=3.4
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: jinja2>=3.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: zensical; extra == "docs"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Markdown Macros
|
|
35
|
+
|
|
36
|
+
[](https://github.com/barcar/markdown-macros/actions/workflows/pages.yml)
|
|
37
|
+
|
|
38
|
+
**Author:** [BarCar](https://github.com/barcar) · **Repository:** [github.com/barcar/markdown-macros](https://github.com/barcar/markdown-macros)
|
|
39
|
+
|
|
40
|
+
A **Python-Markdown extension** that brings [MkDocs Macros](https://mkdocs-macros-plugin.readthedocs.io/)–style **Jinja2 templating** to any Markdown pipeline: variables (config + YAML front matter), **macros**, and **filters**. Works with [Zensical](https://zensical.org), MkDocs, or plain Python—without the MkDocs plugin.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Variables** from config, YAML front matter, or `include_yaml`
|
|
45
|
+
- **Macros and filters** via `define_env(env)` in a Python module (same API as MkDocs Macros)
|
|
46
|
+
- **YAML front matter** parsed and exposed as `md.Meta` / `md.front_matter`
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install markdown-macros-extension
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires: `markdown>=3.4`, `pyyaml>=6.0`, `jinja2>=3.0`.
|
|
55
|
+
|
|
56
|
+
## Quick example
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import markdown
|
|
60
|
+
from markdown_macros import MacrosExtension
|
|
61
|
+
|
|
62
|
+
text = """---
|
|
63
|
+
title: My Page
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
# {{ title }}
|
|
67
|
+
|
|
68
|
+
The answer is {{ 6 * 7 }}.
|
|
69
|
+
"""
|
|
70
|
+
md = markdown.Markdown(extensions=[MacrosExtension()])
|
|
71
|
+
html = md.convert(text) # title and 42 rendered
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In Zensical or MkDocs, add `markdown_macros` to your Markdown extensions and optionally set `module_name`, `variables`, `include_yaml`, etc. See the [documentation](https://barcar.github.io/markdown-macros/) for configuration, usage, and the full API.
|
|
75
|
+
|
|
76
|
+
**Compatibility:** API-aligned with the [MkDocs Macros plugin](https://mkdocs-macros-plugin.readthedocs.io/) (variables, macros, filters, `define_env`); see [Compatibility](https://barcar.github.io/markdown-macros/compatibility/) in the docs.
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
**Online:** [Documentation](https://barcar.github.io/markdown-macros/) (GitHub Pages).
|
|
81
|
+
|
|
82
|
+
To build the docs locally:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install -e ".[docs]"
|
|
86
|
+
zensical serve
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
markdown_macros/__init__.py
|
|
5
|
+
markdown_macros/front_matter.py
|
|
6
|
+
markdown_macros/macros.py
|
|
7
|
+
markdown_macros_extension.egg-info/PKG-INFO
|
|
8
|
+
markdown_macros_extension.egg-info/SOURCES.txt
|
|
9
|
+
markdown_macros_extension.egg-info/dependency_links.txt
|
|
10
|
+
markdown_macros_extension.egg-info/entry_points.txt
|
|
11
|
+
markdown_macros_extension.egg-info/requires.txt
|
|
12
|
+
markdown_macros_extension.egg-info/top_level.txt
|
|
13
|
+
tests/test_front_matter.py
|
|
14
|
+
tests/test_macros.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
markdown_macros
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "markdown-macros-extension"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python-Markdown extension: MkDocs-Macros–style Jinja2 templating (variables, macros, filters) with YAML front matter for Zensical and MkDocs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{ name = "BarCar" }]
|
|
13
|
+
keywords = ["markdown", "frontmatter", "yaml", "zensical", "mkdocs", "macros"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Text Processing :: Markup",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"markdown>=3.4",
|
|
29
|
+
"pyyaml>=6.0",
|
|
30
|
+
"jinja2>=3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7",
|
|
36
|
+
"pytest-cov",
|
|
37
|
+
]
|
|
38
|
+
docs = [
|
|
39
|
+
"zensical",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.entry-points."markdown.extensions"]
|
|
43
|
+
markdown_macros = "markdown_macros:MacrosExtension"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Documentation = "https://github.com/barcar/markdown-macros#readme"
|
|
47
|
+
Repository = "https://github.com/barcar/markdown-macros"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["."]
|
|
51
|
+
include = ["markdown_macros*"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Tests for the Front Matter extension."""
|
|
2
|
+
|
|
3
|
+
import markdown
|
|
4
|
+
from markdown_macros import FrontMatterExtension
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_parses_yaml_front_matter():
|
|
8
|
+
text = """---
|
|
9
|
+
title: My Page
|
|
10
|
+
description: A short summary
|
|
11
|
+
tags:
|
|
12
|
+
- blog
|
|
13
|
+
- docs
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Hello
|
|
17
|
+
|
|
18
|
+
Content here.
|
|
19
|
+
"""
|
|
20
|
+
md = markdown.Markdown(extensions=[FrontMatterExtension()])
|
|
21
|
+
html = md.convert(text)
|
|
22
|
+
|
|
23
|
+
assert "title" in md.Meta
|
|
24
|
+
assert md.Meta["title"] == ["My Page"]
|
|
25
|
+
assert md.Meta["description"] == ["A short summary"]
|
|
26
|
+
assert md.Meta["tags"] == ["blog", "docs"]
|
|
27
|
+
|
|
28
|
+
assert md.front_matter["title"] == "My Page"
|
|
29
|
+
assert md.front_matter["description"] == "A short summary"
|
|
30
|
+
assert md.front_matter["tags"] == ["blog", "docs"]
|
|
31
|
+
|
|
32
|
+
assert "<h1>Hello</h1>" in html
|
|
33
|
+
assert "---" not in html
|
|
34
|
+
assert "title:" not in html
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_no_front_matter():
|
|
38
|
+
text = "# No front matter\n\nJust content."
|
|
39
|
+
md = markdown.Markdown(extensions=[FrontMatterExtension()])
|
|
40
|
+
html = md.convert(text)
|
|
41
|
+
|
|
42
|
+
assert getattr(md, "Meta", {}) == {} or md.Meta == {}
|
|
43
|
+
assert getattr(md, "front_matter", {}) == {} or md.front_matter == {}
|
|
44
|
+
assert "<h1>No front matter</h1>" in html
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_front_matter_extension_alone_sets_meta_and_front_matter():
|
|
48
|
+
"""FrontMatterExtension only: no Jinja2; md.Meta and md.front_matter are set."""
|
|
49
|
+
text = """---
|
|
50
|
+
title: Solo
|
|
51
|
+
count: 1
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Body
|
|
55
|
+
"""
|
|
56
|
+
md = markdown.Markdown(extensions=[FrontMatterExtension()])
|
|
57
|
+
html = md.convert(text)
|
|
58
|
+
assert md.Meta.get("title") == ["Solo"]
|
|
59
|
+
assert md.Meta.get("count") == ["1"]
|
|
60
|
+
assert md.front_matter.get("title") == "Solo"
|
|
61
|
+
assert md.front_matter.get("count") == 1
|
|
62
|
+
assert "Body" in html
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_extension_by_name():
|
|
66
|
+
text = """---
|
|
67
|
+
key: value
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
Body.
|
|
71
|
+
"""
|
|
72
|
+
html = markdown.markdown(text, extensions=["markdown_macros"])
|
|
73
|
+
# Extension runs when loaded by name; we can't easily get md.Meta from markdown()
|
|
74
|
+
# so just ensure no crash and body is present
|
|
75
|
+
assert "Body" in html
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Tests for the Macros (Jinja2) extension."""
|
|
2
|
+
|
|
3
|
+
import markdown
|
|
4
|
+
from markdown_macros import MacrosExtension
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_include_yaml_list(tmp_path):
|
|
8
|
+
"""Variables from include_yaml (list of paths) are in context."""
|
|
9
|
+
vars_file = tmp_path / "vars.yaml"
|
|
10
|
+
vars_file.write_text("from_yaml: loaded\nkey2: value2\n", encoding="utf-8")
|
|
11
|
+
text = "Result: {{ from_yaml }} and {{ key2 }}."
|
|
12
|
+
md = markdown.Markdown(
|
|
13
|
+
extensions=[
|
|
14
|
+
MacrosExtension(
|
|
15
|
+
include_yaml=[str(vars_file)],
|
|
16
|
+
project_root=str(tmp_path),
|
|
17
|
+
)
|
|
18
|
+
]
|
|
19
|
+
)
|
|
20
|
+
html = md.convert(text)
|
|
21
|
+
assert "loaded" in html
|
|
22
|
+
assert "value2" in html
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_include_yaml_dict(tmp_path):
|
|
26
|
+
"""Variables from include_yaml (dict key→path) are nested under key."""
|
|
27
|
+
data_file = tmp_path / "data.yaml"
|
|
28
|
+
data_file.write_text("nested: value\n", encoding="utf-8")
|
|
29
|
+
text = "Result: {{ data.nested }}."
|
|
30
|
+
md = markdown.Markdown(
|
|
31
|
+
extensions=[
|
|
32
|
+
MacrosExtension(
|
|
33
|
+
include_yaml={"data": str(data_file)},
|
|
34
|
+
project_root=str(tmp_path),
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
html = md.convert(text)
|
|
39
|
+
assert "value" in html
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_front_matter_as_variables():
|
|
43
|
+
text = """---
|
|
44
|
+
title: My Page
|
|
45
|
+
price: 42
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# {{ title }}
|
|
49
|
+
|
|
50
|
+
Price is {{ price }}.
|
|
51
|
+
"""
|
|
52
|
+
md = markdown.Markdown(extensions=[MacrosExtension()])
|
|
53
|
+
html = md.convert(text)
|
|
54
|
+
assert "My Page" in html
|
|
55
|
+
assert "42" in html
|
|
56
|
+
assert "{{" not in html
|
|
57
|
+
assert md.front_matter["title"] == "My Page"
|
|
58
|
+
assert md.front_matter["price"] == 42
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_config_variables():
|
|
62
|
+
text = "Unit price: {{ unit_price }}."
|
|
63
|
+
md = markdown.Markdown(
|
|
64
|
+
extensions=[MacrosExtension(variables={"unit_price": 10})]
|
|
65
|
+
)
|
|
66
|
+
html = md.convert(text)
|
|
67
|
+
assert "10" in html
|
|
68
|
+
assert "{{" not in html
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_no_jinja2_leave_unchanged():
|
|
72
|
+
text = "No variables here."
|
|
73
|
+
md = markdown.Markdown(extensions=[MacrosExtension()])
|
|
74
|
+
html = md.convert(text)
|
|
75
|
+
assert "No variables here" in html
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_render_by_default_false_skips_without_render_macros():
|
|
79
|
+
text = """---
|
|
80
|
+
title: Page
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# {{ title }}
|
|
84
|
+
"""
|
|
85
|
+
md = markdown.Markdown(
|
|
86
|
+
extensions=[MacrosExtension(render_by_default=False)]
|
|
87
|
+
)
|
|
88
|
+
html = md.convert(text)
|
|
89
|
+
# Without render_macros: true, Jinja2 is skipped
|
|
90
|
+
assert "{{ title }}" in html
|
|
91
|
+
assert md.front_matter["title"] == "Page"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_render_by_default_false_renders_when_render_macros_true():
|
|
95
|
+
text = """---
|
|
96
|
+
title: Page
|
|
97
|
+
render_macros: true
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
# {{ title }}
|
|
101
|
+
"""
|
|
102
|
+
md = markdown.Markdown(
|
|
103
|
+
extensions=[MacrosExtension(render_by_default=False)]
|
|
104
|
+
)
|
|
105
|
+
html = md.convert(text)
|
|
106
|
+
assert "Page" in html
|
|
107
|
+
assert "{{ title }}" not in html
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_title_field_jinja2_expansion():
|
|
111
|
+
"""MkDocs Macros compatibility: title in front matter can contain Jinja2."""
|
|
112
|
+
text = """---
|
|
113
|
+
title: Page for {{ product_name }}
|
|
114
|
+
product_name: Widget
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
# {{ title }}
|
|
118
|
+
|
|
119
|
+
Content.
|
|
120
|
+
"""
|
|
121
|
+
md = markdown.Markdown(extensions=[MacrosExtension()])
|
|
122
|
+
html = md.convert(text)
|
|
123
|
+
assert "Page for Widget" in html
|
|
124
|
+
assert md.front_matter["title"] == "Page for Widget"
|
|
125
|
+
assert "{{" not in html
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_on_undefined_strict_raises():
|
|
129
|
+
import pytest
|
|
130
|
+
text = "Hello {{ missing }}"
|
|
131
|
+
# on_error_fail=True so the UndefinedError propagates
|
|
132
|
+
md = markdown.Markdown(
|
|
133
|
+
extensions=[MacrosExtension(on_undefined="strict", on_error_fail=True)]
|
|
134
|
+
)
|
|
135
|
+
with pytest.raises(Exception):
|
|
136
|
+
md.convert(text)
|