1# Copyright (c) 2023 Intel Corporation
2# SPDX-License-Identifier: Apache-2.0
3
4from pathlib import Path
5from typing import Any
6
7import doxmlparser
8from docutils import nodes
9from doxmlparser.compound import DoxCompoundKind
10from sphinx.application import Sphinx
11from sphinx.util.docutils import SphinxDirective
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(
60            text=f"v{since.strip()}.0", refuri=f"{github_uri}/v{since.strip()}.0"
61        )
62        reference.attributes["internal"] = True
63        since_url += reference
64    else:
65        since_url = nodes.Text("")
66
67    url_base = Path(app.config.api_overview_doxygen_base_url)
68    url = url_base / f"{cdef.get_id()}.html"
69
70    title = cdef.get_title()
71
72    row_node = nodes.row()
73
74    # Next entry will contain the spacer and the link with API name
75    entry = nodes.entry()
76    span = nodes.Text("".join(["\U000000A0"] * indent))
77    entry += span
78
79    # API name with link
80    inline = nodes.inline()
81    reference = nodes.reference(text=title, refuri=str(url))
82    reference.attributes["internal"] = True
83    inline += reference
84    entry += inline
85    row_node += entry
86
87    version_node = nodes.Text(version)
88    # Finally, add version and since
89    for cell in [version_node, since_url]:
90        entry = nodes.entry()
91        entry += cell
92        row_node += entry
93    rows.append(row_node)
94
95    for innergroup in cdef.get_innergroup():
96        visit_group(
97            app, get_group(innergroup, all_groups), all_groups, rows, indent + 6
98        )
99
100
101def parse_xml_dir(dir_name):
102    groups = []
103    root = doxmlparser.index.parse(Path(dir_name) / "index.xml", True)
104    for compound in root.get_compound():
105        if compound.get_kind() == DoxCompoundKind.GROUP:
106            file_name = Path(dir_name) / f"{compound.get_refid()}.xml"
107            groups.append(doxmlparser.compound.parse(file_name, True))
108
109    return groups
110
111
112def generate_table(app, toplevel, groups):
113    table = nodes.table()
114    tgroup = nodes.tgroup()
115
116    thead = nodes.thead()
117    thead_row = nodes.row()
118    for header_name in ["API", "Version", "Available in Zephyr Since"]:
119        colspec = nodes.colspec()
120        tgroup += colspec
121
122        entry = nodes.entry()
123        entry += nodes.Text(header_name)
124        thead_row += entry
125    thead += thead_row
126    tgroup += thead
127
128    rows = []
129    tbody = nodes.tbody()
130    for t in toplevel:
131        visit_group(app, t, groups, rows)
132    tbody.extend(rows)
133    tgroup += tbody
134
135    table += tgroup
136
137    return table
138
139
140def sync_contents(app: Sphinx) -> None:
141    if app.config.doxyrunner_outdir:
142        doxygen_out_dir = Path(app.config.doxyrunner_outdir)
143    else:
144        doxygen_out_dir = Path(app.outdir) / "_doxygen"
145
146    if not app.env.doxygen_input_changed:
147        return
148
149    doxygen_xml_dir = doxygen_out_dir / "xml"
150    groups = parse_xml_dir(doxygen_xml_dir)
151
152    toplevel = [
153        g
154        for g in groups
155        if g.get_compounddef()[0].get_id()
156        not in [
157            i.get_refid()
158            for h in [j.get_compounddef()[0].get_innergroup() for j in groups]
159            for i in h
160        ]
161    ]
162
163    app.builder.env.api_overview_table = generate_table(app, toplevel, groups)
164
165
166def setup(app) -> dict[str, Any]:
167    app.add_config_value("api_overview_doxygen_xml_dir", "html/doxygen/xml", "env")
168    app.add_config_value("api_overview_doxygen_base_url", "../../doxygen/html", "env")
169
170    app.add_directive("api-overview-table", ApiOverview)
171
172    app.connect("builder-inited", sync_contents)
173
174    return {
175        "version": "0.1",
176        "parallel_read_safe": True,
177        "parallel_write_safe": True,
178    }
179