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 l = '\t' + l + '.' 232 lst.append((n, l + '\n')) 233 if 'tags' in info: 234 for (n, name, l) in info.get('tags', []): 235 if name != 'return': 236 name = 'param ' + name 237 l = decorate(l) 238 l = '\t:%s: %s' % (name, l) 239 l = '\n\t\t'.join(l.split('\n')) 240 lst.append((n, l)) 241 lst.append((n+1, '')) 242 if 'desc' in info: 243 desc = info['desc'] 244 n = desc[0] 245 r = '' 246 for l in desc[1:]: 247 l = decorate(l) 248 r += '\t' + l + '\n' 249 lst.append((n, r)) 250 return lst 251 252def extract(f, filename): 253 # type: (TextIOWrapper, str) -> List[Tuple[int, str]] 254 res = process_file(f) 255 res = [ i for r in res for i in convert_to_rst(r) ] 256 return res 257 258def dump_doc(lst): 259 # type: (List[Tuple[int, str]]) -> None 260 for (n, lines) in lst: 261 for l in lines.split('\n'): 262 print('%4d: %s' % (n, l)) 263 n += 1 264 265if __name__ == '__main__': 266 """ extract the doc from stdin """ 267 import sys 268 269 dump_doc(extract(sys.stdin, '<stdin>')) 270 271 272from sphinx.ext.autodoc import AutodocReporter 273import docutils 274import os 275class CDocDirective(docutils.parsers.rst.Directive): 276 required_argument = 1 277 optional_arguments = 1 278 has_content = False 279 option_spec = { 280 } 281 282 def run(self): 283 env = self.state.document.settings.env 284 filename = os.path.join(env.config.cdoc_srcdir, self.arguments[0]) 285 env.note_dependency(os.path.abspath(filename)) 286 287 ## create a (view) list from the extracted doc 288 lst = docutils.statemachine.ViewList() 289 f = open(filename, 'r') 290 for (lineno, lines) in extract(f, filename): 291 for l in lines.split('\n'): 292 lst.append(l.expandtabs(8), filename, lineno) 293 lineno += 1 294 295 ## let parse this new reST content 296 memo = self.state.memo 297 save = memo.reporter, memo.title_styles, memo.section_level 298 memo.reporter = AutodocReporter(lst, memo.reporter) 299 node = docutils.nodes.section() 300 try: 301 self.state.nested_parse(lst, 0, node, match_titles=1) 302 finally: 303 memo.reporter, memo.title_styles, memo.section_level = save 304 return node.children 305 306def setup(app): 307 app.add_config_value('cdoc_srcdir', None, 'env') 308 app.add_directive_to_domain('c', 'autodoc', CDocDirective) 309 310 return { 311 'version': '1.0', 312 'parallel_read_safe': True, 313 } 314 315# vim: tabstop=4 316