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