markdown: Add support for a pretty syntax for message links.

Links to zulip messages can now be written as
`#**channel_name > topic_name @ message_id**.`
The `message_id` is replaced with `💬` in the rendered
message.

Fixes part of #31920
This commit is contained in:
Kislay Udbhav Verma 2024-10-27 18:40:11 +05:30 committed by Tim Abbott
parent 65c9b249b7
commit 000cc7bcde
2 changed files with 75 additions and 0 deletions

View File

@ -194,6 +194,31 @@ def get_compiled_stream_topic_link_regex() -> Pattern[str]:
) )
STREAM_TOPIC_MESSAGE_LINK_REGEX = rf"""
{BEFORE_MENTION_ALLOWED_REGEX} # Start after whitespace or specified chars
\#\*\* # and after hash sign followed by double asterisks
(?P<stream_name>[^\*>]+) # stream name can contain anything except >
> # > acts as separator
(?P<topic_name>[^\*]+) # topic name can contain anything
@
(?P<message_id>\d+) # message id
\*\* # ends by double asterisks
"""
@lru_cache(None)
def get_compiled_stream_topic_message_link_regex() -> Pattern[str]:
# Not using verbose_compile as it adds ^(.*?) and
# (.*?)$ which cause extra overhead of matching
# pattern which is not required.
# With new InlineProcessor these extra patterns
# are not required.
return re.compile(
STREAM_TOPIC_MESSAGE_LINK_REGEX,
re.DOTALL | re.VERBOSE,
)
@lru_cache(None) @lru_cache(None)
def get_web_link_regex() -> Pattern[str]: def get_web_link_regex() -> Pattern[str]:
# We create this one time, but not at startup. So the # We create this one time, but not at startup. So the
@ -2041,6 +2066,29 @@ class StreamTopicPattern(StreamTopicMessageProcessor):
return el, m.start(), m.end() return el, m.start(), m.end()
class StreamTopicMessagePattern(StreamTopicMessageProcessor):
@override
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
self, m: Match[str], data: str
) -> tuple[Element | str | None, int | None, int | None]:
stream_name = m.group("stream_name")
topic_name = m.group("topic_name")
message_id = m.group("message_id")
stream_id = self.find_stream_id(stream_name)
if stream_id is None or topic_name is None:
return None, None, None
el = Element("a")
el.set("class", "message-link")
stream_url = encode_stream(stream_id, stream_name)
topic_url = hash_util_encode(topic_name)
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}/near/{message_id}"
el.set("href", link)
text = f"#{stream_name} > {topic_name} @ 💬"
el.text = markdown.util.AtomicString(text)
return el, m.start(), m.end()
def possible_linked_stream_names(content: str) -> set[str]: def possible_linked_stream_names(content: str) -> set[str]:
return { return {
*re.findall(STREAM_LINK_REGEX, content, re.VERBOSE), *re.findall(STREAM_LINK_REGEX, content, re.VERBOSE),
@ -2305,6 +2353,11 @@ class ZulipMarkdown(markdown.Markdown):
) )
reg.register(UserMentionPattern(mention.MENTIONS_RE, self), "usermention", 95) reg.register(UserMentionPattern(mention.MENTIONS_RE, self), "usermention", 95)
reg.register(Tex(TEX_RE, self), "tex", 90) reg.register(Tex(TEX_RE, self), "tex", 90)
reg.register(
StreamTopicMessagePattern(get_compiled_stream_topic_message_link_regex(), self),
"stream_topic_message",
89,
)
reg.register(StreamTopicPattern(get_compiled_stream_topic_link_regex(), self), "topic", 87) reg.register(StreamTopicPattern(get_compiled_stream_topic_link_regex(), self), "topic", 87)
reg.register(StreamPattern(get_compiled_stream_link_regex(), self), "stream", 85) reg.register(StreamPattern(get_compiled_stream_link_regex(), self), "stream", 85)
reg.register(Timestamp(TIMESTAMP_RE), "timestamp", 75) reg.register(Timestamp(TIMESTAMP_RE), "timestamp", 75)

View File

@ -3108,6 +3108,28 @@ class MarkdownStreamMentionTests(ZulipTestCase):
".</p>", ".</p>",
) )
def test_message_id_multiple(self) -> None:
denmark = get_stream("Denmark", get_realm("zulip"))
sender_user_profile = self.example_user("othello")
msg = Message(
sender=sender_user_profile,
sending_client=get_client("test"),
realm=sender_user_profile.realm,
)
content = "As mentioned in #**Denmark>danish@123** and #**Denmark>danish@456**."
self.assertEqual(
render_message_markdown(msg, content).rendered_content,
"<p>As mentioned in "
f'<a class="message-link" '
f'href="/#narrow/channel/{denmark.id}-{denmark.name}/topic/danish/near/123">'
f"#Denmark &gt; danish @ 💬</a>"
" and "
f'<a class="message-link" '
f'href="/#narrow/channel/{denmark.id}-{denmark.name}/topic/danish/near/456">'
f"#Denmark &gt; danish @ 💬</a>"
".</p>",
)
def test_possible_stream_names(self) -> None: def test_possible_stream_names(self) -> None:
content = """#**test here** content = """#**test here**
This mentions #**Denmark** too. This mentions #**Denmark** too.