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