Skip to content

Commit 774ab69

Browse files
vintaclaude
andcommitted
feat(website): add sponsors section parsed from README
Parse the # Sponsors heading in README.md into structured data and render a dedicated sponsor band above the library index on the site. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 222f566 commit 774ab69

4 files changed

Lines changed: 222 additions & 2 deletions

File tree

website/build.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import TypedDict
1010

1111
from jinja2 import Environment, FileSystemLoader
12-
from readme_parser import parse_readme
12+
from readme_parser import parse_readme, parse_sponsors
1313

1414

1515
class StarData(TypedDict):
@@ -147,6 +147,7 @@ def build(repo_root: str) -> None:
147147
break
148148

149149
parsed_groups = parse_readme(readme_text)
150+
sponsors = parse_sponsors(readme_text)
150151

151152
categories = [cat for g in parsed_groups for cat in g["categories"]]
152153
total_entries = sum(c["entry_count"] for c in categories)
@@ -189,6 +190,7 @@ def build(repo_root: str) -> None:
189190
total_categories=len(categories),
190191
repo_stars=repo_stars,
191192
build_date=datetime.now(timezone.utc).strftime("%B %d, %Y"),
193+
sponsors=sponsors,
192194
),
193195
encoding="utf-8",
194196
)

website/readme_parser.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ class ParsedGroup(TypedDict):
3737
categories: list[ParsedSection]
3838

3939

40+
class ParsedSponsor(TypedDict):
41+
name: str
42+
url: str
43+
description: str # inline HTML, properly escaped
44+
45+
4046
# --- Slugify ----------------------------------------------------------------
4147

4248
_SLUG_NON_ALNUM_RE = re.compile(r"[^a-z0-9\s-]")
@@ -350,6 +356,80 @@ def flush_group() -> None:
350356
return groups
351357

352358

359+
_SPONSOR_SEP_RE = re.compile(r"^\s*[:\-\u2013\u2014]\s*")
360+
361+
362+
def _find_link_deep(node: SyntaxTreeNode) -> SyntaxTreeNode | None:
363+
"""Find the first link anywhere in the subtree (including nested in strong/em)."""
364+
for child in node.children:
365+
if child.type == "link":
366+
return child
367+
found = _find_link_deep(child)
368+
if found:
369+
return found
370+
return None
371+
372+
373+
def _parse_sponsor_item(inline: SyntaxTreeNode) -> ParsedSponsor | None:
374+
"""Parse `**[name](url)**: description` (or `[name](url) - description`)."""
375+
link = _find_link_deep(inline)
376+
if link is None:
377+
return None
378+
name = render_inline_text(link.children)
379+
url = link.attrGet("href") or ""
380+
381+
split_idx = None
382+
for i, child in enumerate(inline.children):
383+
if child is link or _find_link_deep(child) is link:
384+
split_idx = i
385+
break
386+
if split_idx is None:
387+
return None
388+
desc_html = render_inline_html(inline.children[split_idx + 1 :])
389+
desc_html = _SPONSOR_SEP_RE.sub("", desc_html)
390+
return ParsedSponsor(name=name, url=url, description=desc_html)
391+
392+
393+
def parse_sponsors(text: str) -> list[ParsedSponsor]:
394+
"""Parse the `# Sponsors` section of README.md into a list of sponsors.
395+
396+
Expects bullets in the form `**[name](url)**: description`.
397+
Returns [] if no Sponsors section exists.
398+
"""
399+
md = MarkdownIt("commonmark")
400+
tokens = md.parse(text)
401+
root = SyntaxTreeNode(tokens)
402+
children = root.children
403+
404+
start_idx = None
405+
end_idx = len(children)
406+
for i, node in enumerate(children):
407+
if node.type == "heading" and node.tag == "h1":
408+
title = _heading_text(node).strip().lower()
409+
if start_idx is None and title == "sponsors":
410+
start_idx = i + 1
411+
elif start_idx is not None:
412+
end_idx = i
413+
break
414+
if start_idx is None:
415+
return []
416+
417+
sponsors: list[ParsedSponsor] = []
418+
for node in children[start_idx:end_idx]:
419+
if node.type != "bullet_list":
420+
continue
421+
for list_item in node.children:
422+
if list_item.type != "list_item":
423+
continue
424+
inline = _find_inline(list_item)
425+
if inline is None:
426+
continue
427+
sponsor = _parse_sponsor_item(inline)
428+
if sponsor:
429+
sponsors.append(sponsor)
430+
return sponsors
431+
432+
353433
def parse_readme(text: str) -> list[ParsedGroup]:
354434
"""Parse README.md text into grouped categories.
355435

website/static/style.css

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,109 @@ kbd {
392392
outline-offset: 3px;
393393
}
394394

395+
.sponsor-band {
396+
padding-block: clamp(2.5rem, 5.5vw, 4rem);
397+
background:
398+
linear-gradient(180deg, var(--bg-paper-strong), var(--bg-paper));
399+
border-bottom: 1px solid var(--line);
400+
}
401+
402+
.sponsor-shell {
403+
display: grid;
404+
grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
405+
gap: clamp(1.5rem, 5vw, 3.5rem);
406+
align-items: start;
407+
}
408+
409+
.sponsor-meta {
410+
display: flex;
411+
flex-direction: column;
412+
gap: 1rem;
413+
}
414+
415+
.sponsor-meta .section-label {
416+
margin-bottom: 0;
417+
}
418+
419+
.sponsor-become {
420+
display: inline-flex;
421+
align-items: center;
422+
gap: 0.4rem;
423+
align-self: start;
424+
color: var(--ink-soft);
425+
font-size: var(--text-sm);
426+
font-weight: 700;
427+
letter-spacing: 0.01em;
428+
border-bottom: 1px solid var(--line-strong);
429+
padding-bottom: 0.2rem;
430+
transition:
431+
color 180ms ease,
432+
border-color 180ms ease;
433+
}
434+
435+
.sponsor-become:hover {
436+
color: var(--accent-deep);
437+
border-bottom-color: var(--accent);
438+
}
439+
440+
.sponsor-become-arrow {
441+
transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
442+
}
443+
444+
.sponsor-become:hover .sponsor-become-arrow {
445+
transform: translateX(0.3rem);
446+
}
447+
448+
.sponsor-list {
449+
list-style: none;
450+
padding: 0;
451+
margin: 0;
452+
display: grid;
453+
gap: clamp(1.5rem, 3vw, 2.25rem);
454+
}
455+
456+
.sponsor {
457+
display: grid;
458+
gap: 0.65rem;
459+
}
460+
461+
.sponsor-link {
462+
display: inline-flex;
463+
align-items: baseline;
464+
gap: 0.5rem;
465+
color: var(--ink);
466+
transition: color 180ms ease;
467+
}
468+
469+
.sponsor-link:hover {
470+
color: var(--accent-deep);
471+
}
472+
473+
.sponsor-name {
474+
font-family: var(--font-display);
475+
font-size: clamp(2.25rem, 4.2vw, 3.25rem);
476+
font-weight: 600;
477+
line-height: 0.95;
478+
letter-spacing: -0.025em;
479+
}
480+
481+
.sponsor-desc {
482+
color: var(--ink-soft);
483+
font-size: clamp(1rem, 1.5vw, 1.1rem);
484+
line-height: 1.55;
485+
}
486+
487+
.sponsor-desc a {
488+
color: var(--accent-deep);
489+
text-decoration: underline;
490+
text-decoration-color: var(--accent-underline);
491+
text-underline-offset: 0.18em;
492+
}
493+
494+
.sponsor-desc a:hover {
495+
color: var(--accent);
496+
}
497+
395498
.results-intro h2,
396499
.final-cta h2 {
397500
font-family: var(--font-display);
@@ -1025,7 +1128,8 @@ th[data-sort].sort-asc::after {
10251128
}
10261129

10271130
.hero-grid,
1028-
.results-intro {
1131+
.results-intro,
1132+
.sponsor-shell {
10291133
grid-template-columns: 1fr;
10301134
}
10311135

website/templates/index.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,40 @@ <h1>Awesome Python</h1>
6565
</header>
6666
{% endblock %}
6767
{% block content %}
68+
{% if sponsors %}
69+
<section class="sponsor-band" aria-labelledby="sponsor-heading" data-reveal>
70+
<div class="section-shell sponsor-shell">
71+
<header class="sponsor-meta">
72+
<p class="section-label" id="sponsor-heading">Sponsors</p>
73+
<a
74+
class="sponsor-become"
75+
href="https://github.com/vinta/awesome-python/blob/master/SPONSORSHIP.md"
76+
target="_blank"
77+
rel="noopener"
78+
>
79+
Become a sponsor
80+
<span class="sponsor-become-arrow" aria-hidden="true">&rarr;</span>
81+
</a>
82+
</header>
83+
<ul class="sponsor-list">
84+
{% for sponsor in sponsors %}
85+
<li class="sponsor">
86+
<a
87+
class="sponsor-link"
88+
href="{{ sponsor.url }}"
89+
target="_blank"
90+
rel="noopener"
91+
>
92+
<span class="sponsor-name">{{ sponsor.name }}</span>
93+
</a>
94+
<p class="sponsor-desc">{{ sponsor.description | safe }}</p>
95+
</li>
96+
{% endfor %}
97+
</ul>
98+
</div>
99+
</section>
100+
{% endif %}
101+
68102
<section class="results-section" id="library-index">
69103
<div class="results-intro section-shell" data-reveal>
70104
<div>

0 commit comments

Comments
 (0)