1
# Author: David Goodger
2
# Contact: goodger@users.sourceforge.net
3
# Revision: $Revision: 4219 $
4
# Date: $Date: 2005-12-15 15:32:01 +0100 (Thu, 15 Dec 2005) $
5
# Copyright: This module has been placed in the public domain.
8
Simple HyperText Markup Language document tree Writer.
10
The output conforms to the XHTML version 1.0 Transitional DTD
11
(*almost* strict). The output contains a minimum of formatting
12
information. The cascading style sheet "html4css1.css" is required
13
for proper viewing with a modern graphical browser.
16
__docformat__ = 'reStructuredText'
24
from types import ListType
26
import Image # check for the Python Imaging Library
30
from docutils import frontend, nodes, utils, writers, languages
33
class Writer(writers.Writer):
35
supported = ('html', 'html4css1', 'xhtml')
36
"""Formats this writer supports."""
38
default_stylesheet = 'html4css1.css'
40
default_stylesheet_path = utils.relative_path(
41
os.path.join(os.getcwd(), 'dummy'),
42
os.path.join(os.path.dirname(__file__), default_stylesheet))
45
'HTML-Specific Options',
47
(('Specify a stylesheet URL, used verbatim. Overrides '
50
{'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
51
('Specify a stylesheet file, relative to the current working '
52
'directory. The path is adjusted relative to the output HTML '
53
'file. Overrides --stylesheet. Default: "%s"'
54
% default_stylesheet_path,
55
['--stylesheet-path'],
56
{'metavar': '<file>', 'overrides': 'stylesheet',
57
'default': default_stylesheet_path}),
58
('Embed the stylesheet in the output HTML file. The stylesheet '
59
'file must be accessible during processing (--stylesheet-path is '
60
'recommended). This is the default.',
61
['--embed-stylesheet'],
62
{'default': 1, 'action': 'store_true',
63
'validator': frontend.validate_boolean}),
64
('Link to the stylesheet in the output HTML file. Default: '
65
'embed the stylesheet, do not link to it.',
66
['--link-stylesheet'],
67
{'dest': 'embed_stylesheet', 'action': 'store_false',
68
'validator': frontend.validate_boolean}),
69
('Specify the initial header level. Default is 1 for "<h1>". '
70
'Does not affect document title & subtitle (see --no-doc-title).',
71
['--initial-header-level'],
72
{'choices': '1 2 3 4 5 6'.split(), 'default': '1',
73
'metavar': '<level>'}),
74
('Specify the maximum width (in characters) for one-column field '
75
'names. Longer field names will span an entire row of the table '
76
'used to render the field list. Default is 14 characters. '
77
'Use 0 for "no limit".',
78
['--field-name-limit'],
79
{'default': 14, 'metavar': '<level>',
80
'validator': frontend.validate_nonnegative_int}),
81
('Specify the maximum width (in characters) for options in option '
82
'lists. Longer options will span an entire row of the table used '
83
'to render the option list. Default is 14 characters. '
84
'Use 0 for "no limit".',
86
{'default': 14, 'metavar': '<level>',
87
'validator': frontend.validate_nonnegative_int}),
88
('Format for footnote references: one of "superscript" or '
89
'"brackets". Default is "brackets".',
90
['--footnote-references'],
91
{'choices': ['superscript', 'brackets'], 'default': 'brackets',
92
'metavar': '<format>',
93
'overrides': 'trim_footnote_reference_space'}),
94
('Format for block quote attributions: one of "dash" (em-dash '
95
'prefix), "parentheses"/"parens", or "none". Default is "dash".',
97
{'choices': ['dash', 'parentheses', 'parens', 'none'],
98
'default': 'dash', 'metavar': '<format>'}),
99
('Remove extra vertical whitespace between items of "simple" bullet '
100
'lists and enumerated lists. Default: enabled.',
102
{'default': 1, 'action': 'store_true',
103
'validator': frontend.validate_boolean}),
104
('Disable compact simple bullet and enumerated lists.',
105
['--no-compact-lists'],
106
{'dest': 'compact_lists', 'action': 'store_false'}),
107
('Remove extra vertical whitespace between items of simple field '
108
'lists. Default: enabled.',
109
['--compact-field-lists'],
110
{'default': 1, 'action': 'store_true',
111
'validator': frontend.validate_boolean}),
112
('Disable compact simple field lists.',
113
['--no-compact-field-lists'],
114
{'dest': 'compact_field_lists', 'action': 'store_false'}),
115
('Omit the XML declaration. Use with caution.',
116
['--no-xml-declaration'],
117
{'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
118
'validator': frontend.validate_boolean}),
119
('Obfuscate email addresses to confuse harvesters while still '
120
'keeping email links usable with standards-compliant browsers.',
121
['--cloak-email-addresses'],
122
{'action': 'store_true', 'validator': frontend.validate_boolean}),))
124
settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
126
relative_path_settings = ('stylesheet_path',)
128
config_section = 'html4css1 writer'
129
config_section_dependencies = ('writers',)
132
writers.Writer.__init__(self)
133
self.translator_class = HTMLTranslator
136
self.visitor = visitor = self.translator_class(self.document)
137
self.document.walkabout(visitor)
138
self.output = visitor.astext()
139
for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
140
'body_pre_docinfo', 'docinfo', 'body', 'fragment',
142
setattr(self, attr, getattr(visitor, attr))
144
def assemble_parts(self):
145
writers.Writer.assemble_parts(self)
146
for part in ('title', 'subtitle', 'docinfo', 'body', 'header',
147
'footer', 'meta', 'stylesheet', 'fragment',
148
'html_prolog', 'html_head', 'html_title', 'html_subtitle',
150
self.parts[part] = ''.join(getattr(self.visitor, part))
153
class HTMLTranslator(nodes.NodeVisitor):
156
This HTML writer has been optimized to produce visually compact
157
lists (less vertical whitespace). HTML's mixed content models
158
allow list items to contain "<li><p>body elements</p></li>" or
159
"<li>just text</li>" or even "<li>text<p>and body
160
elements</p>combined</li>", each with different effects. It would
161
be best to stick with strict body elements in list items, but they
162
affect vertical spacing in browsers (although they really
165
Here is an outline of the optimization:
167
- Check for and omit <p> tags in "simple" lists: list items
168
contain either a single paragraph, a nested simple list, or a
169
paragraph followed by a nested simple list. This means that
170
this list can be compact:
175
But this list cannot be compact:
179
This second paragraph forces space between list items.
183
- In non-list contexts, omit <p> tags on a paragraph if that
184
paragraph is the only child of its parent (footnotes & citations
185
are allowed a label first).
187
- Regardless of the above, in definitions, table cells, field bodies,
188
option descriptions, and list items, mark the first child with
189
'class="first"' and the last child with 'class="last"'. The stylesheet
190
sets the margins (top & bottom respectively) to 0 for these elements.
192
The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
193
option) disables list whitespace optimization.
196
xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
197
doctype = ('<!DOCTYPE html'
198
' PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
199
' "http://www.w3.org/TR/xhtml1/DTD/'
200
'xhtml1-transitional.dtd">\n')
201
head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
202
' xml:lang="%s" lang="%s">\n<head>\n')
203
content_type = ('<meta http-equiv="Content-Type"'
204
' content="text/html; charset=%s" />\n')
205
generator = ('<meta name="generator" content="Docutils %s: '
206
'http://docutils.sourceforge.net/" />\n')
207
stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
208
embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
209
named_tags = ['a', 'applet', 'form', 'frame', 'iframe', 'img', 'map']
210
words_and_spaces = re.compile(r'\S+| +|\n')
212
def __init__(self, document):
213
nodes.NodeVisitor.__init__(self, document)
214
self.settings = settings = document.settings
215
lcode = settings.language_code
216
self.language = languages.get_language(lcode)
217
self.meta = [self.content_type % settings.output_encoding,
218
self.generator % docutils.__version__]
219
self.head_prefix = []
220
self.html_prolog = []
221
if settings.xml_declaration:
222
self.head_prefix.append(self.xml_declaration
223
% settings.output_encoding)
224
# encoding not interpolated:
225
self.html_prolog.append(self.xml_declaration)
226
self.head_prefix.extend([self.doctype,
227
self.head_prefix_template % (lcode, lcode)])
228
self.html_prolog.append(self.doctype)
229
self.head = self.meta[:]
230
stylesheet = utils.get_stylesheet_reference(settings)
233
if settings.embed_stylesheet:
234
stylesheet = utils.get_stylesheet_reference(
235
settings, os.path.join(os.getcwd(), 'dummy'))
236
settings.record_dependencies.add(stylesheet)
237
stylesheet_text = open(stylesheet).read()
238
self.stylesheet = [self.embedded_stylesheet % stylesheet_text]
240
self.stylesheet = [self.stylesheet_link
241
% self.encode(stylesheet)]
242
self.body_prefix = ['</head>\n<body>\n']
243
# document title, subtitle display
244
self.body_pre_docinfo = []
249
self.body_suffix = ['</body>\n</html>\n']
250
self.section_level = 0
251
self.initial_header_level = int(settings.initial_header_level)
252
# A heterogenous stack used in conjunction with the tree traversal.
253
# Make sure that the pops correspond to the pushes:
255
self.topic_classes = []
258
self.compact_simple = None
259
self.compact_field_list = None
260
self.in_docinfo = None
261
self.in_sidebar = None
266
self.html_head = [self.content_type] # charset not interpolated
268
self.html_subtitle = []
270
self.in_document_title = 0
272
self.author_in_authors = None
275
return ''.join(self.head_prefix + self.head
276
+ self.stylesheet + self.body_prefix
277
+ self.body_pre_docinfo + self.docinfo
278
+ self.body + self.body_suffix)
280
def encode(self, text):
281
"""Encode special characters in `text` & return."""
282
# @@@ A codec to do these and all other HTML entities would be nice.
283
text = text.replace("&", "&")
284
text = text.replace("<", "<")
285
text = text.replace('"', """)
286
text = text.replace(">", ">")
287
text = text.replace("@", "@") # may thwart some address harvesters
288
# Replace the non-breaking space character with the HTML entity:
289
text = text.replace(u'\u00a0', " ")
292
def cloak_mailto(self, uri):
293
"""Try to hide a mailto: URL from harvesters."""
294
# Encode "@" using a URL octet reference (see RFC 1738).
295
# Further cloaking with HTML entities will be done in the
297
return uri.replace('@', '%40')
299
def cloak_email(self, addr):
300
"""Try to hide the link text of a email link from harversters."""
301
# Surround at-signs and periods with <span> tags. ("@" has
302
# already been encoded to "@" by the `encode` method.)
303
addr = addr.replace('@', '<span>@</span>')
304
addr = addr.replace('.', '<span>.</span>')
307
def attval(self, text,
308
whitespace=re.compile('[\n\r\t\v\f]')):
309
"""Cleanse, HTML encode, and return attribute value text."""
310
encoded = self.encode(whitespace.sub(' ', text))
311
if self.in_mailto and self.settings.cloak_email_addresses:
312
# Cloak at-signs ("%40") and periods with HTML entities.
313
encoded = encoded.replace('%40', '%40')
314
encoded = encoded.replace('.', '.')
317
def starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
319
Construct and return a start tag given a node (id & class attributes
320
are extracted), tag name, and optional attributes.
322
tagname = tagname.lower()
326
for (name, value) in attributes.items():
327
atts[name.lower()] = value
328
classes = node.get('classes', [])
329
if atts.has_key('class'):
330
classes.append(atts['class'])
332
atts['class'] = ' '.join(classes)
333
assert not atts.has_key('id')
334
ids.extend(node.get('ids', []))
335
if atts.has_key('ids'):
336
ids.extend(atts['ids'])
341
# Add empty "span" elements for additional IDs. Note
342
# that we cannot use empty "a" elements because there
343
# may be targets inside of references, but nested "a"
344
# elements aren't allowed in XHTML (even if they do
345
# not all have a "href" attribute).
347
# Empty tag. Insert target right in front of element.
348
prefix.append('<span id="%s"></span>' % id)
350
# Non-empty tag. Place the auxiliary <span> tag
351
# *inside* the element, as the first child.
352
suffix += '<span id="%s"></span>' % id
353
# !!! next 2 lines to be removed in Docutils 0.5:
354
if atts.has_key('id') and tagname in self.named_tags:
355
atts['name'] = atts['id'] # for compatibility with old browsers
356
attlist = atts.items()
359
for name, value in attlist:
360
# value=None was used for boolean attributes without
361
# value, but this isn't supported by XHTML.
362
assert value is not None
363
if isinstance(value, ListType):
364
values = [unicode(v) for v in value]
365
parts.append('%s="%s"' % (name.lower(),
366
self.attval(' '.join(values))))
369
uval = unicode(value)
370
except TypeError: # for Python 2.1 compatibility:
371
uval = unicode(str(value))
372
parts.append('%s="%s"' % (name.lower(), self.attval(uval)))
377
return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
379
def emptytag(self, node, tagname, suffix='\n', **attributes):
380
"""Construct and return an XML-compatible empty tag."""
381
return self.starttag(node, tagname, suffix, empty=1, **attributes)
383
# !!! to be removed in Docutils 0.5 (change calls to use "starttag"):
384
def start_tag_with_title(self, node, tagname, **atts):
385
"""ID and NAME attributes will be handled in the title."""
386
node = {'classes': node.get('classes', [])}
387
return self.starttag(node, tagname, **atts)
389
def set_class_on_child(self, node, class_, index=0):
391
Set class `class_` on the visible child no. index of `node`.
392
Do nothing if node has fewer children than `index`.
394
children = [n for n in node if not isinstance(n, nodes.Invisible)]
396
child = children[index]
399
child['classes'].append(class_)
401
def set_first_last(self, node):
402
self.set_class_on_child(node, 'first', 0)
403
self.set_class_on_child(node, 'last', -1)
405
def visit_Text(self, node):
407
encoded = self.encode(text)
408
if self.in_mailto and self.settings.cloak_email_addresses:
409
encoded = self.cloak_email(encoded)
410
self.body.append(encoded)
412
def depart_Text(self, node):
415
def visit_abbreviation(self, node):
416
# @@@ implementation incomplete ("title" attribute)
417
self.body.append(self.starttag(node, 'abbr', ''))
419
def depart_abbreviation(self, node):
420
self.body.append('</abbr>')
422
def visit_acronym(self, node):
423
# @@@ implementation incomplete ("title" attribute)
424
self.body.append(self.starttag(node, 'acronym', ''))
426
def depart_acronym(self, node):
427
self.body.append('</acronym>')
429
def visit_address(self, node):
430
self.visit_docinfo_item(node, 'address', meta=None)
431
self.body.append(self.starttag(node, 'pre', CLASS='address'))
433
def depart_address(self, node):
434
self.body.append('\n</pre>\n')
435
self.depart_docinfo_item()
437
def visit_admonition(self, node, name=''):
438
self.body.append(self.start_tag_with_title(
439
node, 'div', CLASS=(name or 'admonition')))
441
node.insert(0, nodes.title(name, self.language.labels[name]))
442
self.set_first_last(node)
444
def depart_admonition(self, node=None):
445
self.body.append('</div>\n')
447
def visit_attention(self, node):
448
self.visit_admonition(node, 'attention')
450
def depart_attention(self, node):
451
self.depart_admonition()
453
attribution_formats = {'dash': ('—', ''),
454
'parentheses': ('(', ')'),
455
'parens': ('(', ')'),
458
def visit_attribution(self, node):
459
prefix, suffix = self.attribution_formats[self.settings.attribution]
460
self.context.append(suffix)
462
self.starttag(node, 'p', prefix, CLASS='attribution'))
464
def depart_attribution(self, node):
465
self.body.append(self.context.pop() + '</p>\n')
467
def visit_author(self, node):
468
if isinstance(node.parent, nodes.authors):
469
if self.author_in_authors:
470
self.body.append('\n<br />')
472
self.visit_docinfo_item(node, 'author')
474
def depart_author(self, node):
475
if isinstance(node.parent, nodes.authors):
476
self.author_in_authors += 1
478
self.depart_docinfo_item()
480
def visit_authors(self, node):
481
self.visit_docinfo_item(node, 'authors')
482
self.author_in_authors = 0 # initialize counter
484
def depart_authors(self, node):
485
self.depart_docinfo_item()
486
self.author_in_authors = None
488
def visit_block_quote(self, node):
489
self.body.append(self.starttag(node, 'blockquote'))
491
def depart_block_quote(self, node):
492
self.body.append('</blockquote>\n')
494
def check_simple_list(self, node):
495
"""Check for a simple list that can be rendered compactly."""
496
visitor = SimpleListChecker(self.document)
499
except nodes.NodeFound:
504
def is_compactable(self, node):
505
return ('compact' in node['classes']
506
or (self.settings.compact_lists
507
and 'open' not in node['classes']
508
and (self.compact_simple
509
or self.topic_classes == ['contents']
510
or self.check_simple_list(node))))
512
def visit_bullet_list(self, node):
514
old_compact_simple = self.compact_simple
515
self.context.append((self.compact_simple, self.compact_p))
516
self.compact_p = None
517
self.compact_simple = self.is_compactable(node)
518
if self.compact_simple and not old_compact_simple:
519
atts['class'] = 'simple'
520
self.body.append(self.starttag(node, 'ul', **atts))
522
def depart_bullet_list(self, node):
523
self.compact_simple, self.compact_p = self.context.pop()
524
self.body.append('</ul>\n')
526
def visit_caption(self, node):
527
self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
529
def depart_caption(self, node):
530
self.body.append('</p>\n')
532
def visit_caution(self, node):
533
self.visit_admonition(node, 'caution')
535
def depart_caution(self, node):
536
self.depart_admonition()
538
def visit_citation(self, node):
539
self.body.append(self.starttag(node, 'table',
540
CLASS='docutils citation',
541
frame="void", rules="none"))
542
self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
543
'<tbody valign="top">\n'
545
self.footnote_backrefs(node)
547
def depart_citation(self, node):
548
self.body.append('</td></tr>\n'
549
'</tbody>\n</table>\n')
551
def visit_citation_reference(self, node):
552
href = '#' + node['refid']
553
self.body.append(self.starttag(
554
node, 'a', '[', CLASS='citation-reference', href=href))
556
def depart_citation_reference(self, node):
557
self.body.append(']</a>')
559
def visit_classifier(self, node):
560
self.body.append(' <span class="classifier-delimiter">:</span> ')
561
self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
563
def depart_classifier(self, node):
564
self.body.append('</span>')
566
def visit_colspec(self, node):
567
self.colspecs.append(node)
568
# "stubs" list is an attribute of the tgroup element:
569
node.parent.stubs.append(node.attributes.get('stub'))
571
def depart_colspec(self, node):
574
def write_colspecs(self):
576
for node in self.colspecs:
577
width += node['colwidth']
578
for node in self.colspecs:
579
colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
580
self.body.append(self.emptytag(node, 'col',
581
width='%i%%' % colwidth))
584
def visit_comment(self, node,
585
sub=re.compile('-(?=-)').sub):
586
"""Escape double-dashes in comment text."""
587
self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
588
# Content already processed:
591
def visit_compound(self, node):
592
self.body.append(self.starttag(node, 'div', CLASS='compound'))
594
node[0]['classes'].append('compound-first')
595
node[-1]['classes'].append('compound-last')
596
for child in node[1:-1]:
597
child['classes'].append('compound-middle')
599
def depart_compound(self, node):
600
self.body.append('</div>\n')
602
def visit_container(self, node):
603
self.body.append(self.starttag(node, 'div', CLASS='container'))
605
def depart_container(self, node):
606
self.body.append('</div>\n')
608
def visit_contact(self, node):
609
self.visit_docinfo_item(node, 'contact', meta=None)
611
def depart_contact(self, node):
612
self.depart_docinfo_item()
614
def visit_copyright(self, node):
615
self.visit_docinfo_item(node, 'copyright')
617
def depart_copyright(self, node):
618
self.depart_docinfo_item()
620
def visit_danger(self, node):
621
self.visit_admonition(node, 'danger')
623
def depart_danger(self, node):
624
self.depart_admonition()
626
def visit_date(self, node):
627
self.visit_docinfo_item(node, 'date')
629
def depart_date(self, node):
630
self.depart_docinfo_item()
632
def visit_decoration(self, node):
635
def depart_decoration(self, node):
638
def visit_definition(self, node):
639
self.body.append('</dt>\n')
640
self.body.append(self.starttag(node, 'dd', ''))
641
self.set_first_last(node)
643
def depart_definition(self, node):
644
self.body.append('</dd>\n')
646
def visit_definition_list(self, node):
647
self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
649
def depart_definition_list(self, node):
650
self.body.append('</dl>\n')
652
def visit_definition_list_item(self, node):
655
def depart_definition_list_item(self, node):
658
def visit_description(self, node):
659
self.body.append(self.starttag(node, 'td', ''))
660
self.set_first_last(node)
662
def depart_description(self, node):
663
self.body.append('</td>')
665
def visit_docinfo(self, node):
666
self.context.append(len(self.body))
667
self.body.append(self.starttag(node, 'table',
669
frame="void", rules="none"))
670
self.body.append('<col class="docinfo-name" />\n'
671
'<col class="docinfo-content" />\n'
672
'<tbody valign="top">\n')
675
def depart_docinfo(self, node):
676
self.body.append('</tbody>\n</table>\n')
677
self.in_docinfo = None
678
start = self.context.pop()
679
self.docinfo = self.body[start:]
682
def visit_docinfo_item(self, node, name, meta=1):
684
meta_tag = '<meta name="%s" content="%s" />\n' \
685
% (name, self.attval(node.astext()))
686
self.add_meta(meta_tag)
687
self.body.append(self.starttag(node, 'tr', ''))
688
self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
689
% self.language.labels[name])
691
if isinstance(node[0], nodes.Element):
692
node[0]['classes'].append('first')
693
if isinstance(node[-1], nodes.Element):
694
node[-1]['classes'].append('last')
696
def depart_docinfo_item(self):
697
self.body.append('</td></tr>\n')
699
def visit_doctest_block(self, node):
700
self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
702
def depart_doctest_block(self, node):
703
self.body.append('\n</pre>\n')
705
def visit_document(self, node):
706
self.head.append('<title>%s</title>\n'
707
% self.encode(node.get('title', '')))
709
def depart_document(self, node):
710
self.fragment.extend(self.body)
711
self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
712
self.body_suffix.insert(0, '</div>\n')
713
# skip content-type meta tag with interpolated charset value:
714
self.html_head.extend(self.head[1:])
715
self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
716
+ self.docinfo + self.body
717
+ self.body_suffix[:-1])
719
def visit_emphasis(self, node):
720
self.body.append('<em>')
722
def depart_emphasis(self, node):
723
self.body.append('</em>')
725
def visit_entry(self, node):
727
if isinstance(node.parent.parent, nodes.thead):
728
atts['class'].append('head')
729
if node.parent.parent.parent.stubs[node.parent.column]:
730
# "stubs" list is an attribute of the tgroup element
731
atts['class'].append('stub')
734
atts['class'] = ' '.join(atts['class'])
738
node.parent.column += 1
739
if node.has_key('morerows'):
740
atts['rowspan'] = node['morerows'] + 1
741
if node.has_key('morecols'):
742
atts['colspan'] = node['morecols'] + 1
743
node.parent.column += node['morecols']
744
self.body.append(self.starttag(node, tagname, '', **atts))
745
self.context.append('</%s>\n' % tagname.lower())
746
if len(node) == 0: # empty cell
747
self.body.append(' ')
748
self.set_first_last(node)
750
def depart_entry(self, node):
751
self.body.append(self.context.pop())
753
def visit_enumerated_list(self, node):
755
The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
756
CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
760
if node.has_key('start'):
761
atts['start'] = node['start']
762
if node.has_key('enumtype'):
763
atts['class'] = node['enumtype']
764
# @@@ To do: prefix, suffix. How? Change prefix/suffix to a
765
# single "format" attribute? Use CSS2?
766
old_compact_simple = self.compact_simple
767
self.context.append((self.compact_simple, self.compact_p))
768
self.compact_p = None
769
self.compact_simple = self.is_compactable(node)
770
if self.compact_simple and not old_compact_simple:
771
atts['class'] = (atts.get('class', '') + ' simple').strip()
772
self.body.append(self.starttag(node, 'ol', **atts))
774
def depart_enumerated_list(self, node):
775
self.compact_simple, self.compact_p = self.context.pop()
776
self.body.append('</ol>\n')
778
def visit_error(self, node):
779
self.visit_admonition(node, 'error')
781
def depart_error(self, node):
782
self.depart_admonition()
784
def visit_field(self, node):
785
self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
787
def depart_field(self, node):
788
self.body.append('</tr>\n')
790
def visit_field_body(self, node):
791
self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
792
self.set_class_on_child(node, 'first', 0)
794
if (self.compact_field_list or
795
isinstance(field.parent, nodes.docinfo) or
796
field.parent.index(field) == len(field.parent) - 1):
797
# If we are in a compact list, the docinfo, or if this is
798
# the last field of the field list, do not add vertical
799
# space after last element.
800
self.set_class_on_child(node, 'last', -1)
802
def depart_field_body(self, node):
803
self.body.append('</td>\n')
805
def visit_field_list(self, node):
806
self.context.append((self.compact_field_list, self.compact_p))
807
self.compact_p = None
808
if 'compact' in node['classes']:
809
self.compact_field_list = 1
810
elif (self.settings.compact_field_lists
811
and 'open' not in node['classes']):
812
self.compact_field_list = 1
813
if self.compact_field_list:
815
field_body = field[-1]
816
assert isinstance(field_body, nodes.field_body)
817
children = [n for n in field_body
818
if not isinstance(n, nodes.Invisible)]
819
if not (len(children) == 0 or
820
len(children) == 1 and
821
isinstance(children[0], nodes.paragraph)):
822
self.compact_field_list = 0
824
self.body.append(self.starttag(node, 'table', frame='void',
826
CLASS='docutils field-list'))
827
self.body.append('<col class="field-name" />\n'
828
'<col class="field-body" />\n'
829
'<tbody valign="top">\n')
831
def depart_field_list(self, node):
832
self.body.append('</tbody>\n</table>\n')
833
self.compact_field_list, self.compact_p = self.context.pop()
835
def visit_field_name(self, node):
838
atts['class'] = 'docinfo-name'
840
atts['class'] = 'field-name'
841
if ( self.settings.field_name_limit
842
and len(node.astext()) > self.settings.field_name_limit):
844
self.context.append('</tr>\n<tr><td> </td>')
846
self.context.append('')
847
self.body.append(self.starttag(node, 'th', '', **atts))
849
def depart_field_name(self, node):
850
self.body.append(':</th>')
851
self.body.append(self.context.pop())
853
def visit_figure(self, node):
854
atts = {'class': 'figure'}
855
if node.get('width'):
856
atts['style'] = 'width: %spx' % node['width']
857
if node.get('align'):
858
atts['align'] = node['align']
859
self.body.append(self.starttag(node, 'div', **atts))
861
def depart_figure(self, node):
862
self.body.append('</div>\n')
864
def visit_footer(self, node):
865
self.context.append(len(self.body))
867
def depart_footer(self, node):
868
start = self.context.pop()
869
footer = [self.starttag(node, 'div', CLASS='footer'),
870
'<hr class="footer" />\n']
871
footer.extend(self.body[start:])
872
footer.append('\n</div>\n')
873
self.footer.extend(footer)
874
self.body_suffix[:0] = footer
875
del self.body[start:]
877
def visit_footnote(self, node):
878
self.body.append(self.starttag(node, 'table',
879
CLASS='docutils footnote',
880
frame="void", rules="none"))
881
self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
882
'<tbody valign="top">\n'
884
self.footnote_backrefs(node)
886
def footnote_backrefs(self, node):
888
backrefs = node['backrefs']
889
if self.settings.footnote_backlinks and backrefs:
890
if len(backrefs) == 1:
891
self.context.append('')
893
'<a class="fn-backref" href="#%s" name="%s">'
894
% (backrefs[0], node['ids'][0]))
897
for backref in backrefs:
898
backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
901
self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
902
self.context.append('<a name="%s">' % node['ids'][0])
904
self.context.append('')
905
self.context.append('<a name="%s">' % node['ids'][0])
906
# If the node does not only consist of a label.
908
# If there are preceding backlinks, we do not set class
909
# 'first', because we need to retain the top-margin.
911
node[1]['classes'].append('first')
912
node[-1]['classes'].append('last')
914
def depart_footnote(self, node):
915
self.body.append('</td></tr>\n'
916
'</tbody>\n</table>\n')
918
def visit_footnote_reference(self, node):
919
href = '#' + node['refid']
920
format = self.settings.footnote_references
921
if format == 'brackets':
923
self.context.append(']')
925
assert format == 'superscript'
927
self.context.append('</sup>')
928
self.body.append(self.starttag(node, 'a', suffix,
929
CLASS='footnote-reference', href=href))
931
def depart_footnote_reference(self, node):
932
self.body.append(self.context.pop() + '</a>')
934
def visit_generated(self, node):
937
def depart_generated(self, node):
940
def visit_header(self, node):
941
self.context.append(len(self.body))
943
def depart_header(self, node):
944
start = self.context.pop()
945
header = [self.starttag(node, 'div', CLASS='header')]
946
header.extend(self.body[start:])
947
header.append('\n<hr class="header"/>\n</div>\n')
948
self.body_prefix.extend(header)
949
self.header.extend(header)
950
del self.body[start:]
952
def visit_hint(self, node):
953
self.visit_admonition(node, 'hint')
955
def depart_hint(self, node):
956
self.depart_admonition()
958
def visit_image(self, node):
960
atts['src'] = node['uri']
961
if node.has_key('width'):
962
atts['width'] = node['width']
963
if node.has_key('height'):
964
atts['height'] = node['height']
965
if node.has_key('scale'):
966
if Image and not (node.has_key('width')
967
and node.has_key('height')):
969
im = Image.open(str(atts['src']))
970
except (IOError, # Source image can't be found or opened
971
UnicodeError): # PIL doesn't like Unicode paths.
974
if not atts.has_key('width'):
975
atts['width'] = str(im.size[0])
976
if not atts.has_key('height'):
977
atts['height'] = str(im.size[1])
979
for att_name in 'width', 'height':
980
if atts.has_key(att_name):
981
match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
983
atts[att_name] = '%s%s' % (
984
float(match.group(1)) * (float(node['scale']) / 100),
987
for att_name in 'width', 'height':
988
if atts.has_key(att_name):
989
if re.match(r'^[0-9.]+$', atts[att_name]):
990
# Interpret unitless values as pixels.
991
atts[att_name] += 'px'
992
style.append('%s: %s;' % (att_name, atts[att_name]))
995
atts['style'] = ' '.join(style)
996
atts['alt'] = node.get('alt', atts['src'])
997
if (isinstance(node.parent, nodes.TextElement) or
998
(isinstance(node.parent, nodes.reference) and
999
not isinstance(node.parent.parent, nodes.TextElement))):
1000
# Inline context or surrounded by <a>...</a>.
1004
if node.has_key('align'):
1005
if node['align'] == 'center':
1006
# "align" attribute is set in surrounding "div" element.
1007
self.body.append('<div align="center" class="align-center">')
1008
self.context.append('</div>\n')
1011
# "align" attribute is set in "img" element.
1012
atts['align'] = node['align']
1013
self.context.append('')
1014
atts['class'] = 'align-%s' % node['align']
1016
self.context.append('')
1017
self.body.append(self.emptytag(node, 'img', suffix, **atts))
1019
def depart_image(self, node):
1020
self.body.append(self.context.pop())
1022
def visit_important(self, node):
1023
self.visit_admonition(node, 'important')
1025
def depart_important(self, node):
1026
self.depart_admonition()
1028
def visit_inline(self, node):
1029
self.body.append(self.starttag(node, 'span', ''))
1031
def depart_inline(self, node):
1032
self.body.append('</span>')
1034
def visit_label(self, node):
1035
self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1038
def depart_label(self, node):
1039
self.body.append(']</a></td><td>%s' % self.context.pop())
1041
def visit_legend(self, node):
1042
self.body.append(self.starttag(node, 'div', CLASS='legend'))
1044
def depart_legend(self, node):
1045
self.body.append('</div>\n')
1047
def visit_line(self, node):
1048
self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1050
self.body.append('<br />')
1052
def depart_line(self, node):
1053
self.body.append('</div>\n')
1055
def visit_line_block(self, node):
1056
self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1058
def depart_line_block(self, node):
1059
self.body.append('</div>\n')
1061
def visit_list_item(self, node):
1062
self.body.append(self.starttag(node, 'li', ''))
1064
node[0]['classes'].append('first')
1066
def depart_list_item(self, node):
1067
self.body.append('</li>\n')
1069
def visit_literal(self, node):
1070
"""Process text to prevent tokens from wrapping."""
1072
self.starttag(node, 'tt', '', CLASS='docutils literal'))
1073
text = node.astext()
1074
for token in self.words_and_spaces.findall(text):
1076
# Protect text like "--an-option" from bad line wrapping:
1077
self.body.append('<span class="pre">%s</span>'
1078
% self.encode(token))
1079
elif token in ('\n', ' '):
1080
# Allow breaks at whitespace:
1081
self.body.append(token)
1083
# Protect runs of multiple spaces; the last space can wrap:
1084
self.body.append(' ' * (len(token) - 1) + ' ')
1085
self.body.append('</tt>')
1086
# Content already processed:
1087
raise nodes.SkipNode
1089
def visit_literal_block(self, node):
1090
self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1092
def depart_literal_block(self, node):
1093
self.body.append('\n</pre>\n')
1095
def visit_meta(self, node):
1096
meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1099
def depart_meta(self, node):
1102
def add_meta(self, tag):
1103
self.meta.append(tag)
1104
self.head.append(tag)
1106
def visit_note(self, node):
1107
self.visit_admonition(node, 'note')
1109
def depart_note(self, node):
1110
self.depart_admonition()
1112
def visit_option(self, node):
1113
if self.context[-1]:
1114
self.body.append(', ')
1115
self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1117
def depart_option(self, node):
1118
self.body.append('</span>')
1119
self.context[-1] += 1
1121
def visit_option_argument(self, node):
1122
self.body.append(node.get('delimiter', ' '))
1123
self.body.append(self.starttag(node, 'var', ''))
1125
def depart_option_argument(self, node):
1126
self.body.append('</var>')
1128
def visit_option_group(self, node):
1130
if ( self.settings.option_limit
1131
and len(node.astext()) > self.settings.option_limit):
1133
self.context.append('</tr>\n<tr><td> </td>')
1135
self.context.append('')
1137
self.starttag(node, 'td', CLASS='option-group', **atts))
1138
self.body.append('<kbd>')
1139
self.context.append(0) # count number of options
1141
def depart_option_group(self, node):
1143
self.body.append('</kbd></td>\n')
1144
self.body.append(self.context.pop())
1146
def visit_option_list(self, node):
1148
self.starttag(node, 'table', CLASS='docutils option-list',
1149
frame="void", rules="none"))
1150
self.body.append('<col class="option" />\n'
1151
'<col class="description" />\n'
1152
'<tbody valign="top">\n')
1154
def depart_option_list(self, node):
1155
self.body.append('</tbody>\n</table>\n')
1157
def visit_option_list_item(self, node):
1158
self.body.append(self.starttag(node, 'tr', ''))
1160
def depart_option_list_item(self, node):
1161
self.body.append('</tr>\n')
1163
def visit_option_string(self, node):
1166
def depart_option_string(self, node):
1169
def visit_organization(self, node):
1170
self.visit_docinfo_item(node, 'organization')
1172
def depart_organization(self, node):
1173
self.depart_docinfo_item()
1175
def should_be_compact_paragraph(self, node):
1177
Determine if the <p> tags around paragraph ``node`` can be omitted.
1179
if (isinstance(node.parent, nodes.document) or
1180
isinstance(node.parent, nodes.compound)):
1181
# Never compact paragraphs in document or compound.
1183
for key, value in node.attlist():
1184
if (node.is_not_default(key) and
1185
not (key == 'classes' and value in
1186
([], ['first'], ['last'], ['first', 'last']))):
1187
# Attribute which needs to survive.
1189
first = isinstance(node.parent[0], nodes.label) # skip label
1190
for child in node.parent.children[first:]:
1191
# only first paragraph can be compact
1192
if isinstance(child, nodes.Invisible):
1197
if ( self.compact_simple
1198
or self.compact_field_list
1200
and (len(node.parent) == 1
1201
or len(node.parent) == 2
1202
and isinstance(node.parent[0], nodes.label)))):
1206
def visit_paragraph(self, node):
1207
if self.should_be_compact_paragraph(node):
1208
self.context.append('')
1210
self.body.append(self.starttag(node, 'p', ''))
1211
self.context.append('</p>\n')
1213
def depart_paragraph(self, node):
1214
self.body.append(self.context.pop())
1216
def visit_problematic(self, node):
1217
if node.hasattr('refid'):
1218
self.body.append('<a href="#%s" name="%s">' % (node['refid'],
1220
self.context.append('</a>')
1222
self.context.append('')
1223
self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1225
def depart_problematic(self, node):
1226
self.body.append('</span>')
1227
self.body.append(self.context.pop())
1229
def visit_raw(self, node):
1230
if 'html' in node.get('format', '').split():
1231
t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1233
self.body.append(self.starttag(node, t, suffix=''))
1234
self.body.append(node.astext())
1236
self.body.append('</%s>' % t)
1237
# Keep non-HTML raw text out of output:
1238
raise nodes.SkipNode
1240
def visit_reference(self, node):
1241
if node.has_key('refuri'):
1242
href = node['refuri']
1243
if ( self.settings.cloak_email_addresses
1244
and href.startswith('mailto:')):
1245
href = self.cloak_mailto(href)
1248
assert node.has_key('refid'), \
1249
'References must have "refuri" or "refid" attribute.'
1250
href = '#' + node['refid']
1251
atts = {'href': href, 'class': 'reference'}
1252
if not isinstance(node.parent, nodes.TextElement):
1253
assert len(node) == 1 and isinstance(node[0], nodes.image)
1254
atts['class'] += ' image-reference'
1255
self.body.append(self.starttag(node, 'a', '', **atts))
1257
def depart_reference(self, node):
1258
self.body.append('</a>')
1259
if not isinstance(node.parent, nodes.TextElement):
1260
self.body.append('\n')
1263
def visit_revision(self, node):
1264
self.visit_docinfo_item(node, 'revision', meta=None)
1266
def depart_revision(self, node):
1267
self.depart_docinfo_item()
1269
def visit_row(self, node):
1270
self.body.append(self.starttag(node, 'tr', ''))
1273
def depart_row(self, node):
1274
self.body.append('</tr>\n')
1276
def visit_rubric(self, node):
1277
self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1279
def depart_rubric(self, node):
1280
self.body.append('</p>\n')
1282
def visit_section(self, node):
1283
self.section_level += 1
1285
self.start_tag_with_title(node, 'div', CLASS='section'))
1287
def depart_section(self, node):
1288
self.section_level -= 1
1289
self.body.append('</div>\n')
1291
def visit_sidebar(self, node):
1293
self.start_tag_with_title(node, 'div', CLASS='sidebar'))
1294
self.set_first_last(node)
1297
def depart_sidebar(self, node):
1298
self.body.append('</div>\n')
1299
self.in_sidebar = None
1301
def visit_status(self, node):
1302
self.visit_docinfo_item(node, 'status', meta=None)
1304
def depart_status(self, node):
1305
self.depart_docinfo_item()
1307
def visit_strong(self, node):
1308
self.body.append('<strong>')
1310
def depart_strong(self, node):
1311
self.body.append('</strong>')
1313
def visit_subscript(self, node):
1314
self.body.append(self.starttag(node, 'sub', ''))
1316
def depart_subscript(self, node):
1317
self.body.append('</sub>')
1319
def visit_substitution_definition(self, node):
1320
"""Internal only."""
1321
raise nodes.SkipNode
1323
def visit_substitution_reference(self, node):
1324
self.unimplemented_visit(node)
1326
def visit_subtitle(self, node):
1327
if isinstance(node.parent, nodes.sidebar):
1328
self.body.append(self.starttag(node, 'p', '',
1329
CLASS='sidebar-subtitle'))
1330
self.context.append('</p>\n')
1331
elif isinstance(node.parent, nodes.document):
1332
self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1333
self.context.append('</h2>\n')
1334
self.in_document_title = len(self.body)
1335
elif isinstance(node.parent, nodes.section):
1336
tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1338
self.starttag(node, tag, '', CLASS='section-subtitle') +
1339
self.starttag({}, 'span', '', CLASS='section-subtitle'))
1340
self.context.append('</span></%s>\n' % tag)
1342
def depart_subtitle(self, node):
1343
self.body.append(self.context.pop())
1344
if self.in_document_title:
1345
self.subtitle = self.body[self.in_document_title:-1]
1346
self.in_document_title = 0
1347
self.body_pre_docinfo.extend(self.body)
1348
self.html_subtitle.extend(self.body)
1351
def visit_superscript(self, node):
1352
self.body.append(self.starttag(node, 'sup', ''))
1354
def depart_superscript(self, node):
1355
self.body.append('</sup>')
1357
def visit_system_message(self, node):
1358
self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1359
self.body.append('<p class="system-message-title">')
1363
attr['name'] = node['ids'][0]
1364
if len(node['backrefs']):
1365
backrefs = node['backrefs']
1366
if len(backrefs) == 1:
1367
backref_text = ('; <em><a href="#%s">backlink</a></em>'
1372
for backref in backrefs:
1373
backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1375
backref_text = ('; <em>backlinks: %s</em>'
1376
% ', '.join(backlinks))
1377
if node.hasattr('line'):
1378
line = ', line %s' % node['line']
1382
a_start = self.starttag({}, 'a', '', **attr)
1385
a_start = a_end = ''
1386
self.body.append('System Message: %s%s/%s%s '
1387
'(<tt class="docutils">%s</tt>%s)%s</p>\n'
1388
% (a_start, node['type'], node['level'], a_end,
1389
self.encode(node['source']), line, backref_text))
1391
def depart_system_message(self, node):
1392
self.body.append('</div>\n')
1394
def visit_table(self, node):
1396
self.starttag(node, 'table', CLASS='docutils', border="1"))
1398
def depart_table(self, node):
1399
self.body.append('</table>\n')
1401
def visit_target(self, node):
1402
if not (node.has_key('refuri') or node.has_key('refid')
1403
or node.has_key('refname')):
1404
self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1405
self.context.append('</span>')
1407
self.context.append('')
1409
def depart_target(self, node):
1410
self.body.append(self.context.pop())
1412
def visit_tbody(self, node):
1413
self.write_colspecs()
1414
self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1415
self.body.append(self.starttag(node, 'tbody', valign='top'))
1417
def depart_tbody(self, node):
1418
self.body.append('</tbody>\n')
1420
def visit_term(self, node):
1421
self.body.append(self.starttag(node, 'dt', ''))
1423
def depart_term(self, node):
1425
Leave the end tag to `self.visit_definition()`, in case there's a
1430
def visit_tgroup(self, node):
1431
# Mozilla needs <colgroup>:
1432
self.body.append(self.starttag(node, 'colgroup'))
1433
# Appended by thead or tbody:
1434
self.context.append('</colgroup>\n')
1437
def depart_tgroup(self, node):
1440
def visit_thead(self, node):
1441
self.write_colspecs()
1442
self.body.append(self.context.pop()) # '</colgroup>\n'
1443
# There may or may not be a <thead>; this is for <tbody> to use:
1444
self.context.append('')
1445
self.body.append(self.starttag(node, 'thead', valign='bottom'))
1447
def depart_thead(self, node):
1448
self.body.append('</thead>\n')
1450
def visit_tip(self, node):
1451
self.visit_admonition(node, 'tip')
1453
def depart_tip(self, node):
1454
self.depart_admonition()
1456
def visit_title(self, node, move_ids=1):
1457
"""Only 6 section levels are supported by HTML."""
1459
close_tag = '</p>\n'
1460
if isinstance(node.parent, nodes.topic):
1462
self.starttag(node, 'p', '', CLASS='topic-title first'))
1464
elif isinstance(node.parent, nodes.sidebar):
1466
self.starttag(node, 'p', '', CLASS='sidebar-title'))
1468
elif isinstance(node.parent, nodes.Admonition):
1470
self.starttag(node, 'p', '', CLASS='admonition-title'))
1472
elif isinstance(node.parent, nodes.table):
1474
self.starttag(node, 'caption', ''))
1476
close_tag = '</caption>\n'
1477
elif isinstance(node.parent, nodes.document):
1478
self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1479
self.context.append('</h1>\n')
1480
self.in_document_title = len(self.body)
1482
assert isinstance(node.parent, nodes.section)
1483
h_level = self.section_level + self.initial_header_level - 1
1485
if (len(node.parent) >= 2 and
1486
isinstance(node.parent[1], nodes.subtitle)):
1487
atts['CLASS'] = 'with-subtitle'
1489
self.starttag(node, 'h%s' % h_level, '', **atts))
1491
# !!! conditional to be removed in Docutils 0.5:
1493
if node.parent['ids']:
1494
atts['ids'] = node.parent['ids']
1495
if node.hasattr('refid'):
1496
atts['class'] = 'toc-backref'
1497
atts['href'] = '#' + node['refid']
1499
self.body.append(self.starttag({}, 'a', '', **atts))
1500
self.context.append('</a></h%s>\n' % (h_level))
1502
self.context.append('</h%s>\n' % (h_level))
1503
# !!! conditional to be removed in Docutils 0.5:
1505
if node.parent['ids']:
1506
atts={'ids': node.parent['ids']}
1508
self.starttag({}, 'a', '', **atts))
1509
self.context.append('</a>' + close_tag)
1511
self.context.append(close_tag)
1513
def depart_title(self, node):
1514
self.body.append(self.context.pop())
1515
if self.in_document_title:
1516
self.title = self.body[self.in_document_title:-1]
1517
self.in_document_title = 0
1518
self.body_pre_docinfo.extend(self.body)
1519
self.html_title.extend(self.body)
1522
def visit_title_reference(self, node):
1523
self.body.append(self.starttag(node, 'cite', ''))
1525
def depart_title_reference(self, node):
1526
self.body.append('</cite>')
1528
def visit_topic(self, node):
1529
self.body.append(self.start_tag_with_title(node, 'div', CLASS='topic'))
1530
self.topic_classes = node['classes']
1532
def depart_topic(self, node):
1533
self.body.append('</div>\n')
1534
self.topic_classes = []
1536
def visit_transition(self, node):
1537
self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1539
def depart_transition(self, node):
1542
def visit_version(self, node):
1543
self.visit_docinfo_item(node, 'version', meta=None)
1545
def depart_version(self, node):
1546
self.depart_docinfo_item()
1548
def visit_warning(self, node):
1549
self.visit_admonition(node, 'warning')
1551
def depart_warning(self, node):
1552
self.depart_admonition()
1554
def unimplemented_visit(self, node):
1555
raise NotImplementedError('visiting unimplemented node type: %s'
1556
% node.__class__.__name__)
1559
class SimpleListChecker(nodes.GenericNodeVisitor):
1562
Raise `nodes.NodeFound` if non-simple list item is encountered.
1564
Here "simple" means a list item containing nothing other than a single
1565
paragraph, a simple list, or a paragraph followed by a simple list.
1568
def default_visit(self, node):
1569
raise nodes.NodeFound
1571
def visit_bullet_list(self, node):
1574
def visit_enumerated_list(self, node):
1577
def visit_list_item(self, node):
1579
for child in node.children:
1580
if not isinstance(child, nodes.Invisible):
1581
children.append(child)
1582
if (children and isinstance(children[0], nodes.paragraph)
1583
and (isinstance(children[-1], nodes.bullet_list)
1584
or isinstance(children[-1], nodes.enumerated_list))):
1586
if len(children) <= 1:
1589
raise nodes.NodeFound
1591
def visit_paragraph(self, node):
1592
raise nodes.SkipNode
1594
def invisible_visit(self, node):
1595
"""Invisible nodes should be ignored."""
1596
raise nodes.SkipNode
1598
visit_comment = invisible_visit
1599
visit_substitution_definition = invisible_visit
1600
visit_target = invisible_visit
1601
visit_pending = invisible_visit