# 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. import abstractapi import logging import metaname 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 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) class MultiChildTreeNode(TreeNode): def __init__(self): TreeNode.__init__(self) self.children = ChildrenList(self) class ParagraphPart(TreeNode): pass 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): def __init__(self, cname): ParagraphPart.__init__(self) self.cname = cname self.relatedObject = None def translate(self, docTranslator, **kargs): return docTranslator.translate_reference(self, **kargs) class ClassReference(Reference): def resolve(self, api): try: self.relatedObject = api.classesIndex[self.cname] except KeyError: logging.warning('doc reference pointing on an unknown object ({0})'.format(self.cname)) class FunctionReference(Reference): def resolve(self, api): try: self.relatedObject = api.methodsIndex[self.cname] except KeyError: logging.warning('doc reference pointing on an unknown object ({0})'.format(self.cname)) class Paragraph(MultiChildTreeNode): @property def parts(self): return self.children @parts.setter def parts(self, parts): self.children = parts def resolve_all_references(self, api): for part in self.parts: if isinstance(part, Reference): part.resolve(api) elif isinstance(part, (Section, ParameterList)): part.resolve_all_references(api) def translate(self, docTranslator, **kargs): return docTranslator._translate_paragraph(self, **kargs) 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): def __init__(self): MultiChildTreeNode.__init__(self) self.relatedObject = None @property def paragraphs(self): return self.children @paragraphs.setter def paragraphs(self, paragraphs): self.children = paragraphs def resolve_all_references(self, api): for paragraph in self.paragraphs: paragraph.resolve_all_references(api) def translate(self, translator, **kargs): return translator.translate_description(self, **kargs) class Parser: def __init__(self): self.constants_regex = re.compile('(?:^|\W)(TRUE|FALSE|NULL)(?:\W|$)') def parse_description(self, node): if node is None: return None desc = Description() for paraNode in node.findall('./para'): desc.paragraphs += self._parse_paragraph(paraNode) return desc def _parse_paragraph(self, node): paragraphs = [] paragraph = Paragraph() 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: text = partNode.text if text is not None: 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:])) 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)) def _parse_reference(self, node): if node.text.endswith('()'): return FunctionReference(node.text[0:-2]) else: return ClassReference(node.text) 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]) class Translator: def __init__(self, langCode): self.textWidth = 80 self.nameTranslator = metaname.Translator.get(langCode) self.langTranslator = abstractapi.Translator.get(langCode) self.displaySelfParam = True if langCode == 'C' else False def translate_description(self, description, tagAsBrief=False): if description is None: return None paras = self._translate_description(description) lines = self._paragraphs_to_lines(paras) if tagAsBrief: self._tag_as_brief(lines) lines = self._crop_text(lines, self.textWidth) translatedDoc = {'lines': []} for line in lines: translatedDoc['lines'].append({'line': line}) return translatedDoc 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() namespaceObj = description.relatedObject.find_first_ancestor_by_type((abstractapi.Namespace, abstractapi.Class)) 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 != ''] def _translate_paragraph(self, para): strPara = '' for part in para.parts: try: if isinstance(part, str): strPara += part else: strPara += part.translate(self) except ReferenceTranslationError: strPara += part.cname return strPara 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 def _crop_text(self, inputLines, width): outputLines = [] for line in inputLines: outputLines += self._split_line(line, width) return outputLines 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:] 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) 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 class DoxygenTranslator(Translator): def _tag_as_brief(self, lines): if len(lines) > 0: lines[0] = '@brief ' + lines[0] def translate_reference(self, ref): refStr = Translator.translate_reference(self, ref) if isinstance(ref.relatedObject, (abstractapi.Class, abstractapi.Enum)): return '#' + refStr elif isinstance(ref.relatedObject, abstractapi.Method): return refStr + '()' else: raise ReferenceTranslationError(ref.cname) 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 class JavaDocTranslator(DoxygenTranslator): def __init__(self): DoxygenTranslator.__init__(self, 'C') def _tag_as_brief(self, lines): pass 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): def _tag_as_brief(self, lines): if len(lines) > 0: lines.insert(0, '') lines.append('') def translate_reference(self, ref): refStr = Translator.translate_reference(self, ref, absName=True) if isinstance(ref, FunctionReference): refStr += '()' return ''.format(refStr)