mirror of
https://github.com/vinta/awesome-python.git
synced 2026-11-08 02:15:12 -06:00
Merge branch 'feature/tag-navigation' into feature/seo-redesign
This commit is contained in:
+2
-9
@@ -276,7 +276,8 @@ def group_section_entries(section: ParsedSection, entries_by_key: dict[tuple[str
|
||||
groups: dict[str, EntryGroup] = {}
|
||||
for parsed in section["entries"]:
|
||||
name = parsed["subcategory"]
|
||||
group = groups.setdefault(name, EntryGroup(name=name, slug=slugify(name) if name else "", url="", entries=[]))
|
||||
slug = slugify(name) if name else ""
|
||||
group = groups.setdefault(name, EntryGroup(name=name, slug=slug, url=subcategory_path(section["slug"], slug) if name else "", entries=[]))
|
||||
group["entries"].append(entries_by_key[(parsed["url"], parsed["name"])])
|
||||
return list(groups.values())
|
||||
|
||||
@@ -678,12 +679,7 @@ def build(repo_root: Path) -> None:
|
||||
filter_urls: dict[str, str] = dict(category_urls)
|
||||
for group in parsed_groups:
|
||||
filter_urls[group["name"]] = group_path(group["slug"])
|
||||
for entry in entries:
|
||||
for sub in entry.get("subcategories", []):
|
||||
filter_urls[sub["value"]] = sub["url"]
|
||||
builtin_entries = [e for e in entries if e.get("source_type") == BUILTIN_FILTER]
|
||||
if builtin_entries:
|
||||
filter_urls[BUILTIN_FILTER] = BUILTIN_PATH
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(website / "templates"),
|
||||
@@ -696,7 +692,6 @@ def build(repo_root: Path) -> None:
|
||||
shutil.rmtree(site_dir)
|
||||
site_dir.mkdir(parents=True)
|
||||
|
||||
filter_urls_json = json.dumps(filter_urls, sort_keys=True, ensure_ascii=False).replace("</", "<\\/")
|
||||
homepage_json_ld = json.dumps(
|
||||
build_homepage_json_ld(entries, len(categories)),
|
||||
ensure_ascii=False,
|
||||
@@ -715,7 +710,6 @@ def build(repo_root: Path) -> None:
|
||||
sponsors=sponsors,
|
||||
category_urls=category_urls,
|
||||
filter_urls=filter_urls,
|
||||
filter_urls_json=filter_urls_json,
|
||||
homepage_json_ld=homepage_json_ld,
|
||||
),
|
||||
encoding="utf-8",
|
||||
@@ -762,7 +756,6 @@ def build(repo_root: Path) -> None:
|
||||
category_urls=category_urls,
|
||||
current_path=current_path,
|
||||
filter_urls=filter_urls,
|
||||
filter_urls_json=filter_urls_json,
|
||||
parent_category=parent_category,
|
||||
group_categories=group_categories,
|
||||
category_json_ld=category_json_ld,
|
||||
|
||||
+6
-85
@@ -10,15 +10,10 @@ const defaultSort =
|
||||
table && table.dataset.defaultSort === "editorial"
|
||||
? { col: "editorial", order: "asc" }
|
||||
: { col: "downloads", order: "desc" };
|
||||
let activeFilter = null;
|
||||
let activeSort = defaultSort;
|
||||
const searchInput = document.querySelector(".search");
|
||||
const filterBar = document.querySelector(".filter-bar");
|
||||
const filterValue = document.querySelector(".filter-value");
|
||||
const filterClear = document.querySelector(".filter-clear");
|
||||
const noResults = document.querySelector(".no-results");
|
||||
const rows = document.querySelectorAll(".table tbody tr.row");
|
||||
const tags = document.querySelectorAll(".tag");
|
||||
const tbody = document.querySelector(".table tbody");
|
||||
const groupRows = document.querySelectorAll(".table tbody tr.group-row");
|
||||
|
||||
@@ -145,7 +140,7 @@ function collapseAll() {
|
||||
|
||||
function applyFilters() {
|
||||
const query = searchInput ? searchInput.value.toLowerCase().trim() : "";
|
||||
const descRowsVisible = !isIndexDocument || activeFilter !== null;
|
||||
const descRowsVisible = !isIndexDocument;
|
||||
let visibleCount = 0;
|
||||
|
||||
collapseAll();
|
||||
@@ -153,12 +148,7 @@ function applyFilters() {
|
||||
rows.forEach(function (row) {
|
||||
let show = true;
|
||||
|
||||
if (activeFilter) {
|
||||
const rowTags = row.dataset.tags;
|
||||
show = rowTags ? rowTags.split("||").includes(activeFilter) : false;
|
||||
}
|
||||
|
||||
if (show && query) {
|
||||
if (query) {
|
||||
if (!row._searchText) {
|
||||
let text = row.textContent.toLowerCase();
|
||||
if (row._descRow) {
|
||||
@@ -200,35 +190,12 @@ function applyFilters() {
|
||||
|
||||
if (noResults) noResults.hidden = visibleCount > 0;
|
||||
|
||||
tags.forEach(function (tag) {
|
||||
tag.classList.toggle("active", activeFilter === tag.dataset.value);
|
||||
});
|
||||
|
||||
if (filterBar) {
|
||||
if (activeFilter) {
|
||||
filterBar.classList.add("visible");
|
||||
if (filterValue) filterValue.textContent = activeFilter;
|
||||
} else {
|
||||
filterBar.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
|
||||
updateURL();
|
||||
}
|
||||
|
||||
const filterUrlsScript = document.getElementById("filter-urls");
|
||||
const filterToUrl = filterUrlsScript
|
||||
? JSON.parse(filterUrlsScript.textContent)
|
||||
: {};
|
||||
|
||||
const isIndexDocument =
|
||||
location.pathname === "/" || location.pathname === "/index.html";
|
||||
|
||||
const urlToFilter = {};
|
||||
Object.keys(filterToUrl).forEach(function (k) {
|
||||
urlToFilter[filterToUrl[k]] = k;
|
||||
});
|
||||
|
||||
function buildQueryString() {
|
||||
const params = new URLSearchParams();
|
||||
const query = searchInput ? searchInput.value.trim() : "";
|
||||
@@ -243,9 +210,7 @@ function buildQueryString() {
|
||||
|
||||
function updateURL() {
|
||||
if (!isIndexDocument) return;
|
||||
const path =
|
||||
activeFilter && filterToUrl[activeFilter] ? filterToUrl[activeFilter] : "/";
|
||||
history.replaceState(null, "", path + buildQueryString());
|
||||
history.replaceState(null, "", "/" + buildQueryString());
|
||||
}
|
||||
|
||||
function getSortValue(row, col) {
|
||||
@@ -340,8 +305,8 @@ function updateSortIndicators() {
|
||||
// Expand/collapse: event delegation on tbody
|
||||
if (tbody) {
|
||||
tbody.addEventListener("click", function (e) {
|
||||
// Don't toggle if clicking a link or tag button
|
||||
if (e.target.closest("a") || e.target.closest(".tag")) return;
|
||||
// Don't toggle if clicking a link
|
||||
if (e.target.closest("a")) return;
|
||||
|
||||
let row = e.target.closest("tr.row");
|
||||
if (!row) {
|
||||
@@ -370,36 +335,6 @@ if (tbody) {
|
||||
});
|
||||
}
|
||||
|
||||
tags.forEach(function (tag) {
|
||||
tag.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const value = tag.dataset.value;
|
||||
const url = tag.dataset.url;
|
||||
if (isIndexDocument) {
|
||||
activeFilter = activeFilter === value ? null : value;
|
||||
if (activeFilter && url) {
|
||||
history.pushState(null, "", url + buildQueryString());
|
||||
} else {
|
||||
history.pushState(null, "", "/" + buildQueryString());
|
||||
}
|
||||
applyFilters();
|
||||
} else if (url) {
|
||||
window.location.href = url + "#library-index";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (filterClear) {
|
||||
filterClear.addEventListener("click", function () {
|
||||
if (!isIndexDocument) {
|
||||
window.location.href = "/#library-index";
|
||||
return;
|
||||
}
|
||||
activeFilter = null;
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
const noResultsClear = document.querySelector(".no-results-clear");
|
||||
if (noResultsClear) {
|
||||
noResultsClear.addEventListener("click", function () {
|
||||
@@ -408,7 +343,6 @@ if (noResultsClear) {
|
||||
return;
|
||||
}
|
||||
if (searchInput) searchInput.value = "";
|
||||
activeFilter = null;
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
@@ -451,7 +385,6 @@ if (searchInput) {
|
||||
}
|
||||
if (e.key === "Escape" && document.activeElement === searchInput) {
|
||||
searchInput.value = "";
|
||||
activeFilter = null;
|
||||
applyFilters();
|
||||
searchInput.blur();
|
||||
}
|
||||
@@ -512,20 +445,8 @@ if (backToTop) {
|
||||
) {
|
||||
activeSort = { col: sort, order: order };
|
||||
}
|
||||
const matched = urlToFilter[location.pathname];
|
||||
if (matched) activeFilter = matched;
|
||||
if (q || activeFilter || sort) {
|
||||
if (q || sort) {
|
||||
sortRows();
|
||||
}
|
||||
if (activeFilter) {
|
||||
applyFilters();
|
||||
}
|
||||
updateSortIndicators();
|
||||
})();
|
||||
|
||||
window.addEventListener("popstate", function () {
|
||||
if (!isIndexDocument) return;
|
||||
const matched = urlToFilter[location.pathname];
|
||||
activeFilter = matched || null;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
@@ -265,8 +265,7 @@ kbd {
|
||||
|
||||
.hero-topbar-link:active,
|
||||
.hero-action:active,
|
||||
.tag:active,
|
||||
.filter-clear:active {
|
||||
.tag:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@@ -436,7 +435,6 @@ kbd {
|
||||
.hero-topbar-link:focus-visible,
|
||||
.hero-category-link:focus-visible,
|
||||
.search:focus-visible,
|
||||
.filter-clear:focus-visible,
|
||||
.tag:focus-visible,
|
||||
.jump-link:focus-visible,
|
||||
.back-to-top:focus-visible,
|
||||
@@ -771,51 +769,6 @@ kbd {
|
||||
0 1.6rem 3rem -2rem var(--search-focus-shadow);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-height: 2.3rem;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--ink-soft);
|
||||
opacity: 0;
|
||||
transform: translateY(-0.4rem);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.filter-bar.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.filter-bar strong {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-paper);
|
||||
color: var(--ink-soft);
|
||||
padding: 0.42rem 0.82rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
color 180ms ease,
|
||||
background-color 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
color: var(--ink);
|
||||
background: var(--accent-soft);
|
||||
border-color: oklch(68% 0.08 58 / 0.5);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
{% macro entry_rows(entry, index) %}
|
||||
<tr
|
||||
class="row"
|
||||
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Stdlib' %}||Stdlib{% endif %}"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
aria-controls="expand-{{ index }}"
|
||||
@@ -124,7 +123,7 @@
|
||||
</td>
|
||||
<td class="col-cat">
|
||||
{% for subcat in entry.subcategories %}
|
||||
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
|
||||
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ category_urls[subcat.value.split(' > ')[0]] }}#{{ subcat.slug }}">
|
||||
{{ subcat.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -132,8 +131,6 @@
|
||||
<a
|
||||
class="tag{% if category_urls[cat] == current_path %} active{% endif %}"
|
||||
href="{{ category_urls[cat] }}"
|
||||
data-value="{{ cat }}"
|
||||
data-url="{{ category_urls[cat] }}"
|
||||
>{{ cat }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
@@ -142,8 +139,6 @@
|
||||
<a
|
||||
class="tag tag-group{% if group_url == current_path %} active{% endif %}"
|
||||
href="{{ group_url }}"
|
||||
data-value="{{ entry.groups[0] }}"
|
||||
data-url="{{ group_url }}"
|
||||
>
|
||||
{{ entry.groups[0] }}
|
||||
</a>
|
||||
@@ -152,8 +147,6 @@
|
||||
<a
|
||||
class="tag tag-source{% if '/categories/built-in/' == current_path %} active{% endif %}"
|
||||
href="/categories/built-in/"
|
||||
data-value="Stdlib"
|
||||
data-url="/categories/built-in/"
|
||||
>
|
||||
Stdlib
|
||||
</a>
|
||||
@@ -211,7 +204,6 @@
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
|
||||
<section class="results-section" id="library-index">
|
||||
<div class="results-intro section-shell" data-reveal>
|
||||
{% if entry_groups %}
|
||||
@@ -255,12 +247,6 @@
|
||||
aria-label="Search projects"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-bar" aria-live="polite">
|
||||
<span>Filtering for <strong class="filter-value"></strong></span>
|
||||
<button class="filter-clear" aria-label="Clear filter">
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Results</h2>
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
|
||||
<section class="results-section" id="library-index">
|
||||
<div class="results-intro section-shell" data-reveal>
|
||||
<div>
|
||||
@@ -132,12 +131,6 @@
|
||||
aria-label="Search projects"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-bar" aria-live="polite">
|
||||
<span>Filtering for <strong class="filter-value"></strong></span>
|
||||
<button class="filter-clear" aria-label="Clear filter">
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Results</h2>
|
||||
@@ -175,7 +168,6 @@
|
||||
{% for entry in entries %}
|
||||
<tr
|
||||
class="row"
|
||||
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Stdlib' %}||Stdlib{% endif %}"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
aria-controls="expand-{{ loop.index }}"
|
||||
@@ -221,23 +213,19 @@
|
||||
</td>
|
||||
<td class="col-cat">
|
||||
{% for subcat in entry.subcategories %}
|
||||
<a class="tag" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
|
||||
<a class="tag" href="{{ category_urls[subcat.value.split(' > ')[0]] }}#{{ subcat.slug }}">
|
||||
{{ subcat.name }}
|
||||
</a>
|
||||
{% endfor %} {% for cat in entry.categories %}
|
||||
<a
|
||||
class="tag"
|
||||
href="{{ category_urls[cat] }}"
|
||||
data-value="{{ cat }}"
|
||||
data-url="{{ category_urls[cat] }}"
|
||||
>{{ cat }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
<a
|
||||
class="tag tag-group"
|
||||
href="{{ filter_urls[entry.groups[0]] }}"
|
||||
data-value="{{ entry.groups[0] }}"
|
||||
data-url="{{ filter_urls[entry.groups[0]] }}"
|
||||
>
|
||||
{{ entry.groups[0] }}
|
||||
</a>
|
||||
@@ -245,8 +233,6 @@
|
||||
<a
|
||||
class="tag tag-source"
|
||||
href="/categories/built-in/"
|
||||
data-value="Stdlib"
|
||||
data-url="/categories/built-in/"
|
||||
>
|
||||
Stdlib
|
||||
</a>
|
||||
|
||||
+10
-76
@@ -280,7 +280,6 @@ class TestBuild:
|
||||
parser.feed(category_html)
|
||||
|
||||
assert 'href="/categories/widgets/"' in index_html
|
||||
assert 'data-value="Widgets"' in index_html
|
||||
assert parser.title.strip() == "Python Widgets Libraries - Awesome Python"
|
||||
assert parser.meta_by_name["description"] == "Widget libraries. Also see awesome-widgets. Explore 2 curated Python projects in Widgets."
|
||||
assert parser.links_by_rel["canonical"] == "https://awesome-python.com/categories/widgets/"
|
||||
@@ -816,75 +815,6 @@ class TestBuild:
|
||||
{"@type": "ListItem", "position": 2, "name": "Sponsorship", "item": "https://awesome-python.com/sponsorship/"},
|
||||
]
|
||||
|
||||
def test_index_embeds_filter_urls_json(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
## Projects
|
||||
|
||||
**AI & ML**
|
||||
|
||||
## Deep Learning
|
||||
|
||||
- [dl1](https://example.com/dl1) - DL.
|
||||
|
||||
## Machine Learning
|
||||
|
||||
- Classical
|
||||
|
||||
- [ml1](https://example.com/ml1) - ML.
|
||||
|
||||
# Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||
build(tmp_path)
|
||||
|
||||
site = tmp_path / "website" / "output"
|
||||
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
marker = '<script type="application/json" id="filter-urls">'
|
||||
assert marker in index_html
|
||||
start = index_html.index(marker) + len(marker)
|
||||
end = index_html.index("</script>", start)
|
||||
data = json.loads(index_html[start:end])
|
||||
|
||||
assert data["Deep Learning"] == "/categories/deep-learning/"
|
||||
assert data["Machine Learning"] == "/categories/machine-learning/"
|
||||
assert data["AI & ML"] == "/categories/ai-ml/"
|
||||
assert data["Machine Learning > Classical"] == "/categories/machine-learning/classical/"
|
||||
|
||||
def test_filter_urls_json_escapes_closing_script_tag(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
## Projects
|
||||
|
||||
## Sneaky </script><script>x=1</script>
|
||||
|
||||
- [a](https://example.com) - A.
|
||||
|
||||
# Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||
build(tmp_path)
|
||||
|
||||
site = tmp_path / "website" / "output"
|
||||
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
marker = '<script type="application/json" id="filter-urls">'
|
||||
start = index_html.index(marker) + len(marker)
|
||||
end = index_html.index("</script>", start)
|
||||
block = index_html[start:end]
|
||||
assert "</script>" not in block
|
||||
data = json.loads(block)
|
||||
assert any("Sneaky" in key for key in data)
|
||||
|
||||
def test_build_creates_group_pages(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
@@ -925,7 +855,7 @@ class TestBuild:
|
||||
assert "wf1" in web_dev
|
||||
assert "dl1" not in web_dev
|
||||
|
||||
def test_tag_buttons_have_data_url(self, tmp_path):
|
||||
def test_tags_link_to_pages_and_subcategory_anchors(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
@@ -950,11 +880,14 @@ class TestBuild:
|
||||
site = tmp_path / "website" / "output"
|
||||
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
assert 'data-value="Deep Learning"' in index_html
|
||||
assert 'data-url="/categories/deep-learning/"' in index_html
|
||||
assert 'data-value="AI & ML"' in index_html or 'data-value="AI & ML"' in index_html
|
||||
assert 'data-url="/categories/ai-ml/"' in index_html
|
||||
assert 'data-url="/categories/deep-learning/vision/"' in index_html
|
||||
category_html = (site / "categories" / "deep-learning" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
for html in (index_html, category_html):
|
||||
assert 'href="/categories/deep-learning/#vision"' in html
|
||||
assert 'href="/categories/ai-ml/"' in html
|
||||
assert "data-url=" not in html
|
||||
assert 'href="/categories/deep-learning/"' in index_html
|
||||
assert 'id="vision"' in category_html
|
||||
|
||||
_REDIRECT_README = textwrap.dedent("""\
|
||||
# Awesome Python
|
||||
@@ -1057,6 +990,7 @@ class TestBuild:
|
||||
positions = [html.index(marker) for marker in ('<h2 id="small">', ">w2</a", ">w1</a", '<h2 id="large">', ">w3</a")]
|
||||
assert positions == sorted(positions)
|
||||
assert '<a class="jump-link" href="#small">Small</a>' in html
|
||||
assert '<a href="/categories/widgets/small/">Small</a>' in html
|
||||
assert '<tr class="desc-row">' in html
|
||||
|
||||
subcategory_html = (site / "widgets" / "small" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user