Synopsis - Synopsis/Formatters/DocBook/Markup/RST.py

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

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