Html5 writer: Correctly sort docinfo and subtitle into "parts" dict.
[docutils.git] / docutils / docutils / writers / _html_base.py
blobbca1db9226626365ea66a3a5e8aec918be2e299c
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 self.context.append(len(self.body))
685 classes = 'docinfo'
686 if (self.is_compactable(node)):
687 classes += ' simple'
688 self.body.append(self.starttag(node, 'dl', CLASS=classes))
690 def depart_docinfo(self, node):
691 self.body.append('</dl>\n')
692 start = self.context.pop()
693 self.docinfo = self.body[start:]
694 self.body = []
696 def visit_docinfo_item(self, node, name, meta=True):
697 if meta:
698 meta_tag = '<meta name="%s" content="%s" />\n' \
699 % (name, self.attval(node.astext()))
700 self.add_meta(meta_tag)
701 self.body.append('<dt class="%s">%s</dt>\n'
702 % (name, self.language.labels[name]))
703 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
705 def depart_docinfo_item(self):
706 self.body.append('</dd>\n')
708 def visit_doctest_block(self, node):
709 self.body.append(self.starttag(node, 'pre', suffix='',
710 CLASS='code python doctest'))
712 def depart_doctest_block(self, node):
713 self.body.append('\n</pre>\n')
715 def visit_document(self, node):
716 title = (node.get('title', '') or os.path.basename(node['source'])
717 or 'docutils document without title')
718 self.head.append('<title>%s</title>\n' % self.encode(title))
720 def depart_document(self, node):
721 self.head_prefix.extend([self.doctype,
722 self.head_prefix_template %
723 {'lang': self.settings.language_code}])
724 self.html_prolog.append(self.doctype)
725 self.meta.insert(0, self.content_type % self.settings.output_encoding)
726 self.head.insert(0, self.content_type % self.settings.output_encoding)
727 if 'name="dcterms.' in ''.join(self.meta):
728 self.head.append(
729 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/">')
730 if self.math_header:
731 if self.math_output == 'mathjax':
732 self.head.extend(self.math_header)
733 else:
734 self.stylesheet.extend(self.math_header)
735 # skip content-type meta tag with interpolated charset value:
736 self.html_head.extend(self.head[1:])
737 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
738 self.body_suffix.insert(0, '</div>\n')
739 self.fragment.extend(self.body) # self.fragment is the "naked" body
740 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
741 + self.docinfo + self.body
742 + self.body_suffix[:-1])
743 assert not self.context, 'len(context) = %s' % len(self.context)
745 def visit_emphasis(self, node):
746 self.body.append(self.starttag(node, 'em', ''))
748 def depart_emphasis(self, node):
749 self.body.append('</em>')
751 def visit_entry(self, node):
752 atts = {'class': []}
753 if isinstance(node.parent.parent, nodes.thead):
754 atts['class'].append('head')
755 if node.parent.parent.parent.stubs[node.parent.column]:
756 # "stubs" list is an attribute of the tgroup element
757 atts['class'].append('stub')
758 if atts['class']:
759 tagname = 'th'
760 atts['class'] = ' '.join(atts['class'])
761 else:
762 tagname = 'td'
763 del atts['class']
764 node.parent.column += 1
765 if 'morerows' in node:
766 atts['rowspan'] = node['morerows'] + 1
767 if 'morecols' in node:
768 atts['colspan'] = node['morecols'] + 1
769 node.parent.column += node['morecols']
770 self.body.append(self.starttag(node, tagname, '', **atts))
771 self.context.append('</%s>\n' % tagname.lower())
772 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
773 # if len(node) == 0: # empty cell
774 # self.body.append('&#0160;') # no-break space
776 def depart_entry(self, node):
777 self.body.append(self.context.pop())
779 def visit_enumerated_list(self, node):
780 atts = {}
781 if 'start' in node:
782 atts['start'] = node['start']
783 if 'enumtype' in node:
784 atts['class'] = node['enumtype']
785 if self.is_compactable(node):
786 atts['class'] = (atts.get('class', '') + ' simple').strip()
787 self.body.append(self.starttag(node, 'ol', **atts))
789 def depart_enumerated_list(self, node):
790 self.body.append('</ol>\n')
792 def visit_field_list(self, node):
793 # Keep simple paragraphs in the field_body to enable CSS
794 # rule to start body on new line if the label is too long
795 classes = 'field-list'
796 if (self.is_compactable(node)):
797 classes += ' simple'
798 self.body.append(self.starttag(node, 'dl', CLASS=classes))
800 def depart_field_list(self, node):
801 self.body.append('</dl>\n')
803 def visit_field(self, node):
804 pass
806 def depart_field(self, node):
807 pass
809 # as field is ignored, pass class arguments to field-name and field-body:
811 def visit_field_name(self, node):
812 self.body.append(self.starttag(node, 'dt', '',
813 CLASS=''.join(node.parent['classes'])))
815 def depart_field_name(self, node):
816 self.body.append('</dt>\n')
818 def visit_field_body(self, node):
819 self.body.append(self.starttag(node, 'dd', '',
820 CLASS=''.join(node.parent['classes'])))
821 # prevent misalignment of following content if the field is empty:
822 if not node.children:
823 self.body.append('<p></p>')
825 def depart_field_body(self, node):
826 self.body.append('</dd>\n')
828 def visit_figure(self, node):
829 atts = {'class': 'figure'}
830 if node.get('width'):
831 atts['style'] = 'width: %s' % node['width']
832 if node.get('align'):
833 atts['class'] += " align-" + node['align']
834 self.body.append(self.starttag(node, 'div', **atts))
836 def depart_figure(self, node):
837 self.body.append('</div>\n')
839 # use HTML 5 <footer> element?
840 def visit_footer(self, node):
841 self.context.append(len(self.body))
843 def depart_footer(self, node):
844 start = self.context.pop()
845 footer = [self.starttag(node, 'div', CLASS='footer'),
846 '<hr class="footer" />\n']
847 footer.extend(self.body[start:])
848 footer.append('\n</div>\n')
849 self.footer.extend(footer)
850 self.body_suffix[:0] = footer
851 del self.body[start:]
853 # footnotes
854 # ---------
855 # use definition list instead of table for footnote text
857 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
858 def visit_footnote(self, node):
859 if not self.in_footnote_list:
860 classes = 'footnote ' + self.settings.footnote_references
861 self.body.append('<dl class="%s">\n'%classes)
862 self.in_footnote_list = True
864 def depart_footnote(self, node):
865 self.body.append('</dd>\n')
866 if not isinstance(node.next_node(descend=False, siblings=True),
867 nodes.footnote):
868 self.body.append('</dl>\n')
869 self.in_footnote_list = False
871 def visit_footnote_reference(self, node):
872 href = '#' + node['refid']
873 classes = 'footnote-reference ' + self.settings.footnote_references
874 self.body.append(self.starttag(node, 'a', '', #suffix,
875 CLASS=classes, href=href))
877 def depart_footnote_reference(self, node):
878 self.body.append('</a>')
880 # Docutils-generated text: put section numbers in a span for CSS styling:
881 def visit_generated(self, node):
882 if 'sectnum' in node['classes']:
883 # get section number (strip trailing no-break-spaces)
884 sectnum = node.astext().rstrip(u' ')
885 # print sectnum.encode('utf-8')
886 self.body.append('<span class="sectnum">%s</span> '
887 % self.encode(sectnum))
888 # Content already processed:
889 raise nodes.SkipNode
891 def depart_generated(self, node):
892 pass
894 def visit_header(self, node):
895 self.context.append(len(self.body))
897 def depart_header(self, node):
898 start = self.context.pop()
899 header = [self.starttag(node, 'div', CLASS='header')]
900 header.extend(self.body[start:])
901 header.append('\n<hr class="header"/>\n</div>\n')
902 self.body_prefix.extend(header)
903 self.header.extend(header)
904 del self.body[start:]
906 # Image types to place in an <object> element
907 object_image_types = {'.swf': 'application/x-shockwave-flash'}
909 def visit_image(self, node):
910 atts = {}
911 uri = node['uri']
912 ext = os.path.splitext(uri)[1].lower()
913 if ext in self.object_image_types:
914 atts['data'] = uri
915 atts['type'] = self.object_image_types[ext]
916 else:
917 atts['src'] = uri
918 atts['alt'] = node.get('alt', uri)
919 # image size
920 if 'width' in node:
921 atts['width'] = node['width']
922 if 'height' in node:
923 atts['height'] = node['height']
924 if 'scale' in node:
925 if (PIL and not ('width' in node and 'height' in node)
926 and self.settings.file_insertion_enabled):
927 imagepath = urllib.url2pathname(uri)
928 try:
929 img = PIL.Image.open(
930 imagepath.encode(sys.getfilesystemencoding()))
931 except (IOError, UnicodeEncodeError):
932 pass # TODO: warn?
933 else:
934 self.settings.record_dependencies.add(
935 imagepath.replace('\\', '/'))
936 if 'width' not in atts:
937 atts['width'] = '%dpx' % img.size[0]
938 if 'height' not in atts:
939 atts['height'] = '%dpx' % img.size[1]
940 del img
941 for att_name in 'width', 'height':
942 if att_name in atts:
943 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
944 assert match
945 atts[att_name] = '%s%s' % (
946 float(match.group(1)) * (float(node['scale']) / 100),
947 match.group(2))
948 style = []
949 for att_name in 'width', 'height':
950 if att_name in atts:
951 if re.match(r'^[0-9.]+$', atts[att_name]):
952 # Interpret unitless values as pixels.
953 atts[att_name] += 'px'
954 style.append('%s: %s;' % (att_name, atts[att_name]))
955 del atts[att_name]
956 if style:
957 atts['style'] = ' '.join(style)
958 if (isinstance(node.parent, nodes.TextElement) or
959 (isinstance(node.parent, nodes.reference) and
960 not isinstance(node.parent.parent, nodes.TextElement))):
961 # Inline context or surrounded by <a>...</a>.
962 suffix = ''
963 else:
964 suffix = '\n'
965 if 'align' in node:
966 atts['class'] = 'align-%s' % node['align']
967 if ext in self.object_image_types:
968 # do NOT use an empty tag: incorrect rendering in browsers
969 self.body.append(self.starttag(node, 'object', suffix, **atts) +
970 node.get('alt', uri) + '</object>' + suffix)
971 else:
972 self.body.append(self.emptytag(node, 'img', suffix, **atts))
974 def depart_image(self, node):
975 # self.body.append(self.context.pop())
976 pass
978 def visit_inline(self, node):
979 self.body.append(self.starttag(node, 'span', ''))
981 def depart_inline(self, node):
982 self.body.append('</span>')
984 # footnote and citation labels:
985 def visit_label(self, node):
986 if (isinstance(node.parent, nodes.footnote)):
987 classes = self.settings.footnote_references
988 else:
989 classes = 'brackets'
990 # pass parent node to get id into starttag:
991 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
992 self.body.append(self.starttag(node, 'span', '', CLASS=classes))
993 # footnote/citation backrefs:
994 if self.settings.footnote_backlinks:
995 backrefs = node.parent['backrefs']
996 if len(backrefs) == 1:
997 self.body.append('<a class="fn-backref" href="#%s">'
998 % backrefs[0])
1000 def depart_label(self, node):
1001 if self.settings.footnote_backlinks:
1002 backrefs = node.parent['backrefs']
1003 if len(backrefs) == 1:
1004 self.body.append('</a>')
1005 self.body.append('</span>')
1006 if self.settings.footnote_backlinks and len(backrefs) > 1:
1007 backlinks = ['<a href="#%s">%s</a>' % (ref, i)
1008 for (i, ref) in enumerate(backrefs, 1)]
1009 self.body.append('<span class="fn-backref">(%s)</span>'
1010 % ','.join(backlinks))
1011 self.body.append('</dt>\n<dd>')
1013 def visit_legend(self, node):
1014 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1016 def depart_legend(self, node):
1017 self.body.append('</div>\n')
1019 def visit_line(self, node):
1020 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1021 if not len(node):
1022 self.body.append('<br />')
1024 def depart_line(self, node):
1025 self.body.append('</div>\n')
1027 def visit_line_block(self, node):
1028 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1030 def depart_line_block(self, node):
1031 self.body.append('</div>\n')
1033 def visit_list_item(self, node):
1034 self.body.append(self.starttag(node, 'li', ''))
1036 def depart_list_item(self, node):
1037 self.body.append('</li>\n')
1039 # inline literal
1040 def visit_literal(self, node):
1041 # special case: "code" role
1042 classes = node.get('classes', [])
1043 if 'code' in classes:
1044 # filter 'code' from class arguments
1045 node['classes'] = [cls for cls in classes if cls != 'code']
1046 self.body.append(self.starttag(node, 'code', ''))
1047 return
1048 self.body.append(
1049 self.starttag(node, 'span', '', CLASS='docutils literal'))
1050 text = node.astext()
1051 # remove hard line breaks (except if in a parsed-literal block)
1052 if not isinstance(node.parent, nodes.literal_block):
1053 text = text.replace('\n', ' ')
1054 # Protect text like ``--an-option`` and the regular expression
1055 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1056 for token in self.words_and_spaces.findall(text):
1057 if token.strip() and self.in_word_wrap_point.search(token):
1058 self.body.append('<span class="pre">%s</span>'
1059 % self.encode(token))
1060 else:
1061 self.body.append(self.encode(token))
1062 self.body.append('</span>')
1063 # Content already processed:
1064 raise nodes.SkipNode
1066 def depart_literal(self, node):
1067 # skipped unless literal element is from "code" role:
1068 self.body.append('</code>')
1070 def visit_literal_block(self, node):
1071 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1072 if 'code' in node.get('classes', []):
1073 self.body.append('<code>')
1075 def depart_literal_block(self, node):
1076 if 'code' in node.get('classes', []):
1077 self.body.append('</code>')
1078 self.body.append('</pre>\n')
1080 # Mathematics:
1081 # As there is no native HTML math support, we provide alternatives
1082 # for the math-output: LaTeX and MathJax simply wrap the content,
1083 # HTML and MathML also convert the math_code.
1084 # HTML container
1085 math_tags = {# math_output: (block, inline, class-arguments)
1086 'mathml': ('div', '', ''),
1087 'html': ('div', 'span', 'formula'),
1088 'mathjax': ('div', 'span', 'math'),
1089 'latex': ('pre', 'tt', 'math'),
1092 def visit_math(self, node, math_env=''):
1093 # If the method is called from visit_math_block(), math_env != ''.
1095 if self.math_output not in self.math_tags:
1096 self.document.reporter.error(
1097 'math-output format "%s" not supported '
1098 'falling back to "latex"'% self.math_output)
1099 self.math_output = 'latex'
1100 tag = self.math_tags[self.math_output][math_env == '']
1101 clsarg = self.math_tags[self.math_output][2]
1102 # LaTeX container
1103 wrappers = {# math_mode: (inline, block)
1104 'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1105 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1106 'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1107 'latex': (None, None),
1109 wrapper = wrappers[self.math_output][math_env != '']
1110 if self.math_output == 'mathml' and (not self.math_output_options or
1111 self.math_output_options[0] == 'blahtexml'):
1112 wrapper = None
1113 # get and wrap content
1114 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1115 if wrapper:
1116 try: # wrapper with three "%s"
1117 math_code = wrapper % (math_env, math_code, math_env)
1118 except TypeError: # wrapper with one "%s"
1119 math_code = wrapper % math_code
1120 # settings and conversion
1121 if self.math_output in ('latex', 'mathjax'):
1122 math_code = self.encode(math_code)
1123 if self.math_output == 'mathjax' and not self.math_header:
1124 try:
1125 self.mathjax_url = self.math_output_options[0]
1126 except IndexError:
1127 self.document.reporter.warning('No MathJax URL specified, '
1128 'using local fallback (see config.html)')
1129 # append configuration, if not already present in the URL:
1130 # input LaTeX with AMS, output common HTML
1131 if '?' not in self.mathjax_url:
1132 self.mathjax_url += '?config=TeX-AMS_CHTML'
1133 self.math_header = [self.mathjax_script % self.mathjax_url]
1134 elif self.math_output == 'html':
1135 if self.math_output_options and not self.math_header:
1136 self.math_header = [self.stylesheet_call(
1137 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1138 for s in self.math_output_options[0].split(',')]
1139 # TODO: fix display mode in matrices and fractions
1140 math2html.DocumentParameters.displaymode = (math_env != '')
1141 math_code = math2html.math2html(math_code)
1142 elif self.math_output == 'mathml':
1143 if 'XHTML 1' in self.doctype:
1144 self.doctype = self.doctype_mathml
1145 self.content_type = self.content_type_mathml
1146 converter = ' '.join(self.math_output_options).lower()
1147 try:
1148 if converter == 'latexml':
1149 math_code = tex2mathml_extern.latexml(math_code,
1150 self.document.reporter)
1151 elif converter == 'ttm':
1152 math_code = tex2mathml_extern.ttm(math_code,
1153 self.document.reporter)
1154 elif converter == 'blahtexml':
1155 math_code = tex2mathml_extern.blahtexml(math_code,
1156 inline=not(math_env),
1157 reporter=self.document.reporter)
1158 elif not converter:
1159 math_code = latex2mathml.tex2mathml(math_code,
1160 inline=not(math_env))
1161 else:
1162 self.document.reporter.error('option "%s" not supported '
1163 'with math-output "MathML"')
1164 except OSError:
1165 raise OSError('is "latexmlmath" in your PATH?')
1166 except SyntaxError, err:
1167 err_node = self.document.reporter.error(err, base_node=node)
1168 self.visit_system_message(err_node)
1169 self.body.append(self.starttag(node, 'p'))
1170 self.body.append(u','.join(err.args))
1171 self.body.append('</p>\n')
1172 self.body.append(self.starttag(node, 'pre',
1173 CLASS='literal-block'))
1174 self.body.append(self.encode(math_code))
1175 self.body.append('\n</pre>\n')
1176 self.depart_system_message(err_node)
1177 raise nodes.SkipNode
1178 # append to document body
1179 if tag:
1180 self.body.append(self.starttag(node, tag,
1181 suffix='\n'*bool(math_env),
1182 CLASS=clsarg))
1183 self.body.append(math_code)
1184 if math_env: # block mode (equation, display)
1185 self.body.append('\n')
1186 if tag:
1187 self.body.append('</%s>' % tag)
1188 if math_env:
1189 self.body.append('\n')
1190 # Content already processed:
1191 raise nodes.SkipNode
1193 def depart_math(self, node):
1194 pass # never reached
1196 def visit_math_block(self, node):
1197 # print node.astext().encode('utf8')
1198 math_env = pick_math_environment(node.astext())
1199 self.visit_math(node, math_env=math_env)
1201 def depart_math_block(self, node):
1202 pass # never reached
1204 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1205 # HTML5/polyglot recommends using both
1206 def visit_meta(self, node):
1207 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1208 self.add_meta(meta)
1210 def depart_meta(self, node):
1211 pass
1213 def add_meta(self, tag):
1214 self.meta.append(tag)
1215 self.head.append(tag)
1217 def visit_option(self, node):
1218 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1220 def depart_option(self, node):
1221 self.body.append('</span>')
1222 if isinstance(node.next_node(descend=False, siblings=True),
1223 nodes.option):
1224 self.body.append(', ')
1226 def visit_option_argument(self, node):
1227 self.body.append(node.get('delimiter', ' '))
1228 self.body.append(self.starttag(node, 'var', ''))
1230 def depart_option_argument(self, node):
1231 self.body.append('</var>')
1233 def visit_option_group(self, node):
1234 self.body.append(self.starttag(node, 'dt', ''))
1235 self.body.append('<kbd>')
1237 def depart_option_group(self, node):
1238 self.body.append('</kbd></dt>\n')
1240 def visit_option_list(self, node):
1241 self.body.append(
1242 self.starttag(node, 'dl', CLASS='option-list'))
1244 def depart_option_list(self, node):
1245 self.body.append('</dl>\n')
1247 def visit_option_list_item(self, node):
1248 pass
1250 def depart_option_list_item(self, node):
1251 pass
1253 def visit_option_string(self, node):
1254 pass
1256 def depart_option_string(self, node):
1257 pass
1259 def visit_organization(self, node):
1260 self.visit_docinfo_item(node, 'organization')
1262 def depart_organization(self, node):
1263 self.depart_docinfo_item()
1265 # Do not omit <p> tags
1266 # --------------------
1268 # The HTML4CSS1 writer does this to "produce
1269 # visually compact lists (less vertical whitespace)". This writer
1270 # relies on CSS rules for"visual compactness".
1272 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1273 # character data, so you cannot drop the <p> tags.
1274 # * Keeping simple paragraphs in the field_body enables a CSS
1275 # rule to start the field-body on a new line if the label is too long
1276 # * it makes the code simpler.
1278 # TODO: omit paragraph tags in simple table cells?
1280 def visit_paragraph(self, node):
1281 self.body.append(self.starttag(node, 'p', ''))
1283 def depart_paragraph(self, node):
1284 self.body.append('</p>')
1285 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1286 (len(node.parent) == 1)):
1287 self.body.append('\n')
1289 def visit_problematic(self, node):
1290 if node.hasattr('refid'):
1291 self.body.append('<a href="#%s">' % node['refid'])
1292 self.context.append('</a>')
1293 else:
1294 self.context.append('')
1295 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1297 def depart_problematic(self, node):
1298 self.body.append('</span>')
1299 self.body.append(self.context.pop())
1301 def visit_raw(self, node):
1302 if 'html' in node.get('format', '').split():
1303 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1304 if node['classes']:
1305 self.body.append(self.starttag(node, t, suffix=''))
1306 self.body.append(node.astext())
1307 if node['classes']:
1308 self.body.append('</%s>' % t)
1309 # Keep non-HTML raw text out of output:
1310 raise nodes.SkipNode
1312 def visit_reference(self, node):
1313 atts = {'class': 'reference'}
1314 if 'refuri' in node:
1315 atts['href'] = node['refuri']
1316 if ( self.settings.cloak_email_addresses
1317 and atts['href'].startswith('mailto:')):
1318 atts['href'] = self.cloak_mailto(atts['href'])
1319 self.in_mailto = True
1320 atts['class'] += ' external'
1321 else:
1322 assert 'refid' in node, \
1323 'References must have "refuri" or "refid" attribute.'
1324 atts['href'] = '#' + node['refid']
1325 atts['class'] += ' internal'
1326 if not isinstance(node.parent, nodes.TextElement):
1327 assert len(node) == 1 and isinstance(node[0], nodes.image)
1328 atts['class'] += ' image-reference'
1329 self.body.append(self.starttag(node, 'a', '', **atts))
1331 def depart_reference(self, node):
1332 self.body.append('</a>')
1333 if not isinstance(node.parent, nodes.TextElement):
1334 self.body.append('\n')
1335 self.in_mailto = False
1337 def visit_revision(self, node):
1338 self.visit_docinfo_item(node, 'revision', meta=False)
1340 def depart_revision(self, node):
1341 self.depart_docinfo_item()
1343 def visit_row(self, node):
1344 self.body.append(self.starttag(node, 'tr', ''))
1345 node.column = 0
1347 def depart_row(self, node):
1348 self.body.append('</tr>\n')
1350 def visit_rubric(self, node):
1351 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1353 def depart_rubric(self, node):
1354 self.body.append('</p>\n')
1356 # TODO: use the new HTML 5 element <section>?
1357 def visit_section(self, node):
1358 self.section_level += 1
1359 self.body.append(
1360 self.starttag(node, 'div', CLASS='section'))
1362 def depart_section(self, node):
1363 self.section_level -= 1
1364 self.body.append('</div>\n')
1366 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1367 def visit_sidebar(self, node):
1368 self.body.append(
1369 self.starttag(node, 'div', CLASS='sidebar'))
1370 self.in_sidebar = True
1372 def depart_sidebar(self, node):
1373 self.body.append('</div>\n')
1374 self.in_sidebar = False
1376 def visit_status(self, node):
1377 self.visit_docinfo_item(node, 'status', meta=False)
1379 def depart_status(self, node):
1380 self.depart_docinfo_item()
1382 def visit_strong(self, node):
1383 self.body.append(self.starttag(node, 'strong', ''))
1385 def depart_strong(self, node):
1386 self.body.append('</strong>')
1388 def visit_subscript(self, node):
1389 self.body.append(self.starttag(node, 'sub', ''))
1391 def depart_subscript(self, node):
1392 self.body.append('</sub>')
1394 def visit_substitution_definition(self, node):
1395 """Internal only."""
1396 raise nodes.SkipNode
1398 def visit_substitution_reference(self, node):
1399 self.unimplemented_visit(node)
1401 # h1–h6 elements must not be used to markup subheadings, subtitles,
1402 # alternative titles and taglines unless intended to be the heading for a
1403 # new section or subsection.
1404 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1405 def visit_subtitle(self, node):
1406 if isinstance(node.parent, nodes.sidebar):
1407 classes = 'sidebar-subtitle'
1408 elif isinstance(node.parent, nodes.document):
1409 classes = 'subtitle'
1410 self.in_document_title = len(self.body)+1
1411 elif isinstance(node.parent, nodes.section):
1412 classes = 'section-subtitle'
1413 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1415 def depart_subtitle(self, node):
1416 self.body.append('</p>\n')
1417 if isinstance(node.parent, nodes.document):
1418 self.subtitle = self.body[self.in_document_title:-1]
1419 self.in_document_title = 0
1420 self.body_pre_docinfo.extend(self.body)
1421 self.html_subtitle.extend(self.body)
1422 del self.body[:]
1424 def visit_superscript(self, node):
1425 self.body.append(self.starttag(node, 'sup', ''))
1427 def depart_superscript(self, node):
1428 self.body.append('</sup>')
1430 def visit_system_message(self, node):
1431 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1432 self.body.append('<p class="system-message-title">')
1433 backref_text = ''
1434 if len(node['backrefs']):
1435 backrefs = node['backrefs']
1436 if len(backrefs) == 1:
1437 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1438 % backrefs[0])
1439 else:
1440 i = 1
1441 backlinks = []
1442 for backref in backrefs:
1443 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1444 i += 1
1445 backref_text = ('; <em>backlinks: %s</em>'
1446 % ', '.join(backlinks))
1447 if node.hasattr('line'):
1448 line = ', line %s' % node['line']
1449 else:
1450 line = ''
1451 self.body.append('System Message: %s/%s '
1452 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1453 % (node['type'], node['level'],
1454 self.encode(node['source']), line, backref_text))
1456 def depart_system_message(self, node):
1457 self.body.append('</div>\n')
1459 def visit_table(self, node):
1460 atts = {}
1461 classes = [cls.strip(u' \t\n')
1462 for cls in self.settings.table_style.split(',')]
1463 if 'align' in node:
1464 classes.append('align-%s' % node['align'])
1465 if 'width' in node:
1466 atts['style'] = 'width: %s' % node['width']
1467 tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts)
1468 self.body.append(tag)
1470 def depart_table(self, node):
1471 self.body.append('</table>\n')
1473 def visit_target(self, node):
1474 if not ('refuri' in node or 'refid' in node
1475 or 'refname' in node):
1476 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1477 self.context.append('</span>')
1478 else:
1479 self.context.append('')
1481 def depart_target(self, node):
1482 self.body.append(self.context.pop())
1484 # no hard-coded vertical alignment in table body
1485 def visit_tbody(self, node):
1486 self.body.append(self.starttag(node, 'tbody'))
1488 def depart_tbody(self, node):
1489 self.body.append('</tbody>\n')
1491 def visit_term(self, node):
1492 self.body.append(self.starttag(node, 'dt', ''))
1494 def depart_term(self, node):
1496 Leave the end tag to `self.visit_definition()`, in case there's a
1497 classifier.
1499 pass
1501 def visit_tgroup(self, node):
1502 self.colspecs = []
1503 node.stubs = []
1505 def depart_tgroup(self, node):
1506 pass
1508 def visit_thead(self, node):
1509 self.body.append(self.starttag(node, 'thead'))
1511 def depart_thead(self, node):
1512 self.body.append('</thead>\n')
1514 def visit_title(self, node):
1515 """Only 6 section levels are supported by HTML."""
1516 check_id = 0 # TODO: is this a bool (False) or a counter?
1517 close_tag = '</p>\n'
1518 if isinstance(node.parent, nodes.topic):
1519 self.body.append(
1520 self.starttag(node, 'p', '', CLASS='topic-title first'))
1521 elif isinstance(node.parent, nodes.sidebar):
1522 self.body.append(
1523 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1524 elif isinstance(node.parent, nodes.Admonition):
1525 self.body.append(
1526 self.starttag(node, 'p', '', CLASS='admonition-title'))
1527 elif isinstance(node.parent, nodes.table):
1528 self.body.append(
1529 self.starttag(node, 'caption', ''))
1530 close_tag = '</caption>\n'
1531 elif isinstance(node.parent, nodes.document):
1532 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1533 close_tag = '</h1>\n'
1534 self.in_document_title = len(self.body)
1535 else:
1536 assert isinstance(node.parent, nodes.section)
1537 h_level = self.section_level + self.initial_header_level - 1
1538 atts = {}
1539 if (len(node.parent) >= 2 and
1540 isinstance(node.parent[1], nodes.subtitle)):
1541 atts['CLASS'] = 'with-subtitle'
1542 self.body.append(
1543 self.starttag(node, 'h%s' % h_level, '', **atts))
1544 atts = {}
1545 if node.hasattr('refid'):
1546 atts['class'] = 'toc-backref'
1547 atts['href'] = '#' + node['refid']
1548 if atts:
1549 self.body.append(self.starttag({}, 'a', '', **atts))
1550 close_tag = '</a></h%s>\n' % (h_level)
1551 else:
1552 close_tag = '</h%s>\n' % (h_level)
1553 self.context.append(close_tag)
1555 def depart_title(self, node):
1556 self.body.append(self.context.pop())
1557 if self.in_document_title:
1558 self.title = self.body[self.in_document_title:-1]
1559 self.in_document_title = 0
1560 self.body_pre_docinfo.extend(self.body)
1561 self.html_title.extend(self.body)
1562 del self.body[:]
1564 def visit_title_reference(self, node):
1565 self.body.append(self.starttag(node, 'cite', ''))
1567 def depart_title_reference(self, node):
1568 self.body.append('</cite>')
1570 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1571 def visit_topic(self, node):
1572 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1573 self.topic_classes = node['classes']
1574 # TODO: replace with ::
1575 # self.in_contents = 'contents' in node['classes']
1577 def depart_topic(self, node):
1578 self.body.append('</div>\n')
1579 self.topic_classes = []
1580 # TODO self.in_contents = False
1582 def visit_transition(self, node):
1583 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1585 def depart_transition(self, node):
1586 pass
1588 def visit_version(self, node):
1589 self.visit_docinfo_item(node, 'version', meta=False)
1591 def depart_version(self, node):
1592 self.depart_docinfo_item()
1594 def unimplemented_visit(self, node):
1595 raise NotImplementedError('visiting unimplemented node type: %s'
1596 % node.__class__.__name__)
1599 class SimpleListChecker(nodes.GenericNodeVisitor):
1602 Raise `nodes.NodeFound` if non-simple list item is encountered.
1604 Here "simple" means a list item containing nothing other than a single
1605 paragraph, a simple list, or a paragraph followed by a simple list.
1607 This version also checks for simple field lists and docinfo.
1610 def default_visit(self, node):
1611 raise nodes.NodeFound
1613 def visit_list_item(self, node):
1614 # print "visiting list item", node.__class__
1615 children = [child for child in node.children
1616 if not isinstance(child, nodes.Invisible)]
1617 # print "has %s visible children" % len(children)
1618 if (children and isinstance(children[0], nodes.paragraph)
1619 and (isinstance(children[-1], nodes.bullet_list) or
1620 isinstance(children[-1], nodes.enumerated_list) or
1621 isinstance(children[-1], nodes.field_list))):
1622 children.pop()
1623 # print "%s children remain" % len(children)
1624 if len(children) <= 1:
1625 return
1626 else:
1627 # print "found", child.__class__, "in", node.__class__
1628 raise nodes.NodeFound
1630 def pass_node(self, node):
1631 pass
1633 def ignore_node(self, node):
1634 # ignore nodes that are never complex (can contain only inline nodes)
1635 raise nodes.SkipNode
1637 # Paragraphs and text
1638 visit_Text = ignore_node
1639 visit_paragraph = ignore_node
1641 # Lists
1642 visit_bullet_list = pass_node
1643 visit_enumerated_list = pass_node
1644 visit_docinfo = pass_node
1646 # Docinfo nodes:
1647 visit_author = ignore_node
1648 visit_authors = visit_list_item
1649 visit_address = visit_list_item
1650 visit_contact = pass_node
1651 visit_copyright = ignore_node
1652 visit_date = ignore_node
1653 visit_organization = ignore_node
1654 visit_status = ignore_node
1655 visit_version = visit_list_item
1657 # Definition list:
1658 visit_definition_list = pass_node
1659 visit_definition_list_item = pass_node
1660 visit_term = ignore_node
1661 visit_classifier = pass_node
1662 visit_definition = visit_list_item
1664 # Field list:
1665 visit_field_list = pass_node
1666 visit_field = pass_node
1667 # the field body corresponds to a list item
1668 visit_field_body = visit_list_item
1669 visit_field_name = ignore_node
1671 # Invisible nodes should be ignored.
1672 visit_comment = ignore_node
1673 visit_substitution_definition = ignore_node
1674 visit_target = ignore_node
1675 visit_pending = ignore_node