math update
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob81490d388b5504951c96dc0c2e879b12bc85431f
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 unimathsymbols2tex, 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: "MathML"',
129 ['--math-output'],
130 {'default': 'MathML'}),
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(unimathsymbols2tex.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 math_env = pick_math_environment(node.astext())
1201 self.visit_math(node, math_env=math_env)
1203 def depart_math_block(self, node):
1204 pass # never reached
1206 def visit_meta(self, node):
1207 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1208 self.add_meta(meta)
1210 def depart_meta(self, node):
1211 pass
1213 def add_meta(self, tag):
1214 self.meta.append(tag)
1215 self.head.append(tag)
1217 def visit_option(self, node):
1218 if self.context[-1]:
1219 self.body.append(', ')
1220 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1222 def depart_option(self, node):
1223 self.body.append('</span>')
1224 self.context[-1] += 1
1226 def visit_option_argument(self, node):
1227 self.body.append(node.get('delimiter', ' '))
1228 self.body.append(self.starttag(node, 'var', ''))
1230 def depart_option_argument(self, node):
1231 self.body.append('</var>')
1233 def visit_option_group(self, node):
1234 atts = {}
1235 if ( self.settings.option_limit
1236 and len(node.astext()) > self.settings.option_limit):
1237 atts['colspan'] = 2
1238 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1239 else:
1240 self.context.append('')
1241 self.body.append(
1242 self.starttag(node, 'td', CLASS='option-group', **atts))
1243 self.body.append('<kbd>')
1244 self.context.append(0) # count number of options
1246 def depart_option_group(self, node):
1247 self.context.pop()
1248 self.body.append('</kbd></td>\n')
1249 self.body.append(self.context.pop())
1251 def visit_option_list(self, node):
1252 self.body.append(
1253 self.starttag(node, 'table', CLASS='docutils option-list',
1254 frame="void", rules="none"))
1255 self.body.append('<col class="option" />\n'
1256 '<col class="description" />\n'
1257 '<tbody valign="top">\n')
1259 def depart_option_list(self, node):
1260 self.body.append('</tbody>\n</table>\n')
1262 def visit_option_list_item(self, node):
1263 self.body.append(self.starttag(node, 'tr', ''))
1265 def depart_option_list_item(self, node):
1266 self.body.append('</tr>\n')
1268 def visit_option_string(self, node):
1269 pass
1271 def depart_option_string(self, node):
1272 pass
1274 def visit_organization(self, node):
1275 self.visit_docinfo_item(node, 'organization')
1277 def depart_organization(self, node):
1278 self.depart_docinfo_item()
1280 def should_be_compact_paragraph(self, node):
1282 Determine if the <p> tags around paragraph ``node`` can be omitted.
1284 if (isinstance(node.parent, nodes.document) or
1285 isinstance(node.parent, nodes.compound)):
1286 # Never compact paragraphs in document or compound.
1287 return 0
1288 for key, value in node.attlist():
1289 if (node.is_not_default(key) and
1290 not (key == 'classes' and value in
1291 ([], ['first'], ['last'], ['first', 'last']))):
1292 # Attribute which needs to survive.
1293 return 0
1294 first = isinstance(node.parent[0], nodes.label) # skip label
1295 for child in node.parent.children[first:]:
1296 # only first paragraph can be compact
1297 if isinstance(child, nodes.Invisible):
1298 continue
1299 if child is node:
1300 break
1301 return 0
1302 parent_length = len([n for n in node.parent if not isinstance(
1303 n, (nodes.Invisible, nodes.label))])
1304 if ( self.compact_simple
1305 or self.compact_field_list
1306 or self.compact_p and parent_length == 1):
1307 return 1
1308 return 0
1310 def visit_paragraph(self, node):
1311 if self.should_be_compact_paragraph(node):
1312 self.context.append('')
1313 else:
1314 self.body.append(self.starttag(node, 'p', ''))
1315 self.context.append('</p>\n')
1317 def depart_paragraph(self, node):
1318 self.body.append(self.context.pop())
1320 def visit_problematic(self, node):
1321 if node.hasattr('refid'):
1322 self.body.append('<a href="#%s">' % node['refid'])
1323 self.context.append('</a>')
1324 else:
1325 self.context.append('')
1326 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1328 def depart_problematic(self, node):
1329 self.body.append('</span>')
1330 self.body.append(self.context.pop())
1332 def visit_raw(self, node):
1333 if 'html' in node.get('format', '').split():
1334 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1335 if node['classes']:
1336 self.body.append(self.starttag(node, t, suffix=''))
1337 self.body.append(node.astext())
1338 if node['classes']:
1339 self.body.append('</%s>' % t)
1340 # Keep non-HTML raw text out of output:
1341 raise nodes.SkipNode
1343 def visit_reference(self, node):
1344 atts = {'class': 'reference'}
1345 if 'refuri' in node:
1346 atts['href'] = node['refuri']
1347 if ( self.settings.cloak_email_addresses
1348 and atts['href'].startswith('mailto:')):
1349 atts['href'] = self.cloak_mailto(atts['href'])
1350 self.in_mailto = 1
1351 atts['class'] += ' external'
1352 else:
1353 assert 'refid' in node, \
1354 'References must have "refuri" or "refid" attribute.'
1355 atts['href'] = '#' + node['refid']
1356 atts['class'] += ' internal'
1357 if not isinstance(node.parent, nodes.TextElement):
1358 assert len(node) == 1 and isinstance(node[0], nodes.image)
1359 atts['class'] += ' image-reference'
1360 self.body.append(self.starttag(node, 'a', '', **atts))
1362 def depart_reference(self, node):
1363 self.body.append('</a>')
1364 if not isinstance(node.parent, nodes.TextElement):
1365 self.body.append('\n')
1366 self.in_mailto = 0
1368 def visit_revision(self, node):
1369 self.visit_docinfo_item(node, 'revision', meta=None)
1371 def depart_revision(self, node):
1372 self.depart_docinfo_item()
1374 def visit_row(self, node):
1375 self.body.append(self.starttag(node, 'tr', ''))
1376 node.column = 0
1378 def depart_row(self, node):
1379 self.body.append('</tr>\n')
1381 def visit_rubric(self, node):
1382 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1384 def depart_rubric(self, node):
1385 self.body.append('</p>\n')
1387 def visit_section(self, node):
1388 self.section_level += 1
1389 self.body.append(
1390 self.starttag(node, 'div', CLASS='section'))
1392 def depart_section(self, node):
1393 self.section_level -= 1
1394 self.body.append('</div>\n')
1396 def visit_sidebar(self, node):
1397 self.body.append(
1398 self.starttag(node, 'div', CLASS='sidebar'))
1399 self.set_first_last(node)
1400 self.in_sidebar = 1
1402 def depart_sidebar(self, node):
1403 self.body.append('</div>\n')
1404 self.in_sidebar = None
1406 def visit_status(self, node):
1407 self.visit_docinfo_item(node, 'status', meta=None)
1409 def depart_status(self, node):
1410 self.depart_docinfo_item()
1412 def visit_strong(self, node):
1413 self.body.append(self.starttag(node, 'strong', ''))
1415 def depart_strong(self, node):
1416 self.body.append('</strong>')
1418 def visit_subscript(self, node):
1419 self.body.append(self.starttag(node, 'sub', ''))
1421 def depart_subscript(self, node):
1422 self.body.append('</sub>')
1424 def visit_substitution_definition(self, node):
1425 """Internal only."""
1426 raise nodes.SkipNode
1428 def visit_substitution_reference(self, node):
1429 self.unimplemented_visit(node)
1431 def visit_subtitle(self, node):
1432 if isinstance(node.parent, nodes.sidebar):
1433 self.body.append(self.starttag(node, 'p', '',
1434 CLASS='sidebar-subtitle'))
1435 self.context.append('</p>\n')
1436 elif isinstance(node.parent, nodes.document):
1437 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1438 self.context.append('</h2>\n')
1439 self.in_document_title = len(self.body)
1440 elif isinstance(node.parent, nodes.section):
1441 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1442 self.body.append(
1443 self.starttag(node, tag, '', CLASS='section-subtitle') +
1444 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1445 self.context.append('</span></%s>\n' % tag)
1447 def depart_subtitle(self, node):
1448 self.body.append(self.context.pop())
1449 if self.in_document_title:
1450 self.subtitle = self.body[self.in_document_title:-1]
1451 self.in_document_title = 0
1452 self.body_pre_docinfo.extend(self.body)
1453 self.html_subtitle.extend(self.body)
1454 del self.body[:]
1456 def visit_superscript(self, node):
1457 self.body.append(self.starttag(node, 'sup', ''))
1459 def depart_superscript(self, node):
1460 self.body.append('</sup>')
1462 def visit_system_message(self, node):
1463 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1464 self.body.append('<p class="system-message-title">')
1465 backref_text = ''
1466 if len(node['backrefs']):
1467 backrefs = node['backrefs']
1468 if len(backrefs) == 1:
1469 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1470 % backrefs[0])
1471 else:
1472 i = 1
1473 backlinks = []
1474 for backref in backrefs:
1475 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1476 i += 1
1477 backref_text = ('; <em>backlinks: %s</em>'
1478 % ', '.join(backlinks))
1479 if node.hasattr('line'):
1480 line = ', line %s' % node['line']
1481 else:
1482 line = ''
1483 self.body.append('System Message: %s/%s '
1484 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1485 % (node['type'], node['level'],
1486 self.encode(node['source']), line, backref_text))
1488 def depart_system_message(self, node):
1489 self.body.append('</div>\n')
1491 def visit_table(self, node):
1492 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1493 self.body.append(
1494 self.starttag(node, 'table', CLASS=classes, border="1"))
1496 def depart_table(self, node):
1497 self.body.append('</table>\n')
1499 def visit_target(self, node):
1500 if not ('refuri' in node or 'refid' in node
1501 or 'refname' in node):
1502 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1503 self.context.append('</span>')
1504 else:
1505 self.context.append('')
1507 def depart_target(self, node):
1508 self.body.append(self.context.pop())
1510 def visit_tbody(self, node):
1511 self.write_colspecs()
1512 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1513 self.body.append(self.starttag(node, 'tbody', valign='top'))
1515 def depart_tbody(self, node):
1516 self.body.append('</tbody>\n')
1518 def visit_term(self, node):
1519 self.body.append(self.starttag(node, 'dt', ''))
1521 def depart_term(self, node):
1523 Leave the end tag to `self.visit_definition()`, in case there's a
1524 classifier.
1526 pass
1528 def visit_tgroup(self, node):
1529 # Mozilla needs <colgroup>:
1530 self.body.append(self.starttag(node, 'colgroup'))
1531 # Appended by thead or tbody:
1532 self.context.append('</colgroup>\n')
1533 node.stubs = []
1535 def depart_tgroup(self, node):
1536 pass
1538 def visit_thead(self, node):
1539 self.write_colspecs()
1540 self.body.append(self.context.pop()) # '</colgroup>\n'
1541 # There may or may not be a <thead>; this is for <tbody> to use:
1542 self.context.append('')
1543 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1545 def depart_thead(self, node):
1546 self.body.append('</thead>\n')
1548 def visit_title(self, node):
1549 """Only 6 section levels are supported by HTML."""
1550 check_id = 0
1551 close_tag = '</p>\n'
1552 if isinstance(node.parent, nodes.topic):
1553 self.body.append(
1554 self.starttag(node, 'p', '', CLASS='topic-title first'))
1555 elif isinstance(node.parent, nodes.sidebar):
1556 self.body.append(
1557 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1558 elif isinstance(node.parent, nodes.Admonition):
1559 self.body.append(
1560 self.starttag(node, 'p', '', CLASS='admonition-title'))
1561 elif isinstance(node.parent, nodes.table):
1562 self.body.append(
1563 self.starttag(node, 'caption', ''))
1564 close_tag = '</caption>\n'
1565 elif isinstance(node.parent, nodes.document):
1566 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1567 close_tag = '</h1>\n'
1568 self.in_document_title = len(self.body)
1569 else:
1570 assert isinstance(node.parent, nodes.section)
1571 h_level = self.section_level + self.initial_header_level - 1
1572 atts = {}
1573 if (len(node.parent) >= 2 and
1574 isinstance(node.parent[1], nodes.subtitle)):
1575 atts['CLASS'] = 'with-subtitle'
1576 self.body.append(
1577 self.starttag(node, 'h%s' % h_level, '', **atts))
1578 atts = {}
1579 if node.hasattr('refid'):
1580 atts['class'] = 'toc-backref'
1581 atts['href'] = '#' + node['refid']
1582 if atts:
1583 self.body.append(self.starttag({}, 'a', '', **atts))
1584 close_tag = '</a></h%s>\n' % (h_level)
1585 else:
1586 close_tag = '</h%s>\n' % (h_level)
1587 self.context.append(close_tag)
1589 def depart_title(self, node):
1590 self.body.append(self.context.pop())
1591 if self.in_document_title:
1592 self.title = self.body[self.in_document_title:-1]
1593 self.in_document_title = 0
1594 self.body_pre_docinfo.extend(self.body)
1595 self.html_title.extend(self.body)
1596 del self.body[:]
1598 def visit_title_reference(self, node):
1599 self.body.append(self.starttag(node, 'cite', ''))
1601 def depart_title_reference(self, node):
1602 self.body.append('</cite>')
1604 def visit_topic(self, node):
1605 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1606 self.topic_classes = node['classes']
1608 def depart_topic(self, node):
1609 self.body.append('</div>\n')
1610 self.topic_classes = []
1612 def visit_transition(self, node):
1613 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1615 def depart_transition(self, node):
1616 pass
1618 def visit_version(self, node):
1619 self.visit_docinfo_item(node, 'version', meta=None)
1621 def depart_version(self, node):
1622 self.depart_docinfo_item()
1624 def unimplemented_visit(self, node):
1625 raise NotImplementedError('visiting unimplemented node type: %s'
1626 % node.__class__.__name__)
1629 class SimpleListChecker(nodes.GenericNodeVisitor):
1632 Raise `nodes.NodeFound` if non-simple list item is encountered.
1634 Here "simple" means a list item containing nothing other than a single
1635 paragraph, a simple list, or a paragraph followed by a simple list.
1638 def default_visit(self, node):
1639 raise nodes.NodeFound
1641 def visit_bullet_list(self, node):
1642 pass
1644 def visit_enumerated_list(self, node):
1645 pass
1647 def visit_list_item(self, node):
1648 children = []
1649 for child in node.children:
1650 if not isinstance(child, nodes.Invisible):
1651 children.append(child)
1652 if (children and isinstance(children[0], nodes.paragraph)
1653 and (isinstance(children[-1], nodes.bullet_list)
1654 or isinstance(children[-1], nodes.enumerated_list))):
1655 children.pop()
1656 if len(children) <= 1:
1657 return
1658 else:
1659 raise nodes.NodeFound
1661 def visit_paragraph(self, node):
1662 raise nodes.SkipNode
1664 def invisible_visit(self, node):
1665 """Invisible nodes should be ignored."""
1666 raise nodes.SkipNode
1668 visit_comment = invisible_visit
1669 visit_substitution_definition = invisible_visit
1670 visit_target = invisible_visit
1671 visit_pending = invisible_visit