New basic HTML writer: generates polyglott HTML 5 / XHTML 1.1 (transitional)
[docutils.git] / docutils / writers / html_base / __init__.py
blob19f33312c6d852b59e92e45039c418daedc0d397
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 ('Omit the XML declaration. Must be true for HTML5 conformance.',
140 ['--no-xml-declaration'],
141 {'dest': 'xml_declaration', 'default': False,
142 'action': 'store_false', 'validator': frontend.validate_boolean}),
143 ('Obfuscate email addresses to confuse harvesters while still '
144 'keeping email links usable with standards-compliant browsers.',
145 ['--cloak-email-addresses'],
146 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
148 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
150 config_section = 'html-base writer'
151 config_section_dependencies = ('writers',)
153 visitor_attributes = (
154 'head_prefix', 'head', 'stylesheet', 'body_prefix',
155 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
156 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
157 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
158 'html_body')
160 def get_transforms(self):
161 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
163 def __init__(self):
164 writers.Writer.__init__(self)
165 self.translator_class = HTMLTranslator
167 def translate(self):
168 self.visitor = visitor = self.translator_class(self.document)
169 self.document.walkabout(visitor)
170 for attr in self.visitor_attributes:
171 setattr(self, attr, getattr(visitor, attr))
172 self.output = self.apply_template()
174 def apply_template(self):
175 template_file = open(self.document.settings.template, 'rb')
176 template = unicode(template_file.read(), 'utf-8')
177 template_file.close()
178 subs = self.interpolation_dict()
179 return template % subs
181 def interpolation_dict(self):
182 subs = {}
183 settings = self.document.settings
184 for attr in self.visitor_attributes:
185 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
186 subs['encoding'] = settings.output_encoding
187 subs['version'] = docutils.__version__
188 return subs
190 def assemble_parts(self):
191 writers.Writer.assemble_parts(self)
192 for part in self.visitor_attributes:
193 self.parts[part] = ''.join(getattr(self, part))
196 class HTMLTranslator(nodes.NodeVisitor):
199 This writer generates `polyglott markup`: HTML 5 that is also valid XML.
202 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
203 doctype = (
204 '<!DOCTYPE html>\n')
205 doctype_mathml = doctype
207 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
208 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
209 content_type = ('<meta http-equiv="Content-Type"'
210 ' content="text/html; charset=%s" />\n')
211 content_type_mathml = ('<meta http-equiv="Content-Type"'
212 ' content="text/html; charset=%s" />\n')
214 generator = ('<meta name="generator" content="Docutils %s: '
215 'http://docutils.sourceforge.net/" />\n')
217 # Template for the MathJax script in the header:
218 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
219 # The latest version of MathJax from the distributed server:
220 # avaliable to the public under the `MathJax CDN Terms of Service`__
221 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
222 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
223 'config=TeX-AMS-MML_HTMLorMML')
224 # may be overwritten by custom URL appended to "mathjax"
226 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
227 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
228 words_and_spaces = re.compile(r'\S+| +|\n')
229 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
230 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
232 def __init__(self, document):
233 nodes.NodeVisitor.__init__(self, document)
234 self.settings = settings = document.settings
235 lcode = settings.language_code
236 self.language = languages.get_language(lcode, document.reporter)
237 self.meta = [self.generator % docutils.__version__]
238 self.head_prefix = []
239 self.html_prolog = []
240 if settings.xml_declaration:
241 self.head_prefix.append(self.xml_declaration
242 % settings.output_encoding)
243 # encoding not interpolated:
244 self.html_prolog.append(self.xml_declaration)
245 self.head = self.meta[:]
246 self.stylesheet = [self.stylesheet_call(path)
247 for path in utils.get_stylesheet_list(settings)]
248 self.body_prefix = ['</head>\n<body>\n']
249 # document title, subtitle display
250 self.body_pre_docinfo = []
251 # author, date, etc.
252 self.docinfo = []
253 self.body = []
254 self.fragment = []
255 self.body_suffix = ['</body>\n</html>\n']
256 self.section_level = 0
257 self.initial_header_level = int(settings.initial_header_level)
259 self.math_output = settings.math_output.split()
260 self.math_output_options = self.math_output[1:]
261 self.math_output = self.math_output[0].lower()
263 # A heterogenous stack used in conjunction with the tree traversal.
264 # Make sure that the pops correspond to the pushes:
265 self.context = []
267 self.topic_classes = [] # TODO: replace with self_in_contents
268 self.colspecs = []
269 self.compact_p = True
270 self.compact_simple = False
271 self.compact_field_list = False
272 self.in_docinfo = False
273 self.in_sidebar = False
274 self.in_footnote_list = False
275 self.title = []
276 self.subtitle = []
277 self.header = []
278 self.footer = []
279 self.html_head = [self.content_type] # charset not interpolated
280 self.html_title = []
281 self.html_subtitle = []
282 self.html_body = []
283 self.in_document_title = 0 # len(self.body) or 0
284 self.in_mailto = False
285 self.author_in_authors = False
286 self.math_header = []
288 def astext(self):
289 return ''.join(self.head_prefix + self.head
290 + self.stylesheet + self.body_prefix
291 + self.body_pre_docinfo + self.docinfo
292 + self.body + self.body_suffix)
294 def encode(self, text):
295 """Encode special characters in `text` & return."""
296 # @@@ A codec to do these and all other HTML entities would be nice.
297 text = unicode(text)
298 return text.translate({
299 ord('&'): u'&amp;',
300 ord('<'): u'&lt;',
301 ord('"'): u'&quot;',
302 ord('>'): u'&gt;',
303 ord('@'): u'&#64;', # may thwart some address harvesters
306 def cloak_mailto(self, uri):
307 """Try to hide a mailto: URL from harvesters."""
308 # Encode "@" using a URL octet reference (see RFC 1738).
309 # Further cloaking with HTML entities will be done in the
310 # `attval` function.
311 return uri.replace('@', '%40')
313 def cloak_email(self, addr):
314 """Try to hide the link text of a email link from harversters."""
315 # Surround at-signs and periods with <span> tags. ("@" has
316 # already been encoded to "&#64;" by the `encode` method.)
317 addr = addr.replace('&#64;', '<span>&#64;</span>')
318 addr = addr.replace('.', '<span>&#46;</span>')
319 return addr
321 def attval(self, text,
322 whitespace=re.compile('[\n\r\t\v\f]')):
323 """Cleanse, HTML encode, and return attribute value text."""
324 encoded = self.encode(whitespace.sub(' ', text))
325 if self.in_mailto and self.settings.cloak_email_addresses:
326 # Cloak at-signs ("%40") and periods with HTML entities.
327 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
328 encoded = encoded.replace('.', '&#46;')
329 return encoded
331 def stylesheet_call(self, path):
332 """Return code to reference or embed stylesheet file `path`"""
333 if self.settings.embed_stylesheet:
334 try:
335 content = io.FileInput(source_path=path,
336 encoding='utf-8').read()
337 self.settings.record_dependencies.add(path)
338 except IOError, err:
339 msg = u"Cannot embed stylesheet '%s': %s." % (
340 path, SafeString(err.strerror))
341 self.document.reporter.error(msg)
342 return '<--- %s --->\n' % msg
343 return self.embedded_stylesheet % content
344 # else link to style file:
345 if self.settings.stylesheet_path:
346 # adapt path relative to output (cf. config.html#stylesheet-path)
347 path = utils.relative_path(self.settings._destination, path)
348 return self.stylesheet_link % self.encode(path)
350 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
352 Construct and return a start tag given a node (id & class attributes
353 are extracted), tag name, and optional attributes.
355 tagname = tagname.lower()
356 prefix = []
357 atts = {}
358 ids = []
359 for (name, value) in attributes.items():
360 atts[name.lower()] = value
361 classes = []
362 languages = []
363 # unify class arguments and move language specification
364 for cls in node.get('classes', []) + atts.pop('class', '').split() :
365 if cls.startswith('language-'):
366 languages.append(cls[9:])
367 elif cls.strip() and cls not in classes:
368 classes.append(cls)
369 if languages:
370 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
371 atts[self.lang_attribute] = languages[0]
372 if classes:
373 atts['class'] = ' '.join(classes)
374 assert 'id' not in atts
375 ids.extend(node.get('ids', []))
376 if 'ids' in atts:
377 ids.extend(atts['ids'])
378 del atts['ids']
379 if ids:
380 atts['id'] = ids[0]
381 for id in ids[1:]:
382 # Add empty "span" elements for additional IDs. Note
383 # that we cannot use empty "a" elements because there
384 # may be targets inside of references, but nested "a"
385 # elements aren't allowed in XHTML (even if they do
386 # not all have a "href" attribute).
387 if empty:
388 # Empty tag. Insert target right in front of element.
389 prefix.append('<span id="%s"></span>' % id)
390 else:
391 # Non-empty tag. Place the auxiliary <span> tag
392 # *inside* the element, as the first child.
393 suffix += '<span id="%s"></span>' % id
394 attlist = atts.items()
395 attlist.sort()
396 parts = [tagname]
397 for name, value in attlist:
398 # value=None was used for boolean attributes without
399 # value, but this isn't supported by XHTML.
400 assert value is not None
401 if isinstance(value, list):
402 values = [unicode(v) for v in value]
403 parts.append('%s="%s"' % (name.lower(),
404 self.attval(' '.join(values))))
405 else:
406 parts.append('%s="%s"' % (name.lower(),
407 self.attval(unicode(value))))
408 if empty:
409 infix = ' /'
410 else:
411 infix = ''
412 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
414 def emptytag(self, node, tagname, suffix='\n', **attributes):
415 """Construct and return an XML-compatible empty tag."""
416 return self.starttag(node, tagname, suffix, empty=True, **attributes)
418 def set_class_on_child(self, node, class_, index=0):
420 Set class `class_` on the visible child no. index of `node`.
421 Do nothing if node has fewer children than `index`.
423 children = [n for n in node if not isinstance(n, nodes.Invisible)]
424 try:
425 child = children[index]
426 except IndexError:
427 return
428 child['classes'].append(class_)
430 def set_first_last(self, node):
431 pass
432 # TODO: remove calls to this function
434 def visit_Text(self, node):
435 text = node.astext()
436 encoded = self.encode(text)
437 if self.in_mailto and self.settings.cloak_email_addresses:
438 encoded = self.cloak_email(encoded)
439 self.body.append(encoded)
441 def depart_Text(self, node):
442 pass
444 def visit_abbreviation(self, node):
445 # @@@ implementation incomplete ("title" attribute)
446 self.body.append(self.starttag(node, 'abbr', ''))
448 def depart_abbreviation(self, node):
449 self.body.append('</abbr>')
451 def visit_acronym(self, node):
452 # @@@ implementation incomplete ("title" attribute)
453 self.body.append(self.starttag(node, 'abbr', ''))
455 def depart_acronym(self, node):
456 self.body.append('</abbr>')
458 def visit_address(self, node):
459 self.visit_docinfo_item(node, 'address', meta=False)
460 self.body.append(self.starttag(node, 'pre', '', CLASS='address'))
462 def depart_address(self, node):
463 self.body.append('\n</pre>\n')
464 self.depart_docinfo_item()
466 def visit_admonition(self, node):
467 node['classes'].insert(0, 'admonition')
468 self.body.append(self.starttag(node, 'div'))
469 self.set_first_last(node)
471 def depart_admonition(self, node=None):
472 self.body.append('</div>\n')
474 attribution_formats = {'dash': ('&mdash;', ''),
475 'parentheses': ('(', ')'),
476 'parens': ('(', ')'),
477 'none': ('', '')}
479 def visit_attribution(self, node):
480 prefix, suffix = self.attribution_formats[self.settings.attribution]
481 self.context.append(suffix)
482 self.body.append(
483 self.starttag(node, 'p', prefix, CLASS='attribution'))
485 def depart_attribution(self, node):
486 self.body.append(self.context.pop() + '</p>\n')
488 # author, authors
489 # ---------------
490 # Use paragraphs instead of hard-coded linebreaks.
492 def visit_author(self, node):
493 if not(isinstance(node.parent, nodes.authors)):
494 self.visit_docinfo_item(node, 'author')
495 self.body.append('<p>')
497 def depart_author(self, node):
498 self.body.append('</p>')
499 if isinstance(node.parent, nodes.authors):
500 self.body.append('\n')
501 else:
502 self.depart_docinfo_item()
504 def visit_authors(self, node):
505 self.visit_docinfo_item(node, 'authors', meta=False)
507 def depart_authors(self, node):
508 self.depart_docinfo_item()
510 def visit_block_quote(self, node):
511 self.body.append(self.starttag(node, 'blockquote'))
513 def depart_block_quote(self, node):
514 self.body.append('</blockquote>\n')
516 def check_simple_list(self, node):
517 """Check for a simple list that can be rendered compactly."""
518 visitor = SimpleListChecker(self.document)
519 try:
520 node.walk(visitor)
521 except nodes.NodeFound:
522 return None
523 else:
524 return 1
526 # Compact lists
527 # ------------
528 # Include definition lists and field lists (in addition to ordered
529 # and unordered lists) in the test if a list is "simple" (cf. the
530 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
531 # the end of this file).
533 def is_compactable(self, node):
534 # print "is_compactable %s ?" % node.__class__,
535 # explicite class arguments have precedence
536 if 'compact' in node['classes']:
537 # print "explicitely compact"
538 return True
539 if 'open' in node['classes']:
540 # print "explicitely open"
541 return False
542 # check config setting:
543 if (isinstance(node, nodes.field_list) or
544 isinstance(node, nodes.definition_list)
545 ) and not self.settings.compact_field_lists:
546 # print "`compact-field-lists` is False"
547 return False
548 if (isinstance(node, nodes.enumerated_list) or
549 isinstance(node, nodes.bullet_list)
550 ) and not self.settings.compact_lists:
551 # print "`compact-lists` is False"
552 return False
553 # more special cases:
554 if (self.topic_classes == ['contents']): # TODO: self.in_contents
555 return True
556 # check the list items:
557 visitor = SimpleListChecker(self.document)
558 try:
559 node.walk(visitor)
560 except nodes.NodeFound:
561 # print "complex node"
562 return False
563 else:
564 # print "simple list"
565 return True
567 def visit_bullet_list(self, node):
568 atts = {}
569 old_compact_simple = self.compact_simple
570 self.context.append((self.compact_simple, self.compact_p))
571 self.compact_p = None
572 self.compact_simple = self.is_compactable(node)
573 if self.compact_simple and not old_compact_simple:
574 atts['class'] = 'simple'
575 self.body.append(self.starttag(node, 'ul', **atts))
577 def depart_bullet_list(self, node):
578 self.compact_simple, self.compact_p = self.context.pop()
579 self.body.append('</ul>\n')
581 def visit_caption(self, node):
582 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
584 def depart_caption(self, node):
585 self.body.append('</p>\n')
587 # citations
588 # ---------
589 # Use definition list instead of table for bibliographic references.
590 # Join adjacent citation entries.
592 def visit_citation(self, node):
593 if not self.in_footnote_list:
594 self.body.append('<dl class="citation">\n')
595 self.in_footnote_list = True
597 def depart_citation(self, node):
598 self.body.append('</dd>\n')
599 if not isinstance(node.next_node(descend=False, siblings=True),
600 nodes.citation):
601 self.body.append('</dl>\n')
602 self.in_footnote_list = False
604 def visit_citation_reference(self, node):
605 href = '#'
606 if 'refid' in node:
607 href += node['refid']
608 elif 'refname' in node:
609 href += self.document.nameids[node['refname']]
610 # else: # TODO system message (or already in the transform)?
611 # 'Citation reference missing.'
612 self.body.append(self.starttag(
613 node, 'a', '[', CLASS='citation-reference', href=href))
615 def depart_citation_reference(self, node):
616 self.body.append(']</a>')
618 # classifier
619 # ----------
620 # don't insert classifier-delimiter here (done by CSS)
622 def visit_classifier(self, node):
623 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
625 def depart_classifier(self, node):
626 self.body.append('</span>')
628 def visit_colspec(self, node):
629 self.colspecs.append(node)
630 # "stubs" list is an attribute of the tgroup element:
631 node.parent.stubs.append(node.attributes.get('stub'))
633 def depart_colspec(self, node):
634 pass
636 def write_colspecs(self):
637 width = 0
638 for node in self.colspecs:
639 width += node['colwidth']
640 for node in self.colspecs:
641 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
642 self.body.append(self.emptytag(node, 'col',
643 style='width: %i%%' % colwidth))
644 self.colspecs = []
646 def visit_comment(self, node,
647 sub=re.compile('-(?=-)').sub):
648 """Escape double-dashes in comment text."""
649 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
650 # Content already processed:
651 raise nodes.SkipNode
653 def visit_compound(self, node):
654 self.body.append(self.starttag(node, 'div', CLASS='compound'))
655 if len(node) > 1:
656 node[0]['classes'].append('compound-first')
657 node[-1]['classes'].append('compound-last')
658 for child in node[1:-1]:
659 child['classes'].append('compound-middle')
661 def depart_compound(self, node):
662 self.body.append('</div>\n')
664 def visit_container(self, node):
665 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
667 def depart_container(self, node):
668 self.body.append('</div>\n')
670 def visit_contact(self, node):
671 self.visit_docinfo_item(node, 'contact', meta=False)
673 def depart_contact(self, node):
674 self.depart_docinfo_item()
676 def visit_copyright(self, node):
677 self.visit_docinfo_item(node, 'copyright', meta=False)
679 def depart_copyright(self, node):
680 self.depart_docinfo_item()
682 def visit_date(self, node):
683 self.visit_docinfo_item(node, 'date', meta=False)
685 def depart_date(self, node):
686 self.depart_docinfo_item()
688 def visit_decoration(self, node):
689 pass
691 def depart_decoration(self, node):
692 pass
694 def visit_definition(self, node):
695 self.body.append('</dt>\n')
696 self.body.append(self.starttag(node, 'dd', ''))
697 self.set_first_last(node)
699 def depart_definition(self, node):
700 self.body.append('</dd>\n')
702 def visit_definition_list(self, node):
703 classes = node.setdefault('classes', [])
704 if self.is_compactable(node):
705 classes.append('simple')
706 self.body.append(self.starttag(node, 'dl'))
708 def depart_definition_list(self, node):
709 self.body.append('</dl>\n')
711 def visit_definition_list_item(self, node):
712 # pass class arguments, ids and names to definition term:
713 node.children[0]['classes'] = (
714 node.get('classes', []) + node.children[0].get('classes', []))
715 node.children[0]['ids'] = (
716 node.get('ids', []) + node.children[0].get('ids', []))
717 node.children[0]['names'] = (
718 node.get('names', []) + node.children[0].get('names', []))
720 def depart_definition_list_item(self, node):
721 pass
723 def visit_description(self, node):
724 self.body.append(self.starttag(node, 'dd', ''))
726 def depart_description(self, node):
727 self.body.append('</dd>\n')
730 # docinfo
731 # -------
732 # use definition list instead of table
734 def visit_docinfo(self, node):
735 classes = 'docinfo'
736 if (self.is_compactable(node)):
737 classes += ' simple'
738 self.body.append(self.starttag(node, 'dl', CLASS=classes))
740 def depart_docinfo(self, node):
741 self.body.append('</dl>\n')
743 def visit_docinfo_item(self, node, name, meta=True):
744 if meta:
745 meta_tag = '<meta name="%s" content="%s" />\n' \
746 % (name, self.attval(node.astext()))
747 self.add_meta(meta_tag)
748 self.body.append('<dt class="%s">%s</dt>\n'
749 % (name, self.language.labels[name]))
750 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
752 def depart_docinfo_item(self):
753 self.body.append('</dd>\n')
755 # TODO: RSt-parser should treat this as code-block with class "pycon".
756 def visit_doctest_block(self, node):
757 self.body.append(self.starttag(node, 'pre', suffix='',
758 CLASS='code pycon doctest-block'))
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 if len(node) == 0: # empty cell
817 self.body.append('&nbsp;')
818 self.set_first_last(node)
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 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
835 # single "format" attribute? Use CSS2?
836 old_compact_simple = self.compact_simple
837 self.context.append((self.compact_simple, self.compact_p))
838 self.compact_p = None
839 self.compact_simple = self.is_compactable(node)
840 if self.compact_simple and not old_compact_simple:
841 atts['class'] = (atts.get('class', '') + ' simple').strip()
842 self.body.append(self.starttag(node, 'ol', **atts))
844 def depart_enumerated_list(self, node):
845 self.compact_simple, self.compact_p = self.context.pop()
846 self.body.append('</ol>\n')
848 # field-list
849 # ----------
850 # set as definition list, styled with CSS
852 def visit_field_list(self, node):
853 # Keep simple paragraphs in the field_body to enable CSS
854 # rule to start body on new line if the label is too long
855 classes = 'field-list'
856 if (self.is_compactable(node)):
857 classes += ' simple'
858 self.body.append(self.starttag(node, 'dl', CLASS=classes))
860 def depart_field_list(self, node):
861 self.body.append('</dl>\n')
863 def visit_field(self, node):
864 pass
866 def depart_field(self, node):
867 pass
869 def visit_field_name(self, node):
870 self.body.append(self.starttag(node, 'dt', ''))
872 def depart_field_name(self, node):
873 self.body.append('</dt>\n')
875 def visit_field_body(self, node):
876 self.body.append(self.starttag(node, 'dd', ''))
878 def depart_field_body(self, node):
879 self.body.append('</dd>\n')
881 def visit_figure(self, node):
882 atts = {'class': 'figure'}
883 if node.get('width'):
884 atts['style'] = 'width: %s' % node['width']
885 if node.get('align'):
886 atts['class'] += " align-" + node['align']
887 self.body.append(self.starttag(node, 'div', **atts))
889 def depart_figure(self, node):
890 self.body.append('</div>\n')
892 # use HTML 5 <footer> element?
893 def visit_footer(self, node):
894 self.context.append(len(self.body))
896 def depart_footer(self, node):
897 start = self.context.pop()
898 footer = [self.starttag(node, 'div', CLASS='footer'),
899 '<hr class="footer" />\n']
900 footer.extend(self.body[start:])
901 footer.append('\n</div>\n')
902 self.footer.extend(footer)
903 self.body_suffix[:0] = footer
904 del self.body[start:]
906 # footnotes
907 # ---------
908 # use definition list instead of table for footnote text
910 def visit_footnote(self, node):
911 if not self.in_footnote_list:
912 self.body.append('<dl class="footnote">\n')
913 self.in_footnote_list = True
915 def depart_footnote(self, node):
916 self.body.append('</dd>\n')
917 if not isinstance(node.next_node(descend=False, siblings=True),
918 nodes.footnote):
919 self.body.append('</dl>\n')
920 self.in_footnote_list = False
922 def visit_footnote_reference(self, node):
923 href = '#' + node['refid']
924 format = self.settings.footnote_references
925 if format == 'brackets':
926 suffix = '['
927 self.context.append(']')
928 else:
929 assert format == 'superscript'
930 suffix = '<sup>'
931 self.context.append('</sup>')
932 self.body.append(self.starttag(node, 'a', suffix,
933 CLASS='footnote-reference', href=href))
935 def depart_footnote_reference(self, node):
936 self.body.append(self.context.pop() + '</a>')
938 def visit_generated(self, node):
939 if 'sectnum' in node['classes']:
940 # get section number (strip trailing no-break-spaces)
941 sectnum = node.astext().rstrip(u' ')
942 # print sectnum.encode('utf-8')
943 self.body.append('<span class="sectnum">%s</span> '
944 % self.encode(sectnum))
945 # Content already processed:
946 raise nodes.SkipNode
948 def depart_generated(self, node):
949 pass
951 def visit_header(self, node):
952 self.context.append(len(self.body))
954 def depart_header(self, node):
955 start = self.context.pop()
956 header = [self.starttag(node, 'div', CLASS='header')]
957 header.extend(self.body[start:])
958 header.append('\n<hr class="header"/>\n</div>\n')
959 self.body_prefix.extend(header)
960 self.header.extend(header)
961 del self.body[start:]
963 # Image types to place in an <object> element
964 # SVG not supported by IE up to version 8
965 # (html4css1 strives for IE6 compatibility)
966 object_image_types = {'.svg': 'image/svg+xml',
967 '.swf': 'application/x-shockwave-flash'}
969 def visit_image(self, node):
970 atts = {}
971 uri = node['uri']
972 ext = os.path.splitext(uri)[1].lower()
973 if ext in self.object_image_types: # ('.svg', '.swf'):
974 atts['data'] = uri
975 atts['type'] = self.object_image_types[ext]
976 else:
977 atts['src'] = uri
978 atts['alt'] = node.get('alt', uri)
979 # image size
980 if 'width' in node:
981 atts['width'] = node['width']
982 if 'height' in node:
983 atts['height'] = node['height']
984 if 'scale' in node:
985 if (PIL and not ('width' in node and 'height' in node)
986 and self.settings.file_insertion_enabled):
987 imagepath = urllib.url2pathname(uri)
988 try:
989 img = PIL.Image.open(
990 imagepath.encode(sys.getfilesystemencoding()))
991 except (IOError, UnicodeEncodeError):
992 pass # TODO: warn?
993 else:
994 self.settings.record_dependencies.add(
995 imagepath.replace('\\', '/'))
996 if 'width' not in atts:
997 atts['width'] = '%dpx' % img.size[0]
998 if 'height' not in atts:
999 atts['height'] = '%dpx' % img.size[1]
1000 del img
1001 for att_name in 'width', 'height':
1002 if att_name in atts:
1003 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1004 assert match
1005 atts[att_name] = '%s%s' % (
1006 float(match.group(1)) * (float(node['scale']) / 100),
1007 match.group(2))
1008 style = []
1009 for att_name in 'width', 'height':
1010 if att_name in atts:
1011 if re.match(r'^[0-9.]+$', atts[att_name]):
1012 # Interpret unitless values as pixels.
1013 atts[att_name] += 'px'
1014 style.append('%s: %s;' % (att_name, atts[att_name]))
1015 del atts[att_name]
1016 if style:
1017 atts['style'] = ' '.join(style)
1018 if (isinstance(node.parent, nodes.TextElement) or
1019 (isinstance(node.parent, nodes.reference) and
1020 not isinstance(node.parent.parent, nodes.TextElement))):
1021 # Inline context or surrounded by <a>...</a>.
1022 suffix = ''
1023 else:
1024 suffix = '\n'
1025 if 'align' in node:
1026 atts['class'] = 'align-%s' % node['align']
1027 if ext in self.object_image_types: # ('.svg', '.swf')
1028 # do NOT use an empty tag: incorrect rendering in browsers
1029 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1030 node.get('alt', uri) + '</object>' + suffix)
1031 else:
1032 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1034 def depart_image(self, node):
1035 # self.body.append(self.context.pop())
1036 pass
1038 def visit_inline(self, node):
1039 self.body.append(self.starttag(node, 'span', ''))
1041 def depart_inline(self, node):
1042 self.body.append('</span>')
1044 # footnote and citation label
1045 def label_delim(self, node, bracket, superscript):
1046 """put brackets around label?"""
1047 if isinstance(node.parent, nodes.footnote):
1048 if self.settings.footnote_references == 'brackets':
1049 return bracket
1050 else:
1051 return superscript
1052 assert isinstance(node.parent, nodes.citation)
1053 return bracket
1055 def visit_label(self, node):
1056 # pass parent node to get id into starttag:
1057 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
1058 # footnote/citation backrefs:
1059 if self.settings.footnote_backlinks:
1060 backrefs = node.parent['backrefs']
1061 if len(backrefs) == 1:
1062 self.body.append('<a class="fn-backref" href="#%s">'
1063 % backrefs[0])
1064 self.body.append(self.label_delim(node, '[', ''))
1066 def depart_label(self, node):
1067 self.body.append(self.label_delim(node, ']', ''))
1068 if self.settings.footnote_backlinks:
1069 backrefs = node.parent['backrefs']
1070 if len(backrefs) == 1:
1071 self.body.append('</a>')
1072 elif len(backrefs) > 1:
1073 # Python 2.4 fails with enumerate(backrefs, 1)
1074 backlinks = ['<a href="#%s">%s</a>' % (ref, i+1)
1075 for (i, ref) in enumerate(backrefs)]
1076 self.body.append('<span class="fn-backref">(%s)</span>'
1077 % ','.join(backlinks))
1078 self.body.append('</dt>\n<dd>')
1080 def visit_legend(self, node):
1081 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1083 def depart_legend(self, node):
1084 self.body.append('</div>\n')
1086 def visit_line(self, node):
1087 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1088 if not len(node):
1089 self.body.append('<br />')
1091 def depart_line(self, node):
1092 self.body.append('</div>\n')
1094 def visit_line_block(self, node):
1095 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1097 def depart_line_block(self, node):
1098 self.body.append('</div>\n')
1100 def visit_list_item(self, node):
1101 self.body.append(self.starttag(node, 'li', ''))
1103 def depart_list_item(self, node):
1104 self.body.append('</li>\n')
1106 # inline literal
1107 def visit_literal(self, node):
1108 # special case: "code" role
1109 classes = node.get('classes', [])
1110 if 'code' in classes:
1111 # filter 'code' from class arguments
1112 node['classes'] = [cls for cls in classes if cls != 'code']
1113 self.body.append(self.starttag(node, 'code', ''))
1114 return
1115 self.body.append(
1116 self.starttag(node, 'span', '', CLASS='docutils literal'))
1117 text = node.astext()
1118 # remove hard line breaks (except if in a parsed-literal block)
1119 if not isinstance(node.parent, nodes.literal_block):
1120 text = text.replace('\n', ' ')
1121 # Protect text like ``--an-option`` and the regular expression
1122 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1123 for token in self.words_and_spaces.findall(text):
1124 if token.strip() and self.sollbruchstelle.search(token):
1125 self.body.append('<span class="pre">%s</span>'
1126 % self.encode(token))
1127 else:
1128 self.body.append(self.encode(token))
1129 self.body.append('</span>')
1130 # Content already processed:
1131 raise nodes.SkipNode
1133 def depart_literal(self, node):
1134 # skipped unless literal element is from "code" role:
1135 self.body.append('</code>')
1137 def visit_literal_block(self, node):
1138 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1140 def depart_literal_block(self, node):
1141 self.body.append('\n</pre>\n')
1143 def visit_math(self, node, math_env=''):
1144 # If the method is called from visit_math_block(), math_env != ''.
1146 # As there is no native HTML math support, we provide alternatives:
1147 # LaTeX and MathJax math_output modes simply wrap the content,
1148 # HTML and MathML math_output modes also convert the math_code.
1149 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1150 self.document.reporter.error(
1151 'math-output format "%s" not supported '
1152 'falling back to "latex"'% self.math_output)
1153 self.math_output = 'latex'
1155 # HTML container
1156 tags = {# math_output: (block, inline, class-arguments)
1157 'mathml': ('div', '', ''),
1158 'html': ('div', 'span', 'formula'),
1159 'mathjax': ('div', 'span', 'math'),
1160 'latex': ('pre', 'tt', 'math'),
1162 tag = tags[self.math_output][math_env == '']
1163 clsarg = tags[self.math_output][2]
1164 # LaTeX container
1165 wrappers = {# math_mode: (inline, block)
1166 'mathml': (None, None),
1167 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1168 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1169 'latex': (None, None),
1171 wrapper = wrappers[self.math_output][math_env != '']
1172 # get and wrap content
1173 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1174 if wrapper and math_env:
1175 math_code = wrapper % (math_env, math_code, math_env)
1176 elif wrapper:
1177 math_code = wrapper % math_code
1178 # settings and conversion
1179 if self.math_output in ('latex', 'mathjax'):
1180 math_code = self.encode(math_code)
1181 if self.math_output == 'mathjax' and not self.math_header:
1182 if self.math_output_options:
1183 self.mathjax_url = self.math_output_options[0]
1184 self.math_header = [self.mathjax_script % self.mathjax_url]
1185 elif self.math_output == 'html':
1186 if self.math_output_options and not self.math_header:
1187 self.math_header = [self.stylesheet_call(
1188 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1189 for s in self.math_output_options[0].split(',')]
1190 # TODO: fix display mode in matrices and fractions
1191 math2html.DocumentParameters.displaymode = (math_env != '')
1192 math_code = math2html.math2html(math_code)
1193 elif self.math_output == 'mathml':
1194 self.doctype = self.doctype_mathml
1195 self.content_type = self.content_type_mathml
1196 try:
1197 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1198 math_code = ''.join(mathml_tree.xml())
1199 except SyntaxError, err:
1200 err_node = self.document.reporter.error(err, base_node=node)
1201 self.visit_system_message(err_node)
1202 self.body.append(self.starttag(node, 'p'))
1203 self.body.append(u','.join(err.args))
1204 self.body.append('</p>\n')
1205 self.body.append(self.starttag(node, 'pre',
1206 CLASS='literal-block'))
1207 self.body.append(self.encode(math_code))
1208 self.body.append('\n</pre>\n')
1209 self.depart_system_message(err_node)
1210 raise nodes.SkipNode
1211 # append to document body
1212 if tag:
1213 self.body.append(self.starttag(node, tag,
1214 suffix='\n'*bool(math_env),
1215 CLASS=clsarg))
1216 self.body.append(math_code)
1217 if math_env: # block mode (equation, display)
1218 self.body.append('\n')
1219 if tag:
1220 self.body.append('</%s>' % tag)
1221 if math_env:
1222 self.body.append('\n')
1223 # Content already processed:
1224 raise nodes.SkipNode
1226 def depart_math(self, node):
1227 pass # never reached
1229 def visit_math_block(self, node):
1230 # print node.astext().encode('utf8')
1231 math_env = pick_math_environment(node.astext())
1232 self.visit_math(node, math_env=math_env)
1234 def depart_math_block(self, node):
1235 pass # never reached
1237 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1238 # HTML5/polyglott recommends using both
1239 def visit_meta(self, node):
1240 if node.hasattr('lang'):
1241 node['xml:lang'] = node['lang']
1242 # del(node['lang'])
1243 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1244 self.add_meta(meta)
1246 def depart_meta(self, node):
1247 pass
1249 def add_meta(self, tag):
1250 self.meta.append(tag)
1251 self.head.append(tag)
1253 def visit_option(self, node):
1254 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1256 def depart_option(self, node):
1257 self.body.append('</span>')
1258 if isinstance(node.next_node(descend=False, siblings=True),
1259 nodes.option):
1260 self.body.append(', ')
1262 def visit_option_argument(self, node):
1263 self.body.append(node.get('delimiter', ' '))
1264 self.body.append(self.starttag(node, 'var', ''))
1266 def depart_option_argument(self, node):
1267 self.body.append('</var>')
1269 def visit_option_group(self, node):
1270 self.body.append(self.starttag(node, 'dt', ''))
1271 self.body.append('<kbd>')
1273 def depart_option_group(self, node):
1274 self.body.append('</kbd></dt>\n')
1276 def visit_option_list(self, node):
1277 self.body.append(
1278 self.starttag(node, 'dl', CLASS='option-list'))
1280 def depart_option_list(self, node):
1281 self.body.append('</dl>\n')
1283 def visit_option_list_item(self, node):
1284 pass
1286 def depart_option_list_item(self, node):
1287 pass
1289 def visit_option_string(self, node):
1290 pass
1292 def depart_option_string(self, node):
1293 pass
1295 def visit_organization(self, node):
1296 self.visit_docinfo_item(node, 'organization', meta=False)
1298 def depart_organization(self, node):
1299 self.depart_docinfo_item()
1301 # Do not omit <p> tags
1302 # --------------------
1304 # The HTML4CSS1 writer does this to "produce
1305 # visually compact lists (less vertical whitespace)". This writer
1306 # relies on CSS rules for"visual compactness".
1308 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1309 # character data, so you cannot drop the <p> tags.
1310 # * Keeping simple paragraphs in the field_body enables a CSS
1311 # rule to start the field-body on a new line if the label is too long
1312 # * it makes the code simpler.
1314 # TODO: omit paragraph tags in simple table cells?
1316 def visit_paragraph(self, node):
1317 self.body.append(self.starttag(node, 'p', ''))
1319 def depart_paragraph(self, node):
1320 self.body.append('</p>')
1321 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1322 (len(node.parent) == 1)):
1323 self.body.append('\n')
1325 def visit_problematic(self, node):
1326 if node.hasattr('refid'):
1327 self.body.append('<a href="#%s">' % node['refid'])
1328 self.context.append('</a>')
1329 else:
1330 self.context.append('')
1331 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1333 def depart_problematic(self, node):
1334 self.body.append('</span>')
1335 self.body.append(self.context.pop())
1337 def visit_raw(self, node):
1338 if 'html' in node.get('format', '').split():
1339 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1340 if node['classes']:
1341 self.body.append(self.starttag(node, t, suffix=''))
1342 self.body.append(node.astext())
1343 if node['classes']:
1344 self.body.append('</%s>' % t)
1345 # Keep non-HTML raw text out of output:
1346 raise nodes.SkipNode
1348 def visit_reference(self, node):
1349 atts = {'class': 'reference'}
1350 if 'refuri' in node:
1351 atts['href'] = node['refuri']
1352 if ( self.settings.cloak_email_addresses
1353 and atts['href'].startswith('mailto:')):
1354 atts['href'] = self.cloak_mailto(atts['href'])
1355 self.in_mailto = True
1356 atts['class'] += ' external'
1357 else:
1358 assert 'refid' in node, \
1359 'References must have "refuri" or "refid" attribute.'
1360 atts['href'] = '#' + node['refid']
1361 atts['class'] += ' internal'
1362 if not isinstance(node.parent, nodes.TextElement):
1363 assert len(node) == 1 and isinstance(node[0], nodes.image)
1364 atts['class'] += ' image-reference'
1365 self.body.append(self.starttag(node, 'a', '', **atts))
1367 def depart_reference(self, node):
1368 self.body.append('</a>')
1369 if not isinstance(node.parent, nodes.TextElement):
1370 self.body.append('\n')
1371 self.in_mailto = False
1373 def visit_revision(self, node):
1374 self.visit_docinfo_item(node, 'revision', meta=False)
1376 def depart_revision(self, node):
1377 self.depart_docinfo_item()
1379 def visit_row(self, node):
1380 self.body.append(self.starttag(node, 'tr', ''))
1381 node.column = 0
1383 def depart_row(self, node):
1384 self.body.append('</tr>\n')
1386 def visit_rubric(self, node):
1387 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1389 def depart_rubric(self, node):
1390 self.body.append('</p>\n')
1392 # TODO: use the new HTML 5 element <section>?
1393 def visit_section(self, node):
1394 self.section_level += 1
1395 self.body.append(
1396 self.starttag(node, 'div', CLASS='section'))
1398 def depart_section(self, node):
1399 self.section_level -= 1
1400 self.body.append('</div>\n')
1402 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1403 def visit_sidebar(self, node):
1404 self.body.append(
1405 self.starttag(node, 'div', CLASS='sidebar'))
1406 self.set_first_last(node)
1407 self.in_sidebar = True
1409 def depart_sidebar(self, node):
1410 self.body.append('</div>\n')
1411 self.in_sidebar = False
1413 def visit_status(self, node):
1414 self.visit_docinfo_item(node, 'status', meta=False)
1416 def depart_status(self, node):
1417 self.depart_docinfo_item()
1419 def visit_strong(self, node):
1420 self.body.append(self.starttag(node, 'strong', ''))
1422 def depart_strong(self, node):
1423 self.body.append('</strong>')
1425 def visit_subscript(self, node):
1426 self.body.append(self.starttag(node, 'sub', ''))
1428 def depart_subscript(self, node):
1429 self.body.append('</sub>')
1431 def visit_substitution_definition(self, node):
1432 """Internal only."""
1433 raise nodes.SkipNode
1435 def visit_substitution_reference(self, node):
1436 self.unimplemented_visit(node)
1438 # h1–h6 elements must not be used to markup subheadings, subtitles,
1439 # alternative titles and taglines unless intended to be the heading for a
1440 # new section or subsection.
1441 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1443 def visit_subtitle(self, node):
1444 if isinstance(node.parent, nodes.sidebar):
1445 classes = 'sidebar-subtitle'
1446 elif isinstance(node.parent, nodes.document):
1447 classes = 'subtitle'
1448 self.in_document_title = len(self.body)
1449 elif isinstance(node.parent, nodes.section):
1450 classes = 'section-subtitle'
1451 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1453 def depart_subtitle(self, node):
1454 self.body.append('</p>\n')
1455 if self.in_document_title:
1456 self.subtitle = self.body[self.in_document_title:-1]
1457 self.in_document_title = 0
1458 self.body_pre_docinfo.extend(self.body)
1459 self.html_subtitle.extend(self.body)
1460 del self.body[:]
1462 def visit_superscript(self, node):
1463 self.body.append(self.starttag(node, 'sup', ''))
1465 def depart_superscript(self, node):
1466 self.body.append('</sup>')
1468 def visit_system_message(self, node):
1469 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1470 self.body.append('<p class="system-message-title">')
1471 backref_text = ''
1472 if len(node['backrefs']):
1473 backrefs = node['backrefs']
1474 if len(backrefs) == 1:
1475 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1476 % backrefs[0])
1477 else:
1478 i = 1
1479 backlinks = []
1480 for backref in backrefs:
1481 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1482 i += 1
1483 backref_text = ('; <em>backlinks: %s</em>'
1484 % ', '.join(backlinks))
1485 if node.hasattr('line'):
1486 line = ', line %s' % node['line']
1487 else:
1488 line = ''
1489 self.body.append('System Message: %s/%s '
1490 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1491 % (node['type'], node['level'],
1492 self.encode(node['source']), line, backref_text))
1494 def depart_system_message(self, node):
1495 self.body.append('</div>\n')
1497 # tables
1498 # ------
1499 # no hard-coded border setting in the table head::
1501 def visit_table(self, node):
1502 classes = [cls.strip(u' \t\n')
1503 for cls in self.settings.table_style.split(',')]
1504 tag = self.starttag(node, 'table', CLASS=' '.join(classes))
1505 self.body.append(tag)
1507 def depart_table(self, node):
1508 self.body.append('</table>\n')
1510 def visit_target(self, node):
1511 if not ('refuri' in node or 'refid' in node
1512 or 'refname' in node):
1513 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1514 self.context.append('</span>')
1515 else:
1516 self.context.append('')
1518 def depart_target(self, node):
1519 self.body.append(self.context.pop())
1521 # no hard-coded vertical alignment in table body::
1523 def visit_tbody(self, node):
1524 self.write_colspecs()
1525 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1526 self.body.append(self.starttag(node, 'tbody'))
1528 def depart_tbody(self, node):
1529 self.body.append('</tbody>\n')
1531 def visit_term(self, node):
1532 self.body.append(self.starttag(node, 'dt', ''))
1534 def depart_term(self, node):
1536 Leave the end tag to `self.visit_definition()`, in case there's a
1537 classifier.
1539 pass
1541 def visit_tgroup(self, node):
1542 # Mozilla needs <colgroup>:
1543 self.body.append(self.starttag(node, 'colgroup'))
1544 # Appended by thead or tbody:
1545 self.context.append('</colgroup>\n')
1546 node.stubs = []
1548 def depart_tgroup(self, node):
1549 pass
1551 def visit_thead(self, node):
1552 self.write_colspecs()
1553 self.body.append(self.context.pop()) # '</colgroup>\n'
1554 # There may or may not be a <thead>; this is for <tbody> to use:
1555 self.context.append('')
1556 self.body.append(self.starttag(node, 'thead'))
1558 def depart_thead(self, node):
1559 self.body.append('</thead>\n')
1561 def visit_title(self, node):
1562 """Only 6 section levels are supported by HTML."""
1563 check_id = 0 # TODO: is this a bool (False) or a counter?
1564 close_tag = '</p>\n'
1565 if isinstance(node.parent, nodes.topic):
1566 self.body.append(
1567 self.starttag(node, 'p', '', CLASS='topic-title first'))
1568 elif isinstance(node.parent, nodes.sidebar):
1569 self.body.append(
1570 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1571 elif isinstance(node.parent, nodes.Admonition):
1572 self.body.append(
1573 self.starttag(node, 'p', '', CLASS='admonition-title'))
1574 elif isinstance(node.parent, nodes.table):
1575 self.body.append(
1576 self.starttag(node, 'caption', ''))
1577 close_tag = '</caption>\n'
1578 elif isinstance(node.parent, nodes.document):
1579 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1580 close_tag = '</h1>\n'
1581 self.in_document_title = len(self.body)
1582 else:
1583 assert isinstance(node.parent, nodes.section)
1584 h_level = self.section_level + self.initial_header_level - 1
1585 atts = {}
1586 if (len(node.parent) >= 2 and
1587 isinstance(node.parent[1], nodes.subtitle)):
1588 atts['CLASS'] = 'with-subtitle'
1589 self.body.append(
1590 self.starttag(node, 'h%s' % h_level, '', **atts))
1591 atts = {}
1592 if node.hasattr('refid'):
1593 atts['class'] = 'toc-backref'
1594 atts['href'] = '#' + node['refid']
1595 if atts:
1596 self.body.append(self.starttag({}, 'a', '', **atts))
1597 close_tag = '</a></h%s>\n' % (h_level)
1598 else:
1599 close_tag = '</h%s>\n' % (h_level)
1600 self.context.append(close_tag)
1602 def depart_title(self, node):
1603 self.body.append(self.context.pop())
1604 if self.in_document_title:
1605 self.title = self.body[self.in_document_title:-1]
1606 self.in_document_title = 0
1607 self.body_pre_docinfo.extend(self.body)
1608 self.html_title.extend(self.body)
1609 del self.body[:]
1611 def visit_title_reference(self, node):
1612 self.body.append(self.starttag(node, 'cite', ''))
1614 def depart_title_reference(self, node):
1615 self.body.append('</cite>')
1617 def visit_topic(self, node):
1618 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1619 self.topic_classes = node['classes']
1620 # TODO: replace with ::
1621 # self.in_contents = 'contents' in node['classes']
1623 def depart_topic(self, node):
1624 self.body.append('</div>\n')
1625 self.topic_classes = []
1626 # TODO self.in_contents = False
1628 def visit_transition(self, node):
1629 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1631 def depart_transition(self, node):
1632 pass
1634 def visit_version(self, node):
1635 self.visit_docinfo_item(node, 'version', meta=False)
1637 def depart_version(self, node):
1638 self.depart_docinfo_item()
1640 def unimplemented_visit(self, node):
1641 raise NotImplementedError('visiting unimplemented node type: %s'
1642 % node.__class__.__name__)
1645 class SimpleListChecker(nodes.GenericNodeVisitor):
1648 Raise `nodes.NodeFound` if non-simple list item is encountered.
1650 Here "simple" means a list item containing nothing other than a single
1651 paragraph, a simple list, or a paragraph followed by a simple list.
1653 This version also checks for simple field lists and docinfo.
1656 def default_visit(self, node):
1657 raise nodes.NodeFound
1659 def visit_list_item(self, node):
1660 # print "visiting list item", node.__class__
1661 children = [child for child in node.children
1662 if not isinstance(child, nodes.Invisible)]
1663 # print "has %s visible children" % len(children)
1664 if (children and isinstance(children[0], nodes.paragraph)
1665 and (isinstance(children[-1], nodes.bullet_list) or
1666 isinstance(children[-1], nodes.enumerated_list) or
1667 isinstance(children[-1], nodes.field_list))):
1668 children.pop()
1669 # print "%s children remain" % len(children)
1670 if len(children) <= 1:
1671 return
1672 else:
1673 # print "found", child.__class__, "in", node.__class__
1674 raise nodes.NodeFound
1676 def pass_node(self, node):
1677 pass
1679 def ignore_node(self, node):
1680 # ignore nodes that are never complex (can contain only inline nodes)
1681 raise nodes.SkipNode
1683 # Paragraphs and text
1684 visit_Text = ignore_node
1685 visit_paragraph = ignore_node
1687 # Lists
1688 visit_bullet_list = pass_node
1689 visit_enumerated_list = pass_node
1690 visit_docinfo = pass_node
1692 # Docinfo nodes:
1693 visit_author = ignore_node
1694 visit_authors = visit_list_item
1695 visit_address = visit_list_item
1696 visit_contact = pass_node
1697 visit_copyright = ignore_node
1698 visit_date = ignore_node
1699 visit_organization = ignore_node
1700 visit_status = ignore_node
1701 visit_version = visit_list_item
1703 # Definition list:
1704 visit_definition_list = pass_node
1705 visit_definition_list_item = pass_node
1706 visit_term = ignore_node
1707 visit_classifier = pass_node
1708 visit_definition = visit_list_item
1710 # Field list:
1711 visit_field_list = pass_node
1712 visit_field = pass_node
1713 # the field body corresponds to a list item
1714 visit_field_body = visit_list_item
1715 visit_field_name = ignore_node
1717 # Invisible nodes should be ignored.
1718 visit_comment = ignore_node
1719 visit_substitution_definition = ignore_node
1720 visit_target = ignore_node
1721 visit_pending = ignore_node