Diff of /docs/scripts/cards.py [000000] .. [cad161]

Switch to unified view

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)