Skip to content

Commit 9bbc448

Browse files
committed
Implement automatic maturity classification for spellers
Replace manual GitHub topics with automatic classification based on version number and lemma count from badge data. Classification rules: - Production: version >= 1.0.0 - Beta: version < 1.0.0 and lemmaCount >= 10k - Alpha: version < 1.0.0 and lemmaCount 1k-10k - Experimental: version < 1.0.0 and lemmaCount < 1k - Undefined: missing version or lemmaCount data New functions: - fetchBadgeData(): Fetch JSON from badgedata/*.json files - parseVersion(): Parse semver strings (handles v-prefix) - parseLemmaCount(): Parse counts (handles space + case-insensitive K) - classifySpellerMaturity(): Classify repos with caching - addSpellerRepoTableByMaturity(): Async table generation - addSpellerUnorderedListByMaturity(): Async list for undefined Features: - Caching prevents duplicate fetches across categories - Transparent, objective classification criteria - Clear upgrade path via version number - Undefined category shows repos needing badge data
1 parent ff5600d commit 9bbc448

2 files changed

Lines changed: 207 additions & 5 deletions

File tree

assets/js/spellertable.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,94 @@
33
// REQUIRES: tablecommon.js and langtable.js must be loaded first
44
// (uses addLemmaCount and addCoreCI from langtable.js)
55

6+
// Automatic maturity classification based on version and lemma count
7+
8+
async function fetchBadgeData(repo, badgeFile) {
9+
try {
10+
const url = `https://raw.githubusercontent.com/giellalt/${repo.name}/gh-pages/badgedata/${badgeFile}`;
11+
const response = await fetch(url);
12+
if (!response.ok) {
13+
return null;
14+
}
15+
const data = await response.json();
16+
return data.message || null;
17+
} catch (error) {
18+
return null;
19+
}
20+
}
21+
22+
function parseVersion(versionString) {
23+
if (!versionString) return null;
24+
// Remove 'v' prefix if present, extract version number
25+
const match = versionString.match(/v?(\d+)\.(\d+)\.(\d+)/);
26+
if (!match) return null;
27+
return {
28+
major: parseInt(match[1]),
29+
minor: parseInt(match[2]),
30+
patch: parseInt(match[3])
31+
};
32+
}
33+
34+
function parseLemmaCount(countString) {
35+
if (!countString) return null;
36+
// Handle formats like "12.3k", "47 K", "1.2k", "234"
37+
// Allow optional whitespace before k/K
38+
const match = countString.match(/^([\d.]+)\s*k?$/i);
39+
if (!match) return null;
40+
const number = parseFloat(match[1]);
41+
// If it has 'k' or 'K' suffix (case-insensitive), multiply by 1000
42+
if (/k/i.test(countString)) {
43+
return Math.floor(number * 1000);
44+
}
45+
return Math.floor(number);
46+
}
47+
48+
// Cache for maturity classifications to avoid re-fetching
49+
const maturityCache = new Map();
50+
51+
async function classifySpellerMaturity(repo) {
52+
// Check cache first
53+
if (maturityCache.has(repo.name)) {
54+
return maturityCache.get(repo.name);
55+
}
56+
57+
// Fetch version and lemma count data
58+
const versionStr = await fetchBadgeData(repo, 'speller-version.json');
59+
const lemmaCountStr = await fetchBadgeData(repo, 'fst-lemmacount.json');
60+
61+
// Parse the data
62+
const version = parseVersion(versionStr);
63+
const lemmaCount = parseLemmaCount(lemmaCountStr);
64+
65+
let result;
66+
67+
// If either is missing, classify as undefined
68+
if (!version || lemmaCount === null) {
69+
result = 'undefined';
70+
}
71+
// Classification logic:
72+
// Production: version >= 1.0.0
73+
else if (version.major >= 1) {
74+
result = 'production';
75+
}
76+
// Beta: version < 1.0.0 and lemmaCount >= 10000
77+
else if (lemmaCount >= 10000) {
78+
result = 'beta';
79+
}
80+
// Alpha: version < 1.0.0 and lemmaCount between 1000 and 10000
81+
else if (lemmaCount >= 1000) {
82+
result = 'alpha';
83+
}
84+
// Experimental: version < 1.0.0 and lemmaCount < 1000
85+
else {
86+
result = 'experimental';
87+
}
88+
89+
// Cache the result
90+
maturityCache.set(repo.name, result);
91+
return result;
92+
}
93+
694
// Spellchecker-specific list item generation
795

896
function addSpellerLi(repo) {
@@ -193,3 +281,107 @@ function addSpellerTR(repo) {
193281

194282
return row;
195283
}
284+
285+
// New maturity-based table generation
286+
287+
async function addSpellerRepoTableByMaturity(repos, mainFilter, maturityLevel) {
288+
let table = document.createElement('table');
289+
let thead = document.createElement('thead');
290+
let tbody = document.createElement('tbody');
291+
292+
table.appendChild(thead);
293+
table.appendChild(tbody);
294+
thead.appendChild(addSpellerTableHeader());
295+
296+
// Handle case where GitHub API data is not available
297+
if (!repos || !Array.isArray(repos)) {
298+
const errorRow = document.createElement('tr');
299+
const errorCell = document.createElement('td');
300+
errorCell.colSpan = 5; // Match number of columns in header
301+
errorCell.innerHTML = '<strong>⚠️ GitHub repository data is temporarily unavailable</strong><br><em>This usually resolves automatically. Please try refreshing the page in a few minutes.</em>';
302+
errorCell.style.textAlign = 'center';
303+
errorCell.style.padding = '30px 20px';
304+
errorCell.style.backgroundColor = '#fff3cd';
305+
errorCell.style.border = '1px solid #ffeaa7';
306+
errorCell.style.borderRadius = '8px';
307+
errorCell.style.color = '#856404';
308+
errorRow.appendChild(errorCell);
309+
tbody.appendChild(errorRow);
310+
return table;
311+
}
312+
313+
// Filter repos by mainFilter and classify them
314+
const langRepos = repos.filter(repo => repo.name.startsWith(mainFilter));
315+
316+
// Classify all repos in parallel
317+
const classifications = await Promise.all(
318+
langRepos.map(async repo => ({
319+
repo: repo,
320+
maturity: await classifySpellerMaturity(repo)
321+
}))
322+
);
323+
324+
// Filter by desired maturity level
325+
const filteredRepos = classifications
326+
.filter(item => item.maturity === maturityLevel)
327+
.map(item => item.repo);
328+
329+
// Add rows to table
330+
for (const repo of filteredRepos) {
331+
tbody.appendChild(addSpellerTR(repo));
332+
}
333+
334+
// If no repos found, inform the user:
335+
if (!tbody.firstChild) {
336+
tbody.appendChild(addEmptyRow(5));
337+
}
338+
339+
return table;
340+
}
341+
342+
async function addSpellerUnorderedListByMaturity(repos, mainFilter) {
343+
const ul = document.createElement('ul');
344+
345+
// Handle case where GitHub API data is not available
346+
if (!repos || !Array.isArray(repos)) {
347+
const p = document.createElement('p');
348+
p.innerHTML = '<strong>⚠️ GitHub repository data is temporarily unavailable</strong><br><em>This usually resolves automatically. Please try refreshing the page in a few minutes.</em>';
349+
p.style.textAlign = 'center';
350+
p.style.padding = '20px';
351+
p.style.backgroundColor = '#fff3cd';
352+
p.style.border = '1px solid #ffeaa7';
353+
p.style.borderRadius = '8px';
354+
p.style.color = '#856404';
355+
return p;
356+
}
357+
358+
// Filter repos by mainFilter and classify them
359+
const langRepos = repos.filter(repo => repo.name.startsWith(mainFilter));
360+
361+
// Classify all repos in parallel
362+
const classifications = await Promise.all(
363+
langRepos.map(async repo => ({
364+
repo: repo,
365+
maturity: await classifySpellerMaturity(repo)
366+
}))
367+
);
368+
369+
// Filter by undefined maturity
370+
const undefinedRepos = classifications
371+
.filter(item => item.maturity === 'undefined')
372+
.map(item => item.repo);
373+
374+
// Add items to list
375+
for (const repo of undefinedRepos) {
376+
ul.appendChild(addSpellerLi(repo));
377+
}
378+
379+
// If no repos found, inform the user:
380+
if (!ul.firstChild) {
381+
const p = document.createElement('p');
382+
p.appendChild(document.createTextNode('No repos found.'));
383+
return p;
384+
}
385+
386+
return ul;
387+
}

proof/spelling/SpellerOverview.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,35 @@ Being in the **Production** group means the speller has been tested and is consi
4545
<!-- Scripts for maturity classes: -->
4646
<script>
4747
const domProdSpellers = document.querySelector('#prod_spellers');
48-
domProdSpellers.appendChild(addSpellerRepoTable({{lang_repos}}, 'lang-', ['maturity-prod', 'speller']))
48+
addSpellerRepoTableByMaturity({{lang_repos}}, 'lang-', 'production').then(table => {
49+
domProdSpellers.appendChild(table);
50+
});
4951
</script>
5052

5153
<script>
5254
const domBetaSpellers = document.querySelector('#beta_spellers');
53-
domBetaSpellers.appendChild(addSpellerRepoTable({{lang_repos}}, 'lang-', ['maturity-beta', 'speller']))
55+
addSpellerRepoTableByMaturity({{lang_repos}}, 'lang-', 'beta').then(table => {
56+
domBetaSpellers.appendChild(table);
57+
});
5458
</script>
5559

5660
<script>
5761
const domAlphaSpellers = document.querySelector('#alpha_spellers');
58-
domAlphaSpellers.appendChild(addSpellerRepoTable({{lang_repos}}, 'lang-', ['maturity-alpha', 'speller']))
62+
addSpellerRepoTableByMaturity({{lang_repos}}, 'lang-', 'alpha').then(table => {
63+
domAlphaSpellers.appendChild(table);
64+
});
5965
</script>
6066

6167
<script>
6268
const domExperSpellers = document.querySelector('#exper_spellers');
63-
domExperSpellers.appendChild(addSpellerRepoTable({{lang_repos}}, 'lang-', ['maturity-exper', 'speller']))
69+
addSpellerRepoTableByMaturity({{lang_repos}}, 'lang-', 'experimental').then(table => {
70+
domExperSpellers.appendChild(table);
71+
});
6472
</script>
6573

6674
<script>
6775
const domUndefSpellers = document.querySelector('#undef_spellers');
68-
domUndefSpellers.appendChild(addNegSpellerUnorderedList({{lang_repos}}, 'lang-', ['maturity-exper', 'maturity-beta', 'maturity-alpha', 'maturity-prod', 'speller']))
76+
addSpellerUnorderedListByMaturity({{lang_repos}}, 'lang-').then(list => {
77+
domUndefSpellers.appendChild(list);
78+
});
6979
</script>

0 commit comments

Comments
 (0)