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 docutils import (
26
class TexinfoWriter(writers.Writer):
28
supported = ('texinfo',)
29
settings_spec = ('No options here.', '', ())
30
settings_defaults = {}
34
def __init__(self, builder):
35
writers.Writer.__init__(self)
36
self.builder = builder
39
visitor = TexinfoTranslator(self.document, self.builder)
40
self.document.walkabout(visitor)
41
self.output = visitor.body
44
class TexinfoTranslator(nodes.NodeVisitor):
46
section_names = ['chapter', 'section', 'subsection', 'subsubsection']
47
"""texinfo section names differ from the sphinx ones.
49
Since this can be confusing, the correspondences are shown below
54
subsection -> subsubsection
56
Additionally, sphinx defines subsubsections and paragraphs which are
57
handled as @heading (unnumbered).
60
def __init__(self, document, builder):
61
nodes.NodeVisitor.__init__(self, document)
62
self.builder = builder
63
# toctree uses some nodes for different purposes (namely:
64
# compact_paragraph, bullet_list) that needs to know when they are
65
# processing a toctree.
66
self.in_toctree = False
67
# sections can be embedded and produce different directives depending
69
self.section_level = -1
70
# By default paragraghs are separated by newlines, but there are some
71
# exceptions that set it to '' for some subtrees instead
72
self.paragraph_sep = '\n'
76
def visit_document(self, node):
79
sys.stdout.write(node.pformat().encode('utf8'))
80
set_item_list_collector(node, 'text')
82
def depart_document(self, node):
83
# FIXME: info requires a Top node for each info file, but unless we
84
# chose a global layout to divide the overall documentation into a set
85
# of info files, there is no criteria to decide for a title.
87
This file has been converted using a beta rst->texinfo converter.
88
Most of the info links are currently bogus, don't report bugs about them,
89
this is currently worked on.
93
self.body = top_cmd + ''.join(node['text'])
97
def visit_section(self, node):
98
self.section_level += 1
99
set_item_list_collector(node, 'text')
101
def depart_section(self, node):
102
title = node['title']
103
ids = node.get('ids', [])
105
section_name = self.section_names[self.section_level]
107
# Just use @heading, it's not numbered anyway
108
section_name = 'heading'
110
# There shouldn't be different ids for a section, so until we
111
# encounter bugs, just take the first one.
112
node_cmd = '@node %s\n' % (ids[0],)
115
section_cmd = '@%s %s\n' % (section_name, title)
116
text = ''.join(node['text'])
117
node.parent.collect_text(node_cmd + section_cmd + text)
118
self.section_level -= 1
120
def visit_topic(self, node):
123
def depart_topic(self, node):
126
def visit_compound(self, node):
127
# compound is new in sphinx >= 1.0 and just add a optional layer so we
128
# relay the text to the parent when it occurs. This may requires a
129
# cleaner approach once we settle on which sphinx versions we want to
131
set_item_list_collector(node, 'text')
133
def depart_compound(self, node):
134
text = ''.join(node['text'])
135
node.parent.collect_text(text)
137
def visit_paragraph(self, node):
138
set_item_list_collector(node, 'text')
140
def depart_paragraph(self, node):
141
# End the paragraph with a new line (or '' depending on the parent) and
142
# leave a blank line after it.
143
text = ''.join(node['text']) + self.paragraph_sep * 2
144
node.parent.collect_text(text)
146
def visit_compact_paragraph(self, node):
147
set_item_list_collector(node, 'text')
148
if node.has_key('toctree'):
149
self.in_toctree = True
150
elif self.in_toctree:
151
set_item_collector(node, 'reference')
153
def depart_compact_paragraph(self, node):
154
# FIXME: Using a different visitor specific to toctree may be a better
155
# design and makes code clearer. -- vila 20100708
156
if node.has_key('toctree'):
158
node.parent.collect_text('@menu\n')
159
node.parent.collect_text(''.join(node['text']))
160
node.parent.collect_text('@end menu\n')
161
self.in_toctree = False
162
elif self.in_toctree:
163
# * FIRST-ENTRY-NAME:(FILENAME)NODENAME. DESCRIPTION
164
# XXX: the file name should probably be adjusted to the targeted
166
node_name, file_name, entry_name = node['reference']
168
node_name = entry_name
169
description = '' # We can't specify a description in rest AFAICS
170
# XXX: What if :maxdepth: is not 1 ?
171
text = '* %s: (%s)%s. %s\n' % (entry_name, file_name,
172
node_name, description)
173
node.parent.collect_text(text)
175
# End the paragraph with a new line (or '' depending on the parent)
176
# and leave a blank line after it.
177
text = ''.join(node['text']) + self.paragraph_sep * 2
178
node.parent.collect_text(text)
180
def visit_literal_block(self, node):
181
set_item_collector(node, 'text')
183
def depart_literal_block(self, node):
184
text = '@samp{%s}' % ''.join(node['text']) + self.paragraph_sep * 2
185
node.parent.collect_text(text)
187
def visit_block_quote(self, node):
188
set_item_list_collector(node, 'text')
190
def depart_block_quote(self, node):
191
node.parent.collect_text('@example\n')
192
node.parent.collect_text(''.join(node['text']))
193
node.parent.collect_text('@end example\n\n')
195
def depart_warning(self, node):
198
def visit_warning(self, node):
199
raise nodes.SkipNode # Not implemented yet
201
def visit_note(self, node):
202
raise nodes.SkipNode # Not implemented yet
204
def depart_note(self, node):
207
def visit_footnote(self, node):
208
raise nodes.SkipNode # Not implemented yet
210
def depart_footnote(self, node):
213
def visit_comment(self, node):
214
raise nodes.SkipNode # Not implemented yet
218
def visit_title(self, node):
219
set_item_collector(node, 'text')
221
def depart_title(self, node):
222
node.parent['title'] = node['text']
224
def visit_label(self, node):
225
raise nodes.SkipNode # Not implemented yet
227
def visit_substitution_definition(self, node):
228
raise nodes.SkipNode # Not implemented yet
232
def visit_Text(self, node):
235
def depart_Text(self, node):
238
text = text.replace('@', '@@')
240
text = text.replace('{', '@{')
242
text = text.replace('}', '@}')
243
node.parent.collect_text(text)
248
def visit_emphasis(self, node):
249
set_item_collector(node, 'text')
251
def depart_emphasis(self, node):
252
text = '@emph{%s}' % node['text']
253
node.parent.collect_text(text)
255
def visit_strong(self, node):
256
set_item_collector(node, 'text')
258
def depart_strong(self, node):
259
text = '@strong{%s}' % node['text']
260
node.parent.collect_text(text)
262
def visit_literal(self, node):
263
set_item_collector(node, 'text')
265
def depart_literal(self, node):
266
text = '@code{%s}' % node['text']
267
node.parent.collect_text(text)
271
def _decorate_list(self, item_list, collect, item_fmt='%s',
272
head=None, foot=None):
275
for item in item_list:
276
collect(item_fmt % item)
280
def visit_bullet_list(self, node):
281
set_item_list_collector(node, 'list_item')
283
def depart_bullet_list(self, node):
284
l = node['list_item']
286
self._decorate_list(node['list_item'], node.parent.collect_text)
288
self._decorate_list(node['list_item'], node.parent.collect_text,
290
# FIXME: Should respect the 'bullet' attribute
291
'@itemize @bullet\n', '@end itemize\n')
293
def visit_enumerated_list(self, node):
294
set_item_list_collector(node, 'list_item')
296
def depart_enumerated_list(self, node):
297
self._decorate_list(node['list_item'], node.parent.collect_text,
299
'@enumerate\n', '@end enumerate\n')
301
def visit_definition_list(self, node):
302
raise nodes.SkipNode # Not implemented yet
304
def depart_definition_list(self, node):
305
raise nodes.SkipNode # Not implemented yet
307
def visit_definition_list_item(self, node):
308
raise nodes.SkipNode # Not implemented yet
310
def depart_definition_list_item(self, node):
313
def visit_term(self, node):
314
raise nodes.SkipNode # Not implemented yet
316
def depart_term(self, node):
319
def visit_definition(self, node):
320
raise nodes.SkipNode # Not implemented yet
322
def depart_definition(self, node):
325
def visit_field_list(self, node):
326
raise nodes.SkipNode # Not implemented yet
328
def depart_field_list(self, node):
331
def visit_field(self, node):
332
raise nodes.SkipNode # Not implemented yet
334
def depart_field(self, node):
337
def visit_field_name(self, node):
338
raise nodes.SkipNode # Not implemented yet
340
def depart_field_name(self, node):
343
def visit_field_body(self, node):
344
raise nodes.SkipNode # Not implemented yet
346
def depart_field_body(self, node):
349
def visit_list_item(self, node):
350
set_item_list_collector(node, 'text')
352
def depart_list_item(self, node):
353
text = ''.join(node['text'])
354
node.parent.collect_list_item(text)
356
def visit_option_list(self, node):
357
raise nodes.SkipNode # Not implemented yet
359
def depart_option_list(self, node):
362
def visit_option_list_item(self, node):
365
def depart_option_list_item(self, node):
368
def visit_option_group(self, node):
371
def depart_option_group(self, node):
374
def visit_option(self, node):
377
def depart_option(self, node):
380
def visit_option_string(self, node):
382
def depart_option_string(self, node):
385
def visit_option_argument(self, node):
388
def depart_option_argument(self, node):
391
def visit_description(self, node):
393
def depart_description(self, node):
397
def visit_table(self, node):
398
set_item_collector(node, 'table')
400
def depart_table(self, node):
401
node.parent.collect_text(node['table'])
403
def visit_tgroup(self, node):
404
set_item_list_collector(node, 'colspec')
405
set_item_collector(node, 'head_entries')
406
set_item_collector(node, 'body_rows')
408
def depart_tgroup(self, node):
410
# The '@multitable {xxx}{xxx}' line
411
self._decorate_list(node['colspec'], header.append,
412
'{%s}', '@multitable ', '\n')
413
# The '@headitem xxx @tab yyy...' line
414
head_entries = node['head_entries']
415
if head_entries is not None:
416
# Not all tables define titles for the columns... rest parser bug ?
418
self._decorate_list(head_entries[1:], header.append,
420
'@headitem %s' % head_entries[0], '\n')
421
header = ''.join(header)
422
# The '@item xxx\n @tab yyy\n ...' lines
423
body_rows = node['body_rows']
426
self._decorate_list(r[1:], rows.append,
427
'@tab %s\n', '@item %s\n' % r[0])
428
footer = '@end multitable\n'
429
node.parent.collect_table(header + ''.join(rows) + footer)
431
def visit_colspec(self, node):
434
def depart_colspec(self, node):
435
node.parent.collect_colspec('x' * node['colwidth'])
437
def visit_thead(self, node):
438
set_item_collector(node, 'row')
440
def depart_thead(self, node):
441
node.parent.collect_head_entries(node['row'])
443
def visit_tbody(self, node):
444
set_item_list_collector(node, 'row')
446
def depart_tbody(self, node):
447
node.parent.collect_body_rows(node['row'])
449
def visit_row(self, node):
450
set_item_list_collector(node, 'entry')
452
def depart_row(self, node):
453
node.parent.collect_row(node['entry'])
455
def visit_entry(self, node):
456
set_item_list_collector(node, 'text')
457
node['par_sep_orig'] = self.paragraph_sep
458
self.paragraph_sep = ''
460
def depart_entry(self, node):
461
node.parent.collect_entry(''.join(node['text']))
462
self.paragraph_sep = node['par_sep_orig']
466
def visit_reference(self, node):
467
for c in node.children:
468
if getattr(c, 'parent', None) is None:
471
set_item_collector(node, 'text')
473
def depart_reference(self, node):
474
anchorname = node.get('anchorname', None)
475
refuri = node.get('refuri', None)
476
refid = node.get('refid', None)
477
text = ''.join(node['text'])
478
collect = getattr(node.parent, 'collect_reference', None)
479
if collect is not None:
480
if not self.in_toctree:
481
raise AssertionError('collect_reference is specific to toctree')
482
if anchorname is None:
486
collect((anchorname, refuri, text))
487
elif refuri is not None:
488
node.parent.collect_text('@uref{%s,%s}' % (refuri, text))
489
elif refid is not None:
490
# Info format requires that a reference is followed by some
491
# punctuation char ('.', ','. ')', etc). Rest is more liberal. To
492
# accommodate, we use pxref inside parenthesis.
493
node.parent.collect_text('%s (@pxref{%s})' % (text, refid))
495
def visit_footnote_reference(self, node):
496
raise nodes.SkipNode # Not implemented yet
498
def visit_citation_reference(self, node):
499
raise nodes.SkipNode # Not implemented yet
501
def visit_title_reference(self, node):
502
raise nodes.SkipNode # Not implemented yet
504
def depart_title_reference(self, node):
507
def visit_target(self, node):
508
raise nodes.SkipNode # Not implemented yet
510
def depart_target(self, node):
513
def visit_image(self, node):
514
raise nodes.SkipNode # Not implemented yet
516
# Helpers to collect data in parent node
518
def set_item_collector(node, name):
522
setattr(node, 'collect_' + name, set_item)
525
def set_item_list_collector(node, name, sep=''):
527
node[name + '_sep'] = sep
528
def append_item(item):
529
node[name].append(item)
530
setattr(node, 'collect_' + name, append_item)