root/releases/pkgcore/0.2.14/man/manpage.py @ ferringb%2540gmail.com-20070227015709-h69mwumonxz46mvq

Revision ferringb%2540gmail.com-20070227015709-h69mwumonxz46mvq, 17.3 KB (checked in by Brian Harring <ferringb@…>, 23 months ago)

mild hack; add newlines if needed between item types.

Line 
1# Copyright: 2007 Brian Harring <ferringb@gmail.com>
2# Copyright: ?-2006 Engelbert Gruber <grubert@users.sourceforge.net>
3# Original Author: Englebert Gruber
4# License: Public Domain
5
6"""
7Man page formatting for reStructuredText.
8
9See http://www.tldp.org/HOWTO/Man-Page for a start.
10
11Man pages have no subsection only parts.
12Standard parts
13  NAME ,
14  SYNOPSIS ,
15  DESCRIPTION ,
16  OPTIONS ,
17  FILES ,
18  SEE ALSO ,
19  BUGS ,
20and
21  AUTHOR .
22
23"""
24
25# NOTE: the macros only work when at line start, so try the rule
26#       start new lines in visit_ functions.
27
28__docformat__ = 'reStructuredText'
29
30import sys
31import os
32import time
33import re
34from types import ListType
35
36import docutils
37from docutils import nodes, utils, writers, languages
38
39
40class Writer(writers.Writer):
41
42    supported = ('manpage')
43    """Formats this writer supports."""
44
45    output = None
46    """Final translated form of `document`."""
47
48    def __init__(self, debug_file):
49        writers.Writer.__init__(self)
50        self.translator_class = Translator
51        self.debug_file = debug_file
52
53    def translate(self):
54        visitor = self.translator_class(self.document)
55        if self.debug_file:
56            open(self.debug_file, 'w').write(str(self.document))
57        self.document.walkabout(visitor)
58        self.output = visitor.astext()
59
60
61class Table:
62    def __init__(self):
63        self._rows = []
64        self._options = ['center', ]
65        self._tab_char = '\t'
66        self._coldefs = []
67    def new_row(self):
68        self._rows.append([])
69    def append_cell(self, text):
70        self._rows[-1].append(text)
71        if len(self._coldefs) < len(self._rows[-1]):
72            self._coldefs.append('l')
73    def astext(self):
74        text = '.TS\n'
75        text += ' '.join(self._options) + ';\n'
76        text += '|%s|.\n' % ('|'.join(self._coldefs))
77        for row in self._rows:
78            # row = array of cells. cell = array of lines.
79            # line above
80            text += '_\n'
81            nr_of_cells = len(row)
82            ln_cnt = -1
83            while ln_cnt < 10: # safety belt
84                ln_cnt += 1
85                line = []
86                last_line = 1
87                for cell in row:
88                    if len(cell) > ln_cnt:
89                        line.append(cell[ln_cnt])
90                        last_line = 0
91                    else:
92                        line.append(" ")
93                if last_line:
94                    break
95                text += self._tab_char.join(line) + '\n'
96        text += '_\n'
97        text += '.TE\n'
98        return text
99
100class Translator(nodes.NodeVisitor):
101    """"""
102
103    words_and_spaces = re.compile(r'\S+| +|\n')
104    document_start = """Man page generated from reStructeredText."""
105
106
107    def unimplemented_visit(self, node):
108        raise NotImplementedError('visiting unimplemented node type: %s'
109                                  % node.__class__.__name__)
110
111    def noop(self, node):
112        pass
113
114    simple_defs = {
115        'definition' : ('', ''),
116        'definition_list' : ('', ''),
117        'definition_list_item' : ('\n.TP', ''),
118        'description' : ('\n', ''),
119        'field_name' : ('\n.TP\n.B ', '\n'),
120        'literal_block' : ('\n.nf\n', '\n.fi\n'),
121        'option_list' : ('', ''),
122        'option_list_item' : ('\n.TP', ''),
123        'reference' : ('', ''),
124        'strong' : ('\n.B ', ''),
125        'term' : ('\n.B ', '\n'),
126    }
127
128    def f(mode, k, val):
129        def f2(self, node):
130            if val:
131                if self.body and self.body[-1].endswith('\n'):
132                    self.body.append(val.lstrip('\n'))
133                else:
134                    self.body.append(val)
135        return f2
136
137    for k,v in simple_defs.iteritems():
138        locals()['visit_%s' % k] = f('visit', k, v[0])
139        locals()['depart_%s' % k] = f('depart', k, v[1])
140
141    def __init__(self, document):
142        nodes.NodeVisitor.__init__(self, document)
143        self.settings = settings = document.settings
144        lcode = settings.language_code
145        self.language = languages.get_language(lcode)
146        self.head = []
147        self.body = []
148        self.foot = []
149        self.section_level = 0
150        self.context = []
151        self.topic_class = ''
152        self.colspecs = []
153        self.compact_p = 1
154        self.compact_simple = None
155        # the list style "*" bullet or "#" numbered
156        self._list_char = []
157        # writing the header .TH and .SH NAME is postboned after
158        # docinfo.
159        self._docinfo = {
160                "title" : "", "subtitle" : "",
161                "manual_section" : "", "manual_group" : "",
162                "author" : "",
163                "date" : "",
164                "copyright" : "",
165                "version" : "",
166                    }
167        self._in_docinfo = None
168        self._active_table = None
169        self._in_entry = None
170        self.header_written = 0
171        self.authors = []
172        self.section_level = 0
173        # central definition of simple processing rules
174        # what to output on : visit, depart
175        # TODO dont specify the newline before a dot-command, but ensure
176        # check it is there.
177
178    def comment_begin(self, text):
179        """Return commented version of the passed text WITHOUT end of line/comment."""
180        prefix = '\n.\\" '
181        return prefix+prefix.join(text.split('\n'))
182
183    def comment(self, text):
184        """Return commented version of the passed text."""
185        return self.comment_begin(text)+'\n'
186
187    def astext(self):
188        """Return the final formatted document as a string."""
189        return ''.join(self.head + self.body + self.foot)
190
191    def visit_Text(self, node):
192        text = node.astext().replace('-','\-')
193        text = text.replace("'","\\'")
194        self.body.append(text)
195
196    def list_start(self, node):
197        class enum_char:
198            enum_style = {
199                    'arabic'     : (3,1),
200                    'loweralpha' : (2,'a'),
201                    'upperalpha' : (2,'A'),
202                    'lowerroman' : (5,'i'),
203                    'upperroman' : (5,'I'),
204                    'bullet'     : (2,'\\(bu'),
205                    'emdash'     : (2,'\\(em'),
206                     }
207            def __init__(self, style):
208                self._style = self.enum_style[style]
209                self._cnt = -1
210            def next(self):
211                self._cnt += 1
212                try:
213                    return "%d." % (self._style[1] + self._cnt)
214                except:
215                    if self._style[1][0] == '\\':
216                        return self._style[1]
217                    # BUG romans dont work
218                    # BUG alpha only a...z
219                    return "%c." % (ord(self._style[1])+self._cnt)
220            def get_width(self):
221                return self._style[0]
222
223        if node.has_key('enumtype'):
224            self._list_char.append(enum_char(node['enumtype']))
225        else:
226            self._list_char.append(enum_char('bullet'))
227        if len(self._list_char) > 1:
228            # indent nested lists
229            # BUG indentation depends on indentation of parent list.
230            self.body.append('\n.RS %d' % self._list_char[-2].get_width())
231
232    def list_end(self):
233        self._list_char.pop()
234        if len(self._list_char) > 0:
235            self.body.append('\n.RE\n')
236
237    def append_header(self):
238        """append header with .TH and .SH NAME"""
239        # TODO before everything
240        # .TH title section date source manual
241        if self.header_written:
242            return
243        tmpl = (".TH %(title)s %(manual_section)s"
244                " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
245                ".SH NAME\n"
246                "%(title)s \- %(subtitle)s\n")
247        self.body.append(tmpl % self._docinfo)
248        self.header_written = 1
249
250    def visit_author(self, node):
251        self._docinfo['author'] = node.astext()
252        raise nodes.SkipNode
253
254    def visit_authors(self, node):
255        self.body.append(self.comment('visit_authors'))
256
257    def depart_authors(self, node):
258        self.body.append(self.comment('depart_authors'))
259
260    def visit_block_quote(self, node):
261        self.body.append(self.comment('visit_block_quote'))
262
263    def depart_block_quote(self, node):
264        self.body.append(self.comment('depart_block_quote'))
265
266    def visit_bullet_list(self, node):
267        self.list_start(node)
268
269    def depart_bullet_list(self, node):
270        self.list_end()
271
272    def visit_colspec(self, node):
273        self.colspecs.append(node)
274
275    def write_colspecs(self):
276        self.body.append("%s.\n" % ('L '*len(self.colspecs)))
277
278    def visit_comment(self, node,
279                      sub=re.compile('-(?=-)').sub):
280        self.body.append(self.comment(node.astext()))
281        raise nodes.SkipNode
282
283    def visit_contact(self, node):
284        self.visit_docinfo_item(node, 'contact')
285
286    def depart_contact(self, node):
287        self.depart_docinfo_item()
288
289    def visit_copyright(self, node):
290        self._docinfo['copyright'] = node.astext()
291        raise nodes.SkipNode
292
293    def visit_date(self, node):
294        self._docinfo['date'] = node.astext()
295        raise nodes.SkipNode
296
297    visit_decoration = depart_decoration = noop
298
299    def visit_docinfo(self, node):
300        self._in_docinfo = 1
301
302    def depart_docinfo(self, node):
303        self._in_docinfo = None
304        # TODO nothing should be written before this
305        self.append_header()
306
307    def visit_docinfo_item(self, node, name):
308        self.body.append(self.comment('%s: ' % self.language.labels[name]))
309        if len(node):
310            return
311            if isinstance(node[0], nodes.Element):
312                node[0].set_class('first')
313            if isinstance(node[0], nodes.Element):
314                node[-1].set_class('last')
315
316    def visit_document(self, node):
317        self.body.append(self.comment(self.document_start))
318        # writing header is postboned
319        self.header_written = 0
320
321    def depart_document(self, node):
322        if self._docinfo['author']:
323            self.body.append('\n.SH AUTHOR\n%s\n'
324                    % self._docinfo['author'])
325        if self._docinfo['copyright']:
326            self.body.append('\n.SH COPYRIGHT\n%s\n'
327                    % self._docinfo['copyright'])
328        self.body.append(
329                self.comment(
330                        'Generated by docutils manpage writer on %s.\n'
331                        % (time.strftime('%Y-%m-%d %H:%M')) ) )
332
333    def visit_emphasis(self, node):
334        self.body.append('\n.I ')
335
336    def depart_emphasis(self, node):
337        self.body.append('\n')
338
339    def visit_enumerated_list(self, node):
340        self.list_start(node)
341
342    def depart_enumerated_list(self, node):
343        self.list_end()
344
345    visit_field = depart_field = noop
346
347    def visit_field_body(self, node):
348        if self._in_docinfo:
349            self._docinfo[
350                    self._field_name.lower().replace(" ","_")] = node.astext()
351            raise nodes.SkipNode
352
353    def depart_field_body(self, node):
354        self.body.append(self.comment('depart_field_body'))
355
356    def visit_field_list(self, node):
357        self.body.append(self.comment('visit_field_list'))
358
359    def depart_field_list(self, node):
360        self.body.append(self.comment('depart_field_list'))
361
362    def visit_field_name(self, node):
363        if self._in_docinfo:
364            self._field_name = node.astext()
365            raise nodes.SkipNode
366        else:
367            self.body.append(self.simple_defs['field_name'][0])
368
369    def depart_field_name(self, node):
370        self.body.append(self.simple_defs['field_name'][1])
371
372    visit_generated = depart_generated = noop
373
374    def visit_line_block(self, node):
375        self.body.append('\n')
376
377    def depart_line_block(self, node):
378        self.body.append('\n')
379
380    def visit_list_item(self, node):
381        self.body.append('\n.TP %d\n%s' % (
382                self._list_char[-1].get_width(),
383                self._list_char[-1].next(),) )
384
385    def visit_option(self, node):
386        # each form of the option will be presented separately
387        if self.context[-1]>0:
388            self.body.append(', ')
389        if self.context[-3] == '.BI':
390            self.body.append('\\')
391        self.body.append(' ')
392
393    def depart_option(self, node):
394        self.context[-1] += 1
395
396    def visit_option_group(self, node):
397        # as one option could have several forms it is a group
398        # options without parameter bold only, .B, -v
399        # options with parameter bold italic, .BI, -f file
400       
401        # we do not know if .B or .BI
402        self.context.append('.B')           # blind guess
403        self.context.append(len(self.body)) # to be able to insert later
404        self.context.append(0)              # option counter
405
406    def depart_option_group(self, node):
407        self.context.pop()  # the counter
408        start_position = self.context.pop()
409        text = self.body[start_position:]
410        del self.body[start_position:]
411        self.body.append('\n%s%s' % (self.context.pop(), ''.join(text)))
412
413    visit_option_string = depart_option_string = noop
414
415    def visit_option_argument(self, node):
416        self.context[-3] = '.BI'
417        if self.body[len(self.body)-1].endswith('='):
418            # a blank only means no blank in output
419            self.body.append(' ')
420        else:
421            # backslash blank blank
422            self.body.append('\\  ')
423
424    def depart_paragraph(self, node):
425        # TODO .PP or an empty line
426        if self.body and not self.body[-1].endswith("\n"):
427            self.body.append('\n')
428
429    def visit_raw(self, node):
430        if node.get('format') == 'manpage':
431            self.body.append(node.astext())
432        # Keep non-HTML raw text out of output:
433        raise nodes.SkipNode
434
435    def visit_revision(self, node):
436        self.visit_docinfo_item(node, 'revision')
437
438    def depart_revision(self, node):
439        self.depart_docinfo_item()
440
441    def visit_row(self, node):
442        self._active_table.new_row()
443
444    def visit_section(self, node):
445        self.section_level += 1
446
447    def depart_section(self, node):
448        self.section_level -= 1
449
450    def visit_substitution_definition(self, node):
451        """Internal only."""
452        raise nodes.SkipNode
453
454    def visit_substitution_reference(self, node):
455        self.unimplemented_visit(node)
456
457    def visit_subtitle(self, node):
458        self._docinfo["subtitle"] = node.astext()
459        raise nodes.SkipNode
460
461    def visit_system_message(self, node):
462        # TODO add report_level
463        #if node['level'] < self.document.reporter['writer'].report_level:
464            # Level is too low to display:
465        #    raise nodes.SkipNode
466        self.body.append('\.SH system-message\n')
467        attr = {}
468        backref_text = ''
469        if node.hasattr('id'):
470            attr['name'] = node['id']
471        if node.hasattr('line'):
472            line = ', line %s' % node['line']
473        else:
474            line = ''
475        self.body.append('System Message: %s/%s (%s:%s)\n'
476                         % (node['type'], node['level'], node['source'], line))
477
478    def depart_system_message(self, node):
479        self.body.append('\n')
480
481    def visit_table(self, node):
482        self._active_table = Table()
483
484    def depart_table(self, node):
485        self.body.append(self._active_table.astext())
486        self._active_table = None
487
488    def visit_target(self, node):
489        self.body.append(self.comment('visit_target'))
490
491    def depart_target(self, node):
492        self.body.append(self.comment('depart_target'))
493
494    visit_tbody = depart_tbody = noop
495    visit_tgroup = depart_tgroup = noop
496
497    def visit_title(self, node):
498        if len(self.body) and not self.body[-1].endswith("\n"):
499            self.body.append("\n")
500        if isinstance(node.parent, nodes.topic):
501            self.body.append(self.comment('topic-title'))
502        elif isinstance(node.parent, nodes.sidebar):
503            self.body.append(self.comment('sidebar-title'))
504        elif isinstance(node.parent, nodes.admonition):
505            self.body.append(self.comment('admonition-title'))
506        elif self.section_level == 0:
507            # document title for .TH
508            self._docinfo['title'] = node.astext()
509            raise nodes.SkipNode
510        elif self.section_level == 1:
511            self.body.append('.SH ')
512        else:
513            self.body.append('.SS ')
514
515    def depart_title(self, node):
516        if not self.body[-1].endswith("\n"):
517            self.body.append('\n')
518
519    def visit_topic(self, node):
520        self.body.append(self.comment('topic: '+node.astext()))
521        raise nodes.SkipNode
522        ##self.topic_class = node.get('class')
523
524    def visit_transition(self, node):
525        # .PP      Begin a new paragraph and reset prevailing indent.
526        # .sp N    leaves N lines of blank space.
527        # .ce      centers the next line
528        self.body.append('\n.sp\n.ce\n----\n')
529
530    def depart_transition(self, node):
531        self.body.append('\n.ce 0\n.sp\n')
532
533    def visit_version(self, node):
534        self._docinfo["version"] = node.astext()
535        raise nodes.SkipNode
536
537    def __getattr__(self, attr):
538        if attr.startswith("visit_"):
539            if not hasattr(self, "depart_%s" % attr[6:]):
540                obj = self.unimplemented_visit
541            else:
542                obj = self.noop
543        elif attr.startswith("depart_"):
544            if not hasattr(self, "visit_%s" % attr[6:]):
545                obj = self.unimplemented_visit
546            else:
547                obj = self.noop
548        else:
549            raise AttributeError(self, attr)
550        setattr(self, attr, obj)
551        return obj
552
553# vim: set et ts=4 ai :
Note: See TracBrowser for help on using the browser.