docs: improved API ref generator with multi-line sigs
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Improved Doxygen→Markdown extractor. Handles multi-line signatures."""
|
||||
import re, os, glob
|
||||
|
||||
def read_until_braces(text, start):
|
||||
"""Read from start position until we find a complete function/struct/enum declaration.
|
||||
Returns (full_signature, end_position)"""
|
||||
pos = start
|
||||
result = []
|
||||
paren_depth = 0
|
||||
angle_depth = 0
|
||||
found_open = False
|
||||
found_template = False
|
||||
|
||||
while pos < len(text):
|
||||
ch = text[pos]
|
||||
result.append(ch)
|
||||
|
||||
if ch == '(':
|
||||
paren_depth += 1
|
||||
found_open = True
|
||||
elif ch == ')':
|
||||
paren_depth -= 1
|
||||
elif ch == '<' and found_open:
|
||||
angle_depth += 1
|
||||
elif ch == '>' and found_open:
|
||||
angle_depth -= 1
|
||||
elif ch == '{' and found_open and paren_depth == 0 and angle_depth == 0:
|
||||
# Function body started
|
||||
break
|
||||
elif ch == ';' and found_open and paren_depth == 0 and angle_depth == 0:
|
||||
# Declaration ended
|
||||
break
|
||||
elif ch == '\n' and not found_open:
|
||||
# Too many newlines without finding a function
|
||||
if len(result) > 200:
|
||||
break
|
||||
|
||||
pos += 1
|
||||
|
||||
return ''.join(result).strip(), pos + 1
|
||||
|
||||
def extract_blocks(text):
|
||||
"""Extract /** */ blocks and the function/struct/enum that follows."""
|
||||
results = []
|
||||
for m in re.finditer(r'/\*\*\s*\n(.*?)\*/', text, re.DOTALL):
|
||||
doc_raw = m.group(1)
|
||||
after_start = m.end()
|
||||
|
||||
# Skip whitespace, includes, comments
|
||||
while after_start < len(text):
|
||||
ch = text[after_start]
|
||||
if ch in ' \t\n\r':
|
||||
after_start += 1
|
||||
continue
|
||||
line_start = after_start
|
||||
line_end = text.find('\n', line_start)
|
||||
if line_end < 0: line_end = len(text)
|
||||
line = text[line_start:line_end].strip()
|
||||
if line.startswith('#') or line.startswith('//') or line.startswith('using ') or line == '':
|
||||
after_start = line_end + 1
|
||||
continue
|
||||
break
|
||||
|
||||
if after_start >= len(text):
|
||||
continue
|
||||
|
||||
# Read full signature (multi-line)
|
||||
sig, end_pos = read_until_braces(text, after_start)
|
||||
|
||||
# Clean doc
|
||||
doc_lines = []
|
||||
for line in doc_raw.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('*'):
|
||||
line = line[1:].strip()
|
||||
if line.startswith('@') or line:
|
||||
doc_lines.append(line)
|
||||
clean_doc = '\n'.join(doc_lines).strip()
|
||||
|
||||
results.append((clean_doc, sig))
|
||||
|
||||
return results
|
||||
|
||||
def to_md(doc, sig):
|
||||
"""Convert doxygen doc + signature to markdown."""
|
||||
brief, params, ret, notes, sees, warns = '', [], '', [], [], []
|
||||
in_code = False
|
||||
|
||||
for line in doc.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('@brief '):
|
||||
brief = line[7:]
|
||||
elif line.startswith('@param '):
|
||||
params.append(line[7:])
|
||||
elif line.startswith('@return ') or line.startswith('@retval '):
|
||||
ret = line.split(' ', 1)[1] if ' ' in line else ''
|
||||
elif line.startswith('@note '):
|
||||
notes.append(f'> {line[6:]}')
|
||||
elif line.startswith('@warning '):
|
||||
warns.append(f'> 🔴 {line[9:]}')
|
||||
elif line.startswith('@see '):
|
||||
sees.append(line[5:].strip())
|
||||
elif line.startswith('@code'):
|
||||
in_code = True
|
||||
elif line.startswith('@endcode'):
|
||||
in_code = False
|
||||
elif line.startswith('@ingroup') or line.startswith('@defgroup'):
|
||||
pass
|
||||
elif not in_code and line and not line.startswith('@') and not line.startswith('\\'):
|
||||
if not brief:
|
||||
brief = line
|
||||
|
||||
if not brief:
|
||||
return ''
|
||||
|
||||
md = f"### {brief}\n\n"
|
||||
|
||||
# Clean signature
|
||||
clean = sig.strip().rstrip(';')
|
||||
if clean and 'namespace' not in clean and clean != '};':
|
||||
# Truncate very long signatures
|
||||
if len(clean) > 200:
|
||||
# Find last complete parameter
|
||||
paren = clean.rfind(')')
|
||||
if paren > 50:
|
||||
clean = clean[:paren+1]
|
||||
md += f"```cpp\n{clean}\n```\n\n"
|
||||
|
||||
if params:
|
||||
md += "| 参数 | 说明 |\n|------|------|\n"
|
||||
for p in params:
|
||||
sp = p.split(' ', 1)
|
||||
name = sp[0]
|
||||
desc = sp[1] if len(sp) > 1 else ''
|
||||
md += f"| `{name}` | {desc} |\n"
|
||||
md += "\n"
|
||||
|
||||
if ret:
|
||||
md += f"**返回**: {ret}\n\n"
|
||||
|
||||
for n in notes:
|
||||
md += f"{n}\n\n"
|
||||
for w in warns:
|
||||
md += f"{w}\n\n"
|
||||
|
||||
if sees:
|
||||
md += f"**参见**: {', '.join(f'`{s}`' for s in sees)}\n\n"
|
||||
|
||||
return md
|
||||
|
||||
# Main
|
||||
all_headers = sorted(glob.glob('/ws/ViewDesignEngine/include/vde/**/*.h', recursive=True))
|
||||
modules = {}
|
||||
for h in all_headers:
|
||||
mod = h.split('/include/vde/')[1].split('/')[0]
|
||||
modules.setdefault(mod, []).append(h)
|
||||
|
||||
out = ['# ViewDesignEngine API 参考手册\n\n> 版本 3.2.0 | 76 个头文件 | 自动生成\n\n']
|
||||
|
||||
names = {
|
||||
'foundation':'基础模块','core':'核心几何','curves':'曲线曲面','mesh':'网格处理',
|
||||
'spatial':'空间索引','boolean':'布尔运算','collision':'碰撞检测','brep':'B-Rep建模',
|
||||
'sdf':'SDF隐式建模','sketch':'草图约束','capi':'C API'
|
||||
}
|
||||
|
||||
for mod in ['foundation','core','curves','mesh','spatial','boolean','collision','brep','sdf','sketch','capi']:
|
||||
if mod not in modules: continue
|
||||
out.append(f'## {names[mod]} (`vde::{mod}`)\n\n')
|
||||
|
||||
for h in sorted(modules[mod]):
|
||||
with open(h) as f: content = f.read()
|
||||
blocks = extract_blocks(content)
|
||||
if not blocks: continue
|
||||
|
||||
fname = os.path.basename(h)
|
||||
out.append(f'### {fname}\n\n')
|
||||
|
||||
for doc, sig in blocks:
|
||||
if len(doc) < 15 and not sig.strip(): continue
|
||||
md = to_md(doc, sig)
|
||||
if md.strip():
|
||||
out.append(md)
|
||||
out.append('\n')
|
||||
|
||||
result = '\n'.join(out)
|
||||
|
||||
# Remove empty ### headers
|
||||
result = re.sub(r'### \n\n+', '', result)
|
||||
|
||||
with open('/ws/ViewDesignEngine/docs/API-REFERENCE.md', 'w') as f:
|
||||
f.write(result)
|
||||
|
||||
print(f'Written: {len(result)} chars, {result.count(chr(10))} lines')
|
||||
Reference in New Issue
Block a user