From 87a5c8fe7a0992f75b5ea0840e16851d47efd50c Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 9 May 2025 16:59:21 +0200 Subject: [PATCH 1/2] Promote name matches to top position + add name match badge after the title. --- app/lib/frontend/templates/admin.dart | 1 + app/lib/frontend/templates/listing.dart | 28 +------------------ app/lib/frontend/templates/package_misc.dart | 3 ++ .../frontend/templates/views/pkg/index.dart | 6 ---- .../templates/views/pkg/package_list.dart | 15 +++++++++- app/lib/search/mem_index.dart | 16 +++++++++-- app/test/search/mem_index_test.dart | 13 +++++---- pkg/web_css/lib/src/_list.scss | 6 ---- pkg/web_css/lib/src/_pkg.scss | 2 +- 9 files changed, 40 insertions(+), 50 deletions(-) diff --git a/app/lib/frontend/templates/admin.dart b/app/lib/frontend/templates/admin.dart index b14098030a..8c7d81e1ac 100644 --- a/app/lib/frontend/templates/admin.dart +++ b/app/lib/frontend/templates/admin.dart @@ -58,6 +58,7 @@ String renderAccountPackagesPage({ searchForm: null, sdkLibraryHits: [], packageHits: packageHits, + nameMatches: null, ), if (nextPackage != null) d.div( diff --git a/app/lib/frontend/templates/listing.dart b/app/lib/frontend/templates/listing.dart index 62b92cf416..4f3f124897 100644 --- a/app/lib/frontend/templates/listing.dart +++ b/app/lib/frontend/templates/listing.dart @@ -5,11 +5,9 @@ import 'dart:math'; import 'package:_pub_shared/search/search_form.dart'; -import 'package:collection/collection.dart'; import '../../package/search_adapter.dart'; import '../../search/search_service.dart'; -import '../../shared/urls.dart' as urls; import '../dom/dom.dart' as d; import '_consts.dart'; @@ -29,6 +27,7 @@ d.Node packageList(SearchResultPage searchResultPage) { searchForm: searchResultPage.form, sdkLibraryHits: searchResultPage.sdkLibraryHits, packageHits: searchResultPage.packageHits, + nameMatches: searchResultPage.nameMatches, ); } @@ -50,7 +49,6 @@ String renderPkgIndexPage( title: topPackages, messageFromBackend: searchResultPage.errorMessage, ), - nameMatches: _nameMatches(searchForm, searchResultPage.nameMatches), packageList: packageList(searchResultPage), pagination: searchResultPage.hasHit ? paginationNode(links) : null, openSections: openSections, @@ -123,27 +121,3 @@ class PageLinks { return min(fromSymmetry, max(currentPage!, fromCount)); } } - -d.Node? _nameMatches(SearchForm form, List? matches) { - if (matches == null || matches.isEmpty) { - return null; - } - final singular = matches.length == 1; - final isExactNameMatch = singular && form.parsedQuery.text == matches.single; - final nameMatchLabel = isExactNameMatch - ? 'Exact package name match: ' - : 'Matching package ${singular ? 'name' : 'names'}: '; - - return d.p(children: [ - d.text(nameMatchLabel), - ...matches.expandIndexed((i, name) { - return [ - if (i > 0) d.text(', '), - d.a( - href: urls.pkgPageUrl(name), - child: d.b(text: name), - ), - ]; - }), - ]); -} diff --git a/app/lib/frontend/templates/package_misc.dart b/app/lib/frontend/templates/package_misc.dart index e63856fe1f..a1162bf6da 100644 --- a/app/lib/frontend/templates/package_misc.dart +++ b/app/lib/frontend/templates/package_misc.dart @@ -29,6 +29,9 @@ final flutterFavoriteBadgeNode = packageBadgeNode( ), ); +/// Renders the name match badge node, used for exact package name hits. +final nameMatchBadgeNode = packageBadgeNode(label: 'Name match'); + /// Renders the null-safe badge used by package listing and package page. d.Node nullSafeBadgeNode({String? title}) { return packageBadgeNode( diff --git a/app/lib/frontend/templates/views/pkg/index.dart b/app/lib/frontend/templates/views/pkg/index.dart index cc5bc889af..f74fae78b6 100644 --- a/app/lib/frontend/templates/views/pkg/index.dart +++ b/app/lib/frontend/templates/views/pkg/index.dart @@ -13,18 +13,12 @@ import '../../../static_files.dart'; d.Node packageListingNode({ required SearchForm searchForm, required d.Node listingInfo, - required d.Node? nameMatches, required d.Node packageList, required d.Node? pagination, required Set? openSections, }) { final innerContent = d.fragment([ listingInfo, - if (nameMatches != null) - d.div( - classes: ['listing-highlight-block'], - child: nameMatches, - ), packageList, if (pagination != null) pagination, d.markdown('Check our help page for details on ' diff --git a/app/lib/frontend/templates/views/pkg/package_list.dart b/app/lib/frontend/templates/views/pkg/package_list.dart index 5063932a54..95b97754e9 100644 --- a/app/lib/frontend/templates/views/pkg/package_list.dart +++ b/app/lib/frontend/templates/views/pkg/package_list.dart @@ -27,12 +27,19 @@ d.Node listOfPackagesNode({ required SearchForm? searchForm, required List sdkLibraryHits, required List packageHits, + required List? nameMatches, }) { + final bestNameMatch = + (nameMatches == null || nameMatches.isEmpty) ? null : nameMatches.first; return d.div( classes: ['packages'], children: [ ...sdkLibraryHits.map(_sdkLibraryItem), - ...packageHits.map((hit) => _packageItem(hit, searchForm: searchForm)), + ...packageHits.map((hit) => _packageItem( + hit, + searchForm: searchForm, + isNameMatch: hit.name == bestNameMatch, + )), imageCarousel(), ], ); @@ -48,6 +55,7 @@ d.Node _sdkLibraryItem(SdkLibraryHit hit) { return _item( url: hit.url!, name: hit.library!, + isNameMatch: false, newTimestamp: null, labeledScoresNode: null, description: hit.description ?? '', @@ -71,6 +79,7 @@ d.Node _sdkLibraryItem(SdkLibraryHit hit) { d.Node _packageItem( PackageView view, { required SearchForm? searchForm, + required bool isNameMatch, }) { final isFlutterFavorite = view.tags.contains(PackageTags.isFlutterFavorite); final isNullSafe = view.tags.contains(PackageVersionTags.isNullSafe); @@ -177,6 +186,7 @@ d.Node _packageItem( screenshotDescriptions: screenshotDescriptions, url: urls.pkgPageUrl(view.name), name: view.name, + isNameMatch: isNameMatch, newTimestamp: view.created, labeledScoresNode: labeledScoresNodeFromPackageView(view), description: view.ellipsizedDescription ?? '', @@ -207,6 +217,7 @@ d.Node _item({ List topics = const [], required String url, required String name, + required bool isNameMatch, required DateTime? newTimestamp, required d.Node? labeledScoresNode, required String description, @@ -230,6 +241,8 @@ d.Node _item({ ], children: [ d.a(href: url, text: name), if (copyIcon != null) copyIcon, + d.text(' '), + if (isNameMatch) nameMatchBadgeNode, ]), if (age != null && age.inDays <= 30) d.div( diff --git a/app/lib/search/mem_index.dart b/app/lib/search/mem_index.dart index fce0fc6c70..6d66e91611 100644 --- a/app/lib/search/mem_index.dart +++ b/app/lib/search/mem_index.dart @@ -23,6 +23,7 @@ final _textSearchTimeout = Duration(milliseconds: 500); class InMemoryPackageIndex { final List _documents; final _documentsByName = {}; + final _nameToIndex = {}; late final PackageNameIndex _packageNameIndex; late final TokenIndex _descrIndex; late final TokenIndex _readmeIndex; @@ -61,6 +62,7 @@ class InMemoryPackageIndex { for (var i = 0; i < _documents.length; i++) { final doc = _documents[i]; _documentsByName[doc.package] = doc; + _nameToIndex[doc.package] = i; // transform tags into numberical IDs final tagIds = []; @@ -265,8 +267,11 @@ class InMemoryPackageIndex { /// it linearly into the [0.4-1.0] range, to allow better /// multiplication outcomes. packageScores.multiplyAllFromValues(_adjustedOverallScores); - indexedHits = _rankWithValues(packageScores, - requiredLengthThreshold: query.offset); + indexedHits = _rankWithValues( + packageScores, + requiredLengthThreshold: query.offset, + bestNameMatch: bestNameMatch, + ); break; case SearchOrder.text: indexedHits = _rankWithValues(packageScores, @@ -465,11 +470,14 @@ class InMemoryPackageIndex { IndexedScore score, { // if the item count is fewer than this threshold, an empty list will be returned int? requiredLengthThreshold, + String? bestNameMatch, }) { final list = []; + final bestNameIndex = + bestNameMatch == null ? null : _nameToIndex[bestNameMatch]; for (var i = 0; i < score.length; i++) { final value = score.getValue(i); - if (value <= 0.0) continue; + if (value <= 0.0 && i != bestNameIndex) continue; list.add(IndexedPackageHit( i, PackageHit(package: score.keys[i], score: value))); } @@ -478,6 +486,8 @@ class InMemoryPackageIndex { return []; } list.sort((a, b) { + if (a.index == bestNameIndex) return -1; + if (b.index == bestNameIndex) return 1; final scoreCompare = -a.hit.score!.compareTo(b.hit.score!); if (scoreCompare != 0) return scoreCompare; // if two packages got the same score, order by last updated diff --git a/app/test/search/mem_index_test.dart b/app/test/search/mem_index_test.dart index de358dff21..f928fe516b 100644 --- a/app/test/search/mem_index_test.dart +++ b/app/test/search/mem_index_test.dart @@ -636,9 +636,9 @@ server.dart adds a small, prescriptive server (PicoServer) that can be configure 'nameMatches': ['abc'], 'sdkLibraryHits': [], 'packageHits': [ - // `abc` is at its natural place - {'package': 'def', 'score': closeTo(0.85, 0.01)}, + // `abc` is at the first position, score is kept {'package': 'abc', 'score': closeTo(0.70, 0.01)}, + {'package': 'def', 'score': closeTo(0.85, 0.01)}, ] }); // exact name match with tags @@ -651,9 +651,9 @@ server.dart adds a small, prescriptive server (PicoServer) that can be configure 'nameMatches': ['abc'], 'sdkLibraryHits': [], 'packageHits': [ - // `abc` is at its natural place - {'package': 'def', 'score': closeTo(0.85, 0.01)}, + // `abc` is at the first position, score is kept {'package': 'abc', 'score': closeTo(0.70, 0.01)}, + {'package': 'def', 'score': closeTo(0.85, 0.01)}, ] }); // absent exact name match with tags @@ -662,11 +662,12 @@ server.dart adds a small, prescriptive server (PicoServer) that can be configure .toJson(), { 'timestamp': isNotEmpty, - 'totalCount': 1, + 'totalCount': 2, 'nameMatches': ['abc'], 'sdkLibraryHits': [], 'packageHits': [ - // `abc` is not present in the package list + // `abc` is at the first position, score is zero + {'package': 'abc', 'score': 0.0}, {'package': 'def', 'score': closeTo(0.85, 0.01)}, ] }); diff --git a/pkg/web_css/lib/src/_list.scss b/pkg/web_css/lib/src/_list.scss index 816e36c23e..7b9cde7f0d 100644 --- a/pkg/web_css/lib/src/_list.scss +++ b/pkg/web_css/lib/src/_list.scss @@ -46,12 +46,6 @@ } } -.listing-highlight-block { - border-left: 0.25em solid var(--pub-markdown-alert-note); - padding: .5rem 1rem; - margin: 4px 0px; -} - .sort-control { position: relative; cursor: pointer; diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 27fd0245c1..0837d38576 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -511,7 +511,7 @@ display: inline-block; height: 20px; width: 20px; - margin-left: 12px; + margin: 0px 12px; .pkg-page-title-copy-icon { display: block; From 4689f4ebefafa63069b4974680dd2389a8cee211 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Mon, 12 May 2025 09:00:55 +0200 Subject: [PATCH 2/2] Use inverted badge. --- app/lib/frontend/templates/package_misc.dart | 5 ++++- pkg/web_css/lib/src/_pkg.scss | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/lib/frontend/templates/package_misc.dart b/app/lib/frontend/templates/package_misc.dart index a1162bf6da..78f30685c7 100644 --- a/app/lib/frontend/templates/package_misc.dart +++ b/app/lib/frontend/templates/package_misc.dart @@ -30,7 +30,10 @@ final flutterFavoriteBadgeNode = packageBadgeNode( ); /// Renders the name match badge node, used for exact package name hits. -final nameMatchBadgeNode = packageBadgeNode(label: 'Name match'); +final nameMatchBadgeNode = packageBadgeNode( + label: 'Name match', + color: 'inverted', +); /// Renders the null-safe badge used by package listing and package page. d.Node nullSafeBadgeNode({String? title}) { diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 0837d38576..7c87cf4b25 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -20,6 +20,12 @@ color: var(--pub-badge-red-color); } + &.package-badge-inverted { + color: var(--pub-neutral-bgColor); + background: var(--pub-badge-default-color); + border: 1px solid var(--pub-badge-default-color); + } + .package-badge-icon { max-width: 13px; max-height: 13px;