File: Synopsis/Formatters/DocBook/Markup/RST.py
   1#
   2# Copyright (C) 2007 Stefan Seefeld
   3# Copyright (C) Ollie Rutherford
   4# All rights reserved.
   5# Licensed to the public under the terms of the GNU LGPL (>= 2),
   6# see the file COPYING for details.
   7#
   8
   9from Synopsis.Formatters.DocBook.Markup import *
  10from docutils import writers, nodes, languages
  11from docutils.nodes import *
  12from docutils.core import *
  13from docutils.parsers.rst import roles
  14import string, re, StringIO
  15
  16class Writer(writers.Writer):
  17
  18    settings_spec = (
  19        'DocBook-Specific Options',
  20        None,
  21        (('Set DocBook document type. '
  22            'Choices are "article", "book", and "chapter". '
  23            'Default is "article".',
  24            ['--doctype'],
  25            {'default': 'article',
  26             'metavar': '<name>',
  27             'type': 'choice',
  28             'choices': ('article', 'book', 'chapter',)
  29            }
  30         ),
  31        )
  32    )
  33
  34
  35    """DocBook does its own section numbering"""
  36    settings_default_overrides = {'enable_section_numbering': 0}
  37
  38    output = None
  39    """Final translated form of `document`."""
  40
  41    def translate(self):
  42        visitor = DocBookTranslator(self.document)
  43        self.document.walkabout(visitor)
  44        self.output = visitor.astext()
  45
  46
  47class DocBookTranslator(nodes.NodeVisitor):
  48
  49    def __init__(self, document):
  50        nodes.NodeVisitor.__init__(self, document)
  51        self.language = languages.get_language(
  52            document.settings.language_code)
  53        self.doctype = document.settings.doctype
  54        self.body = []
  55        self.section = 0
  56        self.context = []
  57        self.colnames = []
  58        self.footnotes = {}
  59        self.footnote_map = {}
  60        self.docinfo = []
  61        self.title = ''
  62        self.subtitle = ''
  63
  64    def astext(self):
  65        return ''.join(self.docinfo
  66                    + self.body)
  67
  68    def encode(self, text):
  69        """Encode special characters in `text` & return."""
  70        # @@@ A codec to do these and all other 
  71        # HTML entities would be nice.
  72        text = text.replace("&", "&amp;")
  73        text = text.replace("<", "&lt;")
  74        text = text.replace('"', "&quot;")
  75        text = text.replace(">", "&gt;")
  76        return text
  77
  78    def encodeattr(self, text):
  79        """Encode attributes characters > 128 as &#XXX;"""
  80        buff = []
  81        for c in text:
  82            if ord(c) >= 128:
  83                buff.append('&#%d;' % ord(c))
  84            else:
  85                buff.append(c)
  86        return ''.join(buff)
  87
  88    def rearrange_footnotes(self):
  89        """
  90        Replaces ``foonote_reference`` placeholders with
  91        ``footnote`` element content as DocBook and reST
  92        handle footnotes differently.
  93
  94        DocBook defines footnotes inline, whereas they
  95        may be anywere in reST.  This function replaces the 
  96        first instance of a ``footnote_reference`` with 
  97        the ``footnote`` element itself, and later 
  98        references of the same a  footnote with 
  99        ``footnoteref`` elements.
 100        """
 101        for (footnote_id,refs) in self.footnote_map.items():
 102            ref_id, context, pos = refs[0]
 103            context[pos] = ''.join(self.footnotes[footnote_id])
 104            for ref_id, context, pos in refs[1:]:
 105                context[pos] = '<footnoteref linkend="%s"/>'% (footnote_id,)
 106
 107    def attval(self, text,
 108               transtable=string.maketrans('\n\r\t\v\f', '     ')):
 109        """Cleanse, encode, and return attribute value text."""
 110        return self.encode(text.translate(transtable))
 111
 112    def starttag(self, node, tagname, suffix='\n', infix='', **attributes):
 113        """
 114        Construct and return a start tag given a node 
 115        (id & class attributes are extracted), tag name, 
 116        and optional attributes.
 117        """
 118        atts = {}
 119        for (name, value) in attributes.items():
 120            atts[name.lower()] = value
 121
 122        for att in ('id',):             # node attribute overrides
 123            if node.has_key(att):
 124                atts[att] = node[att]
 125
 126        attlist = atts.items()
 127        attlist.sort()
 128        parts = [tagname.lower()]
 129        for name, value in attlist:
 130            if value is None:           # boolean attribute
 131                # this came from the html writer, but shouldn't
 132                # apply here, as an element with no attribute
 133                # isn't well-formed XML.
 134                parts.append(name.lower())
 135            elif isinstance(value, list):
 136                values = [str(v) for v in value]
 137                parts.append('%s="%s"' % (name.lower(),
 138                                          self.attval(' '.join(values))))
 139            else:
 140                name = self.encodeattr(name.lower())
 141                value = str(self.encodeattr(unicode(value)))
 142                value = self.attval(value)
 143                parts.append('%s="%s"' % (name,value))
 144
 145        return '<%s%s>%s' % (' '.join(parts), infix, suffix)
 146
 147    def emptytag(self, node, tagname, suffix='\n', **attributes):
 148        """Construct and return an XML-compatible empty tag."""
 149        return self.starttag(node, tagname, suffix, infix=' /', **attributes)
 150
 151    def visit_Text(self, node):
 152        self.body.append(self.encode(node.astext()))
 153
 154    def depart_Text(self, node):
 155        pass
 156
 157    def visit_address(self, node):
 158        # handled by visit_docinfo
 159        pass
 160
 161    def depart_address(self, node):
 162        # handled by visit_docinfo
 163        pass
 164
 165    def visit_admonition(self, node, name=''):
 166        self.body.append(self.starttag(node, 'note'))
 167
 168    def depart_admonition(self, node=None):
 169        self.body.append('</note>\n')
 170
 171    def visit_attention(self, node):
 172        self.body.append(self.starttag(node, 'note'))
 173        self.body.append('\n<title>%s</title>\n'
 174            % (self.language.labels[node.tagname],))
 175
 176    def depart_attention(self, node):
 177        self.body.append('</note>\n')
 178
 179    def visit_attribution(self, node):
 180        # attribution must precede blockquote content
 181        if isinstance(node.parent, nodes.block_quote):
 182            raise nodes.SkipNode
 183        self.body.append(self.starttag(node, 'attribution', ''))
 184
 185    def depart_attribution(self, node):
 186        # attribution must precede blockquote content
 187        if not isinstance(node.parent, nodes.block_quote):
 188            self.body.append('</attribution>\n')
 189
 190    # author is handled in ``visit_docinfo()``
 191    def visit_author(self, node):
 192        raise nodes.SkipNode
 193
 194    # authors is handled in ``visit_docinfo()``
 195    def visit_authors(self, node):
 196        raise nodes.SkipNode
 197
 198    def visit_block_quote(self, node):
 199        self.body.append(self.starttag(node, 'blockquote'))
 200        if isinstance(node[-1], nodes.attribution):
 201            self.body.append('<attribution>%s</attribution>\n' % node[-1].astext())
 202
 203    def depart_block_quote(self, node):
 204        self.body.append('</blockquote>\n')
 205
 206    def visit_bullet_list(self, node):
 207        self.body.append(self.starttag(node, 'itemizedlist'))
 208
 209    def depart_bullet_list(self, node):
 210        self.body.append('</itemizedlist>\n')
 211
 212    def visit_caption(self, node):
 213        # NOTE: ideally, this should probably be stuffed into
 214        # the mediaobject as a "caption" element
 215        self.body.append(self.starttag(node, 'para'))
 216
 217    def depart_caption(self, node):
 218        self.body.append('</para>')
 219
 220    def visit_caution(self, node):
 221        self.body.append(self.starttag(node, 'caution'))
 222        self.body.append('\n<title>%s</title>\n'
 223            % (self.language.labels[node.tagname],))
 224
 225    def depart_caution(self, node):
 226        self.body.append('</caution>\n')
 227
 228    # reST & DocBook ciations are somewhat 
 229    # different creatures.
 230    #
 231    # reST seems to handle citations as a labled
 232    # footnotes, whereas DocBook doesn't from what
 233    # I can tell.  In DocBook, it looks like they're
 234    # an abbreviation for a published work, which 
 235    # might be in the bibliography.
 236    #
 237    # Quote:
 238    #
 239    #   The content of a Citation is assumed to be a reference 
 240    #   string, perhaps identical to an abbreviation in an entry 
 241    #   in a Bibliography. 
 242    #
 243    # I hoped to have citations behave look footnotes,
 244    # using the citation label as the footnote label,
 245    # which would seem functionally equivlent, however
 246    # the DocBook stylesheets for generating HTML & FO 
 247    # output don't seem to be using the label for foonotes
 248    # so this doesn't work very well.
 249    #
 250    # Any ideas or suggestions would be welcome.
 251
 252    def visit_citation(self, node):
 253        self.visit_footnote(node)
 254
 255    def depart_citation(self, node):
 256        self.depart_footnote(node)
 257
 258    def visit_citation_reference(self, node):
 259        self.visit_footnote_reference(node)
 260
 261    def depart_citation_reference(self, node):
 262        # there isn't a a depart_footnote_reference
 263        pass
 264
 265    def visit_classifier(self, node):
 266        self.body.append(self.starttag(node, 'type'))
 267
 268    def depart_classifier(self, node):
 269        self.body.append('</type>\n')
 270
 271    def visit_colspec(self, node):
 272        self.colnames.append('col_%d' % (len(self.colnames) + 1,))
 273        atts = {'colname': self.colnames[-1]}
 274        self.body.append(self.emptytag(node, 'colspec', **atts))
 275
 276    def depart_colspec(self, node):
 277        pass
 278
 279    def visit_comment(self, node, sub=re.compile('-(?=-)').sub):
 280        """Escape double-dashes in comment text."""
 281        self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
 282        raise nodes.SkipNode
 283
 284    # contact is handled in ``visit_docinfo()``
 285    def visit_contact(self, node):
 286        raise nodes.SkipNode
 287
 288    # copyright is handled in ``visit_docinfo()``
 289    def visit_copyright(self, node):
 290        raise nodes.SkipNode
 291
 292    def visit_danger(self, node):
 293        self.body.append(self.starttag(node, 'caution'))
 294        self.body.append('\n<title>%s</title>\n'
 295            % (self.language.labels[node.tagname],))
 296
 297    def depart_danger(self, node):
 298        self.body.append('</caution>\n')
 299
 300    # date is handled in ``visit_docinfo()``
 301    def visit_date(self, node):
 302        raise nodes.SkipNode
 303
 304    def visit_decoration(self, node):
 305        pass
 306    def depart_decoration(self, node):
 307        pass
 308
 309    def visit_definition(self, node):
 310        # "term" is not closed in depart_term
 311        self.body.append('</term>\n')
 312        self.body.append(self.starttag(node, 'listitem'))
 313
 314    def depart_definition(self, node):
 315        self.body.append('</listitem>\n')
 316
 317    def visit_definition_list(self, node):
 318        self.body.append(self.starttag(node, 'variablelist'))
 319
 320    def depart_definition_list(self, node):
 321        self.body.append('</variablelist>\n')
 322
 323    def visit_definition_list_item(self, node):
 324        self.body.append(self.starttag(node, 'varlistentry'))
 325
 326    def depart_definition_list_item(self, node):
 327        self.body.append('</varlistentry>\n')
 328
 329    def visit_description(self, node):
 330        self.body.append(self.starttag(node, 'entry'))
 331
 332    def depart_description(self, node):
 333        self.body.append('</entry>\n')
 334
 335    def visit_docinfo(self, node):
 336        """
 337        Collects all docinfo elements for the document.
 338
 339        Since reST's bibliography elements don't map very
 340        cleanly to DocBook, rather than maintain state and
 341        check dependencies within the different visitor
 342        fuctions all processing of bibliography elements
 343        is dont within this function.
 344
 345        .. NOTE:: Skips processing of all child nodes as
 346                  everything should be collected here.
 347        """
 348
 349        # XXX There are a number of fields in docinfo elements
 350        #     which don't map nicely to docbook elements and 
 351        #     reST allows one to insert arbitrary fields into
 352        #     the header, We need to be able to handle fields
 353        #     which either don't map nicely or are unexpected.
 354        #     I'm thinking of just using DocBook to display these
 355        #     elements in some sort of tabular format -- but
 356        #     to collect them is not straight-forward.  
 357        #     Paragraphs, links, lists, etc... can all live within
 358        #     the values so we either need a separate visitor
 359        #     to translate these elements, or to maintain state
 360        #     in any possible child elements (not something I
 361        #     want to do).
 362
 363        docinfo = ['<%sinfo>\n' % self.doctype]
 364
 365        address = ''
 366        authors = []
 367        author = ''
 368        contact = ''
 369        date = ''
 370        legalnotice = ''
 371        orgname = ''
 372        releaseinfo = ''
 373        revision,version = '',''
 374
 375        docinfo.append('<title>%s</title>\n' % self.title)
 376        if self.subtitle:
 377            docinfo.append('<subtitle>%s</subtitle>\n' % self.subtitle)
 378
 379        for n in node:
 380            if isinstance(n, nodes.address):
 381                address = n.astext()
 382            elif isinstance(n, nodes.author):
 383                author = n.astext()
 384            elif isinstance(n, nodes.authors):
 385                for a in n:
 386                    authors.append(a.astext())
 387            elif isinstance(n, nodes.contact):
 388                contact = n.astext()
 389            elif isinstance(n, nodes.copyright):
 390                legalnotice = n.astext()
 391            elif isinstance(n, nodes.date):
 392                date = n.astext()
 393            elif isinstance(n, nodes.organization):
 394                orgname = n.astext()
 395            elif isinstance(n, nodes.revision):
 396                # XXX yuck
 397                revision = 'Revision ' + n.astext()
 398            elif isinstance(n, nodes.status):
 399                releaseinfo = n.astext()
 400            elif isinstance(n, nodes.version):
 401                # XXX yuck
 402                version = 'Version ' + n.astext()
 403            elif isinstance(n, nodes.field):
 404                # XXX
 405                import sys
 406                print >> sys.stderr, "I don't do 'field' yet"
 407                print n.astext()
 408            # since all child nodes are handled here raise an exception
 409            # if node is not handled, so it doesn't silently slip through.
 410            else:
 411                print dir(n)
 412                print n.astext()
 413                raise self.unimplemented_visit(n)
 414
 415        # can only add author if name is present
 416        # since contact is associate with author, the contact
 417        # can also only be added if an author name is given.
 418        if author:
 419            docinfo.append('<author>\n')
 420            docinfo.append('<othername>%s</othername>\n' % author)
 421            if contact:
 422                docinfo.append('<email>%s</email>\n' % contact)
 423            docinfo.append('</author>\n')
 424
 425        if authors:
 426            docinfo.append('<authorgroup>\n')
 427            for name in authors:
 428                docinfo.append(
 429                    '<author><othername>%s</othername></author>\n' % name)
 430            docinfo.append('</authorgroup>\n')
 431
 432        if revision or version:
 433            edition = version
 434            if edition and revision:
 435                edition += ', ' + revision
 436            elif revision:
 437                edition = revision
 438            docinfo.append('<edition>%s</edition>\n' % edition)
 439
 440        if date:
 441            docinfo.append('<date>%s</date>\n' % date)
 442
 443        if orgname:
 444            docinfo.append('<orgname>%s</orgname>\n' % orgname)
 445
 446        if releaseinfo:
 447            docinfo.append('<releaseinfo>%s</releaseinfo>\n' % releaseinfo)
 448
 449        if legalnotice:
 450            docinfo.append('<legalnotice>\n')
 451            docinfo.append('<para>%s</para>\n' % legalnotice)
 452            docinfo.append('</legalnotice>\n')
 453
 454        if address:
 455            docinfo.append('<address xml:space="preserve">' +
 456                address + '</address>\n')
 457
 458        if len(docinfo) > 1:
 459            docinfo.append('</%sinfo>\n' % self.doctype)
 460
 461        self.docinfo = docinfo
 462
 463        raise nodes.SkipChildren
 464
 465    def depart_docinfo(self, node):
 466        pass
 467
 468    def visit_doctest_block(self, node):
 469        self.body.append('<informalexample>\n')
 470        self.body.append(self.starttag(node, 'programlisting'))
 471
 472    def depart_doctest_block(self, node):
 473        self.body.append('</programlisting>\n')
 474        self.body.append('</informalexample>\n')
 475
 476    def visit_document(self, node):
 477        pass
 478
 479    def depart_document(self, node):
 480        self.rearrange_footnotes()
 481
 482    def visit_emphasis(self, node):
 483        self.body.append('<emphasis>')
 484
 485    def depart_emphasis(self, node):
 486        self.body.append('</emphasis>')
 487
 488    def visit_entry(self, node):
 489        tagname = 'entry'
 490        atts = {}
 491        if node.has_key('morerows'):
 492            atts['morerows'] = node['morerows']
 493        if node.has_key('morecols'):
 494            atts['namest'] = self.colnames[self.entry_level]
 495            atts['nameend'] = self.colnames[self.entry_level+ node['morecols']]
 496        self.entry_level += 1   # for tracking what namest and nameend are
 497        self.body.append(self.starttag(node, tagname, '', **atts))
 498
 499    def depart_entry(self, node):
 500        self.body.append('</entry>\n')
 501
 502    def visit_enumerated_list(self, node):
 503        # TODO: need to specify "mark" type used for list items
 504        self.body.append(self.starttag(node, 'orderedlist'))
 505
 506    def depart_enumerated_list(self, node):
 507        self.body.append('</orderedlist>\n')
 508
 509    def visit_error(self, node):
 510        self.body.append(self.starttag(node, 'caution'))
 511        self.body.append('\n<title>%s</title>\n'
 512            % (self.language.labels[node.tagname],))
 513
 514    def depart_error(self, node):
 515        self.body.append('</caution>\n')
 516
 517    # TODO: wrap with some element (filename used in DocBook example)
 518    def visit_field(self, node):
 519        self.body.append(self.starttag(node, 'varlistentry'))
 520
 521    def depart_field(self, node):
 522        self.body.append('</varlistentry>\n')
 523
 524    # TODO: see if this should be wrapped with some element
 525    def visit_field_argument(self, node):
 526        self.body.append(' ')
 527
 528    def depart_field_argument(self, node):
 529        pass
 530
 531    def visit_field_body(self, node):
 532        # NOTE: this requires that a field body always
 533        #   be present, which looks like the case
 534        #   (from docutils.dtd)
 535        self.body.append(self.context.pop())
 536        self.body.append(self.starttag(node, 'listitem'))
 537
 538    def depart_field_body(self, node):
 539        self.body.append('</listitem>\n')
 540
 541    def visit_field_list(self, node):
 542        self.body.append(self.starttag(node, 'variablelist'))
 543
 544    def depart_field_list(self, node):
 545        self.body.append('</variablelist>\n')
 546
 547    def visit_field_name(self, node):
 548        self.body.append(self.starttag(node, 'term'))
 549        # popped by visit_field_body, so "field_argument" is
 550        # content within "term"
 551        self.context.append('</term>\n')
 552
 553    def depart_field_name(self, node):
 554        pass
 555
 556    def visit_figure(self, node):
 557        self.body.append(self.starttag(node, 'informalfigure'))
 558        self.body.append('<blockquote>')
 559
 560    def depart_figure(self, node):
 561        self.body.append('</blockquote>')
 562        self.body.append('</informalfigure>\n')
 563
 564    # TODO: footer (this is where 'generated by docutils' arrives)
 565    # if that's all that will be there, it could map to "colophon"
 566    def visit_footer(self, node):
 567        raise nodes.SkipChildren
 568
 569    def depart_footer(self, node):
 570        pass
 571
 572    def visit_footnote(self, node):
 573        self.footnotes[node['ids'][0]] = []
 574        atts = {'id': node['ids'][0]}
 575        if isinstance(node[0], nodes.label):
 576            atts['label'] = node[0].astext()
 577        self.footnotes[node['ids'][0]].append(
 578            self.starttag(node, 'footnote', **atts))
 579
 580        # replace body with this with a footnote collector list
 581        # which will hold all the contents for this footnote.
 582        # This needs to be kept separate so it can be used to replace
 583        # the first ``footnote_reference`` as DocBook defines 
 584        # ``footnote`` elements inline. 
 585        self._body = self.body
 586        self.body = self.footnotes[node['ids'][0]]
 587
 588    def depart_footnote(self, node):
 589        # finish footnote and then replace footnote collector
 590        # with real body list.
 591        self.footnotes[node['ids'][0]].append('</footnote>')
 592        self.body = self._body
 593        self._body = None
 594
 595    def visit_footnote_reference(self, node):
 596        if node.has_key('refid'):
 597            refid = node['refid']
 598        else:
 599            refid = self.document.nameids[node['refname']]
 600
 601        # going to replace this footnote reference with the actual
 602        # footnote later on, so store the footnote id to replace
 603        # this reference with and the list and position to replace it
 604        # in. Both list and position are stored in case a footnote
 605        # reference is within a footnote, in which case ``self.body``
 606        # won't really be ``self.body`` but a footnote collector
 607        # list.
 608        refs = self.footnote_map.get(refid, [])
 609        refs.append((node['ids'][0], self.body, len(self.body),))
 610        self.footnote_map[refid] = refs
 611
 612        # add place holder list item which should later be 
 613        # replaced with the contents of the footnote element
 614        # and it's child elements
 615        self.body.append('<!-- REPLACE WITH FOOTNOTE -->')
 616
 617        raise nodes.SkipNode
 618
 619    def visit_header(self, node):
 620        pass
 621    def depart_header(self, node):
 622        pass
 623
 624    # ??? does anything need to be done for generated?
 625    def visit_generated(self, node):
 626        pass
 627    def depart_generated(self, node):
 628        pass
 629
 630    def visit_hint(self, node):
 631        self.body.append(self.starttag(node, 'note'))
 632        self.body.append('\n<title>%s</title>\n'
 633            % (self.language.labels[node.tagname],))
 634
 635    def depart_hint(self, node):
 636        self.body.append('</note>\n')
 637
 638    def visit_image(self, node):
 639        if isinstance(node.parent, nodes.paragraph):
 640            element = 'inlinemediaobject'
 641        elif isinstance(node.parent, nodes.reference):
 642            element = 'inlinemediaobject'
 643        else:
 644            element = 'mediaobject'
 645        atts = node.attributes.copy()
 646        atts['fileref'] = atts['uri']
 647        alt = None
 648        del atts['uri']
 649        if atts.has_key('alt'):
 650            alt = atts['alt']
 651            del atts['alt']
 652        if atts.has_key('height'):
 653            atts['depth'] = atts['height']
 654            del atts['height']
 655        self.body.append('<%s>' % element)
 656        self.body.append('<imageobject>')
 657        self.body.append(self.emptytag(node, 'imagedata', **atts))
 658        self.body.append('</imageobject>')
 659        if alt:
 660            self.body.append('<textobject><phrase>''%s</phrase></textobject>\n' % alt)
 661        self.body.append('</%s>' % element)
 662
 663    def depart_image(self, node):
 664        pass
 665
 666    def visit_important(self, node):
 667        self.body.append(self.starttag(node, 'important'))
 668
 669    def depart_important(self, node):
 670        self.body.append('</important>')
 671
 672    # @@@ Incomplete, pending a proper implementation on the
 673    # Parser/Reader end.
 674    # XXX see if the default for interpreted should be ``citetitle``
 675    def visit_interpreted(self, node):
 676        self.body.append('<constant>\n')
 677
 678    def depart_interpreted(self, node):
 679        self.body.append('</constant>\n')
 680
 681    def visit_label(self, node):
 682        # getting label for "footnote" in ``visit_footnote``
 683        # because label is an attribute for the ``footnote``
 684        # element.
 685        if isinstance(node.parent, nodes.footnote):
 686            raise nodes.SkipNode
 687        # citations are currently treated as footnotes
 688        elif isinstance(node.parent, nodes.citation):
 689            raise nodes.SkipNode
 690
 691    def depart_label(self, node):
 692        pass
 693
 694    def visit_legend(self, node):
 695        # legend is placed inside the figure's ``blockquote``
 696        # so there's nothing special to be done for it
 697        pass
 698
 699    def depart_legend(self, node):
 700        pass
 701
 702    def visit_line_block(self, node):
 703        self.body.append(self.starttag(node, 'literallayout'))
 704
 705    def depart_line_block(self, node):
 706        self.body.append('</literallayout>\n')
 707
 708    def visit_list_item(self, node):
 709        self.body.append(self.starttag(node, 'listitem'))
 710
 711    def depart_list_item(self, node):
 712        self.body.append('</listitem>\n')
 713
 714    def visit_literal(self, node):
 715         self.body.append('<literal>')
 716
 717    def depart_literal(self, node):
 718        self.body.append('</literal>')
 719
 720    def visit_literal_block(self, node):
 721        self.body.append(self.starttag(node, 'programlisting'))
 722
 723    def depart_literal_block(self, node):
 724        self.body.append('</programlisting>\n')
 725
 726    def visit_note(self, node):
 727        self.body.append(self.starttag(node, 'note'))
 728        self.body.append('\n<title>%s</title>\n'
 729            % (self.language.labels[node.tagname],))
 730
 731    def depart_note(self, node):
 732        self.body.append('</note>\n')
 733
 734    def visit_option(self, node):
 735        self.body.append(self.starttag(node, 'command'))
 736        if self.context[-1]:
 737            self.body.append(', ')
 738
 739    def depart_option(self, node):
 740        self.context[-1] += 1
 741        self.body.append('</command>')
 742
 743    def visit_option_argument(self, node):
 744        self.body.append(node.get('delimiter', ' '))
 745        self.body.append(self.starttag(node, 'replaceable', ''))
 746
 747    def depart_option_argument(self, node):
 748        self.body.append('</replaceable>')
 749
 750    def visit_option_group(self, node):
 751        self.body.append(self.starttag(node, 'entry'))
 752        self.context.append(0)
 753
 754    def depart_option_group(self, node):
 755        self.context.pop()
 756        self.body.append('</entry>\n')
 757
 758    def visit_option_list(self, node):
 759        self.body.append(self.starttag(node, 'informaltable', frame='all'))
 760        self.body.append('<tgroup cols="2">\n')
 761        self.body.append('<colspec colname="option_col"/>\n')
 762        self.body.append('<colspec colname="description_col"/>\n')
 763        self.body.append('<tbody>\n')
 764
 765    def depart_option_list(self, node):
 766        self.body.append('</tbody>')
 767        self.body.append('</tgroup>\n')
 768        self.body.append('</informaltable>\n')
 769
 770    def visit_option_list_item(self, node):
 771        self.body.append(self.starttag(node, 'row'))
 772
 773    def depart_option_list_item(self, node):
 774        self.body.append('</row>\n')
 775
 776    def visit_option_string(self, node):
 777        pass
 778
 779    def depart_option_string(self, node):
 780        pass
 781
 782    # organization is handled in ``visit_docinfo()``
 783    def visit_organization(self, node):
 784        raise nodes.SkipNode
 785
 786    def visit_paragraph(self, node):
 787        self.body.append(self.starttag(node, 'para', ''))
 788
 789    def depart_paragraph(self, node):
 790        self.body.append('</para>')
 791
 792    # TODO: problematic
 793    visit_problematic = depart_problematic = lambda self, node: None
 794
 795    def visit_raw(self, node):
 796        if node.has_key('format') and node['format'] == 'docbook':
 797            self.body.append(node.astext())
 798        raise node.SkipNode
 799
 800    def visit_reference(self, node):
 801        atts = {}
 802        if node.has_key('refuri'):
 803            atts['url'] = node['refuri']
 804            self.context.append('ulink')
 805        elif node.has_key('refid'):
 806            atts['linkend'] = node['refid']
 807            self.context.append('link')
 808        elif node.has_key('refname'):
 809            atts['linkend'] = self.document.nameids[node['refname']]
 810            self.context.append('link')
 811        # if parent is a section, 
 812        # wrap link in a para
 813        if isinstance(node.parent, nodes.section):
 814            self.body.append('<para>')
 815        self.body.append(self.starttag(node, self.context[-1], '', **atts))
 816
 817    def depart_reference(self, node):
 818        self.body.append('</%s>' % (self.context.pop(),))
 819        # if parent is a section, 
 820        # wrap link in a para
 821        if isinstance(node.parent, nodes.section):
 822            self.body.append('</para>')
 823
 824    # revision is handled in ``visit_docinfo()``
 825    def visit_revision(self, node):
 826        raise nodes.SkipNode
 827
 828    def visit_row(self, node):
 829        self.entry_level = 0
 830        self.body.append(self.starttag(node, 'row'))
 831
 832    def depart_row(self, node):
 833        self.body.append('</row>\n')
 834
 835    def visit_rubric(self, node):
 836        self.body.append(self.starttag(node, 'bridgehead'))
 837
 838    def depart_rubric(self, node):
 839        self.body.append('</bridgehead>')
 840
 841    def visit_section(self, node):
 842        if self.section == 0 and self.doctype == 'book':
 843            self.body.append(self.starttag(node, 'chapter'))
 844        else:
 845            self.body.append(self.starttag(node, 'section'))
 846        self.section += 1
 847
 848    def depart_section(self, node):
 849        self.section -= 1
 850        if self.section == 0 and self.doctype == 'book':
 851            self.body.append('</chapter>\n')
 852        else:
 853            self.body.append('</section>\n')
 854
 855    def visit_sidebar(self, node):
 856        self.body.append(self.starttag(node, 'sidebar'))
 857        if isinstance(node[0], nodes.title):
 858            self.body.append('<sidebarinfo>\n')
 859            self.body.append('<title>%s</title>\n' % node[0].astext())
 860            if isinstance(node[1], nodes.subtitle):
 861                self.body.append('<subtitle>%s</subtitle>\n' % node[1].astext())
 862            self.body.append('</sidebarinfo>\n')
 863
 864    def depart_sidebar(self, node):
 865        self.body.append('</sidebar>\n')
 866
 867    # author is handled in ``visit_docinfo()``
 868    def visit_status(self, node):
 869        raise nodes.SkipNode
 870
 871    def visit_strong(self, node):
 872        self.body.append('<emphasis role="strong">')
 873
 874    def depart_strong(self, node):
 875        self.body.append('</emphasis>')
 876
 877    def visit_subscript(self, node):
 878        self.body.append(self.starttag(node, 'subscript', ''))
 879
 880    def depart_subscript(self, node):
 881        self.body.append('</subscript>')
 882
 883    def visit_substitution_definition(self, node):
 884        raise nodes.SkipNode
 885
 886    def visit_substitution_reference(self, node):
 887        self.unimplemented_visit(node)
 888
 889    def visit_subtitle(self, node):
 890        # document title needs to go into
 891        # <type>info/subtitle, so save it for
 892        # when we do visit_docinfo
 893        if isinstance(node.parent, nodes.document):
 894            self.subtitle = node.astext()
 895            raise nodes.SkipNode
 896        else:
 897            # sidebar subtitle needs to go into a sidebarinfo element
 898            #if isinstance(node.parent, nodes.sidebar):
 899            #    self.body.append('<sidebarinfo>')
 900            if isinstance(node.parent, nodes.sidebar):
 901                raise nodes.SkipNode
 902            self.body.append(self.starttag(node, 'subtitle', ''))
 903
 904    def depart_subtitle(self, node):
 905        if not isinstance(node.parent, nodes.document):
 906            self.body.append('</subtitle>\n')
 907        #if isinstance(node.parent, nodes.sidebar):
 908        #    self.body.append('</sidebarinfo>\n')
 909
 910    def visit_superscript(self, node):
 911        self.body.append(self.starttag(node, 'superscript', ''))
 912
 913    def depart_superscript(self, node):
 914        self.body.append('</superscript>')
 915
 916    # TODO: system_message
 917    visit_system_message = depart_system_message = lambda self, node: None
 918
 919    def visit_table(self, node):
 920        self.body.append(
 921            self.starttag(node, 'informaltable', frame='all')
 922        )
 923
 924    def depart_table(self, node):
 925        self.body.append('</informaltable>\n')
 926
 927    # don't think anything is needed for targets
 928    def visit_target(self, node):
 929        # XXX this would like to be a transform!
 930        # XXX comment this mess!
 931        handled = 0
 932        siblings = node.parent.children
 933        for i in range(len(siblings)):
 934            if siblings[i] is node:
 935                if i+1 < len(siblings):
 936                    next = siblings[i+1]
 937                    if isinstance(next,nodes.Text):
 938                        pass
 939                    elif not next.attributes.has_key('id'):
 940                        next['id'] = node['ids'][0]
 941                        handled = 1
 942        if not handled:
 943            if not node.parent.attributes.has_key('id'):
 944                # TODO node["ids"] 
 945                node.parent.attributes['id'] = node['ids'][0]
 946                handled = 1
 947        # might need to do more...
 948        # (if not handled, update the referrer to refer to the parent's id)
 949
 950    def depart_target(self, node):
 951        pass
 952
 953    def visit_tbody(self, node):
 954        self.body.append(self.starttag(node, 'tbody'))
 955
 956    def depart_tbody(self, node):
 957        self.body.append('</tbody>\n')
 958
 959    def visit_term(self, node):
 960        self.body.append(self.starttag(node, 'term'))
 961        self.body.append('<varname>')
 962
 963    def depart_term(self, node):
 964        # Leave the end tag "term" to ``visit_definition()``,
 965        # in case there's a classifier.
 966        self.body.append('</varname>')
 967
 968    def visit_tgroup(self, node):
 969        self.colnames = []
 970        atts = {'cols': node['cols']}
 971        self.body.append(self.starttag(node, 'tgroup', **atts))
 972
 973    def depart_tgroup(self, node):
 974        self.body.append('</tgroup>\n')
 975
 976    def visit_thead(self, node):
 977        self.body.append(self.starttag(node, 'thead'))
 978
 979    def depart_thead(self, node):
 980        self.body.append('</thead>\n')
 981
 982    def visit_tip(self, node):
 983        self.body.append(self.starttag(node, 'tip'))
 984
 985    def depart_tip(self, node):
 986        self.body.append('</tip>\n')
 987
 988    def visit_title(self, node):
 989        # document title needs to go inside
 990        # <type>info/title
 991        if isinstance(node.parent, nodes.document):
 992            self.title = node.astext()
 993            raise nodes.SkipNode
 994        elif isinstance(node.parent, nodes.sidebar):
 995            # sidebar title and subtitle are collected in visit_sidebar
 996            raise nodes.SkipNode
 997        else:
 998            self.body.append(self.starttag(node, 'title', ''))
 999
1000    def depart_title(self, node):
1001        if not isinstance(node.parent, nodes.document):
1002            self.body.append('</title>\n')
1003
1004    def visit_title_reference(self, node):
1005        self.body.append('<citetitle>')
1006
1007    def depart_title_reference(self, node):
1008        self.body.append('</citetitle>')
1009
1010    def visit_topic(self, node):
1011        # let DocBook handle Table of Contents generation
1012        if node.get('class') == 'contents':
1013            raise nodes.SkipChildren
1014        elif node.get('class') == 'abstract':
1015            self.body.append(self.starttag(node, 'abstract'))
1016            self.context.append('abstract')
1017        elif node.get('class') == 'dedication':
1018            # docbook only supports dedication in a book,
1019            # so we're faking it for article & chapter
1020            if self.doctype == 'book':
1021                self.body.append(self.starttag(node, 'dedication'))
1022                self.context.append('dedication')
1023            else:
1024                self.body.append(self.starttag(node, 'section'))
1025                self.context.append('section')
1026
1027        # generic "topic" element treated as a section
1028        elif node.get('class','') == '':
1029            self.body.append(self.starttag(node, 'section'))
1030            self.context.append('section')
1031        else:
1032            # XXX DEBUG CODE
1033            print 'class:', node.get('class')
1034            print node.__class__.__name__
1035            print node
1036            print `node`
1037            print dir(node)
1038            self.unimplemented_visit(node)
1039
1040    def depart_topic(self, node):
1041        if len(self.context):
1042            self.body.append('</%s>\n' % (self.context.pop(),))
1043
1044    def visit_transition(self, node):
1045        pass
1046    def depart_transition(self, node):
1047        pass
1048
1049    # author is handled in ``visit_docinfo()``
1050    def visit_version(self, node):
1051        raise nodes.SkipNode
1052
1053    def visit_warning(self, node):
1054        self.body.append(self.starttag(node, 'warning'))
1055
1056    def depart_warning(self, node):
1057        self.body.append('</warning>\n')
1058
1059    def unimplemented_visit(self, node):
1060        raise NotImplementedError('visiting unimplemented node type: %s'
1061                % node.__class__.__name__)
1062
1063
1064class SummaryExtractor(NodeVisitor):
1065    """A SummaryExtractor creates a document containing the first sentence of
1066    a source document."""
1067
1068    def __init__(self, document):
1069
1070        NodeVisitor.__init__(self, document)
1071        self.summary = None
1072
1073
1074    def visit_paragraph(self, node):
1075        """Copy the paragraph but only keep the first sentence."""
1076
1077        if self.summary is not None:
1078            return
1079
1080        summary_pieces = []
1081
1082        # Extract the first sentence.
1083        for child in node:
1084            if isinstance(child, Text):
1085                m = re.match(r'(\s*[\w\W]*?\.)(\s|$)', child.data)
1086                if m:
1087                    summary_pieces.append(Text(m.group(1)))
1088                    break
1089                else:
1090                    summary_pieces.append(Text(child))
1091            else:
1092                summary_pieces.append(child)
1093
1094        self.summary = self.document.copy()
1095        para = node.copy()
1096        para[:] = summary_pieces
1097        self.summary[:] = [para]
1098
1099
1100    def unknown_visit(self, node):
1101        'Ignore all unknown nodes'
1102
1103        pass
1104
1105
1106class RST(Formatter):
1107    """Format summary and detail documentation according to restructured text markup.
1108    """
1109
1110    def format(self, decl):
1111
1112        def ref(name, rawtext, text, lineno, inliner,
1113                options={}, content=[]):
1114
1115            name = utils.unescape(text)
1116            uri = self.lookup_symbol(name, decl.name[:-1])
1117            if uri:
1118                node = reference(rawtext, name, refid=uri, **options)
1119            else:
1120                node = emphasis(rawtext, name)
1121            return [node], []
1122
1123        roles.register_local_role('', ref)
1124
1125        errstream = StringIO.StringIO()
1126        settings = {}
1127        settings['halt_level'] = 2
1128        settings['warning_stream'] = errstream
1129        settings['traceback'] = True
1130
1131        doc = decl.annotations.get('doc')
1132        if doc:
1133            try:
1134                doctree = publish_doctree(doc.text, settings_overrides=settings)
1135                # Extract the summary.
1136                extractor = SummaryExtractor(doctree)
1137                doctree.walk(extractor)
1138
1139                reader = docutils.readers.doctree.Reader(parser_name='null')
1140
1141                # Publish the summary.
1142                if extractor.summary:
1143                    pub = Publisher(reader, None, None,
1144                                    source=io.DocTreeInput(extractor.summary),
1145                                    destination_class=io.StringOutput)
1146                    pub.writer = Writer()
1147                    pub.process_programmatic_settings(None, None, None)
1148                    dummy = pub.publish(enable_exit_status=None)
1149                    summary = pub.writer.output
1150                else:
1151                    summary = ''
1152
1153                # Publish the details.
1154                pub = Publisher(reader, None, None,
1155                                source=io.DocTreeInput(doctree),
1156                                destination_class=io.StringOutput)
1157                pub.writer = Writer()
1158                pub.process_programmatic_settings(None, None, None)
1159                dummy = pub.publish(enable_exit_status=None)
1160                details = pub.writer.output
1161
1162                return Struct(summary, details)
1163
1164            except docutils.utils.SystemMessage, error:
1165                xx, line, message = str(error).split(':', 2)
1166                print 'In DocString attached to declaration at %s:%d:'%(decl.file.name,
1167                                                                        decl.line)
1168                print '  line %s:%s'%(line, message)
1169
1170        return Struct('', '')
1171