mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
stream_topic_link: Add support for empty string topic in syntax.
This commit adds support for empty string as a valid topic name in syntax for linking to topics. The server stores it after empty string is replaced with `realm_empty_topic_display_name` and wrapped with an <em> tag. The web client parses the rendered_content and updates the topic_name part in the HTML with topic_name in user's language + wraps it in a <span> tag with 'empty-topic-display' css class.
This commit is contained in:
committed by
Tim Abbott
parent
3759525807
commit
e08bf15682
@@ -10,6 +10,8 @@ import render_topic_link from "../templates/topic_link.hbs";
|
||||
import marked from "../third/marked/lib/marked.cjs";
|
||||
import type {LinkifierMatch, ParseOptions, RegExpOrStub} from "../third/marked/lib/marked.cjs";
|
||||
|
||||
import * as util from "./util.ts";
|
||||
|
||||
// This contains zulip's frontend Markdown implementation; see
|
||||
// docs/subsystems/markdown.md for docs on our Markdown syntax. The other
|
||||
// main piece in rendering Markdown client-side is
|
||||
@@ -639,14 +641,15 @@ function handleStreamTopic({
|
||||
stream_topic_hash: (stream_id: number, topic: string) => string;
|
||||
}): string | undefined {
|
||||
const stream = get_stream_by_name(stream_name);
|
||||
if (stream === undefined || !topic) {
|
||||
if (stream === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const href = stream_topic_hash(stream.stream_id, topic);
|
||||
return render_topic_link({
|
||||
channel_id: stream.stream_id,
|
||||
channel_name: stream.name,
|
||||
topic_display_name: topic,
|
||||
topic_display_name: util.get_final_topic_display_name(topic),
|
||||
is_empty_string_topic: topic === "",
|
||||
href,
|
||||
});
|
||||
}
|
||||
|
@@ -6,9 +6,11 @@ import assert from "minimalistic-assert";
|
||||
import code_buttons_container from "../templates/code_buttons_container.hbs";
|
||||
import render_markdown_timestamp from "../templates/markdown_timestamp.hbs";
|
||||
import render_mention_content_wrapper from "../templates/mention_content_wrapper.hbs";
|
||||
import render_topic_link from "../templates/topic_link.hbs";
|
||||
|
||||
import * as blueslip from "./blueslip.ts";
|
||||
import {show_copied_confirmation} from "./copied_tooltip.ts";
|
||||
import * as hash_util from "./hash_util.ts";
|
||||
import {$t} from "./i18n.ts";
|
||||
import * as message_store from "./message_store.ts";
|
||||
import type {Message} from "./message_store.ts";
|
||||
@@ -234,20 +236,29 @@ export const update_elements = ($content: JQuery): void => {
|
||||
});
|
||||
|
||||
$content.find("a.stream-topic").each(function (): void {
|
||||
const stream_id_string = $(this).attr("data-stream-id");
|
||||
assert(stream_id_string !== undefined);
|
||||
const stream_id = Number.parseInt(stream_id_string, 10);
|
||||
if (stream_id && $(this).find(".highlight").length === 0) {
|
||||
// Display the current name for stream if it is not
|
||||
// being displayed in search highlight.
|
||||
const stream_name = sub_store.maybe_get_stream_name(stream_id);
|
||||
if (stream_name !== undefined) {
|
||||
// If the stream has been deleted,
|
||||
// sub_store.maybe_get_stream_name might return
|
||||
// undefined. Otherwise, display the current stream name.
|
||||
const text = $(this).text();
|
||||
$(this).text("#" + stream_name + text.slice(text.indexOf(" > ")));
|
||||
}
|
||||
const narrow_url = $(this).attr("href");
|
||||
assert(narrow_url !== undefined);
|
||||
const channel_topic = hash_util.decode_stream_topic_from_url(
|
||||
window.location.origin + narrow_url,
|
||||
);
|
||||
assert(channel_topic !== null);
|
||||
const channel_name = sub_store.maybe_get_stream_name(channel_topic.stream_id);
|
||||
if (channel_name !== undefined && $(this).find(".highlight").length === 0) {
|
||||
// Display the current channel name if it hasn't been deleted
|
||||
// and not being displayed in search highlight.
|
||||
// TODO: Ideally, we should NOT skip this if only topic is highlighted,
|
||||
// but we are doing so currently.
|
||||
const topic_name = channel_topic.topic_name;
|
||||
assert(topic_name !== undefined);
|
||||
const topic_display_name = util.get_final_topic_display_name(topic_name);
|
||||
const topic_link_html = render_topic_link({
|
||||
channel_id: channel_topic.stream_id,
|
||||
channel_name,
|
||||
topic_display_name,
|
||||
is_empty_string_topic: topic_name === "",
|
||||
href: narrow_url,
|
||||
});
|
||||
$(this).replaceWith($(topic_link_html));
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,2 +1,13 @@
|
||||
<a class="stream-topic" data-stream-id="{{channel_id}}" href="{{href}}">#{{channel_name}} > {{topic_display_name}}</a>
|
||||
{{~!-- squash whitespace --~}}
|
||||
{{#if is_empty_string_topic}}
|
||||
<a class="stream-topic" data-stream-id="{{channel_id}}" href="{{href}}">
|
||||
{{~!-- squash whitespace --~}}
|
||||
#{{channel_name}} > <span class="empty-topic-display">{{topic_display_name}}</span>
|
||||
{{~!-- squash whitespace --~}}
|
||||
</a>
|
||||
{{~else}}
|
||||
<a class="stream-topic" data-stream-id="{{channel_id}}" href="{{href}}">
|
||||
{{~!-- squash whitespace --~}}
|
||||
#{{channel_name}} > {{topic_display_name}}
|
||||
{{~!-- squash whitespace --~}}
|
||||
</a>
|
||||
{{~/if}}
|
||||
|
@@ -60,7 +60,8 @@ const stream_data = zrequire("stream_data");
|
||||
const user_groups = zrequire("user_groups");
|
||||
const {initialize_user_settings} = zrequire("user_settings");
|
||||
|
||||
set_realm({});
|
||||
const REALM_EMPTY_TOPIC_DISPLAY_NAME = "general chat";
|
||||
set_realm({realm_empty_topic_display_name: REALM_EMPTY_TOPIC_DISPLAY_NAME});
|
||||
const user_settings = {};
|
||||
initialize_user_settings({user_settings});
|
||||
|
||||
@@ -411,15 +412,15 @@ test("marked", ({override}) => {
|
||||
expected:
|
||||
'<p>This is a <a class="stream-topic" data-stream-id="1" href="#narrow/channel/1-Denmark/topic/some.20topic">#Denmark > some topic</a> stream_topic link</p>',
|
||||
},
|
||||
{
|
||||
input: "This is a #**Denmark>** stream_topic link with empty string topic.",
|
||||
expected: `<p>This is a <a class="stream-topic" data-stream-id="1" href="#narrow/channel/1-Denmark/topic/">#Denmark > <span class="empty-topic-display">translated: ${REALM_EMPTY_TOPIC_DISPLAY_NAME}</span></a> stream_topic link with empty string topic.</p>`,
|
||||
},
|
||||
{
|
||||
input: "This has two links: #**Denmark>some topic** and #**social>other topic**.",
|
||||
expected:
|
||||
'<p>This has two links: <a class="stream-topic" data-stream-id="1" href="#narrow/channel/1-Denmark/topic/some.20topic">#Denmark > some topic</a> and <a class="stream-topic" data-stream-id="2" href="#narrow/channel/2-social/topic/other.20topic">#social > other topic</a>.</p>',
|
||||
},
|
||||
{
|
||||
input: "This is not a #**Denmark>** stream_topic link",
|
||||
expected: "<p>This is not a #**Denmark>** stream_topic link</p>",
|
||||
},
|
||||
{
|
||||
input: "Look at #**Denmark>message_link@100**",
|
||||
expected:
|
||||
|
@@ -35,7 +35,8 @@ mock_esm("../src/settings_data", {
|
||||
const {set_realm} = zrequire("state_data");
|
||||
const {initialize_user_settings} = zrequire("user_settings");
|
||||
|
||||
const realm = {};
|
||||
const REALM_EMPTY_TOPIC_DISPLAY_NAME = "general chat";
|
||||
const realm = {realm_empty_topic_display_name: REALM_EMPTY_TOPIC_DISPLAY_NAME};
|
||||
set_realm(realm);
|
||||
const user_settings = {};
|
||||
initialize_user_settings({user_settings});
|
||||
@@ -407,19 +408,33 @@ run_test("user-group-mention (error)", () => {
|
||||
assert.ok(!$group.hasClass("user-mention-me"));
|
||||
});
|
||||
|
||||
run_test("stream-links", () => {
|
||||
run_test("stream-links", ({mock_template}) => {
|
||||
// Setup
|
||||
const $content = get_content_element();
|
||||
const $stream = $.create("a.stream");
|
||||
$stream.set_find_results(".highlight", false);
|
||||
$stream.attr("data-stream-id", stream.stream_id);
|
||||
|
||||
const $stream_topic = $.create("a.stream-topic");
|
||||
$stream_topic.set_find_results(".highlight", false);
|
||||
$stream_topic.attr("data-stream-id", stream.stream_id);
|
||||
$stream_topic.attr(
|
||||
"href",
|
||||
`/#narrow/channel/${stream.stream_id}-random/topic/topic.20name.20.3E.20still.20the.20topic.20name`,
|
||||
);
|
||||
$stream_topic.replaceWith = noop;
|
||||
$stream_topic.text("#random > topic name > still the topic name");
|
||||
|
||||
$content.set_find_results("a.stream", $array([$stream]));
|
||||
$content.set_find_results("a.stream-topic", $array([$stream_topic]));
|
||||
|
||||
let topic_link_context;
|
||||
let topic_link_rendered_html;
|
||||
mock_template("topic_link.hbs", true, (data, html) => {
|
||||
topic_link_context = data;
|
||||
topic_link_rendered_html = html;
|
||||
return html;
|
||||
});
|
||||
|
||||
// Initial asserts
|
||||
assert.equal($stream.text(), "never-been-set");
|
||||
assert.equal($stream_topic.text(), "#random > topic name > still the topic name");
|
||||
@@ -428,7 +443,48 @@ run_test("stream-links", () => {
|
||||
|
||||
// Final asserts
|
||||
assert.equal($stream.text(), `#${stream.name}`);
|
||||
assert.equal($stream_topic.text(), `#${stream.name} > topic name > still the topic name`);
|
||||
assert.deepEqual(topic_link_context, {
|
||||
channel_id: stream.stream_id,
|
||||
channel_name: stream.name,
|
||||
topic_display_name: "topic name > still the topic name",
|
||||
is_empty_string_topic: false,
|
||||
href: `/#narrow/channel/${stream.stream_id}-random/topic/topic.20name.20.3E.20still.20the.20topic.20name`,
|
||||
});
|
||||
assert.ok(!topic_link_rendered_html.includes("empty-topic-display"));
|
||||
});
|
||||
|
||||
run_test("topic-link (empty string topic)", ({mock_template}) => {
|
||||
// Setup
|
||||
const $content = get_content_element();
|
||||
const $channel_topic = $.create("a.stream-topic(empty-string-topic)");
|
||||
$channel_topic.set_find_results(".highlight", false);
|
||||
$channel_topic.attr("href", `/#narrow/channel/${stream.stream_id}-random/topic/`);
|
||||
$channel_topic.replaceWith = noop;
|
||||
$channel_topic.html(`#random > <em>${REALM_EMPTY_TOPIC_DISPLAY_NAME}</em>`);
|
||||
$content.set_find_results("a.stream-topic", $array([$channel_topic]));
|
||||
|
||||
let topic_link_context;
|
||||
let topic_link_rendered_html;
|
||||
mock_template("topic_link.hbs", true, (data, html) => {
|
||||
topic_link_context = data;
|
||||
topic_link_rendered_html = html;
|
||||
return html;
|
||||
});
|
||||
|
||||
// Initial assert
|
||||
assert.equal($channel_topic.html(), "#random > <em>general chat</em>");
|
||||
|
||||
rm.update_elements($content);
|
||||
|
||||
// Final assert
|
||||
assert.deepEqual(topic_link_context, {
|
||||
channel_id: stream.stream_id,
|
||||
channel_name: stream.name,
|
||||
topic_display_name: `translated: ${REALM_EMPTY_TOPIC_DISPLAY_NAME}`,
|
||||
is_empty_string_topic: true,
|
||||
href: `/#narrow/channel/${stream.stream_id}-random/topic/`,
|
||||
});
|
||||
assert.ok(topic_link_rendered_html.includes("empty-topic-display"));
|
||||
});
|
||||
|
||||
run_test("timestamp without time", () => {
|
||||
|
@@ -546,7 +546,7 @@ inline.zulip = merge({}, inline.breaks, {
|
||||
usermention: /^@(_?)(?:\*\*([^\*]+)\*\*)/, // Match potentially multi-word string between @** **
|
||||
groupmention: /^@(_?)(?:\*([^\*]+)\*)/, // Match multi-word string between @* *
|
||||
stream_topic_message: /^#\*\*([^\*>]+)>([^\*]+)@(\d+)\*\*/,
|
||||
stream_topic: /^#\*\*([^\*>]+)>([^\*]+)\*\*/,
|
||||
stream_topic: /^#\*\*([^\*>]+)>([^\*]*)\*\*/,
|
||||
stream: /^#\*\*([^\*]+)\*\*/,
|
||||
tex: /^(\$\$([^\n_$](\\\$|[^\n$])*)\$\$(?!\$))\B/,
|
||||
timestamp: /^<time:([^>]+)>/,
|
||||
|
@@ -177,7 +177,7 @@ STREAM_TOPIC_LINK_REGEX = rf"""
|
||||
\#\*\* # 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<topic_name>[^\*]*) # topic name can be an empty string or contain anything
|
||||
\*\* # ends by double asterisks
|
||||
"""
|
||||
|
||||
@@ -2066,8 +2066,16 @@ class StreamTopicPattern(StreamTopicMessageProcessor):
|
||||
topic_url = hash_util_encode(topic_name)
|
||||
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}"
|
||||
el.set("href", link)
|
||||
text = f"#{stream_name} > {topic_name}"
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
|
||||
if topic_name == "":
|
||||
topic_el = Element("em")
|
||||
topic_el.text = Message.EMPTY_TOPIC_FALLBACK_NAME
|
||||
el.text = markdown.util.AtomicString(f"#{stream_name} > ")
|
||||
el.append(topic_el)
|
||||
else:
|
||||
text = f"#{stream_name} > {topic_name}"
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
|
||||
return el, m.start(), m.end()
|
||||
|
||||
|
||||
|
@@ -3097,6 +3097,12 @@ class MarkdownStreamMentionTests(ZulipTestCase):
|
||||
render_message_markdown(msg, content).rendered_content,
|
||||
f'<p><a class="stream-topic" data-stream-id="{denmark.id}" href="/#narrow/channel/{denmark.id}-Denmark/topic/some.20topic">#{denmark.name} > some topic</a></p>',
|
||||
)
|
||||
# Empty string as topic name.
|
||||
content = "#**Denmark>**"
|
||||
self.assertEqual(
|
||||
render_message_markdown(msg, content).rendered_content,
|
||||
f'<p><a class="stream-topic" data-stream-id="{denmark.id}" href="/#narrow/channel/{denmark.id}-Denmark/topic/">#{denmark.name} > <em>{Message.EMPTY_TOPIC_FALLBACK_NAME}</em></a></p>',
|
||||
)
|
||||
|
||||
def test_topic_atomic_string(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
|
Reference in New Issue
Block a user