Setting the file. One moment.
Generate From Docs · Code Tour · github/awesome-copilot · Skills Docs
ContentsBack to the top of the page scripts/ generate_from_docs.py
Python · 286 lines · 10 KB
16
17 Examples:
18 python generate_from_docs.py
19 python generate_from_docs.py --persona new-joiner --output .tours/from-readme.tour
20 python generate_from_docs.py --repo-root /path/to/repo --persona vibecoder
21 """
22
23 import json
24 import re
25 import sys
26 import os
27 from pathlib import Path
28 from typing import Optional
29
30
31 # ── Markdown extraction helpers ──────────────────────────────────────────────
32
33 # Matches inline code that looks like a file/directory path
34 _CODE_PATH = re.compile( r "` ([ ^` ] {2,80} ) `" )
35 # Matches headings
36 _HEADING = re.compile( r " ^( # {1,3} )\s + (. + )$ " , re. MULTILINE )
37 # Matches markdown links: [text](url)
38 _LINK = re.compile( r " \[ ([ ^ \] ] + ) \]\( ( https ? :// [ ^) ] + ) \) " )
39 # Patterns that suggest a path (contains / or . with extension)
40 _LOOKS_LIKE_PATH = re.compile( r " ^ \. ? [\w \- ] + ( / [\w \-\. ] + ) + $ | ^ \. / | ^[\w] + \. [ a-z ] {1,5} $ " )
41 # Architecture / structure section keywords
42 _STRUCT_KEYWORDS = re.compile(
43 r " \b( structure | architecture | layout | overview | directory | folder | module | component | "
44 r "design | system | organization | getting . started | quick . start | setup | installation) \b " ,
45 re. IGNORECASE ,
46 )
47
48
49 def _extract_paths_from_text (text: str , repo_root: Path) -> list[ str ]:
50 """Extract inline code that looks like real file/directory paths."""
51 candidates = _CODE_PATH .findall(text)
52 found = []
53 for c in candidates:
54 c = c.strip().lstrip( "./" )
55 if not c:
56 continue
57 if not _LOOKS_LIKE_PATH .match(c) and "/" not in c and "." not in c:
58 continue
59 # check if path actually exists
60 full = repo_root / c
61 if full.exists():
62 found.append(c)
63 return found
64
65
66 def _extract_external_links (text: str ) -> list[tuple[ str , str ]]:
67 """Extract [label](url) pairs for URI steps."""
68 links = _LINK .findall(text)
69 # filter out image links and very generic anchors
70 return [
71 (label, url)
72 for label, url in links
73 if not url.endswith(( ".png" , ".jpg" , ".gif" , ".svg" ))
74 and label.lower() not in ( "here" , "this" , "link" , "click" , "see" )
75 ]
76
77
78 def _split_into_sections (text: str ) -> list[tuple[ str , str ]]:
79 """Split markdown into (heading, body) pairs."""
80 headings = list ( _HEADING .finditer(text))
81 sections = []
82 for i, m in enumerate (headings):
83 heading = m.group( 2 ).strip()
84 start = m.end()
85 end = headings[i + 1 ].start() if i + 1 < len (headings) else len (text)
86 body = text[start:end].strip()
87 sections.append((heading, body))
88 return sections
89
90
91 def _is_structure_section (heading: str ) -> bool :
92 return bool ( _STRUCT_KEYWORDS .search(heading))
93
94
95 # ── Step builders ─────────────────────────────────────────────────────────────
96
97 def _make_content_step (title: str , hint: str ) -> dict :
98 return {
99 "title" : title,
100 "description" : f "[TODO: { hint } ]" ,
101 }
102
103
104 def _make_file_step (path: str , hint: str = "" ) -> dict :
105 step = {
106 "file" : path,
107 "title" : f "[TODO: title for { path } ]" ,
108 "description" : f "[TODO: { hint or 'explain this file for the persona' } ]" ,
109 }
110 return step
111
112
113 def _make_dir_step (path: str , hint: str = "" ) -> dict :
114 return {
115 "directory" : path,
116 "title" : f "[TODO: title for { path } /]" ,
117 "description" : f "[TODO: { hint or 'explain what lives here' } ]" ,
118 }
119
120
121 def _make_uri_step (url: str , label: str ) -> dict :
122 return {
123 "uri" : url,
124 "title" : label,
125 "description" : "[TODO: explain why this link is relevant and what the reader should notice]" ,
126 }
127
128
129 # ── Core generator ────────────────────────────────────────────────────────────
130
131 def generate_skeleton (repo_root: str = "." , persona: str = "new-joiner" ) -> dict :
132 repo = Path(repo_root).resolve()
133
134 # ── Read documentation files ─────────────────────────────────────────
135 doc_files = [ "README.md" , "readme.md" , "Readme.md" ]
136 extra_docs = [ "CONTRIBUTING.md" , "ARCHITECTURE.md" , "docs/architecture.md" , "docs/README.md" ]
137
138 readme_text = ""
139 for name in doc_files:
140 p = repo / name
141 if p.exists():
142 readme_text = p.read_text( errors = "replace" )
143 break
144
145 extra_texts = []
146 for name in extra_docs:
147 p = repo / name
148 if p.exists():
149 extra_texts.append((name, p.read_text( errors = "replace" )))
150
151 all_text = readme_text + " \n " .join(t for _, t in extra_texts)
152
153 # ── Collect steps ─────────────────────────────────────────────────────
154 steps = []
155 seen_paths: set[ str ] = set ()
156
157 # 1. Intro step
158 steps.append(
159 _make_content_step(
160 "Welcome" ,
161 f "Introduce the repo: what it does, who this { persona } tour is for, what they'll understand after finishing." ,
162 )
163 )
164
165 # 2. Parse README sections
166 if readme_text:
167 sections = _split_into_sections(readme_text)
168 for heading, body in sections:
169 # structure / architecture sections → directory steps
170 if _is_structure_section(heading):
171 paths = _extract_paths_from_text(body, repo)
172 for p in paths:
173 if p in seen_paths:
174 continue
175 seen_paths.add(p)
176 full = repo / p
177 if full.is_dir():
178 steps.append(_make_dir_step(p, f "mentioned under ' { heading } ' in README" ))
179 elif full.is_file():
180 steps.append(_make_file_step(p, f "mentioned under ' { heading } ' in README" ))
181
182 # 3. Scan all text for file/dir references not yet captured
183 all_paths = _extract_paths_from_text(all_text, repo)
184 for p in all_paths:
185 if p in seen_paths:
186 continue
187 seen_paths.add(p)
188 full = repo / p
189 if full.is_dir():
190 steps.append(_make_dir_step(p))
191 elif full.is_file():
192 steps.append(_make_file_step(p))
193
194 # 4. If very few file steps found, fall back to top-level directory scan
195 file_and_dir_steps = [s for s in steps if "file" in s or "directory" in s]
196 if len (file_and_dir_steps) < 3 :
197 # add top-level directories
198 for item in sorted (repo.iterdir()):
199 if item.name.startswith( "." ) or item.name in ( "node_modules" , "__pycache__" , ".git" ):
200 continue
201 rel = str (item.relative_to(repo))
202 if rel in seen_paths:
203 continue
204 seen_paths.add(rel)
205 if item.is_dir():
206 steps.append(_make_dir_step(rel, "top-level directory" ))
207 elif item.is_file() and item.suffix in ( ".ts" , ".js" , ".py" , ".go" , ".rs" , ".java" , ".rb" ):
208 steps.append(_make_file_step(rel, "top-level source file" ))
209
210 # 5. URI steps from external links in README
211 links = _extract_external_links(readme_text)
212 # Only include links that look like architecture / design references
213 for label, url in links[: 3 ]: # cap at 3 to avoid noise
214 steps.append(_make_uri_step(url, label))
215
216 # 6. Closing step
217 steps.append(
218 _make_content_step(
219 "What to Explore Next" ,
220 "Summarize what the reader now understands. List 2–3 follow-up tours they should read next." ,
221 )
222 )
223
224 # Deduplicate steps by (file/directory/uri key)
225 seen_keys: set = set ()
226 deduped = []
227 for s in steps:
228 key = s.get( "file" ) or s.get( "directory" ) or s.get( "uri" ) or s.get( "title" )
229 if key in seen_keys:
230 continue
231 seen_keys.add(key)
232 deduped.append(s)
233
234 return {
235 "$schema" : "https://aka.ms/codetour-schema" ,
236 "title" : f "[TODO: descriptive title for { persona } tour]" ,
237 "description" : f "[TODO: one sentence — who this is for and what they'll understand]" ,
238 "_skeleton_generated_by" : "generate_from_docs.py" ,
239 "_instructions" : (
240 "This is a skeleton. Fill in every [TODO: ...] with real content. "
241 "Read each referenced file before writing its description. "
242 "Remove this _skeleton_generated_by and _instructions field before saving."
243 ),
244 "steps" : deduped,
245 }
246
247
248 def main ():
249 args = sys.argv[ 1 :]
250 if "--help" in args or "-h" in args:
251 print ( __doc__ )
252 sys.exit( 0 )
253
254 repo_root = "."
255 persona = "new-joiner"
256 output: Optional[ str ] = None
257
258 i = 0
259 while i < len (args):
260 if args[i] == "--repo-root" and i + 1 < len (args):
261 repo_root = args[i + 1 ]
262 i += 2
263 elif args[i] == "--persona" and i + 1 < len (args):
264 persona = args[i + 1 ]
265 i += 2
266 elif args[i] == "--output" and i + 1 < len (args):
267 output = args[i + 1 ]
268 i += 2
269 else :
270 i += 1
271
272 skeleton = generate_skeleton(repo_root, persona)
273 out_json = json.dumps(skeleton, indent = 2 )
274
275 if output:
276 Path(output).parent.mkdir( parents = True , exist_ok = True )
277 Path(output).write_text(out_json)
278 print ( f "✅ Skeleton written to { output } " )
279 print ( f " { len (skeleton[ 'steps' ]) } steps generated from docs" )
280 print ( f " Fill in all [TODO: ...] entries before sharing" )
281 else :
282 print (out_json)
283
284
285 if __name__ == "__main__" :
286 main()