Synopsis - Cross-Reference

File: /sandbox/rsttodocbook.py
  1"""
  2:Author: Ollie Rutherfurd
  3:Contact: oliver@rutherfurd.net
  4:Revision: $Revision: 1.4 $
  5:Date: $Date: 2003/02/20 23:10:10 $
  6:Copyright: This module has been placed in the public domain.
  7
  8DocBook document tree Writer.
  9
 10This Writer converts a reST document tree to a subset
 11of DocBook.
 12
 13.. Note:: This is an unfinished work in progress.
 14
 15Document Types
 16==============
 17
 18This writer can create 3 types of DocBook documents:
 19
 201. "article" *(default)*
 212. "book"
 223. "chapter"
 23
 24.. Note:: When creating a "book" document, all first-level
 25    sections are output as "chapter" elements instead
 26    of "section" as in "article" and "chapter".
 27
 28Mappings
 29========
 30
 31Option List
 32-----------
 33
 34As there is no direct equivlent for a listing of program options
 35in DocBook_, as defined in reST_, a table containing the
 36option list contents is generated.
 37
 38Field List
 39----------
 40
 41Like `Option List`_, there is not direct equivlent for
 42a Field List in DocBook, so this is done using a
 43"variablelist".
 44
 45.. NOTE:: It might be better to switch Definition List
 46    to glossary or something similar, so Field List
 47    and Definition List are generating the same type
 48    of output.
 49
 50Bibliography Elements
 51---------------------
 52
 53Here's how reST's bibliography elements are mapped
 54to DocBook elements:
 55
 56+--------------+---------------------------------------------+
 57| reST Element | DocBook Element                             |
 58+==============+=============================================+
 59| author       | {doctype}info/author/othername              |
 60|              | or                                          |
 61|              | {doctype}info/authorgroup/author/othername  |
 62|              | if nested under ``authors``                 |
 63+--------------+---------------------------------------------+
 64| authors      | {doctype}info/authorgroup/                  |
 65+--------------+---------------------------------------------+
 66| contact      | {doctype}info/author/email                  |
 67+--------------+---------------------------------------------+
 68| copyright    | {doctype}info/legalnotice                   |
 69+--------------+---------------------------------------------+
 70| date         | {doctype}info/date                          |
 71+--------------+---------------------------------------------+
 72| organization | {doctype}info/orgname                       |
 73+--------------+---------------------------------------------+
 74| revision     | concatenated with ``version`` into          |
 75|              | {doctype}info/edition                       |
 76+--------------+---------------------------------------------+
 77| status       | {doctype}info/releaseinfo                   |
 78+--------------+---------------------------------------------+
 79| version      | concatenated with ``revision`` into         |
 80|              | {doctype}info/edition                       |
 81+--------------+---------------------------------------------+
 82
 83Note: ``{doctype}`` is the type of the DocBook document 
 84being generated, one of the following: ``article``, 
 85``book``, or ``chapter``.
 86
 87Todo
 88====
 89
 90- Inline images -- need to figure out how to identify an inline image
 91- list item marks are not guarenteed to be what was specified (if they
 92  are it is be coincidence, however unless one starts out of order
 93  they should match most of the time).
 94
 95Should para, note, etc... not in a section at the start
 96of the document be stuffed into an untitled ``section``?
 97
 98"""
 99
100__docformat__ = 'reStructuredText'
101
102import re
103import string
104from docutils import writers, nodes, languages
105from types import ListType
106
107class Writer(writers.Writer):
108
109    settings_spec = (
110        'DocBook-Specific Options',
111        None,
112        (('Set DocBook document type. '
113            'Choices are "article", "book", and "chapter". '
114            'Default is "article".',
115            ['--doctype'],
116            {'default': 'article', 
117             'metavar': '<name>',
118             'type': 'choice', 
119             'choices': ('article', 'book', 'chapter',)
120            }
121         ),
122        )
123    )
124
125    output = None
126    """Final translated form of `document`."""
127
128    def translate(self):
129        visitor = DocBookTranslator(self.document)
130        self.document.walkabout(visitor)
131        self.output = visitor.astext()
132
133
134class DocBookTranslator(nodes.NodeVisitor):
135
136    XML_DECL = '<?xml version="1.0" encoding="%s"?>\n'
137
138    DOCTYPE_DECL = """<!DOCTYPE %s 
139        PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN"
140        "http://www.oasis-open.org/docbook/xml/4.0/docbookx.dtd">\n"""
141
142    def __init__(self, document):
143        nodes.NodeVisitor.__init__(self, document)
144        self.language = languages.get_language(
145            document.settings.language_code)
146        self.doctype = document.settings.doctype
147        self.doc_header = [
148            self.XML_DECL % (document.settings.output_encoding,),
149            self.DOCTYPE_DECL % (self.doctype,),
150            '<%s>\n' % (self.doctype,),
151        ]
152        self.doc_footer = [
153            '</%s>\n' % (self.doctype,)
154        ]
155        self.body = []
156        self.section = 0
157        self.context = []
158        self.colnames = []
159        self.footnotes = {}
160        self.footnote_map = {}
161        self.docinfo = []
162
163    def astext(self):
164        return ''.join(self.doc_header
165                    + self.docinfo
166                    + self.body
167                    + self.doc_footer)
168
169    def encode(self, text):
170        """Encode special characters in `text` & return."""
171        # @@@ A codec to do these and all other 
172        # HTML entities would be nice.
173        text = text.replace("&", "&amp;")
174        text = text.replace("<", "&lt;")
175        text = text.replace('"', "&quot;")
176        text = text.replace(">", "&gt;")
177        return text
178
179    def rearrange_footnotes(self):
180        """
181        Replaces ``foonote_reference`` placeholders with
182        ``footnote`` element content as DocBook and reST
183        handle footnotes differently.
184
185        DocBook defines footnotes inline, whereas they
186        may be anywere in reST.  This function replaces the 
187        first instance of a ``footnote_reference`` with 
188        the ``footnote`` element itself, and later 
189        references of the same a  footnote with 
190        ``footnoteref`` elements.
191        """
192        for (footnote_id,refs) in self.footnote_map.items():
193            ref_id, context, pos = refs[0]
194            context[pos] = ''.join(self.footnotes[footnote_id])
195            for ref_id, context, pos in refs[1:]:
196                context[pos] = '<footnoteref linkend="%s"/>' \
197                    % (footnote_id,)
198
199    def attval(self, text,
200               transtable=string.maketrans('\n\r\t\v\f', '     ')):
201        """Cleanse, encode, and return attribute value text."""
202        return self.encode(text.translate(transtable))
203
204    def starttag(self, node, tagname, suffix='\n', infix='', **attributes):
205        """
206        Construct and return a start tag given a node 
207        (id & class attributes are extracted), tag name, 
208        and optional attributes.
209        """
210        atts = {}
211        for (name, value) in attributes.items():
212            atts[name.lower()] = value
213
214        for att in ('id',):             # node attribute overrides
215            if node.has_key(att):
216                atts[att] = node[att]
217
218        attlist = atts.items()
219        attlist.sort()
220        parts = [tagname.lower()]
221        for name, value in attlist:
222            if value is None:           # boolean attribute
223                # According to the HTML spec, ``<element boolean>`` is good,
224                # ``<element boolean="boolean">`` is bad.
225                # (But the XHTML (XML) spec says the opposite.  <sigh>)
226                parts.append(name.lower())
227            elif isinstance(value, ListType):
228                values = [str(v) for v in value]
229                parts.append('%s="%s"' % (name.lower(),
230                                          self.attval(' '.join(values))))
231            else:
232                parts.append('%s="%s"' % (name.lower(),
233                                          self.attval(str(value))))
234        return '<%s%s>%s' % (' '.join(parts), infix, suffix)
235
236    def emptytag(self, node, tagname, suffix='\n', **attributes):
237        """Construct and return an XML-compatible empty tag."""
238        return self.starttag(node, tagname, suffix, infix=' /', **attributes)
239
240    def visit_Text(self, node):
241        self.body.append(self.encode(node.astext()))
242
243    def depart_Text(self, node):
244        pass
245
246    def visit_attention(self, node):
247        self.body.append(self.starttag(node, 'note'))
248        self.body.append('\n<title>%s</title>\n' 
249            % (self.language.labels[node.tagname],))
250
251    def depart_attention(self, node):
252        self.body.append('</note>\n')
253
254    # author is handled in ``visit_docinfo()``
255    def visit_author(self, node):
256        raise nodes.SkipNode
257
258    # authors is handled in ``visit_docinfo()``
259    def visit_authors(self, node):
260        raise nodes.SkipNode
261
262    def visit_block_quote(self, node):
263        self.body.append(self.starttag(node, 'blockquote'))
264
265    def depart_block_quote(self, node):
266        self.body.append('</blockquote>\n')
267
268    def visit_bullet_list(self, node):
269        self.body.append(self.starttag(node, 'itemizedlist'))
270
271    def depart_bullet_list(self, node):
272        self.body.append('</itemizedlist>\n')
273
274    def visit_caption(self, node):
275        # NOTE: ideally, this should probably be stuffed into
276        # the mediaobject as a "caption" element
277        self.body.append(self.starttag(node, 'para'))
278
279    def depart_caption(self, node):
280        self.body.append('</para>')
281
282    def visit_caution(self, node):
283        self.body.append(self.starttag(node, 'caution'))
284        self.body.append('\n<title>%s</title>\n' 
285            % (self.language.labels[node.tagname],))
286
287    def depart_caution(self, node):
288        self.body.append('</caution>\n')
289
290    # reST seems to handle citations as a labled
291    # footnotes, whereas DocBook doesn't from what
292    # I can tell, so I'm not sure how to give DocBook
293    # citations that result in equivlent output
294    # as the docutils html writer.
295    #
296    # Currently, citations are handled as footnotes,
297    # using the citation label as the footnote label
298    # which seems functionally equivlent, but the
299    # DocBook stylesheets for generating HTML output
300    # don't seem to be using the label for foonotes
301    # so this doesn't work.
302    #
303    # So I'm at a bit of a loss as to how to 
304    # handle citations. Any ideas or suggestions would
305    # be welcome.
306
307    # TODO: citation
308    def visit_citation(self, node):
309        self.visit_footnote(node)
310
311    def depart_citation(self, node):
312        self.depart_footnote(node)
313
314    # TODO: citation_reference
315    def visit_citation_reference(self, node):
316        self.visit_footnote_reference(node)
317
318    def depart_citation_reference(self, node):
319        pass
320
321    def visit_classifier(self, node):
322        self.body.append(' : ')
323        self.body.append(self.starttag(node, 'type'))
324
325    def depart_classifier(self, node):
326        self.body.append('</type>\n')
327
328    def visit_colspec(self, node):
329        self.colnames.append('col_%d' % (len(self.colnames) + 1,))
330        atts = {'colname': self.colnames[-1]}
331        self.body.append(self.emptytag(node, 'colspec', **atts))
332
333    def depart_colspec(self, node):
334        pass
335
336    def visit_comment(self, node, sub=re.compile('-(?=-)').sub):
337        """Escape double-dashes in comment text."""
338        self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
339        raise nodes.SkipNode
340
341    # contact is handled in ``visit_docinfo()``
342    def visit_contact(self, node):
343        raise nodes.SkipNode
344
345    # copyright is handled in ``visit_docinfo()``
346    def visit_copyright(self, node):
347        raise nodes.SkipNode
348
349    def visit_danger(self, node):
350        self.body.append(self.starttag(node, 'caution'))
351        self.body.append('\n<title>%s</title>\n' 
352            % (self.language.labels[node.tagname],))
353
354    def depart_danger(self, node):
355        self.body.append('</caution>\n')
356
357    # date is handled in ``visit_docinfo()``
358    def visit_date(self, node):
359        raise nodes.SkipNode
360
361    # TODO: decoration
362    def visit_decoration(self, node):
363        pass
364
365    def depart_decoration(self, node):
366        pass
367
368    def visit_definition(self, node):
369        # "term" is not closed in depart_term
370        self.body.append('</term>\n')
371        self.body.append(self.starttag(node, 'listitem'))
372
373    def depart_definition(self, node):
374        self.body.append('</listitem>\n')
375
376    def visit_definition_list(self, node):
377        self.body.append(self.starttag(node, 'variablelist'))
378
379    def depart_definition_list(self, node):
380        self.body.append('</variablelist>\n')
381
382    def visit_definition_list_item(self, node):
383        self.body.append(self.starttag(node, 'varlistentry'))
384
385    def depart_definition_list_item(self, node):
386        self.body.append('</varlistentry>\n')
387
388    def visit_description(self, node):
389        self.body.append(self.starttag(node, 'entry'))
390
391    def depart_description(self, node):
392        self.body.append('</entry>\n')
393
394    def visit_docinfo(self, node):
395        """
396        Collects all docinfo elements for the document.
397
398        Since reST's bibliography elements don't map very
399        cleanly to DocBook, rather than maintain state and
400        check dependencies within the different visitor
401        fuctions all processing of bibliography elements
402        is dont within this function.
403
404        .. NOTE:: Skips processing of all child nodes as
405                  everything should be collected here.
406        """
407
408        docinfo = ['<%sinfo>\n' % self.doctype]
409
410        authors = []
411        author = ''
412        contact = ''
413        date = ''
414        legalnotice = ''
415        orgname = ''
416        releaseinfo = ''
417        revision,version = '',''
418
419        for n in node:
420            if isinstance(n, nodes.author):
421                author = n.astext()
422            elif isinstance(n, nodes.authors):
423                for a in n:
424                    authors.append(a.astext())
425            elif isinstance(n, nodes.contact):
426                contact = n.astext()
427            elif isinstance(n, nodes.copyright):
428                legalnotice = n.astext()
429            elif isinstance(n, nodes.date):
430                date = n.astext()
431            elif isinstance(n, nodes.organization):
432                orgname = n.astext()
433            elif isinstance(n, nodes.revision):
434                revision = 'Revision ' + n.astext()
435            elif isinstance(n, nodes.status):
436                releaseinfo = n.astext()
437            elif isinstance(n, nodes.version):
438                version = 'Version ' + n.astext()
439            # since all child nodes are handled here raise an exception
440            # if node is not handled, so it doesn't silently slip through.
441            else:
442                raise self.unimplemented_visit(n)
443
444        # can only add author if name is present
445        # since contact is associate with author, the contact
446        # can also only be added if an author name is given.
447        if author:
448            docinfo.append('<author>\n')
449            docinfo.append('<othername>%s</othername>\n' % author)
450            if contact:
451                docinfo.append('<email>%s</email>\n' % contact)
452            docinfo.append('</author>\n')
453
454        if authors:
455            docinfo.append('<authorgroup>\n')
456            for name in authors:
457                docinfo.append(
458                    '<author><othername>%s</othername></author>\n' % name)
459            docinfo.append('</authorgroup>\n')
460
461        if revision or version:
462            edition = version
463            if edition and revision:
464                edition += ', ' + revision
465            elif revision:
466                edition = revision
467            docinfo.append('<edition>%s</edition>\n' % edition)
468
469        if date:
470            docinfo.append('<date>%s</date>\n' % date)
471
472        if orgname:
473            docinfo.append('<orgname>%s</orgname>\n' % orgname)
474
475        if releaseinfo:
476            docinfo.append('<releaseinfo>%s</releaseinfo>\n' % releaseinfo)
477
478        if legalnotice:
479            docinfo.append('<legalnotice>\n')
480            docinfo.append('<para>%s</para>\n' % legalnotice)
481            docinfo.append('</legalnotice>\n')
482
483        if len(docinfo) > 1:
484            docinfo.append('</%sinfo>\n' % self.doctype)
485
486        self.docinfo = docinfo
487
488        raise nodes.SkipChildren
489
490    def depart_docinfo(self, node):
491        pass
492
493    def visit_doctest_block(self, node):
494        self.body.append('<informalexample>\n')
495        self.body.append(self.starttag(node, 'programlisting'))
496
497    def depart_doctest_block(self, node):
498        self.body.append('</programlisting>\n')
499        self.body.append('</informalexample>\n')
500
501    def visit_document(self, node):
502        pass
503
504    def depart_document(self, node):
505        self.rearrange_footnotes()
506
507    def visit_emphasis(self, node):
508        self.body.append(self.starttag(node, 'emphasis'))
509
510    def depart_emphasis(self, node):
511        self.body.append('</emphasis>\n')
512
513    def visit_entry(self, node):
514        tagname = 'entry'
515        atts = {}
516        if node.has_key('morerows'):
517            atts['morerows'] = node['morerows']
518        if node.has_key('morecols'):
519            atts['namest'] = self.colnames[self.entry_level]
520            atts['nameend'] = self.colnames[self.entry_level \
521                + node['morecols']]
522        self.entry_level += 1   # for tracking what namest and nameend are
523        self.body.append(self.starttag(node, tagname, **atts))
524
525    def depart_entry(self, node):
526        self.body.append('</entry>\n')
527
528    def visit_enumerated_list(self, node):
529        # TODO: need to specify "mark" type used for list items
530        self.body.append(self.starttag(node, 'orderedlist'))
531
532    def depart_enumerated_list(self, node):
533        self.body.append('</orderedlist>\n')
534
535    def visit_error(self, node):
536        self.body.append(self.starttag(node, 'caution'))
537        self.body.append('\n<title>%s</title>\n' 
538            % (self.language.labels[node.tagname],))
539
540    def depart_error(self, node):
541        self.body.append('</caution>\n')
542
543    # TODO: wrap with some element (filename used in DocBook example)
544    def visit_field(self, node):
545        self.body.append(self.starttag(node, 'varlistentry'))
546
547    def depart_field(self, node):
548        self.body.append('</varlistentry>\n')
549
550    # TODO: see if this should be wrapped with some element
551    def visit_field_argument(self, node):
552        self.body.append(' ')
553
554    def depart_field_argument(self, node):
555        pass
556
557    def visit_field_body(self, node):
558        # NOTE: this requires that a field body always
559        #   be present, which looks like the case
560        #   (from docutils.dtd)
561        self.body.append(self.context.pop())
562        self.body.append(self.starttag(node, 'listitem'))
563
564    def depart_field_body(self, node):
565        self.body.append('</listitem>\n')
566
567    def visit_field_list(self, node):
568        self.body.append(self.starttag(node, 'variablelist'))
569
570    def depart_field_list(self, node):
571        self.body.append('</variablelist>\n')
572
573    def visit_field_name(self, node):
574        self.body.append(self.starttag(node, 'term'))
575        # popped by visit_field_body, so "field_argument" is
576        # content within "term"
577        self.context.append('</term>\n')
578
579    def depart_field_name(self, node):
580        pass
581
582    def visit_figure(self, node):
583        self.body.append(self.starttag(node, 'informalfigure'))
584        self.body.append('<blockquote>')
585
586    def depart_figure(self, node):
587        self.body.append('</blockquote>')
588        self.body.append('</informalfigure>\n')
589
590    # TODO: footer (this is where 'generated by docutils' arrives)
591    # if that's all that will be there, it could map to "colophon"
592    def visit_footer(self, node):
593        raise nodes.SkipChildren
594
595    def depart_footer(self, node):
596        pass
597
598    def visit_footnote(self, node):
599        self.footnotes[node['id']] = []
600        atts = {'id': node['id']}
601        if isinstance(node[0], nodes.label):
602            # FIXME: this fails with the second auto-sequenece character
603            # used in the test document ``test.txt``.
604            atts['label'] = node[0].astext()
605        self.footnotes[node['id']].append(
606            self.starttag(node, 'footnote', **atts))
607
608        # replace body with this with a footnote collector list
609        # which will hold all the contents for this footnote.
610        # This needs to be kept separate so it can be used to replace
611        # the first ``footnote_reference`` as DocBook defines 
612        # ``footnote`` elements inline. 
613        self._body = self.body
614        self.body = self.footnotes[node['id']]
615
616    def depart_footnote(self, node):
617        # finish footnote and then replace footnote collector
618        # with real body list.
619        self.footnotes[node['id']].append('</footnote>\n')
620        self.body = self._body
621        self._body = None
622
623    def visit_footnote_reference(self, node):
624        if node.has_key('refid'):
625            refid = node['refid']
626        else:
627            refid = self.document.nameids[node['refname']]
628
629        # going to replace this footnote reference with the actual
630        # footnote later on, so store the footnote id to replace
631        # this reference with and the list and position to replace it
632        # in. Both list and position are stored in case a footnote
633        # reference is within a footnote, in which case ``self.body``
634        # won't really be ``self.body`` but a footnote collector
635        # list.
636        refs = self.footnote_map.get(refid, [])
637        refs.append((node['id'], self.body, len(self.body),))
638        self.footnote_map[refid] = refs
639
640        # add place holder list item which should later be 
641        # replaced with the contents of the footnote element
642        # and it's child elements
643        self.body.append('<!-- REPLACE WITH FOOTNOTE -->')
644
645        raise nodes.SkipNode
646
647    # TODO: header
648
649    def visit_hint(self, node):
650        self.body.append(self.starttag(node, 'note'))
651        self.body.append('\n<title>%s</title>\n' 
652            % (self.language.labels[node.tagname],))
653
654    def depart_hint(self, node):
655        self.body.append('</note>\n')
656
657    def visit_image(self, node):
658        atts = node.attributes.copy()
659        atts['fileref'] = atts['uri']
660        alt = None
661        del atts['uri']
662        if atts.has_key('alt'):
663            alt = atts['alt']
664            del atts['alt']
665        if atts.has_key('height'):
666            atts['depth'] = atts['height']
667            del atts['height']
668        # NOTE: using win32 port of xsltproc and docbook-stylesheets-1.51.1
669        # I'm getting the following error when transforming:
670        # Error C:\home\igor\src\gnome-xml\xpath.c:8023: Undefined 
671        # namespace prefix xmlXPathCompiledEval: evaluation failed
672        # When I switched to version 1.49 of the docbook-stylesheets
673        # I didn't have this problem.
674        self.body.append('<mediaobject>\n')
675        self.body.append('<imageobject>\n')
676        self.body.append(self.emptytag(node, 'imagedata', **atts))
677        self.body.append('</imageobject>\n')
678        if alt:
679            self.body.append('<textobject><phrase>' \
680                '%s</phrase></textobject>\n' % alt)
681        self.body.append('</mediaobject>\n')
682
683    def depart_image(self, node):
684        pass
685
686    def visit_important(self, node):
687        self.body.append(self.starttag(node, 'important'))
688
689    def depart_important(self, node):
690        self.body.append('</important>')
691
692    # @@@ Incomplete, pending a proper implementation on the
693    # Parser/Reader end.
694    def visit_interpreted(self, node):
695        self.body.append('<constant>\n')
696
697    def depart_interpreted(self, node):
698        self.body.append('</constant>\n')
699
700    def visit_label(self, node):
701        # getting label for "footnote" in ``visit_footnote``
702        # because label is an attribute for the ``footnote``
703        # element.
704        if isinstance(node.parent, nodes.footnote):
705            raise nodes.SkipNode
706        # TODO: handle citation label
707        elif isinstance(node.parent, nodes.citation):
708            raise nodes.SkipNode
709
710    def depart_label(self, node):
711        pass
712
713    def visit_legend(self, node):
714        # TODO: explain why this is empty....
715        pass
716
717    def depart_legend(self, node):
718        pass
719
720    def visit_list_item(self, node):
721        self.body.append(self.starttag(node, 'listitem'))
722
723    def depart_list_item(self, node):
724        self.body.append('</listitem>\n')
725
726    def visit_literal(self, node):
727         self.body.append('<literal>')
728
729    def depart_literal(self, node):
730        self.body.append('</literal>')
731
732    def visit_literal_block(self, node):
733        self.body.append(self.starttag(node, 'programlisting'))
734
735    def depart_literal_block(self, node):
736        self.body.append('</programlisting>\n')
737
738    def visit_note(self, node):
739        self.body.append(self.starttag(node, 'note'))
740        self.body.append('\n<title>%s</title>\n' 
741            % (self.language.labels[node.tagname],))
742
743    def depart_note(self, node):
744        self.body.append('</note>\n')
745
746    def visit_option(self, node):
747        self.body.append(self.starttag(node, 'command'))
748        if self.context[-1]:
749            self.body.append(', ')
750
751    def depart_option(self, node):
752        self.context[-1] += 1
753        self.body.append('</command>')
754
755    def visit_option_argument(self, node):
756        self.body.append(node.get('delimiter', ' '))
757        self.body.append(self.starttag(node, 'replaceable', ''))
758
759    def depart_option_argument(self, node):
760        self.body.append('</replaceable>')
761
762    def visit_option_group(self, node):
763        self.body.append(self.starttag(node, 'entry'))
764        self.context.append(0)
765
766    def depart_option_group(self, node):
767        self.context.pop()
768        self.body.append('</entry>\n')
769
770    def visit_option_list(self, node):
771        self.body.append(self.starttag(node, 'informaltable', frame='all'))
772        self.body.append('<tgroup cols="2">\n')
773        self.body.append('<colspec colname="option_col"/>\n')
774        self.body.append('<colspec colname="description_col"/>\n')
775        self.body.append('<thead>\n')
776        self.body.append('<row>\n')
777        # FIXME: shouldn't hardcode everything...
778        self.body.append('<entry align="center">Option</entry>\n')
779        self.body.append('<entry align="center">Description</entry>\n')
780        self.body.append('</row>\n')
781        self.body.append('</thead>\n')
782        self.body.append('<tbody>\n')
783
784    def depart_option_list(self, node):
785        self.body.append('</tbody>')
786        self.body.append('</tgroup>\n')
787        self.body.append('</informaltable>\n')
788
789    def visit_option_list_item(self, node):
790        self.body.append(self.starttag(node, 'row'))
791
792    def depart_option_list_item(self, node):
793        self.body.append('</row>\n')
794
795    def visit_option_string(self, node):
796        pass
797
798    def depart_option_string(self, node):
799        pass
800
801    # organization is handled in ``visit_docinfo()``
802    def visit_organization(self, node):
803        raise nodes.SkipNode
804
805    def visit_paragraph(self, node):
806        self.body.append(self.starttag(node, 'para', ''))
807
808    def depart_paragraph(self, node):
809        self.body.append('</para>\n')
810
811    # TODO: problematic
812    visit_problematic = depart_problematic = lambda self, node: None
813
814    def visit_raw(self, node):
815        if node.has_key('format') and node['format'] == 'docbook':
816            self.body.append(node.astext())
817        raise node.SkipNode
818
819    def visit_reference(self, node):
820        atts = {}
821        if node.has_key('refuri'):
822            atts['url'] = node['refuri']
823            self.context.append('ulink')
824        elif node.has_key('refid'):
825            atts['linkend'] = node['refid']
826            self.context.append('link')
827        elif node.has_key('refname'):
828            atts['linkend'] = self.document.nameids[node['refname']]
829            self.context.append('link')
830        self.body.append(self.starttag(node, self.context[-1], '', **atts))
831
832    def depart_reference(self, node):
833        self.body.append('</%s>' % (self.context.pop(),))
834
835    # revision is handled in ``visit_docinfo()``
836    def visit_revision(self, node):
837        raise nodes.SkipNode
838
839    def visit_row(self, node):
840        self.entry_level = 0
841        self.body.append(self.starttag(node, 'row'))
842
843    def depart_row(self, node):
844        self.body.append('</row>\n')
845
846    def visit_section(self, node):
847        if self.section == 0 and self.doctype == 'book':
848            self.body.append(self.starttag(node, 'chapter'))
849        else:
850            self.body.append(self.starttag(node, 'section'))
851        self.section += 1
852
853    def depart_section(self, node):
854        self.section -= 1
855        if self.section == 0 and self.doctype == 'book':
856            self.body.append('</chapter>\n')
857        else:
858            self.body.append('</section>\n')
859
860    def visit_sidebar(self, node):
861        self.body.append(self.starttag(node, 'sidebar'))
862
863    def depart_sidebar(self, node):
864        self.body.append('</sidebar>\n')
865
866    # author is handled in ``visit_docinfo()``
867    def visit_status(self, node):
868        raise nodes.SkipNode
869
870    def visit_strong(self, node):
871        self.body.append(self.starttag(node, 'emphasis', role='strong'))
872
873    def depart_strong(self, node):
874        self.body.append('</emphasis>\n')
875
876    def visit_substitution_definition(self, node):
877        raise nodes.SkipNode
878
879    def visit_substitution_reference(self, node):
880        self.unimplemented_visit(node)
881
882    def visit_subtitle(self, node):
883        self.body.append(self.starttag(node, 'subtitle'))
884
885    def depart_subtitle(self, node):
886        self.body.append('</subtitle>\n')
887
888    # TODO: system_message
889    visit_system_message = depart_system_message = lambda self, node: None
890
891    def visit_table(self, node):
892        self.body.append(
893            self.starttag(node, 'informaltable', frame='all')
894        )
895
896    def depart_table(self, node):
897        self.body.append('</informaltable>\n')
898
899    # TODO: target
900    visit_target = depart_target = lambda self,node: None
901
902    def visit_tbody(self, node):
903        self.body.append(self.starttag(node, 'tbody'))
904
905    def depart_tbody(self, node):
906        self.body.append('</tbody>\n')
907
908    def visit_term(self, node):
909        self.body.append(self.starttag(node, 'term'))
910        self.body.append('<varname>')
911
912    def depart_term(self, node):
913        # Leave the end tag "term" to ``visit_definition()``,
914        # in case there's a classifier.
915        self.body.append('</varname>')
916
917    def visit_tgroup(self, node):
918        self.colnames = []
919        atts = {'cols': node['cols']}
920        self.body.append(self.starttag(node, 'tgroup', **atts))
921
922    def depart_tgroup(self, node):
923        self.body.append('</tgroup>\n')
924
925    def visit_thead(self, node):
926        self.body.append(self.starttag(node, 'thead'))
927
928    def depart_thead(self, node):
929        self.body.append('</thead>\n')
930
931    def visit_tip(self, node):
932        self.body.append(self.starttag(node, 'tip'))
933
934    def depart_tip(self, node):
935        self.body.append('</tip>\n')
936
937    def visit_title(self, node):
938        self.body.append(self.starttag(node, 'title'))
939
940    def depart_title(self, node):
941        self.body.append('</title>\n')
942
943    def visit_topic(self, node):
944        # Table of Contents generation handled by DocBook
945        if node.get('class') == 'contents':
946            raise nodes.SkipChildren
947        elif node.get('class') == 'abstract':
948            self.body.append(self.starttag(node, 'abstract'))
949            self.context.append('abstract')
950        else:
951            print node
952            print `node`
953            print dir(node)
954            self.unimplemented_visit(node)
955
956    def depart_topic(self, node):
957        if len(self.context):
958            self.body.append('</%s>\n' % (self.context.pop(),))
959
960    # QUESTION: what to do for "transition"?
961    def visit_transition(self, node):
962        pass
963
964    def depart_transition(self, node):
965        pass
966
967    # author is handled in ``visit_docinfo()``
968    def visit_version(self, node):
969        raise nodes.SkipNode
970
971    def visit_warning(self, node):
972        self.body.append(self.starttag(node, 'warning'))
973
974    def depart_warning(self, node):
975        self.body.append('</warning>\n')
976
977    def unimplemented_visit(self, node):
978        raise NotImplementedError('visiting unimplemented node type: %s'
979                % node.__class__.__name__)
980