dshmarket 1.38.0 → 1.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -1068,45 +1068,101 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
1068
1068
  function isMarketItself(plugin) {
1069
1069
  return plugin.name === "dsh-market" || plugin.npm === "dshmarket";
1070
1070
  }
1071
+ /** Normalize punctuation-separated package names and human text alike. */
1072
+ function searchText(value) {
1073
+ return value.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().replace(/\s+/g, " ");
1074
+ }
1071
1075
  /**
1072
- * The discover list: category filter, then the published-within window, then
1073
- * search across name / owner / localized description / category ids and
1074
- * localized category labels, then the selected sort.
1075
- * Pure — the section renders exactly this.
1076
+ * Normalized catalog fields are immutable for the lifetime of one registry
1077
+ * entry. Keep them with that entry so typing does not repeat unicode
1078
+ * normalization across the whole catalog, while replaced catalogs remain
1079
+ * collectible. The raw query is intentionally not cached: it is normalized
1080
+ * once per call and would otherwise grow the cache on every keystroke.
1076
1081
  */
1077
- function visiblePlugins(plugins, options) {
1078
- const query = options.query.trim().toLowerCase();
1079
- const list = plugins.filter((p) => {
1080
- if (isMarketItself(p)) return false;
1081
- const categories = pluginCategories(p);
1082
- if (options.category !== "all" && !categories.includes(options.category)) return false;
1083
- if (options.sinceDays !== void 0 && !withinDays(p.added, options.sinceDays)) return false;
1084
- if (query === "") return true;
1085
- const desc = p.description && (p.description[options.lang] || p.description.en) || "";
1086
- const categoryMatches = categories.some((category) => {
1087
- if (category.toLowerCase().includes(query)) return true;
1088
- return Object.values(options.categories?.[category] ?? {}).some((label) => typeof label === "string" && label.toLowerCase().includes(query));
1089
- });
1090
- return p.name.toLowerCase().includes(query) || p.owner.toLowerCase().includes(query) || desc.toLowerCase().includes(query) || categoryMatches;
1091
- });
1082
+ const pluginSearchTextCache = /* @__PURE__ */ new WeakMap();
1083
+ function cachedPluginSearchText(plugin, value) {
1084
+ let fields = pluginSearchTextCache.get(plugin);
1085
+ if (fields === void 0) {
1086
+ fields = /* @__PURE__ */ new Map();
1087
+ pluginSearchTextCache.set(plugin, fields);
1088
+ }
1089
+ const hit = fields.get(value);
1090
+ if (hit !== void 0) return hit;
1091
+ const normalized = searchText(value);
1092
+ fields.set(value, normalized);
1093
+ return normalized;
1094
+ }
1095
+ /**
1096
+ * Relevance within one field. Exact and prefix matches beat phrase matches;
1097
+ * for a multi-word query every word must occur in the same field.
1098
+ */
1099
+ function fieldRelevance(plugin, value, query, tokens, weight) {
1100
+ if (!value) return 0;
1101
+ const text = cachedPluginSearchText(plugin, value);
1102
+ if (text === "" || !tokens.every((token) => text.includes(token))) return 0;
1103
+ if (text === query) return weight + 300;
1104
+ if (text.startsWith(query)) return weight + 250;
1105
+ if (text.includes(query)) return weight + 200;
1106
+ return weight + 150;
1107
+ }
1108
+ /**
1109
+ * Search ranking is field-aware rather than a popularity-only filter:
1110
+ * package identities outrank owners, descriptions, and categories. The
1111
+ * selected popularity/date sort remains the tie-breaker between equally
1112
+ * relevant entries.
1113
+ */
1114
+ function pluginRelevance(plugin, query, tokens, lang, categories) {
1115
+ const descriptions = plugin.description ?? {};
1116
+ const preferredLocale = descriptions[lang] ? lang : descriptions.en ? "en" : null;
1117
+ const preferredDescription = descriptions[lang] || descriptions.en;
1118
+ const otherDescriptions = Object.entries(descriptions).filter(([locale, value]) => locale !== preferredLocale && typeof value === "string").map(([, value]) => value);
1119
+ const categoryIds = pluginCategories(plugin);
1120
+ const categoryLabels = categoryIds.flatMap((category) => Object.values(categories?.[category] ?? {}));
1121
+ return Math.max(fieldRelevance(plugin, plugin.name, query, tokens, 700), fieldRelevance(plugin, plugin.npm, query, tokens, 700), fieldRelevance(plugin, plugin.owner, query, tokens, 400), fieldRelevance(plugin, preferredDescription, query, tokens, 280), ...otherDescriptions.map((value) => fieldRelevance(plugin, value, query, tokens, 240)), ...categoryIds.map((value) => fieldRelevance(plugin, value, query, tokens, 180)), ...categoryLabels.map((value) => fieldRelevance(plugin, value, query, tokens, 180)));
1122
+ }
1123
+ /** Compare two already-filtered entries using the user's selected sort. */
1124
+ function comparePlugins(a, b, sort) {
1092
1125
  const hasDownloads = (p) => typeof p.downloads === "number";
1093
- if (options.sort === "downloads-desc") return [...list].sort((a, b) => {
1126
+ if (sort === "downloads-desc") {
1094
1127
  if (hasDownloads(a) && hasDownloads(b)) return b.downloads - a.downloads;
1095
1128
  if (hasDownloads(a)) return -1;
1096
1129
  if (hasDownloads(b)) return 1;
1097
1130
  return (b.stars ?? -1) - (a.stars ?? -1);
1098
- });
1099
- if (options.sort === "downloads-asc") return [...list].sort((a, b) => {
1131
+ }
1132
+ if (sort === "downloads-asc") {
1100
1133
  if (hasDownloads(a) && hasDownloads(b)) return a.downloads - b.downloads;
1101
1134
  if (hasDownloads(a)) return -1;
1102
1135
  if (hasDownloads(b)) return 1;
1103
1136
  return (a.stars ?? -1) - (b.stars ?? -1);
1104
- });
1105
- if (options.sort === "stars-desc") return [...list].sort((a, b) => (b.stars ?? -1) - (a.stars ?? -1));
1106
- if (options.sort === "stars-asc") return [...list].sort((a, b) => (a.stars ?? -1) - (b.stars ?? -1));
1107
- if (options.sort === "added-desc") return [...list].sort((a, b) => String(b.added).localeCompare(String(a.added)));
1108
- if (options.sort === "added-asc") return [...list].sort((a, b) => String(a.added).localeCompare(String(b.added)));
1109
- return list;
1137
+ }
1138
+ if (sort === "stars-desc") return (b.stars ?? -1) - (a.stars ?? -1);
1139
+ if (sort === "stars-asc") return (a.stars ?? -1) - (b.stars ?? -1);
1140
+ if (sort === "added-desc") return String(b.added).localeCompare(String(a.added));
1141
+ if (sort === "added-asc") return String(a.added).localeCompare(String(b.added));
1142
+ return 0;
1143
+ }
1144
+ /**
1145
+ * The discover list: category filter, then the published-within window, then
1146
+ * relevance-ranked search across package identity / owner / every localized
1147
+ * description / category ids and labels. With no search, only the selected
1148
+ * sort applies, preserving the existing discover-list behaviour.
1149
+ * Pure — the section renders exactly this.
1150
+ */
1151
+ function visiblePlugins(plugins, options) {
1152
+ const query = searchText(options.query);
1153
+ const tokens = query.split(" ").filter(Boolean);
1154
+ return plugins.flatMap((plugin, index) => {
1155
+ if (isMarketItself(plugin)) return [];
1156
+ const categories = pluginCategories(plugin);
1157
+ if (options.category !== "all" && !categories.includes(options.category)) return [];
1158
+ if (options.sinceDays !== void 0 && !withinDays(plugin.added, options.sinceDays)) return [];
1159
+ const relevance = query === "" ? 0 : pluginRelevance(plugin, query, tokens, options.lang, options.categories);
1160
+ return relevance === 0 && query !== "" ? [] : [{
1161
+ plugin,
1162
+ relevance,
1163
+ index
1164
+ }];
1165
+ }).sort((a, b) => b.relevance - a.relevance || comparePlugins(a.plugin, b.plugin, options.sort) || a.index - b.index).map((row) => row.plugin);
1110
1166
  }
1111
1167
  /** The themes tab listing: theme category only, most-starred first. */
1112
1168
  function themePlugins(plugins) {