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$
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:
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.
16 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
18 """common definitions for Docutils HTML writers"""
25 try: # check for the Python Imaging Library
28 try: # sometimes PIL modules are put in PYTHONPATH's root
30 class PIL(object): pass # dummy wrapper
36 from docutils
import nodes
, utils
, writers
, languages
, io
37 from docutils
.utils
.error_reporting
import SafeString
38 from docutils
.transforms
import writer_aux
39 from docutils
.utils
.math
import (unichar2tex
, pick_math_environment
,
40 math2html
, latex2mathml
, tex2mathml_extern
)
43 class Writer(writers
.Writer
):
45 supported
= ('html', 'xhtml') # update in subclass
46 """Formats this writer supports."""
48 # default_stylesheets = [] # set in subclass!
49 # default_stylesheet_dirs = ['.'] # set in subclass!
50 default_template
= 'template.txt'
51 # default_template_path = ... # set in subclass!
52 # settings_spec = ... # set in subclass!
54 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
56 # config_section = ... # set in subclass!
57 config_section_dependencies
= ['writers', 'html writers']
59 visitor_attributes
= (
60 'head_prefix', 'head', 'stylesheet', 'body_prefix',
61 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
62 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
63 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
66 def get_transforms(self
):
67 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
70 self
.visitor
= visitor
= self
.translator_class(self
.document
)
71 self
.document
.walkabout(visitor
)
72 for attr
in self
.visitor_attributes
:
73 setattr(self
, attr
, getattr(visitor
, attr
))
74 self
.output
= self
.apply_template()
76 def apply_template(self
):
77 template_file
= open(self
.document
.settings
.template
, 'rb')
78 template
= unicode(template_file
.read(), 'utf-8')
80 subs
= self
.interpolation_dict()
81 return template
% subs
83 def interpolation_dict(self
):
85 settings
= self
.document
.settings
86 for attr
in self
.visitor_attributes
:
87 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
88 subs
['encoding'] = settings
.output_encoding
89 subs
['version'] = docutils
.__version
__
92 def assemble_parts(self
):
93 writers
.Writer
.assemble_parts(self
)
94 for part
in self
.visitor_attributes
:
95 self
.parts
[part
] = ''.join(getattr(self
, part
))
98 class HTMLTranslator(nodes
.NodeVisitor
):
101 Generic Docutils to HTML translator.
103 See the `html4css1` and `html5_polyglot` writers for full featured
107 The `visit_*` and `depart_*` methods use a
108 heterogeneous stack, `self.context`.
109 When subclassing, make sure to be consistent in its use!
111 Examples for robust coding:
113 a) Override both `visit_*` and `depart_*` methods, don't call the
116 b) Extend both and unconditionally call the parent functions::
118 def visit_example(self, node):
120 self.body.append('<div class="foo">')
121 html4css1.HTMLTranslator.visit_example(self, node)
123 def depart_example(self, node):
124 html4css1.HTMLTranslator.depart_example(self, node)
126 self.body.append('</div>')
128 c) Extend both, calling the parent functions under the same
131 def visit_example(self, node):
133 self.body.append('<div class="foo">\n')
134 else: # call the parent method
135 _html_base.HTMLTranslator.visit_example(self, node)
137 def depart_example(self, node):
139 self.body.append('</div>\n')
140 else: # call the parent method
141 _html_base.HTMLTranslator.depart_example(self, node)
143 d) Extend one method (call the parent), but don't otherwise use the
144 `self.context` stack::
146 def depart_example(self, node):
147 _html_base.HTMLTranslator.depart_example(self, node)
149 # implementation-specific code
150 # that does not use `self.context`
151 self.body.append('</div>\n')
153 This way, changes in stack use will not bite you.
156 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
157 doctype
= '<!DOCTYPE html>\n'
158 doctype_mathml
= doctype
160 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
161 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
162 content_type
= ('<meta charset="%s"/>\n')
163 generator
= ('<meta name="generator" content="Docutils %s: '
164 'http://docutils.sourceforge.net/" />\n')
166 # Template for the MathJax script in the header:
167 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
169 mathjax_url
= 'file:/usr/share/javascript/mathjax/MathJax.js'
171 URL of the MathJax javascript library.
173 The MathJax library ought to be installed on the same
174 server as the rest of the deployed site files and specified
175 in the `math-output` setting appended to "mathjax".
176 See `Docutils Configuration`__.
178 __ http://docutils.sourceforge.net/docs/user/config.html#math-output
180 The fallback tries a local MathJax installation at
181 ``/usr/share/javascript/mathjax/MathJax.js``.
184 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
185 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
186 words_and_spaces
= re
.compile(r
'\S+| +|\n')
187 # wrap point inside word:
188 in_word_wrap_point
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
)
189 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
191 special_characters
= {ord('&'): u
'&',
195 ord('@'): u
'@', # may thwart address harvesters
197 """Character references for characters with a special meaning in HTML."""
200 def __init__(self
, document
):
201 nodes
.NodeVisitor
.__init
__(self
, document
)
202 self
.settings
= settings
= document
.settings
203 lcode
= settings
.language_code
204 self
.language
= languages
.get_language(lcode
, document
.reporter
)
205 self
.meta
= [self
.generator
% docutils
.__version
__]
206 self
.head_prefix
= []
207 self
.html_prolog
= []
208 if settings
.xml_declaration
:
209 self
.head_prefix
.append(self
.xml_declaration
210 % settings
.output_encoding
)
211 # self.content_type = ""
212 # encoding not interpolated:
213 self
.html_prolog
.append(self
.xml_declaration
)
214 self
.head
= self
.meta
[:]
215 self
.stylesheet
= [self
.stylesheet_call(path
)
216 for path
in utils
.get_stylesheet_list(settings
)]
217 self
.body_prefix
= ['</head>\n<body>\n']
218 # document title, subtitle display
219 self
.body_pre_docinfo
= []
224 self
.body_suffix
= ['</body>\n</html>\n']
225 self
.section_level
= 0
226 self
.initial_header_level
= int(settings
.initial_header_level
)
228 self
.math_output
= settings
.math_output
.split()
229 self
.math_output_options
= self
.math_output
[1:]
230 self
.math_output
= self
.math_output
[0].lower()
233 """Heterogeneous stack.
235 Used by visit_* and depart_* functions in conjunction with the tree
236 traversal. Make sure that the pops correspond to the pushes."""
238 self
.topic_classes
= [] # TODO: replace with self_in_contents
240 self
.compact_p
= True
241 self
.compact_simple
= False
242 self
.compact_field_list
= False
243 self
.in_docinfo
= False
244 self
.in_sidebar
= False
245 self
.in_footnote_list
= False
250 self
.html_head
= [self
.content_type
] # charset not interpolated
252 self
.html_subtitle
= []
254 self
.in_document_title
= 0 # len(self.body) or 0
255 self
.in_mailto
= False
256 self
.author_in_authors
= False # for html4css1
257 self
.math_header
= []
260 return ''.join(self
.head_prefix
+ self
.head
261 + self
.stylesheet
+ self
.body_prefix
262 + self
.body_pre_docinfo
+ self
.docinfo
263 + self
.body
+ self
.body_suffix
)
265 def encode(self
, text
):
266 """Encode special characters in `text` & return."""
267 # Use only named entities known in both XML and HTML
268 # other characters are automatically encoded "by number" if required.
269 # @@@ A codec to do these and all other HTML entities would be nice.
271 return text
.translate(self
.special_characters
)
273 def cloak_mailto(self
, uri
):
274 """Try to hide a mailto: URL from harvesters."""
275 # Encode "@" using a URL octet reference (see RFC 1738).
276 # Further cloaking with HTML entities will be done in the
278 return uri
.replace('@', '%40')
280 def cloak_email(self
, addr
):
281 """Try to hide the link text of a email link from harversters."""
282 # Surround at-signs and periods with <span> tags. ("@" has
283 # already been encoded to "@" by the `encode` method.)
284 addr
= addr
.replace('@', '<span>@</span>')
285 addr
= addr
.replace('.', '<span>.</span>')
288 def attval(self
, text
,
289 whitespace
=re
.compile('[\n\r\t\v\f]')):
290 """Cleanse, HTML encode, and return attribute value text."""
291 encoded
= self
.encode(whitespace
.sub(' ', text
))
292 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
293 # Cloak at-signs ("%40") and periods with HTML entities.
294 encoded
= encoded
.replace('%40', '%40')
295 encoded
= encoded
.replace('.', '.')
298 def stylesheet_call(self
, path
):
299 """Return code to reference or embed stylesheet file `path`"""
300 if self
.settings
.embed_stylesheet
:
302 content
= io
.FileInput(source_path
=path
,
303 encoding
='utf-8').read()
304 self
.settings
.record_dependencies
.add(path
)
306 msg
= u
"Cannot embed stylesheet '%s': %s." % (
307 path
, SafeString(err
.strerror
))
308 self
.document
.reporter
.error(msg
)
309 return '<--- %s --->\n' % msg
310 return self
.embedded_stylesheet
% content
311 # else link to style file:
312 if self
.settings
.stylesheet_path
:
313 # adapt path relative to output (cf. config.html#stylesheet-path)
314 path
= utils
.relative_path(self
.settings
._destination
, path
)
315 return self
.stylesheet_link
% self
.encode(path
)
317 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
319 Construct and return a start tag given a node (id & class attributes
320 are extracted), tag name, and optional attributes.
322 tagname
= tagname
.lower()
326 for (name
, value
) in attributes
.items():
327 atts
[name
.lower()] = value
330 # unify class arguments and move language specification
331 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
332 if cls
.startswith('language-'):
333 languages
.append(cls
[9:])
334 elif cls
.strip() and cls
not in classes
:
337 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
338 atts
[self
.lang_attribute
] = languages
[0]
340 atts
['class'] = ' '.join(classes
)
341 assert 'id' not in atts
342 ids
.extend(node
.get('ids', []))
344 ids
.extend(atts
['ids'])
349 # Add empty "span" elements for additional IDs. Note
350 # that we cannot use empty "a" elements because there
351 # may be targets inside of references, but nested "a"
352 # elements aren't allowed in XHTML (even if they do
353 # not all have a "href" attribute).
354 if empty
or isinstance(node
,
355 (nodes
.bullet_list
, nodes
.docinfo
,
356 nodes
.definition_list
, nodes
.enumerated_list
,
357 nodes
.field_list
, nodes
.option_list
,
359 # Insert target right in front of element.
360 prefix
.append('<span id="%s"></span>' % id)
362 # Non-empty tag. Place the auxiliary <span> tag
363 # *inside* the element, as the first child.
364 suffix
+= '<span id="%s"></span>' % id
365 attlist
= atts
.items()
368 for name
, value
in attlist
:
369 # value=None was used for boolean attributes without
370 # value, but this isn't supported by XHTML.
371 assert value
is not None
372 if isinstance(value
, list):
373 values
= [unicode(v
) for v
in value
]
374 parts
.append('%s="%s"' % (name
.lower(),
375 self
.attval(' '.join(values
))))
377 parts
.append('%s="%s"' % (name
.lower(),
378 self
.attval(unicode(value
))))
383 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
385 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
386 """Construct and return an XML-compatible empty tag."""
387 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
389 def set_class_on_child(self
, node
, class_
, index
=0):
391 Set class `class_` on the visible child no. index of `node`.
392 Do nothing if node has fewer children than `index`.
394 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
396 child
= children
[index
]
399 child
['classes'].append(class_
)
401 def visit_Text(self
, node
):
403 encoded
= self
.encode(text
)
404 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
405 encoded
= self
.cloak_email(encoded
)
406 self
.body
.append(encoded
)
408 def depart_Text(self
, node
):
411 def visit_abbreviation(self
, node
):
412 # @@@ implementation incomplete ("title" attribute)
413 self
.body
.append(self
.starttag(node
, 'abbr', ''))
415 def depart_abbreviation(self
, node
):
416 self
.body
.append('</abbr>')
418 def visit_acronym(self
, node
):
419 # @@@ implementation incomplete ("title" attribute)
420 self
.body
.append(self
.starttag(node
, 'acronym', ''))
422 def depart_acronym(self
, node
):
423 self
.body
.append('</acronym>')
425 def visit_address(self
, node
):
426 self
.visit_docinfo_item(node
, 'address', meta
=False)
427 self
.body
.append(self
.starttag(node
, 'pre',
428 suffix
= '', CLASS
='address'))
430 def depart_address(self
, node
):
431 self
.body
.append('\n</pre>\n')
432 self
.depart_docinfo_item()
434 def visit_admonition(self
, node
):
435 node
['classes'].insert(0, 'admonition')
436 self
.body
.append(self
.starttag(node
, 'div'))
438 def depart_admonition(self
, node
=None):
439 self
.body
.append('</div>\n')
441 attribution_formats
= {'dash': (u
'\u2014', ''),
442 'parentheses': ('(', ')'),
443 'parens': ('(', ')'),
446 def visit_attribution(self
, node
):
447 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
448 self
.context
.append(suffix
)
450 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
452 def depart_attribution(self
, node
):
453 self
.body
.append(self
.context
.pop() + '</p>\n')
455 def visit_author(self
, node
):
456 if not(isinstance(node
.parent
, nodes
.authors
)):
457 self
.visit_docinfo_item(node
, 'author')
458 self
.body
.append('<p>')
460 def depart_author(self
, node
):
461 self
.body
.append('</p>')
462 if isinstance(node
.parent
, nodes
.authors
):
463 self
.body
.append('\n')
465 self
.depart_docinfo_item()
467 def visit_authors(self
, node
):
468 self
.visit_docinfo_item(node
, 'authors')
470 def depart_authors(self
, node
):
471 self
.depart_docinfo_item()
473 def visit_block_quote(self
, node
):
474 self
.body
.append(self
.starttag(node
, 'blockquote'))
476 def depart_block_quote(self
, node
):
477 self
.body
.append('</blockquote>\n')
479 def check_simple_list(self
, node
):
480 """Check for a simple list that can be rendered compactly."""
481 visitor
= SimpleListChecker(self
.document
)
484 except nodes
.NodeFound
:
491 # Include definition lists and field lists (in addition to ordered
492 # and unordered lists) in the test if a list is "simple" (cf. the
493 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
494 # the end of this file).
496 def is_compactable(self
, node
):
497 # print "is_compactable %s ?" % node.__class__,
498 # explicite class arguments have precedence
499 if 'compact' in node
['classes']:
501 if 'open' in node
['classes']:
503 # check config setting:
504 if (isinstance(node
, (nodes
.field_list
, nodes
.definition_list
))
505 and not self
.settings
.compact_field_lists
):
506 # print "`compact-field-lists` is False"
508 if (isinstance(node
, (nodes
.enumerated_list
, nodes
.bullet_list
))
509 and not self
.settings
.compact_lists
):
510 # print "`compact-lists` is False"
512 # more special cases:
513 if (self
.topic_classes
== ['contents']): # TODO: self.in_contents
515 # check the list items:
516 return self
.check_simple_list(node
)
518 def visit_bullet_list(self
, node
):
520 old_compact_simple
= self
.compact_simple
521 self
.context
.append((self
.compact_simple
, self
.compact_p
))
522 self
.compact_p
= None
523 self
.compact_simple
= self
.is_compactable(node
)
524 if self
.compact_simple
and not old_compact_simple
:
525 atts
['class'] = 'simple'
526 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
528 def depart_bullet_list(self
, node
):
529 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
530 self
.body
.append('</ul>\n')
532 def visit_caption(self
, node
):
533 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
535 def depart_caption(self
, node
):
536 self
.body
.append('</p>\n')
540 # Use definition list instead of table for bibliographic references.
541 # Join adjacent citation entries.
543 def visit_citation(self
, node
):
544 if not self
.in_footnote_list
:
545 self
.body
.append('<dl class="citation">\n')
546 self
.in_footnote_list
= True
548 def depart_citation(self
, node
):
549 self
.body
.append('</dd>\n')
550 if not isinstance(node
.next_node(descend
=False, siblings
=True),
552 self
.body
.append('</dl>\n')
553 self
.in_footnote_list
= False
555 def visit_citation_reference(self
, node
):
558 href
+= node
['refid']
559 elif 'refname' in node
:
560 href
+= self
.document
.nameids
[node
['refname']]
561 # else: # TODO system message (or already in the transform)?
562 # 'Citation reference missing.'
563 self
.body
.append(self
.starttag(
564 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
566 def depart_citation_reference(self
, node
):
567 self
.body
.append(']</a>')
571 # don't insert classifier-delimiter here (done by CSS)
573 def visit_classifier(self
, node
):
574 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
576 def depart_classifier(self
, node
):
577 self
.body
.append('</span>')
579 def visit_colspec(self
, node
):
580 self
.colspecs
.append(node
)
581 # "stubs" list is an attribute of the tgroup element:
582 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
584 def depart_colspec(self
, node
):
585 # write out <colgroup> when all colspecs are processed
586 if isinstance(node
.next_node(descend
=False, siblings
=True),
589 if 'colwidths-auto' in node
.parent
.parent
['classes'] or (
590 'colwidths-auto' in self
.settings
.table_style
and
591 ('colwidths-given' not in node
.parent
.parent
['classes'])):
593 total_width
= sum(node
['colwidth'] for node
in self
.colspecs
)
594 self
.body
.append(self
.starttag(node
, 'colgroup'))
595 for node
in self
.colspecs
:
596 colwidth
= int(node
['colwidth'] * 100.0 / total_width
+ 0.5)
597 self
.body
.append(self
.emptytag(node
, 'col',
598 style
='width: %i%%' % colwidth
))
599 self
.body
.append('</colgroup>\n')
601 def visit_comment(self
, node
,
602 sub
=re
.compile('-(?=-)').sub
):
603 """Escape double-dashes in comment text."""
604 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
605 # Content already processed:
608 def visit_compound(self
, node
):
609 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
611 node
[0]['classes'].append('compound-first')
612 node
[-1]['classes'].append('compound-last')
613 for child
in node
[1:-1]:
614 child
['classes'].append('compound-middle')
616 def depart_compound(self
, node
):
617 self
.body
.append('</div>\n')
619 def visit_container(self
, node
):
620 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
622 def depart_container(self
, node
):
623 self
.body
.append('</div>\n')
625 def visit_contact(self
, node
):
626 self
.visit_docinfo_item(node
, 'contact', meta
=False)
628 def depart_contact(self
, node
):
629 self
.depart_docinfo_item()
631 def visit_copyright(self
, node
):
632 self
.visit_docinfo_item(node
, 'copyright')
634 def depart_copyright(self
, node
):
635 self
.depart_docinfo_item()
637 def visit_date(self
, node
):
638 self
.visit_docinfo_item(node
, 'date')
640 def depart_date(self
, node
):
641 self
.depart_docinfo_item()
643 def visit_decoration(self
, node
):
646 def depart_decoration(self
, node
):
649 def visit_definition(self
, node
):
650 self
.body
.append('</dt>\n')
651 self
.body
.append(self
.starttag(node
, 'dd', ''))
653 def depart_definition(self
, node
):
654 self
.body
.append('</dd>\n')
656 def visit_definition_list(self
, node
):
657 classes
= node
.setdefault('classes', [])
658 if self
.is_compactable(node
):
659 classes
.append('simple')
660 self
.body
.append(self
.starttag(node
, 'dl'))
662 def depart_definition_list(self
, node
):
663 self
.body
.append('</dl>\n')
665 def visit_definition_list_item(self
, node
):
666 # pass class arguments, ids and names to definition term:
667 node
.children
[0]['classes'] = (
668 node
.get('classes', []) + node
.children
[0].get('classes', []))
669 node
.children
[0]['ids'] = (
670 node
.get('ids', []) + node
.children
[0].get('ids', []))
671 node
.children
[0]['names'] = (
672 node
.get('names', []) + node
.children
[0].get('names', []))
674 def depart_definition_list_item(self
, node
):
677 def visit_description(self
, node
):
678 self
.body
.append(self
.starttag(node
, 'dd', ''))
680 def depart_description(self
, node
):
681 self
.body
.append('</dd>\n')
683 def visit_docinfo(self
, node
):
684 self
.context
.append(len(self
.body
))
686 if (self
.is_compactable(node
)):
688 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
690 def depart_docinfo(self
, node
):
691 self
.body
.append('</dl>\n')
692 start
= self
.context
.pop()
693 self
.docinfo
= self
.body
[start
:]
696 def visit_docinfo_item(self
, node
, name
, meta
=True):
698 meta_tag
= '<meta name="%s" content="%s" />\n' \
699 % (name
, self
.attval(node
.astext()))
700 self
.add_meta(meta_tag
)
701 self
.body
.append('<dt class="%s">%s</dt>\n'
702 % (name
, self
.language
.labels
[name
]))
703 self
.body
.append(self
.starttag(node
, 'dd', '', CLASS
=name
))
705 def depart_docinfo_item(self
):
706 self
.body
.append('</dd>\n')
708 def visit_doctest_block(self
, node
):
709 self
.body
.append(self
.starttag(node
, 'pre', suffix
='',
710 CLASS
='code python doctest'))
712 def depart_doctest_block(self
, node
):
713 self
.body
.append('\n</pre>\n')
715 def visit_document(self
, node
):
716 title
= (node
.get('title', '') or os
.path
.basename(node
['source'])
717 or 'docutils document without title')
718 self
.head
.append('<title>%s</title>\n' % self
.encode(title
))
720 def depart_document(self
, node
):
721 self
.head_prefix
.extend([self
.doctype
,
722 self
.head_prefix_template
%
723 {'lang': self
.settings
.language_code
}])
724 self
.html_prolog
.append(self
.doctype
)
725 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
726 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
727 if 'name="dcterms.' in ''.join(self
.meta
):
729 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/">')
731 if self
.math_output
== 'mathjax':
732 self
.head
.extend(self
.math_header
)
734 self
.stylesheet
.extend(self
.math_header
)
735 # skip content-type meta tag with interpolated charset value:
736 self
.html_head
.extend(self
.head
[1:])
737 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
738 self
.body_suffix
.insert(0, '</div>\n')
739 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
740 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
741 + self
.docinfo
+ self
.body
742 + self
.body_suffix
[:-1])
743 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
745 def visit_emphasis(self
, node
):
746 self
.body
.append(self
.starttag(node
, 'em', ''))
748 def depart_emphasis(self
, node
):
749 self
.body
.append('</em>')
751 def visit_entry(self
, node
):
753 if isinstance(node
.parent
.parent
, nodes
.thead
):
754 atts
['class'].append('head')
755 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
756 # "stubs" list is an attribute of the tgroup element
757 atts
['class'].append('stub')
760 atts
['class'] = ' '.join(atts
['class'])
764 node
.parent
.column
+= 1
765 if 'morerows' in node
:
766 atts
['rowspan'] = node
['morerows'] + 1
767 if 'morecols' in node
:
768 atts
['colspan'] = node
['morecols'] + 1
769 node
.parent
.column
+= node
['morecols']
770 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
771 self
.context
.append('</%s>\n' % tagname
.lower())
772 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
773 # if len(node) == 0: # empty cell
774 # self.body.append(' ') # no-break space
776 def depart_entry(self
, node
):
777 self
.body
.append(self
.context
.pop())
779 def visit_enumerated_list(self
, node
):
782 atts
['start'] = node
['start']
783 if 'enumtype' in node
:
784 atts
['class'] = node
['enumtype']
785 if self
.is_compactable(node
):
786 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
787 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
789 def depart_enumerated_list(self
, node
):
790 self
.body
.append('</ol>\n')
792 def visit_field_list(self
, node
):
793 # Keep simple paragraphs in the field_body to enable CSS
794 # rule to start body on new line if the label is too long
795 classes
= 'field-list'
796 if (self
.is_compactable(node
)):
798 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
800 def depart_field_list(self
, node
):
801 self
.body
.append('</dl>\n')
803 def visit_field(self
, node
):
806 def depart_field(self
, node
):
809 # as field is ignored, pass class arguments to field-name and field-body:
811 def visit_field_name(self
, node
):
812 self
.body
.append(self
.starttag(node
, 'dt', '',
813 CLASS
=''.join(node
.parent
['classes'])))
815 def depart_field_name(self
, node
):
816 self
.body
.append('</dt>\n')
818 def visit_field_body(self
, node
):
819 self
.body
.append(self
.starttag(node
, 'dd', '',
820 CLASS
=''.join(node
.parent
['classes'])))
821 # prevent misalignment of following content if the field is empty:
822 if not node
.children
:
823 self
.body
.append('<p></p>')
825 def depart_field_body(self
, node
):
826 self
.body
.append('</dd>\n')
828 def visit_figure(self
, node
):
829 atts
= {'class': 'figure'}
830 if node
.get('width'):
831 atts
['style'] = 'width: %s' % node
['width']
832 if node
.get('align'):
833 atts
['class'] += " align-" + node
['align']
834 self
.body
.append(self
.starttag(node
, 'div', **atts
))
836 def depart_figure(self
, node
):
837 self
.body
.append('</div>\n')
839 # use HTML 5 <footer> element?
840 def visit_footer(self
, node
):
841 self
.context
.append(len(self
.body
))
843 def depart_footer(self
, node
):
844 start
= self
.context
.pop()
845 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
846 '<hr class="footer" />\n']
847 footer
.extend(self
.body
[start
:])
848 footer
.append('\n</div>\n')
849 self
.footer
.extend(footer
)
850 self
.body_suffix
[:0] = footer
851 del self
.body
[start
:]
855 # use definition list instead of table for footnote text
857 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
858 def visit_footnote(self
, node
):
859 if not self
.in_footnote_list
:
860 classes
= 'footnote ' + self
.settings
.footnote_references
861 self
.body
.append('<dl class="%s">\n'%classes
)
862 self
.in_footnote_list
= True
864 def depart_footnote(self
, node
):
865 self
.body
.append('</dd>\n')
866 if not isinstance(node
.next_node(descend
=False, siblings
=True),
868 self
.body
.append('</dl>\n')
869 self
.in_footnote_list
= False
871 def visit_footnote_reference(self
, node
):
872 href
= '#' + node
['refid']
873 classes
= 'footnote-reference ' + self
.settings
.footnote_references
874 self
.body
.append(self
.starttag(node
, 'a', '', #suffix,
875 CLASS
=classes
, href
=href
))
877 def depart_footnote_reference(self
, node
):
878 self
.body
.append('</a>')
880 # Docutils-generated text: put section numbers in a span for CSS styling:
881 def visit_generated(self
, node
):
882 if 'sectnum' in node
['classes']:
883 # get section number (strip trailing no-break-spaces)
884 sectnum
= node
.astext().rstrip(u
' ')
885 # print sectnum.encode('utf-8')
886 self
.body
.append('<span class="sectnum">%s</span> '
887 % self
.encode(sectnum
))
888 # Content already processed:
891 def depart_generated(self
, node
):
894 def visit_header(self
, node
):
895 self
.context
.append(len(self
.body
))
897 def depart_header(self
, node
):
898 start
= self
.context
.pop()
899 header
= [self
.starttag(node
, 'div', CLASS
='header')]
900 header
.extend(self
.body
[start
:])
901 header
.append('\n<hr class="header"/>\n</div>\n')
902 self
.body_prefix
.extend(header
)
903 self
.header
.extend(header
)
904 del self
.body
[start
:]
906 # Image types to place in an <object> element
907 object_image_types
= {'.swf': 'application/x-shockwave-flash'}
909 def visit_image(self
, node
):
912 ext
= os
.path
.splitext(uri
)[1].lower()
913 if ext
in self
.object_image_types
:
915 atts
['type'] = self
.object_image_types
[ext
]
918 atts
['alt'] = node
.get('alt', uri
)
921 atts
['width'] = node
['width']
923 atts
['height'] = node
['height']
925 if (PIL
and not ('width' in node
and 'height' in node
)
926 and self
.settings
.file_insertion_enabled
):
927 imagepath
= urllib
.url2pathname(uri
)
929 img
= PIL
.Image
.open(
930 imagepath
.encode(sys
.getfilesystemencoding()))
931 except (IOError, UnicodeEncodeError):
934 self
.settings
.record_dependencies
.add(
935 imagepath
.replace('\\', '/'))
936 if 'width' not in atts
:
937 atts
['width'] = '%dpx' % img
.size
[0]
938 if 'height' not in atts
:
939 atts
['height'] = '%dpx' % img
.size
[1]
941 for att_name
in 'width', 'height':
943 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
945 atts
[att_name
] = '%s%s' % (
946 float(match
.group(1)) * (float(node
['scale']) / 100),
949 for att_name
in 'width', 'height':
951 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
952 # Interpret unitless values as pixels.
953 atts
[att_name
] += 'px'
954 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
957 atts
['style'] = ' '.join(style
)
958 if (isinstance(node
.parent
, nodes
.TextElement
) or
959 (isinstance(node
.parent
, nodes
.reference
) and
960 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
961 # Inline context or surrounded by <a>...</a>.
966 atts
['class'] = 'align-%s' % node
['align']
967 if ext
in self
.object_image_types
:
968 # do NOT use an empty tag: incorrect rendering in browsers
969 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
970 node
.get('alt', uri
) + '</object>' + suffix
)
972 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
974 def depart_image(self
, node
):
975 # self.body.append(self.context.pop())
978 def visit_inline(self
, node
):
979 self
.body
.append(self
.starttag(node
, 'span', ''))
981 def depart_inline(self
, node
):
982 self
.body
.append('</span>')
984 # footnote and citation labels:
985 def visit_label(self
, node
):
986 if (isinstance(node
.parent
, nodes
.footnote
)):
987 classes
= self
.settings
.footnote_references
990 # pass parent node to get id into starttag:
991 self
.body
.append(self
.starttag(node
.parent
, 'dt', '', CLASS
='label'))
992 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
=classes
))
993 # footnote/citation backrefs:
994 if self
.settings
.footnote_backlinks
:
995 backrefs
= node
.parent
['backrefs']
996 if len(backrefs
) == 1:
997 self
.body
.append('<a class="fn-backref" href="#%s">'
1000 def depart_label(self
, node
):
1001 if self
.settings
.footnote_backlinks
:
1002 backrefs
= node
.parent
['backrefs']
1003 if len(backrefs
) == 1:
1004 self
.body
.append('</a>')
1005 self
.body
.append('</span>')
1006 if self
.settings
.footnote_backlinks
and len(backrefs
) > 1:
1007 backlinks
= ['<a href="#%s">%s</a>' % (ref
, i
)
1008 for (i
, ref
) in enumerate(backrefs
, 1)]
1009 self
.body
.append('<span class="fn-backref">(%s)</span>'
1010 % ','.join(backlinks
))
1011 self
.body
.append('</dt>\n<dd>')
1013 def visit_legend(self
, node
):
1014 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1016 def depart_legend(self
, node
):
1017 self
.body
.append('</div>\n')
1019 def visit_line(self
, node
):
1020 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1022 self
.body
.append('<br />')
1024 def depart_line(self
, node
):
1025 self
.body
.append('</div>\n')
1027 def visit_line_block(self
, node
):
1028 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1030 def depart_line_block(self
, node
):
1031 self
.body
.append('</div>\n')
1033 def visit_list_item(self
, node
):
1034 self
.body
.append(self
.starttag(node
, 'li', ''))
1036 def depart_list_item(self
, node
):
1037 self
.body
.append('</li>\n')
1040 def visit_literal(self
, node
):
1041 # special case: "code" role
1042 classes
= node
.get('classes', [])
1043 if 'code' in classes
:
1044 # filter 'code' from class arguments
1045 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1046 self
.body
.append(self
.starttag(node
, 'code', ''))
1049 self
.starttag(node
, 'span', '', CLASS
='docutils literal'))
1050 text
= node
.astext()
1051 # remove hard line breaks (except if in a parsed-literal block)
1052 if not isinstance(node
.parent
, nodes
.literal_block
):
1053 text
= text
.replace('\n', ' ')
1054 # Protect text like ``--an-option`` and the regular expression
1055 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1056 for token
in self
.words_and_spaces
.findall(text
):
1057 if token
.strip() and self
.in_word_wrap_point
.search(token
):
1058 self
.body
.append('<span class="pre">%s</span>'
1059 % self
.encode(token
))
1061 self
.body
.append(self
.encode(token
))
1062 self
.body
.append('</span>')
1063 # Content already processed:
1064 raise nodes
.SkipNode
1066 def depart_literal(self
, node
):
1067 # skipped unless literal element is from "code" role:
1068 self
.body
.append('</code>')
1070 def visit_literal_block(self
, node
):
1071 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='literal-block'))
1072 if 'code' in node
.get('classes', []):
1073 self
.body
.append('<code>')
1075 def depart_literal_block(self
, node
):
1076 if 'code' in node
.get('classes', []):
1077 self
.body
.append('</code>')
1078 self
.body
.append('</pre>\n')
1081 # As there is no native HTML math support, we provide alternatives
1082 # for the math-output: LaTeX and MathJax simply wrap the content,
1083 # HTML and MathML also convert the math_code.
1085 math_tags
= {# math_output: (block, inline, class-arguments)
1086 'mathml': ('div', '', ''),
1087 'html': ('div', 'span', 'formula'),
1088 'mathjax': ('div', 'span', 'math'),
1089 'latex': ('pre', 'tt', 'math'),
1092 def visit_math(self
, node
, math_env
=''):
1093 # If the method is called from visit_math_block(), math_env != ''.
1095 if self
.math_output
not in self
.math_tags
:
1096 self
.document
.reporter
.error(
1097 'math-output format "%s" not supported '
1098 'falling back to "latex"'% self
.math_output
)
1099 self
.math_output
= 'latex'
1100 tag
= self
.math_tags
[self
.math_output
][math_env
== '']
1101 clsarg
= self
.math_tags
[self
.math_output
][2]
1103 wrappers
= {# math_mode: (inline, block)
1104 'mathml': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1105 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1106 'mathjax': (r
'\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1107 'latex': (None, None),
1109 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1110 if self
.math_output
== 'mathml' and (not self
.math_output_options
or
1111 self
.math_output_options
[0] == 'blahtexml'):
1113 # get and wrap content
1114 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1116 try: # wrapper with three "%s"
1117 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1118 except TypeError: # wrapper with one "%s"
1119 math_code
= wrapper
% math_code
1120 # settings and conversion
1121 if self
.math_output
in ('latex', 'mathjax'):
1122 math_code
= self
.encode(math_code
)
1123 if self
.math_output
== 'mathjax' and not self
.math_header
:
1125 self
.mathjax_url
= self
.math_output_options
[0]
1127 self
.document
.reporter
.warning('No MathJax URL specified, '
1128 'using local fallback (see config.html)')
1129 # append configuration, if not already present in the URL:
1130 # input LaTeX with AMS, output common HTML
1131 if '?' not in self
.mathjax_url
:
1132 self
.mathjax_url
+= '?config=TeX-AMS_CHTML'
1133 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1134 elif self
.math_output
== 'html':
1135 if self
.math_output_options
and not self
.math_header
:
1136 self
.math_header
= [self
.stylesheet_call(
1137 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1138 for s
in self
.math_output_options
[0].split(',')]
1139 # TODO: fix display mode in matrices and fractions
1140 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1141 math_code
= math2html
.math2html(math_code
)
1142 elif self
.math_output
== 'mathml':
1143 if 'XHTML 1' in self
.doctype
:
1144 self
.doctype
= self
.doctype_mathml
1145 self
.content_type
= self
.content_type_mathml
1146 converter
= ' '.join(self
.math_output_options
).lower()
1148 if converter
== 'latexml':
1149 math_code
= tex2mathml_extern
.latexml(math_code
,
1150 self
.document
.reporter
)
1151 elif converter
== 'ttm':
1152 math_code
= tex2mathml_extern
.ttm(math_code
,
1153 self
.document
.reporter
)
1154 elif converter
== 'blahtexml':
1155 math_code
= tex2mathml_extern
.blahtexml(math_code
,
1156 inline
=not(math_env
),
1157 reporter
=self
.document
.reporter
)
1159 math_code
= latex2mathml
.tex2mathml(math_code
,
1160 inline
=not(math_env
))
1162 self
.document
.reporter
.error('option "%s" not supported '
1163 'with math-output "MathML"')
1165 raise OSError('is "latexmlmath" in your PATH?')
1166 except SyntaxError, err
:
1167 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1168 self
.visit_system_message(err_node
)
1169 self
.body
.append(self
.starttag(node
, 'p'))
1170 self
.body
.append(u
','.join(err
.args
))
1171 self
.body
.append('</p>\n')
1172 self
.body
.append(self
.starttag(node
, 'pre',
1173 CLASS
='literal-block'))
1174 self
.body
.append(self
.encode(math_code
))
1175 self
.body
.append('\n</pre>\n')
1176 self
.depart_system_message(err_node
)
1177 raise nodes
.SkipNode
1178 # append to document body
1180 self
.body
.append(self
.starttag(node
, tag
,
1181 suffix
='\n'*bool(math_env
),
1183 self
.body
.append(math_code
)
1184 if math_env
: # block mode (equation, display)
1185 self
.body
.append('\n')
1187 self
.body
.append('</%s>' % tag
)
1189 self
.body
.append('\n')
1190 # Content already processed:
1191 raise nodes
.SkipNode
1193 def depart_math(self
, node
):
1194 pass # never reached
1196 def visit_math_block(self
, node
):
1197 # print node.astext().encode('utf8')
1198 math_env
= pick_math_environment(node
.astext())
1199 self
.visit_math(node
, math_env
=math_env
)
1201 def depart_math_block(self
, node
):
1202 pass # never reached
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())
1210 def depart_meta(self
, node
):
1213 def add_meta(self
, tag
):
1214 self
.meta
.append(tag
)
1215 self
.head
.append(tag
)
1217 def visit_option(self
, node
):
1218 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1220 def depart_option(self
, node
):
1221 self
.body
.append('</span>')
1222 if isinstance(node
.next_node(descend
=False, siblings
=True),
1224 self
.body
.append(', ')
1226 def visit_option_argument(self
, node
):
1227 self
.body
.append(node
.get('delimiter', ' '))
1228 self
.body
.append(self
.starttag(node
, 'var', ''))
1230 def depart_option_argument(self
, node
):
1231 self
.body
.append('</var>')
1233 def visit_option_group(self
, node
):
1234 self
.body
.append(self
.starttag(node
, 'dt', ''))
1235 self
.body
.append('<kbd>')
1237 def depart_option_group(self
, node
):
1238 self
.body
.append('</kbd></dt>\n')
1240 def visit_option_list(self
, node
):
1242 self
.starttag(node
, 'dl', CLASS
='option-list'))
1244 def depart_option_list(self
, node
):
1245 self
.body
.append('</dl>\n')
1247 def visit_option_list_item(self
, node
):
1250 def depart_option_list_item(self
, node
):
1253 def visit_option_string(self
, node
):
1256 def depart_option_string(self
, node
):
1259 def visit_organization(self
, node
):
1260 self
.visit_docinfo_item(node
, 'organization')
1262 def depart_organization(self
, node
):
1263 self
.depart_docinfo_item()
1265 # Do not omit <p> tags
1266 # --------------------
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".
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.
1278 # TODO: omit paragraph tags in simple table cells?
1280 def visit_paragraph(self
, node
):
1281 self
.body
.append(self
.starttag(node
, 'p', ''))
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')
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>')
1294 self
.context
.append('')
1295 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1297 def depart_problematic(self
, node
):
1298 self
.body
.append('</span>')
1299 self
.body
.append(self
.context
.pop())
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'
1305 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1306 self
.body
.append(node
.astext())
1308 self
.body
.append('</%s>' % t
)
1309 # Keep non-HTML raw text out of output:
1310 raise nodes
.SkipNode
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'
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
))
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
1337 def visit_revision(self
, node
):
1338 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1340 def depart_revision(self
, node
):
1341 self
.depart_docinfo_item()
1343 def visit_row(self
, node
):
1344 self
.body
.append(self
.starttag(node
, 'tr', ''))
1347 def depart_row(self
, node
):
1348 self
.body
.append('</tr>\n')
1350 def visit_rubric(self
, node
):
1351 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1353 def depart_rubric(self
, node
):
1354 self
.body
.append('</p>\n')
1356 # TODO: use the new HTML 5 element <section>?
1357 def visit_section(self
, node
):
1358 self
.section_level
+= 1
1360 self
.starttag(node
, 'div', CLASS
='section'))
1362 def depart_section(self
, node
):
1363 self
.section_level
-= 1
1364 self
.body
.append('</div>\n')
1366 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1367 def visit_sidebar(self
, node
):
1369 self
.starttag(node
, 'div', CLASS
='sidebar'))
1370 self
.in_sidebar
= True
1372 def depart_sidebar(self
, node
):
1373 self
.body
.append('</div>\n')
1374 self
.in_sidebar
= False
1376 def visit_status(self
, node
):
1377 self
.visit_docinfo_item(node
, 'status', meta
=False)
1379 def depart_status(self
, node
):
1380 self
.depart_docinfo_item()
1382 def visit_strong(self
, node
):
1383 self
.body
.append(self
.starttag(node
, 'strong', ''))
1385 def depart_strong(self
, node
):
1386 self
.body
.append('</strong>')
1388 def visit_subscript(self
, node
):
1389 self
.body
.append(self
.starttag(node
, 'sub', ''))
1391 def depart_subscript(self
, node
):
1392 self
.body
.append('</sub>')
1394 def visit_substitution_definition(self
, node
):
1395 """Internal only."""
1396 raise nodes
.SkipNode
1398 def visit_substitution_reference(self
, node
):
1399 self
.unimplemented_visit(node
)
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
))
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
)
1424 def visit_superscript(self
, node
):
1425 self
.body
.append(self
.starttag(node
, 'sup', ''))
1427 def depart_superscript(self
, node
):
1428 self
.body
.append('</sup>')
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">')
1434 if len(node
['backrefs']):
1435 backrefs
= node
['backrefs']
1436 if len(backrefs
) == 1:
1437 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1442 for backref
in backrefs
:
1443 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1445 backref_text
= ('; <em>backlinks: %s</em>'
1446 % ', '.join(backlinks
))
1447 if node
.hasattr('line'):
1448 line
= ', line %s' % node
['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
))
1456 def depart_system_message(self
, node
):
1457 self
.body
.append('</div>\n')
1459 def visit_table(self
, node
):
1461 classes
= [cls
.strip(u
' \t\n')
1462 for cls
in self
.settings
.table_style
.split(',')]
1464 classes
.append('align-%s' % node
['align'])
1466 atts
['style'] = 'width: %s' % node
['width']
1467 tag
= self
.starttag(node
, 'table', CLASS
=' '.join(classes
), **atts
)
1468 self
.body
.append(tag
)
1470 def depart_table(self
, node
):
1471 self
.body
.append('</table>\n')
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>')
1479 self
.context
.append('')
1481 def depart_target(self
, node
):
1482 self
.body
.append(self
.context
.pop())
1484 # no hard-coded vertical alignment in table body
1485 def visit_tbody(self
, node
):
1486 self
.body
.append(self
.starttag(node
, 'tbody'))
1488 def depart_tbody(self
, node
):
1489 self
.body
.append('</tbody>\n')
1491 def visit_term(self
, node
):
1492 self
.body
.append(self
.starttag(node
, 'dt', ''))
1494 def depart_term(self
, node
):
1496 Leave the end tag to `self.visit_definition()`, in case there's a
1501 def visit_tgroup(self
, node
):
1505 def depart_tgroup(self
, node
):
1508 def visit_thead(self
, node
):
1509 self
.body
.append(self
.starttag(node
, 'thead'))
1511 def depart_thead(self
, node
):
1512 self
.body
.append('</thead>\n')
1514 def visit_title(self
, node
):
1515 """Only 6 section levels are supported by HTML."""
1516 check_id
= 0 # TODO: is this a bool (False) or a counter?
1517 close_tag
= '</p>\n'
1518 if isinstance(node
.parent
, nodes
.topic
):
1520 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1521 elif isinstance(node
.parent
, nodes
.sidebar
):
1523 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1524 elif isinstance(node
.parent
, nodes
.Admonition
):
1526 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1527 elif isinstance(node
.parent
, nodes
.table
):
1529 self
.starttag(node
, 'caption', ''))
1530 close_tag
= '</caption>\n'
1531 elif isinstance(node
.parent
, nodes
.document
):
1532 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1533 close_tag
= '</h1>\n'
1534 self
.in_document_title
= len(self
.body
)
1536 assert isinstance(node
.parent
, nodes
.section
)
1537 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1539 if (len(node
.parent
) >= 2 and
1540 isinstance(node
.parent
[1], nodes
.subtitle
)):
1541 atts
['CLASS'] = 'with-subtitle'
1543 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1545 if node
.hasattr('refid'):
1546 atts
['class'] = 'toc-backref'
1547 atts
['href'] = '#' + node
['refid']
1549 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1550 close_tag
= '</a></h%s>\n' % (h_level
)
1552 close_tag
= '</h%s>\n' % (h_level
)
1553 self
.context
.append(close_tag
)
1555 def depart_title(self
, node
):
1556 self
.body
.append(self
.context
.pop())
1557 if self
.in_document_title
:
1558 self
.title
= self
.body
[self
.in_document_title
:-1]
1559 self
.in_document_title
= 0
1560 self
.body_pre_docinfo
.extend(self
.body
)
1561 self
.html_title
.extend(self
.body
)
1564 def visit_title_reference(self
, node
):
1565 self
.body
.append(self
.starttag(node
, 'cite', ''))
1567 def depart_title_reference(self
, node
):
1568 self
.body
.append('</cite>')
1570 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1571 def visit_topic(self
, node
):
1572 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1573 self
.topic_classes
= node
['classes']
1574 # TODO: replace with ::
1575 # self.in_contents = 'contents' in node['classes']
1577 def depart_topic(self
, node
):
1578 self
.body
.append('</div>\n')
1579 self
.topic_classes
= []
1580 # TODO self.in_contents = False
1582 def visit_transition(self
, node
):
1583 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1585 def depart_transition(self
, node
):
1588 def visit_version(self
, node
):
1589 self
.visit_docinfo_item(node
, 'version', meta
=False)
1591 def depart_version(self
, node
):
1592 self
.depart_docinfo_item()
1594 def unimplemented_visit(self
, node
):
1595 raise NotImplementedError('visiting unimplemented node type: %s'
1596 % node
.__class
__.__name
__)
1599 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1602 Raise `nodes.NodeFound` if non-simple list item is encountered.
1604 Here "simple" means a list item containing nothing other than a single
1605 paragraph, a simple list, or a paragraph followed by a simple list.
1607 This version also checks for simple field lists and docinfo.
1610 def default_visit(self
, node
):
1611 raise nodes
.NodeFound
1613 def visit_list_item(self
, node
):
1614 # print "visiting list item", node.__class__
1615 children
= [child
for child
in node
.children
1616 if not isinstance(child
, nodes
.Invisible
)]
1617 # print "has %s visible children" % len(children)
1618 if (children
and isinstance(children
[0], nodes
.paragraph
)
1619 and (isinstance(children
[-1], nodes
.bullet_list
) or
1620 isinstance(children
[-1], nodes
.enumerated_list
) or
1621 isinstance(children
[-1], nodes
.field_list
))):
1623 # print "%s children remain" % len(children)
1624 if len(children
) <= 1:
1627 # print "found", child.__class__, "in", node.__class__
1628 raise nodes
.NodeFound
1630 def pass_node(self
, node
):
1633 def ignore_node(self
, node
):
1634 # ignore nodes that are never complex (can contain only inline nodes)
1635 raise nodes
.SkipNode
1637 # Paragraphs and text
1638 visit_Text
= ignore_node
1639 visit_paragraph
= ignore_node
1642 visit_bullet_list
= pass_node
1643 visit_enumerated_list
= pass_node
1644 visit_docinfo
= pass_node
1647 visit_author
= ignore_node
1648 visit_authors
= visit_list_item
1649 visit_address
= visit_list_item
1650 visit_contact
= pass_node
1651 visit_copyright
= ignore_node
1652 visit_date
= ignore_node
1653 visit_organization
= ignore_node
1654 visit_status
= ignore_node
1655 visit_version
= visit_list_item
1658 visit_definition_list
= pass_node
1659 visit_definition_list_item
= pass_node
1660 visit_term
= ignore_node
1661 visit_classifier
= pass_node
1662 visit_definition
= visit_list_item
1665 visit_field_list
= pass_node
1666 visit_field
= pass_node
1667 # the field body corresponds to a list item
1668 visit_field_body
= visit_list_item
1669 visit_field_name
= ignore_node
1671 # Invisible nodes should be ignored.
1672 visit_comment
= ignore_node
1673 visit_substitution_definition
= ignore_node
1674 visit_target
= ignore_node
1675 visit_pending
= ignore_node