Merge branch 'master' of https://github.com/Tiogaplanet/MPU_Pro_Mini_lib #36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Doxygen API Docs to GitHub Wiki | |
| on: | |
| push: | |
| branches: | |
| - master | |
| - minor-edits | |
| - preflight-checks | |
| jobs: | |
| deploy-wiki: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v4 | |
| - name: Install Doxygen | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y doxygen | |
| - name: Generate Doxygen XML | |
| run: | | |
| # Generate default Doxyfile if not present | |
| if [ ! -f Doxyfile ]; then | |
| doxygen -g | |
| fi | |
| # Configure Doxyfile settings for XML generation and exclude internal namespaces | |
| sed -i 's/GENERATE_XML = NO/GENERATE_XML = YES/' Doxyfile | |
| sed -i 's/XML_OUTPUT = xml/XML_OUTPUT = xml/' Doxyfile | |
| sed -i 's/GENERATE_HTML = YES/GENERATE_HTML = NO/' Doxyfile | |
| sed -i 's/GENERATE_LATEX = YES/GENERATE_LATEX = NO/' Doxyfile | |
| sed -i 's/RECURSIVE = NO/RECURSIVE = YES/' Doxyfile | |
| sed -i 's/EXCLUDE_SYMBOLS =/EXCLUDE_SYMBOLS = mip_detail::*/' Doxyfile | |
| # Run Doxygen | |
| doxygen Doxyfile | |
| - name: Convert Doxygen XML to Sectioned Markdown | |
| run: | | |
| mkdir -p wiki_markdown | |
| python3 - << 'EOF' | |
| import xml.etree.ElementTree as ET | |
| import os | |
| xml_dir = 'xml' | |
| out_dir = 'wiki_markdown' | |
| index_path = os.path.join(xml_dir, 'index.xml') | |
| if not os.path.exists(index_path): | |
| print("No index.xml found.") | |
| exit(0) | |
| def get_text(element): | |
| """ Recursively extracts clean text from XML elements """ | |
| if element is None: | |
| return "" | |
| return "".join(element.itertext()).strip() | |
| def sanitize_cell(text): | |
| """ Strips newlines, collapses spaces, and escapes pipe characters for Markdown table cells """ | |
| if not text: | |
| return "" | |
| text = text.replace('\r', ' ').replace('\n', ' ').replace('|', '\\|') | |
| return " ".join(text.split()) | |
| tree = ET.parse(index_path) | |
| root = tree.getroot() | |
| class_pages = [] | |
| enum_pages = [] | |
| defines_list = [] | |
| seen_enums = set() | |
| seen_defines = set() | |
| # --------------------------------------------------------------------------- | |
| # 1. Parse Classes, Structs, Enums, and Preprocessor Defines | |
| # --------------------------------------------------------------------------- | |
| for compound in root.findall('compound'): | |
| kind = compound.get('kind') | |
| name = compound.find('name').text | |
| refid = compound.get('refid') | |
| # Classes & Structs | |
| if kind in ['class', 'struct'] and not name.startswith('mip_detail::'): | |
| clean_name = name.replace(':', '_').replace('/', '_').replace('.', '_') | |
| doc_filename = f"{clean_name}.md" | |
| class_pages.append((name, clean_name)) | |
| page = [f"# Class `{name}`\n\n"] | |
| detail_path = os.path.join(xml_dir, f"{refid}.xml") | |
| if os.path.exists(detail_path): | |
| d_tree = ET.parse(detail_path) | |
| d_root = d_tree.getroot() | |
| comp_def = d_root.find('compounddef') | |
| # --- Class @brief Description --- | |
| brief = get_text(comp_def.find('briefdescription')) | |
| if brief: | |
| page.append(f"{brief}\n\n") | |
| # --- Class @details Description --- | |
| deta = get_text(comp_def.find('detaileddescription')) | |
| if deta: | |
| page.append(f"## Detailed Description\n\n{deta}\n\n") | |
| # --- Categorize Public Members --- | |
| members = comp_def.findall('.//memberdef') | |
| public_members = [m for m in members if m.get('prot') == 'public'] | |
| constructors = [] | |
| functions = [] | |
| variables = [] | |
| for m in public_members: | |
| m_kind = m.get('kind') | |
| m_name = get_text(m.find('name')) | |
| if m_kind == 'function': | |
| if m_name == name or m_name == f"~{name}": | |
| constructors.append(m) | |
| else: | |
| functions.append(m) | |
| elif m_kind == 'variable': | |
| variables.append(m) | |
| elif m_kind == 'enum' and not m_name.startswith('mip_detail::') and m_name not in seen_enums: | |
| seen_enums.add(m_name) | |
| enum_pages.append((m_name, m_name, m)) | |
| # --- Table of Contents / Section Jump Links --- | |
| toc = ["## Table of Contents\n\n"] | |
| if constructors: | |
| toc.append("- [Constructors & Destructors](#constructors--destructors)\n") | |
| if functions: | |
| toc.append("- [Public Functions](#public-functions)\n") | |
| if variables: | |
| toc.append("- [Public Variables & Constants](#public-variables)\n") | |
| if constructors or functions or variables: | |
| page.append("".join(toc) + "\n---\n\n") | |
| # Helper function to format member elements | |
| def format_member(member): | |
| m_name = get_text(member.find('name')) | |
| m_type = get_text(member.find('type')) | |
| m_args = get_text(member.find('argsstring')) | |
| m_brief = get_text(member.find('briefdescription')) | |
| m_deta_el = member.find('detaileddescription') | |
| sig = f"`{m_type} {m_name}{m_args}`" if m_type else f"`{m_name}{m_args}`" | |
| m_block = [f"### `{m_name}`\n\n", f"**Signature:** {sig}\n\n"] | |
| if m_brief: | |
| m_block.append(f"**Summary:** {m_brief}\n\n") | |
| if m_deta_el is not None: | |
| para_text = [] | |
| for para in m_deta_el.findall('para'): | |
| p_txt = get_text(para) | |
| if p_txt and not para.find('parameterlist') and not para.find('simplesect'): | |
| para_text.append(p_txt) | |
| if para_text: | |
| m_block.append("**Details:**\n\n" + "\n\n".join(para_text) + "\n\n") | |
| param_items = member.findall('.//parameteritem') | |
| if param_items: | |
| m_block.append("**Parameters:**\n\n") | |
| m_block.append("| Parameter | Description |\n|:---|:---|\n") | |
| for p_item in param_items: | |
| p_name = sanitize_cell(get_text(p_item.find('.//parametername'))) | |
| p_desc = sanitize_cell(get_text(p_item.find('.//parameterdescription'))) | |
| m_block.append(f"| `{p_name}` | {p_desc} |\n") | |
| m_block.append("\n") | |
| returns = member.findall(".//simplesect[@kind='return']") | |
| if returns: | |
| ret_desc = get_text(returns[0]) | |
| if ret_desc: | |
| m_block.append(f"**Returns:** {ret_desc}\n\n") | |
| m_block.append("---\n\n") | |
| return "".join(m_block) | |
| # --- Section 1: Constructors & Destructors --- | |
| if constructors: | |
| page.append("## Constructors & Destructors\n\n") | |
| for c in constructors: | |
| page.append(format_member(c)) | |
| # --- Section 2: Public Functions --- | |
| if functions: | |
| page.append("## Public Functions\n\n") | |
| for f in functions: | |
| page.append(format_member(f)) | |
| # --- Section 3: Public Variables & Constants --- | |
| if variables: | |
| page.append("## Public Variables\n\n") | |
| for v in variables: | |
| page.append(format_member(v)) | |
| with open(os.path.join(out_dir, doc_filename), 'w', encoding='utf-8') as f: | |
| f.write("".join(page)) | |
| # Header File Compounds (contain global enums and #defines) | |
| elif kind == 'file': | |
| detail_path = os.path.join(xml_dir, f"{refid}.xml") | |
| if os.path.exists(detail_path): | |
| f_tree = ET.parse(detail_path) | |
| f_root = f_tree.getroot() | |
| # Enums | |
| for enum_def in f_root.findall('.//memberdef[@kind="enum"]'): | |
| e_name = get_text(enum_def.find('name')) | |
| if e_name and not e_name.startswith('mip_detail::') and e_name not in seen_enums: | |
| seen_enums.add(e_name) | |
| enum_pages.append((e_name, e_name, enum_def)) | |
| # Preprocessor Defines (#define) | |
| for def_member in f_root.findall('.//memberdef[@kind="define"]'): | |
| d_name = sanitize_cell(get_text(def_member.find('name'))) | |
| d_init = sanitize_cell(get_text(def_member.find('initializer'))) | |
| d_brief = sanitize_cell(get_text(def_member.find('briefdescription'))) | |
| d_deta = sanitize_cell(get_text(def_member.find('detaileddescription'))) | |
| desc = d_brief if d_brief else d_deta | |
| if d_name and not d_name.startswith('mip_detail::') and d_name not in seen_defines: | |
| seen_defines.add(d_name) | |
| defines_list.append((d_name, d_init, desc)) | |
| # --------------------------------------------------------------------------- | |
| # 2. Format & Write Dedicated Enum Pages | |
| # --------------------------------------------------------------------------- | |
| for orig_name, clean_name, enum_def in enum_pages: | |
| doc_filename = f"{clean_name}.md" | |
| brief = get_text(enum_def.find('briefdescription')) | |
| deta_el = enum_def.find('detaileddescription') | |
| page = [f"# Enum `{orig_name}`\n\n"] | |
| if brief: | |
| page.append(f"{brief}\n\n") | |
| if deta_el is not None: | |
| para_text = [] | |
| for para in deta_el.findall('para'): | |
| p_txt = get_text(para) | |
| if p_txt and not para.find('parameterlist') and not para.find('simplesect'): | |
| para_text.append(p_txt) | |
| if para_text: | |
| page.append("## Detailed Description\n\n" + "\n\n".join(para_text) + "\n\n") | |
| enum_values = enum_def.findall('enumvalue') | |
| if enum_values: | |
| page.append("## Enumerator Values\n\n") | |
| page.append("| Enumerator | Value | Description |\n|:---|:---|:---|\n") | |
| for ev in enum_values: | |
| ev_name = sanitize_cell(get_text(ev.find('name'))) | |
| ev_init = sanitize_cell(get_text(ev.find('initializer'))) | |
| ev_brief = get_text(ev.find('briefdescription')) | |
| ev_deta = get_text(ev.find('detaileddescription')) | |
| desc = sanitize_cell(ev_brief if ev_brief else ev_deta) | |
| val_str = f"`{ev_init}`" if ev_init else "" | |
| page.append(f"| `{ev_name}` | {val_str} | {desc} |\n") | |
| page.append("\n") | |
| with open(os.path.join(out_dir, doc_filename), 'w', encoding='utf-8') as f: | |
| f.write("".join(page)) | |
| # --------------------------------------------------------------------------- | |
| # 3. Write Constants-and-Defines.md | |
| # --------------------------------------------------------------------------- | |
| if defines_list: | |
| with open(os.path.join(out_dir, 'Constants-and-Defines.md'), 'w', encoding='utf-8') as f: | |
| f.write("# Constants and Defines\n\nGlobal preprocessor macros and defines:\n\n") | |
| f.write("| Macro / Define | Value | Description |\n|:---|:---|:---|\n") | |
| for d_name, d_init, d_desc in sorted(defines_list, key=lambda x: x[0]): | |
| val_str = f"`{d_init}`" if d_init else "" | |
| f.write(f"| `{d_name}` | {val_str} | {d_desc} |\n") | |
| f.write("\n") | |
| # --------------------------------------------------------------------------- | |
| # 4. Generate API-Reference.md Index | |
| # --------------------------------------------------------------------------- | |
| with open(os.path.join(out_dir, 'API-Reference.md'), 'w', encoding='utf-8') as f: | |
| f.write("# API Reference Index\n\nList of all public library classes, enumerations, and constants:\n\n") | |
| f.write("## Classes\n\n") | |
| for orig_name, clean_name in sorted(class_pages, key=lambda x: x[0]): | |
| f.write(f"- **[{orig_name}]({clean_name})**\n") | |
| if enum_pages: | |
| f.write("\n## Enumerations\n\n") | |
| for orig_name, clean_name, _ in sorted(enum_pages, key=lambda x: x[0]): | |
| f.write(f"- **[{orig_name}]({clean_name})**\n") | |
| if defines_list: | |
| f.write("\n## Constants and Defines\n\n") | |
| f.write("- **[Constants and Defines](Constants-and-Defines)**\n") | |
| # --------------------------------------------------------------------------- | |
| # 5. Generate _Sidebar.md Navigation | |
| # --------------------------------------------------------------------------- | |
| with open(os.path.join(out_dir, '_Sidebar.md'), 'w', encoding='utf-8') as f: | |
| f.write("### Navigation\n\n") | |
| f.write("- [Home](Home)\n") | |
| f.write("- [API Reference](API-Reference)\n\n") | |
| f.write("#### Classes\n\n") | |
| for orig_name, clean_name in sorted(class_pages, key=lambda x: x[0]): | |
| f.write(f"- [{orig_name}]({clean_name})\n") | |
| if enum_pages: | |
| f.write("\n#### Enumerations\n\n") | |
| for orig_name, clean_name, _ in sorted(enum_pages, key=lambda x: x[0]): | |
| f.write(f"- [{orig_name}]({clean_name})\n") | |
| if defines_list: | |
| f.write("\n#### Constants\n\n") | |
| f.write("- [Constants and Defines](Constants-and-Defines)\n") | |
| print("Generated Class, Enum, and Constants Markdown pages, API-Reference.md, and _Sidebar.md") | |
| EOF | |
| - name: Checkout Wiki Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.repository }}.wiki | |
| path: wiki_repo | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Purge Old Pages and Deploy Fresh Wiki | |
| run: | | |
| cd wiki_repo | |
| # PURGE STEP: Remove generated pages, but keep Home.md, _Footer.md, and .git | |
| find . -maxdepth 1 ! -name '.' ! -name '..' ! -name '.git' \ | |
| ! -name 'Home.md' ! -name '_Footer.md' -exec rm -rf {} + | |
| cd .. | |
| # Copy freshly generated markdown files into wiki_repo without overwriting | |
| # custom Home.md or _Footer.md | |
| rsync -av --exclude='Home.md' --exclude='_Footer.md' wiki_markdown/ wiki_repo/ | |
| # Initialize Home.md if it does not exist in the wiki repository | |
| if [ ! -f wiki_repo/Home.md ]; then | |
| echo "# Welcome to the Library Wiki" > wiki_repo/Home.md | |
| echo "See the [API Reference](API-Reference) for detailed class documentation." >> wiki_repo/Home.md | |
| fi | |
| cd wiki_repo | |
| git config user.name "Tiogaplanet" | |
| git config user.email "tiogaplanet@users.noreply.github.com" | |
| # Stage all deletions and new files | |
| git add -A | |
| if ! git diff-index --quiet HEAD; then | |
| git commit -m "Fresh deployment of API documentation from Doxygen" | |
| git push | |
| else | |
| echo "No changes to commit to Wiki." | |
| fi |