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