• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Sphinx directives for Pigweed module metadata"""
15
16from typing import List
17
18import docutils
19
20# pylint: disable=consider-using-from-import
21import docutils.parsers.rst.directives as directives  # type: ignore
22
23# pylint: enable=consider-using-from-import
24from sphinx.application import Sphinx as SphinxApplication
25from sphinx.util.docutils import SphinxDirective
26from sphinx_design.badges_buttons import ButtonRefDirective  # type: ignore
27from sphinx_design.cards import CardDirective  # type: ignore
28
29
30def status_choice(arg):
31    return directives.choice(arg, ('experimental', 'unstable', 'stable'))
32
33
34def status_badge(module_status: str) -> str:
35    role = ':bdg-primary:'
36    return role + f'`{module_status.title()}`'
37
38
39def cs_url(module_name: str):
40    return f'https://cs.opensource.google/pigweed/pigweed/+/main:{module_name}/'
41
42
43def concat_tags(*tag_lists: List[str]):
44    all_tags = tag_lists[0]
45
46    for tag_list in tag_lists[1:]:
47        if len(tag_list) > 0:
48            all_tags.append(':octicon:`dot-fill`')
49            all_tags.extend(tag_list)
50
51    return all_tags
52
53
54class PigweedModuleDirective(SphinxDirective):
55    """Directive registering module metadata, rendering title & info card."""
56
57    required_arguments = 0
58    final_argument_whitespace = True
59    has_content = True
60    option_spec = {
61        'name': directives.unchanged_required,
62        'tagline': directives.unchanged_required,
63        'status': status_choice,
64        'is-deprecated': directives.flag,
65        'languages': directives.unchanged,
66        'code-size-impact': directives.unchanged,
67        'facade': directives.unchanged,
68        'get-started': directives.unchanged_required,
69        'tutorials': directives.unchanged,
70        'guides': directives.unchanged,
71        'concepts': directives.unchanged,
72        'design': directives.unchanged_required,
73        'api': directives.unchanged,
74    }
75
76    def try_get_option(self, option: str):
77        try:
78            return self.options[option]
79        except KeyError:
80            raise self.error(f' :{option}: option is required')
81
82    def maybe_get_option(self, option: str):
83        try:
84            return self.options[option]
85        except KeyError:
86            return None
87
88    def create_section_button(self, title: str, ref: str):
89        node = docutils.nodes.list_item(classes=['pw-module-section-button'])
90        node += ButtonRefDirective(
91            name='',
92            arguments=[ref],
93            options={'color': 'primary'},
94            content=[title],
95            lineno=0,
96            content_offset=0,
97            block_text='',
98            state=self.state,
99            state_machine=self.state_machine,
100        ).run()
101
102        return node
103
104    def register_metadata(self):
105        module_name = self.try_get_option('name')
106
107        if 'facade' in self.options:
108            facade = self.options['facade']
109
110            # Initialize the module relationship dict if needed
111            if not hasattr(self.env, 'pw_module_relationships'):
112                self.env.pw_module_relationships = {}
113
114            # Initialize the backend list for this facade if needed
115            if facade not in self.env.pw_module_relationships:
116                self.env.pw_module_relationships[facade] = []
117
118            # Add this module as a backend of the provided facade
119            self.env.pw_module_relationships[facade].append(module_name)
120
121        if 'is-deprecated' in self.options:
122            # Initialize the deprecated modules list if needed
123            if not hasattr(self.env, 'pw_modules_deprecated'):
124                self.env.pw_modules_deprecated = []
125
126            self.env.pw_modules_deprecated.append(module_name)
127
128    def run(self):
129        tagline = docutils.nodes.paragraph(
130            text=self.try_get_option('tagline'),
131            classes=['section-subtitle'],
132        )
133
134        status_tags: List[str] = [
135            status_badge(self.try_get_option('status')),
136        ]
137
138        if 'is-deprecated' in self.options:
139            status_tags.append(':bdg-danger:`Deprecated`')
140
141        language_tags = []
142
143        if 'languages' in self.options:
144            languages = self.options['languages'].split(',')
145
146            if len(languages) > 0:
147                for language in languages:
148                    language_tags.append(f':bdg-info:`{language.strip()}`')
149
150        code_size_impact = []
151
152        if code_size_text := self.maybe_get_option('code-size-impact'):
153            code_size_impact.append(f'**Code Size Impact:** {code_size_text}')
154
155        # Move the directive content into a section that we can render wherever
156        # we want.
157        content = docutils.nodes.paragraph()
158        self.state.nested_parse(self.content, 0, content)
159
160        # The card inherits its content from this node's content, which we've
161        # already pulled out. So we can replace this node's content with the
162        # content we need in the card.
163        self.content = docutils.statemachine.StringList(
164            concat_tags(status_tags, language_tags, code_size_impact)
165        )
166
167        card = CardDirective.create_card(
168            inst=self,
169            arguments=[],
170            options={},
171        )
172
173        # Create the top-level section buttons.
174        section_buttons = docutils.nodes.bullet_list(
175            classes=['pw-module-section-buttons']
176        )
177
178        # This is the pattern for required sections.
179        section_buttons += self.create_section_button(
180            'Get Started', self.try_get_option('get-started')
181        )
182
183        # This is the pattern for optional sections.
184        if (tutorials_ref := self.maybe_get_option('tutorials')) is not None:
185            section_buttons += self.create_section_button(
186                'Tutorials', tutorials_ref
187            )
188
189        if (guides_ref := self.maybe_get_option('guides')) is not None:
190            section_buttons += self.create_section_button('Guides', guides_ref)
191
192        if (concepts_ref := self.maybe_get_option('concepts')) is not None:
193            section_buttons += self.create_section_button(
194                'Concepts', concepts_ref
195            )
196
197        section_buttons += self.create_section_button(
198            'Design', self.try_get_option('design')
199        )
200
201        if (api_ref := self.maybe_get_option('api')) is not None:
202            section_buttons += self.create_section_button(
203                'API Reference', api_ref
204            )
205
206        return [tagline, section_buttons, content, card]
207
208
209def build_backend_lists(app, _doctree, _fromdocname):
210    env = app.builder.env
211
212    if not hasattr(env, 'pw_module_relationships'):
213        env.pw_module_relationships = {}
214
215
216def setup(app: SphinxApplication):
217    app.add_directive('pigweed-module', PigweedModuleDirective)
218
219    # At this event, the documents and metadata have been generated, and now we
220    # can modify the doctree to reflect the metadata.
221    app.connect('doctree-resolved', build_backend_lists)
222
223    return {
224        'parallel_read_safe': True,
225        'parallel_write_safe': True,
226    }
227