From 270ce1c8c8457d1bee2a21743775f5cd60145e21 Mon Sep 17 00:00:00 2001 From: Arun Madhavan Date: Tue, 1 Jul 2025 14:34:55 -0400 Subject: [PATCH 1/6] Fix the documentation generation logic from header --- apis/FrameRate/IFrameRate.h | 88 +++++++++++-------- .../generate_md_from_header.py | 22 ++--- .../md_from_h_generator/header_file_parser.py | 75 ++++++++-------- .../md_from_h_generator/markdown_templates.py | 45 ++++++---- 4 files changed, 133 insertions(+), 97 deletions(-) diff --git a/apis/FrameRate/IFrameRate.h b/apis/FrameRate/IFrameRate.h index b848ad52..6b0fb8b0 100644 --- a/apis/FrameRate/IFrameRate.h +++ b/apis/FrameRate/IFrameRate.h @@ -27,6 +27,7 @@ namespace WPEFramework namespace Exchange { /* @json 1.0.0 @text:keep */ + // @plugindescription This plugin allows to collect FPS related data on TV profile stack. struct EXTERNAL IFrameRate : virtual public Core::IUnknown { enum { ID = ID_FRAMERATE }; @@ -38,78 +39,95 @@ namespace WPEFramework // @text onFpsEvent // @brief Triggered by callback from FrameRate after onFpsEvent - // @param average - in - int - // @param min - in - int - // @param max - in - int - virtual void OnFpsEvent(const int average, const int min, const int max) {}; + // @details Triggered at the end of each interval as defined by the setCollectionFrequency method + // after StartFpsCollection method and once after the stopFpsCollection method is invoked + // @param average: The average FPS e.g. 60 + // @param min: The minimum FPS e.g. 30 + // @param max: The maximum FPS e.g. 120 + virtual void OnFpsEvent(const int average /* @in */, const int min /* @in */, const int max /* @in */) {}; // @text onDisplayFrameRateChanging - // @brief Triggered when the framerate changes started - // @param displayFrameRate - in - string - virtual void OnDisplayFrameRateChanging(const string& displayFrameRate) {}; + // @brief Triggered when the framerate change is starting. + // @details This event is triggered when the display frame rate is about to change + // and represented as "WIDTHxHEIGHTxFPS". + // @param displayFrameRate: The new display frame rate e.g. "1920x1080x60" + virtual void OnDisplayFrameRateChanging(const string& displayFrameRate /* @in */) {}; // @text onDisplayFrameRateChanged - // @brief Triggered when the framerate changed. - // @param displayFrameRate - in - string - virtual void OnDisplayFrameRateChanged(const string& displayFrameRate) {}; + // @brief Triggered when the framerate has changed. + // @details This event is triggered after the display frame rate has changed + // and represented as "WIDTHxHEIGHTxFPS". + // @param displayFrameRate: The new display frame rate e.g. "1920x1080x60" + virtual void OnDisplayFrameRateChanged(const string& displayFrameRate /* @in */) {}; }; + // @omit virtual Core::hresult Register(IFrameRate::INotification* notification) = 0; + // @omit virtual Core::hresult Unregister(IFrameRate::INotification* notification) = 0; /** Gets the Display Frame Rate*/ // @text getDisplayFrameRate - // @brief Gets the current display frame rate values. - // @param framerate - out - string - // @param success - out - boolean + // @details Retrieves the current display frame rate as a string in the format "WIDTHxHEIGHTpxFPS" + // @param framerate: The current display frame rate. e.g. "3840x2160px60" + // @param success: Indicates if the operation was successful e.g. True + // @returns Core::hresult virtual Core::hresult GetDisplayFrameRate(string& framerate /* @out */, bool& success /* @out */) = 0; /** Gets framerate mode */ // @text getFrmMode - // @brief Gets the current auto framerate mode. - // @param frmmode - out - int - // @param success - out - boolean + // @details Retrieves the current auto framerate mode as an integer. Expeted values are 0 or 1. + // @param auto-frm-mode: The current auto framerate mode. e.g. 1 + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult virtual Core::hresult GetFrmMode(int &framerateMode /* @out @text:auto-frm-mode */, bool& success /* @out */) = 0; /** Sets the FPS data collection interval */ // @text setCollectionFrequency - // @brief Sets the FPS data collection interval. - // @param frequency - in - int - // @param success - out - boolean - virtual Core::hresult SetCollectionFrequency(const int frequency, bool& success /* @out */) = 0; + // @details Sets the interval for FPS data collection in milliseconds. Default is 10000ms and min is 100ms + // @param frequency: The collection frequency in ms. e.g. 1000 + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult + virtual Core::hresult SetCollectionFrequency(const int frequency /* @in */, bool& success /* @out */) = 0; /** Sets the display framerate values */ // @text setDisplayFrameRate - // @brief Sets the display framerate values. - // @param framerate - in - string - // @param success - out - boolean - virtual Core::hresult SetDisplayFrameRate(const string& framerate, bool& success /* @out */) = 0; + // @details Sets the display framerate to the specified value in the format "WIDTHxHEIGHTpxFPS". + // @param framerate: The display framerate to set. e.g. "3840px2160px30" + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult + virtual Core::hresult SetDisplayFrameRate(const string& framerate /* @in */, bool& success /* @out */) = 0; /** Sets the auto framerate mode */ // @text setFrmMode - // @brief Set the Frm mode. - // @param frmmode - in - int - // @param success - out - boolean + // @details Sets the auto framerate mode to the specified value. Expected values are 0(disable) or 1(enable). + // @param frmmode: The framerate mode to set. e.g. 1 + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult virtual Core::hresult SetFrmMode(const int frmmode /* @in */, bool& success /* @out */) = 0; /** Starts the FPS data collection */ // @text startFpsCollection - // @brief Starts the FPS data collection. Starts the FPS data collection - // @param success - out - boolean + // @details Starts collecting FPS data at the configured interval set by the method SetCollectionFrequency. + // @see SetCollectionFrequency. + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult virtual Core::hresult StartFpsCollection(bool& success /* @out */) = 0; /** Stops the FPS data collection */ // @text stopFpsCollection - // @brief Stops the FPS data collection - // @param success - out - boolean + // @details Stops the FPS data collection. + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult virtual Core::hresult StopFpsCollection(bool& success /* @out */) = 0; /** Update the FPS value */ // @text updateFps - // @brief Update the FPS value - // @param newFpsValue - in - int - // @param success - out - boolean - virtual Core::hresult UpdateFps(const int newFpsValue, bool& success /* @out */) = 0; + // @details Updates the current FPS value to the specified value represented as integer. + // @param newFpsValue: The new FPS value. e.g. 60 + // @param success: Indicates if the operation was successful. e.g. True + // @returns Core::hresult + virtual Core::hresult UpdateFps(const int newFpsValue /* @in */, bool& success /* @out */) = 0; }; } // namespace Exchange } // namespace WPEFramework diff --git a/tools/md_from_h_generator/generate_md_from_header.py b/tools/md_from_h_generator/generate_md_from_header.py index 14f706e4..3986b4d7 100644 --- a/tools/md_from_h_generator/generate_md_from_header.py +++ b/tools/md_from_h_generator/generate_md_from_header.py @@ -35,34 +35,34 @@ def generate_md_from_header(header_file): filename = os.path.basename(header_file) classname, _ = os.path.splitext(filename) - # Remove the leading 'I' from the api's class name - output_file_path = './tools/md_from_h_generator/generated_docs/' + classname[1:] + 'Plugin.md' - log_file_path = './tools/md_from_h_generator/logs/' + classname + '.txt' logger = Logger(log_file_path) header_structure = HeaderFileParser(header_file, logger) + # Remove the leading 'I' from the api's class name + output_file_path = './tools/md_from_h_generator/generated_docs/' + header_structure.classname + 'Plugin.md' + with open(output_file_path, 'w', encoding='utf-8') as file: - file.write(generate_header_toc(classname, header_structure)) - file.write(generate_header_description_markdown(classname)) + file.write(generate_header_toc(header_structure.classname, header_structure)) + file.write(generate_header_description_markdown(header_structure.classname, getattr(header_structure, 'plugindescription', ''))) if len(header_structure.methods.values()) > 0: - file.write(generate_methods_toc(header_structure.methods, classname)) + file.write(generate_methods_toc(header_structure.methods, header_structure.classname)) for method_name, method_info in header_structure.methods.items(): file.write(generate_method_markdown( - method_name, method_info, header_structure.symbols_registry)) + method_name, method_info, header_structure.symbols_registry, header_structure.classname)) file.write("\n") if len(header_structure.properties.values()) > 0: - file.write(generate_properties_toc(header_structure.properties, classname)) + file.write(generate_properties_toc(header_structure.properties, header_structure.classname)) for prop_name, prop_info in header_structure.properties.items(): file.write(generate_property_markdown( - prop_name,prop_info, header_structure.symbols_registry)) + prop_name, prop_info, header_structure.symbols_registry, header_structure.classname)) file.write("\n") if len(header_structure.events.values()) > 0: - file.write(generate_notifications_toc(header_structure.events, classname)) + file.write(generate_notifications_toc(header_structure.events, header_structure.classname)) for event_name, event_info in header_structure.events.items(): file.write(generate_notification_markdown( - event_name, event_info, header_structure.symbols_registry)) + event_name, event_info, header_structure.symbols_registry, header_structure.classname)) logger.write_log() logger.close() diff --git a/tools/md_from_h_generator/header_file_parser.py b/tools/md_from_h_generator/header_file_parser.py index 2b1d7c7a..3a6a5017 100644 --- a/tools/md_from_h_generator/header_file_parser.py +++ b/tools/md_from_h_generator/header_file_parser.py @@ -31,6 +31,8 @@ class HeaderFileParser: """ # List of regexes to match different components of the header file REGEX_LINE_LIST = [ + ('plugindescription', 'doxygen', re.compile(r'(?:\/\*+|\*|//)\s*@plugindescription\s+(.*?)(?=\s*\*\/|$)')), + ] + [ ('text', 'doxygen', re.compile(r'(?:\/\*+|\*|//) (?:@text|@alt)\s+(.*?)(?=\s*\*\/|$)')), ('brief', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @brief\s*(.*?)(?=\s*\*\/|$)')), ('details', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @details\s*(.*?)(?=\s*\*\/|$)')), @@ -38,7 +40,7 @@ class HeaderFileParser: ('return', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @return(?:s)?\s*(.*?)(?=\s*\*\/|$)')), ('see', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @see\s*(.*?)(?=\s*\*\/|$)')), ('omit', 'doxygen', re.compile(r'(?:\/\*+|\*|//)\s*(@json:omit|@omit)')), - ('property','doxygen', re.compile(r'(?:\/\*+|\*|//) @property\s*(.*)')), + ('property','doxygen', re.compile(r'(?:\/\*+|\*|//) @property\s*(.*)')), ('comment', 'doxygen', re.compile(r'(?:\/\*+|\*|//)\s*(.*)')), ('enum', 'cpp_obj', re.compile(r'enum\s+([\w\d]+)\s*(?:\:\s*([\w\d\:\*]*))?\s*\{?')), ('struct', 'cpp_obj', re.compile(r'struct\s+(EXTERNAL\s+)?([\w\d]+)\s*(?:\{)?(?!.*:)')), @@ -47,15 +49,15 @@ class HeaderFileParser: ] # Basic type examples for generating missing symbol examples BASIC_TYPE_EXAMPLES = { - 'int32_t': '0', - 'uint32_t': '0', - 'int64_t': '0', + 'int32_t': '0', + 'uint32_t': '0', + 'int64_t': '0', 'uint64_t': '0', 'int': '0', - 'float': '0.0', - 'double': '0.0', - 'bool': 'true', - 'char': 'a', + 'float': '0.0', + 'double': '0.0', + 'bool': True, + 'char': 'a', 'string': '' } # List of regexes to match different cpp components of the header file @@ -72,16 +74,17 @@ class HeaderFileParser: def __init__(self, header_file_path: str, logger: Logger): """ - Initializes data structures to track different components of a C++ header file, then + Initializes data structures to track different components of a C++ header file, then parses said header file to extract methods, structs, enums, and iterators. - Args: + Args: header_file_path (str): path to the header file logger (Logger): list of regex matching different components of the header file """ # objects to hold the different components and properties of the header file self.header_file_path = header_file_path - self.classname = os.path.splitext(os.path.basename(self.header_file_path))[0] + # All the header files will begin with "I", strip it to get the classname. + self.classname = os.path.splitext(os.path.basename(self.header_file_path))[0][1:] self.methods = {} self.properties = {} self.events = {} @@ -127,7 +130,7 @@ def post_process(self): def parse_header_file(self): """ - Parses the header file line-by-line to track and record the file's components, such as + Parses the header file line-by-line to track and record the file's components, such as methods, properties, events, structs, enums, and iterators. Keeps track of these components' associated doxygen tags. """ @@ -186,7 +189,7 @@ def match_line_with_regex(self, line, line_regex_list): return match.groups(), tag, l_type return None, None, None - def process_method(self, line, method_object, within_method_def, method_paren_count, + def process_method(self, line, method_object, within_method_def, method_paren_count, curr_line_num, scope): """ Processes a line within a method definition. @@ -226,7 +229,7 @@ def process_enum(self, line, enum_object, within_enum_def, enum_braces_count, cu enum_object = '' return enum_object, enum_braces_count, within_enum_def - def process_struct(self, line, struct_object, within_struct_def, struct_braces_count, + def process_struct(self, line, struct_object, within_struct_def, struct_braces_count, curr_line_num): """ Processes a line within a struct definition. @@ -248,10 +251,11 @@ def process_struct(self, line, struct_object, within_struct_def, struct_braces_c def update_doxy_tags(self, groups, line_tag): """ - Updates the doxygen tag object with the given line's information. + Updates the doxygen tag object with the given line's information. """ - if line_tag == 'text': - # self.doxy_tags = {} + if line_tag == 'plugindescription': + self.plugindescription = groups[0] + elif line_tag == 'text': self.doxy_tags['text'] = groups[0] elif line_tag == 'params': self.latest_param = groups[0] @@ -264,16 +268,17 @@ def update_doxy_tags(self, groups, line_tag): return elif self.latest_tag == 'params': self.doxy_tags['params'][self.latest_param] += (' ' + groups[0]) - elif self.latest_tag: + elif self.latest_tag and self.latest_tag in self.doxy_tags and self.latest_tag != 'plugindescription': self.doxy_tags[self.latest_tag] += (' ' + groups[0]) line_tag = self.latest_tag else: self.doxy_tags[line_tag] = groups[0] - self.latest_tag = line_tag + if line_tag != 'plugindescription': + self.latest_tag = line_tag def clean_and_validate_cpp_obj_line(self, line, delimiter, line_num, data_type): """ - Validates a line of a multi-line cpp object by checking that data members are defined on + Validates a line of a multi-line cpp object by checking that data members are defined on separate lines and that comments are formed before the delimiter. """ delim_index = line.find(delimiter) @@ -288,7 +293,7 @@ def clean_and_validate_cpp_obj_line(self, line, delimiter, line_num, data_type): self.logger.log("WARNING", f"Line {line_num + 1} should have only one {data_type} per line.") return line - + def remove_inline_comments(self, line): """ Removes inline comments from a line. @@ -334,7 +339,7 @@ def register_enum(self, enum_object): description = self.clean_description(description) enumerator_value = enumerator_value or len(self.enums_registry[enum_name]) self.enums_registry[enum_name][enumerator_name] = { - 'value': enumerator_value, + 'value': enumerator_value, 'description': description.strip() if description else '' } else: @@ -356,7 +361,7 @@ def register_struct(self, struct_object): member_type, member_name, description = member_match.groups() description = self.clean_description(description) self.structs_registry[struct_name][member_name] = { - 'type': member_type, + 'type': member_type, 'description': description.strip() if description else '' } # register each data member in the global symbol registry @@ -392,14 +397,14 @@ def register_method(self, method_object, doxy_tags, scope): def build_method_info(self, method_return_type, method_parameters, doxy_tags): """ - Helper to build a method info object. Also registers method parameters in the symbol + Helper to build a method info object. Also registers method parameters in the symbol registry. """ doxy_tag_param_info = doxy_tags.get('params', {}) params, results = self.process_and_register_params(method_parameters, doxy_tag_param_info) method_info = { - 'text': doxy_tags.get('text', ''), - 'brief': doxy_tags.get('brief', ''), + 'text': doxy_tags.get('text', ''), + 'brief': doxy_tags.get('brief', ''), 'details': doxy_tags.get('details', ''), 'events': doxy_tags.get('see', {}), 'params': params, @@ -415,7 +420,7 @@ def build_method_info(self, method_return_type, method_parameters, doxy_tags): def process_and_register_params(self, method_parameters, doxy_tag_param_info): """ - Helper to build params and results data structures, using the parameter declaration list + Helper to build params and results data structures, using the parameter declaration list and doxygen tags. """ param_list_info = self.get_info_from_param_declaration(method_parameters) @@ -464,7 +469,7 @@ def get_info_from_param_declaration(self, parameters): def register_symbol(self, symbol_name, symbol_type, description): """ - Registers a symbol by incrementally adding information to the symbols registry, as + Registers a symbol by incrementally adding information to the symbols registry, as information is discovered while parsing. """ unique_id = f"{symbol_name}-{symbol_type}" @@ -498,7 +503,7 @@ def external_struct_tracker(self, line, scope, brace_count): def generate_request_response_objects(self): """ - Generates request and response JSONs for each method and event. Directly modifies the + Generates request and response JSONs for each method and event. Directly modifies the methods, properties, and events registries. """ for method_name, method_info in self.methods.items(): @@ -561,7 +566,7 @@ def generate_response_object(self, method_info): def get_symbol_example(self, unique_id, description): """ - Used in generating request/response JSONs. Pulls an example from either the @param tag + Used in generating request/response JSONs. Pulls an example from either the @param tag description or the symbols registry. """ example_from_description = self.generate_example_from_description(description) @@ -621,7 +626,7 @@ def generate_example_from_symbol_type(self, symbol_type): def wrap_example_if_iterator(self, unique_id, example): """ - Wrap the example in a list if the symbol is an iterator, otherwise simply return the + Wrap the example in a list if the symbol is an iterator, otherwise simply return the example. """ if self.symbols_registry[unique_id]['type'] in self.iterators_registry: @@ -639,7 +644,7 @@ def link_method_to_event(self): self.methods[method_name]['events'][event] = self.events[event].get('brief') self.events[event]['associated_method'] = method_name else: - self.logger.log("ERROR", + self.logger.log("ERROR", f"Event {event} tagged with {method_name} does not exist.") def log_unassociated_events(self): @@ -687,11 +692,11 @@ def fill_and_log_missing_symbol_descriptions(self): result['description'] = self.symbols_registry[f"{result['name']}-{result['type']}"].get('description', '') self.logger.log("INFO", f"Filled missing desc for {result['name']} in property {prop_name}") - + def log_missing_method_info(self): """ - At the end of parsing, if there is still information missing for methods, events, and - symbols, log it. + At the end of parsing, if there is still information missing for methods, events, and + symbols, log it. """ for method_name, method_info in self.methods.items(): if not method_info.get('brief') and not method_info.get('details'): diff --git a/tools/md_from_h_generator/markdown_templates.py b/tools/md_from_h_generator/markdown_templates.py index 8bd56e60..c2301c00 100644 --- a/tools/md_from_h_generator/markdown_templates.py +++ b/tools/md_from_h_generator/markdown_templates.py @@ -56,7 +56,7 @@ | Name | Type | Description | | :-------- | :-------- | :-------- | -| callsign | string | Plugin instance name (default: *{classname}*) | +| callsign | string | Plugin instance name (default: org.rdk.{classname}) | | classname | string | Class name: *{classname}* | | locator | string | Library name: *libWPEFramework{classname}.so* | | autostart | boolean | Determines if the plugin shall be started automatically along with the framework | @@ -152,11 +152,17 @@ def generate_header_toc(classname, document_object, version="1.0.0"): toc += "- [Notifications](#head.Notifications)\n" return toc -def generate_header_description_markdown(classname): +def generate_header_description_markdown(classname, plugindescription=None): """ Generate the header description markdown for the file. """ - return HEADER_DESCRIPTION_TEMPLATE.format(classname=classname) + description_line = ( + plugindescription.strip() if plugindescription else f'The `{classname}` plugin provides an interface for {classname}.' + ) + return HEADER_DESCRIPTION_TEMPLATE.replace( + 'The `{classname}` plugin provides an interface for {classname}.', + description_line + ).format(classname=classname) def generate_methods_toc(methods, classname): """ @@ -168,7 +174,7 @@ def generate_methods_toc(methods, classname): toc += f"| [{method}](#method.{method}) | {method_body['brief'] or method_body['details']} |\n" return toc -def generate_method_markdown(method_name, method_info, symbol_registry): +def generate_method_markdown(method_name, method_info, symbol_registry, classname): """ Generate the markdown for a specific method. """ @@ -177,8 +183,8 @@ def generate_method_markdown(method_name, method_info, symbol_registry): markdown += generate_parameters_section(method_info['params'], symbol_registry) markdown += generate_results_section(method_info['results'], symbol_registry) markdown += "\n### Examples\n" - markdown += generate_request_section(method_info['request'], '') - markdown += generate_response_section(method_info['response'], '') + markdown += generate_request_section(method_info['request'], '', classname) + markdown += generate_response_section(method_info['response'], '', classname) return markdown def generate_events_section(events): @@ -226,16 +232,23 @@ def generate_results_section(results, symbol_registry): markdown += "This method returns no results.\n" return markdown -def generate_request_section(request, method_type): +def generate_request_section(request, method_type, classname=None): """ Generate the request section for a method. """ + # If classname is provided and the request has a 'method' field, update it + if classname and isinstance(request, dict) and 'method' in request: + # Replace the class part in the method string with the correct classname + parts = request['method'].split('.') + if len(parts) > 2: + parts[2] = classname + request['method'] = '.'.join(parts) request_json = json.dumps(request, indent=4) markdown = EXAMPLE_REQUEST_TEMPLATE.format(request_json=request_json, method_type=method_type) markdown += "```" return markdown -def generate_response_section(response, method_type): +def generate_response_section(response, method_type, classname=None): """ Generate the response section for a method. """ @@ -259,7 +272,7 @@ def generate_properties_toc(properties, classname): toc += f"| [{prop}](#property.{prop}){super_script} | {property_body['brief'] or property_body['details']} |\n" return toc -def generate_property_markdown(property_name, property_info, symbol_registry): +def generate_property_markdown(property_name, property_info, symbol_registry, classname): """ Generate the markdown for a specific property. """ @@ -272,11 +285,11 @@ def generate_property_markdown(property_name, property_info, symbol_registry): markdown += generate_values_section((property_info['results'] + property_info['params']), symbol_registry) markdown += "\n### Examples\n" if 'read' in property_info['property']: - markdown += generate_request_section(property_info['get_request'], 'Get ') - markdown += generate_response_section(property_info['get_response'], 'Get ') + markdown += generate_request_section(property_info['get_request'], 'Get ', classname) + markdown += generate_response_section(property_info['get_response'], 'Get ', classname) if 'write' in property_info['property']: - markdown += generate_request_section(property_info['set_request'], 'Set ') - markdown += generate_response_section(property_info['set_response'], 'Set ') + markdown += generate_request_section(property_info['set_request'], 'Set ', classname) + markdown += generate_response_section(property_info['set_response'], 'Set ', classname) return markdown def generate_values_section(values, symbol_registry): @@ -305,12 +318,12 @@ def generate_notifications_toc(events, classname): toc += f"| [{event}](#event.{event}) | {event_body['brief'] or event_body['details']} |\n" return toc -def generate_notification_markdown(event_name, event_info, symbol_registry): +def generate_notification_markdown(event_name, event_info, symbol_registry, classname): """ Generate the markdown for a specific event. """ - markdown = EVENT_MARKDOWN_TEMPLATE.format(event_name=event_name, event_description=event_info['brief'] or event_info['details']) + markdown = EVENT_MARKDOWN_TEMPLATE.format(event_name=event_name, event_description=event_info['brief'] or event_info['details']) markdown += generate_parameters_section(event_info['params'], symbol_registry) markdown += "\n### Examples\n" - markdown += generate_request_section(event_info['request'], '') + markdown += generate_request_section(event_info['request'], '', classname) return markdown From a3a5d886cae01f5a9f4325778013418cc80ea068 Mon Sep 17 00:00:00 2001 From: Arun Madhavan Date: Tue, 1 Jul 2025 15:23:02 -0400 Subject: [PATCH 2/6] Add variable name override support for @in and @out with @text --- .../md_from_h_generator/header_file_parser.py | 34 +++++++++++++++---- .../md_from_h_generator/markdown_templates.py | 14 ++++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/tools/md_from_h_generator/header_file_parser.py b/tools/md_from_h_generator/header_file_parser.py index 3a6a5017..26b48d00 100644 --- a/tools/md_from_h_generator/header_file_parser.py +++ b/tools/md_from_h_generator/header_file_parser.py @@ -427,7 +427,7 @@ def process_and_register_params(self, method_parameters, doxy_tag_param_info): params = [] results = [] # build the params and results lists using the parameter delcaration list and doxygen tags - for symbol_name, (symbol_type, symbol_inline_comment) in param_list_info.items(): + for symbol_name, (symbol_type, symbol_inline_comment, custom_name, direction) in param_list_info.items(): # register string iterators here b/c they are seldom defined outside of a method param if symbol_type == 'RPC::IStringIterator': self.register_iterator(symbol_type) @@ -436,7 +436,9 @@ def process_and_register_params(self, method_parameters, doxy_tag_param_info): symbol_info = { 'name': symbol_name, 'type': symbol_type, - 'description': symbol_description + 'description': symbol_description, + 'custom_name': custom_name, + 'direction': direction } # determine whether the symbol is a result or a parameter if symbol_inline_comment and '@inout' in symbol_inline_comment: @@ -462,7 +464,17 @@ def get_info_from_param_declaration(self, parameters): match = self.CPP_COMPONENT_REGEX['method_param'].match(param) if match: param_type, param_name, param_inline_comment = match.groups() - param_info[param_name] = (param_type, param_inline_comment) + custom_name = None + direction = None + if param_inline_comment: + text_match = re.search(r'@text:([\w\-]+)', param_inline_comment) + if text_match: + custom_name = text_match.group(1) + if '@in' in param_inline_comment: + direction = 'in' + elif '@out' in param_inline_comment: + direction = 'out' + param_info[param_name] = (param_type, param_inline_comment, custom_name, direction) else: self.logger.log("ERROR", f"Could not extract parameter information from: {param}") return param_info @@ -538,11 +550,15 @@ def generate_request_object(self, method_name, method_info): if method_info['params'] != []: request["params"] = {} for param in method_info['params']: - param_name = param.get('name') + # Use custom_name only for @in params + if param.get('direction') == 'in' and param.get('custom_name'): + param_name = param['custom_name'] + else: + param_name = param['name'] param_type = param.get('type') param_desc = param.get('description') request["params"][param_name] = self.get_symbol_example( - f"{param_name}-{param_type}", param_desc) + f"{param['name']}-{param_type}", param_desc) return request def generate_response_object(self, method_info): @@ -557,11 +573,15 @@ def generate_response_object(self, method_info): if method_info['results'] != []: response['result'] = {} for result in method_info['results']: - result_name = result.get('name') + # Use custom_name only for @out params + if result.get('direction') == 'out' and result.get('custom_name'): + result_name = result['custom_name'] + else: + result_name = result['name'] result_type = result.get('type') result_desc = result.get('description') response['result'][result_name] = self.get_symbol_example( - f"{result_name}-{result_type}", result_desc) + f"{result['name']}-{result_type}", result_desc) return response def get_symbol_example(self, unique_id, description): diff --git a/tools/md_from_h_generator/markdown_templates.py b/tools/md_from_h_generator/markdown_templates.py index c2301c00..b79c55a1 100644 --- a/tools/md_from_h_generator/markdown_templates.py +++ b/tools/md_from_h_generator/markdown_templates.py @@ -208,10 +208,15 @@ def generate_parameters_section(params, symbol_registry): if params: markdown += """| Name | Type | Description |\n| :-------- | :-------- | :-------- |\n""" for param in params: + display_name = param.get('custom_name') if param.get('direction') in ['in', 'inout'] and param.get('custom_name') else param['name'] flattened_params = symbol_registry[f"{param['name']}-{param['type']}"]['flattened_description'] for param_name, param_data in flattened_params.items(): cleaned_description = re.sub(r'e\.g\.\s*\".*?(? Date: Tue, 1 Jul 2025 15:36:09 -0400 Subject: [PATCH 3/6] Fix the event and method example template mixup --- .../md_from_h_generator/markdown_templates.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/tools/md_from_h_generator/markdown_templates.py b/tools/md_from_h_generator/markdown_templates.py index b79c55a1..fac1c469 100644 --- a/tools/md_from_h_generator/markdown_templates.py +++ b/tools/md_from_h_generator/markdown_templates.py @@ -129,6 +129,7 @@ ```json {request_json} +``` """ EXAMPLE_RESPONSE_TEMPLATE = """ @@ -137,6 +138,13 @@ ```json {response_json} +``` +""" + +EXAMPLE_NOTIFICATION_TEMPLATE = """ +```json +{request_json} +``` """ def generate_header_toc(classname, document_object, version="1.0.0"): @@ -246,25 +254,21 @@ def generate_request_section(request, method_type, classname=None): """ Generate the request section for a method. """ - # If classname is provided and the request has a 'method' field, update it if classname and isinstance(request, dict) and 'method' in request: - # Replace the class part in the method string with the correct classname parts = request['method'].split('.') if len(parts) > 2: parts[2] = classname request['method'] = '.'.join(parts) - request_json = json.dumps(request, indent=4) + request_json = json.dumps(_convert_json_types(request), indent=4) markdown = EXAMPLE_REQUEST_TEMPLATE.format(request_json=request_json, method_type=method_type) - markdown += "```" return markdown def generate_response_section(response, method_type, classname=None): """ Generate the response section for a method. """ - response_json = json.dumps(response, indent=4) + response_json = json.dumps(_convert_json_types(response), indent=4) markdown = EXAMPLE_RESPONSE_TEMPLATE.format(response_json=response_json, method_type=method_type) - markdown += "```" return markdown def generate_properties_toc(properties, classname): @@ -335,5 +339,34 @@ def generate_notification_markdown(event_name, event_info, symbol_registry, clas markdown = EVENT_MARKDOWN_TEMPLATE.format(event_name=event_name, event_description=event_info['brief'] or event_info['details']) markdown += generate_parameters_section(event_info['params'], symbol_registry) markdown += "\n### Examples\n" - markdown += generate_request_section(event_info['request'], '', classname) + request = event_info['request'] + if classname and isinstance(request, dict) and 'method' in request: + parts = request['method'].split('.') + if len(parts) > 2: + parts[2] = classname + request['method'] = '.'.join(parts) + request_json = json.dumps(_convert_json_types(request), indent=4) + markdown += EXAMPLE_NOTIFICATION_TEMPLATE.format(request_json=request_json) return markdown + +def _convert_json_types(obj): + """ + Recursively convert string numbers and 'true'/'false' strings to int/float/bool in a dict or list. + """ + if isinstance(obj, dict): + return {k: _convert_json_types(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [_convert_json_types(i) for i in obj] + elif isinstance(obj, str): + if obj.lower() == 'true': + return True + if obj.lower() == 'false': + return False + try: + if '.' in obj: + return float(obj) + return int(obj) + except ValueError: + return obj + else: + return obj From 378d4ae226c3d9f55a24e3ccc7f66080d25e8748 Mon Sep 17 00:00:00 2001 From: Arun Madhavan Date: Tue, 1 Jul 2025 15:42:14 -0400 Subject: [PATCH 4/6] Make the ID value unique --- .../generate_md_from_header.py | 10 ++++----- .../md_from_h_generator/markdown_templates.py | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/tools/md_from_h_generator/generate_md_from_header.py b/tools/md_from_h_generator/generate_md_from_header.py index 3986b4d7..5112dea6 100644 --- a/tools/md_from_h_generator/generate_md_from_header.py +++ b/tools/md_from_h_generator/generate_md_from_header.py @@ -48,21 +48,21 @@ def generate_md_from_header(header_file): file.write(generate_header_description_markdown(header_structure.classname, getattr(header_structure, 'plugindescription', ''))) if len(header_structure.methods.values()) > 0: file.write(generate_methods_toc(header_structure.methods, header_structure.classname)) - for method_name, method_info in header_structure.methods.items(): + for idx, (method_name, method_info) in enumerate(header_structure.methods.items(), start=1): file.write(generate_method_markdown( - method_name, method_info, header_structure.symbols_registry, header_structure.classname)) + method_name, method_info, header_structure.symbols_registry, header_structure.classname, idx)) file.write("\n") if len(header_structure.properties.values()) > 0: file.write(generate_properties_toc(header_structure.properties, header_structure.classname)) - for prop_name, prop_info in header_structure.properties.items(): + for idx, (prop_name, prop_info) in enumerate(header_structure.properties.items(), start=1): file.write(generate_property_markdown( prop_name, prop_info, header_structure.symbols_registry, header_structure.classname)) file.write("\n") if len(header_structure.events.values()) > 0: file.write(generate_notifications_toc(header_structure.events, header_structure.classname)) - for event_name, event_info in header_structure.events.items(): + for idx, (event_name, event_info) in enumerate(header_structure.events.items(), start=1): file.write(generate_notification_markdown( - event_name, event_info, header_structure.symbols_registry, header_structure.classname)) + event_name, event_info, header_structure.symbols_registry, header_structure.classname, idx)) logger.write_log() logger.close() diff --git a/tools/md_from_h_generator/markdown_templates.py b/tools/md_from_h_generator/markdown_templates.py index fac1c469..d88b57d4 100644 --- a/tools/md_from_h_generator/markdown_templates.py +++ b/tools/md_from_h_generator/markdown_templates.py @@ -182,7 +182,7 @@ def generate_methods_toc(methods, classname): toc += f"| [{method}](#method.{method}) | {method_body['brief'] or method_body['details']} |\n" return toc -def generate_method_markdown(method_name, method_info, symbol_registry, classname): +def generate_method_markdown(method_name, method_info, symbol_registry, classname, example_id): """ Generate the markdown for a specific method. """ @@ -191,8 +191,8 @@ def generate_method_markdown(method_name, method_info, symbol_registry, classnam markdown += generate_parameters_section(method_info['params'], symbol_registry) markdown += generate_results_section(method_info['results'], symbol_registry) markdown += "\n### Examples\n" - markdown += generate_request_section(method_info['request'], '', classname) - markdown += generate_response_section(method_info['response'], '', classname) + markdown += generate_request_section(method_info['request'], '', classname, example_id) + markdown += generate_response_section(method_info['response'], '', classname, example_id) return markdown def generate_events_section(events): @@ -250,7 +250,7 @@ def generate_results_section(results, symbol_registry): markdown += "This method returns no results.\n" return markdown -def generate_request_section(request, method_type, classname=None): +def generate_request_section(request, method_type, classname=None, example_id=42): """ Generate the request section for a method. """ @@ -259,14 +259,21 @@ def generate_request_section(request, method_type, classname=None): if len(parts) > 2: parts[2] = classname request['method'] = '.'.join(parts) + # Set the id + if isinstance(request, dict): + request = dict(request) # shallow copy + request['id'] = example_id request_json = json.dumps(_convert_json_types(request), indent=4) markdown = EXAMPLE_REQUEST_TEMPLATE.format(request_json=request_json, method_type=method_type) return markdown -def generate_response_section(response, method_type, classname=None): +def generate_response_section(response, method_type, classname=None, example_id=42): """ Generate the response section for a method. """ + if isinstance(response, dict): + response = dict(response) + response['id'] = example_id response_json = json.dumps(_convert_json_types(response), indent=4) markdown = EXAMPLE_RESPONSE_TEMPLATE.format(response_json=response_json, method_type=method_type) return markdown @@ -332,7 +339,7 @@ def generate_notifications_toc(events, classname): toc += f"| [{event}](#event.{event}) | {event_body['brief'] or event_body['details']} |\n" return toc -def generate_notification_markdown(event_name, event_info, symbol_registry, classname): +def generate_notification_markdown(event_name, event_info, symbol_registry, classname, example_id): """ Generate the markdown for a specific event. """ @@ -345,6 +352,9 @@ def generate_notification_markdown(event_name, event_info, symbol_registry, clas if len(parts) > 2: parts[2] = classname request['method'] = '.'.join(parts) + if isinstance(request, dict): + request = dict(request) + request['id'] = example_id request_json = json.dumps(_convert_json_types(request), indent=4) markdown += EXAMPLE_NOTIFICATION_TEMPLATE.format(request_json=request_json) return markdown From 67e011c04cbe429ad363006940c2cd884149c3d2 Mon Sep 17 00:00:00 2001 From: Arun Madhavan Date: Tue, 1 Jul 2025 21:18:53 -0400 Subject: [PATCH 5/6] Logic to handle @see, to add proper description for @text overrides and camelCase change in generated MD --- apis/FrameRate/IFrameRate.h | 12 +- tools/md_from_h_generator/README.md | 19 +- .../generate_md_from_header.py | 3 +- .../md_from_h_generator/header_file_parser.py | 88 +++++++++- .../md_from_h_generator/markdown_templates.py | 166 ++++++++++-------- 5 files changed, 195 insertions(+), 93 deletions(-) diff --git a/apis/FrameRate/IFrameRate.h b/apis/FrameRate/IFrameRate.h index 6b0fb8b0..d483569e 100644 --- a/apis/FrameRate/IFrameRate.h +++ b/apis/FrameRate/IFrameRate.h @@ -38,23 +38,20 @@ namespace WPEFramework enum { ID = ID_FRAMERATE_NOTIFICATION }; // @text onFpsEvent - // @brief Triggered by callback from FrameRate after onFpsEvent - // @details Triggered at the end of each interval as defined by the setCollectionFrequency method - // after StartFpsCollection method and once after the stopFpsCollection method is invoked + // @details Triggered at the end of each interval as defined by the `setCollectionFrequency` method + // after `startFpsCollection` method and once after the `stopFpsCollection` method is invoked // @param average: The average FPS e.g. 60 // @param min: The minimum FPS e.g. 30 // @param max: The maximum FPS e.g. 120 virtual void OnFpsEvent(const int average /* @in */, const int min /* @in */, const int max /* @in */) {}; // @text onDisplayFrameRateChanging - // @brief Triggered when the framerate change is starting. // @details This event is triggered when the display frame rate is about to change // and represented as "WIDTHxHEIGHTxFPS". // @param displayFrameRate: The new display frame rate e.g. "1920x1080x60" virtual void OnDisplayFrameRateChanging(const string& displayFrameRate /* @in */) {}; // @text onDisplayFrameRateChanged - // @brief Triggered when the framerate has changed. // @details This event is triggered after the display frame rate has changed // and represented as "WIDTHxHEIGHTxFPS". // @param displayFrameRate: The new display frame rate e.g. "1920x1080x60" @@ -108,8 +105,8 @@ namespace WPEFramework /** Starts the FPS data collection */ // @text startFpsCollection - // @details Starts collecting FPS data at the configured interval set by the method SetCollectionFrequency. - // @see SetCollectionFrequency. + // @details Starts collecting FPS data at the configured interval set by the method `SetCollectionFrequency`. + // @see onFpsEvent // @param success: Indicates if the operation was successful. e.g. True // @returns Core::hresult virtual Core::hresult StartFpsCollection(bool& success /* @out */) = 0; @@ -124,6 +121,7 @@ namespace WPEFramework /** Update the FPS value */ // @text updateFps // @details Updates the current FPS value to the specified value represented as integer. + // @see onFpsEvent // @param newFpsValue: The new FPS value. e.g. 60 // @param success: Indicates if the operation was successful. e.g. True // @returns Core::hresult diff --git a/tools/md_from_h_generator/README.md b/tools/md_from_h_generator/README.md index d899afb5..522914be 100644 --- a/tools/md_from_h_generator/README.md +++ b/tools/md_from_h_generator/README.md @@ -42,7 +42,7 @@ virtual uint32_t initialize(); - **Required**: Yes (Mandatory tag for all methods/properties/events) - **Usage**: - Use this tag for a short, one-line description of the method. - - The description following this tag will be shown on the method/property/event table of contents + - The description following this tag will be shown on the method/property/event table of contents ### Example: @@ -98,7 +98,7 @@ virtual uint32_t initialize(); - **Required**: No (Optional tag) - **Usage**: - Use this tag to reference related methods, classes, or external documentation. - - The linked event name should appear exactly as it is declared, without parenthesis + - The linked event name should appear exactly as it is declared, without parenthesis - This tag is optional, but should be used if a corresponding event is defined in INotifications ### Example: @@ -140,28 +140,29 @@ virtual void onInitialize(); - Parameter/symbol examples should be defined here (see [Providing Symbol Examples](#providing_examples)) - Specify the parameter name and its description. Format can include colon i.e. `@param [param_name]: [description]` or `@param [para_name] [description]` - IMPORTANTLY, in addition to using the param tag, mark each parameter with inline 'in/out' information in the parameter list. If a parameter does not have inline in/out information, it defaults to 'in'. + - Additionally a parameter name override can be specified by combining `@in` or `@out` followed by `@text:varible-override-name` in the function declaration. ### Example: ***Header File Example:*** ```cpp -/** +/** ... * @param configPath: The config file path for initialization e.g. "../build/test.conf" * @param status The status of the initialization. Set to true if completed. */ -virtual uint32_t initialize(const string& configPath /* @in */, bool status /* @out */); +virtual uint32_t initialize(const string& configPath /* @in @text:config-path */, bool status /* @out @text:status-response */); ``` ***Generated Markdown Example:*** > ### Parameters > | Name | Type | Description | > | :-------- | :-------- | :-------- | -> | config | string | The config file path for initialization | +> | config-path | string | The config file path for initialization | > ### Results > | Name | Type | Description | > | :-------- | :-------- | :-------- | -> | status | bool | The status of the initialization. Set to true if completed. | +> | status-response | bool | The status of the initialization. Set to true if completed. | --- @@ -209,7 +210,7 @@ virtual uint32_t internalMethod(); **Example**: ```cpp -/* +/* * @property * @brief Video output port on the STB used for connection to TV * @param name: video output port name @@ -223,7 +224,7 @@ virtual uint32_t PortName (string& name /* @out */) const = 0; -### Providing Symbol Examples +### Providing Symbol Examples In the RDK Services and Entservices APIs, plugins communicate using RPC. To facilitate this, the documentation includes relevant examples of request and response JSON structures. The md_from_h tool automates the creation of these examples by parsing enums, structs, iterators, and basic types declared in the header file, as well as extracting examples from @param Doxygen tags. The tool maintains a global symbol registry to track the names and types of parameters declared in methods, properties, events, enums, and struct members. The goal of the global symbol registry is to make it easier and more consistent to provide examples for symbols which appear multiple times in a header file (such as preferredLanguages in IUserSettings.h). Examples are generated by analyzing the @param tags, where the tool uses a regular expression to extract text following the pattern `e.g. "(.*)"` in the parameter description. The value inside the quotes is then used as the example for that symbol. The pattern `ex: (.*)` is also matched in cases where examples have double-quotes. Additionally, examples can be derived from structs if their members include descriptive comments. @@ -258,7 +259,7 @@ The following demonstrates how examples are set for struct members: ***Header File Example:*** ```cpp -struct USBDevice { +struct USBDevice { uint8_t deviceClass /* @brief USB class of the device as per USB specification e.g. "10" */ ; uint8_t deviceSubclass /* @brief USB sub class of the device as per USB specification e.g. "6" */; string deviceName /* @brief Name of the USB device e.g. "001/003"*/; diff --git a/tools/md_from_h_generator/generate_md_from_header.py b/tools/md_from_h_generator/generate_md_from_header.py index 5112dea6..763d1b48 100644 --- a/tools/md_from_h_generator/generate_md_from_header.py +++ b/tools/md_from_h_generator/generate_md_from_header.py @@ -39,6 +39,7 @@ def generate_md_from_header(header_file): logger = Logger(log_file_path) header_structure = HeaderFileParser(header_file, logger) + header_structure.build_all_canonical_dicts() # Remove the leading 'I' from the api's class name output_file_path = './tools/md_from_h_generator/generated_docs/' + header_structure.classname + 'Plugin.md' @@ -50,7 +51,7 @@ def generate_md_from_header(header_file): file.write(generate_methods_toc(header_structure.methods, header_structure.classname)) for idx, (method_name, method_info) in enumerate(header_structure.methods.items(), start=1): file.write(generate_method_markdown( - method_name, method_info, header_structure.symbols_registry, header_structure.classname, idx)) + method_name, method_info, header_structure.symbols_registry, header_structure.classname, idx, header_structure.events)) file.write("\n") if len(header_structure.properties.values()) > 0: file.write(generate_properties_toc(header_structure.properties, header_structure.classname)) diff --git a/tools/md_from_h_generator/header_file_parser.py b/tools/md_from_h_generator/header_file_parser.py index 26b48d00..6731ac3a 100644 --- a/tools/md_from_h_generator/header_file_parser.py +++ b/tools/md_from_h_generator/header_file_parser.py @@ -36,7 +36,7 @@ class HeaderFileParser: ('text', 'doxygen', re.compile(r'(?:\/\*+|\*|//) (?:@text|@alt)\s+(.*?)(?=\s*\*\/|$)')), ('brief', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @brief\s*(.*?)(?=\s*\*\/|$)')), ('details', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @details\s*(.*?)(?=\s*\*\/|$)')), - ('params', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @param(?:\[.*\])?\s+(\w+)\s*\:?\s*(.*?)(?=\s*\*\/|$)')), + ('params', 'doxygen', re.compile(r'(?:\/\*+|\*|//)\s*@param(?:\[.*\])?\s+([^\s:]+)\s*:?\s*(.*?)(?=\s*\*\/|$)')), ('return', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @return(?:s)?\s*(.*?)(?=\s*\*\/|$)')), ('see', 'doxygen', re.compile(r'(?:\/\*+|\*|//) @see\s*(.*?)(?=\s*\*\/|$)')), ('omit', 'doxygen', re.compile(r'(?:\/\*+|\*|//)\s*(@json:omit|@omit)')), @@ -252,27 +252,32 @@ def process_struct(self, line, struct_object, within_struct_def, struct_braces_c def update_doxy_tags(self, groups, line_tag): """ Updates the doxygen tag object with the given line's information. + Supports multiline for all tags by accumulating lines until a new tag is found. """ if line_tag == 'plugindescription': self.plugindescription = groups[0] elif line_tag == 'text': self.doxy_tags['text'] = groups[0] + self.latest_tag = 'text' elif line_tag == 'params': self.latest_param = groups[0] self.latest_tag = 'params' self.doxy_tags.setdefault('params', {})[self.latest_param] = groups[1] elif line_tag == 'see': self.doxy_tags.setdefault('see', {})[groups[0]] = '' + self.latest_tag = 'see' elif line_tag == 'comment': if groups[0] == '/': return - elif self.latest_tag == 'params': + # Multiline support: append to last tag + if self.latest_tag == 'params': self.doxy_tags['params'][self.latest_param] += (' ' + groups[0]) elif self.latest_tag and self.latest_tag in self.doxy_tags and self.latest_tag != 'plugindescription': self.doxy_tags[self.latest_tag] += (' ' + groups[0]) line_tag = self.latest_tag else: self.doxy_tags[line_tag] = groups[0] + self.latest_tag = line_tag if line_tag != 'plugindescription': self.latest_tag = line_tag @@ -423,30 +428,65 @@ def process_and_register_params(self, method_parameters, doxy_tag_param_info): Helper to build params and results data structures, using the parameter declaration list and doxygen tags. """ + def normalize_key(key): + return key.replace('_', '-').lower().strip() + + # Build a normalized lookup for param descriptions + normalized_param_info = {normalize_key(k): v for k, v in doxy_tag_param_info.items()} + + # DEBUG: Print the doxygen param info and normalized lookup + self.logger.log("INFO", f"doxy_tag_param_info: {doxy_tag_param_info}") + self.logger.log("INFO", f"normalized_param_info: {normalized_param_info}") + param_list_info = self.get_info_from_param_declaration(method_parameters) + self.logger.log("INFO", f"param_list_info: {param_list_info}") params = [] results = [] - # build the params and results lists using the parameter delcaration list and doxygen tags for symbol_name, (symbol_type, symbol_inline_comment, custom_name, direction) in param_list_info.items(): - # register string iterators here b/c they are seldom defined outside of a method param + self.logger.log("INFO", f"Processing param: symbol_name={symbol_name}, symbol_type={symbol_type}, custom_name={custom_name}, direction={direction}, symbol_inline_comment={symbol_inline_comment}") if symbol_type == 'RPC::IStringIterator': self.register_iterator(symbol_type) symbol_description = doxy_tag_param_info.get(symbol_name, '') + custom_description = None + if symbol_inline_comment: + text_match = re.search(r'@text:([\w\-]+)', symbol_inline_comment) + if text_match: + override_name = text_match.group(1) + custom_name = override_name + norm_override = normalize_key(override_name) + self.logger.log("INFO", f"Found @text override: override_name={override_name}, norm_override={norm_override}") + # Prefer description from normalized doxygen @param for override name + if norm_override in normalized_param_info: + custom_description = normalized_param_info[norm_override] + self.logger.log("INFO", f"Found custom_description for override: {custom_description}") + else: + self.logger.log("INFO", f"Override param name '{override_name}' not found in @param tags for method param '{symbol_name}'.") + self.logger.log("WARNING", f"Override param name '{override_name}' not found in @param tags for method param '{symbol_name}'.") + # Fallback to original param name if not found + if not custom_description and normalize_key(symbol_name) in normalized_param_info: + custom_description = normalized_param_info[normalize_key(symbol_name)] + self.logger.log("INFO", f"Fallback to original param name: {custom_description}") + if not custom_description and normalize_key(symbol_name) in normalized_param_info: + custom_description = normalized_param_info[normalize_key(symbol_name)] + self.logger.log("INFO", f"No override, using original param name: {custom_description}") + if custom_description: + custom_description = re.sub(r'e\.g\.\s*\".*?(? 2: + parts[2] = classname + request['method'] = '.'.join(parts) + # Set the id + if isinstance(request, dict): + request = dict(request) # shallow copy + request['id'] = example_id + request_json = json.dumps(_convert_json_types(request), indent=4) + markdown = EXAMPLE_REQUEST_TEMPLATE.format(request_json=request_json, method_type=method_type) return markdown -def generate_events_section(events): +def generate_response_section(response, method_type, classname=None, example_id=42): """ - Generate the events section for a method. + Generate the response section for a method. """ - markdown = "### Events\n" - if events: - markdown += """| Event | Description |\n| :-------- | :-------- |\n""" - for event in events: - markdown += f"| [{event}](#event.{event}) | {events[event]} |\n" - else: - markdown += "No events are associated with this method.\n" + if isinstance(response, dict): + response = dict(response) + response['id'] = example_id + response_json = json.dumps(_convert_json_types(response), indent=4) + markdown = EXAMPLE_RESPONSE_TEMPLATE.format(response_json=response_json, method_type=method_type) return markdown def generate_parameters_section(params, symbol_registry): """ - Generate the parameters section for a method. + Generate the parameters section for a method, showing the parent object and all fields for all params, using override names and descriptions if present. """ markdown = "### Parameters\n" if params: - markdown += """| Name | Type | Description |\n| :-------- | :-------- | :-------- |\n""" + markdown += "| Name | Type | Description |\n| :-------- | :-------- | :-------- |\n" + markdown += f"| params | object | |\n" for param in params: - display_name = param.get('custom_name') if param.get('direction') in ['in', 'inout'] and param.get('custom_name') else param['name'] - flattened_params = symbol_registry[f"{param['name']}-{param['type']}"]['flattened_description'] - for param_name, param_data in flattened_params.items(): - cleaned_description = re.sub(r'e\.g\.\s*\".*?(? 2: - parts[2] = classname - request['method'] = '.'.join(parts) - # Set the id - if isinstance(request, dict): - request = dict(request) # shallow copy - request['id'] = example_id - request_json = json.dumps(_convert_json_types(request), indent=4) - markdown = EXAMPLE_REQUEST_TEMPLATE.format(request_json=request_json, method_type=method_type) + camel_method = to_camel_case(method_name) + markdown = METHOD_MARKDOWN_TEMPLATE.format(method_name=camel_method, method_description=method_info['brief'] or method_info['details']) + markdown += generate_events_section(method_info['events'], all_events) + # Use canonical dicts for tables + markdown += generate_parameters_section_from_canonical(method_info.get('canonical_params', {})) + markdown += generate_results_section_from_canonical(method_info.get('canonical_results', {})) + markdown += "\n### Examples\n" + markdown += generate_request_section(method_info['request'], '', classname, example_id) + markdown += generate_response_section(method_info['response'], '', classname, example_id) return markdown -def generate_response_section(response, method_type, classname=None, example_id=42): +def generate_events_section(events, all_events=None): """ - Generate the response section for a method. + Generate the events section for a method. + all_events: dict of all event definitions (from document_object.events) """ - if isinstance(response, dict): - response = dict(response) - response['id'] = example_id - response_json = json.dumps(_convert_json_types(response), indent=4) - markdown = EXAMPLE_RESPONSE_TEMPLATE.format(response_json=response_json, method_type=method_type) + markdown = "### Events\n" + if events: + # Only show a list of links to events, not a table + for event in events: + camel_event = to_camel_case(event) + markdown += f"- [{camel_event}](#event.{camel_event})\n" + else: + markdown += "No events are associated with this method.\n" return markdown def generate_properties_toc(properties, classname): @@ -333,17 +359,19 @@ def generate_notifications_toc(events, classname): """ Generate the notifications table of contents for the markdown file. """ - toc = EVENTS_TOC_TEMPLATE.format(classname=classname) + toc = EVENTS_TOC_TEMPLATE.replace('| Method |', '| Event |').format(classname=classname) for event in events: event_body = events[event] - toc += f"| [{event}](#event.{event}) | {event_body['brief'] or event_body['details']} |\n" + camel_event = to_camel_case(event) + toc += f"| [{camel_event}](#event.{camel_event}) | {event_body['brief'] or event_body['details']} |\n" return toc def generate_notification_markdown(event_name, event_info, symbol_registry, classname, example_id): """ Generate the markdown for a specific event. """ - markdown = EVENT_MARKDOWN_TEMPLATE.format(event_name=event_name, event_description=event_info['brief'] or event_info['details']) + camel_event = to_camel_case(event_name) + markdown = EVENT_MARKDOWN_TEMPLATE.format(event_name=camel_event, event_description=event_info['brief'] or event_info['details']) markdown += generate_parameters_section(event_info['params'], symbol_registry) markdown += "\n### Examples\n" request = event_info['request'] From c313d53e517c95b777148268a4ee2e0970c5159b Mon Sep 17 00:00:00 2001 From: Arun Madhavan Date: Tue, 1 Jul 2025 21:41:16 -0400 Subject: [PATCH 6/6] Update the ReadMe.md with @plugindescription usage --- tools/md_from_h_generator/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tools/md_from_h_generator/README.md b/tools/md_from_h_generator/README.md index 522914be..aaa16dec 100644 --- a/tools/md_from_h_generator/README.md +++ b/tools/md_from_h_generator/README.md @@ -218,6 +218,33 @@ virtual uint32_t internalMethod(); virtual uint32_t PortName (string& name /* @out */) const = 0; ``` +### 9. `@plugindescription` +- **Purpose**: Provides option to override the generic plugin description text +- **Required**: No (Mandatory tag if method is a property) +- **Usage**: + - Use this tag for overriding the generic plugin description. + +**Example**: +```cpp + namespace Exchange + { + /* @json 1.0.0 @text:keep */ + // @plugindescription This plugin provides so and so functionalities + struct EXTERNAL IClassName : virtual public Core::IUnknown + { +``` + +***Generated Markdown Example:*** + +> +> # Description +> +> This plugin provides so and so functionalities +> +> The plugin is designed to be loaded and executed within the Thunder framework. For more information about the framework refer > > to [[Thunder](#ref.Thunder)]. +> +> + --- ## 4. Additional Features and Guidelines