# 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)