manpage.py 36 KB

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