1# Copyright 2024 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"""Implementation of the custom pw-status-codes reStructuredText directive. 15 16The main goal of this directive is to improve the consistency of the 17presentation of our API references. All C++ functions and methods that return 18a set of pw::Status codes use this directive. 19 20This directive is mainly used within the Doxygen comments of C++ header files. 21Usage example: 22 23/// @returns @rst 24/// 25/// .. pw-status-codes:: 26/// 27/// OK: The bulk read was successful. 28/// 29/// DEADLINE_EXCEEDED: Can't acquire 30/// bus access. See :ref:`x`. 31/// 32/// @endrst 33 34The word before the colon must be a valid pw_status code. The text after the 35colon is a description of what the status code means in this particular 36scenario. Descriptions may span multiple lines and can contain nested 37reStructuredText. 38""" 39import sys 40from typing import List, Tuple 41 42from docutils import nodes 43from docutils.parsers.rst import Directive 44from docutils.statemachine import ViewList 45from pw_status import Status 46from sphinx.application import Sphinx 47 48 49help_url = 'https://pigweed.dev/docs/style/doxygen.html#pw-status-codes' 50 51 52class PwStatusCodesDirective(Directive): 53 """Renders `pw-status-codes` directives.""" 54 55 has_content = True 56 57 # The main event handler for the directive. 58 # https://docutils.sourceforge.io/docs/howto/rst-directives.html 59 def run(self) -> list[nodes.Node]: 60 merged_data = self._merge() 61 transformed_data = self._transform(merged_data) 62 return self._render(transformed_data) 63 64 def _merge(self) -> List[str]: 65 """Merges the content into a format that's easier to work with. 66 67 Docutils provides content line-by-line, with empty strings 68 representing newlines: 69 70 [ 71 'OK: The bulk read was successful.', 72 '', 73 'DEADLINE_EXCEEDED: Can't acquire', 74 'bus access. See :ref:`x`.', 75 ] 76 77 _merge() consolidates each code and its accompanying description into 78 a single string representing the complete key-value pair (KVP) and 79 removes the empty strings: 80 81 [ 82 'OK: The bulk read was successful.', 83 'DEADLINE_EXCEEDED: Can't acquire bus access. See :ref:`x`.' 84 ] 85 86 We also need to handle the scenario of getting a single line of 87 content: 88 89 [ 90 'OK: The operation was successful.' 91 ] 92 """ 93 kvps = [] # key-value pairs 94 index = 0 95 kvps.append('') 96 content: ViewList = self.content 97 for line in content: 98 # An empty line in the source content means that we're about to 99 # encounter a new kvp. See the empty line after 'OK: The bulk read 100 # was successful.' for an example. 101 if line == '': 102 # Make sure that the current kvp has content before starting 103 # a new kvp. 104 if kvps[index] != '': 105 index += 1 106 kvps.append('') 107 else: # The line has content we need. 108 # If the current line isn't empty and our current kvp also 109 # isn't empty then we're dealing with the multi-line scenario. 110 # See the DEADLINE_EXCEEDED example. We just need to add back 111 # the whitespace that was stripped out. 112 if kvps[index] != '': 113 kvps[index] += ' ' 114 # All edge cases have been handled and it's now safe to always add 115 # the current line to the current kvp. 116 kvps[index] += line 117 return kvps 118 119 def _transform(self, items: List[str]) -> List[Tuple[str, str]]: 120 """Transforms the data into normalized and annotated tuples. 121 122 _normalize() gives us data like this: 123 124 [ 125 'OK: The bulk read was successful.', 126 ] 127 128 _transform() changes it to a list of tuples where the first element 129 is the status code converted into a cross-reference and the second 130 element is the description. 131 132 [ 133 (':c:enumerator:`OK`', 'The bulk read was successful.'), 134 ] 135 136 This function exits if any provided status code is invalid. 137 """ 138 data = [] 139 valid_codes = [member.name for member in Status] 140 for item in items: 141 code = item.split(':', 1)[0] 142 desc = item.split(':', 1)[1].strip() 143 if code not in valid_codes: 144 docname = self.state.document.settings.env.docname 145 error = ( 146 f'[error] pw-status-codes found invalid code: {code}\n' 147 f' offending content: {item}\n' 148 f' included in following file: {docname}.rst\n' 149 f' help: {help_url}\n' 150 ) 151 sys.exit(error) 152 link = f':c:enumerator:`{code}`' 153 data.append((link, desc)) 154 return data 155 156 def _render(self, data: List[Tuple[str, str]]) -> List[nodes.Node]: 157 """Renders the content as a table. 158 159 _transform() gives us data like this: 160 161 [ 162 (':c:enumerator:`OK`', 'The bulk read was successful.'), 163 ] 164 165 _render() structures it as a csv-table: 166 167 .. csv-table: 168 :header: "Code", "Description" 169 :align: left 170 171 ":c:enumerator:`OK`", "The bulk read was successful." 172 173 And then lets Sphinx/Docutils do the rendering. This has the added 174 bonus of making it possible to use inline reStructuredText elements 175 like code formatting and cross-references in the descriptions. 176 """ 177 table = [ 178 '.. csv-table::', 179 ' :header: "Code", "Description"', 180 ' :align: left', 181 '', 182 ] 183 for item in data: 184 table.append(f' "{item[0]}", "{item[1]}"') 185 container = nodes.container() 186 container['classes'] = ['pw-status-codes'] 187 # Docutils doesn't accept normal lists; it must be a ViewList. 188 # inclusive-language: disable 189 # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#viewlists 190 # inclusive-language: enable 191 self.state.nested_parse(ViewList(table), self.content_offset, container) 192 return [container] 193 194 195def setup(app: Sphinx) -> dict[str, bool]: 196 """Registers the pw-status-codes directive in the Sphinx build system.""" 197 app.add_directive('pw-status-codes', PwStatusCodesDirective) 198 return { 199 'parallel_read_safe': True, 200 'parallel_write_safe': True, 201 } 202