Fix :width: option for the table directives.
[docutils.git] / docutils / docutils / writers / _html_base.py
blobd9275d846e52c7fa64cbbdbfafd5b0d84e12c9a4
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
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
18 """common definitions for Docutils HTML writers"""
20 import sys
21 import os.path
22 import re
23 import urllib
25 try: # check for the Python Imaging Library
26 import PIL.Image
27 except ImportError:
28 try: # sometimes PIL modules are put in PYTHONPATH's root
29 import Image
30 class PIL(object): pass # dummy wrapper
31 PIL.Image = Image
32 except ImportError:
33 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', 'html 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))
98 class HTMLTranslator(nodes.NodeVisitor):
101 Generic Docutils to HTML translator.
103 See the `html4css1` and `html5_polyglot` writers for full featured
104 HTML writers.
106 .. IMPORTANT::
107 The `visit_*` and `depart_*` methods use a
108 heterogeneous stack, `self.context`.
109 When subclassing, make sure to be consistent in its use!
111 Examples for robust coding:
113 a) Override both `visit_*` and `depart_*` methods, don't call the
114 parent functions.
116 b) Extend both and unconditionally call the parent functions::
118 def visit_example(self, node):
119 if foo:
120 self.body.append('<div class="foo">')
121 html4css1.HTMLTranslator.visit_example(self, node)
123 def depart_example(self, node):
124 html4css1.HTMLTranslator.depart_example(self, node)
125 if foo:
126 self.body.append('</div>')
128 c) Extend both, calling the parent functions under the same
129 conditions::
131 def visit_example(self, node):
132 if foo:
133 self.body.append('<div class="foo">\n')
134 else: # call the parent method
135 _html_base.HTMLTranslator.visit_example(self, node)
137 def depart_example(self, node):
138 if foo:
139 self.body.append('</div>\n')
140 else: # call the parent method
141 _html_base.HTMLTranslator.depart_example(self, node)
143 d) Extend one method (call the parent), but don't otherwise use the
144 `self.context` stack::
146 def depart_example(self, node):
147 _html_base.HTMLTranslator.depart_example(self, node)
148 if foo:
149 # implementation-specific code
150 # that does not use `self.context`
151 self.body.append('</div>\n')
153 This way, changes in stack use will not bite you.
156 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
157 doctype = '<!DOCTYPE html>\n'
158 doctype_mathml = doctype
160 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
161 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
162 content_type = ('<meta charset="%s"/>\n')
163 generator = ('<meta name="generator" content="Docutils %s: '
164 'http://docutils.sourceforge.net/" />\n')
166 # Template for the MathJax script in the header:
167 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
169 mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
171 URL of the MathJax javascript library.
173 The MathJax library ought to be installed on the same
174 server as the rest of the deployed site files and specified
175 in the `math-output` setting appended to "mathjax".
176 See `Docutils Configuration`__.
178 __ http://docutils.sourceforge.net/docs/user/config.html#math-output
180 The fallback tries a local MathJax installation at
181 ``/usr/share/javascript/mathjax/MathJax.js``.
184 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
185 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
186 words_and_spaces = re.compile(r'\S+| +|\n')
187 # wrap point inside word:
188 in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+', re.U)
189 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
191 special_characters = {ord('&'): u'&amp;',
192 ord('<'): u'&lt;',
193 ord('"'): u'&quot;',
194 ord('>'): u'&gt;',
195 ord('@'): u'&#64;', # may thwart address harvesters
197 """Character references for characters with a special meaning in HTML."""
200 def __init__(self, document):
201 nodes.NodeVisitor.__init__(self, document)
202 self.settings = settings = document.settings
203 lcode = settings.language_code
204 self.language = languages.get_language(lcode, document.reporter)
205 self.meta = [self.generator % docutils.__version__]
206 self.head_prefix = []
207 self.html_prolog = []
208 if settings.xml_declaration:
209 self.head_prefix.append(self.xml_declaration
210 % settings.output_encoding)
211 # self.content_type = ""
212 # encoding not interpolated:
213 self.html_prolog.append(self.xml_declaration)
214 self.head = self.meta[:]
215 self.stylesheet = [self.stylesheet_call(path)
216 for path in utils.get_stylesheet_list(settings)]
217 self.body_prefix = ['</head>\n<body>\n']
218 # document title, subtitle display
219 self.body_pre_docinfo = []
220 # author, date, etc.
221 self.docinfo = []
222 self.body = []
223 self.fragment = []
224 self.body_suffix = ['</body>\n</html>\n']
225 self.section_level = 0
226 self.initial_header_level = int(settings.initial_header_level)
228 self.math_output = settings.math_output.split()
229 self.math_output_options = self.math_output[1:]
230 self.math_output = self.math_output[0].lower()
232 self.context = []
233 """Heterogeneous stack.
235 Used by visit_* and depart_* functions in conjunction with the tree
236 traversal. Make sure that the pops correspond to the pushes."""
238 self.topic_classes = [] # TODO: replace with self_in_contents
239 self.colspecs = []
240 self.compact_p = True
241 self.compact_simple = False
242 self.compact_field_list = False
243 self.in_docinfo = False
244 self.in_sidebar = False
245 self.in_footnote_list = False
246 self.title = []
247 self.subtitle = []
248 self.header = []
249 self.footer = []
250 self.html_head = [self.content_type] # charset not interpolated
251 self.html_title = []
252 self.html_subtitle = []
253 self.html_body = []
254 self.in_document_title = 0 # len(self.body) or 0
255 self.in_mailto = False
256 self.author_in_authors = False # for html4css1
257 self.math_header = []
259 def astext(self):
260 return ''.join(self.head_prefix + self.head
261 + self.stylesheet + self.body_prefix
262 + self.body_pre_docinfo + self.docinfo
263 + self.body + self.body_suffix)
265 def encode(self, text):
266 """Encode special characters in `text` & return."""
267 # Use only named entities known in both XML and HTML
268 # other characters are automatically encoded "by number" if required.
269 # @@@ A codec to do these and all other HTML entities would be nice.
270 text = unicode(text)
271 return text.translate(self.special_characters)
273 def cloak_mailto(self, uri):
274 """Try to hide a mailto: URL from harvesters."""
275 # Encode "@" using a URL octet reference (see RFC 1738).
276 # Further cloaking with HTML entities will be done in the
277 # `attval` function.
278 return uri.replace('@', '%40')
280 def cloak_email(self, addr):
281 """Try to hide the link text of a email link from harversters."""
282 # Surround at-signs and periods with <span> tags. ("@" has
283 # already been encoded to "&#64;" by the `encode` method.)
284 addr = addr.replace('&#64;', '<span>&#64;</span>')
285 addr = addr.replace('.', '<span>&#46;</span>')
286 return addr
288 def attval(self, text,
289 whitespace=re.compile('[\n\r\t\v\f]')):
290 """Cleanse, HTML encode, and return attribute value text."""
291 encoded = self.encode(whitespace.sub(' ', text))
292 if self.in_mailto and self.settings.cloak_email_addresses:
293 # Cloak at-signs ("%40") and periods with HTML entities.
294 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
295 encoded = encoded.replace('.', '&#46;')
296 return encoded
298 def stylesheet_call(self, path):
299 """Return code to reference or embed stylesheet file `path`"""
300 if self.settings.embed_stylesheet:
301 try:
302 content = io.FileInput(source_path=path,
303 encoding='utf-8').read()
304 self.settings.record_dependencies.add(path)
305 except IOError, err:
306 msg = u"Cannot embed stylesheet '%s': %s." % (
307 path, SafeString(err.strerror))
308 self.document.reporter.error(msg)
309 return '<--- %s --->\n' % msg
310 return self.embedded_stylesheet % content
311 # else link to style file:
312 if self.settings.stylesheet_path:
313 # adapt path relative to output (cf. config.html#stylesheet-path)
314 path = utils.relative_path(self.settings._destination, path)
315 return self.stylesheet_link % self.encode(path)
317 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
319 Construct and return a start tag given a node (id & class attributes
320 are extracted), tag name, and optional attributes.
322 tagname = tagname.lower()
323 prefix = []
324 atts = {}
325 ids = []
326 for (name, value) in attributes.items():
327 atts[name.lower()] = value
328 classes = []
329 languages = []
330 # unify class arguments and move language specification
331 for cls in node.get('classes', []) + atts.pop('class', '').split() :
332 if cls.startswith('language-'):
333 languages.append(cls[9:])
334 elif cls.strip() and cls not in classes:
335 classes.append(cls)
336 if languages:
337 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
338 atts[self.lang_attribute] = languages[0]
339 if classes:
340 atts['class'] = ' '.join(classes)
341 assert 'id' not in atts
342 ids.extend(node.get('ids', []))
343 if 'ids' in atts:
344 ids.extend(atts['ids'])
345 del atts['ids']
346 if ids:
347 atts['id'] = ids[0]
348 for id in ids[1:]:
349 # Add empty "span" elements for additional IDs. Note
350 # that we cannot use empty "a" elements because there
351 # may be targets inside of references, but nested "a"
352 # elements aren't allowed in XHTML (even if they do
353 # not all have a "href" attribute).
354 if empty or isinstance(node,
355 (nodes.bullet_list, nodes.docinfo,
356 nodes.definition_list, nodes.enumerated_list,
357 nodes.field_list, nodes.option_list,
358 nodes.table)):
359 # Insert target right in front of element.
360 prefix.append('<span id="%s"></span>' % id)
361 else:
362 # Non-empty tag. Place the auxiliary <span> tag
363 # *inside* the element, as the first child.
364 suffix += '<span id="%s"></span>' % id
365 attlist = atts.items()
366 attlist.sort()
367 parts = [tagname]
368 for name, value in attlist:
369 # value=None was used for boolean attributes without
370 # value, but this isn't supported by XHTML.
371 assert value is not None
372 if isinstance(value, list):
373 values = [unicode(v) for v in value]
374 parts.append('%s="%s"' % (name.lower(),
375 self.attval(' '.join(values))))
376 else:
377 parts.append('%s="%s"' % (name.lower(),
378 self.attval(unicode(value))))
379 if empty:
380 infix = ' /'
381 else:
382 infix = ''
383 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
385 def emptytag(self, node, tagname, suffix='\n', **attributes):
386 """Construct and return an XML-compatible empty tag."""
387 return self.starttag(node, tagname, suffix, empty=True, **attributes)
389 def set_class_on_child(self, node, class_, index=0):
391 Set class `class_` on the visible child no. index of `node`.
392 Do nothing if node has fewer children than `index`.
394 children = [n for n in node if not isinstance(n, nodes.Invisible)]
395 try:
396 child = children[index]
397 except IndexError:
398 return
399 child['classes'].append(class_)
401 def visit_Text(self, node):
402 text = node.astext()
403 encoded = self.encode(text)
404 if self.in_mailto and self.settings.cloak_email_addresses:
405 encoded = self.cloak_email(encoded)
406 self.body.append(encoded)
408 def depart_Text(self, node):
409 pass
411 def visit_abbreviation(self, node):
412 # @@@ implementation incomplete ("title" attribute)
413 self.body.append(self.starttag(node, 'abbr', ''))
415 def depart_abbreviation(self, node):
416 self.body.append('</abbr>')
418 def visit_acronym(self, node):
419 # @@@ implementation incomplete ("title" attribute)
420 self.body.append(self.starttag(node, 'acronym', ''))
422 def depart_acronym(self, node):
423 self.body.append('</acronym>')
425 def visit_address(self, node):
426 self.visit_docinfo_item(node, 'address', meta=False)
427 self.body.append(self.starttag(node, 'pre',
428 suffix= '', CLASS='address'))
430 def depart_address(self, node):
431 self.body.append('\n</pre>\n')
432 self.depart_docinfo_item()
434 def visit_admonition(self, node):
435 node['classes'].insert(0, 'admonition')
436 self.body.append(self.starttag(node, 'div'))
438 def depart_admonition(self, node=None):
439 self.body.append('</div>\n')
441 attribution_formats = {'dash': (u'\u2014', ''),
442 'parentheses': ('(', ')'),
443 'parens': ('(', ')'),
444 'none': ('', '')}
446 def visit_attribution(self, node):
447 prefix, suffix = self.attribution_formats[self.settings.attribution]
448 self.context.append(suffix)
449 self.body.append(
450 self.starttag(node, 'p', prefix, CLASS='attribution'))
452 def depart_attribution(self, node):
453 self.body.append(self.context.pop() + '</p>\n')
455 def visit_author(self, node):
456 if not(isinstance(node.parent, nodes.authors)):
457 self.visit_docinfo_item(node, 'author')
458 self.body.append('<p>')
460 def depart_author(self, node):
461 self.body.append('</p>')
462 if isinstance(node.parent, nodes.authors):
463 self.body.append('\n')
464 else:
465 self.depart_docinfo_item()
467 def visit_authors(self, node):
468 self.visit_docinfo_item(node, 'authors')
470 def depart_authors(self, node):
471 self.depart_docinfo_item()
473 def visit_block_quote(self, node):
474 self.body.append(self.starttag(node, 'blockquote'))
476 def depart_block_quote(self, node):
477 self.body.append('</blockquote>\n')
479 def check_simple_list(self, node):
480 """Check for a simple list that can be rendered compactly."""
481 visitor = SimpleListChecker(self.document)
482 try:
483 node.walk(visitor)
484 except nodes.NodeFound:
485 return False
486 else:
487 return True
489 # Compact lists
490 # ------------
491 # Include definition lists and field lists (in addition to ordered
492 # and unordered lists) in the test if a list is "simple" (cf. the
493 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
494 # the end of this file).
496 def is_compactable(self, node):
497 # print "is_compactable %s ?" % node.__class__,
498 # explicite class arguments have precedence
499 if 'compact' in node['classes']:
500 return True
501 if 'open' in node['classes']:
502 return False
503 # check config setting:
504 if (isinstance(node, (nodes.field_list, nodes.definition_list))
505 and not self.settings.compact_field_lists):
506 # print "`compact-field-lists` is False"
507 return False
508 if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
509 and not self.settings.compact_lists):
510 # print "`compact-lists` is False"
511 return False
512 # more special cases:
513 if (self.topic_classes == ['contents']): # TODO: self.in_contents
514 return True
515 # check the list items:
516 return self.check_simple_list(node)
518 def visit_bullet_list(self, node):
519 atts = {}
520 old_compact_simple = self.compact_simple
521 self.context.append((self.compact_simple, self.compact_p))
522 self.compact_p = None
523 self.compact_simple = self.is_compactable(node)
524 if self.compact_simple and not old_compact_simple:
525 atts['class'] = 'simple'
526 self.body.append(self.starttag(node, 'ul', **atts))
528 def depart_bullet_list(self, node):
529 self.compact_simple, self.compact_p = self.context.pop()
530 self.body.append('</ul>\n')
532 def visit_caption(self, node):
533 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
535 def depart_caption(self, node):
536 self.body.append('</p>\n')
538 # citations
539 # ---------
540 # Use definition list instead of table for bibliographic references.
541 # Join adjacent citation entries.
543 def visit_citation(self, node):
544 if not self.in_footnote_list:
545 self.body.append('<dl class="citation">\n')
546 self.in_footnote_list = True
548 def depart_citation(self, node):
549 self.body.append('</dd>\n')
550 if not isinstance(node.next_node(descend=False, siblings=True),
551 nodes.citation):
552 self.body.append('</dl>\n')
553 self.in_footnote_list = False
555 def visit_citation_reference(self, node):
556 href = '#'
557 if 'refid' in node:
558 href += node['refid']
559 elif 'refname' in node:
560 href += self.document.nameids[node['refname']]
561 # else: # TODO system message (or already in the transform)?
562 # 'Citation reference missing.'
563 self.body.append(self.starttag(
564 node, 'a', '[', CLASS='citation-reference', href=href))
566 def depart_citation_reference(self, node):
567 self.body.append(']</a>')
569 # classifier
570 # ----------
571 # don't insert classifier-delimiter here (done by CSS)
573 def visit_classifier(self, node):
574 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
576 def depart_classifier(self, node):
577 self.body.append('</span>')
579 def visit_colspec(self, node):
580 self.colspecs.append(node)
581 # "stubs" list is an attribute of the tgroup element:
582 node.parent.stubs.append(node.attributes.get('stub'))
584 def depart_colspec(self, node):
585 # write out <colgroup> when all colspecs are processed
586 if isinstance(node.next_node(descend=False, siblings=True),
587 nodes.colspec):
588 return
589 if 'colwidths-auto' in node.parent.parent['classes'] or (
590 'colwidths-auto' in self.settings.table_style and
591 ('colwidths-given' not in node.parent.parent['classes'])):
592 return
593 total_width = sum(node['colwidth'] for node in self.colspecs)
594 self.body.append(self.starttag(node, 'colgroup'))
595 for node in self.colspecs:
596 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
597 self.body.append(self.emptytag(node, 'col',
598 style='width: %i%%' % colwidth))
599 self.body.append('</colgroup>\n')
601 def visit_comment(self, node,
602 sub=re.compile('-(?=-)').sub):
603 """Escape double-dashes in comment text."""
604 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
605 # Content already processed:
606 raise nodes.SkipNode
608 def visit_compound(self, node):
609 self.body.append(self.starttag(node, 'div', CLASS='compound'))
610 if len(node) > 1:
611 node[0]['classes'].append('compound-first')
612 node[-1]['classes'].append('compound-last')
613 for child in node[1:-1]:
614 child['classes'].append('compound-middle')
616 def depart_compound(self, node):
617 self.body.append('</div>\n')
619 def visit_container(self, node):
620 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
622 def depart_container(self, node):
623 self.body.append('</div>\n')
625 def visit_contact(self, node):
626 self.visit_docinfo_item(node, 'contact', meta=False)
628 def depart_contact(self, node):
629 self.depart_docinfo_item()
631 def visit_copyright(self, node):
632 self.visit_docinfo_item(node, 'copyright')
634 def depart_copyright(self, node):
635 self.depart_docinfo_item()
637 def visit_date(self, node):
638 self.visit_docinfo_item(node, 'date')
640 def depart_date(self, node):
641 self.depart_docinfo_item()
643 def visit_decoration(self, node):
644 pass
646 def depart_decoration(self, node):
647 pass
649 def visit_definition(self, node):
650 self.body.append('</dt>\n')
651 self.body.append(self.starttag(node, 'dd', ''))
653 def depart_definition(self, node):
654 self.body.append('</dd>\n')
656 def visit_definition_list(self, node):
657 classes = node.setdefault('classes', [])
658 if self.is_compactable(node):
659 classes.append('simple')
660 self.body.append(self.starttag(node, 'dl'))
662 def depart_definition_list(self, node):
663 self.body.append('</dl>\n')
665 def visit_definition_list_item(self, node):
666 # pass class arguments, ids and names to definition term:
667 node.children[0]['classes'] = (
668 node.get('classes', []) + node.children[0].get('classes', []))
669 node.children[0]['ids'] = (
670 node.get('ids', []) + node.children[0].get('ids', []))
671 node.children[0]['names'] = (
672 node.get('names', []) + node.children[0].get('names', []))
674 def depart_definition_list_item(self, node):
675 pass
677 def visit_description(self, node):
678 self.body.append(self.starttag(node, 'dd', ''))
680 def depart_description(self, node):
681 self.body.append('</dd>\n')
683 def visit_docinfo(self, node):
684 classes = 'docinfo'
685 if (self.is_compactable(node)):
686 classes += ' simple'
687 self.body.append(self.starttag(node, 'dl', CLASS=classes))
689 def depart_docinfo(self, node):
690 self.body.append('</dl>\n')
692 def visit_docinfo_item(self, node, name, meta=True):
693 if meta:
694 meta_tag = '<meta name="%s" content="%s" />\n' \
695 % (name, self.attval(node.astext()))
696 self.add_meta(meta_tag)
697 self.body.append('<dt class="%s">%s</dt>\n'
698 % (name, self.language.labels[name]))
699 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
701 def depart_docinfo_item(self):
702 self.body.append('</dd>\n')
704 def visit_doctest_block(self, node):
705 self.body.append(self.starttag(node, 'pre', suffix='',
706 CLASS='code python doctest'))
708 def depart_doctest_block(self, node):
709 self.body.append('\n</pre>\n')
711 def visit_document(self, node):
712 title = (node.get('title', '') or os.path.basename(node['source'])
713 or 'docutils document without title')
714 self.head.append('<title>%s</title>\n' % self.encode(title))
716 def depart_document(self, node):
717 self.head_prefix.extend([self.doctype,
718 self.head_prefix_template %
719 {'lang': self.settings.language_code}])
720 self.html_prolog.append(self.doctype)
721 self.meta.insert(0, self.content_type % self.settings.output_encoding)
722 self.head.insert(0, self.content_type % self.settings.output_encoding)
723 if 'name="dcterms.' in ''.join(self.meta):
724 self.head.append(
725 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/">')
726 if self.math_header:
727 if self.math_output == 'mathjax':
728 self.head.extend(self.math_header)
729 else:
730 self.stylesheet.extend(self.math_header)
731 # skip content-type meta tag with interpolated charset value:
732 self.html_head.extend(self.head[1:])
733 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
734 self.body_suffix.insert(0, '</div>\n')
735 self.fragment.extend(self.body) # self.fragment is the "naked" body
736 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
737 + self.docinfo + self.body
738 + self.body_suffix[:-1])
739 assert not self.context, 'len(context) = %s' % len(self.context)
741 def visit_emphasis(self, node):
742 self.body.append(self.starttag(node, 'em', ''))
744 def depart_emphasis(self, node):
745 self.body.append('</em>')
747 def visit_entry(self, node):
748 atts = {'class': []}
749 if isinstance(node.parent.parent, nodes.thead):
750 atts['class'].append('head')
751 if node.parent.parent.parent.stubs[node.parent.column]:
752 # "stubs" list is an attribute of the tgroup element
753 atts['class'].append('stub')
754 if atts['class']:
755 tagname = 'th'
756 atts['class'] = ' '.join(atts['class'])
757 else:
758 tagname = 'td'
759 del atts['class']
760 node.parent.column += 1
761 if 'morerows' in node:
762 atts['rowspan'] = node['morerows'] + 1
763 if 'morecols' in node:
764 atts['colspan'] = node['morecols'] + 1
765 node.parent.column += node['morecols']
766 self.body.append(self.starttag(node, tagname, '', **atts))
767 self.context.append('</%s>\n' % tagname.lower())
768 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
769 # if len(node) == 0: # empty cell
770 # self.body.append('&#0160;') # no-break space
772 def depart_entry(self, node):
773 self.body.append(self.context.pop())
775 def visit_enumerated_list(self, node):
776 atts = {}
777 if 'start' in node:
778 atts['start'] = node['start']
779 if 'enumtype' in node:
780 atts['class'] = node['enumtype']
781 if self.is_compactable(node):
782 atts['class'] = (atts.get('class', '') + ' simple').strip()
783 self.body.append(self.starttag(node, 'ol', **atts))
785 def depart_enumerated_list(self, node):
786 self.body.append('</ol>\n')
788 def visit_field_list(self, node):
789 # Keep simple paragraphs in the field_body to enable CSS
790 # rule to start body on new line if the label is too long
791 classes = 'field-list'
792 if (self.is_compactable(node)):
793 classes += ' simple'
794 self.body.append(self.starttag(node, 'dl', CLASS=classes))
796 def depart_field_list(self, node):
797 self.body.append('</dl>\n')
799 def visit_field(self, node):
800 pass
802 def depart_field(self, node):
803 pass
805 # as field is ignored, pass class arguments to field-name and field-body:
807 def visit_field_name(self, node):
808 self.body.append(self.starttag(node, 'dt', '',
809 CLASS=''.join(node.parent['classes'])))
811 def depart_field_name(self, node):
812 self.body.append('</dt>\n')
814 def visit_field_body(self, node):
815 self.body.append(self.starttag(node, 'dd', '',
816 CLASS=''.join(node.parent['classes'])))
817 # prevent misalignment of following content if the field is empty:
818 if not node.children:
819 self.body.append('<p></p>')
821 def depart_field_body(self, node):
822 self.body.append('</dd>\n')
824 def visit_figure(self, node):
825 atts = {'class': 'figure'}
826 if node.get('width'):
827 atts['style'] = 'width: %s' % node['width']
828 if node.get('align'):
829 atts['class'] += " align-" + node['align']
830 self.body.append(self.starttag(node, 'div', **atts))
832 def depart_figure(self, node):
833 self.body.append('</div>\n')
835 # use HTML 5 <footer> element?
836 def visit_footer(self, node):
837 self.context.append(len(self.body))
839 def depart_footer(self, node):
840 start = self.context.pop()
841 footer = [self.starttag(node, 'div', CLASS='footer'),
842 '<hr class="footer" />\n']
843 footer.extend(self.body[start:])
844 footer.append('\n</div>\n')
845 self.footer.extend(footer)
846 self.body_suffix[:0] = footer
847 del self.body[start:]
849 # footnotes
850 # ---------
851 # use definition list instead of table for footnote text
853 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
854 def visit_footnote(self, node):
855 if not self.in_footnote_list:
856 classes = 'footnote ' + self.settings.footnote_references
857 self.body.append('<dl class="%s">\n'%classes)
858 self.in_footnote_list = True
860 def depart_footnote(self, node):
861 self.body.append('</dd>\n')
862 if not isinstance(node.next_node(descend=False, siblings=True),
863 nodes.footnote):
864 self.body.append('</dl>\n')
865 self.in_footnote_list = False
867 def visit_footnote_reference(self, node):
868 href = '#' + node['refid']
869 classes = 'footnote-reference ' + self.settings.footnote_references
870 self.body.append(self.starttag(node, 'a', '', #suffix,
871 CLASS=classes, href=href))
873 def depart_footnote_reference(self, node):
874 self.body.append('</a>')
876 # Docutils-generated text: put section numbers in a span for CSS styling:
877 def visit_generated(self, node):
878 if 'sectnum' in node['classes']:
879 # get section number (strip trailing no-break-spaces)
880 sectnum = node.astext().rstrip(u' ')
881 # print sectnum.encode('utf-8')
882 self.body.append('<span class="sectnum">%s</span> '
883 % self.encode(sectnum))
884 # Content already processed:
885 raise nodes.SkipNode
887 def depart_generated(self, node):
888 pass
890 def visit_header(self, node):
891 self.context.append(len(self.body))
893 def depart_header(self, node):
894 start = self.context.pop()
895 header = [self.starttag(node, 'div', CLASS='header')]
896 header.extend(self.body[start:])
897 header.append('\n<hr class="header"/>\n</div>\n')
898 self.body_prefix.extend(header)
899 self.header.extend(header)
900 del self.body[start:]
902 # Image types to place in an <object> element
903 object_image_types = {'.swf': 'application/x-shockwave-flash'}
905 def visit_image(self, node):
906 atts = {}
907 uri = node['uri']
908 ext = os.path.splitext(uri)[1].lower()
909 if ext in self.object_image_types:
910 atts['data'] = uri
911 atts['type'] = self.object_image_types[ext]
912 else:
913 atts['src'] = uri
914 atts['alt'] = node.get('alt', uri)
915 # image size
916 if 'width' in node:
917 atts['width'] = node['width']
918 if 'height' in node:
919 atts['height'] = node['height']
920 if 'scale' in node:
921 if (PIL and not ('width' in node and 'height' in node)
922 and self.settings.file_insertion_enabled):
923 imagepath = urllib.url2pathname(uri)
924 try:
925 img = PIL.Image.open(
926 imagepath.encode(sys.getfilesystemencoding()))
927 except (IOError, UnicodeEncodeError):
928 pass # TODO: warn?
929 else:
930 self.settings.record_dependencies.add(
931 imagepath.replace('\\', '/'))
932 if 'width' not in atts:
933 atts['width'] = '%dpx' % img.size[0]
934 if 'height' not in atts:
935 atts['height'] = '%dpx' % img.size[1]
936 del img
937 for att_name in 'width', 'height':
938 if att_name in atts:
939 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
940 assert match
941 atts[att_name] = '%s%s' % (
942 float(match.group(1)) * (float(node['scale']) / 100),
943 match.group(2))
944 style = []
945 for att_name in 'width', 'height':
946 if att_name in atts:
947 if re.match(r'^[0-9.]+$', atts[att_name]):
948 # Interpret unitless values as pixels.
949 atts[att_name] += 'px'
950 style.append('%s: %s;' % (att_name, atts[att_name]))
951 del atts[att_name]
952 if style:
953 atts['style'] = ' '.join(style)
954 if (isinstance(node.parent, nodes.TextElement) or
955 (isinstance(node.parent, nodes.reference) and
956 not isinstance(node.parent.parent, nodes.TextElement))):
957 # Inline context or surrounded by <a>...</a>.
958 suffix = ''
959 else:
960 suffix = '\n'
961 if 'align' in node:
962 atts['class'] = 'align-%s' % node['align']
963 if ext in self.object_image_types:
964 # do NOT use an empty tag: incorrect rendering in browsers
965 self.body.append(self.starttag(node, 'object', suffix, **atts) +
966 node.get('alt', uri) + '</object>' + suffix)
967 else:
968 self.body.append(self.emptytag(node, 'img', suffix, **atts))
970 def depart_image(self, node):
971 # self.body.append(self.context.pop())
972 pass
974 def visit_inline(self, node):
975 self.body.append(self.starttag(node, 'span', ''))
977 def depart_inline(self, node):
978 self.body.append('</span>')
980 # footnote and citation labels:
981 def visit_label(self, node):
982 if (isinstance(node.parent, nodes.footnote)):
983 classes = self.settings.footnote_references
984 else:
985 classes = 'brackets'
986 # pass parent node to get id into starttag:
987 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
988 self.body.append(self.starttag(node, 'span', '', CLASS=classes))
989 # footnote/citation backrefs:
990 if self.settings.footnote_backlinks:
991 backrefs = node.parent['backrefs']
992 if len(backrefs) == 1:
993 self.body.append('<a class="fn-backref" href="#%s">'
994 % backrefs[0])
996 def depart_label(self, node):
997 if self.settings.footnote_backlinks:
998 backrefs = node.parent['backrefs']
999 if len(backrefs) == 1:
1000 self.body.append('</a>')
1001 self.body.append('</span>')
1002 if self.settings.footnote_backlinks and len(backrefs) > 1:
1003 backlinks = ['<a href="#%s">%s</a>' % (ref, i)
1004 for (i, ref) in enumerate(backrefs, 1)]
1005 self.body.append('<span class="fn-backref">(%s)</span>'
1006 % ','.join(backlinks))
1007 self.body.append('</dt>\n<dd>')
1009 def visit_legend(self, node):
1010 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1012 def depart_legend(self, node):
1013 self.body.append('</div>\n')
1015 def visit_line(self, node):
1016 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1017 if not len(node):
1018 self.body.append('<br />')
1020 def depart_line(self, node):
1021 self.body.append('</div>\n')
1023 def visit_line_block(self, node):
1024 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1026 def depart_line_block(self, node):
1027 self.body.append('</div>\n')
1029 def visit_list_item(self, node):
1030 self.body.append(self.starttag(node, 'li', ''))
1032 def depart_list_item(self, node):
1033 self.body.append('</li>\n')
1035 # inline literal
1036 def visit_literal(self, node):
1037 # special case: "code" role
1038 classes = node.get('classes', [])
1039 if 'code' in classes:
1040 # filter 'code' from class arguments
1041 node['classes'] = [cls for cls in classes if cls != 'code']
1042 self.body.append(self.starttag(node, 'code', ''))
1043 return
1044 self.body.append(
1045 self.starttag(node, 'span', '', CLASS='docutils literal'))
1046 text = node.astext()
1047 # remove hard line breaks (except if in a parsed-literal block)
1048 if not isinstance(node.parent, nodes.literal_block):
1049 text = text.replace('\n', ' ')
1050 # Protect text like ``--an-option`` and the regular expression
1051 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1052 for token in self.words_and_spaces.findall(text):
1053 if token.strip() and self.in_word_wrap_point.search(token):
1054 self.body.append('<span class="pre">%s</span>'
1055 % self.encode(token))
1056 else:
1057 self.body.append(self.encode(token))
1058 self.body.append('</span>')
1059 # Content already processed:
1060 raise nodes.SkipNode
1062 def depart_literal(self, node):
1063 # skipped unless literal element is from "code" role:
1064 self.body.append('</code>')
1066 def visit_literal_block(self, node):
1067 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1068 if 'code' in node.get('classes', []):
1069 self.body.append('<code>')
1071 def depart_literal_block(self, node):
1072 if 'code' in node.get('classes', []):
1073 self.body.append('</code>')
1074 self.body.append('</pre>\n')
1076 # Mathematics:
1077 # As there is no native HTML math support, we provide alternatives
1078 # for the math-output: LaTeX and MathJax simply wrap the content,
1079 # HTML and MathML also convert the math_code.
1080 # HTML container
1081 math_tags = {# math_output: (block, inline, class-arguments)
1082 'mathml': ('div', '', ''),
1083 'html': ('div', 'span', 'formula'),
1084 'mathjax': ('div', 'span', 'math'),
1085 'latex': ('pre', 'tt', 'math'),
1088 def visit_math(self, node, math_env=''):
1089 # If the method is called from visit_math_block(), math_env != ''.
1091 if self.math_output not in self.math_tags:
1092 self.document.reporter.error(
1093 'math-output format "%s" not supported '
1094 'falling back to "latex"'% self.math_output)
1095 self.math_output = 'latex'
1096 tag = self.math_tags[self.math_output][math_env == '']
1097 clsarg = self.math_tags[self.math_output][2]
1098 # LaTeX container
1099 wrappers = {# math_mode: (inline, block)
1100 'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1101 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1102 'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1103 'latex': (None, None),
1105 wrapper = wrappers[self.math_output][math_env != '']
1106 if self.math_output == 'mathml' and (not self.math_output_options or
1107 self.math_output_options[0] == 'blahtexml'):
1108 wrapper = None
1109 # get and wrap content
1110 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1111 if wrapper:
1112 try: # wrapper with three "%s"
1113 math_code = wrapper % (math_env, math_code, math_env)
1114 except TypeError: # wrapper with one "%s"
1115 math_code = wrapper % math_code
1116 # settings and conversion
1117 if self.math_output in ('latex', 'mathjax'):
1118 math_code = self.encode(math_code)
1119 if self.math_output == 'mathjax' and not self.math_header:
1120 try:
1121 self.mathjax_url = self.math_output_options[0]
1122 except IndexError:
1123 self.document.reporter.warning('No MathJax URL specified, '
1124 'using local fallback (see config.html)')
1125 # append configuration, if not already present in the URL:
1126 # input LaTeX with AMS, output common HTML
1127 if '?' not in self.mathjax_url:
1128 self.mathjax_url += '?config=TeX-AMS_CHTML'
1129 self.math_header = [self.mathjax_script % self.mathjax_url]
1130 elif self.math_output == 'html':
1131 if self.math_output_options and not self.math_header:
1132 self.math_header = [self.stylesheet_call(
1133 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1134 for s in self.math_output_options[0].split(',')]
1135 # TODO: fix display mode in matrices and fractions
1136 math2html.DocumentParameters.displaymode = (math_env != '')
1137 math_code = math2html.math2html(math_code)
1138 elif self.math_output == 'mathml':
1139 if 'XHTML 1' in self.doctype:
1140 self.doctype = self.doctype_mathml
1141 self.content_type = self.content_type_mathml
1142 converter = ' '.join(self.math_output_options).lower()
1143 try:
1144 if converter == 'latexml':
1145 math_code = tex2mathml_extern.latexml(math_code,
1146 self.document.reporter)
1147 elif converter == 'ttm':
1148 math_code = tex2mathml_extern.ttm(math_code,
1149 self.document.reporter)
1150 elif converter == 'blahtexml':
1151 math_code = tex2mathml_extern.blahtexml(math_code,
1152 inline=not(math_env),
1153 reporter=self.document.reporter)
1154 elif not converter:
1155 math_code = latex2mathml.tex2mathml(math_code,
1156 inline=not(math_env))
1157 else:
1158 self.document.reporter.error('option "%s" not supported '
1159 'with math-output "MathML"')
1160 except OSError:
1161 raise OSError('is "latexmlmath" in your PATH?')
1162 except SyntaxError, err:
1163 err_node = self.document.reporter.error(err, base_node=node)
1164 self.visit_system_message(err_node)
1165 self.body.append(self.starttag(node, 'p'))
1166 self.body.append(u','.join(err.args))
1167 self.body.append('</p>\n')
1168 self.body.append(self.starttag(node, 'pre',
1169 CLASS='literal-block'))
1170 self.body.append(self.encode(math_code))
1171 self.body.append('\n</pre>\n')
1172 self.depart_system_message(err_node)
1173 raise nodes.SkipNode
1174 # append to document body
1175 if tag:
1176 self.body.append(self.starttag(node, tag,
1177 suffix='\n'*bool(math_env),
1178 CLASS=clsarg))
1179 self.body.append(math_code)
1180 if math_env: # block mode (equation, display)
1181 self.body.append('\n')
1182 if tag:
1183 self.body.append('</%s>' % tag)
1184 if math_env:
1185 self.body.append('\n')
1186 # Content already processed:
1187 raise nodes.SkipNode
1189 def depart_math(self, node):
1190 pass # never reached
1192 def visit_math_block(self, node):
1193 # print node.astext().encode('utf8')
1194 math_env = pick_math_environment(node.astext())
1195 self.visit_math(node, math_env=math_env)
1197 def depart_math_block(self, node):
1198 pass # never reached
1200 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1201 # HTML5/polyglot recommends using both
1202 def visit_meta(self, node):
1203 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1204 self.add_meta(meta)
1206 def depart_meta(self, node):
1207 pass
1209 def add_meta(self, tag):
1210 self.meta.append(tag)
1211 self.head.append(tag)
1213 def visit_option(self, node):
1214 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1216 def depart_option(self, node):
1217 self.body.append('</span>')
1218 if isinstance(node.next_node(descend=False, siblings=True),
1219 nodes.option):
1220 self.body.append(', ')
1222 def visit_option_argument(self, node):
1223 self.body.append(node.get('delimiter', ' '))
1224 self.body.append(self.starttag(node, 'var', ''))
1226 def depart_option_argument(self, node):
1227 self.body.append('</var>')
1229 def visit_option_group(self, node):
1230 self.body.append(self.starttag(node, 'dt', ''))
1231 self.body.append('<kbd>')
1233 def depart_option_group(self, node):
1234 self.body.append('</kbd></dt>\n')
1236 def visit_option_list(self, node):
1237 self.body.append(
1238 self.starttag(node, 'dl', CLASS='option-list'))
1240 def depart_option_list(self, node):
1241 self.body.append('</dl>\n')
1243 def visit_option_list_item(self, node):
1244 pass
1246 def depart_option_list_item(self, node):
1247 pass
1249 def visit_option_string(self, node):
1250 pass
1252 def depart_option_string(self, node):
1253 pass
1255 def visit_organization(self, node):
1256 self.visit_docinfo_item(node, 'organization')
1258 def depart_organization(self, node):
1259 self.depart_docinfo_item()
1261 # Do not omit <p> tags
1262 # --------------------
1264 # The HTML4CSS1 writer does this to "produce
1265 # visually compact lists (less vertical whitespace)". This writer
1266 # relies on CSS rules for"visual compactness".
1268 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1269 # character data, so you cannot drop the <p> tags.
1270 # * Keeping simple paragraphs in the field_body enables a CSS
1271 # rule to start the field-body on a new line if the label is too long
1272 # * it makes the code simpler.
1274 # TODO: omit paragraph tags in simple table cells?
1276 def visit_paragraph(self, node):
1277 self.body.append(self.starttag(node, 'p', ''))
1279 def depart_paragraph(self, node):
1280 self.body.append('</p>')
1281 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1282 (len(node.parent) == 1)):
1283 self.body.append('\n')
1285 def visit_problematic(self, node):
1286 if node.hasattr('refid'):
1287 self.body.append('<a href="#%s">' % node['refid'])
1288 self.context.append('</a>')
1289 else:
1290 self.context.append('')
1291 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1293 def depart_problematic(self, node):
1294 self.body.append('</span>')
1295 self.body.append(self.context.pop())
1297 def visit_raw(self, node):
1298 if 'html' in node.get('format', '').split():
1299 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1300 if node['classes']:
1301 self.body.append(self.starttag(node, t, suffix=''))
1302 self.body.append(node.astext())
1303 if node['classes']:
1304 self.body.append('</%s>' % t)
1305 # Keep non-HTML raw text out of output:
1306 raise nodes.SkipNode
1308 def visit_reference(self, node):
1309 atts = {'class': 'reference'}
1310 if 'refuri' in node:
1311 atts['href'] = node['refuri']
1312 if ( self.settings.cloak_email_addresses
1313 and atts['href'].startswith('mailto:')):
1314 atts['href'] = self.cloak_mailto(atts['href'])
1315 self.in_mailto = True
1316 atts['class'] += ' external'
1317 else:
1318 assert 'refid' in node, \
1319 'References must have "refuri" or "refid" attribute.'
1320 atts['href'] = '#' + node['refid']
1321 atts['class'] += ' internal'
1322 if not isinstance(node.parent, nodes.TextElement):
1323 assert len(node) == 1 and isinstance(node[0], nodes.image)
1324 atts['class'] += ' image-reference'
1325 self.body.append(self.starttag(node, 'a', '', **atts))
1327 def depart_reference(self, node):
1328 self.body.append('</a>')
1329 if not isinstance(node.parent, nodes.TextElement):
1330 self.body.append('\n')
1331 self.in_mailto = False
1333 def visit_revision(self, node):
1334 self.visit_docinfo_item(node, 'revision', meta=False)
1336 def depart_revision(self, node):
1337 self.depart_docinfo_item()
1339 def visit_row(self, node):
1340 self.body.append(self.starttag(node, 'tr', ''))
1341 node.column = 0
1343 def depart_row(self, node):
1344 self.body.append('</tr>\n')
1346 def visit_rubric(self, node):
1347 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1349 def depart_rubric(self, node):
1350 self.body.append('</p>\n')
1352 # TODO: use the new HTML 5 element <section>?
1353 def visit_section(self, node):
1354 self.section_level += 1
1355 self.body.append(
1356 self.starttag(node, 'div', CLASS='section'))
1358 def depart_section(self, node):
1359 self.section_level -= 1
1360 self.body.append('</div>\n')
1362 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1363 def visit_sidebar(self, node):
1364 self.body.append(
1365 self.starttag(node, 'div', CLASS='sidebar'))
1366 self.in_sidebar = True
1368 def depart_sidebar(self, node):
1369 self.body.append('</div>\n')
1370 self.in_sidebar = False
1372 def visit_status(self, node):
1373 self.visit_docinfo_item(node, 'status', meta=False)
1375 def depart_status(self, node):
1376 self.depart_docinfo_item()
1378 def visit_strong(self, node):
1379 self.body.append(self.starttag(node, 'strong', ''))
1381 def depart_strong(self, node):
1382 self.body.append('</strong>')
1384 def visit_subscript(self, node):
1385 self.body.append(self.starttag(node, 'sub', ''))
1387 def depart_subscript(self, node):
1388 self.body.append('</sub>')
1390 def visit_substitution_definition(self, node):
1391 """Internal only."""
1392 raise nodes.SkipNode
1394 def visit_substitution_reference(self, node):
1395 self.unimplemented_visit(node)
1397 # h1–h6 elements must not be used to markup subheadings, subtitles,
1398 # alternative titles and taglines unless intended to be the heading for a
1399 # new section or subsection.
1400 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1401 def visit_subtitle(self, node):
1402 if isinstance(node.parent, nodes.sidebar):
1403 classes = 'sidebar-subtitle'
1404 elif isinstance(node.parent, nodes.document):
1405 classes = 'subtitle'
1406 self.in_document_title = len(self.body)
1407 elif isinstance(node.parent, nodes.section):
1408 classes = 'section-subtitle'
1409 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1411 def depart_subtitle(self, node):
1412 self.body.append('</p>\n')
1413 if self.in_document_title:
1414 self.subtitle = self.body[self.in_document_title:-1]
1415 self.in_document_title = 0
1416 self.body_pre_docinfo.extend(self.body)
1417 self.html_subtitle.extend(self.body)
1418 del self.body[:]
1420 def visit_superscript(self, node):
1421 self.body.append(self.starttag(node, 'sup', ''))
1423 def depart_superscript(self, node):
1424 self.body.append('</sup>')
1426 def visit_system_message(self, node):
1427 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1428 self.body.append('<p class="system-message-title">')
1429 backref_text = ''
1430 if len(node['backrefs']):
1431 backrefs = node['backrefs']
1432 if len(backrefs) == 1:
1433 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1434 % backrefs[0])
1435 else:
1436 i = 1
1437 backlinks = []
1438 for backref in backrefs:
1439 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1440 i += 1
1441 backref_text = ('; <em>backlinks: %s</em>'
1442 % ', '.join(backlinks))
1443 if node.hasattr('line'):
1444 line = ', line %s' % node['line']
1445 else:
1446 line = ''
1447 self.body.append('System Message: %s/%s '
1448 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1449 % (node['type'], node['level'],
1450 self.encode(node['source']), line, backref_text))
1452 def depart_system_message(self, node):
1453 self.body.append('</div>\n')
1455 def visit_table(self, node):
1456 atts = {}
1457 classes = [cls.strip(u' \t\n')
1458 for cls in self.settings.table_style.split(',')]
1459 if 'align' in node:
1460 classes.append('align-%s' % node['align'])
1461 if 'width' in node:
1462 atts['style'] = 'width: %s' % node['width']
1463 tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts)
1464 self.body.append(tag)
1466 def depart_table(self, node):
1467 self.body.append('</table>\n')
1469 def visit_target(self, node):
1470 if not ('refuri' in node or 'refid' in node
1471 or 'refname' in node):
1472 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1473 self.context.append('</span>')
1474 else:
1475 self.context.append('')
1477 def depart_target(self, node):
1478 self.body.append(self.context.pop())
1480 # no hard-coded vertical alignment in table body
1481 def visit_tbody(self, node):
1482 self.body.append(self.starttag(node, 'tbody'))
1484 def depart_tbody(self, node):
1485 self.body.append('</tbody>\n')
1487 def visit_term(self, node):
1488 self.body.append(self.starttag(node, 'dt', ''))
1490 def depart_term(self, node):
1492 Leave the end tag to `self.visit_definition()`, in case there's a
1493 classifier.
1495 pass
1497 def visit_tgroup(self, node):
1498 self.colspecs = []
1499 node.stubs = []
1501 def depart_tgroup(self, node):
1502 pass
1504 def visit_thead(self, node):
1505 self.body.append(self.starttag(node, 'thead'))
1507 def depart_thead(self, node):
1508 self.body.append('</thead>\n')
1510 def visit_title(self, node):
1511 """Only 6 section levels are supported by HTML."""
1512 check_id = 0 # TODO: is this a bool (False) or a counter?
1513 close_tag = '</p>\n'
1514 if isinstance(node.parent, nodes.topic):
1515 self.body.append(
1516 self.starttag(node, 'p', '', CLASS='topic-title first'))
1517 elif isinstance(node.parent, nodes.sidebar):
1518 self.body.append(
1519 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1520 elif isinstance(node.parent, nodes.Admonition):
1521 self.body.append(
1522 self.starttag(node, 'p', '', CLASS='admonition-title'))
1523 elif isinstance(node.parent, nodes.table):
1524 self.body.append(
1525 self.starttag(node, 'caption', ''))
1526 close_tag = '</caption>\n'
1527 elif isinstance(node.parent, nodes.document):
1528 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1529 close_tag = '</h1>\n'
1530 self.in_document_title = len(self.body)
1531 else:
1532 assert isinstance(node.parent, nodes.section)
1533 h_level = self.section_level + self.initial_header_level - 1
1534 atts = {}
1535 if (len(node.parent) >= 2 and
1536 isinstance(node.parent[1], nodes.subtitle)):
1537 atts['CLASS'] = 'with-subtitle'
1538 self.body.append(
1539 self.starttag(node, 'h%s' % h_level, '', **atts))
1540 atts = {}
1541 if node.hasattr('refid'):
1542 atts['class'] = 'toc-backref'
1543 atts['href'] = '#' + node['refid']
1544 if atts:
1545 self.body.append(self.starttag({}, 'a', '', **atts))
1546 close_tag = '</a></h%s>\n' % (h_level)
1547 else:
1548 close_tag = '</h%s>\n' % (h_level)
1549 self.context.append(close_tag)
1551 def depart_title(self, node):
1552 self.body.append(self.context.pop())
1553 if self.in_document_title:
1554 self.title = self.body[self.in_document_title:-1]
1555 self.in_document_title = 0
1556 self.body_pre_docinfo.extend(self.body)
1557 self.html_title.extend(self.body)
1558 del self.body[:]
1560 def visit_title_reference(self, node):
1561 self.body.append(self.starttag(node, 'cite', ''))
1563 def depart_title_reference(self, node):
1564 self.body.append('</cite>')
1566 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1567 def visit_topic(self, node):
1568 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1569 self.topic_classes = node['classes']
1570 # TODO: replace with ::
1571 # self.in_contents = 'contents' in node['classes']
1573 def depart_topic(self, node):
1574 self.body.append('</div>\n')
1575 self.topic_classes = []
1576 # TODO self.in_contents = False
1578 def visit_transition(self, node):
1579 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1581 def depart_transition(self, node):
1582 pass
1584 def visit_version(self, node):
1585 self.visit_docinfo_item(node, 'version', meta=False)
1587 def depart_version(self, node):
1588 self.depart_docinfo_item()
1590 def unimplemented_visit(self, node):
1591 raise NotImplementedError('visiting unimplemented node type: %s'
1592 % node.__class__.__name__)
1595 class SimpleListChecker(nodes.GenericNodeVisitor):
1598 Raise `nodes.NodeFound` if non-simple list item is encountered.
1600 Here "simple" means a list item containing nothing other than a single
1601 paragraph, a simple list, or a paragraph followed by a simple list.
1603 This version also checks for simple field lists and docinfo.
1606 def default_visit(self, node):
1607 raise nodes.NodeFound
1609 def visit_list_item(self, node):
1610 # print "visiting list item", node.__class__
1611 children = [child for child in node.children
1612 if not isinstance(child, nodes.Invisible)]
1613 # print "has %s visible children" % len(children)
1614 if (children and isinstance(children[0], nodes.paragraph)
1615 and (isinstance(children[-1], nodes.bullet_list) or
1616 isinstance(children[-1], nodes.enumerated_list) or
1617 isinstance(children[-1], nodes.field_list))):
1618 children.pop()
1619 # print "%s children remain" % len(children)
1620 if len(children) <= 1:
1621 return
1622 else:
1623 # print "found", child.__class__, "in", node.__class__
1624 raise nodes.NodeFound
1626 def pass_node(self, node):
1627 pass
1629 def ignore_node(self, node):
1630 # ignore nodes that are never complex (can contain only inline nodes)
1631 raise nodes.SkipNode
1633 # Paragraphs and text
1634 visit_Text = ignore_node
1635 visit_paragraph = ignore_node
1637 # Lists
1638 visit_bullet_list = pass_node
1639 visit_enumerated_list = pass_node
1640 visit_docinfo = pass_node
1642 # Docinfo nodes:
1643 visit_author = ignore_node
1644 visit_authors = visit_list_item
1645 visit_address = visit_list_item
1646 visit_contact = pass_node
1647 visit_copyright = ignore_node
1648 visit_date = ignore_node
1649 visit_organization = ignore_node
1650 visit_status = ignore_node
1651 visit_version = visit_list_item
1653 # Definition list:
1654 visit_definition_list = pass_node
1655 visit_definition_list_item = pass_node
1656 visit_term = ignore_node
1657 visit_classifier = pass_node
1658 visit_definition = visit_list_item
1660 # Field list:
1661 visit_field_list = pass_node
1662 visit_field = pass_node
1663 # the field body corresponds to a list item
1664 visit_field_body = visit_list_item
1665 visit_field_name = ignore_node
1667 # Invisible nodes should be ignored.
1668 visit_comment = ignore_node
1669 visit_substitution_definition = ignore_node
1670 visit_target = ignore_node
1671 visit_pending = ignore_node