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