HTML writers: Outsourcing of common code to _html_base.py.
[docutils.git] / docutils / writers / _html_base.py
blobe00fc1778d751042804d08d837a25013ae812f4c
1 #!/usr/bin/env python
2 # -*- coding: utf8 -*-
3 # :Author: David Goodger, Günter Milde
4 # Based on the html4css1 writer by David Goodger.
5 # :Maintainer: docutils-develop@lists.sourceforge.net
6 # :Revision: $Revision$
7 # :Date: $Date: 2005-06-28$
8 # :Copyright: © 2016 David Goodger, Günter Milde
9 # :License: Released under the terms of the `2-Clause BSD license`_, in short:
11 # Copying and distribution of this file, with or without modification,
12 # are permitted in any medium without royalty provided the copyright
13 # notice and this notice are preserved.
14 # This file is offered as-is, without any warranty.
16 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
19 # _html_base.py: common definitions for Docutils HTML writers
20 # ============================================================
22 import sys
23 import os.path
24 import re
25 import urllib
26 try: # check for the Python Imaging Library
27 import PIL.Image
28 except ImportError:
29 try: # sometimes PIL modules are put in PYTHONPATH's root
30 import Image
31 class PIL(object): pass # dummy wrapper
32 PIL.Image = Image
33 except ImportError:
34 PIL = None
35 import docutils
36 from docutils import nodes, utils, writers, languages, io
37 from docutils.utils.error_reporting import SafeString
38 from docutils.transforms import writer_aux
39 from docutils.utils.math import (unichar2tex, pick_math_environment,
40 math2html, latex2mathml, tex2mathml_extern)
43 class Writer(writers.Writer):
45 supported = ('html', 'xhtml') # update in subclass
46 """Formats this writer supports."""
48 # default_stylesheets = [] # set in subclass!
49 # default_stylesheet_dirs = ['.'] # set in subclass!
50 default_template = 'template.txt'
51 # default_template_path = ... # set in subclass!
52 # settings_spec = ... # set in subclass!
54 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
56 # config_section = ... # set in subclass!
57 config_section_dependencies = ('writers',)
59 visitor_attributes = (
60 'head_prefix', 'head', 'stylesheet', 'body_prefix',
61 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
62 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
63 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
64 'html_body')
66 def get_transforms(self):
67 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
69 def translate(self):
70 self.visitor = visitor = self.translator_class(self.document)
71 self.document.walkabout(visitor)
72 for attr in self.visitor_attributes:
73 setattr(self, attr, getattr(visitor, attr))
74 self.output = self.apply_template()
76 def apply_template(self):
77 template_file = open(self.document.settings.template, 'rb')
78 template = unicode(template_file.read(), 'utf-8')
79 template_file.close()
80 subs = self.interpolation_dict()
81 return template % subs
83 def interpolation_dict(self):
84 subs = {}
85 settings = self.document.settings
86 for attr in self.visitor_attributes:
87 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
88 subs['encoding'] = settings.output_encoding
89 subs['version'] = docutils.__version__
90 return subs
92 def assemble_parts(self):
93 writers.Writer.assemble_parts(self)
94 for part in self.visitor_attributes:
95 self.parts[part] = ''.join(getattr(self, part))
99 class HTMLTranslator(nodes.NodeVisitor):
101 """Generic Docutils to HTML translator.
103 See the html4css1 and html5_polyglott for writers for full featured HTML
104 writers. """
106 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
107 doctype = '<!DOCTYPE html>\n'
108 doctype_mathml = doctype
110 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
111 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
112 content_type = ('<meta charset="%s"/>\n')
113 generator = ('<meta name="generator" content="Docutils %s: '
114 'http://docutils.sourceforge.net/" />\n')
116 # Template for the MathJax script in the header:
117 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
118 # The latest version of MathJax from the distributed server:
119 # avaliable to the public under the `MathJax CDN Terms of Service`__
120 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
121 # may be overwritten by custom URL appended to "mathjax"
122 mathjax_url = ('https://cdn.mathjax.org/mathjax/latest/MathJax.js?'
123 'config=TeX-AMS_CHTML')
125 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
126 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
127 words_and_spaces = re.compile(r'\S+| +|\n')
128 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
129 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
131 special_characters = {ord('&'): u'&amp;',
132 ord('<'): u'&lt;',
133 ord('"'): u'&quot;',
134 ord('>'): u'&gt;',
135 ord('@'): u'&#64;', # may thwart address harvesters
137 """Character references for characters with a special meaning in HTML."""
139 def __init__(self, document):
140 nodes.NodeVisitor.__init__(self, document)
141 self.settings = settings = document.settings
142 lcode = settings.language_code
143 self.language = languages.get_language(lcode, document.reporter)
144 self.meta = [self.generator % docutils.__version__]
145 self.head_prefix = []
146 self.html_prolog = []
147 if settings.xml_declaration:
148 self.head_prefix.append(self.xml_declaration
149 % settings.output_encoding)
150 # self.content_type = ""
151 # encoding not interpolated:
152 self.html_prolog.append(self.xml_declaration)
153 self.head = self.meta[:]
154 self.stylesheet = [self.stylesheet_call(path)
155 for path in utils.get_stylesheet_list(settings)]
156 self.body_prefix = ['</head>\n<body>\n']
157 # document title, subtitle display
158 self.body_pre_docinfo = []
159 # author, date, etc.
160 self.docinfo = []
161 self.body = []
162 self.fragment = []
163 self.body_suffix = ['</body>\n</html>\n']
164 self.section_level = 0
165 self.initial_header_level = int(settings.initial_header_level)
167 self.math_output = settings.math_output.split()
168 self.math_output_options = self.math_output[1:]
169 self.math_output = self.math_output[0].lower()
171 # A heterogenous stack used in conjunction with the tree traversal.
172 # Make sure that the pops correspond to the pushes:
173 self.context = []
175 self.topic_classes = [] # TODO: replace with self_in_contents
176 self.colspecs = []
177 self.compact_p = True
178 self.compact_simple = False
179 self.compact_field_list = False
180 self.in_docinfo = False
181 self.in_sidebar = False
182 self.in_footnote_list = False
183 self.title = []
184 self.subtitle = []
185 self.header = []
186 self.footer = []
187 self.html_head = [self.content_type] # charset not interpolated
188 self.html_title = []
189 self.html_subtitle = []
190 self.html_body = []
191 self.in_document_title = 0 # len(self.body) or 0
192 self.in_mailto = False
193 self.author_in_authors = False # for html4css1
194 self.math_header = []
196 def astext(self):
197 return ''.join(self.head_prefix + self.head
198 + self.stylesheet + self.body_prefix
199 + self.body_pre_docinfo + self.docinfo
200 + self.body + self.body_suffix)
202 def encode(self, text):
203 """Encode special characters in `text` & return."""
204 # Use only named entities known in both XML and HTML
205 # other characters are automatically encoded "by number" if required.
206 # @@@ A codec to do these and all other HTML entities would be nice.
207 text = unicode(text)
208 return text.translate(self.special_characters)
210 def cloak_mailto(self, uri):
211 """Try to hide a mailto: URL from harvesters."""
212 # Encode "@" using a URL octet reference (see RFC 1738).
213 # Further cloaking with HTML entities will be done in the
214 # `attval` function.
215 return uri.replace('@', '%40')
217 def cloak_email(self, addr):
218 """Try to hide the link text of a email link from harversters."""
219 # Surround at-signs and periods with <span> tags. ("@" has
220 # already been encoded to "&#64;" by the `encode` method.)
221 addr = addr.replace('&#64;', '<span>&#64;</span>')
222 addr = addr.replace('.', '<span>&#46;</span>')
223 return addr
225 def attval(self, text,
226 whitespace=re.compile('[\n\r\t\v\f]')):
227 """Cleanse, HTML encode, and return attribute value text."""
228 encoded = self.encode(whitespace.sub(' ', text))
229 if self.in_mailto and self.settings.cloak_email_addresses:
230 # Cloak at-signs ("%40") and periods with HTML entities.
231 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
232 encoded = encoded.replace('.', '&#46;')
233 return encoded
235 def stylesheet_call(self, path):
236 """Return code to reference or embed stylesheet file `path`"""
237 if self.settings.embed_stylesheet:
238 try:
239 content = io.FileInput(source_path=path,
240 encoding='utf-8').read()
241 self.settings.record_dependencies.add(path)
242 except IOError, err:
243 msg = u"Cannot embed stylesheet '%s': %s." % (
244 path, SafeString(err.strerror))
245 self.document.reporter.error(msg)
246 return '<--- %s --->\n' % msg
247 return self.embedded_stylesheet % content
248 # else link to style file:
249 if self.settings.stylesheet_path:
250 # adapt path relative to output (cf. config.html#stylesheet-path)
251 path = utils.relative_path(self.settings._destination, path)
252 return self.stylesheet_link % self.encode(path)
254 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
256 Construct and return a start tag given a node (id & class attributes
257 are extracted), tag name, and optional attributes.
259 tagname = tagname.lower()
260 prefix = []
261 atts = {}
262 ids = []
263 for (name, value) in attributes.items():
264 atts[name.lower()] = value
265 classes = []
266 languages = []
267 # unify class arguments and move language specification
268 for cls in node.get('classes', []) + atts.pop('class', '').split() :
269 if cls.startswith('language-'):
270 languages.append(cls[9:])
271 elif cls.strip() and cls not in classes:
272 classes.append(cls)
273 if languages:
274 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
275 atts[self.lang_attribute] = languages[0]
276 if classes:
277 atts['class'] = ' '.join(classes)
278 assert 'id' not in atts
279 ids.extend(node.get('ids', []))
280 if 'ids' in atts:
281 ids.extend(atts['ids'])
282 del atts['ids']
283 if ids:
284 atts['id'] = ids[0]
285 for id in ids[1:]:
286 # Add empty "span" elements for additional IDs. Note
287 # that we cannot use empty "a" elements because there
288 # may be targets inside of references, but nested "a"
289 # elements aren't allowed in XHTML (even if they do
290 # not all have a "href" attribute).
291 if empty or isinstance(node,
292 (nodes.bullet_list, nodes.enumerated_list,
293 nodes.definition_list, nodes.field_list,
294 nodes.option_list, nodes.docinfo)):
295 # Insert target right in front of element.
296 prefix.append('<span id="%s"></span>' % id)
297 else:
298 # Non-empty tag. Place the auxiliary <span> tag
299 # *inside* the element, as the first child.
300 suffix += '<span id="%s"></span>' % id
301 attlist = atts.items()
302 attlist.sort()
303 parts = [tagname]
304 for name, value in attlist:
305 # value=None was used for boolean attributes without
306 # value, but this isn't supported by XHTML.
307 assert value is not None
308 if isinstance(value, list):
309 values = [unicode(v) for v in value]
310 parts.append('%s="%s"' % (name.lower(),
311 self.attval(' '.join(values))))
312 else:
313 parts.append('%s="%s"' % (name.lower(),
314 self.attval(unicode(value))))
315 if empty:
316 infix = ' /'
317 else:
318 infix = ''
319 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
321 def emptytag(self, node, tagname, suffix='\n', **attributes):
322 """Construct and return an XML-compatible empty tag."""
323 return self.starttag(node, tagname, suffix, empty=True, **attributes)
325 def set_class_on_child(self, node, class_, index=0):
327 Set class `class_` on the visible child no. index of `node`.
328 Do nothing if node has fewer children than `index`.
330 children = [n for n in node if not isinstance(n, nodes.Invisible)]
331 try:
332 child = children[index]
333 except IndexError:
334 return
335 child['classes'].append(class_)
337 def visit_Text(self, node):
338 text = node.astext()
339 encoded = self.encode(text)
340 if self.in_mailto and self.settings.cloak_email_addresses:
341 encoded = self.cloak_email(encoded)
342 self.body.append(encoded)
344 def depart_Text(self, node):
345 pass
347 def visit_abbreviation(self, node):
348 # @@@ implementation incomplete ("title" attribute)
349 self.body.append(self.starttag(node, 'abbr', ''))
351 def depart_abbreviation(self, node):
352 self.body.append('</abbr>')
354 def visit_acronym(self, node):
355 # @@@ implementation incomplete ("title" attribute)
356 self.body.append(self.starttag(node, 'acronym', ''))
358 def depart_acronym(self, node):
359 self.body.append('</acronym>')
361 def visit_address(self, node):
362 self.visit_docinfo_item(node, 'address', meta=False)
363 self.body.append(self.starttag(node, 'pre',
364 suffix= '', CLASS='address'))
366 def depart_address(self, node):
367 self.body.append('\n</pre>\n')
368 self.depart_docinfo_item()
370 def visit_admonition(self, node):
371 node['classes'].insert(0, 'admonition')
372 self.body.append(self.starttag(node, 'div'))
374 def depart_admonition(self, node=None):
375 self.body.append('</div>\n')
377 attribution_formats = {'dash': (u'\u2014', ''),
378 'parentheses': ('(', ')'),
379 'parens': ('(', ')'),
380 'none': ('', '')}
382 def visit_attribution(self, node):
383 prefix, suffix = self.attribution_formats[self.settings.attribution]
384 self.context.append(suffix)
385 self.body.append(
386 self.starttag(node, 'p', prefix, CLASS='attribution'))
388 def depart_attribution(self, node):
389 self.body.append(self.context.pop() + '</p>\n')
391 def visit_author(self, node):
392 if not(isinstance(node.parent, nodes.authors)):
393 self.visit_docinfo_item(node, 'author')
394 self.body.append('<p>')
396 def depart_author(self, node):
397 self.body.append('</p>')
398 if isinstance(node.parent, nodes.authors):
399 self.body.append('\n')
400 else:
401 self.depart_docinfo_item()
403 def visit_authors(self, node):
404 self.visit_docinfo_item(node, 'authors')
406 def depart_authors(self, node):
407 self.depart_docinfo_item()
409 def visit_block_quote(self, node):
410 self.body.append(self.starttag(node, 'blockquote'))
412 def depart_block_quote(self, node):
413 self.body.append('</blockquote>\n')
415 def check_simple_list(self, node):
416 """Check for a simple list that can be rendered compactly."""
417 visitor = SimpleListChecker(self.document)
418 try:
419 node.walk(visitor)
420 except nodes.NodeFound:
421 return False
422 else:
423 return True
425 # Compact lists
426 # ------------
427 # Include definition lists and field lists (in addition to ordered
428 # and unordered lists) in the test if a list is "simple" (cf. the
429 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
430 # the end of this file).
432 def is_compactable(self, node):
433 # print "is_compactable %s ?" % node.__class__,
434 # explicite class arguments have precedence
435 if 'compact' in node['classes']:
436 return True
437 if 'open' in node['classes']:
438 return False
439 # check config setting:
440 if (isinstance(node, (nodes.field_list, nodes.definition_list))
441 and not self.settings.compact_field_lists):
442 # print "`compact-field-lists` is False"
443 return False
444 if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
445 and not self.settings.compact_lists):
446 # print "`compact-lists` is False"
447 return False
448 # more special cases:
449 if (self.topic_classes == ['contents']): # TODO: self.in_contents
450 return True
451 # check the list items:
452 return self.check_simple_list(node)
454 def visit_bullet_list(self, node):
455 atts = {}
456 old_compact_simple = self.compact_simple
457 self.context.append((self.compact_simple, self.compact_p))
458 self.compact_p = None
459 self.compact_simple = self.is_compactable(node)
460 if self.compact_simple and not old_compact_simple:
461 atts['class'] = 'simple'
462 self.body.append(self.starttag(node, 'ul', **atts))
464 def depart_bullet_list(self, node):
465 self.compact_simple, self.compact_p = self.context.pop()
466 self.body.append('</ul>\n')
468 def visit_caption(self, node):
469 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
471 def depart_caption(self, node):
472 self.body.append('</p>\n')
474 # citations
475 # ---------
476 # Use definition list instead of table for bibliographic references.
477 # Join adjacent citation entries.
479 def visit_citation(self, node):
480 if not self.in_footnote_list:
481 self.body.append('<dl class="citation">\n')
482 self.in_footnote_list = True
484 def depart_citation(self, node):
485 self.body.append('</dd>\n')
486 if not isinstance(node.next_node(descend=False, siblings=True),
487 nodes.citation):
488 self.body.append('</dl>\n')
489 self.in_footnote_list = False
491 def visit_citation_reference(self, node):
492 href = '#'
493 if 'refid' in node:
494 href += node['refid']
495 elif 'refname' in node:
496 href += self.document.nameids[node['refname']]
497 # else: # TODO system message (or already in the transform)?
498 # 'Citation reference missing.'
499 self.body.append(self.starttag(
500 node, 'a', '[', CLASS='citation-reference', href=href))
502 def depart_citation_reference(self, node):
503 self.body.append(']</a>')
505 # classifier
506 # ----------
507 # don't insert classifier-delimiter here (done by CSS)
509 def visit_classifier(self, node):
510 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
512 def depart_classifier(self, node):
513 self.body.append('</span>')
515 def visit_colspec(self, node):
516 self.colspecs.append(node)
517 # "stubs" list is an attribute of the tgroup element:
518 node.parent.stubs.append(node.attributes.get('stub'))
520 def depart_colspec(self, node):
521 # write out <colgroup> when all colspecs are processed
522 if isinstance(node.next_node(descend=False, siblings=True),
523 nodes.colspec):
524 return
525 if 'colwidths-auto' in node.parent.parent['classes'] or (
526 'colwidths-auto' in self.settings.table_style and
527 ('colwidths-given' not in node.parent.parent['classes'])):
528 return
529 total_width = sum(node['colwidth'] for node in self.colspecs)
530 self.body.append(self.starttag(node, 'colgroup'))
531 for node in self.colspecs:
532 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
533 self.body.append(self.emptytag(node, 'col',
534 style='width: %i%%' % colwidth))
535 self.body.append('</colgroup>\n')
537 def visit_comment(self, node,
538 sub=re.compile('-(?=-)').sub):
539 """Escape double-dashes in comment text."""
540 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
541 # Content already processed:
542 raise nodes.SkipNode
544 def visit_compound(self, node):
545 self.body.append(self.starttag(node, 'div', CLASS='compound'))
546 if len(node) > 1:
547 node[0]['classes'].append('compound-first')
548 node[-1]['classes'].append('compound-last')
549 for child in node[1:-1]:
550 child['classes'].append('compound-middle')
552 def depart_compound(self, node):
553 self.body.append('</div>\n')
555 def visit_container(self, node):
556 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
558 def depart_container(self, node):
559 self.body.append('</div>\n')
561 def visit_contact(self, node):
562 self.visit_docinfo_item(node, 'contact', meta=False)
564 def depart_contact(self, node):
565 self.depart_docinfo_item()
567 def visit_copyright(self, node):
568 self.visit_docinfo_item(node, 'copyright')
570 def depart_copyright(self, node):
571 self.depart_docinfo_item()
573 def visit_date(self, node):
574 self.visit_docinfo_item(node, 'date')
576 def depart_date(self, node):
577 self.depart_docinfo_item()
579 def visit_decoration(self, node):
580 pass
582 def depart_decoration(self, node):
583 pass
585 def visit_definition(self, node):
586 self.body.append('</dt>\n')
587 self.body.append(self.starttag(node, 'dd', ''))
589 def depart_definition(self, node):
590 self.body.append('</dd>\n')
592 def visit_definition_list(self, node):
593 classes = node.setdefault('classes', [])
594 if self.is_compactable(node):
595 classes.append('simple')
596 self.body.append(self.starttag(node, 'dl'))
598 def depart_definition_list(self, node):
599 self.body.append('</dl>\n')
601 def visit_definition_list_item(self, node):
602 # pass class arguments, ids and names to definition term:
603 node.children[0]['classes'] = (
604 node.get('classes', []) + node.children[0].get('classes', []))
605 node.children[0]['ids'] = (
606 node.get('ids', []) + node.children[0].get('ids', []))
607 node.children[0]['names'] = (
608 node.get('names', []) + node.children[0].get('names', []))
610 def depart_definition_list_item(self, node):
611 pass
613 def visit_description(self, node):
614 self.body.append(self.starttag(node, 'dd', ''))
616 def depart_description(self, node):
617 self.body.append('</dd>\n')
619 def visit_docinfo(self, node):
620 classes = 'docinfo'
621 if (self.is_compactable(node)):
622 classes += ' simple'
623 self.body.append(self.starttag(node, 'dl', CLASS=classes))
625 def depart_docinfo(self, node):
626 self.body.append('</dl>\n')
628 def visit_docinfo_item(self, node, name, meta=True):
629 if meta:
630 meta_tag = '<meta name="%s" content="%s" />\n' \
631 % (name, self.attval(node.astext()))
632 self.add_meta(meta_tag)
633 self.body.append('<dt class="%s">%s</dt>\n'
634 % (name, self.language.labels[name]))
635 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
637 def depart_docinfo_item(self):
638 self.body.append('</dd>\n')
640 def visit_doctest_block(self, node):
641 self.body.append(self.starttag(node, 'pre', suffix='',
642 CLASS='code python doctest'))
644 def depart_doctest_block(self, node):
645 self.body.append('\n</pre>\n')
647 def visit_document(self, node):
648 self.head.append('<title>%s</title>\n'
649 % self.encode(node.get('title', '')))
651 def depart_document(self, node):
652 self.head_prefix.extend([self.doctype,
653 self.head_prefix_template %
654 {'lang': self.settings.language_code}])
655 self.html_prolog.append(self.doctype)
656 self.meta.insert(0, self.content_type % self.settings.output_encoding)
657 self.head.insert(0, self.content_type % self.settings.output_encoding)
658 if self.math_header:
659 if self.math_output == 'mathjax':
660 self.head.extend(self.math_header)
661 else:
662 self.stylesheet.extend(self.math_header)
663 # skip content-type meta tag with interpolated charset value:
664 self.html_head.extend(self.head[1:])
665 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
666 self.body_suffix.insert(0, '</div>\n')
667 self.fragment.extend(self.body) # self.fragment is the "naked" body
668 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
669 + self.docinfo + self.body
670 + self.body_suffix[:-1])
671 assert not self.context, 'len(context) = %s' % len(self.context)
673 def visit_emphasis(self, node):
674 self.body.append(self.starttag(node, 'em', ''))
676 def depart_emphasis(self, node):
677 self.body.append('</em>')
679 def visit_entry(self, node):
680 atts = {'class': []}
681 if isinstance(node.parent.parent, nodes.thead):
682 atts['class'].append('head')
683 if node.parent.parent.parent.stubs[node.parent.column]:
684 # "stubs" list is an attribute of the tgroup element
685 atts['class'].append('stub')
686 if atts['class']:
687 tagname = 'th'
688 atts['class'] = ' '.join(atts['class'])
689 else:
690 tagname = 'td'
691 del atts['class']
692 node.parent.column += 1
693 if 'morerows' in node:
694 atts['rowspan'] = node['morerows'] + 1
695 if 'morecols' in node:
696 atts['colspan'] = node['morecols'] + 1
697 node.parent.column += node['morecols']
698 self.body.append(self.starttag(node, tagname, '', **atts))
699 self.context.append('</%s>\n' % tagname.lower())
700 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
701 # if len(node) == 0: # empty cell
702 # self.body.append('&#0160;') # no-break space
704 def depart_entry(self, node):
705 self.body.append(self.context.pop())
707 def visit_enumerated_list(self, node):
708 atts = {}
709 if 'start' in node:
710 atts['start'] = node['start']
711 if 'enumtype' in node:
712 atts['class'] = node['enumtype']
713 if self.is_compactable(node):
714 atts['class'] = (atts.get('class', '') + ' simple').strip()
715 self.body.append(self.starttag(node, 'ol', **atts))
717 def depart_enumerated_list(self, node):
718 self.body.append('</ol>\n')
720 def visit_field_list(self, node):
721 # Keep simple paragraphs in the field_body to enable CSS
722 # rule to start body on new line if the label is too long
723 classes = 'field-list'
724 if (self.is_compactable(node)):
725 classes += ' simple'
726 self.body.append(self.starttag(node, 'dl', CLASS=classes))
728 def depart_field_list(self, node):
729 self.body.append('</dl>\n')
731 def visit_field(self, node):
732 pass
734 def depart_field(self, node):
735 pass
737 # as field is ignored, pass class arguments to field-name and field-body:
739 def visit_field_name(self, node):
740 self.body.append(self.starttag(node, 'dt', '',
741 CLASS=''.join(node.parent['classes'])))
743 def depart_field_name(self, node):
744 self.body.append('</dt>\n')
746 def visit_field_body(self, node):
747 self.body.append(self.starttag(node, 'dd', '',
748 CLASS=''.join(node.parent['classes'])))
749 # prevent misalignment of following content if the field is empty:
750 if not node.children:
751 self.body.append('<p></p>')
753 def depart_field_body(self, node):
754 self.body.append('</dd>\n')
756 def visit_figure(self, node):
757 atts = {'class': 'figure'}
758 if node.get('width'):
759 atts['style'] = 'width: %s' % node['width']
760 if node.get('align'):
761 atts['class'] += " align-" + node['align']
762 self.body.append(self.starttag(node, 'div', **atts))
764 def depart_figure(self, node):
765 self.body.append('</div>\n')
767 # use HTML 5 <footer> element?
768 def visit_footer(self, node):
769 self.context.append(len(self.body))
771 def depart_footer(self, node):
772 start = self.context.pop()
773 footer = [self.starttag(node, 'div', CLASS='footer'),
774 '<hr class="footer" />\n']
775 footer.extend(self.body[start:])
776 footer.append('\n</div>\n')
777 self.footer.extend(footer)
778 self.body_suffix[:0] = footer
779 del self.body[start:]
781 # footnotes
782 # ---------
783 # use definition list instead of table for footnote text
785 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
786 def visit_footnote(self, node):
787 if not self.in_footnote_list:
788 classes = 'footnote ' + self.settings.footnote_references
789 self.body.append('<dl class="%s">\n'%classes)
790 self.in_footnote_list = True
792 def depart_footnote(self, node):
793 self.body.append('</dd>\n')
794 if not isinstance(node.next_node(descend=False, siblings=True),
795 nodes.footnote):
796 self.body.append('</dl>\n')
797 self.in_footnote_list = False
799 def visit_footnote_reference(self, node):
800 href = '#' + node['refid']
801 classes = 'footnote-reference ' + self.settings.footnote_references
802 self.body.append(self.starttag(node, 'a', '', #suffix,
803 CLASS=classes, href=href))
805 def depart_footnote_reference(self, node):
806 self.body.append('</a>')
808 # Docutils-generated text: put section numbers in a span for CSS styling:
809 def visit_generated(self, node):
810 if 'sectnum' in node['classes']:
811 # get section number (strip trailing no-break-spaces)
812 sectnum = node.astext().rstrip(u' ')
813 # print sectnum.encode('utf-8')
814 self.body.append('<span class="sectnum">%s</span> '
815 % self.encode(sectnum))
816 # Content already processed:
817 raise nodes.SkipNode
819 def depart_generated(self, node):
820 pass
822 def visit_header(self, node):
823 self.context.append(len(self.body))
825 def depart_header(self, node):
826 start = self.context.pop()
827 header = [self.starttag(node, 'div', CLASS='header')]
828 header.extend(self.body[start:])
829 header.append('\n<hr class="header"/>\n</div>\n')
830 self.body_prefix.extend(header)
831 self.header.extend(header)
832 del self.body[start:]
834 # Image types to place in an <object> element
835 object_image_types = {'.swf': 'application/x-shockwave-flash'}
837 def visit_image(self, node):
838 atts = {}
839 uri = node['uri']
840 ext = os.path.splitext(uri)[1].lower()
841 if ext in self.object_image_types:
842 atts['data'] = uri
843 atts['type'] = self.object_image_types[ext]
844 else:
845 atts['src'] = uri
846 atts['alt'] = node.get('alt', uri)
847 # image size
848 if 'width' in node:
849 atts['width'] = node['width']
850 if 'height' in node:
851 atts['height'] = node['height']
852 if 'scale' in node:
853 if (PIL and not ('width' in node and 'height' in node)
854 and self.settings.file_insertion_enabled):
855 imagepath = urllib.url2pathname(uri)
856 try:
857 img = PIL.Image.open(
858 imagepath.encode(sys.getfilesystemencoding()))
859 except (IOError, UnicodeEncodeError):
860 pass # TODO: warn?
861 else:
862 self.settings.record_dependencies.add(
863 imagepath.replace('\\', '/'))
864 if 'width' not in atts:
865 atts['width'] = '%dpx' % img.size[0]
866 if 'height' not in atts:
867 atts['height'] = '%dpx' % img.size[1]
868 del img
869 for att_name in 'width', 'height':
870 if att_name in atts:
871 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
872 assert match
873 atts[att_name] = '%s%s' % (
874 float(match.group(1)) * (float(node['scale']) / 100),
875 match.group(2))
876 style = []
877 for att_name in 'width', 'height':
878 if att_name in atts:
879 if re.match(r'^[0-9.]+$', atts[att_name]):
880 # Interpret unitless values as pixels.
881 atts[att_name] += 'px'
882 style.append('%s: %s;' % (att_name, atts[att_name]))
883 del atts[att_name]
884 if style:
885 atts['style'] = ' '.join(style)
886 if (isinstance(node.parent, nodes.TextElement) or
887 (isinstance(node.parent, nodes.reference) and
888 not isinstance(node.parent.parent, nodes.TextElement))):
889 # Inline context or surrounded by <a>...</a>.
890 suffix = ''
891 else:
892 suffix = '\n'
893 if 'align' in node:
894 atts['class'] = 'align-%s' % node['align']
895 if ext in self.object_image_types:
896 # do NOT use an empty tag: incorrect rendering in browsers
897 self.body.append(self.starttag(node, 'object', suffix, **atts) +
898 node.get('alt', uri) + '</object>' + suffix)
899 else:
900 self.body.append(self.emptytag(node, 'img', suffix, **atts))
902 def depart_image(self, node):
903 # self.body.append(self.context.pop())
904 pass
906 def visit_inline(self, node):
907 self.body.append(self.starttag(node, 'span', ''))
909 def depart_inline(self, node):
910 self.body.append('</span>')
912 # footnote and citation labels:
913 def visit_label(self, node):
914 if (isinstance(node.parent, nodes.footnote)):
915 classes = self.settings.footnote_references
916 else:
917 classes = 'brackets'
918 # pass parent node to get id into starttag:
919 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
920 self.body.append(self.starttag(node, 'span', '', CLASS=classes))
921 # footnote/citation backrefs:
922 if self.settings.footnote_backlinks:
923 backrefs = node.parent['backrefs']
924 if len(backrefs) == 1:
925 self.body.append('<a class="fn-backref" href="#%s">'
926 % backrefs[0])
928 def depart_label(self, node):
929 if self.settings.footnote_backlinks:
930 backrefs = node.parent['backrefs']
931 if len(backrefs) == 1:
932 self.body.append('</a>')
933 self.body.append('</span>')
934 if self.settings.footnote_backlinks and len(backrefs) > 1:
935 # Python 2.4 fails with enumerate(backrefs, 1)
936 backlinks = ['<a href="#%s">%s</a>' % (ref, i+1)
937 for (i, ref) in enumerate(backrefs)]
938 self.body.append('<span class="fn-backref">(%s)</span>'
939 % ','.join(backlinks))
940 self.body.append('</dt>\n<dd>')
942 def visit_legend(self, node):
943 self.body.append(self.starttag(node, 'div', CLASS='legend'))
945 def depart_legend(self, node):
946 self.body.append('</div>\n')
948 def visit_line(self, node):
949 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
950 if not len(node):
951 self.body.append('<br />')
953 def depart_line(self, node):
954 self.body.append('</div>\n')
956 def visit_line_block(self, node):
957 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
959 def depart_line_block(self, node):
960 self.body.append('</div>\n')
962 def visit_list_item(self, node):
963 self.body.append(self.starttag(node, 'li', ''))
965 def depart_list_item(self, node):
966 self.body.append('</li>\n')
968 # inline literal
969 def visit_literal(self, node):
970 # special case: "code" role
971 classes = node.get('classes', [])
972 if 'code' in classes:
973 # filter 'code' from class arguments
974 node['classes'] = [cls for cls in classes if cls != 'code']
975 self.body.append(self.starttag(node, 'code', ''))
976 return
977 self.body.append(
978 self.starttag(node, 'span', '', CLASS='docutils literal'))
979 text = node.astext()
980 # remove hard line breaks (except if in a parsed-literal block)
981 if not isinstance(node.parent, nodes.literal_block):
982 text = text.replace('\n', ' ')
983 # Protect text like ``--an-option`` and the regular expression
984 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
985 for token in self.words_and_spaces.findall(text):
986 if token.strip() and self.sollbruchstelle.search(token):
987 self.body.append('<span class="pre">%s</span>'
988 % self.encode(token))
989 else:
990 self.body.append(self.encode(token))
991 self.body.append('</span>')
992 # Content already processed:
993 raise nodes.SkipNode
995 def depart_literal(self, node):
996 # skipped unless literal element is from "code" role:
997 self.body.append('</code>')
999 def visit_literal_block(self, node):
1000 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1001 if 'code' in node.get('classes', []):
1002 self.body.append('<code>')
1004 def depart_literal_block(self, node):
1005 if 'code' in node.get('classes', []):
1006 self.body.append('</code>')
1007 self.body.append('</pre>\n')
1009 # Mathematics:
1010 # As there is no native HTML math support, we provide alternatives
1011 # for the math-output: LaTeX and MathJax simply wrap the content,
1012 # HTML and MathML also convert the math_code.
1013 # HTML container
1014 math_tags = {# math_output: (block, inline, class-arguments)
1015 'mathml': ('div', '', ''),
1016 'html': ('div', 'span', 'formula'),
1017 'mathjax': ('div', 'span', 'math'),
1018 'latex': ('pre', 'tt', 'math'),
1021 def visit_math(self, node, math_env=''):
1022 # If the method is called from visit_math_block(), math_env != ''.
1024 if self.math_output not in self.math_tags:
1025 self.document.reporter.error(
1026 'math-output format "%s" not supported '
1027 'falling back to "latex"'% self.math_output)
1028 self.math_output = 'latex'
1029 tag = self.math_tags[self.math_output][math_env == '']
1030 clsarg = self.math_tags[self.math_output][2]
1031 # LaTeX container
1032 wrappers = {# math_mode: (inline, block)
1033 'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1034 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1035 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1036 'latex': (None, None),
1038 wrapper = wrappers[self.math_output][math_env != '']
1039 if self.math_output == 'mathml' and (not self.math_output_options or
1040 self.math_output_options[0] == 'blahtexml'):
1041 wrapper = None
1042 # get and wrap content
1043 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1044 if wrapper:
1045 try: # wrapper with three "%s"
1046 math_code = wrapper % (math_env, math_code, math_env)
1047 except TypeError: # wrapper with one "%s"
1048 math_code = wrapper % math_code
1049 # settings and conversion
1050 if self.math_output in ('latex', 'mathjax'):
1051 math_code = self.encode(math_code)
1052 if self.math_output == 'mathjax' and not self.math_header:
1053 if self.math_output_options:
1054 self.mathjax_url = self.math_output_options[0]
1055 self.math_header = [self.mathjax_script % self.mathjax_url]
1056 elif self.math_output == 'html':
1057 if self.math_output_options and not self.math_header:
1058 self.math_header = [self.stylesheet_call(
1059 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1060 for s in self.math_output_options[0].split(',')]
1061 # TODO: fix display mode in matrices and fractions
1062 math2html.DocumentParameters.displaymode = (math_env != '')
1063 math_code = math2html.math2html(math_code)
1064 elif self.math_output == 'mathml':
1065 if 'XHTML 1' in self.doctype:
1066 self.doctype = self.doctype_mathml
1067 self.content_type = self.content_type_mathml
1068 converter = ' '.join(self.math_output_options).lower()
1069 try:
1070 if converter == 'latexml':
1071 math_code = tex2mathml_extern.latexml(math_code,
1072 self.document.reporter)
1073 elif converter == 'ttm':
1074 math_code = tex2mathml_extern.ttm(math_code,
1075 self.document.reporter)
1076 elif converter == 'blahtexml':
1077 math_code = tex2mathml_extern.blahtexml(math_code,
1078 inline=not(math_env),
1079 reporter=self.document.reporter)
1080 elif not converter:
1081 math_code = latex2mathml.tex2mathml(math_code,
1082 inline=not(math_env))
1083 else:
1084 self.document.reporter.error('option "%s" not supported '
1085 'with math-output "MathML"')
1086 except OSError:
1087 raise OSError('is "latexmlmath" in your PATH?')
1088 except SyntaxError, err:
1089 err_node = self.document.reporter.error(err, base_node=node)
1090 self.visit_system_message(err_node)
1091 self.body.append(self.starttag(node, 'p'))
1092 self.body.append(u','.join(err.args))
1093 self.body.append('</p>\n')
1094 self.body.append(self.starttag(node, 'pre',
1095 CLASS='literal-block'))
1096 self.body.append(self.encode(math_code))
1097 self.body.append('\n</pre>\n')
1098 self.depart_system_message(err_node)
1099 raise nodes.SkipNode
1100 # append to document body
1101 if tag:
1102 self.body.append(self.starttag(node, tag,
1103 suffix='\n'*bool(math_env),
1104 CLASS=clsarg))
1105 self.body.append(math_code)
1106 if math_env: # block mode (equation, display)
1107 self.body.append('\n')
1108 if tag:
1109 self.body.append('</%s>' % tag)
1110 if math_env:
1111 self.body.append('\n')
1112 # Content already processed:
1113 raise nodes.SkipNode
1115 def depart_math(self, node):
1116 pass # never reached
1118 def visit_math_block(self, node):
1119 # print node.astext().encode('utf8')
1120 math_env = pick_math_environment(node.astext())
1121 self.visit_math(node, math_env=math_env)
1123 def depart_math_block(self, node):
1124 pass # never reached
1126 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1127 # HTML5/polyglot recommends using both
1128 def visit_meta(self, node):
1129 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1130 self.add_meta(meta)
1132 def depart_meta(self, node):
1133 pass
1135 def add_meta(self, tag):
1136 self.meta.append(tag)
1137 self.head.append(tag)
1139 def visit_option(self, node):
1140 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1142 def depart_option(self, node):
1143 self.body.append('</span>')
1144 if isinstance(node.next_node(descend=False, siblings=True),
1145 nodes.option):
1146 self.body.append(', ')
1148 def visit_option_argument(self, node):
1149 self.body.append(node.get('delimiter', ' '))
1150 self.body.append(self.starttag(node, 'var', ''))
1152 def depart_option_argument(self, node):
1153 self.body.append('</var>')
1155 def visit_option_group(self, node):
1156 self.body.append(self.starttag(node, 'dt', ''))
1157 self.body.append('<kbd>')
1159 def depart_option_group(self, node):
1160 self.body.append('</kbd></dt>\n')
1162 def visit_option_list(self, node):
1163 self.body.append(
1164 self.starttag(node, 'dl', CLASS='option-list'))
1166 def depart_option_list(self, node):
1167 self.body.append('</dl>\n')
1169 def visit_option_list_item(self, node):
1170 pass
1172 def depart_option_list_item(self, node):
1173 pass
1175 def visit_option_string(self, node):
1176 pass
1178 def depart_option_string(self, node):
1179 pass
1181 def visit_organization(self, node):
1182 self.visit_docinfo_item(node, 'organization')
1184 def depart_organization(self, node):
1185 self.depart_docinfo_item()
1187 # Do not omit <p> tags
1188 # --------------------
1190 # The HTML4CSS1 writer does this to "produce
1191 # visually compact lists (less vertical whitespace)". This writer
1192 # relies on CSS rules for"visual compactness".
1194 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1195 # character data, so you cannot drop the <p> tags.
1196 # * Keeping simple paragraphs in the field_body enables a CSS
1197 # rule to start the field-body on a new line if the label is too long
1198 # * it makes the code simpler.
1200 # TODO: omit paragraph tags in simple table cells?
1202 def visit_paragraph(self, node):
1203 self.body.append(self.starttag(node, 'p', ''))
1205 def depart_paragraph(self, node):
1206 self.body.append('</p>')
1207 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1208 (len(node.parent) == 1)):
1209 self.body.append('\n')
1211 def visit_problematic(self, node):
1212 if node.hasattr('refid'):
1213 self.body.append('<a href="#%s">' % node['refid'])
1214 self.context.append('</a>')
1215 else:
1216 self.context.append('')
1217 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1219 def depart_problematic(self, node):
1220 self.body.append('</span>')
1221 self.body.append(self.context.pop())
1223 def visit_raw(self, node):
1224 if 'html' in node.get('format', '').split():
1225 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1226 if node['classes']:
1227 self.body.append(self.starttag(node, t, suffix=''))
1228 self.body.append(node.astext())
1229 if node['classes']:
1230 self.body.append('</%s>' % t)
1231 # Keep non-HTML raw text out of output:
1232 raise nodes.SkipNode
1234 def visit_reference(self, node):
1235 atts = {'class': 'reference'}
1236 if 'refuri' in node:
1237 atts['href'] = node['refuri']
1238 if ( self.settings.cloak_email_addresses
1239 and atts['href'].startswith('mailto:')):
1240 atts['href'] = self.cloak_mailto(atts['href'])
1241 self.in_mailto = True
1242 atts['class'] += ' external'
1243 else:
1244 assert 'refid' in node, \
1245 'References must have "refuri" or "refid" attribute.'
1246 atts['href'] = '#' + node['refid']
1247 atts['class'] += ' internal'
1248 if not isinstance(node.parent, nodes.TextElement):
1249 assert len(node) == 1 and isinstance(node[0], nodes.image)
1250 atts['class'] += ' image-reference'
1251 self.body.append(self.starttag(node, 'a', '', **atts))
1253 def depart_reference(self, node):
1254 self.body.append('</a>')
1255 if not isinstance(node.parent, nodes.TextElement):
1256 self.body.append('\n')
1257 self.in_mailto = False
1259 def visit_revision(self, node):
1260 self.visit_docinfo_item(node, 'revision', meta=False)
1262 def depart_revision(self, node):
1263 self.depart_docinfo_item()
1265 def visit_row(self, node):
1266 self.body.append(self.starttag(node, 'tr', ''))
1267 node.column = 0
1269 def depart_row(self, node):
1270 self.body.append('</tr>\n')
1272 def visit_rubric(self, node):
1273 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1275 def depart_rubric(self, node):
1276 self.body.append('</p>\n')
1278 # TODO: use the new HTML 5 element <section>?
1279 def visit_section(self, node):
1280 self.section_level += 1
1281 self.body.append(
1282 self.starttag(node, 'div', CLASS='section'))
1284 def depart_section(self, node):
1285 self.section_level -= 1
1286 self.body.append('</div>\n')
1288 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1289 def visit_sidebar(self, node):
1290 self.body.append(
1291 self.starttag(node, 'div', CLASS='sidebar'))
1292 self.in_sidebar = True
1294 def depart_sidebar(self, node):
1295 self.body.append('</div>\n')
1296 self.in_sidebar = False
1298 def visit_status(self, node):
1299 self.visit_docinfo_item(node, 'status', meta=False)
1301 def depart_status(self, node):
1302 self.depart_docinfo_item()
1304 def visit_strong(self, node):
1305 self.body.append(self.starttag(node, 'strong', ''))
1307 def depart_strong(self, node):
1308 self.body.append('</strong>')
1310 def visit_subscript(self, node):
1311 self.body.append(self.starttag(node, 'sub', ''))
1313 def depart_subscript(self, node):
1314 self.body.append('</sub>')
1316 def visit_substitution_definition(self, node):
1317 """Internal only."""
1318 raise nodes.SkipNode
1320 def visit_substitution_reference(self, node):
1321 self.unimplemented_visit(node)
1323 # h1–h6 elements must not be used to markup subheadings, subtitles,
1324 # alternative titles and taglines unless intended to be the heading for a
1325 # new section or subsection.
1326 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1327 def visit_subtitle(self, node):
1328 if isinstance(node.parent, nodes.sidebar):
1329 classes = 'sidebar-subtitle'
1330 elif isinstance(node.parent, nodes.document):
1331 classes = 'subtitle'
1332 self.in_document_title = len(self.body)
1333 elif isinstance(node.parent, nodes.section):
1334 classes = 'section-subtitle'
1335 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1337 def depart_subtitle(self, node):
1338 self.body.append('</p>\n')
1339 if self.in_document_title:
1340 self.subtitle = self.body[self.in_document_title:-1]
1341 self.in_document_title = 0
1342 self.body_pre_docinfo.extend(self.body)
1343 self.html_subtitle.extend(self.body)
1344 del self.body[:]
1346 def visit_superscript(self, node):
1347 self.body.append(self.starttag(node, 'sup', ''))
1349 def depart_superscript(self, node):
1350 self.body.append('</sup>')
1352 def visit_system_message(self, node):
1353 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1354 self.body.append('<p class="system-message-title">')
1355 backref_text = ''
1356 if len(node['backrefs']):
1357 backrefs = node['backrefs']
1358 if len(backrefs) == 1:
1359 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1360 % backrefs[0])
1361 else:
1362 i = 1
1363 backlinks = []
1364 for backref in backrefs:
1365 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1366 i += 1
1367 backref_text = ('; <em>backlinks: %s</em>'
1368 % ', '.join(backlinks))
1369 if node.hasattr('line'):
1370 line = ', line %s' % node['line']
1371 else:
1372 line = ''
1373 self.body.append('System Message: %s/%s '
1374 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1375 % (node['type'], node['level'],
1376 self.encode(node['source']), line, backref_text))
1378 def depart_system_message(self, node):
1379 self.body.append('</div>\n')
1381 # tables
1382 # ------
1383 # no hard-coded border setting in the table head::
1385 def visit_table(self, node):
1386 classes = [cls.strip(u' \t\n')
1387 for cls in self.settings.table_style.split(',')]
1388 if 'align' in node:
1389 classes.append('align-%s' % node['align'])
1390 tag = self.starttag(node, 'table', CLASS=' '.join(classes))
1391 self.body.append(tag)
1393 def depart_table(self, node):
1394 self.body.append('</table>\n')
1396 def visit_target(self, node):
1397 if not ('refuri' in node or 'refid' in node
1398 or 'refname' in node):
1399 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1400 self.context.append('</span>')
1401 else:
1402 self.context.append('')
1404 def depart_target(self, node):
1405 self.body.append(self.context.pop())
1407 # no hard-coded vertical alignment in table body
1408 def visit_tbody(self, node):
1409 self.body.append(self.starttag(node, 'tbody'))
1411 def depart_tbody(self, node):
1412 self.body.append('</tbody>\n')
1414 def visit_term(self, node):
1415 self.body.append(self.starttag(node, 'dt', ''))
1417 def depart_term(self, node):
1419 Leave the end tag to `self.visit_definition()`, in case there's a
1420 classifier.
1422 pass
1424 def visit_tgroup(self, node):
1425 self.colspecs = []
1426 node.stubs = []
1428 def depart_tgroup(self, node):
1429 pass
1431 def visit_thead(self, node):
1432 self.body.append(self.starttag(node, 'thead'))
1434 def depart_thead(self, node):
1435 self.body.append('</thead>\n')
1437 def visit_title(self, node):
1438 """Only 6 section levels are supported by HTML."""
1439 check_id = 0 # TODO: is this a bool (False) or a counter?
1440 close_tag = '</p>\n'
1441 if isinstance(node.parent, nodes.topic):
1442 self.body.append(
1443 self.starttag(node, 'p', '', CLASS='topic-title first'))
1444 elif isinstance(node.parent, nodes.sidebar):
1445 self.body.append(
1446 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1447 elif isinstance(node.parent, nodes.Admonition):
1448 self.body.append(
1449 self.starttag(node, 'p', '', CLASS='admonition-title'))
1450 elif isinstance(node.parent, nodes.table):
1451 self.body.append(
1452 self.starttag(node, 'caption', ''))
1453 close_tag = '</caption>\n'
1454 elif isinstance(node.parent, nodes.document):
1455 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1456 close_tag = '</h1>\n'
1457 self.in_document_title = len(self.body)
1458 else:
1459 assert isinstance(node.parent, nodes.section)
1460 h_level = self.section_level + self.initial_header_level - 1
1461 atts = {}
1462 if (len(node.parent) >= 2 and
1463 isinstance(node.parent[1], nodes.subtitle)):
1464 atts['CLASS'] = 'with-subtitle'
1465 self.body.append(
1466 self.starttag(node, 'h%s' % h_level, '', **atts))
1467 atts = {}
1468 if node.hasattr('refid'):
1469 atts['class'] = 'toc-backref'
1470 atts['href'] = '#' + node['refid']
1471 if atts:
1472 self.body.append(self.starttag({}, 'a', '', **atts))
1473 close_tag = '</a></h%s>\n' % (h_level)
1474 else:
1475 close_tag = '</h%s>\n' % (h_level)
1476 self.context.append(close_tag)
1478 def depart_title(self, node):
1479 self.body.append(self.context.pop())
1480 if self.in_document_title:
1481 self.title = self.body[self.in_document_title:-1]
1482 self.in_document_title = 0
1483 self.body_pre_docinfo.extend(self.body)
1484 self.html_title.extend(self.body)
1485 del self.body[:]
1487 def visit_title_reference(self, node):
1488 self.body.append(self.starttag(node, 'cite', ''))
1490 def depart_title_reference(self, node):
1491 self.body.append('</cite>')
1493 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1494 def visit_topic(self, node):
1495 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1496 self.topic_classes = node['classes']
1497 # TODO: replace with ::
1498 # self.in_contents = 'contents' in node['classes']
1500 def depart_topic(self, node):
1501 self.body.append('</div>\n')
1502 self.topic_classes = []
1503 # TODO self.in_contents = False
1505 def visit_transition(self, node):
1506 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1508 def depart_transition(self, node):
1509 pass
1511 def visit_version(self, node):
1512 self.visit_docinfo_item(node, 'version', meta=False)
1514 def depart_version(self, node):
1515 self.depart_docinfo_item()
1517 def unimplemented_visit(self, node):
1518 raise NotImplementedError('visiting unimplemented node type: %s'
1519 % node.__class__.__name__)
1522 class SimpleListChecker(nodes.GenericNodeVisitor):
1525 Raise `nodes.NodeFound` if non-simple list item is encountered.
1527 Here "simple" means a list item containing nothing other than a single
1528 paragraph, a simple list, or a paragraph followed by a simple list.
1530 This version also checks for simple field lists and docinfo.
1533 def default_visit(self, node):
1534 raise nodes.NodeFound
1536 def visit_list_item(self, node):
1537 # print "visiting list item", node.__class__
1538 children = [child for child in node.children
1539 if not isinstance(child, nodes.Invisible)]
1540 # print "has %s visible children" % len(children)
1541 if (children and isinstance(children[0], nodes.paragraph)
1542 and (isinstance(children[-1], nodes.bullet_list) or
1543 isinstance(children[-1], nodes.enumerated_list) or
1544 isinstance(children[-1], nodes.field_list))):
1545 children.pop()
1546 # print "%s children remain" % len(children)
1547 if len(children) <= 1:
1548 return
1549 else:
1550 # print "found", child.__class__, "in", node.__class__
1551 raise nodes.NodeFound
1553 def pass_node(self, node):
1554 pass
1556 def ignore_node(self, node):
1557 # ignore nodes that are never complex (can contain only inline nodes)
1558 raise nodes.SkipNode
1560 # Paragraphs and text
1561 visit_Text = ignore_node
1562 visit_paragraph = ignore_node
1564 # Lists
1565 visit_bullet_list = pass_node
1566 visit_enumerated_list = pass_node
1567 visit_docinfo = pass_node
1569 # Docinfo nodes:
1570 visit_author = ignore_node
1571 visit_authors = visit_list_item
1572 visit_address = visit_list_item
1573 visit_contact = pass_node
1574 visit_copyright = ignore_node
1575 visit_date = ignore_node
1576 visit_organization = ignore_node
1577 visit_status = ignore_node
1578 visit_version = visit_list_item
1580 # Definition list:
1581 visit_definition_list = pass_node
1582 visit_definition_list_item = pass_node
1583 visit_term = ignore_node
1584 visit_classifier = pass_node
1585 visit_definition = visit_list_item
1587 # Field list:
1588 visit_field_list = pass_node
1589 visit_field = pass_node
1590 # the field body corresponds to a list item
1591 visit_field_body = visit_list_item
1592 visit_field_name = ignore_node
1594 # Invisible nodes should be ignored.
1595 visit_comment = ignore_node
1596 visit_substitution_definition = ignore_node
1597 visit_target = ignore_node
1598 visit_pending = ignore_node