Files
inav/docs/development/msp/gen_enum_md.py
T
admin dac37fd077
Make sure docs are updated / settings_md (push) Canceled after 0s
Build firmware / test (push) Canceled after 0s
Build firmware / build-SITL-Windows (push) Canceled after 0s
Build firmware / build-SITL-Mac (push) Canceled after 0s
Build firmware / build-SITL-Linux (push) Canceled after 0s
Build firmware / build-SITL-Linux-arm64 (push) Canceled after 0s
Build firmware / upload-artifacts (push) Canceled after 0s
Build firmware / build-single-target (push) Canceled after 0s
Build firmware / build (9) (push) Canceled after 0s
Build firmware / build (8) (push) Canceled after 0s
Build firmware / build (7) (push) Canceled after 0s
Build firmware / build (6) (push) Canceled after 0s
Build firmware / build (5) (push) Canceled after 0s
Build firmware / build (4) (push) Canceled after 0s
Build firmware / build (3) (push) Canceled after 0s
Build firmware / build (2) (push) Canceled after 0s
Build firmware / build (14) (push) Canceled after 0s
Build firmware / build (13) (push) Canceled after 0s
Build firmware / build (12) (push) Canceled after 0s
Build firmware / build (11) (push) Canceled after 0s
Build firmware / build (10) (push) Canceled after 0s
Build firmware / build (1) (push) Canceled after 0s
Build firmware / build (0) (push) Canceled after 0s
Build firmware / detect (push) Canceled after 0s
Build pre-release / build (push) Canceled after 0s
Build pre-release / Release (push) Canceled after 0s
Sync inav to Gitea
2026-08-03 16:37:40 +08:00

298 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
enumdoc.py — Generate Markdown documentation from C enums (no expression eval).
Rules:
- One Value column only.
* If explicit assignment is a plain int literal (dec/hex/bin/oct) -> show that number.
* If explicit assignment is anything else -> show the raw expression text.
* If no assignment -> auto-increment.
- If auto-increment occurs inside an active preprocessor condition, wrap the number
in parentheses to indicate conditional numbering: e.g., 3, 4, #ifdef, (5), (6).
- Tracks nested #if/#ifdef/#ifndef/#elif/#else/#endif and shows Condition text.
- Handles multiline enumerators (split at the first top-level comma).
"""
import sys
import re
from pathlib import Path
from typing import List, Optional
import json
# ---------- Helpers ----------
BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
def strip_comments(s: str) -> str:
s = BLOCK_COMMENT_RE.sub('', s)
s = re.sub(r'//.*', '', s)
return s
def find_top_level_comma(s: str) -> int:
depth = 0
for i, ch in enumerate(s):
if ch == '(':
depth += 1
elif ch == ')':
depth = max(0, depth - 1)
elif ch == ',' and depth == 0:
return i
return -1
def is_plain_int_literal(expr: str) -> Optional[int]:
"""
Return int value if expr is a plain integer literal (dec/hex/bin/oct),
otherwise None. Whitespace ok; no unary ops/casts/suffixes.
"""
t = expr.strip()
if not t:
return None
if re.fullmatch(r'0[xX][0-9A-Fa-f]+', t) or \
re.fullmatch(r'0[bB][01]+', t) or \
re.fullmatch(r'0[0-7]*', t) or \
re.fullmatch(r'[1-9][0-9]*', t) or \
t == '0':
try:
return int(t, 0)
except Exception:
return None
return None
# ---------- Parsing regexes ----------
RE_ENUM_START = re.compile(r'^\s*typedef\s+enum(?:\s+[A-Za-z_]\w*)?\s*\{')
RE_ENUM_END = re.compile(r'^\s*\}\s*([A-Za-z_]\w*)\s*;')
RE_LINE_COMMENT = re.compile(r'^\s*//\s*(.+?)\s*$')
RE_IFDEF = re.compile(r'^\s*#\s*ifdef\s+(\w+)')
RE_IFNDEF = re.compile(r'^\s*#\s*ifndef\s+(\w+)')
RE_IF = re.compile(r'^\s*#\s*if\s+(.+)$')
RE_ELIF = re.compile(r'^\s*#\s*elif\s+(.+)$')
RE_ELSE = re.compile(r'^\s*#\s*else\s*$')
RE_ENDIF = re.compile(r'^\s*#\s*endif\b')
def normalize_condition_text(text: str) -> str:
t = text.strip()
t = re.sub(r'\bdefined\s*\(\s*(\w+)\s*\)', r'\1', t)
t = re.sub(r'\s+', ' ', t)
return t
class ConditionStack:
def __init__(self):
self.stack: List[str] = []
def push_ifdef(self, sym: str): self.stack.append(sym)
def push_ifndef(self, sym: str): self.stack.append(f'!{sym}')
def push_if(self, expr: str): self.stack.append(normalize_condition_text(expr))
def elif_(self, expr: str):
if self.stack: self.stack.pop()
self.stack.append(normalize_condition_text(expr))
def else_(self):
if not self.stack: return
top = self.stack.pop()
if top.startswith('!'): self.stack.append(top[1:])
elif top and all(ch.isalnum() or ch == '_' for ch in top): self.stack.append(f'!{top}')
else: self.stack.append(f'NOT({top})')
def endif(self):
if self.stack: self.stack.pop()
def current(self) -> str:
return " AND ".join(self.stack) if self.stack else ""
def has_active(self) -> bool:
return bool(self.stack)
# ---------- Model ----------
class EnumItem:
def __init__(self, name: str, value_display: str, cond: str):
self.name = name
self.value_display = value_display # number, (number), or raw expr string
self.cond = cond
class EnumDef:
def __init__(self, name: str, source_note: str):
self.name = name
self.source_note = source_note
self.items: List[EnumItem] = []
# ---------- Core parsing ----------
def parse_files(paths: List[Path]) -> List[EnumDef]:
enums: List[EnumDef] = []
outer_cond = ConditionStack()
for path in paths:
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
i = 0
recent_comment: Optional[str] = None
while i < len(lines):
line = lines[i]
# Track outer preproc
if m := RE_IFDEF.match(line): outer_cond.push_ifdef(m.group(1)); i += 1; continue
if m := RE_IFNDEF.match(line): outer_cond.push_ifndef(m.group(1)); i += 1; continue
if m := RE_IF.match(line): outer_cond.push_if(m.group(1)); i += 1; continue
if m := RE_ELIF.match(line): outer_cond.elif_(m.group(1)); i += 1; continue
if RE_ELSE.match(line): outer_cond.else_(); i += 1; continue
if RE_ENDIF.match(line): outer_cond.endif(); i += 1; continue
# Source comment directly above typedef
mcom = RE_LINE_COMMENT.match(line)
if mcom:
recent_comment = mcom.group(1)
if RE_ENUM_START.match(line):
source_note = recent_comment or str(path)
recent_comment = None
body_lines: List[str] = []
i += 1
local_i = i
while local_i < len(lines):
ln = lines[local_i]
if RE_ENUM_END.match(ln):
enum_name = RE_ENUM_END.match(ln).group(1)
enum = EnumDef(enum_name, source_note)
# second pass: parse enumerators
inner = ConditionStack()
current_numeric: Optional[int] = -1 # known numeric head; None means unknown
idx = 0
while idx < len(body_lines):
bl = body_lines[idx]
# inner preproc
if m := RE_IFDEF.match(bl): inner.push_ifdef(m.group(1)); idx += 1; continue
if m := RE_IFNDEF.match(bl): inner.push_ifndef(m.group(1)); idx += 1; continue
if m := RE_IF.match(bl): inner.push_if(m.group(1)); idx += 1; continue
if m := RE_ELIF.match(bl): inner.elif_(m.group(1)); idx += 1; continue
if RE_ELSE.match(bl): inner.else_(); idx += 1; continue
if RE_ENDIF.match(bl): inner.endif(); idx += 1; continue
# accumulate one item across lines
buf = [bl]
while True:
combined = strip_comments(" ".join(buf)).strip()
if not combined:
break
comma_pos = find_top_level_comma(combined)
if comma_pos != -1:
item_text = combined[:comma_pos].strip()
break
if idx + 1 >= len(body_lines):
item_text = combined
break
nxt = body_lines[idx + 1]
if RE_ENUM_END.match(nxt):
item_text = combined
break
idx += 1
buf.append(body_lines[idx])
if not combined:
idx += 1
continue
# NAME or NAME = expr
mitem = re.match(r'^\s*([A-Za-z_]\w*)\s*(?:=\s*(.*))?$', item_text)
if not mitem:
idx += 1
continue
name = mitem.group(1)
expr = (mitem.group(2) or "").strip()
# active condition text
cond_parts = [p for p in (outer_cond.current(), inner.current()) if p]
cond_text = " AND ".join(cond_parts)
# determine display value
if expr:
lit = is_plain_int_literal(expr)
if lit is not None:
# explicit numeric literal
value_display = str(lit)
current_numeric = lit
else:
# show raw expression; numeric chain becomes unknown
value_display = expr
current_numeric = None
else:
# auto-increment if we know a numeric head; else unknown
if current_numeric is None:
value_display = ""
else:
current_numeric += 1
if inner.has_active():
value_display = f"({current_numeric})"
else:
value_display = str(current_numeric)
enum.items.append(EnumItem(name=name, value_display=value_display, cond=cond_text))
idx += 1
enums.append(enum)
i = local_i + 1
break
else:
body_lines.append(lines[local_i])
local_i += 1
else:
i = local_i
continue
else:
i += 1
return enums
# ---------- Markdown rendering ----------
def render_markdown(enums: List[EnumDef]) -> str:
jsonfile = {}
out = []
out.append("# Enumerations\n")
out.append("**Auto-generated reference for MSP, refer to source for development, not this file, due to variations with #ifdefs which needs verification.**\n")
out.append("## Table of contents\n")
for e in sorted(enums, key=lambda x: x.name.lower()):
out.append(f"- [{e.name}](#enum-{e.name.lower()})")
out.append("")
for e in sorted(enums, key=lambda x: x.name.lower()):
jsonfile[e.name] = {}
out.append("---")
out.append(f"## <a id=\"enum-{e.name.lower()}\"></a>`{e.name}`\n")
if e.source_note:
out.append(f"> Source: {e.source_note}\n")
jsonfile[e.name]['_source'] = e.source_note
out.append("| Enumerator | Value | Condition |")
out.append("|---|---:|---|")
for it in e.items:
name_md = f"`{it.name}`"
val = it.value_display
cond = it.cond
out.append(f"| {name_md} | {val} | {cond} |")
jsonfile[e.name][name_md.strip('`')] = [val, cond] if len(cond)>0 else val
# normalize source to a stable inav/src/... path
if '_source' in jsonfile[e.name]:
jsonfile[e.name]['_source'] = jsonfile[e.name]['_source'].replace('../../../src', 'inav/src')
out.append("")
# While we're at it, chuck this all into a JSON file
Path("inav_enums.json").write_text(json.dumps(jsonfile,indent=4), encoding="utf-8")
return "\n".join(out)
# ---------- Main ----------
def main() -> int:
path = Path("all_enums.h")
if not path.exists():
print(f"Error: {path} not found", file=sys.stderr)
return 1
enums = parse_files([path])
md = render_markdown(enums)
Path("inav_enums_ref.md").write_text(md, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())