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
19 # _html_base.py: common definitions for Docutils HTML writers
20 # ============================================================
26 try: # check for the Python Imaging Library
29 try: # sometimes PIL modules are put in PYTHONPATH's root
31 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',)
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
))
99 class HTMLTranslator(nodes
.NodeVisitor
):
101 """Generic Docutils to HTML translator.
103 See the html4css1 and html5_polyglott for writers for full featured HTML
106 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
107 doctype
= '<!DOCTYPE html>\n'
108 doctype_mathml
= doctype
110 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
111 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
112 content_type
= ('<meta charset="%s"/>\n')
113 generator
= ('<meta name="generator" content="Docutils %s: '
114 'http://docutils.sourceforge.net/" />\n')
116 # Template for the MathJax script in the header:
117 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
118 # The latest version of MathJax from the distributed server:
119 # avaliable to the public under the `MathJax CDN Terms of Service`__
120 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
121 # may be overwritten by custom URL appended to "mathjax"
122 mathjax_url
= ('https://cdn.mathjax.org/mathjax/latest/MathJax.js?'
123 'config=TeX-AMS_CHTML')
125 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
126 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
127 words_and_spaces
= re
.compile(r
'\S+| +|\n')
128 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
129 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
131 special_characters
= {ord('&'): u
'&',
135 ord('@'): u
'@', # may thwart address harvesters
137 """Character references for characters with a special meaning in HTML."""
139 def __init__(self
, document
):
140 nodes
.NodeVisitor
.__init
__(self
, document
)
141 self
.settings
= settings
= document
.settings
142 lcode
= settings
.language_code
143 self
.language
= languages
.get_language(lcode
, document
.reporter
)
144 self
.meta
= [self
.generator
% docutils
.__version
__]
145 self
.head_prefix
= []
146 self
.html_prolog
= []
147 if settings
.xml_declaration
:
148 self
.head_prefix
.append(self
.xml_declaration
149 % settings
.output_encoding
)
150 # self.content_type = ""
151 # encoding not interpolated:
152 self
.html_prolog
.append(self
.xml_declaration
)
153 self
.head
= self
.meta
[:]
154 self
.stylesheet
= [self
.stylesheet_call(path
)
155 for path
in utils
.get_stylesheet_list(settings
)]
156 self
.body_prefix
= ['</head>\n<body>\n']
157 # document title, subtitle display
158 self
.body_pre_docinfo
= []
163 self
.body_suffix
= ['</body>\n</html>\n']
164 self
.section_level
= 0
165 self
.initial_header_level
= int(settings
.initial_header_level
)
167 self
.math_output
= settings
.math_output
.split()
168 self
.math_output_options
= self
.math_output
[1:]
169 self
.math_output
= self
.math_output
[0].lower()
171 # A heterogenous stack used in conjunction with the tree traversal.
172 # Make sure that the pops correspond to the pushes:
175 self
.topic_classes
= [] # TODO: replace with self_in_contents
177 self
.compact_p
= True
178 self
.compact_simple
= False
179 self
.compact_field_list
= False
180 self
.in_docinfo
= False
181 self
.in_sidebar
= False
182 self
.in_footnote_list
= False
187 self
.html_head
= [self
.content_type
] # charset not interpolated
189 self
.html_subtitle
= []
191 self
.in_document_title
= 0 # len(self.body) or 0
192 self
.in_mailto
= False
193 self
.author_in_authors
= False # for html4css1
194 self
.math_header
= []
197 return ''.join(self
.head_prefix
+ self
.head
198 + self
.stylesheet
+ self
.body_prefix
199 + self
.body_pre_docinfo
+ self
.docinfo
200 + self
.body
+ self
.body_suffix
)
202 def encode(self
, text
):
203 """Encode special characters in `text` & return."""
204 # Use only named entities known in both XML and HTML
205 # other characters are automatically encoded "by number" if required.
206 # @@@ A codec to do these and all other HTML entities would be nice.
208 return text
.translate(self
.special_characters
)
210 def cloak_mailto(self
, uri
):
211 """Try to hide a mailto: URL from harvesters."""
212 # Encode "@" using a URL octet reference (see RFC 1738).
213 # Further cloaking with HTML entities will be done in the
215 return uri
.replace('@', '%40')
217 def cloak_email(self
, addr
):
218 """Try to hide the link text of a email link from harversters."""
219 # Surround at-signs and periods with <span> tags. ("@" has
220 # already been encoded to "@" by the `encode` method.)
221 addr
= addr
.replace('@', '<span>@</span>')
222 addr
= addr
.replace('.', '<span>.</span>')
225 def attval(self
, text
,
226 whitespace
=re
.compile('[\n\r\t\v\f]')):
227 """Cleanse, HTML encode, and return attribute value text."""
228 encoded
= self
.encode(whitespace
.sub(' ', text
))
229 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
230 # Cloak at-signs ("%40") and periods with HTML entities.
231 encoded
= encoded
.replace('%40', '%40')
232 encoded
= encoded
.replace('.', '.')
235 def stylesheet_call(self
, path
):
236 """Return code to reference or embed stylesheet file `path`"""
237 if self
.settings
.embed_stylesheet
:
239 content
= io
.FileInput(source_path
=path
,
240 encoding
='utf-8').read()
241 self
.settings
.record_dependencies
.add(path
)
243 msg
= u
"Cannot embed stylesheet '%s': %s." % (
244 path
, SafeString(err
.strerror
))
245 self
.document
.reporter
.error(msg
)
246 return '<--- %s --->\n' % msg
247 return self
.embedded_stylesheet
% content
248 # else link to style file:
249 if self
.settings
.stylesheet_path
:
250 # adapt path relative to output (cf. config.html#stylesheet-path)
251 path
= utils
.relative_path(self
.settings
._destination
, path
)
252 return self
.stylesheet_link
% self
.encode(path
)
254 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
256 Construct and return a start tag given a node (id & class attributes
257 are extracted), tag name, and optional attributes.
259 tagname
= tagname
.lower()
263 for (name
, value
) in attributes
.items():
264 atts
[name
.lower()] = value
267 # unify class arguments and move language specification
268 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
269 if cls
.startswith('language-'):
270 languages
.append(cls
[9:])
271 elif cls
.strip() and cls
not in classes
:
274 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
275 atts
[self
.lang_attribute
] = languages
[0]
277 atts
['class'] = ' '.join(classes
)
278 assert 'id' not in atts
279 ids
.extend(node
.get('ids', []))
281 ids
.extend(atts
['ids'])
286 # Add empty "span" elements for additional IDs. Note
287 # that we cannot use empty "a" elements because there
288 # may be targets inside of references, but nested "a"
289 # elements aren't allowed in XHTML (even if they do
290 # not all have a "href" attribute).
291 if empty
or isinstance(node
,
292 (nodes
.bullet_list
, nodes
.enumerated_list
,
293 nodes
.definition_list
, nodes
.field_list
,
294 nodes
.option_list
, nodes
.docinfo
)):
295 # Insert target right in front of element.
296 prefix
.append('<span id="%s"></span>' % id)
298 # Non-empty tag. Place the auxiliary <span> tag
299 # *inside* the element, as the first child.
300 suffix
+= '<span id="%s"></span>' % id
301 attlist
= atts
.items()
304 for name
, value
in attlist
:
305 # value=None was used for boolean attributes without
306 # value, but this isn't supported by XHTML.
307 assert value
is not None
308 if isinstance(value
, list):
309 values
= [unicode(v
) for v
in value
]
310 parts
.append('%s="%s"' % (name
.lower(),
311 self
.attval(' '.join(values
))))
313 parts
.append('%s="%s"' % (name
.lower(),
314 self
.attval(unicode(value
))))
319 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
321 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
322 """Construct and return an XML-compatible empty tag."""
323 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
325 def set_class_on_child(self
, node
, class_
, index
=0):
327 Set class `class_` on the visible child no. index of `node`.
328 Do nothing if node has fewer children than `index`.
330 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
332 child
= children
[index
]
335 child
['classes'].append(class_
)
337 def visit_Text(self
, node
):
339 encoded
= self
.encode(text
)
340 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
341 encoded
= self
.cloak_email(encoded
)
342 self
.body
.append(encoded
)
344 def depart_Text(self
, node
):
347 def visit_abbreviation(self
, node
):
348 # @@@ implementation incomplete ("title" attribute)
349 self
.body
.append(self
.starttag(node
, 'abbr', ''))
351 def depart_abbreviation(self
, node
):
352 self
.body
.append('</abbr>')
354 def visit_acronym(self
, node
):
355 # @@@ implementation incomplete ("title" attribute)
356 self
.body
.append(self
.starttag(node
, 'acronym', ''))
358 def depart_acronym(self
, node
):
359 self
.body
.append('</acronym>')
361 def visit_address(self
, node
):
362 self
.visit_docinfo_item(node
, 'address', meta
=False)
363 self
.body
.append(self
.starttag(node
, 'pre',
364 suffix
= '', CLASS
='address'))
366 def depart_address(self
, node
):
367 self
.body
.append('\n</pre>\n')
368 self
.depart_docinfo_item()
370 def visit_admonition(self
, node
):
371 node
['classes'].insert(0, 'admonition')
372 self
.body
.append(self
.starttag(node
, 'div'))
374 def depart_admonition(self
, node
=None):
375 self
.body
.append('</div>\n')
377 attribution_formats
= {'dash': (u
'\u2014', ''),
378 'parentheses': ('(', ')'),
379 'parens': ('(', ')'),
382 def visit_attribution(self
, node
):
383 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
384 self
.context
.append(suffix
)
386 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
388 def depart_attribution(self
, node
):
389 self
.body
.append(self
.context
.pop() + '</p>\n')
391 def visit_author(self
, node
):
392 if not(isinstance(node
.parent
, nodes
.authors
)):
393 self
.visit_docinfo_item(node
, 'author')
394 self
.body
.append('<p>')
396 def depart_author(self
, node
):
397 self
.body
.append('</p>')
398 if isinstance(node
.parent
, nodes
.authors
):
399 self
.body
.append('\n')
401 self
.depart_docinfo_item()
403 def visit_authors(self
, node
):
404 self
.visit_docinfo_item(node
, 'authors')
406 def depart_authors(self
, node
):
407 self
.depart_docinfo_item()
409 def visit_block_quote(self
, node
):
410 self
.body
.append(self
.starttag(node
, 'blockquote'))
412 def depart_block_quote(self
, node
):
413 self
.body
.append('</blockquote>\n')
415 def check_simple_list(self
, node
):
416 """Check for a simple list that can be rendered compactly."""
417 visitor
= SimpleListChecker(self
.document
)
420 except nodes
.NodeFound
:
427 # Include definition lists and field lists (in addition to ordered
428 # and unordered lists) in the test if a list is "simple" (cf. the
429 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
430 # the end of this file).
432 def is_compactable(self
, node
):
433 # print "is_compactable %s ?" % node.__class__,
434 # explicite class arguments have precedence
435 if 'compact' in node
['classes']:
437 if 'open' in node
['classes']:
439 # check config setting:
440 if (isinstance(node
, (nodes
.field_list
, nodes
.definition_list
))
441 and not self
.settings
.compact_field_lists
):
442 # print "`compact-field-lists` is False"
444 if (isinstance(node
, (nodes
.enumerated_list
, nodes
.bullet_list
))
445 and not self
.settings
.compact_lists
):
446 # print "`compact-lists` is False"
448 # more special cases:
449 if (self
.topic_classes
== ['contents']): # TODO: self.in_contents
451 # check the list items:
452 return self
.check_simple_list(node
)
454 def visit_bullet_list(self
, node
):
456 old_compact_simple
= self
.compact_simple
457 self
.context
.append((self
.compact_simple
, self
.compact_p
))
458 self
.compact_p
= None
459 self
.compact_simple
= self
.is_compactable(node
)
460 if self
.compact_simple
and not old_compact_simple
:
461 atts
['class'] = 'simple'
462 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
464 def depart_bullet_list(self
, node
):
465 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
466 self
.body
.append('</ul>\n')
468 def visit_caption(self
, node
):
469 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
471 def depart_caption(self
, node
):
472 self
.body
.append('</p>\n')
476 # Use definition list instead of table for bibliographic references.
477 # Join adjacent citation entries.
479 def visit_citation(self
, node
):
480 if not self
.in_footnote_list
:
481 self
.body
.append('<dl class="citation">\n')
482 self
.in_footnote_list
= True
484 def depart_citation(self
, node
):
485 self
.body
.append('</dd>\n')
486 if not isinstance(node
.next_node(descend
=False, siblings
=True),
488 self
.body
.append('</dl>\n')
489 self
.in_footnote_list
= False
491 def visit_citation_reference(self
, node
):
494 href
+= node
['refid']
495 elif 'refname' in node
:
496 href
+= self
.document
.nameids
[node
['refname']]
497 # else: # TODO system message (or already in the transform)?
498 # 'Citation reference missing.'
499 self
.body
.append(self
.starttag(
500 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
502 def depart_citation_reference(self
, node
):
503 self
.body
.append(']</a>')
507 # don't insert classifier-delimiter here (done by CSS)
509 def visit_classifier(self
, node
):
510 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
512 def depart_classifier(self
, node
):
513 self
.body
.append('</span>')
515 def visit_colspec(self
, node
):
516 self
.colspecs
.append(node
)
517 # "stubs" list is an attribute of the tgroup element:
518 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
520 def depart_colspec(self
, node
):
521 # write out <colgroup> when all colspecs are processed
522 if isinstance(node
.next_node(descend
=False, siblings
=True),
525 if 'colwidths-auto' in node
.parent
.parent
['classes'] or (
526 'colwidths-auto' in self
.settings
.table_style
and
527 ('colwidths-given' not in node
.parent
.parent
['classes'])):
529 total_width
= sum(node
['colwidth'] for node
in self
.colspecs
)
530 self
.body
.append(self
.starttag(node
, 'colgroup'))
531 for node
in self
.colspecs
:
532 colwidth
= int(node
['colwidth'] * 100.0 / total_width
+ 0.5)
533 self
.body
.append(self
.emptytag(node
, 'col',
534 style
='width: %i%%' % colwidth
))
535 self
.body
.append('</colgroup>\n')
537 def visit_comment(self
, node
,
538 sub
=re
.compile('-(?=-)').sub
):
539 """Escape double-dashes in comment text."""
540 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
541 # Content already processed:
544 def visit_compound(self
, node
):
545 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
547 node
[0]['classes'].append('compound-first')
548 node
[-1]['classes'].append('compound-last')
549 for child
in node
[1:-1]:
550 child
['classes'].append('compound-middle')
552 def depart_compound(self
, node
):
553 self
.body
.append('</div>\n')
555 def visit_container(self
, node
):
556 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
558 def depart_container(self
, node
):
559 self
.body
.append('</div>\n')
561 def visit_contact(self
, node
):
562 self
.visit_docinfo_item(node
, 'contact', meta
=False)
564 def depart_contact(self
, node
):
565 self
.depart_docinfo_item()
567 def visit_copyright(self
, node
):
568 self
.visit_docinfo_item(node
, 'copyright')
570 def depart_copyright(self
, node
):
571 self
.depart_docinfo_item()
573 def visit_date(self
, node
):
574 self
.visit_docinfo_item(node
, 'date')
576 def depart_date(self
, node
):
577 self
.depart_docinfo_item()
579 def visit_decoration(self
, node
):
582 def depart_decoration(self
, node
):
585 def visit_definition(self
, node
):
586 self
.body
.append('</dt>\n')
587 self
.body
.append(self
.starttag(node
, 'dd', ''))
589 def depart_definition(self
, node
):
590 self
.body
.append('</dd>\n')
592 def visit_definition_list(self
, node
):
593 classes
= node
.setdefault('classes', [])
594 if self
.is_compactable(node
):
595 classes
.append('simple')
596 self
.body
.append(self
.starttag(node
, 'dl'))
598 def depart_definition_list(self
, node
):
599 self
.body
.append('</dl>\n')
601 def visit_definition_list_item(self
, node
):
602 # pass class arguments, ids and names to definition term:
603 node
.children
[0]['classes'] = (
604 node
.get('classes', []) + node
.children
[0].get('classes', []))
605 node
.children
[0]['ids'] = (
606 node
.get('ids', []) + node
.children
[0].get('ids', []))
607 node
.children
[0]['names'] = (
608 node
.get('names', []) + node
.children
[0].get('names', []))
610 def depart_definition_list_item(self
, node
):
613 def visit_description(self
, node
):
614 self
.body
.append(self
.starttag(node
, 'dd', ''))
616 def depart_description(self
, node
):
617 self
.body
.append('</dd>\n')
619 def visit_docinfo(self
, node
):
621 if (self
.is_compactable(node
)):
623 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
625 def depart_docinfo(self
, node
):
626 self
.body
.append('</dl>\n')
628 def visit_docinfo_item(self
, node
, name
, meta
=True):
630 meta_tag
= '<meta name="%s" content="%s" />\n' \
631 % (name
, self
.attval(node
.astext()))
632 self
.add_meta(meta_tag
)
633 self
.body
.append('<dt class="%s">%s</dt>\n'
634 % (name
, self
.language
.labels
[name
]))
635 self
.body
.append(self
.starttag(node
, 'dd', '', CLASS
=name
))
637 def depart_docinfo_item(self
):
638 self
.body
.append('</dd>\n')
640 def visit_doctest_block(self
, node
):
641 self
.body
.append(self
.starttag(node
, 'pre', suffix
='',
642 CLASS
='code python doctest'))
644 def depart_doctest_block(self
, node
):
645 self
.body
.append('\n</pre>\n')
647 def visit_document(self
, node
):
648 self
.head
.append('<title>%s</title>\n'
649 % self
.encode(node
.get('title', '')))
651 def depart_document(self
, node
):
652 self
.head_prefix
.extend([self
.doctype
,
653 self
.head_prefix_template
%
654 {'lang': self
.settings
.language_code
}])
655 self
.html_prolog
.append(self
.doctype
)
656 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
657 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
659 if self
.math_output
== 'mathjax':
660 self
.head
.extend(self
.math_header
)
662 self
.stylesheet
.extend(self
.math_header
)
663 # skip content-type meta tag with interpolated charset value:
664 self
.html_head
.extend(self
.head
[1:])
665 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
666 self
.body_suffix
.insert(0, '</div>\n')
667 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
668 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
669 + self
.docinfo
+ self
.body
670 + self
.body_suffix
[:-1])
671 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
673 def visit_emphasis(self
, node
):
674 self
.body
.append(self
.starttag(node
, 'em', ''))
676 def depart_emphasis(self
, node
):
677 self
.body
.append('</em>')
679 def visit_entry(self
, node
):
681 if isinstance(node
.parent
.parent
, nodes
.thead
):
682 atts
['class'].append('head')
683 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
684 # "stubs" list is an attribute of the tgroup element
685 atts
['class'].append('stub')
688 atts
['class'] = ' '.join(atts
['class'])
692 node
.parent
.column
+= 1
693 if 'morerows' in node
:
694 atts
['rowspan'] = node
['morerows'] + 1
695 if 'morecols' in node
:
696 atts
['colspan'] = node
['morecols'] + 1
697 node
.parent
.column
+= node
['morecols']
698 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
699 self
.context
.append('</%s>\n' % tagname
.lower())
700 # TODO: why does the html4css1 writer insert an NBSP into empty cells?
701 # if len(node) == 0: # empty cell
702 # self.body.append(' ') # no-break space
704 def depart_entry(self
, node
):
705 self
.body
.append(self
.context
.pop())
707 def visit_enumerated_list(self
, node
):
710 atts
['start'] = node
['start']
711 if 'enumtype' in node
:
712 atts
['class'] = node
['enumtype']
713 if self
.is_compactable(node
):
714 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
715 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
717 def depart_enumerated_list(self
, node
):
718 self
.body
.append('</ol>\n')
720 def visit_field_list(self
, node
):
721 # Keep simple paragraphs in the field_body to enable CSS
722 # rule to start body on new line if the label is too long
723 classes
= 'field-list'
724 if (self
.is_compactable(node
)):
726 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
728 def depart_field_list(self
, node
):
729 self
.body
.append('</dl>\n')
731 def visit_field(self
, node
):
734 def depart_field(self
, node
):
737 # as field is ignored, pass class arguments to field-name and field-body:
739 def visit_field_name(self
, node
):
740 self
.body
.append(self
.starttag(node
, 'dt', '',
741 CLASS
=''.join(node
.parent
['classes'])))
743 def depart_field_name(self
, node
):
744 self
.body
.append('</dt>\n')
746 def visit_field_body(self
, node
):
747 self
.body
.append(self
.starttag(node
, 'dd', '',
748 CLASS
=''.join(node
.parent
['classes'])))
749 # prevent misalignment of following content if the field is empty:
750 if not node
.children
:
751 self
.body
.append('<p></p>')
753 def depart_field_body(self
, node
):
754 self
.body
.append('</dd>\n')
756 def visit_figure(self
, node
):
757 atts
= {'class': 'figure'}
758 if node
.get('width'):
759 atts
['style'] = 'width: %s' % node
['width']
760 if node
.get('align'):
761 atts
['class'] += " align-" + node
['align']
762 self
.body
.append(self
.starttag(node
, 'div', **atts
))
764 def depart_figure(self
, node
):
765 self
.body
.append('</div>\n')
767 # use HTML 5 <footer> element?
768 def visit_footer(self
, node
):
769 self
.context
.append(len(self
.body
))
771 def depart_footer(self
, node
):
772 start
= self
.context
.pop()
773 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
774 '<hr class="footer" />\n']
775 footer
.extend(self
.body
[start
:])
776 footer
.append('\n</div>\n')
777 self
.footer
.extend(footer
)
778 self
.body_suffix
[:0] = footer
779 del self
.body
[start
:]
783 # use definition list instead of table for footnote text
785 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
786 def visit_footnote(self
, node
):
787 if not self
.in_footnote_list
:
788 classes
= 'footnote ' + self
.settings
.footnote_references
789 self
.body
.append('<dl class="%s">\n'%classes
)
790 self
.in_footnote_list
= True
792 def depart_footnote(self
, node
):
793 self
.body
.append('</dd>\n')
794 if not isinstance(node
.next_node(descend
=False, siblings
=True),
796 self
.body
.append('</dl>\n')
797 self
.in_footnote_list
= False
799 def visit_footnote_reference(self
, node
):
800 href
= '#' + node
['refid']
801 classes
= 'footnote-reference ' + self
.settings
.footnote_references
802 self
.body
.append(self
.starttag(node
, 'a', '', #suffix,
803 CLASS
=classes
, href
=href
))
805 def depart_footnote_reference(self
, node
):
806 self
.body
.append('</a>')
808 # Docutils-generated text: put section numbers in a span for CSS styling:
809 def visit_generated(self
, node
):
810 if 'sectnum' in node
['classes']:
811 # get section number (strip trailing no-break-spaces)
812 sectnum
= node
.astext().rstrip(u
' ')
813 # print sectnum.encode('utf-8')
814 self
.body
.append('<span class="sectnum">%s</span> '
815 % self
.encode(sectnum
))
816 # Content already processed:
819 def depart_generated(self
, node
):
822 def visit_header(self
, node
):
823 self
.context
.append(len(self
.body
))
825 def depart_header(self
, node
):
826 start
= self
.context
.pop()
827 header
= [self
.starttag(node
, 'div', CLASS
='header')]
828 header
.extend(self
.body
[start
:])
829 header
.append('\n<hr class="header"/>\n</div>\n')
830 self
.body_prefix
.extend(header
)
831 self
.header
.extend(header
)
832 del self
.body
[start
:]
834 # Image types to place in an <object> element
835 object_image_types
= {'.swf': 'application/x-shockwave-flash'}
837 def visit_image(self
, node
):
840 ext
= os
.path
.splitext(uri
)[1].lower()
841 if ext
in self
.object_image_types
:
843 atts
['type'] = self
.object_image_types
[ext
]
846 atts
['alt'] = node
.get('alt', uri
)
849 atts
['width'] = node
['width']
851 atts
['height'] = node
['height']
853 if (PIL
and not ('width' in node
and 'height' in node
)
854 and self
.settings
.file_insertion_enabled
):
855 imagepath
= urllib
.url2pathname(uri
)
857 img
= PIL
.Image
.open(
858 imagepath
.encode(sys
.getfilesystemencoding()))
859 except (IOError, UnicodeEncodeError):
862 self
.settings
.record_dependencies
.add(
863 imagepath
.replace('\\', '/'))
864 if 'width' not in atts
:
865 atts
['width'] = '%dpx' % img
.size
[0]
866 if 'height' not in atts
:
867 atts
['height'] = '%dpx' % img
.size
[1]
869 for att_name
in 'width', 'height':
871 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
873 atts
[att_name
] = '%s%s' % (
874 float(match
.group(1)) * (float(node
['scale']) / 100),
877 for att_name
in 'width', 'height':
879 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
880 # Interpret unitless values as pixels.
881 atts
[att_name
] += 'px'
882 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
885 atts
['style'] = ' '.join(style
)
886 if (isinstance(node
.parent
, nodes
.TextElement
) or
887 (isinstance(node
.parent
, nodes
.reference
) and
888 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
889 # Inline context or surrounded by <a>...</a>.
894 atts
['class'] = 'align-%s' % node
['align']
895 if ext
in self
.object_image_types
:
896 # do NOT use an empty tag: incorrect rendering in browsers
897 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
898 node
.get('alt', uri
) + '</object>' + suffix
)
900 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
902 def depart_image(self
, node
):
903 # self.body.append(self.context.pop())
906 def visit_inline(self
, node
):
907 self
.body
.append(self
.starttag(node
, 'span', ''))
909 def depart_inline(self
, node
):
910 self
.body
.append('</span>')
912 # footnote and citation labels:
913 def visit_label(self
, node
):
914 if (isinstance(node
.parent
, nodes
.footnote
)):
915 classes
= self
.settings
.footnote_references
918 # pass parent node to get id into starttag:
919 self
.body
.append(self
.starttag(node
.parent
, 'dt', '', CLASS
='label'))
920 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
=classes
))
921 # footnote/citation backrefs:
922 if self
.settings
.footnote_backlinks
:
923 backrefs
= node
.parent
['backrefs']
924 if len(backrefs
) == 1:
925 self
.body
.append('<a class="fn-backref" href="#%s">'
928 def depart_label(self
, node
):
929 if self
.settings
.footnote_backlinks
:
930 backrefs
= node
.parent
['backrefs']
931 if len(backrefs
) == 1:
932 self
.body
.append('</a>')
933 self
.body
.append('</span>')
934 if self
.settings
.footnote_backlinks
and len(backrefs
) > 1:
935 # Python 2.4 fails with enumerate(backrefs, 1)
936 backlinks
= ['<a href="#%s">%s</a>' % (ref
, i
+1)
937 for (i
, ref
) in enumerate(backrefs
)]
938 self
.body
.append('<span class="fn-backref">(%s)</span>'
939 % ','.join(backlinks
))
940 self
.body
.append('</dt>\n<dd>')
942 def visit_legend(self
, node
):
943 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
945 def depart_legend(self
, node
):
946 self
.body
.append('</div>\n')
948 def visit_line(self
, node
):
949 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
951 self
.body
.append('<br />')
953 def depart_line(self
, node
):
954 self
.body
.append('</div>\n')
956 def visit_line_block(self
, node
):
957 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
959 def depart_line_block(self
, node
):
960 self
.body
.append('</div>\n')
962 def visit_list_item(self
, node
):
963 self
.body
.append(self
.starttag(node
, 'li', ''))
965 def depart_list_item(self
, node
):
966 self
.body
.append('</li>\n')
969 def visit_literal(self
, node
):
970 # special case: "code" role
971 classes
= node
.get('classes', [])
972 if 'code' in classes
:
973 # filter 'code' from class arguments
974 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
975 self
.body
.append(self
.starttag(node
, 'code', ''))
978 self
.starttag(node
, 'span', '', CLASS
='docutils literal'))
980 # remove hard line breaks (except if in a parsed-literal block)
981 if not isinstance(node
.parent
, nodes
.literal_block
):
982 text
= text
.replace('\n', ' ')
983 # Protect text like ``--an-option`` and the regular expression
984 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
985 for token
in self
.words_and_spaces
.findall(text
):
986 if token
.strip() and self
.sollbruchstelle
.search(token
):
987 self
.body
.append('<span class="pre">%s</span>'
988 % self
.encode(token
))
990 self
.body
.append(self
.encode(token
))
991 self
.body
.append('</span>')
992 # Content already processed:
995 def depart_literal(self
, node
):
996 # skipped unless literal element is from "code" role:
997 self
.body
.append('</code>')
999 def visit_literal_block(self
, node
):
1000 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='literal-block'))
1001 if 'code' in node
.get('classes', []):
1002 self
.body
.append('<code>')
1004 def depart_literal_block(self
, node
):
1005 if 'code' in node
.get('classes', []):
1006 self
.body
.append('</code>')
1007 self
.body
.append('</pre>\n')
1010 # As there is no native HTML math support, we provide alternatives
1011 # for the math-output: LaTeX and MathJax simply wrap the content,
1012 # HTML and MathML also convert the math_code.
1014 math_tags
= {# math_output: (block, inline, class-arguments)
1015 'mathml': ('div', '', ''),
1016 'html': ('div', 'span', 'formula'),
1017 'mathjax': ('div', 'span', 'math'),
1018 'latex': ('pre', 'tt', 'math'),
1021 def visit_math(self
, node
, math_env
=''):
1022 # If the method is called from visit_math_block(), math_env != ''.
1024 if self
.math_output
not in self
.math_tags
:
1025 self
.document
.reporter
.error(
1026 'math-output format "%s" not supported '
1027 'falling back to "latex"'% self
.math_output
)
1028 self
.math_output
= 'latex'
1029 tag
= self
.math_tags
[self
.math_output
][math_env
== '']
1030 clsarg
= self
.math_tags
[self
.math_output
][2]
1032 wrappers
= {# math_mode: (inline, block)
1033 'mathml': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1034 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1035 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1036 'latex': (None, None),
1038 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1039 if self
.math_output
== 'mathml' and (not self
.math_output_options
or
1040 self
.math_output_options
[0] == 'blahtexml'):
1042 # get and wrap content
1043 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1045 try: # wrapper with three "%s"
1046 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1047 except TypeError: # wrapper with one "%s"
1048 math_code
= wrapper
% math_code
1049 # settings and conversion
1050 if self
.math_output
in ('latex', 'mathjax'):
1051 math_code
= self
.encode(math_code
)
1052 if self
.math_output
== 'mathjax' and not self
.math_header
:
1053 if self
.math_output_options
:
1054 self
.mathjax_url
= self
.math_output_options
[0]
1055 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1056 elif self
.math_output
== 'html':
1057 if self
.math_output_options
and not self
.math_header
:
1058 self
.math_header
= [self
.stylesheet_call(
1059 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1060 for s
in self
.math_output_options
[0].split(',')]
1061 # TODO: fix display mode in matrices and fractions
1062 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1063 math_code
= math2html
.math2html(math_code
)
1064 elif self
.math_output
== 'mathml':
1065 if 'XHTML 1' in self
.doctype
:
1066 self
.doctype
= self
.doctype_mathml
1067 self
.content_type
= self
.content_type_mathml
1068 converter
= ' '.join(self
.math_output_options
).lower()
1070 if converter
== 'latexml':
1071 math_code
= tex2mathml_extern
.latexml(math_code
,
1072 self
.document
.reporter
)
1073 elif converter
== 'ttm':
1074 math_code
= tex2mathml_extern
.ttm(math_code
,
1075 self
.document
.reporter
)
1076 elif converter
== 'blahtexml':
1077 math_code
= tex2mathml_extern
.blahtexml(math_code
,
1078 inline
=not(math_env
),
1079 reporter
=self
.document
.reporter
)
1081 math_code
= latex2mathml
.tex2mathml(math_code
,
1082 inline
=not(math_env
))
1084 self
.document
.reporter
.error('option "%s" not supported '
1085 'with math-output "MathML"')
1087 raise OSError('is "latexmlmath" in your PATH?')
1088 except SyntaxError, err
:
1089 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1090 self
.visit_system_message(err_node
)
1091 self
.body
.append(self
.starttag(node
, 'p'))
1092 self
.body
.append(u
','.join(err
.args
))
1093 self
.body
.append('</p>\n')
1094 self
.body
.append(self
.starttag(node
, 'pre',
1095 CLASS
='literal-block'))
1096 self
.body
.append(self
.encode(math_code
))
1097 self
.body
.append('\n</pre>\n')
1098 self
.depart_system_message(err_node
)
1099 raise nodes
.SkipNode
1100 # append to document body
1102 self
.body
.append(self
.starttag(node
, tag
,
1103 suffix
='\n'*bool(math_env
),
1105 self
.body
.append(math_code
)
1106 if math_env
: # block mode (equation, display)
1107 self
.body
.append('\n')
1109 self
.body
.append('</%s>' % tag
)
1111 self
.body
.append('\n')
1112 # Content already processed:
1113 raise nodes
.SkipNode
1115 def depart_math(self
, node
):
1116 pass # never reached
1118 def visit_math_block(self
, node
):
1119 # print node.astext().encode('utf8')
1120 math_env
= pick_math_environment(node
.astext())
1121 self
.visit_math(node
, math_env
=math_env
)
1123 def depart_math_block(self
, node
):
1124 pass # never reached
1126 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1127 # HTML5/polyglot recommends using both
1128 def visit_meta(self
, node
):
1129 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1132 def depart_meta(self
, node
):
1135 def add_meta(self
, tag
):
1136 self
.meta
.append(tag
)
1137 self
.head
.append(tag
)
1139 def visit_option(self
, node
):
1140 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1142 def depart_option(self
, node
):
1143 self
.body
.append('</span>')
1144 if isinstance(node
.next_node(descend
=False, siblings
=True),
1146 self
.body
.append(', ')
1148 def visit_option_argument(self
, node
):
1149 self
.body
.append(node
.get('delimiter', ' '))
1150 self
.body
.append(self
.starttag(node
, 'var', ''))
1152 def depart_option_argument(self
, node
):
1153 self
.body
.append('</var>')
1155 def visit_option_group(self
, node
):
1156 self
.body
.append(self
.starttag(node
, 'dt', ''))
1157 self
.body
.append('<kbd>')
1159 def depart_option_group(self
, node
):
1160 self
.body
.append('</kbd></dt>\n')
1162 def visit_option_list(self
, node
):
1164 self
.starttag(node
, 'dl', CLASS
='option-list'))
1166 def depart_option_list(self
, node
):
1167 self
.body
.append('</dl>\n')
1169 def visit_option_list_item(self
, node
):
1172 def depart_option_list_item(self
, node
):
1175 def visit_option_string(self
, node
):
1178 def depart_option_string(self
, node
):
1181 def visit_organization(self
, node
):
1182 self
.visit_docinfo_item(node
, 'organization')
1184 def depart_organization(self
, node
):
1185 self
.depart_docinfo_item()
1187 # Do not omit <p> tags
1188 # --------------------
1190 # The HTML4CSS1 writer does this to "produce
1191 # visually compact lists (less vertical whitespace)". This writer
1192 # relies on CSS rules for"visual compactness".
1194 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1195 # character data, so you cannot drop the <p> tags.
1196 # * Keeping simple paragraphs in the field_body enables a CSS
1197 # rule to start the field-body on a new line if the label is too long
1198 # * it makes the code simpler.
1200 # TODO: omit paragraph tags in simple table cells?
1202 def visit_paragraph(self
, node
):
1203 self
.body
.append(self
.starttag(node
, 'p', ''))
1205 def depart_paragraph(self
, node
):
1206 self
.body
.append('</p>')
1207 if not (isinstance(node
.parent
, (nodes
.list_item
, nodes
.entry
)) and
1208 (len(node
.parent
) == 1)):
1209 self
.body
.append('\n')
1211 def visit_problematic(self
, node
):
1212 if node
.hasattr('refid'):
1213 self
.body
.append('<a href="#%s">' % node
['refid'])
1214 self
.context
.append('</a>')
1216 self
.context
.append('')
1217 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1219 def depart_problematic(self
, node
):
1220 self
.body
.append('</span>')
1221 self
.body
.append(self
.context
.pop())
1223 def visit_raw(self
, node
):
1224 if 'html' in node
.get('format', '').split():
1225 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1227 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1228 self
.body
.append(node
.astext())
1230 self
.body
.append('</%s>' % t
)
1231 # Keep non-HTML raw text out of output:
1232 raise nodes
.SkipNode
1234 def visit_reference(self
, node
):
1235 atts
= {'class': 'reference'}
1236 if 'refuri' in node
:
1237 atts
['href'] = node
['refuri']
1238 if ( self
.settings
.cloak_email_addresses
1239 and atts
['href'].startswith('mailto:')):
1240 atts
['href'] = self
.cloak_mailto(atts
['href'])
1241 self
.in_mailto
= True
1242 atts
['class'] += ' external'
1244 assert 'refid' in node
, \
1245 'References must have "refuri" or "refid" attribute.'
1246 atts
['href'] = '#' + node
['refid']
1247 atts
['class'] += ' internal'
1248 if not isinstance(node
.parent
, nodes
.TextElement
):
1249 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1250 atts
['class'] += ' image-reference'
1251 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1253 def depart_reference(self
, node
):
1254 self
.body
.append('</a>')
1255 if not isinstance(node
.parent
, nodes
.TextElement
):
1256 self
.body
.append('\n')
1257 self
.in_mailto
= False
1259 def visit_revision(self
, node
):
1260 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1262 def depart_revision(self
, node
):
1263 self
.depart_docinfo_item()
1265 def visit_row(self
, node
):
1266 self
.body
.append(self
.starttag(node
, 'tr', ''))
1269 def depart_row(self
, node
):
1270 self
.body
.append('</tr>\n')
1272 def visit_rubric(self
, node
):
1273 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1275 def depart_rubric(self
, node
):
1276 self
.body
.append('</p>\n')
1278 # TODO: use the new HTML 5 element <section>?
1279 def visit_section(self
, node
):
1280 self
.section_level
+= 1
1282 self
.starttag(node
, 'div', CLASS
='section'))
1284 def depart_section(self
, node
):
1285 self
.section_level
-= 1
1286 self
.body
.append('</div>\n')
1288 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1289 def visit_sidebar(self
, node
):
1291 self
.starttag(node
, 'div', CLASS
='sidebar'))
1292 self
.in_sidebar
= True
1294 def depart_sidebar(self
, node
):
1295 self
.body
.append('</div>\n')
1296 self
.in_sidebar
= False
1298 def visit_status(self
, node
):
1299 self
.visit_docinfo_item(node
, 'status', meta
=False)
1301 def depart_status(self
, node
):
1302 self
.depart_docinfo_item()
1304 def visit_strong(self
, node
):
1305 self
.body
.append(self
.starttag(node
, 'strong', ''))
1307 def depart_strong(self
, node
):
1308 self
.body
.append('</strong>')
1310 def visit_subscript(self
, node
):
1311 self
.body
.append(self
.starttag(node
, 'sub', ''))
1313 def depart_subscript(self
, node
):
1314 self
.body
.append('</sub>')
1316 def visit_substitution_definition(self
, node
):
1317 """Internal only."""
1318 raise nodes
.SkipNode
1320 def visit_substitution_reference(self
, node
):
1321 self
.unimplemented_visit(node
)
1323 # h1–h6 elements must not be used to markup subheadings, subtitles,
1324 # alternative titles and taglines unless intended to be the heading for a
1325 # new section or subsection.
1326 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1327 def visit_subtitle(self
, node
):
1328 if isinstance(node
.parent
, nodes
.sidebar
):
1329 classes
= 'sidebar-subtitle'
1330 elif isinstance(node
.parent
, nodes
.document
):
1331 classes
= 'subtitle'
1332 self
.in_document_title
= len(self
.body
)
1333 elif isinstance(node
.parent
, nodes
.section
):
1334 classes
= 'section-subtitle'
1335 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
=classes
))
1337 def depart_subtitle(self
, node
):
1338 self
.body
.append('</p>\n')
1339 if self
.in_document_title
:
1340 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1341 self
.in_document_title
= 0
1342 self
.body_pre_docinfo
.extend(self
.body
)
1343 self
.html_subtitle
.extend(self
.body
)
1346 def visit_superscript(self
, node
):
1347 self
.body
.append(self
.starttag(node
, 'sup', ''))
1349 def depart_superscript(self
, node
):
1350 self
.body
.append('</sup>')
1352 def visit_system_message(self
, node
):
1353 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1354 self
.body
.append('<p class="system-message-title">')
1356 if len(node
['backrefs']):
1357 backrefs
= node
['backrefs']
1358 if len(backrefs
) == 1:
1359 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1364 for backref
in backrefs
:
1365 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1367 backref_text
= ('; <em>backlinks: %s</em>'
1368 % ', '.join(backlinks
))
1369 if node
.hasattr('line'):
1370 line
= ', line %s' % node
['line']
1373 self
.body
.append('System Message: %s/%s '
1374 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1375 % (node
['type'], node
['level'],
1376 self
.encode(node
['source']), line
, backref_text
))
1378 def depart_system_message(self
, node
):
1379 self
.body
.append('</div>\n')
1383 # no hard-coded border setting in the table head::
1385 def visit_table(self
, node
):
1386 classes
= [cls
.strip(u
' \t\n')
1387 for cls
in self
.settings
.table_style
.split(',')]
1389 classes
.append('align-%s' % node
['align'])
1390 tag
= self
.starttag(node
, 'table', CLASS
=' '.join(classes
))
1391 self
.body
.append(tag
)
1393 def depart_table(self
, node
):
1394 self
.body
.append('</table>\n')
1396 def visit_target(self
, node
):
1397 if not ('refuri' in node
or 'refid' in node
1398 or 'refname' in node
):
1399 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1400 self
.context
.append('</span>')
1402 self
.context
.append('')
1404 def depart_target(self
, node
):
1405 self
.body
.append(self
.context
.pop())
1407 # no hard-coded vertical alignment in table body
1408 def visit_tbody(self
, node
):
1409 self
.body
.append(self
.starttag(node
, 'tbody'))
1411 def depart_tbody(self
, node
):
1412 self
.body
.append('</tbody>\n')
1414 def visit_term(self
, node
):
1415 self
.body
.append(self
.starttag(node
, 'dt', ''))
1417 def depart_term(self
, node
):
1419 Leave the end tag to `self.visit_definition()`, in case there's a
1424 def visit_tgroup(self
, node
):
1428 def depart_tgroup(self
, node
):
1431 def visit_thead(self
, node
):
1432 self
.body
.append(self
.starttag(node
, 'thead'))
1434 def depart_thead(self
, node
):
1435 self
.body
.append('</thead>\n')
1437 def visit_title(self
, node
):
1438 """Only 6 section levels are supported by HTML."""
1439 check_id
= 0 # TODO: is this a bool (False) or a counter?
1440 close_tag
= '</p>\n'
1441 if isinstance(node
.parent
, nodes
.topic
):
1443 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1444 elif isinstance(node
.parent
, nodes
.sidebar
):
1446 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1447 elif isinstance(node
.parent
, nodes
.Admonition
):
1449 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1450 elif isinstance(node
.parent
, nodes
.table
):
1452 self
.starttag(node
, 'caption', ''))
1453 close_tag
= '</caption>\n'
1454 elif isinstance(node
.parent
, nodes
.document
):
1455 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1456 close_tag
= '</h1>\n'
1457 self
.in_document_title
= len(self
.body
)
1459 assert isinstance(node
.parent
, nodes
.section
)
1460 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1462 if (len(node
.parent
) >= 2 and
1463 isinstance(node
.parent
[1], nodes
.subtitle
)):
1464 atts
['CLASS'] = 'with-subtitle'
1466 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1468 if node
.hasattr('refid'):
1469 atts
['class'] = 'toc-backref'
1470 atts
['href'] = '#' + node
['refid']
1472 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1473 close_tag
= '</a></h%s>\n' % (h_level
)
1475 close_tag
= '</h%s>\n' % (h_level
)
1476 self
.context
.append(close_tag
)
1478 def depart_title(self
, node
):
1479 self
.body
.append(self
.context
.pop())
1480 if self
.in_document_title
:
1481 self
.title
= self
.body
[self
.in_document_title
:-1]
1482 self
.in_document_title
= 0
1483 self
.body_pre_docinfo
.extend(self
.body
)
1484 self
.html_title
.extend(self
.body
)
1487 def visit_title_reference(self
, node
):
1488 self
.body
.append(self
.starttag(node
, 'cite', ''))
1490 def depart_title_reference(self
, node
):
1491 self
.body
.append('</cite>')
1493 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1494 def visit_topic(self
, node
):
1495 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1496 self
.topic_classes
= node
['classes']
1497 # TODO: replace with ::
1498 # self.in_contents = 'contents' in node['classes']
1500 def depart_topic(self
, node
):
1501 self
.body
.append('</div>\n')
1502 self
.topic_classes
= []
1503 # TODO self.in_contents = False
1505 def visit_transition(self
, node
):
1506 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1508 def depart_transition(self
, node
):
1511 def visit_version(self
, node
):
1512 self
.visit_docinfo_item(node
, 'version', meta
=False)
1514 def depart_version(self
, node
):
1515 self
.depart_docinfo_item()
1517 def unimplemented_visit(self
, node
):
1518 raise NotImplementedError('visiting unimplemented node type: %s'
1519 % node
.__class
__.__name
__)
1522 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1525 Raise `nodes.NodeFound` if non-simple list item is encountered.
1527 Here "simple" means a list item containing nothing other than a single
1528 paragraph, a simple list, or a paragraph followed by a simple list.
1530 This version also checks for simple field lists and docinfo.
1533 def default_visit(self
, node
):
1534 raise nodes
.NodeFound
1536 def visit_list_item(self
, node
):
1537 # print "visiting list item", node.__class__
1538 children
= [child
for child
in node
.children
1539 if not isinstance(child
, nodes
.Invisible
)]
1540 # print "has %s visible children" % len(children)
1541 if (children
and isinstance(children
[0], nodes
.paragraph
)
1542 and (isinstance(children
[-1], nodes
.bullet_list
) or
1543 isinstance(children
[-1], nodes
.enumerated_list
) or
1544 isinstance(children
[-1], nodes
.field_list
))):
1546 # print "%s children remain" % len(children)
1547 if len(children
) <= 1:
1550 # print "found", child.__class__, "in", node.__class__
1551 raise nodes
.NodeFound
1553 def pass_node(self
, node
):
1556 def ignore_node(self
, node
):
1557 # ignore nodes that are never complex (can contain only inline nodes)
1558 raise nodes
.SkipNode
1560 # Paragraphs and text
1561 visit_Text
= ignore_node
1562 visit_paragraph
= ignore_node
1565 visit_bullet_list
= pass_node
1566 visit_enumerated_list
= pass_node
1567 visit_docinfo
= pass_node
1570 visit_author
= ignore_node
1571 visit_authors
= visit_list_item
1572 visit_address
= visit_list_item
1573 visit_contact
= pass_node
1574 visit_copyright
= ignore_node
1575 visit_date
= ignore_node
1576 visit_organization
= ignore_node
1577 visit_status
= ignore_node
1578 visit_version
= visit_list_item
1581 visit_definition_list
= pass_node
1582 visit_definition_list_item
= pass_node
1583 visit_term
= ignore_node
1584 visit_classifier
= pass_node
1585 visit_definition
= visit_list_item
1588 visit_field_list
= pass_node
1589 visit_field
= pass_node
1590 # the field body corresponds to a list item
1591 visit_field_body
= visit_list_item
1592 visit_field_name
= ignore_node
1594 # Invisible nodes should be ignored.
1595 visit_comment
= ignore_node
1596 visit_substitution_definition
= ignore_node
1597 visit_target
= ignore_node
1598 visit_pending
= ignore_node