Math support update
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob421b374db3329c337a6606a22283f896f8f154a0
1 # $Id$
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
5 """
6 Simple HyperText Markup Language document tree Writer.
8 The output conforms to the XHTML version 1.0 Transitional DTD
9 (*almost* strict). The output contains a minimum of formatting
10 information. The cascading style sheet "html4css1.css" is required
11 for proper viewing with a modern graphical browser.
12 """
14 __docformat__ = 'reStructuredText'
17 import sys
18 import os
19 import os.path
20 import time
21 import re
22 try:
23 import Image # check for the Python Imaging Library
24 except ImportError:
25 Image = None
26 import docutils
27 from docutils import frontend, nodes, utils, writers, languages, io
28 from docutils.transforms import writer_aux
29 from docutils.math import unimathsymbols2tex
30 from docutils.math.latex2mathml import parse_latex_math
31 from docutils.math.math2html import math2html
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))
44 default_template = 'template.txt'
46 default_template_path = utils.relative_path(
47 os.path.join(os.getcwd(), 'dummy'),
48 os.path.join(os.path.dirname(__file__), default_template))
50 settings_spec = (
51 'HTML-Specific Options',
52 None,
53 (('Specify the template file (UTF-8 encoded). Default is "%s".'
54 % default_template_path,
55 ['--template'],
56 {'default': default_template_path, 'metavar': '<file>'}),
57 ('Specify comma separated list of stylesheet URLs. '
58 'Overrides previous --stylesheet and --stylesheet-path settings.',
59 ['--stylesheet'],
60 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
61 ('Specify comma separated list of stylesheet paths. '
62 'With --link-stylesheet, '
63 'the path is rewritten relative to the output HTML file. '
64 'Default: "%s"' % default_stylesheet_path,
65 ['--stylesheet-path'],
66 {'metavar': '<file>', 'overrides': 'stylesheet',
67 'default': default_stylesheet_path}),
68 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
69 'files must be accessible during processing. This is the default.',
70 ['--embed-stylesheet'],
71 {'default': 1, 'action': 'store_true',
72 'validator': frontend.validate_boolean}),
73 ('Link to the stylesheet(s) in the output HTML file. '
74 'Default: embed stylesheets.',
75 ['--link-stylesheet'],
76 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
77 ('Specify the initial header level. Default is 1 for "<h1>". '
78 'Does not affect document title & subtitle (see --no-doc-title).',
79 ['--initial-header-level'],
80 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
81 'metavar': '<level>'}),
82 ('Specify the maximum width (in characters) for one-column field '
83 'names. Longer field names will span an entire row of the table '
84 'used to render the field list. Default is 14 characters. '
85 'Use 0 for "no limit".',
86 ['--field-name-limit'],
87 {'default': 14, 'metavar': '<level>',
88 'validator': frontend.validate_nonnegative_int}),
89 ('Specify the maximum width (in characters) for options in option '
90 'lists. Longer options will span an entire row of the table used '
91 'to render the option list. Default is 14 characters. '
92 'Use 0 for "no limit".',
93 ['--option-limit'],
94 {'default': 14, 'metavar': '<level>',
95 'validator': frontend.validate_nonnegative_int}),
96 ('Format for footnote references: one of "superscript" or '
97 '"brackets". Default is "brackets".',
98 ['--footnote-references'],
99 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
100 'metavar': '<format>',
101 'overrides': 'trim_footnote_reference_space'}),
102 ('Format for block quote attributions: one of "dash" (em-dash '
103 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
104 ['--attribution'],
105 {'choices': ['dash', 'parentheses', 'parens', 'none'],
106 'default': 'dash', 'metavar': '<format>'}),
107 ('Remove extra vertical whitespace between items of "simple" bullet '
108 'lists and enumerated lists. Default: enabled.',
109 ['--compact-lists'],
110 {'default': 1, 'action': 'store_true',
111 'validator': frontend.validate_boolean}),
112 ('Disable compact simple bullet and enumerated lists.',
113 ['--no-compact-lists'],
114 {'dest': 'compact_lists', 'action': 'store_false'}),
115 ('Remove extra vertical whitespace between items of simple field '
116 'lists. Default: enabled.',
117 ['--compact-field-lists'],
118 {'default': 1, 'action': 'store_true',
119 'validator': frontend.validate_boolean}),
120 ('Disable compact simple field lists.',
121 ['--no-compact-field-lists'],
122 {'dest': 'compact_field_lists', 'action': 'store_false'}),
123 ('Added to standard table classes. '
124 'Defined styles: "borderless". Default: ""',
125 ['--table-style'],
126 {'default': ''}),
127 ('Math output format, one of "MathML", "HTML", or "LaTeX". '
128 'Default: "MathML"',
129 ['--math-output'],
130 {'default': 'MathML'}),
131 ('Omit the XML declaration. Use with caution.',
132 ['--no-xml-declaration'],
133 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
134 'validator': frontend.validate_boolean}),
135 ('Obfuscate email addresses to confuse harvesters while still '
136 'keeping email links usable with standards-compliant browsers.',
137 ['--cloak-email-addresses'],
138 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
140 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
142 relative_path_settings = ('stylesheet_path',)
144 config_section = 'html4css1 writer'
145 config_section_dependencies = ('writers',)
147 visitor_attributes = (
148 'head_prefix', 'head', 'stylesheet', 'body_prefix',
149 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
150 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
151 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
152 'html_body')
154 def get_transforms(self):
155 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
157 def __init__(self):
158 writers.Writer.__init__(self)
159 self.translator_class = HTMLTranslator
161 def translate(self):
162 self.visitor = visitor = self.translator_class(self.document)
163 self.document.walkabout(visitor)
164 for attr in self.visitor_attributes:
165 setattr(self, attr, getattr(visitor, attr))
166 self.output = self.apply_template()
168 def apply_template(self):
169 template_file = open(self.document.settings.template, 'rb')
170 template = unicode(template_file.read(), 'utf-8')
171 template_file.close()
172 subs = self.interpolation_dict()
173 return template % subs
175 def interpolation_dict(self):
176 subs = {}
177 settings = self.document.settings
178 for attr in self.visitor_attributes:
179 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
180 subs['encoding'] = settings.output_encoding
181 subs['version'] = docutils.__version__
182 return subs
184 def assemble_parts(self):
185 writers.Writer.assemble_parts(self)
186 for part in self.visitor_attributes:
187 self.parts[part] = ''.join(getattr(self, part))
190 class HTMLTranslator(nodes.NodeVisitor):
193 This HTML writer has been optimized to produce visually compact
194 lists (less vertical whitespace). HTML's mixed content models
195 allow list items to contain "<li><p>body elements</p></li>" or
196 "<li>just text</li>" or even "<li>text<p>and body
197 elements</p>combined</li>", each with different effects. It would
198 be best to stick with strict body elements in list items, but they
199 affect vertical spacing in browsers (although they really
200 shouldn't).
202 Here is an outline of the optimization:
204 - Check for and omit <p> tags in "simple" lists: list items
205 contain either a single paragraph, a nested simple list, or a
206 paragraph followed by a nested simple list. This means that
207 this list can be compact:
209 - Item 1.
210 - Item 2.
212 But this list cannot be compact:
214 - Item 1.
216 This second paragraph forces space between list items.
218 - Item 2.
220 - In non-list contexts, omit <p> tags on a paragraph if that
221 paragraph is the only child of its parent (footnotes & citations
222 are allowed a label first).
224 - Regardless of the above, in definitions, table cells, field bodies,
225 option descriptions, and list items, mark the first child with
226 'class="first"' and the last child with 'class="last"'. The stylesheet
227 sets the margins (top & bottom respectively) to 0 for these elements.
229 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
230 option) disables list whitespace optimization.
233 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
234 doctype = (
235 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
236 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
237 doctype_mathml = doctype
239 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
240 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
241 content_type = ('<meta http-equiv="Content-Type"'
242 ' content="text/html; charset=%s" />\n')
243 content_type_mathml = ('<meta http-equiv="Content-Type"'
244 ' content="application/xhtml+xml; charset=%s" />\n')
246 generator = ('<meta name="generator" content="Docutils %s: '
247 'http://docutils.sourceforge.net/" />\n')
248 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
249 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
250 words_and_spaces = re.compile(r'\S+| +|\n')
251 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
252 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
254 def __init__(self, document):
255 nodes.NodeVisitor.__init__(self, document)
256 self.settings = settings = document.settings
257 lcode = settings.language_code
258 self.language = languages.get_language(lcode, document.reporter)
259 self.meta = [self.content_type % settings.output_encoding,
260 self.generator % docutils.__version__]
261 self.head_prefix = []
262 self.html_prolog = []
263 if settings.xml_declaration:
264 self.head_prefix.append(self.xml_declaration
265 % settings.output_encoding)
266 # encoding not interpolated:
267 self.html_prolog.append(self.xml_declaration)
268 self.head_prefix.extend([self.doctype,
269 self.head_prefix_template % {'lang': lcode}])
270 self.html_prolog.append(self.doctype)
271 self.head = self.meta[:]
272 # stylesheets
273 styles = utils.get_stylesheet_list(settings)
274 if settings.stylesheet_path and not(settings.embed_stylesheet):
275 styles = [utils.relative_path(settings._destination, sheet)
276 for sheet in styles]
277 if settings.embed_stylesheet:
278 settings.record_dependencies.add(*styles)
279 self.stylesheet = [self.embedded_stylesheet %
280 io.FileInput(source_path=sheet, encoding='utf-8').read()
281 for sheet in styles]
282 else: # link to stylesheets
283 self.stylesheet = [self.stylesheet_link % self.encode(stylesheet)
284 for stylesheet in styles]
285 self.body_prefix = ['</head>\n<body>\n']
286 # document title, subtitle display
287 self.body_pre_docinfo = []
288 # author, date, etc.
289 self.docinfo = []
290 self.body = []
291 self.fragment = []
292 self.body_suffix = ['</body>\n</html>\n']
293 self.section_level = 0
294 self.initial_header_level = int(settings.initial_header_level)
295 self.math_output = settings.math_output.lower()
296 # A heterogenous stack used in conjunction with the tree traversal.
297 # Make sure that the pops correspond to the pushes:
298 self.context = []
299 self.topic_classes = []
300 self.colspecs = []
301 self.compact_p = 1
302 self.compact_simple = None
303 self.compact_field_list = None
304 self.in_docinfo = None
305 self.in_sidebar = None
306 self.title = []
307 self.subtitle = []
308 self.header = []
309 self.footer = []
310 self.html_head = [self.content_type] # charset not interpolated
311 self.html_title = []
312 self.html_subtitle = []
313 self.html_body = []
314 self.in_document_title = 0
315 self.in_mailto = 0
316 self.author_in_authors = None
317 self.has_mathml_dtd = False
319 def astext(self):
320 return ''.join(self.head_prefix + self.head
321 + self.stylesheet + self.body_prefix
322 + self.body_pre_docinfo + self.docinfo
323 + self.body + self.body_suffix)
325 def encode(self, text):
326 """Encode special characters in `text` & return."""
327 # @@@ A codec to do these and all other HTML entities would be nice.
328 text = unicode(text)
329 return text.translate({
330 ord('&'): u'&amp;',
331 ord('<'): u'&lt;',
332 ord('"'): u'&quot;',
333 ord('>'): u'&gt;',
334 ord('@'): u'&#64;', # may thwart some address harvesters
335 # TODO: convert non-breaking space only if needed?
336 0xa0: u'&nbsp;'}) # non-breaking space
338 def cloak_mailto(self, uri):
339 """Try to hide a mailto: URL from harvesters."""
340 # Encode "@" using a URL octet reference (see RFC 1738).
341 # Further cloaking with HTML entities will be done in the
342 # `attval` function.
343 return uri.replace('@', '%40')
345 def cloak_email(self, addr):
346 """Try to hide the link text of a email link from harversters."""
347 # Surround at-signs and periods with <span> tags. ("@" has
348 # already been encoded to "&#64;" by the `encode` method.)
349 addr = addr.replace('&#64;', '<span>&#64;</span>')
350 addr = addr.replace('.', '<span>&#46;</span>')
351 return addr
353 def attval(self, text,
354 whitespace=re.compile('[\n\r\t\v\f]')):
355 """Cleanse, HTML encode, and return attribute value text."""
356 encoded = self.encode(whitespace.sub(' ', text))
357 if self.in_mailto and self.settings.cloak_email_addresses:
358 # Cloak at-signs ("%40") and periods with HTML entities.
359 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
360 encoded = encoded.replace('.', '&#46;')
361 return encoded
363 def starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
365 Construct and return a start tag given a node (id & class attributes
366 are extracted), tag name, and optional attributes.
368 tagname = tagname.lower()
369 prefix = []
370 atts = {}
371 ids = []
372 for (name, value) in attributes.items():
373 atts[name.lower()] = value
374 classes = node.get('classes', [])
375 if 'class' in atts:
376 classes.append(atts.pop('class'))
377 # move language specification to 'lang' attribute
378 languages = [cls for cls in classes
379 if cls.startswith('language-')]
380 if languages:
381 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
382 atts[self.lang_attribute] = languages[0][9:]
383 classes.pop(classes.index(languages[0]))
384 classes = ' '.join(classes).strip()
385 if classes:
386 atts['class'] = classes
387 assert 'id' not in atts
388 ids.extend(node.get('ids', []))
389 if 'ids' in atts:
390 ids.extend(atts['ids'])
391 del atts['ids']
392 if ids:
393 atts['id'] = ids[0]
394 for id in ids[1:]:
395 # Add empty "span" elements for additional IDs. Note
396 # that we cannot use empty "a" elements because there
397 # may be targets inside of references, but nested "a"
398 # elements aren't allowed in XHTML (even if they do
399 # not all have a "href" attribute).
400 if empty:
401 # Empty tag. Insert target right in front of element.
402 prefix.append('<span id="%s"></span>' % id)
403 else:
404 # Non-empty tag. Place the auxiliary <span> tag
405 # *inside* the element, as the first child.
406 suffix += '<span id="%s"></span>' % id
407 attlist = atts.items()
408 attlist.sort()
409 parts = [tagname]
410 for name, value in attlist:
411 # value=None was used for boolean attributes without
412 # value, but this isn't supported by XHTML.
413 assert value is not None
414 if isinstance(value, list):
415 values = [unicode(v) for v in value]
416 parts.append('%s="%s"' % (name.lower(),
417 self.attval(' '.join(values))))
418 else:
419 parts.append('%s="%s"' % (name.lower(),
420 self.attval(unicode(value))))
421 if empty:
422 infix = ' /'
423 else:
424 infix = ''
425 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
427 def emptytag(self, node, tagname, suffix='\n', **attributes):
428 """Construct and return an XML-compatible empty tag."""
429 return self.starttag(node, tagname, suffix, empty=1, **attributes)
431 def set_class_on_child(self, node, class_, index=0):
433 Set class `class_` on the visible child no. index of `node`.
434 Do nothing if node has fewer children than `index`.
436 children = [n for n in node if not isinstance(n, nodes.Invisible)]
437 try:
438 child = children[index]
439 except IndexError:
440 return
441 child['classes'].append(class_)
443 def set_first_last(self, node):
444 self.set_class_on_child(node, 'first', 0)
445 self.set_class_on_child(node, 'last', -1)
447 def visit_Text(self, node):
448 text = node.astext()
449 encoded = self.encode(text)
450 if self.in_mailto and self.settings.cloak_email_addresses:
451 encoded = self.cloak_email(encoded)
452 self.body.append(encoded)
454 def depart_Text(self, node):
455 pass
457 def visit_abbreviation(self, node):
458 # @@@ implementation incomplete ("title" attribute)
459 self.body.append(self.starttag(node, 'abbr', ''))
461 def depart_abbreviation(self, node):
462 self.body.append('</abbr>')
464 def visit_acronym(self, node):
465 # @@@ implementation incomplete ("title" attribute)
466 self.body.append(self.starttag(node, 'acronym', ''))
468 def depart_acronym(self, node):
469 self.body.append('</acronym>')
471 def visit_address(self, node):
472 self.visit_docinfo_item(node, 'address', meta=None)
473 self.body.append(self.starttag(node, 'pre', CLASS='address'))
475 def depart_address(self, node):
476 self.body.append('\n</pre>\n')
477 self.depart_docinfo_item()
479 def visit_admonition(self, node):
480 self.body.append(self.starttag(node, 'div'))
481 self.set_first_last(node)
483 def depart_admonition(self, node=None):
484 self.body.append('</div>\n')
486 attribution_formats = {'dash': ('&mdash;', ''),
487 'parentheses': ('(', ')'),
488 'parens': ('(', ')'),
489 'none': ('', '')}
491 def visit_attribution(self, node):
492 prefix, suffix = self.attribution_formats[self.settings.attribution]
493 self.context.append(suffix)
494 self.body.append(
495 self.starttag(node, 'p', prefix, CLASS='attribution'))
497 def depart_attribution(self, node):
498 self.body.append(self.context.pop() + '</p>\n')
500 def visit_author(self, node):
501 if isinstance(node.parent, nodes.authors):
502 if self.author_in_authors:
503 self.body.append('\n<br />')
504 else:
505 self.visit_docinfo_item(node, 'author')
507 def depart_author(self, node):
508 if isinstance(node.parent, nodes.authors):
509 self.author_in_authors += 1
510 else:
511 self.depart_docinfo_item()
513 def visit_authors(self, node):
514 self.visit_docinfo_item(node, 'authors')
515 self.author_in_authors = 0 # initialize counter
517 def depart_authors(self, node):
518 self.depart_docinfo_item()
519 self.author_in_authors = None
521 def visit_block_quote(self, node):
522 self.body.append(self.starttag(node, 'blockquote'))
524 def depart_block_quote(self, node):
525 self.body.append('</blockquote>\n')
527 def check_simple_list(self, node):
528 """Check for a simple list that can be rendered compactly."""
529 visitor = SimpleListChecker(self.document)
530 try:
531 node.walk(visitor)
532 except nodes.NodeFound:
533 return None
534 else:
535 return 1
537 def is_compactable(self, node):
538 return ('compact' in node['classes']
539 or (self.settings.compact_lists
540 and 'open' not in node['classes']
541 and (self.compact_simple
542 or self.topic_classes == ['contents']
543 or self.check_simple_list(node))))
545 def visit_bullet_list(self, node):
546 atts = {}
547 old_compact_simple = self.compact_simple
548 self.context.append((self.compact_simple, self.compact_p))
549 self.compact_p = None
550 self.compact_simple = self.is_compactable(node)
551 if self.compact_simple and not old_compact_simple:
552 atts['class'] = 'simple'
553 self.body.append(self.starttag(node, 'ul', **atts))
555 def depart_bullet_list(self, node):
556 self.compact_simple, self.compact_p = self.context.pop()
557 self.body.append('</ul>\n')
559 def visit_caption(self, node):
560 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
562 def depart_caption(self, node):
563 self.body.append('</p>\n')
565 def visit_citation(self, node):
566 self.body.append(self.starttag(node, 'table',
567 CLASS='docutils citation',
568 frame="void", rules="none"))
569 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
570 '<tbody valign="top">\n'
571 '<tr>')
572 self.footnote_backrefs(node)
574 def depart_citation(self, node):
575 self.body.append('</td></tr>\n'
576 '</tbody>\n</table>\n')
578 def visit_citation_reference(self, node):
579 href = '#' + node['refid']
580 self.body.append(self.starttag(
581 node, 'a', '[', CLASS='citation-reference', href=href))
583 def depart_citation_reference(self, node):
584 self.body.append(']</a>')
586 def visit_classifier(self, node):
587 self.body.append(' <span class="classifier-delimiter">:</span> ')
588 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
590 def depart_classifier(self, node):
591 self.body.append('</span>')
593 def visit_colspec(self, node):
594 self.colspecs.append(node)
595 # "stubs" list is an attribute of the tgroup element:
596 node.parent.stubs.append(node.attributes.get('stub'))
598 def depart_colspec(self, node):
599 pass
601 def write_colspecs(self):
602 width = 0
603 for node in self.colspecs:
604 width += node['colwidth']
605 for node in self.colspecs:
606 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
607 self.body.append(self.emptytag(node, 'col',
608 width='%i%%' % colwidth))
609 self.colspecs = []
611 def visit_comment(self, node,
612 sub=re.compile('-(?=-)').sub):
613 """Escape double-dashes in comment text."""
614 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
615 # Content already processed:
616 raise nodes.SkipNode
618 def visit_compound(self, node):
619 self.body.append(self.starttag(node, 'div', CLASS='compound'))
620 if len(node) > 1:
621 node[0]['classes'].append('compound-first')
622 node[-1]['classes'].append('compound-last')
623 for child in node[1:-1]:
624 child['classes'].append('compound-middle')
626 def depart_compound(self, node):
627 self.body.append('</div>\n')
629 def visit_container(self, node):
630 self.body.append(self.starttag(node, 'div', CLASS='container'))
632 def depart_container(self, node):
633 self.body.append('</div>\n')
635 def visit_contact(self, node):
636 self.visit_docinfo_item(node, 'contact', meta=None)
638 def depart_contact(self, node):
639 self.depart_docinfo_item()
641 def visit_copyright(self, node):
642 self.visit_docinfo_item(node, 'copyright')
644 def depart_copyright(self, node):
645 self.depart_docinfo_item()
647 def visit_date(self, node):
648 self.visit_docinfo_item(node, 'date')
650 def depart_date(self, node):
651 self.depart_docinfo_item()
653 def visit_decoration(self, node):
654 pass
656 def depart_decoration(self, node):
657 pass
659 def visit_definition(self, node):
660 self.body.append('</dt>\n')
661 self.body.append(self.starttag(node, 'dd', ''))
662 self.set_first_last(node)
664 def depart_definition(self, node):
665 self.body.append('</dd>\n')
667 def visit_definition_list(self, node):
668 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
670 def depart_definition_list(self, node):
671 self.body.append('</dl>\n')
673 def visit_definition_list_item(self, node):
674 pass
676 def depart_definition_list_item(self, node):
677 pass
679 def visit_description(self, node):
680 self.body.append(self.starttag(node, 'td', ''))
681 self.set_first_last(node)
683 def depart_description(self, node):
684 self.body.append('</td>')
686 def visit_docinfo(self, node):
687 self.context.append(len(self.body))
688 self.body.append(self.starttag(node, 'table',
689 CLASS='docinfo',
690 frame="void", rules="none"))
691 self.body.append('<col class="docinfo-name" />\n'
692 '<col class="docinfo-content" />\n'
693 '<tbody valign="top">\n')
694 self.in_docinfo = 1
696 def depart_docinfo(self, node):
697 self.body.append('</tbody>\n</table>\n')
698 self.in_docinfo = None
699 start = self.context.pop()
700 self.docinfo = self.body[start:]
701 self.body = []
703 def visit_docinfo_item(self, node, name, meta=1):
704 if meta:
705 meta_tag = '<meta name="%s" content="%s" />\n' \
706 % (name, self.attval(node.astext()))
707 self.add_meta(meta_tag)
708 self.body.append(self.starttag(node, 'tr', ''))
709 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
710 % self.language.labels[name])
711 if len(node):
712 if isinstance(node[0], nodes.Element):
713 node[0]['classes'].append('first')
714 if isinstance(node[-1], nodes.Element):
715 node[-1]['classes'].append('last')
717 def depart_docinfo_item(self):
718 self.body.append('</td></tr>\n')
720 def visit_doctest_block(self, node):
721 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
723 def depart_doctest_block(self, node):
724 self.body.append('\n</pre>\n')
726 def visit_document(self, node):
727 self.head.append('<title>%s</title>\n'
728 % self.encode(node.get('title', '')))
730 def depart_document(self, node):
731 self.fragment.extend(self.body)
732 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
733 self.body_suffix.insert(0, '</div>\n')
734 # skip content-type meta tag with interpolated charset value:
735 self.html_head.extend(self.head[1:])
736 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
737 + self.docinfo + self.body
738 + self.body_suffix[:-1])
739 assert not self.context, 'len(context) = %s' % len(self.context)
741 def visit_emphasis(self, node):
742 self.body.append(self.starttag(node, 'em', ''))
744 def depart_emphasis(self, node):
745 self.body.append('</em>')
747 def visit_entry(self, node):
748 atts = {'class': []}
749 if isinstance(node.parent.parent, nodes.thead):
750 atts['class'].append('head')
751 if node.parent.parent.parent.stubs[node.parent.column]:
752 # "stubs" list is an attribute of the tgroup element
753 atts['class'].append('stub')
754 if atts['class']:
755 tagname = 'th'
756 atts['class'] = ' '.join(atts['class'])
757 else:
758 tagname = 'td'
759 del atts['class']
760 node.parent.column += 1
761 if 'morerows' in node:
762 atts['rowspan'] = node['morerows'] + 1
763 if 'morecols' in node:
764 atts['colspan'] = node['morecols'] + 1
765 node.parent.column += node['morecols']
766 self.body.append(self.starttag(node, tagname, '', **atts))
767 self.context.append('</%s>\n' % tagname.lower())
768 if len(node) == 0: # empty cell
769 self.body.append('&nbsp;')
770 self.set_first_last(node)
772 def depart_entry(self, node):
773 self.body.append(self.context.pop())
775 def visit_enumerated_list(self, node):
777 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
778 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
779 usable.
781 atts = {}
782 if 'start' in node:
783 atts['start'] = node['start']
784 if 'enumtype' in node:
785 atts['class'] = node['enumtype']
786 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
787 # single "format" attribute? Use CSS2?
788 old_compact_simple = self.compact_simple
789 self.context.append((self.compact_simple, self.compact_p))
790 self.compact_p = None
791 self.compact_simple = self.is_compactable(node)
792 if self.compact_simple and not old_compact_simple:
793 atts['class'] = (atts.get('class', '') + ' simple').strip()
794 self.body.append(self.starttag(node, 'ol', **atts))
796 def depart_enumerated_list(self, node):
797 self.compact_simple, self.compact_p = self.context.pop()
798 self.body.append('</ol>\n')
800 def visit_field(self, node):
801 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
803 def depart_field(self, node):
804 self.body.append('</tr>\n')
806 def visit_field_body(self, node):
807 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
808 self.set_class_on_child(node, 'first', 0)
809 field = node.parent
810 if (self.compact_field_list or
811 isinstance(field.parent, nodes.docinfo) or
812 field.parent.index(field) == len(field.parent) - 1):
813 # If we are in a compact list, the docinfo, or if this is
814 # the last field of the field list, do not add vertical
815 # space after last element.
816 self.set_class_on_child(node, 'last', -1)
818 def depart_field_body(self, node):
819 self.body.append('</td>\n')
821 def visit_field_list(self, node):
822 self.context.append((self.compact_field_list, self.compact_p))
823 self.compact_p = None
824 if 'compact' in node['classes']:
825 self.compact_field_list = 1
826 elif (self.settings.compact_field_lists
827 and 'open' not in node['classes']):
828 self.compact_field_list = 1
829 if self.compact_field_list:
830 for field in node:
831 field_body = field[-1]
832 assert isinstance(field_body, nodes.field_body)
833 children = [n for n in field_body
834 if not isinstance(n, nodes.Invisible)]
835 if not (len(children) == 0 or
836 len(children) == 1 and
837 isinstance(children[0],
838 (nodes.paragraph, nodes.line_block))):
839 self.compact_field_list = 0
840 break
841 self.body.append(self.starttag(node, 'table', frame='void',
842 rules='none',
843 CLASS='docutils field-list'))
844 self.body.append('<col class="field-name" />\n'
845 '<col class="field-body" />\n'
846 '<tbody valign="top">\n')
848 def depart_field_list(self, node):
849 self.body.append('</tbody>\n</table>\n')
850 self.compact_field_list, self.compact_p = self.context.pop()
852 def visit_field_name(self, node):
853 atts = {}
854 if self.in_docinfo:
855 atts['class'] = 'docinfo-name'
856 else:
857 atts['class'] = 'field-name'
858 if ( self.settings.field_name_limit
859 and len(node.astext()) > self.settings.field_name_limit):
860 atts['colspan'] = 2
861 self.context.append('</tr>\n'
862 + self.starttag(node.parent, 'tr', '')
863 + '<td>&nbsp;</td>')
864 else:
865 self.context.append('')
866 self.body.append(self.starttag(node, 'th', '', **atts))
868 def depart_field_name(self, node):
869 self.body.append(':</th>')
870 self.body.append(self.context.pop())
872 def visit_figure(self, node):
873 atts = {'class': 'figure'}
874 if node.get('width'):
875 atts['style'] = 'width: %s' % node['width']
876 if node.get('align'):
877 atts['class'] += " align-" + node['align']
878 self.body.append(self.starttag(node, 'div', **atts))
880 def depart_figure(self, node):
881 self.body.append('</div>\n')
883 def visit_footer(self, node):
884 self.context.append(len(self.body))
886 def depart_footer(self, node):
887 start = self.context.pop()
888 footer = [self.starttag(node, 'div', CLASS='footer'),
889 '<hr class="footer" />\n']
890 footer.extend(self.body[start:])
891 footer.append('\n</div>\n')
892 self.footer.extend(footer)
893 self.body_suffix[:0] = footer
894 del self.body[start:]
896 def visit_footnote(self, node):
897 self.body.append(self.starttag(node, 'table',
898 CLASS='docutils footnote',
899 frame="void", rules="none"))
900 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
901 '<tbody valign="top">\n'
902 '<tr>')
903 self.footnote_backrefs(node)
905 def footnote_backrefs(self, node):
906 backlinks = []
907 backrefs = node['backrefs']
908 if self.settings.footnote_backlinks and backrefs:
909 if len(backrefs) == 1:
910 self.context.append('')
911 self.context.append('</a>')
912 self.context.append('<a class="fn-backref" href="#%s">'
913 % backrefs[0])
914 else:
915 i = 1
916 for backref in backrefs:
917 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
918 % (backref, i))
919 i += 1
920 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
921 self.context += ['', '']
922 else:
923 self.context.append('')
924 self.context += ['', '']
925 # If the node does not only consist of a label.
926 if len(node) > 1:
927 # If there are preceding backlinks, we do not set class
928 # 'first', because we need to retain the top-margin.
929 if not backlinks:
930 node[1]['classes'].append('first')
931 node[-1]['classes'].append('last')
933 def depart_footnote(self, node):
934 self.body.append('</td></tr>\n'
935 '</tbody>\n</table>\n')
937 def visit_footnote_reference(self, node):
938 href = '#' + node['refid']
939 format = self.settings.footnote_references
940 if format == 'brackets':
941 suffix = '['
942 self.context.append(']')
943 else:
944 assert format == 'superscript'
945 suffix = '<sup>'
946 self.context.append('</sup>')
947 self.body.append(self.starttag(node, 'a', suffix,
948 CLASS='footnote-reference', href=href))
950 def depart_footnote_reference(self, node):
951 self.body.append(self.context.pop() + '</a>')
953 def visit_generated(self, node):
954 pass
956 def depart_generated(self, node):
957 pass
959 def visit_header(self, node):
960 self.context.append(len(self.body))
962 def depart_header(self, node):
963 start = self.context.pop()
964 header = [self.starttag(node, 'div', CLASS='header')]
965 header.extend(self.body[start:])
966 header.append('\n<hr class="header"/>\n</div>\n')
967 self.body_prefix.extend(header)
968 self.header.extend(header)
969 del self.body[start:]
971 def visit_image(self, node):
972 atts = {}
973 uri = node['uri']
974 # place SVG and SWF images in an <object> element
975 types = {'.svg': 'image/svg+xml',
976 '.swf': 'application/x-shockwave-flash'}
977 ext = os.path.splitext(uri)[1].lower()
978 if ext in ('.svg', '.swf'):
979 atts['data'] = uri
980 atts['type'] = types[ext]
981 else:
982 atts['src'] = uri
983 atts['alt'] = node.get('alt', uri)
984 # image size
985 if 'width' in node:
986 atts['width'] = node['width']
987 if 'height' in node:
988 atts['height'] = node['height']
989 if 'scale' in node:
990 if Image and not ('width' in node and 'height' in node):
991 try:
992 im = Image.open(str(uri))
993 except (IOError, # Source image can't be found or opened
994 UnicodeError): # PIL doesn't like Unicode paths.
995 pass
996 else:
997 if 'width' not in atts:
998 atts['width'] = str(im.size[0])
999 if 'height' not in atts:
1000 atts['height'] = str(im.size[1])
1001 del im
1002 for att_name in 'width', 'height':
1003 if att_name in atts:
1004 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1005 assert match
1006 atts[att_name] = '%s%s' % (
1007 float(match.group(1)) * (float(node['scale']) / 100),
1008 match.group(2))
1009 style = []
1010 for att_name in 'width', 'height':
1011 if att_name in atts:
1012 if re.match(r'^[0-9.]+$', atts[att_name]):
1013 # Interpret unitless values as pixels.
1014 atts[att_name] += 'px'
1015 style.append('%s: %s;' % (att_name, atts[att_name]))
1016 del atts[att_name]
1017 if style:
1018 atts['style'] = ' '.join(style)
1019 if (isinstance(node.parent, nodes.TextElement) or
1020 (isinstance(node.parent, nodes.reference) and
1021 not isinstance(node.parent.parent, nodes.TextElement))):
1022 # Inline context or surrounded by <a>...</a>.
1023 suffix = ''
1024 else:
1025 suffix = '\n'
1026 if 'align' in node:
1027 atts['class'] = 'align-%s' % node['align']
1028 self.context.append('')
1029 if ext in ('.svg', '.swf'): # place in an object element,
1030 # do NOT use an empty tag: incorrect rendering in browsers
1031 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1032 node.get('alt', uri) + '</object>' + suffix)
1033 else:
1034 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1036 def depart_image(self, node):
1037 self.body.append(self.context.pop())
1039 def visit_inline(self, node):
1040 self.body.append(self.starttag(node, 'span', ''))
1042 def depart_inline(self, node):
1043 self.body.append('</span>')
1045 def visit_label(self, node):
1046 # Context added in footnote_backrefs.
1047 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1048 CLASS='label'))
1050 def depart_label(self, node):
1051 # Context added in footnote_backrefs.
1052 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1054 def visit_legend(self, node):
1055 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1057 def depart_legend(self, node):
1058 self.body.append('</div>\n')
1060 def visit_line(self, node):
1061 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1062 if not len(node):
1063 self.body.append('<br />')
1065 def depart_line(self, node):
1066 self.body.append('</div>\n')
1068 def visit_line_block(self, node):
1069 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1071 def depart_line_block(self, node):
1072 self.body.append('</div>\n')
1074 def visit_list_item(self, node):
1075 self.body.append(self.starttag(node, 'li', ''))
1076 if len(node):
1077 node[0]['classes'].append('first')
1079 def depart_list_item(self, node):
1080 self.body.append('</li>\n')
1082 def visit_literal(self, node):
1083 """Process text to prevent tokens from wrapping."""
1084 self.body.append(
1085 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1086 text = node.astext()
1087 for token in self.words_and_spaces.findall(text):
1088 if token.strip():
1089 # Protect text like "--an-option" and the regular expression
1090 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1091 if self.sollbruchstelle.search(token):
1092 self.body.append('<span class="pre">%s</span>'
1093 % self.encode(token))
1094 else:
1095 self.body.append(self.encode(token))
1096 elif token in ('\n', ' '):
1097 # Allow breaks at whitespace:
1098 self.body.append(token)
1099 else:
1100 # Protect runs of multiple spaces; the last space can wrap:
1101 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1102 self.body.append('</tt>')
1103 # Content already processed:
1104 raise nodes.SkipNode
1106 def visit_literal_block(self, node):
1107 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1109 def depart_literal_block(self, node):
1110 self.body.append('\n</pre>\n')
1112 def multiline_math(self, node):
1113 """find out whether `code` is a multi-line equation
1115 This is a very simplified test, looking
1116 for line-breaks (``\\``) outside environments.
1118 code = node.astext()
1119 # cut out environment content:
1120 chunks = code.split(r'\begin{')
1121 toplevel_code = ''.join([chunk.split(r'\end{')[-1]
1122 for chunk in chunks])
1123 return toplevel_code.find(r'\\') >= 0
1125 def visit_math(self, node, inline=True):
1126 math_code = node.astext().translate(unimathsymbols2tex.uni2tex_table)
1128 if self.math_output == 'latex':
1129 self.body.append(self.starttag(node, 'tt', CLASS='math'))
1130 return
1132 if self.math_output == 'html':
1133 wrapper = u'%s'
1134 if not inline and self.multiline_math(node):
1135 math_env = 'align*'
1136 wrapper = u'\n'.join([r'\begin{%s}' % math_env,
1137 '%s',
1138 r'\end{%s}' % math_env])
1140 tag={True: 'span', False: 'div'}
1141 self.body.append(self.starttag(node, tag[inline], CLASS='formula'))
1142 self.body.append(math2html(wrapper % math_code))
1143 self.body.append('</%s>\n' % tag[inline])
1145 elif self.math_output == 'mathml':
1146 # update the doctype:
1147 if not self.has_mathml_dtd:
1148 if self.settings.xml_declaration:
1149 self.head_prefix[1] = self.doctype_mathml
1150 else:
1151 self.head_prefix[0] = self.doctype_mathml
1152 self.html_head[0] = self.content_type_mathml
1153 self.head[0] = self.content_type_mathml % self.settings.output_encoding
1154 self.has_mathml_dtd = True
1155 # Convert from LaTeX to MathML:
1156 try:
1157 mathml_tree = parse_latex_math(math_code, inline=inline)
1158 math_out = ''.join(mathml_tree.xml())
1159 except SyntaxError, err:
1160 err_node = self.document.reporter.error(err, base_node=node)
1161 self.visit_system_message(err_node)
1162 self.body.append(self.starttag(node, 'p'))
1163 self.body.append(u','.join(err.args))
1164 self.body.append('</p>\n')
1165 self.body.append(self.starttag(node, 'pre',
1166 CLASS='literal-block'))
1167 self.body.append(self.encode(math_code))
1168 self.body.append('\n</pre>\n')
1169 self.depart_system_message(err_node)
1170 else:
1171 self.body.append(math_out)
1172 if not inline:
1173 self.body.append('\n')
1174 # Content already processed:
1175 raise nodes.SkipNode
1177 def depart_math(self, node):
1178 if self.math_output == 'latex':
1179 self.body.append('</tt>\n')
1181 def visit_math_block(self, node):
1182 if self.math_output == 'latex':
1183 self.body.append(self.starttag(node, 'pre', CLASS='math'))
1184 else:
1185 self.visit_math(node, inline=False)
1187 def depart_math_block(self, node):
1188 if self.math_output == 'latex':
1189 self.body.append('\n</pre>\n')
1191 def visit_meta(self, node):
1192 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1193 self.add_meta(meta)
1195 def depart_meta(self, node):
1196 pass
1198 def add_meta(self, tag):
1199 self.meta.append(tag)
1200 self.head.append(tag)
1202 def visit_option(self, node):
1203 if self.context[-1]:
1204 self.body.append(', ')
1205 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1207 def depart_option(self, node):
1208 self.body.append('</span>')
1209 self.context[-1] += 1
1211 def visit_option_argument(self, node):
1212 self.body.append(node.get('delimiter', ' '))
1213 self.body.append(self.starttag(node, 'var', ''))
1215 def depart_option_argument(self, node):
1216 self.body.append('</var>')
1218 def visit_option_group(self, node):
1219 atts = {}
1220 if ( self.settings.option_limit
1221 and len(node.astext()) > self.settings.option_limit):
1222 atts['colspan'] = 2
1223 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1224 else:
1225 self.context.append('')
1226 self.body.append(
1227 self.starttag(node, 'td', CLASS='option-group', **atts))
1228 self.body.append('<kbd>')
1229 self.context.append(0) # count number of options
1231 def depart_option_group(self, node):
1232 self.context.pop()
1233 self.body.append('</kbd></td>\n')
1234 self.body.append(self.context.pop())
1236 def visit_option_list(self, node):
1237 self.body.append(
1238 self.starttag(node, 'table', CLASS='docutils option-list',
1239 frame="void", rules="none"))
1240 self.body.append('<col class="option" />\n'
1241 '<col class="description" />\n'
1242 '<tbody valign="top">\n')
1244 def depart_option_list(self, node):
1245 self.body.append('</tbody>\n</table>\n')
1247 def visit_option_list_item(self, node):
1248 self.body.append(self.starttag(node, 'tr', ''))
1250 def depart_option_list_item(self, node):
1251 self.body.append('</tr>\n')
1253 def visit_option_string(self, node):
1254 pass
1256 def depart_option_string(self, node):
1257 pass
1259 def visit_organization(self, node):
1260 self.visit_docinfo_item(node, 'organization')
1262 def depart_organization(self, node):
1263 self.depart_docinfo_item()
1265 def should_be_compact_paragraph(self, node):
1267 Determine if the <p> tags around paragraph ``node`` can be omitted.
1269 if (isinstance(node.parent, nodes.document) or
1270 isinstance(node.parent, nodes.compound)):
1271 # Never compact paragraphs in document or compound.
1272 return 0
1273 for key, value in node.attlist():
1274 if (node.is_not_default(key) and
1275 not (key == 'classes' and value in
1276 ([], ['first'], ['last'], ['first', 'last']))):
1277 # Attribute which needs to survive.
1278 return 0
1279 first = isinstance(node.parent[0], nodes.label) # skip label
1280 for child in node.parent.children[first:]:
1281 # only first paragraph can be compact
1282 if isinstance(child, nodes.Invisible):
1283 continue
1284 if child is node:
1285 break
1286 return 0
1287 parent_length = len([n for n in node.parent if not isinstance(
1288 n, (nodes.Invisible, nodes.label))])
1289 if ( self.compact_simple
1290 or self.compact_field_list
1291 or self.compact_p and parent_length == 1):
1292 return 1
1293 return 0
1295 def visit_paragraph(self, node):
1296 if self.should_be_compact_paragraph(node):
1297 self.context.append('')
1298 else:
1299 self.body.append(self.starttag(node, 'p', ''))
1300 self.context.append('</p>\n')
1302 def depart_paragraph(self, node):
1303 self.body.append(self.context.pop())
1305 def visit_problematic(self, node):
1306 if node.hasattr('refid'):
1307 self.body.append('<a href="#%s">' % node['refid'])
1308 self.context.append('</a>')
1309 else:
1310 self.context.append('')
1311 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1313 def depart_problematic(self, node):
1314 self.body.append('</span>')
1315 self.body.append(self.context.pop())
1317 def visit_raw(self, node):
1318 if 'html' in node.get('format', '').split():
1319 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1320 if node['classes']:
1321 self.body.append(self.starttag(node, t, suffix=''))
1322 self.body.append(node.astext())
1323 if node['classes']:
1324 self.body.append('</%s>' % t)
1325 # Keep non-HTML raw text out of output:
1326 raise nodes.SkipNode
1328 def visit_reference(self, node):
1329 atts = {'class': 'reference'}
1330 if 'refuri' in node:
1331 atts['href'] = node['refuri']
1332 if ( self.settings.cloak_email_addresses
1333 and atts['href'].startswith('mailto:')):
1334 atts['href'] = self.cloak_mailto(atts['href'])
1335 self.in_mailto = 1
1336 atts['class'] += ' external'
1337 else:
1338 assert 'refid' in node, \
1339 'References must have "refuri" or "refid" attribute.'
1340 atts['href'] = '#' + node['refid']
1341 atts['class'] += ' internal'
1342 if not isinstance(node.parent, nodes.TextElement):
1343 assert len(node) == 1 and isinstance(node[0], nodes.image)
1344 atts['class'] += ' image-reference'
1345 self.body.append(self.starttag(node, 'a', '', **atts))
1347 def depart_reference(self, node):
1348 self.body.append('</a>')
1349 if not isinstance(node.parent, nodes.TextElement):
1350 self.body.append('\n')
1351 self.in_mailto = 0
1353 def visit_revision(self, node):
1354 self.visit_docinfo_item(node, 'revision', meta=None)
1356 def depart_revision(self, node):
1357 self.depart_docinfo_item()
1359 def visit_row(self, node):
1360 self.body.append(self.starttag(node, 'tr', ''))
1361 node.column = 0
1363 def depart_row(self, node):
1364 self.body.append('</tr>\n')
1366 def visit_rubric(self, node):
1367 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1369 def depart_rubric(self, node):
1370 self.body.append('</p>\n')
1372 def visit_section(self, node):
1373 self.section_level += 1
1374 self.body.append(
1375 self.starttag(node, 'div', CLASS='section'))
1377 def depart_section(self, node):
1378 self.section_level -= 1
1379 self.body.append('</div>\n')
1381 def visit_sidebar(self, node):
1382 self.body.append(
1383 self.starttag(node, 'div', CLASS='sidebar'))
1384 self.set_first_last(node)
1385 self.in_sidebar = 1
1387 def depart_sidebar(self, node):
1388 self.body.append('</div>\n')
1389 self.in_sidebar = None
1391 def visit_status(self, node):
1392 self.visit_docinfo_item(node, 'status', meta=None)
1394 def depart_status(self, node):
1395 self.depart_docinfo_item()
1397 def visit_strong(self, node):
1398 self.body.append(self.starttag(node, 'strong', ''))
1400 def depart_strong(self, node):
1401 self.body.append('</strong>')
1403 def visit_subscript(self, node):
1404 self.body.append(self.starttag(node, 'sub', ''))
1406 def depart_subscript(self, node):
1407 self.body.append('</sub>')
1409 def visit_substitution_definition(self, node):
1410 """Internal only."""
1411 raise nodes.SkipNode
1413 def visit_substitution_reference(self, node):
1414 self.unimplemented_visit(node)
1416 def visit_subtitle(self, node):
1417 if isinstance(node.parent, nodes.sidebar):
1418 self.body.append(self.starttag(node, 'p', '',
1419 CLASS='sidebar-subtitle'))
1420 self.context.append('</p>\n')
1421 elif isinstance(node.parent, nodes.document):
1422 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1423 self.context.append('</h2>\n')
1424 self.in_document_title = len(self.body)
1425 elif isinstance(node.parent, nodes.section):
1426 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1427 self.body.append(
1428 self.starttag(node, tag, '', CLASS='section-subtitle') +
1429 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1430 self.context.append('</span></%s>\n' % tag)
1432 def depart_subtitle(self, node):
1433 self.body.append(self.context.pop())
1434 if self.in_document_title:
1435 self.subtitle = self.body[self.in_document_title:-1]
1436 self.in_document_title = 0
1437 self.body_pre_docinfo.extend(self.body)
1438 self.html_subtitle.extend(self.body)
1439 del self.body[:]
1441 def visit_superscript(self, node):
1442 self.body.append(self.starttag(node, 'sup', ''))
1444 def depart_superscript(self, node):
1445 self.body.append('</sup>')
1447 def visit_system_message(self, node):
1448 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1449 self.body.append('<p class="system-message-title">')
1450 backref_text = ''
1451 if len(node['backrefs']):
1452 backrefs = node['backrefs']
1453 if len(backrefs) == 1:
1454 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1455 % backrefs[0])
1456 else:
1457 i = 1
1458 backlinks = []
1459 for backref in backrefs:
1460 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1461 i += 1
1462 backref_text = ('; <em>backlinks: %s</em>'
1463 % ', '.join(backlinks))
1464 if node.hasattr('line'):
1465 line = ', line %s' % node['line']
1466 else:
1467 line = ''
1468 self.body.append('System Message: %s/%s '
1469 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1470 % (node['type'], node['level'],
1471 self.encode(node['source']), line, backref_text))
1473 def depart_system_message(self, node):
1474 self.body.append('</div>\n')
1476 def visit_table(self, node):
1477 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1478 self.body.append(
1479 self.starttag(node, 'table', CLASS=classes, border="1"))
1481 def depart_table(self, node):
1482 self.body.append('</table>\n')
1484 def visit_target(self, node):
1485 if not ('refuri' in node or 'refid' in node
1486 or 'refname' in node):
1487 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1488 self.context.append('</span>')
1489 else:
1490 self.context.append('')
1492 def depart_target(self, node):
1493 self.body.append(self.context.pop())
1495 def visit_tbody(self, node):
1496 self.write_colspecs()
1497 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1498 self.body.append(self.starttag(node, 'tbody', valign='top'))
1500 def depart_tbody(self, node):
1501 self.body.append('</tbody>\n')
1503 def visit_term(self, node):
1504 self.body.append(self.starttag(node, 'dt', ''))
1506 def depart_term(self, node):
1508 Leave the end tag to `self.visit_definition()`, in case there's a
1509 classifier.
1511 pass
1513 def visit_tgroup(self, node):
1514 # Mozilla needs <colgroup>:
1515 self.body.append(self.starttag(node, 'colgroup'))
1516 # Appended by thead or tbody:
1517 self.context.append('</colgroup>\n')
1518 node.stubs = []
1520 def depart_tgroup(self, node):
1521 pass
1523 def visit_thead(self, node):
1524 self.write_colspecs()
1525 self.body.append(self.context.pop()) # '</colgroup>\n'
1526 # There may or may not be a <thead>; this is for <tbody> to use:
1527 self.context.append('')
1528 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1530 def depart_thead(self, node):
1531 self.body.append('</thead>\n')
1533 def visit_title(self, node):
1534 """Only 6 section levels are supported by HTML."""
1535 check_id = 0
1536 close_tag = '</p>\n'
1537 if isinstance(node.parent, nodes.topic):
1538 self.body.append(
1539 self.starttag(node, 'p', '', CLASS='topic-title first'))
1540 elif isinstance(node.parent, nodes.sidebar):
1541 self.body.append(
1542 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1543 elif isinstance(node.parent, nodes.Admonition):
1544 self.body.append(
1545 self.starttag(node, 'p', '', CLASS='admonition-title'))
1546 elif isinstance(node.parent, nodes.table):
1547 self.body.append(
1548 self.starttag(node, 'caption', ''))
1549 close_tag = '</caption>\n'
1550 elif isinstance(node.parent, nodes.document):
1551 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1552 close_tag = '</h1>\n'
1553 self.in_document_title = len(self.body)
1554 else:
1555 assert isinstance(node.parent, nodes.section)
1556 h_level = self.section_level + self.initial_header_level - 1
1557 atts = {}
1558 if (len(node.parent) >= 2 and
1559 isinstance(node.parent[1], nodes.subtitle)):
1560 atts['CLASS'] = 'with-subtitle'
1561 self.body.append(
1562 self.starttag(node, 'h%s' % h_level, '', **atts))
1563 atts = {}
1564 if node.hasattr('refid'):
1565 atts['class'] = 'toc-backref'
1566 atts['href'] = '#' + node['refid']
1567 if atts:
1568 self.body.append(self.starttag({}, 'a', '', **atts))
1569 close_tag = '</a></h%s>\n' % (h_level)
1570 else:
1571 close_tag = '</h%s>\n' % (h_level)
1572 self.context.append(close_tag)
1574 def depart_title(self, node):
1575 self.body.append(self.context.pop())
1576 if self.in_document_title:
1577 self.title = self.body[self.in_document_title:-1]
1578 self.in_document_title = 0
1579 self.body_pre_docinfo.extend(self.body)
1580 self.html_title.extend(self.body)
1581 del self.body[:]
1583 def visit_title_reference(self, node):
1584 self.body.append(self.starttag(node, 'cite', ''))
1586 def depart_title_reference(self, node):
1587 self.body.append('</cite>')
1589 def visit_topic(self, node):
1590 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1591 self.topic_classes = node['classes']
1593 def depart_topic(self, node):
1594 self.body.append('</div>\n')
1595 self.topic_classes = []
1597 def visit_transition(self, node):
1598 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1600 def depart_transition(self, node):
1601 pass
1603 def visit_version(self, node):
1604 self.visit_docinfo_item(node, 'version', meta=None)
1606 def depart_version(self, node):
1607 self.depart_docinfo_item()
1609 def unimplemented_visit(self, node):
1610 raise NotImplementedError('visiting unimplemented node type: %s'
1611 % node.__class__.__name__)
1614 class SimpleListChecker(nodes.GenericNodeVisitor):
1617 Raise `nodes.NodeFound` if non-simple list item is encountered.
1619 Here "simple" means a list item containing nothing other than a single
1620 paragraph, a simple list, or a paragraph followed by a simple list.
1623 def default_visit(self, node):
1624 raise nodes.NodeFound
1626 def visit_bullet_list(self, node):
1627 pass
1629 def visit_enumerated_list(self, node):
1630 pass
1632 def visit_list_item(self, node):
1633 children = []
1634 for child in node.children:
1635 if not isinstance(child, nodes.Invisible):
1636 children.append(child)
1637 if (children and isinstance(children[0], nodes.paragraph)
1638 and (isinstance(children[-1], nodes.bullet_list)
1639 or isinstance(children[-1], nodes.enumerated_list))):
1640 children.pop()
1641 if len(children) <= 1:
1642 return
1643 else:
1644 raise nodes.NodeFound
1646 def visit_paragraph(self, node):
1647 raise nodes.SkipNode
1649 def invisible_visit(self, node):
1650 """Invisible nodes should be ignored."""
1651 raise nodes.SkipNode
1653 visit_comment = invisible_visit
1654 visit_substitution_definition = invisible_visit
1655 visit_target = invisible_visit
1656 visit_pending = invisible_visit