mirror of
https://github.com/vinta/awesome-python.git
synced 2026-10-06 17:05:16 +08:00
Merge branch 'group-intro-leads'
This commit is contained in:
+18
-12
@@ -72,6 +72,7 @@ class EntryGroup(TypedDict):
|
||||
name: str # empty for a page with a single unnamed group
|
||||
slug: str
|
||||
url: str # links the group heading to its own page, empty when it has none
|
||||
lead: str # the section intro's first paragraph as HTML, shown under the heading on group pages
|
||||
entries: list[TemplateEntry]
|
||||
|
||||
|
||||
@@ -249,16 +250,16 @@ def category_meta_description(name: str, entry_count: int, description: str, par
|
||||
return f"{count_sentence} Part of the Awesome Python catalog."
|
||||
|
||||
|
||||
def load_category_intro(path: Path) -> tuple[str, str, str]:
|
||||
def load_category_intro(path: Path) -> tuple[str, str, str, str]:
|
||||
"""Render a category intro file to HTML, split at the end of its "How to choose:" list.
|
||||
|
||||
Returns the part shown above the table, the guide shown below it, and the
|
||||
first paragraph as plain text for the meta description. A file without the
|
||||
list keeps everything above the table. Returns empty strings if the category
|
||||
has no intro file.
|
||||
first paragraph as plain text for the meta description and as HTML for
|
||||
group pages. A file without the list keeps everything above the table.
|
||||
Returns empty strings if the category has no intro file.
|
||||
"""
|
||||
if not path.exists():
|
||||
return "", "", ""
|
||||
return "", "", "", ""
|
||||
md = MarkdownIt("commonmark")
|
||||
tokens = md.parse(path.read_text(encoding="utf-8"))
|
||||
for token in tokens:
|
||||
@@ -273,7 +274,12 @@ def load_category_intro(path: Path) -> tuple[str, str, str]:
|
||||
split_at = next(j for j in range(i + 3, len(tokens)) if tokens[j].type == "bullet_list_close" and tokens[j].level == 0) + 1
|
||||
break
|
||||
render = md.renderer.render
|
||||
return render(tokens[:split_at], md.options, {}), render(tokens[split_at:], md.options, {}), render_inline_text(lead.children[0].children)
|
||||
return (
|
||||
render(tokens[:split_at], md.options, {}),
|
||||
render(tokens[split_at:], md.options, {}),
|
||||
render_inline_text(lead.children[0].children),
|
||||
render(lead.to_tokens(), md.options, {}),
|
||||
)
|
||||
|
||||
|
||||
def group_section_entries(section: ParsedSection, entries_by_key: dict[tuple[str, str], TemplateEntry]) -> list[EntryGroup]:
|
||||
@@ -282,12 +288,12 @@ def group_section_entries(section: ParsedSection, entries_by_key: dict[tuple[str
|
||||
for parsed in section["entries"]:
|
||||
name = parsed["subcategory"]
|
||||
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 = groups.setdefault(name, EntryGroup(name=name, slug=slug, url=subcategory_path(section["slug"], slug) if name else "", lead="", entries=[]))
|
||||
group["entries"].append(entries_by_key[(parsed["url"], parsed["name"])])
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def group_entries_by_section(sections: Sequence[ParsedSection], entries_by_key: dict[tuple[str, str], TemplateEntry]) -> list[EntryGroup]:
|
||||
def group_entries_by_section(sections: Sequence[ParsedSection], entries_by_key: dict[tuple[str, str], TemplateEntry], intros_dir: Path) -> list[EntryGroup]:
|
||||
"""Group a thematic group's entries by section, both in README order, listing each entry once."""
|
||||
placed: set[tuple[str, str]] = set()
|
||||
groups: list[EntryGroup] = []
|
||||
@@ -298,7 +304,7 @@ def group_entries_by_section(sections: Sequence[ParsedSection], entries_by_key:
|
||||
if key not in placed:
|
||||
placed.add(key)
|
||||
entries.append(entries_by_key[key])
|
||||
groups.append(EntryGroup(name=section["name"], slug=section["slug"], url=category_path(section), entries=entries))
|
||||
groups.append(EntryGroup(name=section["name"], slug=section["slug"], url=category_path(section), lead=load_category_intro(intros_dir / f"{section['slug']}.md")[3], entries=entries))
|
||||
return groups
|
||||
|
||||
|
||||
@@ -753,7 +759,7 @@ def build(repo_root: Path) -> None:
|
||||
page_dir.mkdir(parents=True, exist_ok=True)
|
||||
parent_name = parent_category["name"] if parent_category else None
|
||||
category_title = category_meta_title(category["name"], parent_name)
|
||||
intro_html, guide_html, intro_lead = load_category_intro(website / "data" / "category_intros" / f"{current_path.removeprefix('/categories/').strip('/')}.md")
|
||||
intro_html, guide_html, intro_lead, _ = load_category_intro(website / "data" / "category_intros" / f"{current_path.removeprefix('/categories/').strip('/')}.md")
|
||||
category_description = intro_lead or category_meta_description(category["name"], len(entries), category["description"], parent_name)
|
||||
breadcrumbs = [("Awesome Python", SITE_URL)]
|
||||
if parent_category:
|
||||
@@ -805,7 +811,7 @@ def build(repo_root: Path) -> None:
|
||||
current_path=group_path(group["slug"]),
|
||||
page_dir=categories_dir / group["slug"],
|
||||
group_categories=group["categories"],
|
||||
entry_groups=group_entries_by_section(group["categories"], entries_by_key),
|
||||
entry_groups=group_entries_by_section(group["categories"], entries_by_key, website / "data" / "category_intros"),
|
||||
)
|
||||
|
||||
if builtin_entries:
|
||||
@@ -855,7 +861,7 @@ def build(repo_root: Path) -> None:
|
||||
current_path=subcategory_path(cat_slug, sub_slug),
|
||||
page_dir=categories_dir / cat_slug / sub_slug,
|
||||
parent_category=cat_by_slug[cat_slug],
|
||||
entry_groups=[EntryGroup(name="", slug="", url="", entries=group["entries"]) for group in section_groups[cat_by_slug[cat_slug]["name"]] if group["name"] == sub_name],
|
||||
entry_groups=[EntryGroup(name="", slug="", url="", lead="", entries=group["entries"]) for group in section_groups[cat_by_slug[cat_slug]["name"]] if group["name"] == sub_name],
|
||||
)
|
||||
|
||||
redirects_file = website / "data" / "redirects.json"
|
||||
|
||||
@@ -845,7 +845,7 @@ kbd {
|
||||
.group-row th {
|
||||
padding-top: 2.25rem;
|
||||
padding-bottom: 0.85rem;
|
||||
padding-left: max(var(--shell-pad), calc(50vw - (var(--shell-max) / 2) + var(--shell-pad)));
|
||||
padding-inline: max(var(--shell-pad), calc(50vw - (var(--shell-max) / 2) + var(--shell-pad)));
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--line-strong);
|
||||
background: var(--bg-paper);
|
||||
@@ -878,6 +878,15 @@ kbd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.group-lead {
|
||||
margin-top: 0.75rem;
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 400;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-sm);
|
||||
@@ -1074,11 +1083,13 @@ th[data-sort].sort-asc::after {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.desc-text a {
|
||||
.desc-text a,
|
||||
.group-lead a {
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.desc-text a:hover {
|
||||
.desc-text a:hover,
|
||||
.group-lead a:hover {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--accent-underline);
|
||||
@@ -1807,7 +1818,8 @@ th[data-sort].sort-asc::after {
|
||||
}
|
||||
|
||||
.table thead th:last-child,
|
||||
.table tbody td:last-child {
|
||||
.table tbody td:last-child,
|
||||
.group-row th {
|
||||
padding-right: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -294,6 +294,9 @@
|
||||
{% if group.url %}<a href="{{ group.url }}">{{ group.name }}</a>{% else %}{{ group.name }}{% endif %}
|
||||
<span class="group-count">{{ group.entries | length }} project{{ "s" if group.entries | length != 1 }}</span>
|
||||
</h2>
|
||||
{% if group.lead %}
|
||||
<div class="group-lead">{{ group.lead | safe }}</div>
|
||||
{% endif %}
|
||||
</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
@@ -1148,6 +1148,39 @@ class TestBuild:
|
||||
assert "repeats-heading" not in html
|
||||
assert 'class="jump-links"' not in html
|
||||
|
||||
def test_group_page_shows_section_intro_lead_under_section_heading(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
## Projects
|
||||
|
||||
**AI & ML**
|
||||
|
||||
## Machine Learning
|
||||
|
||||
- [ml1](https://example.com/ml1) - ML.
|
||||
|
||||
## Deep Learning
|
||||
|
||||
- [dl1](https://example.com/dl1) - DL.
|
||||
|
||||
# Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||
intros_dir = tmp_path / "website" / "data" / "category_intros"
|
||||
intros_dir.mkdir(parents=True)
|
||||
(intros_dir / "machine-learning.md").write_text("Start with [`ml1`](https://example.com/ml1).\n\nMore detail.\n", encoding="utf-8")
|
||||
build(tmp_path)
|
||||
|
||||
html = (tmp_path / "website" / "output" / "categories" / "ai-ml" / "index.html").read_text(encoding="utf-8")
|
||||
lead = html.index('<div class="group-lead"><p>Start with <a href="https://example.com/ml1" target="_blank" rel="noopener"><code>ml1</code></a>.</p>')
|
||||
assert html.index('<a href="/categories/machine-learning/">Machine Learning</a>') < lead < html.index(">ml1</a")
|
||||
assert "More detail." not in html
|
||||
assert html.count('class="group-lead"') == 1
|
||||
|
||||
def test_build_rejects_redirect_to_missing_page(self, tmp_path):
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(self._REDIRECT_README, encoding="utf-8")
|
||||
@@ -1500,17 +1533,18 @@ class TestLoadCategoryIntro:
|
||||
def test_splits_after_how_to_choose_list(self, tmp_path):
|
||||
path = tmp_path / "widgets.md"
|
||||
path.write_text("Use `w1` for most apps.\n\nHow to choose:\n\n- Small apps: w1\n- Big apps: w2\n\nConfigure w1 once.\n\nPin w2.\n", encoding="utf-8")
|
||||
intro_html, guide_html, lead = load_category_intro(path)
|
||||
intro_html, guide_html, lead, lead_html = load_category_intro(path)
|
||||
assert intro_html == "<p>Use <code>w1</code> for most apps.</p>\n<p>How to choose:</p>\n<ul>\n<li>Small apps: w1</li>\n<li>Big apps: w2</li>\n</ul>\n"
|
||||
assert guide_html == "<p>Configure w1 once.</p>\n<p>Pin w2.</p>\n"
|
||||
assert lead == "Use w1 for most apps."
|
||||
assert lead_html == "<p>Use <code>w1</code> for most apps.</p>\n"
|
||||
|
||||
def test_keeps_everything_above_table_without_how_to_choose_list(self, tmp_path):
|
||||
path = tmp_path / "widgets.md"
|
||||
path.write_text("Use w1.\n\n- Small apps: w1\n\nConfigure w1 once.\n", encoding="utf-8")
|
||||
intro_html, guide_html, _ = load_category_intro(path)
|
||||
intro_html, guide_html, _, _ = load_category_intro(path)
|
||||
assert "Configure w1 once." in intro_html
|
||||
assert guide_html == ""
|
||||
|
||||
def test_returns_empty_strings_without_intro_file(self, tmp_path):
|
||||
assert load_category_intro(tmp_path / "missing.md") == ("", "", "")
|
||||
assert load_category_intro(tmp_path / "missing.md") == ("", "", "", "")
|
||||
|
||||
Reference in New Issue
Block a user