Synopsis - Synopsis/Formatters/DocBook/__init__.py

File: Synopsis/Formatters/DocBook/__init__.py

      1#
      2# Copyright (C) 2007 Stefan Seefeld
      3# All rights reserved.
      4# Licensed to the public under the terms of the GNU LGPL (>= 2),
      5# see the file COPYING for details.
      6#
      7
      8"""a DocBook formatter (producing Docbook 4.5 XML output"""
      9
     10from Synopsis.Processor import *
     11from Synopsis import ASG, DeclarationSorter
     12from Synopsis.Formatters import quote_name
     13from Synopsis.Formatters.TOC import TOC
     14from Syntax import *
     15from Markup.Javadoc import Javadoc
     16try:
     17    from Markup.RST import RST
     18except ImportError:
     19    from Markup import Formatter as RST
     20
     21import os
     22
     23def escape(text):
     24
     25    for p in [('&', '&amp;'), ('"', '&quot;'), ('<', '&lt;'), ('>', '&gt;'),]:
     26        text = text.replace(*p)
     27    return text
     28
     29def reference(name):
     30    """Generate an id suitable as a 'linkend' / 'id' attribute, i.e. for linking."""
     31
     32    return quote_name(str(name))
     33
     34class Linker:
     35    """Helper class to be used to resolve references from doc-strings to declarations."""
     36
     37    def link(self, decl):
     38
     39        return reference(decl.name)
     40
     41
     42class _BaseClasses(ASG.Visitor):
     43
     44    def __init__(self):
     45        self.classes = [] # accumulated set of classes
     46        self.classes_once = [] # classes not to be included again
     47
     48    def visit_declared_type_id(self, declared):
     49        declared.declaration.accept(self)
     50
     51    def visit_class(self, class_):
     52        self.classes.append(class_)
     53        for p in class_.parents:
     54            p.accept(self)
     55
     56    def visit_inheritance(self, inheritance):
     57
     58        if 'private' in inheritance.attributes:
     59            return # Ignore private base classes, they are not visible anyway.
     60        elif inheritance.parent in self.classes_once:
     61            return # Do not include virtual base classes more than once.
     62        if 'virtual' in inheritance.attributes:
     63            self.classes_once.append(inheritance.parent)
     64        inheritance.parent.accept(self)
     65
     66
     67_summary_syntax = {
     68    'IDL': CxxSummarySyntax,
     69    'C++': CxxSummarySyntax,
     70    'C': CxxSummarySyntax,
     71    'Python': PythonSummarySyntax
     72    }
     73
     74_detail_syntax = {
     75    'IDL': CxxDetailSyntax,
     76    'C++': CxxDetailSyntax,
     77    'C': CxxDetailSyntax,
     78    'Python': PythonDetailSyntax
     79    }
     80
     81class ModuleLister(ASG.Visitor):
     82    """Maps a module-tree to a (flat) list of modules."""
     83
     84    def __init__(self):
     85
     86        self.modules = []
     87
     88    def visit_module(self, module):
     89
     90        self.modules.append(module)
     91        for d in module.declarations:
     92            d.accept(self)
     93
     94
     95class InheritanceFormatter:
     96
     97    def __init__(self, base_dir, bgcolor):
     98
     99        self.base_dir = base_dir
    100        self.bgcolor = bgcolor
    101
    102    def format_class(self, class_, format):
    103
    104        if not class_.parents:
    105            return ''
    106
    107        from Synopsis.Formatters import Dot
    108        filename = os.path.join(self.base_dir, escape(str(class_.name)) + '.%s'%format)
    109        dot = Dot.Formatter(bgcolor=self.bgcolor)
    110        try:
    111            dot.process(IR.IR(asg = ASG.ASG(declarations = [class_])),
    112                        output=filename,
    113                        format=format,
    114                        type='single')
    115            return filename
    116        except InvalidCommand, e:
    117            print 'Warning : %s'%str(e)
    118            return ''
    119
    120
    121class FormatterBase:
    122
    123    def __init__(self, processor, output, base_dir,
    124                 nested_modules, secondary_index, inheritance_graphs, graph_color):
    125        self.processor = processor
    126        self.output = output
    127        self.base_dir = base_dir
    128        self.nested_modules = nested_modules
    129        self.secondary_index = secondary_index
    130        self.inheritance_graphs = inheritance_graphs
    131        self.graph_color = graph_color
    132        self.__scope = ()
    133        self.__scopestack = []
    134        self.__indent = 0
    135        self.__elements = []
    136
    137    def scope(self): return self.__scope
    138    def push_scope(self, newscope):
    139
    140        self.__scopestack.append(self.__scope)
    141        self.__scope = newscope
    142
    143    def pop_scope(self):
    144
    145        self.__scope = self.__scopestack[-1]
    146        del self.__scopestack[-1]
    147
    148    def write(self, text):
    149        """Write some text to the output stream, replacing \n's with \n's and
    150        indents."""
    151
    152        indent = ' ' * self.__indent
    153        self.output.write(text.replace('\n', '\n'+indent))
    154
    155    def start_element(self, type, **params):
    156        """Write the start of an element, ending with a newline"""
    157
    158        self.__elements.append(type)
    159        param_text = ''
    160        if params: param_text = ' ' + ' '.join(['%s="%s"'%(p[0].lower(), p[1]) for p in params.items()])
    161        self.write('<' + type + param_text + '>')
    162        self.__indent = self.__indent + 2
    163        self.write('\n')
    164
    165    def end_element(self):
    166        """Write the end of an element, starting with a newline"""
    167
    168        type = self.__elements.pop()
    169        self.__indent = self.__indent - 2
    170        self.write('\n</' + type + '>')
    171        self.write('\n')
    172
    173    def write_element(self, element, body, end = '\n', **params):
    174        """Write a single element on one line (though body may contain
    175        newlines)"""
    176
    177        param_text = ''
    178        if params: param_text = ' ' + ' '.join(['%s="%s"'%(p[0].lower(), p[1]) for p in params.items()])
    179        self.write('<' + element + param_text + '>')
    180        self.__indent = self.__indent + 2
    181        self.write(body)
    182        self.__indent = self.__indent - 2
    183        self.write('</' + element + '>' + end)
    184
    185    def element(self, element, body, **params):
    186        """Return but do not write the text for an element on one line"""
    187
    188        param_text = ''
    189        if params: param_text = ' ' + ' '.join(['%s="%s"'%(p[0].lower(), p[1]) for p in params.items()])
    190        return '<%s%s>%s</%s>'%(element, param_text, body, element)
    191
    192
    193class SummaryFormatter(FormatterBase, ASG.Visitor):
    194    """A SummaryFormatter."""
    195
    196    def process_doc(self, decl):
    197
    198        if decl.annotations.get('doc', ''):
    199            summary = self.processor.documentation.summary(decl)
    200            if summary:
    201                # FIXME: Unfortunately, as of xsl-docbook 1.73, a section role
    202                #        doesn't translate into a div class attribute, so there
    203                #        is no way to style summaries and descriptions per se.
    204                #self.write('<section role="summary">\n%s<section>\n'%summary)
    205                self.write('%s\n'%summary)
    206
    207    #################### ASG Visitor ###########################################
    208
    209    def visit_declaration(self, declaration):
    210
    211        language = declaration.file.annotations['language']
    212        syntax = _summary_syntax[language](self.output)
    213        declaration.accept(syntax)
    214        syntax.finish()
    215        self.process_doc(declaration)
    216
    217
    218    visit_module = visit_declaration
    219    visit_class = visit_declaration
    220    def visit_meta_module(self, meta):
    221        self.visit_module(meta.module_declarations[0])
    222
    223    visit_function = visit_declaration
    224
    225    def visit_enum(self, enum):
    226        print "sorry, <enum> not implemented"
    227
    228
    229class DetailFormatter(FormatterBase, ASG.Visitor):
    230
    231
    232    #################### Type Visitor ##########################################
    233
    234    def visit_builtin_type_id(self, type):
    235
    236        self.__type_ref = str(type.name)
    237        self.__type_label = str(type.name)
    238
    239    def visit_unknown_type_id(self, type):
    240
    241        self.__type_ref = str(type.name)
    242        self.__type_label = str(self.scope().prune(type.name))
    243
    244    def visit_declared_type_id(self, type):
    245
    246        self.__type_label = str(self.scope().prune(type.name))
    247        self.__type_ref = str(type.name)
    248
    249    def visit_modifier_type_id(self, type):
    250
    251        type.alias.accept(self)
    252        self.__type_ref = ''.join(type.premod) + ' ' + self.__type_ref + ' ' + ''.join(type.postmod)
    253        self.__type_label = escape(''.join(type.premod) + ' ' + self.__type_label + ' ' + ''.join(type.postmod))
    254
    255    def visit_parametrized_type_id(self, type):
    256
    257        type.template.accept(self)
    258        type_label = self.__type_label + '&lt;'
    259        parameters_label = []
    260        for p in type.parameters:
    261            p.accept(self)
    262            parameters_label.append(self.__type_label)
    263        self.__type_label = type_label + ', '.join(parameters_label) + '&gt;'
    264
    265    def visit_function_type_id(self, type):
    266
    267        # TODO: this needs to be implemented
    268        self.__type_ref = 'function_type'
    269        self.__type_label = 'function_type'
    270
    271    def process_doc(self, decl):
    272
    273        if decl.annotations.get('doc', ''):
    274            detail = self.processor.documentation.details(decl)
    275            if detail:
    276                # FIXME: Unfortunately, as of xsl-docbook 1.73, a section role
    277                #        doesn't translate into a div class attribute, so there
    278                #        is no way to style summaries and descriptions per se.
    279                #self.write('<section role="description">\n%s<section>\n'%detail)
    280                self.write('%s\n'%detail)
    281
    282    #################### ASG Visitor ###########################################
    283
    284    def visit_declaration(self, declaration):
    285        if self.processor.hide_undocumented and not declaration.annotations.get('doc'):
    286            return
    287        self.start_element('section', id=reference(declaration.name))
    288        self.write_element('title', escape(declaration.name[-1]))
    289        if isinstance(declaration, ASG.Function):
    290            # The primary index term is the unqualified name,
    291            # the secondary the qualified name.
    292            # This will result in index terms being grouped by name, with each
    293            # qualified name being listed within that group.
    294            indexterm = self.element('primary', escape(declaration.real_name[-1]))
    295            if self.secondary_index:
    296                indexterm += self.element('secondary', escape(str(declaration.real_name)))
    297            self.write_element('indexterm', indexterm, type='functions')
    298
    299        language = declaration.file.annotations['language']
    300        syntax = _detail_syntax[language](self.output)
    301        declaration.accept(syntax)
    302        syntax.finish()
    303        self.process_doc(declaration)
    304        self.end_element()
    305
    306    def generate_module_list(self, modules):
    307
    308        if modules:
    309            modules.sort(cmp=lambda a,b:cmp(a.name, b.name))
    310            self.start_element('section')
    311            self.write_element('title', modules[0].type.capitalize() + 's')
    312            self.start_element('itemizedlist')
    313            for m in modules:
    314                link = self.element('link', escape(str(m.name)), linkend=reference(m.name))
    315                self.write_element('listitem', self.element('para', link))
    316            self.end_element()
    317            self.end_element()
    318
    319
    320    def format_module_or_group(self, module, title, sort):
    321        self.start_element('section', id=reference(module.name))
    322        self.write_element('title', title)
    323
    324        declarations = module.declarations
    325        if not self.nested_modules:
    326            modules = [d for d in declarations if isinstance(d, ASG.Module)]
    327            declarations = [d for d in declarations if not isinstance(d, ASG.Module)]
    328            self.generate_module_list(modules)
    329
    330        sorter = DeclarationSorter.DeclarationSorter(declarations,
    331                                                     group_as_section=False)
    332        if self.processor.generate_summary:
    333            self.start_element('section')
    334            self.write_element('title', 'Summary')
    335            summary = SummaryFormatter(self.processor, self.output)
    336            if sort:
    337                for s in sorter:
    338                    #if s[-1] == 's': title = s + 'es Summary'
    339                    #else: title = s + 's Summary'
    340                    #self.start_element('section')
    341                    #self.write_element('title', escape(title))
    342                    for d in sorter[s]:
    343                        if not self.processor.hide_undocumented or d.annotations.get('doc'):
    344                            d.accept(summary)
    345                    #self.end_element()
    346            else:
    347                for d in declarations:
    348                    if not self.processor.hide_undocumented or d.annotations.get('doc'):
    349                        d.accept(summary)
    350            self.end_element()
    351            self.write('\n')
    352            self.start_element('section')
    353            self.write_element('title', 'Details')
    354        self.process_doc(module)
    355        self.push_scope(module.name)
    356        suffix = self.processor.generate_summary and ' Details' or ''
    357        if sort:
    358            for section in sorter:
    359                #title = section + suffix
    360                #self.start_element('section')
    361                #self.write_element('title', escape(title))
    362                for d in sorter[section]:
    363                    d.accept(self)
    364                #self.end_element()
    365        else:
    366            for d in declarations:
    367                d.accept(self)
    368        self.pop_scope()
    369        self.end_element()
    370        self.write('\n')
    371        if self.processor.generate_summary:
    372            self.end_element()
    373            self.write('\n')
    374
    375    def visit_module(self, module):
    376        if module.name:
    377            # Only print qualified names when modules are flattened.
    378            name = self.nested_modules and module.name[-1] or str(module.name)
    379            title = '%s %s'%(module.type.capitalize(), name)
    380        else:
    381            title = 'Global %s'%module.type.capitalize()
    382
    383        self.format_module_or_group(module, title, True)
    384
    385    def visit_group(self, group):
    386        self.format_module_or_group(group, group.name[-1].capitalize(), False)
    387
    388    def visit_class(self, class_):
    389
    390        if self.processor.hide_undocumented and not class_.annotations.get('doc'):
    391            return
    392        self.start_element('section', id=reference(class_.name))
    393        title = '%s %s'%(class_.type, class_.name[-1])
    394        self.write_element('title', escape(title))
    395        indexterm = self.element('primary', escape(class_.name[-1]))
    396        if self.secondary_index:
    397            indexterm += self.element('secondary', escape(str(class_.name)))
    398        self.write_element('indexterm', indexterm, type='types')
    399
    400        if self.inheritance_graphs:
    401            formatter = InheritanceFormatter(os.path.join(self.base_dir, 'images'),
    402                                             self.graph_color)
    403            png = formatter.format_class(class_, 'png')
    404            svg = formatter.format_class(class_, 'svg')
    405            if png or svg:
    406                self.start_element('mediaobject')
    407                if png:
    408                    imagedata = self.element('imagedata', '', fileref=png, format='PNG')
    409                    self.write_element('imageobject', imagedata)
    410                if svg:
    411                    imagedata = self.element('imagedata', '', fileref=svg, format='SVG')
    412                    self.write_element('imageobject', imagedata)
    413                self.end_element()
    414
    415        declarations = class_.declarations
    416        # If so desired, flatten inheritance tree
    417        if self.processor.inline_inherited_members:
    418            declarations = class_.declarations[:]
    419            bases = _BaseClasses()
    420            for i in class_.parents:
    421                bases.visit_inheritance(i)
    422            for base in bases.classes:
    423                for d in base.declarations:
    424                    if type(d) == ASG.Operation:
    425                        if d.real_name[-1] == base.name[-1]:
    426                            # Constructor
    427                            continue
    428                        elif d.real_name[-1] == '~' + base.name[-1]:
    429                            # Destructor
    430                            continue
    431                        elif d.real_name[-1] == 'operator=':
    432                            # Assignment
    433                            continue
    434                    declarations.append(d)
    435
    436        sorter = DeclarationSorter.DeclarationSorter(declarations,
    437                                                     group_as_section=False)
    438
    439        if self.processor.generate_summary:
    440            self.start_element('section')
    441            self.write_element('title', 'Summary')
    442            summary = SummaryFormatter(self.processor, self.output)
    443            summary.process_doc(class_)
    444            for section in sorter:
    445                #title = section + ' Summary'
    446                #self.start_element('section')
    447                #self.write_element('title', escape(title))
    448                for d in sorter[section]:
    449                    if not self.processor.hide_undocumented or d.annotations.get('doc'):
    450                        d.accept(summary)
    451                #self.end_element()
    452            self.end_element()
    453            self.write('\n')
    454            self.start_element('section')
    455            self.write_element('title', 'Details')
    456        self.process_doc(class_)
    457        self.push_scope(class_.name)
    458        suffix = self.processor.generate_summary and ' Details' or ''
    459        for section in sorter:
    460            #title = section + suffix
    461            #self.start_element('section')
    462            #self.write_element('title', escape(title))
    463            for d in sorter[section]:
    464                d.accept(self)
    465            #self.end_element()
    466        self.pop_scope()
    467        self.end_element()
    468        self.write('\n')
    469        if self.processor.generate_summary:
    470            self.end_element()
    471            self.write('\n')
    472
    473
    474    def visit_inheritance(self, inheritance):
    475
    476        for a in inheritance.attributes: self.element("modifier", a)
    477        self.element("classname", str(self.scope().prune((inheritance.parent.name))))
    478
    479    def visit_parameter(self, parameter):
    480
    481        parameter.type.accept(self)
    482
    483    visit_function = visit_declaration
    484
    485    def visit_enum(self, enum):
    486
    487        if self.processor.hide_undocumented and not declaration.annotations.get('doc'):
    488            return
    489
    490        self.start_element('section', id=reference(enum.name))
    491        self.write_element('title', escape(enum.name[-1]))
    492        indexterm = self.element('primary', escape(enum.name[-1]))
    493        if self.secondary_index:
    494            indexterm += self.element('secondary', escape(str(enum.name)))
    495        self.write_element('indexterm', indexterm, type='types')
    496
    497        language = enum.file.annotations['language']
    498        syntax = _detail_syntax[language](self.output)
    499        enum.accept(syntax)
    500        syntax.finish()
    501        self.process_doc(enum)
    502        self.end_element()
    503
    504
    505class DocCache:
    506    """"""
    507
    508    def __init__(self, processor, markup_formatters):
    509
    510        self._processor = processor
    511        self._markup_formatters = markup_formatters
    512        # Make sure we have a default markup formatter.
    513        if '' not in self._markup_formatters:
    514            self._markup_formatters[''] = Markup.Formatter()
    515        for f in self._markup_formatters.values():
    516            f.init(self._processor)
    517        self._doc_cache = {}
    518
    519
    520    def _process(self, decl):
    521        """Return the documentation for the given declaration."""
    522
    523        key = id(decl)
    524        if key not in self._doc_cache:
    525            doc = decl.annotations.get('doc')
    526            if doc:
    527                formatter = self._markup_formatters.get(doc.markup,
    528                                                        self._markup_formatters[''])
    529                doc = formatter.format(decl)
    530            else:
    531                doc = Markup.Struct()
    532            self._doc_cache[key] = doc
    533            return doc
    534        else:
    535            return self._doc_cache[key]
    536
    537
    538    def summary(self, decl):
    539        """"""
    540
    541        doc = self._process(decl)
    542        return doc.summary
    543
    544
    545    def details(self, decl):
    546        """"""
    547
    548        doc = self._process(decl)
    549        return doc.details
    550
    551
    552
    553class Formatter(Processor):
    554    """Generate a DocBook reference."""
    555
    556    markup_formatters = Parameter({'rst':RST(),
    557                                   'reStructuredText':RST(),
    558                                   'javadoc':Javadoc()},
    559                                  'Markup-specific formatters.')
    560    title = Parameter(None, 'title to be used in top-level section')
    561    nested_modules = Parameter(False, """Map the module tree to a tree of docbook sections.""")
    562    generate_summary = Parameter(False, 'generate scope summaries')
    563    hide_undocumented = Parameter(False, 'hide declarations without a doc-string')
    564    inline_inherited_members = Parameter(False, 'show inherited members')
    565    secondary_index_terms = Parameter(True, 'add fully-qualified names to index')
    566    with_inheritance_graphs = Parameter(True, 'whether inheritance graphs should be generated')
    567    graph_color = Parameter('#ffcc99', 'base color for inheritance graphs')
    568
    569    def process(self, ir, **kwds):
    570
    571        self.set_parameters(kwds)
    572        if not self.output: raise MissingArgument('output')
    573        self.ir = self.merge_input(ir)
    574
    575        self.documentation = DocCache(self, self.markup_formatters)
    576        self.toc = TOC(Linker())
    577        for d in self.ir.asg.declarations:
    578            d.accept(self.toc)
    579
    580        output = open(self.output, 'w')
    581        output.write('<section>\n')
    582        if self.title:
    583            output.write('<title>%s</title>\n'%self.title)
    584        detail_formatter = DetailFormatter(self, output,
    585                                           os.path.dirname(self.output),
    586                                           self.nested_modules,
    587                                           self.secondary_index_terms,
    588                                           self.with_inheritance_graphs,
    589                                           self.graph_color)
    590
    591        declarations = self.ir.asg.declarations
    592
    593        if not self.nested_modules:
    594
    595            modules = [d for d in declarations if isinstance(d, ASG.Module)]
    596            detail_formatter.generate_module_list(modules)
    597
    598            module_lister = ModuleLister()
    599            for d in self.ir.asg.declarations:
    600                d.accept(module_lister)
    601            modules = module_lister.modules
    602            modules.sort(cmp=lambda a,b:cmp(a.name, b.name))
    603            declarations = [d for d in self.ir.asg.declarations
    604                            if not isinstance(d, ASG.Module)]
    605            declarations.sort(cmp=lambda a,b:cmp(a.name, b.name))
    606            declarations = modules + declarations
    607
    608        for d in declarations:
    609            d.accept(detail_formatter)
    610
    611        output.write('</section>\n')
    612        output.close()
    613
    614        return self.ir
    615

Generated on Thu Apr 16 16:27:13 2009 by
synopsis (version devel)