mirror of
https://github.com/azerothcore/mod-ale
synced 2025-11-29 15:38:17 +08:00
Add documentation generator.
This commit is contained in:
242
docs/ElunaDoc/parser.py
Normal file
242
docs/ElunaDoc/parser.py
Normal file
@@ -0,0 +1,242 @@
|
||||
import re
|
||||
from types import FileType
|
||||
import markdown
|
||||
from typedecorator import params, returns, Nullable
|
||||
|
||||
|
||||
class ParameterDoc(object):
|
||||
"""The documentation data of a parameter or return value for an Eluna method."""
|
||||
|
||||
# The integer ranges that each C++ type is valid for. None means valid for all numbers.
|
||||
valid_ranges = {
|
||||
'float': None,
|
||||
'double': None,
|
||||
'int': ('-2,147,483,647', '2,147,483,647'), # This should be -32767..32767, but it's pretty safe to assume 32-bit.
|
||||
'int8': ('-127', '127'),
|
||||
'uint8': ('0', '255'),
|
||||
'int16': ('-32,767', '32,767'),
|
||||
'uint16': ('0', '65,535'),
|
||||
'int32': ('-2,147,483,647', '2,147,483,647'),
|
||||
'uint32': ('0', '4,294,967,295'),
|
||||
}
|
||||
|
||||
@params(self=object, name=unicode, data_type=str, description=unicode, default_value=Nullable(unicode))
|
||||
def __init__(self, name, data_type, description, default_value=None):
|
||||
"""If `name` is not provided, the Parameter is a returned value instead of a parameter."""
|
||||
self.name = name
|
||||
self.data_type = data_type
|
||||
self.default_value = default_value
|
||||
|
||||
if description:
|
||||
# Capitalize the first letter, add a period, and parse as Markdown.
|
||||
self.description = '{}{}. '.format(description[0].capitalize(), description[1:])
|
||||
self.description = markdown.markdown(self.description)
|
||||
else:
|
||||
self.description = ''
|
||||
|
||||
# If the data type is a C++ number, convert to Lua number and add range info to description.
|
||||
if self.data_type in ['float', 'double', 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32']:
|
||||
range = ParameterDoc.valid_ranges[self.data_type]
|
||||
if range:
|
||||
self.description += '<p><em>Valid numbers</em>: integers from {0} to {1}.</p>'.format(range[0], range[1])
|
||||
else:
|
||||
self.description += '<p><em>Valid numbers</em>: all decimal numbers.</p>'
|
||||
|
||||
self.data_type = 'number'
|
||||
|
||||
|
||||
class MethodDoc(object):
|
||||
"""The documentation data of an Eluna method."""
|
||||
@params(self=object, name=unicode, description=unicode, parameters=[ParameterDoc], returned=[ParameterDoc])
|
||||
def __init__(self, name, description, parameters, returned):
|
||||
self.name = name
|
||||
self.parameters = parameters
|
||||
self.returned = returned
|
||||
|
||||
# Parse the description as Markdown.
|
||||
self.description = markdown.markdown(description)
|
||||
# Pull the first paragraph out of the description as the short description.
|
||||
self.short_description = self.description.split('</p>')[0][3:]
|
||||
# If it has a description, it is "documented".
|
||||
self.documented = self.description != ''
|
||||
|
||||
|
||||
class MangosClassDoc(object):
|
||||
"""The documentation of a MaNGOS class that has Lua methods."""
|
||||
@params(self=object, name=unicode, description=unicode, methods=[MethodDoc])
|
||||
def __init__(self, name, description, methods):
|
||||
self.name = name
|
||||
# Parse the description as Markdown.
|
||||
self.description = markdown.markdown(description)
|
||||
# Pull the first paragraph out of the description as the short description.
|
||||
self.short_description = self.description.split('</p>')[0][3:]
|
||||
# Sort the methods by their names.
|
||||
self.methods = sorted(methods, key=lambda m: m.name)
|
||||
|
||||
# If any of our methods are not documented, we aren't fully documented.
|
||||
for method in methods:
|
||||
if not method.documented:
|
||||
self.fully_documented = False
|
||||
break
|
||||
else:
|
||||
self.fully_documented = True
|
||||
|
||||
# In the same vein, if any of our methods are documented, we aren't fully *un*documented.
|
||||
for method in methods:
|
||||
if method.documented:
|
||||
self.fully_undocumented = False
|
||||
break
|
||||
else:
|
||||
self.fully_undocumented = True
|
||||
|
||||
|
||||
class ClassParser(object):
|
||||
"""Parses a file line-by-line and returns methods when enough information is received to build them."""
|
||||
|
||||
# Various regular expressions to parse different parts of the doc string.
|
||||
# There are used to parse the class's description.
|
||||
class_start_regex = re.compile(r"\s*/\*\*\*") # The start of class documentation, i.e. /***
|
||||
class_body_regex = re.compile(r"\s*\*\s*(.*)") # The "body", i.e. a * and optionally some descriptive text.
|
||||
class_end_regex = re.compile(r"\s*\*/") # The end of the comment portion, i.e. */
|
||||
|
||||
# These are used to parse method documentation.
|
||||
start_regex = re.compile(r"\s*/\*\*") # The start of documentation, i.e. /**
|
||||
body_regex = re.compile(r"\s*\s?\*\s*(.*)") # The "body", i.e. a * and optionally some descriptive text.
|
||||
# An extra optional space (\s?) was thrown in to make it different from `class_body_regex`.
|
||||
|
||||
param_regex = re.compile(r"""\s*\*\s@param\s # The @param tag starts with opt. whitespace followed by "* @param ".
|
||||
([&\w]+)\s(\w+) # The data type, a space, and the name of the param.
|
||||
(?:\s=\s(.+))? # The default value: a = surrounded by spaces, followed by text.
|
||||
(?:\s:\s(.+))? # The description: a colon surrounded by spaces, followed by text.""",
|
||||
re.X)
|
||||
|
||||
# This is the same as the @param tag, minus the default value part.
|
||||
return_regex = re.compile(r"""\s*\*\s@return\s
|
||||
([&\w]+)\s(\w+)
|
||||
(?:\s:\s(.+))?""", re.X)
|
||||
|
||||
comment_end_regex = re.compile(r"\s*\*/") # The end of the comment portion, i.e. */
|
||||
end_regex = re.compile(r"\s*int\s(\w+)\s*\(") # The end of the documentation, i.e. int MethodName(
|
||||
|
||||
def __init__(self, class_name):
|
||||
assert ClassParser.class_body_regex is not ClassParser.body_regex
|
||||
# The methods that have been parsed.
|
||||
self.methods = []
|
||||
# The name of the class being parsed.
|
||||
self.class_name = class_name
|
||||
# The description of the class being parsed.
|
||||
self.class_description = ''
|
||||
# Reset the parser's state machine.
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
# What the last handled regex was, to determine what the next should be.
|
||||
self.last_regex = None
|
||||
|
||||
# These are used to piece together the next `Method`.
|
||||
self.description = ''
|
||||
self.params = []
|
||||
self.returned = []
|
||||
self.method_name = None
|
||||
|
||||
def handle_class_body(self, match):
|
||||
text = match.group(1)
|
||||
self.class_description += text + '\n'
|
||||
|
||||
def handle_body(self, match):
|
||||
text = match.group(1)
|
||||
self.description += text + '\n'
|
||||
|
||||
def handle_param(self, match):
|
||||
data_type, name, default, description = match.group(1), match.group(2), match.group(3), match.group(4)
|
||||
self.params.append(ParameterDoc(name, data_type, description, default))
|
||||
|
||||
def handle_return(self, match):
|
||||
data_type, name, description = match.group(1), match.group(2), match.group(3)
|
||||
self.returned.append(ParameterDoc(name, data_type, description))
|
||||
|
||||
def handle_end(self, match):
|
||||
self.method_name = match.group(1)
|
||||
self.methods.append(MethodDoc(self.method_name, self.description, self.params, self.returned))
|
||||
|
||||
# Table of which handler is used to handle each regular expressions.
|
||||
regex_handlers = {
|
||||
class_start_regex: None,
|
||||
class_body_regex: handle_class_body,
|
||||
class_end_regex: None,
|
||||
start_regex: None,
|
||||
body_regex: handle_body,
|
||||
param_regex: handle_param,
|
||||
return_regex: handle_return,
|
||||
comment_end_regex: None,
|
||||
end_regex: handle_end,
|
||||
}
|
||||
|
||||
# Table of which regular expressions can follow the last handled regex.
|
||||
# `doc_body_regex` must always come LAST when used, since it also matches param, return, and comment_end.
|
||||
next_regexes = {
|
||||
None: [class_start_regex, start_regex, end_regex],
|
||||
class_start_regex: [class_end_regex, class_body_regex],
|
||||
class_body_regex: [class_end_regex, class_body_regex],
|
||||
class_end_regex: [],
|
||||
start_regex: [param_regex, return_regex, comment_end_regex, body_regex],
|
||||
body_regex: [param_regex, return_regex, comment_end_regex, body_regex],
|
||||
param_regex: [param_regex, return_regex, comment_end_regex],
|
||||
return_regex: [return_regex, comment_end_regex],
|
||||
comment_end_regex: [end_regex],
|
||||
end_regex: [],
|
||||
}
|
||||
|
||||
@returns(Nullable(MethodDoc))
|
||||
@params(self=object, line=str)
|
||||
def next_line(self, line):
|
||||
"""Parse the next line of the file.
|
||||
|
||||
This method returns a `Method` when enough data to form a `Method` has been parsed.
|
||||
Otherwise, it returns None.
|
||||
"""
|
||||
# Get the list of expected regular expressions using the last one handled.
|
||||
valid_regexes = self.next_regexes[self.last_regex]
|
||||
|
||||
# Try to find a match.
|
||||
for regex in valid_regexes:
|
||||
match = regex.match(line)
|
||||
|
||||
if match:
|
||||
handler = self.regex_handlers[regex]
|
||||
|
||||
if handler:
|
||||
handler(self, match)
|
||||
|
||||
# Not every regex has a handler, but keep track of where we are anyway.
|
||||
self.last_regex = regex
|
||||
# Break at the first match.
|
||||
break
|
||||
else:
|
||||
# No valid regex was found, reset everything.
|
||||
self.reset()
|
||||
|
||||
@returns(MangosClassDoc)
|
||||
def to_class_doc(self):
|
||||
"""Create an instance of `MangosClassDoc` from the parser's data.
|
||||
|
||||
Is called by `parse_file` once parsing is finished.
|
||||
"""
|
||||
return MangosClassDoc(self.class_name, self.class_description, self.methods)
|
||||
|
||||
@staticmethod
|
||||
@returns(MangosClassDoc)
|
||||
@params(file=FileType)
|
||||
def parse_file(file):
|
||||
"""Parse the file `file` into a documented class."""
|
||||
# Get the class name from "ClassMethods.h" by stripping off "Methods.h".
|
||||
class_name = file.name[:-len('Methods.h')]
|
||||
parser = ClassParser(class_name)
|
||||
|
||||
line = file.readline()
|
||||
|
||||
while line:
|
||||
parser.next_line(line)
|
||||
line = file.readline()
|
||||
|
||||
return parser.to_class_doc()
|
||||
Reference in New Issue
Block a user