Files
LithosAnanake/scripts/doxygen-xml-to-asciidoc.py
T

443 lines
14 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Convert Doxygen XML output to individual AsciiDoc files (flat structure).
This script parses Doxygen's XML output and generates one AsciiDoc file per
source file, placing all output files in a flat directory structure.
Usage:
python3 doxygen-xml-to-asciidoc.py <xml_input_dir> <asciidoc_output_dir>
"""
# StarForth — Steady-State Virtual Machine Runtime
#
# Copyright (c) 20232025 Robert A. James
# All rights reserved.
#
# This file is part of the StarForth project.
#
# Licensed under the StarForth License, Version 1.0 (the "License");
# you may not use this file except in compliance with the License.
#
# You may obtain a copy of the License at:
# https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
#
# This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
# express or implied, including but not limited to the warranties of
# merchantability, fitness for a particular purpose, and noninfringement.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
# StarForth — Steady-State Virtual Machine Runtime
# Copyright (c) 20232025 Robert A. James
# All rights reserved.
#
# This file is part of the StarForth project.
#
# Licensed under the StarForth License, Version 1.0 (the "License");
# you may not use this file except in compliance with the License.
#
# You may obtain a copy of the License at:
# https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
#
# This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
# express or implied, including but not limited to the warranties of
# merchantability, fitness for a particular purpose, and noninfringement.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
# *** StarForth ***
#
# doxygen-xml-to-asciidoc.py- FORTH-79 Standard and ANSI C99 ONLY
# Modified by - rajames
# Last modified - 2025-10-26T16:05:12.167-04
#
#
#
# See <http://creativecommons.org/publicdomain/zero/1.0/> for more information.
#
# /home/rajames/CLionProjects/StarForth/scripts/doxygen-xml-to-asciidoc.py
# *** StarForth ***
#
# doxygen-xml-to-asciidoc.py- FORTH-79 Standard and ANSI C99 ONLY
# Modified by - rajames
# Last modified - 2025-10-26T15:51:32.242-04
#
#
#
# See <http://creativecommons.org/publicdomain/zero/1.0/> for more information.
#
# /home/rajames/CLionProjects/StarForth/scripts/doxygen-xml-to-asciidoc.py
import sys
import os
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, List, Optional, Set
class DoxygenXMLParser:
"""Parse Doxygen XML output and extract documentation."""
def __init__(self, xml_dir: str):
"""Initialize parser with Doxygen XML directory."""
self.xml_dir = Path(xml_dir)
self.index_xml = self.xml_dir / "index.xml"
if not self.index_xml.exists():
raise FileNotFoundError(f"Doxygen index.xml not found at {self.index_xml}")
self.tree = ET.parse(str(self.index_xml))
self.root = self.tree.getroot()
def get_source_files(self) -> List[Dict]:
"""Extract all source files from Doxygen index."""
files = []
# Find all file compounds in the index
for compound in self.root.findall(".//compound[@kind='file']"):
file_info = {
'refid': compound.get('refid'),
'name': compound.findtext('name', ''),
'path': compound.findtext('path', ''),
'language': compound.get('language', 'c'),
}
# Only include C/H files
if file_info['name'].endswith(('.c', '.h', '.asm')):
files.append(file_info)
return sorted(files, key=lambda x: x['name'])
def extract_file_documentation(self, refid: str) -> Optional[str]:
"""Extract documentation for a specific file by refid."""
file_xml = self.xml_dir / f"{refid}.xml"
if not file_xml.exists():
return None
try:
tree = ET.parse(str(file_xml))
root = tree.getroot()
# Find the compounddef element
compounddef = root.find(".//compounddef")
if compounddef is None:
return None
doc = []
# Extract detailed description
detailed = compounddef.find(".//detailedDescription")
if detailed is not None:
doc.append(self._extract_description(detailed))
# Extract function/variable documentation
doc.extend(self._extract_members(compounddef))
return "\n".join(filter(None, doc))
except Exception as e:
print(f"Error parsing {file_xml}: {e}", file=sys.stderr)
return None
def _extract_description(self, desc_elem) -> str:
"""Extract text from a description element."""
text_parts = []
for para in desc_elem.findall(".//para"):
para_text = self._extract_para_text(para)
if para_text:
text_parts.append(para_text)
return "\n".join(text_parts)
def _extract_para_text(self, para_elem) -> str:
"""Extract text content from a paragraph element."""
text = []
if para_elem.text:
text.append(para_elem.text)
for child in para_elem:
if child.tag == 'ref':
text.append(child.text or '')
if child.tail:
text.append(child.tail)
elif child.tag == 'emphasis':
text.append(f"_{child.text or ''}_")
if child.tail:
text.append(child.tail)
elif child.tag == 'bold':
text.append(f"*{child.text or ''}*")
if child.tail:
text.append(child.tail)
elif child.tag == 'computeroutput':
text.append(f"`{child.text or ''}`")
if child.tail:
text.append(child.tail)
else:
if child.text:
text.append(child.text)
if child.tail:
text.append(child.tail)
return "".join(text).strip()
def _extract_members(self, compounddef) -> List[str]:
"""Extract member (function/variable) documentation."""
members = []
sectiondef_list = compounddef.findall(".//sectiondef")
for sectiondef in sectiondef_list:
kind = sectiondef.get('kind', '')
# Create section header based on kind
section_header = self._get_section_header(kind)
if section_header:
members.append(f"\n== {section_header}\n")
for member in sectiondef.findall(".//memberdef"):
member_text = self._extract_member(member)
if member_text:
members.append(member_text)
return members
def _get_section_header(self, kind: str) -> Optional[str]:
"""Map Doxygen section kind to header text."""
headers = {
'public-type': 'Public Types',
'public-func': 'Public Functions',
'public-attrib': 'Public Attributes',
'protected-type': 'Protected Types',
'protected-func': 'Protected Functions',
'protected-attrib': 'Protected Attributes',
'private-type': 'Private Types',
'private-func': 'Private Functions',
'private-attrib': 'Private Attributes',
'user-defined': 'User Defined',
}
return headers.get(kind)
def _extract_member(self, memberdef) -> str:
"""Extract documentation for a single member."""
parts = []
name = memberdef.findtext('name', '')
kind = memberdef.get('kind', '')
if not name:
return ""
# Create member header
parts.append(f"=== {name}")
parts.append("")
# Add member type/signature info
if kind == 'function':
returns = memberdef.findtext(".//type", '')
params = memberdef.findall(".//param")
param_list = []
for param in params:
param_type = param.findtext('type', '')
param_name = param.findtext('declname', '')
if param_type and param_name:
param_list.append(f"{param_type} {param_name}")
if returns or param_list:
sig = f"{returns}(" if returns else "("
sig += ", ".join(param_list) + ")"
parts.append(f"[source,c]\n----\n{sig}\n----")
parts.append("")
# Add documentation
briefdesc = memberdef.find(".//briefDescription")
if briefdesc is not None:
brief_text = self._extract_description(briefdesc)
if brief_text:
parts.append(brief_text)
parts.append("")
detaileddesc = memberdef.find(".//detailedDescription")
if detaileddesc is not None:
detailed_text = self._extract_description(detaileddesc)
if detailed_text:
parts.append(detailed_text)
parts.append("")
return "\n".join(parts)
def generate_asciidoc_file(filename: str, title: str, content: str, all_files: Optional[List[str]] = None) -> str:
"""Generate AsciiDoc file content with header and content."""
doc = []
# Navigation breadcrumb
doc.append("xref:index.adoc[← Back to API Documentation Index]")
doc.append("")
doc.append(f"= {title}")
doc.append(":toc: left")
doc.append(":toc-title: Contents")
doc.append(":toclevels: 2")
doc.append("")
doc.append(f"*File:* `{title}`")
doc.append("")
# Find related files (header for source, source for header)
related = []
if filename.endswith('.c'):
header_file = filename.replace('.c', '.h') + '.adoc'
if all_files and header_file in all_files:
related.append(f"xref:{header_file}[Header file ({filename.replace('.c', '.h')})]")
elif filename.endswith('.h'):
source_file = filename.replace('.h', '.c') + '.adoc'
if all_files and source_file in all_files:
related.append(f"xref:{source_file}[Implementation file ({filename.replace('.h', '.c')})]")
if related:
doc.append("[TIP]")
doc.append("====")
doc.append("*Related files:*")
doc.append("")
for rel in related:
doc.append(f"* {rel}")
doc.append("====")
doc.append("")
doc.append("[NOTE]")
doc.append("====")
doc.append("Auto-generated from Doxygen documentation.")
doc.append("====")
doc.append("")
if content:
doc.append(content)
else:
doc.append("_No documentation available for this file._")
# Navigation footer
doc.append("")
doc.append("---")
doc.append("")
doc.append("xref:index.adoc[← Back to API Documentation Index]")
return "\n".join(doc)
def main():
"""Main entry point."""
if len(sys.argv) != 3:
print("Usage: doxygen-xml-to-asciidoc.py <xml_input_dir> <asciidoc_output_dir>")
sys.exit(1)
xml_dir = sys.argv[1]
output_dir = sys.argv[2]
# Validate input directory
if not os.path.isdir(xml_dir):
print(f"Error: XML input directory not found: {xml_dir}", file=sys.stderr)
sys.exit(1)
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
try:
# Parse Doxygen XML
parser = DoxygenXMLParser(xml_dir)
files = parser.get_source_files()
if not files:
print("Warning: No source files found in Doxygen XML output", file=sys.stderr)
# Generate AsciiDoc files
generated_count = 0
index_entries = []
total_files = len(files)
# Build set of all output filenames for cross-referencing
all_output_files = {f['name'] + '.adoc' for f in files}
for idx, file_info in enumerate(files, 1):
filename = file_info['name']
refid = file_info['refid']
# Extract documentation
doc_content = parser.extract_file_documentation(refid)
# Generate AsciiDoc file
title = f"{filename}"
asciidoc_content = generate_asciidoc_file(filename, title, doc_content or "", all_output_files)
# Write output file (flat structure)
# Replace extension while preserving the source file type (e.g., vm.c.adoc, vm.h.adoc)
output_filename = filename + '.adoc'
output_path = Path(output_dir) / output_filename
with open(output_path, 'w') as f:
f.write(asciidoc_content)
generated_count += 1
index_entries.append({
'filename': output_filename,
'title': filename,
})
# Progress indicator
progress = (idx / total_files) * 100
print(f"[{idx}/{total_files}] {progress:5.1f}% - {output_filename}", file=sys.stderr)
# Generate index file
if index_entries:
index_content = generate_index(index_entries)
index_path = Path(output_dir) / "index.adoc"
with open(index_path, 'w') as f:
f.write(index_content)
print(f"Generated: index.adoc", file=sys.stderr)
print(f"Successfully generated {generated_count} AsciiDoc files", file=sys.stderr)
sys.exit(0)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def generate_index(entries: List[Dict]) -> str:
"""Generate an index AsciiDoc file."""
index = []
# Navigation breadcrumb
index.append("xref:../README.adoc[← Back to Documentation Index]")
index.append("")
index.append("= API Documentation Index")
index.append(":toc: left")
index.append(":toc-title: Contents")
index.append("")
index.append("Auto-generated from Doxygen documentation.")
index.append("")
index.append("== Source Files")
index.append("")
for entry in entries:
index.append(f"* xref:{entry['filename']}[{entry['title']}]")
# Navigation footer
index.append("")
index.append("---")
index.append("")
index.append("xref:../README.adoc[← Back to Documentation Index]")
return "\n".join(index)
if __name__ == "__main__":
main()