diff --git a/docs/gen_api_ref_v2.py b/docs/gen_api_ref_v2.py new file mode 100644 index 0000000..3d481f8 --- /dev/null +++ b/docs/gen_api_ref_v2.py @@ -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')