1+ #!/usr/bin/env python3
2+ """
3+ generate_full_wiki.py
4+
5+ Usage:
6+ cd /home/rich/fujinet-lib
7+ python3 wiki/generate_full_wiki.py [--outdir wiki] [--headers fujinet-fuji.h,fujinet-network.h,fujinet-clock.h]
8+
9+ Creates:
10+ wiki/index.md
11+ wiki/generated/<function>.md
12+ """
13+ import re
14+ import os
15+ import sys
16+ import argparse
17+ import subprocess
18+ from pathlib import Path
19+
20+ parser = argparse .ArgumentParser ()
21+ parser .add_argument ('--outdir' , default = 'wiki' , help = 'Output wiki directory (default: wiki)' )
22+ parser .add_argument ('--headers' , default = 'fujinet-fuji.h,fujinet-network.h,fujinet-clock.h' ,
23+ help = 'Comma-separated header filenames relative to repo root' )
24+ args = parser .parse_args ()
25+
26+ repo_root = Path .cwd ()
27+ outdir = (repo_root / args .outdir ).resolve ()
28+ gen_dir = outdir / 'generated'
29+ gen_dir .mkdir (parents = True , exist_ok = True )
30+
31+ headers = [repo_root / h .strip () for h in args .headers .split (',' )]
32+
33+ def read_file (p ):
34+ try :
35+ return p .read_text (encoding = 'utf-8' )
36+ except Exception :
37+ return ''
38+
39+ # Heuristic: split header text on semicolons to get candidate declarations
40+ def find_prototypes (text ):
41+ candidates = []
42+ parts = re .split (r';' , text )
43+ pos = 0
44+ for part in parts :
45+ seg = part .strip ()
46+ if not seg :
47+ pos += len (part ) + 1
48+ continue
49+ # ignore macros/typedefs/struct/enum lines
50+ if re .search (r'^\s*(#|typedef|struct|enum)\b' , seg ):
51+ pos += len (part ) + 1
52+ continue
53+ # must contain '(' and ')'
54+ if '(' not in seg or ')' not in seg :
55+ pos += len (part ) + 1
56+ continue
57+ # avoid function pointer typedefs that start with 'typedef'
58+ # attempt to match a function-like declaration ending with ')'
59+ candidates .append ((seg , pos ))
60+ pos += len (part ) + 1
61+ return candidates
62+
63+ # Extract function name, return type, params from a segment
64+ def parse_candidate (seg ):
65+ seg = seg .strip ()
66+ # remove possible leading attribute macros like 'extern ' or qualifiers
67+ # attempt regex: (ret_type) (name) (params)
68+ # allow multiline params; remove trailing comments inside
69+ s = re .sub (r'/\*.*?\*/' , '' , seg , flags = re .DOTALL )
70+ s = re .sub (r'//.*$' , '' , s , flags = re .MULTILINE )
71+ # Try to find the last identifier before '('
72+ m = re .search (r'([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*([^)]*)\s*\)\s*$' , s , flags = re .DOTALL )
73+ if not m :
74+ return None
75+ name = m .group (1 )
76+ params = m .group (2 ).strip ()
77+ pre = s [:m .start (1 )]
78+ # derive return type = pre trimmed
79+ ret = pre .strip ()
80+ # normalize whitespace
81+ ret = re .sub (r'\s+' , ' ' , ret )
82+ return {'name' : name , 'params' : params , 'ret' : ret , 'raw' : seg }
83+
84+ # find header comment block (/** ... */) immediately above a substring index
85+ def find_comment_before (text , seg ):
86+ # find segment position
87+ idx = text .find (seg )
88+ if idx == - 1 :
89+ return ''
90+ # search backward for /** ... */ that ends before idx
91+ # we'll find the last /** ... */ that ends before idx
92+ matches = list (re .finditer (r'/\*\*.*?\*/' , text , flags = re .DOTALL ))
93+ for m in reversed (matches ):
94+ if m .end () <= idx :
95+ # return trimmed comment
96+ return m .group (0 ).strip ()
97+ return ''
98+
99+ # run git grep to find implementations (best-effort). Returns list of matches.
100+ def git_grep (fn_name ):
101+ try :
102+ out = subprocess .check_output (['git' , 'grep' , '-n' , '--break' , '--heading' , '-E' , rf'{ re .escape (fn_name )} \s*\(' ],
103+ cwd = repo_root , stderr = subprocess .DEVNULL , text = True )
104+ return [line .rstrip ('\n ' ) for line in out .splitlines ()]
105+ except Exception :
106+ return []
107+
108+ # Search headers and build function map (header -> list of parsed prototypes)
109+ function_map = {} # header path -> list of entries
110+ for h in headers :
111+ text = read_file (h )
112+ function_map [str (h )] = []
113+ if not text :
114+ continue
115+ candidates = find_prototypes (text )
116+ for seg , _pos in candidates :
117+ parsed = parse_candidate (seg )
118+ if parsed :
119+ # filter out some non-functions (e.g., macros that look like calls)
120+ # ensure name not keyword
121+ if parsed ['name' ] and not parsed ['name' ].startswith ('(' ):
122+ parsed ['comment' ] = find_comment_before (text , seg )
123+ function_map [str (h )].append (parsed )
124+
125+ # Write index.md
126+ index_path = outdir / 'index.md'
127+ with index_path .open ('w' , encoding = 'utf-8' ) as f :
128+ f .write ('# FujiNet-lib API Reference\n \n ' )
129+ f .write ('This documentation is generated from the root header files:\n \n ' )
130+ for h in headers :
131+ f .write (f'- `{ h .name } `\n ' )
132+ f .write ('\n ## Headers and Functions\n \n ' )
133+ for h in headers :
134+ f .write (f'### `{ h .name } `\n \n ' )
135+ funcs = function_map .get (str (h ), [])
136+ if not funcs :
137+ f .write ('_No functions detected in this header._\n \n ' )
138+ continue
139+ f .write ('Functions declared in this header:\n \n ' )
140+ for p in funcs :
141+ f .write (f'- [`{ p ["name" ]} `](generated/{ p ["name" ]} .md)\n ' )
142+ f .write ('\n ' )
143+
144+ # Generate per-function pages
145+ for h in headers :
146+ for p in function_map .get (str (h ), []):
147+ name = p ['name' ]
148+ out_file = gen_dir / f'{ name } .md'
149+ with out_file .open ('w' , encoding = 'utf-8' ) as fh :
150+ fh .write (f'# { name } \n \n ' )
151+ fh .write (f'**Declared in:** `{ h .name } `\n \n ' )
152+ fh .write ('## Prototype\n \n ' )
153+ fh .write ('```c\n ' )
154+ fh .write (p ['raw' ].strip () + ';\n ' )
155+ fh .write ('```\n \n ' )
156+ fh .write ('## Description\n \n ' )
157+ fh .write ('_No description available — please add a detailed description of what this function does._\n \n ' )
158+ fh .write ('## Parameters\n \n ' )
159+ if not p ['params' ] or p ['params' ].strip ().lower () == 'void' :
160+ fh .write ('_This function takes no parameters._\n \n ' )
161+ else :
162+ # split parameters by commas at top-level (no parentheses support of nested)
163+ parts = [pp .strip () for pp in re .split (r',\s*(?![^()]*\))' , p ['params' ])]
164+ fh .write ('| Name | Type | Description |\n ' )
165+ fh .write ('|---|---|---|\n ' )
166+ for part in parts :
167+ # try to split last token as name
168+ if not part :
169+ continue
170+ tokens = part .rsplit (None , 1 )
171+ if len (tokens ) == 1 :
172+ ptype = tokens [0 ]
173+ pname = ''
174+ else :
175+ ptype , pname = tokens
176+ fh .write (f'| `{ pname } ` | `{ ptype } ` | _TODO: describe parameter_ |\n ' )
177+ fh .write ('\n ' )
178+ fh .write ('## Return Value\n \n ' )
179+ ret = p ['ret' ] or 'void'
180+ fh .write (f'- **Type:** `{ ret } `\n \n ' )
181+ fh .write ('- **Meaning:** _TODO: describe return value and error conditions._\n \n ' )
182+ fh .write ('## Header notes\n \n ' )
183+ if p .get ('comment' ):
184+ fh .write ('```\n ' )
185+ fh .write (p ['comment' ] + '\n ' )
186+ fh .write ('```\n \n ' )
187+ else :
188+ fh .write ('_No header comments found; placeholder for details._\n \n ' )
189+ fh .write ('## Implementations (git grep results)\n \n ' )
190+ impls = git_grep (name )
191+ if impls :
192+ for line in impls :
193+ fh .write (f'- { line } \n ' )
194+ else :
195+ fh .write ('_No implementations found via git grep._\n ' )
196+ fh .write ('\n ' )
197+ # Platform stub detection removed per user request.
198+ fh .write ('\n ----\n \n ' )
199+ fh .write ('[Back to index](../index.md)\n ' )
200+
201+ print ('Generation complete.' )
202+ print (f'Main index: { index_path } ' )
203+ print (f'Per-function pages: { gen_dir } ' )
0 commit comments