Make the insertion of "admonition" class in the HTML writer
[docutils.git] / docutils / docutils / writers / html4css1 / __init__.py
blob9ceb7fde18edfaa2382cd533326ba4f2e1b72419
1 # $Id$
2 # Author: David Goodger
3 # Maintainer: docutils-develop@lists.sourceforge.net
4 # Copyright: This module has been placed in the public domain.
6 """
7 Simple HyperText Markup Language document tree Writer.
9 The output conforms to the XHTML version 1.0 Transitional DTD
10 (*almost* strict). The output contains a minimum of formatting
11 information. The cascading style sheet "html4css1.css" is required
12 for proper viewing with a modern graphical browser.
13 """
15 __docformat__ = 'reStructuredText'
18 import sys
19 import os
20 import os.path
21 import time
22 import re
23 import urllib
24 try: # check for the Python Imaging Library
25 import PIL.Image
26 except ImportError:
27 try: # sometimes PIL modules are put in PYTHONPATH's root
28 import Image
29 class PIL(object): pass # dummy wrapper
30 PIL.Image = Image
31 except ImportError:
32 PIL = None
33 import docutils
34 from docutils import frontend, nodes, utils, writers, languages, io
35 from docutils.utils.error_reporting import SafeString
36 from docutils.transforms import writer_aux
37 from docutils.utils.math import unichar2tex, pick_math_environment, math2html
38 from docutils.utils.math.latex2mathml import parse_latex_math
40 class Writer(writers.Writer):
42 supported = ('html', 'html4css1', 'xhtml')
43 """Formats this writer supports."""
45 default_stylesheets = ['html4css1.css']
46 default_stylesheet_dirs = ['.', os.path.abspath(os.path.dirname(__file__))]
48 default_template = 'template.txt'
49 default_template_path = os.path.join(
50 os.path.dirname(os.path.abspath(__file__)), default_template)
52 settings_spec = (
53 'HTML-Specific Options',
54 None,
55 (('Specify the template file (UTF-8 encoded). Default is "%s".'
56 % default_template_path,
57 ['--template'],
58 {'default': default_template_path, 'metavar': '<file>'}),
59 ('Comma separated list of stylesheet URLs. '
60 'Overrides previous --stylesheet and --stylesheet-path settings.',
61 ['--stylesheet'],
62 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
63 'validator': frontend.validate_comma_separated_list}),
64 ('Comma separated list of stylesheet paths. '
65 'Relative paths are expanded if a matching file is found in '
66 'the --stylesheet-dirs. With --link-stylesheet, '
67 'the path is rewritten relative to the output HTML file. '
68 'Default: "%s"' % ','.join(default_stylesheets),
69 ['--stylesheet-path'],
70 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
71 'validator': frontend.validate_comma_separated_list,
72 'default': default_stylesheets}),
73 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
74 'files must be accessible during processing. This is the default.',
75 ['--embed-stylesheet'],
76 {'default': 1, 'action': 'store_true',
77 'validator': frontend.validate_boolean}),
78 ('Link to the stylesheet(s) in the output HTML file. '
79 'Default: embed stylesheets.',
80 ['--link-stylesheet'],
81 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
82 ('Comma-separated list of directories where stylesheets are found. '
83 'Used by --stylesheet-path when expanding relative path arguments. '
84 'Default: "%s"' % default_stylesheet_dirs,
85 ['--stylesheet-dirs'],
86 {'metavar': '<dir[,dir,...]>',
87 'validator': frontend.validate_comma_separated_list,
88 'default': default_stylesheet_dirs}),
89 ('Specify the initial header level. Default is 1 for "<h1>". '
90 'Does not affect document title & subtitle (see --no-doc-title).',
91 ['--initial-header-level'],
92 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
93 'metavar': '<level>'}),
94 ('Specify the maximum width (in characters) for one-column field '
95 'names. Longer field names will span an entire row of the table '
96 'used to render the field list. Default is 14 characters. '
97 'Use 0 for "no limit".',
98 ['--field-name-limit'],
99 {'default': 14, 'metavar': '<level>',
100 'validator': frontend.validate_nonnegative_int}),
101 ('Specify the maximum width (in characters) for options in option '
102 'lists. Longer options will span an entire row of the table used '
103 'to render the option list. Default is 14 characters. '
104 'Use 0 for "no limit".',
105 ['--option-limit'],
106 {'default': 14, 'metavar': '<level>',
107 'validator': frontend.validate_nonnegative_int}),
108 ('Format for footnote references: one of "superscript" or '
109 '"brackets". Default is "brackets".',
110 ['--footnote-references'],
111 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
112 'metavar': '<format>',
113 'overrides': 'trim_footnote_reference_space'}),
114 ('Format for block quote attributions: one of "dash" (em-dash '
115 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
116 ['--attribution'],
117 {'choices': ['dash', 'parentheses', 'parens', 'none'],
118 'default': 'dash', 'metavar': '<format>'}),
119 ('Remove extra vertical whitespace between items of "simple" bullet '
120 'lists and enumerated lists. Default: enabled.',
121 ['--compact-lists'],
122 {'default': 1, 'action': 'store_true',
123 'validator': frontend.validate_boolean}),
124 ('Disable compact simple bullet and enumerated lists.',
125 ['--no-compact-lists'],
126 {'dest': 'compact_lists', 'action': 'store_false'}),
127 ('Remove extra vertical whitespace between items of simple field '
128 'lists. Default: enabled.',
129 ['--compact-field-lists'],
130 {'default': 1, 'action': 'store_true',
131 'validator': frontend.validate_boolean}),
132 ('Disable compact simple field lists.',
133 ['--no-compact-field-lists'],
134 {'dest': 'compact_field_lists', 'action': 'store_false'}),
135 ('Added to standard table classes. '
136 'Defined styles: "borderless". Default: ""',
137 ['--table-style'],
138 {'default': ''}),
139 ('Math output format, one of "MathML", "HTML", "MathJax" '
140 'or "LaTeX". Default: "HTML math.css"',
141 ['--math-output'],
142 {'default': 'HTML math.css'}),
143 ('Omit the XML declaration. Use with caution.',
144 ['--no-xml-declaration'],
145 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
146 'validator': frontend.validate_boolean}),
147 ('Obfuscate email addresses to confuse harvesters while still '
148 'keeping email links usable with standards-compliant browsers.',
149 ['--cloak-email-addresses'],
150 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
152 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
154 config_section = 'html4css1 writer'
155 config_section_dependencies = ('writers',)
157 visitor_attributes = (
158 'head_prefix', 'head', 'stylesheet', 'body_prefix',
159 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
160 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
161 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
162 'html_body')
164 def get_transforms(self):
165 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
167 def __init__(self):
168 writers.Writer.__init__(self)
169 self.translator_class = HTMLTranslator
171 def translate(self):
172 self.visitor = visitor = self.translator_class(self.document)
173 self.document.walkabout(visitor)
174 for attr in self.visitor_attributes:
175 setattr(self, attr, getattr(visitor, attr))
176 self.output = self.apply_template()
178 def apply_template(self):
179 template_file = open(self.document.settings.template, 'rb')
180 template = unicode(template_file.read(), 'utf-8')
181 template_file.close()
182 subs = self.interpolation_dict()
183 return template % subs
185 def interpolation_dict(self):
186 subs = {}
187 settings = self.document.settings
188 for attr in self.visitor_attributes:
189 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
190 subs['encoding'] = settings.output_encoding
191 subs['version'] = docutils.__version__
192 return subs
194 def assemble_parts(self):
195 writers.Writer.assemble_parts(self)
196 for part in self.visitor_attributes:
197 self.parts[part] = ''.join(getattr(self, part))
200 class HTMLTranslator(nodes.NodeVisitor):
203 This HTML writer has been optimized to produce visually compact
204 lists (less vertical whitespace). HTML's mixed content models
205 allow list items to contain "<li><p>body elements</p></li>" or
206 "<li>just text</li>" or even "<li>text<p>and body
207 elements</p>combined</li>", each with different effects. It would
208 be best to stick with strict body elements in list items, but they
209 affect vertical spacing in browsers (although they really
210 shouldn't).
212 Here is an outline of the optimization:
214 - Check for and omit <p> tags in "simple" lists: list items
215 contain either a single paragraph, a nested simple list, or a
216 paragraph followed by a nested simple list. This means that
217 this list can be compact:
219 - Item 1.
220 - Item 2.
222 But this list cannot be compact:
224 - Item 1.
226 This second paragraph forces space between list items.
228 - Item 2.
230 - In non-list contexts, omit <p> tags on a paragraph if that
231 paragraph is the only child of its parent (footnotes & citations
232 are allowed a label first).
234 - Regardless of the above, in definitions, table cells, field bodies,
235 option descriptions, and list items, mark the first child with
236 'class="first"' and the last child with 'class="last"'. The stylesheet
237 sets the margins (top & bottom respectively) to 0 for these elements.
239 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
240 option) disables list whitespace optimization.
243 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
244 doctype = (
245 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
246 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
247 doctype_mathml = doctype
249 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
250 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
251 content_type = ('<meta http-equiv="Content-Type"'
252 ' content="text/html; charset=%s" />\n')
253 content_type_mathml = ('<meta http-equiv="Content-Type"'
254 ' content="application/xhtml+xml; charset=%s" />\n')
256 generator = ('<meta name="generator" content="Docutils %s: '
257 'http://docutils.sourceforge.net/" />\n')
259 # Template for the MathJax script in the header:
260 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
261 # The latest version of MathJax from the distributed server:
262 # avaliable to the public under the `MathJax CDN Terms of Service`__
263 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
264 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
265 'config=TeX-AMS-MML_HTMLorMML')
266 # may be overwritten by custom URL appended to "mathjax"
268 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
269 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
270 words_and_spaces = re.compile(r'\S+| +|\n')
271 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
272 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
274 def __init__(self, document):
275 nodes.NodeVisitor.__init__(self, document)
276 self.settings = settings = document.settings
277 lcode = settings.language_code
278 self.language = languages.get_language(lcode, document.reporter)
279 self.meta = [self.generator % docutils.__version__]
280 self.head_prefix = []
281 self.html_prolog = []
282 if settings.xml_declaration:
283 self.head_prefix.append(self.xml_declaration
284 % settings.output_encoding)
285 # encoding not interpolated:
286 self.html_prolog.append(self.xml_declaration)
287 self.head = self.meta[:]
288 self.stylesheet = [self.stylesheet_call(path)
289 for path in utils.get_stylesheet_list(settings)]
290 self.body_prefix = ['</head>\n<body>\n']
291 # document title, subtitle display
292 self.body_pre_docinfo = []
293 # author, date, etc.
294 self.docinfo = []
295 self.body = []
296 self.fragment = []
297 self.body_suffix = ['</body>\n</html>\n']
298 self.section_level = 0
299 self.initial_header_level = int(settings.initial_header_level)
301 self.math_output = settings.math_output.split()
302 self.math_output_options = self.math_output[1:]
303 self.math_output = self.math_output[0].lower()
305 # A heterogenous stack used in conjunction with the tree traversal.
306 # Make sure that the pops correspond to the pushes:
307 self.context = []
308 self.topic_classes = []
309 self.colspecs = []
310 self.compact_p = True
311 self.compact_simple = False
312 self.compact_field_list = False
313 self.in_docinfo = False
314 self.in_sidebar = False
315 self.title = []
316 self.subtitle = []
317 self.header = []
318 self.footer = []
319 self.html_head = [self.content_type] # charset not interpolated
320 self.html_title = []
321 self.html_subtitle = []
322 self.html_body = []
323 self.in_document_title = 0 # len(self.body) or 0
324 self.in_mailto = False
325 self.author_in_authors = False
326 self.math_header = []
328 def astext(self):
329 return ''.join(self.head_prefix + self.head
330 + self.stylesheet + self.body_prefix
331 + self.body_pre_docinfo + self.docinfo
332 + self.body + self.body_suffix)
334 def encode(self, text):
335 """Encode special characters in `text` & return."""
336 # @@@ A codec to do these and all other HTML entities would be nice.
337 text = unicode(text)
338 return text.translate({
339 ord('&'): u'&amp;',
340 ord('<'): u'&lt;',
341 ord('"'): u'&quot;',
342 ord('>'): u'&gt;',
343 ord('@'): u'&#64;', # may thwart some address harvesters
344 # TODO: convert non-breaking space only if needed?
345 0xa0: u'&nbsp;'}) # non-breaking space
347 def cloak_mailto(self, uri):
348 """Try to hide a mailto: URL from harvesters."""
349 # Encode "@" using a URL octet reference (see RFC 1738).
350 # Further cloaking with HTML entities will be done in the
351 # `attval` function.
352 return uri.replace('@', '%40')
354 def cloak_email(self, addr):
355 """Try to hide the link text of a email link from harversters."""
356 # Surround at-signs and periods with <span> tags. ("@" has
357 # already been encoded to "&#64;" by the `encode` method.)
358 addr = addr.replace('&#64;', '<span>&#64;</span>')
359 addr = addr.replace('.', '<span>&#46;</span>')
360 return addr
362 def attval(self, text,
363 whitespace=re.compile('[\n\r\t\v\f]')):
364 """Cleanse, HTML encode, and return attribute value text."""
365 encoded = self.encode(whitespace.sub(' ', text))
366 if self.in_mailto and self.settings.cloak_email_addresses:
367 # Cloak at-signs ("%40") and periods with HTML entities.
368 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
369 encoded = encoded.replace('.', '&#46;')
370 return encoded
372 def stylesheet_call(self, path):
373 """Return code to reference or embed stylesheet file `path`"""
374 if self.settings.embed_stylesheet:
375 try:
376 content = io.FileInput(source_path=path,
377 encoding='utf-8').read()
378 self.settings.record_dependencies.add(path)
379 except IOError, err:
380 msg = u"Cannot embed stylesheet '%s': %s." % (
381 path, SafeString(err.strerror))
382 self.document.reporter.error(msg)
383 return '<--- %s --->\n' % msg
384 return self.embedded_stylesheet % content
385 # else link to style file:
386 if self.settings.stylesheet_path:
387 # adapt path relative to output (cf. config.html#stylesheet-path)
388 path = utils.relative_path(self.settings._destination, path)
389 return self.stylesheet_link % self.encode(path)
391 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
393 Construct and return a start tag given a node (id & class attributes
394 are extracted), tag name, and optional attributes.
396 tagname = tagname.lower()
397 prefix = []
398 atts = {}
399 ids = []
400 for (name, value) in attributes.items():
401 atts[name.lower()] = value
402 classes = []
403 languages = []
404 # unify class arguments and move language specification
405 for cls in node.get('classes', []) + atts.pop('class', '').split() :
406 if cls.startswith('language-'):
407 languages.append(cls[9:])
408 elif cls.strip() and cls not in classes:
409 classes.append(cls)
410 if languages:
411 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
412 atts[self.lang_attribute] = languages[0]
413 if classes:
414 atts['class'] = ' '.join(classes)
415 assert 'id' not in atts
416 ids.extend(node.get('ids', []))
417 if 'ids' in atts:
418 ids.extend(atts['ids'])
419 del atts['ids']
420 if ids:
421 atts['id'] = ids[0]
422 for id in ids[1:]:
423 # Add empty "span" elements for additional IDs. Note
424 # that we cannot use empty "a" elements because there
425 # may be targets inside of references, but nested "a"
426 # elements aren't allowed in XHTML (even if they do
427 # not all have a "href" attribute).
428 if empty:
429 # Empty tag. Insert target right in front of element.
430 prefix.append('<span id="%s"></span>' % id)
431 else:
432 # Non-empty tag. Place the auxiliary <span> tag
433 # *inside* the element, as the first child.
434 suffix += '<span id="%s"></span>' % id
435 attlist = atts.items()
436 attlist.sort()
437 parts = [tagname]
438 for name, value in attlist:
439 # value=None was used for boolean attributes without
440 # value, but this isn't supported by XHTML.
441 assert value is not None
442 if isinstance(value, list):
443 values = [unicode(v) for v in value]
444 parts.append('%s="%s"' % (name.lower(),
445 self.attval(' '.join(values))))
446 else:
447 parts.append('%s="%s"' % (name.lower(),
448 self.attval(unicode(value))))
449 if empty:
450 infix = ' /'
451 else:
452 infix = ''
453 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
455 def emptytag(self, node, tagname, suffix='\n', **attributes):
456 """Construct and return an XML-compatible empty tag."""
457 return self.starttag(node, tagname, suffix, empty=True, **attributes)
459 def set_class_on_child(self, node, class_, index=0):
461 Set class `class_` on the visible child no. index of `node`.
462 Do nothing if node has fewer children than `index`.
464 children = [n for n in node if not isinstance(n, nodes.Invisible)]
465 try:
466 child = children[index]
467 except IndexError:
468 return
469 child['classes'].append(class_)
471 def set_first_last(self, node):
472 self.set_class_on_child(node, 'first', 0)
473 self.set_class_on_child(node, 'last', -1)
475 def visit_Text(self, node):
476 text = node.astext()
477 encoded = self.encode(text)
478 if self.in_mailto and self.settings.cloak_email_addresses:
479 encoded = self.cloak_email(encoded)
480 self.body.append(encoded)
482 def depart_Text(self, node):
483 pass
485 def visit_abbreviation(self, node):
486 # @@@ implementation incomplete ("title" attribute)
487 self.body.append(self.starttag(node, 'abbr', ''))
489 def depart_abbreviation(self, node):
490 self.body.append('</abbr>')
492 def visit_acronym(self, node):
493 # @@@ implementation incomplete ("title" attribute)
494 self.body.append(self.starttag(node, 'acronym', ''))
496 def depart_acronym(self, node):
497 self.body.append('</acronym>')
499 def visit_address(self, node):
500 self.visit_docinfo_item(node, 'address', meta=False)
501 self.body.append(self.starttag(node, 'pre', CLASS='address'))
503 def depart_address(self, node):
504 self.body.append('\n</pre>\n')
505 self.depart_docinfo_item()
507 def visit_admonition(self, node):
508 node['classes'].insert(0, 'admonition')
509 self.body.append(self.starttag(node, 'div'))
510 self.set_first_last(node)
512 def depart_admonition(self, node=None):
513 self.body.append('</div>\n')
515 attribution_formats = {'dash': ('&mdash;', ''),
516 'parentheses': ('(', ')'),
517 'parens': ('(', ')'),
518 'none': ('', '')}
520 def visit_attribution(self, node):
521 prefix, suffix = self.attribution_formats[self.settings.attribution]
522 self.context.append(suffix)
523 self.body.append(
524 self.starttag(node, 'p', prefix, CLASS='attribution'))
526 def depart_attribution(self, node):
527 self.body.append(self.context.pop() + '</p>\n')
529 def visit_author(self, node):
530 if isinstance(node.parent, nodes.authors):
531 if self.author_in_authors:
532 self.body.append('\n<br />')
533 else:
534 self.visit_docinfo_item(node, 'author')
536 def depart_author(self, node):
537 if isinstance(node.parent, nodes.authors):
538 self.author_in_authors = True
539 else:
540 self.depart_docinfo_item()
542 def visit_authors(self, node):
543 self.visit_docinfo_item(node, 'authors')
544 self.author_in_authors = False # initialize
546 def depart_authors(self, node):
547 self.depart_docinfo_item()
549 def visit_block_quote(self, node):
550 self.body.append(self.starttag(node, 'blockquote'))
552 def depart_block_quote(self, node):
553 self.body.append('</blockquote>\n')
555 def check_simple_list(self, node):
556 """Check for a simple list that can be rendered compactly."""
557 visitor = SimpleListChecker(self.document)
558 try:
559 node.walk(visitor)
560 except nodes.NodeFound:
561 return None
562 else:
563 return 1
565 def is_compactable(self, node):
566 return ('compact' in node['classes']
567 or (self.settings.compact_lists
568 and 'open' not in node['classes']
569 and (self.compact_simple
570 or self.topic_classes == ['contents']
571 or self.check_simple_list(node))))
573 def visit_bullet_list(self, node):
574 atts = {}
575 old_compact_simple = self.compact_simple
576 self.context.append((self.compact_simple, self.compact_p))
577 self.compact_p = None
578 self.compact_simple = self.is_compactable(node)
579 if self.compact_simple and not old_compact_simple:
580 atts['class'] = 'simple'
581 self.body.append(self.starttag(node, 'ul', **atts))
583 def depart_bullet_list(self, node):
584 self.compact_simple, self.compact_p = self.context.pop()
585 self.body.append('</ul>\n')
587 def visit_caption(self, node):
588 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
590 def depart_caption(self, node):
591 self.body.append('</p>\n')
593 def visit_citation(self, node):
594 self.body.append(self.starttag(node, 'table',
595 CLASS='docutils citation',
596 frame="void", rules="none"))
597 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
598 '<tbody valign="top">\n'
599 '<tr>')
600 self.footnote_backrefs(node)
602 def depart_citation(self, node):
603 self.body.append('</td></tr>\n'
604 '</tbody>\n</table>\n')
606 def visit_citation_reference(self, node):
607 href = '#'
608 if 'refid' in node:
609 href += node['refid']
610 elif 'refname' in node:
611 href += self.document.nameids[node['refname']]
612 # else: # TODO system message (or already in the transform)?
613 # 'Citation reference missing.'
614 self.body.append(self.starttag(
615 node, 'a', '[', CLASS='citation-reference', href=href))
617 def depart_citation_reference(self, node):
618 self.body.append(']</a>')
620 def visit_classifier(self, node):
621 self.body.append(' <span class="classifier-delimiter">:</span> ')
622 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
624 def depart_classifier(self, node):
625 self.body.append('</span>')
627 def visit_colspec(self, node):
628 self.colspecs.append(node)
629 # "stubs" list is an attribute of the tgroup element:
630 node.parent.stubs.append(node.attributes.get('stub'))
632 def depart_colspec(self, node):
633 pass
635 def write_colspecs(self):
636 width = 0
637 for node in self.colspecs:
638 width += node['colwidth']
639 for node in self.colspecs:
640 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
641 self.body.append(self.emptytag(node, 'col',
642 width='%i%%' % colwidth))
643 self.colspecs = []
645 def visit_comment(self, node,
646 sub=re.compile('-(?=-)').sub):
647 """Escape double-dashes in comment text."""
648 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
649 # Content already processed:
650 raise nodes.SkipNode
652 def visit_compound(self, node):
653 self.body.append(self.starttag(node, 'div', CLASS='compound'))
654 if len(node) > 1:
655 node[0]['classes'].append('compound-first')
656 node[-1]['classes'].append('compound-last')
657 for child in node[1:-1]:
658 child['classes'].append('compound-middle')
660 def depart_compound(self, node):
661 self.body.append('</div>\n')
663 def visit_container(self, node):
664 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
666 def depart_container(self, node):
667 self.body.append('</div>\n')
669 def visit_contact(self, node):
670 self.visit_docinfo_item(node, 'contact', meta=False)
672 def depart_contact(self, node):
673 self.depart_docinfo_item()
675 def visit_copyright(self, node):
676 self.visit_docinfo_item(node, 'copyright')
678 def depart_copyright(self, node):
679 self.depart_docinfo_item()
681 def visit_date(self, node):
682 self.visit_docinfo_item(node, 'date')
684 def depart_date(self, node):
685 self.depart_docinfo_item()
687 def visit_decoration(self, node):
688 pass
690 def depart_decoration(self, node):
691 pass
693 def visit_definition(self, node):
694 self.body.append('</dt>\n')
695 self.body.append(self.starttag(node, 'dd', ''))
696 self.set_first_last(node)
698 def depart_definition(self, node):
699 self.body.append('</dd>\n')
701 def visit_definition_list(self, node):
702 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
704 def depart_definition_list(self, node):
705 self.body.append('</dl>\n')
707 def visit_definition_list_item(self, node):
708 # pass class arguments, ids and names to definition term:
709 node.children[0]['classes'] = (
710 node.get('classes', []) + node.children[0].get('classes', []))
711 node.children[0]['ids'] = (
712 node.get('ids', []) + node.children[0].get('ids', []))
713 node.children[0]['names'] = (
714 node.get('names', []) + node.children[0].get('names', []))
716 def depart_definition_list_item(self, node):
717 pass
719 def visit_description(self, node):
720 self.body.append(self.starttag(node, 'td', ''))
721 self.set_first_last(node)
723 def depart_description(self, node):
724 self.body.append('</td>')
726 def visit_docinfo(self, node):
727 self.context.append(len(self.body))
728 self.body.append(self.starttag(node, 'table',
729 CLASS='docinfo',
730 frame="void", rules="none"))
731 self.body.append('<col class="docinfo-name" />\n'
732 '<col class="docinfo-content" />\n'
733 '<tbody valign="top">\n')
734 self.in_docinfo = True
736 def depart_docinfo(self, node):
737 self.body.append('</tbody>\n</table>\n')
738 self.in_docinfo = False
739 start = self.context.pop()
740 self.docinfo = self.body[start:]
741 self.body = []
743 def visit_docinfo_item(self, node, name, meta=True):
744 if meta:
745 meta_tag = '<meta name="%s" content="%s" />\n' \
746 % (name, self.attval(node.astext()))
747 self.add_meta(meta_tag)
748 self.body.append(self.starttag(node, 'tr', ''))
749 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
750 % self.language.labels[name])
751 if len(node):
752 if isinstance(node[0], nodes.Element):
753 node[0]['classes'].append('first')
754 if isinstance(node[-1], nodes.Element):
755 node[-1]['classes'].append('last')
757 def depart_docinfo_item(self):
758 self.body.append('</td></tr>\n')
760 def visit_doctest_block(self, node):
761 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
763 def depart_doctest_block(self, node):
764 self.body.append('\n</pre>\n')
766 def visit_document(self, node):
767 self.head.append('<title>%s</title>\n'
768 % self.encode(node.get('title', '')))
770 def depart_document(self, node):
771 self.head_prefix.extend([self.doctype,
772 self.head_prefix_template %
773 {'lang': self.settings.language_code}])
774 self.html_prolog.append(self.doctype)
775 self.meta.insert(0, self.content_type % self.settings.output_encoding)
776 self.head.insert(0, self.content_type % self.settings.output_encoding)
777 if self.math_header:
778 if self.math_output == 'mathjax':
779 self.head.extend(self.math_header)
780 else:
781 self.stylesheet.extend(self.math_header)
782 # skip content-type meta tag with interpolated charset value:
783 self.html_head.extend(self.head[1:])
784 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
785 self.body_suffix.insert(0, '</div>\n')
786 self.fragment.extend(self.body) # self.fragment is the "naked" body
787 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
788 + self.docinfo + self.body
789 + self.body_suffix[:-1])
790 assert not self.context, 'len(context) = %s' % len(self.context)
792 def visit_emphasis(self, node):
793 self.body.append(self.starttag(node, 'em', ''))
795 def depart_emphasis(self, node):
796 self.body.append('</em>')
798 def visit_entry(self, node):
799 atts = {'class': []}
800 if isinstance(node.parent.parent, nodes.thead):
801 atts['class'].append('head')
802 if node.parent.parent.parent.stubs[node.parent.column]:
803 # "stubs" list is an attribute of the tgroup element
804 atts['class'].append('stub')
805 if atts['class']:
806 tagname = 'th'
807 atts['class'] = ' '.join(atts['class'])
808 else:
809 tagname = 'td'
810 del atts['class']
811 node.parent.column += 1
812 if 'morerows' in node:
813 atts['rowspan'] = node['morerows'] + 1
814 if 'morecols' in node:
815 atts['colspan'] = node['morecols'] + 1
816 node.parent.column += node['morecols']
817 self.body.append(self.starttag(node, tagname, '', **atts))
818 self.context.append('</%s>\n' % tagname.lower())
819 if len(node) == 0: # empty cell
820 self.body.append('&nbsp;')
821 self.set_first_last(node)
823 def depart_entry(self, node):
824 self.body.append(self.context.pop())
826 def visit_enumerated_list(self, node):
828 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
829 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
830 usable.
832 atts = {}
833 if 'start' in node:
834 atts['start'] = node['start']
835 if 'enumtype' in node:
836 atts['class'] = node['enumtype']
837 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
838 # single "format" attribute? Use CSS2?
839 old_compact_simple = self.compact_simple
840 self.context.append((self.compact_simple, self.compact_p))
841 self.compact_p = None
842 self.compact_simple = self.is_compactable(node)
843 if self.compact_simple and not old_compact_simple:
844 atts['class'] = (atts.get('class', '') + ' simple').strip()
845 self.body.append(self.starttag(node, 'ol', **atts))
847 def depart_enumerated_list(self, node):
848 self.compact_simple, self.compact_p = self.context.pop()
849 self.body.append('</ol>\n')
851 def visit_field(self, node):
852 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
854 def depart_field(self, node):
855 self.body.append('</tr>\n')
857 def visit_field_body(self, node):
858 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
859 self.set_class_on_child(node, 'first', 0)
860 field = node.parent
861 if (self.compact_field_list or
862 isinstance(field.parent, nodes.docinfo) or
863 field.parent.index(field) == len(field.parent) - 1):
864 # If we are in a compact list, the docinfo, or if this is
865 # the last field of the field list, do not add vertical
866 # space after last element.
867 self.set_class_on_child(node, 'last', -1)
869 def depart_field_body(self, node):
870 self.body.append('</td>\n')
872 def visit_field_list(self, node):
873 self.context.append((self.compact_field_list, self.compact_p))
874 self.compact_p = None
875 if 'compact' in node['classes']:
876 self.compact_field_list = True
877 elif (self.settings.compact_field_lists
878 and 'open' not in node['classes']):
879 self.compact_field_list = True
880 if self.compact_field_list:
881 for field in node:
882 field_body = field[-1]
883 assert isinstance(field_body, nodes.field_body)
884 children = [n for n in field_body
885 if not isinstance(n, nodes.Invisible)]
886 if not (len(children) == 0 or
887 len(children) == 1 and
888 isinstance(children[0],
889 (nodes.paragraph, nodes.line_block))):
890 self.compact_field_list = False
891 break
892 self.body.append(self.starttag(node, 'table', frame='void',
893 rules='none',
894 CLASS='docutils field-list'))
895 self.body.append('<col class="field-name" />\n'
896 '<col class="field-body" />\n'
897 '<tbody valign="top">\n')
899 def depart_field_list(self, node):
900 self.body.append('</tbody>\n</table>\n')
901 self.compact_field_list, self.compact_p = self.context.pop()
903 def visit_field_name(self, node):
904 atts = {}
905 if self.in_docinfo:
906 atts['class'] = 'docinfo-name'
907 else:
908 atts['class'] = 'field-name'
909 if ( self.settings.field_name_limit
910 and len(node.astext()) > self.settings.field_name_limit):
911 atts['colspan'] = 2
912 self.context.append('</tr>\n'
913 + self.starttag(node.parent, 'tr', '',
914 CLASS='field')
915 + '<td>&nbsp;</td>')
916 else:
917 self.context.append('')
918 self.body.append(self.starttag(node, 'th', '', **atts))
920 def depart_field_name(self, node):
921 self.body.append(':</th>')
922 self.body.append(self.context.pop())
924 def visit_figure(self, node):
925 atts = {'class': 'figure'}
926 if node.get('width'):
927 atts['style'] = 'width: %s' % node['width']
928 if node.get('align'):
929 atts['class'] += " align-" + node['align']
930 self.body.append(self.starttag(node, 'div', **atts))
932 def depart_figure(self, node):
933 self.body.append('</div>\n')
935 def visit_footer(self, node):
936 self.context.append(len(self.body))
938 def depart_footer(self, node):
939 start = self.context.pop()
940 footer = [self.starttag(node, 'div', CLASS='footer'),
941 '<hr class="footer" />\n']
942 footer.extend(self.body[start:])
943 footer.append('\n</div>\n')
944 self.footer.extend(footer)
945 self.body_suffix[:0] = footer
946 del self.body[start:]
948 def visit_footnote(self, node):
949 self.body.append(self.starttag(node, 'table',
950 CLASS='docutils footnote',
951 frame="void", rules="none"))
952 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
953 '<tbody valign="top">\n'
954 '<tr>')
955 self.footnote_backrefs(node)
957 def footnote_backrefs(self, node):
958 backlinks = []
959 backrefs = node['backrefs']
960 if self.settings.footnote_backlinks and backrefs:
961 if len(backrefs) == 1:
962 self.context.append('')
963 self.context.append('</a>')
964 self.context.append('<a class="fn-backref" href="#%s">'
965 % backrefs[0])
966 else:
967 i = 1
968 for backref in backrefs:
969 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
970 % (backref, i))
971 i += 1
972 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
973 self.context += ['', '']
974 else:
975 self.context.append('')
976 self.context += ['', '']
977 # If the node does not only consist of a label.
978 if len(node) > 1:
979 # If there are preceding backlinks, we do not set class
980 # 'first', because we need to retain the top-margin.
981 if not backlinks:
982 node[1]['classes'].append('first')
983 node[-1]['classes'].append('last')
985 def depart_footnote(self, node):
986 self.body.append('</td></tr>\n'
987 '</tbody>\n</table>\n')
989 def visit_footnote_reference(self, node):
990 href = '#' + node['refid']
991 format = self.settings.footnote_references
992 if format == 'brackets':
993 suffix = '['
994 self.context.append(']')
995 else:
996 assert format == 'superscript'
997 suffix = '<sup>'
998 self.context.append('</sup>')
999 self.body.append(self.starttag(node, 'a', suffix,
1000 CLASS='footnote-reference', href=href))
1002 def depart_footnote_reference(self, node):
1003 self.body.append(self.context.pop() + '</a>')
1005 def visit_generated(self, node):
1006 pass
1008 def depart_generated(self, node):
1009 pass
1011 def visit_header(self, node):
1012 self.context.append(len(self.body))
1014 def depart_header(self, node):
1015 start = self.context.pop()
1016 header = [self.starttag(node, 'div', CLASS='header')]
1017 header.extend(self.body[start:])
1018 header.append('\n<hr class="header"/>\n</div>\n')
1019 self.body_prefix.extend(header)
1020 self.header.extend(header)
1021 del self.body[start:]
1023 # Image types to place in an <object> element
1024 # SVG not supported by IE up to version 8
1025 # (html4css1 strives for IE6 compatibility)
1026 object_image_types = {'.svg': 'image/svg+xml',
1027 '.swf': 'application/x-shockwave-flash'}
1029 def visit_image(self, node):
1030 atts = {}
1031 uri = node['uri']
1032 ext = os.path.splitext(uri)[1].lower()
1033 if ext in self.object_image_types: # ('.svg', '.swf'):
1034 atts['data'] = uri
1035 atts['type'] = self.object_image_types[ext]
1036 else:
1037 atts['src'] = uri
1038 atts['alt'] = node.get('alt', uri)
1039 # image size
1040 if 'width' in node:
1041 atts['width'] = node['width']
1042 if 'height' in node:
1043 atts['height'] = node['height']
1044 if 'scale' in node:
1045 if (PIL and not ('width' in node and 'height' in node)
1046 and self.settings.file_insertion_enabled):
1047 imagepath = urllib.url2pathname(uri)
1048 try:
1049 img = PIL.Image.open(
1050 imagepath.encode(sys.getfilesystemencoding()))
1051 except (IOError, UnicodeEncodeError):
1052 pass # TODO: warn?
1053 else:
1054 self.settings.record_dependencies.add(
1055 imagepath.replace('\\', '/'))
1056 if 'width' not in atts:
1057 atts['width'] = '%dpx' % img.size[0]
1058 if 'height' not in atts:
1059 atts['height'] = '%dpx' % img.size[1]
1060 del img
1061 for att_name in 'width', 'height':
1062 if att_name in atts:
1063 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1064 assert match
1065 atts[att_name] = '%s%s' % (
1066 float(match.group(1)) * (float(node['scale']) / 100),
1067 match.group(2))
1068 style = []
1069 for att_name in 'width', 'height':
1070 if att_name in atts:
1071 if re.match(r'^[0-9.]+$', atts[att_name]):
1072 # Interpret unitless values as pixels.
1073 atts[att_name] += 'px'
1074 style.append('%s: %s;' % (att_name, atts[att_name]))
1075 del atts[att_name]
1076 if style:
1077 atts['style'] = ' '.join(style)
1078 if (isinstance(node.parent, nodes.TextElement) or
1079 (isinstance(node.parent, nodes.reference) and
1080 not isinstance(node.parent.parent, nodes.TextElement))):
1081 # Inline context or surrounded by <a>...</a>.
1082 suffix = ''
1083 else:
1084 suffix = '\n'
1085 if 'align' in node:
1086 atts['class'] = 'align-%s' % node['align']
1087 if ext in self.object_image_types: # ('.svg', '.swf')
1088 # do NOT use an empty tag: incorrect rendering in browsers
1089 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1090 node.get('alt', uri) + '</object>' + suffix)
1091 else:
1092 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1094 def depart_image(self, node):
1095 # self.body.append(self.context.pop())
1096 pass
1098 def visit_inline(self, node):
1099 self.body.append(self.starttag(node, 'span', ''))
1101 def depart_inline(self, node):
1102 self.body.append('</span>')
1104 def visit_label(self, node):
1105 # Context added in footnote_backrefs.
1106 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1107 CLASS='label'))
1109 def depart_label(self, node):
1110 # Context added in footnote_backrefs.
1111 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1113 def visit_legend(self, node):
1114 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1116 def depart_legend(self, node):
1117 self.body.append('</div>\n')
1119 def visit_line(self, node):
1120 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1121 if not len(node):
1122 self.body.append('<br />')
1124 def depart_line(self, node):
1125 self.body.append('</div>\n')
1127 def visit_line_block(self, node):
1128 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1130 def depart_line_block(self, node):
1131 self.body.append('</div>\n')
1133 def visit_list_item(self, node):
1134 self.body.append(self.starttag(node, 'li', ''))
1135 if len(node):
1136 node[0]['classes'].append('first')
1138 def depart_list_item(self, node):
1139 self.body.append('</li>\n')
1141 def visit_literal(self, node):
1142 # special case: "code" role
1143 classes = node.get('classes', [])
1144 if 'code' in classes:
1145 # filter 'code' from class arguments
1146 node['classes'] = [cls for cls in classes if cls != 'code']
1147 self.body.append(self.starttag(node, 'code', ''))
1148 return
1149 self.body.append(
1150 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1151 text = node.astext()
1152 for token in self.words_and_spaces.findall(text):
1153 if token.strip():
1154 # Protect text like "--an-option" and the regular expression
1155 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1156 if self.sollbruchstelle.search(token):
1157 self.body.append('<span class="pre">%s</span>'
1158 % self.encode(token))
1159 else:
1160 self.body.append(self.encode(token))
1161 elif token in ('\n', ' '):
1162 # Allow breaks at whitespace:
1163 self.body.append(token)
1164 else:
1165 # Protect runs of multiple spaces; the last space can wrap:
1166 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1167 self.body.append('</tt>')
1168 # Content already processed:
1169 raise nodes.SkipNode
1171 def depart_literal(self, node):
1172 # skipped unless literal element is from "code" role:
1173 self.body.append('</code>')
1175 def visit_literal_block(self, node):
1176 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1178 def depart_literal_block(self, node):
1179 self.body.append('\n</pre>\n')
1181 def visit_math(self, node, math_env=''):
1182 # If the method is called from visit_math_block(), math_env != ''.
1184 # As there is no native HTML math support, we provide alternatives:
1185 # LaTeX and MathJax math_output modes simply wrap the content,
1186 # HTML and MathML math_output modes also convert the math_code.
1187 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1188 self.document.reporter.error(
1189 'math-output format "%s" not supported '
1190 'falling back to "latex"'% self.math_output)
1191 self.math_output = 'latex'
1193 # HTML container
1194 tags = {# math_output: (block, inline, class-arguments)
1195 'mathml': ('div', '', ''),
1196 'html': ('div', 'span', 'formula'),
1197 'mathjax': ('div', 'span', 'math'),
1198 'latex': ('pre', 'tt', 'math'),
1200 tag = tags[self.math_output][math_env == '']
1201 clsarg = tags[self.math_output][2]
1202 # LaTeX container
1203 wrappers = {# math_mode: (inline, block)
1204 'mathml': (None, None),
1205 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1206 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1207 'latex': (None, None),
1209 wrapper = wrappers[self.math_output][math_env != '']
1210 # get and wrap content
1211 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1212 if wrapper and math_env:
1213 math_code = wrapper % (math_env, math_code, math_env)
1214 elif wrapper:
1215 math_code = wrapper % math_code
1216 # settings and conversion
1217 if self.math_output in ('latex', 'mathjax'):
1218 math_code = self.encode(math_code)
1219 if self.math_output == 'mathjax' and not self.math_header:
1220 if self.math_output_options:
1221 self.mathjax_url = self.math_output_options[0]
1222 self.math_header = [self.mathjax_script % self.mathjax_url]
1223 elif self.math_output == 'html':
1224 if self.math_output_options and not self.math_header:
1225 self.math_header = [self.stylesheet_call(
1226 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1227 for s in self.math_output_options[0].split(',')]
1228 # TODO: fix display mode in matrices and fractions
1229 math2html.DocumentParameters.displaymode = (math_env != '')
1230 math_code = math2html.math2html(math_code)
1231 elif self.math_output == 'mathml':
1232 self.doctype = self.doctype_mathml
1233 self.content_type = self.content_type_mathml
1234 try:
1235 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1236 math_code = ''.join(mathml_tree.xml())
1237 except SyntaxError, err:
1238 err_node = self.document.reporter.error(err, base_node=node)
1239 self.visit_system_message(err_node)
1240 self.body.append(self.starttag(node, 'p'))
1241 self.body.append(u','.join(err.args))
1242 self.body.append('</p>\n')
1243 self.body.append(self.starttag(node, 'pre',
1244 CLASS='literal-block'))
1245 self.body.append(self.encode(math_code))
1246 self.body.append('\n</pre>\n')
1247 self.depart_system_message(err_node)
1248 raise nodes.SkipNode
1249 # append to document body
1250 if tag:
1251 self.body.append(self.starttag(node, tag,
1252 suffix='\n'*bool(math_env),
1253 CLASS=clsarg))
1254 self.body.append(math_code)
1255 if math_env: # block mode (equation, display)
1256 self.body.append('\n')
1257 if tag:
1258 self.body.append('</%s>' % tag)
1259 if math_env:
1260 self.body.append('\n')
1261 # Content already processed:
1262 raise nodes.SkipNode
1264 def depart_math(self, node):
1265 pass # never reached
1267 def visit_math_block(self, node):
1268 # print node.astext().encode('utf8')
1269 math_env = pick_math_environment(node.astext())
1270 self.visit_math(node, math_env=math_env)
1272 def depart_math_block(self, node):
1273 pass # never reached
1275 def visit_meta(self, node):
1276 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1277 self.add_meta(meta)
1279 def depart_meta(self, node):
1280 pass
1282 def add_meta(self, tag):
1283 self.meta.append(tag)
1284 self.head.append(tag)
1286 def visit_option(self, node):
1287 if self.context[-1]:
1288 self.body.append(', ')
1289 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1291 def depart_option(self, node):
1292 self.body.append('</span>')
1293 self.context[-1] += 1
1295 def visit_option_argument(self, node):
1296 self.body.append(node.get('delimiter', ' '))
1297 self.body.append(self.starttag(node, 'var', ''))
1299 def depart_option_argument(self, node):
1300 self.body.append('</var>')
1302 def visit_option_group(self, node):
1303 atts = {}
1304 if ( self.settings.option_limit
1305 and len(node.astext()) > self.settings.option_limit):
1306 atts['colspan'] = 2
1307 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1308 else:
1309 self.context.append('')
1310 self.body.append(
1311 self.starttag(node, 'td', CLASS='option-group', **atts))
1312 self.body.append('<kbd>')
1313 self.context.append(0) # count number of options
1315 def depart_option_group(self, node):
1316 self.context.pop()
1317 self.body.append('</kbd></td>\n')
1318 self.body.append(self.context.pop())
1320 def visit_option_list(self, node):
1321 self.body.append(
1322 self.starttag(node, 'table', CLASS='docutils option-list',
1323 frame="void", rules="none"))
1324 self.body.append('<col class="option" />\n'
1325 '<col class="description" />\n'
1326 '<tbody valign="top">\n')
1328 def depart_option_list(self, node):
1329 self.body.append('</tbody>\n</table>\n')
1331 def visit_option_list_item(self, node):
1332 self.body.append(self.starttag(node, 'tr', ''))
1334 def depart_option_list_item(self, node):
1335 self.body.append('</tr>\n')
1337 def visit_option_string(self, node):
1338 pass
1340 def depart_option_string(self, node):
1341 pass
1343 def visit_organization(self, node):
1344 self.visit_docinfo_item(node, 'organization')
1346 def depart_organization(self, node):
1347 self.depart_docinfo_item()
1349 def should_be_compact_paragraph(self, node):
1351 Determine if the <p> tags around paragraph ``node`` can be omitted.
1353 if (isinstance(node.parent, nodes.document) or
1354 isinstance(node.parent, nodes.compound)):
1355 # Never compact paragraphs in document or compound.
1356 return False
1357 for key, value in node.attlist():
1358 if (node.is_not_default(key) and
1359 not (key == 'classes' and value in
1360 ([], ['first'], ['last'], ['first', 'last']))):
1361 # Attribute which needs to survive.
1362 return False
1363 first = isinstance(node.parent[0], nodes.label) # skip label
1364 for child in node.parent.children[first:]:
1365 # only first paragraph can be compact
1366 if isinstance(child, nodes.Invisible):
1367 continue
1368 if child is node:
1369 break
1370 return False
1371 parent_length = len([n for n in node.parent if not isinstance(
1372 n, (nodes.Invisible, nodes.label))])
1373 if ( self.compact_simple
1374 or self.compact_field_list
1375 or self.compact_p and parent_length == 1):
1376 return True
1377 return False
1379 def visit_paragraph(self, node):
1380 if self.should_be_compact_paragraph(node):
1381 self.context.append('')
1382 else:
1383 self.body.append(self.starttag(node, 'p', ''))
1384 self.context.append('</p>\n')
1386 def depart_paragraph(self, node):
1387 self.body.append(self.context.pop())
1389 def visit_problematic(self, node):
1390 if node.hasattr('refid'):
1391 self.body.append('<a href="#%s">' % node['refid'])
1392 self.context.append('</a>')
1393 else:
1394 self.context.append('')
1395 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1397 def depart_problematic(self, node):
1398 self.body.append('</span>')
1399 self.body.append(self.context.pop())
1401 def visit_raw(self, node):
1402 if 'html' in node.get('format', '').split():
1403 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1404 if node['classes']:
1405 self.body.append(self.starttag(node, t, suffix=''))
1406 self.body.append(node.astext())
1407 if node['classes']:
1408 self.body.append('</%s>' % t)
1409 # Keep non-HTML raw text out of output:
1410 raise nodes.SkipNode
1412 def visit_reference(self, node):
1413 atts = {'class': 'reference'}
1414 if 'refuri' in node:
1415 atts['href'] = node['refuri']
1416 if ( self.settings.cloak_email_addresses
1417 and atts['href'].startswith('mailto:')):
1418 atts['href'] = self.cloak_mailto(atts['href'])
1419 self.in_mailto = True
1420 atts['class'] += ' external'
1421 else:
1422 assert 'refid' in node, \
1423 'References must have "refuri" or "refid" attribute.'
1424 atts['href'] = '#' + node['refid']
1425 atts['class'] += ' internal'
1426 if not isinstance(node.parent, nodes.TextElement):
1427 assert len(node) == 1 and isinstance(node[0], nodes.image)
1428 atts['class'] += ' image-reference'
1429 self.body.append(self.starttag(node, 'a', '', **atts))
1431 def depart_reference(self, node):
1432 self.body.append('</a>')
1433 if not isinstance(node.parent, nodes.TextElement):
1434 self.body.append('\n')
1435 self.in_mailto = False
1437 def visit_revision(self, node):
1438 self.visit_docinfo_item(node, 'revision', meta=False)
1440 def depart_revision(self, node):
1441 self.depart_docinfo_item()
1443 def visit_row(self, node):
1444 self.body.append(self.starttag(node, 'tr', ''))
1445 node.column = 0
1447 def depart_row(self, node):
1448 self.body.append('</tr>\n')
1450 def visit_rubric(self, node):
1451 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1453 def depart_rubric(self, node):
1454 self.body.append('</p>\n')
1456 def visit_section(self, node):
1457 self.section_level += 1
1458 self.body.append(
1459 self.starttag(node, 'div', CLASS='section'))
1461 def depart_section(self, node):
1462 self.section_level -= 1
1463 self.body.append('</div>\n')
1465 def visit_sidebar(self, node):
1466 self.body.append(
1467 self.starttag(node, 'div', CLASS='sidebar'))
1468 self.set_first_last(node)
1469 self.in_sidebar = True
1471 def depart_sidebar(self, node):
1472 self.body.append('</div>\n')
1473 self.in_sidebar = False
1475 def visit_status(self, node):
1476 self.visit_docinfo_item(node, 'status', meta=False)
1478 def depart_status(self, node):
1479 self.depart_docinfo_item()
1481 def visit_strong(self, node):
1482 self.body.append(self.starttag(node, 'strong', ''))
1484 def depart_strong(self, node):
1485 self.body.append('</strong>')
1487 def visit_subscript(self, node):
1488 self.body.append(self.starttag(node, 'sub', ''))
1490 def depart_subscript(self, node):
1491 self.body.append('</sub>')
1493 def visit_substitution_definition(self, node):
1494 """Internal only."""
1495 raise nodes.SkipNode
1497 def visit_substitution_reference(self, node):
1498 self.unimplemented_visit(node)
1500 def visit_subtitle(self, node):
1501 if isinstance(node.parent, nodes.sidebar):
1502 self.body.append(self.starttag(node, 'p', '',
1503 CLASS='sidebar-subtitle'))
1504 self.context.append('</p>\n')
1505 elif isinstance(node.parent, nodes.document):
1506 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1507 self.context.append('</h2>\n')
1508 self.in_document_title = len(self.body)
1509 elif isinstance(node.parent, nodes.section):
1510 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1511 self.body.append(
1512 self.starttag(node, tag, '', CLASS='section-subtitle') +
1513 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1514 self.context.append('</span></%s>\n' % tag)
1516 def depart_subtitle(self, node):
1517 self.body.append(self.context.pop())
1518 if self.in_document_title:
1519 self.subtitle = self.body[self.in_document_title:-1]
1520 self.in_document_title = 0
1521 self.body_pre_docinfo.extend(self.body)
1522 self.html_subtitle.extend(self.body)
1523 del self.body[:]
1525 def visit_superscript(self, node):
1526 self.body.append(self.starttag(node, 'sup', ''))
1528 def depart_superscript(self, node):
1529 self.body.append('</sup>')
1531 def visit_system_message(self, node):
1532 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1533 self.body.append('<p class="system-message-title">')
1534 backref_text = ''
1535 if len(node['backrefs']):
1536 backrefs = node['backrefs']
1537 if len(backrefs) == 1:
1538 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1539 % backrefs[0])
1540 else:
1541 i = 1
1542 backlinks = []
1543 for backref in backrefs:
1544 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1545 i += 1
1546 backref_text = ('; <em>backlinks: %s</em>'
1547 % ', '.join(backlinks))
1548 if node.hasattr('line'):
1549 line = ', line %s' % node['line']
1550 else:
1551 line = ''
1552 self.body.append('System Message: %s/%s '
1553 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1554 % (node['type'], node['level'],
1555 self.encode(node['source']), line, backref_text))
1557 def depart_system_message(self, node):
1558 self.body.append('</div>\n')
1560 def visit_table(self, node):
1561 self.context.append(self.compact_p)
1562 self.compact_p = True
1563 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1564 self.body.append(
1565 self.starttag(node, 'table', CLASS=classes, border="1"))
1567 def depart_table(self, node):
1568 self.compact_p = self.context.pop()
1569 self.body.append('</table>\n')
1571 def visit_target(self, node):
1572 if not ('refuri' in node or 'refid' in node
1573 or 'refname' in node):
1574 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1575 self.context.append('</span>')
1576 else:
1577 self.context.append('')
1579 def depart_target(self, node):
1580 self.body.append(self.context.pop())
1582 def visit_tbody(self, node):
1583 self.write_colspecs()
1584 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1585 self.body.append(self.starttag(node, 'tbody', valign='top'))
1587 def depart_tbody(self, node):
1588 self.body.append('</tbody>\n')
1590 def visit_term(self, node):
1591 self.body.append(self.starttag(node, 'dt', ''))
1593 def depart_term(self, node):
1595 Leave the end tag to `self.visit_definition()`, in case there's a
1596 classifier.
1598 pass
1600 def visit_tgroup(self, node):
1601 # Mozilla needs <colgroup>:
1602 self.body.append(self.starttag(node, 'colgroup'))
1603 # Appended by thead or tbody:
1604 self.context.append('</colgroup>\n')
1605 node.stubs = []
1607 def depart_tgroup(self, node):
1608 pass
1610 def visit_thead(self, node):
1611 self.write_colspecs()
1612 self.body.append(self.context.pop()) # '</colgroup>\n'
1613 # There may or may not be a <thead>; this is for <tbody> to use:
1614 self.context.append('')
1615 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1617 def depart_thead(self, node):
1618 self.body.append('</thead>\n')
1620 def visit_title(self, node):
1621 """Only 6 section levels are supported by HTML."""
1622 check_id = 0 # TODO: is this a bool (False) or a counter?
1623 close_tag = '</p>\n'
1624 if isinstance(node.parent, nodes.topic):
1625 self.body.append(
1626 self.starttag(node, 'p', '', CLASS='topic-title first'))
1627 elif isinstance(node.parent, nodes.sidebar):
1628 self.body.append(
1629 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1630 elif isinstance(node.parent, nodes.Admonition):
1631 self.body.append(
1632 self.starttag(node, 'p', '', CLASS='admonition-title'))
1633 elif isinstance(node.parent, nodes.table):
1634 self.body.append(
1635 self.starttag(node, 'caption', ''))
1636 close_tag = '</caption>\n'
1637 elif isinstance(node.parent, nodes.document):
1638 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1639 close_tag = '</h1>\n'
1640 self.in_document_title = len(self.body)
1641 else:
1642 assert isinstance(node.parent, nodes.section)
1643 h_level = self.section_level + self.initial_header_level - 1
1644 atts = {}
1645 if (len(node.parent) >= 2 and
1646 isinstance(node.parent[1], nodes.subtitle)):
1647 atts['CLASS'] = 'with-subtitle'
1648 self.body.append(
1649 self.starttag(node, 'h%s' % h_level, '', **atts))
1650 atts = {}
1651 if node.hasattr('refid'):
1652 atts['class'] = 'toc-backref'
1653 atts['href'] = '#' + node['refid']
1654 if atts:
1655 self.body.append(self.starttag({}, 'a', '', **atts))
1656 close_tag = '</a></h%s>\n' % (h_level)
1657 else:
1658 close_tag = '</h%s>\n' % (h_level)
1659 self.context.append(close_tag)
1661 def depart_title(self, node):
1662 self.body.append(self.context.pop())
1663 if self.in_document_title:
1664 self.title = self.body[self.in_document_title:-1]
1665 self.in_document_title = 0
1666 self.body_pre_docinfo.extend(self.body)
1667 self.html_title.extend(self.body)
1668 del self.body[:]
1670 def visit_title_reference(self, node):
1671 self.body.append(self.starttag(node, 'cite', ''))
1673 def depart_title_reference(self, node):
1674 self.body.append('</cite>')
1676 def visit_topic(self, node):
1677 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1678 self.topic_classes = node['classes']
1680 def depart_topic(self, node):
1681 self.body.append('</div>\n')
1682 self.topic_classes = []
1684 def visit_transition(self, node):
1685 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1687 def depart_transition(self, node):
1688 pass
1690 def visit_version(self, node):
1691 self.visit_docinfo_item(node, 'version', meta=False)
1693 def depart_version(self, node):
1694 self.depart_docinfo_item()
1696 def unimplemented_visit(self, node):
1697 raise NotImplementedError('visiting unimplemented node type: %s'
1698 % node.__class__.__name__)
1701 class SimpleListChecker(nodes.GenericNodeVisitor):
1704 Raise `nodes.NodeFound` if non-simple list item is encountered.
1706 Here "simple" means a list item containing nothing other than a single
1707 paragraph, a simple list, or a paragraph followed by a simple list.
1710 def default_visit(self, node):
1711 raise nodes.NodeFound
1713 def visit_bullet_list(self, node):
1714 pass
1716 def visit_enumerated_list(self, node):
1717 pass
1719 def visit_list_item(self, node):
1720 children = []
1721 for child in node.children:
1722 if not isinstance(child, nodes.Invisible):
1723 children.append(child)
1724 if (children and isinstance(children[0], nodes.paragraph)
1725 and (isinstance(children[-1], nodes.bullet_list)
1726 or isinstance(children[-1], nodes.enumerated_list))):
1727 children.pop()
1728 if len(children) <= 1:
1729 return
1730 else:
1731 raise nodes.NodeFound
1733 def visit_paragraph(self, node):
1734 raise nodes.SkipNode
1736 def invisible_visit(self, node):
1737 """Invisible nodes should be ignored."""
1738 raise nodes.SkipNode
1740 visit_comment = invisible_visit
1741 visit_substitution_definition = invisible_visit
1742 visit_target = invisible_visit
1743 visit_pending = invisible_visit