Skip to content

Commit 2378cc5

Browse files
authored
Merge pull request #19 from ibra-kdbra/copilot/update-github-workflow-readme-toc
Migrate workflow to GitHub Actions, modernize TOC script, document PR #18 templates
2 parents da47c13 + 2781a3d commit 2378cc5

7 files changed

Lines changed: 206 additions & 83 deletions

File tree

.github/workflows/update-readme.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212

1313
jobs:
1414
regen-readme:
15-
runs-on: self-hosted
15+
runs-on: ubuntu-latest
1616

1717
steps:
1818
- name: 🛎️ Checkout
@@ -21,6 +21,11 @@ jobs:
2121
persist-credentials: true
2222
token: ${{ secrets.GITHUB_TOKEN }}
2323

24+
- name: 🐍 Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: '3.x'
28+
2429
- name: ⚙️ Generate TOC
2530
run: python3 scripts/order.py
2631

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# Virtual environments
7+
venv/
8+
.env/
9+
10+
# IDE
11+
.vscode/
12+
.idea/
13+
14+
# Build directories
15+
build/
16+
cmake-build-*/
17+
18+
# OS files
19+
.DS_Store
20+
Thumbs.db

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,24 @@
3838
<thead>
3939
<tr>
4040
<th style="padding:4px 8px; text-align:center; font-weight:bold; border:1px solid #ddd;">A–I</th>
41-
<th style="padding:4px 8px; text-align:center; font-weight:bold; border:1px solid #ddd;">I–R</th>
41+
<th style="padding:4px 8px; text-align:center; font-weight:bold; border:1px solid #ddd;">J–R</th>
4242
<th style="padding:4px 8px; text-align:center; font-weight:bold; border:1px solid #ddd;">S–W</th>
4343
</tr>
4444
</thead>
4545
<tbody>
4646
<tr>
4747
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#atari-games">Atari-Games</a></td>
48-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#introspective">introspective</a></td>
48+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#js-compiler">JS-compiler</a></td>
4949
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#sfml-multithread-verlet">SFML Multithread verlet</a></td>
5050
</tr>
5151
<tr>
5252
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#beneficial-c-programming">Beneficial-C programming</a></td>
53-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#js-compiler">JS-compiler</a></td>
53+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#json-converter">Json Converter</a></td>
5454
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#simple-code">Simple-Code</a></td>
5555
</tr>
5656
<tr>
5757
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#chess-3-d">Chess 3 D</a></td>
58-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#json-converter">Json Converter</a></td>
58+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#kalman-filtering-simulation">Kalman-Filtering-Simulation</a></td>
5959
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#simple-turso">simple turso</a></td>
6060
</tr>
6161
<tr>
@@ -80,39 +80,44 @@
8080
</tr>
8181
<tr>
8282
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#define-evil">define evil</a></td>
83-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#ofx-async">ofx Async</a></td>
83+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#naive-circuit-simulator">Naive Circuit Simulator</a></td>
8484
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#terminal-file-manager">Terminal File Manager</a></td>
8585
</tr>
8686
<tr>
8787
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#design-patterns">Design-Patterns</a></td>
88-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#open-gl-examples">Open GL examples</a></td>
88+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#ofx-async">ofx Async</a></td>
8989
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#thread-pool">Thread Pool</a></td>
9090
</tr>
9191
<tr>
9292
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#dump-c">Dump C++</a></td>
93-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#pfad">PFAD</a></td>
93+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#open-gl-examples">Open GL examples</a></td>
9494
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#tokenizer-tok">Tokenizer tok</a></td>
9595
</tr>
9696
<tr>
9797
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#file-lock-socket">File Lock Socket</a></td>
98-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#pipes-screen-saver">Pipes Screen Saver</a></td>
98+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#pfad">PFAD</a></td>
9999
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#trigonometric-functions">Trigonometric-Functions</a></td>
100100
</tr>
101101
<tr>
102102
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#gen-xpassword">Gen XPassword</a></td>
103-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#pong">POng</a></td>
103+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#pipes-screen-saver">Pipes Screen Saver</a></td>
104104
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#vite-fa-config">Vite FA config</a></td>
105105
</tr>
106106
<tr>
107107
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#hangman">Hangman</a></td>
108-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#random-utility-tools">Random Utility tools</a></td>
108+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#pong">POng</a></td>
109109
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#volumetric-clouds">volumetric-clouds</a></td>
110110
</tr>
111111
<tr>
112112
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#inject-hook">Inject Hook</a></td>
113-
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#regular-expression-convertors">Regular Expression Convertors</a></td>
113+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#random-utility-tools">Random Utility tools</a></td>
114114
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#word-proc">Word Proc</a></td>
115115
</tr>
116+
<tr>
117+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#introspective">introspective</a></td>
118+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#regular-expression-convertors">Regular Expression Convertors</a></td>
119+
<td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"></td>
120+
</tr>
116121
</tbody>
117122
</table>
118123
<!-- TOC END -->

scripts/order.py

Lines changed: 134 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
11
#!/usr/bin/env python3
2+
"""
3+
README Table of Contents Generator
4+
5+
This script automatically generates and updates the Table of Contents (TOC)
6+
in the README.md file based on project directories in the repository.
7+
"""
28
import os
9+
import sys
310
import math
411
import re
512
import urllib.parse
13+
from pathlib import Path
614
from itertools import zip_longest
715

816
# Configuration
917
README = "README.md"
10-
EXCLUDE = {".git", ".github", "scripts", "__pycache__"}
18+
EXCLUDE = {".git", ".github", "scripts", "__pycache__", "wiki"}
1119
# Override mapping: directory name -> desired header display
1220
OVERRIDES = {
1321
"Dump_c++": "Dump C++",
1422
# can add any other overrides
1523
}
1624

17-
# Derive a human-friendly display name for TOC entries
25+
1826
def display_name(name):
27+
"""Derive a human-friendly display name for TOC entries."""
1928
if name in OVERRIDES:
2029
return OVERRIDES[name]
2130
# Insert spaces before camelCase boundaries, then replace underscores
2231
s = re.sub(r'(?<=[a-z0-9])(?=[A-Z])', ' ', name)
2332
s = s.replace('_', ' ')
2433
return s.strip()
2534

26-
# Create a GitHub-style slug for IDs
35+
2736
def slugify(name):
37+
"""Create a GitHub-style slug for anchor IDs."""
2838
base = OVERRIDES.get(name, name)
2939
# Split camelCase and underscores
3040
s = re.sub(r'(?<=[a-z0-9])(?=[A-Z])', ' ', base)
@@ -37,84 +47,137 @@ def slugify(name):
3747
# Collapse spaces into hyphens
3848
return re.sub(r"\s+", "-", s.strip())
3949

40-
# Convert slug back to a Title Case display without hyphens
50+
4151
def header_display_from_slug(slug):
52+
"""Convert slug back to a Title Case display without hyphens."""
4253
disp = slug.replace('-', ' ')
4354
return disp.title()
4455

45-
# Compute column header ranges
56+
4657
def header_range(col):
58+
"""Compute column header ranges (e.g., 'A-M')."""
4759
if not col:
4860
return ""
4961
first = slugify(col[0])[0].upper()
5062
last = slugify(col[-1])[0].upper()
5163
return f"{first}{last}"
5264

53-
# Collect project directories
54-
projects = sorted(
55-
[d for d in os.listdir('.')
56-
if os.path.isdir(d)
57-
and d not in EXCLUDE
58-
and not d.startswith('.')],
59-
key=str.lower
60-
)
61-
62-
# Split into 3 roughly even columns
63-
cols = 3
64-
chunk = math.ceil(len(projects) / cols)
65-
columns = [projects[i*chunk:(i+1)*chunk] for i in range(cols)]
66-
67-
# Build HTML table
68-
lines = [
69-
'<table style="width:100%; border-collapse: collapse; border:1px solid #ddd;">',
70-
' <thead>',
71-
' <tr>'
72-
]
73-
for col in columns:
74-
lines.append(
75-
f' <th style="padding:4px 8px; text-align:center; font-weight:bold; border:1px solid #ddd;">{header_range(col)}</th>'
65+
66+
def collect_projects(base_path):
67+
"""Collect and return sorted list of project directories."""
68+
base = Path(base_path)
69+
if not base.is_dir():
70+
raise ValueError(f"Base path '{base_path}' is not a valid directory")
71+
72+
projects = sorted(
73+
[d.name for d in base.iterdir()
74+
if d.is_dir()
75+
and d.name not in EXCLUDE
76+
and not d.name.startswith('.')],
77+
key=str.lower
7678
)
77-
lines += [' </tr>', ' </thead>', ' <tbody>']
78-
79-
for row in zip_longest(*columns, fillvalue=""):
80-
lines.append(' <tr>')
81-
for cell in row:
82-
if cell:
83-
slug = slugify(cell)
84-
disp = display_name(cell)
85-
lines.append(
86-
f' <td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#' + slug + f'">{disp}</a></td>'
87-
)
79+
return projects
80+
81+
82+
def build_toc_table(projects):
83+
"""Build HTML table for the Table of Contents."""
84+
# Split into 3 roughly even columns
85+
cols = 3
86+
chunk = math.ceil(len(projects) / cols) if projects else 1
87+
columns = [projects[i*chunk:(i+1)*chunk] for i in range(cols)]
88+
89+
# Build HTML table
90+
lines = [
91+
'<table style="width:100%; border-collapse: collapse; border:1px solid #ddd;">',
92+
' <thead>',
93+
' <tr>'
94+
]
95+
for col in columns:
96+
lines.append(
97+
f' <th style="padding:4px 8px; text-align:center; font-weight:bold; border:1px solid #ddd;">{header_range(col)}</th>'
98+
)
99+
lines += [' </tr>', ' </thead>', ' <tbody>']
100+
101+
for row in zip_longest(*columns, fillvalue=""):
102+
lines.append(' <tr>')
103+
for cell in row:
104+
if cell:
105+
slug = slugify(cell)
106+
disp = display_name(cell)
107+
lines.append(
108+
f' <td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"><a href="#' + slug + f'">{disp}</a></td>'
109+
)
110+
else:
111+
lines.append(
112+
' <td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"></td>'
113+
)
114+
lines.append(' </tr>')
115+
lines += [' </tbody>', '</table>']
116+
return "\n".join(lines)
117+
118+
119+
def normalize_headers(content):
120+
"""Normalize all headers to slug-based Title Case (no underscores or hyphens)."""
121+
pattern = re.compile(r'^(### \[)([^\]]+)(\]\(\./([^\)]+)\))', re.MULTILINE)
122+
123+
def repl(match):
124+
raw = match.group(4)
125+
# Decode URL-encoded directory name
126+
decoded = urllib.parse.unquote(raw)
127+
# Generate slug and display
128+
slug = slugify(decoded)
129+
disp = header_display_from_slug(slug)
130+
# Keep original raw in link
131+
return f"### [{disp}](./{raw})"
132+
133+
return pattern.sub(repl, content)
134+
135+
136+
def update_toc_in_content(content, new_toc):
137+
"""Replace TOC region in content with new TOC."""
138+
toc_re = re.compile(r"(<!-- TOC START -->)(.*?)(<!-- TOC END -->)", re.S)
139+
return toc_re.sub(rf"\1\n{new_toc}\n\3", content)
140+
141+
142+
def main():
143+
"""Main entry point for the TOC generator."""
144+
readme_path = Path(README)
145+
146+
# Verify README exists
147+
if not readme_path.is_file():
148+
print(f"Error: {README} not found in current directory", file=sys.stderr)
149+
sys.exit(1)
150+
151+
try:
152+
# Collect project directories
153+
projects = collect_projects('.')
154+
155+
if not projects:
156+
print("Warning: No project directories found", file=sys.stderr)
157+
158+
# Build new TOC
159+
new_toc = build_toc_table(projects)
160+
161+
# Read README.md
162+
content = readme_path.read_text(encoding="utf-8")
163+
164+
# Normalize headers
165+
content = normalize_headers(content)
166+
167+
# Replace TOC region
168+
updated = update_toc_in_content(content, new_toc)
169+
170+
# Write back if changed
171+
if updated != content:
172+
readme_path.write_text(updated, encoding="utf-8")
173+
print(f"✅ {README} updated successfully")
88174
else:
89-
lines.append(
90-
' <td style="padding:4px 8px; text-align:center; border:1px solid #ddd;"></td>'
91-
)
92-
lines.append(' </tr>')
93-
lines += [' </tbody>', '</table>']
94-
new_toc = "\n".join(lines)
95-
96-
# Read README.md
97-
with open(README, "r", encoding="utf-8") as f:
98-
content = f.read()
99-
100-
# Normalize all headers to slug-based Title Case (no underscores or hyphens)
101-
pattern = re.compile(r'^(### \[)([^\]]+)(\]\(\./([^\)]+)\))', re.MULTILINE)
102-
def repl(match):
103-
raw = match.group(4)
104-
# Decode URL-encoded directory name
105-
decoded = urllib.parse.unquote(raw)
106-
# Generate slug and display
107-
slug = slugify(decoded)
108-
disp = header_display_from_slug(slug)
109-
# Keep original raw in link
110-
return f"### [{disp}](./{raw})"
111-
content = pattern.sub(repl, content)
112-
113-
# Replace TOC region
114-
toc_re = re.compile(r"(<!-- TOC START -->)(.*?)(<!-- TOC END -->)", re.S)
115-
updated = toc_re.sub(rf"\1\n{new_toc}\n\3", content)
116-
117-
# Write back if changed
118-
if updated != content:
119-
with open(README, "w", encoding="utf-8") as f:
120-
f.write(updated)
175+
print(f"ℹ️ {README} already up to date")
176+
177+
except (OSError, ValueError) as e:
178+
print(f"Error: {e}", file=sys.stderr)
179+
sys.exit(1)
180+
181+
182+
if __name__ == "__main__":
183+
main()

0 commit comments

Comments
 (0)