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 
  51 import re
  52 
  53 class 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 
  84 def 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 
  96 def 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 
 110 def 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 
 184 def 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 
 196 def decorate(l):
 197         # type: (str) -> str
 198         l = re.sub(r"@(\w+)", "**\\1**", l)
 199         return l
 200 
 201 def 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 
 252 def 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 
 258 def 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 
 265 if __name__ == '__main__':
 266         """ extract the doc from stdin """
 267         import sys
 268 
 269         dump_doc(extract(sys.stdin, '<stdin>'))
 270 
 271 
 272 from sphinx.ext.autodoc import AutodocReporter
 273 import docutils
 274 import os
 275 class 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 
 306 def 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