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