Update html4css1 writer, math, and functional tests.
[docutils.git] / docutils / writers / html4css1 / __init__.py
blob81b9187393541b11032c25709f66bee90a3c862a
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.latex2mathml import parse_latex_math
30 # from docutils.math.math2html import math2html
32 class Writer(writers.Writer):
34 supported = ('html', 'html4css1', 'xhtml')
35 """Formats this writer supports."""
37 default_stylesheet = 'html4css1.css'
39 default_stylesheet_path = utils.relative_path(
40 os.path.join(os.getcwd(), 'dummy'),
41 os.path.join(os.path.dirname(__file__), default_stylesheet))
43 default_template = 'template.txt'
45 default_template_path = utils.relative_path(
46 os.path.join(os.getcwd(), 'dummy'),
47 os.path.join(os.path.dirname(__file__), default_template))
49 settings_spec = (
50 'HTML-Specific Options',
51 None,
52 (('Specify the template file (UTF-8 encoded). Default is "%s".'
53 % default_template_path,
54 ['--template'],
55 {'default': default_template_path, 'metavar': '<file>'}),
56 ('Specify comma separated list of stylesheet URLs. '
57 'Overrides previous --stylesheet and --stylesheet-path settings.',
58 ['--stylesheet'],
59 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
60 ('Specify comma separated list of stylesheet paths. '
61 'With --link-stylesheet, '
62 'the path is rewritten relative to the output HTML file. '
63 'Default: "%s"' % default_stylesheet_path,
64 ['--stylesheet-path'],
65 {'metavar': '<file>', 'overrides': 'stylesheet',
66 'default': default_stylesheet_path}),
67 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
68 'files must be accessible during processing. This is the default.',
69 ['--embed-stylesheet'],
70 {'default': 1, 'action': 'store_true',
71 'validator': frontend.validate_boolean}),
72 ('Link to the stylesheet(s) in the output HTML file. '
73 'Default: embed stylesheets.',
74 ['--link-stylesheet'],
75 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
76 ('Specify the initial header level. Default is 1 for "<h1>". '
77 'Does not affect document title & subtitle (see --no-doc-title).',
78 ['--initial-header-level'],
79 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
80 'metavar': '<level>'}),
81 ('Specify the maximum width (in characters) for one-column field '
82 'names. Longer field names will span an entire row of the table '
83 'used to render the field list. Default is 14 characters. '
84 'Use 0 for "no limit".',
85 ['--field-name-limit'],
86 {'default': 14, 'metavar': '<level>',
87 'validator': frontend.validate_nonnegative_int}),
88 ('Specify the maximum width (in characters) for options in option '
89 'lists. Longer options will span an entire row of the table used '
90 'to render the option list. Default is 14 characters. '
91 'Use 0 for "no limit".',
92 ['--option-limit'],
93 {'default': 14, 'metavar': '<level>',
94 'validator': frontend.validate_nonnegative_int}),
95 ('Format for footnote references: one of "superscript" or '
96 '"brackets". Default is "brackets".',
97 ['--footnote-references'],
98 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
99 'metavar': '<format>',
100 'overrides': 'trim_footnote_reference_space'}),
101 ('Format for block quote attributions: one of "dash" (em-dash '
102 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
103 ['--attribution'],
104 {'choices': ['dash', 'parentheses', 'parens', 'none'],
105 'default': 'dash', 'metavar': '<format>'}),
106 ('Remove extra vertical whitespace between items of "simple" bullet '
107 'lists and enumerated lists. Default: enabled.',
108 ['--compact-lists'],
109 {'default': 1, 'action': 'store_true',
110 'validator': frontend.validate_boolean}),
111 ('Disable compact simple bullet and enumerated lists.',
112 ['--no-compact-lists'],
113 {'dest': 'compact_lists', 'action': 'store_false'}),
114 ('Remove extra vertical whitespace between items of simple field '
115 'lists. Default: enabled.',
116 ['--compact-field-lists'],
117 {'default': 1, 'action': 'store_true',
118 'validator': frontend.validate_boolean}),
119 ('Disable compact simple field lists.',
120 ['--no-compact-field-lists'],
121 {'dest': 'compact_field_lists', 'action': 'store_false'}),
122 ('Added to standard table classes. '
123 'Defined styles: "borderless". Default: ""',
124 ['--table-style'],
125 {'default': ''}),
126 ('Omit the XML declaration. Use with caution.',
127 ['--no-xml-declaration'],
128 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
129 'validator': frontend.validate_boolean}),
130 ('Obfuscate email addresses to confuse harvesters while still '
131 'keeping email links usable with standards-compliant browsers.',
132 ['--cloak-email-addresses'],
133 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
135 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
137 relative_path_settings = ('stylesheet_path',)
139 config_section = 'html4css1 writer'
140 config_section_dependencies = ('writers',)
142 visitor_attributes = (
143 'head_prefix', 'head', 'stylesheet', 'body_prefix',
144 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
145 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
146 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
147 'html_body')
149 def get_transforms(self):
150 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
152 def __init__(self):
153 writers.Writer.__init__(self)
154 self.translator_class = HTMLTranslator
156 def translate(self):
157 self.visitor = visitor = self.translator_class(self.document)
158 self.document.walkabout(visitor)
159 for attr in self.visitor_attributes:
160 setattr(self, attr, getattr(visitor, attr))
161 self.output = self.apply_template()
163 def apply_template(self):
164 template_file = open(self.document.settings.template, 'rb')
165 template = unicode(template_file.read(), 'utf-8')
166 template_file.close()
167 subs = self.interpolation_dict()
168 return template % subs
170 def interpolation_dict(self):
171 subs = {}
172 settings = self.document.settings
173 for attr in self.visitor_attributes:
174 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
175 subs['encoding'] = settings.output_encoding
176 subs['version'] = docutils.__version__
177 return subs
179 def assemble_parts(self):
180 writers.Writer.assemble_parts(self)
181 for part in self.visitor_attributes:
182 self.parts[part] = ''.join(getattr(self, part))
185 class HTMLTranslator(nodes.NodeVisitor):
188 This HTML writer has been optimized to produce visually compact
189 lists (less vertical whitespace). HTML's mixed content models
190 allow list items to contain "<li><p>body elements</p></li>" or
191 "<li>just text</li>" or even "<li>text<p>and body
192 elements</p>combined</li>", each with different effects. It would
193 be best to stick with strict body elements in list items, but they
194 affect vertical spacing in browsers (although they really
195 shouldn't).
197 Here is an outline of the optimization:
199 - Check for and omit <p> tags in "simple" lists: list items
200 contain either a single paragraph, a nested simple list, or a
201 paragraph followed by a nested simple list. This means that
202 this list can be compact:
204 - Item 1.
205 - Item 2.
207 But this list cannot be compact:
209 - Item 1.
211 This second paragraph forces space between list items.
213 - Item 2.
215 - In non-list contexts, omit <p> tags on a paragraph if that
216 paragraph is the only child of its parent (footnotes & citations
217 are allowed a label first).
219 - Regardless of the above, in definitions, table cells, field bodies,
220 option descriptions, and list items, mark the first child with
221 'class="first"' and the last child with 'class="last"'. The stylesheet
222 sets the margins (top & bottom respectively) to 0 for these elements.
224 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
225 option) disables list whitespace optimization.
228 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
229 doctype = (
230 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
231 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
232 doctype_mathml = doctype
234 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
235 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
236 content_type = ('<meta http-equiv="Content-Type"'
237 ' content="text/html; charset=%s" />\n')
238 content_type_mathml = ('<meta http-equiv="Content-Type"'
239 ' content="application/xhtml+xml; charset=%s" />\n')
241 generator = ('<meta name="generator" content="Docutils %s: '
242 'http://docutils.sourceforge.net/" />\n')
243 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
244 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
245 words_and_spaces = re.compile(r'\S+| +|\n')
246 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
247 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
249 def __init__(self, document):
250 nodes.NodeVisitor.__init__(self, document)
251 self.settings = settings = document.settings
252 # TODO: make this a config value once the HTML math output is ready:
253 self.settings.math_output = "MathML"
254 lcode = settings.language_code
255 self.language = languages.get_language(lcode, document.reporter)
256 self.meta = [self.content_type % settings.output_encoding,
257 self.generator % docutils.__version__]
258 self.head_prefix = []
259 self.html_prolog = []
260 if settings.xml_declaration:
261 self.head_prefix.append(self.xml_declaration
262 % settings.output_encoding)
263 # encoding not interpolated:
264 self.html_prolog.append(self.xml_declaration)
265 self.head_prefix.extend([self.doctype,
266 self.head_prefix_template % {'lang': lcode}])
267 self.html_prolog.append(self.doctype)
268 self.head = self.meta[:]
269 # stylesheets
270 styles = utils.get_stylesheet_list(settings)
271 if settings.stylesheet_path and not(settings.embed_stylesheet):
272 styles = [utils.relative_path(settings._destination, sheet)
273 for sheet in styles]
274 if settings.embed_stylesheet:
275 settings.record_dependencies.add(*styles)
276 self.stylesheet = [self.embedded_stylesheet %
277 io.FileInput(source_path=sheet, encoding='utf-8').read()
278 for sheet in styles]
279 else: # link to stylesheets
280 self.stylesheet = [self.stylesheet_link % self.encode(stylesheet)
281 for stylesheet in styles]
282 self.body_prefix = ['</head>\n<body>\n']
283 # document title, subtitle display
284 self.body_pre_docinfo = []
285 # author, date, etc.
286 self.docinfo = []
287 self.body = []
288 self.fragment = []
289 self.body_suffix = ['</body>\n</html>\n']
290 self.section_level = 0
291 self.initial_header_level = int(settings.initial_header_level)
292 # A heterogenous stack used in conjunction with the tree traversal.
293 # Make sure that the pops correspond to the pushes:
294 self.context = []
295 self.topic_classes = []
296 self.colspecs = []
297 self.compact_p = 1
298 self.compact_simple = None
299 self.compact_field_list = None
300 self.in_docinfo = None
301 self.in_sidebar = None
302 self.title = []
303 self.subtitle = []
304 self.header = []
305 self.footer = []
306 self.html_head = [self.content_type] # charset not interpolated
307 self.html_title = []
308 self.html_subtitle = []
309 self.html_body = []
310 self.in_document_title = 0
311 self.in_mailto = 0
312 self.author_in_authors = None
313 self.has_mathml_dtd = False
315 def astext(self):
316 return ''.join(self.head_prefix + self.head
317 + self.stylesheet + self.body_prefix
318 + self.body_pre_docinfo + self.docinfo
319 + self.body + self.body_suffix)
321 def encode(self, text):
322 """Encode special characters in `text` & return."""
323 # @@@ A codec to do these and all other HTML entities would be nice.
324 text = unicode(text)
325 return text.translate({
326 ord('&'): u'&amp;',
327 ord('<'): u'&lt;',
328 ord('"'): u'&quot;',
329 ord('>'): u'&gt;',
330 ord('@'): u'&#64;', # may thwart some address harvesters
331 # TODO: convert non-breaking space only if needed?
332 0xa0: u'&nbsp;'}) # non-breaking space
334 def cloak_mailto(self, uri):
335 """Try to hide a mailto: URL from harvesters."""
336 # Encode "@" using a URL octet reference (see RFC 1738).
337 # Further cloaking with HTML entities will be done in the
338 # `attval` function.
339 return uri.replace('@', '%40')
341 def cloak_email(self, addr):
342 """Try to hide the link text of a email link from harversters."""
343 # Surround at-signs and periods with <span> tags. ("@" has
344 # already been encoded to "&#64;" by the `encode` method.)
345 addr = addr.replace('&#64;', '<span>&#64;</span>')
346 addr = addr.replace('.', '<span>&#46;</span>')
347 return addr
349 def attval(self, text,
350 whitespace=re.compile('[\n\r\t\v\f]')):
351 """Cleanse, HTML encode, and return attribute value text."""
352 encoded = self.encode(whitespace.sub(' ', text))
353 if self.in_mailto and self.settings.cloak_email_addresses:
354 # Cloak at-signs ("%40") and periods with HTML entities.
355 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
356 encoded = encoded.replace('.', '&#46;')
357 return encoded
359 def starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
361 Construct and return a start tag given a node (id & class attributes
362 are extracted), tag name, and optional attributes.
364 tagname = tagname.lower()
365 prefix = []
366 atts = {}
367 ids = []
368 for (name, value) in attributes.items():
369 atts[name.lower()] = value
370 classes = node.get('classes', [])
371 if 'class' in atts:
372 classes.append(atts.pop('class'))
373 # move language specification to 'lang' attribute
374 languages = [cls for cls in classes
375 if cls.startswith('language-')]
376 if languages:
377 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
378 atts[self.lang_attribute] = languages[0][9:]
379 classes.pop(classes.index(languages[0]))
380 classes = ' '.join(classes).strip()
381 if classes:
382 atts['class'] = classes
383 assert 'id' not in atts
384 ids.extend(node.get('ids', []))
385 if 'ids' in atts:
386 ids.extend(atts['ids'])
387 del atts['ids']
388 if ids:
389 atts['id'] = ids[0]
390 for id in ids[1:]:
391 # Add empty "span" elements for additional IDs. Note
392 # that we cannot use empty "a" elements because there
393 # may be targets inside of references, but nested "a"
394 # elements aren't allowed in XHTML (even if they do
395 # not all have a "href" attribute).
396 if empty:
397 # Empty tag. Insert target right in front of element.
398 prefix.append('<span id="%s"></span>' % id)
399 else:
400 # Non-empty tag. Place the auxiliary <span> tag
401 # *inside* the element, as the first child.
402 suffix += '<span id="%s"></span>' % id
403 attlist = atts.items()
404 attlist.sort()
405 parts = [tagname]
406 for name, value in attlist:
407 # value=None was used for boolean attributes without
408 # value, but this isn't supported by XHTML.
409 assert value is not None
410 if isinstance(value, list):
411 values = [unicode(v) for v in value]
412 parts.append('%s="%s"' % (name.lower(),
413 self.attval(' '.join(values))))
414 else:
415 parts.append('%s="%s"' % (name.lower(),
416 self.attval(unicode(value))))
417 if empty:
418 infix = ' /'
419 else:
420 infix = ''
421 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
423 def emptytag(self, node, tagname, suffix='\n', **attributes):
424 """Construct and return an XML-compatible empty tag."""
425 return self.starttag(node, tagname, suffix, empty=1, **attributes)
427 def set_class_on_child(self, node, class_, index=0):
429 Set class `class_` on the visible child no. index of `node`.
430 Do nothing if node has fewer children than `index`.
432 children = [n for n in node if not isinstance(n, nodes.Invisible)]
433 try:
434 child = children[index]
435 except IndexError:
436 return
437 child['classes'].append(class_)
439 def set_first_last(self, node):
440 self.set_class_on_child(node, 'first', 0)
441 self.set_class_on_child(node, 'last', -1)
443 def visit_Text(self, node):
444 text = node.astext()
445 encoded = self.encode(text)
446 if self.in_mailto and self.settings.cloak_email_addresses:
447 encoded = self.cloak_email(encoded)
448 self.body.append(encoded)
450 def depart_Text(self, node):
451 pass
453 def visit_abbreviation(self, node):
454 # @@@ implementation incomplete ("title" attribute)
455 self.body.append(self.starttag(node, 'abbr', ''))
457 def depart_abbreviation(self, node):
458 self.body.append('</abbr>')
460 def visit_acronym(self, node):
461 # @@@ implementation incomplete ("title" attribute)
462 self.body.append(self.starttag(node, 'acronym', ''))
464 def depart_acronym(self, node):
465 self.body.append('</acronym>')
467 def visit_address(self, node):
468 self.visit_docinfo_item(node, 'address', meta=None)
469 self.body.append(self.starttag(node, 'pre', CLASS='address'))
471 def depart_address(self, node):
472 self.body.append('\n</pre>\n')
473 self.depart_docinfo_item()
475 def visit_admonition(self, node):
476 self.body.append(self.starttag(node, 'div'))
477 self.set_first_last(node)
479 def depart_admonition(self, node=None):
480 self.body.append('</div>\n')
482 attribution_formats = {'dash': ('&mdash;', ''),
483 'parentheses': ('(', ')'),
484 'parens': ('(', ')'),
485 'none': ('', '')}
487 def visit_attribution(self, node):
488 prefix, suffix = self.attribution_formats[self.settings.attribution]
489 self.context.append(suffix)
490 self.body.append(
491 self.starttag(node, 'p', prefix, CLASS='attribution'))
493 def depart_attribution(self, node):
494 self.body.append(self.context.pop() + '</p>\n')
496 def visit_author(self, node):
497 if isinstance(node.parent, nodes.authors):
498 if self.author_in_authors:
499 self.body.append('\n<br />')
500 else:
501 self.visit_docinfo_item(node, 'author')
503 def depart_author(self, node):
504 if isinstance(node.parent, nodes.authors):
505 self.author_in_authors += 1
506 else:
507 self.depart_docinfo_item()
509 def visit_authors(self, node):
510 self.visit_docinfo_item(node, 'authors')
511 self.author_in_authors = 0 # initialize counter
513 def depart_authors(self, node):
514 self.depart_docinfo_item()
515 self.author_in_authors = None
517 def visit_block_quote(self, node):
518 self.body.append(self.starttag(node, 'blockquote'))
520 def depart_block_quote(self, node):
521 self.body.append('</blockquote>\n')
523 def check_simple_list(self, node):
524 """Check for a simple list that can be rendered compactly."""
525 visitor = SimpleListChecker(self.document)
526 try:
527 node.walk(visitor)
528 except nodes.NodeFound:
529 return None
530 else:
531 return 1
533 def is_compactable(self, node):
534 return ('compact' in node['classes']
535 or (self.settings.compact_lists
536 and 'open' not in node['classes']
537 and (self.compact_simple
538 or self.topic_classes == ['contents']
539 or self.check_simple_list(node))))
541 def visit_bullet_list(self, node):
542 atts = {}
543 old_compact_simple = self.compact_simple
544 self.context.append((self.compact_simple, self.compact_p))
545 self.compact_p = None
546 self.compact_simple = self.is_compactable(node)
547 if self.compact_simple and not old_compact_simple:
548 atts['class'] = 'simple'
549 self.body.append(self.starttag(node, 'ul', **atts))
551 def depart_bullet_list(self, node):
552 self.compact_simple, self.compact_p = self.context.pop()
553 self.body.append('</ul>\n')
555 def visit_caption(self, node):
556 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
558 def depart_caption(self, node):
559 self.body.append('</p>\n')
561 def visit_citation(self, node):
562 self.body.append(self.starttag(node, 'table',
563 CLASS='docutils citation',
564 frame="void", rules="none"))
565 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
566 '<tbody valign="top">\n'
567 '<tr>')
568 self.footnote_backrefs(node)
570 def depart_citation(self, node):
571 self.body.append('</td></tr>\n'
572 '</tbody>\n</table>\n')
574 def visit_citation_reference(self, node):
575 href = '#' + node['refid']
576 self.body.append(self.starttag(
577 node, 'a', '[', CLASS='citation-reference', href=href))
579 def depart_citation_reference(self, node):
580 self.body.append(']</a>')
582 def visit_classifier(self, node):
583 self.body.append(' <span class="classifier-delimiter">:</span> ')
584 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
586 def depart_classifier(self, node):
587 self.body.append('</span>')
589 def visit_colspec(self, node):
590 self.colspecs.append(node)
591 # "stubs" list is an attribute of the tgroup element:
592 node.parent.stubs.append(node.attributes.get('stub'))
594 def depart_colspec(self, node):
595 pass
597 def write_colspecs(self):
598 width = 0
599 for node in self.colspecs:
600 width += node['colwidth']
601 for node in self.colspecs:
602 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
603 self.body.append(self.emptytag(node, 'col',
604 width='%i%%' % colwidth))
605 self.colspecs = []
607 def visit_comment(self, node,
608 sub=re.compile('-(?=-)').sub):
609 """Escape double-dashes in comment text."""
610 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
611 # Content already processed:
612 raise nodes.SkipNode
614 def visit_compound(self, node):
615 self.body.append(self.starttag(node, 'div', CLASS='compound'))
616 if len(node) > 1:
617 node[0]['classes'].append('compound-first')
618 node[-1]['classes'].append('compound-last')
619 for child in node[1:-1]:
620 child['classes'].append('compound-middle')
622 def depart_compound(self, node):
623 self.body.append('</div>\n')
625 def visit_container(self, node):
626 self.body.append(self.starttag(node, 'div', CLASS='container'))
628 def depart_container(self, node):
629 self.body.append('</div>\n')
631 def visit_contact(self, node):
632 self.visit_docinfo_item(node, 'contact', meta=None)
634 def depart_contact(self, node):
635 self.depart_docinfo_item()
637 def visit_copyright(self, node):
638 self.visit_docinfo_item(node, 'copyright')
640 def depart_copyright(self, node):
641 self.depart_docinfo_item()
643 def visit_date(self, node):
644 self.visit_docinfo_item(node, 'date')
646 def depart_date(self, node):
647 self.depart_docinfo_item()
649 def visit_decoration(self, node):
650 pass
652 def depart_decoration(self, node):
653 pass
655 def visit_definition(self, node):
656 self.body.append('</dt>\n')
657 self.body.append(self.starttag(node, 'dd', ''))
658 self.set_first_last(node)
660 def depart_definition(self, node):
661 self.body.append('</dd>\n')
663 def visit_definition_list(self, node):
664 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
666 def depart_definition_list(self, node):
667 self.body.append('</dl>\n')
669 def visit_definition_list_item(self, node):
670 pass
672 def depart_definition_list_item(self, node):
673 pass
675 def visit_description(self, node):
676 self.body.append(self.starttag(node, 'td', ''))
677 self.set_first_last(node)
679 def depart_description(self, node):
680 self.body.append('</td>')
682 def visit_docinfo(self, node):
683 self.context.append(len(self.body))
684 self.body.append(self.starttag(node, 'table',
685 CLASS='docinfo',
686 frame="void", rules="none"))
687 self.body.append('<col class="docinfo-name" />\n'
688 '<col class="docinfo-content" />\n'
689 '<tbody valign="top">\n')
690 self.in_docinfo = 1
692 def depart_docinfo(self, node):
693 self.body.append('</tbody>\n</table>\n')
694 self.in_docinfo = None
695 start = self.context.pop()
696 self.docinfo = self.body[start:]
697 self.body = []
699 def visit_docinfo_item(self, node, name, meta=1):
700 if meta:
701 meta_tag = '<meta name="%s" content="%s" />\n' \
702 % (name, self.attval(node.astext()))
703 self.add_meta(meta_tag)
704 self.body.append(self.starttag(node, 'tr', ''))
705 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
706 % self.language.labels[name])
707 if len(node):
708 if isinstance(node[0], nodes.Element):
709 node[0]['classes'].append('first')
710 if isinstance(node[-1], nodes.Element):
711 node[-1]['classes'].append('last')
713 def depart_docinfo_item(self):
714 self.body.append('</td></tr>\n')
716 def visit_doctest_block(self, node):
717 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
719 def depart_doctest_block(self, node):
720 self.body.append('\n</pre>\n')
722 def visit_document(self, node):
723 self.head.append('<title>%s</title>\n'
724 % self.encode(node.get('title', '')))
726 def depart_document(self, node):
727 self.fragment.extend(self.body)
728 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
729 self.body_suffix.insert(0, '</div>\n')
730 # skip content-type meta tag with interpolated charset value:
731 self.html_head.extend(self.head[1:])
732 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
733 + self.docinfo + self.body
734 + self.body_suffix[:-1])
735 assert not self.context, 'len(context) = %s' % len(self.context)
737 def visit_emphasis(self, node):
738 self.body.append(self.starttag(node, 'em', ''))
740 def depart_emphasis(self, node):
741 self.body.append('</em>')
743 def visit_entry(self, node):
744 atts = {'class': []}
745 if isinstance(node.parent.parent, nodes.thead):
746 atts['class'].append('head')
747 if node.parent.parent.parent.stubs[node.parent.column]:
748 # "stubs" list is an attribute of the tgroup element
749 atts['class'].append('stub')
750 if atts['class']:
751 tagname = 'th'
752 atts['class'] = ' '.join(atts['class'])
753 else:
754 tagname = 'td'
755 del atts['class']
756 node.parent.column += 1
757 if 'morerows' in node:
758 atts['rowspan'] = node['morerows'] + 1
759 if 'morecols' in node:
760 atts['colspan'] = node['morecols'] + 1
761 node.parent.column += node['morecols']
762 self.body.append(self.starttag(node, tagname, '', **atts))
763 self.context.append('</%s>\n' % tagname.lower())
764 if len(node) == 0: # empty cell
765 self.body.append('&nbsp;')
766 self.set_first_last(node)
768 def depart_entry(self, node):
769 self.body.append(self.context.pop())
771 def visit_enumerated_list(self, node):
773 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
774 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
775 usable.
777 atts = {}
778 if 'start' in node:
779 atts['start'] = node['start']
780 if 'enumtype' in node:
781 atts['class'] = node['enumtype']
782 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
783 # single "format" attribute? Use CSS2?
784 old_compact_simple = self.compact_simple
785 self.context.append((self.compact_simple, self.compact_p))
786 self.compact_p = None
787 self.compact_simple = self.is_compactable(node)
788 if self.compact_simple and not old_compact_simple:
789 atts['class'] = (atts.get('class', '') + ' simple').strip()
790 self.body.append(self.starttag(node, 'ol', **atts))
792 def depart_enumerated_list(self, node):
793 self.compact_simple, self.compact_p = self.context.pop()
794 self.body.append('</ol>\n')
796 def visit_field(self, node):
797 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
799 def depart_field(self, node):
800 self.body.append('</tr>\n')
802 def visit_field_body(self, node):
803 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
804 self.set_class_on_child(node, 'first', 0)
805 field = node.parent
806 if (self.compact_field_list or
807 isinstance(field.parent, nodes.docinfo) or
808 field.parent.index(field) == len(field.parent) - 1):
809 # If we are in a compact list, the docinfo, or if this is
810 # the last field of the field list, do not add vertical
811 # space after last element.
812 self.set_class_on_child(node, 'last', -1)
814 def depart_field_body(self, node):
815 self.body.append('</td>\n')
817 def visit_field_list(self, node):
818 self.context.append((self.compact_field_list, self.compact_p))
819 self.compact_p = None
820 if 'compact' in node['classes']:
821 self.compact_field_list = 1
822 elif (self.settings.compact_field_lists
823 and 'open' not in node['classes']):
824 self.compact_field_list = 1
825 if self.compact_field_list:
826 for field in node:
827 field_body = field[-1]
828 assert isinstance(field_body, nodes.field_body)
829 children = [n for n in field_body
830 if not isinstance(n, nodes.Invisible)]
831 if not (len(children) == 0 or
832 len(children) == 1 and
833 isinstance(children[0],
834 (nodes.paragraph, nodes.line_block))):
835 self.compact_field_list = 0
836 break
837 self.body.append(self.starttag(node, 'table', frame='void',
838 rules='none',
839 CLASS='docutils field-list'))
840 self.body.append('<col class="field-name" />\n'
841 '<col class="field-body" />\n'
842 '<tbody valign="top">\n')
844 def depart_field_list(self, node):
845 self.body.append('</tbody>\n</table>\n')
846 self.compact_field_list, self.compact_p = self.context.pop()
848 def visit_field_name(self, node):
849 atts = {}
850 if self.in_docinfo:
851 atts['class'] = 'docinfo-name'
852 else:
853 atts['class'] = 'field-name'
854 if ( self.settings.field_name_limit
855 and len(node.astext()) > self.settings.field_name_limit):
856 atts['colspan'] = 2
857 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
858 else:
859 self.context.append('')
860 self.body.append(self.starttag(node, 'th', '', **atts))
862 def depart_field_name(self, node):
863 self.body.append(':</th>')
864 self.body.append(self.context.pop())
866 def visit_figure(self, node):
867 atts = {'class': 'figure'}
868 if node.get('width'):
869 atts['style'] = 'width: %s' % node['width']
870 if node.get('align'):
871 atts['class'] += " align-" + node['align']
872 self.body.append(self.starttag(node, 'div', **atts))
874 def depart_figure(self, node):
875 self.body.append('</div>\n')
877 def visit_footer(self, node):
878 self.context.append(len(self.body))
880 def depart_footer(self, node):
881 start = self.context.pop()
882 footer = [self.starttag(node, 'div', CLASS='footer'),
883 '<hr class="footer" />\n']
884 footer.extend(self.body[start:])
885 footer.append('\n</div>\n')
886 self.footer.extend(footer)
887 self.body_suffix[:0] = footer
888 del self.body[start:]
890 def visit_footnote(self, node):
891 self.body.append(self.starttag(node, 'table',
892 CLASS='docutils footnote',
893 frame="void", rules="none"))
894 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
895 '<tbody valign="top">\n'
896 '<tr>')
897 self.footnote_backrefs(node)
899 def footnote_backrefs(self, node):
900 backlinks = []
901 backrefs = node['backrefs']
902 if self.settings.footnote_backlinks and backrefs:
903 if len(backrefs) == 1:
904 self.context.append('')
905 self.context.append('</a>')
906 self.context.append('<a class="fn-backref" href="#%s">'
907 % backrefs[0])
908 else:
909 i = 1
910 for backref in backrefs:
911 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
912 % (backref, i))
913 i += 1
914 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
915 self.context += ['', '']
916 else:
917 self.context.append('')
918 self.context += ['', '']
919 # If the node does not only consist of a label.
920 if len(node) > 1:
921 # If there are preceding backlinks, we do not set class
922 # 'first', because we need to retain the top-margin.
923 if not backlinks:
924 node[1]['classes'].append('first')
925 node[-1]['classes'].append('last')
927 def depart_footnote(self, node):
928 self.body.append('</td></tr>\n'
929 '</tbody>\n</table>\n')
931 def visit_footnote_reference(self, node):
932 href = '#' + node['refid']
933 format = self.settings.footnote_references
934 if format == 'brackets':
935 suffix = '['
936 self.context.append(']')
937 else:
938 assert format == 'superscript'
939 suffix = '<sup>'
940 self.context.append('</sup>')
941 self.body.append(self.starttag(node, 'a', suffix,
942 CLASS='footnote-reference', href=href))
944 def depart_footnote_reference(self, node):
945 self.body.append(self.context.pop() + '</a>')
947 def visit_generated(self, node):
948 pass
950 def depart_generated(self, node):
951 pass
953 def visit_header(self, node):
954 self.context.append(len(self.body))
956 def depart_header(self, node):
957 start = self.context.pop()
958 header = [self.starttag(node, 'div', CLASS='header')]
959 header.extend(self.body[start:])
960 header.append('\n<hr class="header"/>\n</div>\n')
961 self.body_prefix.extend(header)
962 self.header.extend(header)
963 del self.body[start:]
965 def visit_image(self, node):
966 atts = {}
967 uri = node['uri']
968 # place SVG and SWF images in an <object> element
969 types = {'.svg': 'image/svg+xml',
970 '.swf': 'application/x-shockwave-flash'}
971 ext = os.path.splitext(uri)[1].lower()
972 if ext in ('.svg', '.swf'):
973 atts['data'] = uri
974 atts['type'] = types[ext]
975 else:
976 atts['src'] = uri
977 atts['alt'] = node.get('alt', uri)
978 # image size
979 if 'width' in node:
980 atts['width'] = node['width']
981 if 'height' in node:
982 atts['height'] = node['height']
983 if 'scale' in node:
984 if Image and not ('width' in node and 'height' in node):
985 try:
986 im = Image.open(str(uri))
987 except (IOError, # Source image can't be found or opened
988 UnicodeError): # PIL doesn't like Unicode paths.
989 pass
990 else:
991 if 'width' not in atts:
992 atts['width'] = str(im.size[0])
993 if 'height' not in atts:
994 atts['height'] = str(im.size[1])
995 del im
996 for att_name in 'width', 'height':
997 if att_name in atts:
998 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
999 assert match
1000 atts[att_name] = '%s%s' % (
1001 float(match.group(1)) * (float(node['scale']) / 100),
1002 match.group(2))
1003 style = []
1004 for att_name in 'width', 'height':
1005 if att_name in atts:
1006 if re.match(r'^[0-9.]+$', atts[att_name]):
1007 # Interpret unitless values as pixels.
1008 atts[att_name] += 'px'
1009 style.append('%s: %s;' % (att_name, atts[att_name]))
1010 del atts[att_name]
1011 if style:
1012 atts['style'] = ' '.join(style)
1013 if (isinstance(node.parent, nodes.TextElement) or
1014 (isinstance(node.parent, nodes.reference) and
1015 not isinstance(node.parent.parent, nodes.TextElement))):
1016 # Inline context or surrounded by <a>...</a>.
1017 suffix = ''
1018 else:
1019 suffix = '\n'
1020 if 'align' in node:
1021 atts['class'] = 'align-%s' % node['align']
1022 self.context.append('')
1023 if ext in ('.svg', '.swf'): # place in an object element,
1024 # do NOT use an empty tag: incorrect rendering in browsers
1025 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1026 node.get('alt', uri) + '</object>' + suffix)
1027 else:
1028 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1030 def depart_image(self, node):
1031 self.body.append(self.context.pop())
1033 def visit_inline(self, node):
1034 self.body.append(self.starttag(node, 'span', ''))
1036 def depart_inline(self, node):
1037 self.body.append('</span>')
1039 def visit_label(self, node):
1040 # Context added in footnote_backrefs.
1041 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1042 CLASS='label'))
1044 def depart_label(self, node):
1045 # Context added in footnote_backrefs.
1046 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1048 def visit_legend(self, node):
1049 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1051 def depart_legend(self, node):
1052 self.body.append('</div>\n')
1054 def visit_line(self, node):
1055 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1056 if not len(node):
1057 self.body.append('<br />')
1059 def depart_line(self, node):
1060 self.body.append('</div>\n')
1062 def visit_line_block(self, node):
1063 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1065 def depart_line_block(self, node):
1066 self.body.append('</div>\n')
1068 def visit_list_item(self, node):
1069 self.body.append(self.starttag(node, 'li', ''))
1070 if len(node):
1071 node[0]['classes'].append('first')
1073 def depart_list_item(self, node):
1074 self.body.append('</li>\n')
1076 def visit_literal(self, node):
1077 """Process text to prevent tokens from wrapping."""
1078 self.body.append(
1079 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1080 text = node.astext()
1081 for token in self.words_and_spaces.findall(text):
1082 if token.strip():
1083 # Protect text like "--an-option" and the regular expression
1084 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1085 if self.sollbruchstelle.search(token):
1086 self.body.append('<span class="pre">%s</span>'
1087 % self.encode(token))
1088 else:
1089 self.body.append(self.encode(token))
1090 elif token in ('\n', ' '):
1091 # Allow breaks at whitespace:
1092 self.body.append(token)
1093 else:
1094 # Protect runs of multiple spaces; the last space can wrap:
1095 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1096 self.body.append('</tt>')
1097 # Content already processed:
1098 raise nodes.SkipNode
1100 def visit_literal_block(self, node):
1101 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1103 def depart_literal_block(self, node):
1104 self.body.append('\n</pre>\n')
1106 def visit_math(self, node, inline=True):
1107 math_in = node.astext()
1109 if self.settings.math_output == 'HTML':
1110 tag={True: 'span', False: 'div'}
1111 self.body.append(self.starttag(node, tag[inline], CLASS='formula'))
1112 self.body.append(math2html(math_in))
1113 self.body.append('</%s>\n' % tag[inline])
1115 elif self.settings.math_output == 'MathML':
1116 # MathML export:
1117 # update the doctype:
1118 if not self.has_mathml_dtd:
1119 if self.settings.xml_declaration:
1120 self.head_prefix[1] = self.doctype_mathml
1121 else:
1122 self.head_prefix[0] = self.doctype_mathml
1123 self.html_head[0] = self.content_type_mathml
1124 self.head[0] = self.content_type_mathml % self.settings.output_encoding
1125 self.has_mathml_dtd = True
1126 # Convert from LaTeX to MathML:
1127 try:
1128 mathml_tree = parse_latex_math(math_in, inline=inline)
1129 math_out = ''.join(mathml_tree.xml())
1130 except SyntaxError, err:
1131 self.document.reporter.error(err, base_node=node)
1132 math_out = unicode(err) # TODO: generate system message and link.
1133 self.body.append(math_out)
1134 # Content already processed:
1135 raise nodes.SkipNode
1137 def depart_math(self, node):
1138 pass
1140 def visit_math_block(self, node):
1141 self.visit_math(node, inline=False)
1143 def depart_math_block(self, node):
1144 pass
1145 # for testing:
1146 # def visit_math(self, node):
1147 # self.visit_literal(node)
1149 # def depart_math(self, node):
1150 # self.depart_literal(node)
1152 # def visit_math_block(self, node):
1153 # self.visit_literal_block(node)
1155 # def depart_math_block(self, node):
1156 # self.depart_literal_block(node)
1158 def visit_meta(self, node):
1159 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1160 self.add_meta(meta)
1162 def depart_meta(self, node):
1163 pass
1165 def add_meta(self, tag):
1166 self.meta.append(tag)
1167 self.head.append(tag)
1169 def visit_option(self, node):
1170 if self.context[-1]:
1171 self.body.append(', ')
1172 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1174 def depart_option(self, node):
1175 self.body.append('</span>')
1176 self.context[-1] += 1
1178 def visit_option_argument(self, node):
1179 self.body.append(node.get('delimiter', ' '))
1180 self.body.append(self.starttag(node, 'var', ''))
1182 def depart_option_argument(self, node):
1183 self.body.append('</var>')
1185 def visit_option_group(self, node):
1186 atts = {}
1187 if ( self.settings.option_limit
1188 and len(node.astext()) > self.settings.option_limit):
1189 atts['colspan'] = 2
1190 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1191 else:
1192 self.context.append('')
1193 self.body.append(
1194 self.starttag(node, 'td', CLASS='option-group', **atts))
1195 self.body.append('<kbd>')
1196 self.context.append(0) # count number of options
1198 def depart_option_group(self, node):
1199 self.context.pop()
1200 self.body.append('</kbd></td>\n')
1201 self.body.append(self.context.pop())
1203 def visit_option_list(self, node):
1204 self.body.append(
1205 self.starttag(node, 'table', CLASS='docutils option-list',
1206 frame="void", rules="none"))
1207 self.body.append('<col class="option" />\n'
1208 '<col class="description" />\n'
1209 '<tbody valign="top">\n')
1211 def depart_option_list(self, node):
1212 self.body.append('</tbody>\n</table>\n')
1214 def visit_option_list_item(self, node):
1215 self.body.append(self.starttag(node, 'tr', ''))
1217 def depart_option_list_item(self, node):
1218 self.body.append('</tr>\n')
1220 def visit_option_string(self, node):
1221 pass
1223 def depart_option_string(self, node):
1224 pass
1226 def visit_organization(self, node):
1227 self.visit_docinfo_item(node, 'organization')
1229 def depart_organization(self, node):
1230 self.depart_docinfo_item()
1232 def should_be_compact_paragraph(self, node):
1234 Determine if the <p> tags around paragraph ``node`` can be omitted.
1236 if (isinstance(node.parent, nodes.document) or
1237 isinstance(node.parent, nodes.compound)):
1238 # Never compact paragraphs in document or compound.
1239 return 0
1240 for key, value in node.attlist():
1241 if (node.is_not_default(key) and
1242 not (key == 'classes' and value in
1243 ([], ['first'], ['last'], ['first', 'last']))):
1244 # Attribute which needs to survive.
1245 return 0
1246 first = isinstance(node.parent[0], nodes.label) # skip label
1247 for child in node.parent.children[first:]:
1248 # only first paragraph can be compact
1249 if isinstance(child, nodes.Invisible):
1250 continue
1251 if child is node:
1252 break
1253 return 0
1254 parent_length = len([n for n in node.parent if not isinstance(
1255 n, (nodes.Invisible, nodes.label))])
1256 if ( self.compact_simple
1257 or self.compact_field_list
1258 or self.compact_p and parent_length == 1):
1259 return 1
1260 return 0
1262 def visit_paragraph(self, node):
1263 if self.should_be_compact_paragraph(node):
1264 self.context.append('')
1265 else:
1266 self.body.append(self.starttag(node, 'p', ''))
1267 self.context.append('</p>\n')
1269 def depart_paragraph(self, node):
1270 self.body.append(self.context.pop())
1272 def visit_problematic(self, node):
1273 if node.hasattr('refid'):
1274 self.body.append('<a href="#%s">' % node['refid'])
1275 self.context.append('</a>')
1276 else:
1277 self.context.append('')
1278 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1280 def depart_problematic(self, node):
1281 self.body.append('</span>')
1282 self.body.append(self.context.pop())
1284 def visit_raw(self, node):
1285 if 'html' in node.get('format', '').split():
1286 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1287 if node['classes']:
1288 self.body.append(self.starttag(node, t, suffix=''))
1289 self.body.append(node.astext())
1290 if node['classes']:
1291 self.body.append('</%s>' % t)
1292 # Keep non-HTML raw text out of output:
1293 raise nodes.SkipNode
1295 def visit_reference(self, node):
1296 atts = {'class': 'reference'}
1297 if 'refuri' in node:
1298 atts['href'] = node['refuri']
1299 if ( self.settings.cloak_email_addresses
1300 and atts['href'].startswith('mailto:')):
1301 atts['href'] = self.cloak_mailto(atts['href'])
1302 self.in_mailto = 1
1303 atts['class'] += ' external'
1304 else:
1305 assert 'refid' in node, \
1306 'References must have "refuri" or "refid" attribute.'
1307 atts['href'] = '#' + node['refid']
1308 atts['class'] += ' internal'
1309 if not isinstance(node.parent, nodes.TextElement):
1310 assert len(node) == 1 and isinstance(node[0], nodes.image)
1311 atts['class'] += ' image-reference'
1312 self.body.append(self.starttag(node, 'a', '', **atts))
1314 def depart_reference(self, node):
1315 self.body.append('</a>')
1316 if not isinstance(node.parent, nodes.TextElement):
1317 self.body.append('\n')
1318 self.in_mailto = 0
1320 def visit_revision(self, node):
1321 self.visit_docinfo_item(node, 'revision', meta=None)
1323 def depart_revision(self, node):
1324 self.depart_docinfo_item()
1326 def visit_row(self, node):
1327 self.body.append(self.starttag(node, 'tr', ''))
1328 node.column = 0
1330 def depart_row(self, node):
1331 self.body.append('</tr>\n')
1333 def visit_rubric(self, node):
1334 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1336 def depart_rubric(self, node):
1337 self.body.append('</p>\n')
1339 def visit_section(self, node):
1340 self.section_level += 1
1341 self.body.append(
1342 self.starttag(node, 'div', CLASS='section'))
1344 def depart_section(self, node):
1345 self.section_level -= 1
1346 self.body.append('</div>\n')
1348 def visit_sidebar(self, node):
1349 self.body.append(
1350 self.starttag(node, 'div', CLASS='sidebar'))
1351 self.set_first_last(node)
1352 self.in_sidebar = 1
1354 def depart_sidebar(self, node):
1355 self.body.append('</div>\n')
1356 self.in_sidebar = None
1358 def visit_status(self, node):
1359 self.visit_docinfo_item(node, 'status', meta=None)
1361 def depart_status(self, node):
1362 self.depart_docinfo_item()
1364 def visit_strong(self, node):
1365 self.body.append(self.starttag(node, 'strong', ''))
1367 def depart_strong(self, node):
1368 self.body.append('</strong>')
1370 def visit_subscript(self, node):
1371 self.body.append(self.starttag(node, 'sub', ''))
1373 def depart_subscript(self, node):
1374 self.body.append('</sub>')
1376 def visit_substitution_definition(self, node):
1377 """Internal only."""
1378 raise nodes.SkipNode
1380 def visit_substitution_reference(self, node):
1381 self.unimplemented_visit(node)
1383 def visit_subtitle(self, node):
1384 if isinstance(node.parent, nodes.sidebar):
1385 self.body.append(self.starttag(node, 'p', '',
1386 CLASS='sidebar-subtitle'))
1387 self.context.append('</p>\n')
1388 elif isinstance(node.parent, nodes.document):
1389 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1390 self.context.append('</h2>\n')
1391 self.in_document_title = len(self.body)
1392 elif isinstance(node.parent, nodes.section):
1393 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1394 self.body.append(
1395 self.starttag(node, tag, '', CLASS='section-subtitle') +
1396 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1397 self.context.append('</span></%s>\n' % tag)
1399 def depart_subtitle(self, node):
1400 self.body.append(self.context.pop())
1401 if self.in_document_title:
1402 self.subtitle = self.body[self.in_document_title:-1]
1403 self.in_document_title = 0
1404 self.body_pre_docinfo.extend(self.body)
1405 self.html_subtitle.extend(self.body)
1406 del self.body[:]
1408 def visit_superscript(self, node):
1409 self.body.append(self.starttag(node, 'sup', ''))
1411 def depart_superscript(self, node):
1412 self.body.append('</sup>')
1414 def visit_system_message(self, node):
1415 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1416 self.body.append('<p class="system-message-title">')
1417 backref_text = ''
1418 if len(node['backrefs']):
1419 backrefs = node['backrefs']
1420 if len(backrefs) == 1:
1421 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1422 % backrefs[0])
1423 else:
1424 i = 1
1425 backlinks = []
1426 for backref in backrefs:
1427 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1428 i += 1
1429 backref_text = ('; <em>backlinks: %s</em>'
1430 % ', '.join(backlinks))
1431 if node.hasattr('line'):
1432 line = ', line %s' % node['line']
1433 else:
1434 line = ''
1435 self.body.append('System Message: %s/%s '
1436 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1437 % (node['type'], node['level'],
1438 self.encode(node['source']), line, backref_text))
1440 def depart_system_message(self, node):
1441 self.body.append('</div>\n')
1443 def visit_table(self, node):
1444 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1445 self.body.append(
1446 self.starttag(node, 'table', CLASS=classes, border="1"))
1448 def depart_table(self, node):
1449 self.body.append('</table>\n')
1451 def visit_target(self, node):
1452 if not ('refuri' in node or 'refid' in node
1453 or 'refname' in node):
1454 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1455 self.context.append('</span>')
1456 else:
1457 self.context.append('')
1459 def depart_target(self, node):
1460 self.body.append(self.context.pop())
1462 def visit_tbody(self, node):
1463 self.write_colspecs()
1464 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1465 self.body.append(self.starttag(node, 'tbody', valign='top'))
1467 def depart_tbody(self, node):
1468 self.body.append('</tbody>\n')
1470 def visit_term(self, node):
1471 self.body.append(self.starttag(node, 'dt', ''))
1473 def depart_term(self, node):
1475 Leave the end tag to `self.visit_definition()`, in case there's a
1476 classifier.
1478 pass
1480 def visit_tgroup(self, node):
1481 # Mozilla needs <colgroup>:
1482 self.body.append(self.starttag(node, 'colgroup'))
1483 # Appended by thead or tbody:
1484 self.context.append('</colgroup>\n')
1485 node.stubs = []
1487 def depart_tgroup(self, node):
1488 pass
1490 def visit_thead(self, node):
1491 self.write_colspecs()
1492 self.body.append(self.context.pop()) # '</colgroup>\n'
1493 # There may or may not be a <thead>; this is for <tbody> to use:
1494 self.context.append('')
1495 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1497 def depart_thead(self, node):
1498 self.body.append('</thead>\n')
1500 def visit_title(self, node):
1501 """Only 6 section levels are supported by HTML."""
1502 check_id = 0
1503 close_tag = '</p>\n'
1504 if isinstance(node.parent, nodes.topic):
1505 self.body.append(
1506 self.starttag(node, 'p', '', CLASS='topic-title first'))
1507 elif isinstance(node.parent, nodes.sidebar):
1508 self.body.append(
1509 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1510 elif isinstance(node.parent, nodes.Admonition):
1511 self.body.append(
1512 self.starttag(node, 'p', '', CLASS='admonition-title'))
1513 elif isinstance(node.parent, nodes.table):
1514 self.body.append(
1515 self.starttag(node, 'caption', ''))
1516 close_tag = '</caption>\n'
1517 elif isinstance(node.parent, nodes.document):
1518 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1519 close_tag = '</h1>\n'
1520 self.in_document_title = len(self.body)
1521 else:
1522 assert isinstance(node.parent, nodes.section)
1523 h_level = self.section_level + self.initial_header_level - 1
1524 atts = {}
1525 if (len(node.parent) >= 2 and
1526 isinstance(node.parent[1], nodes.subtitle)):
1527 atts['CLASS'] = 'with-subtitle'
1528 self.body.append(
1529 self.starttag(node, 'h%s' % h_level, '', **atts))
1530 atts = {}
1531 if node.hasattr('refid'):
1532 atts['class'] = 'toc-backref'
1533 atts['href'] = '#' + node['refid']
1534 if atts:
1535 self.body.append(self.starttag({}, 'a', '', **atts))
1536 close_tag = '</a></h%s>\n' % (h_level)
1537 else:
1538 close_tag = '</h%s>\n' % (h_level)
1539 self.context.append(close_tag)
1541 def depart_title(self, node):
1542 self.body.append(self.context.pop())
1543 if self.in_document_title:
1544 self.title = self.body[self.in_document_title:-1]
1545 self.in_document_title = 0
1546 self.body_pre_docinfo.extend(self.body)
1547 self.html_title.extend(self.body)
1548 del self.body[:]
1550 def visit_title_reference(self, node):
1551 self.body.append(self.starttag(node, 'cite', ''))
1553 def depart_title_reference(self, node):
1554 self.body.append('</cite>')
1556 def visit_topic(self, node):
1557 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1558 self.topic_classes = node['classes']
1560 def depart_topic(self, node):
1561 self.body.append('</div>\n')
1562 self.topic_classes = []
1564 def visit_transition(self, node):
1565 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1567 def depart_transition(self, node):
1568 pass
1570 def visit_version(self, node):
1571 self.visit_docinfo_item(node, 'version', meta=None)
1573 def depart_version(self, node):
1574 self.depart_docinfo_item()
1576 def unimplemented_visit(self, node):
1577 raise NotImplementedError('visiting unimplemented node type: %s'
1578 % node.__class__.__name__)
1581 class SimpleListChecker(nodes.GenericNodeVisitor):
1584 Raise `nodes.NodeFound` if non-simple list item is encountered.
1586 Here "simple" means a list item containing nothing other than a single
1587 paragraph, a simple list, or a paragraph followed by a simple list.
1590 def default_visit(self, node):
1591 raise nodes.NodeFound
1593 def visit_bullet_list(self, node):
1594 pass
1596 def visit_enumerated_list(self, node):
1597 pass
1599 def visit_list_item(self, node):
1600 children = []
1601 for child in node.children:
1602 if not isinstance(child, nodes.Invisible):
1603 children.append(child)
1604 if (children and isinstance(children[0], nodes.paragraph)
1605 and (isinstance(children[-1], nodes.bullet_list)
1606 or isinstance(children[-1], nodes.enumerated_list))):
1607 children.pop()
1608 if len(children) <= 1:
1609 return
1610 else:
1611 raise nodes.NodeFound
1613 def visit_paragraph(self, node):
1614 raise nodes.SkipNode
1616 def invisible_visit(self, node):
1617 """Invisible nodes should be ignored."""
1618 raise nodes.SkipNode
1620 visit_comment = invisible_visit
1621 visit_substitution_definition = invisible_visit
1622 visit_target = invisible_visit
1623 visit_pending = invisible_visit