Use ``<code>`` tag for inline "code", do not drop nested inline nodes (syntax highlig...
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob98f1d8aa7995501077afd69697d9c1d9ae9d0e93
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
38 from docutils.utils.math.latex2mathml import parse_latex_math
39 from docutils.utils.math.math2html import math2html
41 class Writer(writers.Writer):
43 supported = ('html', 'html4css1', 'xhtml')
44 """Formats this writer supports."""
46 default_stylesheet = 'html4css1.css'
48 default_stylesheet_path = utils.relative_path(
49 os.path.join(os.getcwd(), 'dummy'),
50 os.path.join(os.path.dirname(__file__), default_stylesheet))
52 default_template = 'template.txt'
54 default_template_path = utils.relative_path(
55 os.path.join(os.getcwd(), 'dummy'),
56 os.path.join(os.path.dirname(__file__), default_template))
58 settings_spec = (
59 'HTML-Specific Options',
60 None,
61 (('Specify the template file (UTF-8 encoded). Default is "%s".'
62 % default_template_path,
63 ['--template'],
64 {'default': default_template_path, 'metavar': '<file>'}),
65 ('Specify comma separated list of stylesheet URLs. '
66 'Overrides previous --stylesheet and --stylesheet-path settings.',
67 ['--stylesheet'],
68 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
69 ('Specify comma separated list of stylesheet paths. '
70 'With --link-stylesheet, '
71 'the path is rewritten relative to the output HTML file. '
72 'Default: "%s"' % default_stylesheet_path,
73 ['--stylesheet-path'],
74 {'metavar': '<file>', 'overrides': 'stylesheet',
75 'default': default_stylesheet_path}),
76 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
77 'files must be accessible during processing. This is the default.',
78 ['--embed-stylesheet'],
79 {'default': 1, 'action': 'store_true',
80 'validator': frontend.validate_boolean}),
81 ('Link to the stylesheet(s) in the output HTML file. '
82 'Default: embed stylesheets.',
83 ['--link-stylesheet'],
84 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
85 ('Specify the initial header level. Default is 1 for "<h1>". '
86 'Does not affect document title & subtitle (see --no-doc-title).',
87 ['--initial-header-level'],
88 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
89 'metavar': '<level>'}),
90 ('Specify the maximum width (in characters) for one-column field '
91 'names. Longer field names will span an entire row of the table '
92 'used to render the field list. Default is 14 characters. '
93 'Use 0 for "no limit".',
94 ['--field-name-limit'],
95 {'default': 14, 'metavar': '<level>',
96 'validator': frontend.validate_nonnegative_int}),
97 ('Specify the maximum width (in characters) for options in option '
98 'lists. Longer options will span an entire row of the table used '
99 'to render the option list. Default is 14 characters. '
100 'Use 0 for "no limit".',
101 ['--option-limit'],
102 {'default': 14, 'metavar': '<level>',
103 'validator': frontend.validate_nonnegative_int}),
104 ('Format for footnote references: one of "superscript" or '
105 '"brackets". Default is "brackets".',
106 ['--footnote-references'],
107 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
108 'metavar': '<format>',
109 'overrides': 'trim_footnote_reference_space'}),
110 ('Format for block quote attributions: one of "dash" (em-dash '
111 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
112 ['--attribution'],
113 {'choices': ['dash', 'parentheses', 'parens', 'none'],
114 'default': 'dash', 'metavar': '<format>'}),
115 ('Remove extra vertical whitespace between items of "simple" bullet '
116 'lists and enumerated lists. Default: enabled.',
117 ['--compact-lists'],
118 {'default': 1, 'action': 'store_true',
119 'validator': frontend.validate_boolean}),
120 ('Disable compact simple bullet and enumerated lists.',
121 ['--no-compact-lists'],
122 {'dest': 'compact_lists', 'action': 'store_false'}),
123 ('Remove extra vertical whitespace between items of simple field '
124 'lists. Default: enabled.',
125 ['--compact-field-lists'],
126 {'default': 1, 'action': 'store_true',
127 'validator': frontend.validate_boolean}),
128 ('Disable compact simple field lists.',
129 ['--no-compact-field-lists'],
130 {'dest': 'compact_field_lists', 'action': 'store_false'}),
131 ('Added to standard table classes. '
132 'Defined styles: "borderless". Default: ""',
133 ['--table-style'],
134 {'default': ''}),
135 ('Math output format, one of "MathML", "HTML", "MathJax" '
136 'or "LaTeX". Default: "MathJax"',
137 ['--math-output'],
138 {'default': 'MathJax'}),
139 ('Omit the XML declaration. Use with caution.',
140 ['--no-xml-declaration'],
141 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
142 'validator': frontend.validate_boolean}),
143 ('Obfuscate email addresses to confuse harvesters while still '
144 'keeping email links usable with standards-compliant browsers.',
145 ['--cloak-email-addresses'],
146 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
148 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
150 relative_path_settings = ('stylesheet_path',)
152 config_section = 'html4css1 writer'
153 config_section_dependencies = ('writers',)
155 visitor_attributes = (
156 'head_prefix', 'head', 'stylesheet', 'body_prefix',
157 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
158 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
159 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
160 'html_body')
162 def get_transforms(self):
163 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
165 def __init__(self):
166 writers.Writer.__init__(self)
167 self.translator_class = HTMLTranslator
169 def translate(self):
170 self.visitor = visitor = self.translator_class(self.document)
171 self.document.walkabout(visitor)
172 for attr in self.visitor_attributes:
173 setattr(self, attr, getattr(visitor, attr))
174 self.output = self.apply_template()
176 def apply_template(self):
177 template_file = open(self.document.settings.template, 'rb')
178 template = unicode(template_file.read(), 'utf-8')
179 template_file.close()
180 subs = self.interpolation_dict()
181 return template % subs
183 def interpolation_dict(self):
184 subs = {}
185 settings = self.document.settings
186 for attr in self.visitor_attributes:
187 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
188 subs['encoding'] = settings.output_encoding
189 subs['version'] = docutils.__version__
190 return subs
192 def assemble_parts(self):
193 writers.Writer.assemble_parts(self)
194 for part in self.visitor_attributes:
195 self.parts[part] = ''.join(getattr(self, part))
198 class HTMLTranslator(nodes.NodeVisitor):
201 This HTML writer has been optimized to produce visually compact
202 lists (less vertical whitespace). HTML's mixed content models
203 allow list items to contain "<li><p>body elements</p></li>" or
204 "<li>just text</li>" or even "<li>text<p>and body
205 elements</p>combined</li>", each with different effects. It would
206 be best to stick with strict body elements in list items, but they
207 affect vertical spacing in browsers (although they really
208 shouldn't).
210 Here is an outline of the optimization:
212 - Check for and omit <p> tags in "simple" lists: list items
213 contain either a single paragraph, a nested simple list, or a
214 paragraph followed by a nested simple list. This means that
215 this list can be compact:
217 - Item 1.
218 - Item 2.
220 But this list cannot be compact:
222 - Item 1.
224 This second paragraph forces space between list items.
226 - Item 2.
228 - In non-list contexts, omit <p> tags on a paragraph if that
229 paragraph is the only child of its parent (footnotes & citations
230 are allowed a label first).
232 - Regardless of the above, in definitions, table cells, field bodies,
233 option descriptions, and list items, mark the first child with
234 'class="first"' and the last child with 'class="last"'. The stylesheet
235 sets the margins (top & bottom respectively) to 0 for these elements.
237 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
238 option) disables list whitespace optimization.
241 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
242 doctype = (
243 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
244 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
245 doctype_mathml = doctype
247 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
248 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
249 content_type = ('<meta http-equiv="Content-Type"'
250 ' content="text/html; charset=%s" />\n')
251 content_type_mathml = ('<meta http-equiv="Content-Type"'
252 ' content="application/xhtml+xml; charset=%s" />\n')
254 generator = ('<meta name="generator" content="Docutils %s: '
255 'http://docutils.sourceforge.net/" />\n')
257 # Template for the MathJax script in the header:
258 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
259 # The latest version of MathJax from the distributed server:
260 # avaliable to the public under the `MathJax CDN Terms of Service`__
261 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
262 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
263 'config=TeX-AMS-MML_HTMLorMML')
264 # TODO: make this configurable:
266 # a) as extra option or
267 # b) appended to math-output="MathJax"?
269 # If b), which delimiter/delimter-set (':', ',', ' ')?
271 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
272 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
273 words_and_spaces = re.compile(r'\S+| +|\n')
274 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
275 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
277 def __init__(self, document):
278 nodes.NodeVisitor.__init__(self, document)
279 self.settings = settings = document.settings
280 lcode = settings.language_code
281 self.language = languages.get_language(lcode, document.reporter)
282 self.meta = [self.generator % docutils.__version__]
283 self.head_prefix = []
284 self.html_prolog = []
285 if settings.xml_declaration:
286 self.head_prefix.append(self.xml_declaration
287 % settings.output_encoding)
288 # encoding not interpolated:
289 self.html_prolog.append(self.xml_declaration)
290 self.head = self.meta[:]
291 self.stylesheet = [self.stylesheet_call(path)
292 for path in utils.get_stylesheet_list(settings)]
293 self.body_prefix = ['</head>\n<body>\n']
294 # document title, subtitle display
295 self.body_pre_docinfo = []
296 # author, date, etc.
297 self.docinfo = []
298 self.body = []
299 self.fragment = []
300 self.body_suffix = ['</body>\n</html>\n']
301 self.section_level = 0
302 self.initial_header_level = int(settings.initial_header_level)
303 self.math_output = settings.math_output.lower()
304 # A heterogenous stack used in conjunction with the tree traversal.
305 # Make sure that the pops correspond to the pushes:
306 self.context = []
307 self.topic_classes = []
308 self.colspecs = []
309 self.compact_p = 1
310 self.compact_simple = False
311 self.compact_field_list = False
312 self.in_docinfo = False
313 self.in_sidebar = False
314 self.title = []
315 self.subtitle = []
316 self.header = []
317 self.footer = []
318 self.html_head = [self.content_type] # charset not interpolated
319 self.html_title = []
320 self.html_subtitle = []
321 self.html_body = []
322 self.in_document_title = 0 # len(self.body) or 0
323 self.in_mailto = False
324 self.author_in_authors = False
325 self.math_header = ''
327 def astext(self):
328 return ''.join(self.head_prefix + self.head
329 + self.stylesheet + self.body_prefix
330 + self.body_pre_docinfo + self.docinfo
331 + self.body + self.body_suffix)
333 def encode(self, text):
334 """Encode special characters in `text` & return."""
335 # @@@ A codec to do these and all other HTML entities would be nice.
336 text = unicode(text)
337 return text.translate({
338 ord('&'): u'&amp;',
339 ord('<'): u'&lt;',
340 ord('"'): u'&quot;',
341 ord('>'): u'&gt;',
342 ord('@'): u'&#64;', # may thwart some address harvesters
343 # TODO: convert non-breaking space only if needed?
344 0xa0: u'&nbsp;'}) # non-breaking space
346 def cloak_mailto(self, uri):
347 """Try to hide a mailto: URL from harvesters."""
348 # Encode "@" using a URL octet reference (see RFC 1738).
349 # Further cloaking with HTML entities will be done in the
350 # `attval` function.
351 return uri.replace('@', '%40')
353 def cloak_email(self, addr):
354 """Try to hide the link text of a email link from harversters."""
355 # Surround at-signs and periods with <span> tags. ("@" has
356 # already been encoded to "&#64;" by the `encode` method.)
357 addr = addr.replace('&#64;', '<span>&#64;</span>')
358 addr = addr.replace('.', '<span>&#46;</span>')
359 return addr
361 def attval(self, text,
362 whitespace=re.compile('[\n\r\t\v\f]')):
363 """Cleanse, HTML encode, and return attribute value text."""
364 encoded = self.encode(whitespace.sub(' ', text))
365 if self.in_mailto and self.settings.cloak_email_addresses:
366 # Cloak at-signs ("%40") and periods with HTML entities.
367 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
368 encoded = encoded.replace('.', '&#46;')
369 return encoded
371 def stylesheet_call(self, path):
372 """Return code to reference or embed stylesheet file `path`"""
373 if self.settings.embed_stylesheet:
374 try:
375 content = io.FileInput(source_path=path,
376 encoding='utf-8').read()
377 self.settings.record_dependencies.add(path)
378 except IOError, err:
379 msg = u"Cannot embed stylesheet '%s': %s." % (
380 path, SafeString(err.strerror))
381 self.document.reporter.error(msg)
382 return '<--- %s --->\n' % msg
383 return self.embedded_stylesheet % content
384 # else link to style file:
385 if self.settings.stylesheet_path:
386 # adapt path relative to output (cf. config.html#stylesheet-path)
387 path = utils.relative_path(self.settings._destination, path)
388 return self.stylesheet_link % self.encode(path)
390 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
392 Construct and return a start tag given a node (id & class attributes
393 are extracted), tag name, and optional attributes.
395 tagname = tagname.lower()
396 prefix = []
397 atts = {}
398 ids = []
399 for (name, value) in attributes.items():
400 atts[name.lower()] = value
401 classes = node.get('classes', [])
402 if 'class' in atts:
403 classes.append(atts.pop('class'))
404 # move language specification to 'lang' attribute
405 languages = [cls for cls in classes
406 if cls.startswith('language-')]
407 if languages:
408 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
409 atts[self.lang_attribute] = languages[0][9:]
410 classes.pop(classes.index(languages[0]))
411 classes = ' '.join(classes).strip()
412 if classes:
413 atts['class'] = classes
414 assert 'id' not in atts
415 ids.extend(node.get('ids', []))
416 if 'ids' in atts:
417 ids.extend(atts['ids'])
418 del atts['ids']
419 if ids:
420 atts['id'] = ids[0]
421 for id in ids[1:]:
422 # Add empty "span" elements for additional IDs. Note
423 # that we cannot use empty "a" elements because there
424 # may be targets inside of references, but nested "a"
425 # elements aren't allowed in XHTML (even if they do
426 # not all have a "href" attribute).
427 if empty:
428 # Empty tag. Insert target right in front of element.
429 prefix.append('<span id="%s"></span>' % id)
430 else:
431 # Non-empty tag. Place the auxiliary <span> tag
432 # *inside* the element, as the first child.
433 suffix += '<span id="%s"></span>' % id
434 attlist = atts.items()
435 attlist.sort()
436 parts = [tagname]
437 for name, value in attlist:
438 # value=None was used for boolean attributes without
439 # value, but this isn't supported by XHTML.
440 assert value is not None
441 if isinstance(value, list):
442 values = [unicode(v) for v in value]
443 parts.append('%s="%s"' % (name.lower(),
444 self.attval(' '.join(values))))
445 else:
446 parts.append('%s="%s"' % (name.lower(),
447 self.attval(unicode(value))))
448 if empty:
449 infix = ' /'
450 else:
451 infix = ''
452 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
454 def emptytag(self, node, tagname, suffix='\n', **attributes):
455 """Construct and return an XML-compatible empty tag."""
456 return self.starttag(node, tagname, suffix, empty=True, **attributes)
458 def set_class_on_child(self, node, class_, index=0):
460 Set class `class_` on the visible child no. index of `node`.
461 Do nothing if node has fewer children than `index`.
463 children = [n for n in node if not isinstance(n, nodes.Invisible)]
464 try:
465 child = children[index]
466 except IndexError:
467 return
468 child['classes'].append(class_)
470 def set_first_last(self, node):
471 self.set_class_on_child(node, 'first', 0)
472 self.set_class_on_child(node, 'last', -1)
474 def visit_Text(self, node):
475 text = node.astext()
476 encoded = self.encode(text)
477 if self.in_mailto and self.settings.cloak_email_addresses:
478 encoded = self.cloak_email(encoded)
479 self.body.append(encoded)
481 def depart_Text(self, node):
482 pass
484 def visit_abbreviation(self, node):
485 # @@@ implementation incomplete ("title" attribute)
486 self.body.append(self.starttag(node, 'abbr', ''))
488 def depart_abbreviation(self, node):
489 self.body.append('</abbr>')
491 def visit_acronym(self, node):
492 # @@@ implementation incomplete ("title" attribute)
493 self.body.append(self.starttag(node, 'acronym', ''))
495 def depart_acronym(self, node):
496 self.body.append('</acronym>')
498 def visit_address(self, node):
499 self.visit_docinfo_item(node, 'address', meta=False)
500 self.body.append(self.starttag(node, 'pre', CLASS='address'))
502 def depart_address(self, node):
503 self.body.append('\n</pre>\n')
504 self.depart_docinfo_item()
506 def visit_admonition(self, node):
507 self.body.append(self.starttag(node, 'div'))
508 self.set_first_last(node)
510 def depart_admonition(self, node=None):
511 self.body.append('</div>\n')
513 attribution_formats = {'dash': ('&mdash;', ''),
514 'parentheses': ('(', ')'),
515 'parens': ('(', ')'),
516 'none': ('', '')}
518 def visit_attribution(self, node):
519 prefix, suffix = self.attribution_formats[self.settings.attribution]
520 self.context.append(suffix)
521 self.body.append(
522 self.starttag(node, 'p', prefix, CLASS='attribution'))
524 def depart_attribution(self, node):
525 self.body.append(self.context.pop() + '</p>\n')
527 def visit_author(self, node):
528 if isinstance(node.parent, nodes.authors):
529 if self.author_in_authors:
530 self.body.append('\n<br />')
531 else:
532 self.visit_docinfo_item(node, 'author')
534 def depart_author(self, node):
535 if isinstance(node.parent, nodes.authors):
536 self.author_in_authors = True
537 else:
538 self.depart_docinfo_item()
540 def visit_authors(self, node):
541 self.visit_docinfo_item(node, 'authors')
542 self.author_in_authors = False # initialize
544 def depart_authors(self, node):
545 self.depart_docinfo_item()
547 def visit_block_quote(self, node):
548 self.body.append(self.starttag(node, 'blockquote'))
550 def depart_block_quote(self, node):
551 self.body.append('</blockquote>\n')
553 def check_simple_list(self, node):
554 """Check for a simple list that can be rendered compactly."""
555 visitor = SimpleListChecker(self.document)
556 try:
557 node.walk(visitor)
558 except nodes.NodeFound:
559 return None
560 else:
561 return 1
563 def is_compactable(self, node):
564 return ('compact' in node['classes']
565 or (self.settings.compact_lists
566 and 'open' not in node['classes']
567 and (self.compact_simple
568 or self.topic_classes == ['contents']
569 or self.check_simple_list(node))))
571 def visit_bullet_list(self, node):
572 atts = {}
573 old_compact_simple = self.compact_simple
574 self.context.append((self.compact_simple, self.compact_p))
575 self.compact_p = None
576 self.compact_simple = self.is_compactable(node)
577 if self.compact_simple and not old_compact_simple:
578 atts['class'] = 'simple'
579 self.body.append(self.starttag(node, 'ul', **atts))
581 def depart_bullet_list(self, node):
582 self.compact_simple, self.compact_p = self.context.pop()
583 self.body.append('</ul>\n')
585 def visit_caption(self, node):
586 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
588 def depart_caption(self, node):
589 self.body.append('</p>\n')
591 def visit_citation(self, node):
592 self.body.append(self.starttag(node, 'table',
593 CLASS='docutils citation',
594 frame="void", rules="none"))
595 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
596 '<tbody valign="top">\n'
597 '<tr>')
598 self.footnote_backrefs(node)
600 def depart_citation(self, node):
601 self.body.append('</td></tr>\n'
602 '</tbody>\n</table>\n')
604 def visit_citation_reference(self, node):
605 href = '#'
606 if 'refid' in node:
607 href += node['refid']
608 elif 'refname' in node:
609 href += self.document.nameids[node['refname']]
610 # else: # TODO system message (or already in the transform)?
611 # 'Citation reference missing.'
612 self.body.append(self.starttag(
613 node, 'a', '[', CLASS='citation-reference', href=href))
615 def depart_citation_reference(self, node):
616 self.body.append(']</a>')
618 def visit_classifier(self, node):
619 self.body.append(' <span class="classifier-delimiter">:</span> ')
620 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
622 def depart_classifier(self, node):
623 self.body.append('</span>')
625 def visit_colspec(self, node):
626 self.colspecs.append(node)
627 # "stubs" list is an attribute of the tgroup element:
628 node.parent.stubs.append(node.attributes.get('stub'))
630 def depart_colspec(self, node):
631 pass
633 def write_colspecs(self):
634 width = 0
635 for node in self.colspecs:
636 width += node['colwidth']
637 for node in self.colspecs:
638 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
639 self.body.append(self.emptytag(node, 'col',
640 width='%i%%' % colwidth))
641 self.colspecs = []
643 def visit_comment(self, node,
644 sub=re.compile('-(?=-)').sub):
645 """Escape double-dashes in comment text."""
646 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
647 # Content already processed:
648 raise nodes.SkipNode
650 def visit_compound(self, node):
651 self.body.append(self.starttag(node, 'div', CLASS='compound'))
652 if len(node) > 1:
653 node[0]['classes'].append('compound-first')
654 node[-1]['classes'].append('compound-last')
655 for child in node[1:-1]:
656 child['classes'].append('compound-middle')
658 def depart_compound(self, node):
659 self.body.append('</div>\n')
661 def visit_container(self, node):
662 self.body.append(self.starttag(node, 'div', CLASS='container'))
664 def depart_container(self, node):
665 self.body.append('</div>\n')
667 def visit_contact(self, node):
668 self.visit_docinfo_item(node, 'contact', meta=False)
670 def depart_contact(self, node):
671 self.depart_docinfo_item()
673 def visit_copyright(self, node):
674 self.visit_docinfo_item(node, 'copyright')
676 def depart_copyright(self, node):
677 self.depart_docinfo_item()
679 def visit_date(self, node):
680 self.visit_docinfo_item(node, 'date')
682 def depart_date(self, node):
683 self.depart_docinfo_item()
685 def visit_decoration(self, node):
686 pass
688 def depart_decoration(self, node):
689 pass
691 def visit_definition(self, node):
692 self.body.append('</dt>\n')
693 self.body.append(self.starttag(node, 'dd', ''))
694 self.set_first_last(node)
696 def depart_definition(self, node):
697 self.body.append('</dd>\n')
699 def visit_definition_list(self, node):
700 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
702 def depart_definition_list(self, node):
703 self.body.append('</dl>\n')
705 def visit_definition_list_item(self, node):
706 pass
708 def depart_definition_list_item(self, node):
709 pass
711 def visit_description(self, node):
712 self.body.append(self.starttag(node, 'td', ''))
713 self.set_first_last(node)
715 def depart_description(self, node):
716 self.body.append('</td>')
718 def visit_docinfo(self, node):
719 self.context.append(len(self.body))
720 self.body.append(self.starttag(node, 'table',
721 CLASS='docinfo',
722 frame="void", rules="none"))
723 self.body.append('<col class="docinfo-name" />\n'
724 '<col class="docinfo-content" />\n'
725 '<tbody valign="top">\n')
726 self.in_docinfo = True
728 def depart_docinfo(self, node):
729 self.body.append('</tbody>\n</table>\n')
730 self.in_docinfo = False
731 start = self.context.pop()
732 self.docinfo = self.body[start:]
733 self.body = []
735 def visit_docinfo_item(self, node, name, meta=True):
736 if meta:
737 meta_tag = '<meta name="%s" content="%s" />\n' \
738 % (name, self.attval(node.astext()))
739 self.add_meta(meta_tag)
740 self.body.append(self.starttag(node, 'tr', ''))
741 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
742 % self.language.labels[name])
743 if len(node):
744 if isinstance(node[0], nodes.Element):
745 node[0]['classes'].append('first')
746 if isinstance(node[-1], nodes.Element):
747 node[-1]['classes'].append('last')
749 def depart_docinfo_item(self):
750 self.body.append('</td></tr>\n')
752 def visit_doctest_block(self, node):
753 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
755 def depart_doctest_block(self, node):
756 self.body.append('\n</pre>\n')
758 def visit_document(self, node):
759 self.head.append('<title>%s</title>\n'
760 % self.encode(node.get('title', '')))
762 def depart_document(self, node):
763 self.head_prefix.extend([self.doctype,
764 self.head_prefix_template %
765 {'lang': self.settings.language_code}])
766 self.html_prolog.append(self.doctype)
767 self.meta.insert(0, self.content_type % self.settings.output_encoding)
768 self.head.insert(0, self.content_type % self.settings.output_encoding)
769 if self.math_header:
770 self.head.append(self.math_header)
771 # skip content-type meta tag with interpolated charset value:
772 self.html_head.extend(self.head[1:])
773 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
774 self.body_suffix.insert(0, '</div>\n')
775 self.fragment.extend(self.body) # self.fragment is the "naked" body
776 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
777 + self.docinfo + self.body
778 + self.body_suffix[:-1])
779 assert not self.context, 'len(context) = %s' % len(self.context)
781 def visit_emphasis(self, node):
782 self.body.append(self.starttag(node, 'em', ''))
784 def depart_emphasis(self, node):
785 self.body.append('</em>')
787 def visit_entry(self, node):
788 atts = {'class': []}
789 if isinstance(node.parent.parent, nodes.thead):
790 atts['class'].append('head')
791 if node.parent.parent.parent.stubs[node.parent.column]:
792 # "stubs" list is an attribute of the tgroup element
793 atts['class'].append('stub')
794 if atts['class']:
795 tagname = 'th'
796 atts['class'] = ' '.join(atts['class'])
797 else:
798 tagname = 'td'
799 del atts['class']
800 node.parent.column += 1
801 if 'morerows' in node:
802 atts['rowspan'] = node['morerows'] + 1
803 if 'morecols' in node:
804 atts['colspan'] = node['morecols'] + 1
805 node.parent.column += node['morecols']
806 self.body.append(self.starttag(node, tagname, '', **atts))
807 self.context.append('</%s>\n' % tagname.lower())
808 if len(node) == 0: # empty cell
809 self.body.append('&nbsp;')
810 self.set_first_last(node)
812 def depart_entry(self, node):
813 self.body.append(self.context.pop())
815 def visit_enumerated_list(self, node):
817 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
818 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
819 usable.
821 atts = {}
822 if 'start' in node:
823 atts['start'] = node['start']
824 if 'enumtype' in node:
825 atts['class'] = node['enumtype']
826 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
827 # single "format" attribute? Use CSS2?
828 old_compact_simple = self.compact_simple
829 self.context.append((self.compact_simple, self.compact_p))
830 self.compact_p = None
831 self.compact_simple = self.is_compactable(node)
832 if self.compact_simple and not old_compact_simple:
833 atts['class'] = (atts.get('class', '') + ' simple').strip()
834 self.body.append(self.starttag(node, 'ol', **atts))
836 def depart_enumerated_list(self, node):
837 self.compact_simple, self.compact_p = self.context.pop()
838 self.body.append('</ol>\n')
840 def visit_field(self, node):
841 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
843 def depart_field(self, node):
844 self.body.append('</tr>\n')
846 def visit_field_body(self, node):
847 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
848 self.set_class_on_child(node, 'first', 0)
849 field = node.parent
850 if (self.compact_field_list or
851 isinstance(field.parent, nodes.docinfo) or
852 field.parent.index(field) == len(field.parent) - 1):
853 # If we are in a compact list, the docinfo, or if this is
854 # the last field of the field list, do not add vertical
855 # space after last element.
856 self.set_class_on_child(node, 'last', -1)
858 def depart_field_body(self, node):
859 self.body.append('</td>\n')
861 def visit_field_list(self, node):
862 self.context.append((self.compact_field_list, self.compact_p))
863 self.compact_p = None
864 if 'compact' in node['classes']:
865 self.compact_field_list = True
866 elif (self.settings.compact_field_lists
867 and 'open' not in node['classes']):
868 self.compact_field_list = True
869 if self.compact_field_list:
870 for field in node:
871 field_body = field[-1]
872 assert isinstance(field_body, nodes.field_body)
873 children = [n for n in field_body
874 if not isinstance(n, nodes.Invisible)]
875 if not (len(children) == 0 or
876 len(children) == 1 and
877 isinstance(children[0],
878 (nodes.paragraph, nodes.line_block))):
879 self.compact_field_list = False
880 break
881 self.body.append(self.starttag(node, 'table', frame='void',
882 rules='none',
883 CLASS='docutils field-list'))
884 self.body.append('<col class="field-name" />\n'
885 '<col class="field-body" />\n'
886 '<tbody valign="top">\n')
888 def depart_field_list(self, node):
889 self.body.append('</tbody>\n</table>\n')
890 self.compact_field_list, self.compact_p = self.context.pop()
892 def visit_field_name(self, node):
893 atts = {}
894 if self.in_docinfo:
895 atts['class'] = 'docinfo-name'
896 else:
897 atts['class'] = 'field-name'
898 if ( self.settings.field_name_limit
899 and len(node.astext()) > self.settings.field_name_limit):
900 atts['colspan'] = 2
901 self.context.append('</tr>\n'
902 + self.starttag(node.parent, 'tr', '')
903 + '<td>&nbsp;</td>')
904 else:
905 self.context.append('')
906 self.body.append(self.starttag(node, 'th', '', **atts))
908 def depart_field_name(self, node):
909 self.body.append(':</th>')
910 self.body.append(self.context.pop())
912 def visit_figure(self, node):
913 atts = {'class': 'figure'}
914 if node.get('width'):
915 atts['style'] = 'width: %s' % node['width']
916 if node.get('align'):
917 atts['class'] += " align-" + node['align']
918 self.body.append(self.starttag(node, 'div', **atts))
920 def depart_figure(self, node):
921 self.body.append('</div>\n')
923 def visit_footer(self, node):
924 self.context.append(len(self.body))
926 def depart_footer(self, node):
927 start = self.context.pop()
928 footer = [self.starttag(node, 'div', CLASS='footer'),
929 '<hr class="footer" />\n']
930 footer.extend(self.body[start:])
931 footer.append('\n</div>\n')
932 self.footer.extend(footer)
933 self.body_suffix[:0] = footer
934 del self.body[start:]
936 def visit_footnote(self, node):
937 self.body.append(self.starttag(node, 'table',
938 CLASS='docutils footnote',
939 frame="void", rules="none"))
940 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
941 '<tbody valign="top">\n'
942 '<tr>')
943 self.footnote_backrefs(node)
945 def footnote_backrefs(self, node):
946 backlinks = []
947 backrefs = node['backrefs']
948 if self.settings.footnote_backlinks and backrefs:
949 if len(backrefs) == 1:
950 self.context.append('')
951 self.context.append('</a>')
952 self.context.append('<a class="fn-backref" href="#%s">'
953 % backrefs[0])
954 else:
955 i = 1
956 for backref in backrefs:
957 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
958 % (backref, i))
959 i += 1
960 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
961 self.context += ['', '']
962 else:
963 self.context.append('')
964 self.context += ['', '']
965 # If the node does not only consist of a label.
966 if len(node) > 1:
967 # If there are preceding backlinks, we do not set class
968 # 'first', because we need to retain the top-margin.
969 if not backlinks:
970 node[1]['classes'].append('first')
971 node[-1]['classes'].append('last')
973 def depart_footnote(self, node):
974 self.body.append('</td></tr>\n'
975 '</tbody>\n</table>\n')
977 def visit_footnote_reference(self, node):
978 href = '#' + node['refid']
979 format = self.settings.footnote_references
980 if format == 'brackets':
981 suffix = '['
982 self.context.append(']')
983 else:
984 assert format == 'superscript'
985 suffix = '<sup>'
986 self.context.append('</sup>')
987 self.body.append(self.starttag(node, 'a', suffix,
988 CLASS='footnote-reference', href=href))
990 def depart_footnote_reference(self, node):
991 self.body.append(self.context.pop() + '</a>')
993 def visit_generated(self, node):
994 pass
996 def depart_generated(self, node):
997 pass
999 def visit_header(self, node):
1000 self.context.append(len(self.body))
1002 def depart_header(self, node):
1003 start = self.context.pop()
1004 header = [self.starttag(node, 'div', CLASS='header')]
1005 header.extend(self.body[start:])
1006 header.append('\n<hr class="header"/>\n</div>\n')
1007 self.body_prefix.extend(header)
1008 self.header.extend(header)
1009 del self.body[start:]
1011 def visit_image(self, node):
1012 atts = {}
1013 uri = node['uri']
1014 # place SVG and SWF images in an <object> element
1015 types = {'.svg': 'image/svg+xml',
1016 '.swf': 'application/x-shockwave-flash'}
1017 ext = os.path.splitext(uri)[1].lower()
1018 if ext in ('.svg', '.swf'):
1019 atts['data'] = uri
1020 atts['type'] = types[ext]
1021 else:
1022 atts['src'] = uri
1023 atts['alt'] = node.get('alt', uri)
1024 # image size
1025 if 'width' in node:
1026 atts['width'] = node['width']
1027 if 'height' in node:
1028 atts['height'] = node['height']
1029 if 'scale' in node:
1030 if (PIL and not ('width' in node and 'height' in node)
1031 and self.settings.file_insertion_enabled):
1032 imagepath = urllib.url2pathname(uri)
1033 try:
1034 img = PIL.Image.open(
1035 imagepath.encode(sys.getfilesystemencoding()))
1036 except (IOError, UnicodeEncodeError):
1037 pass # TODO: warn?
1038 else:
1039 self.settings.record_dependencies.add(
1040 imagepath.replace('\\', '/'))
1041 if 'width' not in atts:
1042 atts['width'] = str(img.size[0])
1043 if 'height' not in atts:
1044 atts['height'] = str(img.size[1])
1045 del img
1046 for att_name in 'width', 'height':
1047 if att_name in atts:
1048 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1049 assert match
1050 atts[att_name] = '%s%s' % (
1051 float(match.group(1)) * (float(node['scale']) / 100),
1052 match.group(2))
1053 style = []
1054 for att_name in 'width', 'height':
1055 if att_name in atts:
1056 if re.match(r'^[0-9.]+$', atts[att_name]):
1057 # Interpret unitless values as pixels.
1058 atts[att_name] += 'px'
1059 style.append('%s: %s;' % (att_name, atts[att_name]))
1060 del atts[att_name]
1061 if style:
1062 atts['style'] = ' '.join(style)
1063 if (isinstance(node.parent, nodes.TextElement) or
1064 (isinstance(node.parent, nodes.reference) and
1065 not isinstance(node.parent.parent, nodes.TextElement))):
1066 # Inline context or surrounded by <a>...</a>.
1067 suffix = ''
1068 else:
1069 suffix = '\n'
1070 if 'align' in node:
1071 atts['class'] = 'align-%s' % node['align']
1072 self.context.append('')
1073 if ext in ('.svg', '.swf'): # place in an object element,
1074 # do NOT use an empty tag: incorrect rendering in browsers
1075 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1076 node.get('alt', uri) + '</object>' + suffix)
1077 else:
1078 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1080 def depart_image(self, node):
1081 self.body.append(self.context.pop())
1083 def visit_inline(self, node):
1084 self.body.append(self.starttag(node, 'span', ''))
1086 def depart_inline(self, node):
1087 self.body.append('</span>')
1089 def visit_label(self, node):
1090 # Context added in footnote_backrefs.
1091 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1092 CLASS='label'))
1094 def depart_label(self, node):
1095 # Context added in footnote_backrefs.
1096 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1098 def visit_legend(self, node):
1099 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1101 def depart_legend(self, node):
1102 self.body.append('</div>\n')
1104 def visit_line(self, node):
1105 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1106 if not len(node):
1107 self.body.append('<br />')
1109 def depart_line(self, node):
1110 self.body.append('</div>\n')
1112 def visit_line_block(self, node):
1113 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1115 def depart_line_block(self, node):
1116 self.body.append('</div>\n')
1118 def visit_list_item(self, node):
1119 self.body.append(self.starttag(node, 'li', ''))
1120 if len(node):
1121 node[0]['classes'].append('first')
1123 def depart_list_item(self, node):
1124 self.body.append('</li>\n')
1126 def visit_literal(self, node):
1127 # special case: inline code:
1128 classes = node.get('classes', [])
1129 if 'code' in classes:
1130 # filter 'code' from class arguments
1131 node['classes'] = [cls for cls in classes if cls != 'code']
1132 self.body.append(self.starttag(node, 'code', ''))
1133 node['classes'] = classes # restore for test in depart_literal()
1134 return
1135 # Process text to prevent tokens from wrapping.
1136 self.body.append(
1137 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1138 text = node.astext()
1139 for token in self.words_and_spaces.findall(text):
1140 if token.strip():
1141 # Protect text like "--an-option" and the regular expression
1142 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1143 if self.sollbruchstelle.search(token):
1144 self.body.append('<span class="pre">%s</span>'
1145 % self.encode(token))
1146 else:
1147 self.body.append(self.encode(token))
1148 elif token in ('\n', ' '):
1149 # Allow breaks at whitespace:
1150 self.body.append(token)
1151 else:
1152 # Protect runs of multiple spaces; the last space can wrap:
1153 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1154 self.body.append('</tt>')
1155 # Content already processed:
1156 raise nodes.SkipNode
1158 def depart_literal(self, node):
1159 if 'code' in node.get('classes', []):
1160 self.body.append('</code>')
1162 def visit_literal_block(self, node):
1163 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1165 def depart_literal_block(self, node):
1166 self.body.append('\n</pre>\n')
1168 def visit_math(self, node, math_env=''):
1169 # As there is no native HTML math support, we provide alternatives:
1170 # LaTeX and MathJax math_output modes simply wrap the content,
1171 # HTML and MathML math_output modes also convert the math_code.
1172 # If the method is called from visit_math_block(), math_env != ''.
1174 # HTML container
1175 tags = {# math_output: (block, inline, class-arguments)
1176 'mathml': ('div', '', ''),
1177 'html': ('div', 'span', 'formula'),
1178 'mathjax': ('div', 'span', 'math'),
1179 'latex': ('pre', 'tt', 'math'),
1181 tag = tags[self.math_output][math_env == '']
1182 clsarg = tags[self.math_output][2]
1183 # LaTeX container
1184 wrappers = {# math_mode: (inline, block)
1185 'mathml': (None, None),
1186 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1187 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1188 'latex': (None, None),
1190 wrapper = wrappers[self.math_output][math_env != '']
1191 # get and wrap content
1192 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1193 if wrapper and math_env:
1194 math_code = wrapper % (math_env, math_code, math_env)
1195 elif wrapper:
1196 math_code = wrapper % math_code
1197 # settings and conversion
1198 if self.math_output in ('latex', 'mathjax'):
1199 math_code = self.encode(math_code)
1200 if self.math_output == 'mathjax':
1201 self.math_header = self.mathjax_script % self.mathjax_url
1202 elif self.math_output == 'html':
1203 math_code = math2html(math_code)
1204 elif self.math_output == 'mathml':
1205 self.doctype = self.doctype_mathml
1206 self.content_type = self.content_type_mathml
1207 try:
1208 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1209 math_code = ''.join(mathml_tree.xml())
1210 except SyntaxError, err:
1211 err_node = self.document.reporter.error(err, base_node=node)
1212 self.visit_system_message(err_node)
1213 self.body.append(self.starttag(node, 'p'))
1214 self.body.append(u','.join(err.args))
1215 self.body.append('</p>\n')
1216 self.body.append(self.starttag(node, 'pre',
1217 CLASS='literal-block'))
1218 self.body.append(self.encode(math_code))
1219 self.body.append('\n</pre>\n')
1220 self.depart_system_message(err_node)
1221 raise nodes.SkipNode
1222 # append to document body
1223 if tag:
1224 self.body.append(self.starttag(node, tag, CLASS=clsarg))
1225 self.body.append(math_code)
1226 if math_env:
1227 self.body.append('\n')
1228 if tag:
1229 self.body.append('</%s>\n' % tag)
1230 # Content already processed:
1231 raise nodes.SkipNode
1233 def depart_math(self, node):
1234 pass # never reached
1236 def visit_math_block(self, node):
1237 # print node.astext().encode('utf8')
1238 math_env = pick_math_environment(node.astext())
1239 self.visit_math(node, math_env=math_env)
1241 def depart_math_block(self, node):
1242 pass # never reached
1244 def visit_meta(self, node):
1245 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1246 self.add_meta(meta)
1248 def depart_meta(self, node):
1249 pass
1251 def add_meta(self, tag):
1252 self.meta.append(tag)
1253 self.head.append(tag)
1255 def visit_option(self, node):
1256 if self.context[-1]:
1257 self.body.append(', ')
1258 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1260 def depart_option(self, node):
1261 self.body.append('</span>')
1262 self.context[-1] += 1
1264 def visit_option_argument(self, node):
1265 self.body.append(node.get('delimiter', ' '))
1266 self.body.append(self.starttag(node, 'var', ''))
1268 def depart_option_argument(self, node):
1269 self.body.append('</var>')
1271 def visit_option_group(self, node):
1272 atts = {}
1273 if ( self.settings.option_limit
1274 and len(node.astext()) > self.settings.option_limit):
1275 atts['colspan'] = 2
1276 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1277 else:
1278 self.context.append('')
1279 self.body.append(
1280 self.starttag(node, 'td', CLASS='option-group', **atts))
1281 self.body.append('<kbd>')
1282 self.context.append(0) # count number of options
1284 def depart_option_group(self, node):
1285 self.context.pop()
1286 self.body.append('</kbd></td>\n')
1287 self.body.append(self.context.pop())
1289 def visit_option_list(self, node):
1290 self.body.append(
1291 self.starttag(node, 'table', CLASS='docutils option-list',
1292 frame="void", rules="none"))
1293 self.body.append('<col class="option" />\n'
1294 '<col class="description" />\n'
1295 '<tbody valign="top">\n')
1297 def depart_option_list(self, node):
1298 self.body.append('</tbody>\n</table>\n')
1300 def visit_option_list_item(self, node):
1301 self.body.append(self.starttag(node, 'tr', ''))
1303 def depart_option_list_item(self, node):
1304 self.body.append('</tr>\n')
1306 def visit_option_string(self, node):
1307 pass
1309 def depart_option_string(self, node):
1310 pass
1312 def visit_organization(self, node):
1313 self.visit_docinfo_item(node, 'organization')
1315 def depart_organization(self, node):
1316 self.depart_docinfo_item()
1318 def should_be_compact_paragraph(self, node):
1320 Determine if the <p> tags around paragraph ``node`` can be omitted.
1322 if (isinstance(node.parent, nodes.document) or
1323 isinstance(node.parent, nodes.compound)):
1324 # Never compact paragraphs in document or compound.
1325 return 0
1326 for key, value in node.attlist():
1327 if (node.is_not_default(key) and
1328 not (key == 'classes' and value in
1329 ([], ['first'], ['last'], ['first', 'last']))):
1330 # Attribute which needs to survive.
1331 return 0
1332 first = isinstance(node.parent[0], nodes.label) # skip label
1333 for child in node.parent.children[first:]:
1334 # only first paragraph can be compact
1335 if isinstance(child, nodes.Invisible):
1336 continue
1337 if child is node:
1338 break
1339 return 0
1340 parent_length = len([n for n in node.parent if not isinstance(
1341 n, (nodes.Invisible, nodes.label))])
1342 if ( self.compact_simple
1343 or self.compact_field_list
1344 or self.compact_p and parent_length == 1):
1345 return 1
1346 return 0
1348 def visit_paragraph(self, node):
1349 if self.should_be_compact_paragraph(node):
1350 self.context.append('')
1351 else:
1352 self.body.append(self.starttag(node, 'p', ''))
1353 self.context.append('</p>\n')
1355 def depart_paragraph(self, node):
1356 self.body.append(self.context.pop())
1358 def visit_problematic(self, node):
1359 if node.hasattr('refid'):
1360 self.body.append('<a href="#%s">' % node['refid'])
1361 self.context.append('</a>')
1362 else:
1363 self.context.append('')
1364 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1366 def depart_problematic(self, node):
1367 self.body.append('</span>')
1368 self.body.append(self.context.pop())
1370 def visit_raw(self, node):
1371 if 'html' in node.get('format', '').split():
1372 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1373 if node['classes']:
1374 self.body.append(self.starttag(node, t, suffix=''))
1375 self.body.append(node.astext())
1376 if node['classes']:
1377 self.body.append('</%s>' % t)
1378 # Keep non-HTML raw text out of output:
1379 raise nodes.SkipNode
1381 def visit_reference(self, node):
1382 atts = {'class': 'reference'}
1383 if 'refuri' in node:
1384 atts['href'] = node['refuri']
1385 if ( self.settings.cloak_email_addresses
1386 and atts['href'].startswith('mailto:')):
1387 atts['href'] = self.cloak_mailto(atts['href'])
1388 self.in_mailto = True
1389 atts['class'] += ' external'
1390 else:
1391 assert 'refid' in node, \
1392 'References must have "refuri" or "refid" attribute.'
1393 atts['href'] = '#' + node['refid']
1394 atts['class'] += ' internal'
1395 if not isinstance(node.parent, nodes.TextElement):
1396 assert len(node) == 1 and isinstance(node[0], nodes.image)
1397 atts['class'] += ' image-reference'
1398 self.body.append(self.starttag(node, 'a', '', **atts))
1400 def depart_reference(self, node):
1401 self.body.append('</a>')
1402 if not isinstance(node.parent, nodes.TextElement):
1403 self.body.append('\n')
1404 self.in_mailto = False
1406 def visit_revision(self, node):
1407 self.visit_docinfo_item(node, 'revision', meta=False)
1409 def depart_revision(self, node):
1410 self.depart_docinfo_item()
1412 def visit_row(self, node):
1413 self.body.append(self.starttag(node, 'tr', ''))
1414 node.column = 0
1416 def depart_row(self, node):
1417 self.body.append('</tr>\n')
1419 def visit_rubric(self, node):
1420 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1422 def depart_rubric(self, node):
1423 self.body.append('</p>\n')
1425 def visit_section(self, node):
1426 self.section_level += 1
1427 self.body.append(
1428 self.starttag(node, 'div', CLASS='section'))
1430 def depart_section(self, node):
1431 self.section_level -= 1
1432 self.body.append('</div>\n')
1434 def visit_sidebar(self, node):
1435 self.body.append(
1436 self.starttag(node, 'div', CLASS='sidebar'))
1437 self.set_first_last(node)
1438 self.in_sidebar = True
1440 def depart_sidebar(self, node):
1441 self.body.append('</div>\n')
1442 self.in_sidebar = False
1444 def visit_status(self, node):
1445 self.visit_docinfo_item(node, 'status', meta=False)
1447 def depart_status(self, node):
1448 self.depart_docinfo_item()
1450 def visit_strong(self, node):
1451 self.body.append(self.starttag(node, 'strong', ''))
1453 def depart_strong(self, node):
1454 self.body.append('</strong>')
1456 def visit_subscript(self, node):
1457 self.body.append(self.starttag(node, 'sub', ''))
1459 def depart_subscript(self, node):
1460 self.body.append('</sub>')
1462 def visit_substitution_definition(self, node):
1463 """Internal only."""
1464 raise nodes.SkipNode
1466 def visit_substitution_reference(self, node):
1467 self.unimplemented_visit(node)
1469 def visit_subtitle(self, node):
1470 if isinstance(node.parent, nodes.sidebar):
1471 self.body.append(self.starttag(node, 'p', '',
1472 CLASS='sidebar-subtitle'))
1473 self.context.append('</p>\n')
1474 elif isinstance(node.parent, nodes.document):
1475 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1476 self.context.append('</h2>\n')
1477 self.in_document_title = len(self.body)
1478 elif isinstance(node.parent, nodes.section):
1479 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1480 self.body.append(
1481 self.starttag(node, tag, '', CLASS='section-subtitle') +
1482 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1483 self.context.append('</span></%s>\n' % tag)
1485 def depart_subtitle(self, node):
1486 self.body.append(self.context.pop())
1487 if self.in_document_title:
1488 self.subtitle = self.body[self.in_document_title:-1]
1489 self.in_document_title = 0
1490 self.body_pre_docinfo.extend(self.body)
1491 self.html_subtitle.extend(self.body)
1492 del self.body[:]
1494 def visit_superscript(self, node):
1495 self.body.append(self.starttag(node, 'sup', ''))
1497 def depart_superscript(self, node):
1498 self.body.append('</sup>')
1500 def visit_system_message(self, node):
1501 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1502 self.body.append('<p class="system-message-title">')
1503 backref_text = ''
1504 if len(node['backrefs']):
1505 backrefs = node['backrefs']
1506 if len(backrefs) == 1:
1507 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1508 % backrefs[0])
1509 else:
1510 i = 1
1511 backlinks = []
1512 for backref in backrefs:
1513 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1514 i += 1
1515 backref_text = ('; <em>backlinks: %s</em>'
1516 % ', '.join(backlinks))
1517 if node.hasattr('line'):
1518 line = ', line %s' % node['line']
1519 else:
1520 line = ''
1521 self.body.append('System Message: %s/%s '
1522 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1523 % (node['type'], node['level'],
1524 self.encode(node['source']), line, backref_text))
1526 def depart_system_message(self, node):
1527 self.body.append('</div>\n')
1529 def visit_table(self, node):
1530 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1531 self.body.append(
1532 self.starttag(node, 'table', CLASS=classes, border="1"))
1534 def depart_table(self, node):
1535 self.body.append('</table>\n')
1537 def visit_target(self, node):
1538 if not ('refuri' in node or 'refid' in node
1539 or 'refname' in node):
1540 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1541 self.context.append('</span>')
1542 else:
1543 self.context.append('')
1545 def depart_target(self, node):
1546 self.body.append(self.context.pop())
1548 def visit_tbody(self, node):
1549 self.write_colspecs()
1550 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1551 self.body.append(self.starttag(node, 'tbody', valign='top'))
1553 def depart_tbody(self, node):
1554 self.body.append('</tbody>\n')
1556 def visit_term(self, node):
1557 self.body.append(self.starttag(node, 'dt', ''))
1559 def depart_term(self, node):
1561 Leave the end tag to `self.visit_definition()`, in case there's a
1562 classifier.
1564 pass
1566 def visit_tgroup(self, node):
1567 # Mozilla needs <colgroup>:
1568 self.body.append(self.starttag(node, 'colgroup'))
1569 # Appended by thead or tbody:
1570 self.context.append('</colgroup>\n')
1571 node.stubs = []
1573 def depart_tgroup(self, node):
1574 pass
1576 def visit_thead(self, node):
1577 self.write_colspecs()
1578 self.body.append(self.context.pop()) # '</colgroup>\n'
1579 # There may or may not be a <thead>; this is for <tbody> to use:
1580 self.context.append('')
1581 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1583 def depart_thead(self, node):
1584 self.body.append('</thead>\n')
1586 def visit_title(self, node):
1587 """Only 6 section levels are supported by HTML."""
1588 check_id = 0 # TODO: is this a bool (False) or a counter?
1589 close_tag = '</p>\n'
1590 if isinstance(node.parent, nodes.topic):
1591 self.body.append(
1592 self.starttag(node, 'p', '', CLASS='topic-title first'))
1593 elif isinstance(node.parent, nodes.sidebar):
1594 self.body.append(
1595 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1596 elif isinstance(node.parent, nodes.Admonition):
1597 self.body.append(
1598 self.starttag(node, 'p', '', CLASS='admonition-title'))
1599 elif isinstance(node.parent, nodes.table):
1600 self.body.append(
1601 self.starttag(node, 'caption', ''))
1602 close_tag = '</caption>\n'
1603 elif isinstance(node.parent, nodes.document):
1604 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1605 close_tag = '</h1>\n'
1606 self.in_document_title = len(self.body)
1607 else:
1608 assert isinstance(node.parent, nodes.section)
1609 h_level = self.section_level + self.initial_header_level - 1
1610 atts = {}
1611 if (len(node.parent) >= 2 and
1612 isinstance(node.parent[1], nodes.subtitle)):
1613 atts['CLASS'] = 'with-subtitle'
1614 self.body.append(
1615 self.starttag(node, 'h%s' % h_level, '', **atts))
1616 atts = {}
1617 if node.hasattr('refid'):
1618 atts['class'] = 'toc-backref'
1619 atts['href'] = '#' + node['refid']
1620 if atts:
1621 self.body.append(self.starttag({}, 'a', '', **atts))
1622 close_tag = '</a></h%s>\n' % (h_level)
1623 else:
1624 close_tag = '</h%s>\n' % (h_level)
1625 self.context.append(close_tag)
1627 def depart_title(self, node):
1628 self.body.append(self.context.pop())
1629 if self.in_document_title:
1630 self.title = self.body[self.in_document_title:-1]
1631 self.in_document_title = 0
1632 self.body_pre_docinfo.extend(self.body)
1633 self.html_title.extend(self.body)
1634 del self.body[:]
1636 def visit_title_reference(self, node):
1637 self.body.append(self.starttag(node, 'cite', ''))
1639 def depart_title_reference(self, node):
1640 self.body.append('</cite>')
1642 def visit_topic(self, node):
1643 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1644 self.topic_classes = node['classes']
1646 def depart_topic(self, node):
1647 self.body.append('</div>\n')
1648 self.topic_classes = []
1650 def visit_transition(self, node):
1651 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1653 def depart_transition(self, node):
1654 pass
1656 def visit_version(self, node):
1657 self.visit_docinfo_item(node, 'version', meta=False)
1659 def depart_version(self, node):
1660 self.depart_docinfo_item()
1662 def unimplemented_visit(self, node):
1663 raise NotImplementedError('visiting unimplemented node type: %s'
1664 % node.__class__.__name__)
1667 class SimpleListChecker(nodes.GenericNodeVisitor):
1670 Raise `nodes.NodeFound` if non-simple list item is encountered.
1672 Here "simple" means a list item containing nothing other than a single
1673 paragraph, a simple list, or a paragraph followed by a simple list.
1676 def default_visit(self, node):
1677 raise nodes.NodeFound
1679 def visit_bullet_list(self, node):
1680 pass
1682 def visit_enumerated_list(self, node):
1683 pass
1685 def visit_list_item(self, node):
1686 children = []
1687 for child in node.children:
1688 if not isinstance(child, nodes.Invisible):
1689 children.append(child)
1690 if (children and isinstance(children[0], nodes.paragraph)
1691 and (isinstance(children[-1], nodes.bullet_list)
1692 or isinstance(children[-1], nodes.enumerated_list))):
1693 children.pop()
1694 if len(children) <= 1:
1695 return
1696 else:
1697 raise nodes.NodeFound
1699 def visit_paragraph(self, node):
1700 raise nodes.SkipNode
1702 def invisible_visit(self, node):
1703 """Invisible nodes should be ignored."""
1704 raise nodes.SkipNode
1706 visit_comment = invisible_visit
1707 visit_substitution_definition = invisible_visit
1708 visit_target = invisible_visit
1709 visit_pending = invisible_visit