2 # :Author: Günter Milde <milde@users.berlios.de>
3 # :Revision: $Revision$
4 # :Date: $Date: 2005-06-28$
5 # :Copyright: © 2005, 2009 Günter Milde.
6 # :License: Released under the terms of the `2-Clause BSD license`_, in short:
8 # Copying and distribution of this file, with or without modification,
9 # are permitted in any medium without royalty provided the copyright
10 # notice and this notice are preserved.
11 # This file is offered as-is, without any warranty.
13 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
15 # Use "best practice" as recommended by the W3C:
16 # http://www.w3.org/2009/cheatsheet/
20 Basic HyperText Markup Language document tree Writer.
22 The output conforms to the `HTML 5` specification as well as
23 to `XHTML 1.0 transitional`.
25 The cascading style sheet "html-base.css" is required for proper viewing.
27 __docformat__
= 'reStructuredText'
34 try: # check for the Python Imaging Library
37 try: # sometimes PIL modules are put in PYTHONPATH's root
39 class PIL(object): pass # dummy wrapper
44 from docutils
import frontend
, nodes
, utils
, writers
, languages
, io
45 from docutils
.utils
.error_reporting
import SafeString
46 from docutils
.transforms
import writer_aux
47 from docutils
.utils
.math
import unichar2tex
, pick_math_environment
, math2html
48 from docutils
.utils
.math
.latex2mathml
import parse_latex_math
50 class Writer(writers
.Writer
):
52 supported
= ('html', 'html5', 'xhtml')
53 """Formats this writer supports."""
55 default_stylesheets
= ['html-base.css']
56 default_stylesheet_dirs
= ['.', os
.path
.abspath(os
.path
.dirname(__file__
))]
58 default_template
= 'template.txt'
59 default_template_path
= os
.path
.join(
60 os
.path
.dirname(os
.path
.abspath(__file__
)), default_template
)
63 'HTML-Specific Options',
65 (('Specify the template file (UTF-8 encoded). Default is "%s".'
66 % default_template_path
,
68 {'default': default_template_path
, 'metavar': '<file>'}),
69 ('Comma separated list of stylesheet URLs. '
70 'Overrides previous --stylesheet and --stylesheet-path settings.',
72 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
73 'validator': frontend
.validate_comma_separated_list
}),
74 ('Comma separated list of stylesheet paths. '
75 'Relative paths are expanded if a matching file is found in '
76 'the --stylesheet-dirs. With --link-stylesheet, '
77 'the path is rewritten relative to the output HTML file. '
78 'Default: "%s"' % ','.join(default_stylesheets
),
79 ['--stylesheet-path'],
80 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
81 'validator': frontend
.validate_comma_separated_list
,
82 'default': default_stylesheets
}),
83 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
84 'files must be accessible during processing. This is the default.',
85 ['--embed-stylesheet'],
86 {'default': 1, 'action': 'store_true',
87 'validator': frontend
.validate_boolean
}),
88 ('Link to the stylesheet(s) in the output HTML file. '
89 'Default: embed stylesheets.',
90 ['--link-stylesheet'],
91 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
92 ('Comma-separated list of directories where stylesheets are found. '
93 'Used by --stylesheet-path when expanding relative path arguments. '
94 'Default: "%s"' % default_stylesheet_dirs
,
95 ['--stylesheet-dirs'],
96 {'metavar': '<dir[,dir,...]>',
97 'validator': frontend
.validate_comma_separated_list
,
98 'default': default_stylesheet_dirs
}),
99 ('Specify the initial header level. Default is 1 for "<h1>". '
100 'Does not affect document title & subtitle (see --no-doc-title).',
101 ['--initial-header-level'],
102 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
103 'metavar': '<level>'}),
104 ('Format for footnote references: one of "superscript" or '
105 '"brackets". Default is "brackets".',
106 ['--footnote-references'],
107 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
108 'metavar': '<format>',
109 'overrides': 'trim_footnote_reference_space'}),
110 ('Format for block quote attributions: one of "dash" (em-dash '
111 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
113 {'choices': ['dash', 'parentheses', 'parens', 'none'],
114 'default': 'dash', 'metavar': '<format>'}),
115 ('Remove extra vertical whitespace between items of "simple" bullet '
116 'lists and enumerated lists. Default: enabled.',
118 {'default': True, 'action': 'store_true',
119 'validator': frontend
.validate_boolean
}),
120 ('Disable compact simple bullet and enumerated lists.',
121 ['--no-compact-lists'],
122 {'dest': 'compact_lists', 'action': 'store_false'}),
123 ('Remove extra vertical whitespace between items of simple field '
124 'lists. Default: enabled.',
125 ['--compact-field-lists'],
126 {'default': True, 'action': 'store_true',
127 'validator': frontend
.validate_boolean
}),
128 ('Disable compact simple field lists.',
129 ['--no-compact-field-lists'],
130 {'dest': 'compact_field_lists', 'action': 'store_false'}),
131 ('Added to standard table classes. '
132 'Defined styles: "borderless". Default: ""',
135 ('Math output format (one of "MathML", "HTML", "MathJax" '
136 'or "LaTeX") and options(s). Default: "HTML math.css"',
138 {'default': 'HTML math.css'}),
139 ('Prepend an XML declaration. (Thwarts HTML5 conformance.) '
141 ['--xml-declaration'],
142 {'default': False, 'action': 'store_true',
143 'validator': frontend
.validate_boolean
}),
144 ('Omit the XML declaration.',
145 ['--no-xml-declaration'],
146 {'dest': 'xml_declaration', 'action': 'store_false'}),
147 ('Obfuscate email addresses to confuse harvesters while still '
148 'keeping email links usable with standards-compliant browsers.',
149 ['--cloak-email-addresses'],
150 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
152 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
154 config_section
= 'html-base writer'
155 config_section_dependencies
= ('writers',)
157 visitor_attributes
= (
158 'head_prefix', 'head', 'stylesheet', 'body_prefix',
159 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
160 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
161 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
164 def get_transforms(self
):
165 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
168 writers
.Writer
.__init
__(self
)
169 self
.translator_class
= HTMLTranslator
172 self
.visitor
= visitor
= self
.translator_class(self
.document
)
173 self
.document
.walkabout(visitor
)
174 for attr
in self
.visitor_attributes
:
175 setattr(self
, attr
, getattr(visitor
, attr
))
176 self
.output
= self
.apply_template()
178 def apply_template(self
):
179 template_file
= open(self
.document
.settings
.template
, 'rb')
180 template
= unicode(template_file
.read(), 'utf-8')
181 template_file
.close()
182 subs
= self
.interpolation_dict()
183 return template
% subs
185 def interpolation_dict(self
):
187 settings
= self
.document
.settings
188 for attr
in self
.visitor_attributes
:
189 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
190 subs
['encoding'] = settings
.output_encoding
191 subs
['version'] = docutils
.__version
__
194 def assemble_parts(self
):
195 writers
.Writer
.assemble_parts(self
)
196 for part
in self
.visitor_attributes
:
197 self
.parts
[part
] = ''.join(getattr(self
, part
))
200 class HTMLTranslator(nodes
.NodeVisitor
):
203 This writer generates `polyglott markup`: HTML 5 that is also valid XML.
206 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
209 doctype_mathml
= doctype
211 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
212 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
213 content_type
= ('<meta http-equiv="Content-Type"'
214 ' content="text/html; charset=%s" />\n')
215 content_type_xml
= ('<meta http-equiv="Content-Type"'
216 ' content="application/xhtml+xml; charset=%s" />\n')
218 generator
= ('<meta name="generator" content="Docutils %s: '
219 'http://docutils.sourceforge.net/" />\n')
221 # Template for the MathJax script in the header:
222 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
223 # The latest version of MathJax from the distributed server:
224 # avaliable to the public under the `MathJax CDN Terms of Service`__
225 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
226 mathjax_url
= ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
227 'config=TeX-AMS-MML_HTMLorMML')
228 # may be overwritten by custom URL appended to "mathjax"
230 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
231 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
232 words_and_spaces
= re
.compile(r
'\S+| +|\n')
233 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
234 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
236 def __init__(self
, document
):
237 nodes
.NodeVisitor
.__init
__(self
, document
)
238 self
.settings
= settings
= document
.settings
239 lcode
= settings
.language_code
240 self
.language
= languages
.get_language(lcode
, document
.reporter
)
241 self
.meta
= [self
.generator
% docutils
.__version
__]
242 self
.head_prefix
= []
243 self
.html_prolog
= []
244 if settings
.xml_declaration
:
245 self
.head_prefix
.append(self
.xml_declaration
246 % settings
.output_encoding
)
247 self
.content_type
= self
.content_type_xml
248 # encoding not interpolated:
249 self
.html_prolog
.append(self
.xml_declaration
)
250 self
.head
= self
.meta
[:]
251 self
.stylesheet
= [self
.stylesheet_call(path
)
252 for path
in utils
.get_stylesheet_list(settings
)]
253 self
.body_prefix
= ['</head>\n<body>\n']
254 # document title, subtitle display
255 self
.body_pre_docinfo
= []
260 self
.body_suffix
= ['</body>\n</html>\n']
261 self
.section_level
= 0
262 self
.initial_header_level
= int(settings
.initial_header_level
)
264 self
.math_output
= settings
.math_output
.split()
265 self
.math_output_options
= self
.math_output
[1:]
266 self
.math_output
= self
.math_output
[0].lower()
268 # A heterogenous stack used in conjunction with the tree traversal.
269 # Make sure that the pops correspond to the pushes:
272 self
.topic_classes
= [] # TODO: replace with self_in_contents
274 self
.compact_p
= True
275 self
.compact_simple
= False
276 self
.compact_field_list
= False
277 self
.in_docinfo
= False
278 self
.in_sidebar
= False
279 self
.in_footnote_list
= False
284 self
.html_head
= [self
.content_type
] # charset not interpolated
286 self
.html_subtitle
= []
288 self
.in_document_title
= 0 # len(self.body) or 0
289 self
.in_mailto
= False
290 self
.author_in_authors
= False
291 self
.math_header
= []
294 return ''.join(self
.head_prefix
+ self
.head
295 + self
.stylesheet
+ self
.body_prefix
296 + self
.body_pre_docinfo
+ self
.docinfo
297 + self
.body
+ self
.body_suffix
)
299 def encode(self
, text
):
300 """Encode special characters in `text` & return."""
301 # Use only named entities known in both XML and HTML
302 # other characters are automatically encoded "by number" if required.
304 return text
.translate({
309 ord('@'): u
'@', # may thwart some address harvesters
312 def cloak_mailto(self
, uri
):
313 """Try to hide a mailto: URL from harvesters."""
314 # Encode "@" using a URL octet reference (see RFC 1738).
315 # Further cloaking with HTML entities will be done in the
317 return uri
.replace('@', '%40')
319 def cloak_email(self
, addr
):
320 """Try to hide the link text of a email link from harversters."""
321 # Surround at-signs and periods with <span> tags. ("@" has
322 # already been encoded to "@" by the `encode` method.)
323 addr
= addr
.replace('@', '<span>@</span>')
324 addr
= addr
.replace('.', '<span>.</span>')
327 def attval(self
, text
,
328 whitespace
=re
.compile('[\n\r\t\v\f]')):
329 """Cleanse, HTML encode, and return attribute value text."""
330 encoded
= self
.encode(whitespace
.sub(' ', text
))
331 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
332 # Cloak at-signs ("%40") and periods with HTML entities.
333 encoded
= encoded
.replace('%40', '%40')
334 encoded
= encoded
.replace('.', '.')
337 def stylesheet_call(self
, path
):
338 """Return code to reference or embed stylesheet file `path`"""
339 if self
.settings
.embed_stylesheet
:
341 content
= io
.FileInput(source_path
=path
,
342 encoding
='utf-8').read()
343 self
.settings
.record_dependencies
.add(path
)
345 msg
= u
"Cannot embed stylesheet '%s': %s." % (
346 path
, SafeString(err
.strerror
))
347 self
.document
.reporter
.error(msg
)
348 return '<--- %s --->\n' % msg
349 return self
.embedded_stylesheet
% content
350 # else link to style file:
351 if self
.settings
.stylesheet_path
:
352 # adapt path relative to output (cf. config.html#stylesheet-path)
353 path
= utils
.relative_path(self
.settings
._destination
, path
)
354 return self
.stylesheet_link
% self
.encode(path
)
356 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
358 Construct and return a start tag given a node (id & class attributes
359 are extracted), tag name, and optional attributes.
361 tagname
= tagname
.lower()
365 for (name
, value
) in attributes
.items():
366 atts
[name
.lower()] = value
369 # unify class arguments and move language specification
370 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
371 if cls
.startswith('language-'):
372 languages
.append(cls
[9:])
373 elif cls
.strip() and cls
not in classes
:
376 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
377 atts
[self
.lang_attribute
] = languages
[0]
379 atts
['class'] = ' '.join(classes
)
380 assert 'id' not in atts
381 ids
.extend(node
.get('ids', []))
383 ids
.extend(atts
['ids'])
388 # Add empty "span" elements for additional IDs. Note
389 # that we cannot use empty "a" elements because there
390 # may be targets inside of references, but nested "a"
391 # elements aren't allowed in XHTML (even if they do
392 # not all have a "href" attribute).
394 # Empty tag. Insert target right in front of element.
395 prefix
.append('<span id="%s"></span>' % id)
397 # Non-empty tag. Place the auxiliary <span> tag
398 # *inside* the element, as the first child.
399 suffix
+= '<span id="%s"></span>' % id
400 attlist
= atts
.items()
403 for name
, value
in attlist
:
404 # value=None was used for boolean attributes without
405 # value, but this isn't supported by XHTML.
406 assert value
is not None
407 if isinstance(value
, list):
408 values
= [unicode(v
) for v
in value
]
409 parts
.append('%s="%s"' % (name
.lower(),
410 self
.attval(' '.join(values
))))
412 parts
.append('%s="%s"' % (name
.lower(),
413 self
.attval(unicode(value
))))
418 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
420 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
421 """Construct and return an XML-compatible empty tag."""
422 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
424 def set_class_on_child(self
, node
, class_
, index
=0):
426 Set class `class_` on the visible child no. index of `node`.
427 Do nothing if node has fewer children than `index`.
429 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
431 child
= children
[index
]
434 child
['classes'].append(class_
)
436 def visit_Text(self
, node
):
438 encoded
= self
.encode(text
)
439 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
440 encoded
= self
.cloak_email(encoded
)
441 self
.body
.append(encoded
)
443 def depart_Text(self
, node
):
446 def visit_abbreviation(self
, node
):
447 # @@@ implementation incomplete ("title" attribute)
448 self
.body
.append(self
.starttag(node
, 'abbr', ''))
450 def depart_abbreviation(self
, node
):
451 self
.body
.append('</abbr>')
453 def visit_acronym(self
, node
):
454 # @@@ implementation incomplete ("title" attribute)
455 self
.body
.append(self
.starttag(node
, 'abbr', ''))
457 def depart_acronym(self
, node
):
458 self
.body
.append('</abbr>')
460 def visit_address(self
, node
):
461 self
.visit_docinfo_item(node
, 'address', meta
=False)
462 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='address'))
464 def depart_address(self
, node
):
465 self
.body
.append('\n</pre>\n')
466 self
.depart_docinfo_item()
468 def visit_admonition(self
, node
):
469 node
['classes'].insert(0, 'admonition')
470 self
.body
.append(self
.starttag(node
, 'div'))
472 def depart_admonition(self
, node
=None):
473 self
.body
.append('</div>\n')
475 attribution_formats
= {'dash': (u
'\u2014', ''),
476 'parentheses': ('(', ')'),
477 'parens': ('(', ')'),
480 def visit_attribution(self
, node
):
481 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
482 self
.context
.append(suffix
)
484 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
485 self
.body
.append(self
.starttag(node
, 'cite', ''))
487 def depart_attribution(self
, node
):
488 self
.body
.append('</cite>' + self
.context
.pop() + '</p>\n')
492 # Use paragraphs instead of hard-coded linebreaks.
494 def visit_author(self
, node
):
495 if not(isinstance(node
.parent
, nodes
.authors
)):
496 self
.visit_docinfo_item(node
, 'author')
497 self
.body
.append('<p>')
499 def depart_author(self
, node
):
500 self
.body
.append('</p>')
501 if isinstance(node
.parent
, nodes
.authors
):
502 self
.body
.append('\n')
504 self
.depart_docinfo_item()
506 def visit_authors(self
, node
):
507 self
.visit_docinfo_item(node
, 'authors', meta
=False)
509 def depart_authors(self
, node
):
510 self
.depart_docinfo_item()
512 def visit_block_quote(self
, node
):
513 self
.body
.append(self
.starttag(node
, 'blockquote'))
515 def depart_block_quote(self
, node
):
516 self
.body
.append('</blockquote>\n')
518 def check_simple_list(self
, node
):
519 """Check for a simple list that can be rendered compactly."""
520 visitor
= SimpleListChecker(self
.document
)
523 except nodes
.NodeFound
:
530 # Include definition lists and field lists (in addition to ordered
531 # and unordered lists) in the test if a list is "simple" (cf. the
532 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
533 # the end of this file).
535 def is_compactable(self
, node
):
536 # print "is_compactable %s ?" % node.__class__,
537 # explicite class arguments have precedence
538 if 'compact' in node
['classes']:
539 # print "explicitely compact"
541 if 'open' in node
['classes']:
542 # print "explicitely open"
544 # check config setting:
545 if (isinstance(node
, nodes
.field_list
) or
546 isinstance(node
, nodes
.definition_list
)
547 ) and not self
.settings
.compact_field_lists
:
548 # print "`compact-field-lists` is False"
550 if (isinstance(node
, nodes
.enumerated_list
) or
551 isinstance(node
, nodes
.bullet_list
)
552 ) and not self
.settings
.compact_lists
:
553 # print "`compact-lists` is False"
555 # more special cases:
556 if (self
.topic_classes
== ['contents']): # TODO: self.in_contents
558 # check the list items:
559 visitor
= SimpleListChecker(self
.document
)
562 except nodes
.NodeFound
:
563 # print "complex node"
566 # print "simple list"
569 def visit_bullet_list(self
, node
):
571 old_compact_simple
= self
.compact_simple
572 self
.context
.append((self
.compact_simple
, self
.compact_p
))
573 self
.compact_p
= None
574 self
.compact_simple
= self
.is_compactable(node
)
575 if self
.compact_simple
and not old_compact_simple
:
576 atts
['class'] = 'simple'
577 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
579 def depart_bullet_list(self
, node
):
580 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
581 self
.body
.append('</ul>\n')
583 def visit_caption(self
, node
):
584 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
586 def depart_caption(self
, node
):
587 self
.body
.append('</p>\n')
591 # Use definition list instead of table for bibliographic references.
592 # Join adjacent citation entries.
594 def visit_citation(self
, node
):
595 if not self
.in_footnote_list
:
596 self
.body
.append('<dl class="citation">\n')
597 self
.in_footnote_list
= True
599 def depart_citation(self
, node
):
600 self
.body
.append('</dd>\n')
601 if not isinstance(node
.next_node(descend
=False, siblings
=True),
603 self
.body
.append('</dl>\n')
604 self
.in_footnote_list
= False
606 def visit_citation_reference(self
, node
):
609 href
+= node
['refid']
610 elif 'refname' in node
:
611 href
+= self
.document
.nameids
[node
['refname']]
612 # else: # TODO system message (or already in the transform)?
613 # 'Citation reference missing.'
614 self
.body
.append(self
.starttag(
615 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
617 def depart_citation_reference(self
, node
):
618 self
.body
.append(']</a>')
622 # don't insert classifier-delimiter here (done by CSS)
624 def visit_classifier(self
, node
):
625 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
627 def depart_classifier(self
, node
):
628 self
.body
.append('</span>')
630 def visit_colspec(self
, node
):
631 self
.colspecs
.append(node
)
632 # "stubs" list is an attribute of the tgroup element:
633 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
635 def depart_colspec(self
, node
):
638 def write_colspecs(self
):
640 for node
in self
.colspecs
:
641 width
+= node
['colwidth']
642 for node
in self
.colspecs
:
643 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
644 self
.body
.append(self
.emptytag(node
, 'col',
645 style
='width: %i%%' % colwidth
))
648 def visit_comment(self
, node
,
649 sub
=re
.compile('-(?=-)').sub
):
650 """Escape double-dashes in comment text."""
651 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
652 # Content already processed:
655 def visit_compound(self
, node
):
656 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
658 node
[0]['classes'].append('compound-first')
659 node
[-1]['classes'].append('compound-last')
660 for child
in node
[1:-1]:
661 child
['classes'].append('compound-middle')
663 def depart_compound(self
, node
):
664 self
.body
.append('</div>\n')
666 def visit_container(self
, node
):
667 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
669 def depart_container(self
, node
):
670 self
.body
.append('</div>\n')
672 def visit_contact(self
, node
):
673 self
.visit_docinfo_item(node
, 'contact', meta
=False)
675 def depart_contact(self
, node
):
676 self
.depart_docinfo_item()
678 def visit_copyright(self
, node
):
679 self
.visit_docinfo_item(node
, 'copyright', meta
=False)
681 def depart_copyright(self
, node
):
682 self
.depart_docinfo_item()
684 def visit_date(self
, node
):
685 self
.visit_docinfo_item(node
, 'date', meta
=False)
687 def depart_date(self
, node
):
688 self
.depart_docinfo_item()
690 def visit_decoration(self
, node
):
693 def depart_decoration(self
, node
):
696 def visit_definition(self
, node
):
697 self
.body
.append('</dt>\n')
698 self
.body
.append(self
.starttag(node
, 'dd', ''))
700 def depart_definition(self
, node
):
701 self
.body
.append('</dd>\n')
703 def visit_definition_list(self
, node
):
704 classes
= node
.setdefault('classes', [])
705 if self
.is_compactable(node
):
706 classes
.append('simple')
707 self
.body
.append(self
.starttag(node
, 'dl'))
709 def depart_definition_list(self
, node
):
710 self
.body
.append('</dl>\n')
712 def visit_definition_list_item(self
, node
):
713 # pass class arguments, ids and names to definition term:
714 node
.children
[0]['classes'] = (
715 node
.get('classes', []) + node
.children
[0].get('classes', []))
716 node
.children
[0]['ids'] = (
717 node
.get('ids', []) + node
.children
[0].get('ids', []))
718 node
.children
[0]['names'] = (
719 node
.get('names', []) + node
.children
[0].get('names', []))
721 def depart_definition_list_item(self
, node
):
724 def visit_description(self
, node
):
725 self
.body
.append(self
.starttag(node
, 'dd', ''))
727 def depart_description(self
, node
):
728 self
.body
.append('</dd>\n')
733 # use definition list instead of table
735 def visit_docinfo(self
, node
):
737 if (self
.is_compactable(node
)):
739 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
741 def depart_docinfo(self
, node
):
742 self
.body
.append('</dl>\n')
744 def visit_docinfo_item(self
, node
, name
, meta
=True):
746 meta_tag
= '<meta name="%s" content="%s" />\n' \
747 % (name
, self
.attval(node
.astext()))
748 self
.add_meta(meta_tag
)
749 self
.body
.append('<dt class="%s">%s</dt>\n'
750 % (name
, self
.language
.labels
[name
]))
751 self
.body
.append(self
.starttag(node
, 'dd', '', CLASS
=name
))
753 def depart_docinfo_item(self
):
754 self
.body
.append('</dd>\n')
756 # TODO: RSt-parser should treat this as code-block with class "pycon".
757 def visit_doctest_block(self
, node
):
758 self
.body
.append(self
.starttag(node
, 'pre', suffix
='',
759 CLASS
='code pycon doctest-block'))
761 def depart_doctest_block(self
, node
):
762 self
.body
.append('\n</pre>\n')
764 def visit_document(self
, node
):
765 self
.head
.append('<title>%s</title>\n'
766 % self
.encode(node
.get('title', '')))
768 def depart_document(self
, node
):
769 self
.head_prefix
.extend([self
.doctype
,
770 self
.head_prefix_template
%
771 {'lang': self
.settings
.language_code
}])
772 self
.html_prolog
.append(self
.doctype
)
773 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
774 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
776 if self
.math_output
== 'mathjax':
777 self
.head
.extend(self
.math_header
)
779 self
.stylesheet
.extend(self
.math_header
)
780 # skip content-type meta tag with interpolated charset value:
781 self
.html_head
.extend(self
.head
[1:])
782 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
783 self
.body_suffix
.insert(0, '</div>\n')
784 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
785 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
786 + self
.docinfo
+ self
.body
787 + self
.body_suffix
[:-1])
788 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
790 def visit_emphasis(self
, node
):
791 self
.body
.append(self
.starttag(node
, 'em', ''))
793 def depart_emphasis(self
, node
):
794 self
.body
.append('</em>')
796 def visit_entry(self
, node
):
798 if isinstance(node
.parent
.parent
, nodes
.thead
):
799 atts
['class'].append('head')
800 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
801 # "stubs" list is an attribute of the tgroup element
802 atts
['class'].append('stub')
805 atts
['class'] = ' '.join(atts
['class'])
809 node
.parent
.column
+= 1
810 if 'morerows' in node
:
811 atts
['rowspan'] = node
['morerows'] + 1
812 if 'morecols' in node
:
813 atts
['colspan'] = node
['morecols'] + 1
814 node
.parent
.column
+= node
['morecols']
815 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
816 self
.context
.append('</%s>\n' % tagname
.lower())
817 # TODO: why did the html4css1 writer insert an NBSP into empty cells?
818 # if len(node) == 0: # empty cell
819 # self.body.append(' ') # no-break space
821 def depart_entry(self
, node
):
822 self
.body
.append(self
.context
.pop())
824 def visit_enumerated_list(self
, node
):
826 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
827 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
832 atts
['start'] = node
['start']
833 if 'enumtype' in node
:
834 atts
['class'] = node
['enumtype']
835 if self
.is_compactable(node
):
836 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
837 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
839 def depart_enumerated_list(self
, node
):
840 self
.body
.append('</ol>\n')
844 # set as definition list, styled with CSS
846 def visit_field_list(self
, node
):
847 # Keep simple paragraphs in the field_body to enable CSS
848 # rule to start body on new line if the label is too long
849 classes
= 'field-list'
850 if (self
.is_compactable(node
)):
852 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
854 def depart_field_list(self
, node
):
855 self
.body
.append('</dl>\n')
857 def visit_field(self
, node
):
860 def depart_field(self
, node
):
863 def visit_field_name(self
, node
):
864 self
.body
.append(self
.starttag(node
, 'dt', ''))
866 def depart_field_name(self
, node
):
867 self
.body
.append('</dt>\n')
869 def visit_field_body(self
, node
):
870 self
.body
.append(self
.starttag(node
, 'dd', ''))
872 def depart_field_body(self
, node
):
873 self
.body
.append('</dd>\n')
875 def visit_figure(self
, node
):
876 atts
= {'class': 'figure'}
877 if node
.get('width'):
878 atts
['style'] = 'width: %s' % node
['width']
879 if node
.get('align'):
880 atts
['class'] += " align-" + node
['align']
881 self
.body
.append(self
.starttag(node
, 'div', **atts
))
883 def depart_figure(self
, node
):
884 self
.body
.append('</div>\n')
886 # use HTML 5 <footer> element?
887 def visit_footer(self
, node
):
888 self
.context
.append(len(self
.body
))
890 def depart_footer(self
, node
):
891 start
= self
.context
.pop()
892 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
893 '<hr class="footer" />\n']
894 footer
.extend(self
.body
[start
:])
895 footer
.append('\n</div>\n')
896 self
.footer
.extend(footer
)
897 self
.body_suffix
[:0] = footer
898 del self
.body
[start
:]
902 # use definition list instead of table for footnote text
904 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
905 def visit_footnote(self
, node
):
906 if not self
.in_footnote_list
:
907 self
.body
.append('<dl class="footnote">\n')
908 self
.in_footnote_list
= True
910 def depart_footnote(self
, node
):
911 self
.body
.append('</dd>\n')
912 if not isinstance(node
.next_node(descend
=False, siblings
=True),
914 self
.body
.append('</dl>\n')
915 self
.in_footnote_list
= False
917 def visit_footnote_reference(self
, node
):
918 href
= '#' + node
['refid']
919 format
= self
.settings
.footnote_references
920 if format
== 'brackets':
922 self
.context
.append(']')
924 assert format
== 'superscript'
926 self
.context
.append('</sup>')
927 self
.body
.append(self
.starttag(node
, 'a', suffix
,
928 CLASS
='footnote-reference', href
=href
))
930 def depart_footnote_reference(self
, node
):
931 self
.body
.append(self
.context
.pop() + '</a>')
933 def visit_generated(self
, node
):
934 if 'sectnum' in node
['classes']:
935 # get section number (strip trailing no-break-spaces)
936 sectnum
= node
.astext().rstrip(u
' ')
937 # print sectnum.encode('utf-8')
938 self
.body
.append('<span class="sectnum">%s</span> '
939 % self
.encode(sectnum
))
940 # Content already processed:
943 def depart_generated(self
, node
):
946 def visit_header(self
, node
):
947 self
.context
.append(len(self
.body
))
949 def depart_header(self
, node
):
950 start
= self
.context
.pop()
951 header
= [self
.starttag(node
, 'div', CLASS
='header')]
952 header
.extend(self
.body
[start
:])
953 header
.append('\n<hr class="header"/>\n</div>\n')
954 self
.body_prefix
.extend(header
)
955 self
.header
.extend(header
)
956 del self
.body
[start
:]
958 # Image types to place in an <object> element
959 # SVG not supported by IE up to version 8
960 # (html4css1 strives for IE6 compatibility)
961 object_image_types
= {#'.svg': 'image/svg+xml',
962 '.swf': 'application/x-shockwave-flash'}
964 def visit_image(self
, node
):
967 ext
= os
.path
.splitext(uri
)[1].lower()
968 if ext
in self
.object_image_types
:
970 atts
['type'] = self
.object_image_types
[ext
]
973 atts
['alt'] = node
.get('alt', uri
)
976 atts
['width'] = node
['width']
978 atts
['height'] = node
['height']
980 if (PIL
and not ('width' in node
and 'height' in node
)
981 and self
.settings
.file_insertion_enabled
):
982 imagepath
= urllib
.url2pathname(uri
)
984 img
= PIL
.Image
.open(
985 imagepath
.encode(sys
.getfilesystemencoding()))
986 except (IOError, UnicodeEncodeError):
989 self
.settings
.record_dependencies
.add(
990 imagepath
.replace('\\', '/'))
991 if 'width' not in atts
:
992 atts
['width'] = '%dpx' % img
.size
[0]
993 if 'height' not in atts
:
994 atts
['height'] = '%dpx' % img
.size
[1]
996 for att_name
in 'width', 'height':
998 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1000 atts
[att_name
] = '%s%s' % (
1001 float(match
.group(1)) * (float(node
['scale']) / 100),
1004 for att_name
in 'width', 'height':
1005 if att_name
in atts
:
1006 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1007 # Interpret unitless values as pixels.
1008 atts
[att_name
] += 'px'
1009 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1012 atts
['style'] = ' '.join(style
)
1013 if (isinstance(node
.parent
, nodes
.TextElement
) or
1014 (isinstance(node
.parent
, nodes
.reference
) and
1015 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1016 # Inline context or surrounded by <a>...</a>.
1021 atts
['class'] = 'align-%s' % node
['align']
1022 if ext
in self
.object_image_types
:
1023 # do NOT use an empty tag: incorrect rendering in browsers
1024 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1025 node
.get('alt', uri
) + '</object>' + suffix
)
1027 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1029 def depart_image(self
, node
):
1030 # self.body.append(self.context.pop())
1033 def visit_inline(self
, node
):
1034 self
.body
.append(self
.starttag(node
, 'span', ''))
1036 def depart_inline(self
, node
):
1037 self
.body
.append('</span>')
1039 # footnote and citation label
1040 def label_delim(self
, node
, bracket
, superscript
):
1041 """put brackets around label?"""
1042 if isinstance(node
.parent
, nodes
.footnote
):
1043 if self
.settings
.footnote_references
== 'brackets':
1047 assert isinstance(node
.parent
, nodes
.citation
)
1050 def visit_label(self
, node
):
1051 # pass parent node to get id into starttag:
1052 self
.body
.append(self
.starttag(node
.parent
, 'dt', '', CLASS
='label'))
1053 # footnote/citation backrefs:
1054 if self
.settings
.footnote_backlinks
:
1055 backrefs
= node
.parent
['backrefs']
1056 if len(backrefs
) == 1:
1057 self
.body
.append('<a class="fn-backref" href="#%s">'
1059 self
.body
.append(self
.label_delim(node
, '[', ''))
1061 def depart_label(self
, node
):
1062 self
.body
.append(self
.label_delim(node
, ']', ''))
1063 if self
.settings
.footnote_backlinks
:
1064 backrefs
= node
.parent
['backrefs']
1065 if len(backrefs
) == 1:
1066 self
.body
.append('</a>')
1067 elif len(backrefs
) > 1:
1068 # Python 2.4 fails with enumerate(backrefs, 1)
1069 backlinks
= ['<a href="#%s">%s</a>' % (ref
, i
+1)
1070 for (i
, ref
) in enumerate(backrefs
)]
1071 self
.body
.append('<span class="fn-backref">(%s)</span>'
1072 % ','.join(backlinks
))
1073 self
.body
.append('</dt>\n<dd>')
1075 def visit_legend(self
, node
):
1076 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1078 def depart_legend(self
, node
):
1079 self
.body
.append('</div>\n')
1081 def visit_line(self
, node
):
1082 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1084 self
.body
.append('<br />')
1086 def depart_line(self
, node
):
1087 self
.body
.append('</div>\n')
1089 def visit_line_block(self
, node
):
1090 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1092 def depart_line_block(self
, node
):
1093 self
.body
.append('</div>\n')
1095 def visit_list_item(self
, node
):
1096 self
.body
.append(self
.starttag(node
, 'li', ''))
1098 def depart_list_item(self
, node
):
1099 self
.body
.append('</li>\n')
1102 def visit_literal(self
, node
):
1103 # special case: "code" role
1104 classes
= node
.get('classes', [])
1105 if 'code' in classes
:
1106 # filter 'code' from class arguments
1107 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1108 self
.body
.append(self
.starttag(node
, 'code', ''))
1111 self
.starttag(node
, 'span', '', CLASS
='docutils literal'))
1112 text
= node
.astext()
1113 # remove hard line breaks (except if in a parsed-literal block)
1114 if not isinstance(node
.parent
, nodes
.literal_block
):
1115 text
= text
.replace('\n', ' ')
1116 # Protect text like ``--an-option`` and the regular expression
1117 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1118 for token
in self
.words_and_spaces
.findall(text
):
1119 if token
.strip() and self
.sollbruchstelle
.search(token
):
1120 self
.body
.append('<span class="pre">%s</span>'
1121 % self
.encode(token
))
1123 self
.body
.append(self
.encode(token
))
1124 self
.body
.append('</span>')
1125 # Content already processed:
1126 raise nodes
.SkipNode
1128 def depart_literal(self
, node
):
1129 # skipped unless literal element is from "code" role:
1130 self
.body
.append('</code>')
1132 def visit_literal_block(self
, node
):
1133 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='literal-block'))
1134 if 'code' in node
.get('classes', []):
1135 self
.body
.append('<code>')
1137 def depart_literal_block(self
, node
):
1138 if 'code' in node
.get('classes', []):
1139 self
.body
.append('</code>')
1140 self
.body
.append('</pre>\n')
1142 def visit_math(self
, node
, math_env
=''):
1143 # If the method is called from visit_math_block(), math_env != ''.
1145 # As there is no native HTML math support, we provide alternatives:
1146 # LaTeX and MathJax math_output modes simply wrap the content,
1147 # HTML and MathML math_output modes also convert the math_code.
1148 if self
.math_output
not in ('mathml', 'html', 'mathjax', 'latex'):
1149 self
.document
.reporter
.error(
1150 'math-output format "%s" not supported '
1151 'falling back to "latex"'% self
.math_output
)
1152 self
.math_output
= 'latex'
1155 tags
= {# math_output: (block, inline, class-arguments)
1156 'mathml': ('div', '', ''),
1157 'html': ('div', 'span', 'formula'),
1158 'mathjax': ('div', 'span', 'math'),
1159 'latex': ('pre', 'tt', 'math'),
1161 tag
= tags
[self
.math_output
][math_env
== '']
1162 clsarg
= tags
[self
.math_output
][2]
1164 wrappers
= {# math_mode: (inline, block)
1165 'mathml': (None, None),
1166 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1167 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1168 'latex': (None, None),
1170 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1171 # get and wrap content
1172 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1173 if wrapper
and math_env
:
1174 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1176 math_code
= wrapper
% math_code
1177 # settings and conversion
1178 if self
.math_output
in ('latex', 'mathjax'):
1179 math_code
= self
.encode(math_code
)
1180 if self
.math_output
== 'mathjax' and not self
.math_header
:
1181 if self
.math_output_options
:
1182 self
.mathjax_url
= self
.math_output_options
[0]
1183 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1184 elif self
.math_output
== 'html':
1185 if self
.math_output_options
and not self
.math_header
:
1186 self
.math_header
= [self
.stylesheet_call(
1187 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1188 for s
in self
.math_output_options
[0].split(',')]
1189 # TODO: fix display mode in matrices and fractions
1190 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1191 math_code
= math2html
.math2html(math_code
)
1192 elif self
.math_output
== 'mathml':
1193 self
.doctype
= self
.doctype_mathml
1194 # self.content_type = self.content_type_mathml
1196 mathml_tree
= parse_latex_math(math_code
, inline
=not(math_env
))
1197 math_code
= ''.join(mathml_tree
.xml())
1198 except SyntaxError, err
:
1199 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1200 self
.visit_system_message(err_node
)
1201 self
.body
.append(self
.starttag(node
, 'p'))
1202 self
.body
.append(u
','.join(err
.args
))
1203 self
.body
.append('</p>\n')
1204 self
.body
.append(self
.starttag(node
, 'pre',
1205 CLASS
='literal-block'))
1206 self
.body
.append(self
.encode(math_code
))
1207 self
.body
.append('\n</pre>\n')
1208 self
.depart_system_message(err_node
)
1209 raise nodes
.SkipNode
1210 # append to document body
1212 self
.body
.append(self
.starttag(node
, tag
,
1213 suffix
='\n'*bool(math_env
),
1215 self
.body
.append(math_code
)
1216 if math_env
: # block mode (equation, display)
1217 self
.body
.append('\n')
1219 self
.body
.append('</%s>' % tag
)
1221 self
.body
.append('\n')
1222 # Content already processed:
1223 raise nodes
.SkipNode
1225 def depart_math(self
, node
):
1226 pass # never reached
1228 def visit_math_block(self
, node
):
1229 # print node.astext().encode('utf8')
1230 math_env
= pick_math_environment(node
.astext())
1231 self
.visit_math(node
, math_env
=math_env
)
1233 def depart_math_block(self
, node
):
1234 pass # never reached
1236 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1237 # HTML5/polyglott recommends using both
1238 def visit_meta(self
, node
):
1239 if node
.hasattr('lang'):
1240 node
['xml:lang'] = node
['lang']
1242 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1245 def depart_meta(self
, node
):
1248 def add_meta(self
, tag
):
1249 self
.meta
.append(tag
)
1250 self
.head
.append(tag
)
1252 def visit_option(self
, node
):
1253 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1255 def depart_option(self
, node
):
1256 self
.body
.append('</span>')
1257 if isinstance(node
.next_node(descend
=False, siblings
=True),
1259 self
.body
.append(', ')
1261 def visit_option_argument(self
, node
):
1262 self
.body
.append(node
.get('delimiter', ' '))
1263 self
.body
.append(self
.starttag(node
, 'var', ''))
1265 def depart_option_argument(self
, node
):
1266 self
.body
.append('</var>')
1268 def visit_option_group(self
, node
):
1269 self
.body
.append(self
.starttag(node
, 'dt', ''))
1270 self
.body
.append('<kbd>')
1272 def depart_option_group(self
, node
):
1273 self
.body
.append('</kbd></dt>\n')
1275 def visit_option_list(self
, node
):
1277 self
.starttag(node
, 'dl', CLASS
='option-list'))
1279 def depart_option_list(self
, node
):
1280 self
.body
.append('</dl>\n')
1282 def visit_option_list_item(self
, node
):
1285 def depart_option_list_item(self
, node
):
1288 def visit_option_string(self
, node
):
1291 def depart_option_string(self
, node
):
1294 def visit_organization(self
, node
):
1295 self
.visit_docinfo_item(node
, 'organization', meta
=False)
1297 def depart_organization(self
, node
):
1298 self
.depart_docinfo_item()
1300 # Do not omit <p> tags
1301 # --------------------
1303 # The HTML4CSS1 writer does this to "produce
1304 # visually compact lists (less vertical whitespace)". This writer
1305 # relies on CSS rules for"visual compactness".
1307 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1308 # character data, so you cannot drop the <p> tags.
1309 # * Keeping simple paragraphs in the field_body enables a CSS
1310 # rule to start the field-body on a new line if the label is too long
1311 # * it makes the code simpler.
1313 # TODO: omit paragraph tags in simple table cells?
1315 def visit_paragraph(self
, node
):
1316 self
.body
.append(self
.starttag(node
, 'p', ''))
1318 def depart_paragraph(self
, node
):
1319 self
.body
.append('</p>')
1320 if not (isinstance(node
.parent
, (nodes
.list_item
, nodes
.entry
)) and
1321 (len(node
.parent
) == 1)):
1322 self
.body
.append('\n')
1324 def visit_problematic(self
, node
):
1325 if node
.hasattr('refid'):
1326 self
.body
.append('<a href="#%s">' % node
['refid'])
1327 self
.context
.append('</a>')
1329 self
.context
.append('')
1330 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1332 def depart_problematic(self
, node
):
1333 self
.body
.append('</span>')
1334 self
.body
.append(self
.context
.pop())
1336 def visit_raw(self
, node
):
1337 if 'html' in node
.get('format', '').split():
1338 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1340 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1341 self
.body
.append(node
.astext())
1343 self
.body
.append('</%s>' % t
)
1344 # Keep non-HTML raw text out of output:
1345 raise nodes
.SkipNode
1347 def visit_reference(self
, node
):
1348 atts
= {'class': 'reference'}
1349 if 'refuri' in node
:
1350 atts
['href'] = node
['refuri']
1351 if ( self
.settings
.cloak_email_addresses
1352 and atts
['href'].startswith('mailto:')):
1353 atts
['href'] = self
.cloak_mailto(atts
['href'])
1354 self
.in_mailto
= True
1355 atts
['class'] += ' external'
1357 assert 'refid' in node
, \
1358 'References must have "refuri" or "refid" attribute.'
1359 atts
['href'] = '#' + node
['refid']
1360 atts
['class'] += ' internal'
1361 if not isinstance(node
.parent
, nodes
.TextElement
):
1362 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1363 atts
['class'] += ' image-reference'
1364 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1366 def depart_reference(self
, node
):
1367 self
.body
.append('</a>')
1368 if not isinstance(node
.parent
, nodes
.TextElement
):
1369 self
.body
.append('\n')
1370 self
.in_mailto
= False
1372 def visit_revision(self
, node
):
1373 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1375 def depart_revision(self
, node
):
1376 self
.depart_docinfo_item()
1378 def visit_row(self
, node
):
1379 self
.body
.append(self
.starttag(node
, 'tr', ''))
1382 def depart_row(self
, node
):
1383 self
.body
.append('</tr>\n')
1385 def visit_rubric(self
, node
):
1386 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1388 def depart_rubric(self
, node
):
1389 self
.body
.append('</p>\n')
1391 # TODO: use the new HTML 5 element <section>?
1392 def visit_section(self
, node
):
1393 self
.section_level
+= 1
1395 self
.starttag(node
, 'div', CLASS
='section'))
1397 def depart_section(self
, node
):
1398 self
.section_level
-= 1
1399 self
.body
.append('</div>\n')
1401 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1402 def visit_sidebar(self
, node
):
1404 self
.starttag(node
, 'div', CLASS
='sidebar'))
1405 self
.in_sidebar
= True
1407 def depart_sidebar(self
, node
):
1408 self
.body
.append('</div>\n')
1409 self
.in_sidebar
= False
1411 def visit_status(self
, node
):
1412 self
.visit_docinfo_item(node
, 'status', meta
=False)
1414 def depart_status(self
, node
):
1415 self
.depart_docinfo_item()
1417 def visit_strong(self
, node
):
1418 self
.body
.append(self
.starttag(node
, 'strong', ''))
1420 def depart_strong(self
, node
):
1421 self
.body
.append('</strong>')
1423 def visit_subscript(self
, node
):
1424 self
.body
.append(self
.starttag(node
, 'sub', ''))
1426 def depart_subscript(self
, node
):
1427 self
.body
.append('</sub>')
1429 def visit_substitution_definition(self
, node
):
1430 """Internal only."""
1431 raise nodes
.SkipNode
1433 def visit_substitution_reference(self
, node
):
1434 self
.unimplemented_visit(node
)
1436 # h1–h6 elements must not be used to markup subheadings, subtitles,
1437 # alternative titles and taglines unless intended to be the heading for a
1438 # new section or subsection.
1439 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1441 def visit_subtitle(self
, node
):
1442 if isinstance(node
.parent
, nodes
.sidebar
):
1443 classes
= 'sidebar-subtitle'
1444 elif isinstance(node
.parent
, nodes
.document
):
1445 classes
= 'subtitle'
1446 self
.in_document_title
= len(self
.body
)
1447 elif isinstance(node
.parent
, nodes
.section
):
1448 classes
= 'section-subtitle'
1449 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
=classes
))
1451 def depart_subtitle(self
, node
):
1452 self
.body
.append('</p>\n')
1453 if self
.in_document_title
:
1454 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1455 self
.in_document_title
= 0
1456 self
.body_pre_docinfo
.extend(self
.body
)
1457 self
.html_subtitle
.extend(self
.body
)
1460 def visit_superscript(self
, node
):
1461 self
.body
.append(self
.starttag(node
, 'sup', ''))
1463 def depart_superscript(self
, node
):
1464 self
.body
.append('</sup>')
1466 def visit_system_message(self
, node
):
1467 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1468 self
.body
.append('<p class="system-message-title">')
1470 if len(node
['backrefs']):
1471 backrefs
= node
['backrefs']
1472 if len(backrefs
) == 1:
1473 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1478 for backref
in backrefs
:
1479 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1481 backref_text
= ('; <em>backlinks: %s</em>'
1482 % ', '.join(backlinks
))
1483 if node
.hasattr('line'):
1484 line
= ', line %s' % node
['line']
1487 self
.body
.append('System Message: %s/%s '
1488 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1489 % (node
['type'], node
['level'],
1490 self
.encode(node
['source']), line
, backref_text
))
1492 def depart_system_message(self
, node
):
1493 self
.body
.append('</div>\n')
1497 # no hard-coded border setting in the table head::
1499 def visit_table(self
, node
):
1500 classes
= [cls
.strip(u
' \t\n')
1501 for cls
in self
.settings
.table_style
.split(',')]
1502 tag
= self
.starttag(node
, 'table', CLASS
=' '.join(classes
))
1503 self
.body
.append(tag
)
1505 def depart_table(self
, node
):
1506 self
.body
.append('</table>\n')
1508 def visit_target(self
, node
):
1509 if not ('refuri' in node
or 'refid' in node
1510 or 'refname' in node
):
1511 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1512 self
.context
.append('</span>')
1514 self
.context
.append('')
1516 def depart_target(self
, node
):
1517 self
.body
.append(self
.context
.pop())
1519 # no hard-coded vertical alignment in table body::
1521 def visit_tbody(self
, node
):
1522 self
.write_colspecs()
1523 self
.body
.append(self
.context
.pop()) # '</colgroup>\n' or ''
1524 self
.body
.append(self
.starttag(node
, 'tbody'))
1526 def depart_tbody(self
, node
):
1527 self
.body
.append('</tbody>\n')
1529 def visit_term(self
, node
):
1530 self
.body
.append(self
.starttag(node
, 'dt', ''))
1532 def depart_term(self
, node
):
1534 Leave the end tag to `self.visit_definition()`, in case there's a
1539 def visit_tgroup(self
, node
):
1540 # Mozilla needs <colgroup>:
1541 self
.body
.append(self
.starttag(node
, 'colgroup'))
1542 # Appended by thead or tbody:
1543 self
.context
.append('</colgroup>\n')
1546 def depart_tgroup(self
, node
):
1549 def visit_thead(self
, node
):
1550 self
.write_colspecs()
1551 self
.body
.append(self
.context
.pop()) # '</colgroup>\n'
1552 # There may or may not be a <thead>; this is for <tbody> to use:
1553 self
.context
.append('')
1554 self
.body
.append(self
.starttag(node
, 'thead'))
1556 def depart_thead(self
, node
):
1557 self
.body
.append('</thead>\n')
1559 def visit_title(self
, node
):
1560 """Only 6 section levels are supported by HTML."""
1561 check_id
= 0 # TODO: is this a bool (False) or a counter?
1562 close_tag
= '</p>\n'
1563 if isinstance(node
.parent
, nodes
.topic
):
1565 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1566 elif isinstance(node
.parent
, nodes
.sidebar
):
1568 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1569 elif isinstance(node
.parent
, nodes
.Admonition
):
1571 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1572 elif isinstance(node
.parent
, nodes
.table
):
1574 self
.starttag(node
, 'caption', ''))
1575 close_tag
= '</caption>\n'
1576 elif isinstance(node
.parent
, nodes
.document
):
1577 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1578 close_tag
= '</h1>\n'
1579 self
.in_document_title
= len(self
.body
)
1581 assert isinstance(node
.parent
, nodes
.section
)
1582 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1584 if (len(node
.parent
) >= 2 and
1585 isinstance(node
.parent
[1], nodes
.subtitle
)):
1586 atts
['CLASS'] = 'with-subtitle'
1588 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1590 if node
.hasattr('refid'):
1591 atts
['class'] = 'toc-backref'
1592 atts
['href'] = '#' + node
['refid']
1594 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1595 close_tag
= '</a></h%s>\n' % (h_level
)
1597 close_tag
= '</h%s>\n' % (h_level
)
1598 self
.context
.append(close_tag
)
1600 def depart_title(self
, node
):
1601 self
.body
.append(self
.context
.pop())
1602 if self
.in_document_title
:
1603 self
.title
= self
.body
[self
.in_document_title
:-1]
1604 self
.in_document_title
= 0
1605 self
.body_pre_docinfo
.extend(self
.body
)
1606 self
.html_title
.extend(self
.body
)
1609 def visit_title_reference(self
, node
):
1610 self
.body
.append(self
.starttag(node
, 'cite', ''))
1612 def depart_title_reference(self
, node
):
1613 self
.body
.append('</cite>')
1615 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1616 def visit_topic(self
, node
):
1617 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1618 self
.topic_classes
= node
['classes']
1619 # TODO: replace with ::
1620 # self.in_contents = 'contents' in node['classes']
1622 def depart_topic(self
, node
):
1623 self
.body
.append('</div>\n')
1624 self
.topic_classes
= []
1625 # TODO self.in_contents = False
1627 def visit_transition(self
, node
):
1628 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1630 def depart_transition(self
, node
):
1633 def visit_version(self
, node
):
1634 self
.visit_docinfo_item(node
, 'version', meta
=False)
1636 def depart_version(self
, node
):
1637 self
.depart_docinfo_item()
1639 def unimplemented_visit(self
, node
):
1640 raise NotImplementedError('visiting unimplemented node type: %s'
1641 % node
.__class
__.__name
__)
1644 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1647 Raise `nodes.NodeFound` if non-simple list item is encountered.
1649 Here "simple" means a list item containing nothing other than a single
1650 paragraph, a simple list, or a paragraph followed by a simple list.
1652 This version also checks for simple field lists and docinfo.
1655 def default_visit(self
, node
):
1656 raise nodes
.NodeFound
1658 def visit_list_item(self
, node
):
1659 # print "visiting list item", node.__class__
1660 children
= [child
for child
in node
.children
1661 if not isinstance(child
, nodes
.Invisible
)]
1662 # print "has %s visible children" % len(children)
1663 if (children
and isinstance(children
[0], nodes
.paragraph
)
1664 and (isinstance(children
[-1], nodes
.bullet_list
) or
1665 isinstance(children
[-1], nodes
.enumerated_list
) or
1666 isinstance(children
[-1], nodes
.field_list
))):
1668 # print "%s children remain" % len(children)
1669 if len(children
) <= 1:
1672 # print "found", child.__class__, "in", node.__class__
1673 raise nodes
.NodeFound
1675 def pass_node(self
, node
):
1678 def ignore_node(self
, node
):
1679 # ignore nodes that are never complex (can contain only inline nodes)
1680 raise nodes
.SkipNode
1682 # Paragraphs and text
1683 visit_Text
= ignore_node
1684 visit_paragraph
= ignore_node
1687 visit_bullet_list
= pass_node
1688 visit_enumerated_list
= pass_node
1689 visit_docinfo
= pass_node
1692 visit_author
= ignore_node
1693 visit_authors
= visit_list_item
1694 visit_address
= visit_list_item
1695 visit_contact
= pass_node
1696 visit_copyright
= ignore_node
1697 visit_date
= ignore_node
1698 visit_organization
= ignore_node
1699 visit_status
= ignore_node
1700 visit_version
= visit_list_item
1703 visit_definition_list
= pass_node
1704 visit_definition_list_item
= pass_node
1705 visit_term
= ignore_node
1706 visit_classifier
= pass_node
1707 visit_definition
= visit_list_item
1710 visit_field_list
= pass_node
1711 visit_field
= pass_node
1712 # the field body corresponds to a list item
1713 visit_field_body
= visit_list_item
1714 visit_field_name
= ignore_node
1716 # Invisible nodes should be ignored.
1717 visit_comment
= ignore_node
1718 visit_substitution_definition
= ignore_node
1719 visit_target
= ignore_node
1720 visit_pending
= ignore_node