markdown-environments 1.0.0__py3-none-any.whl
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_environments/__init__.py +18 -0
- markdown_environments/captioned_figure.py +165 -0
- markdown_environments/cited_blockquote.py +162 -0
- markdown_environments/div.py +130 -0
- markdown_environments/dropdown.py +196 -0
- markdown_environments/mixins.py +87 -0
- markdown_environments/thms.py +435 -0
- markdown_environments/util.py +5 -0
- markdown_environments-1.0.0.dist-info/METADATA +252 -0
- markdown_environments-1.0.0.dist-info/RECORD +12 -0
- markdown_environments-1.0.0.dist-info/WHEEL +4 -0
- markdown_environments-1.0.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
The base Markdown syntax defined by this extension is::
|
|
3
|
+
|
|
4
|
+
\begin{...}
|
|
5
|
+
...
|
|
6
|
+
\end{...}
|
|
7
|
+
|
|
8
|
+
Note that there must be a blank line before each `\\begin{}` and after each `\\end{}`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .captioned_figure import CaptionedFigureExtension
|
|
12
|
+
from .cited_blockquote import CitedBlockquoteExtension
|
|
13
|
+
from .div import DivExtension
|
|
14
|
+
from .dropdown import DropdownExtension
|
|
15
|
+
from .thms import ThmsExtension
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import xml.etree.ElementTree as etree
|
|
3
|
+
|
|
4
|
+
from markdown.blockprocessors import BlockProcessor
|
|
5
|
+
from markdown.extensions import Extension
|
|
6
|
+
|
|
7
|
+
from . import util
|
|
8
|
+
from .mixins import HtmlClassMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CaptionedFigureProcessor(BlockProcessor, HtmlClassMixin):
|
|
12
|
+
|
|
13
|
+
RE_FIGURE_START = r"^\\begin{captioned_figure}"
|
|
14
|
+
RE_FIGURE_END = r"^\\end{captioned_figure}"
|
|
15
|
+
RE_CAPTION_START = r"^\\begin{caption}"
|
|
16
|
+
RE_CAPTION_END = r"^\\end{caption}"
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, html_class: str, caption_html_class: str, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
self.init_html_class(html_class)
|
|
21
|
+
self.caption_html_class = caption_html_class
|
|
22
|
+
|
|
23
|
+
def test(self, parent, block):
|
|
24
|
+
return re.match(self.RE_FIGURE_START, block, re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
def run(self, parent, blocks):
|
|
27
|
+
org_blocks = list(blocks)
|
|
28
|
+
|
|
29
|
+
# remove figure starting delim
|
|
30
|
+
blocks[0] = re.sub(self.RE_FIGURE_START, "", blocks[0], flags=re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
# find and remove caption starting delim
|
|
33
|
+
caption_start_i = None
|
|
34
|
+
for i, block in enumerate(blocks):
|
|
35
|
+
if re.match(self.RE_CAPTION_START, block, re.MULTILINE):
|
|
36
|
+
# remove ending delim and note which block captions started on
|
|
37
|
+
# (as caption content itself is an unknown number of blocks)
|
|
38
|
+
caption_start_i = i
|
|
39
|
+
blocks[i] = re.sub(self.RE_CAPTION_START, "", block, flags=re.MULTILINE)
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
# if no starting delim for caption, restore and do nothing
|
|
43
|
+
if caption_start_i is None:
|
|
44
|
+
# `blocks = org_blocks` doesn't work since lists are passed by pointer in Python (value of reference)
|
|
45
|
+
# so changing the address of `blocks` only updates the local copy of it (the pointer)
|
|
46
|
+
# we need to change the values pointed to by `blocks` (its list elements)
|
|
47
|
+
blocks.clear()
|
|
48
|
+
blocks.extend(org_blocks)
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# find and remove caption ending delim, and extract element
|
|
52
|
+
delim_found = False
|
|
53
|
+
for i, block in enumerate(blocks[caption_start_i:], start=caption_start_i):
|
|
54
|
+
if re.search(self.RE_CAPTION_END, block, flags=re.MULTILINE):
|
|
55
|
+
delim_found = True
|
|
56
|
+
# remove ending delim
|
|
57
|
+
blocks[i] = re.sub(self.RE_CAPTION_END, "", block, flags=re.MULTILINE)
|
|
58
|
+
# build HTML for caption
|
|
59
|
+
elem_caption = etree.Element("figcaption")
|
|
60
|
+
if self.caption_html_class != "":
|
|
61
|
+
elem_caption.set("class", self.caption_html_class)
|
|
62
|
+
self.parser.parseBlocks(elem_caption, blocks[caption_start_i:i + 1])
|
|
63
|
+
# remove used blocks
|
|
64
|
+
for _ in range(caption_start_i, i + 1):
|
|
65
|
+
blocks.pop(caption_start_i)
|
|
66
|
+
break
|
|
67
|
+
# if no ending delim for caption, restore and do nothing
|
|
68
|
+
if not delim_found:
|
|
69
|
+
blocks.clear()
|
|
70
|
+
blocks.extend(org_blocks)
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# find and remove figure ending delim, and extract element
|
|
74
|
+
delim_found = False
|
|
75
|
+
for i, block in enumerate(blocks):
|
|
76
|
+
if re.search(self.RE_FIGURE_END, block, flags=re.MULTILINE):
|
|
77
|
+
delim_found = True
|
|
78
|
+
# remove ending delim
|
|
79
|
+
blocks[i] = re.sub(self.RE_FIGURE_END, "", block, flags=re.MULTILINE)
|
|
80
|
+
# build HTML for figure
|
|
81
|
+
elem_figure = etree.SubElement(parent, "figure")
|
|
82
|
+
if self.html_class != "":
|
|
83
|
+
elem_figure.set("class", self.html_class)
|
|
84
|
+
self.parser.parseBlocks(elem_figure, blocks[:i + 1])
|
|
85
|
+
elem_figure.append(elem_caption) # make sure captions come after everything else
|
|
86
|
+
# remove used blocks
|
|
87
|
+
for _ in range(i + 1):
|
|
88
|
+
blocks.pop(0)
|
|
89
|
+
break
|
|
90
|
+
# if no ending delim for figure, restore and do nothing
|
|
91
|
+
if not delim_found:
|
|
92
|
+
blocks.clear()
|
|
93
|
+
blocks.extend(org_blocks)
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CaptionedFigureExtension(Extension):
|
|
99
|
+
r"""
|
|
100
|
+
Any chunk of content, such as an image, with a caption underneath.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
.. code-block:: py
|
|
104
|
+
|
|
105
|
+
import markdown
|
|
106
|
+
from markdown_environments import CaptionedFigureExtension
|
|
107
|
+
|
|
108
|
+
input_text = ...
|
|
109
|
+
output_text = markdown.markdown(input_text, extensions=[
|
|
110
|
+
CaptionedFigureExtension(html_class="never", caption_html_class="gonna")
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
Markdown usage:
|
|
114
|
+
.. code-block:: md
|
|
115
|
+
|
|
116
|
+
\begin{captioned_figure}
|
|
117
|
+
<figure content>
|
|
118
|
+
|
|
119
|
+
\begin{caption}
|
|
120
|
+
<caption>
|
|
121
|
+
\end{caption}
|
|
122
|
+
|
|
123
|
+
\end{captioned_figure}
|
|
124
|
+
|
|
125
|
+
becomes…
|
|
126
|
+
|
|
127
|
+
.. code-block:: html
|
|
128
|
+
|
|
129
|
+
<figure class="[html_class]">
|
|
130
|
+
[figure content]
|
|
131
|
+
<figcaption class="[caption_html_class]">
|
|
132
|
+
[caption]
|
|
133
|
+
</figcaption>
|
|
134
|
+
</figure>
|
|
135
|
+
|
|
136
|
+
Note that the `caption` block can be placed anywhere within the `captioned_figure` block, as long as, of course,
|
|
137
|
+
there are blank lines before and after the `caption` block.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, **kwargs):
|
|
141
|
+
"""
|
|
142
|
+
Initialize captioned figure extension, with configuration options passed as the following keyword arguments:
|
|
143
|
+
|
|
144
|
+
- **html_class** (*str*) -- HTML `class` attribute to add to figures (default: `""`).
|
|
145
|
+
- **caption_html_class** (*str*) -- HTML `class` attribute to add to captions (default: `""`).
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
self.config = {
|
|
149
|
+
"html_class": [
|
|
150
|
+
"",
|
|
151
|
+
"HTML `class` attribute to add to captioned figure (default: `\"\"`)."
|
|
152
|
+
],
|
|
153
|
+
"caption_html_class": [
|
|
154
|
+
"",
|
|
155
|
+
"HTML `class` attribute to add to captioned figure's caption (default: `\"\"`)."
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
util.init_extension_with_configs(self, **kwargs)
|
|
159
|
+
|
|
160
|
+
def extendMarkdown(self, md):
|
|
161
|
+
md.parser.blockprocessors.register(CaptionedFigureProcessor(md.parser, **self.getConfigs()), "captioned_figure", 105)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def makeExtension(**kwargs):
|
|
165
|
+
return CaptionedFigureExtension(**kwargs)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import xml.etree.ElementTree as etree
|
|
3
|
+
|
|
4
|
+
from markdown.blockprocessors import BlockProcessor
|
|
5
|
+
from markdown.extensions import Extension
|
|
6
|
+
|
|
7
|
+
from . import util
|
|
8
|
+
from .mixins import HtmlClassMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CitedBlockquoteProcessor(BlockProcessor, HtmlClassMixin):
|
|
12
|
+
|
|
13
|
+
RE_BLOCKQUOTE_START = r"^\\begin{cited_blockquote}"
|
|
14
|
+
RE_BLOCKQUOTE_END = r"^\\end{cited_blockquote}"
|
|
15
|
+
RE_CITATION_START = r"^\\begin{citation}"
|
|
16
|
+
RE_CITATION_END = r"^\\end{citation}"
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, html_class: str, citation_html_class: str, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
self.init_html_class(html_class)
|
|
21
|
+
self.citation_html_class = citation_html_class
|
|
22
|
+
|
|
23
|
+
def test(self, parent, block):
|
|
24
|
+
return re.match(self.RE_BLOCKQUOTE_START, block, re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
def run(self, parent, blocks):
|
|
27
|
+
org_blocks = list(blocks)
|
|
28
|
+
|
|
29
|
+
# remove blockquote starting delim
|
|
30
|
+
blocks[0] = re.sub(self.RE_BLOCKQUOTE_START, "", blocks[0], flags=re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
# find and remove citation starting delim
|
|
33
|
+
delim_found = False
|
|
34
|
+
citation_start_i = None
|
|
35
|
+
for i, block in enumerate(blocks):
|
|
36
|
+
if re.match(self.RE_CITATION_START, block, re.MULTILINE):
|
|
37
|
+
delim_found = True
|
|
38
|
+
# remove ending delim and note which block citation started on
|
|
39
|
+
# (as citation content itself is an unknown number of blocks)
|
|
40
|
+
citation_start_i = i
|
|
41
|
+
blocks[i] = re.sub(self.RE_CITATION_START, "", block, flags=re.MULTILINE)
|
|
42
|
+
break
|
|
43
|
+
# if no starting delim for citation, restore and do nothing
|
|
44
|
+
if not delim_found:
|
|
45
|
+
blocks.clear()
|
|
46
|
+
blocks.extend(org_blocks)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# find and remove citation ending delim (starting search from the citation start delim), and extract element
|
|
50
|
+
delim_found = False
|
|
51
|
+
for i, block in enumerate(blocks[citation_start_i:], start=citation_start_i):
|
|
52
|
+
if re.search(self.RE_CITATION_END, block, flags=re.MULTILINE):
|
|
53
|
+
delim_found = True
|
|
54
|
+
# remove ending delim
|
|
55
|
+
blocks[i] = re.sub(self.RE_CITATION_END, "", block, flags=re.MULTILINE)
|
|
56
|
+
# build HTML for citation
|
|
57
|
+
elem_citation = etree.Element("cite")
|
|
58
|
+
if self.citation_html_class != "":
|
|
59
|
+
elem_citation.set("class", self.citation_html_class)
|
|
60
|
+
self.parser.parseBlocks(elem_citation, blocks[citation_start_i:i + 1])
|
|
61
|
+
# remove used blocks
|
|
62
|
+
for _ in range(citation_start_i, i + 1):
|
|
63
|
+
blocks.pop(citation_start_i)
|
|
64
|
+
break
|
|
65
|
+
# if no ending delim for citation, restore and do nothing
|
|
66
|
+
if not delim_found:
|
|
67
|
+
blocks.clear()
|
|
68
|
+
blocks.extend(org_blocks)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# find and remove blockquote ending delim, and extract element
|
|
72
|
+
delim_found = False
|
|
73
|
+
for i, block in enumerate(blocks):
|
|
74
|
+
if re.search(self.RE_BLOCKQUOTE_END, block, flags=re.MULTILINE):
|
|
75
|
+
delim_found = True
|
|
76
|
+
# remove ending delim
|
|
77
|
+
blocks[i] = re.sub(self.RE_BLOCKQUOTE_END, "", block, flags=re.MULTILINE)
|
|
78
|
+
# build HTML for blockquote
|
|
79
|
+
elem_blockquote = etree.SubElement(parent, "blockquote")
|
|
80
|
+
if self.html_class != "":
|
|
81
|
+
elem_blockquote.set("class", self.html_class)
|
|
82
|
+
self.parser.parseBlocks(elem_blockquote, blocks[:i + 1])
|
|
83
|
+
parent.append(elem_citation) # make sure citation comes after everything else
|
|
84
|
+
# remove used blocks
|
|
85
|
+
for _ in range(i + 1):
|
|
86
|
+
blocks.pop(0)
|
|
87
|
+
break
|
|
88
|
+
# if no ending delim for blockquote, restore and do nothing
|
|
89
|
+
if not delim_found:
|
|
90
|
+
blocks.clear()
|
|
91
|
+
blocks.extend(org_blocks)
|
|
92
|
+
return False
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CitedBlockquoteExtension(Extension):
|
|
97
|
+
r"""
|
|
98
|
+
A blockquote with a citation/quote attribution underneath.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
.. code-block:: py
|
|
102
|
+
|
|
103
|
+
import markdown
|
|
104
|
+
from markdown_environments import CitedBlockquoteExtension
|
|
105
|
+
|
|
106
|
+
input_text = ...
|
|
107
|
+
output_text = markdown.markdown(input_text, extensions=[
|
|
108
|
+
CitedBlockquoteExtension(html_class="give", citation_html_class="you")
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
Markdown usage:
|
|
112
|
+
.. code-block:: md
|
|
113
|
+
|
|
114
|
+
\begin{cited_blockquote}
|
|
115
|
+
<quote>
|
|
116
|
+
|
|
117
|
+
\begin{citation}
|
|
118
|
+
<citation>
|
|
119
|
+
\end{citation}
|
|
120
|
+
|
|
121
|
+
\end{cited_blockquote}
|
|
122
|
+
|
|
123
|
+
becomes…
|
|
124
|
+
|
|
125
|
+
.. code-block:: html
|
|
126
|
+
|
|
127
|
+
<blockquote class="[html_class]">
|
|
128
|
+
[quote]
|
|
129
|
+
</blockquote>
|
|
130
|
+
<cite class="[citation_html_class]">
|
|
131
|
+
[citation]
|
|
132
|
+
</cite>
|
|
133
|
+
|
|
134
|
+
Note that the `citation` block can be placed anywhere within the `cited_blockquote` block.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, **kwargs):
|
|
138
|
+
"""
|
|
139
|
+
Initialize cited blockquote extension, with configuration options passed as the following keyword arguments:
|
|
140
|
+
|
|
141
|
+
- **html_class** (*str*) -- HTML `class` attribute to add to blockquotes. Defaults to `""`.
|
|
142
|
+
- **citation_html_class** (*str*) -- HTML `class` attribute to add to captions. Defaults to `""`.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
self.config = {
|
|
146
|
+
"html_class": [
|
|
147
|
+
"",
|
|
148
|
+
"HTML `class` attribute to add to cited blockquote. Defaults to `\"\"`."
|
|
149
|
+
],
|
|
150
|
+
"citation_html_class": [
|
|
151
|
+
"",
|
|
152
|
+
"HTML `class` attribute to add to cited blockquote's citation. Defaults to `\"\"`."
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
util.init_extension_with_configs(self, **kwargs)
|
|
156
|
+
|
|
157
|
+
def extendMarkdown(self, md):
|
|
158
|
+
md.parser.blockprocessors.register(CitedBlockquoteProcessor(md.parser, **self.getConfigs()), "cited_blockquote", 105)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def makeExtension(**kwargs):
|
|
162
|
+
return CitedBlockquoteExtension(**kwargs)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import xml.etree.ElementTree as etree
|
|
3
|
+
|
|
4
|
+
from markdown.blockprocessors import BlockProcessor
|
|
5
|
+
from markdown.extensions import Extension
|
|
6
|
+
|
|
7
|
+
from . import util
|
|
8
|
+
from .mixins import HtmlClassMixin, ThmMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DivProcessor(BlockProcessor, HtmlClassMixin, ThmMixin):
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, types: dict, html_class: str, is_thm: bool, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.init_thm(types, is_thm)
|
|
16
|
+
self.init_html_class(html_class)
|
|
17
|
+
|
|
18
|
+
def test(self, parent, block):
|
|
19
|
+
return ThmMixin.test(self, parent, block)
|
|
20
|
+
|
|
21
|
+
def run(self, parent, blocks):
|
|
22
|
+
org_block_start = blocks[0]
|
|
23
|
+
# generate default thm heading if applicable
|
|
24
|
+
prepend = self.gen_thm_heading_md(blocks[0])
|
|
25
|
+
# remove starting delim (after generating thm heading from it, if applicable)
|
|
26
|
+
blocks[0] = re.sub(self.re_start, "", blocks[0], flags=re.MULTILINE)
|
|
27
|
+
|
|
28
|
+
# find and remove ending delim, and extract element
|
|
29
|
+
delim_found = False
|
|
30
|
+
for i, block in enumerate(blocks):
|
|
31
|
+
if re.search(self.re_end, block, flags=re.MULTILINE):
|
|
32
|
+
delim_found = True
|
|
33
|
+
# remove ending delim
|
|
34
|
+
blocks[i] = re.sub(self.re_end, "", block, flags=re.MULTILINE)
|
|
35
|
+
# build HTML
|
|
36
|
+
elem = etree.SubElement(parent, "div")
|
|
37
|
+
if self.html_class != "" or self.type_opts.get("html_class") != "":
|
|
38
|
+
elem.set("class", f"{self.html_class} {self.type_opts.get('html_class')}")
|
|
39
|
+
self.parser.parseBlocks(elem, blocks[0:i + 1])
|
|
40
|
+
# remove used blocks
|
|
41
|
+
for _ in range(0, i + 1):
|
|
42
|
+
blocks.pop(0)
|
|
43
|
+
break
|
|
44
|
+
# if no ending delim, restore and do nothing
|
|
45
|
+
if not delim_found:
|
|
46
|
+
blocks[0] = org_block_start
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# add thm heading if applicable
|
|
50
|
+
self.prepend_thm_heading_md(elem, prepend)
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DivExtension(Extension):
|
|
55
|
+
r"""
|
|
56
|
+
A general-purpose `<div>` that you can tack on HTML `class` es to.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
.. code-block:: py
|
|
60
|
+
|
|
61
|
+
import markdown
|
|
62
|
+
from markdown_environments import DivExtension
|
|
63
|
+
|
|
64
|
+
input_text = ...
|
|
65
|
+
output_text = markdown.markdown(input_text, extensions=[
|
|
66
|
+
DivExtension(html_class="up", types={
|
|
67
|
+
type1: {},
|
|
68
|
+
type2: {"html_class": "never"}
|
|
69
|
+
})
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
Markdown usage:
|
|
73
|
+
.. code-block:: md
|
|
74
|
+
|
|
75
|
+
\begin{<type>}
|
|
76
|
+
<content>
|
|
77
|
+
\end{<type>}
|
|
78
|
+
|
|
79
|
+
becomes…
|
|
80
|
+
|
|
81
|
+
.. code-block:: html
|
|
82
|
+
|
|
83
|
+
<div class="[html_class] [type's html_class]">
|
|
84
|
+
[content]
|
|
85
|
+
</div>
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, **kwargs):
|
|
89
|
+
r"""
|
|
90
|
+
Initialize div extension, with configuration options passed as the following keyword arguments:
|
|
91
|
+
|
|
92
|
+
- **types** (*dict*) -- Types of div environments to define. Defaults to `{}`.
|
|
93
|
+
- **html_class** (*str*) -- HTML `class` attribute to add to divs. Defaults to `""`.
|
|
94
|
+
|
|
95
|
+
The key for each type defined in `types` is inserted directly into the regex patterns that search for
|
|
96
|
+
`\\begin{<type>}` and `\\end{<type>}`, so anything you specify will be interpreted as regex. In addition, each
|
|
97
|
+
type's value is itself a dictionary with the following possible options:
|
|
98
|
+
|
|
99
|
+
- **html_class** (*str*) -- HTML `class` attribute to add to divs of that type. Defaults to `""`.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
self.config = {
|
|
103
|
+
"types": [
|
|
104
|
+
{},
|
|
105
|
+
"Types of div environments to define. Defaults to `{}`."
|
|
106
|
+
],
|
|
107
|
+
"html_class": [
|
|
108
|
+
"",
|
|
109
|
+
"HTML `class` attribute to add to div. Defaults to `\"\"`."
|
|
110
|
+
],
|
|
111
|
+
"is_thm": [
|
|
112
|
+
False,
|
|
113
|
+
(
|
|
114
|
+
"Whether to use theorem logic (e.g. heading); you shouldn't have to set this value."
|
|
115
|
+
"Defaults to `False`."
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
util.init_extension_with_configs(self, **kwargs)
|
|
120
|
+
|
|
121
|
+
# set default options for individual types
|
|
122
|
+
for type, opts in self.getConfig("types").items():
|
|
123
|
+
opts.setdefault("html_class", "")
|
|
124
|
+
|
|
125
|
+
def extendMarkdown(self, md):
|
|
126
|
+
md.parser.blockprocessors.register(DivProcessor(md.parser, **self.getConfigs()), "div", 105)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def makeExtension(**kwargs):
|
|
130
|
+
return DivExtension(**kwargs)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import xml.etree.ElementTree as etree
|
|
3
|
+
|
|
4
|
+
from markdown.blockprocessors import BlockProcessor
|
|
5
|
+
from markdown.extensions import Extension
|
|
6
|
+
|
|
7
|
+
from . import util
|
|
8
|
+
from .mixins import HtmlClassMixin, ThmMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DropdownProcessor(BlockProcessor, HtmlClassMixin, ThmMixin):
|
|
12
|
+
|
|
13
|
+
RE_SUMMARY_START = r"^\\begin{summary}"
|
|
14
|
+
RE_SUMMARY_END = r"^\\end{summary}"
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, types: dict, html_class: str, summary_html_class: str, content_html_class: str,
|
|
17
|
+
is_thm: bool, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.init_thm(types, is_thm)
|
|
20
|
+
self.init_html_class(html_class)
|
|
21
|
+
self.summary_html_class = summary_html_class
|
|
22
|
+
self.content_html_class = content_html_class
|
|
23
|
+
|
|
24
|
+
def test(self, parent, block):
|
|
25
|
+
return ThmMixin.test(self, parent, block)
|
|
26
|
+
|
|
27
|
+
def run(self, parent, blocks):
|
|
28
|
+
org_blocks = list(blocks)
|
|
29
|
+
# remove summary starting delim that must immediately follow dropdown's starting delim
|
|
30
|
+
# if no starting delim for summary and not a thm dropdown which should provide a default, restore and do nothing
|
|
31
|
+
if not self.is_thm and not re.match(self.RE_SUMMARY_START, blocks[1], re.MULTILINE):
|
|
32
|
+
blocks.clear() # `blocks = org_blocks` doesn't work; must mutate `blocks` instead of reassigning it
|
|
33
|
+
blocks.extend(org_blocks)
|
|
34
|
+
return False
|
|
35
|
+
blocks[1] = re.sub(self.RE_SUMMARY_START, "", blocks[1], flags=re.MULTILINE)
|
|
36
|
+
|
|
37
|
+
# remove dropdown starting delim
|
|
38
|
+
# first generate theorem heading from it to use as default summary if applicable
|
|
39
|
+
thm_heading_md = self.gen_thm_heading_md(blocks[0])
|
|
40
|
+
blocks[0] = re.sub(self.re_start, "", blocks[0], flags=re.MULTILINE)
|
|
41
|
+
|
|
42
|
+
# find and remove summary ending delim, and extract element
|
|
43
|
+
# `elem_summary` initialized outside loop since the loop isn't guaranteed here to find & initialize it
|
|
44
|
+
elem_summary = etree.Element("summary")
|
|
45
|
+
if self.summary_html_class != "":
|
|
46
|
+
elem_summary.set("class", self.summary_html_class)
|
|
47
|
+
has_valid_summary = self.is_thm
|
|
48
|
+
for i, block in enumerate(blocks):
|
|
49
|
+
# if we haven't found summary ending delim but have found the overall dropdown ending delim,
|
|
50
|
+
# then don't keep going; maybe the summary was omitted as it was optional for theorems
|
|
51
|
+
if re.search(self.re_end, block, flags=re.MULTILINE):
|
|
52
|
+
break
|
|
53
|
+
if re.search(self.RE_SUMMARY_END, block, flags=re.MULTILINE):
|
|
54
|
+
has_valid_summary = True
|
|
55
|
+
# remove ending delim
|
|
56
|
+
blocks[i] = re.sub(self.RE_SUMMARY_END, "", block, flags=re.MULTILINE)
|
|
57
|
+
# build HTML for summary
|
|
58
|
+
self.parser.parseBlocks(elem_summary, blocks[:i + 1])
|
|
59
|
+
# remove used blocks
|
|
60
|
+
for _ in range(i + 1):
|
|
61
|
+
blocks.pop(0)
|
|
62
|
+
break
|
|
63
|
+
# if no valid summary (e.g. no ending delim with no default), restore and do nothing
|
|
64
|
+
if not has_valid_summary:
|
|
65
|
+
blocks.clear()
|
|
66
|
+
blocks.extend(org_blocks)
|
|
67
|
+
return False
|
|
68
|
+
# add thm heading to summary if applicable
|
|
69
|
+
self.prepend_thm_heading_md(elem_summary, thm_heading_md)
|
|
70
|
+
|
|
71
|
+
# find and remove dropdown ending delim, and extract element
|
|
72
|
+
delim_found = False
|
|
73
|
+
for i, block in enumerate(blocks):
|
|
74
|
+
if re.search(self.re_end, block, flags=re.MULTILINE):
|
|
75
|
+
delim_found = True
|
|
76
|
+
# remove ending delim
|
|
77
|
+
blocks[i] = re.sub(self.re_end, "", block, flags=re.MULTILINE)
|
|
78
|
+
# build HTML for dropdown
|
|
79
|
+
elem_details = etree.SubElement(parent, "details")
|
|
80
|
+
if self.html_class != "" or self.type_opts.get("html_class") != "":
|
|
81
|
+
elem_details.set("class", f"{self.html_class} {self.type_opts.get('html_class')}")
|
|
82
|
+
elem_details.append(elem_summary)
|
|
83
|
+
elem_details_content = etree.SubElement(elem_details, "div")
|
|
84
|
+
if self.content_html_class != "":
|
|
85
|
+
elem_details_content.set("class", self.content_html_class)
|
|
86
|
+
self.parser.parseBlocks(elem_details_content, blocks[0:i + 1])
|
|
87
|
+
# remove used blocks
|
|
88
|
+
for _ in range(0, i + 1):
|
|
89
|
+
blocks.pop(0)
|
|
90
|
+
break
|
|
91
|
+
# if no ending delim for dropdown, restore and do nothing
|
|
92
|
+
if not delim_found:
|
|
93
|
+
blocks.clear()
|
|
94
|
+
blocks.extend(org_blocks)
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DropdownExtension(Extension):
|
|
100
|
+
r"""
|
|
101
|
+
A dropdown that can be toggled open or closed, with only a preview portion (`<summary>`) shown when closed.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
.. code-block:: py
|
|
105
|
+
|
|
106
|
+
import markdown
|
|
107
|
+
from markdown_environments import DropdownExtension
|
|
108
|
+
|
|
109
|
+
input_text = ...
|
|
110
|
+
output_text = markdown.markdown(input_text, extensions=[
|
|
111
|
+
DropdownExtension(
|
|
112
|
+
html_class="gonna", summary_html_class="let", content_html_class="you",
|
|
113
|
+
types={
|
|
114
|
+
type1: {"html_class": "down"},
|
|
115
|
+
type2: {}
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
Markdown usage:
|
|
121
|
+
.. code-block:: md
|
|
122
|
+
|
|
123
|
+
\begin{<type>}
|
|
124
|
+
|
|
125
|
+
\begin{summary}
|
|
126
|
+
<summary>
|
|
127
|
+
\end{summary}
|
|
128
|
+
|
|
129
|
+
<collapsible content>
|
|
130
|
+
\end{<type>}
|
|
131
|
+
|
|
132
|
+
becomes…
|
|
133
|
+
|
|
134
|
+
.. code-block:: html
|
|
135
|
+
|
|
136
|
+
<details class="[html_class] [type's html_class]">
|
|
137
|
+
<summary class="[summary_html_class]">
|
|
138
|
+
[summary]
|
|
139
|
+
</summary>
|
|
140
|
+
|
|
141
|
+
<div class="[content_html_class]">
|
|
142
|
+
[collapsible content]
|
|
143
|
+
</div>
|
|
144
|
+
</details>
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, **kwargs):
|
|
148
|
+
r"""
|
|
149
|
+
Initialize dropdown extension, with configuration options passed as the following keyword arguments:
|
|
150
|
+
|
|
151
|
+
- **types** (*dict*) -- Types of dropdown environments to define. Defaults to `{}`.
|
|
152
|
+
- **html_class** (*str*) -- HTML `class` attribute to add to dropdowns. Defaults to `""`.
|
|
153
|
+
- **summary_html_class** (*str*) -- HTML `class` attribute to add to dropdown summaries. Defaults to `""`.
|
|
154
|
+
- **content_html_class** (*str*) -- HTML `class` attribute to add to dropdown contents. Defaults to `""`.
|
|
155
|
+
|
|
156
|
+
The key for each type defined in `types` is inserted directly into the regex patterns that search for
|
|
157
|
+
`\\begin{<type>}` and `\\end{<type>}`, so anything you specify will be interpreted as regex. In addition, each
|
|
158
|
+
type's value is itself a dictionary with the following possible options:
|
|
159
|
+
|
|
160
|
+
- **html_class** (*str*) -- HTML `class` attribute to add to dropdowns of that type. Defaults to `""`.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
self.config = {
|
|
164
|
+
"types": [
|
|
165
|
+
{},
|
|
166
|
+
"Types of dropdown environments to define. Defaults to `{}`."
|
|
167
|
+
],
|
|
168
|
+
"html_class": [
|
|
169
|
+
"",
|
|
170
|
+
"HTML `class` attribute to add to dropdown. Defaults to `\"\"`."
|
|
171
|
+
],
|
|
172
|
+
"summary_html_class": [
|
|
173
|
+
"",
|
|
174
|
+
"HTML `class` attribute to add to dropdown summary. Defaults to `\"\"`."
|
|
175
|
+
],
|
|
176
|
+
"content_html_class": [
|
|
177
|
+
"",
|
|
178
|
+
"HTML `class` attribute to add to dropdown content. Defaults to `\"\"`."
|
|
179
|
+
],
|
|
180
|
+
"is_thm": [
|
|
181
|
+
False,
|
|
182
|
+
"Whether to use theorem logic (e.g. heading); used only by `ThmExtension`. Defaults to `False`."
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
util.init_extension_with_configs(self, **kwargs)
|
|
186
|
+
|
|
187
|
+
# set default options for individual types
|
|
188
|
+
for type, opts in self.getConfig("types").items():
|
|
189
|
+
opts.setdefault("html_class", "")
|
|
190
|
+
|
|
191
|
+
def extendMarkdown(self, md):
|
|
192
|
+
md.parser.blockprocessors.register(DropdownProcessor(md.parser, **self.getConfigs()), "dropdown", 105)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def makeExtension(**kwargs):
|
|
196
|
+
return DropdownExtension(**kwargs)
|