feat: merge duplicate entries across multiple categories

Entries appearing in more than one category were previously emitted as
separate rows. They are now deduplicated in build.py by URL, collecting
all category and group names into lists.

The template encodes those lists as pipe-delimited data attributes
(data-cats, data-groups) and renders a tag button per category.
The JS filter is updated to split on '||' and check for membership,
so clicking any category tag correctly shows the merged row.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen 2026-03-20 17:02:22 +08:00
parent c5376618b8
commit 18011f86f3
No known key found for this signature in database
GPG Key ID: B93DE4F003C33630
3 changed files with 30 additions and 14 deletions

View File

@ -243,29 +243,42 @@ def extract_entries(
categories: list[dict],
groups: list[dict],
) -> list[dict]:
"""Flatten categories into individual library entries for table display."""
"""Flatten categories into individual library entries for table display.
Entries appearing in multiple categories are merged into a single entry
with lists of categories and groups.
"""
cat_to_group: dict[str, str] = {}
for group in groups:
for cat in group["categories"]:
cat_to_group[cat["name"]] = group["name"]
seen: dict[str, dict] = {} # url -> entry
entries: list[dict] = []
for cat in categories:
group_name = cat_to_group.get(cat["name"], "Other")
for entry in cat["entries"]:
entries.append(
{
url = entry["url"]
if url in seen:
existing = seen[url]
if cat["name"] not in existing["categories"]:
existing["categories"].append(cat["name"])
if group_name not in existing["groups"]:
existing["groups"].append(group_name)
else:
merged = {
"name": entry["name"],
"url": entry["url"],
"url": url,
"description": entry["description"],
"category": cat["name"],
"group": group_name,
"categories": [cat["name"]],
"groups": [group_name],
"stars": None,
"owner": None,
"last_commit_at": None,
"also_see": entry["also_see"],
}
)
seen[url] = merged
entries.append(merged)
return entries

View File

@ -59,7 +59,8 @@ function applyFilters() {
// Category/group filter
if (activeFilter) {
show = row.dataset[activeFilter.type] === activeFilter.value;
var attr = activeFilter.type === 'cat' ? row.dataset.cats : row.dataset.groups;
show = attr ? attr.split('||').indexOf(activeFilter.value) !== -1 : false;
}
// Text search

View File

@ -84,8 +84,8 @@
<tr
class="row"
role="button"
data-cat="{{ entry.category }}"
data-group="{{ entry.group }}"
data-cats="{{ entry.categories | join('||') }}"
data-groups="{{ entry.groups | join('||') }}"
tabindex="0"
aria-expanded="false"
aria-controls="expand-{{ loop.index }}"
@ -116,11 +116,13 @@
>{% else %}&mdash;{% endif %}
</td>
<td class="col-cat">
<button class="tag" data-type="cat" data-value="{{ entry.category }}">
{{ entry.category }}
{% for cat in entry.categories %}
<button class="tag" data-type="cat" data-value="{{ cat }}">
{{ cat }}
</button>
<button class="tag tag-group" data-type="group" data-value="{{ entry.group }}">
{{ entry.group }}
{% endfor %}
<button class="tag tag-group" data-type="group" data-value="{{ entry.groups[0] }}">
{{ entry.groups[0] }}
</button>
</td>
<td class="col-arrow"><span class="arrow">&rarr;</span></td>