New HTML writer generating `XHTML1.1`_ styled with CSS2.
[docutils.git] / docutils / docutils / writers / html4css1 / __init__.py
blob6cc10cab4184de8e71f7266f77533113e24ecd2c
1 # $Id$
2 # Author: David Goodger
3 # Maintainer: docutils-develop@lists.sourceforge.net
4 # Copyright: This module has been placed in the public domain.
6 """
7 Simple HyperText Markup Language document tree Writer.
9 The output conforms to the XHTML version 1.0 Transitional DTD
10 (*almost* strict). The output contains a minimum of formatting
11 information. The cascading style sheet "html4css1.css" is required
12 for proper viewing with a modern graphical browser.
13 """
15 __docformat__ = 'reStructuredText'
18 import sys
19 import os
20 import os.path
21 import time
22 import re
23 import urllib
24 try: # check for the Python Imaging Library
25 import PIL.Image
26 except ImportError:
27 try: # sometimes PIL modules are put in PYTHONPATH's root
28 import Image
29 class PIL(object): pass # dummy wrapper
30 PIL.Image = Image
31 except ImportError:
32 PIL = None
33 import docutils
34 from docutils import frontend, nodes, utils, writers, languages, io
35 from docutils.utils.error_reporting import SafeString
36 from docutils.transforms import writer_aux
37 from docutils.utils.math import unichar2tex, pick_math_environment, math2html
38 from docutils.utils.math.latex2mathml import parse_latex_math
40 class Writer(writers.Writer):
42 supported = ('html', 'html4css1', 'xhtml')
43 """Formats this writer supports."""
45 default_stylesheets = ['html4css1.css']
46 default_stylesheet_dirs = ['.', os.path.abspath(os.path.dirname(__file__))]
48 default_template = 'template.txt'
49 default_template_path = os.path.join(
50 os.path.dirname(os.path.abspath(__file__)), default_template)
52 settings_spec = (
53 'HTML-Specific Options',
54 None,
55 (('Specify the template file (UTF-8 encoded). Default is "%s".'
56 % default_template_path,
57 ['--template'],
58 {'default': default_template_path, 'metavar': '<file>'}),
59 ('Comma separated list of stylesheet URLs. '
60 'Overrides previous --stylesheet and --stylesheet-path settings.',
61 ['--stylesheet'],
62 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
63 'validator': frontend.validate_comma_separated_list}),
64 ('Comma separated list of stylesheet paths. '
65 'Relative paths are expanded if a matching file is found in '
66 'the --stylesheet-dirs. With --link-stylesheet, '
67 'the path is rewritten relative to the output HTML file. '
68 'Default: "%s"' % ','.join(default_stylesheets),
69 ['--stylesheet-path'],
70 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
71 'validator': frontend.validate_comma_separated_list,
72 'default': default_stylesheets}),
73 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
74 'files must be accessible during processing. This is the default.',
75 ['--embed-stylesheet'],
76 {'default': 1, 'action': 'store_true',
77 'validator': frontend.validate_boolean}),
78 ('Link to the stylesheet(s) in the output HTML file. '
79 'Default: embed stylesheets.',
80 ['--link-stylesheet'],
81 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
82 ('Comma-separated list of directories where stylesheets are found. '
83 'Used by --stylesheet-path when expanding relative path arguments. '
84 'Default: "%s"' % default_stylesheet_dirs,
85 ['--stylesheet-dirs'],
86 {'metavar': '<dir[,dir,...]>',
87 'validator': frontend.validate_comma_separated_list,
88 'default': default_stylesheet_dirs}),
89 ('Specify the initial header level. Default is 1 for "<h1>". '
90 'Does not affect document title & subtitle (see --no-doc-title).',
91 ['--initial-header-level'],
92 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
93 'metavar': '<level>'}),
94 ('Specify the maximum width (in characters) for one-column field '
95 'names. Longer field names will span an entire row of the table '
96 'used to render the field list. Default is 14 characters. '
97 'Use 0 for "no limit".',
98 ['--field-name-limit'],
99 {'default': 14, 'metavar': '<level>',
100 'validator': frontend.validate_nonnegative_int}),
101 ('Specify the maximum width (in characters) for options in option '
102 'lists. Longer options will span an entire row of the table used '
103 'to render the option list. Default is 14 characters. '
104 'Use 0 for "no limit".',
105 ['--option-limit'],
106 {'default': 14, 'metavar': '<level>',
107 'validator': frontend.validate_nonnegative_int}),
108 ('Format for footnote references: one of "superscript" or '
109 '"brackets". Default is "brackets".',
110 ['--footnote-references'],
111 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
112 'metavar': '<format>',
113 'overrides': 'trim_footnote_reference_space'}),
114 ('Format for block quote attributions: one of "dash" (em-dash '
115 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
116 ['--attribution'],
117 {'choices': ['dash', 'parentheses', 'parens', 'none'],
118 'default': 'dash', 'metavar': '<format>'}),
119 ('Remove extra vertical whitespace between items of "simple" bullet '
120 'lists and enumerated lists. Default: enabled.',
121 ['--compact-lists'],
122 {'default': 1, 'action': 'store_true',
123 'validator': frontend.validate_boolean}),
124 ('Disable compact simple bullet and enumerated lists.',
125 ['--no-compact-lists'],
126 {'dest': 'compact_lists', 'action': 'store_false'}),
127 ('Remove extra vertical whitespace between items of simple field '
128 'lists. Default: enabled.',
129 ['--compact-field-lists'],
130 {'default': 1, 'action': 'store_true',
131 'validator': frontend.validate_boolean}),
132 ('Disable compact simple field lists.',
133 ['--no-compact-field-lists'],
134 {'dest': 'compact_field_lists', 'action': 'store_false'}),
135 ('Added to standard table classes. '
136 'Defined styles: "borderless". Default: ""',
137 ['--table-style'],
138 {'default': ''}),
139 ('Math output format, one of "MathML", "HTML", "MathJax" '
140 'or "LaTeX". Default: "HTML math.css"',
141 ['--math-output'],
142 {'default': 'HTML math.css'}),
143 ('Omit the XML declaration. Use with caution.',
144 ['--no-xml-declaration'],
145 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
146 'validator': frontend.validate_boolean}),
147 ('Obfuscate email addresses to confuse harvesters while still '
148 'keeping email links usable with standards-compliant browsers.',
149 ['--cloak-email-addresses'],
150 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
152 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
154 config_section = 'html4css1 writer'
155 config_section_dependencies = ('writers',)
157 visitor_attributes = (
158 'head_prefix', 'head', 'stylesheet', 'body_prefix',
159 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
160 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
161 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
162 'html_body')
164 def get_transforms(self):
165 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
167 def __init__(self):
168 writers.Writer.__init__(self)
169 self.translator_class = HTMLTranslator
171 def translate(self):
172 self.visitor = visitor = self.translator_class(self.document)
173 self.document.walkabout(visitor)
174 for attr in self.visitor_attributes:
175 setattr(self, attr, getattr(visitor, attr))
176 self.output = self.apply_template()
178 def apply_template(self):
179 template_file = open(self.document.settings.template, 'rb')
180 template = unicode(template_file.read(), 'utf-8')
181 template_file.close()
182 subs = self.interpolation_dict()
183 return template % subs
185 def interpolation_dict(self):
186 subs = {}
187 settings = self.document.settings
188 for attr in self.visitor_attributes:
189 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
190 subs['encoding'] = settings.output_encoding
191 subs['version'] = docutils.__version__
192 return subs
194 def assemble_parts(self):
195 writers.Writer.assemble_parts(self)
196 for part in self.visitor_attributes:
197 self.parts[part] = ''.join(getattr(self, part))
200 class HTMLTranslator(nodes.NodeVisitor):
203 This HTML writer has been optimized to produce visually compact
204 lists (less vertical whitespace). HTML's mixed content models
205 allow list items to contain "<li><p>body elements</p></li>" or
206 "<li>just text</li>" or even "<li>text<p>and body
207 elements</p>combined</li>", each with different effects. It would
208 be best to stick with strict body elements in list items, but they
209 affect vertical spacing in browsers (although they really
210 shouldn't).
212 Here is an outline of the optimization:
214 - Check for and omit <p> tags in "simple" lists: list items
215 contain either a single paragraph, a nested simple list, or a
216 paragraph followed by a nested simple list. This means that
217 this list can be compact:
219 - Item 1.
220 - Item 2.
222 But this list cannot be compact:
224 - Item 1.
226 This second paragraph forces space between list items.
228 - Item 2.
230 - In non-list contexts, omit <p> tags on a paragraph if that
231 paragraph is the only child of its parent (footnotes & citations
232 are allowed a label first).
234 - Regardless of the above, in definitions, table cells, field bodies,
235 option descriptions, and list items, mark the first child with
236 'class="first"' and the last child with 'class="last"'. The stylesheet
237 sets the margins (top & bottom respectively) to 0 for these elements.
239 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
240 option) disables list whitespace optimization.
243 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
244 doctype = (
245 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
246 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
247 doctype_mathml = doctype
249 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
250 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
251 content_type = ('<meta http-equiv="Content-Type"'
252 ' content="text/html; charset=%s" />\n')
253 content_type_mathml = ('<meta http-equiv="Content-Type"'
254 ' content="application/xhtml+xml; charset=%s" />\n')
256 generator = ('<meta name="generator" content="Docutils %s: '
257 'http://docutils.sourceforge.net/" />\n')
259 # Template for the MathJax script in the header:
260 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
261 # The latest version of MathJax from the distributed server:
262 # avaliable to the public under the `MathJax CDN Terms of Service`__
263 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
264 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
265 'config=TeX-AMS-MML_HTMLorMML')
266 # may be overwritten by custom URL appended to "mathjax"
268 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
269 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
270 words_and_spaces = re.compile(r'\S+| +|\n')
271 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
272 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
274 def __init__(self, document):
275 nodes.NodeVisitor.__init__(self, document)
276 self.settings = settings = document.settings
277 lcode = settings.language_code
278 self.language = languages.get_language(lcode, document.reporter)
279 self.meta = [self.generator % docutils.__version__]
280 self.head_prefix = []
281 self.html_prolog = []
282 if settings.xml_declaration:
283 self.head_prefix.append(self.xml_declaration
284 % settings.output_encoding)
285 # encoding not interpolated:
286 self.html_prolog.append(self.xml_declaration)
287 self.head = self.meta[:]
288 self.stylesheet = [self.stylesheet_call(path)
289 for path in utils.get_stylesheet_list(settings)]
290 self.body_prefix = ['</head>\n<body>\n']
291 # document title, subtitle display
292 self.body_pre_docinfo = []
293 # author, date, etc.
294 self.docinfo = []
295 self.body = []
296 self.fragment = []
297 self.body_suffix = ['</body>\n</html>\n']
298 self.section_level = 0
299 self.initial_header_level = int(settings.initial_header_level)
301 self.math_output = settings.math_output.split()
302 self.math_output_options = self.math_output[1:]
303 self.math_output = self.math_output[0].lower()
305 # A heterogenous stack used in conjunction with the tree traversal.
306 # Make sure that the pops correspond to the pushes:
307 self.context = []
308 self.topic_classes = []
309 self.colspecs = []
310 self.compact_p = True
311 self.compact_simple = False
312 self.compact_field_list = False
313 self.in_docinfo = False
314 self.in_sidebar = False
315 self.title = []
316 self.subtitle = []
317 self.header = []
318 self.footer = []
319 self.html_head = [self.content_type] # charset not interpolated
320 self.html_title = []
321 self.html_subtitle = []
322 self.html_body = []
323 self.in_document_title = 0 # len(self.body) or 0
324 self.in_mailto = False
325 self.author_in_authors = False
326 self.math_header = []
328 def astext(self):
329 return ''.join(self.head_prefix + self.head
330 + self.stylesheet + self.body_prefix
331 + self.body_pre_docinfo + self.docinfo
332 + self.body + self.body_suffix)
334 def encode(self, text):
335 """Encode special characters in `text` & return."""
336 # @@@ A codec to do these and all other HTML entities would be nice.
337 text = unicode(text)
338 return text.translate({
339 ord('&'): u'&amp;',
340 ord('<'): u'&lt;',
341 ord('"'): u'&quot;',
342 ord('>'): u'&gt;',
343 ord('@'): u'&#64;', # may thwart some address harvesters
344 # TODO: convert non-breaking space only if needed?
345 0xa0: u'&nbsp;'}) # non-breaking space
347 def cloak_mailto(self, uri):
348 """Try to hide a mailto: URL from harvesters."""
349 # Encode "@" using a URL octet reference (see RFC 1738).
350 # Further cloaking with HTML entities will be done in the
351 # `attval` function.
352 return uri.replace('@', '%40')
354 def cloak_email(self, addr):
355 """Try to hide the link text of a email link from harversters."""
356 # Surround at-signs and periods with <span> tags. ("@" has
357 # already been encoded to "&#64;" by the `encode` method.)
358 addr = addr.replace('&#64;', '<span>&#64;</span>')
359 addr = addr.replace('.', '<span>&#46;</span>')
360 return addr
362 def attval(self, text,
363 whitespace=re.compile('[\n\r\t\v\f]')):
364 """Cleanse, HTML encode, and return attribute value text."""
365 encoded = self.encode(whitespace.sub(' ', text))
366 if self.in_mailto and self.settings.cloak_email_addresses:
367 # Cloak at-signs ("%40") and periods with HTML entities.
368 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
369 encoded = encoded.replace('.', '&#46;')
370 return encoded
372 def stylesheet_call(self, path):
373 """Return code to reference or embed stylesheet file `path`"""
374 if self.settings.embed_stylesheet:
375 try:
376 content = io.FileInput(source_path=path,
377 encoding='utf-8').read()
378 self.settings.record_dependencies.add(path)
379 except IOError, err:
380 msg = u"Cannot embed stylesheet '%s': %s." % (
381 path, SafeString(err.strerror))
382 self.document.reporter.error(msg)
383 return '<--- %s --->\n' % msg
384 return self.embedded_stylesheet % content
385 # else link to style file:
386 if self.settings.stylesheet_path:
387 # adapt path relative to output (cf. config.html#stylesheet-path)
388 path = utils.relative_path(self.settings._destination, path)
389 return self.stylesheet_link % self.encode(path)
391 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
393 Construct and return a start tag given a node (id & class attributes
394 are extracted), tag name, and optional attributes.
396 tagname = tagname.lower()
397 prefix = []
398 atts = {}
399 ids = []
400 for (name, value) in attributes.items():
401 atts[name.lower()] = value
402 classes = []
403 languages = []
404 # unify class arguments and move language specification
405 for cls in node.get('classes', []) + atts.pop('class', '').split() :
406 if cls.startswith('language-'):
407 languages.append(cls[9:])
408 elif cls.strip() and cls not in classes:
409 classes.append(cls)
410 if languages:
411 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
412 atts[self.lang_attribute] = languages[0]
413 if classes:
414 atts['class'] = ' '.join(classes)
415 assert 'id' not in atts
416 ids.extend(node.get('ids', []))
417 if 'ids' in atts:
418 ids.extend(atts['ids'])
419 del atts['ids']
420 if ids:
421 atts['id'] = ids[0]
422 for id in ids[1:]:
423 # Add empty "span" elements for additional IDs. Note
424 # that we cannot use empty "a" elements because there
425 # may be targets inside of references, but nested "a"
426 # elements aren't allowed in XHTML (even if they do
427 # not all have a "href" attribute).
428 if empty:
429 # Empty tag. Insert target right in front of element.
430 prefix.append('<span id="%s"></span>' % id)
431 else:
432 # Non-empty tag. Place the auxiliary <span> tag
433 # *inside* the element, as the first child.
434 suffix += '<span id="%s"></span>' % id
435 attlist = atts.items()
436 attlist.sort()
437 parts = [tagname]
438 for name, value in attlist:
439 # value=None was used for boolean attributes without
440 # value, but this isn't supported by XHTML.
441 assert value is not None
442 if isinstance(value, list):
443 values = [unicode(v) for v in value]
444 parts.append('%s="%s"' % (name.lower(),
445 self.attval(' '.join(values))))
446 else:
447 parts.append('%s="%s"' % (name.lower(),
448 self.attval(unicode(value))))
449 if empty:
450 infix = ' /'
451 else:
452 infix = ''
453 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
455 def emptytag(self, node, tagname, suffix='\n', **attributes):
456 """Construct and return an XML-compatible empty tag."""
457 return self.starttag(node, tagname, suffix, empty=True, **attributes)
459 def set_class_on_child(self, node, class_, index=0):
461 Set class `class_` on the visible child no. index of `node`.
462 Do nothing if node has fewer children than `index`.
464 children = [n for n in node if not isinstance(n, nodes.Invisible)]
465 try:
466 child = children[index]
467 except IndexError:
468 return
469 child['classes'].append(class_)
471 def set_first_last(self, node):
472 self.set_class_on_child(node, 'first', 0)
473 self.set_class_on_child(node, 'last', -1)
475 def visit_Text(self, node):
476 text = node.astext()
477 encoded = self.encode(text)
478 if self.in_mailto and self.settings.cloak_email_addresses:
479 encoded = self.cloak_email(encoded)
480 self.body.append(encoded)
482 def depart_Text(self, node):
483 pass
485 def visit_abbreviation(self, node):
486 # @@@ implementation incomplete ("title" attribute)
487 self.body.append(self.starttag(node, 'abbr', ''))
489 def depart_abbreviation(self, node):
490 self.body.append('</abbr>')
492 def visit_acronym(self, node):
493 # @@@ implementation incomplete ("title" attribute)
494 self.body.append(self.starttag(node, 'acronym', ''))
496 def depart_acronym(self, node):
497 self.body.append('</acronym>')
499 def visit_address(self, node):
500 self.visit_docinfo_item(node, 'address', meta=False)
501 self.body.append(self.starttag(node, 'pre', CLASS='address'))
503 def depart_address(self, node):
504 self.body.append('\n</pre>\n')
505 self.depart_docinfo_item()
507 def visit_admonition(self, node):
508 self.body.append(self.starttag(node, 'div'))
509 self.set_first_last(node)
511 def depart_admonition(self, node=None):
512 self.body.append('</div>\n')
514 attribution_formats = {'dash': ('&mdash;', ''),
515 'parentheses': ('(', ')'),
516 'parens': ('(', ')'),
517 'none': ('', '')}
519 def visit_attribution(self, node):
520 prefix, suffix = self.attribution_formats[self.settings.attribution]
521 self.context.append(suffix)
522 self.body.append(
523 self.starttag(node, 'p', prefix, CLASS='attribution'))
525 def depart_attribution(self, node):
526 self.body.append(self.context.pop() + '</p>\n')
528 def visit_author(self, node):
529 if isinstance(node.parent, nodes.authors):
530 if self.author_in_authors:
531 self.body.append('\n<br />')
532 else:
533 self.visit_docinfo_item(node, 'author')
535 def depart_author(self, node):
536 if isinstance(node.parent, nodes.authors):
537 self.author_in_authors = True
538 else:
539 self.depart_docinfo_item()
541 def visit_authors(self, node):
542 self.visit_docinfo_item(node, 'authors')
543 self.author_in_authors = False # initialize
545 def depart_authors(self, node):
546 self.depart_docinfo_item()
548 def visit_block_quote(self, node):
549 self.body.append(self.starttag(node, 'blockquote'))
551 def depart_block_quote(self, node):
552 self.body.append('</blockquote>\n')
554 def check_simple_list(self, node):
555 """Check for a simple list that can be rendered compactly."""
556 visitor = SimpleListChecker(self.document)
557 try:
558 node.walk(visitor)
559 except nodes.NodeFound:
560 return None
561 else:
562 return 1
564 def is_compactable(self, node):
565 return ('compact' in node['classes']
566 or (self.settings.compact_lists
567 and 'open' not in node['classes']
568 and (self.compact_simple
569 or self.topic_classes == ['contents']
570 or self.check_simple_list(node))))
572 def visit_bullet_list(self, node):
573 atts = {}
574 old_compact_simple = self.compact_simple
575 self.context.append((self.compact_simple, self.compact_p))
576 self.compact_p = None
577 self.compact_simple = self.is_compactable(node)
578 if self.compact_simple and not old_compact_simple:
579 atts['class'] = 'simple'
580 self.body.append(self.starttag(node, 'ul', **atts))
582 def depart_bullet_list(self, node):
583 self.compact_simple, self.compact_p = self.context.pop()
584 self.body.append('</ul>\n')
586 def visit_caption(self, node):
587 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
589 def depart_caption(self, node):
590 self.body.append('</p>\n')
592 def visit_citation(self, node):
593 self.body.append(self.starttag(node, 'table',
594 CLASS='docutils citation',
595 frame="void", rules="none"))
596 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
597 '<tbody valign="top">\n'
598 '<tr>')
599 self.footnote_backrefs(node)
601 def depart_citation(self, node):
602 self.body.append('</td></tr>\n'
603 '</tbody>\n</table>\n')
605 def visit_citation_reference(self, node):
606 href = '#'
607 if 'refid' in node:
608 href += node['refid']
609 elif 'refname' in node:
610 href += self.document.nameids[node['refname']]
611 # else: # TODO system message (or already in the transform)?
612 # 'Citation reference missing.'
613 self.body.append(self.starttag(
614 node, 'a', '[', CLASS='citation-reference', href=href))
616 def depart_citation_reference(self, node):
617 self.body.append(']</a>')
619 def visit_classifier(self, node):
620 self.body.append(' <span class="classifier-delimiter">:</span> ')
621 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
623 def depart_classifier(self, node):
624 self.body.append('</span>')
626 def visit_colspec(self, node):
627 self.colspecs.append(node)
628 # "stubs" list is an attribute of the tgroup element:
629 node.parent.stubs.append(node.attributes.get('stub'))
631 def depart_colspec(self, node):
632 pass
634 def write_colspecs(self):
635 width = 0
636 for node in self.colspecs:
637 width += node['colwidth']
638 for node in self.colspecs:
639 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
640 self.body.append(self.emptytag(node, 'col',
641 width='%i%%' % colwidth))
642 self.colspecs = []
644 def visit_comment(self, node,
645 sub=re.compile('-(?=-)').sub):
646 """Escape double-dashes in comment text."""
647 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
648 # Content already processed:
649 raise nodes.SkipNode
651 def visit_compound(self, node):
652 self.body.append(self.starttag(node, 'div', CLASS='compound'))
653 if len(node) > 1:
654 node[0]['classes'].append('compound-first')
655 node[-1]['classes'].append('compound-last')
656 for child in node[1:-1]:
657 child['classes'].append('compound-middle')
659 def depart_compound(self, node):
660 self.body.append('</div>\n')
662 def visit_container(self, node):
663 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
665 def depart_container(self, node):
666 self.body.append('</div>\n')
668 def visit_contact(self, node):
669 self.visit_docinfo_item(node, 'contact', meta=False)
671 def depart_contact(self, node):
672 self.depart_docinfo_item()
674 def visit_copyright(self, node):
675 self.visit_docinfo_item(node, 'copyright')
677 def depart_copyright(self, node):
678 self.depart_docinfo_item()
680 def visit_date(self, node):
681 self.visit_docinfo_item(node, 'date')
683 def depart_date(self, node):
684 self.depart_docinfo_item()
686 def visit_decoration(self, node):
687 pass
689 def depart_decoration(self, node):
690 pass
692 def visit_definition(self, node):
693 self.body.append('</dt>\n')
694 self.body.append(self.starttag(node, 'dd', ''))
695 self.set_first_last(node)
697 def depart_definition(self, node):
698 self.body.append('</dd>\n')
700 def visit_definition_list(self, node):
701 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
703 def depart_definition_list(self, node):
704 self.body.append('</dl>\n')
706 def visit_definition_list_item(self, node):
707 pass
709 def depart_definition_list_item(self, node):
710 pass
712 def visit_description(self, node):
713 self.body.append(self.starttag(node, 'td', ''))
714 self.set_first_last(node)
716 def depart_description(self, node):
717 self.body.append('</td>')
719 def visit_docinfo(self, node):
720 self.context.append(len(self.body))
721 self.body.append(self.starttag(node, 'table',
722 CLASS='docinfo',
723 frame="void", rules="none"))
724 self.body.append('<col class="docinfo-name" />\n'
725 '<col class="docinfo-content" />\n'
726 '<tbody valign="top">\n')
727 self.in_docinfo = True
729 def depart_docinfo(self, node):
730 self.body.append('</tbody>\n</table>\n')
731 self.in_docinfo = False
732 start = self.context.pop()
733 self.docinfo = self.body[start:]
734 self.body = []
736 def visit_docinfo_item(self, node, name, meta=True):
737 if meta:
738 meta_tag = '<meta name="%s" content="%s" />\n' \
739 % (name, self.attval(node.astext()))
740 self.add_meta(meta_tag)
741 self.body.append(self.starttag(node, 'tr', ''))
742 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
743 % self.language.labels[name])
744 if len(node):
745 if isinstance(node[0], nodes.Element):
746 node[0]['classes'].append('first')
747 if isinstance(node[-1], nodes.Element):
748 node[-1]['classes'].append('last')
750 def depart_docinfo_item(self):
751 self.body.append('</td></tr>\n')
753 def visit_doctest_block(self, node):
754 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
756 def depart_doctest_block(self, node):
757 self.body.append('\n</pre>\n')
759 def visit_document(self, node):
760 self.head.append('<title>%s</title>\n'
761 % self.encode(node.get('title', '')))
763 def depart_document(self, node):
764 self.head_prefix.extend([self.doctype,
765 self.head_prefix_template %
766 {'lang': self.settings.language_code}])
767 self.html_prolog.append(self.doctype)
768 self.meta.insert(0, self.content_type % self.settings.output_encoding)
769 self.head.insert(0, self.content_type % self.settings.output_encoding)
770 if self.math_header:
771 if self.math_output == 'mathjax':
772 self.head.extend(self.math_header)
773 else:
774 self.stylesheet.extend(self.math_header)
775 # skip content-type meta tag with interpolated charset value:
776 self.html_head.extend(self.head[1:])
777 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
778 self.body_suffix.insert(0, '</div>\n')
779 self.fragment.extend(self.body) # self.fragment is the "naked" body
780 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
781 + self.docinfo + self.body
782 + self.body_suffix[:-1])
783 assert not self.context, 'len(context) = %s' % len(self.context)
785 def visit_emphasis(self, node):
786 self.body.append(self.starttag(node, 'em', ''))
788 def depart_emphasis(self, node):
789 self.body.append('</em>')
791 def visit_entry(self, node):
792 atts = {'class': []}
793 if isinstance(node.parent.parent, nodes.thead):
794 atts['class'].append('head')
795 if node.parent.parent.parent.stubs[node.parent.column]:
796 # "stubs" list is an attribute of the tgroup element
797 atts['class'].append('stub')
798 if atts['class']:
799 tagname = 'th'
800 atts['class'] = ' '.join(atts['class'])
801 else:
802 tagname = 'td'
803 del atts['class']
804 node.parent.column += 1
805 if 'morerows' in node:
806 atts['rowspan'] = node['morerows'] + 1
807 if 'morecols' in node:
808 atts['colspan'] = node['morecols'] + 1
809 node.parent.column += node['morecols']
810 self.body.append(self.starttag(node, tagname, '', **atts))
811 self.context.append('</%s>\n' % tagname.lower())
812 if len(node) == 0: # empty cell
813 self.body.append('&nbsp;')
814 self.set_first_last(node)
816 def depart_entry(self, node):
817 self.body.append(self.context.pop())
819 def visit_enumerated_list(self, node):
821 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
822 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
823 usable.
825 atts = {}
826 if 'start' in node:
827 atts['start'] = node['start']
828 if 'enumtype' in node:
829 atts['class'] = node['enumtype']
830 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
831 # single "format" attribute? Use CSS2?
832 old_compact_simple = self.compact_simple
833 self.context.append((self.compact_simple, self.compact_p))
834 self.compact_p = None
835 self.compact_simple = self.is_compactable(node)
836 if self.compact_simple and not old_compact_simple:
837 atts['class'] = (atts.get('class', '') + ' simple').strip()
838 self.body.append(self.starttag(node, 'ol', **atts))
840 def depart_enumerated_list(self, node):
841 self.compact_simple, self.compact_p = self.context.pop()
842 self.body.append('</ol>\n')
844 def visit_field(self, node):
845 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
847 def depart_field(self, node):
848 self.body.append('</tr>\n')
850 def visit_field_body(self, node):
851 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
852 self.set_class_on_child(node, 'first', 0)
853 field = node.parent
854 if (self.compact_field_list or
855 isinstance(field.parent, nodes.docinfo) or
856 field.parent.index(field) == len(field.parent) - 1):
857 # If we are in a compact list, the docinfo, or if this is
858 # the last field of the field list, do not add vertical
859 # space after last element.
860 self.set_class_on_child(node, 'last', -1)
862 def depart_field_body(self, node):
863 self.body.append('</td>\n')
865 def visit_field_list(self, node):
866 self.context.append((self.compact_field_list, self.compact_p))
867 self.compact_p = None
868 if 'compact' in node['classes']:
869 self.compact_field_list = True
870 elif (self.settings.compact_field_lists
871 and 'open' not in node['classes']):
872 self.compact_field_list = True
873 if self.compact_field_list:
874 for field in node:
875 field_body = field[-1]
876 assert isinstance(field_body, nodes.field_body)
877 children = [n for n in field_body
878 if not isinstance(n, nodes.Invisible)]
879 if not (len(children) == 0 or
880 len(children) == 1 and
881 isinstance(children[0],
882 (nodes.paragraph, nodes.line_block))):
883 self.compact_field_list = False
884 break
885 self.body.append(self.starttag(node, 'table', frame='void',
886 rules='none',
887 CLASS='docutils field-list'))
888 self.body.append('<col class="field-name" />\n'
889 '<col class="field-body" />\n'
890 '<tbody valign="top">\n')
892 def depart_field_list(self, node):
893 self.body.append('</tbody>\n</table>\n')
894 self.compact_field_list, self.compact_p = self.context.pop()
896 def visit_field_name(self, node):
897 atts = {}
898 if self.in_docinfo:
899 atts['class'] = 'docinfo-name'
900 else:
901 atts['class'] = 'field-name'
902 if ( self.settings.field_name_limit
903 and len(node.astext()) > self.settings.field_name_limit):
904 atts['colspan'] = 2
905 self.context.append('</tr>\n'
906 + self.starttag(node.parent, 'tr', '',
907 CLASS='field')
908 + '<td>&nbsp;</td>')
909 else:
910 self.context.append('')
911 self.body.append(self.starttag(node, 'th', '', **atts))
913 def depart_field_name(self, node):
914 self.body.append(':</th>')
915 self.body.append(self.context.pop())
917 def visit_figure(self, node):
918 atts = {'class': 'figure'}
919 if node.get('width'):
920 atts['style'] = 'width: %s' % node['width']
921 if node.get('align'):
922 atts['class'] += " align-" + node['align']
923 self.body.append(self.starttag(node, 'div', **atts))
925 def depart_figure(self, node):
926 self.body.append('</div>\n')
928 def visit_footer(self, node):
929 self.context.append(len(self.body))
931 def depart_footer(self, node):
932 start = self.context.pop()
933 footer = [self.starttag(node, 'div', CLASS='footer'),
934 '<hr class="footer" />\n']
935 footer.extend(self.body[start:])
936 footer.append('\n</div>\n')
937 self.footer.extend(footer)
938 self.body_suffix[:0] = footer
939 del self.body[start:]
941 def visit_footnote(self, node):
942 self.body.append(self.starttag(node, 'table',
943 CLASS='docutils footnote',
944 frame="void", rules="none"))
945 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
946 '<tbody valign="top">\n'
947 '<tr>')
948 self.footnote_backrefs(node)
950 def footnote_backrefs(self, node):
951 backlinks = []
952 backrefs = node['backrefs']
953 if self.settings.footnote_backlinks and backrefs:
954 if len(backrefs) == 1:
955 self.context.append('')
956 self.context.append('</a>')
957 self.context.append('<a class="fn-backref" href="#%s">'
958 % backrefs[0])
959 else:
960 i = 1
961 for backref in backrefs:
962 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
963 % (backref, i))
964 i += 1
965 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
966 self.context += ['', '']
967 else:
968 self.context.append('')
969 self.context += ['', '']
970 # If the node does not only consist of a label.
971 if len(node) > 1:
972 # If there are preceding backlinks, we do not set class
973 # 'first', because we need to retain the top-margin.
974 if not backlinks:
975 node[1]['classes'].append('first')
976 node[-1]['classes'].append('last')
978 def depart_footnote(self, node):
979 self.body.append('</td></tr>\n'
980 '</tbody>\n</table>\n')
982 def visit_footnote_reference(self, node):
983 href = '#' + node['refid']
984 format = self.settings.footnote_references
985 if format == 'brackets':
986 suffix = '['
987 self.context.append(']')
988 else:
989 assert format == 'superscript'
990 suffix = '<sup>'
991 self.context.append('</sup>')
992 self.body.append(self.starttag(node, 'a', suffix,
993 CLASS='footnote-reference', href=href))
995 def depart_footnote_reference(self, node):
996 self.body.append(self.context.pop() + '</a>')
998 def visit_generated(self, node):
999 pass
1001 def depart_generated(self, node):
1002 pass
1004 def visit_header(self, node):
1005 self.context.append(len(self.body))
1007 def depart_header(self, node):
1008 start = self.context.pop()
1009 header = [self.starttag(node, 'div', CLASS='header')]
1010 header.extend(self.body[start:])
1011 header.append('\n<hr class="header"/>\n</div>\n')
1012 self.body_prefix.extend(header)
1013 self.header.extend(header)
1014 del self.body[start:]
1016 def visit_image(self, node):
1017 atts = {}
1018 uri = node['uri']
1019 # place SVG and SWF images in an <object> element
1020 types = {'.svg': 'image/svg+xml',
1021 '.swf': 'application/x-shockwave-flash'}
1022 ext = os.path.splitext(uri)[1].lower()
1023 if ext in ('.svg', '.swf'):
1024 atts['data'] = uri
1025 atts['type'] = types[ext]
1026 else:
1027 atts['src'] = uri
1028 atts['alt'] = node.get('alt', uri)
1029 # image size
1030 if 'width' in node:
1031 atts['width'] = node['width']
1032 if 'height' in node:
1033 atts['height'] = node['height']
1034 if 'scale' in node:
1035 if (PIL and not ('width' in node and 'height' in node)
1036 and self.settings.file_insertion_enabled):
1037 imagepath = urllib.url2pathname(uri)
1038 try:
1039 img = PIL.Image.open(
1040 imagepath.encode(sys.getfilesystemencoding()))
1041 except (IOError, UnicodeEncodeError):
1042 pass # TODO: warn?
1043 else:
1044 self.settings.record_dependencies.add(
1045 imagepath.replace('\\', '/'))
1046 if 'width' not in atts:
1047 atts['width'] = '%dpx' % img.size[0]
1048 if 'height' not in atts:
1049 atts['height'] = '%dpx' % img.size[1]
1050 del img
1051 for att_name in 'width', 'height':
1052 if att_name in atts:
1053 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1054 assert match
1055 atts[att_name] = '%s%s' % (
1056 float(match.group(1)) * (float(node['scale']) / 100),
1057 match.group(2))
1058 style = []
1059 for att_name in 'width', 'height':
1060 if att_name in atts:
1061 if re.match(r'^[0-9.]+$', atts[att_name]):
1062 # Interpret unitless values as pixels.
1063 atts[att_name] += 'px'
1064 style.append('%s: %s;' % (att_name, atts[att_name]))
1065 del atts[att_name]
1066 if style:
1067 atts['style'] = ' '.join(style)
1068 if (isinstance(node.parent, nodes.TextElement) or
1069 (isinstance(node.parent, nodes.reference) and
1070 not isinstance(node.parent.parent, nodes.TextElement))):
1071 # Inline context or surrounded by <a>...</a>.
1072 suffix = ''
1073 else:
1074 suffix = '\n'
1075 if 'align' in node:
1076 atts['class'] = 'align-%s' % node['align']
1077 self.context.append('')
1078 if ext in ('.svg', '.swf'): # place in an object element,
1079 # do NOT use an empty tag: incorrect rendering in browsers
1080 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1081 node.get('alt', uri) + '</object>' + suffix)
1082 else:
1083 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1085 def depart_image(self, node):
1086 self.body.append(self.context.pop())
1088 def visit_inline(self, node):
1089 self.body.append(self.starttag(node, 'span', ''))
1091 def depart_inline(self, node):
1092 self.body.append('</span>')
1094 def visit_label(self, node):
1095 # Context added in footnote_backrefs.
1096 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1097 CLASS='label'))
1099 def depart_label(self, node):
1100 # Context added in footnote_backrefs.
1101 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1103 def visit_legend(self, node):
1104 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1106 def depart_legend(self, node):
1107 self.body.append('</div>\n')
1109 def visit_line(self, node):
1110 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1111 if not len(node):
1112 self.body.append('<br />')
1114 def depart_line(self, node):
1115 self.body.append('</div>\n')
1117 def visit_line_block(self, node):
1118 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1120 def depart_line_block(self, node):
1121 self.body.append('</div>\n')
1123 def visit_list_item(self, node):
1124 self.body.append(self.starttag(node, 'li', ''))
1125 if len(node):
1126 node[0]['classes'].append('first')
1128 def depart_list_item(self, node):
1129 self.body.append('</li>\n')
1131 def visit_literal(self, node):
1132 # special case: "code" role
1133 classes = node.get('classes', [])
1134 if 'code' in classes:
1135 # filter 'code' from class arguments
1136 node['classes'] = [cls for cls in classes if cls != 'code']
1137 self.body.append(self.starttag(node, 'code', ''))
1138 return
1139 self.body.append(
1140 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1141 text = node.astext()
1142 for token in self.words_and_spaces.findall(text):
1143 if token.strip():
1144 # Protect text like "--an-option" and the regular expression
1145 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1146 if self.sollbruchstelle.search(token):
1147 self.body.append('<span class="pre">%s</span>'
1148 % self.encode(token))
1149 else:
1150 self.body.append(self.encode(token))
1151 elif token in ('\n', ' '):
1152 # Allow breaks at whitespace:
1153 self.body.append(token)
1154 else:
1155 # Protect runs of multiple spaces; the last space can wrap:
1156 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1157 self.body.append('</tt>')
1158 # Content already processed:
1159 raise nodes.SkipNode
1161 def depart_literal(self, node):
1162 # skipped unless literal element is from "code" role:
1163 self.body.append('</code>')
1165 def visit_literal_block(self, node):
1166 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1168 def depart_literal_block(self, node):
1169 self.body.append('\n</pre>\n')
1171 def visit_math(self, node, math_env=''):
1172 # If the method is called from visit_math_block(), math_env != ''.
1174 # As there is no native HTML math support, we provide alternatives:
1175 # LaTeX and MathJax math_output modes simply wrap the content,
1176 # HTML and MathML math_output modes also convert the math_code.
1177 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1178 self.document.reporter.error(
1179 'math-output format "%s" not supported '
1180 'falling back to "latex"'% self.math_output)
1181 self.math_output = 'latex'
1183 # HTML container
1184 tags = {# math_output: (block, inline, class-arguments)
1185 'mathml': ('div', '', ''),
1186 'html': ('div', 'span', 'formula'),
1187 'mathjax': ('div', 'span', 'math'),
1188 'latex': ('pre', 'tt', 'math'),
1190 tag = tags[self.math_output][math_env == '']
1191 clsarg = tags[self.math_output][2]
1192 # LaTeX container
1193 wrappers = {# math_mode: (inline, block)
1194 'mathml': (None, None),
1195 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1196 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1197 'latex': (None, None),
1199 wrapper = wrappers[self.math_output][math_env != '']
1200 # get and wrap content
1201 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1202 if wrapper and math_env:
1203 math_code = wrapper % (math_env, math_code, math_env)
1204 elif wrapper:
1205 math_code = wrapper % math_code
1206 # settings and conversion
1207 if self.math_output in ('latex', 'mathjax'):
1208 math_code = self.encode(math_code)
1209 if self.math_output == 'mathjax' and not self.math_header:
1210 if self.math_output_options:
1211 self.mathjax_url = self.math_output_options[0]
1212 self.math_header = [self.mathjax_script % self.mathjax_url]
1213 elif self.math_output == 'html':
1214 if self.math_output_options and not self.math_header:
1215 self.math_header = [self.stylesheet_call(
1216 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1217 for s in self.math_output_options[0].split(',')]
1218 # TODO: fix display mode in matrices and fractions
1219 math2html.DocumentParameters.displaymode = (math_env != '')
1220 math_code = math2html.math2html(math_code)
1221 elif self.math_output == 'mathml':
1222 self.doctype = self.doctype_mathml
1223 self.content_type = self.content_type_mathml
1224 try:
1225 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1226 math_code = ''.join(mathml_tree.xml())
1227 except SyntaxError, err:
1228 err_node = self.document.reporter.error(err, base_node=node)
1229 self.visit_system_message(err_node)
1230 self.body.append(self.starttag(node, 'p'))
1231 self.body.append(u','.join(err.args))
1232 self.body.append('</p>\n')
1233 self.body.append(self.starttag(node, 'pre',
1234 CLASS='literal-block'))
1235 self.body.append(self.encode(math_code))
1236 self.body.append('\n</pre>\n')
1237 self.depart_system_message(err_node)
1238 raise nodes.SkipNode
1239 # append to document body
1240 if tag:
1241 self.body.append(self.starttag(node, tag,
1242 suffix='\n'*bool(math_env),
1243 CLASS=clsarg))
1244 self.body.append(math_code)
1245 if math_env: # block mode (equation, display)
1246 self.body.append('\n')
1247 if tag:
1248 self.body.append('</%s>' % tag)
1249 if math_env:
1250 self.body.append('\n')
1251 # Content already processed:
1252 raise nodes.SkipNode
1254 def depart_math(self, node):
1255 pass # never reached
1257 def visit_math_block(self, node):
1258 # print node.astext().encode('utf8')
1259 math_env = pick_math_environment(node.astext())
1260 self.visit_math(node, math_env=math_env)
1262 def depart_math_block(self, node):
1263 pass # never reached
1265 def visit_meta(self, node):
1266 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1267 self.add_meta(meta)
1269 def depart_meta(self, node):
1270 pass
1272 def add_meta(self, tag):
1273 self.meta.append(tag)
1274 self.head.append(tag)
1276 def visit_option(self, node):
1277 if self.context[-1]:
1278 self.body.append(', ')
1279 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1281 def depart_option(self, node):
1282 self.body.append('</span>')
1283 self.context[-1] += 1
1285 def visit_option_argument(self, node):
1286 self.body.append(node.get('delimiter', ' '))
1287 self.body.append(self.starttag(node, 'var', ''))
1289 def depart_option_argument(self, node):
1290 self.body.append('</var>')
1292 def visit_option_group(self, node):
1293 atts = {}
1294 if ( self.settings.option_limit
1295 and len(node.astext()) > self.settings.option_limit):
1296 atts['colspan'] = 2
1297 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
1298 else:
1299 self.context.append('')
1300 self.body.append(
1301 self.starttag(node, 'td', CLASS='option-group', **atts))
1302 self.body.append('<kbd>')
1303 self.context.append(0) # count number of options
1305 def depart_option_group(self, node):
1306 self.context.pop()
1307 self.body.append('</kbd></td>\n')
1308 self.body.append(self.context.pop())
1310 def visit_option_list(self, node):
1311 self.body.append(
1312 self.starttag(node, 'table', CLASS='docutils option-list',
1313 frame="void", rules="none"))
1314 self.body.append('<col class="option" />\n'
1315 '<col class="description" />\n'
1316 '<tbody valign="top">\n')
1318 def depart_option_list(self, node):
1319 self.body.append('</tbody>\n</table>\n')
1321 def visit_option_list_item(self, node):
1322 self.body.append(self.starttag(node, 'tr', ''))
1324 def depart_option_list_item(self, node):
1325 self.body.append('</tr>\n')
1327 def visit_option_string(self, node):
1328 pass
1330 def depart_option_string(self, node):
1331 pass
1333 def visit_organization(self, node):
1334 self.visit_docinfo_item(node, 'organization')
1336 def depart_organization(self, node):
1337 self.depart_docinfo_item()
1339 def should_be_compact_paragraph(self, node):
1341 Determine if the <p> tags around paragraph ``node`` can be omitted.
1343 if (isinstance(node.parent, nodes.document) or
1344 isinstance(node.parent, nodes.compound)):
1345 # Never compact paragraphs in document or compound.
1346 return False
1347 for key, value in node.attlist():
1348 if (node.is_not_default(key) and
1349 not (key == 'classes' and value in
1350 ([], ['first'], ['last'], ['first', 'last']))):
1351 # Attribute which needs to survive.
1352 return False
1353 first = isinstance(node.parent[0], nodes.label) # skip label
1354 for child in node.parent.children[first:]:
1355 # only first paragraph can be compact
1356 if isinstance(child, nodes.Invisible):
1357 continue
1358 if child is node:
1359 break
1360 return False
1361 parent_length = len([n for n in node.parent if not isinstance(
1362 n, (nodes.Invisible, nodes.label))])
1363 if ( self.compact_simple
1364 or self.compact_field_list
1365 or self.compact_p and parent_length == 1):
1366 return True
1367 return False
1369 def visit_paragraph(self, node):
1370 if self.should_be_compact_paragraph(node):
1371 self.context.append('')
1372 else:
1373 self.body.append(self.starttag(node, 'p', ''))
1374 self.context.append('</p>\n')
1376 def depart_paragraph(self, node):
1377 self.body.append(self.context.pop())
1379 def visit_problematic(self, node):
1380 if node.hasattr('refid'):
1381 self.body.append('<a href="#%s">' % node['refid'])
1382 self.context.append('</a>')
1383 else:
1384 self.context.append('')
1385 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1387 def depart_problematic(self, node):
1388 self.body.append('</span>')
1389 self.body.append(self.context.pop())
1391 def visit_raw(self, node):
1392 if 'html' in node.get('format', '').split():
1393 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1394 if node['classes']:
1395 self.body.append(self.starttag(node, t, suffix=''))
1396 self.body.append(node.astext())
1397 if node['classes']:
1398 self.body.append('</%s>' % t)
1399 # Keep non-HTML raw text out of output:
1400 raise nodes.SkipNode
1402 def visit_reference(self, node):
1403 atts = {'class': 'reference'}
1404 if 'refuri' in node:
1405 atts['href'] = node['refuri']
1406 if ( self.settings.cloak_email_addresses
1407 and atts['href'].startswith('mailto:')):
1408 atts['href'] = self.cloak_mailto(atts['href'])
1409 self.in_mailto = True
1410 atts['class'] += ' external'
1411 else:
1412 assert 'refid' in node, \
1413 'References must have "refuri" or "refid" attribute.'
1414 atts['href'] = '#' + node['refid']
1415 atts['class'] += ' internal'
1416 if not isinstance(node.parent, nodes.TextElement):
1417 assert len(node) == 1 and isinstance(node[0], nodes.image)
1418 atts['class'] += ' image-reference'
1419 self.body.append(self.starttag(node, 'a', '', **atts))
1421 def depart_reference(self, node):
1422 self.body.append('</a>')
1423 if not isinstance(node.parent, nodes.TextElement):
1424 self.body.append('\n')
1425 self.in_mailto = False
1427 def visit_revision(self, node):
1428 self.visit_docinfo_item(node, 'revision', meta=False)
1430 def depart_revision(self, node):
1431 self.depart_docinfo_item()
1433 def visit_row(self, node):
1434 self.body.append(self.starttag(node, 'tr', ''))
1435 node.column = 0
1437 def depart_row(self, node):
1438 self.body.append('</tr>\n')
1440 def visit_rubric(self, node):
1441 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1443 def depart_rubric(self, node):
1444 self.body.append('</p>\n')
1446 def visit_section(self, node):
1447 self.section_level += 1
1448 self.body.append(
1449 self.starttag(node, 'div', CLASS='section'))
1451 def depart_section(self, node):
1452 self.section_level -= 1
1453 self.body.append('</div>\n')
1455 def visit_sidebar(self, node):
1456 self.body.append(
1457 self.starttag(node, 'div', CLASS='sidebar'))
1458 self.set_first_last(node)
1459 self.in_sidebar = True
1461 def depart_sidebar(self, node):
1462 self.body.append('</div>\n')
1463 self.in_sidebar = False
1465 def visit_status(self, node):
1466 self.visit_docinfo_item(node, 'status', meta=False)
1468 def depart_status(self, node):
1469 self.depart_docinfo_item()
1471 def visit_strong(self, node):
1472 self.body.append(self.starttag(node, 'strong', ''))
1474 def depart_strong(self, node):
1475 self.body.append('</strong>')
1477 def visit_subscript(self, node):
1478 self.body.append(self.starttag(node, 'sub', ''))
1480 def depart_subscript(self, node):
1481 self.body.append('</sub>')
1483 def visit_substitution_definition(self, node):
1484 """Internal only."""
1485 raise nodes.SkipNode
1487 def visit_substitution_reference(self, node):
1488 self.unimplemented_visit(node)
1490 def visit_subtitle(self, node):
1491 if isinstance(node.parent, nodes.sidebar):
1492 self.body.append(self.starttag(node, 'p', '',
1493 CLASS='sidebar-subtitle'))
1494 self.context.append('</p>\n')
1495 elif isinstance(node.parent, nodes.document):
1496 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
1497 self.context.append('</h2>\n')
1498 self.in_document_title = len(self.body)
1499 elif isinstance(node.parent, nodes.section):
1500 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
1501 self.body.append(
1502 self.starttag(node, tag, '', CLASS='section-subtitle') +
1503 self.starttag({}, 'span', '', CLASS='section-subtitle'))
1504 self.context.append('</span></%s>\n' % tag)
1506 def depart_subtitle(self, node):
1507 self.body.append(self.context.pop())
1508 if self.in_document_title:
1509 self.subtitle = self.body[self.in_document_title:-1]
1510 self.in_document_title = 0
1511 self.body_pre_docinfo.extend(self.body)
1512 self.html_subtitle.extend(self.body)
1513 del self.body[:]
1515 def visit_superscript(self, node):
1516 self.body.append(self.starttag(node, 'sup', ''))
1518 def depart_superscript(self, node):
1519 self.body.append('</sup>')
1521 def visit_system_message(self, node):
1522 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1523 self.body.append('<p class="system-message-title">')
1524 backref_text = ''
1525 if len(node['backrefs']):
1526 backrefs = node['backrefs']
1527 if len(backrefs) == 1:
1528 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1529 % backrefs[0])
1530 else:
1531 i = 1
1532 backlinks = []
1533 for backref in backrefs:
1534 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1535 i += 1
1536 backref_text = ('; <em>backlinks: %s</em>'
1537 % ', '.join(backlinks))
1538 if node.hasattr('line'):
1539 line = ', line %s' % node['line']
1540 else:
1541 line = ''
1542 self.body.append('System Message: %s/%s '
1543 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1544 % (node['type'], node['level'],
1545 self.encode(node['source']), line, backref_text))
1547 def depart_system_message(self, node):
1548 self.body.append('</div>\n')
1550 def visit_table(self, node):
1551 self.context.append(self.compact_p)
1552 self.compact_p = True
1553 classes = ' '.join(['docutils', self.settings.table_style]).strip()
1554 self.body.append(
1555 self.starttag(node, 'table', CLASS=classes, border="1"))
1557 def depart_table(self, node):
1558 self.compact_p = self.context.pop()
1559 self.body.append('</table>\n')
1561 def visit_target(self, node):
1562 if not ('refuri' in node or 'refid' in node
1563 or 'refname' in node):
1564 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1565 self.context.append('</span>')
1566 else:
1567 self.context.append('')
1569 def depart_target(self, node):
1570 self.body.append(self.context.pop())
1572 def visit_tbody(self, node):
1573 self.write_colspecs()
1574 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
1575 self.body.append(self.starttag(node, 'tbody', valign='top'))
1577 def depart_tbody(self, node):
1578 self.body.append('</tbody>\n')
1580 def visit_term(self, node):
1581 self.body.append(self.starttag(node, 'dt', ''))
1583 def depart_term(self, node):
1585 Leave the end tag to `self.visit_definition()`, in case there's a
1586 classifier.
1588 pass
1590 def visit_tgroup(self, node):
1591 # Mozilla needs <colgroup>:
1592 self.body.append(self.starttag(node, 'colgroup'))
1593 # Appended by thead or tbody:
1594 self.context.append('</colgroup>\n')
1595 node.stubs = []
1597 def depart_tgroup(self, node):
1598 pass
1600 def visit_thead(self, node):
1601 self.write_colspecs()
1602 self.body.append(self.context.pop()) # '</colgroup>\n'
1603 # There may or may not be a <thead>; this is for <tbody> to use:
1604 self.context.append('')
1605 self.body.append(self.starttag(node, 'thead', valign='bottom'))
1607 def depart_thead(self, node):
1608 self.body.append('</thead>\n')
1610 def visit_title(self, node):
1611 """Only 6 section levels are supported by HTML."""
1612 check_id = 0 # TODO: is this a bool (False) or a counter?
1613 close_tag = '</p>\n'
1614 if isinstance(node.parent, nodes.topic):
1615 self.body.append(
1616 self.starttag(node, 'p', '', CLASS='topic-title first'))
1617 elif isinstance(node.parent, nodes.sidebar):
1618 self.body.append(
1619 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1620 elif isinstance(node.parent, nodes.Admonition):
1621 self.body.append(
1622 self.starttag(node, 'p', '', CLASS='admonition-title'))
1623 elif isinstance(node.parent, nodes.table):
1624 self.body.append(
1625 self.starttag(node, 'caption', ''))
1626 close_tag = '</caption>\n'
1627 elif isinstance(node.parent, nodes.document):
1628 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1629 close_tag = '</h1>\n'
1630 self.in_document_title = len(self.body)
1631 else:
1632 assert isinstance(node.parent, nodes.section)
1633 h_level = self.section_level + self.initial_header_level - 1
1634 atts = {}
1635 if (len(node.parent) >= 2 and
1636 isinstance(node.parent[1], nodes.subtitle)):
1637 atts['CLASS'] = 'with-subtitle'
1638 self.body.append(
1639 self.starttag(node, 'h%s' % h_level, '', **atts))
1640 atts = {}
1641 if node.hasattr('refid'):
1642 atts['class'] = 'toc-backref'
1643 atts['href'] = '#' + node['refid']
1644 if atts:
1645 self.body.append(self.starttag({}, 'a', '', **atts))
1646 close_tag = '</a></h%s>\n' % (h_level)
1647 else:
1648 close_tag = '</h%s>\n' % (h_level)
1649 self.context.append(close_tag)
1651 def depart_title(self, node):
1652 self.body.append(self.context.pop())
1653 if self.in_document_title:
1654 self.title = self.body[self.in_document_title:-1]
1655 self.in_document_title = 0
1656 self.body_pre_docinfo.extend(self.body)
1657 self.html_title.extend(self.body)
1658 del self.body[:]
1660 def visit_title_reference(self, node):
1661 self.body.append(self.starttag(node, 'cite', ''))
1663 def depart_title_reference(self, node):
1664 self.body.append('</cite>')
1666 def visit_topic(self, node):
1667 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1668 self.topic_classes = node['classes']
1670 def depart_topic(self, node):
1671 self.body.append('</div>\n')
1672 self.topic_classes = []
1674 def visit_transition(self, node):
1675 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1677 def depart_transition(self, node):
1678 pass
1680 def visit_version(self, node):
1681 self.visit_docinfo_item(node, 'version', meta=False)
1683 def depart_version(self, node):
1684 self.depart_docinfo_item()
1686 def unimplemented_visit(self, node):
1687 raise NotImplementedError('visiting unimplemented node type: %s'
1688 % node.__class__.__name__)
1691 class SimpleListChecker(nodes.GenericNodeVisitor):
1694 Raise `nodes.NodeFound` if non-simple list item is encountered.
1696 Here "simple" means a list item containing nothing other than a single
1697 paragraph, a simple list, or a paragraph followed by a simple list.
1700 def default_visit(self, node):
1701 raise nodes.NodeFound
1703 def visit_bullet_list(self, node):
1704 pass
1706 def visit_enumerated_list(self, node):
1707 pass
1709 def visit_list_item(self, node):
1710 children = []
1711 for child in node.children:
1712 if not isinstance(child, nodes.Invisible):
1713 children.append(child)
1714 if (children and isinstance(children[0], nodes.paragraph)
1715 and (isinstance(children[-1], nodes.bullet_list)
1716 or isinstance(children[-1], nodes.enumerated_list))):
1717 children.pop()
1718 if len(children) <= 1:
1719 return
1720 else:
1721 raise nodes.NodeFound
1723 def visit_paragraph(self, node):
1724 raise nodes.SkipNode
1726 def invisible_visit(self, node):
1727 """Invisible nodes should be ignored."""
1728 raise nodes.SkipNode
1730 visit_comment = invisible_visit
1731 visit_substitution_definition = invisible_visit
1732 visit_target = invisible_visit
1733 visit_pending = invisible_visit