Skip to content

Commit 032713d

Browse files
committed
compose: Introduce fallbackMarkdownLink function
1 parent 5fc6a99 commit 032713d

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

lib/model/compose.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,56 @@ String wildcardMention(WildcardMentionOption wildcardOption, {
185185
String userGroupMention(String userGroupName, {bool silent = false}) =>
186186
'@${silent ? '_' : ''}*$userGroupName*';
187187

188+
// Corresponds to `topic_link_util.escape_invalid_stream_topic_characters`
189+
// in Zulip web:
190+
// https://github.com/zulip/zulip/blob/b42d3e77e/web/src/topic_link_util.ts#L15-L34
191+
const _channelTopicFaultyCharsReplacements = {
192+
'`': '`',
193+
'>': '>',
194+
'*': '*',
195+
'&': '&',
196+
'[': '[',
197+
']': ']',
198+
r'$$': '$$',
199+
};
200+
201+
final _channelTopicFaultyCharsRegex = RegExp(r'[`>*&[\]]|(?:\$\$)');
202+
203+
/// Markdown link for channel, topic, or message when the channel or topic name
204+
/// includes characters that will break normal markdown rendering.
205+
///
206+
/// Refer to [_channelTopicFaultyCharsReplacements] for a complete list of
207+
/// these characters.
208+
// Corresponds to `topic_link_util.get_fallback_markdown_link` in Zulip web;
209+
// https://github.com/zulip/zulip/blob/b42d3e77e/web/src/topic_link_util.ts#L96-L108
210+
String fallbackMarkdownLink({
211+
required PerAccountStore store,
212+
required ZulipStream channel,
213+
TopicName? topic,
214+
int? nearMessageId,
215+
}) {
216+
assert(nearMessageId == null || topic != null);
217+
218+
String replaceFaultyChars(String str) {
219+
return str.replaceAllMapped(_channelTopicFaultyCharsRegex,
220+
(match) => _channelTopicFaultyCharsReplacements[match[0]]!);
221+
}
222+
223+
final text = StringBuffer(replaceFaultyChars(channel.name));
224+
if (topic != null) {
225+
text.write(' > ${replaceFaultyChars(topic.displayName ?? store.realmEmptyTopicDisplayName)}');
226+
}
227+
if (nearMessageId != null) {
228+
text.write(' @ 💬');
229+
}
230+
231+
final narrow = topic == null
232+
? ChannelNarrow(channel.streamId) : TopicNarrow(channel.streamId, topic);
233+
final linkFragment = narrowLinkFragment(store, narrow, nearMessageId: nearMessageId);
234+
235+
return inlineLink(text.toString(), '#$linkFragment');
236+
}
237+
188238
/// https://spec.commonmark.org/0.30/#inline-link
189239
///
190240
/// The "link text" is made by enclosing [visibleText] in square brackets.

test/model/compose_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:checks/checks.dart';
22
import 'package:test/scaffolding.dart';
33
import 'package:zulip/api/model/events.dart';
4+
import 'package:zulip/api/model/model.dart';
45
import 'package:zulip/model/compose.dart';
56
import 'package:zulip/model/localizations.dart';
67
import 'package:zulip/model/store.dart';
@@ -334,6 +335,38 @@ hello
334335
});
335336
});
336337

338+
test('fallbackMarkdownLink', () async {
339+
final store = eg.store();
340+
final channels = [
341+
eg.stream(streamId: 1, name: '`code`'),
342+
eg.stream(streamId: 2, name: 'score > 90'),
343+
eg.stream(streamId: 3, name: 'A*'),
344+
eg.stream(streamId: 4, name: 'R&D'),
345+
eg.stream(streamId: 5, name: 'UI [v2]'),
346+
eg.stream(streamId: 6, name: r'Save $$'),
347+
];
348+
await store.addStreams(channels);
349+
350+
check(fallbackMarkdownLink(store: store,
351+
channel: channels[1 - 1]))
352+
.equals('[`code`](#narrow/channel/1-.60code.60)');
353+
check(fallbackMarkdownLink(store: store,
354+
channel: channels[2 - 1], topic: TopicName('topic')))
355+
.equals('[score > 90 > topic](#narrow/channel/2-score-.3E-90/topic/topic)');
356+
check(fallbackMarkdownLink(store: store,
357+
channel: channels[3 - 1], topic: TopicName('R&D')))
358+
.equals('[A* > R&D](#narrow/channel/3-A*/topic/R.26D)');
359+
check(fallbackMarkdownLink(store: store,
360+
channel: channels[4 - 1], topic: TopicName('topic'), nearMessageId: 10))
361+
.equals('[R&D > topic @ 💬](#narrow/channel/4-R.26D/topic/topic/near/10)');
362+
check(fallbackMarkdownLink(store: store,
363+
channel: channels[5 - 1], topic: TopicName(r'Save $$'), nearMessageId: 10))
364+
.equals('[UI [v2] > Save $$ @ 💬](#narrow/channel/5-UI-.5Bv2.5D/topic/Save.20.24.24/near/10)');
365+
check(() => fallbackMarkdownLink(store: store,
366+
channel: channels[6 - 1], nearMessageId: 10))
367+
.throws<AssertionError>();
368+
});
369+
337370
test('inlineLink', () {
338371
check(inlineLink('CZO', 'https://chat.zulip.org/')).equals('[CZO](https://chat.zulip.org/)');
339372
check(inlineLink('Uploading file.txt…', '')).equals('[Uploading file.txt…]()');

0 commit comments

Comments
 (0)