2 # :Author: Günter Milde <milde@users.berlios.de>
3 # :Revision: $Revision$
4 # :Date: $Date: 2005-06-28$
5 # :Copyright: © 2005, 2009 Günter Milde.
6 # :License: Released under the terms of the `2-Clause BSD license`_, in short:
8 # Copying and distribution of this file, with or without modification,
9 # are permitted in any medium without royalty provided the copyright
10 # notice and this notice are preserved.
11 # This file is offered as-is, without any warranty.
13 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
15 # Use "best practice" as recommended by the W3C:
16 # http://www.w3.org/2009/cheatsheet/
20 Basic HyperText Markup Language document tree Writer.
22 The output conforms to the `HTML 5` specification as well as
23 to `XHTML 1.0 transitional`.
25 The cascading style sheet "minimal.css" is required for proper viewing,
26 the style sheet "plain.css" provides a better reading experience.
28 __docformat__
= 'reStructuredText'
35 try: # check for the Python Imaging Library
38 try: # sometimes PIL modules are put in PYTHONPATH's root
40 class PIL(object): pass # dummy wrapper
45 from docutils
import frontend
, nodes
, utils
, writers
, languages
, io
46 from docutils
.utils
.error_reporting
import SafeString
47 from docutils
.transforms
import writer_aux
48 from docutils
.utils
.math
import (unichar2tex
, pick_math_environment
,
49 math2html
, latex2mathml
, tex2mathml_extern
)
51 class Writer(writers
.Writer
):
53 supported
= ('html', 'html5', 'html4', 'xhtml', 'xhtml10')
54 """Formats this writer supports."""
56 default_stylesheets
= ['minimal.css','plain.css']
57 default_stylesheet_dirs
= ['.', os
.path
.abspath(os
.path
.dirname(__file__
))]
59 default_template
= 'template.txt'
60 default_template_path
= os
.path
.join(
61 os
.path
.dirname(os
.path
.abspath(__file__
)), default_template
)
64 'HTML-Specific Options',
66 (('Specify the template file (UTF-8 encoded). Default is "%s".'
67 % default_template_path
,
69 {'default': default_template_path
, 'metavar': '<file>'}),
70 ('Comma separated list of stylesheet URLs. '
71 'Overrides previous --stylesheet and --stylesheet-path settings.',
73 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
74 'validator': frontend
.validate_comma_separated_list
}),
75 ('Comma separated list of stylesheet paths. '
76 'Relative paths are expanded if a matching file is found in '
77 'the --stylesheet-dirs. With --link-stylesheet, '
78 'the path is rewritten relative to the output HTML file. '
79 'Default: "%s"' % ','.join(default_stylesheets
),
80 ['--stylesheet-path'],
81 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
82 'validator': frontend
.validate_comma_separated_list
,
83 'default': default_stylesheets
}),
84 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
85 'files must be accessible during processing. This is the default.',
86 ['--embed-stylesheet'],
87 {'default': 1, 'action': 'store_true',
88 'validator': frontend
.validate_boolean
}),
89 ('Link to the stylesheet(s) in the output HTML file. '
90 'Default: embed stylesheets.',
91 ['--link-stylesheet'],
92 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
93 ('Comma-separated list of directories where stylesheets are found. '
94 'Used by --stylesheet-path when expanding relative path arguments. '
95 'Default: "%s"' % default_stylesheet_dirs
,
96 ['--stylesheet-dirs'],
97 {'metavar': '<dir[,dir,...]>',
98 'validator': frontend
.validate_comma_separated_list
,
99 'default': default_stylesheet_dirs
}),
100 ('Specify the initial header level. Default is 1 for "<h1>". '
101 'Does not affect document title & subtitle (see --no-doc-title).',
102 ['--initial-header-level'],
103 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
104 'metavar': '<level>'}),
105 ('Format for footnote references: one of "superscript" or '
106 '"brackets". Default is "brackets".',
107 ['--footnote-references'],
108 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
109 'metavar': '<format>',
110 'overrides': 'trim_footnote_reference_space'}),
111 ('Format for block quote attributions: one of "dash" (em-dash '
112 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
114 {'choices': ['dash', 'parentheses', 'parens', 'none'],
115 'default': 'dash', 'metavar': '<format>'}),
116 ('Remove extra vertical whitespace between items of "simple" bullet '
117 'lists and enumerated lists. Default: enabled.',
119 {'default': True, 'action': 'store_true',
120 'validator': frontend
.validate_boolean
}),
121 ('Disable compact simple bullet and enumerated lists.',
122 ['--no-compact-lists'],
123 {'dest': 'compact_lists', 'action': 'store_false'}),
124 ('Remove extra vertical whitespace between items of simple field '
125 'lists. Default: enabled.',
126 ['--compact-field-lists'],
127 {'default': True, 'action': 'store_true',
128 'validator': frontend
.validate_boolean
}),
129 ('Disable compact simple field lists.',
130 ['--no-compact-field-lists'],
131 {'dest': 'compact_field_lists', 'action': 'store_false'}),
132 ('Added to standard table classes. '
133 'Defined styles: "borderless". Default: ""',
136 ('Math output format (one of "MathML", "HTML", "MathJax", '
137 'or "LaTeX") and option(s). '
138 'Default: "HTML math.css"',
140 {'default': 'HTML math.css'}),
141 ('Prepend an XML declaration. (Thwarts HTML5 conformance.) '
143 ['--xml-declaration'],
144 {'default': False, 'action': 'store_true',
145 'validator': frontend
.validate_boolean
}),
146 ('Omit the XML declaration.',
147 ['--no-xml-declaration'],
148 {'dest': 'xml_declaration', 'action': 'store_false'}),
149 ('Obfuscate email addresses to confuse harvesters while still '
150 'keeping email links usable with standards-compliant browsers.',
151 ['--cloak-email-addresses'],
152 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
154 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
156 config_section
= 'html-base writer'
157 config_section_dependencies
= ('writers',)
159 visitor_attributes
= (
160 'head_prefix', 'head', 'stylesheet', 'body_prefix',
161 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
162 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
163 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
166 def get_transforms(self
):
167 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
170 writers
.Writer
.__init
__(self
)
171 self
.translator_class
= HTMLTranslator
174 self
.visitor
= visitor
= self
.translator_class(self
.document
)
175 self
.document
.walkabout(visitor
)
176 for attr
in self
.visitor_attributes
:
177 setattr(self
, attr
, getattr(visitor
, attr
))
178 self
.output
= self
.apply_template()
180 def apply_template(self
):
181 template_file
= open(self
.document
.settings
.template
, 'rb')
182 template
= unicode(template_file
.read(), 'utf-8')
183 template_file
.close()
184 subs
= self
.interpolation_dict()
185 return template
% subs
187 def interpolation_dict(self
):
189 settings
= self
.document
.settings
190 for attr
in self
.visitor_attributes
:
191 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
192 subs
['encoding'] = settings
.output_encoding
193 subs
['version'] = docutils
.__version
__
196 def assemble_parts(self
):
197 writers
.Writer
.assemble_parts(self
)
198 for part
in self
.visitor_attributes
:
199 self
.parts
[part
] = ''.join(getattr(self
, part
))
202 class HTMLTranslator(nodes
.NodeVisitor
):
205 This writer generates `polyglott markup`: HTML 5 that is also valid XML.
208 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
211 doctype_mathml
= doctype
213 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
214 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
215 content_type
= ('<meta http-equiv="Content-Type"'
216 ' content="text/html; charset=%s" />\n')
217 content_type_xml
= ('<meta http-equiv="Content-Type"'
218 ' content="application/xhtml+xml; charset=%s" />\n')
220 generator
= ('<meta name="generator" content="Docutils %s: '
221 'http://docutils.sourceforge.net/" />\n')
223 # Template for the MathJax script in the header:
224 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
225 # The latest version of MathJax from the distributed server:
226 # avaliable to the public under the `MathJax CDN Terms of Service`__
227 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
228 mathjax_url
= ('https://cdn.mathjax.org/mathjax/latest/MathJax.js?'
229 'config=TeX-AMS_CHTML')
230 # may be overwritten by custom URL appended to "mathjax"
232 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
233 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
234 words_and_spaces
= re
.compile(r
'\S+| +|\n')
235 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
236 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
238 def __init__(self
, document
):
239 nodes
.NodeVisitor
.__init
__(self
, document
)
240 self
.settings
= settings
= document
.settings
241 lcode
= settings
.language_code
242 self
.language
= languages
.get_language(lcode
, document
.reporter
)
243 self
.meta
= [self
.generator
% docutils
.__version
__]
244 self
.head_prefix
= []
245 self
.html_prolog
= []
246 if settings
.xml_declaration
:
247 self
.head_prefix
.append(self
.xml_declaration
248 % settings
.output_encoding
)
249 self
.content_type
= self
.content_type_xml
250 # encoding not interpolated:
251 self
.html_prolog
.append(self
.xml_declaration
)
252 self
.head
= self
.meta
[:]
253 self
.stylesheet
= [self
.stylesheet_call(path
)
254 for path
in utils
.get_stylesheet_list(settings
)]
255 self
.body_prefix
= ['</head>\n<body>\n']
256 # document title, subtitle display
257 self
.body_pre_docinfo
= []
262 self
.body_suffix
= ['</body>\n</html>\n']
263 self
.section_level
= 0
264 self
.initial_header_level
= int(settings
.initial_header_level
)
266 self
.math_output
= settings
.math_output
.split()
267 self
.math_output_options
= self
.math_output
[1:]
268 self
.math_output
= self
.math_output
[0].lower()
270 # A heterogenous stack used in conjunction with the tree traversal.
271 # Make sure that the pops correspond to the pushes:
274 self
.topic_classes
= [] # TODO: replace with self_in_contents
276 self
.compact_p
= True
277 self
.compact_simple
= False
278 self
.compact_field_list
= False
279 self
.in_docinfo
= False
280 self
.in_sidebar
= False
281 self
.in_footnote_list
= False
286 self
.html_head
= [self
.content_type
] # charset not interpolated
288 self
.html_subtitle
= []
290 self
.in_document_title
= 0 # len(self.body) or 0
291 self
.in_mailto
= False
292 self
.author_in_authors
= False
293 self
.math_header
= []
296 return ''.join(self
.head_prefix
+ self
.head
297 + self
.stylesheet
+ self
.body_prefix
298 + self
.body_pre_docinfo
+ self
.docinfo
299 + self
.body
+ self
.body_suffix
)
301 def encode(self
, text
):
302 """Encode special characters in `text` & return."""
303 # Use only named entities known in both XML and HTML
304 # other characters are automatically encoded "by number" if required.
306 return text
.translate({
311 ord('@'): u
'@', # may thwart some address harvesters
314 def cloak_mailto(self
, uri
):
315 """Try to hide a mailto: URL from harvesters."""
316 # Encode "@" using a URL octet reference (see RFC 1738).
317 # Further cloaking with HTML entities will be done in the
319 return uri
.replace('@', '%40')
321 def cloak_email(self
, addr
):
322 """Try to hide the link text of a email link from harversters."""
323 # Surround at-signs and periods with <span> tags. ("@" has
324 # already been encoded to "@" by the `encode` method.)
325 addr
= addr
.replace('@', '<span>@</span>')
326 addr
= addr
.replace('.', '<span>.</span>')
329 def attval(self
, text
,
330 whitespace
=re
.compile('[\n\r\t\v\f]')):
331 """Cleanse, HTML encode, and return attribute value text."""
332 encoded
= self
.encode(whitespace
.sub(' ', text
))
333 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
334 # Cloak at-signs ("%40") and periods with HTML entities.
335 encoded
= encoded
.replace('%40', '%40')
336 encoded
= encoded
.replace('.', '.')
339 def stylesheet_call(self
, path
):
340 """Return code to reference or embed stylesheet file `path`"""
341 if self
.settings
.embed_stylesheet
:
343 content
= io
.FileInput(source_path
=path
,
344 encoding
='utf-8').read()
345 self
.settings
.record_dependencies
.add(path
)
347 msg
= u
"Cannot embed stylesheet '%s': %s." % (
348 path
, SafeString(err
.strerror
))
349 self
.document
.reporter
.error(msg
)
350 return '<--- %s --->\n' % msg
351 return self
.embedded_stylesheet
% content
352 # else link to style file:
353 if self
.settings
.stylesheet_path
:
354 # adapt path relative to output (cf. config.html#stylesheet-path)
355 path
= utils
.relative_path(self
.settings
._destination
, path
)
356 return self
.stylesheet_link
% self
.encode(path
)
358 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
360 Construct and return a start tag given a node (id & class attributes
361 are extracted), tag name, and optional attributes.
363 tagname
= tagname
.lower()
367 for (name
, value
) in attributes
.items():
368 atts
[name
.lower()] = value
371 # unify class arguments and move language specification
372 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
373 if cls
.startswith('language-'):
374 languages
.append(cls
[9:])
375 elif cls
.strip() and cls
not in classes
:
378 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
379 atts
[self
.lang_attribute
] = languages
[0]
381 atts
['class'] = ' '.join(classes
)
382 assert 'id' not in atts
383 ids
.extend(node
.get('ids', []))
385 ids
.extend(atts
['ids'])
390 # Add empty "span" elements for additional IDs. Note
391 # that we cannot use empty "a" elements because there
392 # may be targets inside of references, but nested "a"
393 # elements aren't allowed in XHTML (even if they do
394 # not all have a "href" attribute).
396 # Empty tag. Insert target right in front of element.
397 prefix
.append('<span id="%s"></span>' % id)
399 # Non-empty tag. Place the auxiliary <span> tag
400 # *inside* the element, as the first child.
401 suffix
+= '<span id="%s"></span>' % id
402 attlist
= atts
.items()
405 for name
, value
in attlist
:
406 # value=None was used for boolean attributes without
407 # value, but this isn't supported by XHTML.
408 assert value
is not None
409 if isinstance(value
, list):
410 values
= [unicode(v
) for v
in value
]
411 parts
.append('%s="%s"' % (name
.lower(),
412 self
.attval(' '.join(values
))))
414 parts
.append('%s="%s"' % (name
.lower(),
415 self
.attval(unicode(value
))))
420 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
422 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
423 """Construct and return an XML-compatible empty tag."""
424 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
426 def set_class_on_child(self
, node
, class_
, index
=0):
428 Set class `class_` on the visible child no. index of `node`.
429 Do nothing if node has fewer children than `index`.
431 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
433 child
= children
[index
]
436 child
['classes'].append(class_
)
438 def visit_Text(self
, node
):
440 encoded
= self
.encode(text
)
441 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
442 encoded
= self
.cloak_email(encoded
)
443 self
.body
.append(encoded
)
445 def depart_Text(self
, node
):
448 def visit_abbreviation(self
, node
):
449 # @@@ implementation incomplete ("title" attribute)
450 self
.body
.append(self
.starttag(node
, 'abbr', ''))
452 def depart_abbreviation(self
, node
):
453 self
.body
.append('</abbr>')
455 def visit_acronym(self
, node
):
456 # @@@ implementation incomplete ("title" attribute)
457 self
.body
.append(self
.starttag(node
, 'abbr', ''))
459 def depart_acronym(self
, node
):
460 self
.body
.append('</abbr>')
462 def visit_address(self
, node
):
463 self
.visit_docinfo_item(node
, 'address', meta
=False)
464 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='address'))
466 def depart_address(self
, node
):
467 self
.body
.append('\n</pre>\n')
468 self
.depart_docinfo_item()
470 def visit_admonition(self
, node
):
471 node
['classes'].insert(0, 'admonition')
472 self
.body
.append(self
.starttag(node
, 'div'))
474 def depart_admonition(self
, node
=None):
475 self
.body
.append('</div>\n')
477 attribution_formats
= {'dash': (u
'\u2014', ''),
478 'parentheses': ('(', ')'),
479 'parens': ('(', ')'),
482 def visit_attribution(self
, node
):
483 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
484 self
.context
.append(suffix
)
486 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
487 self
.body
.append(self
.starttag(node
, 'cite', ''))
489 def depart_attribution(self
, node
):
490 self
.body
.append('</cite>' + self
.context
.pop() + '</p>\n')
494 # Use paragraphs instead of hard-coded linebreaks.
496 def visit_author(self
, node
):
497 if not(isinstance(node
.parent
, nodes
.authors
)):
498 self
.visit_docinfo_item(node
, 'author')
499 self
.body
.append('<p>')
501 def depart_author(self
, node
):
502 self
.body
.append('</p>')
503 if isinstance(node
.parent
, nodes
.authors
):
504 self
.body
.append('\n')
506 self
.depart_docinfo_item()
508 def visit_authors(self
, node
):
509 self
.visit_docinfo_item(node
, 'authors', meta
=False)
511 def depart_authors(self
, node
):
512 self
.depart_docinfo_item()
514 def visit_block_quote(self
, node
):
515 self
.body
.append(self
.starttag(node
, 'blockquote'))
517 def depart_block_quote(self
, node
):
518 self
.body
.append('</blockquote>\n')
520 def check_simple_list(self
, node
):
521 """Check for a simple list that can be rendered compactly."""
522 visitor
= SimpleListChecker(self
.document
)
525 except nodes
.NodeFound
:
532 # Include definition lists and field lists (in addition to ordered
533 # and unordered lists) in the test if a list is "simple" (cf. the
534 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
535 # the end of this file).
537 def is_compactable(self
, node
):
538 # print "is_compactable %s ?" % node.__class__,
539 # explicite class arguments have precedence
540 if 'compact' in node
['classes']:
541 # print "explicitely compact"
543 if 'open' in node
['classes']:
544 # print "explicitely open"
546 # check config setting:
547 if (isinstance(node
, nodes
.field_list
) or
548 isinstance(node
, nodes
.definition_list
)
549 ) and not self
.settings
.compact_field_lists
:
550 # print "`compact-field-lists` is False"
552 if (isinstance(node
, nodes
.enumerated_list
) or
553 isinstance(node
, nodes
.bullet_list
)
554 ) and not self
.settings
.compact_lists
:
555 # print "`compact-lists` is False"
557 # more special cases:
558 if (self
.topic_classes
== ['contents']): # TODO: self.in_contents
560 # check the list items:
561 visitor
= SimpleListChecker(self
.document
)
564 except nodes
.NodeFound
:
565 # print "complex node"
568 # print "simple list"
571 def visit_bullet_list(self
, node
):
573 old_compact_simple
= self
.compact_simple
574 self
.context
.append((self
.compact_simple
, self
.compact_p
))
575 self
.compact_p
= None
576 self
.compact_simple
= self
.is_compactable(node
)
577 if self
.compact_simple
and not old_compact_simple
:
578 atts
['class'] = 'simple'
579 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
581 def depart_bullet_list(self
, node
):
582 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
583 self
.body
.append('</ul>\n')
585 def visit_caption(self
, node
):
586 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
588 def depart_caption(self
, node
):
589 self
.body
.append('</p>\n')
593 # Use definition list instead of table for bibliographic references.
594 # Join adjacent citation entries.
596 def visit_citation(self
, node
):
597 if not self
.in_footnote_list
:
598 self
.body
.append('<dl class="citation">\n')
599 self
.in_footnote_list
= True
601 def depart_citation(self
, node
):
602 self
.body
.append('</dd>\n')
603 if not isinstance(node
.next_node(descend
=False, siblings
=True),
605 self
.body
.append('</dl>\n')
606 self
.in_footnote_list
= False
608 def visit_citation_reference(self
, 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>')
624 # don't insert classifier-delimiter here (done by CSS)
626 def visit_classifier(self
, node
):
627 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
629 def depart_classifier(self
, node
):
630 self
.body
.append('</span>')
632 def visit_colspec(self
, node
):
633 self
.colspecs
.append(node
)
634 # "stubs" list is an attribute of the tgroup element:
635 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
637 def depart_colspec(self
, node
):
640 def write_colspecs(self
):
642 for node
in self
.colspecs
:
643 width
+= node
['colwidth']
644 for node
in self
.colspecs
:
645 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
646 self
.body
.append(self
.emptytag(node
, 'col',
647 style
='width: %i%%' % colwidth
))
650 def visit_comment(self
, node
,
651 sub
=re
.compile('-(?=-)').sub
):
652 """Escape double-dashes in comment text."""
653 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
654 # Content already processed:
657 def visit_compound(self
, node
):
658 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
660 node
[0]['classes'].append('compound-first')
661 node
[-1]['classes'].append('compound-last')
662 for child
in node
[1:-1]:
663 child
['classes'].append('compound-middle')
665 def depart_compound(self
, node
):
666 self
.body
.append('</div>\n')
668 def visit_container(self
, node
):
669 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
671 def depart_container(self
, node
):
672 self
.body
.append('</div>\n')
674 def visit_contact(self
, node
):
675 self
.visit_docinfo_item(node
, 'contact', meta
=False)
677 def depart_contact(self
, node
):
678 self
.depart_docinfo_item()
680 def visit_copyright(self
, node
):
681 self
.visit_docinfo_item(node
, 'copyright', meta
=False)
683 def depart_copyright(self
, node
):
684 self
.depart_docinfo_item()
686 def visit_date(self
, node
):
687 self
.visit_docinfo_item(node
, 'date', meta
=False)
689 def depart_date(self
, node
):
690 self
.depart_docinfo_item()
692 def visit_decoration(self
, node
):
695 def depart_decoration(self
, node
):
698 def visit_definition(self
, node
):
699 self
.body
.append('</dt>\n')
700 self
.body
.append(self
.starttag(node
, 'dd', ''))
702 def depart_definition(self
, node
):
703 self
.body
.append('</dd>\n')
705 def visit_definition_list(self
, node
):
706 classes
= node
.setdefault('classes', [])
707 if self
.is_compactable(node
):
708 classes
.append('simple')
709 self
.body
.append(self
.starttag(node
, 'dl'))
711 def depart_definition_list(self
, node
):
712 self
.body
.append('</dl>\n')
714 def visit_definition_list_item(self
, node
):
715 # pass class arguments, ids and names to definition term:
716 node
.children
[0]['classes'] = (
717 node
.get('classes', []) + node
.children
[0].get('classes', []))
718 node
.children
[0]['ids'] = (
719 node
.get('ids', []) + node
.children
[0].get('ids', []))
720 node
.children
[0]['names'] = (
721 node
.get('names', []) + node
.children
[0].get('names', []))
723 def depart_definition_list_item(self
, node
):
726 def visit_description(self
, node
):
727 self
.body
.append(self
.starttag(node
, 'dd', ''))
729 def depart_description(self
, node
):
730 self
.body
.append('</dd>\n')
735 # use definition list instead of table
737 def visit_docinfo(self
, node
):
739 if (self
.is_compactable(node
)):
741 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
743 def depart_docinfo(self
, node
):
744 self
.body
.append('</dl>\n')
746 def visit_docinfo_item(self
, node
, name
, meta
=True):
748 meta_tag
= '<meta name="%s" content="%s" />\n' \
749 % (name
, self
.attval(node
.astext()))
750 self
.add_meta(meta_tag
)
751 self
.body
.append('<dt class="%s">%s</dt>\n'
752 % (name
, self
.language
.labels
[name
]))
753 self
.body
.append(self
.starttag(node
, 'dd', '', CLASS
=name
))
755 def depart_docinfo_item(self
):
756 self
.body
.append('</dd>\n')
758 def visit_doctest_block(self
, node
):
759 self
.body
.append(self
.starttag(node
, 'pre', suffix
='',
760 CLASS
='code python doctest'))
762 def depart_doctest_block(self
, node
):
763 self
.body
.append('\n</pre>\n')
765 def visit_document(self
, node
):
766 self
.head
.append('<title>%s</title>\n'
767 % self
.encode(node
.get('title', '')))
769 def depart_document(self
, node
):
770 self
.head_prefix
.extend([self
.doctype
,
771 self
.head_prefix_template
%
772 {'lang': self
.settings
.language_code
}])
773 self
.html_prolog
.append(self
.doctype
)
774 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
775 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
777 if self
.math_output
== 'mathjax':
778 self
.head
.extend(self
.math_header
)
780 self
.stylesheet
.extend(self
.math_header
)
781 # skip content-type meta tag with interpolated charset value:
782 self
.html_head
.extend(self
.head
[1:])
783 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
784 self
.body_suffix
.insert(0, '</div>\n')
785 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
786 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
787 + self
.docinfo
+ self
.body
788 + self
.body_suffix
[:-1])
789 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
791 def visit_emphasis(self
, node
):
792 self
.body
.append(self
.starttag(node
, 'em', ''))
794 def depart_emphasis(self
, node
):
795 self
.body
.append('</em>')
797 def visit_entry(self
, node
):
799 if isinstance(node
.parent
.parent
, nodes
.thead
):
800 atts
['class'].append('head')
801 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
802 # "stubs" list is an attribute of the tgroup element
803 atts
['class'].append('stub')
806 atts
['class'] = ' '.join(atts
['class'])
810 node
.parent
.column
+= 1
811 if 'morerows' in node
:
812 atts
['rowspan'] = node
['morerows'] + 1
813 if 'morecols' in node
:
814 atts
['colspan'] = node
['morecols'] + 1
815 node
.parent
.column
+= node
['morecols']
816 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
817 self
.context
.append('</%s>\n' % tagname
.lower())
818 # TODO: why did the html4css1 writer insert an NBSP into empty cells?
819 # if len(node) == 0: # empty cell
820 # self.body.append(' ') # no-break space
822 def depart_entry(self
, node
):
823 self
.body
.append(self
.context
.pop())
825 def visit_enumerated_list(self
, node
):
827 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
828 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
833 atts
['start'] = node
['start']
834 if 'enumtype' in node
:
835 atts
['class'] = node
['enumtype']
836 if self
.is_compactable(node
):
837 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
838 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
840 def depart_enumerated_list(self
, node
):
841 self
.body
.append('</ol>\n')
845 # set as definition list, styled with CSS
847 def visit_field_list(self
, node
):
848 # Keep simple paragraphs in the field_body to enable CSS
849 # rule to start body on new line if the label is too long
850 classes
= 'field-list'
851 if (self
.is_compactable(node
)):
853 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
855 def depart_field_list(self
, node
):
856 self
.body
.append('</dl>\n')
858 def visit_field(self
, node
):
861 def depart_field(self
, node
):
864 # as field is ignored, pass class arguments to field-name and field-body:
866 def visit_field_name(self
, node
):
867 self
.body
.append(self
.starttag(node
, 'dt', '',
868 CLASS
=''.join(node
.parent
['classes'])))
870 def depart_field_name(self
, node
):
871 self
.body
.append('</dt>\n')
873 def visit_field_body(self
, node
):
874 self
.body
.append(self
.starttag(node
, 'dd', '',
875 CLASS
=''.join(node
.parent
['classes'])))
877 def depart_field_body(self
, node
):
878 self
.body
.append('</dd>\n')
880 def visit_figure(self
, node
):
881 atts
= {'class': 'figure'}
882 if node
.get('width'):
883 atts
['style'] = 'width: %s' % node
['width']
884 if node
.get('align'):
885 atts
['class'] += " align-" + node
['align']
886 self
.body
.append(self
.starttag(node
, 'div', **atts
))
888 def depart_figure(self
, node
):
889 self
.body
.append('</div>\n')
891 # use HTML 5 <footer> element?
892 def visit_footer(self
, node
):
893 self
.context
.append(len(self
.body
))
895 def depart_footer(self
, node
):
896 start
= self
.context
.pop()
897 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
898 '<hr class="footer" />\n']
899 footer
.extend(self
.body
[start
:])
900 footer
.append('\n</div>\n')
901 self
.footer
.extend(footer
)
902 self
.body_suffix
[:0] = footer
903 del self
.body
[start
:]
907 # use definition list instead of table for footnote text
909 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
910 def visit_footnote(self
, node
):
911 if not self
.in_footnote_list
:
912 classes
= 'footnote ' + self
.settings
.footnote_references
913 self
.body
.append('<dl class="%s">\n'%classes
)
914 self
.in_footnote_list
= True
916 def depart_footnote(self
, node
):
917 self
.body
.append('</dd>\n')
918 if not isinstance(node
.next_node(descend
=False, siblings
=True),
920 self
.body
.append('</dl>\n')
921 self
.in_footnote_list
= False
923 def visit_footnote_reference(self
, node
):
924 href
= '#' + node
['refid']
925 classes
= 'footnote-reference ' + self
.settings
.footnote_references
926 self
.body
.append(self
.starttag(node
, 'a', '', #suffix,
927 CLASS
=classes
, href
=href
))
929 def depart_footnote_reference(self
, node
):
930 # self.body.append(self.context.pop() + '</a>')
931 self
.body
.append('</a>')
933 def visit_generated(self
, node
):
934 if 'sectnum' in node
['classes']:
935 # get section number (strip trailing no-break-spaces)
936 sectnum
= node
.astext().rstrip(u
' ')
937 # print sectnum.encode('utf-8')
938 self
.body
.append('<span class="sectnum">%s</span> '
939 % self
.encode(sectnum
))
940 # Content already processed:
943 def depart_generated(self
, node
):
946 def visit_header(self
, node
):
947 self
.context
.append(len(self
.body
))
949 def depart_header(self
, node
):
950 start
= self
.context
.pop()
951 header
= [self
.starttag(node
, 'div', CLASS
='header')]
952 header
.extend(self
.body
[start
:])
953 header
.append('\n<hr class="header"/>\n</div>\n')
954 self
.body_prefix
.extend(header
)
955 self
.header
.extend(header
)
956 del self
.body
[start
:]
958 # Image types to place in an <object> element
959 # SVG not supported by IE up to version 8
960 # (html4css1 strives for IE6 compatibility)
961 object_image_types
= {#'.svg': 'image/svg+xml',
962 '.swf': 'application/x-shockwave-flash'}
964 def visit_image(self
, node
):
967 ext
= os
.path
.splitext(uri
)[1].lower()
968 if ext
in self
.object_image_types
:
970 atts
['type'] = self
.object_image_types
[ext
]
973 atts
['alt'] = node
.get('alt', uri
)
976 atts
['width'] = node
['width']
978 atts
['height'] = node
['height']
980 if (PIL
and not ('width' in node
and 'height' in node
)
981 and self
.settings
.file_insertion_enabled
):
982 imagepath
= urllib
.url2pathname(uri
)
984 img
= PIL
.Image
.open(
985 imagepath
.encode(sys
.getfilesystemencoding()))
986 except (IOError, UnicodeEncodeError):
989 self
.settings
.record_dependencies
.add(
990 imagepath
.replace('\\', '/'))
991 if 'width' not in atts
:
992 atts
['width'] = '%dpx' % img
.size
[0]
993 if 'height' not in atts
:
994 atts
['height'] = '%dpx' % img
.size
[1]
996 for att_name
in 'width', 'height':
998 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1000 atts
[att_name
] = '%s%s' % (
1001 float(match
.group(1)) * (float(node
['scale']) / 100),
1004 for att_name
in 'width', 'height':
1005 if att_name
in atts
:
1006 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1007 # Interpret unitless values as pixels.
1008 atts
[att_name
] += 'px'
1009 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1012 atts
['style'] = ' '.join(style
)
1013 if (isinstance(node
.parent
, nodes
.TextElement
) or
1014 (isinstance(node
.parent
, nodes
.reference
) and
1015 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1016 # Inline context or surrounded by <a>...</a>.
1021 atts
['class'] = 'align-%s' % node
['align']
1022 if ext
in self
.object_image_types
:
1023 # do NOT use an empty tag: incorrect rendering in browsers
1024 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1025 node
.get('alt', uri
) + '</object>' + suffix
)
1027 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1029 def depart_image(self
, node
):
1030 # self.body.append(self.context.pop())
1033 def visit_inline(self
, node
):
1034 self
.body
.append(self
.starttag(node
, 'span', ''))
1036 def depart_inline(self
, node
):
1037 self
.body
.append('</span>')
1039 # footnote and citation labels:
1040 def visit_label(self
, node
):
1041 if (isinstance(node
.parent
, nodes
.footnote
)):
1042 classes
= self
.settings
.footnote_references
1044 classes
= 'brackets'
1045 # pass parent node to get id into starttag:
1046 self
.body
.append(self
.starttag(node
.parent
, 'dt', '', CLASS
='label'))
1047 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
=classes
))
1048 # footnote/citation backrefs:
1049 if self
.settings
.footnote_backlinks
:
1050 backrefs
= node
.parent
['backrefs']
1051 if len(backrefs
) == 1:
1052 self
.body
.append('<a class="fn-backref" href="#%s">'
1055 def depart_label(self
, node
):
1056 self
.body
.append('</span>')
1057 if self
.settings
.footnote_backlinks
:
1058 backrefs
= node
.parent
['backrefs']
1059 if len(backrefs
) == 1:
1060 self
.body
.append('</a>')
1061 elif len(backrefs
) > 1:
1062 # Python 2.4 fails with enumerate(backrefs, 1)
1063 backlinks
= ['<a href="#%s">%s</a>' % (ref
, i
+1)
1064 for (i
, ref
) in enumerate(backrefs
)]
1065 self
.body
.append('<span class="fn-backref">(%s)</span>'
1066 % ','.join(backlinks
))
1067 self
.body
.append('</dt>\n<dd>')
1069 def visit_legend(self
, node
):
1070 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1072 def depart_legend(self
, node
):
1073 self
.body
.append('</div>\n')
1075 def visit_line(self
, node
):
1076 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1078 self
.body
.append('<br />')
1080 def depart_line(self
, node
):
1081 self
.body
.append('</div>\n')
1083 def visit_line_block(self
, node
):
1084 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1086 def depart_line_block(self
, node
):
1087 self
.body
.append('</div>\n')
1089 def visit_list_item(self
, node
):
1090 self
.body
.append(self
.starttag(node
, 'li', ''))
1092 def depart_list_item(self
, node
):
1093 self
.body
.append('</li>\n')
1096 def visit_literal(self
, node
):
1097 # special case: "code" role
1098 classes
= node
.get('classes', [])
1099 if 'code' in classes
:
1100 # filter 'code' from class arguments
1101 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1102 self
.body
.append(self
.starttag(node
, 'code', ''))
1105 self
.starttag(node
, 'span', '', CLASS
='docutils literal'))
1106 text
= node
.astext()
1107 # remove hard line breaks (except if in a parsed-literal block)
1108 if not isinstance(node
.parent
, nodes
.literal_block
):
1109 text
= text
.replace('\n', ' ')
1110 # Protect text like ``--an-option`` and the regular expression
1111 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1112 for token
in self
.words_and_spaces
.findall(text
):
1113 if token
.strip() and self
.sollbruchstelle
.search(token
):
1114 self
.body
.append('<span class="pre">%s</span>'
1115 % self
.encode(token
))
1117 self
.body
.append(self
.encode(token
))
1118 self
.body
.append('</span>')
1119 # Content already processed:
1120 raise nodes
.SkipNode
1122 def depart_literal(self
, node
):
1123 # skipped unless literal element is from "code" role:
1124 self
.body
.append('</code>')
1126 def visit_literal_block(self
, node
):
1127 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='literal-block'))
1128 if 'code' in node
.get('classes', []):
1129 self
.body
.append('<code>')
1131 def depart_literal_block(self
, node
):
1132 if 'code' in node
.get('classes', []):
1133 self
.body
.append('</code>')
1134 self
.body
.append('</pre>\n')
1137 # As there is no native HTML math support, we provide alternatives
1138 # for the math-output: LaTeX and MathJax simply wrap the content,
1139 # HTML and MathML also convert the math_code.
1141 math_tags
= {# math_output: (block, inline, class-arguments)
1142 'mathml': ('div', '', ''),
1143 'html': ('div', 'span', 'formula'),
1144 'mathjax': ('div', 'span', 'math'),
1145 'latex': ('pre', 'tt', 'math'),
1148 def visit_math(self
, node
, math_env
=''):
1149 # If the method is called from visit_math_block(), math_env != ''.
1151 if self
.math_output
not in self
.math_tags
:
1152 self
.document
.reporter
.error(
1153 'math-output format "%s" not supported '
1154 'falling back to "latex"'% self
.math_output
)
1155 self
.math_output
= 'latex'
1156 tag
= self
.math_tags
[self
.math_output
][math_env
== '']
1157 clsarg
= self
.math_tags
[self
.math_output
][2]
1159 wrappers
= {# math_mode: (inline, block)
1160 'mathml': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1161 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1162 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1163 'latex': (None, None),
1165 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1166 if self
.math_output
== 'mathml' and (not self
.math_output_options
or
1167 self
.math_output_options
[0] == 'blahtexml'):
1169 # get and wrap content
1170 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1172 try: # wrapper with three "%s"
1173 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1174 except TypeError: # wrapper with one "%s"
1175 math_code
= wrapper
% math_code
1176 # settings and conversion
1177 if self
.math_output
in ('latex', 'mathjax'):
1178 math_code
= self
.encode(math_code
)
1179 if self
.math_output
== 'mathjax' and not self
.math_header
:
1180 if self
.math_output_options
:
1181 self
.mathjax_url
= self
.math_output_options
[0]
1182 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1183 elif self
.math_output
== 'html':
1184 if self
.math_output_options
and not self
.math_header
:
1185 self
.math_header
= [self
.stylesheet_call(
1186 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1187 for s
in self
.math_output_options
[0].split(',')]
1188 # TODO: fix display mode in matrices and fractions
1189 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1190 math_code
= math2html
.math2html(math_code
)
1191 elif self
.math_output
== 'mathml':
1192 self
.doctype
= self
.doctype_mathml
1193 # self.content_type = self.content_type_mathml
1194 converter
= ' '.join(self
.math_output_options
).lower()
1196 if converter
== 'latexml':
1197 math_code
= tex2mathml_extern
.latexml(math_code
,
1198 self
.document
.reporter
)
1199 elif converter
== 'ttm':
1200 math_code
= tex2mathml_extern
.ttm(math_code
,
1201 self
.document
.reporter
)
1202 elif converter
== 'blahtexml':
1203 math_code
= tex2mathml_extern
.blahtexml(math_code
,
1204 inline
=not(math_env
),
1205 reporter
=self
.document
.reporter
)
1207 math_code
= latex2mathml
.tex2mathml(math_code
,
1208 inline
=not(math_env
))
1210 self
.document
.reporter
.error('option "%s" not supported '
1211 'with math-output "MathML"')
1213 raise OSError('is "latexmlmath" in your PATH?')
1214 except SyntaxError, err
:
1215 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1216 self
.visit_system_message(err_node
)
1217 self
.body
.append(self
.starttag(node
, 'p'))
1218 self
.body
.append(u
','.join(err
.args
))
1219 self
.body
.append('</p>\n')
1220 self
.body
.append(self
.starttag(node
, 'pre',
1221 CLASS
='literal-block'))
1222 self
.body
.append(self
.encode(math_code
))
1223 self
.body
.append('\n</pre>\n')
1224 self
.depart_system_message(err_node
)
1225 raise nodes
.SkipNode
1226 # append to document body
1228 self
.body
.append(self
.starttag(node
, tag
,
1229 suffix
='\n'*bool(math_env
),
1231 self
.body
.append(math_code
)
1232 if math_env
: # block mode (equation, display)
1233 self
.body
.append('\n')
1235 self
.body
.append('</%s>' % tag
)
1237 self
.body
.append('\n')
1238 # Content already processed:
1239 raise nodes
.SkipNode
1241 def depart_math(self
, node
):
1242 pass # never reached
1244 def visit_math_block(self
, node
):
1245 # print node.astext().encode('utf8')
1246 math_env
= pick_math_environment(node
.astext())
1247 self
.visit_math(node
, math_env
=math_env
)
1249 def depart_math_block(self
, node
):
1250 pass # never reached
1252 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1253 # HTML5/polyglott recommends using both
1254 def visit_meta(self
, node
):
1255 if node
.hasattr('lang'):
1256 node
['xml:lang'] = node
['lang']
1258 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1261 def depart_meta(self
, node
):
1264 def add_meta(self
, tag
):
1265 self
.meta
.append(tag
)
1266 self
.head
.append(tag
)
1268 def visit_option(self
, node
):
1269 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1271 def depart_option(self
, node
):
1272 self
.body
.append('</span>')
1273 if isinstance(node
.next_node(descend
=False, siblings
=True),
1275 self
.body
.append(', ')
1277 def visit_option_argument(self
, node
):
1278 self
.body
.append(node
.get('delimiter', ' '))
1279 self
.body
.append(self
.starttag(node
, 'var', ''))
1281 def depart_option_argument(self
, node
):
1282 self
.body
.append('</var>')
1284 def visit_option_group(self
, node
):
1285 self
.body
.append(self
.starttag(node
, 'dt', ''))
1286 self
.body
.append('<kbd>')
1288 def depart_option_group(self
, node
):
1289 self
.body
.append('</kbd></dt>\n')
1291 def visit_option_list(self
, node
):
1293 self
.starttag(node
, 'dl', CLASS
='option-list'))
1295 def depart_option_list(self
, node
):
1296 self
.body
.append('</dl>\n')
1298 def visit_option_list_item(self
, node
):
1301 def depart_option_list_item(self
, node
):
1304 def visit_option_string(self
, node
):
1307 def depart_option_string(self
, node
):
1310 def visit_organization(self
, node
):
1311 self
.visit_docinfo_item(node
, 'organization', meta
=False)
1313 def depart_organization(self
, node
):
1314 self
.depart_docinfo_item()
1316 # Do not omit <p> tags
1317 # --------------------
1319 # The HTML4CSS1 writer does this to "produce
1320 # visually compact lists (less vertical whitespace)". This writer
1321 # relies on CSS rules for"visual compactness".
1323 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1324 # character data, so you cannot drop the <p> tags.
1325 # * Keeping simple paragraphs in the field_body enables a CSS
1326 # rule to start the field-body on a new line if the label is too long
1327 # * it makes the code simpler.
1329 # TODO: omit paragraph tags in simple table cells?
1331 def visit_paragraph(self
, node
):
1332 self
.body
.append(self
.starttag(node
, 'p', ''))
1334 def depart_paragraph(self
, node
):
1335 self
.body
.append('</p>')
1336 if not (isinstance(node
.parent
, (nodes
.list_item
, nodes
.entry
)) and
1337 (len(node
.parent
) == 1)):
1338 self
.body
.append('\n')
1340 def visit_problematic(self
, node
):
1341 if node
.hasattr('refid'):
1342 self
.body
.append('<a href="#%s">' % node
['refid'])
1343 self
.context
.append('</a>')
1345 self
.context
.append('')
1346 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1348 def depart_problematic(self
, node
):
1349 self
.body
.append('</span>')
1350 self
.body
.append(self
.context
.pop())
1352 def visit_raw(self
, node
):
1353 if 'html' in node
.get('format', '').split():
1354 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1356 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1357 self
.body
.append(node
.astext())
1359 self
.body
.append('</%s>' % t
)
1360 # Keep non-HTML raw text out of output:
1361 raise nodes
.SkipNode
1363 def visit_reference(self
, node
):
1364 atts
= {'class': 'reference'}
1365 if 'refuri' in node
:
1366 atts
['href'] = node
['refuri']
1367 if ( self
.settings
.cloak_email_addresses
1368 and atts
['href'].startswith('mailto:')):
1369 atts
['href'] = self
.cloak_mailto(atts
['href'])
1370 self
.in_mailto
= True
1371 atts
['class'] += ' external'
1373 assert 'refid' in node
, \
1374 'References must have "refuri" or "refid" attribute.'
1375 atts
['href'] = '#' + node
['refid']
1376 atts
['class'] += ' internal'
1377 if not isinstance(node
.parent
, nodes
.TextElement
):
1378 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1379 atts
['class'] += ' image-reference'
1380 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1382 def depart_reference(self
, node
):
1383 self
.body
.append('</a>')
1384 if not isinstance(node
.parent
, nodes
.TextElement
):
1385 self
.body
.append('\n')
1386 self
.in_mailto
= False
1388 def visit_revision(self
, node
):
1389 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1391 def depart_revision(self
, node
):
1392 self
.depart_docinfo_item()
1394 def visit_row(self
, node
):
1395 self
.body
.append(self
.starttag(node
, 'tr', ''))
1398 def depart_row(self
, node
):
1399 self
.body
.append('</tr>\n')
1401 def visit_rubric(self
, node
):
1402 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1404 def depart_rubric(self
, node
):
1405 self
.body
.append('</p>\n')
1407 # TODO: use the new HTML 5 element <section>?
1408 def visit_section(self
, node
):
1409 self
.section_level
+= 1
1411 self
.starttag(node
, 'div', CLASS
='section'))
1413 def depart_section(self
, node
):
1414 self
.section_level
-= 1
1415 self
.body
.append('</div>\n')
1417 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1418 def visit_sidebar(self
, node
):
1420 self
.starttag(node
, 'div', CLASS
='sidebar'))
1421 self
.in_sidebar
= True
1423 def depart_sidebar(self
, node
):
1424 self
.body
.append('</div>\n')
1425 self
.in_sidebar
= False
1427 def visit_status(self
, node
):
1428 self
.visit_docinfo_item(node
, 'status', meta
=False)
1430 def depart_status(self
, node
):
1431 self
.depart_docinfo_item()
1433 def visit_strong(self
, node
):
1434 self
.body
.append(self
.starttag(node
, 'strong', ''))
1436 def depart_strong(self
, node
):
1437 self
.body
.append('</strong>')
1439 def visit_subscript(self
, node
):
1440 self
.body
.append(self
.starttag(node
, 'sub', ''))
1442 def depart_subscript(self
, node
):
1443 self
.body
.append('</sub>')
1445 def visit_substitution_definition(self
, node
):
1446 """Internal only."""
1447 raise nodes
.SkipNode
1449 def visit_substitution_reference(self
, node
):
1450 self
.unimplemented_visit(node
)
1452 # h1–h6 elements must not be used to markup subheadings, subtitles,
1453 # alternative titles and taglines unless intended to be the heading for a
1454 # new section or subsection.
1455 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1457 def visit_subtitle(self
, node
):
1458 if isinstance(node
.parent
, nodes
.sidebar
):
1459 classes
= 'sidebar-subtitle'
1460 elif isinstance(node
.parent
, nodes
.document
):
1461 classes
= 'subtitle'
1462 self
.in_document_title
= len(self
.body
)
1463 elif isinstance(node
.parent
, nodes
.section
):
1464 classes
= 'section-subtitle'
1465 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
=classes
))
1467 def depart_subtitle(self
, node
):
1468 self
.body
.append('</p>\n')
1469 if self
.in_document_title
:
1470 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1471 self
.in_document_title
= 0
1472 self
.body_pre_docinfo
.extend(self
.body
)
1473 self
.html_subtitle
.extend(self
.body
)
1476 def visit_superscript(self
, node
):
1477 self
.body
.append(self
.starttag(node
, 'sup', ''))
1479 def depart_superscript(self
, node
):
1480 self
.body
.append('</sup>')
1482 def visit_system_message(self
, node
):
1483 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1484 self
.body
.append('<p class="system-message-title">')
1486 if len(node
['backrefs']):
1487 backrefs
= node
['backrefs']
1488 if len(backrefs
) == 1:
1489 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1494 for backref
in backrefs
:
1495 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1497 backref_text
= ('; <em>backlinks: %s</em>'
1498 % ', '.join(backlinks
))
1499 if node
.hasattr('line'):
1500 line
= ', line %s' % node
['line']
1503 self
.body
.append('System Message: %s/%s '
1504 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1505 % (node
['type'], node
['level'],
1506 self
.encode(node
['source']), line
, backref_text
))
1508 def depart_system_message(self
, node
):
1509 self
.body
.append('</div>\n')
1513 # no hard-coded border setting in the table head::
1515 def visit_table(self
, node
):
1516 classes
= [cls
.strip(u
' \t\n')
1517 for cls
in self
.settings
.table_style
.split(',')]
1518 tag
= self
.starttag(node
, 'table', CLASS
=' '.join(classes
))
1519 self
.body
.append(tag
)
1521 def depart_table(self
, node
):
1522 self
.body
.append('</table>\n')
1524 def visit_target(self
, node
):
1525 if not ('refuri' in node
or 'refid' in node
1526 or 'refname' in node
):
1527 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1528 self
.context
.append('</span>')
1530 self
.context
.append('')
1532 def depart_target(self
, node
):
1533 self
.body
.append(self
.context
.pop())
1535 # no hard-coded vertical alignment in table body::
1537 def visit_tbody(self
, node
):
1538 self
.write_colspecs()
1539 self
.body
.append(self
.context
.pop()) # '</colgroup>\n' or ''
1540 self
.body
.append(self
.starttag(node
, 'tbody'))
1542 def depart_tbody(self
, node
):
1543 self
.body
.append('</tbody>\n')
1545 def visit_term(self
, node
):
1546 self
.body
.append(self
.starttag(node
, 'dt', ''))
1548 def depart_term(self
, node
):
1550 Leave the end tag to `self.visit_definition()`, in case there's a
1555 def visit_tgroup(self
, node
):
1556 # Mozilla needs <colgroup>:
1557 self
.body
.append(self
.starttag(node
, 'colgroup'))
1558 # Appended by thead or tbody:
1559 self
.context
.append('</colgroup>\n')
1562 def depart_tgroup(self
, node
):
1565 def visit_thead(self
, node
):
1566 self
.write_colspecs()
1567 self
.body
.append(self
.context
.pop()) # '</colgroup>\n'
1568 # There may or may not be a <thead>; this is for <tbody> to use:
1569 self
.context
.append('')
1570 self
.body
.append(self
.starttag(node
, 'thead'))
1572 def depart_thead(self
, node
):
1573 self
.body
.append('</thead>\n')
1575 def visit_title(self
, node
):
1576 """Only 6 section levels are supported by HTML."""
1577 check_id
= 0 # TODO: is this a bool (False) or a counter?
1578 close_tag
= '</p>\n'
1579 if isinstance(node
.parent
, nodes
.topic
):
1581 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1582 elif isinstance(node
.parent
, nodes
.sidebar
):
1584 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1585 elif isinstance(node
.parent
, nodes
.Admonition
):
1587 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1588 elif isinstance(node
.parent
, nodes
.table
):
1590 self
.starttag(node
, 'caption', ''))
1591 close_tag
= '</caption>\n'
1592 elif isinstance(node
.parent
, nodes
.document
):
1593 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1594 close_tag
= '</h1>\n'
1595 self
.in_document_title
= len(self
.body
)
1597 assert isinstance(node
.parent
, nodes
.section
)
1598 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1600 if (len(node
.parent
) >= 2 and
1601 isinstance(node
.parent
[1], nodes
.subtitle
)):
1602 atts
['CLASS'] = 'with-subtitle'
1604 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1606 if node
.hasattr('refid'):
1607 atts
['class'] = 'toc-backref'
1608 atts
['href'] = '#' + node
['refid']
1610 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1611 close_tag
= '</a></h%s>\n' % (h_level
)
1613 close_tag
= '</h%s>\n' % (h_level
)
1614 self
.context
.append(close_tag
)
1616 def depart_title(self
, node
):
1617 self
.body
.append(self
.context
.pop())
1618 if self
.in_document_title
:
1619 self
.title
= self
.body
[self
.in_document_title
:-1]
1620 self
.in_document_title
= 0
1621 self
.body_pre_docinfo
.extend(self
.body
)
1622 self
.html_title
.extend(self
.body
)
1625 def visit_title_reference(self
, node
):
1626 self
.body
.append(self
.starttag(node
, 'cite', ''))
1628 def depart_title_reference(self
, node
):
1629 self
.body
.append('</cite>')
1631 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1632 def visit_topic(self
, node
):
1633 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1634 self
.topic_classes
= node
['classes']
1635 # TODO: replace with ::
1636 # self.in_contents = 'contents' in node['classes']
1638 def depart_topic(self
, node
):
1639 self
.body
.append('</div>\n')
1640 self
.topic_classes
= []
1641 # TODO self.in_contents = False
1643 def visit_transition(self
, node
):
1644 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1646 def depart_transition(self
, node
):
1649 def visit_version(self
, node
):
1650 self
.visit_docinfo_item(node
, 'version', meta
=False)
1652 def depart_version(self
, node
):
1653 self
.depart_docinfo_item()
1655 def unimplemented_visit(self
, node
):
1656 raise NotImplementedError('visiting unimplemented node type: %s'
1657 % node
.__class
__.__name
__)
1660 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1663 Raise `nodes.NodeFound` if non-simple list item is encountered.
1665 Here "simple" means a list item containing nothing other than a single
1666 paragraph, a simple list, or a paragraph followed by a simple list.
1668 This version also checks for simple field lists and docinfo.
1671 def default_visit(self
, node
):
1672 raise nodes
.NodeFound
1674 def visit_list_item(self
, node
):
1675 # print "visiting list item", node.__class__
1676 children
= [child
for child
in node
.children
1677 if not isinstance(child
, nodes
.Invisible
)]
1678 # print "has %s visible children" % len(children)
1679 if (children
and isinstance(children
[0], nodes
.paragraph
)
1680 and (isinstance(children
[-1], nodes
.bullet_list
) or
1681 isinstance(children
[-1], nodes
.enumerated_list
) or
1682 isinstance(children
[-1], nodes
.field_list
))):
1684 # print "%s children remain" % len(children)
1685 if len(children
) <= 1:
1688 # print "found", child.__class__, "in", node.__class__
1689 raise nodes
.NodeFound
1691 def pass_node(self
, node
):
1694 def ignore_node(self
, node
):
1695 # ignore nodes that are never complex (can contain only inline nodes)
1696 raise nodes
.SkipNode
1698 # Paragraphs and text
1699 visit_Text
= ignore_node
1700 visit_paragraph
= ignore_node
1703 visit_bullet_list
= pass_node
1704 visit_enumerated_list
= pass_node
1705 visit_docinfo
= pass_node
1708 visit_author
= ignore_node
1709 visit_authors
= visit_list_item
1710 visit_address
= visit_list_item
1711 visit_contact
= pass_node
1712 visit_copyright
= ignore_node
1713 visit_date
= ignore_node
1714 visit_organization
= ignore_node
1715 visit_status
= ignore_node
1716 visit_version
= visit_list_item
1719 visit_definition_list
= pass_node
1720 visit_definition_list_item
= pass_node
1721 visit_term
= ignore_node
1722 visit_classifier
= pass_node
1723 visit_definition
= visit_list_item
1726 visit_field_list
= pass_node
1727 visit_field
= pass_node
1728 # the field body corresponds to a list item
1729 visit_field_body
= visit_list_item
1730 visit_field_name
= ignore_node
1732 # Invisible nodes should be ignored.
1733 visit_comment
= ignore_node
1734 visit_substitution_definition
= ignore_node
1735 visit_target
= ignore_node
1736 visit_pending
= ignore_node