File: block.py

package info (click to toggle)
pymdown-extensions 10.13-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,104 kB
  • sloc: python: 60,117; javascript: 846; sh: 8; makefile: 5
file content (382 lines) | stat: -rw-r--r-- 10,320 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
"""Block class."""
from abc import ABCMeta, abstractmethod
import functools
import copy
import re
from markdown import util as mutil

RE_IDENT = re.compile(
    r'''
    (?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f])+|--)
    (?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f])*)
    ''',
    re.I | re.X
)

RE_INDENT = re.compile(r'(?m)^([ ]*)[^ \n]')

RE_DEDENT = re.compile(r'(?m)^([ ]*)($)?')


def _type_multi(value, types=None):
    """Multi types."""

    for t in types:
        try:
            return t(value)
        except ValueError:  # noqa: PERF203
            pass

    raise ValueError(f"Type '{type(value)}' did not match any of the provided types")


def type_multi(*args):
    """Validate a type with multiple type functions."""

    return functools.partial(_type_multi, types=args)


def type_any(value):
    """Accepts any type."""

    return value


def type_none(value):
    """Ensure type None or fail."""

    if value is not None:
        raise ValueError(f'{type(value)} is not None')


def _ranged_number(value, minimum, maximum, number_type):
    """Check the range of the given number type."""

    value = number_type(value)
    if minimum is not None and value < minimum:
        raise ValueError(f'{value} is not greater than {minimum}')

    if maximum is not None and value > maximum:
        raise ValueError(f'{value} is not greater than {minimum}')

    return value


def type_number(value):
    """Ensure type number or fail."""

    if not isinstance(value, (float, int)):
        raise ValueError(f"Could not convert type {type(value)} to a number")

    return value


def type_integer(value):
    """Ensure type integer or fail."""

    if not isinstance(value, int):
        if not isinstance(value, float) or not value.is_integer():
            raise ValueError(f"Could not convert type {type(value)} to an integer")
        value = int(value)

    return value


def type_ranged_number(minimum=None, maximum=None):
    """Ensure typed number is within range."""

    return functools.partial(_ranged_number, minimum=minimum, maximum=maximum, number_type=type_number)


def type_ranged_integer(minimum=None, maximum=None):
    """Ensured type integer is within range."""

    return functools.partial(_ranged_number, minimum=minimum, maximum=maximum, number_type=type_integer)


def type_boolean(value):
    """Ensure type boolean or fail."""

    if not isinstance(value, bool):
        raise ValueError(f"Could not convert type {type(value)} to a boolean")
    return value


type_ternary = type_multi(type_none, type_boolean)


def type_string(value):
    """Ensure type string or fail."""

    if isinstance(value, str):
        return value

    raise ValueError(f"Could not convert type {type(value)} to a string")


def type_string_insensitive(value):
    """Ensure type string and normalize case."""

    return type_string(value).lower()


def type_html_identifier(value):
    """Ensure type HTML attribute name or fail."""

    value = type_string(value)
    m = RE_IDENT.fullmatch(value)
    if m is None:
        raise ValueError('A valid attribute name must be provided')
    return m.group(0)


def _delimiter(string, split, string_type):
    """Split the string by the delimiter and then parse with the parser."""

    l = []
    # Ensure input is a string
    string = type_string(string)
    for s in string.split(split):
        s = s.strip()
        if not s:
            continue
        # Ensure each part conforms to the desired string type
        s = string_type(s)
        l.append(s)
    return l


def _string_in(value, accepted, string_type):
    """Ensure type string is within the accepted values."""

    value = string_type(value)
    if value not in accepted:
        raise ValueError(f'{value} not found in {accepted!s}')
    return value


def type_string_in(accepted, insensitive=True):
    """Ensure type string is within the accepted list."""

    return functools.partial(
        _string_in,
        accepted=accepted,
        string_type=type_string_insensitive if insensitive else type_string
    )


def type_string_delimiter(split, string_type=type_string):
    """String delimiter function."""

    return functools.partial(_delimiter, split=split, string_type=string_type)


def type_html_attribute_dict(value):
    """Attribute dictionary."""

    if not isinstance(value, dict):
        raise ValueError('Attributes should be contained within a dictionary')

    attributes = {}
    for k, v in value.items():
        k = type_html_identifier(k)
        if k.lower() == 'class':
            k = 'class'
            v = type_html_classes(v)
        elif k.lower() == 'id':
            k = 'id'
            v = type_html_identifier(v)
        else:
            v = type_string(v)
        attributes[k] = v

    return attributes


# Ensure class(es) or fail
type_html_classes = type_string_delimiter(' ', type_html_identifier)


class Block(metaclass=ABCMeta):
    """Block."""

    # Set to something if argument should be split.
    # Arguments will be split and white space stripped.
    NAME = ''

    # Instance arguments and options
    ARGUMENT = False
    OPTIONS = {}

    def __init__(self, length, tracker, block_mgr, config):
        """
        Initialize.

        - `length` specifies the length (number of slashes) that the header used
        - `tracker` is a persistent storage for the life of the current Markdown page.
          It is a dictionary where we can keep references until the parent extension is reset.
        - `md` is the Markdown object just in case access is needed to something we
          didn't think about.

        """

        # Setup up the argument and options spec
        # Note that `attributes` is handled special and we always override it
        self.arg_spec = self.ARGUMENT
        self.option_spec = copy.deepcopy(self.OPTIONS)
        if 'attrs' in self.option_spec:  # pragma: no cover
            raise ValueError("'attrs' is a reserved option name and cannot be overriden")
        self.option_spec['attrs'] = [{}, type_html_attribute_dict]

        self._block_mgr = block_mgr
        self.length = length
        self.tracker = tracker
        self.md = block_mgr.md
        self.arguments = []
        self.options = {}
        self.config = config
        self.on_init()

    def is_raw(self, tag):
        """Is raw element."""

        return self._block_mgr.is_raw(tag)

    def is_block(self, tag):  # pragma: no cover
        """Is block element."""

        return self._block_mgr.is_block(tag)

    def html_escape(self, text):
        """Basic html escaping."""

        text = text.replace('&', '&amp;')
        text = text.replace('<', '&lt;')
        text = text.replace('>', '&gt;')
        return text

    def dedent(self, text, length=None):
        """Dedent raw text."""

        if length is None:
            length = self.md.tab_length

        min_length = float('inf')
        for x in RE_INDENT.findall(text):
            min_length = min(len(x), min_length)
        min_length = min(min_length, length)

        return RE_DEDENT.sub(lambda m, l=min_length: '' if m.group(2) is not None else m.group(1)[l:], text)

    def on_init(self):
        """On initialize."""

        return

    def on_markdown(self):
        """Check how element should be treated by the Markdown parser."""

        return "auto"

    def _validate(self, parent, arg, **options):
        """Parse configuration."""

        # Check argument
        if (self.arg_spec is not None and ((arg and not self.arg_spec) or (not arg and self.arg_spec))):
            return False

        self.argument = arg

        # Fill in defaults options
        spec = self.option_spec
        parsed = {}
        for k, v in spec.items():
            parsed[k] = v[0]

        # Parse provided options
        for k, v in options.items():

            # Parameter not in spec
            if k not in spec:
                # Unrecognized parameter name
                return False

            # Spec explicitly handles parameter
            else:
                parser = spec[k][1]
                if parser is not None:
                    try:
                        v = parser(v)
                    except Exception:
                        # Invalid parameter value
                        return False
            parsed[k] = v

        # Add parsed options to options
        self.options = parsed

        return self.on_validate(parent)

    def on_validate(self, parent):
        """
        Handle validation event.

        Run after config parsing completes and allows for the opportunity
        to invalidate the block if argument, options, or even the parent
        element do not meet certain criteria.

        Return `False` to invalidate the block.
        """

        return True

    @abstractmethod
    def on_create(self, parent):
        """Create the needed element and return it."""

    def _create(self, parent):
        """Create the element."""

        el = self.on_create(parent)

        # Handle general HTML attributes
        attrib = el.attrib
        for k, v in self.options['attrs'].items():
            if k == 'class':
                if k in attrib:
                    # Don't validate what the developer as already attached
                    v = type_string_delimiter(' ')(attrib['class']) + v
                attrib['class'] = ' '.join(v)
            else:
                attrib[k] = v
        return el

    def _end(self, block):
        """Reached end of the block, dedent raw blocks and call `on_end` hook."""

        mode = self.on_markdown()
        add = self.on_add(block)
        if mode == 'raw' or (mode == 'auto' and self.is_raw(add)):
            add.text = mutil.AtomicString(self.dedent(add.text))

        self.on_end(block)

    def on_end(self, block):
        """Perform any action on end."""

        return

    def on_add(self, block):
        """
        Adjust where the content is added and return the desired element.

        Is there a sub-element where this content should go?
        This runs before processing every new block.
        """

        return block

    def on_inline_end(self, block):
        """Perform action on the block after inline parsing."""

        return