1
# Copyright (C) 2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""A sphinx/docutil writer producing texinfo output."""
19
from __future__ import absolute_import
21
from docutils import (
28
class TexinfoWriter(writers.Writer):
30
supported = ('texinfo',)
31
settings_spec = ('No options here.', '', ())
32
settings_defaults = {}
36
def __init__(self, builder):
37
writers.Writer.__init__(self)
38
self.builder = builder
41
visitor = TexinfoTranslator(self.document, self.builder)
42
self.document.walkabout(visitor)
43
self.output = visitor.body
46
class TexinfoTranslator(nodes.NodeVisitor):
48
section_names = ['chapter', 'section', 'subsection', 'subsubsection']
49
"""texinfo section names differ from the sphinx ones.
51
Since this can be confusing, the correspondences are shown below
56
subsection -> subsubsection
58
Additionally, sphinx defines subsubsections and paragraphs which are
59
handled as @heading (unnumbered).
62
def __init__(self, document, builder):
63
nodes.NodeVisitor.__init__(self, document)
64
self.builder = builder
65
# toctree uses some nodes for different purposes (namely:
66
# compact_paragraph, bullet_list) that needs to know when they are
67
# processing a toctree.
68
self.in_toctree = False
69
# sections can be embedded and produce different directives depending
71
self.section_level = -1
72
# By default paragraghs are separated by newlines, but there are some
73
# exceptions that set it to '' for some subtrees instead
74
self.paragraph_sep = '\n'
78
def visit_document(self, node):
81
sys.stdout.write(node.pformat().encode('utf8'))
82
set_item_list_collector(node, 'text')
84
def depart_document(self, node):
85
# FIXME: info requires a Top node for each info file, but unless we
86
# chose a global layout to divide the overall documentation into a set
87
# of info files, there is no criteria to decide for a title.
89
This file has been converted using a beta rst->texinfo converter.
90
Most of the info links are currently bogus, don't report bugs about them,
91
this is currently worked on.
95
self.body = top_cmd + ''.join(node['text'])
99
def visit_section(self, node):
100
self.section_level += 1
101
set_item_list_collector(node, 'text')
103
def depart_section(self, node):
104
title = node['title']
105
ids = node.get('ids', [])
107
section_name = self.section_names[self.section_level]
109
# Just use @heading, it's not numbered anyway
110
section_name = 'heading'
112
# There shouldn't be different ids for a section, so until we
113
# encounter bugs, just take the first one.
114
node_cmd = '@node %s\n' % (ids[0],)
117
section_cmd = '@%s %s\n' % (section_name, title)
118
text = ''.join(node['text'])
119
node.parent.collect_text(node_cmd + section_cmd + text)
120
self.section_level -= 1
122
def visit_topic(self, node):
125
def depart_topic(self, node):
128
def visit_compound(self, node):
129
# compound is new in sphinx >= 1.0 and just add a optional layer so we
130
# relay the text to the parent when it occurs. This may requires a
131
# cleaner approach once we settle on which sphinx versions we want to
133
set_item_list_collector(node, 'text')
135
def depart_compound(self, node):
136
text = ''.join(node['text'])
137
node.parent.collect_text(text)
139
def visit_paragraph(self, node):
140
set_item_list_collector(node, 'text')
142
def depart_paragraph(self, node):
143
# End the paragraph with a new line (or '' depending on the parent) and
144
# leave a blank line after it.
145
text = ''.join(node['text']) + self.paragraph_sep * 2
146
node.parent.collect_text(text)
148
def visit_compact_paragraph(self, node):
149
set_item_list_collector(node, 'text')
150
if node.has_key('toctree'):
151
self.in_toctree = True
152
elif self.in_toctree:
153
set_item_collector(node, 'reference')
155
def depart_compact_paragraph(self, node):
156
# FIXME: Using a different visitor specific to toctree may be a better
157
# design and makes code clearer. -- vila 20100708
158
if node.has_key('toctree'):
160
node.parent.collect_text('@menu\n')
161
node.parent.collect_text(''.join(node['text']))
162
node.parent.collect_text('@end menu\n')
163
self.in_toctree = False
164
elif self.in_toctree:
165
# * FIRST-ENTRY-NAME:(FILENAME)NODENAME. DESCRIPTION
166
# XXX: the file name should probably be adjusted to the targeted
168
node_name, file_name, entry_name = node['reference']
170
node_name = entry_name
171
description = '' # We can't specify a description in rest AFAICS
172
# XXX: What if :maxdepth: is not 1 ?
173
text = '* %s: (%s)%s. %s\n' % (entry_name, file_name,
174
node_name, description)
175
node.parent.collect_text(text)
177
# End the paragraph with a new line (or '' depending on the parent)
178
# and leave a blank line after it.
179
text = ''.join(node['text']) + self.paragraph_sep * 2
180
node.parent.collect_text(text)
182
def visit_literal_block(self, node):
183
set_item_collector(node, 'text')
185
def depart_literal_block(self, node):
186
text = '@samp{%s}' % ''.join(node['text']) + self.paragraph_sep * 2
187
node.parent.collect_text(text)
189
def visit_block_quote(self, node):
190
set_item_list_collector(node, 'text')
192
def depart_block_quote(self, node):
193
node.parent.collect_text('@example\n')
194
node.parent.collect_text(''.join(node['text']))
195
node.parent.collect_text('@end example\n\n')
197
def depart_warning(self, node):
200
def visit_warning(self, node):
201
raise nodes.SkipNode # Not implemented yet
203
def visit_note(self, node):
204
raise nodes.SkipNode # Not implemented yet
206
def depart_note(self, node):
209
def visit_footnote(self, node):
210
raise nodes.SkipNode # Not implemented yet
212
def depart_footnote(self, node):
215
def visit_comment(self, node):
216
raise nodes.SkipNode # Not implemented yet
220
def visit_title(self, node):
221
set_item_collector(node, 'text')
223
def depart_title(self, node):
224
node.parent['title'] = node['text']
226
def visit_label(self, node):
227
raise nodes.SkipNode # Not implemented yet
229
def visit_substitution_definition(self, node):
230
raise nodes.SkipNode # Not implemented yet
234
def visit_Text(self, node):
237
def depart_Text(self, node):
240
text = text.replace('@', '@@')
242
text = text.replace('{', '@{')
244
text = text.replace('}', '@}')
245
node.parent.collect_text(text)
250
def visit_emphasis(self, node):
251
set_item_collector(node, 'text')
253
def depart_emphasis(self, node):
254
text = '@emph{%s}' % node['text']
255
node.parent.collect_text(text)
257
def visit_strong(self, node):
258
set_item_collector(node, 'text')
260
def depart_strong(self, node):
261
text = '@strong{%s}' % node['text']
262
node.parent.collect_text(text)
264
def visit_literal(self, node):
265
set_item_collector(node, 'text')
267
def depart_literal(self, node):
268
text = '@code{%s}' % node['text']
269
node.parent.collect_text(text)
273
def _decorate_list(self, item_list, collect, item_fmt='%s',
274
head=None, foot=None):
277
for item in item_list:
278
collect(item_fmt % item)
282
def visit_bullet_list(self, node):
283
set_item_list_collector(node, 'list_item')
285
def depart_bullet_list(self, node):
286
l = node['list_item']
288
self._decorate_list(node['list_item'], node.parent.collect_text)
290
self._decorate_list(node['list_item'], node.parent.collect_text,
292
# FIXME: Should respect the 'bullet' attribute
293
'@itemize @bullet\n', '@end itemize\n')
295
def visit_enumerated_list(self, node):
296
set_item_list_collector(node, 'list_item')
298
def depart_enumerated_list(self, node):
299
self._decorate_list(node['list_item'], node.parent.collect_text,
301
'@enumerate\n', '@end enumerate\n')
303
def visit_definition_list(self, node):
304
raise nodes.SkipNode # Not implemented yet
306
def depart_definition_list(self, node):
307
raise nodes.SkipNode # Not implemented yet
309
def visit_definition_list_item(self, node):
310
raise nodes.SkipNode # Not implemented yet
312
def depart_definition_list_item(self, node):
315
def visit_term(self, node):
316
raise nodes.SkipNode # Not implemented yet
318
def depart_term(self, node):
321
def visit_definition(self, node):
322
raise nodes.SkipNode # Not implemented yet
324
def depart_definition(self, node):
327
def visit_field_list(self, node):
328
raise nodes.SkipNode # Not implemented yet
330
def depart_field_list(self, node):
333
def visit_field(self, node):
334
raise nodes.SkipNode # Not implemented yet
336
def depart_field(self, node):
339
def visit_field_name(self, node):
340
raise nodes.SkipNode # Not implemented yet
342
def depart_field_name(self, node):
345
def visit_field_body(self, node):
346
raise nodes.SkipNode # Not implemented yet
348
def depart_field_body(self, node):
351
def visit_list_item(self, node):
352
set_item_list_collector(node, 'text')
354
def depart_list_item(self, node):
355
text = ''.join(node['text'])
356
node.parent.collect_list_item(text)
358
def visit_option_list(self, node):
359
raise nodes.SkipNode # Not implemented yet
361
def depart_option_list(self, node):
364
def visit_option_list_item(self, node):
367
def depart_option_list_item(self, node):
370
def visit_option_group(self, node):
373
def depart_option_group(self, node):
376
def visit_option(self, node):
379
def depart_option(self, node):
382
def visit_option_string(self, node):
384
def depart_option_string(self, node):
387
def visit_option_argument(self, node):
390
def depart_option_argument(self, node):
393
def visit_description(self, node):
395
def depart_description(self, node):
399
def visit_table(self, node):
400
set_item_collector(node, 'table')
402
def depart_table(self, node):
403
node.parent.collect_text(node['table'])
405
def visit_tgroup(self, node):
406
set_item_list_collector(node, 'colspec')
407
set_item_collector(node, 'head_entries')
408
set_item_collector(node, 'body_rows')
410
def depart_tgroup(self, node):
412
# The '@multitable {xxx}{xxx}' line
413
self._decorate_list(node['colspec'], header.append,
414
'{%s}', '@multitable ', '\n')
415
# The '@headitem xxx @tab yyy...' line
416
head_entries = node['head_entries']
417
if head_entries is not None:
418
# Not all tables define titles for the columns... rest parser bug ?
420
self._decorate_list(head_entries[1:], header.append,
422
'@headitem %s' % head_entries[0], '\n')
423
header = ''.join(header)
424
# The '@item xxx\n @tab yyy\n ...' lines
425
body_rows = node['body_rows']
428
self._decorate_list(r[1:], rows.append,
429
'@tab %s\n', '@item %s\n' % r[0])
430
footer = '@end multitable\n'
431
node.parent.collect_table(header + ''.join(rows) + footer)
433
def visit_colspec(self, node):
436
def depart_colspec(self, node):
437
node.parent.collect_colspec('x' * node['colwidth'])
439
def visit_thead(self, node):
440
set_item_collector(node, 'row')
442
def depart_thead(self, node):
443
node.parent.collect_head_entries(node['row'])
445
def visit_tbody(self, node):
446
set_item_list_collector(node, 'row')
448
def depart_tbody(self, node):
449
node.parent.collect_body_rows(node['row'])
451
def visit_row(self, node):
452
set_item_list_collector(node, 'entry')
454
def depart_row(self, node):
455
node.parent.collect_row(node['entry'])
457
def visit_entry(self, node):
458
set_item_list_collector(node, 'text')
459
node['par_sep_orig'] = self.paragraph_sep
460
self.paragraph_sep = ''
462
def depart_entry(self, node):
463
node.parent.collect_entry(''.join(node['text']))
464
self.paragraph_sep = node['par_sep_orig']
468
def visit_reference(self, node):
469
for c in node.children:
470
if getattr(c, 'parent', None) is None:
473
set_item_collector(node, 'text')
475
def depart_reference(self, node):
476
anchorname = node.get('anchorname', None)
477
refuri = node.get('refuri', None)
478
refid = node.get('refid', None)
479
text = ''.join(node['text'])
480
collect = getattr(node.parent, 'collect_reference', None)
481
if collect is not None:
482
if not self.in_toctree:
483
raise AssertionError('collect_reference is specific to toctree')
484
if anchorname is None:
488
collect((anchorname, refuri, text))
489
elif refuri is not None:
490
node.parent.collect_text('@uref{%s,%s}' % (refuri, text))
491
elif refid is not None:
492
# Info format requires that a reference is followed by some
493
# punctuation char ('.', ','. ')', etc). Rest is more liberal. To
494
# accommodate, we use pxref inside parenthesis.
495
node.parent.collect_text('%s (@pxref{%s})' % (text, refid))
497
def visit_footnote_reference(self, node):
498
raise nodes.SkipNode # Not implemented yet
500
def visit_citation_reference(self, node):
501
raise nodes.SkipNode # Not implemented yet
503
def visit_title_reference(self, node):
504
raise nodes.SkipNode # Not implemented yet
506
def depart_title_reference(self, node):
509
def visit_target(self, node):
510
raise nodes.SkipNode # Not implemented yet
512
def depart_target(self, node):
515
def visit_image(self, node):
516
raise nodes.SkipNode # Not implemented yet
518
# Helpers to collect data in parent node
520
def set_item_collector(node, name):
524
setattr(node, 'collect_' + name, set_item)
527
def set_item_list_collector(node, name, sep=''):
529
node[name + '_sep'] = sep
530
def append_item(item):
531
node[name].append(item)
532
setattr(node, 'collect_' + name, append_item)