1# $Id: manpage.py 5554 2008-05-15 07:12:34Z grubert $
2# Author: Engelbert Gruber <grubert@users.sourceforge.net>
3# Copyright: This module is put into the public domain.
4
5"""
6Simple man page writer for reStructuredText.
7
8Man pages (short for "manual pages") contain system documentation on unix-like
9systems. The pages are grouped in numbered sections:
10
11 1 executable programs and shell commands
12 2 system calls
13 3 library functions
14 4 special files
15 5 file formats
16 6 games
17 7 miscellaneous
18 8 system administration
19
20Man pages are written *troff*, a text file formatting system.
21
22See http://www.tldp.org/HOWTO/Man-Page for a start.
23
24Man pages have no subsection only parts.
25Standard parts
26
27  NAME ,
28  SYNOPSIS ,
29  DESCRIPTION ,
30  OPTIONS ,
31  FILES ,
32  SEE ALSO ,
33  BUGS ,
34
35and
36
37  AUTHOR .
38
39A unix-like system keeps an index of the DESCRIPTIONs, which is accesable
40by the command whatis or apropos.
41
42"""
43
44# NOTE: the macros only work when at line start, so try the rule
45#       start new lines in visit_ functions.
46
47__docformat__ = 'reStructuredText'
48
49import sys
50import os
51import time
52import re
53from types import ListType
54
55import docutils
56from docutils import nodes, utils, writers, languages
57
58FIELD_LIST_INDENT = 7
59DEFINITION_LIST_INDENT = 7
60OPTION_LIST_INDENT = 7
61BLOCKQOUTE_INDENT = 3.5
62
63# Define two macros so man/roff can calculate the
64# indent/unindent margins by itself
65MACRO_DEF = (r"""
66.nr rst2man-indent-level 0
67.
68.de1 rstReportMargin
69\\$1 \\n[an-margin]
70level \\n[rst2man-indent-level]
71level magin: \\n[rst2man-indent\\n[rst2man-indent-level]]
72-
73\\n[rst2man-indent0]
74\\n[rst2man-indent1]
75\\n[rst2man-indent2]
76..
77.de1 INDENT
78.\" .rstReportMargin pre:
79. RS \\$1
80. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
81. nr rst2man-indent-level +1
82.\" .rstReportMargin post:
83..
84.de UNINDENT
85. RE
86.\" indent \\n[an-margin]
87.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
88.nr rst2man-indent-level -1
89.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
90.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
91..
92""")
93
94class Writer(writers.Writer):
95
96    supported = ('manpage')
97    """Formats this writer supports."""
98
99    output = None
100    """Final translated form of `document`."""
101
102    def __init__(self):
103        writers.Writer.__init__(self)
104        self.translator_class = Translator
105
106    def translate(self):
107        visitor = self.translator_class(self.document)
108        self.document.walkabout(visitor)
109        self.output = visitor.astext()
110
111
112class Table:
113    def __init__(self):
114        self._rows = []
115        self._options = ['center', ]
116        self._tab_char = '\t'
117        self._coldefs = []
118    def new_row(self):
119        self._rows.append([])
120    def append_cell(self, cell_lines):
121        """cell_lines is an array of lines"""
122        self._rows[-1].append(cell_lines)
123        if len(self._coldefs) < len(self._rows[-1]):
124            self._coldefs.append('l')
125    def astext(self):
126        text = '.TS\n'
127        text += ' '.join(self._options) + ';\n'
128        text += '|%s|.\n' % ('|'.join(self._coldefs))
129        for row in self._rows:
130            # row = array of cells. cell = array of lines.
131            # line above
132            text += '_\n'
133            max_lns_in_cell = 0
134            for cell in row:
135                max_lns_in_cell = max(len(cell), max_lns_in_cell)
136            for ln_cnt in range(max_lns_in_cell):
137                line = []
138                for cell in row:
139                    if len(cell) > ln_cnt:
140                        line.append(cell[ln_cnt])
141                    else:
142                        line.append(" ")
143                text += self._tab_char.join(line) + '\n'
144        text += '_\n'
145        text += '.TE\n'
146        return text
147
148class Translator(nodes.NodeVisitor):
149    """"""
150
151    words_and_spaces = re.compile(r'\S+| +|\n')
152    document_start = """Man page generated from reStructeredText."""
153
154    def __init__(self, document):
155        nodes.NodeVisitor.__init__(self, document)
156        self.settings = settings = document.settings
157        lcode = settings.language_code
158        self.language = languages.get_language(lcode)
159        self.head = []
160        self.body = []
161        self.foot = []
162        self.section_level = 0
163        self.context = []
164        self.topic_class = ''
165        self.colspecs = []
166        self.compact_p = 1
167        self.compact_simple = None
168        # the list style "*" bullet or "#" numbered
169        self._list_char = []
170        # writing the header .TH and .SH NAME is postboned after
171        # docinfo.
172        self._docinfo = {
173                "title" : "", "subtitle" : "",
174                "manual_section" : "", "manual_group" : "",
175                "author" : "",
176                "date" : "",
177                "copyright" : "",
178                "version" : "",
179                    }
180        self._in_docinfo = None
181        self._active_table = None
182        self._in_entry = None
183        self.header_written = 0
184        self.authors = []
185        self.section_level = 0
186        self._indent = [0]
187        # central definition of simple processing rules
188        # what to output on : visit, depart
189        self.defs = {
190                'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
191                'definition' : ('', ''),
192                'definition_list' : ('', '.TP 0\n'),
193                'definition_list_item' : ('\n.TP', ''),
194                #field_list
195                #field
196                'field_name' : ('\n.TP\n.B ', '\n'),
197                'field_body' : ('', '.RE\n', ),
198                'literal' : ('\\fB', '\\fP'),
199                'literal_block' : ('\n.nf\n', '\n.fi\n'),
200
201                #option_list
202                'option_list_item' : ('\n.TP', ''),
203                #option_group, option
204                'description' : ('\n', ''),
205
206                'reference' : (r'\fI\%', r'\fP'),
207                #'target'   : (r'\fI\%', r'\fP'),
208                'emphasis': ('\\fI', '\\fP'),
209                'strong' : ('\\fB', '\\fP'),
210                'term' : ('\n.B ', '\n'),
211                'title_reference' : ('\\fI', '\\fP'),
212                    }
213        # TODO dont specify the newline before a dot-command, but ensure
214        # check it is there.
215
216    def comment_begin(self, text):
217        """Return commented version of the passed text WITHOUT end of line/comment."""
218        prefix = '\n.\\" '
219        return prefix+prefix.join(text.split('\n'))
220
221    def comment(self, text):
222        """Return commented version of the passed text."""
223        return self.comment_begin(text)+'\n'
224
225    def astext(self):
226        """Return the final formatted document as a string."""
227        if not self.header_written:
228            # ensure we get a ".TH" as viewers require it.
229            self.head.append(self.header())
230        return ''.join(self.head + self.body + self.foot)
231
232    def visit_Text(self, node):
233        text = node.astext().replace('-','\-')
234        text = text.replace("'","\\'")
235        self.body.append(text)
236
237    def depart_Text(self, node):
238        pass
239
240    def list_start(self, node):
241        class enum_char:
242            enum_style = {
243                    'arabic'     : (3,1),
244                    'loweralpha' : (3,'a'),
245                    'upperalpha' : (3,'A'),
246                    'lowerroman' : (5,'i'),
247                    'upperroman' : (5,'I'),
248                    'bullet'     : (2,'\\(bu'),
249                    'emdash'     : (2,'\\(em'),
250                     }
251            def __init__(self, style):
252                if style == 'arabic':
253                    if node.has_key('start'):
254                        start = node['start']
255                    else:
256                        start = 1
257                    self._style = (
258                            len(str(len(node.children)))+2,
259                            start )
260                # BUG: fix start for alpha
261                else:
262                    self._style = self.enum_style[style]
263                self._cnt = -1
264            def next(self):
265                self._cnt += 1
266                # BUG add prefix postfix
267                try:
268                    return "%d." % (self._style[1] + self._cnt)
269                except:
270                    if self._style[1][0] == '\\':
271                        return self._style[1]
272                    # BUG romans dont work
273                    # BUG alpha only a...z
274                    return "%c." % (ord(self._style[1])+self._cnt)
275            def get_width(self):
276                return self._style[0]
277            def __repr__(self):
278                return 'enum_style%r' % list(self._style)
279
280        if node.has_key('enumtype'):
281            self._list_char.append(enum_char(node['enumtype']))
282        else:
283            self._list_char.append(enum_char('bullet'))
284        if len(self._list_char) > 1:
285            # indent nested lists
286            # BUG indentation depends on indentation of parent list.
287            self.indent(self._list_char[-2].get_width())
288        else:
289            self.indent(self._list_char[-1].get_width())
290
291    def list_end(self):
292        self.dedent()
293        self._list_char.pop()
294
295    def header(self):
296        tmpl = (".TH %(title)s %(manual_section)s"
297                " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
298                ".SH NAME\n"
299                "%(title)s \- %(subtitle)s\n")
300        return tmpl % self._docinfo
301
302    def append_header(self):
303        """append header with .TH and .SH NAME"""
304        # TODO before everything
305        # .TH title section date source manual
306        if self.header_written:
307            return
308        self.body.append(self.header())
309        self.body.append(MACRO_DEF)
310        self.header_written = 1
311
312    def visit_address(self, node):
313        raise NotImplementedError, node.astext()
314        self.visit_docinfo_item(node, 'address', meta=None)
315
316    def depart_address(self, node):
317        self.depart_docinfo_item()
318
319    def visit_admonition(self, node, name):
320        raise NotImplementedError, node.astext()
321        self.body.append(self.starttag(node, 'div', CLASS=name))
322        self.body.append('<p class="admonition-title">'
323                         + self.language.labels[name] + '</p>\n')
324
325    def depart_admonition(self):
326        raise NotImplementedError, node.astext()
327        self.body.append('</div>\n')
328
329    def visit_attention(self, node):
330        self.visit_admonition(node, 'attention')
331
332    def depart_attention(self, node):
333        self.depart_admonition()
334
335    def visit_author(self, node):
336        self._docinfo['author'] = node.astext()
337        raise nodes.SkipNode
338
339    def depart_author(self, node):
340        pass
341
342    def visit_authors(self, node):
343        self.body.append(self.comment('visit_authors'))
344
345    def depart_authors(self, node):
346        self.body.append(self.comment('depart_authors'))
347
348    def visit_block_quote(self, node):
349        #self.body.append(self.comment('visit_block_quote'))
350        # BUG/HACK: indent alway uses the _last_ indention,
351        # thus we need two of them.
352        self.indent(BLOCKQOUTE_INDENT)
353        self.indent(0)
354
355    def depart_block_quote(self, node):
356        #self.body.append(self.comment('depart_block_quote'))
357        self.dedent()
358        self.dedent()
359
360    def visit_bullet_list(self, node):
361        self.list_start(node)
362
363    def depart_bullet_list(self, node):
364        self.list_end()
365
366    def visit_caption(self, node):
367        raise NotImplementedError, node.astext()
368        self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
369
370    def depart_caption(self, node):
371        raise NotImplementedError, node.astext()
372        self.body.append('</p>\n')
373
374    def visit_caution(self, node):
375        self.visit_admonition(node, 'caution')
376
377    def depart_caution(self, node):
378        self.depart_admonition()
379
380    def visit_citation(self, node):
381        raise NotImplementedError, node.astext()
382        self.body.append(self.starttag(node, 'table', CLASS='citation',
383                                       frame="void", rules="none"))
384        self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
385                         '<col />\n'
386                         '<tbody valign="top">\n'
387                         '<tr>')
388        self.footnote_backrefs(node)
389
390    def depart_citation(self, node):
391        raise NotImplementedError, node.astext()
392        self.body.append('</td></tr>\n'
393                         '</tbody>\n</table>\n')
394
395    def visit_citation_reference(self, node):
396        raise NotImplementedError, node.astext()
397        href = ''
398        if node.has_key('refid'):
399            href = '#' + node['refid']
400        elif node.has_key('refname'):
401            href = '#' + self.document.nameids[node['refname']]
402        self.body.append(self.starttag(node, 'a', '[', href=href,
403                                       CLASS='citation-reference'))
404
405    def depart_citation_reference(self, node):
406        raise NotImplementedError, node.astext()
407        self.body.append(']</a>')
408
409    def visit_classifier(self, node):
410        raise NotImplementedError, node.astext()
411        self.body.append(' <span class="classifier-delimiter">:</span> ')
412        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
413
414    def depart_classifier(self, node):
415        raise NotImplementedError, node.astext()
416        self.body.append('</span>')
417
418    def visit_colspec(self, node):
419        self.colspecs.append(node)
420
421    def depart_colspec(self, node):
422        pass
423
424    def write_colspecs(self):
425        self.body.append("%s.\n" % ('L '*len(self.colspecs)))
426
427    def visit_comment(self, node,
428                      sub=re.compile('-(?=-)').sub):
429        self.body.append(self.comment(node.astext()))
430        raise nodes.SkipNode
431
432    def visit_contact(self, node):
433        self.visit_docinfo_item(node, 'contact')
434
435    def depart_contact(self, node):
436        self.depart_docinfo_item()
437
438    def visit_copyright(self, node):
439        self._docinfo['copyright'] = node.astext()
440        raise nodes.SkipNode
441
442    def visit_danger(self, node):
443        self.visit_admonition(node, 'danger')
444
445    def depart_danger(self, node):
446        self.depart_admonition()
447
448    def visit_date(self, node):
449        self._docinfo['date'] = node.astext()
450        raise nodes.SkipNode
451
452    def visit_decoration(self, node):
453        pass
454
455    def depart_decoration(self, node):
456        pass
457
458    def visit_definition(self, node):
459        self.body.append(self.defs['definition'][0])
460
461    def depart_definition(self, node):
462        self.body.append(self.defs['definition'][1])
463
464    def visit_definition_list(self, node):
465        self.indent(DEFINITION_LIST_INDENT)
466
467    def depart_definition_list(self, node):
468        self.dedent()
469
470    def visit_definition_list_item(self, node):
471        self.body.append(self.defs['definition_list_item'][0])
472
473    def depart_definition_list_item(self, node):
474        self.body.append(self.defs['definition_list_item'][1])
475
476    def visit_description(self, node):
477        self.body.append(self.defs['description'][0])
478
479    def depart_description(self, node):
480        self.body.append(self.defs['description'][1])
481
482    def visit_docinfo(self, node):
483        self._in_docinfo = 1
484
485    def depart_docinfo(self, node):
486        self._in_docinfo = None
487        # TODO nothing should be written before this
488        self.append_header()
489
490    def visit_docinfo_item(self, node, name):
491        self.body.append(self.comment('%s: ' % self.language.labels[name]))
492        if len(node):
493            return
494            if isinstance(node[0], nodes.Element):
495                node[0].set_class('first')
496            if isinstance(node[0], nodes.Element):
497                node[-1].set_class('last')
498
499    def depart_docinfo_item(self):
500        pass
501
502    def visit_doctest_block(self, node):
503        raise NotImplementedError, node.astext()
504        self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
505
506    def depart_doctest_block(self, node):
507        raise NotImplementedError, node.astext()
508        self.body.append('\n</pre>\n')
509
510    def visit_document(self, node):
511        self.body.append(self.comment(self.document_start).lstrip())
512        # writing header is postboned
513        self.header_written = 0
514
515    def depart_document(self, node):
516        if self._docinfo['author']:
517            self.body.append('\n.SH AUTHOR\n%s\n'
518                    % self._docinfo['author'])
519        if self._docinfo['copyright']:
520            self.body.append('\n.SH COPYRIGHT\n%s\n'
521                    % self._docinfo['copyright'])
522        self.body.append(
523                self.comment(
524                        'Generated by docutils manpage writer on %s.\n'
525                        % (time.strftime('%Y-%m-%d %H:%M')) ) )
526
527    def visit_emphasis(self, node):
528        self.body.append(self.defs['emphasis'][0])
529
530    def depart_emphasis(self, node):
531        self.body.append(self.defs['emphasis'][1])
532
533    def visit_entry(self, node):
534        # BUG entries have to be on one line separated by tab force it.
535        self.context.append(len(self.body))
536        self._in_entry = 1
537
538    def depart_entry(self, node):
539        start = self.context.pop()
540        self._active_table.append_cell(self.body[start:])
541        del self.body[start:]
542        self._in_entry = 0
543
544    def visit_enumerated_list(self, node):
545        self.list_start(node)
546
547    def depart_enumerated_list(self, node):
548        self.list_end()
549
550    def visit_error(self, node):
551        self.visit_admonition(node, 'error')
552
553    def depart_error(self, node):
554        self.depart_admonition()
555
556    def visit_field(self, node):
557        #self.body.append(self.comment('visit_field'))
558        pass
559
560    def depart_field(self, node):
561        #self.body.append(self.comment('depart_field'))
562        pass
563
564    def visit_field_body(self, node):
565        #self.body.append(self.comment('visit_field_body'))
566        if self._in_docinfo:
567            self._docinfo[
568                    self._field_name.lower().replace(" ","_")] = node.astext()
569            raise nodes.SkipNode
570
571    def depart_field_body(self, node):
572        pass
573
574    def visit_field_list(self, node):
575        self.indent(FIELD_LIST_INDENT)
576
577    def depart_field_list(self, node):
578        self.dedent('depart_field_list')
579
580
581    def visit_field_name(self, node):
582        if self._in_docinfo:
583            self._field_name = node.astext()
584            raise nodes.SkipNode
585        else:
586            self.body.append(self.defs['field_name'][0])
587
588    def depart_field_name(self, node):
589        self.body.append(self.defs['field_name'][1])
590
591    def visit_figure(self, node):
592        raise NotImplementedError, node.astext()
593
594    def depart_figure(self, node):
595        raise NotImplementedError, node.astext()
596
597    def visit_footer(self, node):
598        raise NotImplementedError, node.astext()
599
600    def depart_footer(self, node):
601        raise NotImplementedError, node.astext()
602        start = self.context.pop()
603        footer = (['<hr class="footer"/>\n',
604                   self.starttag(node, 'div', CLASS='footer')]
605                  + self.body[start:] + ['</div>\n'])
606        self.body_suffix[:0] = footer
607        del self.body[start:]
608
609    def visit_footnote(self, node):
610        raise NotImplementedError, node.astext()
611        self.body.append(self.starttag(node, 'table', CLASS='footnote',
612                                       frame="void", rules="none"))
613        self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
614                         '<tbody valign="top">\n'
615                         '<tr>')
616        self.footnote_backrefs(node)
617
618    def footnote_backrefs(self, node):
619        raise NotImplementedError, node.astext()
620        if self.settings.footnote_backlinks and node.hasattr('backrefs'):
621            backrefs = node['backrefs']
622            if len(backrefs) == 1:
623                self.context.append('')
624                self.context.append('<a class="fn-backref" href="#%s" '
625                                    'name="%s">' % (backrefs[0], node['id']))
626            else:
627                i = 1
628                backlinks = []
629                for backref in backrefs:
630                    backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
631                                     % (backref, i))
632                    i += 1
633                self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
634                self.context.append('<a name="%s">' % node['id'])
635        else:
636            self.context.append('')
637            self.context.append('<a name="%s">' % node['id'])
638
639    def depart_footnote(self, node):
640        raise NotImplementedError, node.astext()
641        self.body.append('</td></tr>\n'
642                         '</tbody>\n</table>\n')
643
644    def visit_footnote_reference(self, node):
645        raise NotImplementedError, node.astext()
646        href = ''
647        if node.has_key('refid'):
648            href = '#' + node['refid']
649        elif node.has_key('refname'):
650            href = '#' + self.document.nameids[node['refname']]
651        format = self.settings.footnote_references
652        if format == 'brackets':
653            suffix = '['
654            self.context.append(']')
655        elif format == 'superscript':
656            suffix = '<sup>'
657            self.context.append('</sup>')
658        else:                           # shouldn't happen
659            suffix = '???'
660            self.content.append('???')
661        self.body.append(self.starttag(node, 'a', suffix, href=href,
662                                       CLASS='footnote-reference'))
663
664    def depart_footnote_reference(self, node):
665        raise NotImplementedError, node.astext()
666        self.body.append(self.context.pop() + '</a>')
667
668    def visit_generated(self, node):
669        pass
670
671    def depart_generated(self, node):
672        pass
673
674    def visit_header(self, node):
675        raise NotImplementedError, node.astext()
676        self.context.append(len(self.body))
677
678    def depart_header(self, node):
679        raise NotImplementedError, node.astext()
680        start = self.context.pop()
681        self.body_prefix.append(self.starttag(node, 'div', CLASS='header'))
682        self.body_prefix.extend(self.body[start:])
683        self.body_prefix.append('<hr />\n</div>\n')
684        del self.body[start:]
685
686    def visit_hint(self, node):
687        self.visit_admonition(node, 'hint')
688
689    def depart_hint(self, node):
690        self.depart_admonition()
691
692    def visit_image(self, node):
693        raise NotImplementedError, node.astext()
694        atts = node.attributes.copy()
695        atts['src'] = atts['uri']
696        del atts['uri']
697        if not atts.has_key('alt'):
698            atts['alt'] = atts['src']
699        if isinstance(node.parent, nodes.TextElement):
700            self.context.append('')
701        else:
702            self.body.append('<p>')
703            self.context.append('</p>\n')
704        self.body.append(self.emptytag(node, 'img', '', **atts))
705
706    def depart_image(self, node):
707        raise NotImplementedError, node.astext()
708        self.body.append(self.context.pop())
709
710    def visit_important(self, node):
711        self.visit_admonition(node, 'important')
712
713    def depart_important(self, node):
714        self.depart_admonition()
715
716    def visit_label(self, node):
717        raise NotImplementedError, node.astext()
718        self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
719                                       CLASS='label'))
720
721    def depart_label(self, node):
722        raise NotImplementedError, node.astext()
723        self.body.append(']</a></td><td>%s' % self.context.pop())
724
725    def visit_legend(self, node):
726        raise NotImplementedError, node.astext()
727        self.body.append(self.starttag(node, 'div', CLASS='legend'))
728
729    def depart_legend(self, node):
730        raise NotImplementedError, node.astext()
731        self.body.append('</div>\n')
732
733    def visit_line_block(self, node):
734        self.body.append('\n')
735
736    def depart_line_block(self, node):
737        self.body.append('\n')
738
739    def visit_line(self, node):
740        pass
741
742    def depart_line(self, node):
743        self.body.append('\n.br\n')
744
745    def visit_list_item(self, node):
746        # man 7 man argues to use ".IP" instead of ".TP"
747        self.body.append('\n.IP %s %d\n' % (
748                self._list_char[-1].next(),
749                self._list_char[-1].get_width(),) )
750
751    def depart_list_item(self, node):
752        pass
753
754    def visit_literal(self, node):
755        self.body.append(self.defs['literal'][0])
756
757    def depart_literal(self, node):
758        self.body.append(self.defs['literal'][1])
759
760    def visit_literal_block(self, node):
761        self.body.append(self.defs['literal_block'][0])
762
763    def depart_literal_block(self, node):
764        self.body.append(self.defs['literal_block'][1])
765
766    def visit_meta(self, node):
767        raise NotImplementedError, node.astext()
768        self.head.append(self.emptytag(node, 'meta', **node.attributes))
769
770    def depart_meta(self, node):
771        pass
772
773    def visit_note(self, node):
774        self.visit_admonition(node, 'note')
775
776    def depart_note(self, node):
777        self.depart_admonition()
778
779    def indent(self, by=0.5):
780        # if we are in a section ".SH" there already is a .RS
781        #self.body.append('\n[[debug: listchar: %r]]\n' % map(repr, self._list_char))
782        #self.body.append('\n[[debug: indent %r]]\n' % self._indent)
783        step = self._indent[-1]
784        self._indent.append(by)
785        self.body.append(self.defs['indent'][0] % step)
786
787    def dedent(self, name=''):
788        #self.body.append('\n[[debug: dedent %s %r]]\n' % (name, self._indent))
789        self._indent.pop()
790        self.body.append(self.defs['indent'][1])
791
792    def visit_option_list(self, node):
793        self.indent(OPTION_LIST_INDENT)
794
795    def depart_option_list(self, node):
796        self.dedent()
797
798    def visit_option_list_item(self, node):
799        # one item of the list
800        self.body.append(self.defs['option_list_item'][0])
801
802    def depart_option_list_item(self, node):
803        self.body.append(self.defs['option_list_item'][1])
804
805    def visit_option_group(self, node):
806        # as one option could have several forms it is a group
807        # options without parameter bold only, .B, -v
808        # options with parameter bold italic, .BI, -f file
809
810        # we do not know if .B or .BI
811        self.context.append('.B')           # blind guess
812        self.context.append(len(self.body)) # to be able to insert later
813        self.context.append(0)              # option counter
814
815    def depart_option_group(self, node):
816        self.context.pop()  # the counter
817        start_position = self.context.pop()
818        text = self.body[start_position:]
819        del self.body[start_position:]
820        self.body.append('\n%s%s' % (self.context.pop(), ''.join(text)))
821
822    def visit_option(self, node):
823        # each form of the option will be presented separately
824        if self.context[-1]>0:
825            self.body.append(' ,')
826        if self.context[-3] == '.BI':
827            self.body.append('\\')
828        self.body.append(' ')
829
830    def depart_option(self, node):
831        self.context[-1] += 1
832
833    def visit_option_string(self, node):
834        # do not know if .B or .BI
835        pass
836
837    def depart_option_string(self, node):
838        pass
839
840    def visit_option_argument(self, node):
841        self.context[-3] = '.BI' # bold/italic alternate
842        if node['delimiter'] != ' ':
843            self.body.append('\\fn%s ' % node['delimiter'] )
844        elif self.body[len(self.body)-1].endswith('='):
845            # a blank only means no blank in output, just changing font
846            self.body.append(' ')
847        else:
848            # backslash blank blank
849            self.body.append('\\  ')
850
851    def depart_option_argument(self, node):
852        pass
853
854    def visit_organization(self, node):
855        raise NotImplementedError, node.astext()
856        self.visit_docinfo_item(node, 'organization')
857
858    def depart_organization(self, node):
859        raise NotImplementedError, node.astext()
860        self.depart_docinfo_item()
861
862    def visit_paragraph(self, node):
863        # BUG every but the first paragraph in a list must be intended
864        # TODO .PP or new line
865        return
866
867    def depart_paragraph(self, node):
868        # TODO .PP or an empty line
869        if not self._in_entry:
870            self.body.append('\n\n')
871
872    def visit_problematic(self, node):
873        raise NotImplementedError, node.astext()
874        if node.hasattr('refid'):
875            self.body.append('<a href="#%s" name="%s">' % (node['refid'],
876                                                           node['id']))
877            self.context.append('</a>')
878        else:
879            self.context.append('')
880        self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
881
882    def depart_problematic(self, node):
883        raise NotImplementedError, node.astext()
884        self.body.append('</span>')
885        self.body.append(self.context.pop())
886
887    def visit_raw(self, node):
888        if node.get('format') == 'manpage':
889            self.body.append(node.astext())
890        # Keep non-manpage raw text out of output:
891        raise nodes.SkipNode
892
893    def visit_reference(self, node):
894        """E.g. link or email address."""
895        self.body.append(self.defs['reference'][0])
896
897    def depart_reference(self, node):
898        self.body.append(self.defs['reference'][1])
899
900    def visit_revision(self, node):
901        self.visit_docinfo_item(node, 'revision')
902
903    def depart_revision(self, node):
904        self.depart_docinfo_item()
905
906    def visit_row(self, node):
907        self._active_table.new_row()
908
909    def depart_row(self, node):
910        pass
911
912    def visit_section(self, node):
913        self.section_level += 1
914
915    def depart_section(self, node):
916        self.section_level -= 1
917
918    def visit_status(self, node):
919        raise NotImplementedError, node.astext()
920        self.visit_docinfo_item(node, 'status', meta=None)
921
922    def depart_status(self, node):
923        self.depart_docinfo_item()
924
925    def visit_strong(self, node):
926        self.body.append(self.defs['strong'][1])
927
928    def depart_strong(self, node):
929        self.body.append(self.defs['strong'][1])
930
931    def visit_substitution_definition(self, node):
932        """Internal only."""
933        raise nodes.SkipNode
934
935    def visit_substitution_reference(self, node):
936        self.unimplemented_visit(node)
937
938    def visit_subtitle(self, node):
939        self._docinfo["subtitle"] = node.astext()
940        raise nodes.SkipNode
941
942    def visit_system_message(self, node):
943        # TODO add report_level
944        #if node['level'] < self.document.reporter['writer'].report_level:
945            # Level is too low to display:
946        #    raise nodes.SkipNode
947        self.body.append('\.SH system-message\n')
948        attr = {}
949        backref_text = ''
950        if node.hasattr('id'):
951            attr['name'] = node['id']
952        if node.hasattr('line'):
953            line = ', line %s' % node['line']
954        else:
955            line = ''
956        self.body.append('System Message: %s/%s (%s:%s)\n'
957                         % (node['type'], node['level'], node['source'], line))
958
959    def depart_system_message(self, node):
960        self.body.append('\n')
961
962    def visit_table(self, node):
963        self._active_table = Table()
964
965    def depart_table(self, node):
966        self.body.append(self._active_table.astext())
967        self._active_table = None
968
969    def visit_target(self, node):
970        self.body.append(self.comment('visit_target'))
971        #self.body.append(self.defs['target'][0])
972        #self.body.append(node['refuri'])
973
974    def depart_target(self, node):
975        self.body.append(self.comment('depart_target'))
976        #self.body.append(self.defs['target'][1])
977
978    def visit_tbody(self, node):
979        pass
980
981    def depart_tbody(self, node):
982        pass
983
984    def visit_term(self, node):
985        self.body.append(self.defs['term'][0])
986
987    def depart_term(self, node):
988        self.body.append(self.defs['term'][1])
989
990    def visit_tgroup(self, node):
991        pass
992
993    def depart_tgroup(self, node):
994        pass
995
996    def visit_thead(self, node):
997        raise NotImplementedError, node.astext()
998        self.write_colspecs()
999        self.body.append(self.context.pop()) # '</colgroup>\n'
1000        # There may or may not be a <thead>; this is for <tbody> to use:
1001        self.context.append('')
1002        self.body.append(self.starttag(node, 'thead', valign='bottom'))
1003
1004    def depart_thead(self, node):
1005        raise NotImplementedError, node.astext()
1006        self.body.append('</thead>\n')
1007
1008    def visit_tip(self, node):
1009        self.visit_admonition(node, 'tip')
1010
1011    def depart_tip(self, node):
1012        self.depart_admonition()
1013
1014    def visit_title(self, node):
1015        if isinstance(node.parent, nodes.topic):
1016            self.body.append(self.comment('topic-title'))
1017        elif isinstance(node.parent, nodes.sidebar):
1018            self.body.append(self.comment('sidebar-title'))
1019        elif isinstance(node.parent, nodes.admonition):
1020            self.body.append(self.comment('admonition-title'))
1021        elif self.section_level == 0:
1022            # document title for .TH
1023            self._docinfo['title'] = node.astext()
1024            raise nodes.SkipNode
1025        elif self.section_level == 1:
1026            self.body.append('\n.SH ')
1027        else:
1028            self.body.append('\n.SS ')
1029
1030    def depart_title(self, node):
1031        self.body.append('\n')
1032
1033    def visit_title_reference(self, node):
1034        """inline citation reference"""
1035        self.body.append(self.defs['title_reference'][0])
1036
1037    def depart_title_reference(self, node):
1038        self.body.append(self.defs['title_reference'][1])
1039
1040    def visit_topic(self, node):
1041        self.body.append(self.comment('topic: '+node.astext()))
1042        raise nodes.SkipNode
1043        ##self.topic_class = node.get('class')
1044
1045    def depart_topic(self, node):
1046        ##self.topic_class = ''
1047        pass
1048
1049    def visit_transition(self, node):
1050        # .PP      Begin a new paragraph and reset prevailing indent.
1051        # .sp N    leaves N lines of blank space.
1052        # .ce      centers the next line
1053        self.body.append('\n.sp\n.ce\n----\n')
1054
1055    def depart_transition(self, node):
1056        self.body.append('\n.ce 0\n.sp\n')
1057
1058    def visit_version(self, node):
1059        self._docinfo["version"] = node.astext()
1060        raise nodes.SkipNode
1061
1062    def visit_warning(self, node):
1063        self.visit_admonition(node, 'warning')
1064
1065    def depart_warning(self, node):
1066        self.depart_admonition()
1067
1068    def unimplemented_visit(self, node):
1069        raise NotImplementedError('visiting unimplemented node type: %s'
1070                                  % node.__class__.__name__)
1071
1072# vim: set et ts=4 ai :
1073