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