Implement feature request #40 Option to embed images as data URI.
[docutils.git] / docutils / docutils / writers / _html_base.py
blobc7ff96e81381f1576374259d9439b2059883eeb6
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: https://opensource.org/licenses/BSD-2-Clause
18 """common definitions for Docutils HTML writers"""
20 import base64
21 import mimetypes
22 import os, os.path
23 import re
24 import sys
26 try: # check for the Python Imaging Library
27 import PIL.Image
28 except ImportError:
29 try: # sometimes PIL modules are put in PYTHONPATH's root
30 import Image
31 class PIL(object): pass # dummy wrapper
32 PIL.Image = Image
33 except ImportError:
34 PIL = None
36 import docutils
37 from docutils import nodes, utils, writers, languages, io
38 from docutils.utils.error_reporting import SafeString
39 from docutils.transforms import writer_aux
40 from docutils.utils.math import (unichar2tex, pick_math_environment,
41 math2html, latex2mathml, tex2mathml_extern)
43 if sys.version_info >= (3, 0):
44 from urllib.request import url2pathname
45 else:
46 from urllib import url2pathname
48 if sys.version_info >= (3, 0):
49 unicode = str # noqa
52 class Writer(writers.Writer):
54 supported = ('html', 'xhtml') # update in subclass
55 """Formats this writer supports."""
57 # default_stylesheets = [] # set in subclass!
58 # default_stylesheet_dirs = ['.'] # set in subclass!
59 default_template = 'template.txt'
60 # default_template_path = ... # set in subclass!
61 # settings_spec = ... # set in subclass!
63 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
65 # config_section = ... # set in subclass!
66 config_section_dependencies = ('writers', 'html writers')
68 visitor_attributes = (
69 'head_prefix', 'head', 'stylesheet', 'body_prefix',
70 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
71 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
72 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
73 'html_body')
75 def get_transforms(self):
76 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
78 def translate(self):
79 self.visitor = visitor = self.translator_class(self.document)
80 self.document.walkabout(visitor)
81 for attr in self.visitor_attributes:
82 setattr(self, attr, getattr(visitor, attr))
83 self.output = self.apply_template()
85 def apply_template(self):
86 template_file = open(self.document.settings.template, 'rb')
87 template = unicode(template_file.read(), 'utf-8')
88 template_file.close()
89 subs = self.interpolation_dict()
90 return template % subs
92 def interpolation_dict(self):
93 subs = {}
94 settings = self.document.settings
95 for attr in self.visitor_attributes:
96 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
97 subs['encoding'] = settings.output_encoding
98 subs['version'] = docutils.__version__
99 return subs
101 def assemble_parts(self):
102 writers.Writer.assemble_parts(self)
103 for part in self.visitor_attributes:
104 self.parts[part] = ''.join(getattr(self, part))
107 class HTMLTranslator(nodes.NodeVisitor):
110 Generic Docutils to HTML translator.
112 See the `html4css1` and `html5_polyglot` writers for full featured
113 HTML writers.
115 .. IMPORTANT::
116 The `visit_*` and `depart_*` methods use a
117 heterogeneous stack, `self.context`.
118 When subclassing, make sure to be consistent in its use!
120 Examples for robust coding:
122 a) Override both `visit_*` and `depart_*` methods, don't call the
123 parent functions.
125 b) Extend both and unconditionally call the parent functions::
127 def visit_example(self, node):
128 if foo:
129 self.body.append('<div class="foo">')
130 html4css1.HTMLTranslator.visit_example(self, node)
132 def depart_example(self, node):
133 html4css1.HTMLTranslator.depart_example(self, node)
134 if foo:
135 self.body.append('</div>')
137 c) Extend both, calling the parent functions under the same
138 conditions::
140 def visit_example(self, node):
141 if foo:
142 self.body.append('<div class="foo">\n')
143 else: # call the parent method
144 _html_base.HTMLTranslator.visit_example(self, node)
146 def depart_example(self, node):
147 if foo:
148 self.body.append('</div>\n')
149 else: # call the parent method
150 _html_base.HTMLTranslator.depart_example(self, node)
152 d) Extend one method (call the parent), but don't otherwise use the
153 `self.context` stack::
155 def depart_example(self, node):
156 _html_base.HTMLTranslator.depart_example(self, node)
157 if foo:
158 # implementation-specific code
159 # that does not use `self.context`
160 self.body.append('</div>\n')
162 This way, changes in stack use will not bite you.
165 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
166 doctype = '<!DOCTYPE html>\n'
167 doctype_mathml = doctype
169 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
170 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
171 content_type = ('<meta charset="%s"/>\n')
172 generator = ('<meta name="generator" content="Docutils %s: '
173 'http://docutils.sourceforge.net/" />\n')
175 # Template for the MathJax script in the header:
176 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
178 mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
180 URL of the MathJax javascript library.
182 The MathJax library ought to be installed on the same
183 server as the rest of the deployed site files and specified
184 in the `math-output` setting appended to "mathjax".
185 See `Docutils Configuration`__.
187 __ http://docutils.sourceforge.net/docs/user/config.html#math-output
189 The fallback tries a local MathJax installation at
190 ``/usr/share/javascript/mathjax/MathJax.js``.
193 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
194 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
195 words_and_spaces = re.compile(r'[^ \n]+| +|\n')
196 # wrap point inside word:
197 in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+', re.U)
198 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
200 special_characters = {ord('&'): u'&amp;',
201 ord('<'): u'&lt;',
202 ord('"'): u'&quot;',
203 ord('>'): u'&gt;',
204 ord('@'): u'&#64;', # may thwart address harvesters
206 """Character references for characters with a special meaning in HTML."""
209 def __init__(self, document):
210 nodes.NodeVisitor.__init__(self, document)
211 self.settings = settings = document.settings
212 lcode = settings.language_code
213 self.language = languages.get_language(lcode, document.reporter)
214 self.meta = [self.generator % docutils.__version__]
215 self.head_prefix = []
216 self.html_prolog = []
217 if settings.xml_declaration:
218 self.head_prefix.append(self.xml_declaration
219 % settings.output_encoding)
220 # self.content_type = ""
221 # encoding not interpolated:
222 self.html_prolog.append(self.xml_declaration)
223 self.head = self.meta[:]
224 self.stylesheet = [self.stylesheet_call(path)
225 for path in utils.get_stylesheet_list(settings)]
226 self.body_prefix = ['</head>\n<body>\n']
227 # document title, subtitle display
228 self.body_pre_docinfo = []
229 # author, date, etc.
230 self.docinfo = []
231 self.body = []
232 self.fragment = []
233 self.body_suffix = ['</body>\n</html>\n']
234 self.section_level = 0
235 self.initial_header_level = int(settings.initial_header_level)
237 self.math_output = settings.math_output.split()
238 self.math_output_options = self.math_output[1:]
239 self.math_output = self.math_output[0].lower()
241 self.context = []
242 """Heterogeneous stack.
244 Used by visit_* and depart_* functions in conjunction with the tree
245 traversal. Make sure that the pops correspond to the pushes."""
247 self.topic_classes = [] # TODO: replace with self_in_contents
248 self.colspecs = []
249 self.compact_p = True
250 self.compact_simple = False
251 self.compact_field_list = False
252 self.in_docinfo = False
253 self.in_sidebar = False
254 self.in_footnote_list = False
255 self.title = []
256 self.subtitle = []
257 self.header = []
258 self.footer = []
259 self.html_head = [self.content_type] # charset not interpolated
260 self.html_title = []
261 self.html_subtitle = []
262 self.html_body = []
263 self.in_document_title = 0 # len(self.body) or 0
264 self.in_mailto = False
265 self.author_in_authors = False # for html4css1
266 self.math_header = []
268 def astext(self):
269 return ''.join(self.head_prefix + self.head
270 + self.stylesheet + self.body_prefix
271 + self.body_pre_docinfo + self.docinfo
272 + self.body + self.body_suffix)
274 def encode(self, text):
275 """Encode special characters in `text` & return."""
276 # Use only named entities known in both XML and HTML
277 # other characters are automatically encoded "by number" if required.
278 # @@@ A codec to do these and all other HTML entities would be nice.
279 text = unicode(text)
280 return text.translate(self.special_characters)
282 def cloak_mailto(self, uri):
283 """Try to hide a mailto: URL from harvesters."""
284 # Encode "@" using a URL octet reference (see RFC 1738).
285 # Further cloaking with HTML entities will be done in the
286 # `attval` function.
287 return uri.replace('@', '%40')
289 def cloak_email(self, addr):
290 """Try to hide the link text of a email link from harversters."""
291 # Surround at-signs and periods with <span> tags. ("@" has
292 # already been encoded to "&#64;" by the `encode` method.)
293 addr = addr.replace('&#64;', '<span>&#64;</span>')
294 addr = addr.replace('.', '<span>&#46;</span>')
295 return addr
297 def attval(self, text,
298 whitespace=re.compile('[\n\r\t\v\f]')):
299 """Cleanse, HTML encode, and return attribute value text."""
300 encoded = self.encode(whitespace.sub(' ', text))
301 if self.in_mailto and self.settings.cloak_email_addresses:
302 # Cloak at-signs ("%40") and periods with HTML entities.
303 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
304 encoded = encoded.replace('.', '&#46;')
305 return encoded
307 def stylesheet_call(self, path):
308 """Return code to reference or embed stylesheet file `path`"""
309 if self.settings.embed_stylesheet:
310 try:
311 content = io.FileInput(source_path=path,
312 encoding='utf-8').read()
313 self.settings.record_dependencies.add(path)
314 except IOError as err:
315 msg = u"Cannot embed stylesheet '%s': %s." % (
316 path, SafeString(err.strerror))
317 self.document.reporter.error(msg)
318 return '<--- %s --->\n' % msg
319 return self.embedded_stylesheet % content
320 # else link to style file:
321 if self.settings.stylesheet_path:
322 # adapt path relative to output (cf. config.html#stylesheet-path)
323 path = utils.relative_path(self.settings._destination, path)
324 return self.stylesheet_link % self.encode(path)
326 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
328 Construct and return a start tag given a node (id & class attributes
329 are extracted), tag name, and optional attributes.
331 tagname = tagname.lower()
332 prefix = []
333 atts = {}
334 ids = []
335 for (name, value) in attributes.items():
336 atts[name.lower()] = value
337 classes = []
338 languages = []
339 # unify class arguments and move language specification
340 for cls in node.get('classes', []) + atts.pop('class', '').split():
341 if cls.startswith('language-'):
342 languages.append(cls[9:])
343 elif cls.strip() and cls not in classes:
344 classes.append(cls)
345 if languages:
346 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
347 atts[self.lang_attribute] = languages[0]
348 if classes:
349 atts['class'] = ' '.join(classes)
350 assert 'id' not in atts
351 ids.extend(node.get('ids', []))
352 if 'ids' in atts:
353 ids.extend(atts['ids'])
354 del atts['ids']
355 if ids:
356 atts['id'] = ids[0]
357 for id in ids[1:]:
358 # Add empty "span" elements for additional IDs. Note
359 # that we cannot use empty "a" elements because there
360 # may be targets inside of references, but nested "a"
361 # elements aren't allowed in XHTML (even if they do
362 # not all have a "href" attribute).
363 if empty or isinstance(node,
364 (nodes.bullet_list, nodes.docinfo,
365 nodes.definition_list, nodes.enumerated_list,
366 nodes.field_list, nodes.option_list,
367 nodes.table)):
368 # Insert target right in front of element.
369 prefix.append('<span id="%s"></span>' % id)
370 else:
371 # Non-empty tag. Place the auxiliary <span> tag
372 # *inside* the element, as the first child.
373 suffix += '<span id="%s"></span>' % id
374 attlist = sorted(atts.items())
375 parts = [tagname]
376 for name, value in attlist:
377 # value=None was used for boolean attributes without
378 # value, but this isn't supported by XHTML.
379 assert value is not None
380 if isinstance(value, list):
381 values = [unicode(v) for v in value]
382 parts.append('%s="%s"' % (name.lower(),
383 self.attval(' '.join(values))))
384 else:
385 parts.append('%s="%s"' % (name.lower(),
386 self.attval(unicode(value))))
387 if empty:
388 infix = ' /'
389 else:
390 infix = ''
391 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
393 def emptytag(self, node, tagname, suffix='\n', **attributes):
394 """Construct and return an XML-compatible empty tag."""
395 return self.starttag(node, tagname, suffix, empty=True, **attributes)
397 def set_class_on_child(self, node, class_, index=0):
399 Set class `class_` on the visible child no. index of `node`.
400 Do nothing if node has fewer children than `index`.
402 children = [n for n in node if not isinstance(n, nodes.Invisible)]
403 try:
404 child = children[index]
405 except IndexError:
406 return
407 child['classes'].append(class_)
409 def visit_Text(self, node):
410 text = node.astext()
411 encoded = self.encode(text)
412 if self.in_mailto and self.settings.cloak_email_addresses:
413 encoded = self.cloak_email(encoded)
414 self.body.append(encoded)
416 def depart_Text(self, node):
417 pass
419 def visit_abbreviation(self, node):
420 # @@@ implementation incomplete ("title" attribute)
421 self.body.append(self.starttag(node, 'abbr', ''))
423 def depart_abbreviation(self, node):
424 self.body.append('</abbr>')
426 def visit_acronym(self, node):
427 # @@@ implementation incomplete ("title" attribute)
428 self.body.append(self.starttag(node, 'acronym', ''))
430 def depart_acronym(self, node):
431 self.body.append('</acronym>')
433 def visit_address(self, node):
434 self.visit_docinfo_item(node, 'address', meta=False)
435 self.body.append(self.starttag(node, 'pre',
436 suffix= '', CLASS='address'))
438 def depart_address(self, node):
439 self.body.append('\n</pre>\n')
440 self.depart_docinfo_item()
442 def visit_admonition(self, node):
443 node['classes'].insert(0, 'admonition')
444 self.body.append(self.starttag(node, 'div'))
446 def depart_admonition(self, node=None):
447 self.body.append('</div>\n')
449 attribution_formats = {'dash': (u'\u2014', ''),
450 'parentheses': ('(', ')'),
451 'parens': ('(', ')'),
452 'none': ('', '')}
454 def visit_attribution(self, node):
455 prefix, suffix = self.attribution_formats[self.settings.attribution]
456 self.context.append(suffix)
457 self.body.append(
458 self.starttag(node, 'p', prefix, CLASS='attribution'))
460 def depart_attribution(self, node):
461 self.body.append(self.context.pop() + '</p>\n')
463 def visit_author(self, node):
464 if not(isinstance(node.parent, nodes.authors)):
465 self.visit_docinfo_item(node, 'author')
466 self.body.append('<p>')
468 def depart_author(self, node):
469 self.body.append('</p>')
470 if isinstance(node.parent, nodes.authors):
471 self.body.append('\n')
472 else:
473 self.depart_docinfo_item()
475 def visit_authors(self, node):
476 self.visit_docinfo_item(node, 'authors')
478 def depart_authors(self, node):
479 self.depart_docinfo_item()
481 def visit_block_quote(self, node):
482 self.body.append(self.starttag(node, 'blockquote'))
484 def depart_block_quote(self, node):
485 self.body.append('</blockquote>\n')
487 def check_simple_list(self, node):
488 """Check for a simple list that can be rendered compactly."""
489 visitor = SimpleListChecker(self.document)
490 try:
491 node.walk(visitor)
492 except nodes.NodeFound:
493 return False
494 else:
495 return True
497 # Compact lists
498 # ------------
499 # Include definition lists and field lists (in addition to ordered
500 # and unordered lists) in the test if a list is "simple" (cf. the
501 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
502 # the end of this file).
504 def is_compactable(self, node):
505 # explicite class arguments have precedence
506 if 'compact' in node['classes']:
507 return True
508 if 'open' in node['classes']:
509 return False
510 # check config setting:
511 if (isinstance(node, (nodes.field_list, nodes.definition_list))
512 and not self.settings.compact_field_lists):
513 return False
514 if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
515 and not self.settings.compact_lists):
516 return False
517 # more special cases:
518 if (self.topic_classes == ['contents']): # TODO: self.in_contents
519 return True
520 # check the list items:
521 return self.check_simple_list(node)
523 def visit_bullet_list(self, node):
524 atts = {}
525 old_compact_simple = self.compact_simple
526 self.context.append((self.compact_simple, self.compact_p))
527 self.compact_p = None
528 self.compact_simple = self.is_compactable(node)
529 if self.compact_simple and not old_compact_simple:
530 atts['class'] = 'simple'
531 self.body.append(self.starttag(node, 'ul', **atts))
533 def depart_bullet_list(self, node):
534 self.compact_simple, self.compact_p = self.context.pop()
535 self.body.append('</ul>\n')
537 def visit_caption(self, node):
538 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
540 def depart_caption(self, node):
541 self.body.append('</p>\n')
543 # citations
544 # ---------
545 # Use definition list instead of table for bibliographic references.
546 # Join adjacent citation entries.
548 def visit_citation(self, node):
549 if not self.in_footnote_list:
550 self.body.append('<dl class="citation">\n')
551 self.in_footnote_list = True
553 def depart_citation(self, node):
554 self.body.append('</dd>\n')
555 if not isinstance(node.next_node(descend=False, siblings=True),
556 nodes.citation):
557 self.body.append('</dl>\n')
558 self.in_footnote_list = False
560 def visit_citation_reference(self, node):
561 href = '#'
562 if 'refid' in node:
563 href += node['refid']
564 elif 'refname' in node:
565 href += self.document.nameids[node['refname']]
566 # else: # TODO system message (or already in the transform)?
567 # 'Citation reference missing.'
568 self.body.append(self.starttag(
569 node, 'a', '[', CLASS='citation-reference', href=href))
571 def depart_citation_reference(self, node):
572 self.body.append(']</a>')
574 # classifier
575 # ----------
576 # don't insert classifier-delimiter here (done by CSS)
578 def visit_classifier(self, node):
579 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
581 def depart_classifier(self, node):
582 self.body.append('</span>')
584 def visit_colspec(self, node):
585 self.colspecs.append(node)
586 # "stubs" list is an attribute of the tgroup element:
587 node.parent.stubs.append(node.attributes.get('stub'))
589 def depart_colspec(self, node):
590 # write out <colgroup> when all colspecs are processed
591 if isinstance(node.next_node(descend=False, siblings=True),
592 nodes.colspec):
593 return
594 if 'colwidths-auto' in node.parent.parent['classes'] or (
595 'colwidths-auto' in self.settings.table_style and
596 ('colwidths-given' not in node.parent.parent['classes'])):
597 return
598 total_width = sum(node['colwidth'] for node in self.colspecs)
599 self.body.append(self.starttag(node, 'colgroup'))
600 for node in self.colspecs:
601 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
602 self.body.append(self.emptytag(node, 'col',
603 style='width: %i%%' % colwidth))
604 self.body.append('</colgroup>\n')
606 def visit_comment(self, node,
607 sub=re.compile('-(?=-)').sub):
608 """Escape double-dashes in comment text."""
609 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
610 # Content already processed:
611 raise nodes.SkipNode
613 def visit_compound(self, node):
614 self.body.append(self.starttag(node, 'div', CLASS='compound'))
615 if len(node) > 1:
616 node[0]['classes'].append('compound-first')
617 node[-1]['classes'].append('compound-last')
618 for child in node[1:-1]:
619 child['classes'].append('compound-middle')
621 def depart_compound(self, node):
622 self.body.append('</div>\n')
624 def visit_container(self, node):
625 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
627 def depart_container(self, node):
628 self.body.append('</div>\n')
630 def visit_contact(self, node):
631 self.visit_docinfo_item(node, 'contact', meta=False)
633 def depart_contact(self, node):
634 self.depart_docinfo_item()
636 def visit_copyright(self, node):
637 self.visit_docinfo_item(node, 'copyright')
639 def depart_copyright(self, node):
640 self.depart_docinfo_item()
642 def visit_date(self, node):
643 self.visit_docinfo_item(node, 'date')
645 def depart_date(self, node):
646 self.depart_docinfo_item()
648 def visit_decoration(self, node):
649 pass
651 def depart_decoration(self, node):
652 pass
654 def visit_definition(self, node):
655 self.body.append('</dt>\n')
656 self.body.append(self.starttag(node, 'dd', ''))
658 def depart_definition(self, node):
659 self.body.append('</dd>\n')
661 def visit_definition_list(self, node):
662 classes = node.setdefault('classes', [])
663 if self.is_compactable(node):
664 classes.append('simple')
665 self.body.append(self.starttag(node, 'dl'))
667 def depart_definition_list(self, node):
668 self.body.append('</dl>\n')
670 def visit_definition_list_item(self, node):
671 # pass class arguments, ids and names to definition term:
672 node.children[0]['classes'] = (
673 node.get('classes', []) + node.children[0].get('classes', []))
674 node.children[0]['ids'] = (
675 node.get('ids', []) + node.children[0].get('ids', []))
676 node.children[0]['names'] = (
677 node.get('names', []) + node.children[0].get('names', []))
679 def depart_definition_list_item(self, node):
680 pass
682 def visit_description(self, node):
683 self.body.append(self.starttag(node, 'dd', ''))
685 def depart_description(self, node):
686 self.body.append('</dd>\n')
688 def visit_docinfo(self, node):
689 self.context.append(len(self.body))
690 classes = 'docinfo'
691 if (self.is_compactable(node)):
692 classes += ' simple'
693 self.body.append(self.starttag(node, 'dl', CLASS=classes))
695 def depart_docinfo(self, node):
696 self.body.append('</dl>\n')
697 start = self.context.pop()
698 self.docinfo = self.body[start:]
699 self.body = []
701 def visit_docinfo_item(self, node, name, meta=True):
702 if meta:
703 meta_tag = '<meta name="%s" content="%s" />\n' \
704 % (name, self.attval(node.astext()))
705 self.add_meta(meta_tag)
706 self.body.append('<dt class="%s">%s</dt>\n'
707 % (name, self.language.labels[name]))
708 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
710 def depart_docinfo_item(self):
711 self.body.append('</dd>\n')
713 def visit_doctest_block(self, node):
714 self.body.append(self.starttag(node, 'pre', suffix='',
715 CLASS='code python doctest'))
717 def depart_doctest_block(self, node):
718 self.body.append('\n</pre>\n')
720 def visit_document(self, node):
721 title = (node.get('title', '') or os.path.basename(node['source'])
722 or 'docutils document without title')
723 self.head.append('<title>%s</title>\n' % self.encode(title))
725 def depart_document(self, node):
726 self.head_prefix.extend([self.doctype,
727 self.head_prefix_template %
728 {'lang': self.settings.language_code}])
729 self.html_prolog.append(self.doctype)
730 self.meta.insert(0, self.content_type % self.settings.output_encoding)
731 self.head.insert(0, self.content_type % self.settings.output_encoding)
732 if 'name="dcterms.' in ''.join(self.meta):
733 self.head.append(
734 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/"/>')
735 if self.math_header:
736 if self.math_output == 'mathjax':
737 self.head.extend(self.math_header)
738 else:
739 self.stylesheet.extend(self.math_header)
740 # skip content-type meta tag with interpolated charset value:
741 self.html_head.extend(self.head[1:])
742 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
743 self.body_suffix.insert(0, '</div>\n')
744 self.fragment.extend(self.body) # self.fragment is the "naked" body
745 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
746 + self.docinfo + self.body
747 + self.body_suffix[:-1])
748 assert not self.context, 'len(context) = %s' % len(self.context)
750 def visit_emphasis(self, node):
751 self.body.append(self.starttag(node, 'em', ''))
753 def depart_emphasis(self, node):
754 self.body.append('</em>')
756 def visit_entry(self, node):
757 atts = {'class': []}
758 if isinstance(node.parent.parent, nodes.thead):
759 atts['class'].append('head')
760 if node.parent.parent.parent.stubs[node.parent.column]:
761 # "stubs" list is an attribute of the tgroup element
762 atts['class'].append('stub')
763 if atts['class']:
764 tagname = 'th'
765 atts['class'] = ' '.join(atts['class'])
766 else:
767 tagname = 'td'
768 del atts['class']
769 node.parent.column += 1
770 if 'morerows' in node:
771 atts['rowspan'] = node['morerows'] + 1
772 if 'morecols' in node:
773 atts['colspan'] = node['morecols'] + 1
774 node.parent.column += node['morecols']
775 self.body.append(self.starttag(node, tagname, '', **atts))
776 self.context.append('</%s>\n' % tagname.lower())
777 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
778 # if len(node) == 0: # empty cell
779 # self.body.append('&#0160;') # no-break space
781 def depart_entry(self, node):
782 self.body.append(self.context.pop())
784 def visit_enumerated_list(self, node):
785 atts = {}
786 if 'start' in node:
787 atts['start'] = node['start']
788 if 'enumtype' in node:
789 atts['class'] = node['enumtype']
790 if self.is_compactable(node):
791 atts['class'] = (atts.get('class', '') + ' simple').strip()
792 self.body.append(self.starttag(node, 'ol', **atts))
794 def depart_enumerated_list(self, node):
795 self.body.append('</ol>\n')
797 def visit_field_list(self, node):
798 # Keep simple paragraphs in the field_body to enable CSS
799 # rule to start body on new line if the label is too long
800 classes = 'field-list'
801 if (self.is_compactable(node)):
802 classes += ' simple'
803 self.body.append(self.starttag(node, 'dl', CLASS=classes))
805 def depart_field_list(self, node):
806 self.body.append('</dl>\n')
808 def visit_field(self, node):
809 pass
811 def depart_field(self, node):
812 pass
814 # as field is ignored, pass class arguments to field-name and field-body:
816 def visit_field_name(self, node):
817 self.body.append(self.starttag(node, 'dt', '',
818 CLASS=''.join(node.parent['classes'])))
820 def depart_field_name(self, node):
821 self.body.append('</dt>\n')
823 def visit_field_body(self, node):
824 self.body.append(self.starttag(node, 'dd', '',
825 CLASS=''.join(node.parent['classes'])))
826 # prevent misalignment of following content if the field is empty:
827 if not node.children:
828 self.body.append('<p></p>')
830 def depart_field_body(self, node):
831 self.body.append('</dd>\n')
833 def visit_figure(self, node):
834 atts = {'class': 'figure'}
835 if node.get('width'):
836 atts['style'] = 'width: %s' % node['width']
837 if node.get('align'):
838 atts['class'] += " align-" + node['align']
839 self.body.append(self.starttag(node, 'div', **atts))
841 def depart_figure(self, node):
842 self.body.append('</div>\n')
844 # use HTML 5 <footer> element?
845 def visit_footer(self, node):
846 self.context.append(len(self.body))
848 def depart_footer(self, node):
849 start = self.context.pop()
850 footer = [self.starttag(node, 'div', CLASS='footer'),
851 '<hr class="footer" />\n']
852 footer.extend(self.body[start:])
853 footer.append('\n</div>\n')
854 self.footer.extend(footer)
855 self.body_suffix[:0] = footer
856 del self.body[start:]
858 # footnotes
859 # ---------
860 # use definition list instead of table for footnote text
862 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
863 def visit_footnote(self, node):
864 if not self.in_footnote_list:
865 classes = 'footnote ' + self.settings.footnote_references
866 self.body.append('<dl class="%s">\n'%classes)
867 self.in_footnote_list = True
869 def depart_footnote(self, node):
870 self.body.append('</dd>\n')
871 if not isinstance(node.next_node(descend=False, siblings=True),
872 nodes.footnote):
873 self.body.append('</dl>\n')
874 self.in_footnote_list = False
876 def visit_footnote_reference(self, node):
877 href = '#' + node['refid']
878 classes = 'footnote-reference ' + self.settings.footnote_references
879 self.body.append(self.starttag(node, 'a', '', #suffix,
880 CLASS=classes, href=href))
882 def depart_footnote_reference(self, node):
883 self.body.append('</a>')
885 # Docutils-generated text: put section numbers in a span for CSS styling:
886 def visit_generated(self, node):
887 if 'sectnum' in node['classes']:
888 # get section number (strip trailing no-break-spaces)
889 sectnum = node.astext().rstrip(u' ')
890 self.body.append('<span class="sectnum">%s</span> '
891 % self.encode(sectnum))
892 # Content already processed:
893 raise nodes.SkipNode
895 def depart_generated(self, node):
896 pass
898 def visit_header(self, node):
899 self.context.append(len(self.body))
901 def depart_header(self, node):
902 start = self.context.pop()
903 header = [self.starttag(node, 'div', CLASS='header')]
904 header.extend(self.body[start:])
905 header.append('\n<hr class="header"/>\n</div>\n')
906 self.body_prefix.extend(header)
907 self.header.extend(header)
908 del self.body[start:]
910 def visit_image(self, node):
911 atts = {}
912 uri = node['uri']
913 mimetype = mimetypes.guess_type(uri)[0]
914 # image size
915 if 'width' in node:
916 atts['width'] = node['width']
917 if 'height' in node:
918 atts['height'] = node['height']
919 if 'scale' in node:
920 if (PIL and not ('width' in node and 'height' in node)
921 and self.settings.file_insertion_enabled):
922 imagepath = url2pathname(uri)
923 try:
924 img = PIL.Image.open(
925 imagepath.encode(sys.getfilesystemencoding()))
926 except (IOError, UnicodeEncodeError):
927 pass # TODO: warn?
928 else:
929 self.settings.record_dependencies.add(
930 imagepath.replace('\\', '/'))
931 if 'width' not in atts:
932 atts['width'] = '%dpx' % img.size[0]
933 if 'height' not in atts:
934 atts['height'] = '%dpx' % img.size[1]
935 del img
936 for att_name in 'width', 'height':
937 if att_name in atts:
938 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
939 assert match
940 atts[att_name] = '%s%s' % (
941 float(match.group(1)) * (float(node['scale']) / 100),
942 match.group(2))
943 style = []
944 for att_name in 'width', 'height':
945 if att_name in atts:
946 if re.match(r'^[0-9.]+$', atts[att_name]):
947 # Interpret unitless values as pixels.
948 atts[att_name] += 'px'
949 style.append('%s: %s;' % (att_name, atts[att_name]))
950 del atts[att_name]
951 if style:
952 atts['style'] = ' '.join(style)
953 if isinstance(node.parent, (nodes.figure, nodes.compound)):
954 suffix = '\n'
955 elif not isinstance(node.parent, (nodes.TextElement, nodes.reference)):
956 self.body.append('<p class="image-wrapper">')
957 suffix = '</p>\n'
958 else:
959 suffix = ''
960 if 'align' in node:
961 atts['class'] = 'align-%s' % node['align']
962 # Embed image file (embedded SVG or data URI):
963 if self.settings.embed_images or ('embed' in node):
964 err_msg = ''
965 if not mimetype:
966 err_msg = 'unknown MIME type for "%s"' % uri
967 if not self.settings.file_insertion_enabled:
968 err_msg = 'file insertion disabled.'
969 try:
970 with open(url2pathname(uri), 'rb') as imagefile:
971 imagedata = imagefile.read()
972 except IOError as err:
973 err_msg = str(err)
974 if not err_msg:
975 # TODO (test mimetype for SVG and insert directly)
976 data64 = base64.b64encode(imagedata).decode()
977 uri = u'data:%s;base64,%s' % (mimetype, data64)
978 else:
979 # raise NotImplementedError(os.getcwd() + err_msg)
980 self.document.reporter.error("Cannot embed image\n "+err_msg)
982 if mimetype == 'application/x-shockwave-flash':
983 atts['type'] = mimetype
984 # do NOT use an empty tag: incorrect rendering in browsers
985 tag = (self.starttag(node, 'object', '', data=uri, **atts)
986 + node.get('alt', uri) + '</object>' + suffix)
987 else:
988 atts['alt'] = node.get('alt', node['uri'])
989 tag = self.emptytag(node, 'img', suffix, src=uri, **atts)
990 self.body.append(tag)
992 def depart_image(self, node):
993 pass
995 def visit_inline(self, node):
996 self.body.append(self.starttag(node, 'span', ''))
998 def depart_inline(self, node):
999 self.body.append('</span>')
1001 # footnote and citation labels:
1002 def visit_label(self, node):
1003 if (isinstance(node.parent, nodes.footnote)):
1004 classes = self.settings.footnote_references
1005 else:
1006 classes = 'brackets'
1007 # pass parent node to get id into starttag:
1008 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
1009 self.body.append(self.starttag(node, 'span', '', CLASS=classes))
1010 # footnote/citation backrefs:
1011 if self.settings.footnote_backlinks:
1012 backrefs = node.parent['backrefs']
1013 if len(backrefs) == 1:
1014 self.body.append('<a class="fn-backref" href="#%s">'
1015 % backrefs[0])
1017 def depart_label(self, node):
1018 if self.settings.footnote_backlinks:
1019 backrefs = node.parent['backrefs']
1020 if len(backrefs) == 1:
1021 self.body.append('</a>')
1022 self.body.append('</span>')
1023 if self.settings.footnote_backlinks and len(backrefs) > 1:
1024 backlinks = ['<a href="#%s">%s</a>' % (ref, i)
1025 for (i, ref) in enumerate(backrefs, 1)]
1026 self.body.append('<span class="fn-backref">(%s)</span>'
1027 % ','.join(backlinks))
1028 self.body.append('</dt>\n<dd>')
1030 def visit_legend(self, node):
1031 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1033 def depart_legend(self, node):
1034 self.body.append('</div>\n')
1036 def visit_line(self, node):
1037 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1038 if not len(node):
1039 self.body.append('<br />')
1041 def depart_line(self, node):
1042 self.body.append('</div>\n')
1044 def visit_line_block(self, node):
1045 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1047 def depart_line_block(self, node):
1048 self.body.append('</div>\n')
1050 def visit_list_item(self, node):
1051 self.body.append(self.starttag(node, 'li', ''))
1053 def depart_list_item(self, node):
1054 self.body.append('</li>\n')
1056 # inline literal
1057 def visit_literal(self, node):
1058 # special case: "code" role
1059 classes = node.get('classes', [])
1060 if 'code' in classes:
1061 # filter 'code' from class arguments
1062 node['classes'] = [cls for cls in classes if cls != 'code']
1063 self.body.append(self.starttag(node, 'code', ''))
1064 return
1065 self.body.append(
1066 self.starttag(node, 'span', '', CLASS='docutils literal'))
1067 text = node.astext()
1068 # remove hard line breaks (except if in a parsed-literal block)
1069 if not isinstance(node.parent, nodes.literal_block):
1070 text = text.replace('\n', ' ')
1071 # Protect text like ``--an-option`` and the regular expression
1072 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1073 for token in self.words_and_spaces.findall(text):
1074 if token.strip() and self.in_word_wrap_point.search(token):
1075 self.body.append('<span class="pre">%s</span>'
1076 % self.encode(token))
1077 else:
1078 self.body.append(self.encode(token))
1079 self.body.append('</span>')
1080 # Content already processed:
1081 raise nodes.SkipNode
1083 def depart_literal(self, node):
1084 # skipped unless literal element is from "code" role:
1085 self.body.append('</code>')
1087 def visit_literal_block(self, node):
1088 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1089 if 'code' in node.get('classes', []):
1090 self.body.append('<code>')
1092 def depart_literal_block(self, node):
1093 if 'code' in node.get('classes', []):
1094 self.body.append('</code>')
1095 self.body.append('</pre>\n')
1097 # Mathematics:
1098 # As there is no native HTML math support, we provide alternatives
1099 # for the math-output: LaTeX and MathJax simply wrap the content,
1100 # HTML and MathML also convert the math_code.
1101 # HTML container
1102 math_tags = {# math_output: (block, inline, class-arguments)
1103 'mathml': ('div', '', ''),
1104 'html': ('div', 'span', 'formula'),
1105 'mathjax': ('div', 'span', 'math'),
1106 'latex': ('pre', 'tt', 'math'),
1109 def visit_math(self, node, math_env=''):
1110 # If the method is called from visit_math_block(), math_env != ''.
1112 if self.math_output not in self.math_tags:
1113 self.document.reporter.error(
1114 'math-output format "%s" not supported '
1115 'falling back to "latex"'% self.math_output)
1116 self.math_output = 'latex'
1117 tag = self.math_tags[self.math_output][math_env == '']
1118 clsarg = self.math_tags[self.math_output][2]
1119 # LaTeX container
1120 wrappers = {# math_mode: (inline, block)
1121 'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1122 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1123 'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1124 'latex': (None, None),
1126 wrapper = wrappers[self.math_output][math_env != '']
1127 if self.math_output == 'mathml' and (not self.math_output_options or
1128 self.math_output_options[0] == 'blahtexml'):
1129 wrapper = None
1130 # get and wrap content
1131 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1132 if wrapper:
1133 try: # wrapper with three "%s"
1134 math_code = wrapper % (math_env, math_code, math_env)
1135 except TypeError: # wrapper with one "%s"
1136 math_code = wrapper % math_code
1137 # settings and conversion
1138 if self.math_output in ('latex', 'mathjax'):
1139 math_code = self.encode(math_code)
1140 if self.math_output == 'mathjax' and not self.math_header:
1141 try:
1142 self.mathjax_url = self.math_output_options[0]
1143 except IndexError:
1144 self.document.reporter.warning('No MathJax URL specified, '
1145 'using local fallback (see config.html)')
1146 # append configuration, if not already present in the URL:
1147 # input LaTeX with AMS, output common HTML
1148 if '?' not in self.mathjax_url:
1149 self.mathjax_url += '?config=TeX-AMS_CHTML'
1150 self.math_header = [self.mathjax_script % self.mathjax_url]
1151 elif self.math_output == 'html':
1152 if self.math_output_options and not self.math_header:
1153 self.math_header = [self.stylesheet_call(
1154 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1155 for s in self.math_output_options[0].split(',')]
1156 # TODO: fix display mode in matrices and fractions
1157 math2html.DocumentParameters.displaymode = (math_env != '')
1158 math_code = math2html.math2html(math_code)
1159 elif self.math_output == 'mathml':
1160 if 'XHTML 1' in self.doctype:
1161 self.doctype = self.doctype_mathml
1162 self.content_type = self.content_type_mathml
1163 converter = ' '.join(self.math_output_options).lower()
1164 try:
1165 if converter == 'latexml':
1166 math_code = tex2mathml_extern.latexml(math_code,
1167 self.document.reporter)
1168 elif converter == 'ttm':
1169 math_code = tex2mathml_extern.ttm(math_code,
1170 self.document.reporter)
1171 elif converter == 'blahtexml':
1172 math_code = tex2mathml_extern.blahtexml(math_code,
1173 inline=not(math_env),
1174 reporter=self.document.reporter)
1175 elif not converter:
1176 math_code = latex2mathml.tex2mathml(math_code,
1177 inline=not(math_env))
1178 else:
1179 self.document.reporter.error('option "%s" not supported '
1180 'with math-output "MathML"')
1181 except OSError:
1182 raise OSError('is "latexmlmath" in your PATH?')
1183 except SyntaxError as err:
1184 err_node = self.document.reporter.error(err, base_node=node)
1185 self.visit_system_message(err_node)
1186 self.body.append(self.starttag(node, 'p'))
1187 self.body.append(u','.join(err.args))
1188 self.body.append('</p>\n')
1189 self.body.append(self.starttag(node, 'pre',
1190 CLASS='literal-block'))
1191 self.body.append(self.encode(math_code))
1192 self.body.append('\n</pre>\n')
1193 self.depart_system_message(err_node)
1194 raise nodes.SkipNode
1195 # append to document body
1196 if tag:
1197 self.body.append(self.starttag(node, tag,
1198 suffix='\n'*bool(math_env),
1199 CLASS=clsarg))
1200 self.body.append(math_code)
1201 if math_env: # block mode (equation, display)
1202 self.body.append('\n')
1203 if tag:
1204 self.body.append('</%s>' % tag)
1205 if math_env:
1206 self.body.append('\n')
1207 # Content already processed:
1208 raise nodes.SkipNode
1210 def depart_math(self, node):
1211 pass # never reached
1213 def visit_math_block(self, node):
1214 math_env = pick_math_environment(node.astext())
1215 self.visit_math(node, math_env=math_env)
1217 def depart_math_block(self, node):
1218 pass # never reached
1220 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1221 # HTML5/polyglot recommends using both
1222 def visit_meta(self, node):
1223 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1224 self.add_meta(meta)
1226 def depart_meta(self, node):
1227 pass
1229 def add_meta(self, tag):
1230 self.meta.append(tag)
1231 self.head.append(tag)
1233 def visit_option(self, node):
1234 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1236 def depart_option(self, node):
1237 self.body.append('</span>')
1238 if isinstance(node.next_node(descend=False, siblings=True),
1239 nodes.option):
1240 self.body.append(', ')
1242 def visit_option_argument(self, node):
1243 self.body.append(node.get('delimiter', ' '))
1244 self.body.append(self.starttag(node, 'var', ''))
1246 def depart_option_argument(self, node):
1247 self.body.append('</var>')
1249 def visit_option_group(self, node):
1250 self.body.append(self.starttag(node, 'dt', ''))
1251 self.body.append('<kbd>')
1253 def depart_option_group(self, node):
1254 self.body.append('</kbd></dt>\n')
1256 def visit_option_list(self, node):
1257 self.body.append(
1258 self.starttag(node, 'dl', CLASS='option-list'))
1260 def depart_option_list(self, node):
1261 self.body.append('</dl>\n')
1263 def visit_option_list_item(self, node):
1264 pass
1266 def depart_option_list_item(self, node):
1267 pass
1269 def visit_option_string(self, node):
1270 pass
1272 def depart_option_string(self, node):
1273 pass
1275 def visit_organization(self, node):
1276 self.visit_docinfo_item(node, 'organization')
1278 def depart_organization(self, node):
1279 self.depart_docinfo_item()
1281 # Do not omit <p> tags
1282 # --------------------
1284 # The HTML4CSS1 writer does this to "produce
1285 # visually compact lists (less vertical whitespace)". This writer
1286 # relies on CSS rules for"visual compactness".
1288 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1289 # character data, so you cannot drop the <p> tags.
1290 # * Keeping simple paragraphs in the field_body enables a CSS
1291 # rule to start the field-body on a new line if the label is too long
1292 # * it makes the code simpler.
1294 # TODO: omit paragraph tags in simple table cells?
1296 def visit_paragraph(self, node):
1297 self.body.append(self.starttag(node, 'p', ''))
1299 def depart_paragraph(self, node):
1300 self.body.append('</p>')
1301 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1302 (len(node.parent) == 1)):
1303 self.body.append('\n')
1305 def visit_problematic(self, node):
1306 if node.hasattr('refid'):
1307 self.body.append('<a href="#%s">' % node['refid'])
1308 self.context.append('</a>')
1309 else:
1310 self.context.append('')
1311 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1313 def depart_problematic(self, node):
1314 self.body.append('</span>')
1315 self.body.append(self.context.pop())
1317 def visit_raw(self, node):
1318 if 'html' in node.get('format', '').split():
1319 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1320 if node['classes']:
1321 self.body.append(self.starttag(node, t, suffix=''))
1322 self.body.append(node.astext())
1323 if node['classes']:
1324 self.body.append('</%s>' % t)
1325 # Keep non-HTML raw text out of output:
1326 raise nodes.SkipNode
1328 def visit_reference(self, node):
1329 atts = {'class': 'reference'}
1330 if 'refuri' in node:
1331 atts['href'] = node['refuri']
1332 if ( self.settings.cloak_email_addresses
1333 and atts['href'].startswith('mailto:')):
1334 atts['href'] = self.cloak_mailto(atts['href'])
1335 self.in_mailto = True
1336 atts['class'] += ' external'
1337 else:
1338 assert 'refid' in node, \
1339 'References must have "refuri" or "refid" attribute.'
1340 atts['href'] = '#' + node['refid']
1341 atts['class'] += ' internal'
1342 if len(node) == 1 and isinstance(node[0], nodes.image):
1343 atts['class'] += ' image-reference'
1344 if not isinstance(node.parent, nodes.TextElement):
1345 assert len(node) == 1 and isinstance(node[0], nodes.image)
1346 if not isinstance(node.parent, (nodes.figure, nodes.compound)):
1347 self.body.append('<p class="image-wrapper">')
1348 self.body.append(self.starttag(node, 'a', '', **atts))
1350 def depart_reference(self, node):
1351 self.body.append('</a>')
1352 if not isinstance(node.parent, nodes.TextElement):
1353 if not isinstance(node.parent, (nodes.figure, nodes.compound)):
1354 self.body.append('</p>')
1355 self.body.append('\n')
1356 self.in_mailto = False
1358 def visit_revision(self, node):
1359 self.visit_docinfo_item(node, 'revision', meta=False)
1361 def depart_revision(self, node):
1362 self.depart_docinfo_item()
1364 def visit_row(self, node):
1365 self.body.append(self.starttag(node, 'tr', ''))
1366 node.column = 0
1368 def depart_row(self, node):
1369 self.body.append('</tr>\n')
1371 def visit_rubric(self, node):
1372 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1374 def depart_rubric(self, node):
1375 self.body.append('</p>\n')
1377 # TODO: use the new HTML 5 element <section>?
1378 def visit_section(self, node):
1379 self.section_level += 1
1380 self.body.append(
1381 self.starttag(node, 'div', CLASS='section'))
1383 def depart_section(self, node):
1384 self.section_level -= 1
1385 self.body.append('</div>\n')
1387 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1388 def visit_sidebar(self, node):
1389 self.body.append(
1390 self.starttag(node, 'div', CLASS='sidebar'))
1391 self.in_sidebar = True
1393 def depart_sidebar(self, node):
1394 self.body.append('</div>\n')
1395 self.in_sidebar = False
1397 def visit_status(self, node):
1398 self.visit_docinfo_item(node, 'status', meta=False)
1400 def depart_status(self, node):
1401 self.depart_docinfo_item()
1403 def visit_strong(self, node):
1404 self.body.append(self.starttag(node, 'strong', ''))
1406 def depart_strong(self, node):
1407 self.body.append('</strong>')
1409 def visit_subscript(self, node):
1410 self.body.append(self.starttag(node, 'sub', ''))
1412 def depart_subscript(self, node):
1413 self.body.append('</sub>')
1415 def visit_substitution_definition(self, node):
1416 """Internal only."""
1417 raise nodes.SkipNode
1419 def visit_substitution_reference(self, node):
1420 self.unimplemented_visit(node)
1422 # h1–h6 elements must not be used to markup subheadings, subtitles,
1423 # alternative titles and taglines unless intended to be the heading for a
1424 # new section or subsection.
1425 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1426 def visit_subtitle(self, node):
1427 if isinstance(node.parent, nodes.sidebar):
1428 classes = 'sidebar-subtitle'
1429 elif isinstance(node.parent, nodes.document):
1430 classes = 'subtitle'
1431 self.in_document_title = len(self.body)+1
1432 elif isinstance(node.parent, nodes.section):
1433 classes = 'section-subtitle'
1434 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1436 def depart_subtitle(self, node):
1437 self.body.append('</p>\n')
1438 if isinstance(node.parent, nodes.document):
1439 self.subtitle = self.body[self.in_document_title:-1]
1440 self.in_document_title = 0
1441 self.body_pre_docinfo.extend(self.body)
1442 self.html_subtitle.extend(self.body)
1443 del self.body[:]
1445 def visit_superscript(self, node):
1446 self.body.append(self.starttag(node, 'sup', ''))
1448 def depart_superscript(self, node):
1449 self.body.append('</sup>')
1451 def visit_system_message(self, node):
1452 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1453 self.body.append('<p class="system-message-title">')
1454 backref_text = ''
1455 if len(node['backrefs']):
1456 backrefs = node['backrefs']
1457 if len(backrefs) == 1:
1458 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1459 % backrefs[0])
1460 else:
1461 i = 1
1462 backlinks = []
1463 for backref in backrefs:
1464 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1465 i += 1
1466 backref_text = ('; <em>backlinks: %s</em>'
1467 % ', '.join(backlinks))
1468 if node.hasattr('line'):
1469 line = ', line %s' % node['line']
1470 else:
1471 line = ''
1472 self.body.append('System Message: %s/%s '
1473 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1474 % (node['type'], node['level'],
1475 self.encode(node['source']), line, backref_text))
1477 def depart_system_message(self, node):
1478 self.body.append('</div>\n')
1480 def visit_table(self, node):
1481 atts = {}
1482 classes = [cls.strip(u' \t\n')
1483 for cls in self.settings.table_style.split(',')]
1484 if 'align' in node:
1485 classes.append('align-%s' % node['align'])
1486 if 'width' in node:
1487 atts['style'] = 'width: %s' % node['width']
1488 tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts)
1489 self.body.append(tag)
1491 def depart_table(self, node):
1492 self.body.append('</table>\n')
1494 def visit_target(self, node):
1495 if not ('refuri' in node or 'refid' in node
1496 or 'refname' in node):
1497 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1498 self.context.append('</span>')
1499 else:
1500 self.context.append('')
1502 def depart_target(self, node):
1503 self.body.append(self.context.pop())
1505 # no hard-coded vertical alignment in table body
1506 def visit_tbody(self, node):
1507 self.body.append(self.starttag(node, 'tbody'))
1509 def depart_tbody(self, node):
1510 self.body.append('</tbody>\n')
1512 def visit_term(self, node):
1513 self.body.append(self.starttag(node, 'dt', ''))
1515 def depart_term(self, node):
1517 Leave the end tag to `self.visit_definition()`, in case there's a
1518 classifier.
1520 pass
1522 def visit_tgroup(self, node):
1523 self.colspecs = []
1524 node.stubs = []
1526 def depart_tgroup(self, node):
1527 pass
1529 def visit_thead(self, node):
1530 self.body.append(self.starttag(node, 'thead'))
1532 def depart_thead(self, node):
1533 self.body.append('</thead>\n')
1535 def visit_title(self, node):
1536 """Only 6 section levels are supported by HTML."""
1537 close_tag = '</p>\n'
1538 if isinstance(node.parent, nodes.topic):
1539 self.body.append(
1540 self.starttag(node, 'p', '', CLASS='topic-title'))
1541 elif isinstance(node.parent, nodes.sidebar):
1542 self.body.append(
1543 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1544 elif isinstance(node.parent, nodes.Admonition):
1545 self.body.append(
1546 self.starttag(node, 'p', '', CLASS='admonition-title'))
1547 elif isinstance(node.parent, nodes.table):
1548 self.body.append(
1549 self.starttag(node, 'caption', ''))
1550 close_tag = '</caption>\n'
1551 elif isinstance(node.parent, nodes.document):
1552 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1553 close_tag = '</h1>\n'
1554 self.in_document_title = len(self.body)
1555 else:
1556 assert isinstance(node.parent, nodes.section)
1557 h_level = self.section_level + self.initial_header_level - 1
1558 atts = {}
1559 if (len(node.parent) >= 2 and
1560 isinstance(node.parent[1], nodes.subtitle)):
1561 atts['CLASS'] = 'with-subtitle'
1562 self.body.append(
1563 self.starttag(node, 'h%s' % h_level, '', **atts))
1564 atts = {}
1565 if node.hasattr('refid'):
1566 atts['class'] = 'toc-backref'
1567 atts['href'] = '#' + node['refid']
1568 if atts:
1569 self.body.append(self.starttag({}, 'a', '', **atts))
1570 close_tag = '</a></h%s>\n' % (h_level)
1571 else:
1572 close_tag = '</h%s>\n' % (h_level)
1573 self.context.append(close_tag)
1575 def depart_title(self, node):
1576 self.body.append(self.context.pop())
1577 if self.in_document_title:
1578 self.title = self.body[self.in_document_title:-1]
1579 self.in_document_title = 0
1580 self.body_pre_docinfo.extend(self.body)
1581 self.html_title.extend(self.body)
1582 del self.body[:]
1584 def visit_title_reference(self, node):
1585 self.body.append(self.starttag(node, 'cite', ''))
1587 def depart_title_reference(self, node):
1588 self.body.append('</cite>')
1590 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1591 def visit_topic(self, node):
1592 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1593 self.topic_classes = node['classes']
1594 # TODO: replace with ::
1595 # self.in_contents = 'contents' in node['classes']
1597 def depart_topic(self, node):
1598 self.body.append('</div>\n')
1599 self.topic_classes = []
1600 # TODO self.in_contents = False
1602 def visit_transition(self, node):
1603 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1605 def depart_transition(self, node):
1606 pass
1608 def visit_version(self, node):
1609 self.visit_docinfo_item(node, 'version', meta=False)
1611 def depart_version(self, node):
1612 self.depart_docinfo_item()
1614 def unimplemented_visit(self, node):
1615 raise NotImplementedError('visiting unimplemented node type: %s'
1616 % node.__class__.__name__)
1619 class SimpleListChecker(nodes.GenericNodeVisitor):
1622 Raise `nodes.NodeFound` if non-simple list item is encountered.
1624 Here "simple" means a list item containing nothing other than a single
1625 paragraph, a simple list, or a paragraph followed by a simple list.
1627 This version also checks for simple field lists and docinfo.
1630 def default_visit(self, node):
1631 raise nodes.NodeFound
1633 def visit_list_item(self, node):
1634 children = [child for child in node.children
1635 if not isinstance(child, nodes.Invisible)]
1636 if (children and isinstance(children[0], nodes.paragraph)
1637 and (isinstance(children[-1], nodes.bullet_list) or
1638 isinstance(children[-1], nodes.enumerated_list) or
1639 isinstance(children[-1], nodes.field_list))):
1640 children.pop()
1641 if len(children) <= 1:
1642 return
1643 else:
1644 raise nodes.NodeFound
1646 def pass_node(self, node):
1647 pass
1649 def ignore_node(self, node):
1650 # ignore nodes that are never complex (can contain only inline nodes)
1651 raise nodes.SkipNode
1653 # Paragraphs and text
1654 visit_Text = ignore_node
1655 visit_paragraph = ignore_node
1657 # Lists
1658 visit_bullet_list = pass_node
1659 visit_enumerated_list = pass_node
1660 visit_docinfo = pass_node
1662 # Docinfo nodes:
1663 visit_author = ignore_node
1664 visit_authors = visit_list_item
1665 visit_address = visit_list_item
1666 visit_contact = pass_node
1667 visit_copyright = ignore_node
1668 visit_date = ignore_node
1669 visit_organization = ignore_node
1670 visit_status = ignore_node
1671 visit_version = visit_list_item
1673 # Definition list:
1674 visit_definition_list = pass_node
1675 visit_definition_list_item = pass_node
1676 visit_term = ignore_node
1677 visit_classifier = pass_node
1678 visit_definition = visit_list_item
1680 # Field list:
1681 visit_field_list = pass_node
1682 visit_field = pass_node
1683 # the field body corresponds to a list item
1684 visit_field_body = visit_list_item
1685 visit_field_name = ignore_node
1687 # Invisible nodes should be ignored.
1688 visit_comment = ignore_node
1689 visit_substitution_definition = ignore_node
1690 visit_target = ignore_node
1691 visit_pending = ignore_node