diff --git a/web/src/markdown.ts b/web/src/markdown.ts index 471f9c7461..1e2b544555 100644 --- a/web/src/markdown.ts +++ b/web/src/markdown.ts @@ -647,6 +647,33 @@ function handleStreamTopic({ )}" href="/${_.escape(href)}">${_.escape(text)}`; } +function handleStreamTopicMessage({ + stream_name, + topic, + message_id, + get_stream_by_name, + stream_topic_hash, +}: { + stream_name: string; + topic: string; + message_id: number; + get_stream_by_name: (stream_name: string) => + | { + stream_id: number; + name: string; + } + | undefined; + stream_topic_hash: (stream_id: number, topic: string) => string; +}): string | undefined { + const stream = get_stream_by_name(stream_name); + if (stream === undefined || !topic) { + return undefined; + } + const href = stream_topic_hash(stream.stream_id, topic) + "/near/" + message_id; + const text = `#${stream.name} > ${topic} @ 💬`; + return `${_.escape(text)}`; +} + function handleTex(tex: string, fullmatch: string): string { try { return katex.renderToString(tex); @@ -755,6 +782,20 @@ export function parse({ }); } + function streamTopicMessageHandler( + stream_name: string, + topic: string, + message_id: number, + ): string | undefined { + return handleStreamTopicMessage({ + stream_name, + topic, + message_id, + get_stream_by_name: helper_config.get_stream_by_name, + stream_topic_hash: helper_config.stream_topic_hash, + }); + } + function emojiHandler(emoji_name: string): string { return handleEmoji({ emoji_name, @@ -782,6 +823,7 @@ export function parse({ unicodeEmojiHandler, streamHandler, streamTopicHandler, + streamTopicMessageHandler, texHandler: handleTex, timestampHandler: handleTimestamp, gfm: true, diff --git a/web/tests/markdown.test.cjs b/web/tests/markdown.test.cjs index d8d675e0f6..dc999e2776 100644 --- a/web/tests/markdown.test.cjs +++ b/web/tests/markdown.test.cjs @@ -420,6 +420,15 @@ test("marked", ({override}) => { input: "This is not a #**Denmark>** stream_topic link", expected: "

This is not a #**Denmark>** stream_topic link

", }, + { + input: "Look at #**Denmark>message_link@100**", + expected: + '

Look at #Denmark > message_link @ 💬

', + }, + { + input: "Look at #**Unknown>message_link@100**", + expected: "

Look at #**Unknown>message_link@100**

", + }, { input: "mmm...:burrito:s", expected: diff --git a/web/third/marked/lib/marked.cjs b/web/third/marked/lib/marked.cjs index 133a791fed..e0fbe9349d 100644 --- a/web/third/marked/lib/marked.cjs +++ b/web/third/marked/lib/marked.cjs @@ -545,6 +545,7 @@ inline.zulip = merge({}, inline.breaks, { unicodeemoji: possible_emoji_regex, usermention: /^@(_?)(?:\*\*([^\*]+)\*\*)/, // Match potentially multi-word string between @** ** groupmention: /^@(_?)(?:\*([^\*]+)\*)/, // Match multi-word string between @* * + stream_topic_message: /^#\*\*([^\*>]+)>([^\*]+)@(\d+)\*\*/, stream_topic: /^#\*\*([^\*>]+)>([^\*]+)\*\*/, stream: /^#\*\*([^\*]+)\*\*/, tex: /^(\$\$([^\n_$](\\\$|[^\n$])*)\$\$(?!\$))\B/, @@ -754,6 +755,13 @@ InlineLexer.prototype.output = function(src) { continue; } + // stream_topic_message (Zulip) + if (cap = this.rules.stream_topic_message.exec(src)) { + src = src.substring(cap[0].length); + out += this.stream_topic_message(unescape(cap[1]), unescape(cap[2]), unescape(cap[3]), cap[0]); + continue; + } + // stream_topic (Zulip) if (cap = this.rules.stream_topic.exec(src)) { src = src.substring(cap[0].length); @@ -950,6 +958,18 @@ InlineLexer.prototype.stream_topic = function (streamName, topic, orig) { return orig; }; +InlineLexer.prototype.stream_topic_message = function (streamName, topic, message_id, orig) { + orig = escape(orig); + if (typeof this.options.streamTopicMessageHandler !== 'function') + return orig; + + var handled = this.options.streamTopicMessageHandler(streamName, topic, message_id); + if (handled !== undefined) { + return handled; + } + return orig; +}; + /** * Smartypants Transformations */