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

rafalp / Misago / 15379643178

01 Jun 2025 09:31PM UTC coverage: 97.18% (+0.002%) from 97.178%
15379643178

push

github

web-flow
Update formatting bbcode to respect escaping (#1935)

268 of 288 new or added lines in 13 files covered. (93.06%)

9 existing lines in 3 files now uncovered.

71712 of 73793 relevant lines covered (97.18%)

0.97 hits per line

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

90.98
/misago/parser/bbcode.py
1
from dataclasses import dataclass
1✔
2
from typing import Callable
1✔
3

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

7
BBCodeArgsParser = Callable[[str], dict | None]
1✔
8

9

10
@dataclass(frozen=True)
1✔
11
class BBCodeBlockStart:
1✔
12
    start: int
1✔
13
    end: int
1✔
14
    markup: str
1✔
15
    attrs: dict | None
1✔
16

17

18
@dataclass(frozen=True)
1✔
19
class BBCodeBlockEnd:
1✔
20
    start: int
1✔
21
    end: int
1✔
22
    markup: str
1✔
23

24

25
class BBCodeBlockRule:
1✔
26
    name: str
1✔
27
    bbcode: str
1✔
28
    bbcode_len: int
1✔
29
    element: str
1✔
30
    args_parser: BBCodeArgsParser | None
1✔
31

32
    def __init__(
1✔
33
        self,
34
        name: str,
35
        bbcode: str,
36
        element: str,
37
        args_parser: BBCodeArgsParser,
38
    ):
39
        self.name = name
1✔
40
        self.bbcode = bbcode
1✔
41
        self.bbcode_len = len(bbcode)
1✔
42
        self.element = element
1✔
43
        self.args_parser = args_parser
1✔
44

45
    def __call__(
1✔
46
        self, state: StateBlock, startLine: int, endLine: int, silent: bool
47
    ) -> bool:
48
        if state.is_code_block(startLine):
1✔
49
            return False
×
50

51
        if self.parse_single_line(state, startLine, silent):
1✔
52
            return True
1✔
53

54
        return self.parse_multiple_lines(state, startLine, endLine, silent)
1✔
55

56
    def parse_single_line(
1✔
57
        self,
58
        state: StateBlock,
59
        line: int,
60
        silent: bool,
61
    ) -> bool:
62
        start = self.find_single_line_bbcode_block_start(state, line)
1✔
63
        if not start:
1✔
64
            return False
1✔
65

66
        end = self.find_single_line_bbcode_block_end(state, line, start.end)
1✔
67
        if not end:
1✔
68
            return False
1✔
69

70
        if silent:
1✔
71
            return True
1✔
72

73
        old_parent_type = state.parentType
1✔
74

75
        state.parentType = self.name
1✔
76
        self.state_push_open_token(state, line, line, start.markup, start.attrs)
1✔
77

78
        state.parentType = "paragraph"
1✔
79
        token = state.push("paragraph_open", "p", 1)
1✔
80
        token.map = [line, line]
1✔
81

82
        token = state.push("inline", "", 0)
1✔
83
        token.content = state.src[start.end : end.start].strip()
1✔
84
        token.map = [line, line]
1✔
85
        token.children = []
1✔
86

87
        token = state.push("paragraph_close", "p", -1)
1✔
88

89
        self.state_push_close_token(state, end.markup)
1✔
90

91
        state.parentType = old_parent_type
1✔
92
        state.line += 1
1✔
93

94
        return True
1✔
95

96
    def find_single_line_bbcode_block_start(
1✔
97
        self, state: StateBlock, line: int
98
    ) -> BBCodeBlockStart | None:
99
        pos = state.bMarks[line] + state.tShift[line]
1✔
100
        maximum = state.eMarks[line]
1✔
101

102
        while pos < maximum:
1✔
103
            if state.src[pos] == "[":
1✔
104
                break
1✔
105
            elif state.src[pos] == " ":
1✔
NEW
106
                pos += 1
×
107
            else:
108
                return None
1✔
109
        else:
110
            return None
×
111

112
        start = pos
1✔
113

114
        pos += 1
1✔
115
        if state.src[pos : pos + self.bbcode_len].lower() != self.bbcode:
1✔
116
            return None
1✔
117

118
        pos += self.bbcode_len
1✔
119
        if state.src[pos] == "]":
1✔
120
            end = pos + 1
1✔
121

122
            return BBCodeBlockStart(
1✔
123
                start=start,
124
                end=end,
125
                markup=state.src[start:end],
126
                attrs=None,
127
            )
128

129
        if state.src[pos] != "=" or not self.args_parser:
1✔
130
            return None
×
131

132
        pos += 1
1✔
133

134
        level = 1
1✔
135
        args_start = pos
1✔
136

137
        while pos < maximum:
1✔
138
            if state.src[pos] == "\\":
1✔
NEW
139
                pos += 2
×
140

141
            elif state.src[pos] == "[":
1✔
NEW
142
                level += 1
×
NEW
143
                pos += 1
×
144

145
            elif state.src[pos] == "]":
1✔
146
                level -= 1
1✔
147
                if not level:
1✔
148
                    attrs = None
1✔
149
                    if args_str := state.src[args_start:pos].strip():
1✔
150
                        attrs = self.parse_args(args_str)
1✔
151

152
                    end = pos + 1
1✔
153
                    return BBCodeBlockStart(
1✔
154
                        start=start,
155
                        end=end,
156
                        markup=state.src[start:end],
157
                        attrs=attrs,
158
                    )
159

NEW
160
                pos += 1
×
161

162
            else:
163
                pos += 1
1✔
164

165
        else:
NEW
166
            return None
×
167

168
    def find_single_line_bbcode_block_end(
1✔
169
        self, state: StateBlock, line: int, minimum: int
170
    ) -> BBCodeBlockEnd | None:
171
        maximum = state.eMarks[line]
1✔
172

173
        start = None
1✔
174
        end = None
1✔
175

176
        pos = minimum
1✔
177
        while pos < maximum:
1✔
178
            if state.src[pos] == "\\":
1✔
179
                pos += 2
1✔
180
            else:
181
                bbcode_end = pos + self.bbcode_len + 3
1✔
182
                if state.src[pos:bbcode_end].lower() == f"[/{self.bbcode}]":
1✔
183
                    start = pos
1✔
184
                    end = bbcode_end
1✔
185
                pos += 1
1✔
186
        else:
187
            if start is None:
1✔
188
                return None
1✔
189

190
        if state.src[end:maximum].strip():
1✔
191
            return None
1✔
192

193
        return BBCodeBlockEnd(
1✔
194
            start=start,
195
            end=end,
196
            markup=state.src[start:end],
197
        )
198

199
    def parse_multiple_lines(
1✔
200
        self,
201
        state: StateBlock,
202
        startLine: int,
203
        endLine: int,
204
        silent: bool,
205
    ) -> bool:
206
        line = startLine
1✔
207

208
        start = self.find_multi_line_bbcode_block_start(state, line)
1✔
209
        if not start:
1✔
210
            return False
1✔
211

212
        end = None
1✔
213
        nesting = 1
1✔
214

215
        while line < endLine:
1✔
216
            line += 1
1✔
217

218
            if (
1✔
219
                state.isEmpty(line)
220
                or state.is_code_block(line)
221
                or self.parse_single_line(state, line, True)
222
                or line > state.lineMax
223
            ):
224
                continue
1✔
225

226
            if self.find_multi_line_bbcode_block_start(state, line):
1✔
227
                nesting += 1
1✔
228

229
            elif match := self.find_multi_line_bbcode_block_end(state, line):
1✔
230
                nesting -= 1
1✔
231

232
                if nesting == 0:
1✔
233
                    end = match
1✔
234
                    break
1✔
235

236
        if silent or not end:
1✔
237
            return nesting == 0
1✔
238

239
        self.state_push_open_token(state, startLine, line, start.markup, start.attrs)
1✔
240
        self.state_push_children(state, startLine + 1, line)
1✔
241
        self.state_push_close_token(state, end.markup)
1✔
242

243
        state.line = line + 1
1✔
244
        return True
1✔
245

246
    def find_multi_line_bbcode_block_start(
1✔
247
        self, state: StateBlock, line: int
248
    ) -> BBCodeBlockStart | None:
249
        pos = state.bMarks[line] + state.tShift[line]
1✔
250
        maximum = state.eMarks[line]
1✔
251

252
        while pos < maximum:
1✔
253
            if state.src[pos] == "[":
1✔
254
                break
1✔
255
            elif state.src[pos] == " ":
1✔
NEW
256
                pos += 1
×
257
            else:
258
                return None
1✔
259
        else:
NEW
260
            return None
×
261

262
        start = pos
1✔
263

264
        pos += 1
1✔
265
        if state.src[pos : pos + self.bbcode_len].lower() != self.bbcode:
1✔
266
            return None
1✔
267

268
        pos += self.bbcode_len
1✔
269
        if state.src[pos] == "]":
1✔
270
            end = pos + 1
1✔
271

272
            if state.src[end:maximum].strip():
1✔
273
                return None
1✔
274

275
            return BBCodeBlockStart(
1✔
276
                start=start,
277
                end=end,
278
                markup=state.src[start:end],
279
                attrs=None,
280
            )
281

282
        if state.src[pos] != "=":
1✔
NEW
283
            return None
×
284

285
        if not self.args_parser:
1✔
NEW
286
            return None
×
287

288
        pos += 1
1✔
289

290
        level = 1
1✔
291
        args_start = pos
1✔
292

293
        while pos < maximum:
1✔
294
            if state.src[pos] == "\\":
1✔
NEW
295
                pos += 2
×
296

297
            elif state.src[pos] == "[":
1✔
NEW
298
                level += 1
×
NEW
299
                pos += 1
×
300

301
            elif state.src[pos] == "]":
1✔
302
                level -= 1
1✔
303
                if not level:
1✔
304
                    attrs = None
1✔
305
                    if args_str := state.src[args_start:pos].strip():
1✔
306
                        attrs = self.parse_args(args_str)
1✔
307

308
                    end = pos + 1
1✔
309
                    if state.src[end:maximum].strip():
1✔
NEW
310
                        return None
×
311

312
                    return BBCodeBlockStart(
1✔
313
                        start=start,
314
                        end=pos,
315
                        markup=state.src[start:pos],
316
                        attrs=attrs,
317
                    )
318

NEW
319
                pos += 1
×
320

321
            else:
322
                pos += 1
1✔
323

324
        else:
NEW
325
            return None
×
326

327
    def find_multi_line_bbcode_block_end(
1✔
328
        self, state: StateBlock, line: int
329
    ) -> BBCodeBlockEnd | None:
330
        pos = state.bMarks[line] + state.tShift[line]
1✔
331
        maximum = state.eMarks[line]
1✔
332

333
        while pos < maximum:
1✔
334
            if state.src[pos] == "[":
1✔
335
                start = pos
1✔
336
                break
1✔
337
            elif state.src[pos] == " ":
1✔
NEW
338
                pos += 1
×
339
            else:
340
                return None
1✔
341

342
        pos += 1
1✔
343
        if state.src[pos] != "/":
1✔
344
            return None
1✔
345

346
        pos += 1
1✔
347
        if state.src[pos : pos + self.bbcode_len].lower() != self.bbcode:
1✔
348
            return None
1✔
349

350
        pos += self.bbcode_len
1✔
351
        if state.src[pos] != "]":
1✔
NEW
352
            return None
×
353

354
        end = pos + 1
1✔
355
        if state.src[end:maximum].strip():
1✔
NEW
356
            return None
×
357

358
        return BBCodeBlockEnd(
1✔
359
            start=start,
360
            end=end,
361
            markup=state.src[start:end],
362
        )
363

364
    def parse_args(self, args_str: str) -> dict | None:
1✔
365
        if args_str and (
1✔
366
            (args_str[0] == '"' and args_str[-1] == '"')
367
            or (args_str[0] == "'" and args_str[-1] == "'")
368
        ):
369
            args_str = args_str[1:-1]
1✔
370

371
        args_str = args_str.strip() or None
1✔
372
        if args_str:
1✔
373
            return self.args_parser(args_str)
1✔
374

375
        return None
1✔
376

377
    def state_push_open_token(
1✔
378
        self,
379
        state: StateBlock,
380
        startLine: int,
381
        endLine: int,
382
        markup: str,
383
        attrs: dict | None = None,
384
    ) -> Token:
385
        token = state.push(f"{self.name}_open", self.element, 1)
1✔
386
        token.markup = markup
1✔
387
        token.map = [startLine, endLine]
1✔
388

389
        if attrs:
1✔
390
            for attr_name, attr_value in attrs.items():
1✔
391
                token.attrSet(attr_name, attr_value)
1✔
392

393
            if meta := self.get_meta(attrs):
1✔
394
                token.meta = meta
1✔
395

396
        return token
1✔
397

398
    def state_push_close_token(self, state: StateBlock, markup: str) -> Token:
1✔
399
        token = state.push(f"{self.name}_close", self.element, -1)
1✔
400
        token.markup = markup
1✔
401
        return token
1✔
402

403
    def state_push_void_token(
1✔
404
        self, state: StateBlock, line: int, markup: str, attrs: dict | None
405
    ) -> Token:
406
        token = state.push(self.name, self.element, 0)
1✔
407
        token.markup = markup
1✔
408
        token.map = [line, line]
1✔
409

410
        if attrs:
1✔
411
            for attr_name, attr_value in attrs.items():
1✔
412
                token.attrSet(attr_name, attr_value)
1✔
413

414
            if meta := self.get_meta(attrs):
1✔
415
                token.meta = meta
1✔
416

417
        return token
1✔
418

419
    def state_push_children(self, state: StateBlock, startLine: int, endLine: int):
1✔
420
        if startLine == endLine:
1✔
421
            return
×
422

423
        old_line_max = state.lineMax
1✔
424
        old_parent = state.parentType
1✔
425

426
        state.lineMax = endLine
1✔
427
        state.parentType = self.name
1✔
428

429
        state.level += 1
1✔
430
        state.md.block.tokenize(state, startLine, endLine)
1✔
431
        state.level -= 1
1✔
432

433
        state.lineMax = old_line_max
1✔
434
        state.parentType = old_parent
1✔
435

436
    def get_meta(self, attrs: dict) -> dict | None:
1✔
437
        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

© 2026 Coveralls, Inc