#!/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])