diff --git a/pkg/_pub_shared/lib/search/search_form.dart b/pkg/_pub_shared/lib/search/search_form.dart index a773152e35..198a6c9590 100644 --- a/pkg/_pub_shared/lib/search/search_form.dart +++ b/pkg/_pub_shared/lib/search/search_form.dart @@ -17,7 +17,8 @@ final RegExp _allDependencyRegExp = final _sortRegExp = RegExp('sort:([a-z]+)'); final _updatedRegExp = RegExp('updated:([0-9][0-9a-z]*)'); final _tagRegExp = - RegExp(r'([\+|\-]?[a-z0-9]+:[a-z0-9\-_\.]+)', caseSensitive: false); + RegExp(r'([\+\-]?[a-z0-9]+:[a-z0-9\-_\.]+)', caseSensitive: false); +final _topicRegExp = RegExp(r'([\+\-])?#([a-z0-9-]{2,32})'); /// The tag prefixes that we can detect in the user-provided search query. final _detectedTagPrefixes = { @@ -152,6 +153,10 @@ class TagsPredicate { } else if (tag.startsWith('+')) { tag = tag.substring(1); } + + if (tag.startsWith('#')) { + tag = 'topic:${tag.substring(1)}'; + } p._values[tag] = required; } return p; @@ -222,7 +227,10 @@ class TagsPredicate { /// Returns the list of tag values that can be passed to search service URL. List toQueryParameters() { - return _values.entries.map((e) => e.value ? e.key : '-${e.key}').toList(); + return _values.entries.map((e) { + final tag = e.key; + return e.value ? tag : '-$tag'; + }).toList(); } } @@ -263,10 +271,14 @@ class ParsedQueryText { queryText = queryText.replaceFirst(_packageRegexp, ' '); } - List extractRegExp(RegExp regExp, {bool Function(String?)? where}) { + List extractRegExp( + RegExp regExp, { + bool Function(String?)? where, + String? Function(Match m)? matchMapFn, + }) { final values = regExp .allMatches(queryText!) - .map((Match m) => m.group(1)) + .map((Match m) => matchMapFn == null ? m.group(1) : matchMapFn(m)) .where((s) => where == null || where(s)) .cast() .toList(); @@ -289,7 +301,13 @@ class ParsedQueryText { _tagRegExp, where: (tag) => _detectedTagPrefixes.any((p) => tag!.startsWith(p)), ); - final tagsPredicate = TagsPredicate.parseQueryValues(tagValues); + final topicValues = extractRegExp(_topicRegExp, matchMapFn: (m) { + final sign = m.group(1) ?? ''; + final value = m.group(2); + return '${sign}topic:$value'; + }); + final tagsPredicate = + TagsPredicate.parseQueryValues([...tagValues, ...topicValues]); queryText = queryText!.replaceAll(_whitespacesRegExp, ' ').trim(); if (queryText!.isEmpty) { diff --git a/pkg/_pub_shared/test/search/search_form_test.dart b/pkg/_pub_shared/test/search/search_form_test.dart index 85504c7a38..e7e498cceb 100644 --- a/pkg/_pub_shared/test/search/search_form_test.dart +++ b/pkg/_pub_shared/test/search/search_form_test.dart @@ -199,6 +199,39 @@ void main() { expect(query.parsedQuery.tagsPredicate.toQueryParameters(), ['publisher:example.com']); }); + + test('text + topic with tag prefix', () { + final query = SearchForm(query: 'abc topic:xyz'); + final pq = query.parsedQuery; + expect(pq.text, 'abc'); + expect(pq.tagsPredicate.toQueryParameters(), ['topic:xyz']); + expect(pq.toString(), 'topic:xyz abc'); + }); + + test('text + topic with hash prefix', () { + final query = SearchForm(query: 'abc #xyz'); + final pq = query.parsedQuery; + expect(pq.text, 'abc'); + expect(pq.tagsPredicate.toQueryParameters(), ['topic:xyz']); + expect(pq.tagsPredicate.matches(['topic:xyz']), true); + expect(pq.tagsPredicate.matches(['topic:abc']), false); + expect(pq.toString(), 'topic:xyz abc'); + }); + + test('negative hash is parsed', () { + final query = SearchForm(query: '-#abc'); + final pq = query.parsedQuery; + expect(pq.tagsPredicate.toQueryParameters(), ['-topic:abc']); + expect(pq.tagsPredicate.matches(['topic:xyz']), true); + expect(pq.tagsPredicate.matches(['topic:abc']), false); + expect(pq.toString(), '-topic:abc'); + }); + + test('negative tag predicate in the serialization parsing', () { + final tp = TagsPredicate.parseQueryValues(['-#abc']); + expect(tp.matches(['topic:abc']), false); + expect(tp.matches(['topic:xyz']), true); + }); }); group('Search URLs', () {