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