1# Copyright (c) 2023 Intel Corporation
2# SPDX-License-Identifier: Apache-2.0
3
4import os
5from pathlib import Path
6from typing import Any
7
8import doxmlparser
9from docutils import nodes
10from doxmlparser.compound import DoxCompoundKind
11from sphinx.util.docutils import SphinxDirective
12
13
14def get_group(innergroup, all_groups):
15    try:
16        return [
17            group
18            for group in all_groups
19            if group.get_compounddef()[0].get_id() == innergroup.get_refid()
20        ][0]
21    except IndexError as e:
22        raise Exception(f"Unexpected group {innergroup.get_refid()}") from e
23
24
25def parse_xml_dir(dir_name):
26    groups = []
27    root = doxmlparser.index.parse(Path(dir_name) / "index.xml", True)
28    for compound in root.get_compound():
29        if compound.get_kind() == DoxCompoundKind.GROUP:
30            file_name = Path(dir_name) / f"{compound.get_refid()}.xml"
31            groups.append(doxmlparser.compound.parse(file_name, True))
32
33    return groups
34
35
36class ApiOverview(SphinxDirective):
37    """
38    This is a Zephyr directive to generate a table containing an overview
39    of all APIs. This table will show the API name, version and since which
40    version it is present - all information extracted from Doxygen XML output.
41
42    It is exclusively used by the doc/develop/api/overview.rst page.
43
44    Configuration options:
45
46    api_overview_doxygen_out_dir: Doxygen output directory
47    """
48
49    def run(self):
50        groups = parse_xml_dir(self.config.api_overview_doxygen_out_dir + "/xml")
51
52        inners = [group.get_compounddef()[0].get_innergroup() for group in groups]
53        inner_ids = [i.get_refid() for inner in inners for i in inner]
54        toplevel = [
55            group for group in groups if group.get_compounddef()[0].get_id() not in inner_ids
56        ]
57
58        return [self.generate_table(toplevel, groups)]
59
60    def generate_table(self, toplevel, groups):
61        table = nodes.table()
62        tgroup = nodes.tgroup()
63
64        thead = nodes.thead()
65        thead_row = nodes.row()
66        for header_name in ["API", "Version", "Available in Zephyr Since"]:
67            colspec = nodes.colspec()
68            tgroup += colspec
69
70            entry = nodes.entry()
71            entry += nodes.Text(header_name)
72            thead_row += entry
73        thead += thead_row
74        tgroup += thead
75
76        rows = []
77        tbody = nodes.tbody()
78        for t in toplevel:
79            self.visit_group(t, groups, rows)
80        tbody.extend(rows)
81        tgroup += tbody
82
83        table += tgroup
84
85        return table
86
87    def visit_group(self, group, all_groups, rows, indent=0):
88        version = since = ""
89        github_uri = self.config.api_overview_base_url + "/releases/tag/"
90        cdef = group.get_compounddef()[0]
91
92        ssects = [
93            s for p in cdef.get_detaileddescription().get_para() for s in p.get_simplesect()
94        ]
95        for sect in ssects:
96            if sect.get_kind() == "since":
97                since = sect.get_para()[0].get_valueOf_()
98            elif sect.get_kind() == "version":
99                version = sect.get_para()[0].get_valueOf_()
100
101        if since:
102            since_url = nodes.inline()
103            reference = nodes.reference(
104                text=f"v{since.strip()}.0", refuri=f"{github_uri}/v{since.strip()}.0"
105            )
106            reference.attributes["internal"] = True
107            since_url += reference
108        else:
109            since_url = nodes.Text("")
110
111        url_base = Path(self.config.api_overview_doxygen_out_dir + "/html")
112        abs_url = url_base / f"{cdef.get_id()}.html"
113        doc_dir = os.path.dirname(self.get_source_info()[0])
114        doc_dest = os.path.join(
115            self.env.app.outdir,
116            os.path.relpath(doc_dir, self.env.app.srcdir),
117        )
118        url = os.path.relpath(abs_url, doc_dest)
119
120        title = cdef.get_title()
121
122        row_node = nodes.row()
123
124        # Next entry will contain the spacer and the link with API name
125        entry = nodes.entry()
126        span = nodes.Text("".join(["\U000000A0"] * indent))
127        entry += span
128
129        # API name with link
130        inline = nodes.inline()
131        reference = nodes.reference(text=title, refuri=str(url))
132        reference.attributes["internal"] = True
133        inline += reference
134        entry += inline
135        row_node += entry
136
137        version_node = nodes.Text(version)
138        # Finally, add version and since
139        for cell in [version_node, since_url]:
140            entry = nodes.entry()
141            entry += cell
142            row_node += entry
143        rows.append(row_node)
144
145        for innergroup in cdef.get_innergroup():
146            self.visit_group(
147                get_group(innergroup, all_groups), all_groups, rows, indent + 6
148            )
149
150
151def setup(app) -> dict[str, Any]:
152    app.add_config_value("api_overview_doxygen_out_dir", "", "env")
153    app.add_config_value("api_overview_base_url", "", "env")
154
155    app.add_directive("api-overview-table", ApiOverview)
156
157    return {
158        "version": "0.1",
159        "parallel_read_safe": True,
160        "parallel_write_safe": True,
161    }
162