add missing links of category descriptions

This commit is contained in:
Vinta Chen 2026-05-02 23:35:24 +08:00
parent c98cbe2cb1
commit b00395a301
No known key found for this signature in database
GPG key ID: B93DE4F003C33630
5 changed files with 34 additions and 14 deletions

View file

@ -27,6 +27,7 @@ class ParsedSection(TypedDict):
name: str name: str
slug: str slug: str
description: str # plain text, links resolved to text description: str # plain text, links resolved to text
description_html: str # inline HTML, properly escaped
entries: list[ParsedEntry] entries: list[ParsedEntry]
entry_count: int entry_count: int
@ -113,22 +114,22 @@ def _heading_text(node: SyntaxTreeNode) -> str:
return "" return ""
def _extract_description(nodes: list[SyntaxTreeNode]) -> str: def _extract_description_children(nodes: list[SyntaxTreeNode]) -> list[SyntaxTreeNode]:
"""Extract description from the first paragraph if it's a single <em> block. """Extract description children from the first paragraph if it's a single <em> block.
Pattern: _Libraries for foo._ -> "Libraries for foo." Pattern: _Libraries for foo._ -> "Libraries for foo."
""" """
if not nodes: if not nodes:
return "" return []
first = nodes[0] first = nodes[0]
if first.type != "paragraph": if first.type != "paragraph":
return "" return []
for child in first.children: for child in first.children:
if child.type == "inline" and len(child.children) == 1: if child.type == "inline" and len(child.children) == 1:
em = child.children[0] em = child.children[0]
if em.type == "em": if em.type == "em":
return render_inline_text(em.children) return em.children
return "" return []
# --- Entry extraction -------------------------------------------------------- # --- Entry extraction --------------------------------------------------------
@ -258,14 +259,17 @@ def _parse_section_entries(content_nodes: list[SyntaxTreeNode]) -> list[ParsedEn
def _build_section(name: str, body: list[SyntaxTreeNode]) -> ParsedSection: def _build_section(name: str, body: list[SyntaxTreeNode]) -> ParsedSection:
"""Build a ParsedSection from a heading name and its body nodes.""" """Build a ParsedSection from a heading name and its body nodes."""
desc = _extract_description(body) desc_children = _extract_description_children(body)
content_nodes = body[1:] if desc else body desc = render_inline_text(desc_children) if desc_children else ""
desc_html = render_inline_html(desc_children) if desc_children else ""
content_nodes = body[1:] if desc_children else body
entries = _parse_section_entries(content_nodes) entries = _parse_section_entries(content_nodes)
entry_count = len(entries) + sum(len(e["also_see"]) for e in entries) entry_count = len(entries) + sum(len(e["also_see"]) for e in entries)
return ParsedSection( return ParsedSection(
name=name, name=name,
slug=slugify(name), slug=slugify(name),
description=desc, description=desc,
description_html=desc_html,
entries=entries, entries=entries,
entry_count=entry_count, entry_count=entry_count,
) )

View file

@ -423,6 +423,17 @@ kbd {
text-wrap: pretty; text-wrap: pretty;
} }
.category-subtitle a {
color: var(--hero-text);
text-decoration: underline;
text-decoration-color: oklch(100% 0 0 / 0.32);
text-underline-offset: 0.2em;
}
.category-subtitle a:hover {
text-decoration-color: oklch(100% 0 0 / 0.7);
}
.category-results { .category-results {
padding-top: clamp(2.5rem, 5vw, 3.75rem); padding-top: clamp(2.5rem, 5vw, 3.75rem);
} }

View file

@ -25,8 +25,8 @@
<div class="category-hero-copy"> <div class="category-hero-copy">
<h1>{{ category.name }}</h1> <h1>{{ category.name }}</h1>
{% if category.description %} {% if category.description_html %}
<p class="category-subtitle">{{ category.description }}</p> <p class="category-subtitle">{{ category.description_html | safe }}</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

@ -146,7 +146,7 @@ class TestBuild:
## Widgets ## Widgets
_Widget libraries._ _Widget libraries. Also see [awesome-widgets](https://example.com/widgets)._
- [w1](https://example.com) - A widget. - [w1](https://example.com) - A widget.
@ -235,7 +235,7 @@ class TestBuild:
## Widgets ## Widgets
_Widget libraries._ _Widget libraries. Also see [awesome-widgets](https://example.com/widgets)._
- [w1](https://example.com/w1) - A widget. - [w1](https://example.com/w1) - A widget.
- [w2](https://github.com/owner/w2) - A starred widget. - [w2](https://github.com/owner/w2) - A starred widget.
@ -276,12 +276,12 @@ class TestBuild:
assert 'href="/categories/widgets/"' in index_html assert 'href="/categories/widgets/"' in index_html
assert 'data-value="Widgets"' in index_html assert 'data-value="Widgets"' in index_html
assert parser.title.strip() == "Widgets Python Libraries | Awesome Python" assert parser.title.strip() == "Widgets Python Libraries | Awesome Python"
assert parser.meta_by_name["description"] == "Explore 2 curated Python projects in Widgets. Widget libraries." assert parser.meta_by_name["description"] == "Explore 2 curated Python projects in Widgets. Widget libraries. Also see awesome-widgets."
assert parser.links_by_rel["canonical"] == "https://awesome-python.com/categories/widgets/" assert parser.links_by_rel["canonical"] == "https://awesome-python.com/categories/widgets/"
assert parser.meta_by_property["og:url"] == "https://awesome-python.com/categories/widgets/" assert parser.meta_by_property["og:url"] == "https://awesome-python.com/categories/widgets/"
assert '<link rel="alternate" type="text/markdown" href="/index.md" />' not in category_html assert '<link rel="alternate" type="text/markdown" href="/index.md" />' not in category_html
assert "<h1>Widgets</h1>" in category_html assert "<h1>Widgets</h1>" in category_html
assert "Widget libraries." in category_html assert 'Widget libraries. Also see <a href="https://example.com/widgets" target="_blank" rel="noopener">awesome-widgets</a>.' in category_html
assert 'href="https://example.com/w1"' in category_html assert 'href="https://example.com/w1"' in category_html
assert "A widget." in category_html assert "A widget." in category_html
assert 'href="https://github.com/owner/w2"' in category_html assert 'href="https://github.com/owner/w2"' in category_html

View file

@ -180,7 +180,9 @@ class TestParseReadmeSections:
groups = parse_readme(MINIMAL_README) groups = parse_readme(MINIMAL_README)
cats = groups[0]["categories"] cats = groups[0]["categories"]
assert cats[0]["description"] == "Libraries for alpha stuff." assert cats[0]["description"] == "Libraries for alpha stuff."
assert cats[0]["description_html"] == "Libraries for alpha stuff."
assert cats[1]["description"] == "Tools for beta." assert cats[1]["description"] == "Tools for beta."
assert cats[1]["description_html"] == "Tools for beta."
def test_contributing_skipped(self): def test_contributing_skipped(self):
groups = parse_readme(MINIMAL_README) groups = parse_readme(MINIMAL_README)
@ -216,6 +218,7 @@ class TestParseReadmeSections:
groups = parse_readme(readme) groups = parse_readme(readme)
cats = groups[0]["categories"] cats = groups[0]["categories"]
assert cats[0]["description"] == "" assert cats[0]["description"] == ""
assert cats[0]["description_html"] == ""
assert cats[0]["entries"][0]["name"] == "item" assert cats[0]["entries"][0]["name"] == "item"
def test_description_with_link_stripped(self): def test_description_with_link_stripped(self):
@ -237,6 +240,7 @@ class TestParseReadmeSections:
groups = parse_readme(readme) groups = parse_readme(readme)
cats = groups[0]["categories"] cats = groups[0]["categories"]
assert cats[0]["description"] == "Algorithms. Also see awesome-algos." assert cats[0]["description"] == "Algorithms. Also see awesome-algos."
assert cats[0]["description_html"] == 'Algorithms. Also see <a href="https://example.com" target="_blank" rel="noopener">awesome-algos</a>.'
class TestParseGroupedReadme: class TestParseGroupedReadme:
@ -481,6 +485,7 @@ class TestParseRealReadme:
algos = next(c for c in self.cats if c["name"] == "Algorithms and Design Patterns") algos = next(c for c in self.cats if c["name"] == "Algorithms and Design Patterns")
assert "awesome-algorithms" in algos["description"] assert "awesome-algorithms" in algos["description"]
assert "https://" not in algos["description"] assert "https://" not in algos["description"]
assert 'href="https://github.com/tayllan/awesome-algorithms"' in algos["description_html"]
def test_miscellaneous_in_own_group(self): def test_miscellaneous_in_own_group(self):
misc_group = next((g for g in self.groups if g["name"] == "Miscellaneous"), None) misc_group = next((g for g in self.groups if g["name"] == "Miscellaneous"), None)