New CSS 2.1 style-sheet for xhtml11 writer.
[docutils.git] / docutils / writers / html4css1 / __init__.py
blobe031367b3a40244f6d27fc3eedfbef0aa2fa1c23
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 = []
309 self.topic_classes = [] # TODO: replace with self_in_contents
310 self.colspecs = []
311 self.compact_p = True
312 self.compact_simple = False
313 self.compact_field_list = False
314 self.in_docinfo = False
315 self.in_sidebar = False
316 self.title = []
317 self.subtitle = []
318 self.header = []
319 self.footer = []
320 self.html_head = [self.content_type] # charset not interpolated
321 self.html_title = []
322 self.html_subtitle = []
323 self.html_body = []
324 self.in_document_title = 0 # len(self.body) or 0
325 self.in_mailto = False
326 self.author_in_authors = False
327 self.math_header = []
329 def astext(self):
330 return ''.join(self.head_prefix + self.head
331 + self.stylesheet + self.body_prefix
332 + self.body_pre_docinfo + self.docinfo
333 + self.body + self.body_suffix)
335 def encode(self, text):
336 """Encode special characters in `text` & return."""
337 # @@@ A codec to do these and all other HTML entities would be nice.
338 text = unicode(text)
339 return text.translate({
340 ord('&'): u'&amp;',
341 ord('<'): u'&lt;',
342 ord('"'): u'&quot;',
343 ord('>'): u'&gt;',
344 ord('@'): u'&#64;', # may thwart some address harvesters
345 # TODO: convert non-breaking space only if needed?
346 0xa0: u'&nbsp;'}) # non-breaking space
348 def cloak_mailto(self, uri):
349 """Try to hide a mailto: URL from harvesters."""
350 # Encode "@" using a URL octet reference (see RFC 1738).
351 # Further cloaking with HTML entities will be done in the
352 # `attval` function.
353 return uri.replace('@', '%40')
355 def cloak_email(self, addr):
356 """Try to hide the link text of a email link from harversters."""
357 # Surround at-signs and periods with <span> tags. ("@" has
358 # already been encoded to "&#64;" by the `encode` method.)
359 addr = addr.replace('&#64;', '<span>&#64;</span>')
360 addr = addr.replace('.', '<span>&#46;</span>')
361 return addr
363 def attval(self, text,
364 whitespace=re.compile('[\n\r\t\v\f]')):
365 """Cleanse, HTML encode, and return attribute value text."""
366 encoded = self.encode(whitespace.sub(' ', text))
367 if self.in_mailto and self.settings.cloak_email_addresses:
368 # Cloak at-signs ("%40") and periods with HTML entities.
369 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
370 encoded = encoded.replace('.', '&#46;')
371 return encoded
373 def stylesheet_call(self, path):
374 """Return code to reference or embed stylesheet file `path`"""
375 if self.settings.embed_stylesheet:
376 try:
377 content = io.FileInput(source_path=path,
378 encoding='utf-8').read()
379 self.settings.record_dependencies.add(path)
380 except IOError, err:
381 msg = u"Cannot embed stylesheet '%s': %s." % (
382 path, SafeString(err.strerror))
383 self.document.reporter.error(msg)
384 return '<--- %s --->\n' % msg
385 return self.embedded_stylesheet % content
386 # else link to style file:
387 if self.settings.stylesheet_path:
388 # adapt path relative to output (cf. config.html#stylesheet-path)
389 path = utils.relative_path(self.settings._destination, path)
390 return self.stylesheet_link % self.encode(path)
392 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
394 Construct and return a start tag given a node (id & class attributes
395 are extracted), tag name, and optional attributes.
397 tagname = tagname.lower()
398 prefix = []
399 atts = {}
400 ids = []
401 for (name, value) in attributes.items():
402 atts[name.lower()] = value
403 classes = []
404 languages = []
405 # unify class arguments and move language specification
406 for cls in node.get('classes', []) + atts.pop('class', '').split() :
407 if cls.startswith('language-'):
408 languages.append(cls[9:])
409 elif cls.strip() and cls not in classes:
410 classes.append(cls)
411 if languages:
412 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
413 atts[self.lang_attribute] = languages[0]
414 if classes:
415 atts['class'] = ' '.join(classes)
416 assert 'id' not in atts
417 ids.extend(node.get('ids', []))
418 if 'ids' in atts:
419 ids.extend(atts['ids'])
420 del atts['ids']
421 if ids:
422 atts['id'] = ids[0]
423 for id in ids[1:]:
424 # Add empty "span" elements for additional IDs. Note
425 # that we cannot use empty "a" elements because there
426 # may be targets inside of references, but nested "a"
427 # elements aren't allowed in XHTML (even if they do
428 # not all have a "href" attribute).
429 if empty:
430 # Empty tag. Insert target right in front of element.
431 prefix.append('<span id="%s"></span>' % id)
432 else:
433 # Non-empty tag. Place the auxiliary <span> tag
434 # *inside* the element, as the first child.
435 suffix += '<span id="%s"></span>' % id
436 attlist = atts.items()
437 attlist.sort()
438 parts = [tagname]
439 for name, value in attlist:
440 # value=None was used for boolean attributes without
441 # value, but this isn't supported by XHTML.
442 assert value is not None
443 if isinstance(value, list):
444 values = [unicode(v) for v in value]
445 parts.append('%s="%s"' % (name.lower(),
446 self.attval(' '.join(values))))
447 else:
448 parts.append('%s="%s"' % (name.lower(),
449 self.attval(unicode(value))))
450 if empty:
451 infix = ' /'
452 else:
453 infix = ''
454 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
456 def emptytag(self, node, tagname, suffix='\n', **attributes):
457 """Construct and return an XML-compatible empty tag."""
458 return self.starttag(node, tagname, suffix, empty=True, **attributes)
460 def set_class_on_child(self, node, class_, index=0):
462 Set class `class_` on the visible child no. index of `node`.
463 Do nothing if node has fewer children than `index`.
465 children = [n for n in node if not isinstance(n, nodes.Invisible)]
466 try:
467 child = children[index]
468 except IndexError:
469 return
470 child['classes'].append(class_)
472 def set_first_last(self, node):
473 self.set_class_on_child(node, 'first', 0)
474 self.set_class_on_child(node, 'last', -1)
476 def visit_Text(self, node):
477 text = node.astext()
478 encoded = self.encode(text)
479 if self.in_mailto and self.settings.cloak_email_addresses:
480 encoded = self.cloak_email(encoded)
481 self.body.append(encoded)
483 def depart_Text(self, node):
484 pass
486 def visit_abbreviation(self, node):
487 # @@@ implementation incomplete ("title" attribute)
488 self.body.append(self.starttag(node, 'abbr', ''))
490 def depart_abbreviation(self, node):
491 self.body.append('</abbr>')
493 def visit_acronym(self, node):
494 # @@@ implementation incomplete ("title" attribute)
495 self.body.append(self.starttag(node, 'acronym', ''))
497 def depart_acronym(self, node):
498 self.body.append('</acronym>')
500 def visit_address(self, node):
501 self.visit_docinfo_item(node, 'address', meta=False)
502 self.body.append(self.starttag(node, 'pre', CLASS='address'))
504 def depart_address(self, node):
505 self.body.append('\n</pre>\n')
506 self.depart_docinfo_item()
508 def visit_admonition(self, node):
509 node['classes'].insert(0, 'admonition')
510 self.body.append(self.starttag(node, 'div'))
511 self.set_first_last(node)
513 def depart_admonition(self, node=None):
514 self.body.append('</div>\n')
516 attribution_formats = {'dash': ('&mdash;', ''),
517 'parentheses': ('(', ')'),
518 'parens': ('(', ')'),
519 'none': ('', '')}
521 def visit_attribution(self, node):
522 prefix, suffix = self.attribution_formats[self.settings.attribution]
523 self.context.append(suffix)
524 self.body.append(
525 self.starttag(node, 'p', prefix, CLASS='attribution'))
527 def depart_attribution(self, node):
528 self.body.append(self.context.pop() + '</p>\n')
530 def visit_author(self, node):
531 if isinstance(node.parent, nodes.authors):
532 if self.author_in_authors:
533 self.body.append('\n<br />')
534 else:
535 self.visit_docinfo_item(node, 'author')
537 def depart_author(self, node):
538 if isinstance(node.parent, nodes.authors):
539 self.author_in_authors = True
540 else:
541 self.depart_docinfo_item()
543 def visit_authors(self, node):
544 self.visit_docinfo_item(node, 'authors')
545 self.author_in_authors = False # initialize
547 def depart_authors(self, node):
548 self.depart_docinfo_item()
550 def visit_block_quote(self, node):
551 self.body.append(self.starttag(node, 'blockquote'))
553 def depart_block_quote(self, node):
554 self.body.append('</blockquote>\n')
556 def check_simple_list(self, node):
557 """Check for a simple list that can be rendered compactly."""
558 visitor = SimpleListChecker(self.document)
559 try:
560 node.walk(visitor)
561 except nodes.NodeFound:
562 return None
563 else:
564 return 1
566 def is_compactable(self, node):
567 return ('compact' in node['classes']
568 or (self.settings.compact_lists
569 and 'open' not in node['classes']
570 and (self.compact_simple
571 or self.topic_classes == ['contents']
572 # TODO: self.in_contents
573 or self.check_simple_list(node))))
575 def visit_bullet_list(self, node):
576 atts = {}
577 old_compact_simple = self.compact_simple
578 self.context.append((self.compact_simple, self.compact_p))
579 self.compact_p = None
580 self.compact_simple = self.is_compactable(node)
581 if self.compact_simple and not old_compact_simple:
582 atts['class'] = 'simple'
583 self.body.append(self.starttag(node, 'ul', **atts))
585 def depart_bullet_list(self, node):
586 self.compact_simple, self.compact_p = self.context.pop()
587 self.body.append('</ul>\n')
589 def visit_caption(self, node):
590 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
592 def depart_caption(self, node):
593 self.body.append('</p>\n')
595 def visit_citation(self, node):
596 self.body.append(self.starttag(node, 'table',
597 CLASS='docutils citation',
598 frame="void", rules="none"))
599 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
600 '<tbody valign="top">\n'
601 '<tr>')
602 self.footnote_backrefs(node)
604 def depart_citation(self, node):
605 self.body.append('</td></tr>\n'
606 '</tbody>\n</table>\n')
608 def visit_citation_reference(self, node):
609 href = '#'
610 if 'refid' in node:
611 href += node['refid']
612 elif 'refname' in node:
613 href += self.document.nameids[node['refname']]
614 # else: # TODO system message (or already in the transform)?
615 # 'Citation reference missing.'
616 self.body.append(self.starttag(
617 node, 'a', '[', CLASS='citation-reference', href=href))
619 def depart_citation_reference(self, node):
620 self.body.append(']</a>')
622 def visit_classifier(self, node):
623 self.body.append(' <span class="classifier-delimiter">:</span> ')
624 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
626 def depart_classifier(self, node):
627 self.body.append('</span>')
629 def visit_colspec(self, node):
630 self.colspecs.append(node)
631 # "stubs" list is an attribute of the tgroup element:
632 node.parent.stubs.append(node.attributes.get('stub'))
634 def depart_colspec(self, node):
635 pass
637 def write_colspecs(self):
638 width = 0
639 for node in self.colspecs:
640 width += node['colwidth']
641 for node in self.colspecs:
642 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
643 self.body.append(self.emptytag(node, 'col',
644 width='%i%%' % colwidth))
645 self.colspecs = []
647 def visit_comment(self, node,
648 sub=re.compile('-(?=-)').sub):
649 """Escape double-dashes in comment text."""
650 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
651 # Content already processed:
652 raise nodes.SkipNode
654 def visit_compound(self, node):
655 self.body.append(self.starttag(node, 'div', CLASS='compound'))
656 if len(node) > 1:
657 node[0]['classes'].append('compound-first')
658 node[-1]['classes'].append('compound-last')
659 for child in node[1:-1]:
660 child['classes'].append('compound-middle')
662 def depart_compound(self, node):
663 self.body.append('</div>\n')
665 def visit_container(self, node):
666 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
668 def depart_container(self, node):
669 self.body.append('</div>\n')
671 def visit_contact(self, node):
672 self.visit_docinfo_item(node, 'contact', meta=False)
674 def depart_contact(self, node):
675 self.depart_docinfo_item()
677 def visit_copyright(self, node):
678 self.visit_docinfo_item(node, 'copyright')
680 def depart_copyright(self, node):
681 self.depart_docinfo_item()
683 def visit_date(self, node):
684 self.visit_docinfo_item(node, 'date')
686 def depart_date(self, node):
687 self.depart_docinfo_item()
689 def visit_decoration(self, node):
690 pass
692 def depart_decoration(self, node):
693 pass
695 def visit_definition(self, node):
696 self.body.append('</dt>\n')
697 self.body.append(self.starttag(node, 'dd', ''))
698 self.set_first_last(node)
700 def depart_definition(self, node):
701 self.body.append('</dd>\n')
703 def visit_definition_list(self, node):
704 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
706 def depart_definition_list(self, node):
707 self.body.append('</dl>\n')
709 def visit_definition_list_item(self, node):
710 # pass class arguments, ids and names to definition term:
711 node.children[0]['classes'] = (
712 node.get('classes', []) + node.children[0].get('classes', []))
713 node.children[0]['ids'] = (
714 node.get('ids', []) + node.children[0].get('ids', []))
715 node.children[0]['names'] = (
716 node.get('names', []) + node.children[0].get('names', []))
718 def depart_definition_list_item(self, node):
719 pass
721 def visit_description(self, node):
722 self.body.append(self.starttag(node, 'td', ''))
723 self.set_first_last(node)
725 def depart_description(self, node):
726 self.body.append('</td>')
728 def visit_docinfo(self, node):
729 self.context.append(len(self.body))
730 self.body.append(self.starttag(node, 'table',
731 CLASS='docinfo',
732 frame="void", rules="none"))
733 self.body.append('<col class="docinfo-name" />\n'
734 '<col class="docinfo-content" />\n'
735 '<tbody valign="top">\n')
736 self.in_docinfo = True
738 def depart_docinfo(self, node):
739 self.body.append('</tbody>\n</table>\n')
740 self.in_docinfo = False
741 start = self.context.pop()
742 self.docinfo = self.body[start:]
743 self.body = []
745 def visit_docinfo_item(self, node, name, meta=True):
746 if meta:
747 meta_tag = '<meta name="%s" content="%s" />\n' \
748 % (name, self.attval(node.astext()))
749 self.add_meta(meta_tag)
750 self.body.append(self.starttag(node, 'tr', ''))
751 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
752 % self.language.labels[name])
753 if len(node):
754 if isinstance(node[0], nodes.Element):
755 node[0]['classes'].append('first')
756 if isinstance(node[-1], nodes.Element):
757 node[-1]['classes'].append('last')
759 def depart_docinfo_item(self):
760 self.body.append('</td></tr>\n')
762 def visit_doctest_block(self, node):
763 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
765 def depart_doctest_block(self, node):
766 self.body.append('\n</pre>\n')
768 def visit_document(self, node):
769 self.head.append('<title>%s</title>\n'
770 % self.encode(node.get('title', '')))
772 def depart_document(self, node):
773 self.head_prefix.extend([self.doctype,
774 self.head_prefix_template %
775 {'lang': self.settings.language_code}])
776 self.html_prolog.append(self.doctype)
777 self.meta.insert(0, self.content_type % self.settings.output_encoding)
778 self.head.insert(0, self.content_type % self.settings.output_encoding)
779 if self.math_header:
780 if self.math_output == 'mathjax':
781 self.head.extend(self.math_header)
782 else:
783 self.stylesheet.extend(self.math_header)
784 # skip content-type meta tag with interpolated charset value:
785 self.html_head.extend(self.head[1:])
786 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
787 self.body_suffix.insert(0, '</div>\n')
788 self.fragment.extend(self.body) # self.fragment is the "naked" body
789 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
790 + self.docinfo + self.body
791 + self.body_suffix[:-1])
792 assert not self.context, 'len(context) = %s' % len(self.context)
794 def visit_emphasis(self, node):
795 self.body.append(self.starttag(node, 'em', ''))
797 def depart_emphasis(self, node):
798 self.body.append('</em>')
800 def visit_entry(self, node):
801 atts = {'class': []}
802 if isinstance(node.parent.parent, nodes.thead):
803 atts['class'].append('head')
804 if node.parent.parent.parent.stubs[node.parent.column]:
805 # "stubs" list is an attribute of the tgroup element
806 atts['class'].append('stub')
807 if atts['class']:
808 tagname = 'th'
809 atts['class'] = ' '.join(atts['class'])
810 else:
811 tagname = 'td'
812 del atts['class']
813 node.parent.column += 1
814 if 'morerows' in node:
815 atts['rowspan'] = node['morerows'] + 1
816 if 'morecols' in node:
817 atts['colspan'] = node['morecols'] + 1
818 node.parent.column += node['morecols']
819 self.body.append(self.starttag(node, tagname, '', **atts))
820 self.context.append('</%s>\n' % tagname.lower())
821 if len(node) == 0: # empty cell
822 self.body.append('&nbsp;')
823 self.set_first_last(node)
825 def depart_entry(self, node):
826 self.body.append(self.context.pop())
828 def visit_enumerated_list(self, node):
830 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
831 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
832 usable.
834 atts = {}
835 if 'start' in node:
836 atts['start'] = node['start']
837 if 'enumtype' in node:
838 atts['class'] = node['enumtype']
839 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
840 # single "format" attribute? Use CSS2?
841 old_compact_simple = self.compact_simple
842 self.context.append((self.compact_simple, self.compact_p))
843 self.compact_p = None
844 self.compact_simple = self.is_compactable(node)
845 if self.compact_simple and not old_compact_simple:
846 atts['class'] = (atts.get('class', '') + ' simple').strip()
847 self.body.append(self.starttag(node, 'ol', **atts))
849 def depart_enumerated_list(self, node):
850 self.compact_simple, self.compact_p = self.context.pop()
851 self.body.append('</ol>\n')
853 def visit_field(self, node):
854 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
856 def depart_field(self, node):
857 self.body.append('</tr>\n')
859 def visit_field_body(self, node):
860 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
861 self.set_class_on_child(node, 'first', 0)
862 field = node.parent
863 if (self.compact_field_list or
864 isinstance(field.parent, nodes.docinfo) or
865 field.parent.index(field) == len(field.parent) - 1):
866 # If we are in a compact list, the docinfo, or if this is
867 # the last field of the field list, do not add vertical
868 # space after last element.
869 self.set_class_on_child(node, 'last', -1)
871 def depart_field_body(self, node):
872 self.body.append('</td>\n')
874 def visit_field_list(self, node):
875 self.context.append((self.compact_field_list, self.compact_p))
876 self.compact_p = None
877 if 'compact' in node['classes']:
878 self.compact_field_list = True
879 elif (self.settings.compact_field_lists
880 and 'open' not in node['classes']):
881 self.compact_field_list = True
882 if self.compact_field_list:
883 for field in node:
884 field_body = field[-1]
885 assert isinstance(field_body, nodes.field_body)
886 children = [n for n in field_body
887 if not isinstance(n, nodes.Invisible)]
888 if not (len(children) == 0 or
889 len(children) == 1 and
890 isinstance(children[0],
891 (nodes.paragraph, nodes.line_block))):
892 self.compact_field_list = False
893 break
894 self.body.append(self.starttag(node, 'table', frame='void',
895 rules='none',
896 CLASS='docutils field-list'))
897 self.body.append('<col class="field-name" />\n'
898 '<col class="field-body" />\n'
899 '<tbody valign="top">\n')
901 def depart_field_list(self, node):
902 self.body.append('</tbody>\n</table>\n')
903 self.compact_field_list, self.compact_p = self.context.pop()
905 def visit_field_name(self, node):
906 atts = {}
907 if self.in_docinfo:
908 atts['class'] = 'docinfo-name'
909 else:
910 atts['class'] = 'field-name'
911 if ( self.settings.field_name_limit
912 and len(node.astext()) > self.settings.field_name_limit):
913 atts['colspan'] = 2
914 self.context.append('</tr>\n'
915 + self.starttag(node.parent, 'tr', '',
916 CLASS='field')
917 + '<td>&nbsp;</td>')
918 else:
919 self.context.append('')
920 self.body.append(self.starttag(node, 'th', '', **atts))
922 def depart_field_name(self, node):
923 self.body.append(':</th>')
924 self.body.append(self.context.pop())
926 def visit_figure(self, node):
927 atts = {'class': 'figure'}
928 if node.get('width'):
929 atts['style'] = 'width: %s' % node['width']
930 if node.get('align'):
931 atts['class'] += " align-" + node['align']
932 self.body.append(self.starttag(node, 'div', **atts))
934 def depart_figure(self, node):
935 self.body.append('</div>\n')
937 def visit_footer(self, node):
938 self.context.append(len(self.body))
940 def depart_footer(self, node):
941 start = self.context.pop()
942 footer = [self.starttag(node, 'div', CLASS='footer'),
943 '<hr class="footer" />\n']
944 footer.extend(self.body[start:])
945 footer.append('\n</div>\n')
946 self.footer.extend(footer)
947 self.body_suffix[:0] = footer
948 del self.body[start:]
950 def visit_footnote(self, node):
951 self.body.append(self.starttag(node, 'table',
952 CLASS='docutils footnote',
953 frame="void", rules="none"))
954 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
955 '<tbody valign="top">\n'
956 '<tr>')
957 self.footnote_backrefs(node)
959 def footnote_backrefs(self, node):
960 backlinks = []
961 backrefs = node['backrefs']
962 if self.settings.footnote_backlinks and backrefs:
963 if len(backrefs) == 1:
964 self.context.append('')
965 self.context.append('</a>')
966 self.context.append('<a class="fn-backref" href="#%s">'
967 % backrefs[0])
968 else:
969 # Python 2.4 fails with enumerate(backrefs, 1)
970 for (i, backref) in enumerate(backrefs):
971 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
972 % (backref, i+1))
973 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
974 self.context += ['', '']
975 else:
976 self.context.append('')
977 self.context += ['', '']
978 # If the node does not only consist of a label.
979 if len(node) > 1:
980 # If there are preceding backlinks, we do not set class
981 # 'first', because we need to retain the top-margin.
982 if not backlinks:
983 node[1]['classes'].append('first')
984 node[-1]['classes'].append('last')
986 def depart_footnote(self, node):
987 self.body.append('</td></tr>\n'
988 '</tbody>\n</table>\n')
990 def visit_footnote_reference(self, node):
991 href = '#' + node['refid']
992 format = self.settings.footnote_references
993 if format == 'brackets':
994 suffix = '['
995 self.context.append(']')
996 else:
997 assert format == 'superscript'
998 suffix = '<sup>'
999 self.context.append('</sup>')
1000 self.body.append(self.starttag(node, 'a', suffix,
1001 CLASS='footnote-reference', href=href))
1003 def depart_footnote_reference(self, node):
1004 self.body.append(self.context.pop() + '</a>')
1006 def visit_generated(self, node):
1007 pass
1009 def depart_generated(self, node):
1010 pass
1012 def visit_header(self, node):
1013 self.context.append(len(self.body))
1015 def depart_header(self, node):
1016 start = self.context.pop()
1017 header = [self.starttag(node, 'div', CLASS='header')]
1018 header.extend(self.body[start:])
1019 header.append('\n<hr class="header"/>\n</div>\n')
1020 self.body_prefix.extend(header)
1021 self.header.extend(header)
1022 del self.body[start:]
1024 # Image types to place in an <object> element
1025 # SVG not supported by IE up to version 8
1026 # (html4css1 strives for IE6 compatibility)
1027 object_image_types = {'.svg': 'image/svg+xml',
1028 '.swf': 'application/x-shockwave-flash'}
1030 def visit_image(self, node):
1031 atts = {}
1032 uri = node['uri']
1033 ext = os.path.splitext(uri)[1].lower()
1034 if ext in self.object_image_types: # ('.svg', '.swf'):
1035 atts['data'] = uri
1036 atts['type'] = self.object_image_types[ext]
1037 else:
1038 atts['src'] = uri
1039 atts['alt'] = node.get('alt', uri)
1040 # image size
1041 if 'width' in node:
1042 atts['width'] = node['width']
1043 if 'height' in node:
1044 atts['height'] = node['height']
1045 if 'scale' in node:
1046 if (PIL and not ('width' in node and 'height' in node)
1047 and self.settings.file_insertion_enabled):
1048 imagepath = urllib.url2pathname(uri)
1049 try:
1050 img = PIL.Image.open(
1051 imagepath.encode(sys.getfilesystemencoding()))
1052 except (IOError, UnicodeEncodeError):
1053 pass # TODO: warn?
1054 else:
1055 self.settings.record_dependencies.add(
1056 imagepath.replace('\\', '/'))
1057 if 'width' not in atts:
1058 atts['width'] = '%dpx' % img.size[0]
1059 if 'height' not in atts:
1060 atts['height'] = '%dpx' % img.size[1]
1061 del img
1062 for att_name in 'width', 'height':
1063 if att_name in atts:
1064 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1065 assert match
1066 atts[att_name] = '%s%s' % (
1067 float(match.group(1)) * (float(node['scale']) / 100),
1068 match.group(2))
1069 style = []
1070 for att_name in 'width', 'height':
1071 if att_name in atts:
1072 if re.match(r'^[0-9.]+$', atts[att_name]):
1073 # Interpret unitless values as pixels.
1074 atts[att_name] += 'px'
1075 style.append('%s: %s;' % (att_name, atts[att_name]))
1076 del atts[att_name]
1077 if style:
1078 atts['style'] = ' '.join(style)
1079 if (isinstance(node.parent, nodes.TextElement) or
1080 (isinstance(node.parent, nodes.reference) and
1081 not isinstance(node.parent.parent, nodes.TextElement))):
1082 # Inline context or surrounded by <a>...</a>.
1083 suffix = ''
1084 else:
1085 suffix = '\n'
1086 if 'align' in node:
1087 atts['class'] = 'align-%s' % node['align']
1088 if ext in self.object_image_types: # ('.svg', '.swf')
1089 # do NOT use an empty tag: incorrect rendering in browsers
1090 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1091 node.get('alt', uri) + '</object>' + suffix)
1092 else:
1093 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1095 def depart_image(self, node):
1096 # self.body.append(self.context.pop())
1097 pass
1099 def visit_inline(self, node):
1100 self.body.append(self.starttag(node, 'span', ''))
1102 def depart_inline(self, node):
1103 self.body.append('</span>')
1105 def visit_label(self, node):
1106 # Context added in footnote_backrefs.
1107 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1108 CLASS='label'))
1110 def depart_label(self, node):
1111 # Context added in footnote_backrefs.
1112 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1114 def visit_legend(self, node):
1115 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1117 def depart_legend(self, node):
1118 self.body.append('</div>\n')
1120 def visit_line(self, node):
1121 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1122 if not len(node):
1123 self.body.append('<br />')
1125 def depart_line(self, node):
1126 self.body.append('</div>\n')
1128 def visit_line_block(self, node):
1129 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1131 def depart_line_block(self, node):
1132 self.body.append('</div>\n')
1134 def visit_list_item(self, node):
1135 self.body.append(self.starttag(node, 'li', ''))
1136 if len(node):
1137 node[0]['classes'].append('first')
1139 def depart_list_item(self, node):
1140 self.body.append('</li>\n')
1142 def visit_literal(self, node):
1143 # special case: "code" role
1144 classes = node.get('classes', [])
1145 if 'code' in classes:
1146 # filter 'code' from class arguments
1147 node['classes'] = [cls for cls in classes if cls != 'code']
1148 self.body.append(self.starttag(node, 'code', ''))
1149 return
1150 self.body.append(
1151 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1152 text = node.astext()
1153 for token in self.words_and_spaces.findall(text):
1154 if token.strip():
1155 # Protect text like "--an-option" and the regular expression
1156 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1157 if self.sollbruchstelle.search(token):
1158 self.body.append('<span class="pre">%s</span>'
1159 % self.encode(token))
1160 else:
1161 self.body.append(self.encode(token))
1162 elif token in ('\n', ' '):
1163 # Allow breaks at whitespace:
1164 self.body.append(token)
1165 else:
1166 # Protect runs of multiple spaces; the last space can wrap:
1167 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1168 self.body.append('</tt>')
1169 # Content already processed:
1170 raise nodes.SkipNode
1172 def depart_literal(self, node):
1173 # skipped unless literal element is from "code" role:
1174 self.body.append('</code>')
1176 def visit_literal_block(self, node):
1177 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1179 def depart_literal_block(self, node):
1180 self.body.append('\n</pre>\n')
1182 def visit_math(self, node, math_env=''):
1183 # If the method is called from visit_math_block(), math_env != ''.
1185 # As there is no native HTML math support, we provide alternatives:
1186 # LaTeX and MathJax math_output modes simply wrap the content,
1187 # HTML and MathML math_output modes also convert the math_code.
1188 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1189 self.document.reporter.error(
1190 'math-output format "%s" not supported '
1191 'falling back to "latex"'% self.math_output)
1192 self.math_output = 'latex'
1194 # HTML container
1195 tags = {# math_output: (block, inline, class-arguments)
1196 'mathml': ('div', '', ''),
1197 'html': ('div', 'span', 'formula'),
1198 'mathjax': ('div', 'span', 'math'),
1199 'latex': ('pre', 'tt', 'math'),
1201 tag = tags[self.math_output][math_env == '']
1202 clsarg = tags[self.math_output][2]
1203 # LaTeX container
1204 wrappers = {# math_mode: (inline, block)
1205 'mathml': (None, None),
1206 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1207 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1208 'latex': (None, None),
1210 wrapper = wrappers[self.math_output][math_env != '']
1211 # get and wrap content
1212 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1213 if wrapper and math_env:
1214 math_code = wrapper % (math_env, math_code, math_env)
1215 elif wrapper:
1216 math_code = wrapper % math_code
1217 # settings and conversion
1218 if self.math_output in ('latex', 'mathjax'):
1219 math_code = self.encode(math_code)
1220 if self.math_output == 'mathjax' and not self.math_header:
1221 if self.math_output_options:
1222 self.mathjax_url = self.math_output_options[0]
1223 self.math_header = [self.mathjax_script % self.mathjax_url]
1224 elif self.math_output == 'html':
1225 if self.math_output_options and not self.math_header:
1226 self.math_header = [self.stylesheet_call(
1227 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1228 for s in self.math_output_options[0].split(',')]
1229 # TODO: fix display mode in matrices and fractions
1230 math2html.DocumentParameters.displaymode = (math_env != '')
1231 math_code = math2html.math2html(math_code)
1232 elif self.math_output == 'mathml':
1233 self.doctype = self.doctype_mathml
1234 self.content_type = self.content_type_mathml
1235 try:
1236 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1237 math_code = ''.join(mathml_tree.xml())
1238 except SyntaxError, err:
1239 err_node = self.document.reporter.error(err, base_node=node)
1240 self.visit_system_message(err_node)
1241 self.body.append(self.starttag(node, 'p'))
1242 self.body.append(u','.join(err.args))
1243 self.body.append('</p>\n')
1244 self.body.append(self.starttag(node, 'pre',
1245 CLASS='literal-block'))
1246 self.body.append(self.encode(math_code))
1247 self.body.append('\n</pre>\n')
1248 self.depart_system_message(err_node)
1249 raise nodes.SkipNode
1250 # append to document body
1251 if tag:
1252 self.body.append(self.starttag(node, tag,
1253 suffix='\n'*bool(math_env),
1254 CLASS=clsarg))
1255 self.body.append(math_code)
1256 if math_env: # block mode (equation, display)
1257 self.body.append('\n')
1258 if tag:
1259 self.body.append('</%s>' % tag)
1260 if math_env:
1261 self.body.append('\n')
1262 # Content already processed:
1263 raise nodes.SkipNode
1265 def depart_math(self, node):
1266 pass # never reached
1268 def visit_math_block(self, node):
1269 # print node.astext().encode('utf8')
1270 math_env = pick_math_environment(node.astext())
1271 self.visit_math(node, math_env=math_env)
1273 def depart_math_block(self, node):
1274 pass # never reached
1276 def visit_meta(self, node):
1277 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1278 self.add_meta(meta)
1280 def depart_meta(self, node):
1281 pass
1283 def add_meta(self, tag):
1284 self.meta.append(tag)
1285 self.head.append(tag)
1287 def visit_option(self, node):
1288 if self.context[-1]:
1289 self.body.append(', ')
1290 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1292 def depart_option(self, node):
1293 self.body.append('</span>')
1294 self.context[-1] += 1
1296 def visit_option_argument(self, node):
1297 self.body.append(node.get('delimiter', ' '))
1298 self.body.append(self.starttag(node, 'var', ''))
1300 def depart_option_argument(self, node):
1301 self.body.append('</var>')
1303 def visit_option_group(self, node):
1304 atts = {}
1305 if ( self.settings.option_limit
1306 and len(node.astext()) > self.settings.option_limit):
1307 atts['colspan'] = 2
1308 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1309 else:
1310 self.context.append('')
1311 self.body.append(
1312 self.starttag(node, 'td', CLASS='option-group', **atts))
1313 self.body.append('<kbd>')
1314 self.context.append(0) # count number of options
1316 def depart_option_group(self, node):
1317 self.context.pop()
1318 self.body.append('</kbd></td>\n')
1319 self.body.append(self.context.pop())
1321 def visit_option_list(self, node):
1322 self.body.append(
1323 self.starttag(node, 'table', CLASS='docutils option-list',
1324 frame="void", rules="none"))
1325 self.body.append('<col class="option" />\n'
1326 '<col class="description" />\n'
1327 '<tbody valign="top">\n')
1329 def depart_option_list(self, node):
1330 self.body.append('</tbody>\n</table>\n')
1332 def visit_option_list_item(self, node):
1333 self.body.append(self.starttag(node, 'tr', ''))
1335 def depart_option_list_item(self, node):
1336 self.body.append('</tr>\n')
1338 def visit_option_string(self, node):
1339 pass
1341 def depart_option_string(self, node):
1342 pass
1344 def visit_organization(self, node):
1345 self.visit_docinfo_item(node, 'organization')
1347 def depart_organization(self, node):
1348 self.depart_docinfo_item()
1350 def should_be_compact_paragraph(self, node):
1352 Determine if the <p> tags around paragraph ``node`` can be omitted.
1354 if (isinstance(node.parent, nodes.document) or
1355 isinstance(node.parent, nodes.compound)):
1356 # Never compact paragraphs in document or compound.
1357 return False
1358 for key, value in node.attlist():
1359 if (node.is_not_default(key) and
1360 not (key == 'classes' and value in
1361 ([], ['first'], ['last'], ['first', 'last']))):
1362 # Attribute which needs to survive.
1363 return False
1364 first = isinstance(node.parent[0], nodes.label) # skip label
1365 for child in node.parent.children[first:]:
1366 # only first paragraph can be compact
1367 if isinstance(child, nodes.Invisible):
1368 continue
1369 if child is node:
1370 break
1371 return False
1372 parent_length = len([n for n in node.parent if not isinstance(
1373 n, (nodes.Invisible, nodes.label))])
1374 if ( self.compact_simple
1375 or self.compact_field_list
1376 or self.compact_p and parent_length == 1):
1377 return True
1378 return False
1380 def visit_paragraph(self, node):
1381 if self.should_be_compact_paragraph(node):
1382 self.context.append('')
1383 else:
1384 self.body.append(self.starttag(node, 'p', ''))
1385 self.context.append('</p>\n')
1387 def depart_paragraph(self, node):
1388 self.body.append(self.context.pop())
1390 def visit_problematic(self, node):
1391 if node.hasattr('refid'):
1392 self.body.append('<a href="#%s">' % node['refid'])
1393 self.context.append('</a>')
1394 else:
1395 self.context.append('')
1396 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1398 def depart_problematic(self, node):
1399 self.body.append('</span>')
1400 self.body.append(self.context.pop())
1402 def visit_raw(self, node):
1403 if 'html' in node.get('format', '').split():
1404 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1405 if node['classes']:
1406 self.body.append(self.starttag(node, t, suffix=''))
1407 self.body.append(node.astext())
1408 if node['classes']:
1409 self.body.append('</%s>' % t)
1410 # Keep non-HTML raw text out of output:
1411 raise nodes.SkipNode
1413 def visit_reference(self, node):
1414 atts = {'class': 'reference'}
1415 if 'refuri' in node:
1416 atts['href'] = node['refuri']
1417 if ( self.settings.cloak_email_addresses
1418 and atts['href'].startswith('mailto:')):
1419 atts['href'] = self.cloak_mailto(atts['href'])
1420 self.in_mailto = True
1421 atts['class'] += ' external'
1422 else:
1423 assert 'refid' in node, \
1424 'References must have "refuri" or "refid" attribute.'
1425 atts['href'] = '#' + node['refid']
1426 atts['class'] += ' internal'
1427 if not isinstance(node.parent, nodes.TextElement):
1428 assert len(node) == 1 and isinstance(node[0], nodes.image)
1429 atts['class'] += ' image-reference'
1430 self.body.append(self.starttag(node, 'a', '', **atts))
1432 def depart_reference(self, node):
1433 self.body.append('</a>')
1434 if not isinstance(node.parent, nodes.TextElement):
1435 self.body.append('\n')
1436 self.in_mailto = False
1438 def visit_revision(self, node):
1439 self.visit_docinfo_item(node, 'revision', meta=False)
1441 def depart_revision(self, node):
1442 self.depart_docinfo_item()
1444 def visit_row(self, node):
1445 self.body.append(self.starttag(node, 'tr', ''))
1446 node.column = 0
1448 def depart_row(self, node):
1449 self.body.append('</tr>\n')
1451 def visit_rubric(self, node):
1452 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1454 def depart_rubric(self, node):
1455 self.body.append('</p>\n')
1457 def visit_section(self, node):
1458 self.section_level += 1
1459 self.body.append(
1460 self.starttag(node, 'div', CLASS='section'))
1462 def depart_section(self, node):
1463 self.section_level -= 1
1464 self.body.append('</div>\n')
1466 def visit_sidebar(self, node):
1467 self.body.append(
1468 self.starttag(node, 'div', CLASS='sidebar'))
1469 self.set_first_last(node)
1470 self.in_sidebar = True
1472 def depart_sidebar(self, node):
1473 self.body.append('</div>\n')
1474 self.in_sidebar = False
1476 def visit_status(self, node):
1477 self.visit_docinfo_item(node, 'status', meta=False)
1479 def depart_status(self, node):
1480 self.depart_docinfo_item()
1482 def visit_strong(self, node):
1483 self.body.append(self.starttag(node, 'strong', ''))
1485 def depart_strong(self, node):
1486 self.body.append('</strong>')
1488 def visit_subscript(self, node):
1489 self.body.append(self.starttag(node, 'sub', ''))
1491 def depart_subscript(self, node):
1492 self.body.append('</sub>')
1494 def visit_substitution_definition(self, node):
1495 """Internal only."""
1496 raise nodes.SkipNode
1498 def visit_substitution_reference(self, node):
1499 self.unimplemented_visit(node)
1501 def visit_subtitle(self, node):
1502 if isinstance(node.parent, nodes.sidebar):
1503 self.body.append(self.starttag(node, 'p', '',
1504 CLASS='sidebar-subtitle'))
1505 self.context.append('</p>\n')
1506 elif isinstance(node.parent, nodes.document):
1507 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1508 self.context.append('</h2>\n')
1509 self.in_document_title = len(self.body)
1510 elif isinstance(node.parent, nodes.section):
1511 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1512 self.body.append(
1513 self.starttag(node, tag, '', CLASS='section-subtitle') +
1514 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1515 self.context.append('</span></%s>\n' % tag)
1517 def depart_subtitle(self, node):
1518 self.body.append(self.context.pop())
1519 if self.in_document_title:
1520 self.subtitle = self.body[self.in_document_title:-1]
1521 self.in_document_title = 0
1522 self.body_pre_docinfo.extend(self.body)
1523 self.html_subtitle.extend(self.body)
1524 del self.body[:]
1526 def visit_superscript(self, node):
1527 self.body.append(self.starttag(node, 'sup', ''))
1529 def depart_superscript(self, node):
1530 self.body.append('</sup>')
1532 def visit_system_message(self, node):
1533 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1534 self.body.append('<p class="system-message-title">')
1535 backref_text = ''
1536 if len(node['backrefs']):
1537 backrefs = node['backrefs']
1538 if len(backrefs) == 1:
1539 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1540 % backrefs[0])
1541 else:
1542 i = 1
1543 backlinks = []
1544 for backref in backrefs:
1545 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1546 i += 1
1547 backref_text = ('; <em>backlinks: %s</em>'
1548 % ', '.join(backlinks))
1549 if node.hasattr('line'):
1550 line = ', line %s' % node['line']
1551 else:
1552 line = ''
1553 self.body.append('System Message: %s/%s '
1554 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1555 % (node['type'], node['level'],
1556 self.encode(node['source']), line, backref_text))
1558 def depart_system_message(self, node):
1559 self.body.append('</div>\n')
1561 def visit_table(self, node):
1562 self.context.append(self.compact_p)
1563 self.compact_p = True
1564 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1565 self.body.append(
1566 self.starttag(node, 'table', CLASS=classes, border="1"))
1568 def depart_table(self, node):
1569 self.compact_p = self.context.pop()
1570 self.body.append('</table>\n')
1572 def visit_target(self, node):
1573 if not ('refuri' in node or 'refid' in node
1574 or 'refname' in node):
1575 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1576 self.context.append('</span>')
1577 else:
1578 self.context.append('')
1580 def depart_target(self, node):
1581 self.body.append(self.context.pop())
1583 def visit_tbody(self, node):
1584 self.write_colspecs()
1585 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1586 self.body.append(self.starttag(node, 'tbody', valign='top'))
1588 def depart_tbody(self, node):
1589 self.body.append('</tbody>\n')
1591 def visit_term(self, node):
1592 self.body.append(self.starttag(node, 'dt', ''))
1594 def depart_term(self, node):
1596 Leave the end tag to `self.visit_definition()`, in case there's a
1597 classifier.
1599 pass
1601 def visit_tgroup(self, node):
1602 # Mozilla needs <colgroup>:
1603 self.body.append(self.starttag(node, 'colgroup'))
1604 # Appended by thead or tbody:
1605 self.context.append('</colgroup>\n')
1606 node.stubs = []
1608 def depart_tgroup(self, node):
1609 pass
1611 def visit_thead(self, node):
1612 self.write_colspecs()
1613 self.body.append(self.context.pop()) # '</colgroup>\n'
1614 # There may or may not be a <thead>; this is for <tbody> to use:
1615 self.context.append('')
1616 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1618 def depart_thead(self, node):
1619 self.body.append('</thead>\n')
1621 def visit_title(self, node):
1622 """Only 6 section levels are supported by HTML."""
1623 check_id = 0 # TODO: is this a bool (False) or a counter?
1624 close_tag = '</p>\n'
1625 if isinstance(node.parent, nodes.topic):
1626 self.body.append(
1627 self.starttag(node, 'p', '', CLASS='topic-title first'))
1628 elif isinstance(node.parent, nodes.sidebar):
1629 self.body.append(
1630 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1631 elif isinstance(node.parent, nodes.Admonition):
1632 self.body.append(
1633 self.starttag(node, 'p', '', CLASS='admonition-title'))
1634 elif isinstance(node.parent, nodes.table):
1635 self.body.append(
1636 self.starttag(node, 'caption', ''))
1637 close_tag = '</caption>\n'
1638 elif isinstance(node.parent, nodes.document):
1639 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1640 close_tag = '</h1>\n'
1641 self.in_document_title = len(self.body)
1642 else:
1643 assert isinstance(node.parent, nodes.section)
1644 h_level = self.section_level + self.initial_header_level - 1
1645 atts = {}
1646 if (len(node.parent) >= 2 and
1647 isinstance(node.parent[1], nodes.subtitle)):
1648 atts['CLASS'] = 'with-subtitle'
1649 self.body.append(
1650 self.starttag(node, 'h%s' % h_level, '', **atts))
1651 atts = {}
1652 if node.hasattr('refid'):
1653 atts['class'] = 'toc-backref'
1654 atts['href'] = '#' + node['refid']
1655 if atts:
1656 self.body.append(self.starttag({}, 'a', '', **atts))
1657 close_tag = '</a></h%s>\n' % (h_level)
1658 else:
1659 close_tag = '</h%s>\n' % (h_level)
1660 self.context.append(close_tag)
1662 def depart_title(self, node):
1663 self.body.append(self.context.pop())
1664 if self.in_document_title:
1665 self.title = self.body[self.in_document_title:-1]
1666 self.in_document_title = 0
1667 self.body_pre_docinfo.extend(self.body)
1668 self.html_title.extend(self.body)
1669 del self.body[:]
1671 def visit_title_reference(self, node):
1672 self.body.append(self.starttag(node, 'cite', ''))
1674 def depart_title_reference(self, node):
1675 self.body.append('</cite>')
1677 def visit_topic(self, node):
1678 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1679 self.topic_classes = node['classes']
1680 # TODO: replace with ::
1681 # self.in_contents = 'contents' in node['classes']
1683 def depart_topic(self, node):
1684 self.body.append('</div>\n')
1685 self.topic_classes = []
1686 # TODO self.in_contents = False
1688 def visit_transition(self, node):
1689 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1691 def depart_transition(self, node):
1692 pass
1694 def visit_version(self, node):
1695 self.visit_docinfo_item(node, 'version', meta=False)
1697 def depart_version(self, node):
1698 self.depart_docinfo_item()
1700 def unimplemented_visit(self, node):
1701 raise NotImplementedError('visiting unimplemented node type: %s'
1702 % node.__class__.__name__)
1705 class SimpleListChecker(nodes.GenericNodeVisitor):
1708 Raise `nodes.NodeFound` if non-simple list item is encountered.
1710 Here "simple" means a list item containing nothing other than a single
1711 paragraph, a simple list, or a paragraph followed by a simple list.
1714 def default_visit(self, node):
1715 raise nodes.NodeFound
1717 def visit_bullet_list(self, node):
1718 pass
1720 def visit_enumerated_list(self, node):
1721 pass
1723 def visit_list_item(self, node):
1724 children = []
1725 for child in node.children:
1726 if not isinstance(child, nodes.Invisible):
1727 children.append(child)
1728 if (children and isinstance(children[0], nodes.paragraph)
1729 and (isinstance(children[-1], nodes.bullet_list)
1730 or isinstance(children[-1], nodes.enumerated_list))):
1731 children.pop()
1732 if len(children) <= 1:
1733 return
1734 else:
1735 raise nodes.NodeFound
1737 def visit_paragraph(self, node):
1738 raise nodes.SkipNode
1740 def invisible_visit(self, node):
1741 """Invisible nodes should be ignored."""
1742 raise nodes.SkipNode
1744 visit_comment = invisible_visit
1745 visit_substitution_definition = invisible_visit
1746 visit_target = invisible_visit
1747 visit_pending = invisible_visit