1
# Author: David Goodger
2
# Contact: goodger@users.sourceforge.net
3
# Revision: $Revision: 1.109 $
4
# Date: $Date: 2004/05/08 22:47:28 $
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 HTML 4.01 Transitional DTD and to the Extensible
11
HTML version 1.0 Transitional DTD (*almost* strict). The output contains a
12
minimum of formatting information. A cascading style sheet ("default.css" by
13
default) is required 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."""
39
'HTML-Specific Options',
41
(('Specify a stylesheet URL, used verbatim. Default is '
42
'"default.css". Overridden by --stylesheet-path.',
44
{'default': 'default.css', 'metavar': '<URL>'}),
45
('Specify a stylesheet file, relative to the current working '
46
'directory. The path is adjusted relative to the output HTML '
47
'file. Overrides --stylesheet.',
48
['--stylesheet-path'],
49
{'metavar': '<file>'}),
50
('Link to the stylesheet in the output HTML file. This is the '
52
['--link-stylesheet'],
53
{'dest': 'embed_stylesheet', 'action': 'store_false',
54
'validator': frontend.validate_boolean}),
55
('Embed the stylesheet in the output HTML file. The stylesheet '
56
'file must be accessible during processing (--stylesheet-path is '
57
'recommended). The stylesheet is embedded inside a comment, so it '
58
'must not contain the text "--" (two hyphens). Default: link the '
59
'stylesheet, do not embed it.',
60
['--embed-stylesheet'],
61
{'action': 'store_true', 'validator': frontend.validate_boolean}),
62
('Specify the initial header level. Default is 1 for "<h1>". '
63
'Does not affect document title & subtitle (see --no-doc-title).',
64
['--initial-header-level'],
65
{'choices': '1 2 3 4 5 6'.split(), 'default': '1',
66
'metavar': '<level>'}),
67
('Format for footnote references: one of "superscript" or '
68
'"brackets". Default is "superscript".',
69
['--footnote-references'],
70
{'choices': ['superscript', 'brackets'], 'default': 'superscript',
71
'metavar': '<format>'}),
72
('Format for block quote attributions: one of "dash" (em-dash '
73
'prefix), "parentheses"/"parens", or "none". Default is "dash".',
75
{'choices': ['dash', 'parentheses', 'parens', 'none'],
76
'default': 'dash', 'metavar': '<format>'}),
77
('Remove extra vertical whitespace between items of bullet lists '
78
'and enumerated lists, when list items are "simple" (i.e., all '
79
'items each contain one paragraph and/or one "simple" sublist '
80
'only). Default: enabled.',
82
{'default': 1, 'action': 'store_true',
83
'validator': frontend.validate_boolean}),
84
('Disable compact simple bullet and enumerated lists.',
85
['--no-compact-lists'],
86
{'dest': 'compact_lists', 'action': 'store_false'}),
87
('Omit the XML declaration. Use with caution.',
88
['--no-xml-declaration'],
89
{'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
90
'validator': frontend.validate_boolean}),))
92
relative_path_settings = ('stylesheet_path',)
94
config_section = 'html4css1 writer'
95
config_section_dependencies = ('writers',)
98
writers.Writer.__init__(self)
99
self.translator_class = HTMLTranslator
102
visitor = self.translator_class(self.document)
103
self.document.walkabout(visitor)
104
self.output = visitor.astext()
105
self.visitor = visitor
106
for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
107
'body_pre_docinfo', 'docinfo', 'body', 'fragment',
109
setattr(self, attr, getattr(visitor, attr))
111
def assemble_parts(self):
112
writers.Writer.assemble_parts(self)
113
for part in ('title', 'subtitle', 'docinfo', 'body', 'header',
114
'footer', 'meta', 'stylesheet', 'fragment'):
115
self.parts[part] = ''.join(getattr(self.visitor, part))
118
class HTMLTranslator(nodes.NodeVisitor):
121
This HTML writer has been optimized to produce visually compact
122
lists (less vertical whitespace). HTML's mixed content models
123
allow list items to contain "<li><p>body elements</p></li>" or
124
"<li>just text</li>" or even "<li>text<p>and body
125
elements</p>combined</li>", each with different effects. It would
126
be best to stick with strict body elements in list items, but they
127
affect vertical spacing in browsers (although they really
130
Here is an outline of the optimization:
132
- Check for and omit <p> tags in "simple" lists: list items
133
contain either a single paragraph, a nested simple list, or a
134
paragraph followed by a nested simple list. This means that
135
this list can be compact:
140
But this list cannot be compact:
144
This second paragraph forces space between list items.
148
- In non-list contexts, omit <p> tags on a paragraph if that
149
paragraph is the only child of its parent (footnotes & citations
150
are allowed a label first).
152
- Regardless of the above, in definitions, table cells, field bodies,
153
option descriptions, and list items, mark the first child with
154
'class="first"' and the last child with 'class="last"'. The stylesheet
155
sets the margins (top & bottom respectively) to 0 for these elements.
157
The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
158
option) disables list whitespace optimization.
161
xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
162
doctype = ('<!DOCTYPE html'
163
' PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
164
' "http://www.w3.org/TR/xhtml1/DTD/'
165
'xhtml1-transitional.dtd">\n')
166
html_head = ('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="%s" '
167
'lang="%s">\n<head>\n')
168
content_type = ('<meta http-equiv="Content-Type" content="text/html; '
170
generator = ('<meta name="generator" content="Docutils %s: '
171
'http://docutils.sourceforge.net/" />\n')
172
stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
173
embedded_stylesheet = '<style type="text/css"><!--\n\n%s\n--></style>\n'
174
named_tags = {'a': 1, 'applet': 1, 'form': 1, 'frame': 1, 'iframe': 1,
176
words_and_spaces = re.compile(r'\S+| +|\n')
178
def __init__(self, document):
179
nodes.NodeVisitor.__init__(self, document)
180
self.settings = settings = document.settings
181
lcode = settings.language_code
182
self.language = languages.get_language(lcode)
183
self.meta = [self.content_type % settings.output_encoding,
184
self.generator % docutils.__version__]
187
self.html_head % (lcode, lcode)]
188
self.head_prefix.extend(self.meta)
189
if settings.xml_declaration:
190
self.head_prefix.insert(0, self.xml_declaration
191
% settings.output_encoding)
193
if settings.embed_stylesheet:
194
stylesheet = self.get_stylesheet_reference(
195
os.path.join(os.getcwd(), 'dummy'))
196
stylesheet_text = open(stylesheet).read()
197
self.stylesheet = [self.embedded_stylesheet % stylesheet_text]
199
stylesheet = self.get_stylesheet_reference()
201
self.stylesheet = [self.stylesheet_link % stylesheet]
204
self.body_prefix = ['</head>\n<body>\n']
205
# document title, subtitle display
206
self.body_pre_docinfo = []
211
self.body_suffix = ['</body>\n</html>\n']
212
self.section_level = 0
213
self.initial_header_level = int(settings.initial_header_level)
214
# A heterogenous stack used in conjunction with the tree traversal.
215
# Make sure that the pops correspond to the pushes:
217
self.topic_class = ''
220
self.compact_simple = None
221
self.in_docinfo = None
222
self.in_sidebar = None
227
self.in_document_title = 0
229
def get_stylesheet_reference(self, relative_to=None):
230
settings = self.settings
231
if settings.stylesheet_path:
232
if relative_to == None:
233
relative_to = settings._destination
234
return utils.relative_path(relative_to, settings.stylesheet_path)
236
return settings.stylesheet
239
return ''.join(self.head_prefix + self.head
240
+ self.stylesheet + self.body_prefix
241
+ self.body_pre_docinfo + self.docinfo
242
+ self.body + self.body_suffix)
244
def encode(self, text):
245
"""Encode special characters in `text` & return."""
246
# @@@ A codec to do these and all other HTML entities would be nice.
247
text = text.replace("&", "&")
248
text = text.replace("<", "<")
249
text = text.replace('"', """)
250
text = text.replace(">", ">")
251
text = text.replace("@", "@") # may thwart some address harvesters
254
def attval(self, text,
255
whitespace=re.compile('[\n\r\t\v\f]')):
256
"""Cleanse, HTML encode, and return attribute value text."""
257
return self.encode(whitespace.sub(' ', text))
259
def starttag(self, node, tagname, suffix='\n', infix='', **attributes):
261
Construct and return a start tag given a node (id & class attributes
262
are extracted), tag name, and optional attributes.
264
tagname = tagname.lower()
266
for (name, value) in attributes.items():
267
atts[name.lower()] = value
268
for att in ('class',): # append to node attribute
269
if node.has_key(att) or atts.has_key(att):
271
(node.get(att, '') + ' ' + atts.get(att, '')).strip()
272
for att in ('id',): # node attribute overrides
273
if node.has_key(att):
274
atts[att] = node[att]
275
if atts.has_key('id') and self.named_tags.has_key(tagname):
276
atts['name'] = atts['id'] # for compatibility with old browsers
277
attlist = atts.items()
280
for name, value in attlist:
281
if value is None: # boolean attribute
282
# According to the HTML spec, ``<element boolean>`` is good,
283
# ``<element boolean="boolean">`` is bad.
284
# (But the XHTML (XML) spec says the opposite. <sigh>)
285
parts.append(name.lower())
286
elif isinstance(value, ListType):
287
values = [unicode(v) for v in value]
288
parts.append('%s="%s"' % (name.lower(),
289
self.attval(' '.join(values))))
292
uval = unicode(value)
293
except TypeError: # for Python 2.1 compatibility:
294
uval = unicode(str(value))
295
parts.append('%s="%s"' % (name.lower(), self.attval(uval)))
296
return '<%s%s>%s' % (' '.join(parts), infix, suffix)
298
def emptytag(self, node, tagname, suffix='\n', **attributes):
299
"""Construct and return an XML-compatible empty tag."""
300
return self.starttag(node, tagname, suffix, infix=' /', **attributes)
302
def visit_Text(self, node):
303
self.body.append(self.encode(node.astext()))
305
def depart_Text(self, node):
308
def visit_abbreviation(self, node):
309
# @@@ implementation incomplete ("title" attribute)
310
self.body.append(self.starttag(node, 'abbr', ''))
312
def depart_abbreviation(self, node):
313
self.body.append('</abbr>')
315
def visit_acronym(self, node):
316
# @@@ implementation incomplete ("title" attribute)
317
self.body.append(self.starttag(node, 'acronym', ''))
319
def depart_acronym(self, node):
320
self.body.append('</acronym>')
322
def visit_address(self, node):
323
self.visit_docinfo_item(node, 'address', meta=None)
324
self.body.append(self.starttag(node, 'pre', CLASS='address'))
326
def depart_address(self, node):
327
self.body.append('\n</pre>\n')
328
self.depart_docinfo_item()
330
def visit_admonition(self, node, name=''):
331
self.body.append(self.starttag(node, 'div',
332
CLASS=(name or 'admonition')))
334
self.body.append('<p class="admonition-title first">'
335
+ self.language.labels[name] + '</p>\n')
337
def depart_admonition(self, node=None):
338
self.body.append('</div>\n')
340
def visit_attention(self, node):
341
self.visit_admonition(node, 'attention')
343
def depart_attention(self, node):
344
self.depart_admonition()
346
attribution_formats = {'dash': ('—', ''),
347
'parentheses': ('(', ')'),
348
'parens': ('(', ')'),
351
def visit_attribution(self, node):
352
prefix, suffix = self.attribution_formats[self.settings.attribution]
353
self.context.append(suffix)
355
self.starttag(node, 'p', prefix, CLASS='attribution'))
357
def depart_attribution(self, node):
358
self.body.append(self.context.pop() + '</p>\n')
360
def visit_author(self, node):
361
self.visit_docinfo_item(node, 'author')
363
def depart_author(self, node):
364
self.depart_docinfo_item()
366
def visit_authors(self, node):
369
def depart_authors(self, node):
372
def visit_block_quote(self, node):
373
self.body.append(self.starttag(node, 'blockquote'))
375
def depart_block_quote(self, node):
376
self.body.append('</blockquote>\n')
378
def check_simple_list(self, node):
379
"""Check for a simple list that can be rendered compactly."""
380
visitor = SimpleListChecker(self.document)
383
except nodes.NodeFound:
388
def visit_bullet_list(self, node):
390
old_compact_simple = self.compact_simple
391
self.context.append((self.compact_simple, self.compact_p))
392
self.compact_p = None
393
self.compact_simple = (self.settings.compact_lists and
395
or self.topic_class == 'contents'
396
or self.check_simple_list(node)))
397
if self.compact_simple and not old_compact_simple:
398
atts['class'] = 'simple'
399
self.body.append(self.starttag(node, 'ul', **atts))
401
def depart_bullet_list(self, node):
402
self.compact_simple, self.compact_p = self.context.pop()
403
self.body.append('</ul>\n')
405
def visit_caption(self, node):
406
self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
408
def depart_caption(self, node):
409
self.body.append('</p>\n')
411
def visit_caution(self, node):
412
self.visit_admonition(node, 'caution')
414
def depart_caution(self, node):
415
self.depart_admonition()
417
def visit_citation(self, node):
418
self.body.append(self.starttag(node, 'table', CLASS='citation',
419
frame="void", rules="none"))
420
self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
422
'<tbody valign="top">\n'
424
self.footnote_backrefs(node)
426
def depart_citation(self, node):
427
self.body.append('</td></tr>\n'
428
'</tbody>\n</table>\n')
430
def visit_citation_reference(self, node):
432
if node.has_key('refid'):
433
href = '#' + node['refid']
434
elif node.has_key('refname'):
435
href = '#' + self.document.nameids[node['refname']]
436
self.body.append(self.starttag(node, 'a', '[', href=href,
437
CLASS='citation-reference'))
439
def depart_citation_reference(self, node):
440
self.body.append(']</a>')
442
def visit_classifier(self, node):
443
self.body.append(' <span class="classifier-delimiter">:</span> ')
444
self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
446
def depart_classifier(self, node):
447
self.body.append('</span>')
449
def visit_colspec(self, node):
450
self.colspecs.append(node)
452
def depart_colspec(self, node):
455
def write_colspecs(self):
457
for node in self.colspecs:
458
width += node['colwidth']
459
for node in self.colspecs:
460
colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
461
self.body.append(self.emptytag(node, 'col',
462
width='%i%%' % colwidth))
465
def visit_comment(self, node,
466
sub=re.compile('-(?=-)').sub):
467
"""Escape double-dashes in comment text."""
468
self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
469
# Content already processed:
472
def visit_contact(self, node):
473
self.visit_docinfo_item(node, 'contact', meta=None)
475
def depart_contact(self, node):
476
self.depart_docinfo_item()
478
def visit_copyright(self, node):
479
self.visit_docinfo_item(node, 'copyright')
481
def depart_copyright(self, node):
482
self.depart_docinfo_item()
484
def visit_danger(self, node):
485
self.visit_admonition(node, 'danger')
487
def depart_danger(self, node):
488
self.depart_admonition()
490
def visit_date(self, node):
491
self.visit_docinfo_item(node, 'date')
493
def depart_date(self, node):
494
self.depart_docinfo_item()
496
def visit_decoration(self, node):
499
def depart_decoration(self, node):
502
def visit_definition(self, node):
503
self.body.append('</dt>\n')
504
self.body.append(self.starttag(node, 'dd', ''))
506
node[0].set_class('first')
507
node[-1].set_class('last')
509
def depart_definition(self, node):
510
self.body.append('</dd>\n')
512
def visit_definition_list(self, node):
513
self.body.append(self.starttag(node, 'dl'))
515
def depart_definition_list(self, node):
516
self.body.append('</dl>\n')
518
def visit_definition_list_item(self, node):
521
def depart_definition_list_item(self, node):
524
def visit_description(self, node):
525
self.body.append(self.starttag(node, 'td', ''))
527
node[0].set_class('first')
528
node[-1].set_class('last')
530
def depart_description(self, node):
531
self.body.append('</td>')
533
def visit_docinfo(self, node):
534
self.context.append(len(self.body))
535
self.body.append(self.starttag(node, 'table', CLASS='docinfo',
536
frame="void", rules="none"))
537
self.body.append('<col class="docinfo-name" />\n'
538
'<col class="docinfo-content" />\n'
539
'<tbody valign="top">\n')
542
def depart_docinfo(self, node):
543
self.body.append('</tbody>\n</table>\n')
544
self.in_docinfo = None
545
start = self.context.pop()
546
self.docinfo = self.body[start:]
549
def visit_docinfo_item(self, node, name, meta=1):
551
meta_tag = '<meta name="%s" content="%s" />\n' \
552
% (name, self.attval(node.astext()))
553
self.add_meta(meta_tag)
554
self.body.append(self.starttag(node, 'tr', ''))
555
self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
556
% self.language.labels[name])
558
if isinstance(node[0], nodes.Element):
559
node[0].set_class('first')
560
if isinstance(node[-1], nodes.Element):
561
node[-1].set_class('last')
563
def depart_docinfo_item(self):
564
self.body.append('</td></tr>\n')
566
def visit_doctest_block(self, node):
567
self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
569
def depart_doctest_block(self, node):
570
self.body.append('\n</pre>\n')
572
def visit_document(self, node):
573
# empty or untitled document?
574
if not len(node) or not isinstance(node[0], nodes.title):
575
# for XHTML conformance, modulo IE6 appeasement:
576
self.head.insert(0, '<title></title>\n')
578
def depart_document(self, node):
579
self.fragment.extend(self.body)
580
self.body.insert(0, self.starttag(node, 'div', CLASS='document'))
581
self.body.append('</div>\n')
583
def visit_emphasis(self, node):
584
self.body.append('<em>')
586
def depart_emphasis(self, node):
587
self.body.append('</em>')
589
def visit_entry(self, node):
590
if isinstance(node.parent.parent, nodes.thead):
595
if node.has_key('morerows'):
596
atts['rowspan'] = node['morerows'] + 1
597
if node.has_key('morecols'):
598
atts['colspan'] = node['morecols'] + 1
599
self.body.append(self.starttag(node, tagname, '', **atts))
600
self.context.append('</%s>\n' % tagname.lower())
601
if len(node) == 0: # empty cell
602
self.body.append(' ')
604
node[0].set_class('first')
605
node[-1].set_class('last')
607
def depart_entry(self, node):
608
self.body.append(self.context.pop())
610
def visit_enumerated_list(self, node):
612
The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
613
CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
617
if node.has_key('start'):
618
atts['start'] = node['start']
619
if node.has_key('enumtype'):
620
atts['class'] = node['enumtype']
621
# @@@ To do: prefix, suffix. How? Change prefix/suffix to a
622
# single "format" attribute? Use CSS2?
623
old_compact_simple = self.compact_simple
624
self.context.append((self.compact_simple, self.compact_p))
625
self.compact_p = None
626
self.compact_simple = (self.settings.compact_lists and
628
or self.topic_class == 'contents'
629
or self.check_simple_list(node)))
630
if self.compact_simple and not old_compact_simple:
631
atts['class'] = (atts.get('class', '') + ' simple').strip()
632
self.body.append(self.starttag(node, 'ol', **atts))
634
def depart_enumerated_list(self, node):
635
self.compact_simple, self.compact_p = self.context.pop()
636
self.body.append('</ol>\n')
638
def visit_error(self, node):
639
self.visit_admonition(node, 'error')
641
def depart_error(self, node):
642
self.depart_admonition()
644
def visit_field(self, node):
645
self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
647
def depart_field(self, node):
648
self.body.append('</tr>\n')
650
def visit_field_body(self, node):
651
self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
653
node[0].set_class('first')
654
node[-1].set_class('last')
656
def depart_field_body(self, node):
657
self.body.append('</td>\n')
659
def visit_field_list(self, node):
660
self.body.append(self.starttag(node, 'table', frame='void',
661
rules='none', CLASS='field-list'))
662
self.body.append('<col class="field-name" />\n'
663
'<col class="field-body" />\n'
664
'<tbody valign="top">\n')
666
def depart_field_list(self, node):
667
self.body.append('</tbody>\n</table>\n')
669
def visit_field_name(self, node):
672
atts['class'] = 'docinfo-name'
674
atts['class'] = 'field-name'
675
if len(node.astext()) > 14:
677
self.context.append('</tr>\n<tr><td> </td>')
679
self.context.append('')
680
self.body.append(self.starttag(node, 'th', '', **atts))
682
def depart_field_name(self, node):
683
self.body.append(':</th>')
684
self.body.append(self.context.pop())
686
def visit_figure(self, node):
687
atts = {'class': 'figure'}
688
if node.get('width'):
689
atts['style'] = 'width: %spx' % node['width']
690
self.body.append(self.starttag(node, 'div', **atts))
692
def depart_figure(self, node):
693
self.body.append('</div>\n')
695
def visit_footer(self, node):
696
self.context.append(len(self.body))
698
def depart_footer(self, node):
699
start = self.context.pop()
700
footer = (['<hr class="footer" />\n',
701
self.starttag(node, 'div', CLASS='footer')]
702
+ self.body[start:] + ['</div>\n'])
703
self.footer.extend(footer)
704
self.body_suffix[:0] = footer
705
del self.body[start:]
707
def visit_footnote(self, node):
708
self.body.append(self.starttag(node, 'table', CLASS='footnote',
709
frame="void", rules="none"))
710
self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
711
'<tbody valign="top">\n'
713
self.footnote_backrefs(node)
715
def footnote_backrefs(self, node):
716
if self.settings.footnote_backlinks and node.hasattr('backrefs'):
717
backrefs = node['backrefs']
718
if len(backrefs) == 1:
719
self.context.append('')
720
self.context.append('<a class="fn-backref" href="#%s" '
721
'name="%s">' % (backrefs[0], node['id']))
725
for backref in backrefs:
726
backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
729
self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
730
self.context.append('<a name="%s">' % node['id'])
732
self.context.append('')
733
self.context.append('<a name="%s">' % node['id'])
735
def depart_footnote(self, node):
736
self.body.append('</td></tr>\n'
737
'</tbody>\n</table>\n')
739
def visit_footnote_reference(self, node):
741
if node.has_key('refid'):
742
href = '#' + node['refid']
743
elif node.has_key('refname'):
744
href = '#' + self.document.nameids[node['refname']]
745
format = self.settings.footnote_references
746
if format == 'brackets':
748
self.context.append(']')
749
elif format == 'superscript':
751
self.context.append('</sup>')
752
else: # shouldn't happen
754
self.content.append('???')
755
self.body.append(self.starttag(node, 'a', suffix, href=href,
756
CLASS='footnote-reference'))
758
def depart_footnote_reference(self, node):
759
self.body.append(self.context.pop() + '</a>')
761
def visit_generated(self, node):
764
def depart_generated(self, node):
767
def visit_header(self, node):
768
self.context.append(len(self.body))
770
def depart_header(self, node):
771
start = self.context.pop()
772
header = [self.starttag(node, 'div', CLASS='header')]
773
header.extend(self.body[start:])
774
header.append('<hr />\n</div>\n')
775
self.body_prefix.extend(header)
777
del self.body[start:]
779
def visit_hint(self, node):
780
self.visit_admonition(node, 'hint')
782
def depart_hint(self, node):
783
self.depart_admonition()
785
def visit_image(self, node):
786
atts = node.attributes.copy()
787
if atts.has_key('class'):
788
del atts['class'] # prevent duplication with node attrs
789
atts['src'] = atts['uri']
791
if atts.has_key('scale'):
792
if Image and not (atts.has_key('width')
793
and atts.has_key('height')):
795
im = Image.open(str(atts['src']))
796
except (IOError, # Source image can't be found or opened
797
UnicodeError): # PIL doesn't like Unicode paths.
800
if not atts.has_key('width'):
801
atts['width'] = im.size[0]
802
if not atts.has_key('height'):
803
atts['height'] = im.size[1]
805
if atts.has_key('width'):
806
atts['width'] = int(round(atts['width']
807
* (float(atts['scale']) / 100)))
808
if atts.has_key('height'):
809
atts['height'] = int(round(atts['height']
810
* (float(atts['scale']) / 100)))
812
if not atts.has_key('alt'):
813
atts['alt'] = atts['src']
814
if isinstance(node.parent, nodes.TextElement):
815
self.context.append('')
817
if atts.has_key('align'):
818
self.body.append('<p align="%s">' %
819
(self.attval(atts['align'],)))
821
self.body.append('<p>')
822
self.context.append('</p>\n')
823
self.body.append(self.emptytag(node, 'img', '', **atts))
825
def depart_image(self, node):
826
self.body.append(self.context.pop())
828
def visit_important(self, node):
829
self.visit_admonition(node, 'important')
831
def depart_important(self, node):
832
self.depart_admonition()
834
def visit_inline(self, node):
835
self.body.append(self.starttag(node, 'span', ''))
837
def depart_inline(self, node):
838
self.body.append('</span>')
840
def visit_label(self, node):
841
self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
844
def depart_label(self, node):
845
self.body.append(']</a></td><td>%s' % self.context.pop())
847
def visit_legend(self, node):
848
self.body.append(self.starttag(node, 'div', CLASS='legend'))
850
def depart_legend(self, node):
851
self.body.append('</div>\n')
853
def visit_line_block(self, node):
854
self.body.append(self.starttag(node, 'pre', CLASS='line-block'))
856
def depart_line_block(self, node):
857
self.body.append('\n</pre>\n')
859
def visit_list_item(self, node):
860
self.body.append(self.starttag(node, 'li', ''))
862
node[0].set_class('first')
864
def depart_list_item(self, node):
865
self.body.append('</li>\n')
867
def visit_literal(self, node):
868
"""Process text to prevent tokens from wrapping."""
869
self.body.append(self.starttag(node, 'tt', '', CLASS='literal'))
871
for token in self.words_and_spaces.findall(text):
873
# Protect text like "--an-option" from bad line wrapping:
874
self.body.append('<span class="pre">%s</span>'
875
% self.encode(token))
876
elif token in ('\n', ' '):
877
# Allow breaks at whitespace:
878
self.body.append(token)
880
# Protect runs of multiple spaces; the last space can wrap:
881
self.body.append(' ' * (len(token) - 1) + ' ')
882
self.body.append('</tt>')
883
# Content already processed:
886
def visit_literal_block(self, node):
887
self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
889
def depart_literal_block(self, node):
890
self.body.append('\n</pre>\n')
892
def visit_meta(self, node):
893
meta = self.emptytag(node, 'meta', **node.attributes)
896
def depart_meta(self, node):
899
def add_meta(self, tag):
900
self.meta.append(tag)
901
self.head.append(tag)
903
def visit_note(self, node):
904
self.visit_admonition(node, 'note')
906
def depart_note(self, node):
907
self.depart_admonition()
909
def visit_option(self, node):
911
self.body.append(', ')
913
def depart_option(self, node):
914
self.context[-1] += 1
916
def visit_option_argument(self, node):
917
self.body.append(node.get('delimiter', ' '))
918
self.body.append(self.starttag(node, 'var', ''))
920
def depart_option_argument(self, node):
921
self.body.append('</var>')
923
def visit_option_group(self, node):
925
if len(node.astext()) > 14:
927
self.context.append('</tr>\n<tr><td> </td>')
929
self.context.append('')
930
self.body.append(self.starttag(node, 'td', **atts))
931
self.body.append('<kbd>')
932
self.context.append(0) # count number of options
934
def depart_option_group(self, node):
936
self.body.append('</kbd></td>\n')
937
self.body.append(self.context.pop())
939
def visit_option_list(self, node):
941
self.starttag(node, 'table', CLASS='option-list',
942
frame="void", rules="none"))
943
self.body.append('<col class="option" />\n'
944
'<col class="description" />\n'
945
'<tbody valign="top">\n')
947
def depart_option_list(self, node):
948
self.body.append('</tbody>\n</table>\n')
950
def visit_option_list_item(self, node):
951
self.body.append(self.starttag(node, 'tr', ''))
953
def depart_option_list_item(self, node):
954
self.body.append('</tr>\n')
956
def visit_option_string(self, node):
957
self.body.append(self.starttag(node, 'span', '', CLASS='option'))
959
def depart_option_string(self, node):
960
self.body.append('</span>')
962
def visit_organization(self, node):
963
self.visit_docinfo_item(node, 'organization')
965
def depart_organization(self, node):
966
self.depart_docinfo_item()
968
def visit_paragraph(self, node):
969
# Omit <p> tags if this is an only child and optimizable.
970
if (self.compact_simple or
971
self.compact_p and (len(node.parent) == 1 or
972
len(node.parent) == 2 and
973
isinstance(node.parent[0], nodes.label))):
974
self.context.append('')
976
self.body.append(self.starttag(node, 'p', ''))
977
self.context.append('</p>\n')
979
def depart_paragraph(self, node):
980
self.body.append(self.context.pop())
982
def visit_problematic(self, node):
983
if node.hasattr('refid'):
984
self.body.append('<a href="#%s" name="%s">' % (node['refid'],
986
self.context.append('</a>')
988
self.context.append('')
989
self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
991
def depart_problematic(self, node):
992
self.body.append('</span>')
993
self.body.append(self.context.pop())
995
def visit_raw(self, node):
996
if node.get('format') == 'html':
997
self.body.append(node.astext())
998
# Keep non-HTML raw text out of output:
1001
def visit_reference(self, node):
1002
if isinstance(node.parent, nodes.TextElement):
1003
self.context.append('')
1005
self.body.append('<p>')
1006
self.context.append('</p>\n')
1007
if node.has_key('refuri'):
1008
href = node['refuri']
1009
elif node.has_key('refid'):
1010
href = '#' + node['refid']
1011
elif node.has_key('refname'):
1012
href = '#' + self.document.nameids[node['refname']]
1013
self.body.append(self.starttag(node, 'a', '', href=href,
1016
def depart_reference(self, node):
1017
self.body.append('</a>')
1018
self.body.append(self.context.pop())
1020
def visit_revision(self, node):
1021
self.visit_docinfo_item(node, 'revision', meta=None)
1023
def depart_revision(self, node):
1024
self.depart_docinfo_item()
1026
def visit_row(self, node):
1027
self.body.append(self.starttag(node, 'tr', ''))
1029
def depart_row(self, node):
1030
self.body.append('</tr>\n')
1032
def visit_rubric(self, node):
1033
self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1035
def depart_rubric(self, node):
1036
self.body.append('</p>\n')
1038
def visit_section(self, node):
1039
self.section_level += 1
1040
self.body.append(self.starttag(node, 'div', CLASS='section'))
1042
def depart_section(self, node):
1043
self.section_level -= 1
1044
self.body.append('</div>\n')
1046
def visit_sidebar(self, node):
1047
self.body.append(self.starttag(node, 'div', CLASS='sidebar'))
1050
def depart_sidebar(self, node):
1051
self.body.append('</div>\n')
1052
self.in_sidebar = None
1054
def visit_status(self, node):
1055
self.visit_docinfo_item(node, 'status', meta=None)
1057
def depart_status(self, node):
1058
self.depart_docinfo_item()
1060
def visit_strong(self, node):
1061
self.body.append('<strong>')
1063
def depart_strong(self, node):
1064
self.body.append('</strong>')
1066
def visit_subscript(self, node):
1067
self.body.append(self.starttag(node, 'sub', ''))
1069
def depart_subscript(self, node):
1070
self.body.append('</sub>')
1072
def visit_substitution_definition(self, node):
1073
"""Internal only."""
1074
raise nodes.SkipNode
1076
def visit_substitution_reference(self, node):
1077
self.unimplemented_visit(node)
1079
def visit_subtitle(self, node):
1080
if isinstance(node.parent, nodes.sidebar):
1081
self.body.append(self.starttag(node, 'p', '',
1082
CLASS='sidebar-subtitle'))
1083
self.context.append('</p>\n')
1084
elif isinstance(node.parent, nodes.document):
1085
self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1086
self.context.append('</h2>\n')
1087
self.in_document_title = len(self.body)
1089
def depart_subtitle(self, node):
1090
self.body.append(self.context.pop())
1091
if self.in_document_title:
1092
self.subtitle = self.body[self.in_document_title:-1]
1093
self.in_document_title = 0
1094
self.body_pre_docinfo.extend(self.body)
1097
def visit_superscript(self, node):
1098
self.body.append(self.starttag(node, 'sup', ''))
1100
def depart_superscript(self, node):
1101
self.body.append('</sup>')
1103
def visit_system_message(self, node):
1104
if node['level'] < self.document.reporter['writer'].report_level:
1105
# Level is too low to display:
1106
raise nodes.SkipNode
1107
self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1108
self.body.append('<p class="system-message-title">')
1111
if node.hasattr('id'):
1112
attr['name'] = node['id']
1113
if node.hasattr('backrefs'):
1114
backrefs = node['backrefs']
1115
if len(backrefs) == 1:
1116
backref_text = ('; <em><a href="#%s">backlink</a></em>'
1121
for backref in backrefs:
1122
backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1124
backref_text = ('; <em>backlinks: %s</em>'
1125
% ', '.join(backlinks))
1126
if node.hasattr('line'):
1127
line = ', line %s' % node['line']
1131
a_start = self.starttag({}, 'a', '', **attr)
1134
a_start = a_end = ''
1135
self.body.append('System Message: %s%s/%s%s (<tt>%s</tt>%s)%s</p>\n'
1136
% (a_start, node['type'], node['level'], a_end,
1137
self.encode(node['source']), line, backref_text))
1139
def depart_system_message(self, node):
1140
self.body.append('</div>\n')
1142
def visit_table(self, node):
1144
# "border=None" is a boolean attribute;
1145
# it means "standard border", not "no border":
1146
self.starttag(node, 'table', CLASS="table", border=None))
1148
def depart_table(self, node):
1149
self.body.append('</table>\n')
1151
def visit_target(self, node):
1152
if not (node.has_key('refuri') or node.has_key('refid')
1153
or node.has_key('refname')):
1154
self.body.append(self.starttag(node, 'a', '', CLASS='target'))
1155
self.context.append('</a>')
1157
self.context.append('')
1159
def depart_target(self, node):
1160
self.body.append(self.context.pop())
1162
def visit_tbody(self, node):
1163
self.write_colspecs()
1164
self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1165
self.body.append(self.starttag(node, 'tbody', valign='top'))
1167
def depart_tbody(self, node):
1168
self.body.append('</tbody>\n')
1170
def visit_term(self, node):
1171
self.body.append(self.starttag(node, 'dt', ''))
1173
def depart_term(self, node):
1175
Leave the end tag to `self.visit_definition()`, in case there's a
1180
def visit_tgroup(self, node):
1181
# Mozilla needs <colgroup>:
1182
self.body.append(self.starttag(node, 'colgroup'))
1183
# Appended by thead or tbody:
1184
self.context.append('</colgroup>\n')
1186
def depart_tgroup(self, node):
1189
def visit_thead(self, node):
1190
self.write_colspecs()
1191
self.body.append(self.context.pop()) # '</colgroup>\n'
1192
# There may or may not be a <thead>; this is for <tbody> to use:
1193
self.context.append('')
1194
self.body.append(self.starttag(node, 'thead', valign='bottom'))
1196
def depart_thead(self, node):
1197
self.body.append('</thead>\n')
1199
def visit_tip(self, node):
1200
self.visit_admonition(node, 'tip')
1202
def depart_tip(self, node):
1203
self.depart_admonition()
1205
def visit_title(self, node):
1206
"""Only 6 section levels are supported by HTML."""
1208
close_tag = '</p>\n'
1209
if isinstance(node.parent, nodes.topic):
1211
self.starttag(node, 'p', '', CLASS='topic-title first'))
1213
elif isinstance(node.parent, nodes.sidebar):
1215
self.starttag(node, 'p', '', CLASS='sidebar-title first'))
1217
elif isinstance(node.parent, nodes.admonition):
1219
self.starttag(node, 'p', '', CLASS='admonition-title first'))
1221
elif isinstance(node.parent, nodes.table):
1223
self.starttag(node, 'caption', ''))
1225
close_tag = '</caption>\n'
1226
elif self.section_level == 0:
1228
self.head.append('<title>%s</title>\n'
1229
% self.encode(node.astext()))
1230
self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1231
self.context.append('</h1>\n')
1232
self.in_document_title = len(self.body)
1234
h_level = self.section_level + self.initial_header_level - 1
1236
self.starttag(node, 'h%s' % h_level, ''))
1238
if node.parent.hasattr('id'):
1239
atts['name'] = node.parent['id']
1240
if node.hasattr('refid'):
1241
atts['class'] = 'toc-backref'
1242
atts['href'] = '#' + node['refid']
1243
self.body.append(self.starttag({}, 'a', '', **atts))
1244
self.context.append('</a></h%s>\n' % (h_level))
1246
if node.parent.hasattr('id'):
1248
self.starttag({}, 'a', '', name=node.parent['id']))
1249
self.context.append('</a>' + close_tag)
1251
self.context.append(close_tag)
1253
def depart_title(self, node):
1254
self.body.append(self.context.pop())
1255
if self.in_document_title:
1256
self.title = self.body[self.in_document_title:-1]
1257
self.in_document_title = 0
1258
self.body_pre_docinfo.extend(self.body)
1261
def visit_title_reference(self, node):
1262
self.body.append(self.starttag(node, 'cite', ''))
1264
def depart_title_reference(self, node):
1265
self.body.append('</cite>')
1267
def visit_topic(self, node):
1268
self.body.append(self.starttag(node, 'div', CLASS='topic'))
1269
self.topic_class = node.get('class')
1271
def depart_topic(self, node):
1272
self.body.append('</div>\n')
1273
self.topic_class = ''
1275
def visit_transition(self, node):
1276
self.body.append(self.emptytag(node, 'hr'))
1278
def depart_transition(self, node):
1281
def visit_version(self, node):
1282
self.visit_docinfo_item(node, 'version', meta=None)
1284
def depart_version(self, node):
1285
self.depart_docinfo_item()
1287
def visit_warning(self, node):
1288
self.visit_admonition(node, 'warning')
1290
def depart_warning(self, node):
1291
self.depart_admonition()
1293
def unimplemented_visit(self, node):
1294
raise NotImplementedError('visiting unimplemented node type: %s'
1295
% node.__class__.__name__)
1298
class SimpleListChecker(nodes.GenericNodeVisitor):
1301
Raise `nodes.SkipNode` if non-simple list item is encountered.
1303
Here "simple" means a list item containing nothing other than a single
1304
paragraph, a simple list, or a paragraph followed by a simple list.
1307
def default_visit(self, node):
1308
raise nodes.NodeFound
1310
def visit_bullet_list(self, node):
1313
def visit_enumerated_list(self, node):
1316
def visit_list_item(self, node):
1318
for child in node.get_children():
1319
if not isinstance(child, nodes.Invisible):
1320
children.append(child)
1321
if (children and isinstance(children[0], nodes.paragraph)
1322
and (isinstance(children[-1], nodes.bullet_list)
1323
or isinstance(children[-1], nodes.enumerated_list))):
1325
if len(children) <= 1:
1328
raise nodes.NodeFound
1330
def visit_paragraph(self, node):
1331
raise nodes.SkipNode
1333
def invisible_visit(self, node):
1334
"""Invisible nodes should be ignored."""
1337
visit_comment = invisible_visit
1338
visit_substitution_definition = invisible_visit
1339
visit_target = invisible_visit
1340
visit_pending = invisible_visit