diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/README.md b/README.md index ece4826..0343362 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Download the latest generated definitions from the build workflow action, altern ## Installation 1. **Install LuaLS**: Follow the [LuaLS installation instructions](https://luals.github.io/#vscode-install). 2. **Configure Your Workspace**: Point your workspace/LSP configuration to the location of the generated definitions. +3. **Enable parameter inference**: In the LuaLS configuration, enable Lua.type.inferParamType, this is required for callback parameters to function properly. ## Usage 1. Install Python 3, Pip, and dependencies: diff --git a/parser.py b/parser.py index 6f00b59..f510f01 100644 --- a/parser.py +++ b/parser.py @@ -1,7 +1,10 @@ import os import sys +import re from bs4 import BeautifulSoup +debug = False + def extract_method_info(soup): method_section = soup.find('h1') class_name_tag = method_section.find_next() @@ -105,6 +108,62 @@ def extract_argument_name_opt(argument_name): argument_optional_value = optional_part return argument_name, argument_optional, argument_optional_value +def extract_table(soup): + # Extract the header row + header = soup.find('thead') + if header is None: + return [] + + header_row = header.find_all('th') + if not header_row: + return [] + + # Extract the column names (keys) from the header + headers = [header.get_text(strip=True) for header in header_row] + + # Find all the table rows, skipping the header + rows = soup.find_all('tr')[1:] + if not rows: + return [] + + row_list = [] + + for row in rows: + cells = row.find_all('td') + if not cells: + return [] + + # Ensure the row has the same number of columns as the header + if len(cells) != len(headers): + continue # Skip rows that don't match the header + + cell_data = {} + for idx, cell in enumerate(cells): + # Check if the cell contains any elements + if cell.find_all('span'): + cell_data[headers[idx]] = extract_parameters(cell) + else: + cell_data[headers[idx]] = cell.get_text(strip=True) + + row_list.append(cell_data) + + return row_list + +def extract_parameters(cell): + parameters = {} + + # Find all tags in the 'cell' + spans = cell.find_all('span') + + # Extract the 'title' as the key and text as the value + for span in spans: + title = span.get('title') + text = span.get_text(strip=True) + if title: + parameters[text] = title + + return parameters + def extract_return_values(soup): returns_section = soup.find('h2', id='returns') if returns_section is None: @@ -143,9 +202,9 @@ def extract_return_values(soup): return_values_list.append({'type': return_type, 'name': return_name, 'description': return_description}) return return_values_list -def write_lua_stub(class_name, method_string, arguments_list, return_values_list, output_directory): +def write_lua_stub(class_name, method_string, arguments_list, table_list, return_values_list, output_directory): with open(os.path.join(output_directory, class_name + ".lua"), "a") as lua_file: - # Write the parameter and return values + # Write the parameter definitions for argument in arguments_list: lua_file.write("---@param " + argument['name']) if argument['optional']: @@ -157,6 +216,27 @@ def write_lua_stub(class_name, method_string, arguments_list, return_values_list lua_file.write(" Default value: (" + argument['optional_val'] + ")") lua_file.write(" " + argument['description'] + "\n") + + # Hard code table parsing for register events for now (function overload definitions) + if(class_name == "Global") and re.match(r"^Register\w+Event$", method_string): + for row in table_list: + # Only create function overloads for properly formatted parameter data + if 'Parameters' in row and isinstance(row['Parameters'], dict): + lua_file.write("---@overload fun(event: " + row['ID'] + ", func: fun(") + + # Loop through parameters and check for 'event', we want to define this as the event ID and not a type + params = [] + for key, value in row['Parameters'].items(): + if key == 'event': + params.append(f'{key}: {row["ID"]}') + else: + params.append(f'{key}: {value}') + + # Join the parameters and write to the file + lua_file.write(', '.join(params)) + lua_file.write("), shots?: number): function\n") + + # Write the return value definitions for return_value in return_values_list: lua_file.write("---@return " + return_value['type'] + " " + return_value['name'] + " " + return_value['description'] + "\n") @@ -173,7 +253,9 @@ def write_lua_stub(class_name, method_string, arguments_list, return_values_list lua_file.write(") end\n\n") - print(f"'{method_string}' stubs appended to '{class_name}.lua'") + global debug + if debug: + print(f"'{method_string}' stubs appended to '{class_name}.lua'") def write_lua_class(class_name, inherited_objects, output_directory): with open(os.path.join(output_directory, class_name + ".lua"), "w") as lua_file: @@ -184,8 +266,10 @@ def write_lua_class(class_name, inherited_objects, output_directory): lua_file.write(f": {', '.join(inherited_objects)}") lua_file.write(f"\n{class_name} = {{}}\n\n") - - print(f"'{class_name}' class information appended to '{class_name}.lua'") + + global debug + if debug: + print(f"'{class_name}' class information appended to '{class_name}.lua'") def process_html_file(html_file, filename, output_directory): with open(html_file, "r") as f: @@ -202,19 +286,25 @@ def process_html_file(html_file, filename, output_directory): # Otherwise process like normal class_name, method_string = extract_method_info(soup) arguments_list = extract_arguments(soup) + table_list = extract_table(soup) return_values_list = extract_return_values(soup) - write_lua_stub(class_name, method_string, arguments_list, return_values_list, output_directory) + write_lua_stub(class_name, method_string, arguments_list, table_list, return_values_list, output_directory) def main(): # Check if command-line arguments are provided - if len(sys.argv) != 3: - print("Usage: python parser.py ") + if len(sys.argv) < 3: + print("Usage: python parser.py ") sys.exit(1) directory = sys.argv[1] output_directory = sys.argv[2] + # Debug (Optional) - Default to false if not provided + if(sys.argv[3]): + global debug + debug = True if sys.argv[3].lower() == 'true' else False + # Special case to process our class definitions html_file = os.path.join(directory, "index.html") process_html_file(html_file, "index.html", output_directory) diff --git a/runParser.ps1 b/runParser.ps1 index 5e0531f..d23e36d 100644 --- a/runParser.ps1 +++ b/runParser.ps1 @@ -1,9 +1,11 @@ # Define the paths to the parser script and the parent directory containing Eluna API HTML files -$parserScriptPath = "C:\Path\To\parser.py" -$htmlParentDirectory = "C:\Path\To\ElunaLuaEngine.github.io-master" +$parserScriptPath = "C:\Users\foereaper\Desktop\luaLS\LuaLS-Eluna-Parser\parser.py" +$htmlParentDirectory = "C:\Users\foereaper\Desktop\tc\elunatrinitywotlk\src\server\game\LuaEngine\docs\build" # Define the output directory for the LuaLS workspace -$outputDirectory = "C:\Path\To\LuaLS\Workspace" +$outputDirectory = "C:\Users\foereaper\Desktop\luaLS\LuaLS-Eluna-Parser\build" + +$debug = $false # Define the list of subdirectories to process $subdirectories = @("Aura", "BattleGround", "Corpse", "Creature", "ElunaQuery", "GameObject", "Group", "Guild", "Global", "Item", "Map", "Object", "Player", "Quest", "Spell", "Unit", "Vehicle", "WorldObject", "WorldPacket") @@ -11,5 +13,5 @@ $subdirectories = @("Aura", "BattleGround", "Corpse", "Creature", "ElunaQuery", # Iterate over each subdirectory foreach ($subdir in $subdirectories) { $htmlDirectory = Join-Path -Path $htmlParentDirectory -ChildPath $subdir - python $parserScriptPath $htmlDirectory $outputDirectory + python $parserScriptPath $htmlDirectory $outputDirectory $debug } \ No newline at end of file