#!/bin/python3 # Copyright (C) 2024 olang mantainers # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import re from enum import Enum class Field: def __init__(self, kind, name, type, description): self.kind = kind self.name = name self.type = type self.description = description NODE_KIND = Enum('NODE_KIND', ['FUNCTION', 'TYPEDEF_STRUCT', 'TYPEDEF_UNION', 'MACRO']) class DocString: def __init__(self, comment_lines, code_node): self.comment_lines = comment_lines self.code_node = code_node def get_name(self): return self.code_node.get_name() def get_title(self): return self.comment_lines[0] def get_description(self): description = "" for line in self.comment_lines[2:]: if line.startswith('@'): break elif len(description) > 0 and description[-1] != '\n': description += ' ' description += line + '\n' return description.strip() def get_fields(self): name = "" description = "" fields_map = {} for line in self.comment_lines[2:]: if line.startswith('@'): if name: fields_map[name] = description name = line[1:].split(':')[0] description = ":".join(line.split(':')[1:]) elif name: description += ' ' + line if name: fields_map[name] = description for field in self.code_node.get_fields(): if field.name in fields_map: field.description = fields_map.get(field.name) yield field class MacroNode: def __init__(self, lines): self.lines = lines self.kind = NODE_KIND.MACRO def get_name(self): return self.lines[0].split(' ')[1] def get_fields(self): return [] class TypedefNode: def __init__(self, lines, kind): self.lines = lines self.kind = kind def get_name(self): return self.lines[-1].split(' ')[1].replace(';', '') def get_fields(self): description = None for line in self.lines[2:-1]: line = line.strip() if line.startswith('/*'): description = line.replace('/* ', '').replace('*/', '') continue # FIXME: handle anonymous struct/inline field_name = line.split(' ')[-1].replace(';', '') field_type = ' '.join(line.split(' ')[:-1]) yield Field('field', field_name, field_type, description) description = None return [] class FunctionNode: def __init__(self, lines): self.lines = lines self.kind = NODE_KIND.FUNCTION def get_ret_type(self): return self.lines[0] def get_name(self): return self.lines[1].split('(')[0] def get_fields(self): all_lines = "".join(self.lines) args = all_lines.split('(')[1].split(')')[0].strip() for arg in re.finditer("(((?P((struct|union|enum) *)*([\\w]+ *\\**)) *(?P[\\w]+))[, ]*)", args): yield Field('arg', arg.group('arg_name'), arg.group('arg_type').strip(), None) yield Field('return', 'return', self.get_ret_type(), None) def get_code_node(lines): assert(len(lines) > 0) if lines[0].startswith("#define"): return MacroNode(lines) if lines[0].startswith("typedef union"): return TypedefNode(lines, NODE_KIND.TYPEDEF_UNION) if lines[0].startswith("typedef struct"): return TypedefNode(lines, NODE_KIND.TYPEDEF_STRUCT) if lines[0].startswith("typedef enum"): return TypedefNode(lines, NODE_KIND.TYPEDEF_STRUCT) return FunctionNode(lines) def extract_comment(lines, index): if not lines[index].startswith("/**"): return None, index comment_lines = [] for line in lines[index + 1:]: if line.strip().startswith("*/"): break comment_lines.append(line.strip()[2:]) index += 1 return comment_lines, index def extract_code(lines, index): """ This is highly tied to olang's code style and require the curly brackets to be open into a new line. """ code_lines = [] is_block = False for line in lines[index + 1:]: code_lines.append(line) if is_block: if len(line) == 0: continue if line[0] == '}': break; if len(line) == 0: break if line[0] == '{': is_block = True index += 1 return code_lines, index def extract_docstring(file): lines = file.split('\n') index = 0 while index < len(lines): comment_lines, index = extract_comment(lines, index) if comment_lines: index += 1 code_lines, index = extract_code(lines, index) yield DocString(comment_lines, get_code_node(code_lines)) index += 1 def generate_docs(file_name): with open(file_name) as file: for docstring in extract_docstring(file.read()): node = docstring.code_node if node.kind == NODE_KIND.FUNCTION: print(f".TH {docstring.get_name()} 3 {docstring.get_name()} 1988-12-31 \"Olang Hacker's manual\"") print(f".SH NAME") print(f"{docstring.get_name()} \\- {docstring.get_title()}") print(f".SH SYNOPSIS") print(f".nf") print(f".B #include <{sys.argv[1].replace('include/', '')}>") print(f".P") fields = list(node.get_fields())[:-1] paren = f"{node.get_ret_type()} {docstring.get_name()}(" paren_len = len(paren) post = "," for index, field in enumerate(fields): if index == len(fields) - 1: post = ");" print(f".BI \"{paren}{field.type} \" {field.name} {post}") paren = " " * paren_len print(f".fi") print(f".SH DESCRIPTION") print(f"{docstring.get_description()}") if __name__ == "__main__": generate_docs(sys.argv[1])