Change HTML math-output default to MathJax
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob4daa74fbba845cca202c2d9fbfdf7e8697057fab
1 # $Id$
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
5 """
6 Simple HyperText Markup Language document tree Writer.
8 The output conforms to the XHTML version 1.0 Transitional DTD
9 (*almost* strict). The output contains a minimum of formatting
10 information. The cascading style sheet "html4css1.css" is required
11 for proper viewing with a modern graphical browser.
12 """
14 __docformat__ = 'reStructuredText'
17 import sys
18 import os
19 import os.path
20 import time
21 import re
22 try:
23 import Image # check for the Python Imaging Library
24 except ImportError:
25 Image = None
26 import docutils
27 from docutils import frontend, nodes, utils, writers, languages, io
28 from docutils.transforms import writer_aux
29 from docutils.math import unichar2tex, pick_math_environment
30 from docutils.math.latex2mathml import parse_latex_math
31 from docutils.math.math2html import math2html
33 class Writer(writers.Writer):
35 supported = ('html', 'html4css1', 'xhtml')
36 """Formats this writer supports."""
38 default_stylesheet = 'html4css1.css'
40 default_stylesheet_path = utils.relative_path(
41 os.path.join(os.getcwd(), 'dummy'),
42 os.path.join(os.path.dirname(__file__), default_stylesheet))
44 default_template = 'template.txt'
46 default_template_path = utils.relative_path(
47 os.path.join(os.getcwd(), 'dummy'),
48 os.path.join(os.path.dirname(__file__), default_template))
50 settings_spec = (
51 'HTML-Specific Options',
52 None,
53 (('Specify the template file (UTF-8 encoded). Default is "%s".'
54 % default_template_path,
55 ['--template'],
56 {'default': default_template_path, 'metavar': '<file>'}),
57 ('Specify comma separated list of stylesheet URLs. '
58 'Overrides previous --stylesheet and --stylesheet-path settings.',
59 ['--stylesheet'],
60 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
61 ('Specify comma separated list of stylesheet paths. '
62 'With --link-stylesheet, '
63 'the path is rewritten relative to the output HTML file. '
64 'Default: "%s"' % default_stylesheet_path,
65 ['--stylesheet-path'],
66 {'metavar': '<file>', 'overrides': 'stylesheet',
67 'default': default_stylesheet_path}),
68 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
69 'files must be accessible during processing. This is the default.',
70 ['--embed-stylesheet'],
71 {'default': 1, 'action': 'store_true',
72 'validator': frontend.validate_boolean}),
73 ('Link to the stylesheet(s) in the output HTML file. '
74 'Default: embed stylesheets.',
75 ['--link-stylesheet'],
76 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
77 ('Specify the initial header level. Default is 1 for "<h1>". '
78 'Does not affect document title & subtitle (see --no-doc-title).',
79 ['--initial-header-level'],
80 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
81 'metavar': '<level>'}),
82 ('Specify the maximum width (in characters) for one-column field '
83 'names. Longer field names will span an entire row of the table '
84 'used to render the field list. Default is 14 characters. '
85 'Use 0 for "no limit".',
86 ['--field-name-limit'],
87 {'default': 14, 'metavar': '<level>',
88 'validator': frontend.validate_nonnegative_int}),
89 ('Specify the maximum width (in characters) for options in option '
90 'lists. Longer options will span an entire row of the table used '
91 'to render the option list. Default is 14 characters. '
92 'Use 0 for "no limit".',
93 ['--option-limit'],
94 {'default': 14, 'metavar': '<level>',
95 'validator': frontend.validate_nonnegative_int}),
96 ('Format for footnote references: one of "superscript" or '
97 '"brackets". Default is "brackets".',
98 ['--footnote-references'],
99 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
100 'metavar': '<format>',
101 'overrides': 'trim_footnote_reference_space'}),
102 ('Format for block quote attributions: one of "dash" (em-dash '
103 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
104 ['--attribution'],
105 {'choices': ['dash', 'parentheses', 'parens', 'none'],
106 'default': 'dash', 'metavar': '<format>'}),
107 ('Remove extra vertical whitespace between items of "simple" bullet '
108 'lists and enumerated lists. Default: enabled.',
109 ['--compact-lists'],
110 {'default': 1, 'action': 'store_true',
111 'validator': frontend.validate_boolean}),
112 ('Disable compact simple bullet and enumerated lists.',
113 ['--no-compact-lists'],
114 {'dest': 'compact_lists', 'action': 'store_false'}),
115 ('Remove extra vertical whitespace between items of simple field '
116 'lists. Default: enabled.',
117 ['--compact-field-lists'],
118 {'default': 1, 'action': 'store_true',
119 'validator': frontend.validate_boolean}),
120 ('Disable compact simple field lists.',
121 ['--no-compact-field-lists'],
122 {'dest': 'compact_field_lists', 'action': 'store_false'}),
123 ('Added to standard table classes. '
124 'Defined styles: "borderless". Default: ""',
125 ['--table-style'],
126 {'default': ''}),
127 ('Math output format, one of "MathML", "HTML", "MathJax" '
128 'or "LaTeX". Default: "MathJax"',
129 ['--math-output'],
130 {'default': 'MathJax'}),
131 ('Omit the XML declaration. Use with caution.',
132 ['--no-xml-declaration'],
133 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
134 'validator': frontend.validate_boolean}),
135 ('Obfuscate email addresses to confuse harvesters while still '
136 'keeping email links usable with standards-compliant browsers.',
137 ['--cloak-email-addresses'],
138 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
140 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
142 relative_path_settings = ('stylesheet_path',)
144 config_section = 'html4css1 writer'
145 config_section_dependencies = ('writers',)
147 visitor_attributes = (
148 'head_prefix', 'head', 'stylesheet', 'body_prefix',
149 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
150 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
151 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
152 'html_body')
154 def get_transforms(self):
155 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
157 def __init__(self):
158 writers.Writer.__init__(self)
159 self.translator_class = HTMLTranslator
161 def translate(self):
162 self.visitor = visitor = self.translator_class(self.document)
163 self.document.walkabout(visitor)
164 for attr in self.visitor_attributes:
165 setattr(self, attr, getattr(visitor, attr))
166 self.output = self.apply_template()
168 def apply_template(self):
169 template_file = open(self.document.settings.template, 'rb')
170 template = unicode(template_file.read(), 'utf-8')
171 template_file.close()
172 subs = self.interpolation_dict()
173 return template % subs
175 def interpolation_dict(self):
176 subs = {}
177 settings = self.document.settings
178 for attr in self.visitor_attributes:
179 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
180 subs['encoding'] = settings.output_encoding
181 subs['version'] = docutils.__version__
182 return subs
184 def assemble_parts(self):
185 writers.Writer.assemble_parts(self)
186 for part in self.visitor_attributes:
187 self.parts[part] = ''.join(getattr(self, part))
190 class HTMLTranslator(nodes.NodeVisitor):
193 This HTML writer has been optimized to produce visually compact
194 lists (less vertical whitespace). HTML's mixed content models
195 allow list items to contain "<li><p>body elements</p></li>" or
196 "<li>just text</li>" or even "<li>text<p>and body
197 elements</p>combined</li>", each with different effects. It would
198 be best to stick with strict body elements in list items, but they
199 affect vertical spacing in browsers (although they really
200 shouldn't).
202 Here is an outline of the optimization:
204 - Check for and omit <p> tags in "simple" lists: list items
205 contain either a single paragraph, a nested simple list, or a
206 paragraph followed by a nested simple list. This means that
207 this list can be compact:
209 - Item 1.
210 - Item 2.
212 But this list cannot be compact:
214 - Item 1.
216 This second paragraph forces space between list items.
218 - Item 2.
220 - In non-list contexts, omit <p> tags on a paragraph if that
221 paragraph is the only child of its parent (footnotes & citations
222 are allowed a label first).
224 - Regardless of the above, in definitions, table cells, field bodies,
225 option descriptions, and list items, mark the first child with
226 'class="first"' and the last child with 'class="last"'. The stylesheet
227 sets the margins (top & bottom respectively) to 0 for these elements.
229 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
230 option) disables list whitespace optimization.
233 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
234 doctype = (
235 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
236 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
237 doctype_mathml = doctype
239 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
240 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
241 content_type = ('<meta http-equiv="Content-Type"'
242 ' content="text/html; charset=%s" />\n')
243 content_type_mathml = ('<meta http-equiv="Content-Type"'
244 ' content="application/xhtml+xml; charset=%s" />\n')
246 generator = ('<meta name="generator" content="Docutils %s: '
247 'http://docutils.sourceforge.net/" />\n')
249 # Template for the MathJax script in the header:
250 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
251 # The latest version of MathJax from the distributed server:
252 # avaliable to the public under the `MathJax CDN Terms of Service`__
253 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
254 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
255 'config=TeX-AMS-MML_HTMLorMML')
256 # TODO: make this configurable:
258 # a) as extra option or
259 # b) appended to math-output="MathJax"?
261 # If b), which delimiter/delimter-set (':', ',', ' ')?
263 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
264 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
265 words_and_spaces = re.compile(r'\S+| +|\n')
266 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
267 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
269 def __init__(self, document):
270 nodes.NodeVisitor.__init__(self, document)
271 self.settings = settings = document.settings
272 lcode = settings.language_code
273 self.language = languages.get_language(lcode, document.reporter)
274 self.meta = [self.generator % docutils.__version__]
275 self.head_prefix = []
276 self.html_prolog = []
277 if settings.xml_declaration:
278 self.head_prefix.append(self.xml_declaration
279 % settings.output_encoding)
280 # encoding not interpolated:
281 self.html_prolog.append(self.xml_declaration)
282 self.head = self.meta[:]
283 # stylesheets
284 styles = utils.get_stylesheet_list(settings)
285 if settings.stylesheet_path and not(settings.embed_stylesheet):
286 styles = [utils.relative_path(settings._destination, sheet)
287 for sheet in styles]
288 if settings.embed_stylesheet:
289 settings.record_dependencies.add(*styles)
290 self.stylesheet = [self.embedded_stylesheet %
291 io.FileInput(source_path=sheet, encoding='utf-8').read()
292 for sheet in styles]
293 else: # link to stylesheets
294 self.stylesheet = [self.stylesheet_link % self.encode(stylesheet)
295 for stylesheet in styles]
296 self.body_prefix = ['</head>\n<body>\n']
297 # document title, subtitle display
298 self.body_pre_docinfo = []
299 # author, date, etc.
300 self.docinfo = []
301 self.body = []
302 self.fragment = []
303 self.body_suffix = ['</body>\n</html>\n']
304 self.section_level = 0
305 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 = None
314 self.compact_field_list = None
315 self.in_docinfo = None
316 self.in_sidebar = None
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
326 self.in_mailto = 0
327 self.author_in_authors = None
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 starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
376 Construct and return a start tag given a node (id & class attributes
377 are extracted), tag name, and optional attributes.
379 tagname = tagname.lower()
380 prefix = []
381 atts = {}
382 ids = []
383 for (name, value) in attributes.items():
384 atts[name.lower()] = value
385 classes = node.get('classes', [])
386 if 'class' in atts:
387 classes.append(atts.pop('class'))
388 # move language specification to 'lang' attribute
389 languages = [cls for cls in classes
390 if cls.startswith('language-')]
391 if languages:
392 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
393 atts[self.lang_attribute] = languages[0][9:]
394 classes.pop(classes.index(languages[0]))
395 classes = ' '.join(classes).strip()
396 if classes:
397 atts['class'] = classes
398 assert 'id' not in atts
399 ids.extend(node.get('ids', []))
400 if 'ids' in atts:
401 ids.extend(atts['ids'])
402 del atts['ids']
403 if ids:
404 atts['id'] = ids[0]
405 for id in ids[1:]:
406 # Add empty "span" elements for additional IDs. Note
407 # that we cannot use empty "a" elements because there
408 # may be targets inside of references, but nested "a"
409 # elements aren't allowed in XHTML (even if they do
410 # not all have a "href" attribute).
411 if empty:
412 # Empty tag. Insert target right in front of element.
413 prefix.append('<span id="%s"></span>' % id)
414 else:
415 # Non-empty tag. Place the auxiliary <span> tag
416 # *inside* the element, as the first child.
417 suffix += '<span id="%s"></span>' % id
418 attlist = atts.items()
419 attlist.sort()
420 parts = [tagname]
421 for name, value in attlist:
422 # value=None was used for boolean attributes without
423 # value, but this isn't supported by XHTML.
424 assert value is not None
425 if isinstance(value, list):
426 values = [unicode(v) for v in value]
427 parts.append('%s="%s"' % (name.lower(),
428 self.attval(' '.join(values))))
429 else:
430 parts.append('%s="%s"' % (name.lower(),
431 self.attval(unicode(value))))
432 if empty:
433 infix = ' /'
434 else:
435 infix = ''
436 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
438 def emptytag(self, node, tagname, suffix='\n', **attributes):
439 """Construct and return an XML-compatible empty tag."""
440 return self.starttag(node, tagname, suffix, empty=1, **attributes)
442 def set_class_on_child(self, node, class_, index=0):
444 Set class `class_` on the visible child no. index of `node`.
445 Do nothing if node has fewer children than `index`.
447 children = [n for n in node if not isinstance(n, nodes.Invisible)]
448 try:
449 child = children[index]
450 except IndexError:
451 return
452 child['classes'].append(class_)
454 def set_first_last(self, node):
455 self.set_class_on_child(node, 'first', 0)
456 self.set_class_on_child(node, 'last', -1)
458 def visit_Text(self, node):
459 text = node.astext()
460 encoded = self.encode(text)
461 if self.in_mailto and self.settings.cloak_email_addresses:
462 encoded = self.cloak_email(encoded)
463 self.body.append(encoded)
465 def depart_Text(self, node):
466 pass
468 def visit_abbreviation(self, node):
469 # @@@ implementation incomplete ("title" attribute)
470 self.body.append(self.starttag(node, 'abbr', ''))
472 def depart_abbreviation(self, node):
473 self.body.append('</abbr>')
475 def visit_acronym(self, node):
476 # @@@ implementation incomplete ("title" attribute)
477 self.body.append(self.starttag(node, 'acronym', ''))
479 def depart_acronym(self, node):
480 self.body.append('</acronym>')
482 def visit_address(self, node):
483 self.visit_docinfo_item(node, 'address', meta=None)
484 self.body.append(self.starttag(node, 'pre', CLASS='address'))
486 def depart_address(self, node):
487 self.body.append('\n</pre>\n')
488 self.depart_docinfo_item()
490 def visit_admonition(self, node):
491 self.body.append(self.starttag(node, 'div'))
492 self.set_first_last(node)
494 def depart_admonition(self, node=None):
495 self.body.append('</div>\n')
497 attribution_formats = {'dash': ('&mdash;', ''),
498 'parentheses': ('(', ')'),
499 'parens': ('(', ')'),
500 'none': ('', '')}
502 def visit_attribution(self, node):
503 prefix, suffix = self.attribution_formats[self.settings.attribution]
504 self.context.append(suffix)
505 self.body.append(
506 self.starttag(node, 'p', prefix, CLASS='attribution'))
508 def depart_attribution(self, node):
509 self.body.append(self.context.pop() + '</p>\n')
511 def visit_author(self, node):
512 if isinstance(node.parent, nodes.authors):
513 if self.author_in_authors:
514 self.body.append('\n<br />')
515 else:
516 self.visit_docinfo_item(node, 'author')
518 def depart_author(self, node):
519 if isinstance(node.parent, nodes.authors):
520 self.author_in_authors += 1
521 else:
522 self.depart_docinfo_item()
524 def visit_authors(self, node):
525 self.visit_docinfo_item(node, 'authors')
526 self.author_in_authors = 0 # initialize counter
528 def depart_authors(self, node):
529 self.depart_docinfo_item()
530 self.author_in_authors = None
532 def visit_block_quote(self, node):
533 self.body.append(self.starttag(node, 'blockquote'))
535 def depart_block_quote(self, node):
536 self.body.append('</blockquote>\n')
538 def check_simple_list(self, node):
539 """Check for a simple list that can be rendered compactly."""
540 visitor = SimpleListChecker(self.document)
541 try:
542 node.walk(visitor)
543 except nodes.NodeFound:
544 return None
545 else:
546 return 1
548 def is_compactable(self, node):
549 return ('compact' in node['classes']
550 or (self.settings.compact_lists
551 and 'open' not in node['classes']
552 and (self.compact_simple
553 or self.topic_classes == ['contents']
554 or self.check_simple_list(node))))
556 def visit_bullet_list(self, node):
557 atts = {}
558 old_compact_simple = self.compact_simple
559 self.context.append((self.compact_simple, self.compact_p))
560 self.compact_p = None
561 self.compact_simple = self.is_compactable(node)
562 if self.compact_simple and not old_compact_simple:
563 atts['class'] = 'simple'
564 self.body.append(self.starttag(node, 'ul', **atts))
566 def depart_bullet_list(self, node):
567 self.compact_simple, self.compact_p = self.context.pop()
568 self.body.append('</ul>\n')
570 def visit_caption(self, node):
571 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
573 def depart_caption(self, node):
574 self.body.append('</p>\n')
576 def visit_citation(self, node):
577 self.body.append(self.starttag(node, 'table',
578 CLASS='docutils citation',
579 frame="void", rules="none"))
580 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
581 '<tbody valign="top">\n'
582 '<tr>')
583 self.footnote_backrefs(node)
585 def depart_citation(self, node):
586 self.body.append('</td></tr>\n'
587 '</tbody>\n</table>\n')
589 def visit_citation_reference(self, node):
590 href = '#' + node['refid']
591 self.body.append(self.starttag(
592 node, 'a', '[', CLASS='citation-reference', href=href))
594 def depart_citation_reference(self, node):
595 self.body.append(']</a>')
597 def visit_classifier(self, node):
598 self.body.append(' <span class="classifier-delimiter">:</span> ')
599 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
601 def depart_classifier(self, node):
602 self.body.append('</span>')
604 def visit_colspec(self, node):
605 self.colspecs.append(node)
606 # "stubs" list is an attribute of the tgroup element:
607 node.parent.stubs.append(node.attributes.get('stub'))
609 def depart_colspec(self, node):
610 pass
612 def write_colspecs(self):
613 width = 0
614 for node in self.colspecs:
615 width += node['colwidth']
616 for node in self.colspecs:
617 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
618 self.body.append(self.emptytag(node, 'col',
619 width='%i%%' % colwidth))
620 self.colspecs = []
622 def visit_comment(self, node,
623 sub=re.compile('-(?=-)').sub):
624 """Escape double-dashes in comment text."""
625 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
626 # Content already processed:
627 raise nodes.SkipNode
629 def visit_compound(self, node):
630 self.body.append(self.starttag(node, 'div', CLASS='compound'))
631 if len(node) > 1:
632 node[0]['classes'].append('compound-first')
633 node[-1]['classes'].append('compound-last')
634 for child in node[1:-1]:
635 child['classes'].append('compound-middle')
637 def depart_compound(self, node):
638 self.body.append('</div>\n')
640 def visit_container(self, node):
641 self.body.append(self.starttag(node, 'div', CLASS='container'))
643 def depart_container(self, node):
644 self.body.append('</div>\n')
646 def visit_contact(self, node):
647 self.visit_docinfo_item(node, 'contact', meta=None)
649 def depart_contact(self, node):
650 self.depart_docinfo_item()
652 def visit_copyright(self, node):
653 self.visit_docinfo_item(node, 'copyright')
655 def depart_copyright(self, node):
656 self.depart_docinfo_item()
658 def visit_date(self, node):
659 self.visit_docinfo_item(node, 'date')
661 def depart_date(self, node):
662 self.depart_docinfo_item()
664 def visit_decoration(self, node):
665 pass
667 def depart_decoration(self, node):
668 pass
670 def visit_definition(self, node):
671 self.body.append('</dt>\n')
672 self.body.append(self.starttag(node, 'dd', ''))
673 self.set_first_last(node)
675 def depart_definition(self, node):
676 self.body.append('</dd>\n')
678 def visit_definition_list(self, node):
679 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
681 def depart_definition_list(self, node):
682 self.body.append('</dl>\n')
684 def visit_definition_list_item(self, node):
685 pass
687 def depart_definition_list_item(self, node):
688 pass
690 def visit_description(self, node):
691 self.body.append(self.starttag(node, 'td', ''))
692 self.set_first_last(node)
694 def depart_description(self, node):
695 self.body.append('</td>')
697 def visit_docinfo(self, node):
698 self.context.append(len(self.body))
699 self.body.append(self.starttag(node, 'table',
700 CLASS='docinfo',
701 frame="void", rules="none"))
702 self.body.append('<col class="docinfo-name" />\n'
703 '<col class="docinfo-content" />\n'
704 '<tbody valign="top">\n')
705 self.in_docinfo = 1
707 def depart_docinfo(self, node):
708 self.body.append('</tbody>\n</table>\n')
709 self.in_docinfo = None
710 start = self.context.pop()
711 self.docinfo = self.body[start:]
712 self.body = []
714 def visit_docinfo_item(self, node, name, meta=1):
715 if meta:
716 meta_tag = '<meta name="%s" content="%s" />\n' \
717 % (name, self.attval(node.astext()))
718 self.add_meta(meta_tag)
719 self.body.append(self.starttag(node, 'tr', ''))
720 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
721 % self.language.labels[name])
722 if len(node):
723 if isinstance(node[0], nodes.Element):
724 node[0]['classes'].append('first')
725 if isinstance(node[-1], nodes.Element):
726 node[-1]['classes'].append('last')
728 def depart_docinfo_item(self):
729 self.body.append('</td></tr>\n')
731 def visit_doctest_block(self, node):
732 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
734 def depart_doctest_block(self, node):
735 self.body.append('\n</pre>\n')
737 def visit_document(self, node):
738 self.head.append('<title>%s</title>\n'
739 % self.encode(node.get('title', '')))
741 def depart_document(self, node):
742 self.head_prefix.extend([self.doctype,
743 self.head_prefix_template %
744 {'lang': self.settings.language_code}])
745 self.html_prolog.append(self.doctype)
746 self.meta.insert(0, self.content_type % self.settings.output_encoding)
747 self.head.insert(0, self.content_type % self.settings.output_encoding)
748 if self.math_header:
749 self.head.append(self.math_header)
750 # skip content-type meta tag with interpolated charset value:
751 self.html_head.extend(self.head[1:])
752 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
753 self.body_suffix.insert(0, '</div>\n')
754 self.fragment.extend(self.body) # self.fragment is the "naked" body
755 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
756 + self.docinfo + self.body
757 + self.body_suffix[:-1])
758 assert not self.context, 'len(context) = %s' % len(self.context)
760 def visit_emphasis(self, node):
761 self.body.append(self.starttag(node, 'em', ''))
763 def depart_emphasis(self, node):
764 self.body.append('</em>')
766 def visit_entry(self, node):
767 atts = {'class': []}
768 if isinstance(node.parent.parent, nodes.thead):
769 atts['class'].append('head')
770 if node.parent.parent.parent.stubs[node.parent.column]:
771 # "stubs" list is an attribute of the tgroup element
772 atts['class'].append('stub')
773 if atts['class']:
774 tagname = 'th'
775 atts['class'] = ' '.join(atts['class'])
776 else:
777 tagname = 'td'
778 del atts['class']
779 node.parent.column += 1
780 if 'morerows' in node:
781 atts['rowspan'] = node['morerows'] + 1
782 if 'morecols' in node:
783 atts['colspan'] = node['morecols'] + 1
784 node.parent.column += node['morecols']
785 self.body.append(self.starttag(node, tagname, '', **atts))
786 self.context.append('</%s>\n' % tagname.lower())
787 if len(node) == 0: # empty cell
788 self.body.append('&nbsp;')
789 self.set_first_last(node)
791 def depart_entry(self, node):
792 self.body.append(self.context.pop())
794 def visit_enumerated_list(self, node):
796 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
797 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
798 usable.
800 atts = {}
801 if 'start' in node:
802 atts['start'] = node['start']
803 if 'enumtype' in node:
804 atts['class'] = node['enumtype']
805 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
806 # single "format" attribute? Use CSS2?
807 old_compact_simple = self.compact_simple
808 self.context.append((self.compact_simple, self.compact_p))
809 self.compact_p = None
810 self.compact_simple = self.is_compactable(node)
811 if self.compact_simple and not old_compact_simple:
812 atts['class'] = (atts.get('class', '') + ' simple').strip()
813 self.body.append(self.starttag(node, 'ol', **atts))
815 def depart_enumerated_list(self, node):
816 self.compact_simple, self.compact_p = self.context.pop()
817 self.body.append('</ol>\n')
819 def visit_field(self, node):
820 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
822 def depart_field(self, node):
823 self.body.append('</tr>\n')
825 def visit_field_body(self, node):
826 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
827 self.set_class_on_child(node, 'first', 0)
828 field = node.parent
829 if (self.compact_field_list or
830 isinstance(field.parent, nodes.docinfo) or
831 field.parent.index(field) == len(field.parent) - 1):
832 # If we are in a compact list, the docinfo, or if this is
833 # the last field of the field list, do not add vertical
834 # space after last element.
835 self.set_class_on_child(node, 'last', -1)
837 def depart_field_body(self, node):
838 self.body.append('</td>\n')
840 def visit_field_list(self, node):
841 self.context.append((self.compact_field_list, self.compact_p))
842 self.compact_p = None
843 if 'compact' in node['classes']:
844 self.compact_field_list = 1
845 elif (self.settings.compact_field_lists
846 and 'open' not in node['classes']):
847 self.compact_field_list = 1
848 if self.compact_field_list:
849 for field in node:
850 field_body = field[-1]
851 assert isinstance(field_body, nodes.field_body)
852 children = [n for n in field_body
853 if not isinstance(n, nodes.Invisible)]
854 if not (len(children) == 0 or
855 len(children) == 1 and
856 isinstance(children[0],
857 (nodes.paragraph, nodes.line_block))):
858 self.compact_field_list = 0
859 break
860 self.body.append(self.starttag(node, 'table', frame='void',
861 rules='none',
862 CLASS='docutils field-list'))
863 self.body.append('<col class="field-name" />\n'
864 '<col class="field-body" />\n'
865 '<tbody valign="top">\n')
867 def depart_field_list(self, node):
868 self.body.append('</tbody>\n</table>\n')
869 self.compact_field_list, self.compact_p = self.context.pop()
871 def visit_field_name(self, node):
872 atts = {}
873 if self.in_docinfo:
874 atts['class'] = 'docinfo-name'
875 else:
876 atts['class'] = 'field-name'
877 if ( self.settings.field_name_limit
878 and len(node.astext()) > self.settings.field_name_limit):
879 atts['colspan'] = 2
880 self.context.append('</tr>\n'
881 + self.starttag(node.parent, 'tr', '')
882 + '<td>&nbsp;</td>')
883 else:
884 self.context.append('')
885 self.body.append(self.starttag(node, 'th', '', **atts))
887 def depart_field_name(self, node):
888 self.body.append(':</th>')
889 self.body.append(self.context.pop())
891 def visit_figure(self, node):
892 atts = {'class': 'figure'}
893 if node.get('width'):
894 atts['style'] = 'width: %s' % node['width']
895 if node.get('align'):
896 atts['class'] += " align-" + node['align']
897 self.body.append(self.starttag(node, 'div', **atts))
899 def depart_figure(self, node):
900 self.body.append('</div>\n')
902 def visit_footer(self, node):
903 self.context.append(len(self.body))
905 def depart_footer(self, node):
906 start = self.context.pop()
907 footer = [self.starttag(node, 'div', CLASS='footer'),
908 '<hr class="footer" />\n']
909 footer.extend(self.body[start:])
910 footer.append('\n</div>\n')
911 self.footer.extend(footer)
912 self.body_suffix[:0] = footer
913 del self.body[start:]
915 def visit_footnote(self, node):
916 self.body.append(self.starttag(node, 'table',
917 CLASS='docutils footnote',
918 frame="void", rules="none"))
919 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
920 '<tbody valign="top">\n'
921 '<tr>')
922 self.footnote_backrefs(node)
924 def footnote_backrefs(self, node):
925 backlinks = []
926 backrefs = node['backrefs']
927 if self.settings.footnote_backlinks and backrefs:
928 if len(backrefs) == 1:
929 self.context.append('')
930 self.context.append('</a>')
931 self.context.append('<a class="fn-backref" href="#%s">'
932 % backrefs[0])
933 else:
934 i = 1
935 for backref in backrefs:
936 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
937 % (backref, i))
938 i += 1
939 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
940 self.context += ['', '']
941 else:
942 self.context.append('')
943 self.context += ['', '']
944 # If the node does not only consist of a label.
945 if len(node) > 1:
946 # If there are preceding backlinks, we do not set class
947 # 'first', because we need to retain the top-margin.
948 if not backlinks:
949 node[1]['classes'].append('first')
950 node[-1]['classes'].append('last')
952 def depart_footnote(self, node):
953 self.body.append('</td></tr>\n'
954 '</tbody>\n</table>\n')
956 def visit_footnote_reference(self, node):
957 href = '#' + node['refid']
958 format = self.settings.footnote_references
959 if format == 'brackets':
960 suffix = '['
961 self.context.append(']')
962 else:
963 assert format == 'superscript'
964 suffix = '<sup>'
965 self.context.append('</sup>')
966 self.body.append(self.starttag(node, 'a', suffix,
967 CLASS='footnote-reference', href=href))
969 def depart_footnote_reference(self, node):
970 self.body.append(self.context.pop() + '</a>')
972 def visit_generated(self, node):
973 pass
975 def depart_generated(self, node):
976 pass
978 def visit_header(self, node):
979 self.context.append(len(self.body))
981 def depart_header(self, node):
982 start = self.context.pop()
983 header = [self.starttag(node, 'div', CLASS='header')]
984 header.extend(self.body[start:])
985 header.append('\n<hr class="header"/>\n</div>\n')
986 self.body_prefix.extend(header)
987 self.header.extend(header)
988 del self.body[start:]
990 def visit_image(self, node):
991 atts = {}
992 uri = node['uri']
993 # place SVG and SWF images in an <object> element
994 types = {'.svg': 'image/svg+xml',
995 '.swf': 'application/x-shockwave-flash'}
996 ext = os.path.splitext(uri)[1].lower()
997 if ext in ('.svg', '.swf'):
998 atts['data'] = uri
999 atts['type'] = types[ext]
1000 else:
1001 atts['src'] = uri
1002 atts['alt'] = node.get('alt', uri)
1003 # image size
1004 if 'width' in node:
1005 atts['width'] = node['width']
1006 if 'height' in node:
1007 atts['height'] = node['height']
1008 if 'scale' in node:
1009 if Image and not ('width' in node and 'height' in node):
1010 try:
1011 im = Image.open(str(uri))
1012 except (IOError, # Source image can't be found or opened
1013 UnicodeError): # PIL doesn't like Unicode paths.
1014 pass
1015 else:
1016 if 'width' not in atts:
1017 atts['width'] = str(im.size[0])
1018 if 'height' not in atts:
1019 atts['height'] = str(im.size[1])
1020 del im
1021 for att_name in 'width', 'height':
1022 if att_name in atts:
1023 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1024 assert match
1025 atts[att_name] = '%s%s' % (
1026 float(match.group(1)) * (float(node['scale']) / 100),
1027 match.group(2))
1028 style = []
1029 for att_name in 'width', 'height':
1030 if att_name in atts:
1031 if re.match(r'^[0-9.]+$', atts[att_name]):
1032 # Interpret unitless values as pixels.
1033 atts[att_name] += 'px'
1034 style.append('%s: %s;' % (att_name, atts[att_name]))
1035 del atts[att_name]
1036 if style:
1037 atts['style'] = ' '.join(style)
1038 if (isinstance(node.parent, nodes.TextElement) or
1039 (isinstance(node.parent, nodes.reference) and
1040 not isinstance(node.parent.parent, nodes.TextElement))):
1041 # Inline context or surrounded by <a>...</a>.
1042 suffix = ''
1043 else:
1044 suffix = '\n'
1045 if 'align' in node:
1046 atts['class'] = 'align-%s' % node['align']
1047 self.context.append('')
1048 if ext in ('.svg', '.swf'): # place in an object element,
1049 # do NOT use an empty tag: incorrect rendering in browsers
1050 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1051 node.get('alt', uri) + '</object>' + suffix)
1052 else:
1053 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1055 def depart_image(self, node):
1056 self.body.append(self.context.pop())
1058 def visit_inline(self, node):
1059 self.body.append(self.starttag(node, 'span', ''))
1061 def depart_inline(self, node):
1062 self.body.append('</span>')
1064 def visit_label(self, node):
1065 # Context added in footnote_backrefs.
1066 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1067 CLASS='label'))
1069 def depart_label(self, node):
1070 # Context added in footnote_backrefs.
1071 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1073 def visit_legend(self, node):
1074 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1076 def depart_legend(self, node):
1077 self.body.append('</div>\n')
1079 def visit_line(self, node):
1080 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1081 if not len(node):
1082 self.body.append('<br />')
1084 def depart_line(self, node):
1085 self.body.append('</div>\n')
1087 def visit_line_block(self, node):
1088 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1090 def depart_line_block(self, node):
1091 self.body.append('</div>\n')
1093 def visit_list_item(self, node):
1094 self.body.append(self.starttag(node, 'li', ''))
1095 if len(node):
1096 node[0]['classes'].append('first')
1098 def depart_list_item(self, node):
1099 self.body.append('</li>\n')
1101 def visit_literal(self, node):
1102 """Process text to prevent tokens from wrapping."""
1103 self.body.append(
1104 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1105 text = node.astext()
1106 for token in self.words_and_spaces.findall(text):
1107 if token.strip():
1108 # Protect text like "--an-option" and the regular expression
1109 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1110 if self.sollbruchstelle.search(token):
1111 self.body.append('<span class="pre">%s</span>'
1112 % self.encode(token))
1113 else:
1114 self.body.append(self.encode(token))
1115 elif token in ('\n', ' '):
1116 # Allow breaks at whitespace:
1117 self.body.append(token)
1118 else:
1119 # Protect runs of multiple spaces; the last space can wrap:
1120 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1121 self.body.append('</tt>')
1122 # Content already processed:
1123 raise nodes.SkipNode
1125 def visit_literal_block(self, node):
1126 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1128 def depart_literal_block(self, node):
1129 self.body.append('\n</pre>\n')
1131 def visit_math(self, node, math_env=''):
1132 # As there is no native HTML math support, we provide alternatives:
1133 # LaTeX and MathJax math_output modes simply wrap the content,
1134 # HTML and MathML math_output modes also convert the math_code.
1135 # If the method is called from visit_math_block(), math_env != ''.
1137 # HTML container
1138 tags = {# math_output: (block, inline, class-arguments)
1139 'mathml': ('div', '', ''),
1140 'html': ('div', 'span', 'formula'),
1141 'mathjax': ('div', 'span', 'math'),
1142 'latex': ('pre', 'tt', 'math'),
1144 tag = tags[self.math_output][math_env == '']
1145 clsarg = tags[self.math_output][2]
1146 # LaTeX container
1147 wrappers = {# math_mode: (inline, block)
1148 'mathml': (None, None),
1149 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1150 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1151 'latex': (None, None),
1153 wrapper = wrappers[self.math_output][math_env != '']
1154 # get and wrap content
1155 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1156 if wrapper and math_env:
1157 math_code = wrapper % (math_env, math_code, math_env)
1158 elif wrapper:
1159 math_code = wrapper % math_code
1160 # settings and conversion
1161 if self.math_output in ('latex', 'mathjax'):
1162 math_code = self.encode(math_code)
1163 if self.math_output == 'mathjax':
1164 self.math_header = self.mathjax_script % self.mathjax_url
1165 elif self.math_output == 'html':
1166 math_code = math2html(math_code)
1167 elif self.math_output == 'mathml':
1168 self.doctype = self.doctype_mathml
1169 self.content_type = self.content_type_mathml
1170 try:
1171 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1172 math_code = ''.join(mathml_tree.xml())
1173 except SyntaxError, err:
1174 err_node = self.document.reporter.error(err, base_node=node)
1175 self.visit_system_message(err_node)
1176 self.body.append(self.starttag(node, 'p'))
1177 self.body.append(u','.join(err.args))
1178 self.body.append('</p>\n')
1179 self.body.append(self.starttag(node, 'pre',
1180 CLASS='literal-block'))
1181 self.body.append(self.encode(math_code))
1182 self.body.append('\n</pre>\n')
1183 self.depart_system_message(err_node)
1184 raise nodes.SkipNode
1185 # append to document body
1186 if tag:
1187 self.body.append(self.starttag(node, tag, CLASS=clsarg))
1188 self.body.append(math_code)
1189 if math_env:
1190 self.body.append('\n')
1191 if tag:
1192 self.body.append('</%s>\n' % tag)
1193 # Content already processed:
1194 raise nodes.SkipNode
1196 def depart_math(self, node):
1197 pass # never reached
1199 def visit_math_block(self, node):
1200 # print node.astext().encode('utf8')
1201 math_env = pick_math_environment(node.astext())
1202 self.visit_math(node, math_env=math_env)
1204 def depart_math_block(self, node):
1205 pass # never reached
1207 def visit_meta(self, node):
1208 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1209 self.add_meta(meta)
1211 def depart_meta(self, node):
1212 pass
1214 def add_meta(self, tag):
1215 self.meta.append(tag)
1216 self.head.append(tag)
1218 def visit_option(self, node):
1219 if self.context[-1]:
1220 self.body.append(', ')
1221 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1223 def depart_option(self, node):
1224 self.body.append('</span>')
1225 self.context[-1] += 1
1227 def visit_option_argument(self, node):
1228 self.body.append(node.get('delimiter', ' '))
1229 self.body.append(self.starttag(node, 'var', ''))
1231 def depart_option_argument(self, node):
1232 self.body.append('</var>')
1234 def visit_option_group(self, node):
1235 atts = {}
1236 if ( self.settings.option_limit
1237 and len(node.astext()) > self.settings.option_limit):
1238 atts['colspan'] = 2
1239 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1240 else:
1241 self.context.append('')
1242 self.body.append(
1243 self.starttag(node, 'td', CLASS='option-group', **atts))
1244 self.body.append('<kbd>')
1245 self.context.append(0) # count number of options
1247 def depart_option_group(self, node):
1248 self.context.pop()
1249 self.body.append('</kbd></td>\n')
1250 self.body.append(self.context.pop())
1252 def visit_option_list(self, node):
1253 self.body.append(
1254 self.starttag(node, 'table', CLASS='docutils option-list',
1255 frame="void", rules="none"))
1256 self.body.append('<col class="option" />\n'
1257 '<col class="description" />\n'
1258 '<tbody valign="top">\n')
1260 def depart_option_list(self, node):
1261 self.body.append('</tbody>\n</table>\n')
1263 def visit_option_list_item(self, node):
1264 self.body.append(self.starttag(node, 'tr', ''))
1266 def depart_option_list_item(self, node):
1267 self.body.append('</tr>\n')
1269 def visit_option_string(self, node):
1270 pass
1272 def depart_option_string(self, node):
1273 pass
1275 def visit_organization(self, node):
1276 self.visit_docinfo_item(node, 'organization')
1278 def depart_organization(self, node):
1279 self.depart_docinfo_item()
1281 def should_be_compact_paragraph(self, node):
1283 Determine if the <p> tags around paragraph ``node`` can be omitted.
1285 if (isinstance(node.parent, nodes.document) or
1286 isinstance(node.parent, nodes.compound)):
1287 # Never compact paragraphs in document or compound.
1288 return 0
1289 for key, value in node.attlist():
1290 if (node.is_not_default(key) and
1291 not (key == 'classes' and value in
1292 ([], ['first'], ['last'], ['first', 'last']))):
1293 # Attribute which needs to survive.
1294 return 0
1295 first = isinstance(node.parent[0], nodes.label) # skip label
1296 for child in node.parent.children[first:]:
1297 # only first paragraph can be compact
1298 if isinstance(child, nodes.Invisible):
1299 continue
1300 if child is node:
1301 break
1302 return 0
1303 parent_length = len([n for n in node.parent if not isinstance(
1304 n, (nodes.Invisible, nodes.label))])
1305 if ( self.compact_simple
1306 or self.compact_field_list
1307 or self.compact_p and parent_length == 1):
1308 return 1
1309 return 0
1311 def visit_paragraph(self, node):
1312 if self.should_be_compact_paragraph(node):
1313 self.context.append('')
1314 else:
1315 self.body.append(self.starttag(node, 'p', ''))
1316 self.context.append('</p>\n')
1318 def depart_paragraph(self, node):
1319 self.body.append(self.context.pop())
1321 def visit_problematic(self, node):
1322 if node.hasattr('refid'):
1323 self.body.append('<a href="#%s">' % node['refid'])
1324 self.context.append('</a>')
1325 else:
1326 self.context.append('')
1327 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1329 def depart_problematic(self, node):
1330 self.body.append('</span>')
1331 self.body.append(self.context.pop())
1333 def visit_raw(self, node):
1334 if 'html' in node.get('format', '').split():
1335 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1336 if node['classes']:
1337 self.body.append(self.starttag(node, t, suffix=''))
1338 self.body.append(node.astext())
1339 if node['classes']:
1340 self.body.append('</%s>' % t)
1341 # Keep non-HTML raw text out of output:
1342 raise nodes.SkipNode
1344 def visit_reference(self, node):
1345 atts = {'class': 'reference'}
1346 if 'refuri' in node:
1347 atts['href'] = node['refuri']
1348 if ( self.settings.cloak_email_addresses
1349 and atts['href'].startswith('mailto:')):
1350 atts['href'] = self.cloak_mailto(atts['href'])
1351 self.in_mailto = 1
1352 atts['class'] += ' external'
1353 else:
1354 assert 'refid' in node, \
1355 'References must have "refuri" or "refid" attribute.'
1356 atts['href'] = '#' + node['refid']
1357 atts['class'] += ' internal'
1358 if not isinstance(node.parent, nodes.TextElement):
1359 assert len(node) == 1 and isinstance(node[0], nodes.image)
1360 atts['class'] += ' image-reference'
1361 self.body.append(self.starttag(node, 'a', '', **atts))
1363 def depart_reference(self, node):
1364 self.body.append('</a>')
1365 if not isinstance(node.parent, nodes.TextElement):
1366 self.body.append('\n')
1367 self.in_mailto = 0
1369 def visit_revision(self, node):
1370 self.visit_docinfo_item(node, 'revision', meta=None)
1372 def depart_revision(self, node):
1373 self.depart_docinfo_item()
1375 def visit_row(self, node):
1376 self.body.append(self.starttag(node, 'tr', ''))
1377 node.column = 0
1379 def depart_row(self, node):
1380 self.body.append('</tr>\n')
1382 def visit_rubric(self, node):
1383 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1385 def depart_rubric(self, node):
1386 self.body.append('</p>\n')
1388 def visit_section(self, node):
1389 self.section_level += 1
1390 self.body.append(
1391 self.starttag(node, 'div', CLASS='section'))
1393 def depart_section(self, node):
1394 self.section_level -= 1
1395 self.body.append('</div>\n')
1397 def visit_sidebar(self, node):
1398 self.body.append(
1399 self.starttag(node, 'div', CLASS='sidebar'))
1400 self.set_first_last(node)
1401 self.in_sidebar = 1
1403 def depart_sidebar(self, node):
1404 self.body.append('</div>\n')
1405 self.in_sidebar = None
1407 def visit_status(self, node):
1408 self.visit_docinfo_item(node, 'status', meta=None)
1410 def depart_status(self, node):
1411 self.depart_docinfo_item()
1413 def visit_strong(self, node):
1414 self.body.append(self.starttag(node, 'strong', ''))
1416 def depart_strong(self, node):
1417 self.body.append('</strong>')
1419 def visit_subscript(self, node):
1420 self.body.append(self.starttag(node, 'sub', ''))
1422 def depart_subscript(self, node):
1423 self.body.append('</sub>')
1425 def visit_substitution_definition(self, node):
1426 """Internal only."""
1427 raise nodes.SkipNode
1429 def visit_substitution_reference(self, node):
1430 self.unimplemented_visit(node)
1432 def visit_subtitle(self, node):
1433 if isinstance(node.parent, nodes.sidebar):
1434 self.body.append(self.starttag(node, 'p', '',
1435 CLASS='sidebar-subtitle'))
1436 self.context.append('</p>\n')
1437 elif isinstance(node.parent, nodes.document):
1438 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1439 self.context.append('</h2>\n')
1440 self.in_document_title = len(self.body)
1441 elif isinstance(node.parent, nodes.section):
1442 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1443 self.body.append(
1444 self.starttag(node, tag, '', CLASS='section-subtitle') +
1445 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1446 self.context.append('</span></%s>\n' % tag)
1448 def depart_subtitle(self, node):
1449 self.body.append(self.context.pop())
1450 if self.in_document_title:
1451 self.subtitle = self.body[self.in_document_title:-1]
1452 self.in_document_title = 0
1453 self.body_pre_docinfo.extend(self.body)
1454 self.html_subtitle.extend(self.body)
1455 del self.body[:]
1457 def visit_superscript(self, node):
1458 self.body.append(self.starttag(node, 'sup', ''))
1460 def depart_superscript(self, node):
1461 self.body.append('</sup>')
1463 def visit_system_message(self, node):
1464 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1465 self.body.append('<p class="system-message-title">')
1466 backref_text = ''
1467 if len(node['backrefs']):
1468 backrefs = node['backrefs']
1469 if len(backrefs) == 1:
1470 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1471 % backrefs[0])
1472 else:
1473 i = 1
1474 backlinks = []
1475 for backref in backrefs:
1476 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1477 i += 1
1478 backref_text = ('; <em>backlinks: %s</em>'
1479 % ', '.join(backlinks))
1480 if node.hasattr('line'):
1481 line = ', line %s' % node['line']
1482 else:
1483 line = ''
1484 self.body.append('System Message: %s/%s '
1485 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1486 % (node['type'], node['level'],
1487 self.encode(node['source']), line, backref_text))
1489 def depart_system_message(self, node):
1490 self.body.append('</div>\n')
1492 def visit_table(self, node):
1493 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1494 self.body.append(
1495 self.starttag(node, 'table', CLASS=classes, border="1"))
1497 def depart_table(self, node):
1498 self.body.append('</table>\n')
1500 def visit_target(self, node):
1501 if not ('refuri' in node or 'refid' in node
1502 or 'refname' in node):
1503 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1504 self.context.append('</span>')
1505 else:
1506 self.context.append('')
1508 def depart_target(self, node):
1509 self.body.append(self.context.pop())
1511 def visit_tbody(self, node):
1512 self.write_colspecs()
1513 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1514 self.body.append(self.starttag(node, 'tbody', valign='top'))
1516 def depart_tbody(self, node):
1517 self.body.append('</tbody>\n')
1519 def visit_term(self, node):
1520 self.body.append(self.starttag(node, 'dt', ''))
1522 def depart_term(self, node):
1524 Leave the end tag to `self.visit_definition()`, in case there's a
1525 classifier.
1527 pass
1529 def visit_tgroup(self, node):
1530 # Mozilla needs <colgroup>:
1531 self.body.append(self.starttag(node, 'colgroup'))
1532 # Appended by thead or tbody:
1533 self.context.append('</colgroup>\n')
1534 node.stubs = []
1536 def depart_tgroup(self, node):
1537 pass
1539 def visit_thead(self, node):
1540 self.write_colspecs()
1541 self.body.append(self.context.pop()) # '</colgroup>\n'
1542 # There may or may not be a <thead>; this is for <tbody> to use:
1543 self.context.append('')
1544 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1546 def depart_thead(self, node):
1547 self.body.append('</thead>\n')
1549 def visit_title(self, node):
1550 """Only 6 section levels are supported by HTML."""
1551 check_id = 0
1552 close_tag = '</p>\n'
1553 if isinstance(node.parent, nodes.topic):
1554 self.body.append(
1555 self.starttag(node, 'p', '', CLASS='topic-title first'))
1556 elif isinstance(node.parent, nodes.sidebar):
1557 self.body.append(
1558 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1559 elif isinstance(node.parent, nodes.Admonition):
1560 self.body.append(
1561 self.starttag(node, 'p', '', CLASS='admonition-title'))
1562 elif isinstance(node.parent, nodes.table):
1563 self.body.append(
1564 self.starttag(node, 'caption', ''))
1565 close_tag = '</caption>\n'
1566 elif isinstance(node.parent, nodes.document):
1567 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1568 close_tag = '</h1>\n'
1569 self.in_document_title = len(self.body)
1570 else:
1571 assert isinstance(node.parent, nodes.section)
1572 h_level = self.section_level + self.initial_header_level - 1
1573 atts = {}
1574 if (len(node.parent) >= 2 and
1575 isinstance(node.parent[1], nodes.subtitle)):
1576 atts['CLASS'] = 'with-subtitle'
1577 self.body.append(
1578 self.starttag(node, 'h%s' % h_level, '', **atts))
1579 atts = {}
1580 if node.hasattr('refid'):
1581 atts['class'] = 'toc-backref'
1582 atts['href'] = '#' + node['refid']
1583 if atts:
1584 self.body.append(self.starttag({}, 'a', '', **atts))
1585 close_tag = '</a></h%s>\n' % (h_level)
1586 else:
1587 close_tag = '</h%s>\n' % (h_level)
1588 self.context.append(close_tag)
1590 def depart_title(self, node):
1591 self.body.append(self.context.pop())
1592 if self.in_document_title:
1593 self.title = self.body[self.in_document_title:-1]
1594 self.in_document_title = 0
1595 self.body_pre_docinfo.extend(self.body)
1596 self.html_title.extend(self.body)
1597 del self.body[:]
1599 def visit_title_reference(self, node):
1600 self.body.append(self.starttag(node, 'cite', ''))
1602 def depart_title_reference(self, node):
1603 self.body.append('</cite>')
1605 def visit_topic(self, node):
1606 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1607 self.topic_classes = node['classes']
1609 def depart_topic(self, node):
1610 self.body.append('</div>\n')
1611 self.topic_classes = []
1613 def visit_transition(self, node):
1614 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1616 def depart_transition(self, node):
1617 pass
1619 def visit_version(self, node):
1620 self.visit_docinfo_item(node, 'version', meta=None)
1622 def depart_version(self, node):
1623 self.depart_docinfo_item()
1625 def unimplemented_visit(self, node):
1626 raise NotImplementedError('visiting unimplemented node type: %s'
1627 % node.__class__.__name__)
1630 class SimpleListChecker(nodes.GenericNodeVisitor):
1633 Raise `nodes.NodeFound` if non-simple list item is encountered.
1635 Here "simple" means a list item containing nothing other than a single
1636 paragraph, a simple list, or a paragraph followed by a simple list.
1639 def default_visit(self, node):
1640 raise nodes.NodeFound
1642 def visit_bullet_list(self, node):
1643 pass
1645 def visit_enumerated_list(self, node):
1646 pass
1648 def visit_list_item(self, node):
1649 children = []
1650 for child in node.children:
1651 if not isinstance(child, nodes.Invisible):
1652 children.append(child)
1653 if (children and isinstance(children[0], nodes.paragraph)
1654 and (isinstance(children[-1], nodes.bullet_list)
1655 or isinstance(children[-1], nodes.enumerated_list))):
1656 children.pop()
1657 if len(children) <= 1:
1658 return
1659 else:
1660 raise nodes.NodeFound
1662 def visit_paragraph(self, node):
1663 raise nodes.SkipNode
1665 def invisible_visit(self, node):
1666 """Invisible nodes should be ignored."""
1667 raise nodes.SkipNode
1669 visit_comment = invisible_visit
1670 visit_substitution_definition = invisible_visit
1671 visit_target = invisible_visit
1672 visit_pending = invisible_visit