comparison env/lib/python3.9/site-packages/docutils/writers/_html_base.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # :Author: David Goodger, Günter Milde
4 # Based on the html4css1 writer by David Goodger.
5 # :Maintainer: docutils-develop@lists.sourceforge.net
6 # :Revision: $Revision: 8412 $
7 # :Date: $Date: 2005-06-28$
8 # :Copyright: © 2016 David Goodger, Günter Milde
9 # :License: Released under the terms of the `2-Clause BSD license`_, in short:
10 #
11 # Copying and distribution of this file, with or without modification,
12 # are permitted in any medium without royalty provided the copyright
13 # notice and this notice are preserved.
14 # This file is offered as-is, without any warranty.
15 #
16 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
17
18 """common definitions for Docutils HTML writers"""
19
20 import sys
21 import os.path
22 import re
23
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
34 import docutils
35 from docutils import nodes, utils, writers, languages, io
36 from docutils.utils.error_reporting import SafeString
37 from docutils.transforms import writer_aux
38 from docutils.utils.math import (unichar2tex, pick_math_environment,
39 math2html, latex2mathml, tex2mathml_extern)
40
41 if sys.version_info >= (3, 0):
42 from urllib.request import url2pathname
43 else:
44 from urllib import url2pathname
45
46 if sys.version_info >= (3, 0):
47 unicode = str # noqa
48
49
50 class Writer(writers.Writer):
51
52 supported = ('html', 'xhtml') # update in subclass
53 """Formats this writer supports."""
54
55 # default_stylesheets = [] # set in subclass!
56 # default_stylesheet_dirs = ['.'] # set in subclass!
57 default_template = 'template.txt'
58 # default_template_path = ... # set in subclass!
59 # settings_spec = ... # set in subclass!
60
61 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
62
63 # config_section = ... # set in subclass!
64 config_section_dependencies = ('writers', 'html writers')
65
66 visitor_attributes = (
67 'head_prefix', 'head', 'stylesheet', 'body_prefix',
68 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
69 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
70 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
71 'html_body')
72
73 def get_transforms(self):
74 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
75
76 def translate(self):
77 self.visitor = visitor = self.translator_class(self.document)
78 self.document.walkabout(visitor)
79 for attr in self.visitor_attributes:
80 setattr(self, attr, getattr(visitor, attr))
81 self.output = self.apply_template()
82
83 def apply_template(self):
84 template_file = open(self.document.settings.template, 'rb')
85 template = unicode(template_file.read(), 'utf-8')
86 template_file.close()
87 subs = self.interpolation_dict()
88 return template % subs
89
90 def interpolation_dict(self):
91 subs = {}
92 settings = self.document.settings
93 for attr in self.visitor_attributes:
94 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
95 subs['encoding'] = settings.output_encoding
96 subs['version'] = docutils.__version__
97 return subs
98
99 def assemble_parts(self):
100 writers.Writer.assemble_parts(self)
101 for part in self.visitor_attributes:
102 self.parts[part] = ''.join(getattr(self, part))
103
104
105 class HTMLTranslator(nodes.NodeVisitor):
106
107 """
108 Generic Docutils to HTML translator.
109
110 See the `html4css1` and `html5_polyglot` writers for full featured
111 HTML writers.
112
113 .. IMPORTANT::
114 The `visit_*` and `depart_*` methods use a
115 heterogeneous stack, `self.context`.
116 When subclassing, make sure to be consistent in its use!
117
118 Examples for robust coding:
119
120 a) Override both `visit_*` and `depart_*` methods, don't call the
121 parent functions.
122
123 b) Extend both and unconditionally call the parent functions::
124
125 def visit_example(self, node):
126 if foo:
127 self.body.append('<div class="foo">')
128 html4css1.HTMLTranslator.visit_example(self, node)
129
130 def depart_example(self, node):
131 html4css1.HTMLTranslator.depart_example(self, node)
132 if foo:
133 self.body.append('</div>')
134
135 c) Extend both, calling the parent functions under the same
136 conditions::
137
138 def visit_example(self, node):
139 if foo:
140 self.body.append('<div class="foo">\n')
141 else: # call the parent method
142 _html_base.HTMLTranslator.visit_example(self, node)
143
144 def depart_example(self, node):
145 if foo:
146 self.body.append('</div>\n')
147 else: # call the parent method
148 _html_base.HTMLTranslator.depart_example(self, node)
149
150 d) Extend one method (call the parent), but don't otherwise use the
151 `self.context` stack::
152
153 def depart_example(self, node):
154 _html_base.HTMLTranslator.depart_example(self, node)
155 if foo:
156 # implementation-specific code
157 # that does not use `self.context`
158 self.body.append('</div>\n')
159
160 This way, changes in stack use will not bite you.
161 """
162
163 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
164 doctype = '<!DOCTYPE html>\n'
165 doctype_mathml = doctype
166
167 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
168 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
169 content_type = ('<meta charset="%s"/>\n')
170 generator = ('<meta name="generator" content="Docutils %s: '
171 'http://docutils.sourceforge.net/" />\n')
172
173 # Template for the MathJax script in the header:
174 mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
175
176 mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
177 """
178 URL of the MathJax javascript library.
179
180 The MathJax library ought to be installed on the same
181 server as the rest of the deployed site files and specified
182 in the `math-output` setting appended to "mathjax".
183 See `Docutils Configuration`__.
184
185 __ http://docutils.sourceforge.net/docs/user/config.html#math-output
186
187 The fallback tries a local MathJax installation at
188 ``/usr/share/javascript/mathjax/MathJax.js``.
189 """
190
191 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
192 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
193 words_and_spaces = re.compile(r'[^ \n]+| +|\n')
194 # wrap point inside word:
195 in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+', re.U)
196 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
197
198 special_characters = {ord('&'): u'&amp;',
199 ord('<'): u'&lt;',
200 ord('"'): u'&quot;',
201 ord('>'): u'&gt;',
202 ord('@'): u'&#64;', # may thwart address harvesters
203 }
204 """Character references for characters with a special meaning in HTML."""
205
206
207 def __init__(self, document):
208 nodes.NodeVisitor.__init__(self, document)
209 self.settings = settings = document.settings
210 lcode = settings.language_code
211 self.language = languages.get_language(lcode, document.reporter)
212 self.meta = [self.generator % docutils.__version__]
213 self.head_prefix = []
214 self.html_prolog = []
215 if settings.xml_declaration:
216 self.head_prefix.append(self.xml_declaration
217 % settings.output_encoding)
218 # self.content_type = ""
219 # encoding not interpolated:
220 self.html_prolog.append(self.xml_declaration)
221 self.head = self.meta[:]
222 self.stylesheet = [self.stylesheet_call(path)
223 for path in utils.get_stylesheet_list(settings)]
224 self.body_prefix = ['</head>\n<body>\n']
225 # document title, subtitle display
226 self.body_pre_docinfo = []
227 # author, date, etc.
228 self.docinfo = []
229 self.body = []
230 self.fragment = []
231 self.body_suffix = ['</body>\n</html>\n']
232 self.section_level = 0
233 self.initial_header_level = int(settings.initial_header_level)
234
235 self.math_output = settings.math_output.split()
236 self.math_output_options = self.math_output[1:]
237 self.math_output = self.math_output[0].lower()
238
239 self.context = []
240 """Heterogeneous stack.
241
242 Used by visit_* and depart_* functions in conjunction with the tree
243 traversal. Make sure that the pops correspond to the pushes."""
244
245 self.topic_classes = [] # TODO: replace with self_in_contents
246 self.colspecs = []
247 self.compact_p = True
248 self.compact_simple = False
249 self.compact_field_list = False
250 self.in_docinfo = False
251 self.in_sidebar = False
252 self.in_footnote_list = False
253 self.title = []
254 self.subtitle = []
255 self.header = []
256 self.footer = []
257 self.html_head = [self.content_type] # charset not interpolated
258 self.html_title = []
259 self.html_subtitle = []
260 self.html_body = []
261 self.in_document_title = 0 # len(self.body) or 0
262 self.in_mailto = False
263 self.author_in_authors = False # for html4css1
264 self.math_header = []
265
266 def astext(self):
267 return ''.join(self.head_prefix + self.head
268 + self.stylesheet + self.body_prefix
269 + self.body_pre_docinfo + self.docinfo
270 + self.body + self.body_suffix)
271
272 def encode(self, text):
273 """Encode special characters in `text` & return."""
274 # Use only named entities known in both XML and HTML
275 # other characters are automatically encoded "by number" if required.
276 # @@@ A codec to do these and all other HTML entities would be nice.
277 text = unicode(text)
278 return text.translate(self.special_characters)
279
280 def cloak_mailto(self, uri):
281 """Try to hide a mailto: URL from harvesters."""
282 # Encode "@" using a URL octet reference (see RFC 1738).
283 # Further cloaking with HTML entities will be done in the
284 # `attval` function.
285 return uri.replace('@', '%40')
286
287 def cloak_email(self, addr):
288 """Try to hide the link text of a email link from harversters."""
289 # Surround at-signs and periods with <span> tags. ("@" has
290 # already been encoded to "&#64;" by the `encode` method.)
291 addr = addr.replace('&#64;', '<span>&#64;</span>')
292 addr = addr.replace('.', '<span>&#46;</span>')
293 return addr
294
295 def attval(self, text,
296 whitespace=re.compile('[\n\r\t\v\f]')):
297 """Cleanse, HTML encode, and return attribute value text."""
298 encoded = self.encode(whitespace.sub(' ', text))
299 if self.in_mailto and self.settings.cloak_email_addresses:
300 # Cloak at-signs ("%40") and periods with HTML entities.
301 encoded = encoded.replace('%40', '&#37;&#52;&#48;')
302 encoded = encoded.replace('.', '&#46;')
303 return encoded
304
305 def stylesheet_call(self, path):
306 """Return code to reference or embed stylesheet file `path`"""
307 if self.settings.embed_stylesheet:
308 try:
309 content = io.FileInput(source_path=path,
310 encoding='utf-8').read()
311 self.settings.record_dependencies.add(path)
312 except IOError as err:
313 msg = u"Cannot embed stylesheet '%s': %s." % (
314 path, SafeString(err.strerror))
315 self.document.reporter.error(msg)
316 return '<--- %s --->\n' % msg
317 return self.embedded_stylesheet % content
318 # else link to style file:
319 if self.settings.stylesheet_path:
320 # adapt path relative to output (cf. config.html#stylesheet-path)
321 path = utils.relative_path(self.settings._destination, path)
322 return self.stylesheet_link % self.encode(path)
323
324 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
325 """
326 Construct and return a start tag given a node (id & class attributes
327 are extracted), tag name, and optional attributes.
328 """
329 tagname = tagname.lower()
330 prefix = []
331 atts = {}
332 ids = []
333 for (name, value) in attributes.items():
334 atts[name.lower()] = value
335 classes = []
336 languages = []
337 # unify class arguments and move language specification
338 for cls in node.get('classes', []) + atts.pop('class', '').split():
339 if cls.startswith('language-'):
340 languages.append(cls[9:])
341 elif cls.strip() and cls not in classes:
342 classes.append(cls)
343 if languages:
344 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
345 atts[self.lang_attribute] = languages[0]
346 if classes:
347 atts['class'] = ' '.join(classes)
348 assert 'id' not in atts
349 ids.extend(node.get('ids', []))
350 if 'ids' in atts:
351 ids.extend(atts['ids'])
352 del atts['ids']
353 if ids:
354 atts['id'] = ids[0]
355 for id in ids[1:]:
356 # Add empty "span" elements for additional IDs. Note
357 # that we cannot use empty "a" elements because there
358 # may be targets inside of references, but nested "a"
359 # elements aren't allowed in XHTML (even if they do
360 # not all have a "href" attribute).
361 if empty or isinstance(node,
362 (nodes.bullet_list, nodes.docinfo,
363 nodes.definition_list, nodes.enumerated_list,
364 nodes.field_list, nodes.option_list,
365 nodes.table)):
366 # Insert target right in front of element.
367 prefix.append('<span id="%s"></span>' % id)
368 else:
369 # Non-empty tag. Place the auxiliary <span> tag
370 # *inside* the element, as the first child.
371 suffix += '<span id="%s"></span>' % id
372 attlist = sorted(atts.items())
373 parts = [tagname]
374 for name, value in attlist:
375 # value=None was used for boolean attributes without
376 # value, but this isn't supported by XHTML.
377 assert value is not None
378 if isinstance(value, list):
379 values = [unicode(v) for v in value]
380 parts.append('%s="%s"' % (name.lower(),
381 self.attval(' '.join(values))))
382 else:
383 parts.append('%s="%s"' % (name.lower(),
384 self.attval(unicode(value))))
385 if empty:
386 infix = ' /'
387 else:
388 infix = ''
389 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
390
391 def emptytag(self, node, tagname, suffix='\n', **attributes):
392 """Construct and return an XML-compatible empty tag."""
393 return self.starttag(node, tagname, suffix, empty=True, **attributes)
394
395 def set_class_on_child(self, node, class_, index=0):
396 """
397 Set class `class_` on the visible child no. index of `node`.
398 Do nothing if node has fewer children than `index`.
399 """
400 children = [n for n in node if not isinstance(n, nodes.Invisible)]
401 try:
402 child = children[index]
403 except IndexError:
404 return
405 child['classes'].append(class_)
406
407 def visit_Text(self, node):
408 text = node.astext()
409 encoded = self.encode(text)
410 if self.in_mailto and self.settings.cloak_email_addresses:
411 encoded = self.cloak_email(encoded)
412 self.body.append(encoded)
413
414 def depart_Text(self, node):
415 pass
416
417 def visit_abbreviation(self, node):
418 # @@@ implementation incomplete ("title" attribute)
419 self.body.append(self.starttag(node, 'abbr', ''))
420
421 def depart_abbreviation(self, node):
422 self.body.append('</abbr>')
423
424 def visit_acronym(self, node):
425 # @@@ implementation incomplete ("title" attribute)
426 self.body.append(self.starttag(node, 'acronym', ''))
427
428 def depart_acronym(self, node):
429 self.body.append('</acronym>')
430
431 def visit_address(self, node):
432 self.visit_docinfo_item(node, 'address', meta=False)
433 self.body.append(self.starttag(node, 'pre',
434 suffix= '', CLASS='address'))
435
436 def depart_address(self, node):
437 self.body.append('\n</pre>\n')
438 self.depart_docinfo_item()
439
440 def visit_admonition(self, node):
441 node['classes'].insert(0, 'admonition')
442 self.body.append(self.starttag(node, 'div'))
443
444 def depart_admonition(self, node=None):
445 self.body.append('</div>\n')
446
447 attribution_formats = {'dash': (u'\u2014', ''),
448 'parentheses': ('(', ')'),
449 'parens': ('(', ')'),
450 'none': ('', '')}
451
452 def visit_attribution(self, node):
453 prefix, suffix = self.attribution_formats[self.settings.attribution]
454 self.context.append(suffix)
455 self.body.append(
456 self.starttag(node, 'p', prefix, CLASS='attribution'))
457
458 def depart_attribution(self, node):
459 self.body.append(self.context.pop() + '</p>\n')
460
461 def visit_author(self, node):
462 if not(isinstance(node.parent, nodes.authors)):
463 self.visit_docinfo_item(node, 'author')
464 self.body.append('<p>')
465
466 def depart_author(self, node):
467 self.body.append('</p>')
468 if isinstance(node.parent, nodes.authors):
469 self.body.append('\n')
470 else:
471 self.depart_docinfo_item()
472
473 def visit_authors(self, node):
474 self.visit_docinfo_item(node, 'authors')
475
476 def depart_authors(self, node):
477 self.depart_docinfo_item()
478
479 def visit_block_quote(self, node):
480 self.body.append(self.starttag(node, 'blockquote'))
481
482 def depart_block_quote(self, node):
483 self.body.append('</blockquote>\n')
484
485 def check_simple_list(self, node):
486 """Check for a simple list that can be rendered compactly."""
487 visitor = SimpleListChecker(self.document)
488 try:
489 node.walk(visitor)
490 except nodes.NodeFound:
491 return False
492 else:
493 return True
494
495 # Compact lists
496 # ------------
497 # Include definition lists and field lists (in addition to ordered
498 # and unordered lists) in the test if a list is "simple" (cf. the
499 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
500 # the end of this file).
501
502 def is_compactable(self, node):
503 # explicite class arguments have precedence
504 if 'compact' in node['classes']:
505 return True
506 if 'open' in node['classes']:
507 return False
508 # check config setting:
509 if (isinstance(node, (nodes.field_list, nodes.definition_list))
510 and not self.settings.compact_field_lists):
511 return False
512 if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
513 and not self.settings.compact_lists):
514 return False
515 # more special cases:
516 if (self.topic_classes == ['contents']): # TODO: self.in_contents
517 return True
518 # check the list items:
519 return self.check_simple_list(node)
520
521 def visit_bullet_list(self, node):
522 atts = {}
523 old_compact_simple = self.compact_simple
524 self.context.append((self.compact_simple, self.compact_p))
525 self.compact_p = None
526 self.compact_simple = self.is_compactable(node)
527 if self.compact_simple and not old_compact_simple:
528 atts['class'] = 'simple'
529 self.body.append(self.starttag(node, 'ul', **atts))
530
531 def depart_bullet_list(self, node):
532 self.compact_simple, self.compact_p = self.context.pop()
533 self.body.append('</ul>\n')
534
535 def visit_caption(self, node):
536 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
537
538 def depart_caption(self, node):
539 self.body.append('</p>\n')
540
541 # citations
542 # ---------
543 # Use definition list instead of table for bibliographic references.
544 # Join adjacent citation entries.
545
546 def visit_citation(self, node):
547 if not self.in_footnote_list:
548 self.body.append('<dl class="citation">\n')
549 self.in_footnote_list = True
550
551 def depart_citation(self, node):
552 self.body.append('</dd>\n')
553 if not isinstance(node.next_node(descend=False, siblings=True),
554 nodes.citation):
555 self.body.append('</dl>\n')
556 self.in_footnote_list = False
557
558 def visit_citation_reference(self, node):
559 href = '#'
560 if 'refid' in node:
561 href += node['refid']
562 elif 'refname' in node:
563 href += self.document.nameids[node['refname']]
564 # else: # TODO system message (or already in the transform)?
565 # 'Citation reference missing.'
566 self.body.append(self.starttag(
567 node, 'a', '[', CLASS='citation-reference', href=href))
568
569 def depart_citation_reference(self, node):
570 self.body.append(']</a>')
571
572 # classifier
573 # ----------
574 # don't insert classifier-delimiter here (done by CSS)
575
576 def visit_classifier(self, node):
577 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
578
579 def depart_classifier(self, node):
580 self.body.append('</span>')
581
582 def visit_colspec(self, node):
583 self.colspecs.append(node)
584 # "stubs" list is an attribute of the tgroup element:
585 node.parent.stubs.append(node.attributes.get('stub'))
586
587 def depart_colspec(self, node):
588 # write out <colgroup> when all colspecs are processed
589 if isinstance(node.next_node(descend=False, siblings=True),
590 nodes.colspec):
591 return
592 if 'colwidths-auto' in node.parent.parent['classes'] or (
593 'colwidths-auto' in self.settings.table_style and
594 ('colwidths-given' not in node.parent.parent['classes'])):
595 return
596 total_width = sum(node['colwidth'] for node in self.colspecs)
597 self.body.append(self.starttag(node, 'colgroup'))
598 for node in self.colspecs:
599 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
600 self.body.append(self.emptytag(node, 'col',
601 style='width: %i%%' % colwidth))
602 self.body.append('</colgroup>\n')
603
604 def visit_comment(self, node,
605 sub=re.compile('-(?=-)').sub):
606 """Escape double-dashes in comment text."""
607 self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
608 # Content already processed:
609 raise nodes.SkipNode
610
611 def visit_compound(self, node):
612 self.body.append(self.starttag(node, 'div', CLASS='compound'))
613 if len(node) > 1:
614 node[0]['classes'].append('compound-first')
615 node[-1]['classes'].append('compound-last')
616 for child in node[1:-1]:
617 child['classes'].append('compound-middle')
618
619 def depart_compound(self, node):
620 self.body.append('</div>\n')
621
622 def visit_container(self, node):
623 self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
624
625 def depart_container(self, node):
626 self.body.append('</div>\n')
627
628 def visit_contact(self, node):
629 self.visit_docinfo_item(node, 'contact', meta=False)
630
631 def depart_contact(self, node):
632 self.depart_docinfo_item()
633
634 def visit_copyright(self, node):
635 self.visit_docinfo_item(node, 'copyright')
636
637 def depart_copyright(self, node):
638 self.depart_docinfo_item()
639
640 def visit_date(self, node):
641 self.visit_docinfo_item(node, 'date')
642
643 def depart_date(self, node):
644 self.depart_docinfo_item()
645
646 def visit_decoration(self, node):
647 pass
648
649 def depart_decoration(self, node):
650 pass
651
652 def visit_definition(self, node):
653 self.body.append('</dt>\n')
654 self.body.append(self.starttag(node, 'dd', ''))
655
656 def depart_definition(self, node):
657 self.body.append('</dd>\n')
658
659 def visit_definition_list(self, node):
660 classes = node.setdefault('classes', [])
661 if self.is_compactable(node):
662 classes.append('simple')
663 self.body.append(self.starttag(node, 'dl'))
664
665 def depart_definition_list(self, node):
666 self.body.append('</dl>\n')
667
668 def visit_definition_list_item(self, node):
669 # pass class arguments, ids and names to definition term:
670 node.children[0]['classes'] = (
671 node.get('classes', []) + node.children[0].get('classes', []))
672 node.children[0]['ids'] = (
673 node.get('ids', []) + node.children[0].get('ids', []))
674 node.children[0]['names'] = (
675 node.get('names', []) + node.children[0].get('names', []))
676
677 def depart_definition_list_item(self, node):
678 pass
679
680 def visit_description(self, node):
681 self.body.append(self.starttag(node, 'dd', ''))
682
683 def depart_description(self, node):
684 self.body.append('</dd>\n')
685
686 def visit_docinfo(self, node):
687 self.context.append(len(self.body))
688 classes = 'docinfo'
689 if (self.is_compactable(node)):
690 classes += ' simple'
691 self.body.append(self.starttag(node, 'dl', CLASS=classes))
692
693 def depart_docinfo(self, node):
694 self.body.append('</dl>\n')
695 start = self.context.pop()
696 self.docinfo = self.body[start:]
697 self.body = []
698
699 def visit_docinfo_item(self, node, name, meta=True):
700 if meta:
701 meta_tag = '<meta name="%s" content="%s" />\n' \
702 % (name, self.attval(node.astext()))
703 self.add_meta(meta_tag)
704 self.body.append('<dt class="%s">%s</dt>\n'
705 % (name, self.language.labels[name]))
706 self.body.append(self.starttag(node, 'dd', '', CLASS=name))
707
708 def depart_docinfo_item(self):
709 self.body.append('</dd>\n')
710
711 def visit_doctest_block(self, node):
712 self.body.append(self.starttag(node, 'pre', suffix='',
713 CLASS='code python doctest'))
714
715 def depart_doctest_block(self, node):
716 self.body.append('\n</pre>\n')
717
718 def visit_document(self, node):
719 title = (node.get('title', '') or os.path.basename(node['source'])
720 or 'docutils document without title')
721 self.head.append('<title>%s</title>\n' % self.encode(title))
722
723 def depart_document(self, node):
724 self.head_prefix.extend([self.doctype,
725 self.head_prefix_template %
726 {'lang': self.settings.language_code}])
727 self.html_prolog.append(self.doctype)
728 self.meta.insert(0, self.content_type % self.settings.output_encoding)
729 self.head.insert(0, self.content_type % self.settings.output_encoding)
730 if 'name="dcterms.' in ''.join(self.meta):
731 self.head.append(
732 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/">')
733 if self.math_header:
734 if self.math_output == 'mathjax':
735 self.head.extend(self.math_header)
736 else:
737 self.stylesheet.extend(self.math_header)
738 # skip content-type meta tag with interpolated charset value:
739 self.html_head.extend(self.head[1:])
740 self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
741 self.body_suffix.insert(0, '</div>\n')
742 self.fragment.extend(self.body) # self.fragment is the "naked" body
743 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
744 + self.docinfo + self.body
745 + self.body_suffix[:-1])
746 assert not self.context, 'len(context) = %s' % len(self.context)
747
748 def visit_emphasis(self, node):
749 self.body.append(self.starttag(node, 'em', ''))
750
751 def depart_emphasis(self, node):
752 self.body.append('</em>')
753
754 def visit_entry(self, node):
755 atts = {'class': []}
756 if isinstance(node.parent.parent, nodes.thead):
757 atts['class'].append('head')
758 if node.parent.parent.parent.stubs[node.parent.column]:
759 # "stubs" list is an attribute of the tgroup element
760 atts['class'].append('stub')
761 if atts['class']:
762 tagname = 'th'
763 atts['class'] = ' '.join(atts['class'])
764 else:
765 tagname = 'td'
766 del atts['class']
767 node.parent.column += 1
768 if 'morerows' in node:
769 atts['rowspan'] = node['morerows'] + 1
770 if 'morecols' in node:
771 atts['colspan'] = node['morecols'] + 1
772 node.parent.column += node['morecols']
773 self.body.append(self.starttag(node, tagname, '', **atts))
774 self.context.append('</%s>\n' % tagname.lower())
775 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
776 # if len(node) == 0: # empty cell
777 # self.body.append('&#0160;') # no-break space
778
779 def depart_entry(self, node):
780 self.body.append(self.context.pop())
781
782 def visit_enumerated_list(self, node):
783 atts = {}
784 if 'start' in node:
785 atts['start'] = node['start']
786 if 'enumtype' in node:
787 atts['class'] = node['enumtype']
788 if self.is_compactable(node):
789 atts['class'] = (atts.get('class', '') + ' simple').strip()
790 self.body.append(self.starttag(node, 'ol', **atts))
791
792 def depart_enumerated_list(self, node):
793 self.body.append('</ol>\n')
794
795 def visit_field_list(self, node):
796 # Keep simple paragraphs in the field_body to enable CSS
797 # rule to start body on new line if the label is too long
798 classes = 'field-list'
799 if (self.is_compactable(node)):
800 classes += ' simple'
801 self.body.append(self.starttag(node, 'dl', CLASS=classes))
802
803 def depart_field_list(self, node):
804 self.body.append('</dl>\n')
805
806 def visit_field(self, node):
807 pass
808
809 def depart_field(self, node):
810 pass
811
812 # as field is ignored, pass class arguments to field-name and field-body:
813
814 def visit_field_name(self, node):
815 self.body.append(self.starttag(node, 'dt', '',
816 CLASS=''.join(node.parent['classes'])))
817
818 def depart_field_name(self, node):
819 self.body.append('</dt>\n')
820
821 def visit_field_body(self, node):
822 self.body.append(self.starttag(node, 'dd', '',
823 CLASS=''.join(node.parent['classes'])))
824 # prevent misalignment of following content if the field is empty:
825 if not node.children:
826 self.body.append('<p></p>')
827
828 def depart_field_body(self, node):
829 self.body.append('</dd>\n')
830
831 def visit_figure(self, node):
832 atts = {'class': 'figure'}
833 if node.get('width'):
834 atts['style'] = 'width: %s' % node['width']
835 if node.get('align'):
836 atts['class'] += " align-" + node['align']
837 self.body.append(self.starttag(node, 'div', **atts))
838
839 def depart_figure(self, node):
840 self.body.append('</div>\n')
841
842 # use HTML 5 <footer> element?
843 def visit_footer(self, node):
844 self.context.append(len(self.body))
845
846 def depart_footer(self, node):
847 start = self.context.pop()
848 footer = [self.starttag(node, 'div', CLASS='footer'),
849 '<hr class="footer" />\n']
850 footer.extend(self.body[start:])
851 footer.append('\n</div>\n')
852 self.footer.extend(footer)
853 self.body_suffix[:0] = footer
854 del self.body[start:]
855
856 # footnotes
857 # ---------
858 # use definition list instead of table for footnote text
859
860 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
861 def visit_footnote(self, node):
862 if not self.in_footnote_list:
863 classes = 'footnote ' + self.settings.footnote_references
864 self.body.append('<dl class="%s">\n'%classes)
865 self.in_footnote_list = True
866
867 def depart_footnote(self, node):
868 self.body.append('</dd>\n')
869 if not isinstance(node.next_node(descend=False, siblings=True),
870 nodes.footnote):
871 self.body.append('</dl>\n')
872 self.in_footnote_list = False
873
874 def visit_footnote_reference(self, node):
875 href = '#' + node['refid']
876 classes = 'footnote-reference ' + self.settings.footnote_references
877 self.body.append(self.starttag(node, 'a', '', #suffix,
878 CLASS=classes, href=href))
879
880 def depart_footnote_reference(self, node):
881 self.body.append('</a>')
882
883 # Docutils-generated text: put section numbers in a span for CSS styling:
884 def visit_generated(self, node):
885 if 'sectnum' in node['classes']:
886 # get section number (strip trailing no-break-spaces)
887 sectnum = node.astext().rstrip(u' ')
888 self.body.append('<span class="sectnum">%s</span> '
889 % self.encode(sectnum))
890 # Content already processed:
891 raise nodes.SkipNode
892
893 def depart_generated(self, node):
894 pass
895
896 def visit_header(self, node):
897 self.context.append(len(self.body))
898
899 def depart_header(self, node):
900 start = self.context.pop()
901 header = [self.starttag(node, 'div', CLASS='header')]
902 header.extend(self.body[start:])
903 header.append('\n<hr class="header"/>\n</div>\n')
904 self.body_prefix.extend(header)
905 self.header.extend(header)
906 del self.body[start:]
907
908 # Image types to place in an <object> element
909 object_image_types = {'.swf': 'application/x-shockwave-flash'}
910
911 def visit_image(self, node):
912 atts = {}
913 uri = node['uri']
914 ext = os.path.splitext(uri)[1].lower()
915 if ext in self.object_image_types:
916 atts['data'] = uri
917 atts['type'] = self.object_image_types[ext]
918 else:
919 atts['src'] = uri
920 atts['alt'] = node.get('alt', uri)
921 # image size
922 if 'width' in node:
923 atts['width'] = node['width']
924 if 'height' in node:
925 atts['height'] = node['height']
926 if 'scale' in node:
927 if (PIL and not ('width' in node and 'height' in node)
928 and self.settings.file_insertion_enabled):
929 imagepath = url2pathname(uri)
930 try:
931 img = PIL.Image.open(
932 imagepath.encode(sys.getfilesystemencoding()))
933 except (IOError, UnicodeEncodeError):
934 pass # TODO: warn?
935 else:
936 self.settings.record_dependencies.add(
937 imagepath.replace('\\', '/'))
938 if 'width' not in atts:
939 atts['width'] = '%dpx' % img.size[0]
940 if 'height' not in atts:
941 atts['height'] = '%dpx' % img.size[1]
942 del img
943 for att_name in 'width', 'height':
944 if att_name in atts:
945 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
946 assert match
947 atts[att_name] = '%s%s' % (
948 float(match.group(1)) * (float(node['scale']) / 100),
949 match.group(2))
950 style = []
951 for att_name in 'width', 'height':
952 if att_name in atts:
953 if re.match(r'^[0-9.]+$', atts[att_name]):
954 # Interpret unitless values as pixels.
955 atts[att_name] += 'px'
956 style.append('%s: %s;' % (att_name, atts[att_name]))
957 del atts[att_name]
958 if style:
959 atts['style'] = ' '.join(style)
960 if (isinstance(node.parent, nodes.TextElement) or
961 (isinstance(node.parent, nodes.reference) and
962 not isinstance(node.parent.parent, nodes.TextElement))):
963 # Inline context or surrounded by <a>...</a>.
964 suffix = ''
965 else:
966 suffix = '\n'
967 if 'align' in node:
968 atts['class'] = 'align-%s' % node['align']
969 if ext in self.object_image_types:
970 # do NOT use an empty tag: incorrect rendering in browsers
971 self.body.append(self.starttag(node, 'object', suffix, **atts) +
972 node.get('alt', uri) + '</object>' + suffix)
973 else:
974 self.body.append(self.emptytag(node, 'img', suffix, **atts))
975
976 def depart_image(self, node):
977 pass
978
979 def visit_inline(self, node):
980 self.body.append(self.starttag(node, 'span', ''))
981
982 def depart_inline(self, node):
983 self.body.append('</span>')
984
985 # footnote and citation labels:
986 def visit_label(self, node):
987 if (isinstance(node.parent, nodes.footnote)):
988 classes = self.settings.footnote_references
989 else:
990 classes = 'brackets'
991 # pass parent node to get id into starttag:
992 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
993 self.body.append(self.starttag(node, 'span', '', CLASS=classes))
994 # footnote/citation backrefs:
995 if self.settings.footnote_backlinks:
996 backrefs = node.parent['backrefs']
997 if len(backrefs) == 1:
998 self.body.append('<a class="fn-backref" href="#%s">'
999 % backrefs[0])
1000
1001 def depart_label(self, node):
1002 if self.settings.footnote_backlinks:
1003 backrefs = node.parent['backrefs']
1004 if len(backrefs) == 1:
1005 self.body.append('</a>')
1006 self.body.append('</span>')
1007 if self.settings.footnote_backlinks and len(backrefs) > 1:
1008 backlinks = ['<a href="#%s">%s</a>' % (ref, i)
1009 for (i, ref) in enumerate(backrefs, 1)]
1010 self.body.append('<span class="fn-backref">(%s)</span>'
1011 % ','.join(backlinks))
1012 self.body.append('</dt>\n<dd>')
1013
1014 def visit_legend(self, node):
1015 self.body.append(self.starttag(node, 'div', CLASS='legend'))
1016
1017 def depart_legend(self, node):
1018 self.body.append('</div>\n')
1019
1020 def visit_line(self, node):
1021 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1022 if not len(node):
1023 self.body.append('<br />')
1024
1025 def depart_line(self, node):
1026 self.body.append('</div>\n')
1027
1028 def visit_line_block(self, node):
1029 self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1030
1031 def depart_line_block(self, node):
1032 self.body.append('</div>\n')
1033
1034 def visit_list_item(self, node):
1035 self.body.append(self.starttag(node, 'li', ''))
1036
1037 def depart_list_item(self, node):
1038 self.body.append('</li>\n')
1039
1040 # inline literal
1041 def visit_literal(self, node):
1042 # special case: "code" role
1043 classes = node.get('classes', [])
1044 if 'code' in classes:
1045 # filter 'code' from class arguments
1046 node['classes'] = [cls for cls in classes if cls != 'code']
1047 self.body.append(self.starttag(node, 'code', ''))
1048 return
1049 self.body.append(
1050 self.starttag(node, 'span', '', CLASS='docutils literal'))
1051 text = node.astext()
1052 # remove hard line breaks (except if in a parsed-literal block)
1053 if not isinstance(node.parent, nodes.literal_block):
1054 text = text.replace('\n', ' ')
1055 # Protect text like ``--an-option`` and the regular expression
1056 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1057 for token in self.words_and_spaces.findall(text):
1058 if token.strip() and self.in_word_wrap_point.search(token):
1059 self.body.append('<span class="pre">%s</span>'
1060 % self.encode(token))
1061 else:
1062 self.body.append(self.encode(token))
1063 self.body.append('</span>')
1064 # Content already processed:
1065 raise nodes.SkipNode
1066
1067 def depart_literal(self, node):
1068 # skipped unless literal element is from "code" role:
1069 self.body.append('</code>')
1070
1071 def visit_literal_block(self, node):
1072 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1073 if 'code' in node.get('classes', []):
1074 self.body.append('<code>')
1075
1076 def depart_literal_block(self, node):
1077 if 'code' in node.get('classes', []):
1078 self.body.append('</code>')
1079 self.body.append('</pre>\n')
1080
1081 # Mathematics:
1082 # As there is no native HTML math support, we provide alternatives
1083 # for the math-output: LaTeX and MathJax simply wrap the content,
1084 # HTML and MathML also convert the math_code.
1085 # HTML container
1086 math_tags = {# math_output: (block, inline, class-arguments)
1087 'mathml': ('div', '', ''),
1088 'html': ('div', 'span', 'formula'),
1089 'mathjax': ('div', 'span', 'math'),
1090 'latex': ('pre', 'tt', 'math'),
1091 }
1092
1093 def visit_math(self, node, math_env=''):
1094 # If the method is called from visit_math_block(), math_env != ''.
1095
1096 if self.math_output not in self.math_tags:
1097 self.document.reporter.error(
1098 'math-output format "%s" not supported '
1099 'falling back to "latex"'% self.math_output)
1100 self.math_output = 'latex'
1101 tag = self.math_tags[self.math_output][math_env == '']
1102 clsarg = self.math_tags[self.math_output][2]
1103 # LaTeX container
1104 wrappers = {# math_mode: (inline, block)
1105 'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1106 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
1107 'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1108 'latex': (None, None),
1109 }
1110 wrapper = wrappers[self.math_output][math_env != '']
1111 if self.math_output == 'mathml' and (not self.math_output_options or
1112 self.math_output_options[0] == 'blahtexml'):
1113 wrapper = None
1114 # get and wrap content
1115 math_code = node.astext().translate(unichar2tex.uni2tex_table)
1116 if wrapper:
1117 try: # wrapper with three "%s"
1118 math_code = wrapper % (math_env, math_code, math_env)
1119 except TypeError: # wrapper with one "%s"
1120 math_code = wrapper % math_code
1121 # settings and conversion
1122 if self.math_output in ('latex', 'mathjax'):
1123 math_code = self.encode(math_code)
1124 if self.math_output == 'mathjax' and not self.math_header:
1125 try:
1126 self.mathjax_url = self.math_output_options[0]
1127 except IndexError:
1128 self.document.reporter.warning('No MathJax URL specified, '
1129 'using local fallback (see config.html)')
1130 # append configuration, if not already present in the URL:
1131 # input LaTeX with AMS, output common HTML
1132 if '?' not in self.mathjax_url:
1133 self.mathjax_url += '?config=TeX-AMS_CHTML'
1134 self.math_header = [self.mathjax_script % self.mathjax_url]
1135 elif self.math_output == 'html':
1136 if self.math_output_options and not self.math_header:
1137 self.math_header = [self.stylesheet_call(
1138 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1139 for s in self.math_output_options[0].split(',')]
1140 # TODO: fix display mode in matrices and fractions
1141 math2html.DocumentParameters.displaymode = (math_env != '')
1142 math_code = math2html.math2html(math_code)
1143 elif self.math_output == 'mathml':
1144 if 'XHTML 1' in self.doctype:
1145 self.doctype = self.doctype_mathml
1146 self.content_type = self.content_type_mathml
1147 converter = ' '.join(self.math_output_options).lower()
1148 try:
1149 if converter == 'latexml':
1150 math_code = tex2mathml_extern.latexml(math_code,
1151 self.document.reporter)
1152 elif converter == 'ttm':
1153 math_code = tex2mathml_extern.ttm(math_code,
1154 self.document.reporter)
1155 elif converter == 'blahtexml':
1156 math_code = tex2mathml_extern.blahtexml(math_code,
1157 inline=not(math_env),
1158 reporter=self.document.reporter)
1159 elif not converter:
1160 math_code = latex2mathml.tex2mathml(math_code,
1161 inline=not(math_env))
1162 else:
1163 self.document.reporter.error('option "%s" not supported '
1164 'with math-output "MathML"')
1165 except OSError:
1166 raise OSError('is "latexmlmath" in your PATH?')
1167 except SyntaxError as err:
1168 err_node = self.document.reporter.error(err, base_node=node)
1169 self.visit_system_message(err_node)
1170 self.body.append(self.starttag(node, 'p'))
1171 self.body.append(u','.join(err.args))
1172 self.body.append('</p>\n')
1173 self.body.append(self.starttag(node, 'pre',
1174 CLASS='literal-block'))
1175 self.body.append(self.encode(math_code))
1176 self.body.append('\n</pre>\n')
1177 self.depart_system_message(err_node)
1178 raise nodes.SkipNode
1179 # append to document body
1180 if tag:
1181 self.body.append(self.starttag(node, tag,
1182 suffix='\n'*bool(math_env),
1183 CLASS=clsarg))
1184 self.body.append(math_code)
1185 if math_env: # block mode (equation, display)
1186 self.body.append('\n')
1187 if tag:
1188 self.body.append('</%s>' % tag)
1189 if math_env:
1190 self.body.append('\n')
1191 # Content already processed:
1192 raise nodes.SkipNode
1193
1194 def depart_math(self, node):
1195 pass # never reached
1196
1197 def visit_math_block(self, node):
1198 math_env = pick_math_environment(node.astext())
1199 self.visit_math(node, math_env=math_env)
1200
1201 def depart_math_block(self, node):
1202 pass # never reached
1203
1204 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1205 # HTML5/polyglot recommends using both
1206 def visit_meta(self, node):
1207 meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1208 self.add_meta(meta)
1209
1210 def depart_meta(self, node):
1211 pass
1212
1213 def add_meta(self, tag):
1214 self.meta.append(tag)
1215 self.head.append(tag)
1216
1217 def visit_option(self, node):
1218 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1219
1220 def depart_option(self, node):
1221 self.body.append('</span>')
1222 if isinstance(node.next_node(descend=False, siblings=True),
1223 nodes.option):
1224 self.body.append(', ')
1225
1226 def visit_option_argument(self, node):
1227 self.body.append(node.get('delimiter', ' '))
1228 self.body.append(self.starttag(node, 'var', ''))
1229
1230 def depart_option_argument(self, node):
1231 self.body.append('</var>')
1232
1233 def visit_option_group(self, node):
1234 self.body.append(self.starttag(node, 'dt', ''))
1235 self.body.append('<kbd>')
1236
1237 def depart_option_group(self, node):
1238 self.body.append('</kbd></dt>\n')
1239
1240 def visit_option_list(self, node):
1241 self.body.append(
1242 self.starttag(node, 'dl', CLASS='option-list'))
1243
1244 def depart_option_list(self, node):
1245 self.body.append('</dl>\n')
1246
1247 def visit_option_list_item(self, node):
1248 pass
1249
1250 def depart_option_list_item(self, node):
1251 pass
1252
1253 def visit_option_string(self, node):
1254 pass
1255
1256 def depart_option_string(self, node):
1257 pass
1258
1259 def visit_organization(self, node):
1260 self.visit_docinfo_item(node, 'organization')
1261
1262 def depart_organization(self, node):
1263 self.depart_docinfo_item()
1264
1265 # Do not omit <p> tags
1266 # --------------------
1267 #
1268 # The HTML4CSS1 writer does this to "produce
1269 # visually compact lists (less vertical whitespace)". This writer
1270 # relies on CSS rules for"visual compactness".
1271 #
1272 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1273 # character data, so you cannot drop the <p> tags.
1274 # * Keeping simple paragraphs in the field_body enables a CSS
1275 # rule to start the field-body on a new line if the label is too long
1276 # * it makes the code simpler.
1277 #
1278 # TODO: omit paragraph tags in simple table cells?
1279
1280 def visit_paragraph(self, node):
1281 self.body.append(self.starttag(node, 'p', ''))
1282
1283 def depart_paragraph(self, node):
1284 self.body.append('</p>')
1285 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1286 (len(node.parent) == 1)):
1287 self.body.append('\n')
1288
1289 def visit_problematic(self, node):
1290 if node.hasattr('refid'):
1291 self.body.append('<a href="#%s">' % node['refid'])
1292 self.context.append('</a>')
1293 else:
1294 self.context.append('')
1295 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1296
1297 def depart_problematic(self, node):
1298 self.body.append('</span>')
1299 self.body.append(self.context.pop())
1300
1301 def visit_raw(self, node):
1302 if 'html' in node.get('format', '').split():
1303 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1304 if node['classes']:
1305 self.body.append(self.starttag(node, t, suffix=''))
1306 self.body.append(node.astext())
1307 if node['classes']:
1308 self.body.append('</%s>' % t)
1309 # Keep non-HTML raw text out of output:
1310 raise nodes.SkipNode
1311
1312 def visit_reference(self, node):
1313 atts = {'class': 'reference'}
1314 if 'refuri' in node:
1315 atts['href'] = node['refuri']
1316 if ( self.settings.cloak_email_addresses
1317 and atts['href'].startswith('mailto:')):
1318 atts['href'] = self.cloak_mailto(atts['href'])
1319 self.in_mailto = True
1320 atts['class'] += ' external'
1321 else:
1322 assert 'refid' in node, \
1323 'References must have "refuri" or "refid" attribute.'
1324 atts['href'] = '#' + node['refid']
1325 atts['class'] += ' internal'
1326 if not isinstance(node.parent, nodes.TextElement):
1327 assert len(node) == 1 and isinstance(node[0], nodes.image)
1328 atts['class'] += ' image-reference'
1329 self.body.append(self.starttag(node, 'a', '', **atts))
1330
1331 def depart_reference(self, node):
1332 self.body.append('</a>')
1333 if not isinstance(node.parent, nodes.TextElement):
1334 self.body.append('\n')
1335 self.in_mailto = False
1336
1337 def visit_revision(self, node):
1338 self.visit_docinfo_item(node, 'revision', meta=False)
1339
1340 def depart_revision(self, node):
1341 self.depart_docinfo_item()
1342
1343 def visit_row(self, node):
1344 self.body.append(self.starttag(node, 'tr', ''))
1345 node.column = 0
1346
1347 def depart_row(self, node):
1348 self.body.append('</tr>\n')
1349
1350 def visit_rubric(self, node):
1351 self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1352
1353 def depart_rubric(self, node):
1354 self.body.append('</p>\n')
1355
1356 # TODO: use the new HTML 5 element <section>?
1357 def visit_section(self, node):
1358 self.section_level += 1
1359 self.body.append(
1360 self.starttag(node, 'div', CLASS='section'))
1361
1362 def depart_section(self, node):
1363 self.section_level -= 1
1364 self.body.append('</div>\n')
1365
1366 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1367 def visit_sidebar(self, node):
1368 self.body.append(
1369 self.starttag(node, 'div', CLASS='sidebar'))
1370 self.in_sidebar = True
1371
1372 def depart_sidebar(self, node):
1373 self.body.append('</div>\n')
1374 self.in_sidebar = False
1375
1376 def visit_status(self, node):
1377 self.visit_docinfo_item(node, 'status', meta=False)
1378
1379 def depart_status(self, node):
1380 self.depart_docinfo_item()
1381
1382 def visit_strong(self, node):
1383 self.body.append(self.starttag(node, 'strong', ''))
1384
1385 def depart_strong(self, node):
1386 self.body.append('</strong>')
1387
1388 def visit_subscript(self, node):
1389 self.body.append(self.starttag(node, 'sub', ''))
1390
1391 def depart_subscript(self, node):
1392 self.body.append('</sub>')
1393
1394 def visit_substitution_definition(self, node):
1395 """Internal only."""
1396 raise nodes.SkipNode
1397
1398 def visit_substitution_reference(self, node):
1399 self.unimplemented_visit(node)
1400
1401 # h1–h6 elements must not be used to markup subheadings, subtitles,
1402 # alternative titles and taglines unless intended to be the heading for a
1403 # new section or subsection.
1404 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1405 def visit_subtitle(self, node):
1406 if isinstance(node.parent, nodes.sidebar):
1407 classes = 'sidebar-subtitle'
1408 elif isinstance(node.parent, nodes.document):
1409 classes = 'subtitle'
1410 self.in_document_title = len(self.body)+1
1411 elif isinstance(node.parent, nodes.section):
1412 classes = 'section-subtitle'
1413 self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1414
1415 def depart_subtitle(self, node):
1416 self.body.append('</p>\n')
1417 if isinstance(node.parent, nodes.document):
1418 self.subtitle = self.body[self.in_document_title:-1]
1419 self.in_document_title = 0
1420 self.body_pre_docinfo.extend(self.body)
1421 self.html_subtitle.extend(self.body)
1422 del self.body[:]
1423
1424 def visit_superscript(self, node):
1425 self.body.append(self.starttag(node, 'sup', ''))
1426
1427 def depart_superscript(self, node):
1428 self.body.append('</sup>')
1429
1430 def visit_system_message(self, node):
1431 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1432 self.body.append('<p class="system-message-title">')
1433 backref_text = ''
1434 if len(node['backrefs']):
1435 backrefs = node['backrefs']
1436 if len(backrefs) == 1:
1437 backref_text = ('; <em><a href="#%s">backlink</a></em>'
1438 % backrefs[0])
1439 else:
1440 i = 1
1441 backlinks = []
1442 for backref in backrefs:
1443 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1444 i += 1
1445 backref_text = ('; <em>backlinks: %s</em>'
1446 % ', '.join(backlinks))
1447 if node.hasattr('line'):
1448 line = ', line %s' % node['line']
1449 else:
1450 line = ''
1451 self.body.append('System Message: %s/%s '
1452 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1453 % (node['type'], node['level'],
1454 self.encode(node['source']), line, backref_text))
1455
1456 def depart_system_message(self, node):
1457 self.body.append('</div>\n')
1458
1459 def visit_table(self, node):
1460 atts = {}
1461 classes = [cls.strip(u' \t\n')
1462 for cls in self.settings.table_style.split(',')]
1463 if 'align' in node:
1464 classes.append('align-%s' % node['align'])
1465 if 'width' in node:
1466 atts['style'] = 'width: %s' % node['width']
1467 tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts)
1468 self.body.append(tag)
1469
1470 def depart_table(self, node):
1471 self.body.append('</table>\n')
1472
1473 def visit_target(self, node):
1474 if not ('refuri' in node or 'refid' in node
1475 or 'refname' in node):
1476 self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1477 self.context.append('</span>')
1478 else:
1479 self.context.append('')
1480
1481 def depart_target(self, node):
1482 self.body.append(self.context.pop())
1483
1484 # no hard-coded vertical alignment in table body
1485 def visit_tbody(self, node):
1486 self.body.append(self.starttag(node, 'tbody'))
1487
1488 def depart_tbody(self, node):
1489 self.body.append('</tbody>\n')
1490
1491 def visit_term(self, node):
1492 self.body.append(self.starttag(node, 'dt', ''))
1493
1494 def depart_term(self, node):
1495 """
1496 Leave the end tag to `self.visit_definition()`, in case there's a
1497 classifier.
1498 """
1499 pass
1500
1501 def visit_tgroup(self, node):
1502 self.colspecs = []
1503 node.stubs = []
1504
1505 def depart_tgroup(self, node):
1506 pass
1507
1508 def visit_thead(self, node):
1509 self.body.append(self.starttag(node, 'thead'))
1510
1511 def depart_thead(self, node):
1512 self.body.append('</thead>\n')
1513
1514 def visit_title(self, node):
1515 """Only 6 section levels are supported by HTML."""
1516 close_tag = '</p>\n'
1517 if isinstance(node.parent, nodes.topic):
1518 self.body.append(
1519 self.starttag(node, 'p', '', CLASS='topic-title'))
1520 elif isinstance(node.parent, nodes.sidebar):
1521 self.body.append(
1522 self.starttag(node, 'p', '', CLASS='sidebar-title'))
1523 elif isinstance(node.parent, nodes.Admonition):
1524 self.body.append(
1525 self.starttag(node, 'p', '', CLASS='admonition-title'))
1526 elif isinstance(node.parent, nodes.table):
1527 self.body.append(
1528 self.starttag(node, 'caption', ''))
1529 close_tag = '</caption>\n'
1530 elif isinstance(node.parent, nodes.document):
1531 self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1532 close_tag = '</h1>\n'
1533 self.in_document_title = len(self.body)
1534 else:
1535 assert isinstance(node.parent, nodes.section)
1536 h_level = self.section_level + self.initial_header_level - 1
1537 atts = {}
1538 if (len(node.parent) >= 2 and
1539 isinstance(node.parent[1], nodes.subtitle)):
1540 atts['CLASS'] = 'with-subtitle'
1541 self.body.append(
1542 self.starttag(node, 'h%s' % h_level, '', **atts))
1543 atts = {}
1544 if node.hasattr('refid'):
1545 atts['class'] = 'toc-backref'
1546 atts['href'] = '#' + node['refid']
1547 if atts:
1548 self.body.append(self.starttag({}, 'a', '', **atts))
1549 close_tag = '</a></h%s>\n' % (h_level)
1550 else:
1551 close_tag = '</h%s>\n' % (h_level)
1552 self.context.append(close_tag)
1553
1554 def depart_title(self, node):
1555 self.body.append(self.context.pop())
1556 if self.in_document_title:
1557 self.title = self.body[self.in_document_title:-1]
1558 self.in_document_title = 0
1559 self.body_pre_docinfo.extend(self.body)
1560 self.html_title.extend(self.body)
1561 del self.body[:]
1562
1563 def visit_title_reference(self, node):
1564 self.body.append(self.starttag(node, 'cite', ''))
1565
1566 def depart_title_reference(self, node):
1567 self.body.append('</cite>')
1568
1569 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1570 def visit_topic(self, node):
1571 self.body.append(self.starttag(node, 'div', CLASS='topic'))
1572 self.topic_classes = node['classes']
1573 # TODO: replace with ::
1574 # self.in_contents = 'contents' in node['classes']
1575
1576 def depart_topic(self, node):
1577 self.body.append('</div>\n')
1578 self.topic_classes = []
1579 # TODO self.in_contents = False
1580
1581 def visit_transition(self, node):
1582 self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1583
1584 def depart_transition(self, node):
1585 pass
1586
1587 def visit_version(self, node):
1588 self.visit_docinfo_item(node, 'version', meta=False)
1589
1590 def depart_version(self, node):
1591 self.depart_docinfo_item()
1592
1593 def unimplemented_visit(self, node):
1594 raise NotImplementedError('visiting unimplemented node type: %s'
1595 % node.__class__.__name__)
1596
1597
1598 class SimpleListChecker(nodes.GenericNodeVisitor):
1599
1600 """
1601 Raise `nodes.NodeFound` if non-simple list item is encountered.
1602
1603 Here "simple" means a list item containing nothing other than a single
1604 paragraph, a simple list, or a paragraph followed by a simple list.
1605
1606 This version also checks for simple field lists and docinfo.
1607 """
1608
1609 def default_visit(self, node):
1610 raise nodes.NodeFound
1611
1612 def visit_list_item(self, node):
1613 children = [child for child in node.children
1614 if not isinstance(child, nodes.Invisible)]
1615 if (children and isinstance(children[0], nodes.paragraph)
1616 and (isinstance(children[-1], nodes.bullet_list) or
1617 isinstance(children[-1], nodes.enumerated_list) or
1618 isinstance(children[-1], nodes.field_list))):
1619 children.pop()
1620 if len(children) <= 1:
1621 return
1622 else:
1623 raise nodes.NodeFound
1624
1625 def pass_node(self, node):
1626 pass
1627
1628 def ignore_node(self, node):
1629 # ignore nodes that are never complex (can contain only inline nodes)
1630 raise nodes.SkipNode
1631
1632 # Paragraphs and text
1633 visit_Text = ignore_node
1634 visit_paragraph = ignore_node
1635
1636 # Lists
1637 visit_bullet_list = pass_node
1638 visit_enumerated_list = pass_node
1639 visit_docinfo = pass_node
1640
1641 # Docinfo nodes:
1642 visit_author = ignore_node
1643 visit_authors = visit_list_item
1644 visit_address = visit_list_item
1645 visit_contact = pass_node
1646 visit_copyright = ignore_node
1647 visit_date = ignore_node
1648 visit_organization = ignore_node
1649 visit_status = ignore_node
1650 visit_version = visit_list_item
1651
1652 # Definition list:
1653 visit_definition_list = pass_node
1654 visit_definition_list_item = pass_node
1655 visit_term = ignore_node
1656 visit_classifier = pass_node
1657 visit_definition = visit_list_item
1658
1659 # Field list:
1660 visit_field_list = pass_node
1661 visit_field = pass_node
1662 # the field body corresponds to a list item
1663 visit_field_body = visit_list_item
1664 visit_field_name = ignore_node
1665
1666 # Invisible nodes should be ignored.
1667 visit_comment = ignore_node
1668 visit_substitution_definition = ignore_node
1669 visit_target = ignore_node
1670 visit_pending = ignore_node