Fix handling of missing stylesheets. Updated and simplied tests.
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob6d94f944fe97e31b6c37f0660cba27190b55c2dc
1 # $Id$
2 # Author: David Goodger
3 # Maintainer: docutils-develop@lists.sourceforge.net
4 # Copyright: This module has been placed in the public domain.
6 """
7 Simple HyperText Markup Language document tree Writer.
9 The output conforms to the XHTML version 1.0 Transitional DTD
10 (*almost* strict). The output contains a minimum of formatting
11 information. The cascading style sheet "html4css1.css" is required
12 for proper viewing with a modern graphical browser.
13 """
15 __docformat__ = 'reStructuredText'
18 import sys
19 import os
20 import os.path
21 import time
22 import re
23 import urllib
24 try: # check for the Python Imaging Library
25 import PIL
26 except ImportError:
27 try: # sometimes PIL modules are put in PYTHONPATH's root
28 import Image
29 class PIL(object): pass # dummy wrapper
30 PIL.Image = Image
31 except ImportError:
32 PIL = None
33 import docutils
34 from docutils import frontend, nodes, utils, writers, languages, io
35 from docutils.error_reporting import SafeString
36 from docutils.transforms import writer_aux
37 from docutils.math import unichar2tex, pick_math_environment
38 from docutils.math.latex2mathml import parse_latex_math
39 from docutils.math.math2html import math2html
41 class Writer(writers.Writer):
43 supported = ('html', 'html4css1', 'xhtml')
44 """Formats this writer supports."""
46 default_stylesheet = 'html4css1.css'
48 default_stylesheet_path = utils.relative_path(
49 os.path.join(os.getcwd(), 'dummy'),
50 os.path.join(os.path.dirname(__file__), default_stylesheet))
52 default_template = 'template.txt'
54 default_template_path = utils.relative_path(
55 os.path.join(os.getcwd(), 'dummy'),
56 os.path.join(os.path.dirname(__file__), default_template))
58 settings_spec = (
59 'HTML-Specific Options',
60 None,
61 (('Specify the template file (UTF-8 encoded). Default is "%s".'
62 % default_template_path,
63 ['--template'],
64 {'default': default_template_path, 'metavar': '<file>'}),
65 ('Specify comma separated list of stylesheet URLs. '
66 'Overrides previous --stylesheet and --stylesheet-path settings.',
67 ['--stylesheet'],
68 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
69 ('Specify comma separated list of stylesheet paths. '
70 'With --link-stylesheet, '
71 'the path is rewritten relative to the output HTML file. '
72 'Default: "%s"' % default_stylesheet_path,
73 ['--stylesheet-path'],
74 {'metavar': '<file>', 'overrides': 'stylesheet',
75 'default': default_stylesheet_path}),
76 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
77 'files must be accessible during processing. This is the default.',
78 ['--embed-stylesheet'],
79 {'default': 1, 'action': 'store_true',
80 'validator': frontend.validate_boolean}),
81 ('Link to the stylesheet(s) in the output HTML file. '
82 'Default: embed stylesheets.',
83 ['--link-stylesheet'],
84 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
85 ('Specify the initial header level. Default is 1 for "<h1>". '
86 'Does not affect document title & subtitle (see --no-doc-title).',
87 ['--initial-header-level'],
88 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
89 'metavar': '<level>'}),
90 ('Specify the maximum width (in characters) for one-column field '
91 'names. Longer field names will span an entire row of the table '
92 'used to render the field list. Default is 14 characters. '
93 'Use 0 for "no limit".',
94 ['--field-name-limit'],
95 {'default': 14, 'metavar': '<level>',
96 'validator': frontend.validate_nonnegative_int}),
97 ('Specify the maximum width (in characters) for options in option '
98 'lists. Longer options will span an entire row of the table used '
99 'to render the option list. Default is 14 characters. '
100 'Use 0 for "no limit".',
101 ['--option-limit'],
102 {'default': 14, 'metavar': '<level>',
103 'validator': frontend.validate_nonnegative_int}),
104 ('Format for footnote references: one of "superscript" or '
105 '"brackets". Default is "brackets".',
106 ['--footnote-references'],
107 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
108 'metavar': '<format>',
109 'overrides': 'trim_footnote_reference_space'}),
110 ('Format for block quote attributions: one of "dash" (em-dash '
111 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
112 ['--attribution'],
113 {'choices': ['dash', 'parentheses', 'parens', 'none'],
114 'default': 'dash', 'metavar': '<format>'}),
115 ('Remove extra vertical whitespace between items of "simple" bullet '
116 'lists and enumerated lists. Default: enabled.',
117 ['--compact-lists'],
118 {'default': 1, 'action': 'store_true',
119 'validator': frontend.validate_boolean}),
120 ('Disable compact simple bullet and enumerated lists.',
121 ['--no-compact-lists'],
122 {'dest': 'compact_lists', 'action': 'store_false'}),
123 ('Remove extra vertical whitespace between items of simple field '
124 'lists. Default: enabled.',
125 ['--compact-field-lists'],
126 {'default': 1, 'action': 'store_true',
127 'validator': frontend.validate_boolean}),
128 ('Disable compact simple field lists.',
129 ['--no-compact-field-lists'],
130 {'dest': 'compact_field_lists', 'action': 'store_false'}),
131 ('Added to standard table classes. '
132 'Defined styles: "borderless". Default: ""',
133 ['--table-style'],
134 {'default': ''}),
135 ('Math output format, one of "MathML", "HTML", "MathJax" '
136 'or "LaTeX". Default: "MathJax"',
137 ['--math-output'],
138 {'default': 'MathJax'}),
139 ('Omit the XML declaration. Use with caution.',
140 ['--no-xml-declaration'],
141 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
142 'validator': frontend.validate_boolean}),
143 ('Obfuscate email addresses to confuse harvesters while still '
144 'keeping email links usable with standards-compliant browsers.',
145 ['--cloak-email-addresses'],
146 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
148 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
150 relative_path_settings = ('stylesheet_path',)
152 config_section = 'html4css1 writer'
153 config_section_dependencies = ('writers',)
155 visitor_attributes = (
156 'head_prefix', 'head', 'stylesheet', 'body_prefix',
157 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
158 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
159 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
160 'html_body')
162 def get_transforms(self):
163 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
165 def __init__(self):
166 writers.Writer.__init__(self)
167 self.translator_class = HTMLTranslator
169 def translate(self):
170 self.visitor = visitor = self.translator_class(self.document)
171 self.document.walkabout(visitor)
172 for attr in self.visitor_attributes:
173 setattr(self, attr, getattr(visitor, attr))
174 self.output = self.apply_template()
176 def apply_template(self):
177 template_file = open(self.document.settings.template, 'rb')
178 template = unicode(template_file.read(), 'utf-8')
179 template_file.close()
180 subs = self.interpolation_dict()
181 return template % subs
183 def interpolation_dict(self):
184 subs = {}
185 settings = self.document.settings
186 for attr in self.visitor_attributes:
187 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
188 subs['encoding'] = settings.output_encoding
189 subs['version'] = docutils.__version__
190 return subs
192 def assemble_parts(self):
193 writers.Writer.assemble_parts(self)
194 for part in self.visitor_attributes:
195 self.parts[part] = ''.join(getattr(self, part))
198 class HTMLTranslator(nodes.NodeVisitor):
201 This HTML writer has been optimized to produce visually compact
202 lists (less vertical whitespace). HTML's mixed content models
203 allow list items to contain "<li><p>body elements</p></li>" or
204 "<li>just text</li>" or even "<li>text<p>and body
205 elements</p>combined</li>", each with different effects. It would
206 be best to stick with strict body elements in list items, but they
207 affect vertical spacing in browsers (although they really
208 shouldn't).
210 Here is an outline of the optimization:
212 - Check for and omit <p> tags in "simple" lists: list items
213 contain either a single paragraph, a nested simple list, or a
214 paragraph followed by a nested simple list. This means that
215 this list can be compact:
217 - Item 1.
218 - Item 2.
220 But this list cannot be compact:
222 - Item 1.
224 This second paragraph forces space between list items.
226 - Item 2.
228 - In non-list contexts, omit <p> tags on a paragraph if that
229 paragraph is the only child of its parent (footnotes & citations
230 are allowed a label first).
232 - Regardless of the above, in definitions, table cells, field bodies,
233 option descriptions, and list items, mark the first child with
234 'class="first"' and the last child with 'class="last"'. The stylesheet
235 sets the margins (top & bottom respectively) to 0 for these elements.
237 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
238 option) disables list whitespace optimization.
241 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
242 doctype = (
243 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
244 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
245 doctype_mathml = doctype
247 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
248 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
249 content_type = ('<meta http-equiv="Content-Type"'
250 ' content="text/html; charset=%s" />\n')
251 content_type_mathml = ('<meta http-equiv="Content-Type"'
252 ' content="application/xhtml+xml; charset=%s" />\n')
254 generator = ('<meta name="generator" content="Docutils %s: '
255 'http://docutils.sourceforge.net/" />\n')
257 # Template for the MathJax script in the header:
258 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
259 # The latest version of MathJax from the distributed server:
260 # avaliable to the public under the `MathJax CDN Terms of Service`__
261 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
262 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
263 'config=TeX-AMS-MML_HTMLorMML')
264 # TODO: make this configurable:
266 # a) as extra option or
267 # b) appended to math-output="MathJax"?
269 # If b), which delimiter/delimter-set (':', ',', ' ')?
271 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
272 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
273 words_and_spaces = re.compile(r'\S+| +|\n')
274 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
275 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
277 def __init__(self, document):
278 nodes.NodeVisitor.__init__(self, document)
279 self.settings = settings = document.settings
280 lcode = settings.language_code
281 self.language = languages.get_language(lcode, document.reporter)
282 self.meta = [self.generator % docutils.__version__]
283 self.head_prefix = []
284 self.html_prolog = []
285 if settings.xml_declaration:
286 self.head_prefix.append(self.xml_declaration
287 % settings.output_encoding)
288 # encoding not interpolated:
289 self.html_prolog.append(self.xml_declaration)
290 self.head = self.meta[:]
291 self.stylesheet = [self.stylesheet_call(path)
292 for path in utils.get_stylesheet_list(settings)]
293 self.body_prefix = ['</head>\n<body>\n']
294 # document title, subtitle display
295 self.body_pre_docinfo = []
296 # author, date, etc.
297 self.docinfo = []
298 self.body = []
299 self.fragment = []
300 self.body_suffix = ['</body>\n</html>\n']
301 self.section_level = 0
302 self.initial_header_level = int(settings.initial_header_level)
303 self.math_output = settings.math_output.lower()
304 # A heterogenous stack used in conjunction with the tree traversal.
305 # Make sure that the pops correspond to the pushes:
306 self.context = []
307 self.topic_classes = []
308 self.colspecs = []
309 self.compact_p = 1
310 self.compact_simple = None
311 self.compact_field_list = None
312 self.in_docinfo = None
313 self.in_sidebar = None
314 self.title = []
315 self.subtitle = []
316 self.header = []
317 self.footer = []
318 self.html_head = [self.content_type] # charset not interpolated
319 self.html_title = []
320 self.html_subtitle = []
321 self.html_body = []
322 self.in_document_title = 0
323 self.in_mailto = 0
324 self.author_in_authors = None
325 self.math_header = ''
327 def astext(self):
328 return ''.join(self.head_prefix + self.head
329 + self.stylesheet + self.body_prefix
330 + self.body_pre_docinfo + self.docinfo
331 + self.body + self.body_suffix)
333 def encode(self, text):
334 """Encode special characters in `text` & return."""
335 # @@@ A codec to do these and all other HTML entities would be nice.
336 text = unicode(text)
337 return text.translate({
338 ord('&'): u'&amp;',
339 ord('<'): u'&lt;',
340 ord('"'): u'&quot;',
341 ord('>'): u'&gt;',
342 ord('@'): u'&#64;', # may thwart some address harvesters
343 # TODO: convert non-breaking space only if needed?
344 0xa0: u'&nbsp;'}) # non-breaking space
346 def cloak_mailto(self, uri):
347 """Try to hide a mailto: URL from harvesters."""
348 # Encode "@" using a URL octet reference (see RFC 1738).
349 # Further cloaking with HTML entities will be done in the
350 # `attval` function.
351 return uri.replace('@', '%40')
353 def cloak_email(self, addr):
354 """Try to hide the link text of a email link from harversters."""
355 # Surround at-signs and periods with <span> tags. ("@" has
356 # already been encoded to "&#64;" by the `encode` method.)
357 addr = addr.replace('&#64;', '<span>&#64;</span>')
358 addr = addr.replace('.', '<span>&#46;</span>')
359 return addr
361 def attval(self, text,
362 whitespace=re.compile('[\n\r\t\v\f]')):
363 """Cleanse, HTML encode, and return attribute value text."""
364 encoded = self.encode(whitespace.sub(' ', text))
365 if self.in_mailto and self.settings.cloak_email_addresses:
366 # Cloak at-signs ("%40") and periods with HTML entities.
367 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
368 encoded = encoded.replace('.', '&#46;')
369 return encoded
371 def stylesheet_call(self, path):
372 """Return code to reference or embed stylesheet file `path`"""
373 if self.settings.embed_stylesheet:
374 try:
375 content = io.FileInput(source_path=path,
376 encoding='utf-8',
377 handle_io_errors=False).read()
378 self.settings.record_dependencies.add(path)
379 except IOError, err:
380 msg = u"Cannot embed stylesheet '%s': %s." % (
381 path, SafeString(err.strerror))
382 self.document.reporter.error(msg)
383 return '<--- %s --->\n' % msg
384 return self.embedded_stylesheet % content
385 # else link to style file:
386 if self.settings.stylesheet_path:
387 # adapt path relative to output (cf. config.html#stylesheet-path)
388 path = utils.relative_path(self.settings._destination, path)
389 return self.stylesheet_link % self.encode(path)
391 def starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
393 Construct and return a start tag given a node (id & class attributes
394 are extracted), tag name, and optional attributes.
396 tagname = tagname.lower()
397 prefix = []
398 atts = {}
399 ids = []
400 for (name, value) in attributes.items():
401 atts[name.lower()] = value
402 classes = node.get('classes', [])
403 if 'class' in atts:
404 classes.append(atts.pop('class'))
405 # move language specification to 'lang' attribute
406 languages = [cls for cls in classes
407 if cls.startswith('language-')]
408 if languages:
409 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
410 atts[self.lang_attribute] = languages[0][9:]
411 classes.pop(classes.index(languages[0]))
412 classes = ' '.join(classes).strip()
413 if classes:
414 atts['class'] = classes
415 assert 'id' not in atts
416 ids.extend(node.get('ids', []))
417 if 'ids' in atts:
418 ids.extend(atts['ids'])
419 del atts['ids']
420 if ids:
421 atts['id'] = ids[0]
422 for id in ids[1:]:
423 # Add empty "span" elements for additional IDs. Note
424 # that we cannot use empty "a" elements because there
425 # may be targets inside of references, but nested "a"
426 # elements aren't allowed in XHTML (even if they do
427 # not all have a "href" attribute).
428 if empty:
429 # Empty tag. Insert target right in front of element.
430 prefix.append('<span id="%s"></span>' % id)
431 else:
432 # Non-empty tag. Place the auxiliary <span> tag
433 # *inside* the element, as the first child.
434 suffix += '<span id="%s"></span>' % id
435 attlist = atts.items()
436 attlist.sort()
437 parts = [tagname]
438 for name, value in attlist:
439 # value=None was used for boolean attributes without
440 # value, but this isn't supported by XHTML.
441 assert value is not None
442 if isinstance(value, list):
443 values = [unicode(v) for v in value]
444 parts.append('%s="%s"' % (name.lower(),
445 self.attval(' '.join(values))))
446 else:
447 parts.append('%s="%s"' % (name.lower(),
448 self.attval(unicode(value))))
449 if empty:
450 infix = ' /'
451 else:
452 infix = ''
453 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
455 def emptytag(self, node, tagname, suffix='\n', **attributes):
456 """Construct and return an XML-compatible empty tag."""
457 return self.starttag(node, tagname, suffix, empty=1, **attributes)
459 def set_class_on_child(self, node, class_, index=0):
461 Set class `class_` on the visible child no. index of `node`.
462 Do nothing if node has fewer children than `index`.
464 children = [n for n in node if not isinstance(n, nodes.Invisible)]
465 try:
466 child = children[index]
467 except IndexError:
468 return
469 child['classes'].append(class_)
471 def set_first_last(self, node):
472 self.set_class_on_child(node, 'first', 0)
473 self.set_class_on_child(node, 'last', -1)
475 def visit_Text(self, node):
476 text = node.astext()
477 encoded = self.encode(text)
478 if self.in_mailto and self.settings.cloak_email_addresses:
479 encoded = self.cloak_email(encoded)
480 self.body.append(encoded)
482 def depart_Text(self, node):
483 pass
485 def visit_abbreviation(self, node):
486 # @@@ implementation incomplete ("title" attribute)
487 self.body.append(self.starttag(node, 'abbr', ''))
489 def depart_abbreviation(self, node):
490 self.body.append('</abbr>')
492 def visit_acronym(self, node):
493 # @@@ implementation incomplete ("title" attribute)
494 self.body.append(self.starttag(node, 'acronym', ''))
496 def depart_acronym(self, node):
497 self.body.append('</acronym>')
499 def visit_address(self, node):
500 self.visit_docinfo_item(node, 'address', meta=None)
501 self.body.append(self.starttag(node, 'pre', CLASS='address'))
503 def depart_address(self, node):
504 self.body.append('\n</pre>\n')
505 self.depart_docinfo_item()
507 def visit_admonition(self, node):
508 self.body.append(self.starttag(node, 'div'))
509 self.set_first_last(node)
511 def depart_admonition(self, node=None):
512 self.body.append('</div>\n')
514 attribution_formats = {'dash': ('&mdash;', ''),
515 'parentheses': ('(', ')'),
516 'parens': ('(', ')'),
517 'none': ('', '')}
519 def visit_attribution(self, node):
520 prefix, suffix = self.attribution_formats[self.settings.attribution]
521 self.context.append(suffix)
522 self.body.append(
523 self.starttag(node, 'p', prefix, CLASS='attribution'))
525 def depart_attribution(self, node):
526 self.body.append(self.context.pop() + '</p>\n')
528 def visit_author(self, node):
529 if isinstance(node.parent, nodes.authors):
530 if self.author_in_authors:
531 self.body.append('\n<br />')
532 else:
533 self.visit_docinfo_item(node, 'author')
535 def depart_author(self, node):
536 if isinstance(node.parent, nodes.authors):
537 self.author_in_authors += 1
538 else:
539 self.depart_docinfo_item()
541 def visit_authors(self, node):
542 self.visit_docinfo_item(node, 'authors')
543 self.author_in_authors = 0 # initialize counter
545 def depart_authors(self, node):
546 self.depart_docinfo_item()
547 self.author_in_authors = None
549 def visit_block_quote(self, node):
550 self.body.append(self.starttag(node, 'blockquote'))
552 def depart_block_quote(self, node):
553 self.body.append('</blockquote>\n')
555 def check_simple_list(self, node):
556 """Check for a simple list that can be rendered compactly."""
557 visitor = SimpleListChecker(self.document)
558 try:
559 node.walk(visitor)
560 except nodes.NodeFound:
561 return None
562 else:
563 return 1
565 def is_compactable(self, node):
566 return ('compact' in node['classes']
567 or (self.settings.compact_lists
568 and 'open' not in node['classes']
569 and (self.compact_simple
570 or self.topic_classes == ['contents']
571 or self.check_simple_list(node))))
573 def visit_bullet_list(self, node):
574 atts = {}
575 old_compact_simple = self.compact_simple
576 self.context.append((self.compact_simple, self.compact_p))
577 self.compact_p = None
578 self.compact_simple = self.is_compactable(node)
579 if self.compact_simple and not old_compact_simple:
580 atts['class'] = 'simple'
581 self.body.append(self.starttag(node, 'ul', **atts))
583 def depart_bullet_list(self, node):
584 self.compact_simple, self.compact_p = self.context.pop()
585 self.body.append('</ul>\n')
587 def visit_caption(self, node):
588 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
590 def depart_caption(self, node):
591 self.body.append('</p>\n')
593 def visit_citation(self, node):
594 self.body.append(self.starttag(node, 'table',
595 CLASS='docutils citation',
596 frame="void", rules="none"))
597 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
598 '<tbody valign="top">\n'
599 '<tr>')
600 self.footnote_backrefs(node)
602 def depart_citation(self, node):
603 self.body.append('</td></tr>\n'
604 '</tbody>\n</table>\n')
606 def visit_citation_reference(self, node):
607 href = '#' + node['refid']
608 self.body.append(self.starttag(
609 node, 'a', '[', CLASS='citation-reference', href=href))
611 def depart_citation_reference(self, node):
612 self.body.append(']</a>')
614 def visit_classifier(self, node):
615 self.body.append(' <span class="classifier-delimiter">:</span> ')
616 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
618 def depart_classifier(self, node):
619 self.body.append('</span>')
621 def visit_colspec(self, node):
622 self.colspecs.append(node)
623 # "stubs" list is an attribute of the tgroup element:
624 node.parent.stubs.append(node.attributes.get('stub'))
626 def depart_colspec(self, node):
627 pass
629 def write_colspecs(self):
630 width = 0
631 for node in self.colspecs:
632 width += node['colwidth']
633 for node in self.colspecs:
634 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
635 self.body.append(self.emptytag(node, 'col',
636 width='%i%%' % colwidth))
637 self.colspecs = []
639 def visit_comment(self, node,
640 sub=re.compile('-(?=-)').sub):
641 """Escape double-dashes in comment text."""
642 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
643 # Content already processed:
644 raise nodes.SkipNode
646 def visit_compound(self, node):
647 self.body.append(self.starttag(node, 'div', CLASS='compound'))
648 if len(node) > 1:
649 node[0]['classes'].append('compound-first')
650 node[-1]['classes'].append('compound-last')
651 for child in node[1:-1]:
652 child['classes'].append('compound-middle')
654 def depart_compound(self, node):
655 self.body.append('</div>\n')
657 def visit_container(self, node):
658 self.body.append(self.starttag(node, 'div', CLASS='container'))
660 def depart_container(self, node):
661 self.body.append('</div>\n')
663 def visit_contact(self, node):
664 self.visit_docinfo_item(node, 'contact', meta=None)
666 def depart_contact(self, node):
667 self.depart_docinfo_item()
669 def visit_copyright(self, node):
670 self.visit_docinfo_item(node, 'copyright')
672 def depart_copyright(self, node):
673 self.depart_docinfo_item()
675 def visit_date(self, node):
676 self.visit_docinfo_item(node, 'date')
678 def depart_date(self, node):
679 self.depart_docinfo_item()
681 def visit_decoration(self, node):
682 pass
684 def depart_decoration(self, node):
685 pass
687 def visit_definition(self, node):
688 self.body.append('</dt>\n')
689 self.body.append(self.starttag(node, 'dd', ''))
690 self.set_first_last(node)
692 def depart_definition(self, node):
693 self.body.append('</dd>\n')
695 def visit_definition_list(self, node):
696 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
698 def depart_definition_list(self, node):
699 self.body.append('</dl>\n')
701 def visit_definition_list_item(self, node):
702 pass
704 def depart_definition_list_item(self, node):
705 pass
707 def visit_description(self, node):
708 self.body.append(self.starttag(node, 'td', ''))
709 self.set_first_last(node)
711 def depart_description(self, node):
712 self.body.append('</td>')
714 def visit_docinfo(self, node):
715 self.context.append(len(self.body))
716 self.body.append(self.starttag(node, 'table',
717 CLASS='docinfo',
718 frame="void", rules="none"))
719 self.body.append('<col class="docinfo-name" />\n'
720 '<col class="docinfo-content" />\n'
721 '<tbody valign="top">\n')
722 self.in_docinfo = 1
724 def depart_docinfo(self, node):
725 self.body.append('</tbody>\n</table>\n')
726 self.in_docinfo = None
727 start = self.context.pop()
728 self.docinfo = self.body[start:]
729 self.body = []
731 def visit_docinfo_item(self, node, name, meta=1):
732 if meta:
733 meta_tag = '<meta name="%s" content="%s" />\n' \
734 % (name, self.attval(node.astext()))
735 self.add_meta(meta_tag)
736 self.body.append(self.starttag(node, 'tr', ''))
737 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
738 % self.language.labels[name])
739 if len(node):
740 if isinstance(node[0], nodes.Element):
741 node[0]['classes'].append('first')
742 if isinstance(node[-1], nodes.Element):
743 node[-1]['classes'].append('last')
745 def depart_docinfo_item(self):
746 self.body.append('</td></tr>\n')
748 def visit_doctest_block(self, node):
749 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
751 def depart_doctest_block(self, node):
752 self.body.append('\n</pre>\n')
754 def visit_document(self, node):
755 self.head.append('<title>%s</title>\n'
756 % self.encode(node.get('title', '')))
758 def depart_document(self, node):
759 self.head_prefix.extend([self.doctype,
760 self.head_prefix_template %
761 {'lang': self.settings.language_code}])
762 self.html_prolog.append(self.doctype)
763 self.meta.insert(0, self.content_type % self.settings.output_encoding)
764 self.head.insert(0, self.content_type % self.settings.output_encoding)
765 if self.math_header:
766 self.head.append(self.math_header)
767 # skip content-type meta tag with interpolated charset value:
768 self.html_head.extend(self.head[1:])
769 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
770 self.body_suffix.insert(0, '</div>\n')
771 self.fragment.extend(self.body) # self.fragment is the "naked" body
772 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
773 + self.docinfo + self.body
774 + self.body_suffix[:-1])
775 assert not self.context, 'len(context) = %s' % len(self.context)
777 def visit_emphasis(self, node):
778 self.body.append(self.starttag(node, 'em', ''))
780 def depart_emphasis(self, node):
781 self.body.append('</em>')
783 def visit_entry(self, node):
784 atts = {'class': []}
785 if isinstance(node.parent.parent, nodes.thead):
786 atts['class'].append('head')
787 if node.parent.parent.parent.stubs[node.parent.column]:
788 # "stubs" list is an attribute of the tgroup element
789 atts['class'].append('stub')
790 if atts['class']:
791 tagname = 'th'
792 atts['class'] = ' '.join(atts['class'])
793 else:
794 tagname = 'td'
795 del atts['class']
796 node.parent.column += 1
797 if 'morerows' in node:
798 atts['rowspan'] = node['morerows'] + 1
799 if 'morecols' in node:
800 atts['colspan'] = node['morecols'] + 1
801 node.parent.column += node['morecols']
802 self.body.append(self.starttag(node, tagname, '', **atts))
803 self.context.append('</%s>\n' % tagname.lower())
804 if len(node) == 0: # empty cell
805 self.body.append('&nbsp;')
806 self.set_first_last(node)
808 def depart_entry(self, node):
809 self.body.append(self.context.pop())
811 def visit_enumerated_list(self, node):
813 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
814 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
815 usable.
817 atts = {}
818 if 'start' in node:
819 atts['start'] = node['start']
820 if 'enumtype' in node:
821 atts['class'] = node['enumtype']
822 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
823 # single "format" attribute? Use CSS2?
824 old_compact_simple = self.compact_simple
825 self.context.append((self.compact_simple, self.compact_p))
826 self.compact_p = None
827 self.compact_simple = self.is_compactable(node)
828 if self.compact_simple and not old_compact_simple:
829 atts['class'] = (atts.get('class', '') + ' simple').strip()
830 self.body.append(self.starttag(node, 'ol', **atts))
832 def depart_enumerated_list(self, node):
833 self.compact_simple, self.compact_p = self.context.pop()
834 self.body.append('</ol>\n')
836 def visit_field(self, node):
837 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
839 def depart_field(self, node):
840 self.body.append('</tr>\n')
842 def visit_field_body(self, node):
843 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
844 self.set_class_on_child(node, 'first', 0)
845 field = node.parent
846 if (self.compact_field_list or
847 isinstance(field.parent, nodes.docinfo) or
848 field.parent.index(field) == len(field.parent) - 1):
849 # If we are in a compact list, the docinfo, or if this is
850 # the last field of the field list, do not add vertical
851 # space after last element.
852 self.set_class_on_child(node, 'last', -1)
854 def depart_field_body(self, node):
855 self.body.append('</td>\n')
857 def visit_field_list(self, node):
858 self.context.append((self.compact_field_list, self.compact_p))
859 self.compact_p = None
860 if 'compact' in node['classes']:
861 self.compact_field_list = 1
862 elif (self.settings.compact_field_lists
863 and 'open' not in node['classes']):
864 self.compact_field_list = 1
865 if self.compact_field_list:
866 for field in node:
867 field_body = field[-1]
868 assert isinstance(field_body, nodes.field_body)
869 children = [n for n in field_body
870 if not isinstance(n, nodes.Invisible)]
871 if not (len(children) == 0 or
872 len(children) == 1 and
873 isinstance(children[0],
874 (nodes.paragraph, nodes.line_block))):
875 self.compact_field_list = 0
876 break
877 self.body.append(self.starttag(node, 'table', frame='void',
878 rules='none',
879 CLASS='docutils field-list'))
880 self.body.append('<col class="field-name" />\n'
881 '<col class="field-body" />\n'
882 '<tbody valign="top">\n')
884 def depart_field_list(self, node):
885 self.body.append('</tbody>\n</table>\n')
886 self.compact_field_list, self.compact_p = self.context.pop()
888 def visit_field_name(self, node):
889 atts = {}
890 if self.in_docinfo:
891 atts['class'] = 'docinfo-name'
892 else:
893 atts['class'] = 'field-name'
894 if ( self.settings.field_name_limit
895 and len(node.astext()) > self.settings.field_name_limit):
896 atts['colspan'] = 2
897 self.context.append('</tr>\n'
898 + self.starttag(node.parent, 'tr', '')
899 + '<td>&nbsp;</td>')
900 else:
901 self.context.append('')
902 self.body.append(self.starttag(node, 'th', '', **atts))
904 def depart_field_name(self, node):
905 self.body.append(':</th>')
906 self.body.append(self.context.pop())
908 def visit_figure(self, node):
909 atts = {'class': 'figure'}
910 if node.get('width'):
911 atts['style'] = 'width: %s' % node['width']
912 if node.get('align'):
913 atts['class'] += " align-" + node['align']
914 self.body.append(self.starttag(node, 'div', **atts))
916 def depart_figure(self, node):
917 self.body.append('</div>\n')
919 def visit_footer(self, node):
920 self.context.append(len(self.body))
922 def depart_footer(self, node):
923 start = self.context.pop()
924 footer = [self.starttag(node, 'div', CLASS='footer'),
925 '<hr class="footer" />\n']
926 footer.extend(self.body[start:])
927 footer.append('\n</div>\n')
928 self.footer.extend(footer)
929 self.body_suffix[:0] = footer
930 del self.body[start:]
932 def visit_footnote(self, node):
933 self.body.append(self.starttag(node, 'table',
934 CLASS='docutils footnote',
935 frame="void", rules="none"))
936 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
937 '<tbody valign="top">\n'
938 '<tr>')
939 self.footnote_backrefs(node)
941 def footnote_backrefs(self, node):
942 backlinks = []
943 backrefs = node['backrefs']
944 if self.settings.footnote_backlinks and backrefs:
945 if len(backrefs) == 1:
946 self.context.append('')
947 self.context.append('</a>')
948 self.context.append('<a class="fn-backref" href="#%s">'
949 % backrefs[0])
950 else:
951 i = 1
952 for backref in backrefs:
953 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
954 % (backref, i))
955 i += 1
956 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
957 self.context += ['', '']
958 else:
959 self.context.append('')
960 self.context += ['', '']
961 # If the node does not only consist of a label.
962 if len(node) > 1:
963 # If there are preceding backlinks, we do not set class
964 # 'first', because we need to retain the top-margin.
965 if not backlinks:
966 node[1]['classes'].append('first')
967 node[-1]['classes'].append('last')
969 def depart_footnote(self, node):
970 self.body.append('</td></tr>\n'
971 '</tbody>\n</table>\n')
973 def visit_footnote_reference(self, node):
974 href = '#' + node['refid']
975 format = self.settings.footnote_references
976 if format == 'brackets':
977 suffix = '['
978 self.context.append(']')
979 else:
980 assert format == 'superscript'
981 suffix = '<sup>'
982 self.context.append('</sup>')
983 self.body.append(self.starttag(node, 'a', suffix,
984 CLASS='footnote-reference', href=href))
986 def depart_footnote_reference(self, node):
987 self.body.append(self.context.pop() + '</a>')
989 def visit_generated(self, node):
990 pass
992 def depart_generated(self, node):
993 pass
995 def visit_header(self, node):
996 self.context.append(len(self.body))
998 def depart_header(self, node):
999 start = self.context.pop()
1000 header = [self.starttag(node, 'div', CLASS='header')]
1001 header.extend(self.body[start:])
1002 header.append('\n<hr class="header"/>\n</div>\n')
1003 self.body_prefix.extend(header)
1004 self.header.extend(header)
1005 del self.body[start:]
1007 def visit_image(self, node):
1008 atts = {}
1009 uri = node['uri']
1010 # place SVG and SWF images in an <object> element
1011 types = {'.svg': 'image/svg+xml',
1012 '.swf': 'application/x-shockwave-flash'}
1013 ext = os.path.splitext(uri)[1].lower()
1014 if ext in ('.svg', '.swf'):
1015 atts['data'] = uri
1016 atts['type'] = types[ext]
1017 else:
1018 atts['src'] = uri
1019 atts['alt'] = node.get('alt', uri)
1020 # image size
1021 if 'width' in node:
1022 atts['width'] = node['width']
1023 if 'height' in node:
1024 atts['height'] = node['height']
1025 if 'scale' in node:
1026 if (PIL and not ('width' in node and 'height' in node)
1027 and self.settings.file_insertion_enabled):
1028 imagepath = urllib.url2pathname(uri)
1029 try:
1030 img = PIL.Image.open(
1031 imagepath.encode(sys.getfilesystemencoding()))
1032 except (IOError, UnicodeEncodeError):
1033 pass # TODO: warn?
1034 else:
1035 self.settings.record_dependencies.add(
1036 imagepath.replace('\\', '/'))
1037 if 'width' not in atts:
1038 atts['width'] = str(img.size[0])
1039 if 'height' not in atts:
1040 atts['height'] = str(img.size[1])
1041 del img
1042 for att_name in 'width', 'height':
1043 if att_name in atts:
1044 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1045 assert match
1046 atts[att_name] = '%s%s' % (
1047 float(match.group(1)) * (float(node['scale']) / 100),
1048 match.group(2))
1049 style = []
1050 for att_name in 'width', 'height':
1051 if att_name in atts:
1052 if re.match(r'^[0-9.]+$', atts[att_name]):
1053 # Interpret unitless values as pixels.
1054 atts[att_name] += 'px'
1055 style.append('%s: %s;' % (att_name, atts[att_name]))
1056 del atts[att_name]
1057 if style:
1058 atts['style'] = ' '.join(style)
1059 if (isinstance(node.parent, nodes.TextElement) or
1060 (isinstance(node.parent, nodes.reference) and
1061 not isinstance(node.parent.parent, nodes.TextElement))):
1062 # Inline context or surrounded by <a>...</a>.
1063 suffix = ''
1064 else:
1065 suffix = '\n'
1066 if 'align' in node:
1067 atts['class'] = 'align-%s' % node['align']
1068 self.context.append('')
1069 if ext in ('.svg', '.swf'): # place in an object element,
1070 # do NOT use an empty tag: incorrect rendering in browsers
1071 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1072 node.get('alt', uri) + '</object>' + suffix)
1073 else:
1074 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1076 def depart_image(self, node):
1077 self.body.append(self.context.pop())
1079 def visit_inline(self, node):
1080 self.body.append(self.starttag(node, 'span', ''))
1082 def depart_inline(self, node):
1083 self.body.append('</span>')
1085 def visit_label(self, node):
1086 # Context added in footnote_backrefs.
1087 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1088 CLASS='label'))
1090 def depart_label(self, node):
1091 # Context added in footnote_backrefs.
1092 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1094 def visit_legend(self, node):
1095 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1097 def depart_legend(self, node):
1098 self.body.append('</div>\n')
1100 def visit_line(self, node):
1101 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1102 if not len(node):
1103 self.body.append('<br />')
1105 def depart_line(self, node):
1106 self.body.append('</div>\n')
1108 def visit_line_block(self, node):
1109 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1111 def depart_line_block(self, node):
1112 self.body.append('</div>\n')
1114 def visit_list_item(self, node):
1115 self.body.append(self.starttag(node, 'li', ''))
1116 if len(node):
1117 node[0]['classes'].append('first')
1119 def depart_list_item(self, node):
1120 self.body.append('</li>\n')
1122 def visit_literal(self, node):
1123 """Process text to prevent tokens from wrapping."""
1124 self.body.append(
1125 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1126 text = node.astext()
1127 for token in self.words_and_spaces.findall(text):
1128 if token.strip():
1129 # Protect text like "--an-option" and the regular expression
1130 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1131 if self.sollbruchstelle.search(token):
1132 self.body.append('<span class="pre">%s</span>'
1133 % self.encode(token))
1134 else:
1135 self.body.append(self.encode(token))
1136 elif token in ('\n', ' '):
1137 # Allow breaks at whitespace:
1138 self.body.append(token)
1139 else:
1140 # Protect runs of multiple spaces; the last space can wrap:
1141 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1142 self.body.append('</tt>')
1143 # Content already processed:
1144 raise nodes.SkipNode
1146 def visit_literal_block(self, node):
1147 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1149 def depart_literal_block(self, node):
1150 self.body.append('\n</pre>\n')
1152 def visit_math(self, node, math_env=''):
1153 # As there is no native HTML math support, we provide alternatives:
1154 # LaTeX and MathJax math_output modes simply wrap the content,
1155 # HTML and MathML math_output modes also convert the math_code.
1156 # If the method is called from visit_math_block(), math_env != ''.
1158 # HTML container
1159 tags = {# math_output: (block, inline, class-arguments)
1160 'mathml': ('div', '', ''),
1161 'html': ('div', 'span', 'formula'),
1162 'mathjax': ('div', 'span', 'math'),
1163 'latex': ('pre', 'tt', 'math'),
1165 tag = tags[self.math_output][math_env == '']
1166 clsarg = tags[self.math_output][2]
1167 # LaTeX container
1168 wrappers = {# math_mode: (inline, block)
1169 'mathml': (None, None),
1170 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1171 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1172 'latex': (None, None),
1174 wrapper = wrappers[self.math_output][math_env != '']
1175 # get and wrap content
1176 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1177 if wrapper and math_env:
1178 math_code = wrapper % (math_env, math_code, math_env)
1179 elif wrapper:
1180 math_code = wrapper % math_code
1181 # settings and conversion
1182 if self.math_output in ('latex', 'mathjax'):
1183 math_code = self.encode(math_code)
1184 if self.math_output == 'mathjax':
1185 self.math_header = self.mathjax_script % self.mathjax_url
1186 elif self.math_output == 'html':
1187 math_code = math2html(math_code)
1188 elif self.math_output == 'mathml':
1189 self.doctype = self.doctype_mathml
1190 self.content_type = self.content_type_mathml
1191 try:
1192 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1193 math_code = ''.join(mathml_tree.xml())
1194 except SyntaxError, err:
1195 err_node = self.document.reporter.error(err, base_node=node)
1196 self.visit_system_message(err_node)
1197 self.body.append(self.starttag(node, 'p'))
1198 self.body.append(u','.join(err.args))
1199 self.body.append('</p>\n')
1200 self.body.append(self.starttag(node, 'pre',
1201 CLASS='literal-block'))
1202 self.body.append(self.encode(math_code))
1203 self.body.append('\n</pre>\n')
1204 self.depart_system_message(err_node)
1205 raise nodes.SkipNode
1206 # append to document body
1207 if tag:
1208 self.body.append(self.starttag(node, tag, CLASS=clsarg))
1209 self.body.append(math_code)
1210 if math_env:
1211 self.body.append('\n')
1212 if tag:
1213 self.body.append('</%s>\n' % tag)
1214 # Content already processed:
1215 raise nodes.SkipNode
1217 def depart_math(self, node):
1218 pass # never reached
1220 def visit_math_block(self, node):
1221 # print node.astext().encode('utf8')
1222 math_env = pick_math_environment(node.astext())
1223 self.visit_math(node, math_env=math_env)
1225 def depart_math_block(self, node):
1226 pass # never reached
1228 def visit_meta(self, node):
1229 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1230 self.add_meta(meta)
1232 def depart_meta(self, node):
1233 pass
1235 def add_meta(self, tag):
1236 self.meta.append(tag)
1237 self.head.append(tag)
1239 def visit_option(self, node):
1240 if self.context[-1]:
1241 self.body.append(', ')
1242 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1244 def depart_option(self, node):
1245 self.body.append('</span>')
1246 self.context[-1] += 1
1248 def visit_option_argument(self, node):
1249 self.body.append(node.get('delimiter', ' '))
1250 self.body.append(self.starttag(node, 'var', ''))
1252 def depart_option_argument(self, node):
1253 self.body.append('</var>')
1255 def visit_option_group(self, node):
1256 atts = {}
1257 if ( self.settings.option_limit
1258 and len(node.astext()) > self.settings.option_limit):
1259 atts['colspan'] = 2
1260 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1261 else:
1262 self.context.append('')
1263 self.body.append(
1264 self.starttag(node, 'td', CLASS='option-group', **atts))
1265 self.body.append('<kbd>')
1266 self.context.append(0) # count number of options
1268 def depart_option_group(self, node):
1269 self.context.pop()
1270 self.body.append('</kbd></td>\n')
1271 self.body.append(self.context.pop())
1273 def visit_option_list(self, node):
1274 self.body.append(
1275 self.starttag(node, 'table', CLASS='docutils option-list',
1276 frame="void", rules="none"))
1277 self.body.append('<col class="option" />\n'
1278 '<col class="description" />\n'
1279 '<tbody valign="top">\n')
1281 def depart_option_list(self, node):
1282 self.body.append('</tbody>\n</table>\n')
1284 def visit_option_list_item(self, node):
1285 self.body.append(self.starttag(node, 'tr', ''))
1287 def depart_option_list_item(self, node):
1288 self.body.append('</tr>\n')
1290 def visit_option_string(self, node):
1291 pass
1293 def depart_option_string(self, node):
1294 pass
1296 def visit_organization(self, node):
1297 self.visit_docinfo_item(node, 'organization')
1299 def depart_organization(self, node):
1300 self.depart_docinfo_item()
1302 def should_be_compact_paragraph(self, node):
1304 Determine if the <p> tags around paragraph ``node`` can be omitted.
1306 if (isinstance(node.parent, nodes.document) or
1307 isinstance(node.parent, nodes.compound)):
1308 # Never compact paragraphs in document or compound.
1309 return 0
1310 for key, value in node.attlist():
1311 if (node.is_not_default(key) and
1312 not (key == 'classes' and value in
1313 ([], ['first'], ['last'], ['first', 'last']))):
1314 # Attribute which needs to survive.
1315 return 0
1316 first = isinstance(node.parent[0], nodes.label) # skip label
1317 for child in node.parent.children[first:]:
1318 # only first paragraph can be compact
1319 if isinstance(child, nodes.Invisible):
1320 continue
1321 if child is node:
1322 break
1323 return 0
1324 parent_length = len([n for n in node.parent if not isinstance(
1325 n, (nodes.Invisible, nodes.label))])
1326 if ( self.compact_simple
1327 or self.compact_field_list
1328 or self.compact_p and parent_length == 1):
1329 return 1
1330 return 0
1332 def visit_paragraph(self, node):
1333 if self.should_be_compact_paragraph(node):
1334 self.context.append('')
1335 else:
1336 self.body.append(self.starttag(node, 'p', ''))
1337 self.context.append('</p>\n')
1339 def depart_paragraph(self, node):
1340 self.body.append(self.context.pop())
1342 def visit_problematic(self, node):
1343 if node.hasattr('refid'):
1344 self.body.append('<a href="#%s">' % node['refid'])
1345 self.context.append('</a>')
1346 else:
1347 self.context.append('')
1348 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1350 def depart_problematic(self, node):
1351 self.body.append('</span>')
1352 self.body.append(self.context.pop())
1354 def visit_raw(self, node):
1355 if 'html' in node.get('format', '').split():
1356 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1357 if node['classes']:
1358 self.body.append(self.starttag(node, t, suffix=''))
1359 self.body.append(node.astext())
1360 if node['classes']:
1361 self.body.append('</%s>' % t)
1362 # Keep non-HTML raw text out of output:
1363 raise nodes.SkipNode
1365 def visit_reference(self, node):
1366 atts = {'class': 'reference'}
1367 if 'refuri' in node:
1368 atts['href'] = node['refuri']
1369 if ( self.settings.cloak_email_addresses
1370 and atts['href'].startswith('mailto:')):
1371 atts['href'] = self.cloak_mailto(atts['href'])
1372 self.in_mailto = 1
1373 atts['class'] += ' external'
1374 else:
1375 assert 'refid' in node, \
1376 'References must have "refuri" or "refid" attribute.'
1377 atts['href'] = '#' + node['refid']
1378 atts['class'] += ' internal'
1379 if not isinstance(node.parent, nodes.TextElement):
1380 assert len(node) == 1 and isinstance(node[0], nodes.image)
1381 atts['class'] += ' image-reference'
1382 self.body.append(self.starttag(node, 'a', '', **atts))
1384 def depart_reference(self, node):
1385 self.body.append('</a>')
1386 if not isinstance(node.parent, nodes.TextElement):
1387 self.body.append('\n')
1388 self.in_mailto = 0
1390 def visit_revision(self, node):
1391 self.visit_docinfo_item(node, 'revision', meta=None)
1393 def depart_revision(self, node):
1394 self.depart_docinfo_item()
1396 def visit_row(self, node):
1397 self.body.append(self.starttag(node, 'tr', ''))
1398 node.column = 0
1400 def depart_row(self, node):
1401 self.body.append('</tr>\n')
1403 def visit_rubric(self, node):
1404 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1406 def depart_rubric(self, node):
1407 self.body.append('</p>\n')
1409 def visit_section(self, node):
1410 self.section_level += 1
1411 self.body.append(
1412 self.starttag(node, 'div', CLASS='section'))
1414 def depart_section(self, node):
1415 self.section_level -= 1
1416 self.body.append('</div>\n')
1418 def visit_sidebar(self, node):
1419 self.body.append(
1420 self.starttag(node, 'div', CLASS='sidebar'))
1421 self.set_first_last(node)
1422 self.in_sidebar = 1
1424 def depart_sidebar(self, node):
1425 self.body.append('</div>\n')
1426 self.in_sidebar = None
1428 def visit_status(self, node):
1429 self.visit_docinfo_item(node, 'status', meta=None)
1431 def depart_status(self, node):
1432 self.depart_docinfo_item()
1434 def visit_strong(self, node):
1435 self.body.append(self.starttag(node, 'strong', ''))
1437 def depart_strong(self, node):
1438 self.body.append('</strong>')
1440 def visit_subscript(self, node):
1441 self.body.append(self.starttag(node, 'sub', ''))
1443 def depart_subscript(self, node):
1444 self.body.append('</sub>')
1446 def visit_substitution_definition(self, node):
1447 """Internal only."""
1448 raise nodes.SkipNode
1450 def visit_substitution_reference(self, node):
1451 self.unimplemented_visit(node)
1453 def visit_subtitle(self, node):
1454 if isinstance(node.parent, nodes.sidebar):
1455 self.body.append(self.starttag(node, 'p', '',
1456 CLASS='sidebar-subtitle'))
1457 self.context.append('</p>\n')
1458 elif isinstance(node.parent, nodes.document):
1459 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1460 self.context.append('</h2>\n')
1461 self.in_document_title = len(self.body)
1462 elif isinstance(node.parent, nodes.section):
1463 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1464 self.body.append(
1465 self.starttag(node, tag, '', CLASS='section-subtitle') +
1466 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1467 self.context.append('</span></%s>\n' % tag)
1469 def depart_subtitle(self, node):
1470 self.body.append(self.context.pop())
1471 if self.in_document_title:
1472 self.subtitle = self.body[self.in_document_title:-1]
1473 self.in_document_title = 0
1474 self.body_pre_docinfo.extend(self.body)
1475 self.html_subtitle.extend(self.body)
1476 del self.body[:]
1478 def visit_superscript(self, node):
1479 self.body.append(self.starttag(node, 'sup', ''))
1481 def depart_superscript(self, node):
1482 self.body.append('</sup>')
1484 def visit_system_message(self, node):
1485 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1486 self.body.append('<p class="system-message-title">')
1487 backref_text = ''
1488 if len(node['backrefs']):
1489 backrefs = node['backrefs']
1490 if len(backrefs) == 1:
1491 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1492 % backrefs[0])
1493 else:
1494 i = 1
1495 backlinks = []
1496 for backref in backrefs:
1497 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1498 i += 1
1499 backref_text = ('; <em>backlinks: %s</em>'
1500 % ', '.join(backlinks))
1501 if node.hasattr('line'):
1502 line = ', line %s' % node['line']
1503 else:
1504 line = ''
1505 self.body.append('System Message: %s/%s '
1506 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1507 % (node['type'], node['level'],
1508 self.encode(node['source']), line, backref_text))
1510 def depart_system_message(self, node):
1511 self.body.append('</div>\n')
1513 def visit_table(self, node):
1514 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1515 self.body.append(
1516 self.starttag(node, 'table', CLASS=classes, border="1"))
1518 def depart_table(self, node):
1519 self.body.append('</table>\n')
1521 def visit_target(self, node):
1522 if not ('refuri' in node or 'refid' in node
1523 or 'refname' in node):
1524 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1525 self.context.append('</span>')
1526 else:
1527 self.context.append('')
1529 def depart_target(self, node):
1530 self.body.append(self.context.pop())
1532 def visit_tbody(self, node):
1533 self.write_colspecs()
1534 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1535 self.body.append(self.starttag(node, 'tbody', valign='top'))
1537 def depart_tbody(self, node):
1538 self.body.append('</tbody>\n')
1540 def visit_term(self, node):
1541 self.body.append(self.starttag(node, 'dt', ''))
1543 def depart_term(self, node):
1545 Leave the end tag to `self.visit_definition()`, in case there's a
1546 classifier.
1548 pass
1550 def visit_tgroup(self, node):
1551 # Mozilla needs <colgroup>:
1552 self.body.append(self.starttag(node, 'colgroup'))
1553 # Appended by thead or tbody:
1554 self.context.append('</colgroup>\n')
1555 node.stubs = []
1557 def depart_tgroup(self, node):
1558 pass
1560 def visit_thead(self, node):
1561 self.write_colspecs()
1562 self.body.append(self.context.pop()) # '</colgroup>\n'
1563 # There may or may not be a <thead>; this is for <tbody> to use:
1564 self.context.append('')
1565 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1567 def depart_thead(self, node):
1568 self.body.append('</thead>\n')
1570 def visit_title(self, node):
1571 """Only 6 section levels are supported by HTML."""
1572 check_id = 0
1573 close_tag = '</p>\n'
1574 if isinstance(node.parent, nodes.topic):
1575 self.body.append(
1576 self.starttag(node, 'p', '', CLASS='topic-title first'))
1577 elif isinstance(node.parent, nodes.sidebar):
1578 self.body.append(
1579 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1580 elif isinstance(node.parent, nodes.Admonition):
1581 self.body.append(
1582 self.starttag(node, 'p', '', CLASS='admonition-title'))
1583 elif isinstance(node.parent, nodes.table):
1584 self.body.append(
1585 self.starttag(node, 'caption', ''))
1586 close_tag = '</caption>\n'
1587 elif isinstance(node.parent, nodes.document):
1588 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1589 close_tag = '</h1>\n'
1590 self.in_document_title = len(self.body)
1591 else:
1592 assert isinstance(node.parent, nodes.section)
1593 h_level = self.section_level + self.initial_header_level - 1
1594 atts = {}
1595 if (len(node.parent) >= 2 and
1596 isinstance(node.parent[1], nodes.subtitle)):
1597 atts['CLASS'] = 'with-subtitle'
1598 self.body.append(
1599 self.starttag(node, 'h%s' % h_level, '', **atts))
1600 atts = {}
1601 if node.hasattr('refid'):
1602 atts['class'] = 'toc-backref'
1603 atts['href'] = '#' + node['refid']
1604 if atts:
1605 self.body.append(self.starttag({}, 'a', '', **atts))
1606 close_tag = '</a></h%s>\n' % (h_level)
1607 else:
1608 close_tag = '</h%s>\n' % (h_level)
1609 self.context.append(close_tag)
1611 def depart_title(self, node):
1612 self.body.append(self.context.pop())
1613 if self.in_document_title:
1614 self.title = self.body[self.in_document_title:-1]
1615 self.in_document_title = 0
1616 self.body_pre_docinfo.extend(self.body)
1617 self.html_title.extend(self.body)
1618 del self.body[:]
1620 def visit_title_reference(self, node):
1621 self.body.append(self.starttag(node, 'cite', ''))
1623 def depart_title_reference(self, node):
1624 self.body.append('</cite>')
1626 def visit_topic(self, node):
1627 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1628 self.topic_classes = node['classes']
1630 def depart_topic(self, node):
1631 self.body.append('</div>\n')
1632 self.topic_classes = []
1634 def visit_transition(self, node):
1635 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1637 def depart_transition(self, node):
1638 pass
1640 def visit_version(self, node):
1641 self.visit_docinfo_item(node, 'version', meta=None)
1643 def depart_version(self, node):
1644 self.depart_docinfo_item()
1646 def unimplemented_visit(self, node):
1647 raise NotImplementedError('visiting unimplemented node type: %s'
1648 % node.__class__.__name__)
1651 class SimpleListChecker(nodes.GenericNodeVisitor):
1654 Raise `nodes.NodeFound` if non-simple list item is encountered.
1656 Here "simple" means a list item containing nothing other than a single
1657 paragraph, a simple list, or a paragraph followed by a simple list.
1660 def default_visit(self, node):
1661 raise nodes.NodeFound
1663 def visit_bullet_list(self, node):
1664 pass
1666 def visit_enumerated_list(self, node):
1667 pass
1669 def visit_list_item(self, node):
1670 children = []
1671 for child in node.children:
1672 if not isinstance(child, nodes.Invisible):
1673 children.append(child)
1674 if (children and isinstance(children[0], nodes.paragraph)
1675 and (isinstance(children[-1], nodes.bullet_list)
1676 or isinstance(children[-1], nodes.enumerated_list))):
1677 children.pop()
1678 if len(children) <= 1:
1679 return
1680 else:
1681 raise nodes.NodeFound
1683 def visit_paragraph(self, node):
1684 raise nodes.SkipNode
1686 def invisible_visit(self, node):
1687 """Invisible nodes should be ignored."""
1688 raise nodes.SkipNode
1690 visit_comment = invisible_visit
1691 visit_substitution_definition = invisible_visit
1692 visit_target = invisible_visit
1693 visit_pending = invisible_visit