Bugfix: do not make "stylesheet-path"s absolute.
[docutils.git] / docutils / writers / html4css1 / __init__.py
blobbf07494283c2f9668ac07acd739691261599c8ee
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_stylesheet = 'html4css1.css'
46 default_stylesheet_dirs = ['.', utils.relative_path(
47 os.path.join(os.getcwd(), 'dummy'), os.path.dirname(__file__))]
49 default_template = 'template.txt'
51 default_template_path = utils.relative_path(
52 os.path.join(os.getcwd(), 'dummy'),
53 os.path.join(os.path.dirname(__file__), default_template))
55 settings_spec = (
56 'HTML-Specific Options',
57 None,
58 (('Specify the template file (UTF-8 encoded). Default is "%s".'
59 % default_template_path,
60 ['--template'],
61 {'default': default_template_path, 'metavar': '<file>'}),
62 ('Comma separated list of stylesheet URLs. '
63 'Overrides previous --stylesheet and --stylesheet-path settings.',
64 ['--stylesheet'],
65 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
66 'validator': frontend.validate_comma_separated_list}),
67 ('Comma separated list of stylesheet paths. '
68 'Relative paths are expanded if a matching file is found in '
69 'the --stylesheet-dirs. With --link-stylesheet, '
70 'the path is rewritten relative to the output HTML file. '
71 'Default: "%s"' % default_stylesheet,
72 ['--stylesheet-path'],
73 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
74 'validator': frontend.validate_comma_separated_list,
75 'default': [default_stylesheet]}),
76 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
77 'files must be accessible during processing. This is the default.',
78 ['--embed-stylesheet'],
79 {'default': 1, 'action': 'store_true',
80 'validator': frontend.validate_boolean}),
81 ('Link to the stylesheet(s) in the output HTML file. '
82 'Default: embed stylesheets.',
83 ['--link-stylesheet'],
84 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
85 ('Comma-separated list of directories where stylesheets are found. '
86 'Used by --stylesheet-path when expanding relative path arguments. '
87 'Default: "%s"' % default_stylesheet_dirs,
88 ['--stylesheet-dirs'],
89 {'metavar': '<dir[,dir,...]>',
90 'validator': frontend.validate_comma_separated_list,
91 'default': default_stylesheet_dirs}),
92 ('Specify the initial header level. Default is 1 for "<h1>". '
93 'Does not affect document title & subtitle (see --no-doc-title).',
94 ['--initial-header-level'],
95 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
96 'metavar': '<level>'}),
97 ('Specify the maximum width (in characters) for one-column field '
98 'names. Longer field names will span an entire row of the table '
99 'used to render the field list. Default is 14 characters. '
100 'Use 0 for "no limit".',
101 ['--field-name-limit'],
102 {'default': 14, 'metavar': '<level>',
103 'validator': frontend.validate_nonnegative_int}),
104 ('Specify the maximum width (in characters) for options in option '
105 'lists. Longer options will span an entire row of the table used '
106 'to render the option list. Default is 14 characters. '
107 'Use 0 for "no limit".',
108 ['--option-limit'],
109 {'default': 14, 'metavar': '<level>',
110 'validator': frontend.validate_nonnegative_int}),
111 ('Format for footnote references: one of "superscript" or '
112 '"brackets". Default is "brackets".',
113 ['--footnote-references'],
114 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
115 'metavar': '<format>',
116 'overrides': 'trim_footnote_reference_space'}),
117 ('Format for block quote attributions: one of "dash" (em-dash '
118 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
119 ['--attribution'],
120 {'choices': ['dash', 'parentheses', 'parens', 'none'],
121 'default': 'dash', 'metavar': '<format>'}),
122 ('Remove extra vertical whitespace between items of "simple" bullet '
123 'lists and enumerated lists. Default: enabled.',
124 ['--compact-lists'],
125 {'default': 1, 'action': 'store_true',
126 'validator': frontend.validate_boolean}),
127 ('Disable compact simple bullet and enumerated lists.',
128 ['--no-compact-lists'],
129 {'dest': 'compact_lists', 'action': 'store_false'}),
130 ('Remove extra vertical whitespace between items of simple field '
131 'lists. Default: enabled.',
132 ['--compact-field-lists'],
133 {'default': 1, 'action': 'store_true',
134 'validator': frontend.validate_boolean}),
135 ('Disable compact simple field lists.',
136 ['--no-compact-field-lists'],
137 {'dest': 'compact_field_lists', 'action': 'store_false'}),
138 ('Added to standard table classes. '
139 'Defined styles: "borderless". Default: ""',
140 ['--table-style'],
141 {'default': ''}),
142 ('Math output format, one of "MathML", "HTML", "MathJax" '
143 'or "LaTeX". Default: "HTML math.css"',
144 ['--math-output'],
145 {'default': 'HTML math.css'}),
146 ('Omit the XML declaration. Use with caution.',
147 ['--no-xml-declaration'],
148 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
149 'validator': frontend.validate_boolean}),
150 ('Obfuscate email addresses to confuse harvesters while still '
151 'keeping email links usable with standards-compliant browsers.',
152 ['--cloak-email-addresses'],
153 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
155 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
157 config_section = 'html4css1 writer'
158 config_section_dependencies = ('writers',)
160 visitor_attributes = (
161 'head_prefix', 'head', 'stylesheet', 'body_prefix',
162 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
163 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
164 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
165 'html_body')
167 def get_transforms(self):
168 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
170 def __init__(self):
171 writers.Writer.__init__(self)
172 self.translator_class = HTMLTranslator
174 def translate(self):
175 self.visitor = visitor = self.translator_class(self.document)
176 self.document.walkabout(visitor)
177 for attr in self.visitor_attributes:
178 setattr(self, attr, getattr(visitor, attr))
179 self.output = self.apply_template()
181 def apply_template(self):
182 template_file = open(self.document.settings.template, 'rb')
183 template = unicode(template_file.read(), 'utf-8')
184 template_file.close()
185 subs = self.interpolation_dict()
186 return template % subs
188 def interpolation_dict(self):
189 subs = {}
190 settings = self.document.settings
191 for attr in self.visitor_attributes:
192 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
193 subs['encoding'] = settings.output_encoding
194 subs['version'] = docutils.__version__
195 return subs
197 def assemble_parts(self):
198 writers.Writer.assemble_parts(self)
199 for part in self.visitor_attributes:
200 self.parts[part] = ''.join(getattr(self, part))
203 class HTMLTranslator(nodes.NodeVisitor):
206 This HTML writer has been optimized to produce visually compact
207 lists (less vertical whitespace). HTML's mixed content models
208 allow list items to contain "<li><p>body elements</p></li>" or
209 "<li>just text</li>" or even "<li>text<p>and body
210 elements</p>combined</li>", each with different effects. It would
211 be best to stick with strict body elements in list items, but they
212 affect vertical spacing in browsers (although they really
213 shouldn't).
215 Here is an outline of the optimization:
217 - Check for and omit <p> tags in "simple" lists: list items
218 contain either a single paragraph, a nested simple list, or a
219 paragraph followed by a nested simple list. This means that
220 this list can be compact:
222 - Item 1.
223 - Item 2.
225 But this list cannot be compact:
227 - Item 1.
229 This second paragraph forces space between list items.
231 - Item 2.
233 - In non-list contexts, omit <p> tags on a paragraph if that
234 paragraph is the only child of its parent (footnotes & citations
235 are allowed a label first).
237 - Regardless of the above, in definitions, table cells, field bodies,
238 option descriptions, and list items, mark the first child with
239 'class="first"' and the last child with 'class="last"'. The stylesheet
240 sets the margins (top & bottom respectively) to 0 for these elements.
242 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
243 option) disables list whitespace optimization.
246 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
247 doctype = (
248 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
249 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
250 doctype_mathml = doctype
252 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
253 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
254 content_type = ('<meta http-equiv="Content-Type"'
255 ' content="text/html; charset=%s" />\n')
256 content_type_mathml = ('<meta http-equiv="Content-Type"'
257 ' content="application/xhtml+xml; charset=%s" />\n')
259 generator = ('<meta name="generator" content="Docutils %s: '
260 'http://docutils.sourceforge.net/" />\n')
262 # Template for the MathJax script in the header:
263 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
264 # The latest version of MathJax from the distributed server:
265 # avaliable to the public under the `MathJax CDN Terms of Service`__
266 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
267 mathjax_url = ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
268 'config=TeX-AMS-MML_HTMLorMML')
269 # may be overwritten by custom URL appended to "mathjax"
271 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
272 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
273 words_and_spaces = re.compile(r'\S+| +|\n')
274 sollbruchstelle = re.compile(r'.+\W\W.+|[-?].+', re.U) # wrap point inside word
275 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
277 def __init__(self, document):
278 nodes.NodeVisitor.__init__(self, document)
279 self.settings = settings = document.settings
280 lcode = settings.language_code
281 self.language = languages.get_language(lcode, document.reporter)
282 self.meta = [self.generator % docutils.__version__]
283 self.head_prefix = []
284 self.html_prolog = []
285 if settings.xml_declaration:
286 self.head_prefix.append(self.xml_declaration
287 % settings.output_encoding)
288 # encoding not interpolated:
289 self.html_prolog.append(self.xml_declaration)
290 self.head = self.meta[:]
291 self.stylesheet = [self.stylesheet_call(path)
292 for path in utils.get_stylesheet_list(settings)]
293 self.body_prefix = ['</head>\n<body>\n']
294 # document title, subtitle display
295 self.body_pre_docinfo = []
296 # author, date, etc.
297 self.docinfo = []
298 self.body = []
299 self.fragment = []
300 self.body_suffix = ['</body>\n</html>\n']
301 self.section_level = 0
302 self.initial_header_level = int(settings.initial_header_level)
304 self.math_output = settings.math_output.split()
305 self.math_output_options = self.math_output[1:]
306 self.math_output = self.math_output[0].lower()
308 # A heterogenous stack used in conjunction with the tree traversal.
309 # Make sure that the pops correspond to the pushes:
310 self.context = []
311 self.topic_classes = []
312 self.colspecs = []
313 self.compact_p = True
314 self.compact_simple = False
315 self.compact_field_list = False
316 self.in_docinfo = False
317 self.in_sidebar = False
318 self.title = []
319 self.subtitle = []
320 self.header = []
321 self.footer = []
322 self.html_head = [self.content_type] # charset not interpolated
323 self.html_title = []
324 self.html_subtitle = []
325 self.html_body = []
326 self.in_document_title = 0 # len(self.body) or 0
327 self.in_mailto = False
328 self.author_in_authors = False
329 self.math_header = []
331 def astext(self):
332 return ''.join(self.head_prefix + self.head
333 + self.stylesheet + self.body_prefix
334 + self.body_pre_docinfo + self.docinfo
335 + self.body + self.body_suffix)
337 def encode(self, text):
338 """Encode special characters in `text` & return."""
339 # @@@ A codec to do these and all other HTML entities would be nice.
340 text = unicode(text)
341 return text.translate({
342 ord('&'): u'&amp;',
343 ord('<'): u'&lt;',
344 ord('"'): u'&quot;',
345 ord('>'): u'&gt;',
346 ord('@'): u'&#64;', # may thwart some address harvesters
347 # TODO: convert non-breaking space only if needed?
348 0xa0: u'&nbsp;'}) # non-breaking space
350 def cloak_mailto(self, uri):
351 """Try to hide a mailto: URL from harvesters."""
352 # Encode "@" using a URL octet reference (see RFC 1738).
353 # Further cloaking with HTML entities will be done in the
354 # `attval` function.
355 return uri.replace('@', '%40')
357 def cloak_email(self, addr):
358 """Try to hide the link text of a email link from harversters."""
359 # Surround at-signs and periods with <span> tags. ("@" has
360 # already been encoded to "&#64;" by the `encode` method.)
361 addr = addr.replace('&#64;', '<span>&#64;</span>')
362 addr = addr.replace('.', '<span>&#46;</span>')
363 return addr
365 def attval(self, text,
366 whitespace=re.compile('[\n\r\t\v\f]')):
367 """Cleanse, HTML encode, and return attribute value text."""
368 encoded = self.encode(whitespace.sub(' ', text))
369 if self.in_mailto and self.settings.cloak_email_addresses:
370 # Cloak at-signs ("%40") and periods with HTML entities.
371 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
372 encoded = encoded.replace('.', '&#46;')
373 return encoded
375 def stylesheet_call(self, path):
376 """Return code to reference or embed stylesheet file `path`"""
377 if self.settings.embed_stylesheet:
378 try:
379 content = io.FileInput(source_path=path,
380 encoding='utf-8').read()
381 self.settings.record_dependencies.add(path)
382 except IOError, err:
383 msg = u"Cannot embed stylesheet '%s': %s." % (
384 path, SafeString(err.strerror))
385 self.document.reporter.error(msg)
386 return '<--- %s --->\n' % msg
387 return self.embedded_stylesheet % content
388 # else link to style file:
389 if self.settings.stylesheet_path:
390 # adapt path relative to output (cf. config.html#stylesheet-path)
391 path = utils.relative_path(self.settings._destination, path)
392 return self.stylesheet_link % self.encode(path)
394 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
396 Construct and return a start tag given a node (id & class attributes
397 are extracted), tag name, and optional attributes.
399 tagname = tagname.lower()
400 prefix = []
401 atts = {}
402 ids = []
403 for (name, value) in attributes.items():
404 atts[name.lower()] = value
405 classes = node.get('classes', [])
406 if 'class' in atts:
407 classes.append(atts.pop('class'))
408 # move language specification to 'lang' attribute
409 languages = [cls for cls in classes
410 if cls.startswith('language-')]
411 if languages:
412 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
413 atts[self.lang_attribute] = languages[0][9:]
414 classes.pop(classes.index(languages[0]))
415 classes = ' '.join(classes).strip()
416 if classes:
417 atts['class'] = classes
418 assert 'id' not in atts
419 ids.extend(node.get('ids', []))
420 if 'ids' in atts:
421 ids.extend(atts['ids'])
422 del atts['ids']
423 if ids:
424 atts['id'] = ids[0]
425 for id in ids[1:]:
426 # Add empty "span" elements for additional IDs. Note
427 # that we cannot use empty "a" elements because there
428 # may be targets inside of references, but nested "a"
429 # elements aren't allowed in XHTML (even if they do
430 # not all have a "href" attribute).
431 if empty:
432 # Empty tag. Insert target right in front of element.
433 prefix.append('<span id="%s"></span>' % id)
434 else:
435 # Non-empty tag. Place the auxiliary <span> tag
436 # *inside* the element, as the first child.
437 suffix += '<span id="%s"></span>' % id
438 attlist = atts.items()
439 attlist.sort()
440 parts = [tagname]
441 for name, value in attlist:
442 # value=None was used for boolean attributes without
443 # value, but this isn't supported by XHTML.
444 assert value is not None
445 if isinstance(value, list):
446 values = [unicode(v) for v in value]
447 parts.append('%s="%s"' % (name.lower(),
448 self.attval(' '.join(values))))
449 else:
450 parts.append('%s="%s"' % (name.lower(),
451 self.attval(unicode(value))))
452 if empty:
453 infix = ' /'
454 else:
455 infix = ''
456 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
458 def emptytag(self, node, tagname, suffix='\n', **attributes):
459 """Construct and return an XML-compatible empty tag."""
460 return self.starttag(node, tagname, suffix, empty=True, **attributes)
462 def set_class_on_child(self, node, class_, index=0):
464 Set class `class_` on the visible child no. index of `node`.
465 Do nothing if node has fewer children than `index`.
467 children = [n for n in node if not isinstance(n, nodes.Invisible)]
468 try:
469 child = children[index]
470 except IndexError:
471 return
472 child['classes'].append(class_)
474 def set_first_last(self, node):
475 self.set_class_on_child(node, 'first', 0)
476 self.set_class_on_child(node, 'last', -1)
478 def visit_Text(self, node):
479 text = node.astext()
480 encoded = self.encode(text)
481 if self.in_mailto and self.settings.cloak_email_addresses:
482 encoded = self.cloak_email(encoded)
483 self.body.append(encoded)
485 def depart_Text(self, node):
486 pass
488 def visit_abbreviation(self, node):
489 # @@@ implementation incomplete ("title" attribute)
490 self.body.append(self.starttag(node, 'abbr', ''))
492 def depart_abbreviation(self, node):
493 self.body.append('</abbr>')
495 def visit_acronym(self, node):
496 # @@@ implementation incomplete ("title" attribute)
497 self.body.append(self.starttag(node, 'acronym', ''))
499 def depart_acronym(self, node):
500 self.body.append('</acronym>')
502 def visit_address(self, node):
503 self.visit_docinfo_item(node, 'address', meta=False)
504 self.body.append(self.starttag(node, 'pre', CLASS='address'))
506 def depart_address(self, node):
507 self.body.append('\n</pre>\n')
508 self.depart_docinfo_item()
510 def visit_admonition(self, node):
511 self.body.append(self.starttag(node, 'div'))
512 self.set_first_last(node)
514 def depart_admonition(self, node=None):
515 self.body.append('</div>\n')
517 attribution_formats = {'dash': ('&mdash;', ''),
518 'parentheses': ('(', ')'),
519 'parens': ('(', ')'),
520 'none': ('', '')}
522 def visit_attribution(self, node):
523 prefix, suffix = self.attribution_formats[self.settings.attribution]
524 self.context.append(suffix)
525 self.body.append(
526 self.starttag(node, 'p', prefix, CLASS='attribution'))
528 def depart_attribution(self, node):
529 self.body.append(self.context.pop() + '</p>\n')
531 def visit_author(self, node):
532 if isinstance(node.parent, nodes.authors):
533 if self.author_in_authors:
534 self.body.append('\n<br />')
535 else:
536 self.visit_docinfo_item(node, 'author')
538 def depart_author(self, node):
539 if isinstance(node.parent, nodes.authors):
540 self.author_in_authors = True
541 else:
542 self.depart_docinfo_item()
544 def visit_authors(self, node):
545 self.visit_docinfo_item(node, 'authors')
546 self.author_in_authors = False # initialize
548 def depart_authors(self, node):
549 self.depart_docinfo_item()
551 def visit_block_quote(self, node):
552 self.body.append(self.starttag(node, 'blockquote'))
554 def depart_block_quote(self, node):
555 self.body.append('</blockquote>\n')
557 def check_simple_list(self, node):
558 """Check for a simple list that can be rendered compactly."""
559 visitor = SimpleListChecker(self.document)
560 try:
561 node.walk(visitor)
562 except nodes.NodeFound:
563 return None
564 else:
565 return 1
567 def is_compactable(self, node):
568 return ('compact' in node['classes']
569 or (self.settings.compact_lists
570 and 'open' not in node['classes']
571 and (self.compact_simple
572 or self.topic_classes == ['contents']
573 or self.check_simple_list(node))))
575 def visit_bullet_list(self, node):
576 atts = {}
577 old_compact_simple = self.compact_simple
578 self.context.append((self.compact_simple, self.compact_p))
579 self.compact_p = None
580 self.compact_simple = self.is_compactable(node)
581 if self.compact_simple and not old_compact_simple:
582 atts['class'] = 'simple'
583 self.body.append(self.starttag(node, 'ul', **atts))
585 def depart_bullet_list(self, node):
586 self.compact_simple, self.compact_p = self.context.pop()
587 self.body.append('</ul>\n')
589 def visit_caption(self, node):
590 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
592 def depart_caption(self, node):
593 self.body.append('</p>\n')
595 def visit_citation(self, node):
596 self.body.append(self.starttag(node, 'table',
597 CLASS='docutils citation',
598 frame="void", rules="none"))
599 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
600 '<tbody valign="top">\n'
601 '<tr>')
602 self.footnote_backrefs(node)
604 def depart_citation(self, node):
605 self.body.append('</td></tr>\n'
606 '</tbody>\n</table>\n')
608 def visit_citation_reference(self, node):
609 href = '#'
610 if 'refid' in node:
611 href += node['refid']
612 elif 'refname' in node:
613 href += self.document.nameids[node['refname']]
614 # else: # TODO system message (or already in the transform)?
615 # 'Citation reference missing.'
616 self.body.append(self.starttag(
617 node, 'a', '[', CLASS='citation-reference', href=href))
619 def depart_citation_reference(self, node):
620 self.body.append(']</a>')
622 def visit_classifier(self, node):
623 self.body.append(' <span class="classifier-delimiter">:</span> ')
624 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
626 def depart_classifier(self, node):
627 self.body.append('</span>')
629 def visit_colspec(self, node):
630 self.colspecs.append(node)
631 # "stubs" list is an attribute of the tgroup element:
632 node.parent.stubs.append(node.attributes.get('stub'))
634 def depart_colspec(self, node):
635 pass
637 def write_colspecs(self):
638 width = 0
639 for node in self.colspecs:
640 width += node['colwidth']
641 for node in self.colspecs:
642 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
643 self.body.append(self.emptytag(node, 'col',
644 width='%i%%' % colwidth))
645 self.colspecs = []
647 def visit_comment(self, node,
648 sub=re.compile('-(?=-)').sub):
649 """Escape double-dashes in comment text."""
650 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
651 # Content already processed:
652 raise nodes.SkipNode
654 def visit_compound(self, node):
655 self.body.append(self.starttag(node, 'div', CLASS='compound'))
656 if len(node) > 1:
657 node[0]['classes'].append('compound-first')
658 node[-1]['classes'].append('compound-last')
659 for child in node[1:-1]:
660 child['classes'].append('compound-middle')
662 def depart_compound(self, node):
663 self.body.append('</div>\n')
665 def visit_container(self, node):
666 self.body.append(self.starttag(node, 'div', CLASS='container'))
668 def depart_container(self, node):
669 self.body.append('</div>\n')
671 def visit_contact(self, node):
672 self.visit_docinfo_item(node, 'contact', meta=False)
674 def depart_contact(self, node):
675 self.depart_docinfo_item()
677 def visit_copyright(self, node):
678 self.visit_docinfo_item(node, 'copyright')
680 def depart_copyright(self, node):
681 self.depart_docinfo_item()
683 def visit_date(self, node):
684 self.visit_docinfo_item(node, 'date')
686 def depart_date(self, node):
687 self.depart_docinfo_item()
689 def visit_decoration(self, node):
690 pass
692 def depart_decoration(self, node):
693 pass
695 def visit_definition(self, node):
696 self.body.append('</dt>\n')
697 self.body.append(self.starttag(node, 'dd', ''))
698 self.set_first_last(node)
700 def depart_definition(self, node):
701 self.body.append('</dd>\n')
703 def visit_definition_list(self, node):
704 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
706 def depart_definition_list(self, node):
707 self.body.append('</dl>\n')
709 def visit_definition_list_item(self, node):
710 pass
712 def depart_definition_list_item(self, node):
713 pass
715 def visit_description(self, node):
716 self.body.append(self.starttag(node, 'td', ''))
717 self.set_first_last(node)
719 def depart_description(self, node):
720 self.body.append('</td>')
722 def visit_docinfo(self, node):
723 self.context.append(len(self.body))
724 self.body.append(self.starttag(node, 'table',
725 CLASS='docinfo',
726 frame="void", rules="none"))
727 self.body.append('<col class="docinfo-name" />\n'
728 '<col class="docinfo-content" />\n'
729 '<tbody valign="top">\n')
730 self.in_docinfo = True
732 def depart_docinfo(self, node):
733 self.body.append('</tbody>\n</table>\n')
734 self.in_docinfo = False
735 start = self.context.pop()
736 self.docinfo = self.body[start:]
737 self.body = []
739 def visit_docinfo_item(self, node, name, meta=True):
740 if meta:
741 meta_tag = '<meta name="%s" content="%s" />\n' \
742 % (name, self.attval(node.astext()))
743 self.add_meta(meta_tag)
744 self.body.append(self.starttag(node, 'tr', ''))
745 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
746 % self.language.labels[name])
747 if len(node):
748 if isinstance(node[0], nodes.Element):
749 node[0]['classes'].append('first')
750 if isinstance(node[-1], nodes.Element):
751 node[-1]['classes'].append('last')
753 def depart_docinfo_item(self):
754 self.body.append('</td></tr>\n')
756 def visit_doctest_block(self, node):
757 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
759 def depart_doctest_block(self, node):
760 self.body.append('\n</pre>\n')
762 def visit_document(self, node):
763 self.head.append('<title>%s</title>\n'
764 % self.encode(node.get('title', '')))
766 def depart_document(self, node):
767 self.head_prefix.extend([self.doctype,
768 self.head_prefix_template %
769 {'lang': self.settings.language_code}])
770 self.html_prolog.append(self.doctype)
771 self.meta.insert(0, self.content_type % self.settings.output_encoding)
772 self.head.insert(0, self.content_type % self.settings.output_encoding)
773 if self.math_header:
774 if self.math_output == 'mathjax':
775 self.head.extend(self.math_header)
776 else:
777 self.stylesheet.extend(self.math_header)
778 # skip content-type meta tag with interpolated charset value:
779 self.html_head.extend(self.head[1:])
780 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
781 self.body_suffix.insert(0, '</div>\n')
782 self.fragment.extend(self.body) # self.fragment is the "naked" body
783 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
784 + self.docinfo + self.body
785 + self.body_suffix[:-1])
786 assert not self.context, 'len(context) = %s' % len(self.context)
788 def visit_emphasis(self, node):
789 self.body.append(self.starttag(node, 'em', ''))
791 def depart_emphasis(self, node):
792 self.body.append('</em>')
794 def visit_entry(self, node):
795 atts = {'class': []}
796 if isinstance(node.parent.parent, nodes.thead):
797 atts['class'].append('head')
798 if node.parent.parent.parent.stubs[node.parent.column]:
799 # "stubs" list is an attribute of the tgroup element
800 atts['class'].append('stub')
801 if atts['class']:
802 tagname = 'th'
803 atts['class'] = ' '.join(atts['class'])
804 else:
805 tagname = 'td'
806 del atts['class']
807 node.parent.column += 1
808 if 'morerows' in node:
809 atts['rowspan'] = node['morerows'] + 1
810 if 'morecols' in node:
811 atts['colspan'] = node['morecols'] + 1
812 node.parent.column += node['morecols']
813 self.body.append(self.starttag(node, tagname, '', **atts))
814 self.context.append('</%s>\n' % tagname.lower())
815 if len(node) == 0: # empty cell
816 self.body.append('&nbsp;')
817 self.set_first_last(node)
819 def depart_entry(self, node):
820 self.body.append(self.context.pop())
822 def visit_enumerated_list(self, node):
824 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
825 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
826 usable.
828 atts = {}
829 if 'start' in node:
830 atts['start'] = node['start']
831 if 'enumtype' in node:
832 atts['class'] = node['enumtype']
833 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
834 # single "format" attribute? Use CSS2?
835 old_compact_simple = self.compact_simple
836 self.context.append((self.compact_simple, self.compact_p))
837 self.compact_p = None
838 self.compact_simple = self.is_compactable(node)
839 if self.compact_simple and not old_compact_simple:
840 atts['class'] = (atts.get('class', '') + ' simple').strip()
841 self.body.append(self.starttag(node, 'ol', **atts))
843 def depart_enumerated_list(self, node):
844 self.compact_simple, self.compact_p = self.context.pop()
845 self.body.append('</ol>\n')
847 def visit_field(self, node):
848 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
850 def depart_field(self, node):
851 self.body.append('</tr>\n')
853 def visit_field_body(self, node):
854 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
855 self.set_class_on_child(node, 'first', 0)
856 field = node.parent
857 if (self.compact_field_list or
858 isinstance(field.parent, nodes.docinfo) or
859 field.parent.index(field) == len(field.parent) - 1):
860 # If we are in a compact list, the docinfo, or if this is
861 # the last field of the field list, do not add vertical
862 # space after last element.
863 self.set_class_on_child(node, 'last', -1)
865 def depart_field_body(self, node):
866 self.body.append('</td>\n')
868 def visit_field_list(self, node):
869 self.context.append((self.compact_field_list, self.compact_p))
870 self.compact_p = None
871 if 'compact' in node['classes']:
872 self.compact_field_list = True
873 elif (self.settings.compact_field_lists
874 and 'open' not in node['classes']):
875 self.compact_field_list = True
876 if self.compact_field_list:
877 for field in node:
878 field_body = field[-1]
879 assert isinstance(field_body, nodes.field_body)
880 children = [n for n in field_body
881 if not isinstance(n, nodes.Invisible)]
882 if not (len(children) == 0 or
883 len(children) == 1 and
884 isinstance(children[0],
885 (nodes.paragraph, nodes.line_block))):
886 self.compact_field_list = False
887 break
888 self.body.append(self.starttag(node, 'table', frame='void',
889 rules='none',
890 CLASS='docutils field-list'))
891 self.body.append('<col class="field-name" />\n'
892 '<col class="field-body" />\n'
893 '<tbody valign="top">\n')
895 def depart_field_list(self, node):
896 self.body.append('</tbody>\n</table>\n')
897 self.compact_field_list, self.compact_p = self.context.pop()
899 def visit_field_name(self, node):
900 atts = {}
901 if self.in_docinfo:
902 atts['class'] = 'docinfo-name'
903 else:
904 atts['class'] = 'field-name'
905 if ( self.settings.field_name_limit
906 and len(node.astext()) > self.settings.field_name_limit):
907 atts['colspan'] = 2
908 self.context.append('</tr>\n'
909 + self.starttag(node.parent, 'tr', '')
910 + '<td>&nbsp;</td>')
911 else:
912 self.context.append('')
913 self.body.append(self.starttag(node, 'th', '', **atts))
915 def depart_field_name(self, node):
916 self.body.append(':</th>')
917 self.body.append(self.context.pop())
919 def visit_figure(self, node):
920 atts = {'class': 'figure'}
921 if node.get('width'):
922 atts['style'] = 'width: %s' % node['width']
923 if node.get('align'):
924 atts['class'] += " align-" + node['align']
925 self.body.append(self.starttag(node, 'div', **atts))
927 def depart_figure(self, node):
928 self.body.append('</div>\n')
930 def visit_footer(self, node):
931 self.context.append(len(self.body))
933 def depart_footer(self, node):
934 start = self.context.pop()
935 footer = [self.starttag(node, 'div', CLASS='footer'),
936 '<hr class="footer" />\n']
937 footer.extend(self.body[start:])
938 footer.append('\n</div>\n')
939 self.footer.extend(footer)
940 self.body_suffix[:0] = footer
941 del self.body[start:]
943 def visit_footnote(self, node):
944 self.body.append(self.starttag(node, 'table',
945 CLASS='docutils footnote',
946 frame="void", rules="none"))
947 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
948 '<tbody valign="top">\n'
949 '<tr>')
950 self.footnote_backrefs(node)
952 def footnote_backrefs(self, node):
953 backlinks = []
954 backrefs = node['backrefs']
955 if self.settings.footnote_backlinks and backrefs:
956 if len(backrefs) == 1:
957 self.context.append('')
958 self.context.append('</a>')
959 self.context.append('<a class="fn-backref" href="#%s">'
960 % backrefs[0])
961 else:
962 i = 1
963 for backref in backrefs:
964 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
965 % (backref, i))
966 i += 1
967 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
968 self.context += ['', '']
969 else:
970 self.context.append('')
971 self.context += ['', '']
972 # If the node does not only consist of a label.
973 if len(node) > 1:
974 # If there are preceding backlinks, we do not set class
975 # 'first', because we need to retain the top-margin.
976 if not backlinks:
977 node[1]['classes'].append('first')
978 node[-1]['classes'].append('last')
980 def depart_footnote(self, node):
981 self.body.append('</td></tr>\n'
982 '</tbody>\n</table>\n')
984 def visit_footnote_reference(self, node):
985 href = '#' + node['refid']
986 format = self.settings.footnote_references
987 if format == 'brackets':
988 suffix = '['
989 self.context.append(']')
990 else:
991 assert format == 'superscript'
992 suffix = '<sup>'
993 self.context.append('</sup>')
994 self.body.append(self.starttag(node, 'a', suffix,
995 CLASS='footnote-reference', href=href))
997 def depart_footnote_reference(self, node):
998 self.body.append(self.context.pop() + '</a>')
1000 def visit_generated(self, node):
1001 pass
1003 def depart_generated(self, node):
1004 pass
1006 def visit_header(self, node):
1007 self.context.append(len(self.body))
1009 def depart_header(self, node):
1010 start = self.context.pop()
1011 header = [self.starttag(node, 'div', CLASS='header')]
1012 header.extend(self.body[start:])
1013 header.append('\n<hr class="header"/>\n</div>\n')
1014 self.body_prefix.extend(header)
1015 self.header.extend(header)
1016 del self.body[start:]
1018 def visit_image(self, node):
1019 atts = {}
1020 uri = node['uri']
1021 # place SVG and SWF images in an <object> element
1022 types = {'.svg': 'image/svg+xml',
1023 '.swf': 'application/x-shockwave-flash'}
1024 ext = os.path.splitext(uri)[1].lower()
1025 if ext in ('.svg', '.swf'):
1026 atts['data'] = uri
1027 atts['type'] = types[ext]
1028 else:
1029 atts['src'] = uri
1030 atts['alt'] = node.get('alt', uri)
1031 # image size
1032 if 'width' in node:
1033 atts['width'] = node['width']
1034 if 'height' in node:
1035 atts['height'] = node['height']
1036 if 'scale' in node:
1037 if (PIL and not ('width' in node and 'height' in node)
1038 and self.settings.file_insertion_enabled):
1039 imagepath = urllib.url2pathname(uri)
1040 try:
1041 img = PIL.Image.open(
1042 imagepath.encode(sys.getfilesystemencoding()))
1043 except (IOError, UnicodeEncodeError):
1044 pass # TODO: warn?
1045 else:
1046 self.settings.record_dependencies.add(
1047 imagepath.replace('\\', '/'))
1048 if 'width' not in atts:
1049 atts['width'] = str(img.size[0])
1050 if 'height' not in atts:
1051 atts['height'] = str(img.size[1])
1052 del img
1053 for att_name in 'width', 'height':
1054 if att_name in atts:
1055 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
1056 assert match
1057 atts[att_name] = '%s%s' % (
1058 float(match.group(1)) * (float(node['scale']) / 100),
1059 match.group(2))
1060 style = []
1061 for att_name in 'width', 'height':
1062 if att_name in atts:
1063 if re.match(r'^[0-9.]+$', atts[att_name]):
1064 # Interpret unitless values as pixels.
1065 atts[att_name] += 'px'
1066 style.append('%s: %s;' % (att_name, atts[att_name]))
1067 del atts[att_name]
1068 if style:
1069 atts['style'] = ' '.join(style)
1070 if (isinstance(node.parent, nodes.TextElement) or
1071 (isinstance(node.parent, nodes.reference) and
1072 not isinstance(node.parent.parent, nodes.TextElement))):
1073 # Inline context or surrounded by <a>...</a>.
1074 suffix = ''
1075 else:
1076 suffix = '\n'
1077 if 'align' in node:
1078 atts['class'] = 'align-%s' % node['align']
1079 self.context.append('')
1080 if ext in ('.svg', '.swf'): # place in an object element,
1081 # do NOT use an empty tag: incorrect rendering in browsers
1082 self.body.append(self.starttag(node, 'object', suffix, **atts) +
1083 node.get('alt', uri) + '</object>' + suffix)
1084 else:
1085 self.body.append(self.emptytag(node, 'img', suffix, **atts))
1087 def depart_image(self, node):
1088 self.body.append(self.context.pop())
1090 def visit_inline(self, node):
1091 self.body.append(self.starttag(node, 'span', ''))
1093 def depart_inline(self, node):
1094 self.body.append('</span>')
1096 def visit_label(self, node):
1097 # Context added in footnote_backrefs.
1098 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
1099 CLASS='label'))
1101 def depart_label(self, node):
1102 # Context added in footnote_backrefs.
1103 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
1105 def visit_legend(self, node):
1106 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1108 def depart_legend(self, node):
1109 self.body.append('</div>\n')
1111 def visit_line(self, node):
1112 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1113 if not len(node):
1114 self.body.append('<br />')
1116 def depart_line(self, node):
1117 self.body.append('</div>\n')
1119 def visit_line_block(self, node):
1120 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1122 def depart_line_block(self, node):
1123 self.body.append('</div>\n')
1125 def visit_list_item(self, node):
1126 self.body.append(self.starttag(node, 'li', ''))
1127 if len(node):
1128 node[0]['classes'].append('first')
1130 def depart_list_item(self, node):
1131 self.body.append('</li>\n')
1133 def visit_literal(self, node):
1134 # special case: "code" role
1135 classes = node.get('classes', [])
1136 if 'code' in classes:
1137 # filter 'code' from class arguments
1138 node['classes'] = [cls for cls in classes if cls != 'code']
1139 self.body.append(self.starttag(node, 'code', ''))
1140 return
1141 self.body.append(
1142 self.starttag(node, 'tt', '', CLASS='docutils literal'))
1143 text = node.astext()
1144 for token in self.words_and_spaces.findall(text):
1145 if token.strip():
1146 # Protect text like "--an-option" and the regular expression
1147 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1148 if self.sollbruchstelle.search(token):
1149 self.body.append('<span class="pre">%s</span>'
1150 % self.encode(token))
1151 else:
1152 self.body.append(self.encode(token))
1153 elif token in ('\n', ' '):
1154 # Allow breaks at whitespace:
1155 self.body.append(token)
1156 else:
1157 # Protect runs of multiple spaces; the last space can wrap:
1158 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
1159 self.body.append('</tt>')
1160 # Content already processed:
1161 raise nodes.SkipNode
1163 def depart_literal(self, node):
1164 # skipped unless literal element is from "code" role:
1165 self.body.append('</code>')
1167 def visit_literal_block(self, node):
1168 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
1170 def depart_literal_block(self, node):
1171 self.body.append('\n</pre>\n')
1173 def visit_math(self, node, math_env=''):
1174 # If the method is called from visit_math_block(), math_env != ''.
1176 # As there is no native HTML math support, we provide alternatives:
1177 # LaTeX and MathJax math_output modes simply wrap the content,
1178 # HTML and MathML math_output modes also convert the math_code.
1179 if self.math_output not in ('mathml', 'html', 'mathjax', 'latex'):
1180 self.document.reporter.error(
1181 'math-output format "%s" not supported '
1182 'falling back to "latex"'% self.math_output)
1183 self.math_output = 'latex'
1185 # HTML container
1186 tags = {# math_output: (block, inline, class-arguments)
1187 'mathml': ('div', '', ''),
1188 'html': ('div', 'span', 'formula'),
1189 'mathjax': ('div', 'span', 'math'),
1190 'latex': ('pre', 'tt', 'math'),
1192 tag = tags[self.math_output][math_env == '']
1193 clsarg = tags[self.math_output][2]
1194 # LaTeX container
1195 wrappers = {# math_mode: (inline, block)
1196 'mathml': (None, None),
1197 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1198 'mathjax': ('\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1199 'latex': (None, None),
1201 wrapper = wrappers[self.math_output][math_env != '']
1202 # get and wrap content
1203 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1204 if wrapper and math_env:
1205 math_code = wrapper % (math_env, math_code, math_env)
1206 elif wrapper:
1207 math_code = wrapper % math_code
1208 # settings and conversion
1209 if self.math_output in ('latex', 'mathjax'):
1210 math_code = self.encode(math_code)
1211 if self.math_output == 'mathjax' and not self.math_header:
1212 if self.math_output_options:
1213 self.mathjax_url = self.math_output_options[0]
1214 self.math_header = [self.mathjax_script % self.mathjax_url]
1215 elif self.math_output == 'html':
1216 if self.math_output_options and not self.math_header:
1217 self.math_header = [self.stylesheet_call(
1218 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1219 for s in self.math_output_options[0].split(',')]
1220 # TODO: fix display mode in matrices and fractions
1221 math2html.DocumentParameters.displaymode = (math_env != '')
1222 math_code = math2html.math2html(math_code)
1223 elif self.math_output == 'mathml':
1224 self.doctype = self.doctype_mathml
1225 self.content_type = self.content_type_mathml
1226 try:
1227 mathml_tree = parse_latex_math(math_code, inline=not(math_env))
1228 math_code = ''.join(mathml_tree.xml())
1229 except SyntaxError, err:
1230 err_node = self.document.reporter.error(err, base_node=node)
1231 self.visit_system_message(err_node)
1232 self.body.append(self.starttag(node, 'p'))
1233 self.body.append(u','.join(err.args))
1234 self.body.append('</p>\n')
1235 self.body.append(self.starttag(node, 'pre',
1236 CLASS='literal-block'))
1237 self.body.append(self.encode(math_code))
1238 self.body.append('\n</pre>\n')
1239 self.depart_system_message(err_node)
1240 raise nodes.SkipNode
1241 # append to document body
1242 if tag:
1243 self.body.append(self.starttag(node, tag,
1244 suffix='\n'*bool(math_env),
1245 CLASS=clsarg))
1246 self.body.append(math_code)
1247 if math_env:
1248 self.body.append('\n')
1249 if tag:
1250 self.body.append('</%s>\n' % tag)
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