MDit 0.0.0.dev0__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.
- mdit-0.0.0.dev0/PKG-INFO +11 -0
- mdit-0.0.0.dev0/README.md +1 -0
- mdit-0.0.0.dev0/pyproject.toml +31 -0
- mdit-0.0.0.dev0/setup.cfg +4 -0
- mdit-0.0.0.dev0/src/MDit.egg-info/PKG-INFO +11 -0
- mdit-0.0.0.dev0/src/MDit.egg-info/SOURCES.txt +14 -0
- mdit-0.0.0.dev0/src/MDit.egg-info/dependency_links.txt +1 -0
- mdit-0.0.0.dev0/src/MDit.egg-info/not-zip-safe +1 -0
- mdit-0.0.0.dev0/src/MDit.egg-info/requires.txt +7 -0
- mdit-0.0.0.dev0/src/MDit.egg-info/top_level.txt +1 -0
- mdit-0.0.0.dev0/src/mdit/__init__.py +6 -0
- mdit-0.0.0.dev0/src/mdit/container.py +71 -0
- mdit-0.0.0.dev0/src/mdit/display.py +52 -0
- mdit-0.0.0.dev0/src/mdit/element.py +558 -0
- mdit-0.0.0.dev0/src/mdit/parse.py +31 -0
- mdit-0.0.0.dev0/src/mdit/render.py +396 -0
mdit-0.0.0.dev0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: MDit
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Requires-Dist: IPython
|
|
6
|
+
Requires-Dist: PyProtocol
|
|
7
|
+
Requires-Dist: markdown-it-py
|
|
8
|
+
Requires-Dist: mdit-py-plugins
|
|
9
|
+
Requires-Dist: linkify-it-py
|
|
10
|
+
Requires-Dist: readme-renderer
|
|
11
|
+
Requires-Dist: cmarkgfm
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MDit
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools>=61.0", "versioningit"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ----------------------------------------- setuptools -------------------------------------------
|
|
8
|
+
[tool.setuptools]
|
|
9
|
+
include-package-data = true
|
|
10
|
+
zip-safe = false
|
|
11
|
+
|
|
12
|
+
[tool.setuptools.packages.find]
|
|
13
|
+
where = ["src"]
|
|
14
|
+
namespaces = true
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ----------------------------------------- Project Metadata -------------------------------------
|
|
18
|
+
#
|
|
19
|
+
[project]
|
|
20
|
+
version = "0.0.0.dev0"
|
|
21
|
+
name = "MDit"
|
|
22
|
+
requires-python = ">=3.10"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"IPython",
|
|
25
|
+
"PyProtocol",
|
|
26
|
+
"markdown-it-py",
|
|
27
|
+
"mdit-py-plugins",
|
|
28
|
+
"linkify-it-py",
|
|
29
|
+
"readme-renderer",
|
|
30
|
+
"cmarkgfm",
|
|
31
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: MDit
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Requires-Dist: IPython
|
|
6
|
+
Requires-Dist: PyProtocol
|
|
7
|
+
Requires-Dist: markdown-it-py
|
|
8
|
+
Requires-Dist: mdit-py-plugins
|
|
9
|
+
Requires-Dist: linkify-it-py
|
|
10
|
+
Requires-Dist: readme-renderer
|
|
11
|
+
Requires-Dist: cmarkgfm
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/MDit.egg-info/PKG-INFO
|
|
4
|
+
src/MDit.egg-info/SOURCES.txt
|
|
5
|
+
src/MDit.egg-info/dependency_links.txt
|
|
6
|
+
src/MDit.egg-info/not-zip-safe
|
|
7
|
+
src/MDit.egg-info/requires.txt
|
|
8
|
+
src/MDit.egg-info/top_level.txt
|
|
9
|
+
src/mdit/__init__.py
|
|
10
|
+
src/mdit/container.py
|
|
11
|
+
src/mdit/display.py
|
|
12
|
+
src/mdit/element.py
|
|
13
|
+
src/mdit/parse.py
|
|
14
|
+
src/mdit/render.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mdit
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Type as _Type
|
|
2
|
+
|
|
3
|
+
from pyprotocol import Stringable as _Stringable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ContentType = _Stringable
|
|
7
|
+
ContentInputType = (
|
|
8
|
+
dict[str | int, ContentType]
|
|
9
|
+
| list[ContentType]
|
|
10
|
+
| tuple[ContentType]
|
|
11
|
+
| None
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Container(_Stringable):
|
|
16
|
+
|
|
17
|
+
def __init__(self, *unlabeled_contents: ContentType, **labeled_contents: ContentType):
|
|
18
|
+
self._data = {}
|
|
19
|
+
self.add(*unlabeled_contents, **labeled_contents)
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
def add(self, *unlabeled_contents: ContentType, **labeled_contents: ContentType) -> list[int] | None:
|
|
23
|
+
if labeled_contents:
|
|
24
|
+
for key, value in labeled_contents.items():
|
|
25
|
+
if key in self._data:
|
|
26
|
+
raise ValueError("Key already exists in content.")
|
|
27
|
+
self._data[key] = value
|
|
28
|
+
if unlabeled_contents:
|
|
29
|
+
first_available_int_key = max(key for key in self._data.keys() if isinstance(key, int)) + 1
|
|
30
|
+
for idx, elem in enumerate(unlabeled_contents):
|
|
31
|
+
self._data[first_available_int_key + idx] = elem
|
|
32
|
+
return list(range(first_available_int_key, first_available_int_key + len(unlabeled_contents)))
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def get(self, key: str | int, default=None):
|
|
36
|
+
return self._data.get(key, default)
|
|
37
|
+
|
|
38
|
+
def keys(self):
|
|
39
|
+
return self._data.keys()
|
|
40
|
+
|
|
41
|
+
def values(self):
|
|
42
|
+
return self._data.values()
|
|
43
|
+
|
|
44
|
+
def items(self):
|
|
45
|
+
return self._data.items()
|
|
46
|
+
|
|
47
|
+
def __getitem__(self, item):
|
|
48
|
+
return self._data[item]
|
|
49
|
+
|
|
50
|
+
def __setitem__(self, key, value):
|
|
51
|
+
self._data[key] = value
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
def __contains__(self, item):
|
|
55
|
+
return item in self._data
|
|
56
|
+
|
|
57
|
+
def __bool__(self):
|
|
58
|
+
return bool(self._data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create(
|
|
62
|
+
content: ContentInputType,
|
|
63
|
+
container_class: _Type[Container] = Container
|
|
64
|
+
) -> Container:
|
|
65
|
+
if not content:
|
|
66
|
+
return container_class()
|
|
67
|
+
if isinstance(content, dict):
|
|
68
|
+
return container_class(**content)
|
|
69
|
+
if isinstance(content, (list, tuple)):
|
|
70
|
+
return container_class(*content)
|
|
71
|
+
return container_class(content)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Display HTML and Markdown content in web browser or IPython notebook."""
|
|
2
|
+
|
|
3
|
+
import webbrowser as _webbrowser
|
|
4
|
+
import tempfile as _tempfile
|
|
5
|
+
import time as _time
|
|
6
|
+
from pathlib import Path as _Path
|
|
7
|
+
|
|
8
|
+
from IPython import display as _display
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def browser(content: str) -> None:
|
|
12
|
+
"""Display HTML content in a web browser.
|
|
13
|
+
|
|
14
|
+
This function writes the content to a temporary file and opens it in the system's default web browser.
|
|
15
|
+
It then waits for 10 seconds (ensuring the browser has enough time to load the content)
|
|
16
|
+
before deleting the temporary file.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
content : str
|
|
21
|
+
HTML content to display.
|
|
22
|
+
"""
|
|
23
|
+
with _tempfile.NamedTemporaryFile('w', delete=False, suffix='.html') as temp_file:
|
|
24
|
+
temp_file.write(content)
|
|
25
|
+
temp_file.flush()
|
|
26
|
+
temp_filepath = temp_file.name
|
|
27
|
+
_webbrowser.open(f'file://{temp_filepath}')
|
|
28
|
+
_time.sleep(10)
|
|
29
|
+
_Path(temp_filepath).unlink()
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ipython(content: str, as_md: bool = False) -> None:
|
|
34
|
+
"""Display HTML or Markdown content in an IPython notebook.
|
|
35
|
+
|
|
36
|
+
This function uses the `IPython.display` module to render the content
|
|
37
|
+
in the current cell of an IPython notebook.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
content : str
|
|
42
|
+
HTML or Markdown content to display.
|
|
43
|
+
as_md : bool, default: False
|
|
44
|
+
If True, the function uses the `IPython.display.Markdown` renderer,
|
|
45
|
+
otherwise (by default) it uses the `IPython.display.HTML` renderer
|
|
46
|
+
"""
|
|
47
|
+
if ipython:
|
|
48
|
+
renderer = _display.Markdown if as_md else _display.HTML
|
|
49
|
+
_display.display(renderer(content))
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
return
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING as _TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if _TYPE_CHECKING:
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Element:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
block: bool,
|
|
12
|
+
leaf: bool = True,
|
|
13
|
+
contents: _ElementContentInputType = None,
|
|
14
|
+
newlines_before: int | None = None,
|
|
15
|
+
newlines_after: int | None = None,
|
|
16
|
+
):
|
|
17
|
+
super().__init__(contents=contents)
|
|
18
|
+
self._block = block
|
|
19
|
+
self._leaf = leaf
|
|
20
|
+
self.newlines_before = newlines_before
|
|
21
|
+
self.newlines_after = newlines_after
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
if not self._block:
|
|
26
|
+
if any(not isinstance(elem, str) for elem in self._content.values()):
|
|
27
|
+
raise ValueError("Inline elements must have string content.")
|
|
28
|
+
elif self._leaf:
|
|
29
|
+
if any(isinstance(elem, Element) and elem.is_block for elem in self._content.values()):
|
|
30
|
+
raise ValueError("Leaf block elements cannot contain block content.")
|
|
31
|
+
content = "".join(str(elem) for elem in self._content.values())
|
|
32
|
+
md = self._md.replace("${{content}}", content)
|
|
33
|
+
newlines_before, newlines_after = [
|
|
34
|
+
newlines_count if isinstance(newlines_count, int) else (1 if self._block else 0)
|
|
35
|
+
for newlines_count in (self.newlines_before, self.newlines_after)
|
|
36
|
+
]
|
|
37
|
+
return f"{newlines_before * '\n'}{md}{newlines_after * '\n'}"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def _md(self) -> str:
|
|
41
|
+
return "${{content}}"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_block(self) -> bool:
|
|
45
|
+
return self._block
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_leaf(self) -> bool:
|
|
49
|
+
return self._leaf
|
|
50
|
+
|
|
51
|
+
def display(self, ipython: bool = True, as_md: bool = True) -> None:
|
|
52
|
+
"""Display the element in an IPython notebook."""
|
|
53
|
+
super().display(ipython=ipython, as_md=as_md)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ThematicBreak(Element):
|
|
58
|
+
def __init__(self, char: Literal["-", "_", "*"] = "-"):
|
|
59
|
+
super().__init__(block=True)
|
|
60
|
+
self.char = char
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def char(self):
|
|
65
|
+
return self._char
|
|
66
|
+
|
|
67
|
+
@char.setter
|
|
68
|
+
def char(self, value: _Literal["-", "_", "*"]):
|
|
69
|
+
if value not in ("-", "_", "*"):
|
|
70
|
+
raise ValueError("Invalid thematic break character.")
|
|
71
|
+
self._char = value
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def _md(self) -> str:
|
|
76
|
+
return self.char * 3
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ATXHeading(Element):
|
|
80
|
+
def __init__(self, level: _Literal[1, 2, 3, 4, 5, 6], contents: _ElementContentInputType = ""):
|
|
81
|
+
super().__init__(block=True, leaf=True, contents=contents)
|
|
82
|
+
self._level = level
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def level(self):
|
|
87
|
+
return self._level
|
|
88
|
+
|
|
89
|
+
@level.setter
|
|
90
|
+
def level(self, value: _Literal[1, 2, 3, 4, 5, 6]):
|
|
91
|
+
if value not in (1, 2, 3, 4, 5, 6):
|
|
92
|
+
raise ValueError("Invalid heading level.")
|
|
93
|
+
self._level = value
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def _md(self) -> str:
|
|
98
|
+
return f"{'#' * self.level} ${{content}}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class FieldListElement(Element):
|
|
102
|
+
def __init__(self, name: _ElementContentType, body: _ElementContentInputType = "", indent_size: int = 4):
|
|
103
|
+
super().__init__(block=True, leaf=False, contents=body)
|
|
104
|
+
self.name = name
|
|
105
|
+
self.indent_size = indent_size
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def _md(self) -> str:
|
|
110
|
+
body = "".join(str(elem) for elem in self._content.values())
|
|
111
|
+
first_line, *lines = body.strip().split("\n")
|
|
112
|
+
body = "\n".join([first_line] + [f"{' ' * self.indent_size}{line}" for line in lines])
|
|
113
|
+
return f":{self.name}: {body}".strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class FieldList(Element):
|
|
117
|
+
def __init__(self, elements: list[FieldListElement], indent_size: int = 4):
|
|
118
|
+
super().__init__(block=True, leaf=False)
|
|
119
|
+
self.indent_size = indent_size
|
|
120
|
+
self.elements = elements
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def _md(self) -> str:
|
|
125
|
+
elements_md = []
|
|
126
|
+
for elem in self.elements:
|
|
127
|
+
indent_orig = elem.indent_size
|
|
128
|
+
elem.indent_size = self.indent_size
|
|
129
|
+
elements_md.append(elem._md)
|
|
130
|
+
elem.indent_size = indent_orig
|
|
131
|
+
return "\n".join(elements_md)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class HTMLBlock(Element):
|
|
135
|
+
|
|
136
|
+
def __init__(self, contents: _ElementContentInputType = None):
|
|
137
|
+
super().__init__(block=True, leaf=True, contents=contents, newlines_after=2)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def _md(self) -> str:
|
|
142
|
+
return "${{content}}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class CodeFence(Element):
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
contents: _ElementContentInputType = None,
|
|
150
|
+
info: _Stringable = "",
|
|
151
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
152
|
+
):
|
|
153
|
+
super().__init__(block=True, leaf=False, contents=contents)
|
|
154
|
+
self._info = info
|
|
155
|
+
self.fence = fence
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def info(self) -> _Stringable:
|
|
160
|
+
return self._info
|
|
161
|
+
|
|
162
|
+
@info.setter
|
|
163
|
+
def info(self, value: _Stringable):
|
|
164
|
+
if "\n" in str(value):
|
|
165
|
+
raise ValueError("Info string cannot contain newlines.")
|
|
166
|
+
self._info = value
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def fence(self):
|
|
171
|
+
return self._fence
|
|
172
|
+
|
|
173
|
+
@fence.setter
|
|
174
|
+
def fence(self, value: _Literal["`", "~", ":"]):
|
|
175
|
+
if value not in ("`", "~", ":"):
|
|
176
|
+
raise ValueError("Invalid code fence character.")
|
|
177
|
+
self._fence = value
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def _md(self) -> str:
|
|
182
|
+
return f"{self._start_line}\n${{content}}\n{self._end_line}"
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def fence_count(self):
|
|
186
|
+
return max(
|
|
187
|
+
[child.fence_count for child in self.content.values() if isinstance(child, CodeFence)],
|
|
188
|
+
default=3
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def _start_line(self) -> str:
|
|
193
|
+
return f"{self.fence * self.fence_count}{self.info}"
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def _end_line(self) -> str:
|
|
197
|
+
return self.fence * self.fence_count
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Directive(CodeFence):
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
name: _Stringable,
|
|
204
|
+
contents: _ElementContentInputType = None,
|
|
205
|
+
arg: _Stringable = "",
|
|
206
|
+
options: dict[_Stringable, _Stringable] | None = None,
|
|
207
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
208
|
+
):
|
|
209
|
+
super().__init__(contents=contents, fence=fence)
|
|
210
|
+
self.name = name
|
|
211
|
+
self.arg = arg
|
|
212
|
+
self.options = options or {}
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def info(self) -> str:
|
|
217
|
+
return f"{{{self.name}}} {self.arg}"
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def _md(self) -> str:
|
|
221
|
+
options = []
|
|
222
|
+
for key, value in self.options.items():
|
|
223
|
+
val_str = str(value) if value is not None else ""
|
|
224
|
+
if "\n" in val_str:
|
|
225
|
+
val_content = "\n".join(f"{' ' * 4}{line}" for line in val_str.split("\n"))
|
|
226
|
+
val_str = f"|\n{val_content}"
|
|
227
|
+
options.append(f":{key}: {val_str}")
|
|
228
|
+
options = "\n".join(options)
|
|
229
|
+
options_section = f"{options}\n\n" if options else ""
|
|
230
|
+
return f"{self._start_line}\n{options_section}${{content}}\n{self._end_line}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def thematic_break(char: _Literal["-", "_", "*"] = "-") -> ThematicBreak:
|
|
234
|
+
"""Create a [thematic break](https://github.github.com/gfm/#thematic-break).
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
char : {'*', '_', '-'}, default: '-'
|
|
239
|
+
Thematic break character.
|
|
240
|
+
"""
|
|
241
|
+
return ThematicBreak(char=char)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def heading(level: _Literal[1, 2, 3, 4, 5, 6], content: _ElementContentInputType = "") -> ATXHeading:
|
|
245
|
+
"""Create an ATX heading.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
level : {1, 2, 3, 4, 5, 6}
|
|
250
|
+
Heading level.
|
|
251
|
+
content : str
|
|
252
|
+
Heading content.
|
|
253
|
+
"""
|
|
254
|
+
return ATXHeading(level=level, contents=content)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def field_list_element(
|
|
258
|
+
name: _ElementContentType,
|
|
259
|
+
body: _ElementContentInputType = "",
|
|
260
|
+
indent_size: int = 4,
|
|
261
|
+
) -> FieldListElement:
|
|
262
|
+
"""Create a field list element.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
name : ElementContentType
|
|
267
|
+
Field name.
|
|
268
|
+
body : ElementContentInputType, optional
|
|
269
|
+
Field body.
|
|
270
|
+
indent_size : int, default: 4
|
|
271
|
+
Indent size.
|
|
272
|
+
"""
|
|
273
|
+
return FieldListElement(name=name, body=body, indent_size=indent_size)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def field_list(
|
|
277
|
+
elements: list[FieldListElement | tuple[_ElementContentType, _ElementContentInputType]],
|
|
278
|
+
indent_size: int = 4,
|
|
279
|
+
) -> FieldList:
|
|
280
|
+
"""Create a field list.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
elements : list[FieldListElement]
|
|
285
|
+
Field list elements.
|
|
286
|
+
indent_size : int, default: 4
|
|
287
|
+
Indent size.
|
|
288
|
+
"""
|
|
289
|
+
elements = [
|
|
290
|
+
elem if isinstance(elem, FieldListElement) else field_list_element(name=elem[0], body=elem[1])
|
|
291
|
+
for elem in elements
|
|
292
|
+
]
|
|
293
|
+
return FieldList(elements=elements, indent_size=indent_size)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def html_block(content: _ElementContentInputType = None) -> HTMLBlock:
|
|
297
|
+
"""Create an [HTML block](https://github.github.com/gfm/#html-block).
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
content : ElementContentInputType, optional
|
|
302
|
+
HTML content.
|
|
303
|
+
"""
|
|
304
|
+
return HTMLBlock(contents=content)
|
|
305
|
+
|
|
306
|
+
def code_fence(
|
|
307
|
+
content: _ElementContentInputType = None,
|
|
308
|
+
info: _Stringable = "",
|
|
309
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
310
|
+
) -> CodeFence:
|
|
311
|
+
"""Create a [fenced code block](https://github.github.com/gfm/#fenced-code-block).
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
content : ElementContentInputType, optional
|
|
316
|
+
Code block content.
|
|
317
|
+
info : Stringable, optional
|
|
318
|
+
Code block [info string](https://github.github.com/gfm/#info-string).
|
|
319
|
+
fence: {'`', '~', ':'}, default: '`'
|
|
320
|
+
Fence character.
|
|
321
|
+
"""
|
|
322
|
+
return CodeFence(contents=content, info=info, fence=fence)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def directive(
|
|
326
|
+
name: _Stringable,
|
|
327
|
+
content: _ElementContentInputType = None,
|
|
328
|
+
arg: _Stringable = "",
|
|
329
|
+
options: dict[_Stringable, _Stringable] | None = None,
|
|
330
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
331
|
+
) -> Directive:
|
|
332
|
+
"""Create a directive.
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
name : Stringable
|
|
337
|
+
Directive name.
|
|
338
|
+
content : ElementContentInputType, optional
|
|
339
|
+
Directive content.
|
|
340
|
+
arg : Stringable, optional
|
|
341
|
+
Directive argument.
|
|
342
|
+
options : dict[Stringable, Stringable], optional
|
|
343
|
+
Directive options.
|
|
344
|
+
fence: {'`', '~', ':'}, default: '`'
|
|
345
|
+
Fence character.
|
|
346
|
+
"""
|
|
347
|
+
return Directive(name=name, contents=content, arg=arg, options=options, fence=fence)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def admonition(
|
|
351
|
+
title: _ElementContentType,
|
|
352
|
+
content: _ElementContentInputType,
|
|
353
|
+
class_: str | list[str] | None = None,
|
|
354
|
+
name: _Stringable | None = None,
|
|
355
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
356
|
+
) -> Directive:
|
|
357
|
+
"""Create a [MyST admonition](https://myst-parser.readthedocs.io/en/latest/syntax/admonitions.html).
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
title : ElementContentType
|
|
362
|
+
Admonition title.
|
|
363
|
+
content : ElementContentInputType
|
|
364
|
+
Admonition content.
|
|
365
|
+
class_ : str | list[str], optional
|
|
366
|
+
CSS class names to add to the admonition. These must conform to the
|
|
367
|
+
[identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
|
|
368
|
+
name : Stringable, optional
|
|
369
|
+
A reference target name for the admonition
|
|
370
|
+
(for [cross-referencing](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#syntax-referencing)).
|
|
371
|
+
fence: {'`', '~', ':'}, default: '`'
|
|
372
|
+
Fence character.
|
|
373
|
+
"""
|
|
374
|
+
options = process_directive_options({"class": class_, "name": name})
|
|
375
|
+
return Directive(name="admonition", contents=content, arg=title, options=options, fence=fence)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def code_block(
|
|
379
|
+
language: str | None,
|
|
380
|
+
content: _ElementContentType,
|
|
381
|
+
caption: _ElementContentType | None = None,
|
|
382
|
+
class_: str | list[str] | None = None,
|
|
383
|
+
name: _Stringable | None = None,
|
|
384
|
+
lineno_start: int | None = None,
|
|
385
|
+
emphasize_lines: list[int] | None = None,
|
|
386
|
+
force: bool = False,
|
|
387
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
388
|
+
):
|
|
389
|
+
"""Create a MyST [code block directive](https://myst-parser.readthedocs.io/en/latest/syntax/code_and_apis.html#adding-a-caption).
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
language : str, optional
|
|
394
|
+
Language of the code, e.g. 'python', 'json', 'bash', 'html'.
|
|
395
|
+
content : ElementContentType
|
|
396
|
+
Code to be included in the code block.
|
|
397
|
+
caption : ElementContentType, optional
|
|
398
|
+
Caption for the code block.
|
|
399
|
+
class_ : list[str], optional
|
|
400
|
+
CSS class names to add to the code block. These must conform to the
|
|
401
|
+
[identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
|
|
402
|
+
name : Stringable, optional
|
|
403
|
+
A reference target name for the code block
|
|
404
|
+
(for [cross-referencing](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#syntax-referencing)).
|
|
405
|
+
lineno_start : int, optional
|
|
406
|
+
Starting line number for the code block.
|
|
407
|
+
emphasize_lines : list[int], optional
|
|
408
|
+
Line numbers to highlight in the code block.
|
|
409
|
+
Note that `lineno-start` must be set for this to work.
|
|
410
|
+
force : bool, default: False
|
|
411
|
+
Allow minor errors on highlighting to be ignored.
|
|
412
|
+
fence: {'`', '~', ':'}, default: '`'
|
|
413
|
+
Fence character.
|
|
414
|
+
"""
|
|
415
|
+
options = process_directive_options(
|
|
416
|
+
{k: v for k, v in locals() if k not in ("language", "content", "fence")}
|
|
417
|
+
)
|
|
418
|
+
return Directive(name="code-block", contents=content, arg=language, options=options, fence=fence)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def tab_item(
|
|
422
|
+
title: _Stringable,
|
|
423
|
+
content: _ElementContentInputType,
|
|
424
|
+
selected: bool = False,
|
|
425
|
+
name: _Stringable | None = None,
|
|
426
|
+
sync: _Stringable | None = None,
|
|
427
|
+
class_container: str | list[str] | None = None,
|
|
428
|
+
class_label: str | list[str] | None = None,
|
|
429
|
+
class_content: str | list[str] | None = None,
|
|
430
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
431
|
+
) -> Directive:
|
|
432
|
+
"""Create a [Sphinx-Design tab item](https://sphinx-design.readthedocs.io/en/furo-theme/tabs.html).
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
title : Stringable
|
|
437
|
+
Tab title.
|
|
438
|
+
content : ElementContentInputType
|
|
439
|
+
Tab content.
|
|
440
|
+
selected : bool, default: False
|
|
441
|
+
Whether the tab item is selected by default.
|
|
442
|
+
name : Stringable, optional
|
|
443
|
+
A reference target name for the tab item
|
|
444
|
+
(for [cross-referencing](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#syntax-referencing)).
|
|
445
|
+
sync : Stringable, optional
|
|
446
|
+
A key that is used to sync the selected tab across multiple tab-sets.
|
|
447
|
+
class_container : str | list[str], optional
|
|
448
|
+
CSS class names to add to the container element. These must conform to the
|
|
449
|
+
[identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
|
|
450
|
+
class_label : str | list[str], optional
|
|
451
|
+
CSS class names to add to the label element. These must conform to the
|
|
452
|
+
[identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
|
|
453
|
+
class_content : str | list[str], optional
|
|
454
|
+
CSS class names to add to the content element. These must conform to the
|
|
455
|
+
[identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
|
|
456
|
+
fence: {'`', '~', ':'}, default: '`'
|
|
457
|
+
Fence character.
|
|
458
|
+
"""
|
|
459
|
+
options = process_directive_options(
|
|
460
|
+
{k: v for k, v in locals() if k not in ("title", "content", "fence")}
|
|
461
|
+
)
|
|
462
|
+
return Directive(name="tab-item", contents=content, arg=title, options=options, fence=fence)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def tab_set(
|
|
466
|
+
content: list[Directive],
|
|
467
|
+
class_: list[str] | None = None,
|
|
468
|
+
sync_group: _Stringable | None = None,
|
|
469
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
470
|
+
) -> Directive:
|
|
471
|
+
"""Create a [Sphinx-Design tab set](https://sphinx-design.readthedocs.io/en/furo-theme/tabs.html).
|
|
472
|
+
|
|
473
|
+
Parameters
|
|
474
|
+
----------
|
|
475
|
+
content : list[Directive]
|
|
476
|
+
Tab items.
|
|
477
|
+
class_ : list[str], optional
|
|
478
|
+
CSS class names to add to the tab set. These must conform to the
|
|
479
|
+
[identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
|
|
480
|
+
sync_group : Stringable, optional
|
|
481
|
+
Group name for synchronized tab sets.
|
|
482
|
+
fence: {'`', '~', ':'}, default: '`'
|
|
483
|
+
Fence character.
|
|
484
|
+
"""
|
|
485
|
+
options = process_directive_options(
|
|
486
|
+
{k: v for k, v in locals() if k not in ("content", "fence")}
|
|
487
|
+
)
|
|
488
|
+
return Directive(name="tab-set", contents=content, options=options, fence=fence)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def card(
|
|
492
|
+
header_content: _ElementContentInputType = None,
|
|
493
|
+
body_content: _ElementContentInputType = None,
|
|
494
|
+
footer_content: _ElementContentInputType = None,
|
|
495
|
+
body_title: _Stringable = "",
|
|
496
|
+
width: _Literal["auto"] | int | None = None,
|
|
497
|
+
margin: _Literal["auto", 0, 1, 2, 3, 4, 5] | tuple[_Literal["auto", 0, 1, 2, 3, 4, 5], ...] | None = None,
|
|
498
|
+
text_align: _Literal["left", "center", "right", "justify"] | None = None,
|
|
499
|
+
img_background: _Stringable | None = None,
|
|
500
|
+
img_top: _Stringable | None = None,
|
|
501
|
+
img_bottom: _Stringable | None = None,
|
|
502
|
+
img_alt: _Stringable | None = None,
|
|
503
|
+
link: _Stringable | None = None,
|
|
504
|
+
link_type: _Literal["url", "ref", "doc", "any"] | None = None,
|
|
505
|
+
link_alt: _Stringable | None = None,
|
|
506
|
+
shadow: _Literal["sm", "md", "lg", "none"] | None = None,
|
|
507
|
+
class_card: list[str] | None = None,
|
|
508
|
+
class_header: list[str] | None = None,
|
|
509
|
+
class_body: list[str] | None = None,
|
|
510
|
+
class_footer: list[str] | None = None,
|
|
511
|
+
class_title: list[str] | None = None,
|
|
512
|
+
class_img_top: list[str] | None = None,
|
|
513
|
+
class_img_bottom: list[str] | None = None,
|
|
514
|
+
fence: _Literal["`", "~", ":"] = "`",
|
|
515
|
+
) -> Directive:
|
|
516
|
+
|
|
517
|
+
def process_content(content, key_prefix: str):
|
|
518
|
+
if isinstance(content, (list, tuple)):
|
|
519
|
+
return {f"{key_prefix}_{idx}": elem for idx, elem in enumerate(content)}
|
|
520
|
+
if not isinstance(content, dict):
|
|
521
|
+
return {key_prefix: content}
|
|
522
|
+
return content
|
|
523
|
+
|
|
524
|
+
options = process_directive_options(
|
|
525
|
+
{
|
|
526
|
+
k: v for k, v in locals() if k not in (
|
|
527
|
+
"header_content", "body_content", "footer_content", "body_title", "fence"
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
content = {}
|
|
532
|
+
if header_content:
|
|
533
|
+
for header_id, header in process_content(header_content, "header"):
|
|
534
|
+
content[header_id] = header
|
|
535
|
+
content["header_body_separator"] = "^^^"
|
|
536
|
+
if body_content:
|
|
537
|
+
for body_id, body in process_content(body_content, "body"):
|
|
538
|
+
content[body_id] = body
|
|
539
|
+
if footer_content:
|
|
540
|
+
content["body_footer_separator"] = "+++"
|
|
541
|
+
for footer_id, footer in process_content(footer_content, "footer"):
|
|
542
|
+
content[footer_id] = footer
|
|
543
|
+
return Directive(name="card", contents=content, arg=body_title, options=options, fence=fence)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def process_directive_options(options: dict) -> dict:
|
|
547
|
+
final_options = {}
|
|
548
|
+
for key, val in options.items():
|
|
549
|
+
if val is None or val is False:
|
|
550
|
+
continue
|
|
551
|
+
if isinstance(val, (list, tuple)):
|
|
552
|
+
val = " ".join([str(e) for e in val])
|
|
553
|
+
elif isinstance(val, bool):
|
|
554
|
+
val = ""
|
|
555
|
+
key_name = str(key).removesuffix("_").replace("_", "-")
|
|
556
|
+
final_options[key_name] = val
|
|
557
|
+
return final_options
|
|
558
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re as _re
|
|
3
|
+
|
|
4
|
+
import pyserials as _ps
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def frontmatter(file_content: str) -> dict:
|
|
8
|
+
match = _re.match(r'^---+\s*\n(.*?)(?=\n---+\s*(\n|$))', file_content, _re.DOTALL)
|
|
9
|
+
if not match:
|
|
10
|
+
return {}
|
|
11
|
+
frontmatter_text = match.group(1).strip()
|
|
12
|
+
frontmatter_dict = _ps.read.yaml_from_string(frontmatter_text)
|
|
13
|
+
return frontmatter_dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def title(file_content: str) -> str | None:
|
|
17
|
+
match = _re.search(r"^# (.*)", file_content, _re.MULTILINE)
|
|
18
|
+
return match.group(1) if match else ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def toctree(file_content: str) -> tuple[str, ...] | None:
|
|
22
|
+
matches = _re.findall(r"(:{3,}){toctree}\s((.|\s)*?)\s\1", file_content, _re.DOTALL)
|
|
23
|
+
if not matches:
|
|
24
|
+
return
|
|
25
|
+
toctree_str = matches[0][1]
|
|
26
|
+
toctree_entries = []
|
|
27
|
+
for line in toctree_str.splitlines():
|
|
28
|
+
entry = line.strip()
|
|
29
|
+
if entry and not entry.startswith(":"):
|
|
30
|
+
toctree_entries.append(entry)
|
|
31
|
+
return tuple(toctree_entries)
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
from typing import Literal as _Literal, Callable as _Callable
|
|
2
|
+
from functools import partial as _partial
|
|
3
|
+
import cmarkgfm as _gfm_pypi
|
|
4
|
+
from readme_renderer import markdown as _readme_renderer_md, clean as _readme_renderer_clean
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import markdown_it as _mdit
|
|
10
|
+
import markdown_it.utils as _mdit_utils
|
|
11
|
+
|
|
12
|
+
from mdit_py_plugins.amsmath import amsmath_plugin as _mdit_plugin_amsmath
|
|
13
|
+
from mdit_py_plugins.anchors import anchors_plugin as _mdit_plugin_anchors
|
|
14
|
+
from mdit_py_plugins.attrs import attrs_block_plugin as _mdit_plugin_attrs_block, attrs_plugin as _mdit_plugin_attrs
|
|
15
|
+
from mdit_py_plugins.colon_fence import colon_fence_plugin as _mdit_plugin_colon_fence
|
|
16
|
+
from mdit_py_plugins.deflist import deflist_plugin as _mdit_plugin_deflist
|
|
17
|
+
from mdit_py_plugins.dollarmath import dollarmath_plugin as _mdit_plugin_dollarmath
|
|
18
|
+
from mdit_py_plugins.field_list import fieldlist_plugin as _mdit_plugin_fieldlist
|
|
19
|
+
from mdit_py_plugins.footnote import footnote_plugin as _mdit_plugin_footnote
|
|
20
|
+
from mdit_py_plugins.front_matter import front_matter_plugin as _mdit_plugin_front_matter
|
|
21
|
+
from mdit_py_plugins.myst_blocks import myst_block_plugin as _mdit_plugin_myst_block
|
|
22
|
+
from mdit_py_plugins.myst_role import myst_role_plugin as _mdit_plugin_myst_role
|
|
23
|
+
from mdit_py_plugins.substitution import substitution_plugin as _mdit_plugin_substitution
|
|
24
|
+
from mdit_py_plugins.tasklists import tasklists_plugin as _mdit_plugin_tasklists
|
|
25
|
+
from mdit_py_plugins.wordcount import wordcount_plugin as _mdit_plugin_wordcount
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# from myst_parser.config.main import MdParserConfig
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_html(
|
|
32
|
+
source: str,
|
|
33
|
+
components_core: set[
|
|
34
|
+
_Literal['block', 'inline', 'linkify', 'normalize', 'replacements', 'smartquotes', 'text_join']
|
|
35
|
+
] = ('block', 'inline', 'linkify', 'normalize', 'replacements', 'smartquotes', 'text_join'),
|
|
36
|
+
components_block: set[
|
|
37
|
+
_Literal[
|
|
38
|
+
'blockquote',
|
|
39
|
+
'code',
|
|
40
|
+
'fence',
|
|
41
|
+
'heading',
|
|
42
|
+
'hr',
|
|
43
|
+
'html_block',
|
|
44
|
+
'lheading',
|
|
45
|
+
'list',
|
|
46
|
+
'paragraph',
|
|
47
|
+
'reference',
|
|
48
|
+
'table',
|
|
49
|
+
]
|
|
50
|
+
] = (
|
|
51
|
+
'blockquote',
|
|
52
|
+
'code',
|
|
53
|
+
'fence',
|
|
54
|
+
'heading',
|
|
55
|
+
'hr',
|
|
56
|
+
'html_block',
|
|
57
|
+
'lheading',
|
|
58
|
+
'list',
|
|
59
|
+
'paragraph',
|
|
60
|
+
'reference',
|
|
61
|
+
'table',
|
|
62
|
+
),
|
|
63
|
+
components_inline: set[
|
|
64
|
+
_Literal[
|
|
65
|
+
'autolink',
|
|
66
|
+
'backticks',
|
|
67
|
+
'balance_pairs',
|
|
68
|
+
'emphasis',
|
|
69
|
+
'entity',
|
|
70
|
+
'escape',
|
|
71
|
+
'fragments_join',
|
|
72
|
+
'html_inline',
|
|
73
|
+
'image',
|
|
74
|
+
'link',
|
|
75
|
+
'linkify',
|
|
76
|
+
'newline',
|
|
77
|
+
'strikethrough',
|
|
78
|
+
'text'
|
|
79
|
+
]
|
|
80
|
+
] = (
|
|
81
|
+
'autolink',
|
|
82
|
+
'backticks',
|
|
83
|
+
'balance_pairs',
|
|
84
|
+
'emphasis',
|
|
85
|
+
'entity',
|
|
86
|
+
'escape',
|
|
87
|
+
'fragments_join',
|
|
88
|
+
'html_inline',
|
|
89
|
+
'image',
|
|
90
|
+
'link',
|
|
91
|
+
'linkify',
|
|
92
|
+
'newline',
|
|
93
|
+
'strikethrough',
|
|
94
|
+
'text'
|
|
95
|
+
),
|
|
96
|
+
plugins: set[_Callable | tuple[_Callable, dict]] = (
|
|
97
|
+
_partial(_mdit_plugin_amsmath, renderer=None),
|
|
98
|
+
_partial(
|
|
99
|
+
_mdit_plugin_anchors,
|
|
100
|
+
min_level=1,
|
|
101
|
+
max_level=6,
|
|
102
|
+
slug_func=None,
|
|
103
|
+
permalink=True,
|
|
104
|
+
permalinkSymbol='¶',
|
|
105
|
+
permalinkBefore=False,
|
|
106
|
+
permalinkSpace=True,
|
|
107
|
+
),
|
|
108
|
+
_partial(
|
|
109
|
+
_mdit_plugin_attrs,
|
|
110
|
+
after=('image', 'code_inline', 'link_close', 'span_close'),
|
|
111
|
+
spans=True,
|
|
112
|
+
span_after='link',
|
|
113
|
+
),
|
|
114
|
+
_mdit_plugin_attrs_block,
|
|
115
|
+
_mdit_plugin_colon_fence,
|
|
116
|
+
_mdit_plugin_deflist,
|
|
117
|
+
_partial(
|
|
118
|
+
_mdit_plugin_dollarmath,
|
|
119
|
+
allow_labels=True,
|
|
120
|
+
allow_space=True,
|
|
121
|
+
allow_digits=True,
|
|
122
|
+
allow_blank_lines=True,
|
|
123
|
+
double_inline=False,
|
|
124
|
+
),
|
|
125
|
+
_mdit_plugin_fieldlist,
|
|
126
|
+
_partial(_mdit_plugin_footnote, inline=True, move_to_end=True, always_match_refs=True),
|
|
127
|
+
_mdit_plugin_front_matter,
|
|
128
|
+
_mdit_plugin_myst_block,
|
|
129
|
+
_mdit_plugin_myst_role,
|
|
130
|
+
_partial(_mdit_plugin_substitution, start_delimiter='{', end_delimiter='}'),
|
|
131
|
+
_partial(_mdit_plugin_tasklists, enabled=False, label=False, label_after=False),
|
|
132
|
+
_partial(_mdit_plugin_wordcount, per_minute=200, store_text=False),
|
|
133
|
+
),
|
|
134
|
+
env: dict | None = None,
|
|
135
|
+
html: bool = True,
|
|
136
|
+
linkify: bool = True,
|
|
137
|
+
linkify_fuzzy_links: bool = True,
|
|
138
|
+
typographer: bool = True,
|
|
139
|
+
quotes: str = '“”‘’',
|
|
140
|
+
xhtml_out: bool = True,
|
|
141
|
+
breaks: bool = True,
|
|
142
|
+
lang_prefix: str = 'language-',
|
|
143
|
+
highlight: _Callable[[str, str, str], str] = None,
|
|
144
|
+
):
|
|
145
|
+
"""Convert Markdown to HTML using the [`markdown-it-py`](https://markdown-it-py.readthedocs.io/) library.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
source : str
|
|
150
|
+
Markdown source text to convert to HTML.
|
|
151
|
+
components_core : set of {'block', 'inline', 'linkify', 'normalize', 'replacements', 'smartquotes', 'text_join'}
|
|
152
|
+
Enabled core components
|
|
153
|
+
(cf. [`markdown-it-py` source code](https://github.com/executablebooks/markdown-it-py/tree/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_core)).
|
|
154
|
+
components_block : set of {'blockquote', 'code', 'fence', 'heading', 'hr', 'html_block', 'lheading', 'list', 'paragraph', 'reference', 'table'}
|
|
155
|
+
Enabled block components
|
|
156
|
+
(cf. [`markdown-it-py` source code](https://github.com/executablebooks/markdown-it-py/tree/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_block)).
|
|
157
|
+
components_inline : set of {'autolink', 'backticks', 'emphasis', 'entity', 'escape', 'html_inline', 'image', 'link', 'linkify', 'newline', 'strikethrough', 'text'}
|
|
158
|
+
Enabled inline components
|
|
159
|
+
(cf. [`markdown-it-py` source code](https://github.com/executablebooks/markdown-it-py/tree/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_inline)).
|
|
160
|
+
plugins : set of Callable[[MarkdownIt], None] or tuple[Callable[[MarkdownIt], None], dict], default: (front_matter_plugin,)
|
|
161
|
+
List of plugins to apply to the parser.
|
|
162
|
+
Each entry can either be a callable, or a tuple of a callable and a dictionary of keyword arguments.
|
|
163
|
+
The callable should take as its first argument the `MarkdownIt` parser instance,
|
|
164
|
+
followed by any additional arguments.
|
|
165
|
+
By default, all plugins from the [`mdit_py_plugins` library](https://mdit-py-plugins.readthedocs.io)
|
|
166
|
+
(cf. [source code](https://github.com/executablebooks/mdit-py-plugins/tree/d11bdaf0979e6fae01c35db5a4d1f6a4b4dd8843/mdit_py_plugins))
|
|
167
|
+
except for `admon_plugin`, `container_plugin`, and `texmath_plugin`
|
|
168
|
+
are enabled with their default configurations.
|
|
169
|
+
env : dict, optional
|
|
170
|
+
Environment variables to pass to the parser.
|
|
171
|
+
It is used to pass data between “distributed” rules and return additional metadata
|
|
172
|
+
like reference info, needed for the renderer.
|
|
173
|
+
It can also be used to inject data, e.g., when using the `substitution_plugin`.
|
|
174
|
+
html : bool, default: True
|
|
175
|
+
Allow raw HTML tags in the source text.
|
|
176
|
+
linkify : bool, default: True
|
|
177
|
+
Automatically convert URL-like text to links using the
|
|
178
|
+
[`linkify-it-py`](https://github.com/tsutsu3/linkify-it-py) library.
|
|
179
|
+
linkify_fuzzy_links : bool, default: True
|
|
180
|
+
Enable fuzzy link detection for `linkify`.
|
|
181
|
+
This allows URLs without a protocol schema (e.g., `repodynamics.com`) to be detected as links.
|
|
182
|
+
typographer : bool, default: True
|
|
183
|
+
Enable smartquotes and replacements.
|
|
184
|
+
This will automatically add the `smartquotes` and `replacements` core components as well.
|
|
185
|
+
quotes : str, default: '“”‘’'
|
|
186
|
+
Quote characters.
|
|
187
|
+
xhtml_out : bool, default: True
|
|
188
|
+
Use '/' to close single tags (e.g., `<br />`).
|
|
189
|
+
breaks : bool, default: True
|
|
190
|
+
Convert newlines in paragraphs into `<br>` tags.
|
|
191
|
+
lang_prefix : str, default: 'language-'
|
|
192
|
+
CSS language prefix for fenced blocks.
|
|
193
|
+
highlight: Callable[[str, str, str], str] or None, default: None
|
|
194
|
+
An optional highlighter function `f(content, language, attributes) -> str`
|
|
195
|
+
to apply syntax highlighting to code blocks.
|
|
196
|
+
|
|
197
|
+
References
|
|
198
|
+
----------
|
|
199
|
+
- [`markdown-it` Parser options](https://markdown-it-py.readthedocs.io/en/latest/api/markdown_it.utils.html#markdown_it.utils.OptionsType)
|
|
200
|
+
"""
|
|
201
|
+
options = _mdit_utils.OptionsType(
|
|
202
|
+
maxNesting=50,
|
|
203
|
+
html=html,
|
|
204
|
+
linkify=linkify,
|
|
205
|
+
typographer=typographer,
|
|
206
|
+
quotes=quotes,
|
|
207
|
+
xhtmlOut=xhtml_out,
|
|
208
|
+
breaks=breaks,
|
|
209
|
+
langPrefix=lang_prefix,
|
|
210
|
+
highlight=highlight,
|
|
211
|
+
)
|
|
212
|
+
inline_rules = []
|
|
213
|
+
inline_rules2 = []
|
|
214
|
+
# For some reason (?!), `markdown-it-py` has 'inline' and 'inline2' rules.
|
|
215
|
+
# Enabling 'emphasis' and 'strikethrough' adds them to both 'inline' and 'inline2',
|
|
216
|
+
# while enabling 'balance_pairs' and 'fragments_join' only adds them to 'inline2'.
|
|
217
|
+
# All other components are only added to 'inline'.
|
|
218
|
+
# See: https://markdown-it-py.readthedocs.io/en/latest/using.html#the-parser
|
|
219
|
+
# Code: https://github.com/executablebooks/markdown-it-py/blob/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/parser_inline.py#L38-L51
|
|
220
|
+
# For simplicity we have merged 'inline' and 'inline2' inputs into the 'components_inline' parameter.
|
|
221
|
+
# Now we need to separate them again.
|
|
222
|
+
for rule in components_inline:
|
|
223
|
+
if rule in ("emphasis", "strikethrough"):
|
|
224
|
+
inline_rules.append(rule)
|
|
225
|
+
inline_rules2.append(rule)
|
|
226
|
+
elif rule in ("balance_pairs", "fragments_join"):
|
|
227
|
+
inline_rules2.append(rule)
|
|
228
|
+
else:
|
|
229
|
+
inline_rules.append(rule)
|
|
230
|
+
components = {
|
|
231
|
+
"core": {"rules": list(components_core)},
|
|
232
|
+
"block": {"rules": list(components_block)},
|
|
233
|
+
"inline": {"rules": inline_rules, "rules2": inline_rules2},
|
|
234
|
+
}
|
|
235
|
+
config = _mdit_utils.PresetType(options=options, components=components)
|
|
236
|
+
parser = _mdit.MarkdownIt(config=config)
|
|
237
|
+
if typographer:
|
|
238
|
+
parser.enable(["replacements", "smartquotes"])
|
|
239
|
+
if parser.linkify is not None:
|
|
240
|
+
parser.linkify.set({"fuzzy_link": linkify_fuzzy_links})
|
|
241
|
+
for plugin in plugins:
|
|
242
|
+
if isinstance(plugin, (list, tuple)):
|
|
243
|
+
plugin_func, plugin_config = plugin
|
|
244
|
+
else:
|
|
245
|
+
plugin_func = plugin
|
|
246
|
+
plugin_config = {}
|
|
247
|
+
parser.use(plugin_func, **plugin_config)
|
|
248
|
+
return parser.render(src=source, env=env)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# def create_md_parser(
|
|
253
|
+
# config: MdParserConfig, renderer: Callable[[MarkdownIt], RendererProtocol]
|
|
254
|
+
# ) -> MarkdownIt:
|
|
255
|
+
# """Return a Markdown parser with the required MyST configuration."""
|
|
256
|
+
#
|
|
257
|
+
# md.options.update(
|
|
258
|
+
# {
|
|
259
|
+
# "typographer": typographer,
|
|
260
|
+
# "linkify": "linkify" in config.enable_extensions,
|
|
261
|
+
# "myst_config": config,
|
|
262
|
+
# }
|
|
263
|
+
# )
|
|
264
|
+
#
|
|
265
|
+
# return md
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def gfm_to_html_pypi(
|
|
269
|
+
source: str,
|
|
270
|
+
extensions: tuple[str, ...] = ('autolink', 'strikethrough', 'table', 'tagfilter', 'tasklist'),
|
|
271
|
+
unsafe: bool = True,
|
|
272
|
+
smart: bool = False,
|
|
273
|
+
normalize: bool = False,
|
|
274
|
+
hard_breaks: bool = False,
|
|
275
|
+
no_breaks: bool = False,
|
|
276
|
+
source_pos: bool = False,
|
|
277
|
+
footnotes: bool = False,
|
|
278
|
+
validate_utf8: bool = False,
|
|
279
|
+
github_pre_lang: bool = True,
|
|
280
|
+
liberal_html_tag: bool = False,
|
|
281
|
+
strikethrough_double_tilde: bool = False,
|
|
282
|
+
table_prefer_style_attributes: bool = False,
|
|
283
|
+
highlight_code: bool = True,
|
|
284
|
+
sanitize: bool = True,
|
|
285
|
+
):
|
|
286
|
+
"""Convert CommonMark or GitHub Flavored Markdown to HTML
|
|
287
|
+
using the [CMarkGFM](https://github.com/theacodes/cmarkgfm) library.
|
|
288
|
+
|
|
289
|
+
CMarkGFM is the Markdown to HTML converter
|
|
290
|
+
used by the Python Packaging Authority (PyPA)'s
|
|
291
|
+
[`readme_renderer`](https://github.com/pypa/readme_renderer) library to render
|
|
292
|
+
[package READMEs on PYPI](https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme).
|
|
293
|
+
Using this function with the default arguments will exactly replicate the rendering
|
|
294
|
+
used by PyPI.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
source : str
|
|
299
|
+
GitHub Flavored Markdown source text to convert to HTML.
|
|
300
|
+
extensions : Sequence[str], default: ('autolink', 'strikethrough', 'table', 'tagfilter', 'tasklist')
|
|
301
|
+
List of extensions to enable on top of the CommonMark specifications.
|
|
302
|
+
The default value enables all GitHub Flavored Markdown extensions,
|
|
303
|
+
which are currently the only [available extensions in CMarkGFM](https://github.com/theacodes/cmarkgfm/blob/66b131cee950ad30cad9dfbf7f2360270ed105b8/src/cmarkgfm/cmark.py#L118C13-L118C74).
|
|
304
|
+
unsafe: bool, default: True
|
|
305
|
+
Allow rendering unsafe HTML (e.g., `<script>` elements)
|
|
306
|
+
and URLs (e.g., those starting with `javascript:`, `vbscript:`, `file:`, or `data:` (except for `image/png`, `image/gif`, `image/jpeg`, or `image/webp` media types)).
|
|
307
|
+
If set to False, raw HTML is replaced by a placeholder comment and
|
|
308
|
+
potentially dangerous URLs are replaced by an empty string.
|
|
309
|
+
smart: bool, default: False
|
|
310
|
+
Render smart punctuation.
|
|
311
|
+
This is roughly equivalent to the `smartquotes` and `replacements` typographic components
|
|
312
|
+
in [`markdown-it`](https://markdown-it-py.readthedocs.io/en/latest/using.html#typographic-components),
|
|
313
|
+
e.g., converting basic quote marks to their opening and closing variants, and `--` and `---`
|
|
314
|
+
to en-dash `–` and em-dash `—`, respectively.
|
|
315
|
+
normalize: bool, default: False
|
|
316
|
+
Consolidate adjacent text nodes.
|
|
317
|
+
hard_breaks: bool, default: False
|
|
318
|
+
Render line breaks within paragraphs as `<br>` tags.
|
|
319
|
+
no_breaks: bool, default: False
|
|
320
|
+
Render soft line breaks as spaces.
|
|
321
|
+
source_pos: bool, default: False
|
|
322
|
+
Add attribute `data-sourcepos` to HTML tags
|
|
323
|
+
indicating the corresponding line/column ranges in the input.
|
|
324
|
+
footnotes: bool, default: False
|
|
325
|
+
Parse footnotes.
|
|
326
|
+
validate_utf8: bool, default: False
|
|
327
|
+
Validate UTF-8 in the input before parsing,
|
|
328
|
+
replacing illegal sequences with the replacement character `U+FFFD`.
|
|
329
|
+
github_pre_lang: bool, default: True
|
|
330
|
+
Use GitHub style for indicating the language of code blocks.
|
|
331
|
+
If True (default), the code block's language defined in its info string will be used
|
|
332
|
+
as the value of the `lang` attribute of the `<pre>` element
|
|
333
|
+
(e.g., `<pre lang="python"><code>...</code></pre>`),
|
|
334
|
+
otherwise it will be used as the value of the `class` attribute of the `<code>` element
|
|
335
|
+
according to [highlight.js](https://highlightjs.org/) style
|
|
336
|
+
(e.g., `<pre><code class="language-python">...</code></pre>`).
|
|
337
|
+
liberal_html_tag: bool, default: False
|
|
338
|
+
Be liberal in interpreting inline HTML tags.
|
|
339
|
+
strikethrough_double_tilde: bool, default: False
|
|
340
|
+
Only parse strikethroughs if surrounded by exactly 2 tildes.
|
|
341
|
+
Gives some compatibility with redcarpet.
|
|
342
|
+
table_prefer_style_attributes: bool, default: False
|
|
343
|
+
Use style attributes to align table cells instead of align attributes.
|
|
344
|
+
highlight_code : bool, default: True
|
|
345
|
+
Apply syntax highlighting to code blocks using the [`Pygments`](https://pygments.org/) library.
|
|
346
|
+
This exactly replicates the rendering used in PyPI.
|
|
347
|
+
However, notice that `readme_renderer` uses a naive RegEx to detect `<pre>` HTML elements.
|
|
348
|
+
Thus, this may not work on custom-written `<pre>` elements
|
|
349
|
+
(i.e., those not generated from Markdown by CMarkGFM in the previous step).
|
|
350
|
+
sanitize : bool, default: True
|
|
351
|
+
Sanitize the HTML output using the [`nh3`](https://nh3.readthedocs.io/en/latest/)
|
|
352
|
+
library to remove potentially dangerous content.
|
|
353
|
+
PyPI uses this to prevent XSS attacks by allowing only a
|
|
354
|
+
[subset of HTML tags](https://github.com/pypa/readme_renderer/blob/1d0497c37a6033d791c74e800590dcd0d34f6e08/readme_renderer/clean.py#L20-L31)
|
|
355
|
+
and [attributes](https://github.com/pypa/readme_renderer/blob/1d0497c37a6033d791c74e800590dcd0d34f6e08/readme_renderer/clean.py#L33-L65).
|
|
356
|
+
|
|
357
|
+
Notes
|
|
358
|
+
-----
|
|
359
|
+
- [`twine check`](https://twine.readthedocs.io/en/stable/#twine-check) only works for
|
|
360
|
+
reStructuredText (reST) READMEs; it always passes for Markdown content
|
|
361
|
+
(cf. [`twine.commands.check._RENDERERS`](https://github.com/pypa/twine/blob/4f7cd66fa1ceba7f8de5230d3d4ebea0787f17e5/twine/commands/check.py#L32-L37))
|
|
362
|
+
and thus cannot be used to validate Markdown.
|
|
363
|
+
|
|
364
|
+
References
|
|
365
|
+
----------
|
|
366
|
+
- [`cmarkgfm.cmark` module](https://github.com/theacodes/cmarkgfm/blob/66b131cee950ad30cad9dfbf7f2360270ed105b8/src/cmarkgfm/cmark.py)
|
|
367
|
+
- [`readme_renderer.markdown` module](https://github.com/pypa/readme_renderer/blob/1d0497c37a6033d791c74e800590dcd0d34f6e08/readme_renderer/markdown.py)
|
|
368
|
+
"""
|
|
369
|
+
options = 0
|
|
370
|
+
for arg, cmark_arg in (
|
|
371
|
+
(unsafe, _gfm_pypi.Options.CMARK_OPT_UNSAFE),
|
|
372
|
+
(smart, _gfm_pypi.Options.CMARK_OPT_SMART),
|
|
373
|
+
(normalize, _gfm_pypi.Options.CMARK_OPT_NORMALIZE),
|
|
374
|
+
(hard_breaks, _gfm_pypi.Options.CMARK_OPT_HARDBREAKS),
|
|
375
|
+
(no_breaks, _gfm_pypi.Options.CMARK_OPT_NOBREAKS),
|
|
376
|
+
(source_pos, _gfm_pypi.Options.CMARK_OPT_SOURCEPOS),
|
|
377
|
+
(footnotes, _gfm_pypi.Options.CMARK_OPT_FOOTNOTES),
|
|
378
|
+
(validate_utf8, _gfm_pypi.Options.CMARK_OPT_VALIDATE_UTF8),
|
|
379
|
+
(github_pre_lang, _gfm_pypi.Options.CMARK_OPT_GITHUB_PRE_LANG),
|
|
380
|
+
(liberal_html_tag, _gfm_pypi.Options.CMARK_OPT_LIBERAL_HTML_TAG),
|
|
381
|
+
(strikethrough_double_tilde, _gfm_pypi.Options.CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE),
|
|
382
|
+
(table_prefer_style_attributes, _gfm_pypi.Options.CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES),
|
|
383
|
+
):
|
|
384
|
+
if arg:
|
|
385
|
+
options |= cmark_arg
|
|
386
|
+
|
|
387
|
+
html_syntax: str = _gfm_pypi.markdown_to_html_with_extensions(
|
|
388
|
+
text=source,
|
|
389
|
+
options=options,
|
|
390
|
+
extensions=extensions,
|
|
391
|
+
)
|
|
392
|
+
if highlight_code:
|
|
393
|
+
html_syntax = _readme_renderer_md._highlight(html_syntax)
|
|
394
|
+
if sanitize:
|
|
395
|
+
html_syntax = _readme_renderer_clean.clean(html_syntax)
|
|
396
|
+
return html_syntax
|