-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathupdate_lesson_nav.py
More file actions
194 lines (157 loc) · 6.08 KB
/
Copy pathupdate_lesson_nav.py
File metadata and controls
194 lines (157 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""
update_lesson_nav.py — Migrate lesson navigation buttons to card-style <nav>.
Scans all .md files under DOCS_DIR for the old-style lesson nav pattern:
<span class="fs-6">
[Previous: ...](file.md){: .btn .btn-outline }
[Next: ...](file.md){: .btn .btn-outline }
</span>
and replaces it with the new card-style navigation:
<nav class="lesson-nav" aria-label="Lesson navigation">
<a href="file.html" class="nav-prev">
<div class="nav-label">← Previous Lesson</div>
<div class="nav-title">Title</div>
</a>
<a href="file.html" class="nav-next">
<div class="nav-label">Next Lesson →</div>
<div class="nav-title">Title</div>
</a>
</nav>
Handles three cases: prev+next, next-only, and prev-only.
Link extensions are converted from .md to .html for Jekyll compatibility.
Usage:
python scripts/update_lesson_nav.py # dry run (default)
python scripts/update_lesson_nav.py --run # apply changes
Always run dry first and inspect with `git diff` after applying.
"""
import re
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DOCS_DIR = "." # Path to markdown files, relative to repo root
# ---------------------------------------------------------------------------
# Patterns
# ---------------------------------------------------------------------------
# Matches the entire <span class="fs-6">...</span> nav block
BLOCK_RE = re.compile(
r'<span class="fs-6">\s*\n(.*?)</span>',
re.DOTALL
)
# Matches a "Previous" link, with or without leading arrow characters
PREV_RE = re.compile(
r'\[(?:← |← )?Previous:\s*(.*?)\]\((.*?)\.md\)',
re.IGNORECASE
)
# Matches a "Next" link, with or without trailing arrow characters
NEXT_RE = re.compile(
r'\[Next:\s*(.*?)(?: →| →)?\]\((.*?)\.md\)',
re.IGNORECASE
)
# ---------------------------------------------------------------------------
# HTML generation
# ---------------------------------------------------------------------------
def build_nav(prev, next_):
"""Build the new <nav> HTML from extracted link data.
Args:
prev: Tuple of (title, href_without_extension) or None.
next_: Tuple of (title, href_without_extension) or None.
Returns:
HTML string for the new lesson nav block.
"""
links = []
if prev:
links.append(
f' <a href="{prev[1]}.html" class="nav-prev">\n'
f' <div class="nav-label">← Previous Lesson</div>\n'
f' <div class="nav-title">{prev[0]}</div>\n'
f' </a>'
)
if next_:
links.append(
f' <a href="{next_[1]}.html" class="nav-next">\n'
f' <div class="nav-label">Next Lesson →</div>\n'
f' <div class="nav-title">{next_[0]}</div>\n'
f' </a>'
)
return (
'<nav class="lesson-nav" aria-label="Lesson navigation">\n'
+ '\n'.join(links)
+ '\n</nav>'
)
# ---------------------------------------------------------------------------
# File processing
# ---------------------------------------------------------------------------
def process_files(dry_run=True):
"""Scan markdown files and replace old nav blocks with new card-style nav.
Args:
dry_run: If True, only print what would change without modifying files.
Returns:
Dict with summary stats.
"""
stats = {
"scanned": 0,
"updated": 0,
"skipped_no_block": 0,
"skipped_no_links": 0,
"prev_next": 0,
"next_only": 0,
"prev_only": 0,
}
for filepath in sorted(Path(DOCS_DIR).rglob('*.md')):
stats["scanned"] += 1
content = filepath.read_text(encoding='utf-8')
# Look for the old <span class="fs-6"> nav block
block = BLOCK_RE.search(content)
if not block:
stats["skipped_no_block"] += 1
continue
# Extract Previous and Next links from inside the block
inner = block.group(1)
prev = PREV_RE.search(inner)
next_ = NEXT_RE.search(inner)
if not prev and not next_:
stats["skipped_no_links"] += 1
continue
# Determine nav type for reporting
if prev and next_:
nav_type = "prev+next"
stats["prev_next"] += 1
elif next_:
nav_type = "next only"
stats["next_only"] += 1
else:
nav_type = "prev only"
stats["prev_only"] += 1
# Build replacement HTML
prev_data = (prev.group(1).strip(), prev.group(2)) if prev else None
next_data = (next_.group(1).strip(), next_.group(2)) if next_ else None
new_nav = build_nav(prev_data, next_data)
# Replace the old block with the new nav
new_content = content[:block.start()] + new_nav + content[block.end():]
tag = "[DRY RUN] " if dry_run else "[UPDATED] "
print(f" {tag}{filepath} ({nav_type})")
if not dry_run:
filepath.write_text(new_content, encoding='utf-8')
stats["updated"] += 1
return stats
def print_summary(stats, dry_run):
"""Print a summary of what was (or would be) changed."""
mode = "DRY RUN" if dry_run else "COMPLETE"
print(f"\n{'=' * 50}")
print(f" {mode} SUMMARY")
print(f"{'=' * 50}")
print(f" Files scanned: {stats['scanned']}")
print(f" Files updated: {stats['updated']}")
print(f" ├─ prev + next: {stats['prev_next']}")
print(f" ├─ next only: {stats['next_only']}")
print(f" └─ prev only: {stats['prev_only']}")
print(f" Skipped (no block): {stats['skipped_no_block']}")
print(f" Skipped (no links): {stats['skipped_no_links']}")
print(f"{'=' * 50}")
if dry_run:
print(" Re-run with --run to apply changes.")
if __name__ == '__main__':
dry_run = "--run" not in sys.argv
stats = process_files(dry_run=dry_run)
print_summary(stats, dry_run)