Files
upstream-zulip/zerver/lib/topic_link_util.py
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

110 lines
3.8 KiB
Python

# See the Zulip URL spec at https://zulip.com/api/zulip-urls
#
# Keep this synchronized with web/src/topic_link_util.ts
import re
from zerver.lib.url_encoding import encode_channel, encode_hash_component
from zerver.models.messages import Message
invalid_stream_topic_regex = re.compile(r"[`>*&\[\]]|(\$\$)")
def will_produce_broken_stream_topic_link(word: str) -> bool:
return bool(invalid_stream_topic_regex.search(word))
TOPIC_LINK_SYNTAX_FOR_DISPLAY = "#{channel_name} > {topic_name}"
escape_mapping = {
"`": "`",
">": ">",
"*": "*",
"&": "&",
"$$": "$$",
"[": "[",
"]": "]",
}
def escape_invalid_stream_topic_characters(text: str) -> str:
return re.sub(
invalid_stream_topic_regex,
lambda match: escape_mapping.get(match.group(0), match.group(0)),
text,
)
def get_message_link_label(
stream_name: str, topic_name: str | None = None, message_id: int | None = None
) -> str:
"""The text label for a channel/topic/message Markdown link, of the form
"#channel > topic @ 💬" (matching the web app's "Copy link to message").
Characters that would break the surrounding Markdown link syntax are
escaped, and an empty topic is shown using its fallback display name.
"""
escape = escape_invalid_stream_topic_characters
text = f"#{escape(stream_name)}"
if topic_name is not None:
if topic_name == "":
topic_name = Message.EMPTY_TOPIC_FALLBACK_NAME
text += f" > {escape(topic_name)}"
if message_id is not None:
text += " @ 💬"
return text
def get_fallback_markdown_link(
stream_id: int, stream_name: str, topic_name: str | None = None, message_id: int | None = None
) -> str:
"""
Helper that should only be called by other methods in this file.
Generates the vanilla markdown link syntax for a stream/topic/message link, as
a fallback for cases where the nicer Zulip link syntax would not
render properly due to special characters in the channel or topic name.
"""
link = f"#narrow/channel/{encode_channel(stream_id, stream_name)}"
if topic_name is not None:
link += f"/topic/{encode_hash_component(topic_name)}"
if message_id is not None:
link += f"/near/{message_id}"
text = get_message_link_label(stream_name, topic_name, message_id)
return f"[{text}]({link})"
def get_message_link_syntax(
stream_id: int, stream_name: str, topic_name: str, message_id: int
) -> str:
# If the stream/topic name is such that it will
# generate an invalid #**stream>topic@message_id** syntax,
# we revert to generating the normal markdown syntax for a link.
if will_produce_broken_stream_topic_link(topic_name) or will_produce_broken_stream_topic_link(
stream_name
):
return get_fallback_markdown_link(stream_id, stream_name, topic_name, message_id)
return f"#**{stream_name}>{topic_name}@{message_id}**"
def get_stream_topic_link_syntax(stream_id: int, stream_name: str, topic_name: str) -> str:
# If the stream/topic name is such that it will generate an invalid #**stream>topic** syntax,
# we revert to generating the normal markdown syntax for a link.
if will_produce_broken_stream_topic_link(topic_name) or will_produce_broken_stream_topic_link(
stream_name
):
return get_fallback_markdown_link(stream_id, stream_name, topic_name)
return f"#**{stream_name}>{topic_name}**"
def get_stream_link_syntax(stream_id: int, stream_name: str) -> str:
# If the stream name is such that it will generate an invalid #**stream** syntax,
# we revert to generating the normal markdown syntax for a link.
if will_produce_broken_stream_topic_link(stream_name):
return get_fallback_markdown_link(stream_id, stream_name)
return f"#**{stream_name}**"