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
):
685 if (self
.is_compactable(node
)):
687 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
689 def depart_docinfo(self
, node
):
690 self
.body
.append('</dl>\n')
692 def visit_docinfo_item(self
, node
, name
, meta
=True):
694 meta_tag
= '<meta name="%s" content="%s" />\n' \
695 % (name
, self
.attval(node
.astext()))
696 self
.add_meta(meta_tag
)
697 self
.body
.append('<dt class="%s">%s</dt>\n'
698 % (name
, self
.language
.labels
[name
]))
699 self
.body
.append(self
.starttag(node
, 'dd', '', CLASS
=name
))
701 def depart_docinfo_item(self
):
702 self
.body
.append('</dd>\n')
704 def visit_doctest_block(self
, node
):
705 self
.body
.append(self
.starttag(node
, 'pre', suffix
='',
706 CLASS
='code python doctest'))
708 def depart_doctest_block(self
, node
):
709 self
.body
.append('\n</pre>\n')
711 def visit_document(self
, node
):
712 title
= (node
.get('title', '') or os
.path
.basename(node
['source'])
713 or 'docutils document without title')
714 self
.head
.append('<title>%s</title>\n' % self
.encode(title
))
716 def depart_document(self
, node
):
717 self
.head_prefix
.extend([self
.doctype
,
718 self
.head_prefix_template
%
719 {'lang': self
.settings
.language_code
}])
720 self
.html_prolog
.append(self
.doctype
)
721 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
722 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
723 if 'name="dcterms.' in ''.join(self
.meta
):
725 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/">')
727 if self
.math_output
== 'mathjax':
728 self
.head
.extend(self
.math_header
)
730 self
.stylesheet
.extend(self
.math_header
)
731 # skip content-type meta tag with interpolated charset value:
732 self
.html_head
.extend(self
.head
[1:])
733 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
734 self
.body_suffix
.insert(0, '</div>\n')
735 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
736 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
737 + self
.docinfo
+ self
.body
738 + self
.body_suffix
[:-1])
739 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
741 def visit_emphasis(self
, node
):
742 self
.body
.append(self
.starttag(node
, 'em', ''))
744 def depart_emphasis(self
, node
):
745 self
.body
.append('</em>')
747 def visit_entry(self
, node
):
749 if isinstance(node
.parent
.parent
, nodes
.thead
):
750 atts
['class'].append('head')
751 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
752 # "stubs" list is an attribute of the tgroup element
753 atts
['class'].append('stub')
756 atts
['class'] = ' '.join(atts
['class'])
760 node
.parent
.column
+= 1
761 if 'morerows' in node
:
762 atts
['rowspan'] = node
['morerows'] + 1
763 if 'morecols' in node
:
764 atts
['colspan'] = node
['morecols'] + 1
765 node
.parent
.column
+= node
['morecols']
766 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
767 self
.context
.append('</%s>\n' % tagname
.lower())
768 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
769 # if len(node) == 0: # empty cell
770 # self.body.append(' ') # no-break space
772 def depart_entry(self
, node
):
773 self
.body
.append(self
.context
.pop())
775 def visit_enumerated_list(self
, node
):
778 atts
['start'] = node
['start']
779 if 'enumtype' in node
:
780 atts
['class'] = node
['enumtype']
781 if self
.is_compactable(node
):
782 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
783 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
785 def depart_enumerated_list(self
, node
):
786 self
.body
.append('</ol>\n')
788 def visit_field_list(self
, node
):
789 # Keep simple paragraphs in the field_body to enable CSS
790 # rule to start body on new line if the label is too long
791 classes
= 'field-list'
792 if (self
.is_compactable(node
)):
794 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
796 def depart_field_list(self
, node
):
797 self
.body
.append('</dl>\n')
799 def visit_field(self
, node
):
802 def depart_field(self
, node
):
805 # as field is ignored, pass class arguments to field-name and field-body:
807 def visit_field_name(self
, node
):
808 self
.body
.append(self
.starttag(node
, 'dt', '',
809 CLASS
=''.join(node
.parent
['classes'])))
811 def depart_field_name(self
, node
):
812 self
.body
.append('</dt>\n')
814 def visit_field_body(self
, node
):
815 self
.body
.append(self
.starttag(node
, 'dd', '',
816 CLASS
=''.join(node
.parent
['classes'])))
817 # prevent misalignment of following content if the field is empty:
818 if not node
.children
:
819 self
.body
.append('<p></p>')
821 def depart_field_body(self
, node
):
822 self
.body
.append('</dd>\n')
824 def visit_figure(self
, node
):
825 atts
= {'class': 'figure'}
826 if node
.get('width'):
827 atts
['style'] = 'width: %s' % node
['width']
828 if node
.get('align'):
829 atts
['class'] += " align-" + node
['align']
830 self
.body
.append(self
.starttag(node
, 'div', **atts
))
832 def depart_figure(self
, node
):
833 self
.body
.append('</div>\n')
835 # use HTML 5 <footer> element?
836 def visit_footer(self
, node
):
837 self
.context
.append(len(self
.body
))
839 def depart_footer(self
, node
):
840 start
= self
.context
.pop()
841 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
842 '<hr class="footer" />\n']
843 footer
.extend(self
.body
[start
:])
844 footer
.append('\n</div>\n')
845 self
.footer
.extend(footer
)
846 self
.body_suffix
[:0] = footer
847 del self
.body
[start
:]
851 # use definition list instead of table for footnote text
853 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
854 def visit_footnote(self
, node
):
855 if not self
.in_footnote_list
:
856 classes
= 'footnote ' + self
.settings
.footnote_references
857 self
.body
.append('<dl class="%s">\n'%classes
)
858 self
.in_footnote_list
= True
860 def depart_footnote(self
, node
):
861 self
.body
.append('</dd>\n')
862 if not isinstance(node
.next_node(descend
=False, siblings
=True),
864 self
.body
.append('</dl>\n')
865 self
.in_footnote_list
= False
867 def visit_footnote_reference(self
, node
):
868 href
= '#' + node
['refid']
869 classes
= 'footnote-reference ' + self
.settings
.footnote_references
870 self
.body
.append(self
.starttag(node
, 'a', '', #suffix,
871 CLASS
=classes
, href
=href
))
873 def depart_footnote_reference(self
, node
):
874 self
.body
.append('</a>')
876 # Docutils-generated text: put section numbers in a span for CSS styling:
877 def visit_generated(self
, node
):
878 if 'sectnum' in node
['classes']:
879 # get section number (strip trailing no-break-spaces)
880 sectnum
= node
.astext().rstrip(u
' ')
881 # print sectnum.encode('utf-8')
882 self
.body
.append('<span class="sectnum">%s</span> '
883 % self
.encode(sectnum
))
884 # Content already processed:
887 def depart_generated(self
, node
):
890 def visit_header(self
, node
):
891 self
.context
.append(len(self
.body
))
893 def depart_header(self
, node
):
894 start
= self
.context
.pop()
895 header
= [self
.starttag(node
, 'div', CLASS
='header')]
896 header
.extend(self
.body
[start
:])
897 header
.append('\n<hr class="header"/>\n</div>\n')
898 self
.body_prefix
.extend(header
)
899 self
.header
.extend(header
)
900 del self
.body
[start
:]
902 # Image types to place in an <object> element
903 object_image_types
= {'.swf': 'application/x-shockwave-flash'}
905 def visit_image(self
, node
):
908 ext
= os
.path
.splitext(uri
)[1].lower()
909 if ext
in self
.object_image_types
:
911 atts
['type'] = self
.object_image_types
[ext
]
914 atts
['alt'] = node
.get('alt', uri
)
917 atts
['width'] = node
['width']
919 atts
['height'] = node
['height']
921 if (PIL
and not ('width' in node
and 'height' in node
)
922 and self
.settings
.file_insertion_enabled
):
923 imagepath
= urllib
.url2pathname(uri
)
925 img
= PIL
.Image
.open(
926 imagepath
.encode(sys
.getfilesystemencoding()))
927 except (IOError, UnicodeEncodeError):
930 self
.settings
.record_dependencies
.add(
931 imagepath
.replace('\\', '/'))
932 if 'width' not in atts
:
933 atts
['width'] = '%dpx' % img
.size
[0]
934 if 'height' not in atts
:
935 atts
['height'] = '%dpx' % img
.size
[1]
937 for att_name
in 'width', 'height':
939 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
941 atts
[att_name
] = '%s%s' % (
942 float(match
.group(1)) * (float(node
['scale']) / 100),
945 for att_name
in 'width', 'height':
947 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
948 # Interpret unitless values as pixels.
949 atts
[att_name
] += 'px'
950 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
953 atts
['style'] = ' '.join(style
)
954 if (isinstance(node
.parent
, nodes
.TextElement
) or
955 (isinstance(node
.parent
, nodes
.reference
) and
956 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
957 # Inline context or surrounded by <a>...</a>.
962 atts
['class'] = 'align-%s' % node
['align']
963 if ext
in self
.object_image_types
:
964 # do NOT use an empty tag: incorrect rendering in browsers
965 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
966 node
.get('alt', uri
) + '</object>' + suffix
)
968 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
970 def depart_image(self
, node
):
971 # self.body.append(self.context.pop())
974 def visit_inline(self
, node
):
975 self
.body
.append(self
.starttag(node
, 'span', ''))
977 def depart_inline(self
, node
):
978 self
.body
.append('</span>')
980 # footnote and citation labels:
981 def visit_label(self
, node
):
982 if (isinstance(node
.parent
, nodes
.footnote
)):
983 classes
= self
.settings
.footnote_references
986 # pass parent node to get id into starttag:
987 self
.body
.append(self
.starttag(node
.parent
, 'dt', '', CLASS
='label'))
988 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
=classes
))
989 # footnote/citation backrefs:
990 if self
.settings
.footnote_backlinks
:
991 backrefs
= node
.parent
['backrefs']
992 if len(backrefs
) == 1:
993 self
.body
.append('<a class="fn-backref" href="#%s">'
996 def depart_label(self
, node
):
997 if self
.settings
.footnote_backlinks
:
998 backrefs
= node
.parent
['backrefs']
999 if len(backrefs
) == 1:
1000 self
.body
.append('</a>')
1001 self
.body
.append('</span>')
1002 if self
.settings
.footnote_backlinks
and len(backrefs
) > 1:
1003 backlinks
= ['<a href="#%s">%s</a>' % (ref
, i
)
1004 for (i
, ref
) in enumerate(backrefs
, 1)]
1005 self
.body
.append('<span class="fn-backref">(%s)</span>'
1006 % ','.join(backlinks
))
1007 self
.body
.append('</dt>\n<dd>')
1009 def visit_legend(self
, node
):
1010 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1012 def depart_legend(self
, node
):
1013 self
.body
.append('</div>\n')
1015 def visit_line(self
, node
):
1016 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1018 self
.body
.append('<br />')
1020 def depart_line(self
, node
):
1021 self
.body
.append('</div>\n')
1023 def visit_line_block(self
, node
):
1024 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1026 def depart_line_block(self
, node
):
1027 self
.body
.append('</div>\n')
1029 def visit_list_item(self
, node
):
1030 self
.body
.append(self
.starttag(node
, 'li', ''))
1032 def depart_list_item(self
, node
):
1033 self
.body
.append('</li>\n')
1036 def visit_literal(self
, node
):
1037 # special case: "code" role
1038 classes
= node
.get('classes', [])
1039 if 'code' in classes
:
1040 # filter 'code' from class arguments
1041 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1042 self
.body
.append(self
.starttag(node
, 'code', ''))
1045 self
.starttag(node
, 'span', '', CLASS
='docutils literal'))
1046 text
= node
.astext()
1047 # remove hard line breaks (except if in a parsed-literal block)
1048 if not isinstance(node
.parent
, nodes
.literal_block
):
1049 text
= text
.replace('\n', ' ')
1050 # Protect text like ``--an-option`` and the regular expression
1051 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1052 for token
in self
.words_and_spaces
.findall(text
):
1053 if token
.strip() and self
.in_word_wrap_point
.search(token
):
1054 self
.body
.append('<span class="pre">%s</span>'
1055 % self
.encode(token
))
1057 self
.body
.append(self
.encode(token
))
1058 self
.body
.append('</span>')
1059 # Content already processed:
1060 raise nodes
.SkipNode
1062 def depart_literal(self
, node
):
1063 # skipped unless literal element is from "code" role:
1064 self
.body
.append('</code>')
1066 def visit_literal_block(self
, node
):
1067 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='literal-block'))
1068 if 'code' in node
.get('classes', []):
1069 self
.body
.append('<code>')
1071 def depart_literal_block(self
, node
):
1072 if 'code' in node
.get('classes', []):
1073 self
.body
.append('</code>')
1074 self
.body
.append('</pre>\n')
1077 # As there is no native HTML math support, we provide alternatives
1078 # for the math-output: LaTeX and MathJax simply wrap the content,
1079 # HTML and MathML also convert the math_code.
1081 math_tags
= {# math_output: (block, inline, class-arguments)
1082 'mathml': ('div', '', ''),
1083 'html': ('div', 'span', 'formula'),
1084 'mathjax': ('div', 'span', 'math'),
1085 'latex': ('pre', 'tt', 'math'),
1088 def visit_math(self
, node
, math_env
=''):
1089 # If the method is called from visit_math_block(), math_env != ''.
1091 if self
.math_output
not in self
.math_tags
:
1092 self
.document
.reporter
.error(
1093 'math-output format "%s" not supported '
1094 'falling back to "latex"'% self
.math_output
)
1095 self
.math_output
= 'latex'
1096 tag
= self
.math_tags
[self
.math_output
][math_env
== '']
1097 clsarg
= self
.math_tags
[self
.math_output
][2]
1099 wrappers
= {# math_mode: (inline, block)
1100 'mathml': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1101 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1102 'mathjax': (r
'\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1103 'latex': (None, None),
1105 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1106 if self
.math_output
== 'mathml' and (not self
.math_output_options
or
1107 self
.math_output_options
[0] == 'blahtexml'):
1109 # get and wrap content
1110 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1112 try: # wrapper with three "%s"
1113 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1114 except TypeError: # wrapper with one "%s"
1115 math_code
= wrapper
% math_code
1116 # settings and conversion
1117 if self
.math_output
in ('latex', 'mathjax'):
1118 math_code
= self
.encode(math_code
)
1119 if self
.math_output
== 'mathjax' and not self
.math_header
:
1121 self
.mathjax_url
= self
.math_output_options
[0]
1123 self
.document
.reporter
.warning('No MathJax URL specified, '
1124 'using local fallback (see config.html)')
1125 # append configuration, if not already present in the URL:
1126 # input LaTeX with AMS, output common HTML
1127 if '?' not in self
.mathjax_url
:
1128 self
.mathjax_url
+= '?config=TeX-AMS_CHTML'
1129 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1130 elif self
.math_output
== 'html':
1131 if self
.math_output_options
and not self
.math_header
:
1132 self
.math_header
= [self
.stylesheet_call(
1133 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1134 for s
in self
.math_output_options
[0].split(',')]
1135 # TODO: fix display mode in matrices and fractions
1136 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1137 math_code
= math2html
.math2html(math_code
)
1138 elif self
.math_output
== 'mathml':
1139 if 'XHTML 1' in self
.doctype
:
1140 self
.doctype
= self
.doctype_mathml
1141 self
.content_type
= self
.content_type_mathml
1142 converter
= ' '.join(self
.math_output_options
).lower()
1144 if converter
== 'latexml':
1145 math_code
= tex2mathml_extern
.latexml(math_code
,
1146 self
.document
.reporter
)
1147 elif converter
== 'ttm':
1148 math_code
= tex2mathml_extern
.ttm(math_code
,
1149 self
.document
.reporter
)
1150 elif converter
== 'blahtexml':
1151 math_code
= tex2mathml_extern
.blahtexml(math_code
,
1152 inline
=not(math_env
),
1153 reporter
=self
.document
.reporter
)
1155 math_code
= latex2mathml
.tex2mathml(math_code
,
1156 inline
=not(math_env
))
1158 self
.document
.reporter
.error('option "%s" not supported '
1159 'with math-output "MathML"')
1161 raise OSError('is "latexmlmath" in your PATH?')
1162 except SyntaxError, err
:
1163 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1164 self
.visit_system_message(err_node
)
1165 self
.body
.append(self
.starttag(node
, 'p'))
1166 self
.body
.append(u
','.join(err
.args
))
1167 self
.body
.append('</p>\n')
1168 self
.body
.append(self
.starttag(node
, 'pre',
1169 CLASS
='literal-block'))
1170 self
.body
.append(self
.encode(math_code
))
1171 self
.body
.append('\n</pre>\n')
1172 self
.depart_system_message(err_node
)
1173 raise nodes
.SkipNode
1174 # append to document body
1176 self
.body
.append(self
.starttag(node
, tag
,
1177 suffix
='\n'*bool(math_env
),
1179 self
.body
.append(math_code
)
1180 if math_env
: # block mode (equation, display)
1181 self
.body
.append('\n')
1183 self
.body
.append('</%s>' % tag
)
1185 self
.body
.append('\n')
1186 # Content already processed:
1187 raise nodes
.SkipNode
1189 def depart_math(self
, node
):
1190 pass # never reached
1192 def visit_math_block(self
, node
):
1193 # print node.astext().encode('utf8')
1194 math_env
= pick_math_environment(node
.astext())
1195 self
.visit_math(node
, math_env
=math_env
)
1197 def depart_math_block(self
, node
):
1198 pass # never reached
1200 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1201 # HTML5/polyglot recommends using both
1202 def visit_meta(self
, node
):
1203 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1206 def depart_meta(self
, node
):
1209 def add_meta(self
, tag
):
1210 self
.meta
.append(tag
)
1211 self
.head
.append(tag
)
1213 def visit_option(self
, node
):
1214 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1216 def depart_option(self
, node
):
1217 self
.body
.append('</span>')
1218 if isinstance(node
.next_node(descend
=False, siblings
=True),
1220 self
.body
.append(', ')
1222 def visit_option_argument(self
, node
):
1223 self
.body
.append(node
.get('delimiter', ' '))
1224 self
.body
.append(self
.starttag(node
, 'var', ''))
1226 def depart_option_argument(self
, node
):
1227 self
.body
.append('</var>')
1229 def visit_option_group(self
, node
):
1230 self
.body
.append(self
.starttag(node
, 'dt', ''))
1231 self
.body
.append('<kbd>')
1233 def depart_option_group(self
, node
):
1234 self
.body
.append('</kbd></dt>\n')
1236 def visit_option_list(self
, node
):
1238 self
.starttag(node
, 'dl', CLASS
='option-list'))
1240 def depart_option_list(self
, node
):
1241 self
.body
.append('</dl>\n')
1243 def visit_option_list_item(self
, node
):
1246 def depart_option_list_item(self
, node
):
1249 def visit_option_string(self
, node
):
1252 def depart_option_string(self
, node
):
1255 def visit_organization(self
, node
):
1256 self
.visit_docinfo_item(node
, 'organization')
1258 def depart_organization(self
, node
):
1259 self
.depart_docinfo_item()
1261 # Do not omit <p> tags
1262 # --------------------
1264 # The HTML4CSS1 writer does this to "produce
1265 # visually compact lists (less vertical whitespace)". This writer
1266 # relies on CSS rules for"visual compactness".
1268 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1269 # character data, so you cannot drop the <p> tags.
1270 # * Keeping simple paragraphs in the field_body enables a CSS
1271 # rule to start the field-body on a new line if the label is too long
1272 # * it makes the code simpler.
1274 # TODO: omit paragraph tags in simple table cells?
1276 def visit_paragraph(self
, node
):
1277 self
.body
.append(self
.starttag(node
, 'p', ''))
1279 def depart_paragraph(self
, node
):
1280 self
.body
.append('</p>')
1281 if not (isinstance(node
.parent
, (nodes
.list_item
, nodes
.entry
)) and
1282 (len(node
.parent
) == 1)):
1283 self
.body
.append('\n')
1285 def visit_problematic(self
, node
):
1286 if node
.hasattr('refid'):
1287 self
.body
.append('<a href="#%s">' % node
['refid'])
1288 self
.context
.append('</a>')
1290 self
.context
.append('')
1291 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1293 def depart_problematic(self
, node
):
1294 self
.body
.append('</span>')
1295 self
.body
.append(self
.context
.pop())
1297 def visit_raw(self
, node
):
1298 if 'html' in node
.get('format', '').split():
1299 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1301 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1302 self
.body
.append(node
.astext())
1304 self
.body
.append('</%s>' % t
)
1305 # Keep non-HTML raw text out of output:
1306 raise nodes
.SkipNode
1308 def visit_reference(self
, node
):
1309 atts
= {'class': 'reference'}
1310 if 'refuri' in node
:
1311 atts
['href'] = node
['refuri']
1312 if ( self
.settings
.cloak_email_addresses
1313 and atts
['href'].startswith('mailto:')):
1314 atts
['href'] = self
.cloak_mailto(atts
['href'])
1315 self
.in_mailto
= True
1316 atts
['class'] += ' external'
1318 assert 'refid' in node
, \
1319 'References must have "refuri" or "refid" attribute.'
1320 atts
['href'] = '#' + node
['refid']
1321 atts
['class'] += ' internal'
1322 if not isinstance(node
.parent
, nodes
.TextElement
):
1323 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1324 atts
['class'] += ' image-reference'
1325 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1327 def depart_reference(self
, node
):
1328 self
.body
.append('</a>')
1329 if not isinstance(node
.parent
, nodes
.TextElement
):
1330 self
.body
.append('\n')
1331 self
.in_mailto
= False
1333 def visit_revision(self
, node
):
1334 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1336 def depart_revision(self
, node
):
1337 self
.depart_docinfo_item()
1339 def visit_row(self
, node
):
1340 self
.body
.append(self
.starttag(node
, 'tr', ''))
1343 def depart_row(self
, node
):
1344 self
.body
.append('</tr>\n')
1346 def visit_rubric(self
, node
):
1347 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1349 def depart_rubric(self
, node
):
1350 self
.body
.append('</p>\n')
1352 # TODO: use the new HTML 5 element <section>?
1353 def visit_section(self
, node
):
1354 self
.section_level
+= 1
1356 self
.starttag(node
, 'div', CLASS
='section'))
1358 def depart_section(self
, node
):
1359 self
.section_level
-= 1
1360 self
.body
.append('</div>\n')
1362 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1363 def visit_sidebar(self
, node
):
1365 self
.starttag(node
, 'div', CLASS
='sidebar'))
1366 self
.in_sidebar
= True
1368 def depart_sidebar(self
, node
):
1369 self
.body
.append('</div>\n')
1370 self
.in_sidebar
= False
1372 def visit_status(self
, node
):
1373 self
.visit_docinfo_item(node
, 'status', meta
=False)
1375 def depart_status(self
, node
):
1376 self
.depart_docinfo_item()
1378 def visit_strong(self
, node
):
1379 self
.body
.append(self
.starttag(node
, 'strong', ''))
1381 def depart_strong(self
, node
):
1382 self
.body
.append('</strong>')
1384 def visit_subscript(self
, node
):
1385 self
.body
.append(self
.starttag(node
, 'sub', ''))
1387 def depart_subscript(self
, node
):
1388 self
.body
.append('</sub>')
1390 def visit_substitution_definition(self
, node
):
1391 """Internal only."""
1392 raise nodes
.SkipNode
1394 def visit_substitution_reference(self
, node
):
1395 self
.unimplemented_visit(node
)
1397 # h1–h6 elements must not be used to markup subheadings, subtitles,
1398 # alternative titles and taglines unless intended to be the heading for a
1399 # new section or subsection.
1400 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1401 def visit_subtitle(self
, node
):
1402 if isinstance(node
.parent
, nodes
.sidebar
):
1403 classes
= 'sidebar-subtitle'
1404 elif isinstance(node
.parent
, nodes
.document
):
1405 classes
= 'subtitle'
1406 self
.in_document_title
= len(self
.body
)
1407 elif isinstance(node
.parent
, nodes
.section
):
1408 classes
= 'section-subtitle'
1409 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
=classes
))
1411 def depart_subtitle(self
, node
):
1412 self
.body
.append('</p>\n')
1413 if self
.in_document_title
:
1414 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1415 self
.in_document_title
= 0
1416 self
.body_pre_docinfo
.extend(self
.body
)
1417 self
.html_subtitle
.extend(self
.body
)
1420 def visit_superscript(self
, node
):
1421 self
.body
.append(self
.starttag(node
, 'sup', ''))
1423 def depart_superscript(self
, node
):
1424 self
.body
.append('</sup>')
1426 def visit_system_message(self
, node
):
1427 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1428 self
.body
.append('<p class="system-message-title">')
1430 if len(node
['backrefs']):
1431 backrefs
= node
['backrefs']
1432 if len(backrefs
) == 1:
1433 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1438 for backref
in backrefs
:
1439 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1441 backref_text
= ('; <em>backlinks: %s</em>'
1442 % ', '.join(backlinks
))
1443 if node
.hasattr('line'):
1444 line
= ', line %s' % node
['line']
1447 self
.body
.append('System Message: %s/%s '
1448 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1449 % (node
['type'], node
['level'],
1450 self
.encode(node
['source']), line
, backref_text
))
1452 def depart_system_message(self
, node
):
1453 self
.body
.append('</div>\n')
1455 def visit_table(self
, node
):
1457 classes
= [cls
.strip(u
' \t\n')
1458 for cls
in self
.settings
.table_style
.split(',')]
1460 classes
.append('align-%s' % node
['align'])
1462 atts
['style'] = 'width: %s' % node
['width']
1463 tag
= self
.starttag(node
, 'table', CLASS
=' '.join(classes
), **atts
)
1464 self
.body
.append(tag
)
1466 def depart_table(self
, node
):
1467 self
.body
.append('</table>\n')
1469 def visit_target(self
, node
):
1470 if not ('refuri' in node
or 'refid' in node
1471 or 'refname' in node
):
1472 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1473 self
.context
.append('</span>')
1475 self
.context
.append('')
1477 def depart_target(self
, node
):
1478 self
.body
.append(self
.context
.pop())
1480 # no hard-coded vertical alignment in table body
1481 def visit_tbody(self
, node
):
1482 self
.body
.append(self
.starttag(node
, 'tbody'))
1484 def depart_tbody(self
, node
):
1485 self
.body
.append('</tbody>\n')
1487 def visit_term(self
, node
):
1488 self
.body
.append(self
.starttag(node
, 'dt', ''))
1490 def depart_term(self
, node
):
1492 Leave the end tag to `self.visit_definition()`, in case there's a
1497 def visit_tgroup(self
, node
):
1501 def depart_tgroup(self
, node
):
1504 def visit_thead(self
, node
):
1505 self
.body
.append(self
.starttag(node
, 'thead'))
1507 def depart_thead(self
, node
):
1508 self
.body
.append('</thead>\n')
1510 def visit_title(self
, node
):
1511 """Only 6 section levels are supported by HTML."""
1512 check_id
= 0 # TODO: is this a bool (False) or a counter?
1513 close_tag
= '</p>\n'
1514 if isinstance(node
.parent
, nodes
.topic
):
1516 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1517 elif isinstance(node
.parent
, nodes
.sidebar
):
1519 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1520 elif isinstance(node
.parent
, nodes
.Admonition
):
1522 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1523 elif isinstance(node
.parent
, nodes
.table
):
1525 self
.starttag(node
, 'caption', ''))
1526 close_tag
= '</caption>\n'
1527 elif isinstance(node
.parent
, nodes
.document
):
1528 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1529 close_tag
= '</h1>\n'
1530 self
.in_document_title
= len(self
.body
)
1532 assert isinstance(node
.parent
, nodes
.section
)
1533 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1535 if (len(node
.parent
) >= 2 and
1536 isinstance(node
.parent
[1], nodes
.subtitle
)):
1537 atts
['CLASS'] = 'with-subtitle'
1539 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1541 if node
.hasattr('refid'):
1542 atts
['class'] = 'toc-backref'
1543 atts
['href'] = '#' + node
['refid']
1545 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1546 close_tag
= '</a></h%s>\n' % (h_level
)
1548 close_tag
= '</h%s>\n' % (h_level
)
1549 self
.context
.append(close_tag
)
1551 def depart_title(self
, node
):
1552 self
.body
.append(self
.context
.pop())
1553 if self
.in_document_title
:
1554 self
.title
= self
.body
[self
.in_document_title
:-1]
1555 self
.in_document_title
= 0
1556 self
.body_pre_docinfo
.extend(self
.body
)
1557 self
.html_title
.extend(self
.body
)
1560 def visit_title_reference(self
, node
):
1561 self
.body
.append(self
.starttag(node
, 'cite', ''))
1563 def depart_title_reference(self
, node
):
1564 self
.body
.append('</cite>')
1566 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1567 def visit_topic(self
, node
):
1568 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1569 self
.topic_classes
= node
['classes']
1570 # TODO: replace with ::
1571 # self.in_contents = 'contents' in node['classes']
1573 def depart_topic(self
, node
):
1574 self
.body
.append('</div>\n')
1575 self
.topic_classes
= []
1576 # TODO self.in_contents = False
1578 def visit_transition(self
, node
):
1579 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1581 def depart_transition(self
, node
):
1584 def visit_version(self
, node
):
1585 self
.visit_docinfo_item(node
, 'version', meta
=False)
1587 def depart_version(self
, node
):
1588 self
.depart_docinfo_item()
1590 def unimplemented_visit(self
, node
):
1591 raise NotImplementedError('visiting unimplemented node type: %s'
1592 % node
.__class
__.__name
__)
1595 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1598 Raise `nodes.NodeFound` if non-simple list item is encountered.
1600 Here "simple" means a list item containing nothing other than a single
1601 paragraph, a simple list, or a paragraph followed by a simple list.
1603 This version also checks for simple field lists and docinfo.
1606 def default_visit(self
, node
):
1607 raise nodes
.NodeFound
1609 def visit_list_item(self
, node
):
1610 # print "visiting list item", node.__class__
1611 children
= [child
for child
in node
.children
1612 if not isinstance(child
, nodes
.Invisible
)]
1613 # print "has %s visible children" % len(children)
1614 if (children
and isinstance(children
[0], nodes
.paragraph
)
1615 and (isinstance(children
[-1], nodes
.bullet_list
) or
1616 isinstance(children
[-1], nodes
.enumerated_list
) or
1617 isinstance(children
[-1], nodes
.field_list
))):
1619 # print "%s children remain" % len(children)
1620 if len(children
) <= 1:
1623 # print "found", child.__class__, "in", node.__class__
1624 raise nodes
.NodeFound
1626 def pass_node(self
, node
):
1629 def ignore_node(self
, node
):
1630 # ignore nodes that are never complex (can contain only inline nodes)
1631 raise nodes
.SkipNode
1633 # Paragraphs and text
1634 visit_Text
= ignore_node
1635 visit_paragraph
= ignore_node
1638 visit_bullet_list
= pass_node
1639 visit_enumerated_list
= pass_node
1640 visit_docinfo
= pass_node
1643 visit_author
= ignore_node
1644 visit_authors
= visit_list_item
1645 visit_address
= visit_list_item
1646 visit_contact
= pass_node
1647 visit_copyright
= ignore_node
1648 visit_date
= ignore_node
1649 visit_organization
= ignore_node
1650 visit_status
= ignore_node
1651 visit_version
= visit_list_item
1654 visit_definition_list
= pass_node
1655 visit_definition_list_item
= pass_node
1656 visit_term
= ignore_node
1657 visit_classifier
= pass_node
1658 visit_definition
= visit_list_item
1661 visit_field_list
= pass_node
1662 visit_field
= pass_node
1663 # the field body corresponds to a list item
1664 visit_field_body
= visit_list_item
1665 visit_field_name
= ignore_node
1667 # Invisible nodes should be ignored.
1668 visit_comment
= ignore_node
1669 visit_substitution_definition
= ignore_node
1670 visit_target
= ignore_node
1671 visit_pending
= ignore_node