Skip to content

Commit 1546b1a

Browse files
committed
refactor ToC - improve DRYness
1 parent d7d995a commit 1546b1a

File tree

16 files changed

+1625
-114
lines changed

16 files changed

+1625
-114
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
- id: mixed-line-ending
1515

1616
- repo: https://github.com/astral-sh/ruff-pre-commit
17-
rev: v0.8.5
17+
rev: v0.12.2
1818
hooks:
1919
# Run the linter
2020
- id: ruff

README_ALTERNATIVES/README_EXTRA.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
</a>
9898
</td>
9999
<td align="center">
100-
<a href="#workflows-knowledge-guides-">
100+
<a href="#workflows--knowledge-guides-">
101101
<picture>
102102
<source media="(prefers-color-scheme: dark)" srcset="../assets/card-workflows.svg">
103103
<source media="(prefers-color-scheme: light)" srcset="../assets/card-workflows-light-anim-lineprint.svg">

docs/development/toc-anchor-generation.md

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ make test # Includes TOC anchor validation tests
8282

8383
## HTML Fixture Storage
8484

85-
GitHub-rendered HTML fixtures are stored in `tests/fixtures/github-html/` (version controlled):
85+
GitHub-rendered HTML fixtures are stored in `tests/fixtures/github-html/` (version controlled).
86+
Fixture filenames indicate root vs non-root placement to detect potential rendering differences:
8687

87-
| Style | README Path | HTML Fixture Path |
88-
|-------|-------------|-------------------|
89-
| AWESOME | `README.md` | `tests/fixtures/github-html/awesome.html` |
90-
| CLASSIC | `README_ALTERNATIVES/README_CLASSIC.md` | `tests/fixtures/github-html/classic.html` |
91-
| EXTRA | `README_ALTERNATIVES/README_EXTRA.md` | `tests/fixtures/github-html/extra.html` |
92-
| FLAT | `README_ALTERNATIVES/README_FLAT_ALL_AZ.md` | `tests/fixtures/github-html/flat.html` |
88+
| Style | README Path | HTML Fixture | Placement |
89+
|-------|-------------|--------------|-----------|
90+
| AWESOME | `README.md` | `awesome-root.html` | Root |
91+
| CLASSIC | `README_ALTERNATIVES/README_CLASSIC.md` | `classic-non-root.html` | Non-root |
92+
| EXTRA | `README_ALTERNATIVES/README_EXTRA.md` | `extra-non-root.html` | Non-root |
93+
| FLAT | `README_ALTERNATIVES/README_FLAT_ALL_AZ.md` | `flat-non-root.html` | Non-root |
9394

9495
Validation commands:
9596
```bash
@@ -102,17 +103,55 @@ python3 -m scripts.testing.validate_toc_anchors --style extra
102103
python3 -m scripts.testing.validate_toc_anchors --style flat
103104
```
104105

105-
## Remaining Work
106+
## Validation Status
106107

107-
| Style | Validated | Notes |
108-
|-------|-----------|-------|
109-
| AWESOME || Root README, tested and fixed |
110-
| CLASSIC || Tested and fixed (different anchor format due to 🔝 in headings) |
111-
| EXTRA | | Uses explicit `id` attributes; needs validation |
112-
| FLAT | | Simpler format (no subcategories); needs validation |
108+
| Style | Status | Notes |
109+
|-------|--------|-------|
110+
| AWESOME || Root README, 30 TOC anchors verified |
111+
| CLASSIC || Different anchor format due to 🔝 in headings |
112+
| EXTRA | | Uses explicit `id` attributes; template anchor fixed |
113+
| FLAT | | No TOC anchors (flat list format) |
113114

114-
### TODO
115-
- [ ] Download GitHub HTML for EXTRA style and validate
116-
- [ ] Download GitHub HTML for FLAT style and validate
117-
- [ ] Fix any anchor mismatches found in EXTRA/FLAT
118-
- [ ] Consider unifying anchor generation logic into shared helpers
115+
## Future Work
116+
117+
- [ ] Unify anchor generation logic into shared helper with parameterized flags
118+
- [ ] Add CI job to validate TOC anchors on README changes
119+
120+
---
121+
122+
## Architectural Decision: Anchor Generation Unification
123+
124+
**Date**: 2026-01-09
125+
126+
**Context**: TOC anchor generation logic is duplicated across three files (`awesome.py`, `minimal.py`, `visual.py`) with subtle differences due to each style's heading format.
127+
128+
**Options Considered**:
129+
130+
1. **Parameterized flags** - Create shared helper with semantic flags like `has_back_to_top_in_heading`
131+
2. **Unify to ID-based** - Migrate all styles to use explicit `<h2 id="...">` like EXTRA
132+
133+
**Decision**: Option 1 (parameterized flags)
134+
135+
**Rationale**:
136+
- Lower risk: heading markup remains unchanged
137+
- AWESOME style intentionally uses clean markdown (`## Title`) for aesthetic reasons
138+
- ID-based approach would require CLASSIC to restructure its `[🔝](#...)` links
139+
- Parameterized flags are self-documenting and decouple anchor logic from style names
140+
141+
**Proposed API**:
142+
```python
143+
def generate_toc_anchor(
144+
title: str,
145+
icon: str | None = None,
146+
has_back_to_top_in_heading: bool = False,
147+
) -> str:
148+
"""Generate TOC anchor for a heading.
149+
150+
Args:
151+
title: The heading text (e.g., "Agent Skills")
152+
icon: Optional trailing emoji icon (e.g., "🤖")
153+
has_back_to_top_in_heading: True if heading contains 🔝 back-to-top link
154+
"""
155+
```
156+
157+
**Trade-off**: If a style changes its heading format (e.g., CLASSIC removes 🔝), only the flag value changes—not the shared logic.

scripts/readme/helpers/readme_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,63 @@ def get_anchor_suffix_for_icon(icon: str | None) -> str:
6060
return "-"
6161

6262

63+
def generate_toc_anchor(
64+
title: str,
65+
icon: str | None = None,
66+
has_back_to_top_in_heading: bool = False,
67+
) -> str:
68+
"""Generate a TOC anchor for a heading.
69+
70+
Centralizes anchor generation logic across all README styles.
71+
72+
Args:
73+
title: The heading text (e.g., "Agent Skills")
74+
icon: Optional trailing emoji icon (e.g., "🤖"). Each emoji adds a dash.
75+
has_back_to_top_in_heading: True if heading contains 🔝 back-to-top link,
76+
which adds an additional trailing dash to the anchor.
77+
78+
Returns:
79+
The anchor string without the leading '#' (e.g., "agent-skills-")
80+
"""
81+
base = title.lower().replace(" ", "-").replace("&", "").replace("/", "").replace(".", "")
82+
suffix = get_anchor_suffix_for_icon(icon)
83+
back_to_top_suffix = "-" if has_back_to_top_in_heading else ""
84+
return f"{base}{suffix}{back_to_top_suffix}"
85+
86+
87+
def generate_subcategory_anchor(
88+
title: str,
89+
general_counter: int = 0,
90+
has_back_to_top_in_heading: bool = False,
91+
) -> tuple[str, int]:
92+
"""Generate a TOC anchor for a subcategory heading.
93+
94+
Handles the special case of multiple "General" subcategories which need
95+
unique anchors (general, general-1, general-2, etc.).
96+
97+
Args:
98+
title: The subcategory name (e.g., "General", "IDE Integrations")
99+
general_counter: Current count of "General" subcategories seen so far
100+
has_back_to_top_in_heading: True if heading contains 🔝 back-to-top link
101+
102+
Returns:
103+
Tuple of (anchor_string, updated_general_counter)
104+
"""
105+
base = title.lower().replace(" ", "-").replace("&", "").replace("/", "")
106+
back_to_top_suffix = "-" if has_back_to_top_in_heading else ""
107+
108+
if title == "General":
109+
if general_counter == 0:
110+
anchor = f"general{back_to_top_suffix}"
111+
else:
112+
# GitHub uses double-dash before counter when back-to-top present
113+
separator = "-" if has_back_to_top_in_heading else ""
114+
anchor = f"general-{separator}{general_counter}"
115+
return anchor, general_counter + 1
116+
117+
return f"{base}{back_to_top_suffix}", general_counter
118+
119+
63120
def sanitize_filename_from_anchor(anchor: str) -> str:
64121
"""Convert an anchor string to a tidy filename fragment."""
65122
name = anchor.rstrip("-")

scripts/readme/markup/awesome.py

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
from scripts.readme.helpers.readme_paths import asset_path_token
88
from scripts.readme.helpers.readme_utils import (
9-
get_anchor_suffix_for_icon,
9+
generate_subcategory_anchor,
10+
generate_toc_anchor,
1011
parse_resource_date,
1112
)
1213

@@ -60,24 +61,16 @@ def generate_toc(categories: list[dict], csv_data: list[dict]) -> str:
6061
section_title = category.get("name", "")
6162
icon = category.get("icon", "")
6263
subcategories = category.get("subcategories", [])
63-
anchor_suffix = get_anchor_suffix_for_icon(icon)
64-
65-
anchor = (
66-
section_title.lower()
67-
.replace(" ", "-")
68-
.replace("&", "")
69-
.replace("/", "")
70-
.replace(".", "")
71-
)
7264

65+
anchor = generate_toc_anchor(section_title, icon=icon)
7366
display_title = f"{section_title} {icon}" if icon else section_title
7467

7568
if subcategories:
7669
category_name = category.get("name", "")
7770
has_resources = any(r["Category"] == category_name for r in csv_data)
7871

7972
if has_resources:
80-
toc_lines.append(f"- [{display_title}](#{anchor}{anchor_suffix})")
73+
toc_lines.append(f"- [{display_title}](#{anchor})")
8174

8275
for subcat in subcategories:
8376
sub_title = subcat["name"]
@@ -90,20 +83,12 @@ def generate_toc(categories: list[dict], csv_data: list[dict]) -> str:
9083
]
9184

9285
if resources:
93-
sub_anchor = (
94-
sub_title.lower().replace(" ", "-").replace("&", "").replace("/", "")
86+
sub_anchor, general_counter = generate_subcategory_anchor(
87+
sub_title, general_counter
9588
)
96-
97-
if sub_title == "General":
98-
if general_counter == 0:
99-
sub_anchor = "general"
100-
else:
101-
sub_anchor = f"general-{general_counter}"
102-
general_counter += 1
103-
10489
toc_lines.append(f" - [{sub_title}](#{sub_anchor})")
10590
else:
106-
toc_lines.append(f"- [{display_title}](#{anchor}{anchor_suffix})")
91+
toc_lines.append(f"- [{display_title}](#{anchor})")
10792

10893
return "\n".join(toc_lines).strip()
10994

scripts/readme/markup/minimal.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from datetime import datetime, timedelta
66

77
from scripts.readme.helpers.readme_utils import (
8-
get_anchor_suffix_for_icon,
8+
generate_subcategory_anchor,
9+
generate_toc_anchor,
910
parse_resource_date,
1011
)
1112
from scripts.utils.github_utils import parse_github_url
@@ -67,28 +68,21 @@ def generate_toc(categories: list[dict], csv_data: list[dict]) -> str:
6768
toc_lines.append("")
6869

6970
general_counter = 0
71+
# CLASSIC style headings include [🔝](#awesome-claude-code) which adds another dash
72+
has_back_to_top = True
7073

7174
for category in categories:
7275
section_title = category.get("name", "")
7376
icon = category.get("icon", "")
7477
subcategories = category.get("subcategories", [])
75-
anchor_suffix = get_anchor_suffix_for_icon(icon)
76-
# CLASSIC style headings include [🔝](#awesome-claude-code) which adds another dash
77-
back_to_top_suffix = "-"
78-
79-
anchor = (
80-
section_title.lower()
81-
.replace(" ", "-")
82-
.replace("&", "")
83-
.replace("/", "")
84-
.replace(".", "")
78+
79+
anchor = generate_toc_anchor(
80+
section_title, icon=icon, has_back_to_top_in_heading=has_back_to_top
8581
)
8682

8783
if subcategories:
8884
toc_lines.append("- <details open>")
89-
toc_lines.append(
90-
f' <summary><a href="#{anchor}{anchor_suffix}{back_to_top_suffix}">{section_title}</a></summary>'
91-
)
85+
toc_lines.append(f' <summary><a href="#{anchor}">{section_title}</a></summary>')
9286
toc_lines.append("")
9387

9488
for subcat in subcategories:
@@ -103,27 +97,15 @@ def generate_toc(categories: list[dict], csv_data: list[dict]) -> str:
10397
]
10498

10599
if resources:
106-
sub_anchor = (
107-
sub_title.lower().replace(" ", "-").replace("&", "").replace("/", "")
100+
sub_anchor, general_counter = generate_subcategory_anchor(
101+
sub_title, general_counter, has_back_to_top_in_heading=has_back_to_top
108102
)
109-
110-
# CLASSIC subcategory headings include 🔝 which adds a trailing dash
111-
if sub_title == "General":
112-
if general_counter == 0:
113-
sub_anchor = "general-"
114-
else:
115-
# GitHub uses double-dash before counter: general--1, general--2
116-
sub_anchor = f"general--{general_counter}"
117-
general_counter += 1
118-
else:
119-
sub_anchor = f"{sub_anchor}-"
120-
121103
toc_lines.append(f" - [{sub_title}](#{sub_anchor})")
122104

123105
toc_lines.append("")
124106
toc_lines.append(" </details>")
125107
else:
126-
toc_lines.append(f"- [{section_title}](#{anchor}{anchor_suffix}{back_to_top_suffix})")
108+
toc_lines.append(f"- [{section_title}](#{anchor})")
127109

128110
toc_lines.append("")
129111

scripts/readme/markup/visual.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from scripts.readme.helpers.readme_paths import asset_path_token
1818
from scripts.readme.helpers.readme_utils import (
19+
generate_toc_anchor,
1920
parse_resource_date,
2021
sanitize_filename_from_anchor,
2122
)
@@ -163,22 +164,15 @@ def generate_toc_from_categories(
163164
section_title = category["name"]
164165
category_name = category.get("name", "")
165166
category_id = category.get("id", "")
166-
anchor = (
167-
section_title.lower()
168-
.replace(" ", "-")
169-
.replace("&", "")
170-
.replace("/", "")
171-
.replace(".", "")
172-
)
173-
174-
anchor_suffix = "-"
167+
# EXTRA style uses explicit IDs with trailing dash (no icon in anchor)
168+
anchor = generate_toc_anchor(section_title, icon=None, has_back_to_top_in_heading=True)
175169

176170
svg_filename = get_category_svg_filename(category_id)
177171

178172
dark_svg = svg_filename
179173
light_svg = svg_filename.replace(".svg", "-light-anim-scanline.svg")
180174
toc_lines.append('<div style="height:40px;width:400px;overflow:hidden;display:block;">')
181-
toc_lines.append(f'<a href="#{anchor}{anchor_suffix}">')
175+
toc_lines.append(f'<a href="#{anchor}">')
182176
toc_lines.append(" <picture>")
183177
toc_lines.append(
184178
f' <source media="(prefers-color-scheme: dark)" srcset="{asset_path_token(dark_svg)}">'
@@ -285,8 +279,8 @@ def generate_section_content(
285279
lines.append("</div>")
286280
lines.append("")
287281

288-
anchor = title.lower().replace(" ", "-").replace("&", "").replace("/", "").replace(".", "")
289-
anchor_id = f"{anchor}-"
282+
# EXTRA style uses explicit IDs with trailing dash (no icon in anchor)
283+
anchor_id = generate_toc_anchor(title, icon=None, has_back_to_top_in_heading=True)
290284

291285
section_number = str(section_index + 1).zfill(2)
292286
display_title = title

scripts/testing/validate_toc_anchors.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@
4242
EXPECTED_ANCHORS_PATH = REPO_ROOT / "tests" / "fixtures" / "expected_toc_anchors.txt"
4343

4444
# Style configurations: (html_fixture, readme_path)
45+
# HTML fixture names indicate root vs non-root placement on GitHub
4546
STYLE_CONFIGS = {
46-
"awesome": (FIXTURES_DIR / "awesome.html", REPO_ROOT / "README.md"),
47+
"awesome": (FIXTURES_DIR / "awesome-root.html", REPO_ROOT / "README.md"),
4748
"classic": (
48-
FIXTURES_DIR / "classic.html",
49+
FIXTURES_DIR / "classic-non-root.html",
4950
REPO_ROOT / "README_ALTERNATIVES" / "README_CLASSIC.md",
5051
),
5152
"extra": (
52-
FIXTURES_DIR / "extra.html",
53+
FIXTURES_DIR / "extra-non-root.html",
5354
REPO_ROOT / "README_ALTERNATIVES" / "README_EXTRA.md",
5455
),
5556
"flat": (
56-
FIXTURES_DIR / "flat.html",
57+
FIXTURES_DIR / "flat-non-root.html",
5758
REPO_ROOT / "README_ALTERNATIVES" / "README_FLAT_ALL_AZ.md",
5859
),
5960
}

templates/README_EXTRA.template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
</a>
8181
</td>
8282
<td align="center">
83-
<a href="#workflows-knowledge-guides-">
83+
<a href="#workflows--knowledge-guides-">
8484
<picture>
8585
<source media="(prefers-color-scheme: dark)" srcset="{{ASSET_PATH('card-workflows.svg')}}">
8686
<source media="(prefers-color-scheme: light)" srcset="{{ASSET_PATH('card-workflows-light-anim-lineprint.svg')}}">
File renamed without changes.

0 commit comments

Comments
 (0)