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