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