Clarify the use of "pycon" for "Python console" code.
[docutils.git] / docutils / writers / html_base / __init__.py
blob76a88cffcc4d94dc33750b8e8f86cc1480c0c76e
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 def visit_doctest_block(self, node):
757 self.body.append(self.starttag(node, 'pre', suffix='',
758 CLASS='code python doctest'))
760 def depart_doctest_block(self, node):
761 self.body.append('\n</pre>\n')
763 def visit_document(self, node):
764 self.head.append('<title>%s</title>\n'
765 % self.encode(node.get('title', '')))
767 def depart_document(self, node):
768 self.head_prefix.extend([self.doctype,
769 self.head_prefix_template %
770 {'lang': self.settings.language_code}])
771 self.html_prolog.append(self.doctype)
772 self.meta.insert(0, self.content_type % self.settings.output_encoding)
773 self.head.insert(0, self.content_type % self.settings.output_encoding)
774 if self.math_header:
775 if self.math_output == 'mathjax':
776 self.head.extend(self.math_header)
777 else:
778 self.stylesheet.extend(self.math_header)
779 # skip content-type meta tag with interpolated charset value:
780 self.html_head.extend(self.head[1:])
781 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
782 self.body_suffix.insert(0, '</div>\n')
783 self.fragment.extend(self.body) # self.fragment is the "naked" body
784 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
785 + self.docinfo + self.body
786 + self.body_suffix[:-1])
787 assert not self.context, 'len(context) = %s' % len(self.context)
789 def visit_emphasis(self, node):
790 self.body.append(self.starttag(node, 'em', ''))
792 def depart_emphasis(self, node):
793 self.body.append('</em>')
795 def visit_entry(self, node):
796 atts = {'class': []}
797 if isinstance(node.parent.parent, nodes.thead):
798 atts['class'].append('head')
799 if node.parent.parent.parent.stubs[node.parent.column]:
800 # "stubs" list is an attribute of the tgroup element
801 atts['class'].append('stub')
802 if atts['class']:
803 tagname = 'th'
804 atts['class'] = ' '.join(atts['class'])
805 else:
806 tagname = 'td'
807 del atts['class']
808 node.parent.column += 1
809 if 'morerows' in node:
810 atts['rowspan'] = node['morerows'] + 1
811 if 'morecols' in node:
812 atts['colspan'] = node['morecols'] + 1
813 node.parent.column += node['morecols']
814 self.body.append(self.starttag(node, tagname, '', **atts))
815 self.context.append('</%s>\n' % tagname.lower())
816 # TODO: why did the html4css1 writer insert an NBSP into empty cells?
817 # if len(node) == 0: # empty cell
818 # self.body.append('&#0160;') # no-break space
820 def depart_entry(self, node):
821 self.body.append(self.context.pop())
823 def visit_enumerated_list(self, node):
825 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
826 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
827 usable.
829 atts = {}
830 if 'start' in node:
831 atts['start'] = node['start']
832 if 'enumtype' in node:
833 atts['class'] = node['enumtype']
834 if self.is_compactable(node):
835 atts['class'] = (atts.get('class', '') + ' simple').strip()
836 self.body.append(self.starttag(node, 'ol', **atts))
838 def depart_enumerated_list(self, node):
839 self.body.append('</ol>\n')
841 # field-list
842 # ----------
843 # set as definition list, styled with CSS
845 def visit_field_list(self, node):
846 # Keep simple paragraphs in the field_body to enable CSS
847 # rule to start body on new line if the label is too long
848 classes = 'field-list'
849 if (self.is_compactable(node)):
850 classes += ' simple'
851 self.body.append(self.starttag(node, 'dl', CLASS=classes))
853 def depart_field_list(self, node):
854 self.body.append('</dl>\n')
856 def visit_field(self, node):
857 pass
859 def depart_field(self, node):
860 pass
862 def visit_field_name(self, node):
863 self.body.append(self.starttag(node, 'dt', ''))
865 def depart_field_name(self, node):
866 self.body.append('</dt>\n')
868 def visit_field_body(self, node):
869 self.body.append(self.starttag(node, 'dd', ''))
871 def depart_field_body(self, node):
872 self.body.append('</dd>\n')
874 def visit_figure(self, node):
875 atts = {'class': 'figure'}
876 if node.get('width'):
877 atts['style'] = 'width: %s' % node['width']
878 if node.get('align'):
879 atts['class'] += " align-" + node['align']
880 self.body.append(self.starttag(node, 'div', **atts))
882 def depart_figure(self, node):
883 self.body.append('</div>\n')
885 # use HTML 5 <footer> element?
886 def visit_footer(self, node):
887 self.context.append(len(self.body))
889 def depart_footer(self, node):
890 start = self.context.pop()
891 footer = [self.starttag(node, 'div', CLASS='footer'),
892 '<hr class="footer" />\n']
893 footer.extend(self.body[start:])
894 footer.append('\n</div>\n')
895 self.footer.extend(footer)
896 self.body_suffix[:0] = footer
897 del self.body[start:]
899 # footnotes
900 # ---------
901 # use definition list instead of table for footnote text
903 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
904 def visit_footnote(self, node):
905 if not self.in_footnote_list:
906 self.body.append('<dl class="footnote">\n')
907 self.in_footnote_list = True
909 def depart_footnote(self, node):
910 self.body.append('</dd>\n')
911 if not isinstance(node.next_node(descend=False, siblings=True),
912 nodes.footnote):
913 self.body.append('</dl>\n')
914 self.in_footnote_list = False
916 def visit_footnote_reference(self, node):
917 href = '#' + node['refid']
918 format = self.settings.footnote_references
919 if format == 'brackets':
920 suffix = '['
921 self.context.append(']')
922 else:
923 assert format == 'superscript'
924 suffix = '<sup>'
925 self.context.append('</sup>')
926 self.body.append(self.starttag(node, 'a', suffix,
927 CLASS='footnote-reference', href=href))
929 def depart_footnote_reference(self, node):
930 self.body.append(self.context.pop() + '</a>')
932 def visit_generated(self, node):
933 if 'sectnum' in node['classes']:
934 # get section number (strip trailing no-break-spaces)
935 sectnum = node.astext().rstrip(u' ')
936 # print sectnum.encode('utf-8')
937 self.body.append('<span class="sectnum">%s</span> '
938 % self.encode(sectnum))
939 # Content already processed:
940 raise nodes.SkipNode
942 def depart_generated(self, node):
943 pass
945 def visit_header(self, node):
946 self.context.append(len(self.body))
948 def depart_header(self, node):
949 start = self.context.pop()
950 header = [self.starttag(node, 'div', CLASS='header')]
951 header.extend(self.body[start:])
952 header.append('\n<hr class="header"/>\n</div>\n')
953 self.body_prefix.extend(header)
954 self.header.extend(header)
955 del self.body[start:]
957 # Image types to place in an <object> element
958 # SVG not supported by IE up to version 8
959 # (html4css1 strives for IE6 compatibility)
960 object_image_types = {#'.svg': 'image/svg+xml',
961 '.swf': 'application/x-shockwave-flash'}
963 def visit_image(self, node):
964 atts = {}
965 uri = node['uri']
966 ext = os.path.splitext(uri)[1].lower()
967 if ext in self.object_image_types:
968 atts['data'] = uri
969 atts['type'] = self.object_image_types[ext]
970 else:
971 atts['src'] = uri
972 atts['alt'] = node.get('alt', uri)
973 # image size
974 if 'width' in node:
975 atts['width'] = node['width']
976 if 'height' in node:
977 atts['height'] = node['height']
978 if 'scale' in node:
979 if (PIL and not ('width' in node and 'height' in node)
980 and self.settings.file_insertion_enabled):
981 imagepath = urllib.url2pathname(uri)
982 try:
983 img = PIL.Image.open(
984 imagepath.encode(sys.getfilesystemencoding()))
985 except (IOError, UnicodeEncodeError):
986 pass # TODO: warn?
987 else:
988 self.settings.record_dependencies.add(
989 imagepath.replace('\\', '/'))
990 if 'width' not in atts:
991 atts['width'] = '%dpx' % img.size[0]
992 if 'height' not in atts:
993 atts['height'] = '%dpx' % img.size[1]
994 del img
995 for att_name in 'width', 'height':
996 if att_name in atts:
997 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
998 assert match
999 atts[att_name] = '%s%s' % (
1000 float(match.group(1)) * (float(node['scale']) / 100),
1001 match.group(2))
1002 style = []
1003 for att_name in 'width', 'height':
1004 if att_name in atts:
1005 if re.match(r'^[0-9.]+$', atts[att_name]):
1006 # Interpret unitless values as pixels.
1007 atts[att_name] += 'px'
1008 style.append('%s: %s;' % (att_name, atts[att_name]))
1009 del atts[att_name]
1010 if style:
1011 atts['style'] = ' '.join(style)
1012 if (isinstance(node.parent, nodes.TextElement) or
1013 (isinstance(node.parent, nodes.reference) and
1014 not isinstance(node.parent.parent, nodes.TextElement))):
1015 # Inline context or surrounded by <a>...</a>.
1016 suffix = ''
1017 else:
1018 suffix = '\n'
1019 if 'align' in node:
1020 atts['class'] = 'align-%s' % node['align']
1021 if ext in self.object_image_types:
1022 # do NOT use an empty tag: incorrect rendering in browsers
1023 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1024 node.get('alt', uri) + '</object>' + suffix)
1025 else:
1026 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1028 def depart_image(self, node):
1029 # self.body.append(self.context.pop())
1030 pass
1032 def visit_inline(self, node):
1033 self.body.append(self.starttag(node, 'span', ''))
1035 def depart_inline(self, node):
1036 self.body.append('</span>')
1038 # footnote and citation label
1039 def label_delim(self, node, bracket, superscript):
1040 """put brackets around label?"""
1041 if isinstance(node.parent, nodes.footnote):
1042 if self.settings.footnote_references == 'brackets':
1043 return bracket
1044 else:
1045 return superscript
1046 assert isinstance(node.parent, nodes.citation)
1047 return bracket
1049 def visit_label(self, node):
1050 # pass parent node to get id into starttag:
1051 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
1052 # footnote/citation backrefs:
1053 if self.settings.footnote_backlinks:
1054 backrefs = node.parent['backrefs']
1055 if len(backrefs) == 1:
1056 self.body.append('<a class="fn-backref" href="#%s">'
1057 % backrefs[0])
1058 self.body.append(self.label_delim(node, '[', ''))
1060 def depart_label(self, node):
1061 self.body.append(self.label_delim(node, ']', ''))
1062 if self.settings.footnote_backlinks:
1063 backrefs = node.parent['backrefs']
1064 if len(backrefs) == 1:
1065 self.body.append('</a>')
1066 elif len(backrefs) > 1:
1067 # Python 2.4 fails with enumerate(backrefs, 1)
1068 backlinks = ['<a href="#%s">%s</a>' % (ref, i+1)
1069 for (i, ref) in enumerate(backrefs)]
1070 self.body.append('<span class="fn-backref">(%s)</span>'
1071 % ','.join(backlinks))
1072 self.body.append('</dt>\n<dd>')
1074 def visit_legend(self, node):
1075 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1077 def depart_legend(self, node):
1078 self.body.append('</div>\n')
1080 def visit_line(self, node):
1081 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1082 if not len(node):
1083 self.body.append('<br />')
1085 def depart_line(self, node):
1086 self.body.append('</div>\n')
1088 def visit_line_block(self, node):
1089 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1091 def depart_line_block(self, node):
1092 self.body.append('</div>\n')
1094 def visit_list_item(self, node):
1095 self.body.append(self.starttag(node, 'li', ''))
1097 def depart_list_item(self, node):
1098 self.body.append('</li>\n')
1100 # inline literal
1101 def visit_literal(self, node):
1102 # special case: "code" role
1103 classes = node.get('classes', [])
1104 if 'code' in classes:
1105 # filter 'code' from class arguments
1106 node['classes'] = [cls for cls in classes if cls != 'code']
1107 self.body.append(self.starttag(node, 'code', ''))
1108 return
1109 self.body.append(
1110 self.starttag(node, 'span', '', CLASS='docutils literal'))
1111 text = node.astext()
1112 # remove hard line breaks (except if in a parsed-literal block)
1113 if not isinstance(node.parent, nodes.literal_block):
1114 text = text.replace('\n', ' ')
1115 # Protect text like ``--an-option`` and the regular expression
1116 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1117 for token in self.words_and_spaces.findall(text):
1118 if token.strip() and self.sollbruchstelle.search(token):
1119 self.body.append('<span class="pre">%s</span>'
1120 % self.encode(token))
1121 else:
1122 self.body.append(self.encode(token))
1123 self.body.append('</span>')
1124 # Content already processed:
1125 raise nodes.SkipNode
1127 def depart_literal(self, node):
1128 # skipped unless literal element is from "code" role:
1129 self.body.append('</code>')
1131 def visit_literal_block(self, node):
1132 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1133 if 'code' in node.get('classes', []):
1134 self.body.append('<code>')
1136 def depart_literal_block(self, node):
1137 if 'code' in node.get('classes', []):
1138 self.body.append('</code>')
1139 self.body.append('</pre>\n')
1141 def visit_math(self, node, math_env=''):
1142 # If the method is called from visit_math_block(), math_env != ''.
1144 # As there is no native HTML math support, we provide alternatives:
1145 # LaTeX and MathJax math_output modes simply wrap the content,
1146 # HTML and MathML math_output modes also convert the math_code.
1147 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1148 self.document.reporter.error(
1149 'math-output format "%s" not supported '
1150 'falling back to "latex"'% self.math_output)
1151 self.math_output = 'latex'
1153 # HTML container
1154 tags = {# math_output: (block, inline, class-arguments)
1155 'mathml': ('div', '', ''),
1156 'html': ('div', 'span', 'formula'),
1157 'mathjax': ('div', 'span', 'math'),
1158 'latex': ('pre', 'tt', 'math'),
1160 tag = tags[self.math_output][math_env == '']
1161 clsarg = tags[self.math_output][2]
1162 # LaTeX container
1163 wrappers = {# math_mode: (inline, block)
1164 'mathml': (None, None),
1165 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1166 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1167 'latex': (None, None),
1169 wrapper = wrappers[self.math_output][math_env != '']
1170 # get and wrap content
1171 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1172 if wrapper and math_env:
1173 math_code = wrapper % (math_env, math_code, math_env)
1174 elif wrapper:
1175 math_code = wrapper % math_code
1176 # settings and conversion
1177 if self.math_output in ('latex', 'mathjax'):
1178 math_code = self.encode(math_code)
1179 if self.math_output == 'mathjax' and not self.math_header:
1180 if self.math_output_options:
1181 self.mathjax_url = self.math_output_options[0]
1182 self.math_header = [self.mathjax_script % self.mathjax_url]
1183 elif self.math_output == 'html':
1184 if self.math_output_options and not self.math_header:
1185 self.math_header = [self.stylesheet_call(
1186 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1187 for s in self.math_output_options[0].split(',')]
1188 # TODO: fix display mode in matrices and fractions
1189 math2html.DocumentParameters.displaymode = (math_env != '')
1190 math_code = math2html.math2html(math_code)
1191 elif self.math_output == 'mathml':
1192 self.doctype = self.doctype_mathml
1193 # self.content_type = self.content_type_mathml
1194 try:
1195 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1196 math_code = ''.join(mathml_tree.xml())
1197 except SyntaxError, err:
1198 err_node = self.document.reporter.error(err, base_node=node)
1199 self.visit_system_message(err_node)
1200 self.body.append(self.starttag(node, 'p'))
1201 self.body.append(u','.join(err.args))
1202 self.body.append('</p>\n')
1203 self.body.append(self.starttag(node, 'pre',
1204 CLASS='literal-block'))
1205 self.body.append(self.encode(math_code))
1206 self.body.append('\n</pre>\n')
1207 self.depart_system_message(err_node)
1208 raise nodes.SkipNode
1209 # append to document body
1210 if tag:
1211 self.body.append(self.starttag(node, tag,
1212 suffix='\n'*bool(math_env),
1213 CLASS=clsarg))
1214 self.body.append(math_code)
1215 if math_env: # block mode (equation, display)
1216 self.body.append('\n')
1217 if tag:
1218 self.body.append('</%s>' % tag)
1219 if math_env:
1220 self.body.append('\n')
1221 # Content already processed:
1222 raise nodes.SkipNode
1224 def depart_math(self, node):
1225 pass # never reached
1227 def visit_math_block(self, node):
1228 # print node.astext().encode('utf8')
1229 math_env = pick_math_environment(node.astext())
1230 self.visit_math(node, math_env=math_env)
1232 def depart_math_block(self, node):
1233 pass # never reached
1235 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1236 # HTML5/polyglott recommends using both
1237 def visit_meta(self, node):
1238 if node.hasattr('lang'):
1239 node['xml:lang'] = node['lang']
1240 # del(node['lang'])
1241 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1242 self.add_meta(meta)
1244 def depart_meta(self, node):
1245 pass
1247 def add_meta(self, tag):
1248 self.meta.append(tag)
1249 self.head.append(tag)
1251 def visit_option(self, node):
1252 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1254 def depart_option(self, node):
1255 self.body.append('</span>')
1256 if isinstance(node.next_node(descend=False, siblings=True),
1257 nodes.option):
1258 self.body.append(', ')
1260 def visit_option_argument(self, node):
1261 self.body.append(node.get('delimiter', ' '))
1262 self.body.append(self.starttag(node, 'var', ''))
1264 def depart_option_argument(self, node):
1265 self.body.append('</var>')
1267 def visit_option_group(self, node):
1268 self.body.append(self.starttag(node, 'dt', ''))
1269 self.body.append('<kbd>')
1271 def depart_option_group(self, node):
1272 self.body.append('</kbd></dt>\n')
1274 def visit_option_list(self, node):
1275 self.body.append(
1276 self.starttag(node, 'dl', CLASS='option-list'))
1278 def depart_option_list(self, node):
1279 self.body.append('</dl>\n')
1281 def visit_option_list_item(self, node):
1282 pass
1284 def depart_option_list_item(self, node):
1285 pass
1287 def visit_option_string(self, node):
1288 pass
1290 def depart_option_string(self, node):
1291 pass
1293 def visit_organization(self, node):
1294 self.visit_docinfo_item(node, 'organization', meta=False)
1296 def depart_organization(self, node):
1297 self.depart_docinfo_item()
1299 # Do not omit <p> tags
1300 # --------------------
1302 # The HTML4CSS1 writer does this to "produce
1303 # visually compact lists (less vertical whitespace)". This writer
1304 # relies on CSS rules for"visual compactness".
1306 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1307 # character data, so you cannot drop the <p> tags.
1308 # * Keeping simple paragraphs in the field_body enables a CSS
1309 # rule to start the field-body on a new line if the label is too long
1310 # * it makes the code simpler.
1312 # TODO: omit paragraph tags in simple table cells?
1314 def visit_paragraph(self, node):
1315 self.body.append(self.starttag(node, 'p', ''))
1317 def depart_paragraph(self, node):
1318 self.body.append('</p>')
1319 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1320 (len(node.parent) == 1)):
1321 self.body.append('\n')
1323 def visit_problematic(self, node):
1324 if node.hasattr('refid'):
1325 self.body.append('<a href="#%s">' % node['refid'])
1326 self.context.append('</a>')
1327 else:
1328 self.context.append('')
1329 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1331 def depart_problematic(self, node):
1332 self.body.append('</span>')
1333 self.body.append(self.context.pop())
1335 def visit_raw(self, node):
1336 if 'html' in node.get('format', '').split():
1337 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1338 if node['classes']:
1339 self.body.append(self.starttag(node, t, suffix=''))
1340 self.body.append(node.astext())
1341 if node['classes']:
1342 self.body.append('</%s>' % t)
1343 # Keep non-HTML raw text out of output:
1344 raise nodes.SkipNode
1346 def visit_reference(self, node):
1347 atts = {'class': 'reference'}
1348 if 'refuri' in node:
1349 atts['href'] = node['refuri']
1350 if ( self.settings.cloak_email_addresses
1351 and atts['href'].startswith('mailto:')):
1352 atts['href'] = self.cloak_mailto(atts['href'])
1353 self.in_mailto = True
1354 atts['class'] += ' external'
1355 else:
1356 assert 'refid' in node, \
1357 'References must have "refuri" or "refid" attribute.'
1358 atts['href'] = '#' + node['refid']
1359 atts['class'] += ' internal'
1360 if not isinstance(node.parent, nodes.TextElement):
1361 assert len(node) == 1 and isinstance(node[0], nodes.image)
1362 atts['class'] += ' image-reference'
1363 self.body.append(self.starttag(node, 'a', '', **atts))
1365 def depart_reference(self, node):
1366 self.body.append('</a>')
1367 if not isinstance(node.parent, nodes.TextElement):
1368 self.body.append('\n')
1369 self.in_mailto = False
1371 def visit_revision(self, node):
1372 self.visit_docinfo_item(node, 'revision', meta=False)
1374 def depart_revision(self, node):
1375 self.depart_docinfo_item()
1377 def visit_row(self, node):
1378 self.body.append(self.starttag(node, 'tr', ''))
1379 node.column = 0
1381 def depart_row(self, node):
1382 self.body.append('</tr>\n')
1384 def visit_rubric(self, node):
1385 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1387 def depart_rubric(self, node):
1388 self.body.append('</p>\n')
1390 # TODO: use the new HTML 5 element <section>?
1391 def visit_section(self, node):
1392 self.section_level += 1
1393 self.body.append(
1394 self.starttag(node, 'div', CLASS='section'))
1396 def depart_section(self, node):
1397 self.section_level -= 1
1398 self.body.append('</div>\n')
1400 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1401 def visit_sidebar(self, node):
1402 self.body.append(
1403 self.starttag(node, 'div', CLASS='sidebar'))
1404 self.in_sidebar = True
1406 def depart_sidebar(self, node):
1407 self.body.append('</div>\n')
1408 self.in_sidebar = False
1410 def visit_status(self, node):
1411 self.visit_docinfo_item(node, 'status', meta=False)
1413 def depart_status(self, node):
1414 self.depart_docinfo_item()
1416 def visit_strong(self, node):
1417 self.body.append(self.starttag(node, 'strong', ''))
1419 def depart_strong(self, node):
1420 self.body.append('</strong>')
1422 def visit_subscript(self, node):
1423 self.body.append(self.starttag(node, 'sub', ''))
1425 def depart_subscript(self, node):
1426 self.body.append('</sub>')
1428 def visit_substitution_definition(self, node):
1429 """Internal only."""
1430 raise nodes.SkipNode
1432 def visit_substitution_reference(self, node):
1433 self.unimplemented_visit(node)
1435 # h1–h6 elements must not be used to markup subheadings, subtitles,
1436 # alternative titles and taglines unless intended to be the heading for a
1437 # new section or subsection.
1438 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1440 def visit_subtitle(self, node):
1441 if isinstance(node.parent, nodes.sidebar):
1442 classes = 'sidebar-subtitle'
1443 elif isinstance(node.parent, nodes.document):
1444 classes = 'subtitle'
1445 self.in_document_title = len(self.body)
1446 elif isinstance(node.parent, nodes.section):
1447 classes = 'section-subtitle'
1448 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1450 def depart_subtitle(self, node):
1451 self.body.append('</p>\n')
1452 if self.in_document_title:
1453 self.subtitle = self.body[self.in_document_title:-1]
1454 self.in_document_title = 0
1455 self.body_pre_docinfo.extend(self.body)
1456 self.html_subtitle.extend(self.body)
1457 del self.body[:]
1459 def visit_superscript(self, node):
1460 self.body.append(self.starttag(node, 'sup', ''))
1462 def depart_superscript(self, node):
1463 self.body.append('</sup>')
1465 def visit_system_message(self, node):
1466 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1467 self.body.append('<p class="system-message-title">')
1468 backref_text = ''
1469 if len(node['backrefs']):
1470 backrefs = node['backrefs']
1471 if len(backrefs) == 1:
1472 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1473 % backrefs[0])
1474 else:
1475 i = 1
1476 backlinks = []
1477 for backref in backrefs:
1478 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1479 i += 1
1480 backref_text = ('; <em>backlinks: %s</em>'
1481 % ', '.join(backlinks))
1482 if node.hasattr('line'):
1483 line = ', line %s' % node['line']
1484 else:
1485 line = ''
1486 self.body.append('System Message: %s/%s '
1487 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1488 % (node['type'], node['level'],
1489 self.encode(node['source']), line, backref_text))
1491 def depart_system_message(self, node):
1492 self.body.append('</div>\n')
1494 # tables
1495 # ------
1496 # no hard-coded border setting in the table head::
1498 def visit_table(self, node):
1499 classes = [cls.strip(u' \t\n')
1500 for cls in self.settings.table_style.split(',')]
1501 tag = self.starttag(node, 'table', CLASS=' '.join(classes))
1502 self.body.append(tag)
1504 def depart_table(self, node):
1505 self.body.append('</table>\n')
1507 def visit_target(self, node):
1508 if not ('refuri' in node or 'refid' in node
1509 or 'refname' in node):
1510 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1511 self.context.append('</span>')
1512 else:
1513 self.context.append('')
1515 def depart_target(self, node):
1516 self.body.append(self.context.pop())
1518 # no hard-coded vertical alignment in table body::
1520 def visit_tbody(self, node):
1521 self.write_colspecs()
1522 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1523 self.body.append(self.starttag(node, 'tbody'))
1525 def depart_tbody(self, node):
1526 self.body.append('</tbody>\n')
1528 def visit_term(self, node):
1529 self.body.append(self.starttag(node, 'dt', ''))
1531 def depart_term(self, node):
1533 Leave the end tag to `self.visit_definition()`, in case there's a
1534 classifier.
1536 pass
1538 def visit_tgroup(self, node):
1539 # Mozilla needs <colgroup>:
1540 self.body.append(self.starttag(node, 'colgroup'))
1541 # Appended by thead or tbody:
1542 self.context.append('</colgroup>\n')
1543 node.stubs = []
1545 def depart_tgroup(self, node):
1546 pass
1548 def visit_thead(self, node):
1549 self.write_colspecs()
1550 self.body.append(self.context.pop()) # '</colgroup>\n'
1551 # There may or may not be a <thead>; this is for <tbody> to use:
1552 self.context.append('')
1553 self.body.append(self.starttag(node, 'thead'))
1555 def depart_thead(self, node):
1556 self.body.append('</thead>\n')
1558 def visit_title(self, node):
1559 """Only 6 section levels are supported by HTML."""
1560 check_id = 0 # TODO: is this a bool (False) or a counter?
1561 close_tag = '</p>\n'
1562 if isinstance(node.parent, nodes.topic):
1563 self.body.append(
1564 self.starttag(node, 'p', '', CLASS='topic-title first'))
1565 elif isinstance(node.parent, nodes.sidebar):
1566 self.body.append(
1567 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1568 elif isinstance(node.parent, nodes.Admonition):
1569 self.body.append(
1570 self.starttag(node, 'p', '', CLASS='admonition-title'))
1571 elif isinstance(node.parent, nodes.table):
1572 self.body.append(
1573 self.starttag(node, 'caption', ''))
1574 close_tag = '</caption>\n'
1575 elif isinstance(node.parent, nodes.document):
1576 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1577 close_tag = '</h1>\n'
1578 self.in_document_title = len(self.body)
1579 else:
1580 assert isinstance(node.parent, nodes.section)
1581 h_level = self.section_level + self.initial_header_level - 1
1582 atts = {}
1583 if (len(node.parent) >= 2 and
1584 isinstance(node.parent[1], nodes.subtitle)):
1585 atts['CLASS'] = 'with-subtitle'
1586 self.body.append(
1587 self.starttag(node, 'h%s' % h_level, '', **atts))
1588 atts = {}
1589 if node.hasattr('refid'):
1590 atts['class'] = 'toc-backref'
1591 atts['href'] = '#' + node['refid']
1592 if atts:
1593 self.body.append(self.starttag({}, 'a', '', **atts))
1594 close_tag = '</a></h%s>\n' % (h_level)
1595 else:
1596 close_tag = '</h%s>\n' % (h_level)
1597 self.context.append(close_tag)
1599 def depart_title(self, node):
1600 self.body.append(self.context.pop())
1601 if self.in_document_title:
1602 self.title = self.body[self.in_document_title:-1]
1603 self.in_document_title = 0
1604 self.body_pre_docinfo.extend(self.body)
1605 self.html_title.extend(self.body)
1606 del self.body[:]
1608 def visit_title_reference(self, node):
1609 self.body.append(self.starttag(node, 'cite', ''))
1611 def depart_title_reference(self, node):
1612 self.body.append('</cite>')
1614 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1615 def visit_topic(self, node):
1616 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1617 self.topic_classes = node['classes']
1618 # TODO: replace with ::
1619 # self.in_contents = 'contents' in node['classes']
1621 def depart_topic(self, node):
1622 self.body.append('</div>\n')
1623 self.topic_classes = []
1624 # TODO self.in_contents = False
1626 def visit_transition(self, node):
1627 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1629 def depart_transition(self, node):
1630 pass
1632 def visit_version(self, node):
1633 self.visit_docinfo_item(node, 'version', meta=False)
1635 def depart_version(self, node):
1636 self.depart_docinfo_item()
1638 def unimplemented_visit(self, node):
1639 raise NotImplementedError('visiting unimplemented node type: %s'
1640 % node.__class__.__name__)
1643 class SimpleListChecker(nodes.GenericNodeVisitor):
1646 Raise `nodes.NodeFound` if non-simple list item is encountered.
1648 Here "simple" means a list item containing nothing other than a single
1649 paragraph, a simple list, or a paragraph followed by a simple list.
1651 This version also checks for simple field lists and docinfo.
1654 def default_visit(self, node):
1655 raise nodes.NodeFound
1657 def visit_list_item(self, node):
1658 # print "visiting list item", node.__class__
1659 children = [child for child in node.children
1660 if not isinstance(child, nodes.Invisible)]
1661 # print "has %s visible children" % len(children)
1662 if (children and isinstance(children[0], nodes.paragraph)
1663 and (isinstance(children[-1], nodes.bullet_list) or
1664 isinstance(children[-1], nodes.enumerated_list) or
1665 isinstance(children[-1], nodes.field_list))):
1666 children.pop()
1667 # print "%s children remain" % len(children)
1668 if len(children) <= 1:
1669 return
1670 else:
1671 # print "found", child.__class__, "in", node.__class__
1672 raise nodes.NodeFound
1674 def pass_node(self, node):
1675 pass
1677 def ignore_node(self, node):
1678 # ignore nodes that are never complex (can contain only inline nodes)
1679 raise nodes.SkipNode
1681 # Paragraphs and text
1682 visit_Text = ignore_node
1683 visit_paragraph = ignore_node
1685 # Lists
1686 visit_bullet_list = pass_node
1687 visit_enumerated_list = pass_node
1688 visit_docinfo = pass_node
1690 # Docinfo nodes:
1691 visit_author = ignore_node
1692 visit_authors = visit_list_item
1693 visit_address = visit_list_item
1694 visit_contact = pass_node
1695 visit_copyright = ignore_node
1696 visit_date = ignore_node
1697 visit_organization = ignore_node
1698 visit_status = ignore_node
1699 visit_version = visit_list_item
1701 # Definition list:
1702 visit_definition_list = pass_node
1703 visit_definition_list_item = pass_node
1704 visit_term = ignore_node
1705 visit_classifier = pass_node
1706 visit_definition = visit_list_item
1708 # Field list:
1709 visit_field_list = pass_node
1710 visit_field = pass_node
1711 # the field body corresponds to a list item
1712 visit_field_body = visit_list_item
1713 visit_field_name = ignore_node
1715 # Invisible nodes should be ignored.
1716 visit_comment = ignore_node
1717 visit_substitution_definition = ignore_node
1718 visit_target = ignore_node
1719 visit_pending = ignore_node