Skip to content

Commit e49859f

Browse files
committed
Rewrite parser and Register methods to use global keys for event IDs
It is preferred to use the abstraction of the global event key, in case event IDs change in the future
1 parent 8d3af20 commit e49859f

3 files changed

Lines changed: 717 additions & 602 deletions

File tree

docs/ElunaDoc/__main__.py

Lines changed: 169 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,223 @@
11
import os
22
import shutil
33
import typing
4-
from jinja2 import Environment, FileSystemLoader
5-
from typedecorator import params, returns
6-
from ElunaDoc.parser import ClassParser, MethodDoc
74
import glob
85
import time
6+
import re
7+
from jinja2 import Environment, FileSystemLoader
8+
from typedecorator import params, returns
9+
10+
from ElunaDoc.parser import ClassParser
911

1012

1113
@returns([(str, typing.IO)])
1214
@params(search_path=str)
1315
def find_class_files(search_path):
14-
"""Find and open all files containing Eluna class methods in `search_path`.
15-
16-
:param search_path: the path to search for Eluna methods in
17-
:return: a list of all files containing Eluna methods, and the name of their respective classes
18-
"""
19-
# Get the current working dir and switch to the search path.
16+
"""Find and open all files containing Eluna class methods in `search_path`."""
2017
old_dir = os.getcwd()
2118
os.chdir(search_path)
22-
# Search for all files ending in "Methods.h", and exclude BigIntMethods.h
23-
method_file_names = [file_name for file_name in glob.glob('*Methods.h') if file_name != 'BigIntMethods.h']
24-
# Open each file.
25-
method_files = [open(file_name, 'r') for file_name in method_file_names]
26-
# Go back to where we were before.
19+
method_file_names = [file_name for file_name in glob.glob("*Methods.h") if file_name != "BigIntMethods.h"]
20+
method_files = [open(file_name, "r", encoding="utf-8") for file_name in method_file_names]
2721
os.chdir(old_dir)
2822
return method_files
2923

3024

3125
def make_renderer(template_path, link_parser_factory):
3226
"""Return a function that can be used to render Jinja2 templates from the `template_path` directory."""
33-
34-
# Set up jinja2 environment to load templates from the templates folder.
3527
env = Environment(loader=FileSystemLoader(template_path))
3628

37-
3829
def inner(template_name, output_path, level, **kwargs):
39-
env.filters['parse_links'], env.filters['parse_data_type'] = link_parser_factory(level)
30+
env.filters["parse_links"], env.filters["parse_data_type"] = link_parser_factory(level)
4031
template = env.get_template(template_name)
4132
static = make_static(level)
4233
root = make_root(level)
4334

44-
with open('build/' + output_path, 'w') as out:
35+
with open("build/" + output_path, "w", encoding="utf-8") as out:
4536
out.write(template.render(level=level, static=static, root=root, **kwargs))
4637

4738
return inner
4839

4940

5041
def make_static(level):
51-
return lambda file_name: ('../' * level) + 'static/' + file_name
42+
return lambda file_name: ("../" * level) + "static/" + file_name
5243

5344

5445
def make_root(level):
55-
return lambda file_name: ('../' * level) + file_name
46+
return lambda file_name: ("../" * level) + file_name
47+
5648

49+
# ---------------- Hooks.h parsing (events.<category>.<name>) ----------------
5750

58-
if __name__ == '__main__':
51+
_macro_start = re.compile(r"^\s*#define\s+([A-Z0-9_]+)_EVENTS_LIST\(X\)\s*\\\s*$")
52+
_macro_item = re.compile(r'^\s*X\(\s*([A-Z0-9_]+)\s*,\s*([0-9]+)\s*,\s*"([^"]+)"\s*\)\s*\\?\s*$')
53+
54+
# Capture: { "category", SomeEventsTable, CountOf(SomeEventsTable) },
55+
_hook_table_entry = re.compile(
56+
r'^\s*\{\s*"([^"]+)"\s*,\s*([A-Za-z0-9_]+)\s*,\s*CountOf\(\2\)\s*\}\s*,?\s*$'
57+
)
58+
59+
60+
def _table_name_to_macro_key(events_table_name: str) -> str:
61+
"""
62+
Converts 'InstanceEventsTable' -> 'instance'
63+
'GameObjectEventsTable' -> 'gameobject'
64+
'PacketEventsTable' -> 'packet'
65+
"""
66+
suffix = "EventsTable"
67+
if events_table_name.endswith(suffix):
68+
return events_table_name[: -len(suffix)].lower()
69+
return events_table_name.lower()
70+
71+
72+
def parse_macro_lists(hooks_h_path: str) -> dict[str, dict[str, dict]]:
73+
"""
74+
Return mapping per macro_category (derived from *_EVENTS_LIST macro name):
75+
{
76+
"spell": {
77+
"by_id": { 1: "on_cast", ... },
78+
"by_enum": { "SPELL_EVENT_ON_CAST": (1, "on_cast"), ... }
79+
},
80+
...
81+
}
82+
"""
83+
hooks: dict[str, dict[str, dict]] = {}
84+
current: str | None = None
85+
86+
with open(hooks_h_path, "r", encoding="utf-8") as f:
87+
for line in f:
88+
m = _macro_start.match(line)
89+
if m:
90+
current = m.group(1).lower() # e.g. SPELL -> "spell", INSTANCE -> "instance"
91+
hooks.setdefault(current, {"by_id": {}, "by_enum": {}})
92+
continue
93+
94+
if current:
95+
mi = _macro_item.match(line)
96+
if mi:
97+
enum_name = mi.group(1)
98+
id_value = int(mi.group(2))
99+
lua_name = mi.group(3)
100+
hooks[current]["by_id"][id_value] = lua_name
101+
hooks[current]["by_enum"][enum_name] = (id_value, lua_name)
102+
continue
103+
104+
# End macro block when we hit a line not continuing with '\'
105+
if not line.rstrip().endswith("\\"):
106+
current = None
107+
108+
return hooks
109+
110+
111+
def build_exported_hook_map(hooks_h_path: str) -> dict[str, dict]:
112+
"""
113+
Builds a map keyed by exported Lua category from HookTypeTable.
114+
115+
IMPORTANT: the macro key is derived from the backing EventsTable name, not the exported category.
116+
Example:
117+
{ "map", InstanceEventsTable, CountOf(InstanceEventsTable) }
118+
-> macro key = "instance" -> INSTANCE_EVENTS_LIST
119+
"""
120+
macro_lists = parse_macro_lists(hooks_h_path)
121+
122+
exported: dict[str, dict] = {}
123+
in_hook_table = False
124+
125+
with open(hooks_h_path, "r", encoding="utf-8") as f:
126+
for line in f:
127+
if "static constexpr HookStorage HookTypeTable" in line:
128+
in_hook_table = True
129+
continue
130+
if in_hook_table and "};" in line:
131+
in_hook_table = False
132+
continue
133+
if not in_hook_table:
134+
continue
135+
136+
m = _hook_table_entry.match(line)
137+
if not m:
138+
continue
139+
140+
exported_category = m.group(1) # e.g. "map"
141+
events_table_name = m.group(2) # e.g. "InstanceEventsTable"
142+
143+
macro_key = _table_name_to_macro_key(events_table_name) # e.g. "instance"
144+
table_map = macro_lists.get(macro_key)
145+
146+
if not table_map:
147+
exported[exported_category] = {"by_id": {}, "by_enum": {}}
148+
print(
149+
f"[docs] Warning: HookTypeTable exports '{exported_category}' ({events_table_name}) "
150+
f"but no matching *_EVENTS_LIST was found for key '{macro_key}'"
151+
)
152+
continue
153+
154+
exported[exported_category] = table_map
155+
156+
return exported
157+
158+
159+
# ---------------- Main ----------------
160+
161+
if __name__ == "__main__":
59162
# Recreate the build folder and copy static files over.
60-
if os.path.exists('build'):
61-
shutil.rmtree('build')
62-
os.mkdir('build')
63-
shutil.copytree('ElunaDoc/static', 'build/static')
163+
if os.path.exists("build"):
164+
shutil.rmtree("build")
165+
os.mkdir("build")
166+
shutil.copytree("ElunaDoc/static", "build/static")
167+
168+
# Load hook globals (events.<category>.<name>) from Hooks.h
169+
hooks_path = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "hooks", "Hooks.h"))
170+
print(f"Loading hook globals from: {hooks_path}")
171+
exported_hooks = build_exported_hook_map(hooks_path)
64172

65173
# Load up all files with methods we need to parse.
66-
# Hard-coded to the TC files for now. Will have to add core support later on.
67-
print('Finding Eluna method files...')
68-
class_files = find_class_files('../methods/TrinityCore/')
174+
print("Finding Eluna method files...")
175+
class_files = find_class_files("../methods/TrinityCore/")
69176

70177
# Parse all the method files.
71178
classes = []
72179
for f in class_files:
73-
print(f'Parsing file {f.name}...')
74-
classes.append(ClassParser.parse_file(f))
180+
print(f"Parsing file {f.name}...")
181+
classes.append(ClassParser.parse_file(f, exported_hooks=exported_hooks))
75182
f.close()
76183

77184
# Sort the classes so they are in the correct order in lists.
78185
classes.sort(key=lambda c: c.name)
79186

80187
def make_parsers(level):
81-
"""Returns a function that parses content for refs to other classes, methods, or enums,
82-
and automatically inserts the correct link.
83-
"""
84-
# Make lists of all class names and method names.
188+
"""Returns filters that parse refs to other classes/methods/enums and insert links."""
85189
class_names = []
86190
method_names = []
87191

88192
for class_ in classes:
89-
class_names.append('[' + class_.name + ']')
90-
193+
class_names.append("[" + class_.name + "]")
91194
for method in class_.methods:
92-
method_names.append('[' + class_.name + ':' + method.name + ']')
195+
method_names.append("[" + class_.name + ":" + method.name + "]")
93196

94197
def link_parser(content):
95-
# Replace all occurrencies of &Class:Function and then &Class with a link to given func or class
96-
198+
# Replace [Class:Method] and [Class] with links
97199
for name in method_names:
98-
# Take the [] off the front of the method's name.
99200
full_name = name[1:-1]
100-
# Split "Class:Method" into "Class" and "Method".
101-
class_name, method_name = full_name.split(':')
102-
url = '{}{}/{}.html'.format(('../' * level), class_name, method_name)
103-
# Replace occurrencies of &Class:Method with the url created
201+
class_name, method_name = full_name.split(":")
202+
url = "{}{}/{}.html".format(("../" * level), class_name, method_name)
104203
content = content.replace(name, '<a class="fn" href="{}">{}</a>'.format(url, full_name))
105204

106205
for name in class_names:
107-
# Take the [] off the front of the class's name.
108206
class_name = name[1:-1]
109-
url = '{}{}/index.html'.format(('../' * level), class_name)
110-
# Replace occurrencies of &Class:Method with the url created
207+
url = "{}{}/index.html".format(("../" * level), class_name)
111208
content = content.replace(name, '<a class="mod" href="{}">{}</a>'.format(url, class_name))
112209

113210
return content
114211

115212
# Links to the "Programming in Lua" documentation for each Lua type.
116213
lua_type_documentation = {
117-
'nil': 'http://www.lua.org/pil/2.1.html',
118-
'boolean': 'http://www.lua.org/pil/2.2.html',
119-
'number': 'http://www.lua.org/pil/2.3.html',
120-
'string': 'http://www.lua.org/pil/2.4.html',
121-
'table': 'http://www.lua.org/pil/2.5.html',
122-
'function': 'http://www.lua.org/pil/2.6.html',
123-
'...': 'http://www.lua.org/pil/5.2.html',
214+
"nil": "http://www.lua.org/pil/2.1.html",
215+
"boolean": "http://www.lua.org/pil/2.2.html",
216+
"number": "http://www.lua.org/pil/2.3.html",
217+
"string": "http://www.lua.org/pil/2.4.html",
218+
"table": "http://www.lua.org/pil/2.5.html",
219+
"function": "http://www.lua.org/pil/2.6.html",
220+
"...": "http://www.lua.org/pil/5.2.html",
124221
}
125222

126223
def data_type_parser(content):
@@ -132,44 +229,44 @@ def data_type_parser(content):
132229
# Otherwise try to build a link to the proper page.
133230
if content in class_names:
134231
class_name = content[1:-1]
135-
url = '{}{}/index.html'.format(('../' * level), class_name)
232+
url = "{}{}/index.html".format(("../" * level), class_name)
136233
return '<strong><a class="mod" href="{}">{}</a></strong>'.format(url, class_name)
137234

138235
# Case for enums to direct to a search on github
139236
enum_name = content[1:-1]
140-
url = 'https://github.com/ElunaLuaEngine/ElunaTrinityWotlk/search?l=cpp&q=%22enum+{}%22&type=Code&utf8=%E2%9C%93'.format(enum_name)
237+
url = (
238+
'https://github.com/ElunaLuaEngine/ElunaTrinityWotlk/search?l=cpp&q=%22enum+{}%22'
239+
"&type=Code&utf8=%E2%9C%93"
240+
).format(enum_name)
141241
return '<strong><a href="{}">{}</a></strong>'.format(url, enum_name)
142242

143-
# By default we just return the name without the [] around it
144-
return content[1:-1]
145-
146243
return link_parser, data_type_parser
147244

148245
# Create the render function with the template path and parser maker.
149-
render = make_renderer('ElunaDoc/templates', make_parsers)
246+
render = make_renderer("ElunaDoc/templates", make_parsers)
150247

151248
# Render the index.
152-
render('index.html', 'index.html', level=0, classes=classes)
249+
render("index.html", "index.html", level=0, classes=classes)
153250
# Render the search index.
154-
render('search-index.js', 'search-index.js', level=0, classes=classes)
251+
render("search-index.js", "search-index.js", level=0, classes=classes)
155252
# Render the date.
156-
render('date.js', 'date.js', level=0, currdate=time.strftime("%d/%m/%Y"))
253+
render("date.js", "date.js", level=0, currdate=time.strftime("%d/%m/%Y"))
157254

158255
for class_ in classes:
159-
print(f'Rendering pages for class {class_.name}...')
256+
print(f"Rendering pages for class {class_.name}...")
160257

161258
# Make a folder for the class.
162-
os.mkdir('build/' + class_.name)
163-
index_path = '{}/index.html'.format(class_.name)
164-
sidebar_path = '{}/sidebar.js'.format(class_.name)
259+
os.mkdir("build/" + class_.name)
260+
index_path = "{}/index.html".format(class_.name)
261+
sidebar_path = "{}/sidebar.js".format(class_.name)
165262

166263
# Render the class's index page.
167-
render('class.html', index_path, level=1, classes=classes, current_class=class_)
264+
render("class.html", index_path, level=1, classes=classes, current_class=class_)
168265

169266
# Render the class's sidebar script.
170-
render('sidebar.js', sidebar_path, level=1, classes=classes, current_class=class_)
267+
render("sidebar.js", sidebar_path, level=1, classes=classes, current_class=class_)
171268

172269
# Render each method's page.
173270
for method in class_.methods:
174-
method_path = '{}/{}.html'.format(class_.name, method.name)
175-
render('method.html', method_path, level=1, current_class=class_, current_method=method)
271+
method_path = "{}/{}.html".format(class_.name, method.name)
272+
render("method.html", method_path, level=1, current_class=class_, current_method=method)

0 commit comments

Comments
 (0)