• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rafalp / Misago / 9235638887

25 May 2024 12:58PM UTC coverage: 97.61% (-1.1%) from 98.716%
9235638887

Pull #1742

github

web-flow
Merge ef9c8656c into abad4f068
Pull Request #1742: Replace forum options with account settings

166 of 211 new or added lines in 20 files covered. (78.67%)

655 existing lines in 146 files now uncovered.

51625 of 52889 relevant lines covered (97.61%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.69
/misago/parser/parser.py
1
import re
1✔
2
from functools import cached_property
1✔
3
from typing import Callable
1✔
4

5
from django.utils.crypto import get_random_string
1✔
6

7

8
class Pattern:
1✔
9
    pattern_type: str
1✔
10
    pattern: str
1✔
11

12
    def parse(
1✔
13
        self, parser: "Parser", match: str, parents: list[str]
14
    ) -> dict | list[dict]:
UNCOV
15
        raise NotImplementedError()
×
16

17

18
class LineBreak(Pattern):
1✔
19
    pattern_type = "line-break"
1✔
20
    pattern = r" *\n *"
1✔
21

22
    def parse(self, parser: "Parser", match: str, parents: list[str]) -> dict:
1✔
23
        return {"type": self.pattern_type}
1✔
24

25

26
class Parser:
1✔
27
    block_patterns: list[Pattern]
1✔
28
    inline_patterns: list[Pattern]
1✔
29
    post_processors: list[Callable[["Parser", list[dict]], list[dict]]]
1✔
30

31
    reserve_inline_code = re.compile(r"`*`(.|\n)+?``*")
1✔
32
    _reserved_patterns: dict[str, str]
1✔
33

34
    def __init__(
1✔
35
        self,
36
        block_patterns: list[Pattern] | None = None,
37
        inline_patterns: list[Pattern] | None = None,
38
        post_processors: (
39
            list[Callable[["Parser", list[dict]], list[dict]]] | None
40
        ) = None,
41
    ):
42
        self.block_patterns = block_patterns or []
1✔
43
        self.inline_patterns = inline_patterns or []
1✔
44
        self.post_processors = post_processors or []
1✔
45

46
        self._reserved_patterns = {}
1✔
47

48
    def __call__(self, markup: str) -> list[dict]:
1✔
49
        markup = self.reserve_patterns(markup)
1✔
50
        ast = self.parse_blocks(markup, [])
1✔
51
        for post_processor in self.post_processors:
1✔
52
            ast = post_processor(self, ast)
1✔
53
        return ast
1✔
54

55
    def reserve_patterns(self, markup: str) -> str:
1✔
56
        if not "`" in markup:
1✔
57
            return markup
1✔
58

59
        def replace_pattern(match):
1✔
60
            match_str = match.group(0)
1✔
61
            if match_str.startswith("``") or match_str.endswith("``"):
1✔
62
                return match_str
1✔
63

64
            pattern_id = f"%%{get_random_string(12)}%%"
1✔
65
            while pattern_id in markup or pattern_id in self._reserved_patterns:
1✔
UNCOV
66
                pattern_id = f"%%{get_random_string(12)}%%"
×
67

68
            self._reserved_patterns[pattern_id] = match_str
1✔
69
            return pattern_id
1✔
70

71
        return self.reserve_inline_code.sub(replace_pattern, markup)
1✔
72

73
    def reverse_reservations(self, value: str) -> str:
1✔
74
        if not self._reserved_patterns or "%%" not in value:
1✔
75
            return value
1✔
76

77
        for pattern, org in self._reserved_patterns.items():
1✔
78
            value = value.replace(pattern, org)
1✔
79
        return value
1✔
80

81
    def parse_blocks(self, markup: str, parents: list[str]) -> list[dict]:
1✔
82
        cursor = 0
1✔
83

84
        result: list[dict] = []
1✔
85
        for m in self._block_re.finditer(markup):
1✔
86
            for key, pattern in self._final_block_patterns.items():
1✔
87
                block_match = m.group(key)
1✔
88
                if block_match is not None:
1✔
89
                    start = m.start()
1✔
90
                    if start > cursor:
1✔
91
                        result += self.parse_paragraphs(markup[cursor:start], parents)
1✔
92

93
                    block_ast = pattern.parse(self, block_match, parents)
1✔
94
                    if isinstance(block_ast, list):
1✔
95
                        result += block_ast
1✔
96
                    elif isinstance(block_ast, dict):
1✔
97
                        result.append(block_ast)
1✔
98

99
                    cursor = m.end()
1✔
100
                    break
1✔
101

102
        if cursor < len(markup):
1✔
103
            result += self.parse_paragraphs(markup[cursor:], parents)
1✔
104

105
        return result
1✔
106

107
    def parse_paragraphs(self, markup: str, parents: list[str]) -> list[dict]:
1✔
108
        markup = markup.strip()
1✔
109

110
        if not markup:
1✔
111
            return []
1✔
112

113
        parents = parents + ["paragraph"]
1✔
114

115
        result: list[dict] = []
1✔
116
        for m in self._paragraph_re.finditer(markup):
1✔
117
            result.append(
1✔
118
                {
119
                    "type": "paragraph",
120
                    "children": self.parse_inline(
121
                        m.group(0).strip(), parents, reverse_reservations=True
122
                    ),
123
                }
124
            )
125
        return result
1✔
126

127
    def parse_inline(
1✔
128
        self,
129
        markup: str,
130
        parents: list[str],
131
        reverse_reservations: bool = False,
132
    ) -> list[dict]:
133
        if reverse_reservations:
1✔
134
            markup = self.reverse_reservations(markup)
1✔
135

136
        cursor = 0
1✔
137

138
        result: list[dict] = []
1✔
139
        for m in self._inline_re.finditer(markup):
1✔
140
            for key, pattern in self._final_inline_patterns.items():
1✔
141
                block_match = m.group(key)
1✔
142
                if block_match is not None:
1✔
143
                    start = m.start()
1✔
144
                    if start > cursor:
1✔
145
                        if result and result[-1]["type"] == "text":
1✔
UNCOV
146
                            result[-1]["text"] += markup[cursor:start]
×
147
                        else:
148
                            result.append(
1✔
149
                                {"type": "text", "text": markup[cursor:start]}
150
                            )
151

152
                    inline_ast = pattern.parse(self, block_match, parents)
1✔
153
                    if isinstance(inline_ast, list):
1✔
154
                        for child in inline_ast:
1✔
155
                            if (
1✔
156
                                result
157
                                and child["type"] == "text"
158
                                and result[-1]["type"] == "text"
159
                            ):
160
                                result[-1]["text"] += child["text"]
1✔
161
                            else:
162
                                result.append(child)
1✔
163

164
                    elif isinstance(inline_ast, dict):
1✔
165
                        if (
1✔
166
                            result
167
                            and inline_ast["type"] == "text"
168
                            and result[-1]["type"] == "text"
169
                        ):
170
                            result[-1]["text"] += inline_ast["text"]
1✔
171
                        else:
172
                            result.append(inline_ast)
1✔
173

174
                    cursor = m.end()
1✔
175
                    break
1✔
176

177
        if cursor < len(markup):
1✔
178
            if result and result[-1]["type"] == "text":
1✔
179
                result[-1]["text"] += markup[cursor:]
1✔
180
            else:
181
                result.append({"type": "text", "text": markup[cursor:]})
1✔
182

183
        return result
1✔
184

185
    @cached_property
1✔
186
    def _final_block_patterns(self) -> dict[str, Pattern]:
1✔
187
        patterns: list[Pattern] = self.block_patterns.copy()
1✔
188
        return {f"b_{i}": pattern for i, pattern in enumerate(patterns)}
1✔
189

190
    @cached_property
1✔
191
    def _block_re(self) -> re.Pattern:
1✔
192
        return self._build_re_pattern(self._final_block_patterns)
1✔
193

194
    @cached_property
1✔
195
    def _paragraph_re(self) -> re.Pattern:
1✔
196
        return re.compile(r".+(\n.+)*")
1✔
197

198
    @cached_property
1✔
199
    def _final_inline_patterns(self) -> dict[str, Pattern]:
1✔
200
        patterns: list[Pattern] = self.inline_patterns.copy()
1✔
201
        patterns += [LineBreak()]
1✔
202
        return {f"i_{i}": pattern for i, pattern in enumerate(patterns)}
1✔
203

204
    @cached_property
1✔
205
    def _inline_re(self) -> re.Pattern:
1✔
206
        return self._build_re_pattern(self._final_inline_patterns)
1✔
207

208
    def _build_re_pattern(self, patterns: dict[str, Pattern]) -> re.Pattern:
1✔
209
        return re.compile(
1✔
210
            "|".join(
211
                f"(?P<{key}>{pattern.pattern})" for key, pattern in patterns.items()
212
            ),
213
            re.IGNORECASE,
214
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc