This commit is contained in:
wilsonfreitas
2026-03-28 16:21:50 +00:00
parent e4a7660bc4
commit f37ea1616f
5 changed files with 10193 additions and 10104 deletions
+132 -35
View File
@@ -26,42 +26,45 @@ def slugify(text: str) -> str:
def parse_readme(path: str) -> list[dict]:
"""Parse README.md and return a list of project entries (no API data)."""
entries = []
current_language = ""
current_category = ""
re_h2 = re.compile(r"^## (.+)$")
re_h3 = re.compile(r"^### (.+)$")
re_entry = re.compile(r"^\s*- \[(.+?)\]\((.+?)\) - (.+)$")
re_github = re.compile(r"\[GitHub\]\((https://github\.com/[\w-]+/[-\w\.]+)\)")
skip_sections = {"Languages"}
# Strip markdown badge images before parsing
re_badge = re.compile(r"\s*!\[[^\]]*\]\([^)]*\)\s*")
re_langs = re.compile(r'^((?:`[^`]+`\s*)+)-\s*(.*)$')
skip_sections = {"Contents"}
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = re_badge.sub(" ", line).rstrip("\n")
m = re_h2.match(line)
if m:
current_language = m.group(1).strip()
current_category = ""
continue
m = re_h3.match(line)
if m:
current_category = m.group(1).strip()
continue
if current_language in skip_sections:
if current_category in skip_sections:
continue
m = re_entry.match(line)
if m:
name = m.group(1).strip()
url = m.group(2).strip()
desc = m.group(3).strip()
raw_desc = m.group(3).strip()
# Extract inline language tags
m_lang = re_langs.match(raw_desc)
if m_lang:
lang_str = m_lang.group(1)
desc = m_lang.group(2)
languages = re.findall(r'`([^`]+)`', lang_str)
else:
desc = raw_desc
languages = []
primary_language = languages[0] if languages else ""
github_url = ""
gh_match = re_github.search(desc)
@@ -81,15 +84,15 @@ def parse_readme(path: str) -> list[dict]:
is_cran = "cran.r-project.org" in url
is_pypi = "pypi.org" in url or "pypi.python.org" in url
is_commercial = current_language == "Commercial & Proprietary Services"
category = current_category or current_language
section_slug = slugify(category)
is_commercial = current_category == "Commercial & Proprietary Services"
section_slug = slugify(current_category)
entries.append(
{
"project": name,
"language": current_language,
"category": category,
"language": primary_language,
"languages": ",".join(languages),
"category": current_category,
"section_slug": section_slug,
"url": url,
"description": desc,
@@ -121,6 +124,8 @@ def load_csv(path: str) -> list[dict]:
# Extract github_url and repo from CSV data
repo = row.get("repo", "")
row["github_url"] = f"https://github.com/{repo}" if repo else ""
# Ensure languages column exists
row["languages"] = row.get("languages", row.get("language", ""))
# Clean description: strip [GitHub](url) if present
desc = row.get("description", "")
desc = re.sub(
@@ -148,25 +153,28 @@ def build_tags_html(e: dict) -> str:
esc = html.escape
tags = []
# Language tag
lang = e.get("language", "")
if lang and lang != "Commercial & Proprietary Services" and lang != "Related Lists":
lang_slug = slugify(lang)
tags.append(
f'<button class="tag tag-lang" data-filter-type="language" '
f'data-filter-value="{esc(lang)}">{esc(lang_slug)}</button>'
)
# Language tags (from inline backtick tags)
languages_str = e.get("languages", e.get("language", ""))
languages = [l.strip() for l in languages_str.split(",") if l.strip()]
skip_langs = {"Commercial & Proprietary Services", "Related Lists",
"Reproducing Works, Training & Books", "Cross-Language Frameworks"}
for lang in languages:
if lang and lang not in skip_langs:
tags.append(
f'<button class="tag tag-lang" data-filter-type="language" '
f'data-filter-value="{esc(lang)}">{esc(lang.lower())}</button>'
)
# Section tag
section_slug = e.get("section_slug", "")
# Category tag
category = e.get("category", "")
if section_slug and section_slug != slugify(lang):
section_slug = e.get("section_slug", "")
if section_slug:
tags.append(
f'<button class="tag tag-section" data-filter-type="category" '
f'data-filter-value="{esc(category)}">{esc(section_slug)}</button>'
)
# Source tags
# Source tags (unchanged)
if e.get("github"):
tags.append(
'<button class="tag tag-source tag-github" '
@@ -191,16 +199,99 @@ def build_tags_html(e: dict) -> str:
return "\n ".join(tags)
def build_tag_cloud(entries: list[dict]) -> str:
"""Build a tag cloud of popular languages and categories."""
from collections import Counter
# Count language frequencies
lang_counts = Counter()
for e in entries:
langs = [l.strip() for l in e.get("languages", e.get("language", "")).split(",") if l.strip()]
lang_counts.update(langs)
# Remove non-language sections
skip = {"Commercial & Proprietary Services", "Related Lists", "Reproducing Works, Training & Books", "Cross-Language Frameworks"}
for s in skip:
lang_counts.pop(s, None)
# Count category frequencies
cat_counts = Counter(e.get("category", "") for e in entries if e.get("category"))
# Get top items
top_langs = lang_counts.most_common(8) # Top 8 languages
top_cats = cat_counts.most_common(6) # Top 6 categories
if not top_langs and not top_cats:
return ""
# Combine and sort by frequency
all_items = []
for lang, count in top_langs:
all_items.append(("language", lang, count))
for cat, count in top_cats:
all_items.append(("category", cat, count))
# Sort by count descending
all_items.sort(key=lambda x: x[2], reverse=True)
# Calculate size scale (1.0 to 1.6x)
if all_items:
min_count = min(item[2] for item in all_items)
max_count = max(item[2] for item in all_items)
count_range = max_count - min_count if max_count > min_count else 1
else:
min_count = max_count = count_range = 1
tags = []
esc = html.escape
for tag_type, value, count in all_items:
# Calculate font size: 1.0 to 1.6
size = 1.0 + ((count - min_count) / count_range * 0.6) if count_range > 0 else 1.0
if tag_type == "language":
tags.append(
f'<button class="tag tag-lang tag-cloud-item" '
f'data-filter-type="language" data-filter-value="{esc(value)}" '
f'style="font-size: {size:.2f}em;">{esc(value.lower())}</button>'
)
else: # category
tags.append(
f'<button class="tag tag-section tag-cloud-item" '
f'data-filter-type="category" data-filter-value="{esc(value)}" '
f'style="font-size: {size:.2f}em;">{esc(value)}</button>'
)
if not tags:
return ""
return f""" <div class="tag-cloud">
<div class="tag-cloud-label">Popular filters:</div>
<div class="tag-cloud-items">
{chr(10).join(" " + tag for tag in tags)}
</div>
</div>"""
def generate_html(entries: list[dict]) -> str:
"""Generate the full HTML page from project entries."""
languages = sorted(
set(
e["language"]
lang.strip()
for e in entries
if e.get("language") and e["language"] != "Languages"
for lang in e.get("languages", e.get("language", "")).split(",")
if lang.strip()
and lang.strip() not in {
"Commercial & Proprietary Services",
"Related Lists",
"Reproducing Works, Training & Books",
"Cross-Language Frameworks",
}
)
)
# Generate tag cloud
tag_cloud_html = build_tag_cloud(entries)
# Build table rows
rows = []
for i, e in enumerate(entries, 1):
@@ -208,7 +299,6 @@ def generate_html(entries: list[dict]) -> str:
name = esc(e["project"])
url = esc(e["url"])
desc = esc(e["description"])
language = esc(e.get("language", ""))
category = esc(e.get("category", ""))
github_url = esc(e.get("github_url", ""))
repo = esc(e.get("repo", ""))
@@ -219,6 +309,11 @@ def generate_html(entries: list[dict]) -> str:
is_pypi = e.get("pypi", False)
is_commercial = e.get("commercial", False)
# Languages attribute (space-separated)
languages_attr = esc(" ".join(
l.strip() for l in e.get("languages", e.get("language", "")).split(",") if l.strip()
))
# Stars display
stars_html = (
f'<span class="stars" title="{stars:,} stars">'
@@ -250,7 +345,7 @@ def generate_html(entries: list[dict]) -> str:
tags_html = build_tags_html(e)
rows.append(
f""" <tr class="row" data-language="{language}" data-category="{category}" data-sources="{sources_attr}" data-stars="{stars}">
f""" <tr class="row" data-languages="{languages_attr}" data-category="{category}" data-sources="{sources_attr}" data-stars="{stars}">
<td class="col-num">{i}</td>
<td class="col-name">
<a href="{url}" target="_blank" rel="noopener">{name}</a>
@@ -331,6 +426,8 @@ def generate_html(entries: list[dict]) -> str:
</div>
</div>
{tag_cloud_html}
<div class="filter-bar" id="filter-bar" style="display:none">
<span class="filter-label">Filtered by:</span>
<span class="filter-value" id="filter-value"></span>