| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """ | |
| Generate HTML documentation for Device Interface Functions (DIFs) | |
| """ | |
| import copy | |
| import logging as log | |
| import subprocess | |
| import xml.etree.ElementTree as ET | |
| # Turn the Doxygen multi-file XML output into one giant XML file (and parse it | |
| # into a python object), using the provided XLST file. | |
| def get_combined_xml(doxygen_xml_path): | |
| xsltproc_args = [ | |
| "xsltproc", | |
| str(doxygen_xml_path.joinpath("combine.xslt")), | |
| str(doxygen_xml_path.joinpath("index.xml")), | |
| ] | |
| combined_xml_res = subprocess.run(xsltproc_args, check=True, | |
| cwd=str(doxygen_xml_path), stdout=subprocess.PIPE, | |
| universal_newlines=True) | |
| return ET.fromstring(combined_xml_res.stdout) | |
| # Get all information about individual DIF functions that are specified in one | |
| # DIF header. This returns only the Info from the XML that we require. | |
| def get_difref_info(combined_xml, dif_header): | |
| compound = _get_dif_file_compound(combined_xml, dif_header) | |
| if compound == None: | |
| return [] | |
| file_id = _get_dif_file_id(compound) | |
| functions = _get_dif_function_info(compound, file_id) | |
| return functions | |
| # Create HTML List of DIFs, using the info from the combined xml | |
| def gen_listing_html(combined_xml, dif_header, dif_listings_html): | |
| compound = _get_dif_file_compound(combined_xml, dif_header) | |
| if compound == None: | |
| log.error("Doxygen output not found for {}".format(dif_header)) | |
| return | |
| file_id = _get_dif_file_id(compound) | |
| functions = _get_dif_function_info(compound, file_id) | |
| if len(functions) == 0: | |
| log.error("No DIF functions found for {}".format(dif_header)) | |
| return | |
| # Generate DIF listing header | |
| dif_listings_html.write('<p>To use this DIF, include the following C header:</p>') | |
| dif_listings_html.write('<pre><code class=language-c data-lang=c>') | |
| dif_listings_html.write('#include "<a href="/sw/apis/{}.html">{}</a>"'.format(file_id, dif_header)) | |
| dif_listings_html.write('</code></pre>\n') | |
| # Generate DIF function list. | |
| dif_listings_html.write('<p>This header provides the following device interface functions:</p>') | |
| dif_listings_html.write('<ul>\n') | |
| for f in sorted(functions, key=lambda x: x['name']): | |
| dif_listings_html.write('<li title="{prototype}" id="Dif_{name}">'.format(**f)) | |
| dif_listings_html.write('<a href="{full_url}">'.format(**f)) | |
| dif_listings_html.write('<code>{name}</code>'.format(**f)) | |
| dif_listings_html.write('</a>\n') | |
| dif_listings_html.write(f['description']) | |
| dif_listings_html.write('</li>\n') | |
| dif_listings_html.write('</ul>\n') | |
| # Generate HTML link for single function, using info returned from | |
| # get_difref_info | |
| def gen_difref_html(function_info, difref_html): | |
| difref_html.write('<a href="{full_url}">'.format(**function_info)) | |
| difref_html.write('<code>{name}</code>'.format(**function_info)) | |
| difref_html.write('</a>\n') | |
| def _get_dif_file_compound(combined_xml, dif_header): | |
| for c in combined_xml.findall('compounddef[@kind="file"]'): | |
| if c.find("location").attrib["file"] == dif_header: | |
| return c | |
| return None | |
| def _get_dif_file_id(compound): | |
| return compound.attrib["id"] | |
| def _get_dif_function_info(compound, file_id): | |
| funcs = compound.find('sectiondef[@kind="func"]') | |
| if funcs == None: | |
| return [] | |
| # Collect useful info on each function | |
| functions = [] | |
| for m in funcs.findall('memberdef[@kind="function"]'): | |
| func_id = m.attrib['id'] | |
| # Strip refid prefix, which is separated from the funcid by `_1` | |
| if func_id.startswith(file_id + '_1'): | |
| # The +2 here is because of the weird `_1` separator | |
| func_id = func_id[len(file_id) + 2:] | |
| else: | |
| # I think this denotes that this function isn't from this file | |
| continue | |
| func_info = {} | |
| func_info["id"] = m.attrib["id"] | |
| func_info["file_id"] = file_id | |
| func_info["local_id"] = func_id | |
| func_info["full_url"] = "/sw/apis/{}.html#{}".format(file_id, func_id) | |
| func_info["name"] = _get_text_or_empty(m, "name") | |
| func_info["prototype"] = _get_text_or_empty( | |
| m, "definition") + _get_text_or_empty(m, "argsstring") | |
| func_info["description"] = _get_html_or_empty(m, | |
| "briefdescription/para") | |
| functions.append(func_info) | |
| return functions | |
| def _get_html_or_empty(element: ET.Element, xpath: str) -> str: | |
| """ Get a minimal HTML-rendering of the children in an element. | |
| element is expected to be a docCmdGroup according to the DoxyGen schema [1], | |
| but only a very minimal subset of formatting is transferred to semantic | |
| HTML. However, the tag structure is retained by transforming all tags into | |
| HTML `span` tags with a class attribute `doxygentag-ORIGINALTAGNAME`. This | |
| can be used to write CSS targeting specific Doxygen tags and recreate the | |
| intended formatting. | |
| In addtion, the following semantic transformations are performed: | |
| - `computeroutput` is transformed to `code` | |
| [1] https://github.com/doxygen/doxygen/blob/master/templates/xml/compound.xsd""" | |
| inner = element.find(xpath) | |
| # if the element isn't found, return "" | |
| if inner is None: | |
| return "" | |
| # Avoid modifying the passed element argument. | |
| inner_copy = copy.deepcopy(inner) | |
| for c in inner_copy.iter(): | |
| c.set('class', 'doxygentag-' + c.tag) | |
| if c.tag == 'computeroutput': | |
| c.tag = 'code' | |
| else: | |
| c.tag = 'span' | |
| # Create a string from all subelements | |
| text = ET.tostring(inner_copy, encoding="unicode", method="html") | |
| return text or "" | |
| def _get_text_or_empty(element: ET.Element, xpath: str) -> str: | |
| """ Get all text of an element, without any tags """ | |
| inner = element.find(xpath) | |
| if inner is None: | |
| return "" | |
| return ' '.join([e for e in inner.itertext()]) or "" |