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
298 lines
12 KiB
Python
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())
|