mirror of
https://github.com/vinta/awesome-python.git
synced 2026-03-24 01:24:43 +08:00
feat(website): surface subcategory labels as filterable tags
Entries nested under a plain-text subcategory heading (e.g. "Frameworks" inside Testing) now carry a subcategory field populated by the parser. The build pipeline collects these into a subcategories list on each merged entry, and the template renders them as tag-subcat buttons that plug into the existing data-cats filter mechanism. A dedicated .tag-subcat style distinguishes them visually from category tags, and both are hidden on mobile alongside .tag-group. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,9 @@ def extract_entries(
|
|||||||
existing["categories"].append(cat["name"])
|
existing["categories"].append(cat["name"])
|
||||||
if group_name not in existing["groups"]:
|
if group_name not in existing["groups"]:
|
||||||
existing["groups"].append(group_name)
|
existing["groups"].append(group_name)
|
||||||
|
subcat = entry["subcategory"]
|
||||||
|
if subcat and subcat not in existing["subcategories"]:
|
||||||
|
existing["subcategories"].append(subcat)
|
||||||
else:
|
else:
|
||||||
merged = {
|
merged = {
|
||||||
"name": entry["name"],
|
"name": entry["name"],
|
||||||
@@ -109,6 +112,7 @@ def extract_entries(
|
|||||||
"description": entry["description"],
|
"description": entry["description"],
|
||||||
"categories": [cat["name"]],
|
"categories": [cat["name"]],
|
||||||
"groups": [group_name],
|
"groups": [group_name],
|
||||||
|
"subcategories": [entry["subcategory"]] if entry["subcategory"] else [],
|
||||||
"stars": None,
|
"stars": None,
|
||||||
"owner": None,
|
"owner": None,
|
||||||
"last_commit_at": None,
|
"last_commit_at": None,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ParsedEntry(TypedDict):
|
|||||||
url: str
|
url: str
|
||||||
description: str # inline HTML, properly escaped
|
description: str # inline HTML, properly escaped
|
||||||
also_see: list[AlsoSee]
|
also_see: list[AlsoSee]
|
||||||
|
subcategory: str # sub-category label, empty if none
|
||||||
|
|
||||||
|
|
||||||
class ParsedSection(TypedDict):
|
class ParsedSection(TypedDict):
|
||||||
@@ -178,7 +179,11 @@ def _extract_description_html(inline: SyntaxTreeNode, first_link: SyntaxTreeNode
|
|||||||
return _DESC_SEP_RE.sub("", html)
|
return _DESC_SEP_RE.sub("", html)
|
||||||
|
|
||||||
|
|
||||||
def _parse_list_entries(bullet_list: SyntaxTreeNode) -> list[ParsedEntry]:
|
def _parse_list_entries(
|
||||||
|
bullet_list: SyntaxTreeNode,
|
||||||
|
*,
|
||||||
|
subcategory: str = "",
|
||||||
|
) -> list[ParsedEntry]:
|
||||||
"""Extract entries from a bullet_list AST node.
|
"""Extract entries from a bullet_list AST node.
|
||||||
|
|
||||||
Handles three patterns:
|
Handles three patterns:
|
||||||
@@ -200,9 +205,10 @@ def _parse_list_entries(bullet_list: SyntaxTreeNode) -> list[ParsedEntry]:
|
|||||||
|
|
||||||
if first_link is None or not _is_leading_link(inline, first_link):
|
if first_link is None or not _is_leading_link(inline, first_link):
|
||||||
# Subcategory label (plain text or text-before-link) — recurse into nested list
|
# Subcategory label (plain text or text-before-link) — recurse into nested list
|
||||||
|
label = render_inline_text(inline.children)
|
||||||
nested = _find_child(list_item, "bullet_list")
|
nested = _find_child(list_item, "bullet_list")
|
||||||
if nested:
|
if nested:
|
||||||
entries.extend(_parse_list_entries(nested))
|
entries.extend(_parse_list_entries(nested, subcategory=label))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Entry with a link
|
# Entry with a link
|
||||||
@@ -231,6 +237,7 @@ def _parse_list_entries(bullet_list: SyntaxTreeNode) -> list[ParsedEntry]:
|
|||||||
url=url,
|
url=url,
|
||||||
description=desc_html,
|
description=desc_html,
|
||||||
also_see=also_see,
|
also_see=also_see,
|
||||||
|
subcategory=subcategory,
|
||||||
))
|
))
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|||||||
@@ -779,6 +779,12 @@ th[data-sort].sort-asc::after {
|
|||||||
color: var(--hero-ink);
|
color: var(--hero-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-subcat {
|
||||||
|
background: var(--bg-paper-strong);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.back-to-top {
|
.back-to-top {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -991,7 +997,8 @@ th[data-sort].sort-asc::after {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-group {
|
.tag-group,
|
||||||
|
.tag-subcat {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<tr
|
<tr
|
||||||
class="row"
|
class="row"
|
||||||
data-cats="{{ entry.categories | join('||') }}{% if entry.source_type == 'Built-in' %}||Built-in{% endif %}"
|
data-cats="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | join('||') }}{% endif %}{% if entry.source_type == 'Built-in' %}||Built-in{% endif %}"
|
||||||
data-groups="{{ entry.groups | join('||') }}"
|
data-groups="{{ entry.groups | join('||') }}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
@@ -160,6 +160,14 @@
|
|||||||
<button class="tag" data-type="cat" data-value="{{ cat }}">
|
<button class="tag" data-type="cat" data-value="{{ cat }}">
|
||||||
{{ cat }}
|
{{ cat }}
|
||||||
</button>
|
</button>
|
||||||
|
{% endfor %} {% for subcat in entry.subcategories %}
|
||||||
|
<button
|
||||||
|
class="tag tag-subcat"
|
||||||
|
data-type="cat"
|
||||||
|
data-value="{{ subcat }}"
|
||||||
|
>
|
||||||
|
{{ subcat }}
|
||||||
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button
|
<button
|
||||||
class="tag tag-group"
|
class="tag tag-group"
|
||||||
|
|||||||
Reference in New Issue
Block a user