markdown: Update characters allowed before @ and stream mentions.

Now the following characters are allowed before @-mentions and stream
references (starting with #) for proper rendering - {, [, /.

This commit makes the markdown rendering consistent with autocomplete
(anything that is autocompleted is also rendered properly).
This commit is contained in:
N-Shar-ma 2022-06-13 09:32:57 +05:30 committed by Tim Abbott
parent e671decd29
commit ef044b8697
3 changed files with 59 additions and 15 deletions

View File

@ -54,7 +54,12 @@ from zerver.lib.emoji import EMOTICON_RE, codepoint_to_name, name_to_codepoint,
from zerver.lib.exceptions import MarkdownRenderingException from zerver.lib.exceptions import MarkdownRenderingException
from zerver.lib.markdown import fenced_code from zerver.lib.markdown import fenced_code
from zerver.lib.markdown.fenced_code import FENCE_RE from zerver.lib.markdown.fenced_code import FENCE_RE
from zerver.lib.mention import FullNameInfo, MentionBackend, MentionData from zerver.lib.mention import (
BEFORE_MENTION_ALLOWED_REGEX,
FullNameInfo,
MentionBackend,
MentionData,
)
from zerver.lib.outgoing_http import OutgoingSession from zerver.lib.outgoing_http import OutgoingSession
from zerver.lib.subdomains import is_static_or_current_realm_url from zerver.lib.subdomains import is_static_or_current_realm_url
from zerver.lib.tex import render_tex from zerver.lib.tex import render_tex
@ -162,8 +167,8 @@ def verbose_compile(pattern: str) -> Pattern[str]:
) )
STREAM_LINK_REGEX = r""" STREAM_LINK_REGEX = rf"""
(?<![^\s'"\(,:<]) # Start after whitespace or specified chars {BEFORE_MENTION_ALLOWED_REGEX} # Start after whitespace or specified chars
\#\*\* # and after hash sign followed by double asterisks \#\*\* # and after hash sign followed by double asterisks
(?P<stream_name>[^\*]+) # stream name can contain anything (?P<stream_name>[^\*]+) # stream name can contain anything
\*\* # ends by double asterisks \*\* # ends by double asterisks
@ -183,8 +188,8 @@ def get_compiled_stream_link_regex() -> Pattern[str]:
) )
STREAM_TOPIC_LINK_REGEX = r""" STREAM_TOPIC_LINK_REGEX = rf"""
(?<![^\s'"\(,:<]) # Start after whitespace or specified chars {BEFORE_MENTION_ALLOWED_REGEX} # Start after whitespace or specified chars
\#\*\* # and after hash sign followed by double asterisks \#\*\* # and after hash sign followed by double asterisks
(?P<stream_name>[^\*>]+) # stream name can contain anything except > (?P<stream_name>[^\*>]+) # stream name can contain anything except >
> # > acts as separator > # > acts as separator

View File

@ -7,10 +7,16 @@ from django.db.models import Q
from zerver.models import UserGroup, UserProfile, get_linkable_streams from zerver.models import UserGroup, UserProfile, get_linkable_streams
BEFORE_MENTION_ALLOWED_REGEX = r"(?<![^\s\'\"\(\{\[\/<])"
# Match multi-word string between @** ** or match any one-word # Match multi-word string between @** ** or match any one-word
# sequences after @ # sequences after @
MENTIONS_RE = re.compile(r"(?<![^\s\'\"\(,:<])@(?P<silent>_?)(\*\*(?P<match>[^\*]+)\*\*)") MENTIONS_RE = re.compile(
USER_GROUP_MENTIONS_RE = re.compile(r"(?<![^\s\'\"\(,:<])@(?P<silent>_?)(\*(?P<match>[^\*]+)\*)") rf"{BEFORE_MENTION_ALLOWED_REGEX}@(?P<silent>_?)(\*\*(?P<match>[^\*]+)\*\*)"
)
USER_GROUP_MENTIONS_RE = re.compile(
rf"{BEFORE_MENTION_ALLOWED_REGEX}@(?P<silent>_?)(\*(?P<match>[^\*]+)\*)"
)
wildcards = ["all", "everyone", "stream"] wildcards = ["all", "everyone", "stream"]

View File

@ -1,6 +1,7 @@
import copy import copy
import os import os
import re import re
from html import escape
from textwrap import dedent from textwrap import dedent
from typing import Any, Dict, List, Optional, Set, Tuple, cast from typing import Any, Dict, List, Optional, Set, Tuple, cast
from unittest import mock from unittest import mock
@ -1946,6 +1947,38 @@ class MarkdownTest(ZulipTestCase):
) )
self.assertEqual(rendering_result.mentions_user_ids, {user_profile.id}) self.assertEqual(rendering_result.mentions_user_ids, {user_profile.id})
def test_mention_with_valid_special_characters_before(self) -> None:
sender_user_profile = self.example_user("othello")
user_profile = self.example_user("hamlet")
msg = Message(sender=sender_user_profile, sending_client=get_client("test"))
user_id = user_profile.id
valid_characters_before_mention = ["(", "{", "[", "/", "<"]
for character in valid_characters_before_mention:
content = f"{character}@**King Hamlet**"
rendering_result = render_markdown(msg, content)
self.assertEqual(
rendering_result.rendered_content,
f'<p>{escape(character)}<span class="user-mention" '
f'data-user-id="{user_id}">'
"@King Hamlet</span></p>",
)
self.assertEqual(rendering_result.mentions_user_ids, {user_profile.id})
def test_mention_with_invalid_special_characters_before(self) -> None:
sender_user_profile = self.example_user("othello")
msg = Message(sender=sender_user_profile, sending_client=get_client("test"))
invalid_characters_before_mention = [".", ",", ";", ":", "#"]
for character in invalid_characters_before_mention:
content = f"{character}@**King Hamlet**"
rendering_result = render_markdown(msg, content)
unicode_character = escape(character)
self.assertEqual(
rendering_result.rendered_content,
f"<p>{unicode_character}@<strong>King Hamlet</strong></p>",
)
def test_mention_silent(self) -> None: def test_mention_silent(self) -> None:
sender_user_profile = self.example_user("othello") sender_user_profile = self.example_user("othello")
user_profile = self.example_user("hamlet") user_profile = self.example_user("hamlet")