|
a |
|
b/docs/scripts/cards.py |
|
|
1 |
""" |
|
|
2 |
Adapted from pymdownx.tabbed (https://github.com/facelessuser/pymdown-extensions/) |
|
|
3 |
""" |
|
|
4 |
import re |
|
|
5 |
import xml.etree.ElementTree as etree |
|
|
6 |
|
|
|
7 |
from markdown import Extension |
|
|
8 |
from markdown.blockprocessors import BlockProcessor |
|
|
9 |
from markdown.extensions.attr_list import AttrListTreeprocessor, get_attrs |
|
|
10 |
|
|
|
11 |
|
|
|
12 |
def assign_attrs(elem, attrs): |
|
|
13 |
"""Assign `attrs` to element.""" |
|
|
14 |
for k, v in get_attrs(attrs): |
|
|
15 |
if k == ".": |
|
|
16 |
# add to class |
|
|
17 |
cls = elem.get("class") |
|
|
18 |
if cls: |
|
|
19 |
elem.set("class", "{} {}".format(cls, v)) |
|
|
20 |
else: |
|
|
21 |
elem.set("class", v) |
|
|
22 |
else: |
|
|
23 |
# assign attribute `k` with `v` |
|
|
24 |
elem.set(AttrListTreeprocessor.NAME_RE.sub("_", k), v) |
|
|
25 |
|
|
|
26 |
|
|
|
27 |
class CardProcessor(BlockProcessor): |
|
|
28 |
"""card block processor.""" |
|
|
29 |
|
|
|
30 |
START = re.compile(r"(?:^|\n)={3} *(card)?(?: +({:.*?}) *(?:\n|$))?") |
|
|
31 |
COMPRESS_SPACES = re.compile(r" {2,}") |
|
|
32 |
|
|
|
33 |
def __init__(self, parser, config): |
|
|
34 |
"""Initialize.""" |
|
|
35 |
|
|
|
36 |
super().__init__(parser) |
|
|
37 |
self.card_group_count = 0 |
|
|
38 |
self.current_sibling = None |
|
|
39 |
self.content_indention = 0 |
|
|
40 |
|
|
|
41 |
def detab_by_length(self, text, length): |
|
|
42 |
"""Remove a card from the front of each line of the given text.""" |
|
|
43 |
|
|
|
44 |
newtext = [] |
|
|
45 |
lines = text.split("\n") |
|
|
46 |
for line in lines: |
|
|
47 |
if line.startswith(" " * length): |
|
|
48 |
newtext.append(line[length:]) |
|
|
49 |
elif not line.strip(): |
|
|
50 |
newtext.append("") # pragma: no cover |
|
|
51 |
else: |
|
|
52 |
break |
|
|
53 |
return "\n".join(newtext), "\n".join(lines[len(newtext) :]) |
|
|
54 |
|
|
|
55 |
def parse_content(self, parent, block): |
|
|
56 |
""" |
|
|
57 |
Get sibling card. |
|
|
58 |
|
|
|
59 |
Retrieve the appropriate sibling element. This can get tricky when |
|
|
60 |
dealing with lists. |
|
|
61 |
|
|
|
62 |
""" |
|
|
63 |
|
|
|
64 |
old_block = block |
|
|
65 |
non_cards = "" |
|
|
66 |
card_set = "card-set" |
|
|
67 |
|
|
|
68 |
# We already acquired the block via test |
|
|
69 |
if self.current_sibling is not None: |
|
|
70 |
sibling = self.current_sibling |
|
|
71 |
block, non_cards = self.detab_by_length(block, self.content_indent) |
|
|
72 |
self.current_sibling = None |
|
|
73 |
self.content_indent = 0 |
|
|
74 |
return sibling, block, non_cards |
|
|
75 |
|
|
|
76 |
sibling = self.lastChild(parent) |
|
|
77 |
|
|
|
78 |
if ( |
|
|
79 |
sibling is None |
|
|
80 |
or sibling.tag.lower() != "div" |
|
|
81 |
or sibling.attrib.get("class", "") != card_set |
|
|
82 |
): |
|
|
83 |
sibling = None |
|
|
84 |
else: |
|
|
85 |
# If the last child is a list and the content is indented sufficient |
|
|
86 |
# to be under it, then the content's is sibling is in the list. |
|
|
87 |
last_child = self.lastChild(sibling) |
|
|
88 |
card_content = "card-content" |
|
|
89 |
child_class = ( |
|
|
90 |
last_child.attrib.get("class", "") if last_child is not None else "" |
|
|
91 |
) |
|
|
92 |
indent = 0 |
|
|
93 |
while last_child is not None: |
|
|
94 |
if ( |
|
|
95 |
sibling is not None |
|
|
96 |
and block.startswith(" " * self.tab_length * 2) |
|
|
97 |
and last_child is not None |
|
|
98 |
and ( |
|
|
99 |
last_child.tag in ("ul", "ol", "dl") |
|
|
100 |
or (last_child.tag == "div" and child_class == card_content) |
|
|
101 |
) |
|
|
102 |
): |
|
|
103 |
# Handle nested card content |
|
|
104 |
if last_child.tag == "div" and child_class == card_content: |
|
|
105 |
temp_child = self.lastChild(last_child) |
|
|
106 |
if temp_child is None or temp_child.tag not in ( |
|
|
107 |
"ul", |
|
|
108 |
"ol", |
|
|
109 |
"dl", |
|
|
110 |
): |
|
|
111 |
break |
|
|
112 |
last_child = temp_child |
|
|
113 |
|
|
|
114 |
# The expectation is that we'll find an `<li>`. |
|
|
115 |
# We should get it's last child as well. |
|
|
116 |
sibling = self.lastChild(last_child) |
|
|
117 |
last_child = ( |
|
|
118 |
self.lastChild(sibling) if sibling is not None else None |
|
|
119 |
) |
|
|
120 |
child_class = ( |
|
|
121 |
last_child.attrib.get("class", "") |
|
|
122 |
if last_child is not None |
|
|
123 |
else "" |
|
|
124 |
) |
|
|
125 |
|
|
|
126 |
# Context has been lost at this point, so we must adjust the |
|
|
127 |
# text's indentation level so it will be evaluated correctly |
|
|
128 |
# under the list. |
|
|
129 |
block = block[self.tab_length :] |
|
|
130 |
indent += self.tab_length |
|
|
131 |
else: |
|
|
132 |
last_child = None |
|
|
133 |
|
|
|
134 |
if not block.startswith(" " * self.tab_length): |
|
|
135 |
sibling = None |
|
|
136 |
|
|
|
137 |
if sibling is not None: |
|
|
138 |
indent += self.tab_length |
|
|
139 |
block, non_cards = self.detab_by_length(old_block, indent) |
|
|
140 |
self.current_sibling = sibling |
|
|
141 |
self.content_indent = indent |
|
|
142 |
|
|
|
143 |
return sibling, block, non_cards |
|
|
144 |
|
|
|
145 |
def test(self, parent, block): |
|
|
146 |
"""Test block.""" |
|
|
147 |
|
|
|
148 |
if self.START.search(block): |
|
|
149 |
return True |
|
|
150 |
else: |
|
|
151 |
return self.parse_content(parent, block)[0] is not None |
|
|
152 |
|
|
|
153 |
def run(self, parent, blocks): |
|
|
154 |
"""Convert to card block.""" |
|
|
155 |
|
|
|
156 |
block = blocks.pop(0) |
|
|
157 |
m = self.START.search(block) |
|
|
158 |
card_set = "card-set" |
|
|
159 |
|
|
|
160 |
if m: |
|
|
161 |
# removes the first line |
|
|
162 |
if m.start() > 0: |
|
|
163 |
self.parser.parseBlocks(parent, [block[: m.start()]]) |
|
|
164 |
block = block[m.end() :] |
|
|
165 |
sibling = self.lastChild(parent) |
|
|
166 |
block, non_cards = self.detab(block) |
|
|
167 |
else: |
|
|
168 |
sibling, block, non_cards = self.parse_content(parent, block) |
|
|
169 |
|
|
|
170 |
if m: |
|
|
171 |
if ( |
|
|
172 |
sibling is not None |
|
|
173 |
and sibling.tag.lower() == "div" |
|
|
174 |
and sibling.attrib.get("class", "") == card_set |
|
|
175 |
): |
|
|
176 |
card_group = sibling |
|
|
177 |
else: |
|
|
178 |
self.card_group_count += 1 |
|
|
179 |
card_group = etree.SubElement( |
|
|
180 |
parent, |
|
|
181 |
"div", |
|
|
182 |
{ |
|
|
183 |
"class": card_set, |
|
|
184 |
"data-cards": "%d:0" % self.card_group_count, |
|
|
185 |
}, |
|
|
186 |
) |
|
|
187 |
|
|
|
188 |
data = card_group.attrib["data-cards"].split(":") |
|
|
189 |
card_set = int(data[0]) |
|
|
190 |
card_count = int(data[1]) + 1 |
|
|
191 |
|
|
|
192 |
div = etree.SubElement( |
|
|
193 |
card_group, |
|
|
194 |
"div", |
|
|
195 |
{ |
|
|
196 |
"class": "card-content", |
|
|
197 |
}, |
|
|
198 |
) |
|
|
199 |
attributes = m.group(2) |
|
|
200 |
|
|
|
201 |
if attributes: |
|
|
202 |
attr_m = AttrListTreeprocessor.INLINE_RE.search(attributes) |
|
|
203 |
if attr_m: |
|
|
204 |
assign_attrs(div, attr_m.group(1)) |
|
|
205 |
if div.get("href"): |
|
|
206 |
div.tag = "a" |
|
|
207 |
|
|
|
208 |
card_group.attrib["data-cards"] = "%d:%d" % (card_set, card_count) |
|
|
209 |
else: |
|
|
210 |
if sibling.tag in ("li", "dd") and sibling.text: |
|
|
211 |
# Sibling is a list item, but we need to wrap it's content should be |
|
|
212 |
# wrapped in <p> |
|
|
213 |
text = sibling.text |
|
|
214 |
sibling.text = "" |
|
|
215 |
p = etree.SubElement(sibling, "p") |
|
|
216 |
p.text = text |
|
|
217 |
div = sibling |
|
|
218 |
elif sibling.tag == "div" and sibling.attrib.get("class", "") == card_set: |
|
|
219 |
# Get `card-content` under `card-set` |
|
|
220 |
div = self.lastChild(sibling) |
|
|
221 |
else: |
|
|
222 |
# Pass anything else as the parent |
|
|
223 |
div = sibling |
|
|
224 |
|
|
|
225 |
self.parser.parseChunk(div, block) |
|
|
226 |
|
|
|
227 |
if non_cards: |
|
|
228 |
# Insert the card content back into blocks |
|
|
229 |
blocks.insert(0, non_cards) |
|
|
230 |
|
|
|
231 |
|
|
|
232 |
class CardExtension(Extension): |
|
|
233 |
"""Add card extension.""" |
|
|
234 |
|
|
|
235 |
def __init__(self, *args, **kwargs): |
|
|
236 |
"""Initialize.""" |
|
|
237 |
|
|
|
238 |
self.config = { |
|
|
239 |
"slugify": [ |
|
|
240 |
0, |
|
|
241 |
"Slugify function used to create card specific IDs - Default: None", |
|
|
242 |
], |
|
|
243 |
"combine_header_slug": [ |
|
|
244 |
False, |
|
|
245 |
"Combine the card slug with the slug of the parent header - " |
|
|
246 |
"Default: False", |
|
|
247 |
], |
|
|
248 |
"separator": ["-", "Slug separator - Default: '-'"], |
|
|
249 |
} |
|
|
250 |
|
|
|
251 |
super(CardExtension, self).__init__(*args, **kwargs) |
|
|
252 |
|
|
|
253 |
def extendMarkdown(self, md): |
|
|
254 |
"""Add card to Markdown instance.""" |
|
|
255 |
md.registerExtension(self) |
|
|
256 |
|
|
|
257 |
config = self.getConfigs() |
|
|
258 |
|
|
|
259 |
self.card_processor = CardProcessor(md.parser, config) |
|
|
260 |
|
|
|
261 |
md.parser.blockprocessors.register( |
|
|
262 |
self.card_processor, |
|
|
263 |
"card", |
|
|
264 |
105, |
|
|
265 |
) |
|
|
266 |
|
|
|
267 |
def reset(self): |
|
|
268 |
"""Reset.""" |
|
|
269 |
|
|
|
270 |
self.card_processor.card_group_count = 0 |
|
|
271 |
|
|
|
272 |
|
|
|
273 |
def makeExtension(*args, **kwargs): |
|
|
274 |
"""Return extension.""" |
|
|
275 |
|
|
|
276 |
return CardExtension(*args, **kwargs) |