From 1d0117759d3a4a27938f85ab763735f8cb80700c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 25 Nov 2024 15:10:13 -0800 Subject: [PATCH 1/3] content [nfc]: Pull out parseUserMention, having regexp do less work This gives us more breathing room for making this handling more complicated, as we'll need for the new "channel-wildcard-mention" class (#1064). --- lib/model/content.dart | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index 7a16b9da2c..bb32ae5435 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -872,6 +872,29 @@ class _ZulipContentParser { return descendant4.text.trim(); } + UserMentionNode? parseUserMention(dom.Element element) { + assert(_debugParserContext == _ParserContext.inline); + assert(element.localName == 'span'); + final debugHtmlNode = kDebugMode ? element : null; + + final classes = element.className.split(' ')..sort(); + assert(classes.contains('user-mention') + || classes.contains('user-group-mention')); + switch (classes) { + case ['user-mention' || 'user-group-mention']: + case ['silent', 'user-mention' || 'user-group-mention']: + break; + default: + return null; + } + + // TODO assert UserMentionNode can't contain LinkNode; + // either a debug-mode check, or perhaps we can make expectations much + // tighter on a UserMentionNode's contents overall. + final nodes = parseInlineContentList(element.nodes); + return UserMentionNode(nodes: nodes, debugHtmlNode: debugHtmlNode); + } + /// The links found so far in the current block inline container. /// /// Empty is represented as null. @@ -884,12 +907,12 @@ class _ZulipContentParser { return result; } - static final _userMentionClassNameRegexp = () { - // This matches a class `user-mention` or `user-group-mention`, - // plus an optional class `silent`, appearing in either order. - const mentionClass = r"user(?:-group)?-mention"; - return RegExp("^(?:$mentionClass(?: silent)?|silent $mentionClass)\$"); - }(); + /// Matches all className values that could be a UserMentionNode, + /// and no className values that could be any other type of node. + // Specifically, checks for `user-mention` or `user-group-mention` + // as a member of the list. + static final _userMentionClassNameRegexp = RegExp( + r"(^| )" r"user(?:-group)?-mention" r"( |$)"); static final _emojiClassNameRegexp = () { const specificEmoji = r"emoji(?:-[0-9a-f]+)+"; @@ -944,10 +967,7 @@ class _ZulipContentParser { if (localName == 'span' && _userMentionClassNameRegexp.hasMatch(className)) { - // TODO assert UserMentionNode can't contain LinkNode; - // either a debug-mode check, or perhaps we can make expectations much - // tighter on a UserMentionNode's contents overall. - return UserMentionNode(nodes: nodes(), debugHtmlNode: debugHtmlNode); + return parseUserMention(element) ?? unimplemented(); } if (localName == 'span' From 07f882d5a159416bbdaf674e751ee6034647f42c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 25 Nov 2024 15:31:44 -0800 Subject: [PATCH 2/3] content [nfc]: Update comments in UserMentionNode about UI design --- lib/model/content.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index bb32ae5435..a9877cfde2 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -706,17 +706,15 @@ class UserMentionNode extends InlineContainerNode { const UserMentionNode({ super.debugHtmlNode, required super.nodes, - // required this.mentionType, - // required this.isSilent, }); - // We don't currently seem to need this information in code. Instead, + // For the legacy design, we don't need this information in code; instead, // the inner text already shows how to communicate it to the user // (e.g., silent mentions' text lacks a leading "@"), // and we show that text in the same style for all types of @-mention. - // If we need this information in the future, go ahead and add it here. - // final UserMentionType mentionType; - // final bool isSilent; + // We'll need these for implementing the post-2023 Zulip design, though. + // final UserMentionType mentionType; // TODO(#646) + // final bool isSilent; // TODO(#647) } sealed class EmojiNode extends InlineContentNode { From d21d76bbf9ec6680bfc46a63c417f9c679235dd4 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 25 Nov 2024 15:26:26 -0800 Subject: [PATCH 3/3] content [nfc]: Expand parseUserMention to a more sequential structure This should further help make space for adding more logic here, both for the "channel-wildcard-mention" class (#1064) and for distinguishing different types of mentions (#646, #647). --- lib/model/content.dart | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index a9877cfde2..7186400ae9 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -878,12 +878,24 @@ class _ZulipContentParser { final classes = element.className.split(' ')..sort(); assert(classes.contains('user-mention') || classes.contains('user-group-mention')); - switch (classes) { - case ['user-mention' || 'user-group-mention']: - case ['silent', 'user-mention' || 'user-group-mention']: - break; - default: - return null; + int i = 0; + + if (i >= classes.length) return null; + if (classes[i] == 'silent') { + // A silent @-mention. We ignore this flag; see [UserMentionNode]. + i++; + } + + if (i >= classes.length) return null; + if (classes[i] == 'user-mention' || classes[i] == 'user-group-mention') { + // The class we already knew we'd find before we called this function. + // We ignore the distinction between these; see [UserMentionNode]. + i++; + } + + if (i != classes.length) { + // There was some class we didn't expect. + return null; } // TODO assert UserMentionNode can't contain LinkNode;