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