diff --git a/.github/workflows/deploy-ghpages.yml b/.github/workflows/deploy-ghpages.yml
index 13899a7c..ed22f155 100644
--- a/.github/workflows/deploy-ghpages.yml
+++ b/.github/workflows/deploy-ghpages.yml
@@ -5,6 +5,10 @@ on:
branches:
- main
- emily/gh-pages-deploy
+ schedule:
+ # Rebuild weekly (Saturday night / Sunday 00:00 UTC)
+ - cron: "0 0 * * 0"
+ workflow_dispatch: # Allow manual trigger
jobs:
build_and_deploy:
diff --git a/plugins/repo-data-omit-list.json b/plugins/repo-data-omit-list.json
index eaf34965..f5c00a2e 100644
--- a/plugins/repo-data-omit-list.json
+++ b/plugins/repo-data-omit-list.json
@@ -1,4 +1,37 @@
{
- "description": "List of repositories to exclude from the Repository Explorer. Add repository full names (e.g., 'owner/repo-name') to the omitRepos array.",
- "omitRepos": ["voqal/voqal"]
+ "description": "Configuration for Repository Explorer exclusions and tag normalization.",
+ "omitRepos": ["voqal/voqal", "Jailsonfs/community"],
+ "implicitTags": [
+ "talonvoice",
+ "talon",
+ "voice",
+ "voice-recognition",
+ "speech-recognition",
+ "voice-commands",
+ "voice-control"
+ ],
+ "tagAliases": {
+ "a11y": "accessibility",
+ "macos-accessibility": "accessibility",
+ "maths": "math",
+ "python3": "python",
+ "hci": "human-computer-interaction",
+ "games": "game",
+ "garrysmod": "game",
+ "gmod": "game",
+ "slaythespire": "game",
+ "slaythespire-mod": "game",
+ "gameboyadvance": "game",
+ "mgba": "game",
+ "blazor-server": "blazor",
+ "blazor-components": "blazor",
+ "awesome": "list",
+ "awesome-list": "list",
+ "gpt": "ai",
+ "openai": "ai",
+ "chatgpt": "ai",
+ "llm": "ai",
+ "copilot": "ai"
+ },
+ "matchNamesToExistingTags": true
}
diff --git a/plugins/repo-data-plugin.js b/plugins/repo-data-plugin.js
index e3f2800c..2e20b5ff 100644
--- a/plugins/repo-data-plugin.js
+++ b/plugins/repo-data-plugin.js
@@ -11,8 +11,11 @@ module.exports = function (context, options) {
"../.docusaurus/repo-data-plugin/default/repos.json",
);
- // Load omit configuration from repo-explorer-omit-list.json
+ // Load omit configuration from repo-data-omit-list.json
let omitRepos = [];
+ let implicitTags = [];
+ let tagAliases = {};
+ let matchNamesToExistingTags = false;
try {
const omitListFile = path.join(__dirname, "repo-data-omit-list.json");
@@ -21,6 +24,21 @@ module.exports = function (context, options) {
if (omitConfig.omitRepos && Array.isArray(omitConfig.omitRepos)) {
omitRepos = omitConfig.omitRepos;
}
+ if (
+ omitConfig.implicitTags &&
+ Array.isArray(omitConfig.implicitTags)
+ ) {
+ implicitTags = omitConfig.implicitTags;
+ }
+ if (
+ omitConfig.tagAliases &&
+ typeof omitConfig.tagAliases === "object"
+ ) {
+ tagAliases = omitConfig.tagAliases;
+ }
+ if (omitConfig.matchNamesToExistingTags === true) {
+ matchNamesToExistingTags = true;
+ }
}
} catch (error) {
console.warn("Failed to load repo-data-omit-list.json:", error.message);
@@ -33,7 +51,70 @@ module.exports = function (context, options) {
console.log(
`Repository omit list loaded: ${omitRepos.length} repositories will be excluded`,
);
- } // Determine if we should fetch fresh data
+ }
+
+ // canonicalTags is built after repos are loaded, since we need
+ // to include tags that actually exist across repos.
+ let canonicalTags = null;
+
+ function buildCanonicalTags(repos) {
+ const tags = new Set(Object.values(tagAliases));
+ repos.forEach((repo) => {
+ repo.topics.forEach((t) => {
+ const canonical = tagAliases[t] || t;
+ if (!implicitTags.includes(canonical)) {
+ tags.add(canonical);
+ }
+ });
+ });
+ return tags;
+ }
+
+ /**
+ * Split a repo name into words, handling kebab-case, snake_case,
+ * camelCase, PascalCase, and mixed conventions.
+ * e.g. "VoiceLauncherBlazor" -> ["voice", "launcher", "blazor"]
+ * "talon-mouse-rig" -> ["talon", "mouse", "rig"]
+ * "talon_mgba_http" -> ["talon", "mgba", "http"]
+ */
+ function splitRepoName(name) {
+ return (
+ name
+ // Insert boundary before uppercase runs: "VoiceLauncher" -> "Voice Launcher"
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
+ // Split acronym from next word: "HTTPServer" -> "HTTP Server"
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
+ .split(/[-_.\s]+/)
+ .map((w) => w.toLowerCase())
+ .filter((w) => w.length > 2)
+ );
+ }
+
+ /**
+ * Infer tags from a repo's name by matching words against known
+ * canonical tags. Only adds tags the repo doesn't already have
+ * (after alias resolution).
+ */
+ function inferTags(repo) {
+ if (!matchNamesToExistingTags || !canonicalTags) return;
+ const words = splitRepoName(repo.name);
+ const existingCanonical = new Set(
+ repo.topics.map((t) => tagAliases[t] || t),
+ );
+ for (const word of words) {
+ const canonical = tagAliases[word] || word;
+ if (
+ canonicalTags.has(canonical) &&
+ !existingCanonical.has(canonical) &&
+ !implicitTags.includes(canonical)
+ ) {
+ repo.topics.push(word);
+ existingCanonical.add(canonical);
+ }
+ }
+ }
+
+ // Determine if we should fetch fresh data
const isUpdateRepos =
(process.env.npm_config_argv &&
JSON.parse(process.env.npm_config_argv).original.includes(
@@ -74,12 +155,17 @@ module.exports = function (context, options) {
);
}
+ canonicalTags = buildCanonicalTags(filteredRepos);
+ filteredRepos.forEach(inferTags);
+
return {
...cachedData,
repositories: filteredRepos,
filtered_count: filteredRepos.length,
omitted_count:
(cachedData.repositories.length || 0) - filteredRepos.length,
+ implicitTags,
+ tagAliases,
};
}
} catch (error) {
@@ -157,12 +243,17 @@ module.exports = function (context, options) {
`After filtering: ${filteredRepos.length} repositories (${allRepos.length - filteredRepos.length} omitted)`,
);
+ canonicalTags = buildCanonicalTags(filteredRepos);
+ filteredRepos.forEach(inferTags);
+
return {
repositories: filteredRepos,
total_count: totalCount,
filtered_count: filteredRepos.length,
omitted_count: allRepos.length - filteredRepos.length,
generated_at: new Date().toISOString(),
+ implicitTags,
+ tagAliases,
};
} catch (error) {
console.error("Failed to fetch repository data:", error);
@@ -179,6 +270,9 @@ module.exports = function (context, options) {
return !omitRepos.includes(fullName);
});
+ canonicalTags = buildCanonicalTags(filteredRepos);
+ filteredRepos.forEach(inferTags);
+
return {
...cachedData,
repositories: filteredRepos,
@@ -186,6 +280,8 @@ module.exports = function (context, options) {
omitted_count:
(cachedData.repositories.length || 0) - filteredRepos.length,
error: `Build-time fetch failed: ${error.message}. Using cached data.`,
+ implicitTags,
+ tagAliases,
};
}
} catch (cacheError) {
@@ -198,6 +294,8 @@ module.exports = function (context, options) {
total_count: 0,
generated_at: new Date().toISOString(),
error: error.message,
+ implicitTags,
+ tagAliases,
};
}
},
diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx
index a78fe5ea..3cc81978 100644
--- a/src/components/RepoExplorer/index.tsx
+++ b/src/components/RepoExplorer/index.tsx
@@ -25,8 +25,73 @@ interface RepoData {
total_count: number;
generated_at: string;
error?: string;
+ implicitTags?: string[];
+ tagAliases?: Record
talonvoice
- ⚠️ Use at your own risk: These repositories may - not be curated or tested. For curated packages, visit{" "} + ⚠️ Use at your own risk: This compilation + wasn't reviewed. For a curated list, visit{" "} - talon_user_file_sets + Talon user file sets + + . Do not download or run anything from unvetted repositories + without careful review. To report a suspicious repository, + please{" "} + + open an issue .