1#!/usr/bin/env python 2# SPDX_License-Identifier: MIT 3# 4# Copyright (C) 2018 Luc Van Oostenryck <luc.vanoostenryck@gmail.com> 5# 6 7""" 8/// 9// Sparse source files may contain documentation inside block-comments 10// specifically formatted:: 11// 12// /// 13// // Here is some doc 14// // and here is some more. 15// 16// More precisely, a doc-block begins with a line containing only ``///`` 17// and continues with lines beginning by ``//`` followed by either a space, 18// a tab or nothing, the first space after ``//`` is ignored. 19// 20// For functions, some additional syntax must be respected inside the 21// block-comment:: 22// 23// /// 24// // <mandatory short one-line description> 25// // <optional blank line> 26// // @<1st parameter's name>: <description> 27// // @<2nd parameter's name>: <long description 28// // <tab>which needs multiple lines> 29// // @return: <description> (absent for void functions) 30// // <optional blank line> 31// // <optional long multi-line description> 32// int somefunction(void *ptr, int count); 33// 34// Inside the description fields, parameter's names can be referenced 35// by using ``@<parameter name>``. A function doc-block must directly precede 36// the function it documents. This function can span multiple lines and 37// can either be a function prototype (ending with ``;``) or a 38// function definition. 39// 40// Some future versions will also allow to document structures, unions, 41// enums, typedefs and variables. 42// 43// This documentation can be extracted into a .rst document by using 44// the *autodoc* directive:: 45// 46// .. c:autodoc:: file.c 47// 48 49""" 50 51import re 52 53class Lines: 54 def __init__(self, lines): 55 # type: (Iterable[str]) -> None 56 self.index = 0 57 self.lines = lines 58 self.last = None 59 self.back = False 60 61 def __iter__(self): 62 # type: () -> Lines 63 return self 64 65 def memo(self): 66 # type: () -> Tuple[int, str] 67 return (self.index, self.last) 68 69 def __next__(self): 70 # type: () -> Tuple[int, str] 71 if not self.back: 72 self.last = next(self.lines).rstrip() 73 self.index += 1 74 else: 75 self.back = False 76 return self.memo() 77 def next(self): 78 return self.__next__() 79 80 def undo(self): 81 # type: () -> None 82 self.back = True 83 84def readline_multi(lines, line): 85 # type: (Lines, str) -> str 86 try: 87 while True: 88 (n, l) = next(lines) 89 if not l.startswith('//\t'): 90 raise StopIteration 91 line += '\n' + l[3:] 92 except: 93 lines.undo() 94 return line 95 96def readline_delim(lines, delim): 97 # type: (Lines, Tuple[str, str]) -> Tuple[int, str] 98 try: 99 (lineno, line) = next(lines) 100 if line == '': 101 raise StopIteration 102 while line[-1] not in delim: 103 (n, l) = next(lines) 104 line += ' ' + l.lstrip() 105 except: 106 line = '' 107 return (lineno, line) 108 109 110def process_block(lines): 111 # type: (Lines) -> Dict[str, Any] 112 info = { } 113 tags = [] 114 desc = [] 115 state = 'START' 116 117 (n, l) = lines.memo() 118 #print('processing line ' + str(n) + ': ' + l) 119 120 ## is it a single line comment ? 121 m = re.match(r"^///\s+(.+)$", l) # /// ... 122 if m: 123 info['type'] = 'single' 124 info['desc'] = (n, m.group(1).rstrip()) 125 return info 126 127 ## read the multi line comment 128 for (n, l) in lines: 129 #print('state %d: %4d: %s' % (state, n, l)) 130 if l.startswith('// '): 131 l = l[3:] ## strip leading '// ' 132 elif l.startswith('//\t') or l == '//': 133 l = l[2:] ## strip leading '//' 134 else: 135 lines.undo() ## end of doc-block 136 break 137 138 if state == 'START': ## one-line short description 139 info['short'] = (n ,l) 140 state = 'PRE-TAGS' 141 elif state == 'PRE-TAGS': ## ignore empty line 142 if l != '': 143 lines.undo() 144 state = 'TAGS' 145 elif state == 'TAGS': ## match the '@tagnames' 146 m = re.match(r"^@([\w-]*)(:?\s*)(.*)", l) 147 if m: 148 tag = m.group(1) 149 sep = m.group(2) 150 ## FIXME/ warn if sep != ': ' 151 l = m.group(3) 152 l = readline_multi(lines, l) 153 tags.append((n, tag, l)) 154 else: 155 lines.undo() 156 state = 'PRE-DESC' 157 elif state == 'PRE-DESC': ## ignore the first empty lines 158 if l != '': ## or first line of description 159 desc = [n, l] 160 state = 'DESC' 161 elif state == 'DESC': ## remaining lines -> description 162 desc.append(l) 163 else: 164 pass 165 166 ## fill the info 167 if len(tags): 168 info['tags'] = tags 169 if len(desc): 170 info['desc'] = desc 171 172 ## read the item (function only for now) 173 (n, line) = readline_delim(lines, (')', ';')) 174 if len(line): 175 line = line.rstrip(';') 176 #print('function: %4d: %s' % (n, line)) 177 info['type'] = 'func' 178 info['func'] = (n, line) 179 else: 180 info['type'] = 'bloc' 181 182 return info 183 184def process_file(f): 185 # type: (TextIOWrapper) -> List[Dict[str, Any]] 186 docs = [] 187 lines = Lines(f) 188 for (n, l) in lines: 189 #print("%4d: %s" % (n, l)) 190 if l.startswith('///'): 191 info = process_block(lines) 192 docs.append(info) 193 194 return docs 195 196def decorate(l): 197 # type: (str) -> str 198 l = re.sub(r"@(\w+)", "**\\1**", l) 199 return l 200 201def convert_to_rst(info): 202 # type: (Dict[str, Any]) -> List[Tuple[int, str]] 203 lst = [] 204 #print('info= ' + str(info)) 205 typ = info.get('type', '???') 206 if typ == '???': 207 ## uh ? 208 pass 209 elif typ == 'bloc': 210 if 'short' in info: 211 (n, l) = info['short'] 212 lst.append((n, l)) 213 if 'desc' in info: 214 desc = info['desc'] 215 n = desc[0] - 1 216 desc.append('') 217 for i in range(1, len(desc)): 218 l = desc[i] 219 lst.append((n+i, l)) 220 # auto add a blank line for a list 221 if re.search(r":$", desc[i]) and re.search(r"\S", desc[i+1]): 222 lst.append((n+i, '')) 223 224 elif typ == 'func': 225 (n, l) = info['func'] 226 l = '.. c:function:: ' + l 227 lst.append((n, l + '\n')) 228 if 'short' in info: 229 (n, l) = info['short'] 230 l = l[0].capitalize() + l[1:].strip('.') 231 if l[-1] != '?': 232 l = l + '.' 233 lst.append((n, '\t' + l + '\n')) 234 if 'tags' in info: 235 for (n, name, l) in info.get('tags', []): 236 if name != 'return': 237 name = 'param ' + name 238 l = decorate(l) 239 l = '\t:%s: %s' % (name, l) 240 l = '\n\t\t'.join(l.split('\n')) 241 lst.append((n, l)) 242 lst.append((n+1, '')) 243 if 'desc' in info: 244 desc = info['desc'] 245 n = desc[0] 246 r = '' 247 for l in desc[1:]: 248 l = decorate(l) 249 r += '\t' + l + '\n' 250 lst.append((n, r)) 251 return lst 252 253def extract(f, filename): 254 # type: (TextIOWrapper, str) -> List[Tuple[int, str]] 255 res = process_file(f) 256 res = [ i for r in res for i in convert_to_rst(r) ] 257 return res 258 259def dump_doc(lst): 260 # type: (List[Tuple[int, str]]) -> None 261 for (n, lines) in lst: 262 for l in lines.split('\n'): 263 print('%4d: %s' % (n, l)) 264 n += 1 265 266if __name__ == '__main__': 267 """ extract the doc from stdin """ 268 import sys 269 270 dump_doc(extract(sys.stdin, '<stdin>')) 271 272 273from sphinx.util.docutils import switch_source_input 274import docutils 275import os 276class CDocDirective(docutils.parsers.rst.Directive): 277 required_argument = 1 278 optional_arguments = 1 279 has_content = False 280 option_spec = { 281 } 282 283 def run(self): 284 env = self.state.document.settings.env 285 filename = os.path.join(env.config.cdoc_srcdir, self.arguments[0]) 286 env.note_dependency(os.path.abspath(filename)) 287 288 ## create a (view) list from the extracted doc 289 lst = docutils.statemachine.ViewList() 290 f = open(filename, 'r') 291 for (lineno, lines) in extract(f, filename): 292 for l in lines.split('\n'): 293 lst.append(l.expandtabs(8), filename, lineno) 294 lineno += 1 295 296 ## let parse this new reST content 297 memo = self.state.memo 298 save = memo.title_styles, memo.section_level 299 node = docutils.nodes.section() 300 try: 301 with switch_source_input(self.state, lst): 302 self.state.nested_parse(lst, 0, node, match_titles=1) 303 finally: 304 memo.title_styles, memo.section_level = save 305 return node.children 306 307def setup(app): 308 app.add_config_value('cdoc_srcdir', None, 'env') 309 app.add_directive_to_domain('c', 'autodoc', CDocDirective) 310 311 return { 312 'version': '1.0', 313 'parallel_read_safe': True, 314 } 315 316# vim: tabstop=4 317