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

rafalp / Misago / 14252691788

03 Apr 2025 09:01PM UTC coverage: 97.097% (-0.08%) from 97.173%
14252691788

push

github

web-flow
Replace default parser with `markdown-it-py` (#1901)

1902 of 1969 new or added lines in 66 files covered. (96.6%)

20 existing lines in 7 files now uncovered.

68959 of 71021 relevant lines covered (97.1%)

0.97 hits per line

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

92.93
/misago/parser/bbcode.py
1
from typing import Callable
1✔
2

3
from markdown_it.rules_block.state_block import StateBlock
1✔
4
from markdown_it.token import Token
1✔
5

6
BBCodeBlockStart = tuple[str, dict | None, int, int]
1✔
7
BBCodeBlockEnd = tuple[str, int, int]
1✔
8
BBCodeBlockStartRule = Callable[[StateBlock, int], BBCodeBlockStart | None]
1✔
9
BBCodeBlockEndRule = Callable[[StateBlock, int], BBCodeBlockEnd | None]
1✔
10

11

12
class BBCodeBlockRule:
1✔
13
    name: str
1✔
14
    element: str
1✔
15
    start: BBCodeBlockStartRule
1✔
16
    end: BBCodeBlockEndRule
1✔
17

18
    def __init__(
1✔
19
        self,
20
        name: str,
21
        element: str,
22
        start: BBCodeBlockStartRule,
23
        end: BBCodeBlockEndRule,
24
    ):
25
        self.name = name
1✔
26
        self.element = element
1✔
27

28
        self.start = start
1✔
29
        self.end = end
1✔
30

31
    def __call__(
1✔
32
        self, state: StateBlock, startLine: int, endLine: int, silent: bool
33
    ) -> bool:
34
        if state.is_code_block(startLine):
1✔
NEW
35
            return False
×
36

37
        start, end = self.scan_full_line(state, startLine)
1✔
38
        if start and end:
1✔
39
            if silent:
1✔
40
                return True
1✔
41

42
            return self.parse_single_line(state, startLine, start, end)
1✔
43

44
        if not start:
1✔
45
            return False
1✔
46

47
        return self.parse_multiple_lines(state, startLine, endLine, silent, start)
1✔
48

49
    def scan_full_line(
1✔
50
        self, state: StateBlock, line: int
51
    ) -> tuple[BBCodeBlockStart | None, BBCodeBlockEnd | None]:
52
        start = self.scan_line_for_start(state, line)
1✔
53

54
        if start:
1✔
55
            end = self.scan_line_for_end(state, line, True)
1✔
56
            if end and end[1] > start[3]:
1✔
57
                return start, end
1✔
58

59
        return start, None
1✔
60

61
    def scan_line_for_start(
1✔
62
        self, state: StateBlock, line: int
63
    ) -> BBCodeBlockStart | None:
64
        return self._scan_line_from_start(state, line, self.start)
1✔
65

66
    def scan_line_for_end(
1✔
67
        self, state: StateBlock, line: int, closing: bool = False
68
    ) -> BBCodeBlockEnd | None:
69
        if closing:
1✔
70
            return self._scan_line_from_end(state, line, self.end)
1✔
71

72
        return self._scan_line_from_start(state, line, self.end)
1✔
73

74
    def _scan_line_from_start(
1✔
75
        self,
76
        state: StateBlock,
77
        line: int,
78
        rule: BBCodeBlockStartRule | BBCodeBlockEndRule,
79
    ) -> BBCodeBlockStart | BBCodeBlockEnd | None:
80
        org_bmark = state.bMarks[line]
1✔
81
        maximum = state.eMarks[line]
1✔
82
        steps = 0
1✔
83

84
        try:
1✔
85
            while (state.bMarks[line] + state.tShift[line]) < maximum and steps < 3:
1✔
86
                if match := rule(state, line):
1✔
87
                    return match
1✔
88

89
                if state.src[state.bMarks[line] + state.tShift[line]]:
1✔
90
                    return None
1✔
91

NEW
92
                state.bMarks[line] += 1
×
NEW
93
                steps += 1
×
94

NEW
95
            return None
×
NEW
96
        except Exception:
×
NEW
97
            raise
×
98
        finally:
99
            state.bMarks[line] = org_bmark
1✔
100

101
    def _scan_line_from_end(
1✔
102
        self,
103
        state: StateBlock,
104
        line: int,
105
        rule: BBCodeBlockEndRule,
106
    ) -> BBCodeBlockEnd | None:
107
        org_bmark = state.bMarks[line]
1✔
108
        state.bMarks[line] = state.eMarks[line]
1✔
109
        maximum = state.eMarks[line]
1✔
110

111
        try:
1✔
112
            while state.bMarks[line] >= org_bmark:
1✔
113
                if match := rule(state, line):
1✔
114
                    if state.src[match[2] : maximum].strip():
1✔
NEW
115
                        return None
×
116

117
                    return match
1✔
118

119
                state.bMarks[line] -= 1
1✔
120

121
            return None
1✔
NEW
122
        except Exception:
×
NEW
123
            raise
×
124
        finally:
125
            state.bMarks[line] = org_bmark
1✔
126

127
    def parse_single_line(
1✔
128
        self,
129
        state: StateBlock,
130
        startLine: int,
131
        start: BBCodeBlockStart,
132
        end: BBCodeBlockEnd,
133
    ):
134
        content_start = start[3]
1✔
135
        content_end = end[1]
1✔
136
        content = state.src[content_start:content_end].strip()
1✔
137

138
        old_parent_type = state.parentType
1✔
139

140
        state.parentType = self.name
1✔
141
        self.state_push_open_token(state, startLine, startLine, start)
1✔
142

143
        state.parentType = "paragraph"
1✔
144
        token = state.push("paragraph_open", "p", 1)
1✔
145
        token.map = [startLine, startLine]
1✔
146

147
        token = state.push("inline", "", 0)
1✔
148
        token.content = content
1✔
149
        token.map = [startLine, state.line]
1✔
150
        token.children = []
1✔
151

152
        token = state.push("paragraph_close", "p", -1)
1✔
153

154
        self.state_push_close_token(state, end)
1✔
155

156
        state.parentType = old_parent_type
1✔
157
        state.line += 1
1✔
158

159
        return True
1✔
160

161
    def parse_multiple_lines(
1✔
162
        self,
163
        state: StateBlock,
164
        startLine: int,
165
        endLine: int,
166
        silent: bool,
167
        start: BBCodeBlockStart,
168
    ) -> bool:
169
        line = startLine
1✔
170
        pos = state.bMarks[line] + state.tShift[line] + start[3]
1✔
171
        maximum = state.eMarks[line]
1✔
172

173
        if state.src[pos:maximum].strip():
1✔
NEW
174
            return False
×
175

176
        end = None
1✔
177
        nesting = 1
1✔
178

179
        while line < endLine:
1✔
180
            line += 1
1✔
181

182
            if (
1✔
183
                state.isEmpty(line)
184
                or state.is_code_block(line)
185
                or all(self.scan_full_line(state, line))
186
                or line > state.lineMax
187
            ):
188
                continue
1✔
189

190
            if self.scan_line_for_start(state, line):
1✔
191
                nesting += 1
1✔
192

193
            elif match := self.scan_line_for_end(state, line):
1✔
194
                nesting -= 1
1✔
195

196
                if nesting == 0:
1✔
197
                    end = match
1✔
198
                    break
1✔
199

200
        if silent or not end:
1✔
201
            return nesting == 0
1✔
202

203
        self.state_push_open_token(state, startLine, line, start)
1✔
204
        self.state_push_children(state, startLine + 1, line)
1✔
205
        self.state_push_close_token(state, end)
1✔
206

207
        state.line = line + 1
1✔
208
        return True
1✔
209

210
    def state_push_open_token(
1✔
211
        self, state: StateBlock, startLine: int, endLine: int, start: BBCodeBlockStart
212
    ) -> Token:
213
        token = state.push(f"{self.name}_open", self.element, 1)
1✔
214
        token.markup = start[0]
1✔
215
        token.map = [startLine, endLine]
1✔
216

217
        if attrs := start[1]:
1✔
218
            for attr_name, attr_value in attrs.items():
1✔
219
                token.attrSet(attr_name, attr_value)
1✔
220

221
            if meta := self.get_meta(attrs):
1✔
222
                token.meta = meta
1✔
223

224
        return token
1✔
225

226
    def state_push_close_token(self, state: StateBlock, end: BBCodeBlockEnd) -> Token:
1✔
227
        token = state.push(f"{self.name}_close", self.element, -1)
1✔
228
        token.markup = end[0]
1✔
229
        return token
1✔
230

231
    def state_push_void_token(
1✔
232
        self, state: StateBlock, startLine: int, start: BBCodeBlockStart
233
    ) -> Token:
234
        token = state.push(f"{self.name}", self.element, 0)
1✔
235
        token.markup = start[0]
1✔
236
        token.map = [startLine, startLine]
1✔
237

238
        if attrs := start[1]:
1✔
239
            for attr_name, attr_value in attrs.items():
1✔
240
                token.attrSet(attr_name, attr_value)
1✔
241

242
            if meta := self.get_meta(attrs):
1✔
243
                token.meta = meta
1✔
244

245
        return token
1✔
246

247
    def state_push_children(self, state: StateBlock, startLine: int, endLine: int):
1✔
248
        if startLine == endLine:
1✔
NEW
249
            return
×
250

251
        old_line_max = state.lineMax
1✔
252
        old_parent = state.parentType
1✔
253

254
        state.lineMax = startLine
1✔
255
        state.parentType = self.name
1✔
256

257
        state.level += 1
1✔
258
        state.md.block.tokenize(state, startLine, endLine)
1✔
259
        state.level -= 1
1✔
260

261
        state.lineMax = old_line_max
1✔
262
        state.parentType = old_parent
1✔
263

264
    def get_meta(self, attrs: dict) -> dict | None:
1✔
265
        return None
1✔
266

267

268
def bbcode_block_start_rule(
1✔
269
    bbcode: str, state: StateBlock, line: int, args: bool = False
270
) -> tuple[str, str | None, int, int] | None:
271
    start = state.bMarks[line] + state.tShift[line]
1✔
272
    maximum = state.eMarks[line]
1✔
273
    src = state.src[start:maximum]
1✔
274

275
    block_bbcode = f"[{bbcode}".lower()
1✔
276
    block_bbcode_len = len(block_bbcode)
1✔
277

278
    if src.lower()[:block_bbcode_len] != block_bbcode:
1✔
279
        return None
1✔
280

281
    if "]" not in src[block_bbcode_len:]:
1✔
NEW
282
        return None
×
283

284
    end = src.index("]", 0, maximum - start)
1✔
285
    if end == block_bbcode_len:
1✔
286
        return src[: block_bbcode_len + 1], None, start, start + end + 1
1✔
287

288
    if src[block_bbcode_len] != "=" or not args:
1✔
NEW
289
        return None
×
290

291
    args_str = src[block_bbcode_len + 1 : end]
1✔
292
    if args_str and (
1✔
293
        (args_str[0] == '"' and args_str[-1] == '"')
294
        or (args_str[0] == "'" and args_str[-1] == "'")
295
    ):
296
        args_str = args_str[1:-1]
1✔
297

298
    args_str = args_str.strip() or None
1✔
299

300
    return src[: end + 1], args_str, start, end + 1
1✔
301

302

303
def bbcode_block_end_rule(
1✔
304
    bbcode: str, state: StateBlock, line: int
305
) -> tuple[str, int, int] | None:
306
    start = state.bMarks[line] + state.tShift[line]
1✔
307
    maximum = state.eMarks[line]
1✔
308
    src = state.src[start:maximum]
1✔
309

310
    block_bbcode = f"[/{bbcode}]".lower()
1✔
311
    block_bbcode_len = len(block_bbcode)
1✔
312

313
    if src[:block_bbcode_len].lower() == block_bbcode:
1✔
314
        return src[:block_bbcode_len], start, start + block_bbcode_len
1✔
315

316
    return None
1✔
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

© 2025 Coveralls, Inc