Let "html-base" and "xhtml11" writers wrap SVG images in <img> tags.
[docutils.git] / docutils / writers / html_base / __init__.py
blob6dac17d6552cd96ab0b6d28bc4cb6c318010c5ab
1 # .. coding: utf8
2 # :Author: Günter Milde <milde@users.berlios.de>
3 # :Revision: $Revision$
4 # :Date: $Date: 2005-06-28$
5 # :Copyright: © 2005, 2009 Günter Milde.
6 # :License: Released under the terms of the `2-Clause BSD license`_, in short:
8 # Copying and distribution of this file, with or without modification,
9 # are permitted in any medium without royalty provided the copyright
10 # notice and this notice are preserved.
11 # This file is offered as-is, without any warranty.
13 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
15 # Use "best practice" as recommended by the W3C:
16 # http://www.w3.org/2009/cheatsheet/
19 """
20 Basic HyperText Markup Language document tree Writer.
22 The output conforms to the `HTML 5` specification as well as
23 to `XHTML 1.0 transitional`.
25 The cascading style sheet "html-base.css" is required for proper viewing.
26 """
27 __docformat__ = 'reStructuredText'
29 import sys
30 import os
31 import os.path
32 import re
33 import urllib
34 try: # check for the Python Imaging Library
35 import PIL.Image
36 except ImportError:
37 try: # sometimes PIL modules are put in PYTHONPATH's root
38 import Image
39 class PIL(object): pass # dummy wrapper
40 PIL.Image = Image
41 except ImportError:
42 PIL = None
43 import docutils
44 from docutils import frontend, nodes, utils, writers, languages, io
45 from docutils.utils.error_reporting import SafeString
46 from docutils.transforms import writer_aux
47 from docutils.utils.math import unichar2tex, pick_math_environment, math2html
48 from docutils.utils.math.latex2mathml import parse_latex_math
50 class Writer(writers.Writer):
52 supported = ('html', 'html5', 'xhtml')
53 """Formats this writer supports."""
55 default_stylesheets = ['html-base.css']
56 default_stylesheet_dirs = ['.', os.path.abspath(os.path.dirname(__file__))]
58 default_template = 'template.txt'
59 default_template_path = os.path.join(
60 os.path.dirname(os.path.abspath(__file__)), default_template)
62 settings_spec = (
63 'HTML-Specific Options',
64 None,
65 (('Specify the template file (UTF-8 encoded). Default is "%s".'
66 % default_template_path,
67 ['--template'],
68 {'default': default_template_path, 'metavar': '<file>'}),
69 ('Comma separated list of stylesheet URLs. '
70 'Overrides previous --stylesheet and --stylesheet-path settings.',
71 ['--stylesheet'],
72 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
73 'validator': frontend.validate_comma_separated_list}),
74 ('Comma separated list of stylesheet paths. '
75 'Relative paths are expanded if a matching file is found in '
76 'the --stylesheet-dirs. With --link-stylesheet, '
77 'the path is rewritten relative to the output HTML file. '
78 'Default: "%s"' % ','.join(default_stylesheets),
79 ['--stylesheet-path'],
80 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
81 'validator': frontend.validate_comma_separated_list,
82 'default': default_stylesheets}),
83 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
84 'files must be accessible during processing. This is the default.',
85 ['--embed-stylesheet'],
86 {'default': 1, 'action': 'store_true',
87 'validator': frontend.validate_boolean}),
88 ('Link to the stylesheet(s) in the output HTML file. '
89 'Default: embed stylesheets.',
90 ['--link-stylesheet'],
91 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
92 ('Comma-separated list of directories where stylesheets are found. '
93 'Used by --stylesheet-path when expanding relative path arguments. '
94 'Default: "%s"' % default_stylesheet_dirs,
95 ['--stylesheet-dirs'],
96 {'metavar': '<dir[,dir,...]>',
97 'validator': frontend.validate_comma_separated_list,
98 'default': default_stylesheet_dirs}),
99 ('Specify the initial header level. Default is 1 for "<h1>". '
100 'Does not affect document title & subtitle (see --no-doc-title).',
101 ['--initial-header-level'],
102 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
103 'metavar': '<level>'}),
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': True, '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': True, '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") and options(s). Default: "HTML math.css"',
137 ['--math-output'],
138 {'default': 'HTML math.css'}),
139 ('Prepend an XML declaration. (Thwarts HTML5 conformance.) '
140 'Default: False',
141 ['--xml-declaration'],
142 {'default': False, 'action': 'store_true',
143 'validator': frontend.validate_boolean}),
144 ('Omit the XML declaration.',
145 ['--no-xml-declaration'],
146 {'dest': 'xml_declaration', 'action': 'store_false'}),
147 ('Obfuscate email addresses to confuse harvesters while still '
148 'keeping email links usable with standards-compliant browsers.',
149 ['--cloak-email-addresses'],
150 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
152 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
154 config_section = 'html-base writer'
155 config_section_dependencies = ('writers',)
157 visitor_attributes = (
158 'head_prefix', 'head', 'stylesheet', 'body_prefix',
159 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
160 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
161 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
162 'html_body')
164 def get_transforms(self):
165 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
167 def __init__(self):
168 writers.Writer.__init__(self)
169 self.translator_class = HTMLTranslator
171 def translate(self):
172 self.visitor = visitor = self.translator_class(self.document)
173 self.document.walkabout(visitor)
174 for attr in self.visitor_attributes:
175 setattr(self, attr, getattr(visitor, attr))
176 self.output = self.apply_template()
178 def apply_template(self):
179 template_file = open(self.document.settings.template, 'rb')
180 template = unicode(template_file.read(), 'utf-8')
181 template_file.close()
182 subs = self.interpolation_dict()
183 return template % subs
185 def interpolation_dict(self):
186 subs = {}
187 settings = self.document.settings
188 for attr in self.visitor_attributes:
189 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
190 subs['encoding'] = settings.output_encoding
191 subs['version'] = docutils.__version__
192 return subs
194 def assemble_parts(self):
195 writers.Writer.assemble_parts(self)
196 for part in self.visitor_attributes:
197 self.parts[part] = ''.join(getattr(self, part))
200 class HTMLTranslator(nodes.NodeVisitor):
203 This writer generates `polyglott markup`: HTML 5 that is also valid XML.
206 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
207 doctype = (
208 '<!DOCTYPE html>\n')
209 doctype_mathml = doctype
211 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
212 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
213 content_type = ('<meta http-equiv="Content-Type"'
214 ' content="text/html; charset=%s" />\n')
215 content_type_xml = ('<meta http-equiv="Content-Type"'
216 ' content="application/xhtml+xml; charset=%s" />\n')
218 generator = ('<meta name="generator" content="Docutils %s: '
219 'http://docutils.sourceforge.net/" />\n')
221 # Template for the MathJax script in the header:
222 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
223 # The latest version of MathJax from the distributed server:
224 # avaliable to the public under the `MathJax CDN Terms of Service`__
225 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
226 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
227 'config=TeX-AMS-MML_HTMLorMML')
228 # may be overwritten by custom URL appended to "mathjax"
230 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
231 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
232 words_and_spaces = re.compile(r'\S+| +|\n')
233 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
234 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
236 def __init__(self, document):
237 nodes.NodeVisitor.__init__(self, document)
238 self.settings = settings = document.settings
239 lcode = settings.language_code
240 self.language = languages.get_language(lcode, document.reporter)
241 self.meta = [self.generator % docutils.__version__]
242 self.head_prefix = []
243 self.html_prolog = []
244 if settings.xml_declaration:
245 self.head_prefix.append(self.xml_declaration
246 % settings.output_encoding)
247 self.content_type = self.content_type_xml
248 # encoding not interpolated:
249 self.html_prolog.append(self.xml_declaration)
250 self.head = self.meta[:]
251 self.stylesheet = [self.stylesheet_call(path)
252 for path in utils.get_stylesheet_list(settings)]
253 self.body_prefix = ['</head>\n<body>\n']
254 # document title, subtitle display
255 self.body_pre_docinfo = []
256 # author, date, etc.
257 self.docinfo = []
258 self.body = []
259 self.fragment = []
260 self.body_suffix = ['</body>\n</html>\n']
261 self.section_level = 0
262 self.initial_header_level = int(settings.initial_header_level)
264 self.math_output = settings.math_output.split()
265 self.math_output_options = self.math_output[1:]
266 self.math_output = self.math_output[0].lower()
268 # A heterogenous stack used in conjunction with the tree traversal.
269 # Make sure that the pops correspond to the pushes:
270 self.context = []
272 self.topic_classes = [] # TODO: replace with self_in_contents
273 self.colspecs = []
274 self.compact_p = True
275 self.compact_simple = False
276 self.compact_field_list = False
277 self.in_docinfo = False
278 self.in_sidebar = False
279 self.in_footnote_list = False
280 self.title = []
281 self.subtitle = []
282 self.header = []
283 self.footer = []
284 self.html_head = [self.content_type] # charset not interpolated
285 self.html_title = []
286 self.html_subtitle = []
287 self.html_body = []
288 self.in_document_title = 0 # len(self.body) or 0
289 self.in_mailto = False
290 self.author_in_authors = False
291 self.math_header = []
293 def astext(self):
294 return ''.join(self.head_prefix + self.head
295 + self.stylesheet + self.body_prefix
296 + self.body_pre_docinfo + self.docinfo
297 + self.body + self.body_suffix)
299 def encode(self, text):
300 """Encode special characters in `text` & return."""
301 # Use only named entities known in both XML and HTML
302 # other characters are automatically encoded "by number" if required.
303 text = unicode(text)
304 return text.translate({
305 ord('&'): u'&amp;',
306 ord('<'): u'&lt;',
307 ord('"'): u'&quot;',
308 ord('>'): u'&gt;',
309 ord('@'): u'&#64;', # may thwart some address harvesters
312 def cloak_mailto(self, uri):
313 """Try to hide a mailto: URL from harvesters."""
314 # Encode "@" using a URL octet reference (see RFC 1738).
315 # Further cloaking with HTML entities will be done in the
316 # `attval` function.
317 return uri.replace('@', '%40')
319 def cloak_email(self, addr):
320 """Try to hide the link text of a email link from harversters."""
321 # Surround at-signs and periods with <span> tags. ("@" has
322 # already been encoded to "&#64;" by the `encode` method.)
323 addr = addr.replace('&#64;', '<span>&#64;</span>')
324 addr = addr.replace('.', '<span>&#46;</span>')
325 return addr
327 def attval(self, text,
328 whitespace=re.compile('[\n\r\t\v\f]')):
329 """Cleanse, HTML encode, and return attribute value text."""
330 encoded = self.encode(whitespace.sub(' ', text))
331 if self.in_mailto and self.settings.cloak_email_addresses:
332 # Cloak at-signs ("%40") and periods with HTML entities.
333 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
334 encoded = encoded.replace('.', '&#46;')
335 return encoded
337 def stylesheet_call(self, path):
338 """Return code to reference or embed stylesheet file `path`"""
339 if self.settings.embed_stylesheet:
340 try:
341 content = io.FileInput(source_path=path,
342 encoding='utf-8').read()
343 self.settings.record_dependencies.add(path)
344 except IOError, err:
345 msg = u"Cannot embed stylesheet '%s': %s." % (
346 path, SafeString(err.strerror))
347 self.document.reporter.error(msg)
348 return '<--- %s --->\n' % msg
349 return self.embedded_stylesheet % content
350 # else link to style file:
351 if self.settings.stylesheet_path:
352 # adapt path relative to output (cf. config.html#stylesheet-path)
353 path = utils.relative_path(self.settings._destination, path)
354 return self.stylesheet_link % self.encode(path)
356 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
358 Construct and return a start tag given a node (id & class attributes
359 are extracted), tag name, and optional attributes.
361 tagname = tagname.lower()
362 prefix = []
363 atts = {}
364 ids = []
365 for (name, value) in attributes.items():
366 atts[name.lower()] = value
367 classes = []
368 languages = []
369 # unify class arguments and move language specification
370 for cls in node.get('classes', []) + atts.pop('class', '').split() :
371 if cls.startswith('language-'):
372 languages.append(cls[9:])
373 elif cls.strip() and cls not in classes:
374 classes.append(cls)
375 if languages:
376 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
377 atts[self.lang_attribute] = languages[0]
378 if classes:
379 atts['class'] = ' '.join(classes)
380 assert 'id' not in atts
381 ids.extend(node.get('ids', []))
382 if 'ids' in atts:
383 ids.extend(atts['ids'])
384 del atts['ids']
385 if ids:
386 atts['id'] = ids[0]
387 for id in ids[1:]:
388 # Add empty "span" elements for additional IDs. Note
389 # that we cannot use empty "a" elements because there
390 # may be targets inside of references, but nested "a"
391 # elements aren't allowed in XHTML (even if they do
392 # not all have a "href" attribute).
393 if empty:
394 # Empty tag. Insert target right in front of element.
395 prefix.append('<span id="%s"></span>' % id)
396 else:
397 # Non-empty tag. Place the auxiliary <span> tag
398 # *inside* the element, as the first child.
399 suffix += '<span id="%s"></span>' % id
400 attlist = atts.items()
401 attlist.sort()
402 parts = [tagname]
403 for name, value in attlist:
404 # value=None was used for boolean attributes without
405 # value, but this isn't supported by XHTML.
406 assert value is not None
407 if isinstance(value, list):
408 values = [unicode(v) for v in value]
409 parts.append('%s="%s"' % (name.lower(),
410 self.attval(' '.join(values))))
411 else:
412 parts.append('%s="%s"' % (name.lower(),
413 self.attval(unicode(value))))
414 if empty:
415 infix = ' /'
416 else:
417 infix = ''
418 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
420 def emptytag(self, node, tagname, suffix='\n', **attributes):
421 """Construct and return an XML-compatible empty tag."""
422 return self.starttag(node, tagname, suffix, empty=True, **attributes)
424 def set_class_on_child(self, node, class_, index=0):
426 Set class `class_` on the visible child no. index of `node`.
427 Do nothing if node has fewer children than `index`.
429 children = [n for n in node if not isinstance(n, nodes.Invisible)]
430 try:
431 child = children[index]
432 except IndexError:
433 return
434 child['classes'].append(class_)
436 def visit_Text(self, node):
437 text = node.astext()
438 encoded = self.encode(text)
439 if self.in_mailto and self.settings.cloak_email_addresses:
440 encoded = self.cloak_email(encoded)
441 self.body.append(encoded)
443 def depart_Text(self, node):
444 pass
446 def visit_abbreviation(self, node):
447 # @@@ implementation incomplete ("title" attribute)
448 self.body.append(self.starttag(node, 'abbr', ''))
450 def depart_abbreviation(self, node):
451 self.body.append('</abbr>')
453 def visit_acronym(self, node):
454 # @@@ implementation incomplete ("title" attribute)
455 self.body.append(self.starttag(node, 'abbr', ''))
457 def depart_acronym(self, node):
458 self.body.append('</abbr>')
460 def visit_address(self, node):
461 self.visit_docinfo_item(node, 'address', meta=False)
462 self.body.append(self.starttag(node, 'pre', '', CLASS='address'))
464 def depart_address(self, node):
465 self.body.append('\n</pre>\n')
466 self.depart_docinfo_item()
468 def visit_admonition(self, node):
469 node['classes'].insert(0, 'admonition')
470 self.body.append(self.starttag(node, 'div'))
472 def depart_admonition(self, node=None):
473 self.body.append('</div>\n')
475 attribution_formats = {'dash': (u'\u2014', ''),
476 'parentheses': ('(', ')'),
477 'parens': ('(', ')'),
478 'none': ('', '')}
480 def visit_attribution(self, node):
481 prefix, suffix = self.attribution_formats[self.settings.attribution]
482 self.context.append(suffix)
483 self.body.append(
484 self.starttag(node, 'p', prefix, CLASS='attribution'))
485 self.body.append(self.starttag(node, 'cite', ''))
487 def depart_attribution(self, node):
488 self.body.append('</cite>' + self.context.pop() + '</p>\n')
490 # author, authors
491 # ---------------
492 # Use paragraphs instead of hard-coded linebreaks.
494 def visit_author(self, node):
495 if not(isinstance(node.parent, nodes.authors)):
496 self.visit_docinfo_item(node, 'author')
497 self.body.append('<p>')
499 def depart_author(self, node):
500 self.body.append('</p>')
501 if isinstance(node.parent, nodes.authors):
502 self.body.append('\n')
503 else:
504 self.depart_docinfo_item()
506 def visit_authors(self, node):
507 self.visit_docinfo_item(node, 'authors', meta=False)
509 def depart_authors(self, node):
510 self.depart_docinfo_item()
512 def visit_block_quote(self, node):
513 self.body.append(self.starttag(node, 'blockquote'))
515 def depart_block_quote(self, node):
516 self.body.append('</blockquote>\n')
518 def check_simple_list(self, node):
519 """Check for a simple list that can be rendered compactly."""
520 visitor = SimpleListChecker(self.document)
521 try:
522 node.walk(visitor)
523 except nodes.NodeFound:
524 return None
525 else:
526 return 1
528 # Compact lists
529 # ------------
530 # Include definition lists and field lists (in addition to ordered
531 # and unordered lists) in the test if a list is "simple" (cf. the
532 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
533 # the end of this file).
535 def is_compactable(self, node):
536 # print "is_compactable %s ?" % node.__class__,
537 # explicite class arguments have precedence
538 if 'compact' in node['classes']:
539 # print "explicitely compact"
540 return True
541 if 'open' in node['classes']:
542 # print "explicitely open"
543 return False
544 # check config setting:
545 if (isinstance(node, nodes.field_list) or
546 isinstance(node, nodes.definition_list)
547 ) and not self.settings.compact_field_lists:
548 # print "`compact-field-lists` is False"
549 return False
550 if (isinstance(node, nodes.enumerated_list) or
551 isinstance(node, nodes.bullet_list)
552 ) and not self.settings.compact_lists:
553 # print "`compact-lists` is False"
554 return False
555 # more special cases:
556 if (self.topic_classes == ['contents']): # TODO: self.in_contents
557 return True
558 # check the list items:
559 visitor = SimpleListChecker(self.document)
560 try:
561 node.walk(visitor)
562 except nodes.NodeFound:
563 # print "complex node"
564 return False
565 else:
566 # print "simple list"
567 return True
569 def visit_bullet_list(self, node):
570 atts = {}
571 old_compact_simple = self.compact_simple
572 self.context.append((self.compact_simple, self.compact_p))
573 self.compact_p = None
574 self.compact_simple = self.is_compactable(node)
575 if self.compact_simple and not old_compact_simple:
576 atts['class'] = 'simple'
577 self.body.append(self.starttag(node, 'ul', **atts))
579 def depart_bullet_list(self, node):
580 self.compact_simple, self.compact_p = self.context.pop()
581 self.body.append('</ul>\n')
583 def visit_caption(self, node):
584 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
586 def depart_caption(self, node):
587 self.body.append('</p>\n')
589 # citations
590 # ---------
591 # Use definition list instead of table for bibliographic references.
592 # Join adjacent citation entries.
594 def visit_citation(self, node):
595 if not self.in_footnote_list:
596 self.body.append('<dl class="citation">\n')
597 self.in_footnote_list = True
599 def depart_citation(self, node):
600 self.body.append('</dd>\n')
601 if not isinstance(node.next_node(descend=False, siblings=True),
602 nodes.citation):
603 self.body.append('</dl>\n')
604 self.in_footnote_list = False
606 def visit_citation_reference(self, node):
607 href = '#'
608 if 'refid' in node:
609 href += node['refid']
610 elif 'refname' in node:
611 href += self.document.nameids[node['refname']]
612 # else: # TODO system message (or already in the transform)?
613 # 'Citation reference missing.'
614 self.body.append(self.starttag(
615 node, 'a', '[', CLASS='citation-reference', href=href))
617 def depart_citation_reference(self, node):
618 self.body.append(']</a>')
620 # classifier
621 # ----------
622 # don't insert classifier-delimiter here (done by CSS)
624 def visit_classifier(self, node):
625 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
627 def depart_classifier(self, node):
628 self.body.append('</span>')
630 def visit_colspec(self, node):
631 self.colspecs.append(node)
632 # "stubs" list is an attribute of the tgroup element:
633 node.parent.stubs.append(node.attributes.get('stub'))
635 def depart_colspec(self, node):
636 pass
638 def write_colspecs(self):
639 width = 0
640 for node in self.colspecs:
641 width += node['colwidth']
642 for node in self.colspecs:
643 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
644 self.body.append(self.emptytag(node, 'col',
645 style='width: %i%%' % colwidth))
646 self.colspecs = []
648 def visit_comment(self, node,
649 sub=re.compile('-(?=-)').sub):
650 """Escape double-dashes in comment text."""
651 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
652 # Content already processed:
653 raise nodes.SkipNode
655 def visit_compound(self, node):
656 self.body.append(self.starttag(node, 'div', CLASS='compound'))
657 if len(node) > 1:
658 node[0]['classes'].append('compound-first')
659 node[-1]['classes'].append('compound-last')
660 for child in node[1:-1]:
661 child['classes'].append('compound-middle')
663 def depart_compound(self, node):
664 self.body.append('</div>\n')
666 def visit_container(self, node):
667 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
669 def depart_container(self, node):
670 self.body.append('</div>\n')
672 def visit_contact(self, node):
673 self.visit_docinfo_item(node, 'contact', meta=False)
675 def depart_contact(self, node):
676 self.depart_docinfo_item()
678 def visit_copyright(self, node):
679 self.visit_docinfo_item(node, 'copyright', meta=False)
681 def depart_copyright(self, node):
682 self.depart_docinfo_item()
684 def visit_date(self, node):
685 self.visit_docinfo_item(node, 'date', meta=False)
687 def depart_date(self, node):
688 self.depart_docinfo_item()
690 def visit_decoration(self, node):
691 pass
693 def depart_decoration(self, node):
694 pass
696 def visit_definition(self, node):
697 self.body.append('</dt>\n')
698 self.body.append(self.starttag(node, 'dd', ''))
700 def depart_definition(self, node):
701 self.body.append('</dd>\n')
703 def visit_definition_list(self, node):
704 classes = node.setdefault('classes', [])
705 if self.is_compactable(node):
706 classes.append('simple')
707 self.body.append(self.starttag(node, 'dl'))
709 def depart_definition_list(self, node):
710 self.body.append('</dl>\n')
712 def visit_definition_list_item(self, node):
713 # pass class arguments, ids and names to definition term:
714 node.children[0]['classes'] = (
715 node.get('classes', []) + node.children[0].get('classes', []))
716 node.children[0]['ids'] = (
717 node.get('ids', []) + node.children[0].get('ids', []))
718 node.children[0]['names'] = (
719 node.get('names', []) + node.children[0].get('names', []))
721 def depart_definition_list_item(self, node):
722 pass
724 def visit_description(self, node):
725 self.body.append(self.starttag(node, 'dd', ''))
727 def depart_description(self, node):
728 self.body.append('</dd>\n')
731 # docinfo
732 # -------
733 # use definition list instead of table
735 def visit_docinfo(self, node):
736 classes = 'docinfo'
737 if (self.is_compactable(node)):
738 classes += ' simple'
739 self.body.append(self.starttag(node, 'dl', CLASS=classes))
741 def depart_docinfo(self, node):
742 self.body.append('</dl>\n')
744 def visit_docinfo_item(self, node, name, meta=True):
745 if meta:
746 meta_tag = '<meta name="%s" content="%s" />\n' \
747 % (name, self.attval(node.astext()))
748 self.add_meta(meta_tag)
749 self.body.append('<dt class="%s">%s</dt>\n'
750 % (name, self.language.labels[name]))
751 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
753 def depart_docinfo_item(self):
754 self.body.append('</dd>\n')
756 # TODO: RSt-parser should treat this as code-block with class "pycon".
757 def visit_doctest_block(self, node):
758 self.body.append(self.starttag(node, 'pre', suffix='',
759 CLASS='code pycon doctest-block'))
761 def depart_doctest_block(self, node):
762 self.body.append('\n</pre>\n')
764 def visit_document(self, node):
765 self.head.append('<title>%s</title>\n'
766 % self.encode(node.get('title', '')))
768 def depart_document(self, node):
769 self.head_prefix.extend([self.doctype,
770 self.head_prefix_template %
771 {'lang': self.settings.language_code}])
772 self.html_prolog.append(self.doctype)
773 self.meta.insert(0, self.content_type % self.settings.output_encoding)
774 self.head.insert(0, self.content_type % self.settings.output_encoding)
775 if self.math_header:
776 if self.math_output == 'mathjax':
777 self.head.extend(self.math_header)
778 else:
779 self.stylesheet.extend(self.math_header)
780 # skip content-type meta tag with interpolated charset value:
781 self.html_head.extend(self.head[1:])
782 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
783 self.body_suffix.insert(0, '</div>\n')
784 self.fragment.extend(self.body) # self.fragment is the "naked" body
785 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
786 + self.docinfo + self.body
787 + self.body_suffix[:-1])
788 assert not self.context, 'len(context) = %s' % len(self.context)
790 def visit_emphasis(self, node):
791 self.body.append(self.starttag(node, 'em', ''))
793 def depart_emphasis(self, node):
794 self.body.append('</em>')
796 def visit_entry(self, node):
797 atts = {'class': []}
798 if isinstance(node.parent.parent, nodes.thead):
799 atts['class'].append('head')
800 if node.parent.parent.parent.stubs[node.parent.column]:
801 # "stubs" list is an attribute of the tgroup element
802 atts['class'].append('stub')
803 if atts['class']:
804 tagname = 'th'
805 atts['class'] = ' '.join(atts['class'])
806 else:
807 tagname = 'td'
808 del atts['class']
809 node.parent.column += 1
810 if 'morerows' in node:
811 atts['rowspan'] = node['morerows'] + 1
812 if 'morecols' in node:
813 atts['colspan'] = node['morecols'] + 1
814 node.parent.column += node['morecols']
815 self.body.append(self.starttag(node, tagname, '', **atts))
816 self.context.append('</%s>\n' % tagname.lower())
817 # TODO: why did the html4css1 writer insert an NBSP into empty cells?
818 # if len(node) == 0: # empty cell
819 # self.body.append('&#0160;') # no-break space
821 def depart_entry(self, node):
822 self.body.append(self.context.pop())
824 def visit_enumerated_list(self, node):
826 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
827 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
828 usable.
830 atts = {}
831 if 'start' in node:
832 atts['start'] = node['start']
833 if 'enumtype' in node:
834 atts['class'] = node['enumtype']
835 if self.is_compactable(node):
836 atts['class'] = (atts.get('class', '') + ' simple').strip()
837 self.body.append(self.starttag(node, 'ol', **atts))
839 def depart_enumerated_list(self, node):
840 self.body.append('</ol>\n')
842 # field-list
843 # ----------
844 # set as definition list, styled with CSS
846 def visit_field_list(self, node):
847 # Keep simple paragraphs in the field_body to enable CSS
848 # rule to start body on new line if the label is too long
849 classes = 'field-list'
850 if (self.is_compactable(node)):
851 classes += ' simple'
852 self.body.append(self.starttag(node, 'dl', CLASS=classes))
854 def depart_field_list(self, node):
855 self.body.append('</dl>\n')
857 def visit_field(self, node):
858 pass
860 def depart_field(self, node):
861 pass
863 def visit_field_name(self, node):
864 self.body.append(self.starttag(node, 'dt', ''))
866 def depart_field_name(self, node):
867 self.body.append('</dt>\n')
869 def visit_field_body(self, node):
870 self.body.append(self.starttag(node, 'dd', ''))
872 def depart_field_body(self, node):
873 self.body.append('</dd>\n')
875 def visit_figure(self, node):
876 atts = {'class': 'figure'}
877 if node.get('width'):
878 atts['style'] = 'width: %s' % node['width']
879 if node.get('align'):
880 atts['class'] += " align-" + node['align']
881 self.body.append(self.starttag(node, 'div', **atts))
883 def depart_figure(self, node):
884 self.body.append('</div>\n')
886 # use HTML 5 <footer> element?
887 def visit_footer(self, node):
888 self.context.append(len(self.body))
890 def depart_footer(self, node):
891 start = self.context.pop()
892 footer = [self.starttag(node, 'div', CLASS='footer'),
893 '<hr class="footer" />\n']
894 footer.extend(self.body[start:])
895 footer.append('\n</div>\n')
896 self.footer.extend(footer)
897 self.body_suffix[:0] = footer
898 del self.body[start:]
900 # footnotes
901 # ---------
902 # use definition list instead of table for footnote text
904 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
905 def visit_footnote(self, node):
906 if not self.in_footnote_list:
907 self.body.append('<dl class="footnote">\n')
908 self.in_footnote_list = True
910 def depart_footnote(self, node):
911 self.body.append('</dd>\n')
912 if not isinstance(node.next_node(descend=False, siblings=True),
913 nodes.footnote):
914 self.body.append('</dl>\n')
915 self.in_footnote_list = False
917 def visit_footnote_reference(self, node):
918 href = '#' + node['refid']
919 format = self.settings.footnote_references
920 if format == 'brackets':
921 suffix = '['
922 self.context.append(']')
923 else:
924 assert format == 'superscript'
925 suffix = '<sup>'
926 self.context.append('</sup>')
927 self.body.append(self.starttag(node, 'a', suffix,
928 CLASS='footnote-reference', href=href))
930 def depart_footnote_reference(self, node):
931 self.body.append(self.context.pop() + '</a>')
933 def visit_generated(self, node):
934 if 'sectnum' in node['classes']:
935 # get section number (strip trailing no-break-spaces)
936 sectnum = node.astext().rstrip(u' ')
937 # print sectnum.encode('utf-8')
938 self.body.append('<span class="sectnum">%s</span> '
939 % self.encode(sectnum))
940 # Content already processed:
941 raise nodes.SkipNode
943 def depart_generated(self, node):
944 pass
946 def visit_header(self, node):
947 self.context.append(len(self.body))
949 def depart_header(self, node):
950 start = self.context.pop()
951 header = [self.starttag(node, 'div', CLASS='header')]
952 header.extend(self.body[start:])
953 header.append('\n<hr class="header"/>\n</div>\n')
954 self.body_prefix.extend(header)
955 self.header.extend(header)
956 del self.body[start:]
958 # Image types to place in an <object> element
959 # SVG not supported by IE up to version 8
960 # (html4css1 strives for IE6 compatibility)
961 object_image_types = {#'.svg': 'image/svg+xml',
962 '.swf': 'application/x-shockwave-flash'}
964 def visit_image(self, node):
965 atts = {}
966 uri = node['uri']
967 ext = os.path.splitext(uri)[1].lower()
968 if ext in self.object_image_types:
969 atts['data'] = uri
970 atts['type'] = self.object_image_types[ext]
971 else:
972 atts['src'] = uri
973 atts['alt'] = node.get('alt', uri)
974 # image size
975 if 'width' in node:
976 atts['width'] = node['width']
977 if 'height' in node:
978 atts['height'] = node['height']
979 if 'scale' in node:
980 if (PIL and not ('width' in node and 'height' in node)
981 and self.settings.file_insertion_enabled):
982 imagepath = urllib.url2pathname(uri)
983 try:
984 img = PIL.Image.open(
985 imagepath.encode(sys.getfilesystemencoding()))
986 except (IOError, UnicodeEncodeError):
987 pass # TODO: warn?
988 else:
989 self.settings.record_dependencies.add(
990 imagepath.replace('\\', '/'))
991 if 'width' not in atts:
992 atts['width'] = '%dpx' % img.size[0]
993 if 'height' not in atts:
994 atts['height'] = '%dpx' % img.size[1]
995 del img
996 for att_name in 'width', 'height':
997 if att_name in atts:
998 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
999 assert match
1000 atts[att_name] = '%s%s' % (
1001 float(match.group(1)) * (float(node['scale']) / 100),
1002 match.group(2))
1003 style = []
1004 for att_name in 'width', 'height':
1005 if att_name in atts:
1006 if re.match(r'^[0-9.]+$', atts[att_name]):
1007 # Interpret unitless values as pixels.
1008 atts[att_name] += 'px'
1009 style.append('%s: %s;' % (att_name, atts[att_name]))
1010 del atts[att_name]
1011 if style:
1012 atts['style'] = ' '.join(style)
1013 if (isinstance(node.parent, nodes.TextElement) or
1014 (isinstance(node.parent, nodes.reference) and
1015 not isinstance(node.parent.parent, nodes.TextElement))):
1016 # Inline context or surrounded by <a>...</a>.
1017 suffix = ''
1018 else:
1019 suffix = '\n'
1020 if 'align' in node:
1021 atts['class'] = 'align-%s' % node['align']
1022 if ext in self.object_image_types:
1023 # do NOT use an empty tag: incorrect rendering in browsers
1024 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1025 node.get('alt', uri) + '</object>' + suffix)
1026 else:
1027 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1029 def depart_image(self, node):
1030 # self.body.append(self.context.pop())
1031 pass
1033 def visit_inline(self, node):
1034 self.body.append(self.starttag(node, 'span', ''))
1036 def depart_inline(self, node):
1037 self.body.append('</span>')
1039 # footnote and citation label
1040 def label_delim(self, node, bracket, superscript):
1041 """put brackets around label?"""
1042 if isinstance(node.parent, nodes.footnote):
1043 if self.settings.footnote_references == 'brackets':
1044 return bracket
1045 else:
1046 return superscript
1047 assert isinstance(node.parent, nodes.citation)
1048 return bracket
1050 def visit_label(self, node):
1051 # pass parent node to get id into starttag:
1052 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
1053 # footnote/citation backrefs:
1054 if self.settings.footnote_backlinks:
1055 backrefs = node.parent['backrefs']
1056 if len(backrefs) == 1:
1057 self.body.append('<a class="fn-backref" href="#%s">'
1058 % backrefs[0])
1059 self.body.append(self.label_delim(node, '[', ''))
1061 def depart_label(self, node):
1062 self.body.append(self.label_delim(node, ']', ''))
1063 if self.settings.footnote_backlinks:
1064 backrefs = node.parent['backrefs']
1065 if len(backrefs) == 1:
1066 self.body.append('</a>')
1067 elif len(backrefs) > 1:
1068 # Python 2.4 fails with enumerate(backrefs, 1)
1069 backlinks = ['<a href="#%s">%s</a>' % (ref, i+1)
1070 for (i, ref) in enumerate(backrefs)]
1071 self.body.append('<span class="fn-backref">(%s)</span>'
1072 % ','.join(backlinks))
1073 self.body.append('</dt>\n<dd>')
1075 def visit_legend(self, node):
1076 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1078 def depart_legend(self, node):
1079 self.body.append('</div>\n')
1081 def visit_line(self, node):
1082 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1083 if not len(node):
1084 self.body.append('<br />')
1086 def depart_line(self, node):
1087 self.body.append('</div>\n')
1089 def visit_line_block(self, node):
1090 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1092 def depart_line_block(self, node):
1093 self.body.append('</div>\n')
1095 def visit_list_item(self, node):
1096 self.body.append(self.starttag(node, 'li', ''))
1098 def depart_list_item(self, node):
1099 self.body.append('</li>\n')
1101 # inline literal
1102 def visit_literal(self, node):
1103 # special case: "code" role
1104 classes = node.get('classes', [])
1105 if 'code' in classes:
1106 # filter 'code' from class arguments
1107 node['classes'] = [cls for cls in classes if cls != 'code']
1108 self.body.append(self.starttag(node, 'code', ''))
1109 return
1110 self.body.append(
1111 self.starttag(node, 'span', '', CLASS='docutils literal'))
1112 text = node.astext()
1113 # remove hard line breaks (except if in a parsed-literal block)
1114 if not isinstance(node.parent, nodes.literal_block):
1115 text = text.replace('\n', ' ')
1116 # Protect text like ``--an-option`` and the regular expression
1117 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1118 for token in self.words_and_spaces.findall(text):
1119 if token.strip() and self.sollbruchstelle.search(token):
1120 self.body.append('<span class="pre">%s</span>'
1121 % self.encode(token))
1122 else:
1123 self.body.append(self.encode(token))
1124 self.body.append('</span>')
1125 # Content already processed:
1126 raise nodes.SkipNode
1128 def depart_literal(self, node):
1129 # skipped unless literal element is from "code" role:
1130 self.body.append('</code>')
1132 def visit_literal_block(self, node):
1133 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1134 if 'code' in node.get('classes', []):
1135 self.body.append('<code>')
1137 def depart_literal_block(self, node):
1138 if 'code' in node.get('classes', []):
1139 self.body.append('</code>')
1140 self.body.append('</pre>\n')
1142 def visit_math(self, node, math_env=''):
1143 # If the method is called from visit_math_block(), math_env != ''.
1145 # As there is no native HTML math support, we provide alternatives:
1146 # LaTeX and MathJax math_output modes simply wrap the content,
1147 # HTML and MathML math_output modes also convert the math_code.
1148 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1149 self.document.reporter.error(
1150 'math-output format "%s" not supported '
1151 'falling back to "latex"'% self.math_output)
1152 self.math_output = 'latex'
1154 # HTML container
1155 tags = {# math_output: (block, inline, class-arguments)
1156 'mathml': ('div', '', ''),
1157 'html': ('div', 'span', 'formula'),
1158 'mathjax': ('div', 'span', 'math'),
1159 'latex': ('pre', 'tt', 'math'),
1161 tag = tags[self.math_output][math_env == '']
1162 clsarg = tags[self.math_output][2]
1163 # LaTeX container
1164 wrappers = {# math_mode: (inline, block)
1165 'mathml': (None, None),
1166 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1167 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1168 'latex': (None, None),
1170 wrapper = wrappers[self.math_output][math_env != '']
1171 # get and wrap content
1172 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1173 if wrapper and math_env:
1174 math_code = wrapper % (math_env, math_code, math_env)
1175 elif wrapper:
1176 math_code = wrapper % math_code
1177 # settings and conversion
1178 if self.math_output in ('latex', 'mathjax'):
1179 math_code = self.encode(math_code)
1180 if self.math_output == 'mathjax' and not self.math_header:
1181 if self.math_output_options:
1182 self.mathjax_url = self.math_output_options[0]
1183 self.math_header = [self.mathjax_script % self.mathjax_url]
1184 elif self.math_output == 'html':
1185 if self.math_output_options and not self.math_header:
1186 self.math_header = [self.stylesheet_call(
1187 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1188 for s in self.math_output_options[0].split(',')]
1189 # TODO: fix display mode in matrices and fractions
1190 math2html.DocumentParameters.displaymode = (math_env != '')
1191 math_code = math2html.math2html(math_code)
1192 elif self.math_output == 'mathml':
1193 self.doctype = self.doctype_mathml
1194 # self.content_type = self.content_type_mathml
1195 try:
1196 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1197 math_code = ''.join(mathml_tree.xml())
1198 except SyntaxError, err:
1199 err_node = self.document.reporter.error(err, base_node=node)
1200 self.visit_system_message(err_node)
1201 self.body.append(self.starttag(node, 'p'))
1202 self.body.append(u','.join(err.args))
1203 self.body.append('</p>\n')
1204 self.body.append(self.starttag(node, 'pre',
1205 CLASS='literal-block'))
1206 self.body.append(self.encode(math_code))
1207 self.body.append('\n</pre>\n')
1208 self.depart_system_message(err_node)
1209 raise nodes.SkipNode
1210 # append to document body
1211 if tag:
1212 self.body.append(self.starttag(node, tag,
1213 suffix='\n'*bool(math_env),
1214 CLASS=clsarg))
1215 self.body.append(math_code)
1216 if math_env: # block mode (equation, display)
1217 self.body.append('\n')
1218 if tag:
1219 self.body.append('</%s>' % tag)
1220 if math_env:
1221 self.body.append('\n')
1222 # Content already processed:
1223 raise nodes.SkipNode
1225 def depart_math(self, node):
1226 pass # never reached
1228 def visit_math_block(self, node):
1229 # print node.astext().encode('utf8')
1230 math_env = pick_math_environment(node.astext())
1231 self.visit_math(node, math_env=math_env)
1233 def depart_math_block(self, node):
1234 pass # never reached
1236 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1237 # HTML5/polyglott recommends using both
1238 def visit_meta(self, node):
1239 if node.hasattr('lang'):
1240 node['xml:lang'] = node['lang']
1241 # del(node['lang'])
1242 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1243 self.add_meta(meta)
1245 def depart_meta(self, node):
1246 pass
1248 def add_meta(self, tag):
1249 self.meta.append(tag)
1250 self.head.append(tag)
1252 def visit_option(self, node):
1253 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1255 def depart_option(self, node):
1256 self.body.append('</span>')
1257 if isinstance(node.next_node(descend=False, siblings=True),
1258 nodes.option):
1259 self.body.append(', ')
1261 def visit_option_argument(self, node):
1262 self.body.append(node.get('delimiter', ' '))
1263 self.body.append(self.starttag(node, 'var', ''))
1265 def depart_option_argument(self, node):
1266 self.body.append('</var>')
1268 def visit_option_group(self, node):
1269 self.body.append(self.starttag(node, 'dt', ''))
1270 self.body.append('<kbd>')
1272 def depart_option_group(self, node):
1273 self.body.append('</kbd></dt>\n')
1275 def visit_option_list(self, node):
1276 self.body.append(
1277 self.starttag(node, 'dl', CLASS='option-list'))
1279 def depart_option_list(self, node):
1280 self.body.append('</dl>\n')
1282 def visit_option_list_item(self, node):
1283 pass
1285 def depart_option_list_item(self, node):
1286 pass
1288 def visit_option_string(self, node):
1289 pass
1291 def depart_option_string(self, node):
1292 pass
1294 def visit_organization(self, node):
1295 self.visit_docinfo_item(node, 'organization', meta=False)
1297 def depart_organization(self, node):
1298 self.depart_docinfo_item()
1300 # Do not omit <p> tags
1301 # --------------------
1303 # The HTML4CSS1 writer does this to "produce
1304 # visually compact lists (less vertical whitespace)". This writer
1305 # relies on CSS rules for"visual compactness".
1307 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1308 # character data, so you cannot drop the <p> tags.
1309 # * Keeping simple paragraphs in the field_body enables a CSS
1310 # rule to start the field-body on a new line if the label is too long
1311 # * it makes the code simpler.
1313 # TODO: omit paragraph tags in simple table cells?
1315 def visit_paragraph(self, node):
1316 self.body.append(self.starttag(node, 'p', ''))
1318 def depart_paragraph(self, node):
1319 self.body.append('</p>')
1320 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1321 (len(node.parent) == 1)):
1322 self.body.append('\n')
1324 def visit_problematic(self, node):
1325 if node.hasattr('refid'):
1326 self.body.append('<a href="#%s">' % node['refid'])
1327 self.context.append('</a>')
1328 else:
1329 self.context.append('')
1330 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1332 def depart_problematic(self, node):
1333 self.body.append('</span>')
1334 self.body.append(self.context.pop())
1336 def visit_raw(self, node):
1337 if 'html' in node.get('format', '').split():
1338 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1339 if node['classes']:
1340 self.body.append(self.starttag(node, t, suffix=''))
1341 self.body.append(node.astext())
1342 if node['classes']:
1343 self.body.append('</%s>' % t)
1344 # Keep non-HTML raw text out of output:
1345 raise nodes.SkipNode
1347 def visit_reference(self, node):
1348 atts = {'class': 'reference'}
1349 if 'refuri' in node:
1350 atts['href'] = node['refuri']
1351 if ( self.settings.cloak_email_addresses
1352 and atts['href'].startswith('mailto:')):
1353 atts['href'] = self.cloak_mailto(atts['href'])
1354 self.in_mailto = True
1355 atts['class'] += ' external'
1356 else:
1357 assert 'refid' in node, \
1358 'References must have "refuri" or "refid" attribute.'
1359 atts['href'] = '#' + node['refid']
1360 atts['class'] += ' internal'
1361 if not isinstance(node.parent, nodes.TextElement):
1362 assert len(node) == 1 and isinstance(node[0], nodes.image)
1363 atts['class'] += ' image-reference'
1364 self.body.append(self.starttag(node, 'a', '', **atts))
1366 def depart_reference(self, node):
1367 self.body.append('</a>')
1368 if not isinstance(node.parent, nodes.TextElement):
1369 self.body.append('\n')
1370 self.in_mailto = False
1372 def visit_revision(self, node):
1373 self.visit_docinfo_item(node, 'revision', meta=False)
1375 def depart_revision(self, node):
1376 self.depart_docinfo_item()
1378 def visit_row(self, node):
1379 self.body.append(self.starttag(node, 'tr', ''))
1380 node.column = 0
1382 def depart_row(self, node):
1383 self.body.append('</tr>\n')
1385 def visit_rubric(self, node):
1386 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1388 def depart_rubric(self, node):
1389 self.body.append('</p>\n')
1391 # TODO: use the new HTML 5 element <section>?
1392 def visit_section(self, node):
1393 self.section_level += 1
1394 self.body.append(
1395 self.starttag(node, 'div', CLASS='section'))
1397 def depart_section(self, node):
1398 self.section_level -= 1
1399 self.body.append('</div>\n')
1401 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1402 def visit_sidebar(self, node):
1403 self.body.append(
1404 self.starttag(node, 'div', CLASS='sidebar'))
1405 self.in_sidebar = True
1407 def depart_sidebar(self, node):
1408 self.body.append('</div>\n')
1409 self.in_sidebar = False
1411 def visit_status(self, node):
1412 self.visit_docinfo_item(node, 'status', meta=False)
1414 def depart_status(self, node):
1415 self.depart_docinfo_item()
1417 def visit_strong(self, node):
1418 self.body.append(self.starttag(node, 'strong', ''))
1420 def depart_strong(self, node):
1421 self.body.append('</strong>')
1423 def visit_subscript(self, node):
1424 self.body.append(self.starttag(node, 'sub', ''))
1426 def depart_subscript(self, node):
1427 self.body.append('</sub>')
1429 def visit_substitution_definition(self, node):
1430 """Internal only."""
1431 raise nodes.SkipNode
1433 def visit_substitution_reference(self, node):
1434 self.unimplemented_visit(node)
1436 # h1–h6 elements must not be used to markup subheadings, subtitles,
1437 # alternative titles and taglines unless intended to be the heading for a
1438 # new section or subsection.
1439 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1441 def visit_subtitle(self, node):
1442 if isinstance(node.parent, nodes.sidebar):
1443 classes = 'sidebar-subtitle'
1444 elif isinstance(node.parent, nodes.document):
1445 classes = 'subtitle'
1446 self.in_document_title = len(self.body)
1447 elif isinstance(node.parent, nodes.section):
1448 classes = 'section-subtitle'
1449 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1451 def depart_subtitle(self, node):
1452 self.body.append('</p>\n')
1453 if self.in_document_title:
1454 self.subtitle = self.body[self.in_document_title:-1]
1455 self.in_document_title = 0
1456 self.body_pre_docinfo.extend(self.body)
1457 self.html_subtitle.extend(self.body)
1458 del self.body[:]
1460 def visit_superscript(self, node):
1461 self.body.append(self.starttag(node, 'sup', ''))
1463 def depart_superscript(self, node):
1464 self.body.append('</sup>')
1466 def visit_system_message(self, node):
1467 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1468 self.body.append('<p class="system-message-title">')
1469 backref_text = ''
1470 if len(node['backrefs']):
1471 backrefs = node['backrefs']
1472 if len(backrefs) == 1:
1473 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1474 % backrefs[0])
1475 else:
1476 i = 1
1477 backlinks = []
1478 for backref in backrefs:
1479 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1480 i += 1
1481 backref_text = ('; <em>backlinks: %s</em>'
1482 % ', '.join(backlinks))
1483 if node.hasattr('line'):
1484 line = ', line %s' % node['line']
1485 else:
1486 line = ''
1487 self.body.append('System Message: %s/%s '
1488 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1489 % (node['type'], node['level'],
1490 self.encode(node['source']), line, backref_text))
1492 def depart_system_message(self, node):
1493 self.body.append('</div>\n')
1495 # tables
1496 # ------
1497 # no hard-coded border setting in the table head::
1499 def visit_table(self, node):
1500 classes = [cls.strip(u' \t\n')
1501 for cls in self.settings.table_style.split(',')]
1502 tag = self.starttag(node, 'table', CLASS=' '.join(classes))
1503 self.body.append(tag)
1505 def depart_table(self, node):
1506 self.body.append('</table>\n')
1508 def visit_target(self, node):
1509 if not ('refuri' in node or 'refid' in node
1510 or 'refname' in node):
1511 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1512 self.context.append('</span>')
1513 else:
1514 self.context.append('')
1516 def depart_target(self, node):
1517 self.body.append(self.context.pop())
1519 # no hard-coded vertical alignment in table body::
1521 def visit_tbody(self, node):
1522 self.write_colspecs()
1523 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1524 self.body.append(self.starttag(node, 'tbody'))
1526 def depart_tbody(self, node):
1527 self.body.append('</tbody>\n')
1529 def visit_term(self, node):
1530 self.body.append(self.starttag(node, 'dt', ''))
1532 def depart_term(self, node):
1534 Leave the end tag to `self.visit_definition()`, in case there's a
1535 classifier.
1537 pass
1539 def visit_tgroup(self, node):
1540 # Mozilla needs <colgroup>:
1541 self.body.append(self.starttag(node, 'colgroup'))
1542 # Appended by thead or tbody:
1543 self.context.append('</colgroup>\n')
1544 node.stubs = []
1546 def depart_tgroup(self, node):
1547 pass
1549 def visit_thead(self, node):
1550 self.write_colspecs()
1551 self.body.append(self.context.pop()) # '</colgroup>\n'
1552 # There may or may not be a <thead>; this is for <tbody> to use:
1553 self.context.append('')
1554 self.body.append(self.starttag(node, 'thead'))
1556 def depart_thead(self, node):
1557 self.body.append('</thead>\n')
1559 def visit_title(self, node):
1560 """Only 6 section levels are supported by HTML."""
1561 check_id = 0 # TODO: is this a bool (False) or a counter?
1562 close_tag = '</p>\n'
1563 if isinstance(node.parent, nodes.topic):
1564 self.body.append(
1565 self.starttag(node, 'p', '', CLASS='topic-title first'))
1566 elif isinstance(node.parent, nodes.sidebar):
1567 self.body.append(
1568 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1569 elif isinstance(node.parent, nodes.Admonition):
1570 self.body.append(
1571 self.starttag(node, 'p', '', CLASS='admonition-title'))
1572 elif isinstance(node.parent, nodes.table):
1573 self.body.append(
1574 self.starttag(node, 'caption', ''))
1575 close_tag = '</caption>\n'
1576 elif isinstance(node.parent, nodes.document):
1577 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1578 close_tag = '</h1>\n'
1579 self.in_document_title = len(self.body)
1580 else:
1581 assert isinstance(node.parent, nodes.section)
1582 h_level = self.section_level + self.initial_header_level - 1
1583 atts = {}
1584 if (len(node.parent) >= 2 and
1585 isinstance(node.parent[1], nodes.subtitle)):
1586 atts['CLASS'] = 'with-subtitle'
1587 self.body.append(
1588 self.starttag(node, 'h%s' % h_level, '', **atts))
1589 atts = {}
1590 if node.hasattr('refid'):
1591 atts['class'] = 'toc-backref'
1592 atts['href'] = '#' + node['refid']
1593 if atts:
1594 self.body.append(self.starttag({}, 'a', '', **atts))
1595 close_tag = '</a></h%s>\n' % (h_level)
1596 else:
1597 close_tag = '</h%s>\n' % (h_level)
1598 self.context.append(close_tag)
1600 def depart_title(self, node):
1601 self.body.append(self.context.pop())
1602 if self.in_document_title:
1603 self.title = self.body[self.in_document_title:-1]
1604 self.in_document_title = 0
1605 self.body_pre_docinfo.extend(self.body)
1606 self.html_title.extend(self.body)
1607 del self.body[:]
1609 def visit_title_reference(self, node):
1610 self.body.append(self.starttag(node, 'cite', ''))
1612 def depart_title_reference(self, node):
1613 self.body.append('</cite>')
1615 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1616 def visit_topic(self, node):
1617 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1618 self.topic_classes = node['classes']
1619 # TODO: replace with ::
1620 # self.in_contents = 'contents' in node['classes']
1622 def depart_topic(self, node):
1623 self.body.append('</div>\n')
1624 self.topic_classes = []
1625 # TODO self.in_contents = False
1627 def visit_transition(self, node):
1628 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1630 def depart_transition(self, node):
1631 pass
1633 def visit_version(self, node):
1634 self.visit_docinfo_item(node, 'version', meta=False)
1636 def depart_version(self, node):
1637 self.depart_docinfo_item()
1639 def unimplemented_visit(self, node):
1640 raise NotImplementedError('visiting unimplemented node type: %s'
1641 % node.__class__.__name__)
1644 class SimpleListChecker(nodes.GenericNodeVisitor):
1647 Raise `nodes.NodeFound` if non-simple list item is encountered.
1649 Here "simple" means a list item containing nothing other than a single
1650 paragraph, a simple list, or a paragraph followed by a simple list.
1652 This version also checks for simple field lists and docinfo.
1655 def default_visit(self, node):
1656 raise nodes.NodeFound
1658 def visit_list_item(self, node):
1659 # print "visiting list item", node.__class__
1660 children = [child for child in node.children
1661 if not isinstance(child, nodes.Invisible)]
1662 # print "has %s visible children" % len(children)
1663 if (children and isinstance(children[0], nodes.paragraph)
1664 and (isinstance(children[-1], nodes.bullet_list) or
1665 isinstance(children[-1], nodes.enumerated_list) or
1666 isinstance(children[-1], nodes.field_list))):
1667 children.pop()
1668 # print "%s children remain" % len(children)
1669 if len(children) <= 1:
1670 return
1671 else:
1672 # print "found", child.__class__, "in", node.__class__
1673 raise nodes.NodeFound
1675 def pass_node(self, node):
1676 pass
1678 def ignore_node(self, node):
1679 # ignore nodes that are never complex (can contain only inline nodes)
1680 raise nodes.SkipNode
1682 # Paragraphs and text
1683 visit_Text = ignore_node
1684 visit_paragraph = ignore_node
1686 # Lists
1687 visit_bullet_list = pass_node
1688 visit_enumerated_list = pass_node
1689 visit_docinfo = pass_node
1691 # Docinfo nodes:
1692 visit_author = ignore_node
1693 visit_authors = visit_list_item
1694 visit_address = visit_list_item
1695 visit_contact = pass_node
1696 visit_copyright = ignore_node
1697 visit_date = ignore_node
1698 visit_organization = ignore_node
1699 visit_status = ignore_node
1700 visit_version = visit_list_item
1702 # Definition list:
1703 visit_definition_list = pass_node
1704 visit_definition_list_item = pass_node
1705 visit_term = ignore_node
1706 visit_classifier = pass_node
1707 visit_definition = visit_list_item
1709 # Field list:
1710 visit_field_list = pass_node
1711 visit_field = pass_node
1712 # the field body corresponds to a list item
1713 visit_field_body = visit_list_item
1714 visit_field_name = ignore_node
1716 # Invisible nodes should be ignored.
1717 visit_comment = ignore_node
1718 visit_substitution_definition = ignore_node
1719 visit_target = ignore_node
1720 visit_pending = ignore_node