Repository: rafalp/Misago

Issue:
Fix inline BBCode nesting in message parser
`[u]lorem [u]ipsum[/u] dolor[/u]` should be equal to `[u]lorem ipsum dolor[/u]`

Discussion:

Solution patch:
diff --git a/misago/parser/plugins/formattingbbcode.py b/misago/parser/plugins/formattingbbcode.py
index 66555ca52..01c82fa17 100644
--- a/misago/parser/plugins/formattingbbcode.py
+++ b/misago/parser/plugins/formattingbbcode.py
@@ -38,10 +38,16 @@ def get_formatting_bbcode_rule(name: str, markup: str):
 
         pos = start + 3
         maximum = state.posMax
+        nesting = 1
 
         while pos + 4 <= maximum:
+            if state.src[pos : pos + 3].lower() == markup_open:
+                nesting += 1
+
             if state.src[pos : pos + 4].lower() == markup_close:
-                break
+                nesting -= 1
+                if nesting == 0:
+                    break
 
             pos += 1
 
diff --git a/misago/parser/tokenizer.py b/misago/parser/tokenizer.py
index 6f0f90dd0..73e300ff1 100644
--- a/misago/parser/tokenizer.py
+++ b/misago/parser/tokenizer.py
@@ -23,6 +23,7 @@ def tokenize(parser: MarkdownIt, markup: str) -> list[Token]:
             shorten_link_text,
             extract_attachments,
             remove_repeated_hrs,
+            remove_nested_inline_bbcodes,
             replace_blockquotes_with_misago_quotes,
         ],
     )
@@ -220,6 +221,57 @@ def remove_repeated_hrs(tokens: list[Token]) -> list[Token]:
     return new_tokens
 
 
+def remove_nested_inline_bbcodes(tokens: list[Token]) -> list[Token]:
+    if not tokens:
+        return []
+
+    for token in tokens:
+        if token.type == "inline":
+            _remove_nested_inline_bbcodes_from_inline_token(token)
+
+    return tokens
+
+
+INLINE_BBCODE_OPEN_TAGS = (
+    "bold_bbcode_open",
+    "italics_bbcode_open",
+    "underline_bbcode_open",
+    "strikethrough_bbcode_open",
+)
+
+INLINE_BBCODE_CLOSE_TAGS = (
+    "bold_bbcode_close",
+    "italics_bbcode_close",
+    "underline_bbcode_close",
+    "strikethrough_bbcode_close",
+)
+
+
+def _remove_nested_inline_bbcodes_from_inline_token(token_inline: Token):
+    new_children: list[Token] = []
+    stack: list[str] = []
+
+    for token in token_inline.children:
+        if token.type in INLINE_BBCODE_OPEN_TAGS:
+            if token.tag not in stack:
+                new_children.append(token)
+            stack.append(token.tag)
+
+        elif token.type in INLINE_BBCODE_CLOSE_TAGS:
+            if stack and stack[-1] == token.tag:
+                stack.pop(-1)
+            if token.tag not in stack:
+                new_children.append(token)
+
+        elif token.type == "text" and new_children and new_children[-1].type == "text":
+            new_children[-1].content += token.content
+
+        else:
+            new_children.append(token)
+
+    token_inline.children = new_children
+
+
 def replace_blockquotes_with_misago_quotes(tokens: list[Token]) -> list[Token]:
     for token in tokens:
         if token.tag == "blockquote":


Test patch:
diff --git a/misago/parser/tests/test_formatting_bbcode.py b/misago/parser/tests/test_formatting_bbcode.py
index f8a2b38c6..baf0d68f4 100644
--- a/misago/parser/tests/test_formatting_bbcode.py
+++ b/misago/parser/tests/test_formatting_bbcode.py
@@ -23,6 +23,11 @@ def test_nested_formatting_bbcode(parse_to_html):
     assert html == "<p>Hello <b>wo<u>r</u>ld</b>!</p>"
 
 
+def test_nested_sameformatting_bbcode(parse_to_html):
+    html = parse_to_html("Hello [b]wo[b]r[/b]ld[/b]!")
+    assert html == "<p>Hello <b>world</b>!</p>"
+
+
 def test_formatting_bbcode_parses_contents(parse_to_html):
     html = parse_to_html("Hello [b]<http://example.com>[/b]!")
     assert html == (