Use validate_comma_separated_string_list for stylesheet setting.
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob569c5a211d9270b3433f927c7721c4aa9b27d16c
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 'validator': frontend.validate_comma_separated_list}),
70 ('Specify comma separated list of stylesheet paths. '
71 'With --link-stylesheet, '
72 'the path is rewritten relative to the output HTML file. '
73 'Default: "%s"' % default_stylesheet_path,
74 ['--stylesheet-path'],
75 {'metavar': '<file>', 'overrides': 'stylesheet',
76 'validator': frontend.validate_comma_separated_list,
77 'default': [default_stylesheet_path]}),
78 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
79 'files must be accessible during processing. This is the default.',
80 ['--embed-stylesheet'],
81 {'default': 1, 'action': 'store_true',
82 'validator': frontend.validate_boolean}),
83 ('Link to the stylesheet(s) in the output HTML file. '
84 'Default: embed stylesheets.',
85 ['--link-stylesheet'],
86 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
87 ('Specify the initial header level. Default is 1 for "<h1>". '
88 'Does not affect document title & subtitle (see --no-doc-title).',
89 ['--initial-header-level'],
90 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
91 'metavar': '<level>'}),
92 ('Specify the maximum width (in characters) for one-column field '
93 'names. Longer field names will span an entire row of the table '
94 'used to render the field list. Default is 14 characters. '
95 'Use 0 for "no limit".',
96 ['--field-name-limit'],
97 {'default': 14, 'metavar': '<level>',
98 'validator': frontend.validate_nonnegative_int}),
99 ('Specify the maximum width (in characters) for options in option '
100 'lists. Longer options will span an entire row of the table used '
101 'to render the option list. Default is 14 characters. '
102 'Use 0 for "no limit".',
103 ['--option-limit'],
104 {'default': 14, 'metavar': '<level>',
105 'validator': frontend.validate_nonnegative_int}),
106 ('Format for footnote references: one of "superscript" or '
107 '"brackets". Default is "brackets".',
108 ['--footnote-references'],
109 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
110 'metavar': '<format>',
111 'overrides': 'trim_footnote_reference_space'}),
112 ('Format for block quote attributions: one of "dash" (em-dash '
113 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
114 ['--attribution'],
115 {'choices': ['dash', 'parentheses', 'parens', 'none'],
116 'default': 'dash', 'metavar': '<format>'}),
117 ('Remove extra vertical whitespace between items of "simple" bullet '
118 'lists and enumerated lists. Default: enabled.',
119 ['--compact-lists'],
120 {'default': 1, 'action': 'store_true',
121 'validator': frontend.validate_boolean}),
122 ('Disable compact simple bullet and enumerated lists.',
123 ['--no-compact-lists'],
124 {'dest': 'compact_lists', 'action': 'store_false'}),
125 ('Remove extra vertical whitespace between items of simple field '
126 'lists. Default: enabled.',
127 ['--compact-field-lists'],
128 {'default': 1, 'action': 'store_true',
129 'validator': frontend.validate_boolean}),
130 ('Disable compact simple field lists.',
131 ['--no-compact-field-lists'],
132 {'dest': 'compact_field_lists', 'action': 'store_false'}),
133 ('Added to standard table classes. '
134 'Defined styles: "borderless". Default: ""',
135 ['--table-style'],
136 {'default': ''}),
137 ('Math output format, one of "MathML", "HTML", "MathJax" '
138 'or "LaTeX". Default: "MathJax"',
139 ['--math-output'],
140 {'default': 'MathJax'}),
141 ('Omit the XML declaration. Use with caution.',
142 ['--no-xml-declaration'],
143 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
144 'validator': frontend.validate_boolean}),
145 ('Obfuscate email addresses to confuse harvesters while still '
146 'keeping email links usable with standards-compliant browsers.',
147 ['--cloak-email-addresses'],
148 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
150 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
152 relative_path_settings = ('stylesheet_path',)
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 # TODO: make this configurable:
268 # a) as extra option or
269 # b) appended to math-output="MathJax"?
271 # If b), which delimiter/delimter-set (':', ',', ' ')?
273 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
274 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
275 words_and_spaces = re.compile(r'\S+| +|\n')
276 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
277 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
279 def __init__(self, document):
280 nodes.NodeVisitor.__init__(self, document)
281 self.settings = settings = document.settings
282 lcode = settings.language_code
283 self.language = languages.get_language(lcode, document.reporter)
284 self.meta = [self.generator % docutils.__version__]
285 self.head_prefix = []
286 self.html_prolog = []
287 if settings.xml_declaration:
288 self.head_prefix.append(self.xml_declaration
289 % settings.output_encoding)
290 # encoding not interpolated:
291 self.html_prolog.append(self.xml_declaration)
292 self.head = self.meta[:]
293 self.stylesheet = [self.stylesheet_call(path)
294 for path in utils.get_stylesheet_list(settings)]
295 self.body_prefix = ['</head>\n<body>\n']
296 # document title, subtitle display
297 self.body_pre_docinfo = []
298 # author, date, etc.
299 self.docinfo = []
300 self.body = []
301 self.fragment = []
302 self.body_suffix = ['</body>\n</html>\n']
303 self.section_level = 0
304 self.initial_header_level = int(settings.initial_header_level)
306 self.math_output = settings.math_output.lower()
307 # A heterogenous stack used in conjunction with the tree traversal.
308 # Make sure that the pops correspond to the pushes:
309 self.context = []
310 self.topic_classes = []
311 self.colspecs = []
312 self.compact_p = 1
313 self.compact_simple = False
314 self.compact_field_list = False
315 self.in_docinfo = False
316 self.in_sidebar = False
317 self.title = []
318 self.subtitle = []
319 self.header = []
320 self.footer = []
321 self.html_head = [self.content_type] # charset not interpolated
322 self.html_title = []
323 self.html_subtitle = []
324 self.html_body = []
325 self.in_document_title = 0 # len(self.body) or 0
326 self.in_mailto = False
327 self.author_in_authors = False
328 self.math_header = ''
330 def astext(self):
331 return ''.join(self.head_prefix + self.head
332 + self.stylesheet + self.body_prefix
333 + self.body_pre_docinfo + self.docinfo
334 + self.body + self.body_suffix)
336 def encode(self, text):
337 """Encode special characters in `text` & return."""
338 # @@@ A codec to do these and all other HTML entities would be nice.
339 text = unicode(text)
340 return text.translate({
341 ord('&'): u'&amp;',
342 ord('<'): u'&lt;',
343 ord('"'): u'&quot;',
344 ord('>'): u'&gt;',
345 ord('@'): u'&#64;', # may thwart some address harvesters
346 # TODO: convert non-breaking space only if needed?
347 0xa0: u'&nbsp;'}) # non-breaking space
349 def cloak_mailto(self, uri):
350 """Try to hide a mailto: URL from harvesters."""
351 # Encode "@" using a URL octet reference (see RFC 1738).
352 # Further cloaking with HTML entities will be done in the
353 # `attval` function.
354 return uri.replace('@', '%40')
356 def cloak_email(self, addr):
357 """Try to hide the link text of a email link from harversters."""
358 # Surround at-signs and periods with <span> tags. ("@" has
359 # already been encoded to "&#64;" by the `encode` method.)
360 addr = addr.replace('&#64;', '<span>&#64;</span>')
361 addr = addr.replace('.', '<span>&#46;</span>')
362 return addr
364 def attval(self, text,
365 whitespace=re.compile('[\n\r\t\v\f]')):
366 """Cleanse, HTML encode, and return attribute value text."""
367 encoded = self.encode(whitespace.sub(' ', text))
368 if self.in_mailto and self.settings.cloak_email_addresses:
369 # Cloak at-signs ("%40") and periods with HTML entities.
370 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
371 encoded = encoded.replace('.', '&#46;')
372 return encoded
374 def stylesheet_call(self, path):
375 """Return code to reference or embed stylesheet file `path`"""
376 if self.settings.embed_stylesheet:
377 try:
378 content = io.FileInput(source_path=path,
379 encoding='utf-8').read()
380 self.settings.record_dependencies.add(path)
381 except IOError, err:
382 msg = u"Cannot embed stylesheet '%s': %s." % (
383 path, SafeString(err.strerror))
384 self.document.reporter.error(msg)
385 return '<--- %s --->\n' % msg
386 return self.embedded_stylesheet % content
387 # else link to style file:
388 if self.settings.stylesheet_path:
389 # adapt path relative to output (cf. config.html#stylesheet-path)
390 path = utils.relative_path(self.settings._destination, path)
391 return self.stylesheet_link % self.encode(path)
393 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
395 Construct and return a start tag given a node (id & class attributes
396 are extracted), tag name, and optional attributes.
398 tagname = tagname.lower()
399 prefix = []
400 atts = {}
401 ids = []
402 for (name, value) in attributes.items():
403 atts[name.lower()] = value
404 classes = node.get('classes', [])
405 if 'class' in atts:
406 classes.append(atts.pop('class'))
407 # move language specification to 'lang' attribute
408 languages = [cls for cls in classes
409 if cls.startswith('language-')]
410 if languages:
411 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
412 atts[self.lang_attribute] = languages[0][9:]
413 classes.pop(classes.index(languages[0]))
414 classes = ' '.join(classes).strip()
415 if classes:
416 atts['class'] = classes
417 assert 'id' not in atts
418 ids.extend(node.get('ids', []))
419 if 'ids' in atts:
420 ids.extend(atts['ids'])
421 del atts['ids']
422 if ids:
423 atts['id'] = ids[0]
424 for id in ids[1:]:
425 # Add empty "span" elements for additional IDs. Note
426 # that we cannot use empty "a" elements because there
427 # may be targets inside of references, but nested "a"
428 # elements aren't allowed in XHTML (even if they do
429 # not all have a "href" attribute).
430 if empty:
431 # Empty tag. Insert target right in front of element.
432 prefix.append('<span id="%s"></span>' % id)
433 else:
434 # Non-empty tag. Place the auxiliary <span> tag
435 # *inside* the element, as the first child.
436 suffix += '<span id="%s"></span>' % id
437 attlist = atts.items()
438 attlist.sort()
439 parts = [tagname]
440 for name, value in attlist:
441 # value=None was used for boolean attributes without
442 # value, but this isn't supported by XHTML.
443 assert value is not None
444 if isinstance(value, list):
445 values = [unicode(v) for v in value]
446 parts.append('%s="%s"' % (name.lower(),
447 self.attval(' '.join(values))))
448 else:
449 parts.append('%s="%s"' % (name.lower(),
450 self.attval(unicode(value))))
451 if empty:
452 infix = ' /'
453 else:
454 infix = ''
455 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
457 def emptytag(self, node, tagname, suffix='\n', **attributes):
458 """Construct and return an XML-compatible empty tag."""
459 return self.starttag(node, tagname, suffix, empty=True, **attributes)
461 def set_class_on_child(self, node, class_, index=0):
463 Set class `class_` on the visible child no. index of `node`.
464 Do nothing if node has fewer children than `index`.
466 children = [n for n in node if not isinstance(n, nodes.Invisible)]
467 try:
468 child = children[index]
469 except IndexError:
470 return
471 child['classes'].append(class_)
473 def set_first_last(self, node):
474 self.set_class_on_child(node, 'first', 0)
475 self.set_class_on_child(node, 'last', -1)
477 def visit_Text(self, node):
478 text = node.astext()
479 encoded = self.encode(text)
480 if self.in_mailto and self.settings.cloak_email_addresses:
481 encoded = self.cloak_email(encoded)
482 self.body.append(encoded)
484 def depart_Text(self, node):
485 pass
487 def visit_abbreviation(self, node):
488 # @@@ implementation incomplete ("title" attribute)
489 self.body.append(self.starttag(node, 'abbr', ''))
491 def depart_abbreviation(self, node):
492 self.body.append('</abbr>')
494 def visit_acronym(self, node):
495 # @@@ implementation incomplete ("title" attribute)
496 self.body.append(self.starttag(node, 'acronym', ''))
498 def depart_acronym(self, node):
499 self.body.append('</acronym>')
501 def visit_address(self, node):
502 self.visit_docinfo_item(node, 'address', meta=False)
503 self.body.append(self.starttag(node, 'pre', CLASS='address'))
505 def depart_address(self, node):
506 self.body.append('\n</pre>\n')
507 self.depart_docinfo_item()
509 def visit_admonition(self, node):
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 or self.check_simple_list(node))))
574 def visit_bullet_list(self, node):
575 atts = {}
576 old_compact_simple = self.compact_simple
577 self.context.append((self.compact_simple, self.compact_p))
578 self.compact_p = None
579 self.compact_simple = self.is_compactable(node)
580 if self.compact_simple and not old_compact_simple:
581 atts['class'] = 'simple'
582 self.body.append(self.starttag(node, 'ul', **atts))
584 def depart_bullet_list(self, node):
585 self.compact_simple, self.compact_p = self.context.pop()
586 self.body.append('</ul>\n')
588 def visit_caption(self, node):
589 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
591 def depart_caption(self, node):
592 self.body.append('</p>\n')
594 def visit_citation(self, node):
595 self.body.append(self.starttag(node, 'table',
596 CLASS='docutils citation',
597 frame="void", rules="none"))
598 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
599 '<tbody valign="top">\n'
600 '<tr>')
601 self.footnote_backrefs(node)
603 def depart_citation(self, node):
604 self.body.append('</td></tr>\n'
605 '</tbody>\n</table>\n')
607 def visit_citation_reference(self, node):
608 href = '#'
609 if 'refid' in node:
610 href += node['refid']
611 elif 'refname' in node:
612 href += self.document.nameids[node['refname']]
613 # else: # TODO system message (or already in the transform)?
614 # 'Citation reference missing.'
615 self.body.append(self.starttag(
616 node, 'a', '[', CLASS='citation-reference', href=href))
618 def depart_citation_reference(self, node):
619 self.body.append(']</a>')
621 def visit_classifier(self, node):
622 self.body.append(' <span class="classifier-delimiter">:</span> ')
623 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
625 def depart_classifier(self, node):
626 self.body.append('</span>')
628 def visit_colspec(self, node):
629 self.colspecs.append(node)
630 # "stubs" list is an attribute of the tgroup element:
631 node.parent.stubs.append(node.attributes.get('stub'))
633 def depart_colspec(self, node):
634 pass
636 def write_colspecs(self):
637 width = 0
638 for node in self.colspecs:
639 width += node['colwidth']
640 for node in self.colspecs:
641 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
642 self.body.append(self.emptytag(node, 'col',
643 width='%i%%' % colwidth))
644 self.colspecs = []
646 def visit_comment(self, node,
647 sub=re.compile('-(?=-)').sub):
648 """Escape double-dashes in comment text."""
649 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
650 # Content already processed:
651 raise nodes.SkipNode
653 def visit_compound(self, node):
654 self.body.append(self.starttag(node, 'div', CLASS='compound'))
655 if len(node) > 1:
656 node[0]['classes'].append('compound-first')
657 node[-1]['classes'].append('compound-last')
658 for child in node[1:-1]:
659 child['classes'].append('compound-middle')
661 def depart_compound(self, node):
662 self.body.append('</div>\n')
664 def visit_container(self, node):
665 self.body.append(self.starttag(node, 'div', CLASS='container'))
667 def depart_container(self, node):
668 self.body.append('</div>\n')
670 def visit_contact(self, node):
671 self.visit_docinfo_item(node, 'contact', meta=False)
673 def depart_contact(self, node):
674 self.depart_docinfo_item()
676 def visit_copyright(self, node):
677 self.visit_docinfo_item(node, 'copyright')
679 def depart_copyright(self, node):
680 self.depart_docinfo_item()
682 def visit_date(self, node):
683 self.visit_docinfo_item(node, 'date')
685 def depart_date(self, node):
686 self.depart_docinfo_item()
688 def visit_decoration(self, node):
689 pass
691 def depart_decoration(self, node):
692 pass
694 def visit_definition(self, node):
695 self.body.append('</dt>\n')
696 self.body.append(self.starttag(node, 'dd', ''))
697 self.set_first_last(node)
699 def depart_definition(self, node):
700 self.body.append('</dd>\n')
702 def visit_definition_list(self, node):
703 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
705 def depart_definition_list(self, node):
706 self.body.append('</dl>\n')
708 def visit_definition_list_item(self, node):
709 pass
711 def depart_definition_list_item(self, node):
712 pass
714 def visit_description(self, node):
715 self.body.append(self.starttag(node, 'td', ''))
716 self.set_first_last(node)
718 def depart_description(self, node):
719 self.body.append('</td>')
721 def visit_docinfo(self, node):
722 self.context.append(len(self.body))
723 self.body.append(self.starttag(node, 'table',
724 CLASS='docinfo',
725 frame="void", rules="none"))
726 self.body.append('<col class="docinfo-name" />\n'
727 '<col class="docinfo-content" />\n'
728 '<tbody valign="top">\n')
729 self.in_docinfo = True
731 def depart_docinfo(self, node):
732 self.body.append('</tbody>\n</table>\n')
733 self.in_docinfo = False
734 start = self.context.pop()
735 self.docinfo = self.body[start:]
736 self.body = []
738 def visit_docinfo_item(self, node, name, meta=True):
739 if meta:
740 meta_tag = '<meta name="%s" content="%s" />\n' \
741 % (name, self.attval(node.astext()))
742 self.add_meta(meta_tag)
743 self.body.append(self.starttag(node, 'tr', ''))
744 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
745 % self.language.labels[name])
746 if len(node):
747 if isinstance(node[0], nodes.Element):
748 node[0]['classes'].append('first')
749 if isinstance(node[-1], nodes.Element):
750 node[-1]['classes'].append('last')
752 def depart_docinfo_item(self):
753 self.body.append('</td></tr>\n')
755 def visit_doctest_block(self, node):
756 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
758 def depart_doctest_block(self, node):
759 self.body.append('\n</pre>\n')
761 def visit_document(self, node):
762 self.head.append('<title>%s</title>\n'
763 % self.encode(node.get('title', '')))
765 def depart_document(self, node):
766 self.head_prefix.extend([self.doctype,
767 self.head_prefix_template %
768 {'lang': self.settings.language_code}])
769 self.html_prolog.append(self.doctype)
770 self.meta.insert(0, self.content_type % self.settings.output_encoding)
771 self.head.insert(0, self.content_type % self.settings.output_encoding)
772 if self.math_header:
773 self.head.append(self.math_header)
774 # skip content-type meta tag with interpolated charset value:
775 self.html_head.extend(self.head[1:])
776 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
777 self.body_suffix.insert(0, '</div>\n')
778 self.fragment.extend(self.body) # self.fragment is the "naked" body
779 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
780 + self.docinfo + self.body
781 + self.body_suffix[:-1])
782 assert not self.context, 'len(context) = %s' % len(self.context)
784 def visit_emphasis(self, node):
785 self.body.append(self.starttag(node, 'em', ''))
787 def depart_emphasis(self, node):
788 self.body.append('</em>')
790 def visit_entry(self, node):
791 atts = {'class': []}
792 if isinstance(node.parent.parent, nodes.thead):
793 atts['class'].append('head')
794 if node.parent.parent.parent.stubs[node.parent.column]:
795 # "stubs" list is an attribute of the tgroup element
796 atts['class'].append('stub')
797 if atts['class']:
798 tagname = 'th'
799 atts['class'] = ' '.join(atts['class'])
800 else:
801 tagname = 'td'
802 del atts['class']
803 node.parent.column += 1
804 if 'morerows' in node:
805 atts['rowspan'] = node['morerows'] + 1
806 if 'morecols' in node:
807 atts['colspan'] = node['morecols'] + 1
808 node.parent.column += node['morecols']
809 self.body.append(self.starttag(node, tagname, '', **atts))
810 self.context.append('</%s>\n' % tagname.lower())
811 if len(node) == 0: # empty cell
812 self.body.append('&nbsp;')
813 self.set_first_last(node)
815 def depart_entry(self, node):
816 self.body.append(self.context.pop())
818 def visit_enumerated_list(self, node):
820 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
821 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
822 usable.
824 atts = {}
825 if 'start' in node:
826 atts['start'] = node['start']
827 if 'enumtype' in node:
828 atts['class'] = node['enumtype']
829 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
830 # single "format" attribute? Use CSS2?
831 old_compact_simple = self.compact_simple
832 self.context.append((self.compact_simple, self.compact_p))
833 self.compact_p = None
834 self.compact_simple = self.is_compactable(node)
835 if self.compact_simple and not old_compact_simple:
836 atts['class'] = (atts.get('class', '') + ' simple').strip()
837 self.body.append(self.starttag(node, 'ol', **atts))
839 def depart_enumerated_list(self, node):
840 self.compact_simple, self.compact_p = self.context.pop()
841 self.body.append('</ol>\n')
843 def visit_field(self, node):
844 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
846 def depart_field(self, node):
847 self.body.append('</tr>\n')
849 def visit_field_body(self, node):
850 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
851 self.set_class_on_child(node, 'first', 0)
852 field = node.parent
853 if (self.compact_field_list or
854 isinstance(field.parent, nodes.docinfo) or
855 field.parent.index(field) == len(field.parent) - 1):
856 # If we are in a compact list, the docinfo, or if this is
857 # the last field of the field list, do not add vertical
858 # space after last element.
859 self.set_class_on_child(node, 'last', -1)
861 def depart_field_body(self, node):
862 self.body.append('</td>\n')
864 def visit_field_list(self, node):
865 self.context.append((self.compact_field_list, self.compact_p))
866 self.compact_p = None
867 if 'compact' in node['classes']:
868 self.compact_field_list = True
869 elif (self.settings.compact_field_lists
870 and 'open' not in node['classes']):
871 self.compact_field_list = True
872 if self.compact_field_list:
873 for field in node:
874 field_body = field[-1]
875 assert isinstance(field_body, nodes.field_body)
876 children = [n for n in field_body
877 if not isinstance(n, nodes.Invisible)]
878 if not (len(children) == 0 or
879 len(children) == 1 and
880 isinstance(children[0],
881 (nodes.paragraph, nodes.line_block))):
882 self.compact_field_list = False
883 break
884 self.body.append(self.starttag(node, 'table', frame='void',
885 rules='none',
886 CLASS='docutils field-list'))
887 self.body.append('<col class="field-name" />\n'
888 '<col class="field-body" />\n'
889 '<tbody valign="top">\n')
891 def depart_field_list(self, node):
892 self.body.append('</tbody>\n</table>\n')
893 self.compact_field_list, self.compact_p = self.context.pop()
895 def visit_field_name(self, node):
896 atts = {}
897 if self.in_docinfo:
898 atts['class'] = 'docinfo-name'
899 else:
900 atts['class'] = 'field-name'
901 if ( self.settings.field_name_limit
902 and len(node.astext()) > self.settings.field_name_limit):
903 atts['colspan'] = 2
904 self.context.append('</tr>\n'
905 + self.starttag(node.parent, 'tr', '')
906 + '<td>&nbsp;</td>')
907 else:
908 self.context.append('')
909 self.body.append(self.starttag(node, 'th', '', **atts))
911 def depart_field_name(self, node):
912 self.body.append(':</th>')
913 self.body.append(self.context.pop())
915 def visit_figure(self, node):
916 atts = {'class': 'figure'}
917 if node.get('width'):
918 atts['style'] = 'width: %s' % node['width']
919 if node.get('align'):
920 atts['class'] += " align-" + node['align']
921 self.body.append(self.starttag(node, 'div', **atts))
923 def depart_figure(self, node):
924 self.body.append('</div>\n')
926 def visit_footer(self, node):
927 self.context.append(len(self.body))
929 def depart_footer(self, node):
930 start = self.context.pop()
931 footer = [self.starttag(node, 'div', CLASS='footer'),
932 '<hr class="footer" />\n']
933 footer.extend(self.body[start:])
934 footer.append('\n</div>\n')
935 self.footer.extend(footer)
936 self.body_suffix[:0] = footer
937 del self.body[start:]
939 def visit_footnote(self, node):
940 self.body.append(self.starttag(node, 'table',
941 CLASS='docutils footnote',
942 frame="void", rules="none"))
943 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
944 '<tbody valign="top">\n'
945 '<tr>')
946 self.footnote_backrefs(node)
948 def footnote_backrefs(self, node):
949 backlinks = []
950 backrefs = node['backrefs']
951 if self.settings.footnote_backlinks and backrefs:
952 if len(backrefs) == 1:
953 self.context.append('')
954 self.context.append('</a>')
955 self.context.append('<a class="fn-backref" href="#%s">'
956 % backrefs[0])
957 else:
958 i = 1
959 for backref in backrefs:
960 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
961 % (backref, i))
962 i += 1
963 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
964 self.context += ['', '']
965 else:
966 self.context.append('')
967 self.context += ['', '']
968 # If the node does not only consist of a label.
969 if len(node) > 1:
970 # If there are preceding backlinks, we do not set class
971 # 'first', because we need to retain the top-margin.
972 if not backlinks:
973 node[1]['classes'].append('first')
974 node[-1]['classes'].append('last')
976 def depart_footnote(self, node):
977 self.body.append('</td></tr>\n'
978 '</tbody>\n</table>\n')
980 def visit_footnote_reference(self, node):
981 href = '#' + node['refid']
982 format = self.settings.footnote_references
983 if format == 'brackets':
984 suffix = '['
985 self.context.append(']')
986 else:
987 assert format == 'superscript'
988 suffix = '<sup>'
989 self.context.append('</sup>')
990 self.body.append(self.starttag(node, 'a', suffix,
991 CLASS='footnote-reference', href=href))
993 def depart_footnote_reference(self, node):
994 self.body.append(self.context.pop() + '</a>')
996 def visit_generated(self, node):
997 pass
999 def depart_generated(self, node):
1000 pass
1002 def visit_header(self, node):
1003 self.context.append(len(self.body))
1005 def depart_header(self, node):
1006 start = self.context.pop()
1007 header = [self.starttag(node, 'div', CLASS='header')]
1008 header.extend(self.body[start:])
1009 header.append('\n<hr class="header"/>\n</div>\n')
1010 self.body_prefix.extend(header)
1011 self.header.extend(header)
1012 del self.body[start:]
1014 def visit_image(self, node):
1015 atts = {}
1016 uri = node['uri']
1017 # place SVG and SWF images in an <object> element
1018 types = {'.svg': 'image/svg+xml',
1019 '.swf': 'application/x-shockwave-flash'}
1020 ext = os.path.splitext(uri)[1].lower()
1021 if ext in ('.svg', '.swf'):
1022 atts['data'] = uri
1023 atts['type'] = types[ext]
1024 else:
1025 atts['src'] = uri
1026 atts['alt'] = node.get('alt', uri)
1027 # image size
1028 if 'width' in node:
1029 atts['width'] = node['width']
1030 if 'height' in node:
1031 atts['height'] = node['height']
1032 if 'scale' in node:
1033 if (PIL and not ('width' in node and 'height' in node)
1034 and self.settings.file_insertion_enabled):
1035 imagepath = urllib.url2pathname(uri)
1036 try:
1037 img = PIL.Image.open(
1038 imagepath.encode(sys.getfilesystemencoding()))
1039 except (IOError, UnicodeEncodeError):
1040 pass # TODO: warn?
1041 else:
1042 self.settings.record_dependencies.add(
1043 imagepath.replace('\\', '/'))
1044 if 'width' not in atts:
1045 atts['width'] = str(img.size[0])
1046 if 'height' not in atts:
1047 atts['height'] = str(img.size[1])
1048 del img
1049 for att_name in 'width', 'height':
1050 if att_name in atts:
1051 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1052 assert match
1053 atts[att_name] = '%s%s' % (
1054 float(match.group(1)) * (float(node['scale']) / 100),
1055 match.group(2))
1056 style = []
1057 for att_name in 'width', 'height':
1058 if att_name in atts:
1059 if re.match(r'^[0-9.]+$', atts[att_name]):
1060 # Interpret unitless values as pixels.
1061 atts[att_name] += 'px'
1062 style.append('%s: %s;' % (att_name, atts[att_name]))
1063 del atts[att_name]
1064 if style:
1065 atts['style'] = ' '.join(style)
1066 if (isinstance(node.parent, nodes.TextElement) or
1067 (isinstance(node.parent, nodes.reference) and
1068 not isinstance(node.parent.parent, nodes.TextElement))):
1069 # Inline context or surrounded by <a>...</a>.
1070 suffix = ''
1071 else:
1072 suffix = '\n'
1073 if 'align' in node:
1074 atts['class'] = 'align-%s' % node['align']
1075 self.context.append('')
1076 if ext in ('.svg', '.swf'): # place in an object element,
1077 # do NOT use an empty tag: incorrect rendering in browsers
1078 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1079 node.get('alt', uri) + '</object>' + suffix)
1080 else:
1081 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1083 def depart_image(self, node):
1084 self.body.append(self.context.pop())
1086 def visit_inline(self, node):
1087 self.body.append(self.starttag(node, 'span', ''))
1089 def depart_inline(self, node):
1090 self.body.append('</span>')
1092 def visit_label(self, node):
1093 # Context added in footnote_backrefs.
1094 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1095 CLASS='label'))
1097 def depart_label(self, node):
1098 # Context added in footnote_backrefs.
1099 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1101 def visit_legend(self, node):
1102 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1104 def depart_legend(self, node):
1105 self.body.append('</div>\n')
1107 def visit_line(self, node):
1108 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1109 if not len(node):
1110 self.body.append('<br />')
1112 def depart_line(self, node):
1113 self.body.append('</div>\n')
1115 def visit_line_block(self, node):
1116 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1118 def depart_line_block(self, node):
1119 self.body.append('</div>\n')
1121 def visit_list_item(self, node):
1122 self.body.append(self.starttag(node, 'li', ''))
1123 if len(node):
1124 node[0]['classes'].append('first')
1126 def depart_list_item(self, node):
1127 self.body.append('</li>\n')
1129 def visit_literal(self, node):
1130 # special case: "code" role
1131 classes = node.get('classes', [])
1132 if 'code' in classes:
1133 # filter 'code' from class arguments
1134 node['classes'] = [cls for cls in classes if cls != 'code']
1135 self.body.append(self.starttag(node, 'code', ''))
1136 return
1137 self.body.append(
1138 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1139 text = node.astext()
1140 for token in self.words_and_spaces.findall(text):
1141 if token.strip():
1142 # Protect text like "--an-option" and the regular expression
1143 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1144 if self.sollbruchstelle.search(token):
1145 self.body.append('<span class="pre">%s</span>'
1146 % self.encode(token))
1147 else:
1148 self.body.append(self.encode(token))
1149 elif token in ('\n', ' '):
1150 # Allow breaks at whitespace:
1151 self.body.append(token)
1152 else:
1153 # Protect runs of multiple spaces; the last space can wrap:
1154 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1155 self.body.append('</tt>')
1156 # Content already processed:
1157 raise nodes.SkipNode
1159 def depart_literal(self, node):
1160 # skipped unless literal element is from "code" role:
1161 self.body.append('</code>')
1163 def visit_literal_block(self, node):
1164 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1166 def depart_literal_block(self, node):
1167 self.body.append('\n</pre>\n')
1169 def visit_math(self, node, math_env=''):
1170 # If the method is called from visit_math_block(), math_env != ''.
1172 # As there is no native HTML math support, we provide alternatives:
1173 # LaTeX and MathJax math_output modes simply wrap the content,
1174 # HTML and MathML math_output modes also convert the math_code.
1175 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1176 self.document.reporter.error(
1177 'math-output format "%s" not supported '
1178 'falling back to "latex"'% self.math_output)
1179 self.math_output = 'latex'
1181 # HTML container
1182 tags = {# math_output: (block, inline, class-arguments)
1183 'mathml': ('div', '', ''),
1184 'html': ('div', 'span', 'formula'),
1185 'mathjax': ('div', 'span', 'math'),
1186 'latex': ('pre', 'tt', 'math'),
1188 tag = tags[self.math_output][math_env == '']
1189 clsarg = tags[self.math_output][2]
1190 # LaTeX container
1191 wrappers = {# math_mode: (inline, block)
1192 'mathml': (None, None),
1193 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1194 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1195 'latex': (None, None),
1197 wrapper = wrappers[self.math_output][math_env != '']
1198 # get and wrap content
1199 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1200 if wrapper and math_env:
1201 math_code = wrapper % (math_env, math_code, math_env)
1202 elif wrapper:
1203 math_code = wrapper % math_code
1204 # settings and conversion
1205 if self.math_output in ('latex', 'mathjax'):
1206 math_code = self.encode(math_code)
1207 if self.math_output == 'mathjax':
1208 self.math_header = self.mathjax_script % self.mathjax_url
1209 elif self.math_output == 'html':
1210 math_code = math2html(math_code)
1211 elif self.math_output == 'mathml':
1212 self.doctype = self.doctype_mathml
1213 self.content_type = self.content_type_mathml
1214 try:
1215 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1216 math_code = ''.join(mathml_tree.xml())
1217 except SyntaxError, err:
1218 err_node = self.document.reporter.error(err, base_node=node)
1219 self.visit_system_message(err_node)
1220 self.body.append(self.starttag(node, 'p'))
1221 self.body.append(u','.join(err.args))
1222 self.body.append('</p>\n')
1223 self.body.append(self.starttag(node, 'pre',
1224 CLASS='literal-block'))
1225 self.body.append(self.encode(math_code))
1226 self.body.append('\n</pre>\n')
1227 self.depart_system_message(err_node)
1228 raise nodes.SkipNode
1229 # append to document body
1230 if tag:
1231 self.body.append(self.starttag(node, tag, CLASS=clsarg))
1232 self.body.append(math_code)
1233 if math_env:
1234 self.body.append('\n')
1235 if tag:
1236 self.body.append('</%s>\n' % tag)
1237 # Content already processed:
1238 raise nodes.SkipNode
1240 def depart_math(self, node):
1241 pass # never reached
1243 def visit_math_block(self, node):
1244 # print node.astext().encode('utf8')
1245 math_env = pick_math_environment(node.astext())
1246 self.visit_math(node, math_env=math_env)
1248 def depart_math_block(self, node):
1249 pass # never reached
1251 def visit_meta(self, node):
1252 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1253 self.add_meta(meta)
1255 def depart_meta(self, node):
1256 pass
1258 def add_meta(self, tag):
1259 self.meta.append(tag)
1260 self.head.append(tag)
1262 def visit_option(self, node):
1263 if self.context[-1]:
1264 self.body.append(', ')
1265 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1267 def depart_option(self, node):
1268 self.body.append('</span>')
1269 self.context[-1] += 1
1271 def visit_option_argument(self, node):
1272 self.body.append(node.get('delimiter', ' '))
1273 self.body.append(self.starttag(node, 'var', ''))
1275 def depart_option_argument(self, node):
1276 self.body.append('</var>')
1278 def visit_option_group(self, node):
1279 atts = {}
1280 if ( self.settings.option_limit
1281 and len(node.astext()) > self.settings.option_limit):
1282 atts['colspan'] = 2
1283 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1284 else:
1285 self.context.append('')
1286 self.body.append(
1287 self.starttag(node, 'td', CLASS='option-group', **atts))
1288 self.body.append('<kbd>')
1289 self.context.append(0) # count number of options
1291 def depart_option_group(self, node):
1292 self.context.pop()
1293 self.body.append('</kbd></td>\n')
1294 self.body.append(self.context.pop())
1296 def visit_option_list(self, node):
1297 self.body.append(
1298 self.starttag(node, 'table', CLASS='docutils option-list',
1299 frame="void", rules="none"))
1300 self.body.append('<col class="option" />\n'
1301 '<col class="description" />\n'
1302 '<tbody valign="top">\n')
1304 def depart_option_list(self, node):
1305 self.body.append('</tbody>\n</table>\n')
1307 def visit_option_list_item(self, node):
1308 self.body.append(self.starttag(node, 'tr', ''))
1310 def depart_option_list_item(self, node):
1311 self.body.append('</tr>\n')
1313 def visit_option_string(self, node):
1314 pass
1316 def depart_option_string(self, node):
1317 pass
1319 def visit_organization(self, node):
1320 self.visit_docinfo_item(node, 'organization')
1322 def depart_organization(self, node):
1323 self.depart_docinfo_item()
1325 def should_be_compact_paragraph(self, node):
1327 Determine if the <p> tags around paragraph ``node`` can be omitted.
1329 if (isinstance(node.parent, nodes.document) or
1330 isinstance(node.parent, nodes.compound)):
1331 # Never compact paragraphs in document or compound.
1332 return 0
1333 for key, value in node.attlist():
1334 if (node.is_not_default(key) and
1335 not (key == 'classes' and value in
1336 ([], ['first'], ['last'], ['first', 'last']))):
1337 # Attribute which needs to survive.
1338 return 0
1339 first = isinstance(node.parent[0], nodes.label) # skip label
1340 for child in node.parent.children[first:]:
1341 # only first paragraph can be compact
1342 if isinstance(child, nodes.Invisible):
1343 continue
1344 if child is node:
1345 break
1346 return 0
1347 parent_length = len([n for n in node.parent if not isinstance(
1348 n, (nodes.Invisible, nodes.label))])
1349 if ( self.compact_simple
1350 or self.compact_field_list
1351 or self.compact_p and parent_length == 1):
1352 return 1
1353 return 0
1355 def visit_paragraph(self, node):
1356 if self.should_be_compact_paragraph(node):
1357 self.context.append('')
1358 else:
1359 self.body.append(self.starttag(node, 'p', ''))
1360 self.context.append('</p>\n')
1362 def depart_paragraph(self, node):
1363 self.body.append(self.context.pop())
1365 def visit_problematic(self, node):
1366 if node.hasattr('refid'):
1367 self.body.append('<a href="#%s">' % node['refid'])
1368 self.context.append('</a>')
1369 else:
1370 self.context.append('')
1371 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1373 def depart_problematic(self, node):
1374 self.body.append('</span>')
1375 self.body.append(self.context.pop())
1377 def visit_raw(self, node):
1378 if 'html' in node.get('format', '').split():
1379 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1380 if node['classes']:
1381 self.body.append(self.starttag(node, t, suffix=''))
1382 self.body.append(node.astext())
1383 if node['classes']:
1384 self.body.append('</%s>' % t)
1385 # Keep non-HTML raw text out of output:
1386 raise nodes.SkipNode
1388 def visit_reference(self, node):
1389 atts = {'class': 'reference'}
1390 if 'refuri' in node:
1391 atts['href'] = node['refuri']
1392 if ( self.settings.cloak_email_addresses
1393 and atts['href'].startswith('mailto:')):
1394 atts['href'] = self.cloak_mailto(atts['href'])
1395 self.in_mailto = True
1396 atts['class'] += ' external'
1397 else:
1398 assert 'refid' in node, \
1399 'References must have "refuri" or "refid" attribute.'
1400 atts['href'] = '#' + node['refid']
1401 atts['class'] += ' internal'
1402 if not isinstance(node.parent, nodes.TextElement):
1403 assert len(node) == 1 and isinstance(node[0], nodes.image)
1404 atts['class'] += ' image-reference'
1405 self.body.append(self.starttag(node, 'a', '', **atts))
1407 def depart_reference(self, node):
1408 self.body.append('</a>')
1409 if not isinstance(node.parent, nodes.TextElement):
1410 self.body.append('\n')
1411 self.in_mailto = False
1413 def visit_revision(self, node):
1414 self.visit_docinfo_item(node, 'revision', meta=False)
1416 def depart_revision(self, node):
1417 self.depart_docinfo_item()
1419 def visit_row(self, node):
1420 self.body.append(self.starttag(node, 'tr', ''))
1421 node.column = 0
1423 def depart_row(self, node):
1424 self.body.append('</tr>\n')
1426 def visit_rubric(self, node):
1427 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1429 def depart_rubric(self, node):
1430 self.body.append('</p>\n')
1432 def visit_section(self, node):
1433 self.section_level += 1
1434 self.body.append(
1435 self.starttag(node, 'div', CLASS='section'))
1437 def depart_section(self, node):
1438 self.section_level -= 1
1439 self.body.append('</div>\n')
1441 def visit_sidebar(self, node):
1442 self.body.append(
1443 self.starttag(node, 'div', CLASS='sidebar'))
1444 self.set_first_last(node)
1445 self.in_sidebar = True
1447 def depart_sidebar(self, node):
1448 self.body.append('</div>\n')
1449 self.in_sidebar = False
1451 def visit_status(self, node):
1452 self.visit_docinfo_item(node, 'status', meta=False)
1454 def depart_status(self, node):
1455 self.depart_docinfo_item()
1457 def visit_strong(self, node):
1458 self.body.append(self.starttag(node, 'strong', ''))
1460 def depart_strong(self, node):
1461 self.body.append('</strong>')
1463 def visit_subscript(self, node):
1464 self.body.append(self.starttag(node, 'sub', ''))
1466 def depart_subscript(self, node):
1467 self.body.append('</sub>')
1469 def visit_substitution_definition(self, node):
1470 """Internal only."""
1471 raise nodes.SkipNode
1473 def visit_substitution_reference(self, node):
1474 self.unimplemented_visit(node)
1476 def visit_subtitle(self, node):
1477 if isinstance(node.parent, nodes.sidebar):
1478 self.body.append(self.starttag(node, 'p', '',
1479 CLASS='sidebar-subtitle'))
1480 self.context.append('</p>\n')
1481 elif isinstance(node.parent, nodes.document):
1482 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1483 self.context.append('</h2>\n')
1484 self.in_document_title = len(self.body)
1485 elif isinstance(node.parent, nodes.section):
1486 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1487 self.body.append(
1488 self.starttag(node, tag, '', CLASS='section-subtitle') +
1489 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1490 self.context.append('</span></%s>\n' % tag)
1492 def depart_subtitle(self, node):
1493 self.body.append(self.context.pop())
1494 if self.in_document_title:
1495 self.subtitle = self.body[self.in_document_title:-1]
1496 self.in_document_title = 0
1497 self.body_pre_docinfo.extend(self.body)
1498 self.html_subtitle.extend(self.body)
1499 del self.body[:]
1501 def visit_superscript(self, node):
1502 self.body.append(self.starttag(node, 'sup', ''))
1504 def depart_superscript(self, node):
1505 self.body.append('</sup>')
1507 def visit_system_message(self, node):
1508 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1509 self.body.append('<p class="system-message-title">')
1510 backref_text = ''
1511 if len(node['backrefs']):
1512 backrefs = node['backrefs']
1513 if len(backrefs) == 1:
1514 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1515 % backrefs[0])
1516 else:
1517 i = 1
1518 backlinks = []
1519 for backref in backrefs:
1520 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1521 i += 1
1522 backref_text = ('; <em>backlinks: %s</em>'
1523 % ', '.join(backlinks))
1524 if node.hasattr('line'):
1525 line = ', line %s' % node['line']
1526 else:
1527 line = ''
1528 self.body.append('System Message: %s/%s '
1529 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1530 % (node['type'], node['level'],
1531 self.encode(node['source']), line, backref_text))
1533 def depart_system_message(self, node):
1534 self.body.append('</div>\n')
1536 def visit_table(self, node):
1537 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1538 self.body.append(
1539 self.starttag(node, 'table', CLASS=classes, border="1"))
1541 def depart_table(self, node):
1542 self.body.append('</table>\n')
1544 def visit_target(self, node):
1545 if not ('refuri' in node or 'refid' in node
1546 or 'refname' in node):
1547 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1548 self.context.append('</span>')
1549 else:
1550 self.context.append('')
1552 def depart_target(self, node):
1553 self.body.append(self.context.pop())
1555 def visit_tbody(self, node):
1556 self.write_colspecs()
1557 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1558 self.body.append(self.starttag(node, 'tbody', valign='top'))
1560 def depart_tbody(self, node):
1561 self.body.append('</tbody>\n')
1563 def visit_term(self, node):
1564 self.body.append(self.starttag(node, 'dt', ''))
1566 def depart_term(self, node):
1568 Leave the end tag to `self.visit_definition()`, in case there's a
1569 classifier.
1571 pass
1573 def visit_tgroup(self, node):
1574 # Mozilla needs <colgroup>:
1575 self.body.append(self.starttag(node, 'colgroup'))
1576 # Appended by thead or tbody:
1577 self.context.append('</colgroup>\n')
1578 node.stubs = []
1580 def depart_tgroup(self, node):
1581 pass
1583 def visit_thead(self, node):
1584 self.write_colspecs()
1585 self.body.append(self.context.pop()) # '</colgroup>\n'
1586 # There may or may not be a <thead>; this is for <tbody> to use:
1587 self.context.append('')
1588 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1590 def depart_thead(self, node):
1591 self.body.append('</thead>\n')
1593 def visit_title(self, node):
1594 """Only 6 section levels are supported by HTML."""
1595 check_id = 0 # TODO: is this a bool (False) or a counter?
1596 close_tag = '</p>\n'
1597 if isinstance(node.parent, nodes.topic):
1598 self.body.append(
1599 self.starttag(node, 'p', '', CLASS='topic-title first'))
1600 elif isinstance(node.parent, nodes.sidebar):
1601 self.body.append(
1602 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1603 elif isinstance(node.parent, nodes.Admonition):
1604 self.body.append(
1605 self.starttag(node, 'p', '', CLASS='admonition-title'))
1606 elif isinstance(node.parent, nodes.table):
1607 self.body.append(
1608 self.starttag(node, 'caption', ''))
1609 close_tag = '</caption>\n'
1610 elif isinstance(node.parent, nodes.document):
1611 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1612 close_tag = '</h1>\n'
1613 self.in_document_title = len(self.body)
1614 else:
1615 assert isinstance(node.parent, nodes.section)
1616 h_level = self.section_level + self.initial_header_level - 1
1617 atts = {}
1618 if (len(node.parent) >= 2 and
1619 isinstance(node.parent[1], nodes.subtitle)):
1620 atts['CLASS'] = 'with-subtitle'
1621 self.body.append(
1622 self.starttag(node, 'h%s' % h_level, '', **atts))
1623 atts = {}
1624 if node.hasattr('refid'):
1625 atts['class'] = 'toc-backref'
1626 atts['href'] = '#' + node['refid']
1627 if atts:
1628 self.body.append(self.starttag({}, 'a', '', **atts))
1629 close_tag = '</a></h%s>\n' % (h_level)
1630 else:
1631 close_tag = '</h%s>\n' % (h_level)
1632 self.context.append(close_tag)
1634 def depart_title(self, node):
1635 self.body.append(self.context.pop())
1636 if self.in_document_title:
1637 self.title = self.body[self.in_document_title:-1]
1638 self.in_document_title = 0
1639 self.body_pre_docinfo.extend(self.body)
1640 self.html_title.extend(self.body)
1641 del self.body[:]
1643 def visit_title_reference(self, node):
1644 self.body.append(self.starttag(node, 'cite', ''))
1646 def depart_title_reference(self, node):
1647 self.body.append('</cite>')
1649 def visit_topic(self, node):
1650 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1651 self.topic_classes = node['classes']
1653 def depart_topic(self, node):
1654 self.body.append('</div>\n')
1655 self.topic_classes = []
1657 def visit_transition(self, node):
1658 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1660 def depart_transition(self, node):
1661 pass
1663 def visit_version(self, node):
1664 self.visit_docinfo_item(node, 'version', meta=False)
1666 def depart_version(self, node):
1667 self.depart_docinfo_item()
1669 def unimplemented_visit(self, node):
1670 raise NotImplementedError('visiting unimplemented node type: %s'
1671 % node.__class__.__name__)
1674 class SimpleListChecker(nodes.GenericNodeVisitor):
1677 Raise `nodes.NodeFound` if non-simple list item is encountered.
1679 Here "simple" means a list item containing nothing other than a single
1680 paragraph, a simple list, or a paragraph followed by a simple list.
1683 def default_visit(self, node):
1684 raise nodes.NodeFound
1686 def visit_bullet_list(self, node):
1687 pass
1689 def visit_enumerated_list(self, node):
1690 pass
1692 def visit_list_item(self, node):
1693 children = []
1694 for child in node.children:
1695 if not isinstance(child, nodes.Invisible):
1696 children.append(child)
1697 if (children and isinstance(children[0], nodes.paragraph)
1698 and (isinstance(children[-1], nodes.bullet_list)
1699 or isinstance(children[-1], nodes.enumerated_list))):
1700 children.pop()
1701 if len(children) <= 1:
1702 return
1703 else:
1704 raise nodes.NodeFound
1706 def visit_paragraph(self, node):
1707 raise nodes.SkipNode
1709 def invisible_visit(self, node):
1710 """Invisible nodes should be ignored."""
1711 raise nodes.SkipNode
1713 visit_comment = invisible_visit
1714 visit_substitution_definition = invisible_visit
1715 visit_target = invisible_visit
1716 visit_pending = invisible_visit