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 +84 -28
- package/client/client.js.map +1 -1
- package/lib/settings.js +50 -11
- package/lib/types/settings.d.ts +25 -5
- package/package.json +1 -1
- package/src/client/market-data.ts +121 -51
- package/src/settings.ts +73 -18
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
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
1106
|
-
if (
|
|
1107
|
-
if (
|
|
1108
|
-
if (
|
|
1109
|
-
return
|
|
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) {
|