summaryrefslogtreecommitdiff
path: root/asciidoc/attrs.py
blob: 42a6aae3994226580caa2c0d282f38bd6cde310b (plain)
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
import re
import typing

from . import get_compat_mode
from .utils import get_args, get_kwargs


def parse_attributes(attrs: str, output_dict: typing.Dict) -> None:
    """Update a dictionary with name/value attributes from the attrs string.
    The attrs string is a comma separated list of values and keyword name=value
    pairs. Values must precede keywords and are named '1','2'... The entire
    attributes list is named '0'. If keywords are specified string values must
    be quoted. Examples:

    attrs: ''
    output_dict: {}

    attrs: 'hello,world'
    output_dict: {'0': 'hello,world', '1': 'hello', '2': 'world',}

    attrs: '"hello", planet="earth"'
    output_dict: {'0': '"hello", planet="earth"', '1': 'hello' 'planet': 'earth', }
    """
    if not attrs:
        return
    output_dict['0'] = attrs
    # Replace line separators with spaces so line spanning works.
    s = re.sub(r'\s', ' ', attrs)
    d = legacy_parse(s) if get_compat_mode() == 1 else future_parse(s)
    output_dict.update(d)
    assert len(d) > 0


def future_parse(s: str) -> dict:
    d = {}
    key = ''
    value = ''
    count = 1
    quote = None
    in_quotes = False
    had_quotes = False

    def add_value():
        nonlocal count, d, key, value
        key = key.strip()
        value = value.strip()
        if had_quotes:
            value = value[1:-1]

        if not value and not had_quotes:
            value = None

        if key:
            d[key] = value if value else ''
            key = ''
        else:
            d["{}".format(count)] = value
        count += 1
        value = ''

    for i in range(len(s)):
        char = s[i]

        if char == ',' and not in_quotes:
            add_value()
            had_quotes = False
        elif value and char == '=' and not in_quotes:
            key = value
            value = ''
        elif not in_quotes and (char == '"' or char == "'") \
                and (i == 0 or s[i - 1] != '\\'):
            in_quotes = True
            quote = char
            value += char
        elif in_quotes and char == quote and (i == 0 or s[i - 1] != '\\'):
            in_quotes = False
            had_quotes = True
            quote = None
            value += char
        elif char == '\\' and i < len(s) - 1 and (s[i + 1] == '"' or s[i + 1] == "'"):
            pass
        else:
            value += char

    if key and key[0] == '=' and not value:
        value = key + "="
        key = ""

    if not value and s.rstrip()[-1] == ',':
        value = ' '

    if had_quotes or value or key:
        add_value()
    return d


def legacy_parse(s: str) -> dict:
    d = {}
    try:
        d.update(get_args(s))
        d.update(get_kwargs(s))
        for v in list(d.values()):
            if not (isinstance(v, str)
                    or isinstance(v, int) or isinstance(v, float) or v is None):
                raise Exception
    except Exception:
        s = s.replace('"', '\\"')
        s = s.split(',')
        s = ['"' + x.strip() + '"' for x in s]
        s = ','.join(s)
        try:
            d = {}
            d.update(get_args(s))
            d.update(get_kwargs(s))
        except Exception:
            return  # If there's a syntax error leave with {0}=attrs.
        for k in list(d.keys()):  # Drop any empty positional arguments.
            if d[k] == '':
                del d[k]
    return d