metadoc.py 17.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Copyright (C) 2017 Belledonne Communications SARL
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

17

18
import abstractapi
19 20
import logging
import metaname
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
import re


class ParsingError(RuntimeError):
	pass


class UnreleasedNodeError(ValueError):
	pass


class ChildrenList(list):
	def __init__(self, node):
		list.__init__(self)
		self.node = node
	
	def __setitem__(self, key, child):
		if child.parent is not None:
			raise UnreleasedNodeError()
		self[key].parent = None
		list.__setitem__(self, key, child)
		child.parent = self.node
	
	def __delitem__(self, key):
		self[key].parent = None
		list.__delitem__(self, key)
	
	def __iadd__(self, other):
		list.__iadd__(self, other)
		for child in other:
			child.parent = self.node
		return self
	
	def append(self, child):
		list.append(self, child)
		child.parent = self.node
	
	def removeall(self):
		children = []
		while len(self) > 0:
			children.append(self[0])
			del self[0]
		return children


class TreeNode(object):
	def __init__(self):
		self.parent = None
	
	def find_ancestor(self, ancestorType):
		ancestor = self.parent
		while ancestor is not None and type(ancestor) is not ancestorType:
			ancestor = ancestor.parent
		return ancestor
	
	def find_root(self):
		node = self
		while node.parent is not None:
			node = node.parent
		return node

82

83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
class SingleChildTreeNode(TreeNode):
	def __init__(self):
		TreeNode.__init__(self)
		self._child = None
	
	def _setchild(self, child):
		if child is not None and child.parent is not None:
			raise UnreleasedNodeError()
		if self._child is not None:
			self._child.parent = None
		self._child = child
		if child is not None:
			child.parent = self
	
	def _getchild(self):
		return self._child
	
	def _delchild(self):
		if self._child is not None:
			self._child.parent = None
		del self._child
	
	child = property(fset=_setchild, fget=_getchild, fdel=_delchild)
106

107 108 109 110 111 112 113 114

class MultiChildTreeNode(TreeNode):
	def __init__(self):
		TreeNode.__init__(self)
		self.children = ChildrenList(self)


class ParagraphPart(TreeNode):
115 116 117
	pass


118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
class TextPart(ParagraphPart):
	def __init__(self, text):
		ParagraphPart.__init__(self)
		self.text = text
	
	def translate(self, docTranslator, **kargs):
		return docTranslator.translate_text(self)


class LanguageKeyword(ParagraphPart):
	def __init__(self, keyword):
		ParagraphPart.__init__(self)
		self.keyword = keyword
	
	def translate(self, docTranslator, **kargs):
		return docTranslator.translate_keyword(self)


class Reference(ParagraphPart):
137
	def __init__(self, cname):
138
		ParagraphPart.__init__(self)
139 140
		self.cname = cname
		self.relatedObject = None
141 142 143
	
	def translate(self, docTranslator, **kargs):
		return docTranslator.translate_reference(self, **kargs)
144 145 146 147 148 149 150


class ClassReference(Reference):
	def resolve(self, api):
		try:
			self.relatedObject = api.classesIndex[self.cname]
		except KeyError:
151
			logging.warning('doc reference pointing on an unknown object ({0})'.format(self.cname))
152 153 154 155 156 157 158


class FunctionReference(Reference):
	def resolve(self, api):
		try:
			self.relatedObject = api.methodsIndex[self.cname]
		except KeyError:
159
			logging.warning('doc reference pointing on an unknown object ({0})'.format(self.cname))
160 161


162 163 164 165 166 167 168 169
class Paragraph(MultiChildTreeNode):
	@property
	def parts(self):
		return self.children
	
	@parts.setter
	def parts(self, parts):
		self.children = parts
170
	
171 172 173 174
	def resolve_all_references(self, api):
		for part in self.parts:
			if isinstance(part, Reference):
				part.resolve(api)
175 176 177 178 179
			elif isinstance(part, (Section, ParameterList)):
				part.resolve_all_references(api)
	
	def translate(self, docTranslator, **kargs):
		return docTranslator._translate_paragraph(self, **kargs)
180 181


182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
class Section(SingleChildTreeNode):
	def __init__(self, kind):
		SingleChildTreeNode.__init__(self)
		self.kind = kind
	
	@property
	def paragraph(self):
		return self.child
	
	@paragraph.setter
	def paragraph(self, paragraph):
		self.child = paragraph
	
	def resolve_all_references(self, api):
		if self.paragraph is not None:
			self.paragraph.resolve_all_references(api)
	
	def translate(self, docTranslator, **kargs):
		return docTranslator._translate_section(self, **kargs)


class ParameterDescription(SingleChildTreeNode):
	def __init__(self, name, desc):
		SingleChildTreeNode.__init__(self)
		self.name = name
		self.child = desc
	
	@property
	def desc(self):
		return self.child
	
	@desc.setter
	def desc(self, desc):
		self.child = desc
	
	def is_self_parameter(self):
		method = self.find_ancestor(Description).relatedObject
		return method.type == abstractapi.Method.Type.Instance and self.name not in [arg.name for arg in method.args]


class ParameterList(MultiChildTreeNode):
	@property
	def parameters(self):
		return self.children
	
	@parameters.setter
	def parameters(self, parameters):
		self.children = parameters
	
	def resolve_all_references(self, api):
		for parameter in self.parameters:
			if parameter.desc is not None:
				parameter.desc.resolve_all_references(api)
	
	def translate(self, docTranslator, **kargs):
		return docTranslator._translate_parameter_list(self, **kargs)


class Description(MultiChildTreeNode):
241
	def __init__(self):
242 243 244 245 246 247 248 249 250 251
		MultiChildTreeNode.__init__(self)
		self.relatedObject = None
	
	@property
	def paragraphs(self):
		return self.children
	
	@paragraphs.setter
	def paragraphs(self, paragraphs):
		self.children = paragraphs
252
	
253 254 255
	def resolve_all_references(self, api):
		for paragraph in self.paragraphs:
			paragraph.resolve_all_references(api)
256 257 258
	
	def translate(self, translator, **kargs):
		return translator.translate_description(self, **kargs)
259 260 261


class Parser:
262 263 264
	def __init__(self):
		self.constants_regex = re.compile('(?:^|\W)(TRUE|FALSE|NULL)(?:\W|$)')
	
265
	def parse_description(self, node):
266 267 268
		if node is None:
			return None
		
269
		desc = Description()
270
		for paraNode in node.findall('./para'):
271
			desc.paragraphs += self._parse_paragraph(paraNode)
272 273 274
		return desc
	
	def _parse_paragraph(self, node):
275
		paragraphs = []
276
		paragraph = Paragraph()
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
		
		text = node.text
		if text is not None:
			paragraph.parts += self._parse_text(text)
		
		for partNode in node.findall('*'):
			if partNode.tag == 'ref':
				ref = self._parse_reference(partNode)
				if ref is not None:
					paragraph.parts.append(ref)
			elif partNode.tag == 'simplesect':
				paragraphs.append(paragraph)
				paragraph.parts.append(self._parse_simple_section(partNode))
				paragraph = Paragraph()
			elif partNode.tag == 'xrefsect':
				paragraphs.append(paragraph)
				paragraph.parts.append(self._parse_xref_section(partNode))
				paragraph = Paragraph()
			elif partNode.tag == 'parameterlist' and partNode.get('kind') == 'param':
				paragraphs.append(paragraph)
				paragraphs.append(self._parse_parameter_list(partNode))
				paragraph = Paragraph()
			else:
300 301
				text = partNode.text
				if text is not None:
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
					paragraph.parts += self._parse_text(text)
			
			text = partNode.tail
			if text is not None:
				text = text.strip('\n')
				if len(text) > 0:
					paragraph.parts += self._parse_text(text)
		
		paragraphs.append(paragraph)
		return [x for x in paragraphs if type(x) is not Paragraph or len(x.parts) > 0]
	
	def _parse_text(self, text):
		parts = []
		lastIndex = 0
		
		match = self.constants_regex.search(text)
		while match is not None:
			if match.start(1)-lastIndex > 0:
				parts.append(TextPart(text[lastIndex:match.start(1)]))
				parts.append(self._parse_constant(text[match.start(1):match.end(1)]))
			lastIndex = match.end(1)
			match = self.constants_regex.search(text, lastIndex)
		
		if lastIndex < len(text):
			parts.append(TextPart(text[lastIndex:]))
327
		
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
		return parts
	
	def _parse_constant(self, token):
		if token == 'TRUE':
			return LanguageKeyword(abstractapi.Boolean(True))
		elif token == 'FALSE':
			return LanguageKeyword(abstractapi.Boolean(False))
		elif token == 'NULL':
			return LanguageKeyword(abstractapi.Nil())
		else:
			raise ValueError("invalid C constant token '{0}'".format(token))
	
	def _parse_simple_section(self, sectionNode):
		section = Section(sectionNode.get('kind'))
		para = sectionNode.find('./para')
		paragraphs = self._parse_paragraph(para)
		section.paragraph = paragraphs[0] if len(paragraphs) > 0 else None
		return section
	
	def _parse_parameter_list(self, paramListNode):
		paramList = ParameterList()
		for paramItemNode in paramListNode.findall('./parameteritem'):
			name = metaname.ArgName()
			name.from_snake_case(paramItemNode.find('./parameternamelist/parametername').text)
			desc = self.parse_description(paramItemNode.find('parameterdescription'))
			paramList.parameters.append(ParameterDescription(name, desc))
		return paramList
	
	def _parse_xref_section(self, sectionNode):
		sectionId = sectionNode.get('id')
		if sectionId.startswith('deprecated_'):
			section = Section('deprecated')
			description = self.parse_description(sectionNode.find('./xrefdescription'))
			paras = description.paragraphs.removeall()
			section.paragraph = paras[0] if len(paras) > 0 else None
			return section
		else:
			raise ParsingError('unknown xrefsect type ({0})'.format(sectionId))
366 367 368 369 370 371
	
	def _parse_reference(self, node):
		if node.text.endswith('()'):
			return FunctionReference(node.text[0:-2])
		else:
			return ClassReference(node.text)
372 373


374 375 376 377 378 379 380 381 382 383 384 385
class TranslationError(Exception):
	pass


class ReferenceTranslationError(TranslationError):
	def __init__(self, refName):
		Exception.__init__(self, refName)
	
	def msg(self):
		return '{0} reference could not been translated'.format(self.args[0])


386
class Translator:
387
	def __init__(self, langCode):
388
		self.textWidth = 80
389 390 391
		self.nameTranslator = metaname.Translator.get(langCode)
		self.langTranslator = abstractapi.Translator.get(langCode)
		self.displaySelfParam = True if langCode == 'C' else False
392
	
393
	def translate_description(self, description, tagAsBrief=False):
394 395 396
		if description is None:
			return None
		
397 398 399 400 401 402
		paras = self._translate_description(description)
		
		lines = self._paragraphs_to_lines(paras)
		
		if tagAsBrief:
			self._tag_as_brief(lines)
403
		
404
		lines = self._crop_text(lines, self.textWidth)
405 406 407 408
		
		translatedDoc = {'lines': []}
		for line in lines:
			translatedDoc['lines'].append({'line': line})
409
		
410
		return translatedDoc
411
	
412 413 414 415 416 417 418 419
	def translate_reference(self, ref, absName=False, namespace=None):
		if ref.relatedObject is None:
			raise ReferenceTranslationError(ref.cname)
		if absName:
			commonName = None
		else:
			if namespace is None:
				description = ref.find_root()
420
				namespaceObj = description.relatedObject.find_first_ancestor_by_type(abstractapi.Namespace, abstractapi.Class)
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
				namespace = namespaceObj.name
			if namespace.is_prefix_of(ref.relatedObject.name):
				commonName = namespace
			else:
				commonName = metaname.Name.find_common_parent(ref.relatedObject.name, namespace)
		return ref.relatedObject.name.translate(self.nameTranslator, recursive=True, topAncestor=commonName)
	
	def translate_keyword(self, keyword):
		return keyword.keyword.translate(self.langTranslator)
	
	def translate_text(self, textpart):
		return textpart.text
	
	def _translate_description(self, desc):
		paras = []
		for para in desc.paragraphs:
			paras.append(para.translate(self))
		return [para for para in paras if para != '']
	
440 441 442
	def _translate_paragraph(self, para):
		strPara = ''
		for part in para.parts:
443 444 445 446 447
			try:
				if isinstance(part, str):
					strPara += part
				else:
					strPara += part.translate(self)
448 449
			except ReferenceTranslationError:
				strPara += part.cname
450 451 452
		
		return strPara
	
453 454 455 456 457 458 459
	def _paragraphs_to_lines(self, paragraphs):
		lines = []
		for para in paragraphs:
			if para is not paragraphs[0]:
				lines.append('')
			lines += para.split('\n')
		return lines
460 461 462 463 464 465 466
	
	def _crop_text(self, inputLines, width):
		outputLines = []
		for line in inputLines:
			outputLines += self._split_line(line, width)
		return outputLines
	
467 468 469 470 471 472
	def _split_line(self, line, width, indent=False):
		firstNonTab = next((c for c in line if c != '\t'), None)
		tabCount = line.index(firstNonTab) if firstNonTab is not None else 0
		linePrefix = ('\t' * tabCount)
		line = line[tabCount:]
		
473 474 475 476 477 478 479 480 481 482 483
		lines = []
		while len(line) > width:
			cutIndex = line.rfind(' ', 0, width)
			if cutIndex != -1:
				lines.append(line[0:cutIndex])
				line = line[cutIndex+1:]
			else:
				cutIndex = width
				lines.append(line[0:cutIndex])
				line = line[cutIndex:]
		lines.append(line)
484 485 486 487 488 489 490 491
		
		if indent:
			lines = [line if line is lines[0] else '\t' + line for line in lines]
		
		return [linePrefix + line for line in lines]
	
	def _tag_as_brief(self, lines):
		pass
492 493


494
class DoxygenTranslator(Translator):
495 496 497
	def _tag_as_brief(self, lines):
		if len(lines) > 0:
			lines[0] = '@brief ' + lines[0]
498
	
499 500
	def translate_reference(self, ref):
		refStr = Translator.translate_reference(self, ref)
501
		if isinstance(ref.relatedObject, (abstractapi.Class, abstractapi.Enum)):
502
			return '#' + refStr
503
		elif isinstance(ref.relatedObject, abstractapi.Method):
504
			return refStr + '()'
505 506
		else:
			raise ReferenceTranslationError(ref.cname)
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
	
	def _translate_section(self, section):
		return '@{0} {1}'.format(
			section.kind,
			self._translate_paragraph(section.paragraph)
		)
	
	def _translate_parameter_list(self, parameterList):
		text = ''
		for paramDesc in parameterList.parameters:
			if self.displaySelfParam or not paramDesc.is_self_parameter():
				desc = self._translate_description(paramDesc.desc)
				desc = desc[0] if len(desc) > 0 else ''
				text = ('@param {0} {1}'.format(paramDesc.name.translate(self.nameTranslator), desc))
		return text
522 523


524 525 526 527
class JavaDocTranslator(DoxygenTranslator):
	def __init__(self):
		DoxygenTranslator.__init__(self, 'C')
	
528
	def _tag_as_brief(self, lines):
529
		pass
Sylvain Berfini's avatar
Sylvain Berfini committed
530 531


532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
class SphinxTranslator(Translator):
	def __init__(self, langCode):
		Translator.__init__(self, langCode)
		if langCode == 'C':
			self.domain = 'c'
			self.classDeclarator = 'type'
			self.methodDeclarator = 'function'
			self.enumDeclarator = 'type'
			self.enumeratorDeclarator = 'var'
			self.enumeratorReferencer = 'data'
			self.methodReferencer = 'func'
		elif langCode == 'Cpp':
			self.domain = 'cpp'
			self.classDeclarator = 'class'
			self.methodDeclarator = 'function'
			self.enumDeclarator = 'enum'
			self.enumeratorDeclarator = 'enumerator'
			self.namespaceDeclarator = 'namespace'
			self.methodReferencer = 'func'
		elif langCode == 'CSharp':
			self.domain = 'csharp'
			self.classDeclarator = 'class'
			self.methodDeclarator = 'method'
			self.enumDeclarator = 'enum'
			self.enumeratorDeclarator = 'value'
			self.namespaceDeclarator = 'namespace'
			self.classReferencer = 'type'
			self.enumReferencer = 'type'
			self.enumeratorReferencer = 'enum'
			self.methodReferencer = 'meth'
		else:
			raise ValueError('invalid language code: {0}'.format(langCode))
	
	def get_declarator(self, typeName):
		try:
			attrName = typeName + 'Declarator'
			declarator = getattr(self, attrName)
			return '{0}:{1}'.format(self.domain, declarator)
		except AttributeError:
			raise ValueError("'{0}' declarator type not supported".format(typeName))
	
	def get_referencer(self, typeName):
		try:
			attrName = typeName + 'Referencer'
			if attrName in dir(self):
				referencer = getattr(self, attrName)
				return '{0}:{1}'.format(self.domain, referencer)
			else:
				return self.get_declarator(typeName)
		except AttributeError:
			raise ValueError("'{0}' referencer type not supported".format(typeName))
	
	def translate_reference(self, ref, label=None, namespace=None):
		strRef = Translator.translate_reference(self, ref, absName=True)
		kargs = {
			'tag'   : self._sphinx_ref_tag(ref),
			'ref'   : strRef,
		}
		kargs['label'] = label if label is not None else Translator.translate_reference(self, ref, namespace=namespace)
		if isinstance(ref, FunctionReference):
			kargs['label'] += '()'
		
		return ':{tag}:`{label} <{ref}>`'.format(**kargs)
	
	def translate_keyword(self, keyword):
		translatedKeyword = Translator.translate_keyword(self, keyword)
		return '``{0}``'.format(translatedKeyword)
	
	def _translate_section(self, section):
		strPara = self._translate_paragraph(section.paragraph)
		if section.kind == 'deprecated':
			return '**Deprecated:** {0}\n'.format(strPara)
		else:
			if section.kind == 'see':
				kind = 'seealso'
			else:
				kind = section.kind
			
			if section.kind == 'return':
				return ':return: {0}'.format(strPara)
			else:
				return '.. {0}::\n\t\n\t{1}\n\n'.format(kind, strPara)
	
	def _translate_parameter_list(self, parameterList):
		text = ''
		for paramDesc in parameterList.parameters:
			if self.displaySelfParam or not paramDesc.is_self_parameter():
				desc = self._translate_description(paramDesc.desc)
				desc = desc[0] if len(desc) > 0 else ''
				text += (':param {0}: {1}\n'.format(paramDesc.name.translate(self.nameTranslator), desc))
		text += '\n'
		return text
	
	def _sphinx_ref_tag(self, ref):
		typeName = type(ref.relatedObject).__name__.lower()
		return self.get_referencer(typeName)
	
	isParamDescRegex = re.compile('\t*:(?:param\s+\w+|return):')
	
	def _split_line(self, line, width):
		if SphinxTranslator.isParamDescRegex.match(line) is not None:
			lines = Translator._split_line(self, line, width, indent=True)
			return lines
		else:
			return Translator._split_line(self, line, width)


class SandCastleTranslator(Translator):
Sylvain Berfini's avatar
Sylvain Berfini committed
640
	def _tag_as_brief(self, lines):
641 642 643 644 645 646 647 648 649
		if len(lines) > 0:
			lines.insert(0, '<summary>')
			lines.append('</summary>')
	
	def translate_reference(self, ref):
		refStr = Translator.translate_reference(self, ref, absName=True)
		if isinstance(ref, FunctionReference):
			refStr += '()'
		return '<see cref="{0}" />'.format(refStr)