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 ('Omit the XML declaration. Must be true for HTML5 conformance.',
140 ['--no-xml-declaration'],
141 {'dest': 'xml_declaration', 'default': False,
142 'action': 'store_false', 'validator': frontend
.validate_boolean
}),
143 ('Obfuscate email addresses to confuse harvesters while still '
144 'keeping email links usable with standards-compliant browsers.',
145 ['--cloak-email-addresses'],
146 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
148 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
150 config_section
= 'html-base writer'
151 config_section_dependencies
= ('writers',)
153 visitor_attributes
= (
154 'head_prefix', 'head', 'stylesheet', 'body_prefix',
155 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
156 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
157 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
160 def get_transforms(self
):
161 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
164 writers
.Writer
.__init
__(self
)
165 self
.translator_class
= HTMLTranslator
168 self
.visitor
= visitor
= self
.translator_class(self
.document
)
169 self
.document
.walkabout(visitor
)
170 for attr
in self
.visitor_attributes
:
171 setattr(self
, attr
, getattr(visitor
, attr
))
172 self
.output
= self
.apply_template()
174 def apply_template(self
):
175 template_file
= open(self
.document
.settings
.template
, 'rb')
176 template
= unicode(template_file
.read(), 'utf-8')
177 template_file
.close()
178 subs
= self
.interpolation_dict()
179 return template
% subs
181 def interpolation_dict(self
):
183 settings
= self
.document
.settings
184 for attr
in self
.visitor_attributes
:
185 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
186 subs
['encoding'] = settings
.output_encoding
187 subs
['version'] = docutils
.__version
__
190 def assemble_parts(self
):
191 writers
.Writer
.assemble_parts(self
)
192 for part
in self
.visitor_attributes
:
193 self
.parts
[part
] = ''.join(getattr(self
, part
))
196 class HTMLTranslator(nodes
.NodeVisitor
):
199 This writer generates `polyglott markup`: HTML 5 that is also valid XML.
202 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
205 doctype_mathml
= doctype
207 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
208 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
209 content_type
= ('<meta http-equiv="Content-Type"'
210 ' content="text/html; charset=%s" />\n')
211 content_type_mathml
= ('<meta http-equiv="Content-Type"'
212 ' content="text/html; charset=%s" />\n')
214 generator
= ('<meta name="generator" content="Docutils %s: '
215 'http://docutils.sourceforge.net/" />\n')
217 # Template for the MathJax script in the header:
218 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
219 # The latest version of MathJax from the distributed server:
220 # avaliable to the public under the `MathJax CDN Terms of Service`__
221 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
222 mathjax_url
= ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
223 'config=TeX-AMS-MML_HTMLorMML')
224 # may be overwritten by custom URL appended to "mathjax"
226 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
227 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
228 words_and_spaces
= re
.compile(r
'\S+| +|\n')
229 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
230 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
232 def __init__(self
, document
):
233 nodes
.NodeVisitor
.__init
__(self
, document
)
234 self
.settings
= settings
= document
.settings
235 lcode
= settings
.language_code
236 self
.language
= languages
.get_language(lcode
, document
.reporter
)
237 self
.meta
= [self
.generator
% docutils
.__version
__]
238 self
.head_prefix
= []
239 self
.html_prolog
= []
240 if settings
.xml_declaration
:
241 self
.head_prefix
.append(self
.xml_declaration
242 % settings
.output_encoding
)
243 # encoding not interpolated:
244 self
.html_prolog
.append(self
.xml_declaration
)
245 self
.head
= self
.meta
[:]
246 self
.stylesheet
= [self
.stylesheet_call(path
)
247 for path
in utils
.get_stylesheet_list(settings
)]
248 self
.body_prefix
= ['</head>\n<body>\n']
249 # document title, subtitle display
250 self
.body_pre_docinfo
= []
255 self
.body_suffix
= ['</body>\n</html>\n']
256 self
.section_level
= 0
257 self
.initial_header_level
= int(settings
.initial_header_level
)
259 self
.math_output
= settings
.math_output
.split()
260 self
.math_output_options
= self
.math_output
[1:]
261 self
.math_output
= self
.math_output
[0].lower()
263 # A heterogenous stack used in conjunction with the tree traversal.
264 # Make sure that the pops correspond to the pushes:
267 self
.topic_classes
= [] # TODO: replace with self_in_contents
269 self
.compact_p
= True
270 self
.compact_simple
= False
271 self
.compact_field_list
= False
272 self
.in_docinfo
= False
273 self
.in_sidebar
= False
274 self
.in_footnote_list
= False
279 self
.html_head
= [self
.content_type
] # charset not interpolated
281 self
.html_subtitle
= []
283 self
.in_document_title
= 0 # len(self.body) or 0
284 self
.in_mailto
= False
285 self
.author_in_authors
= False
286 self
.math_header
= []
289 return ''.join(self
.head_prefix
+ self
.head
290 + self
.stylesheet
+ self
.body_prefix
291 + self
.body_pre_docinfo
+ self
.docinfo
292 + self
.body
+ self
.body_suffix
)
294 def encode(self
, text
):
295 """Encode special characters in `text` & return."""
296 # @@@ A codec to do these and all other HTML entities would be nice.
298 return text
.translate({
303 ord('@'): u
'@', # may thwart some address harvesters
306 def cloak_mailto(self
, uri
):
307 """Try to hide a mailto: URL from harvesters."""
308 # Encode "@" using a URL octet reference (see RFC 1738).
309 # Further cloaking with HTML entities will be done in the
311 return uri
.replace('@', '%40')
313 def cloak_email(self
, addr
):
314 """Try to hide the link text of a email link from harversters."""
315 # Surround at-signs and periods with <span> tags. ("@" has
316 # already been encoded to "@" by the `encode` method.)
317 addr
= addr
.replace('@', '<span>@</span>')
318 addr
= addr
.replace('.', '<span>.</span>')
321 def attval(self
, text
,
322 whitespace
=re
.compile('[\n\r\t\v\f]')):
323 """Cleanse, HTML encode, and return attribute value text."""
324 encoded
= self
.encode(whitespace
.sub(' ', text
))
325 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
326 # Cloak at-signs ("%40") and periods with HTML entities.
327 encoded
= encoded
.replace('%40', '%40')
328 encoded
= encoded
.replace('.', '.')
331 def stylesheet_call(self
, path
):
332 """Return code to reference or embed stylesheet file `path`"""
333 if self
.settings
.embed_stylesheet
:
335 content
= io
.FileInput(source_path
=path
,
336 encoding
='utf-8').read()
337 self
.settings
.record_dependencies
.add(path
)
339 msg
= u
"Cannot embed stylesheet '%s': %s." % (
340 path
, SafeString(err
.strerror
))
341 self
.document
.reporter
.error(msg
)
342 return '<--- %s --->\n' % msg
343 return self
.embedded_stylesheet
% content
344 # else link to style file:
345 if self
.settings
.stylesheet_path
:
346 # adapt path relative to output (cf. config.html#stylesheet-path)
347 path
= utils
.relative_path(self
.settings
._destination
, path
)
348 return self
.stylesheet_link
% self
.encode(path
)
350 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
352 Construct and return a start tag given a node (id & class attributes
353 are extracted), tag name, and optional attributes.
355 tagname
= tagname
.lower()
359 for (name
, value
) in attributes
.items():
360 atts
[name
.lower()] = value
363 # unify class arguments and move language specification
364 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
365 if cls
.startswith('language-'):
366 languages
.append(cls
[9:])
367 elif cls
.strip() and cls
not in classes
:
370 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
371 atts
[self
.lang_attribute
] = languages
[0]
373 atts
['class'] = ' '.join(classes
)
374 assert 'id' not in atts
375 ids
.extend(node
.get('ids', []))
377 ids
.extend(atts
['ids'])
382 # Add empty "span" elements for additional IDs. Note
383 # that we cannot use empty "a" elements because there
384 # may be targets inside of references, but nested "a"
385 # elements aren't allowed in XHTML (even if they do
386 # not all have a "href" attribute).
388 # Empty tag. Insert target right in front of element.
389 prefix
.append('<span id="%s"></span>' % id)
391 # Non-empty tag. Place the auxiliary <span> tag
392 # *inside* the element, as the first child.
393 suffix
+= '<span id="%s"></span>' % id
394 attlist
= atts
.items()
397 for name
, value
in attlist
:
398 # value=None was used for boolean attributes without
399 # value, but this isn't supported by XHTML.
400 assert value
is not None
401 if isinstance(value
, list):
402 values
= [unicode(v
) for v
in value
]
403 parts
.append('%s="%s"' % (name
.lower(),
404 self
.attval(' '.join(values
))))
406 parts
.append('%s="%s"' % (name
.lower(),
407 self
.attval(unicode(value
))))
412 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
414 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
415 """Construct and return an XML-compatible empty tag."""
416 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
418 def set_class_on_child(self
, node
, class_
, index
=0):
420 Set class `class_` on the visible child no. index of `node`.
421 Do nothing if node has fewer children than `index`.
423 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
425 child
= children
[index
]
428 child
['classes'].append(class_
)
430 def set_first_last(self
, node
):
432 # TODO: remove calls to this function
434 def visit_Text(self
, node
):
436 encoded
= self
.encode(text
)
437 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
438 encoded
= self
.cloak_email(encoded
)
439 self
.body
.append(encoded
)
441 def depart_Text(self
, node
):
444 def visit_abbreviation(self
, node
):
445 # @@@ implementation incomplete ("title" attribute)
446 self
.body
.append(self
.starttag(node
, 'abbr', ''))
448 def depart_abbreviation(self
, node
):
449 self
.body
.append('</abbr>')
451 def visit_acronym(self
, node
):
452 # @@@ implementation incomplete ("title" attribute)
453 self
.body
.append(self
.starttag(node
, 'abbr', ''))
455 def depart_acronym(self
, node
):
456 self
.body
.append('</abbr>')
458 def visit_address(self
, node
):
459 self
.visit_docinfo_item(node
, 'address', meta
=False)
460 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='address'))
462 def depart_address(self
, node
):
463 self
.body
.append('\n</pre>\n')
464 self
.depart_docinfo_item()
466 def visit_admonition(self
, node
):
467 node
['classes'].insert(0, 'admonition')
468 self
.body
.append(self
.starttag(node
, 'div'))
469 self
.set_first_last(node
)
471 def depart_admonition(self
, node
=None):
472 self
.body
.append('</div>\n')
474 attribution_formats
= {'dash': ('—', ''),
475 'parentheses': ('(', ')'),
476 'parens': ('(', ')'),
479 def visit_attribution(self
, node
):
480 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
481 self
.context
.append(suffix
)
483 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
485 def depart_attribution(self
, node
):
486 self
.body
.append(self
.context
.pop() + '</p>\n')
490 # Use paragraphs instead of hard-coded linebreaks.
492 def visit_author(self
, node
):
493 if not(isinstance(node
.parent
, nodes
.authors
)):
494 self
.visit_docinfo_item(node
, 'author')
495 self
.body
.append('<p>')
497 def depart_author(self
, node
):
498 self
.body
.append('</p>')
499 if isinstance(node
.parent
, nodes
.authors
):
500 self
.body
.append('\n')
502 self
.depart_docinfo_item()
504 def visit_authors(self
, node
):
505 self
.visit_docinfo_item(node
, 'authors', meta
=False)
507 def depart_authors(self
, node
):
508 self
.depart_docinfo_item()
510 def visit_block_quote(self
, node
):
511 self
.body
.append(self
.starttag(node
, 'blockquote'))
513 def depart_block_quote(self
, node
):
514 self
.body
.append('</blockquote>\n')
516 def check_simple_list(self
, node
):
517 """Check for a simple list that can be rendered compactly."""
518 visitor
= SimpleListChecker(self
.document
)
521 except nodes
.NodeFound
:
528 # Include definition lists and field lists (in addition to ordered
529 # and unordered lists) in the test if a list is "simple" (cf. the
530 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
531 # the end of this file).
533 def is_compactable(self
, node
):
534 # print "is_compactable %s ?" % node.__class__,
535 # explicite class arguments have precedence
536 if 'compact' in node
['classes']:
537 # print "explicitely compact"
539 if 'open' in node
['classes']:
540 # print "explicitely open"
542 # check config setting:
543 if (isinstance(node
, nodes
.field_list
) or
544 isinstance(node
, nodes
.definition_list
)
545 ) and not self
.settings
.compact_field_lists
:
546 # print "`compact-field-lists` is False"
548 if (isinstance(node
, nodes
.enumerated_list
) or
549 isinstance(node
, nodes
.bullet_list
)
550 ) and not self
.settings
.compact_lists
:
551 # print "`compact-lists` is False"
553 # more special cases:
554 if (self
.topic_classes
== ['contents']): # TODO: self.in_contents
556 # check the list items:
557 visitor
= SimpleListChecker(self
.document
)
560 except nodes
.NodeFound
:
561 # print "complex node"
564 # print "simple list"
567 def visit_bullet_list(self
, node
):
569 old_compact_simple
= self
.compact_simple
570 self
.context
.append((self
.compact_simple
, self
.compact_p
))
571 self
.compact_p
= None
572 self
.compact_simple
= self
.is_compactable(node
)
573 if self
.compact_simple
and not old_compact_simple
:
574 atts
['class'] = 'simple'
575 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
577 def depart_bullet_list(self
, node
):
578 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
579 self
.body
.append('</ul>\n')
581 def visit_caption(self
, node
):
582 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
584 def depart_caption(self
, node
):
585 self
.body
.append('</p>\n')
589 # Use definition list instead of table for bibliographic references.
590 # Join adjacent citation entries.
592 def visit_citation(self
, node
):
593 if not self
.in_footnote_list
:
594 self
.body
.append('<dl class="citation">\n')
595 self
.in_footnote_list
= True
597 def depart_citation(self
, node
):
598 self
.body
.append('</dd>\n')
599 if not isinstance(node
.next_node(descend
=False, siblings
=True),
601 self
.body
.append('</dl>\n')
602 self
.in_footnote_list
= False
604 def visit_citation_reference(self
, node
):
607 href
+= node
['refid']
608 elif 'refname' in node
:
609 href
+= self
.document
.nameids
[node
['refname']]
610 # else: # TODO system message (or already in the transform)?
611 # 'Citation reference missing.'
612 self
.body
.append(self
.starttag(
613 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
615 def depart_citation_reference(self
, node
):
616 self
.body
.append(']</a>')
620 # don't insert classifier-delimiter here (done by CSS)
622 def visit_classifier(self
, node
):
623 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
625 def depart_classifier(self
, node
):
626 self
.body
.append('</span>')
628 def visit_colspec(self
, node
):
629 self
.colspecs
.append(node
)
630 # "stubs" list is an attribute of the tgroup element:
631 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
633 def depart_colspec(self
, node
):
636 def write_colspecs(self
):
638 for node
in self
.colspecs
:
639 width
+= node
['colwidth']
640 for node
in self
.colspecs
:
641 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
642 self
.body
.append(self
.emptytag(node
, 'col',
643 style
='width: %i%%' % colwidth
))
646 def visit_comment(self
, node
,
647 sub
=re
.compile('-(?=-)').sub
):
648 """Escape double-dashes in comment text."""
649 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
650 # Content already processed:
653 def visit_compound(self
, node
):
654 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
656 node
[0]['classes'].append('compound-first')
657 node
[-1]['classes'].append('compound-last')
658 for child
in node
[1:-1]:
659 child
['classes'].append('compound-middle')
661 def depart_compound(self
, node
):
662 self
.body
.append('</div>\n')
664 def visit_container(self
, node
):
665 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
667 def depart_container(self
, node
):
668 self
.body
.append('</div>\n')
670 def visit_contact(self
, node
):
671 self
.visit_docinfo_item(node
, 'contact', meta
=False)
673 def depart_contact(self
, node
):
674 self
.depart_docinfo_item()
676 def visit_copyright(self
, node
):
677 self
.visit_docinfo_item(node
, 'copyright', meta
=False)
679 def depart_copyright(self
, node
):
680 self
.depart_docinfo_item()
682 def visit_date(self
, node
):
683 self
.visit_docinfo_item(node
, 'date', meta
=False)
685 def depart_date(self
, node
):
686 self
.depart_docinfo_item()
688 def visit_decoration(self
, node
):
691 def depart_decoration(self
, node
):
694 def visit_definition(self
, node
):
695 self
.body
.append('</dt>\n')
696 self
.body
.append(self
.starttag(node
, 'dd', ''))
697 self
.set_first_last(node
)
699 def depart_definition(self
, node
):
700 self
.body
.append('</dd>\n')
702 def visit_definition_list(self
, node
):
703 classes
= node
.setdefault('classes', [])
704 if self
.is_compactable(node
):
705 classes
.append('simple')
706 self
.body
.append(self
.starttag(node
, 'dl'))
708 def depart_definition_list(self
, node
):
709 self
.body
.append('</dl>\n')
711 def visit_definition_list_item(self
, node
):
712 # pass class arguments, ids and names to definition term:
713 node
.children
[0]['classes'] = (
714 node
.get('classes', []) + node
.children
[0].get('classes', []))
715 node
.children
[0]['ids'] = (
716 node
.get('ids', []) + node
.children
[0].get('ids', []))
717 node
.children
[0]['names'] = (
718 node
.get('names', []) + node
.children
[0].get('names', []))
720 def depart_definition_list_item(self
, node
):
723 def visit_description(self
, node
):
724 self
.body
.append(self
.starttag(node
, 'dd', ''))
726 def depart_description(self
, node
):
727 self
.body
.append('</dd>\n')
732 # use definition list instead of table
734 def visit_docinfo(self
, node
):
736 if (self
.is_compactable(node
)):
738 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
740 def depart_docinfo(self
, node
):
741 self
.body
.append('</dl>\n')
743 def visit_docinfo_item(self
, node
, name
, meta
=True):
745 meta_tag
= '<meta name="%s" content="%s" />\n' \
746 % (name
, self
.attval(node
.astext()))
747 self
.add_meta(meta_tag
)
748 self
.body
.append('<dt class="%s">%s</dt>\n'
749 % (name
, self
.language
.labels
[name
]))
750 self
.body
.append(self
.starttag(node
, 'dd', '', CLASS
=name
))
752 def depart_docinfo_item(self
):
753 self
.body
.append('</dd>\n')
755 # TODO: RSt-parser should treat this as code-block with class "pycon".
756 def visit_doctest_block(self
, node
):
757 self
.body
.append(self
.starttag(node
, 'pre', suffix
='',
758 CLASS
='code pycon doctest-block'))
760 def depart_doctest_block(self
, node
):
761 self
.body
.append('\n</pre>\n')
763 def visit_document(self
, node
):
764 self
.head
.append('<title>%s</title>\n'
765 % self
.encode(node
.get('title', '')))
767 def depart_document(self
, node
):
768 self
.head_prefix
.extend([self
.doctype
,
769 self
.head_prefix_template
%
770 {'lang': self
.settings
.language_code
}])
771 self
.html_prolog
.append(self
.doctype
)
772 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
773 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
775 if self
.math_output
== 'mathjax':
776 self
.head
.extend(self
.math_header
)
778 self
.stylesheet
.extend(self
.math_header
)
779 # skip content-type meta tag with interpolated charset value:
780 self
.html_head
.extend(self
.head
[1:])
781 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
782 self
.body_suffix
.insert(0, '</div>\n')
783 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
784 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
785 + self
.docinfo
+ self
.body
786 + self
.body_suffix
[:-1])
787 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
789 def visit_emphasis(self
, node
):
790 self
.body
.append(self
.starttag(node
, 'em', ''))
792 def depart_emphasis(self
, node
):
793 self
.body
.append('</em>')
795 def visit_entry(self
, node
):
797 if isinstance(node
.parent
.parent
, nodes
.thead
):
798 atts
['class'].append('head')
799 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
800 # "stubs" list is an attribute of the tgroup element
801 atts
['class'].append('stub')
804 atts
['class'] = ' '.join(atts
['class'])
808 node
.parent
.column
+= 1
809 if 'morerows' in node
:
810 atts
['rowspan'] = node
['morerows'] + 1
811 if 'morecols' in node
:
812 atts
['colspan'] = node
['morecols'] + 1
813 node
.parent
.column
+= node
['morecols']
814 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
815 self
.context
.append('</%s>\n' % tagname
.lower())
816 if len(node
) == 0: # empty cell
817 self
.body
.append(' ')
818 self
.set_first_last(node
)
820 def depart_entry(self
, node
):
821 self
.body
.append(self
.context
.pop())
823 def visit_enumerated_list(self
, node
):
825 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
826 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
831 atts
['start'] = node
['start']
832 if 'enumtype' in node
:
833 atts
['class'] = node
['enumtype']
834 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
835 # single "format" attribute? Use CSS2?
836 old_compact_simple
= self
.compact_simple
837 self
.context
.append((self
.compact_simple
, self
.compact_p
))
838 self
.compact_p
= None
839 self
.compact_simple
= self
.is_compactable(node
)
840 if self
.compact_simple
and not old_compact_simple
:
841 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
842 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
844 def depart_enumerated_list(self
, node
):
845 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
846 self
.body
.append('</ol>\n')
850 # set as definition list, styled with CSS
852 def visit_field_list(self
, node
):
853 # Keep simple paragraphs in the field_body to enable CSS
854 # rule to start body on new line if the label is too long
855 classes
= 'field-list'
856 if (self
.is_compactable(node
)):
858 self
.body
.append(self
.starttag(node
, 'dl', CLASS
=classes
))
860 def depart_field_list(self
, node
):
861 self
.body
.append('</dl>\n')
863 def visit_field(self
, node
):
866 def depart_field(self
, node
):
869 def visit_field_name(self
, node
):
870 self
.body
.append(self
.starttag(node
, 'dt', ''))
872 def depart_field_name(self
, node
):
873 self
.body
.append('</dt>\n')
875 def visit_field_body(self
, node
):
876 self
.body
.append(self
.starttag(node
, 'dd', ''))
878 def depart_field_body(self
, node
):
879 self
.body
.append('</dd>\n')
881 def visit_figure(self
, node
):
882 atts
= {'class': 'figure'}
883 if node
.get('width'):
884 atts
['style'] = 'width: %s' % node
['width']
885 if node
.get('align'):
886 atts
['class'] += " align-" + node
['align']
887 self
.body
.append(self
.starttag(node
, 'div', **atts
))
889 def depart_figure(self
, node
):
890 self
.body
.append('</div>\n')
892 # use HTML 5 <footer> element?
893 def visit_footer(self
, node
):
894 self
.context
.append(len(self
.body
))
896 def depart_footer(self
, node
):
897 start
= self
.context
.pop()
898 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
899 '<hr class="footer" />\n']
900 footer
.extend(self
.body
[start
:])
901 footer
.append('\n</div>\n')
902 self
.footer
.extend(footer
)
903 self
.body_suffix
[:0] = footer
904 del self
.body
[start
:]
908 # use definition list instead of table for footnote text
910 def visit_footnote(self
, node
):
911 if not self
.in_footnote_list
:
912 self
.body
.append('<dl class="footnote">\n')
913 self
.in_footnote_list
= True
915 def depart_footnote(self
, node
):
916 self
.body
.append('</dd>\n')
917 if not isinstance(node
.next_node(descend
=False, siblings
=True),
919 self
.body
.append('</dl>\n')
920 self
.in_footnote_list
= False
922 def visit_footnote_reference(self
, node
):
923 href
= '#' + node
['refid']
924 format
= self
.settings
.footnote_references
925 if format
== 'brackets':
927 self
.context
.append(']')
929 assert format
== 'superscript'
931 self
.context
.append('</sup>')
932 self
.body
.append(self
.starttag(node
, 'a', suffix
,
933 CLASS
='footnote-reference', href
=href
))
935 def depart_footnote_reference(self
, node
):
936 self
.body
.append(self
.context
.pop() + '</a>')
938 def visit_generated(self
, node
):
939 if 'sectnum' in node
['classes']:
940 # get section number (strip trailing no-break-spaces)
941 sectnum
= node
.astext().rstrip(u
' ')
942 # print sectnum.encode('utf-8')
943 self
.body
.append('<span class="sectnum">%s</span> '
944 % self
.encode(sectnum
))
945 # Content already processed:
948 def depart_generated(self
, node
):
951 def visit_header(self
, node
):
952 self
.context
.append(len(self
.body
))
954 def depart_header(self
, node
):
955 start
= self
.context
.pop()
956 header
= [self
.starttag(node
, 'div', CLASS
='header')]
957 header
.extend(self
.body
[start
:])
958 header
.append('\n<hr class="header"/>\n</div>\n')
959 self
.body_prefix
.extend(header
)
960 self
.header
.extend(header
)
961 del self
.body
[start
:]
963 # Image types to place in an <object> element
964 # SVG not supported by IE up to version 8
965 # (html4css1 strives for IE6 compatibility)
966 object_image_types
= {'.svg': 'image/svg+xml',
967 '.swf': 'application/x-shockwave-flash'}
969 def visit_image(self
, node
):
972 ext
= os
.path
.splitext(uri
)[1].lower()
973 if ext
in self
.object_image_types
: # ('.svg', '.swf'):
975 atts
['type'] = self
.object_image_types
[ext
]
978 atts
['alt'] = node
.get('alt', uri
)
981 atts
['width'] = node
['width']
983 atts
['height'] = node
['height']
985 if (PIL
and not ('width' in node
and 'height' in node
)
986 and self
.settings
.file_insertion_enabled
):
987 imagepath
= urllib
.url2pathname(uri
)
989 img
= PIL
.Image
.open(
990 imagepath
.encode(sys
.getfilesystemencoding()))
991 except (IOError, UnicodeEncodeError):
994 self
.settings
.record_dependencies
.add(
995 imagepath
.replace('\\', '/'))
996 if 'width' not in atts
:
997 atts
['width'] = '%dpx' % img
.size
[0]
998 if 'height' not in atts
:
999 atts
['height'] = '%dpx' % img
.size
[1]
1001 for att_name
in 'width', 'height':
1002 if att_name
in atts
:
1003 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1005 atts
[att_name
] = '%s%s' % (
1006 float(match
.group(1)) * (float(node
['scale']) / 100),
1009 for att_name
in 'width', 'height':
1010 if att_name
in atts
:
1011 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1012 # Interpret unitless values as pixels.
1013 atts
[att_name
] += 'px'
1014 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1017 atts
['style'] = ' '.join(style
)
1018 if (isinstance(node
.parent
, nodes
.TextElement
) or
1019 (isinstance(node
.parent
, nodes
.reference
) and
1020 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1021 # Inline context or surrounded by <a>...</a>.
1026 atts
['class'] = 'align-%s' % node
['align']
1027 if ext
in self
.object_image_types
: # ('.svg', '.swf')
1028 # do NOT use an empty tag: incorrect rendering in browsers
1029 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1030 node
.get('alt', uri
) + '</object>' + suffix
)
1032 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1034 def depart_image(self
, node
):
1035 # self.body.append(self.context.pop())
1038 def visit_inline(self
, node
):
1039 self
.body
.append(self
.starttag(node
, 'span', ''))
1041 def depart_inline(self
, node
):
1042 self
.body
.append('</span>')
1044 # footnote and citation label
1045 def label_delim(self
, node
, bracket
, superscript
):
1046 """put brackets around label?"""
1047 if isinstance(node
.parent
, nodes
.footnote
):
1048 if self
.settings
.footnote_references
== 'brackets':
1052 assert isinstance(node
.parent
, nodes
.citation
)
1055 def visit_label(self
, node
):
1056 # pass parent node to get id into starttag:
1057 self
.body
.append(self
.starttag(node
.parent
, 'dt', '', CLASS
='label'))
1058 # footnote/citation backrefs:
1059 if self
.settings
.footnote_backlinks
:
1060 backrefs
= node
.parent
['backrefs']
1061 if len(backrefs
) == 1:
1062 self
.body
.append('<a class="fn-backref" href="#%s">'
1064 self
.body
.append(self
.label_delim(node
, '[', ''))
1066 def depart_label(self
, node
):
1067 self
.body
.append(self
.label_delim(node
, ']', ''))
1068 if self
.settings
.footnote_backlinks
:
1069 backrefs
= node
.parent
['backrefs']
1070 if len(backrefs
) == 1:
1071 self
.body
.append('</a>')
1072 elif len(backrefs
) > 1:
1073 # Python 2.4 fails with enumerate(backrefs, 1)
1074 backlinks
= ['<a href="#%s">%s</a>' % (ref
, i
+1)
1075 for (i
, ref
) in enumerate(backrefs
)]
1076 self
.body
.append('<span class="fn-backref">(%s)</span>'
1077 % ','.join(backlinks
))
1078 self
.body
.append('</dt>\n<dd>')
1080 def visit_legend(self
, node
):
1081 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1083 def depart_legend(self
, node
):
1084 self
.body
.append('</div>\n')
1086 def visit_line(self
, node
):
1087 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1089 self
.body
.append('<br />')
1091 def depart_line(self
, node
):
1092 self
.body
.append('</div>\n')
1094 def visit_line_block(self
, node
):
1095 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1097 def depart_line_block(self
, node
):
1098 self
.body
.append('</div>\n')
1100 def visit_list_item(self
, node
):
1101 self
.body
.append(self
.starttag(node
, 'li', ''))
1103 def depart_list_item(self
, node
):
1104 self
.body
.append('</li>\n')
1107 def visit_literal(self
, node
):
1108 # special case: "code" role
1109 classes
= node
.get('classes', [])
1110 if 'code' in classes
:
1111 # filter 'code' from class arguments
1112 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1113 self
.body
.append(self
.starttag(node
, 'code', ''))
1116 self
.starttag(node
, 'span', '', CLASS
='docutils literal'))
1117 text
= node
.astext()
1118 # remove hard line breaks (except if in a parsed-literal block)
1119 if not isinstance(node
.parent
, nodes
.literal_block
):
1120 text
= text
.replace('\n', ' ')
1121 # Protect text like ``--an-option`` and the regular expression
1122 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1123 for token
in self
.words_and_spaces
.findall(text
):
1124 if token
.strip() and self
.sollbruchstelle
.search(token
):
1125 self
.body
.append('<span class="pre">%s</span>'
1126 % self
.encode(token
))
1128 self
.body
.append(self
.encode(token
))
1129 self
.body
.append('</span>')
1130 # Content already processed:
1131 raise nodes
.SkipNode
1133 def depart_literal(self
, node
):
1134 # skipped unless literal element is from "code" role:
1135 self
.body
.append('</code>')
1137 def visit_literal_block(self
, node
):
1138 self
.body
.append(self
.starttag(node
, 'pre', '', CLASS
='literal-block'))
1140 def depart_literal_block(self
, node
):
1141 self
.body
.append('\n</pre>\n')
1143 def visit_math(self
, node
, math_env
=''):
1144 # If the method is called from visit_math_block(), math_env != ''.
1146 # As there is no native HTML math support, we provide alternatives:
1147 # LaTeX and MathJax math_output modes simply wrap the content,
1148 # HTML and MathML math_output modes also convert the math_code.
1149 if self
.math_output
not in ('mathml', 'html', 'mathjax', 'latex'):
1150 self
.document
.reporter
.error(
1151 'math-output format "%s" not supported '
1152 'falling back to "latex"'% self
.math_output
)
1153 self
.math_output
= 'latex'
1156 tags
= {# math_output: (block, inline, class-arguments)
1157 'mathml': ('div', '', ''),
1158 'html': ('div', 'span', 'formula'),
1159 'mathjax': ('div', 'span', 'math'),
1160 'latex': ('pre', 'tt', 'math'),
1162 tag
= tags
[self
.math_output
][math_env
== '']
1163 clsarg
= tags
[self
.math_output
][2]
1165 wrappers
= {# math_mode: (inline, block)
1166 'mathml': (None, None),
1167 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1168 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1169 'latex': (None, None),
1171 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1172 # get and wrap content
1173 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1174 if wrapper
and math_env
:
1175 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1177 math_code
= wrapper
% math_code
1178 # settings and conversion
1179 if self
.math_output
in ('latex', 'mathjax'):
1180 math_code
= self
.encode(math_code
)
1181 if self
.math_output
== 'mathjax' and not self
.math_header
:
1182 if self
.math_output_options
:
1183 self
.mathjax_url
= self
.math_output_options
[0]
1184 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1185 elif self
.math_output
== 'html':
1186 if self
.math_output_options
and not self
.math_header
:
1187 self
.math_header
= [self
.stylesheet_call(
1188 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1189 for s
in self
.math_output_options
[0].split(',')]
1190 # TODO: fix display mode in matrices and fractions
1191 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1192 math_code
= math2html
.math2html(math_code
)
1193 elif self
.math_output
== 'mathml':
1194 self
.doctype
= self
.doctype_mathml
1195 self
.content_type
= self
.content_type_mathml
1197 mathml_tree
= parse_latex_math(math_code
, inline
=not(math_env
))
1198 math_code
= ''.join(mathml_tree
.xml())
1199 except SyntaxError, err
:
1200 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1201 self
.visit_system_message(err_node
)
1202 self
.body
.append(self
.starttag(node
, 'p'))
1203 self
.body
.append(u
','.join(err
.args
))
1204 self
.body
.append('</p>\n')
1205 self
.body
.append(self
.starttag(node
, 'pre',
1206 CLASS
='literal-block'))
1207 self
.body
.append(self
.encode(math_code
))
1208 self
.body
.append('\n</pre>\n')
1209 self
.depart_system_message(err_node
)
1210 raise nodes
.SkipNode
1211 # append to document body
1213 self
.body
.append(self
.starttag(node
, tag
,
1214 suffix
='\n'*bool(math_env
),
1216 self
.body
.append(math_code
)
1217 if math_env
: # block mode (equation, display)
1218 self
.body
.append('\n')
1220 self
.body
.append('</%s>' % tag
)
1222 self
.body
.append('\n')
1223 # Content already processed:
1224 raise nodes
.SkipNode
1226 def depart_math(self
, node
):
1227 pass # never reached
1229 def visit_math_block(self
, node
):
1230 # print node.astext().encode('utf8')
1231 math_env
= pick_math_environment(node
.astext())
1232 self
.visit_math(node
, math_env
=math_env
)
1234 def depart_math_block(self
, node
):
1235 pass # never reached
1237 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1238 # HTML5/polyglott recommends using both
1239 def visit_meta(self
, node
):
1240 if node
.hasattr('lang'):
1241 node
['xml:lang'] = node
['lang']
1243 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1246 def depart_meta(self
, node
):
1249 def add_meta(self
, tag
):
1250 self
.meta
.append(tag
)
1251 self
.head
.append(tag
)
1253 def visit_option(self
, node
):
1254 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1256 def depart_option(self
, node
):
1257 self
.body
.append('</span>')
1258 if isinstance(node
.next_node(descend
=False, siblings
=True),
1260 self
.body
.append(', ')
1262 def visit_option_argument(self
, node
):
1263 self
.body
.append(node
.get('delimiter', ' '))
1264 self
.body
.append(self
.starttag(node
, 'var', ''))
1266 def depart_option_argument(self
, node
):
1267 self
.body
.append('</var>')
1269 def visit_option_group(self
, node
):
1270 self
.body
.append(self
.starttag(node
, 'dt', ''))
1271 self
.body
.append('<kbd>')
1273 def depart_option_group(self
, node
):
1274 self
.body
.append('</kbd></dt>\n')
1276 def visit_option_list(self
, node
):
1278 self
.starttag(node
, 'dl', CLASS
='option-list'))
1280 def depart_option_list(self
, node
):
1281 self
.body
.append('</dl>\n')
1283 def visit_option_list_item(self
, node
):
1286 def depart_option_list_item(self
, node
):
1289 def visit_option_string(self
, node
):
1292 def depart_option_string(self
, node
):
1295 def visit_organization(self
, node
):
1296 self
.visit_docinfo_item(node
, 'organization', meta
=False)
1298 def depart_organization(self
, node
):
1299 self
.depart_docinfo_item()
1301 # Do not omit <p> tags
1302 # --------------------
1304 # The HTML4CSS1 writer does this to "produce
1305 # visually compact lists (less vertical whitespace)". This writer
1306 # relies on CSS rules for"visual compactness".
1308 # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1309 # character data, so you cannot drop the <p> tags.
1310 # * Keeping simple paragraphs in the field_body enables a CSS
1311 # rule to start the field-body on a new line if the label is too long
1312 # * it makes the code simpler.
1314 # TODO: omit paragraph tags in simple table cells?
1316 def visit_paragraph(self
, node
):
1317 self
.body
.append(self
.starttag(node
, 'p', ''))
1319 def depart_paragraph(self
, node
):
1320 self
.body
.append('</p>')
1321 if not (isinstance(node
.parent
, (nodes
.list_item
, nodes
.entry
)) and
1322 (len(node
.parent
) == 1)):
1323 self
.body
.append('\n')
1325 def visit_problematic(self
, node
):
1326 if node
.hasattr('refid'):
1327 self
.body
.append('<a href="#%s">' % node
['refid'])
1328 self
.context
.append('</a>')
1330 self
.context
.append('')
1331 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1333 def depart_problematic(self
, node
):
1334 self
.body
.append('</span>')
1335 self
.body
.append(self
.context
.pop())
1337 def visit_raw(self
, node
):
1338 if 'html' in node
.get('format', '').split():
1339 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1341 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1342 self
.body
.append(node
.astext())
1344 self
.body
.append('</%s>' % t
)
1345 # Keep non-HTML raw text out of output:
1346 raise nodes
.SkipNode
1348 def visit_reference(self
, node
):
1349 atts
= {'class': 'reference'}
1350 if 'refuri' in node
:
1351 atts
['href'] = node
['refuri']
1352 if ( self
.settings
.cloak_email_addresses
1353 and atts
['href'].startswith('mailto:')):
1354 atts
['href'] = self
.cloak_mailto(atts
['href'])
1355 self
.in_mailto
= True
1356 atts
['class'] += ' external'
1358 assert 'refid' in node
, \
1359 'References must have "refuri" or "refid" attribute.'
1360 atts
['href'] = '#' + node
['refid']
1361 atts
['class'] += ' internal'
1362 if not isinstance(node
.parent
, nodes
.TextElement
):
1363 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1364 atts
['class'] += ' image-reference'
1365 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1367 def depart_reference(self
, node
):
1368 self
.body
.append('</a>')
1369 if not isinstance(node
.parent
, nodes
.TextElement
):
1370 self
.body
.append('\n')
1371 self
.in_mailto
= False
1373 def visit_revision(self
, node
):
1374 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1376 def depart_revision(self
, node
):
1377 self
.depart_docinfo_item()
1379 def visit_row(self
, node
):
1380 self
.body
.append(self
.starttag(node
, 'tr', ''))
1383 def depart_row(self
, node
):
1384 self
.body
.append('</tr>\n')
1386 def visit_rubric(self
, node
):
1387 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1389 def depart_rubric(self
, node
):
1390 self
.body
.append('</p>\n')
1392 # TODO: use the new HTML 5 element <section>?
1393 def visit_section(self
, node
):
1394 self
.section_level
+= 1
1396 self
.starttag(node
, 'div', CLASS
='section'))
1398 def depart_section(self
, node
):
1399 self
.section_level
-= 1
1400 self
.body
.append('</div>\n')
1402 # TODO: use the new HTML5 element <aside>? (Also for footnote text)
1403 def visit_sidebar(self
, node
):
1405 self
.starttag(node
, 'div', CLASS
='sidebar'))
1406 self
.set_first_last(node
)
1407 self
.in_sidebar
= True
1409 def depart_sidebar(self
, node
):
1410 self
.body
.append('</div>\n')
1411 self
.in_sidebar
= False
1413 def visit_status(self
, node
):
1414 self
.visit_docinfo_item(node
, 'status', meta
=False)
1416 def depart_status(self
, node
):
1417 self
.depart_docinfo_item()
1419 def visit_strong(self
, node
):
1420 self
.body
.append(self
.starttag(node
, 'strong', ''))
1422 def depart_strong(self
, node
):
1423 self
.body
.append('</strong>')
1425 def visit_subscript(self
, node
):
1426 self
.body
.append(self
.starttag(node
, 'sub', ''))
1428 def depart_subscript(self
, node
):
1429 self
.body
.append('</sub>')
1431 def visit_substitution_definition(self
, node
):
1432 """Internal only."""
1433 raise nodes
.SkipNode
1435 def visit_substitution_reference(self
, node
):
1436 self
.unimplemented_visit(node
)
1438 # h1–h6 elements must not be used to markup subheadings, subtitles,
1439 # alternative titles and taglines unless intended to be the heading for a
1440 # new section or subsection.
1441 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1443 def visit_subtitle(self
, node
):
1444 if isinstance(node
.parent
, nodes
.sidebar
):
1445 classes
= 'sidebar-subtitle'
1446 elif isinstance(node
.parent
, nodes
.document
):
1447 classes
= 'subtitle'
1448 self
.in_document_title
= len(self
.body
)
1449 elif isinstance(node
.parent
, nodes
.section
):
1450 classes
= 'section-subtitle'
1451 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
=classes
))
1453 def depart_subtitle(self
, node
):
1454 self
.body
.append('</p>\n')
1455 if self
.in_document_title
:
1456 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1457 self
.in_document_title
= 0
1458 self
.body_pre_docinfo
.extend(self
.body
)
1459 self
.html_subtitle
.extend(self
.body
)
1462 def visit_superscript(self
, node
):
1463 self
.body
.append(self
.starttag(node
, 'sup', ''))
1465 def depart_superscript(self
, node
):
1466 self
.body
.append('</sup>')
1468 def visit_system_message(self
, node
):
1469 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1470 self
.body
.append('<p class="system-message-title">')
1472 if len(node
['backrefs']):
1473 backrefs
= node
['backrefs']
1474 if len(backrefs
) == 1:
1475 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1480 for backref
in backrefs
:
1481 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1483 backref_text
= ('; <em>backlinks: %s</em>'
1484 % ', '.join(backlinks
))
1485 if node
.hasattr('line'):
1486 line
= ', line %s' % node
['line']
1489 self
.body
.append('System Message: %s/%s '
1490 '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1491 % (node
['type'], node
['level'],
1492 self
.encode(node
['source']), line
, backref_text
))
1494 def depart_system_message(self
, node
):
1495 self
.body
.append('</div>\n')
1499 # no hard-coded border setting in the table head::
1501 def visit_table(self
, node
):
1502 classes
= [cls
.strip(u
' \t\n')
1503 for cls
in self
.settings
.table_style
.split(',')]
1504 tag
= self
.starttag(node
, 'table', CLASS
=' '.join(classes
))
1505 self
.body
.append(tag
)
1507 def depart_table(self
, node
):
1508 self
.body
.append('</table>\n')
1510 def visit_target(self
, node
):
1511 if not ('refuri' in node
or 'refid' in node
1512 or 'refname' in node
):
1513 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1514 self
.context
.append('</span>')
1516 self
.context
.append('')
1518 def depart_target(self
, node
):
1519 self
.body
.append(self
.context
.pop())
1521 # no hard-coded vertical alignment in table body::
1523 def visit_tbody(self
, node
):
1524 self
.write_colspecs()
1525 self
.body
.append(self
.context
.pop()) # '</colgroup>\n' or ''
1526 self
.body
.append(self
.starttag(node
, 'tbody'))
1528 def depart_tbody(self
, node
):
1529 self
.body
.append('</tbody>\n')
1531 def visit_term(self
, node
):
1532 self
.body
.append(self
.starttag(node
, 'dt', ''))
1534 def depart_term(self
, node
):
1536 Leave the end tag to `self.visit_definition()`, in case there's a
1541 def visit_tgroup(self
, node
):
1542 # Mozilla needs <colgroup>:
1543 self
.body
.append(self
.starttag(node
, 'colgroup'))
1544 # Appended by thead or tbody:
1545 self
.context
.append('</colgroup>\n')
1548 def depart_tgroup(self
, node
):
1551 def visit_thead(self
, node
):
1552 self
.write_colspecs()
1553 self
.body
.append(self
.context
.pop()) # '</colgroup>\n'
1554 # There may or may not be a <thead>; this is for <tbody> to use:
1555 self
.context
.append('')
1556 self
.body
.append(self
.starttag(node
, 'thead'))
1558 def depart_thead(self
, node
):
1559 self
.body
.append('</thead>\n')
1561 def visit_title(self
, node
):
1562 """Only 6 section levels are supported by HTML."""
1563 check_id
= 0 # TODO: is this a bool (False) or a counter?
1564 close_tag
= '</p>\n'
1565 if isinstance(node
.parent
, nodes
.topic
):
1567 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1568 elif isinstance(node
.parent
, nodes
.sidebar
):
1570 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1571 elif isinstance(node
.parent
, nodes
.Admonition
):
1573 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1574 elif isinstance(node
.parent
, nodes
.table
):
1576 self
.starttag(node
, 'caption', ''))
1577 close_tag
= '</caption>\n'
1578 elif isinstance(node
.parent
, nodes
.document
):
1579 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1580 close_tag
= '</h1>\n'
1581 self
.in_document_title
= len(self
.body
)
1583 assert isinstance(node
.parent
, nodes
.section
)
1584 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1586 if (len(node
.parent
) >= 2 and
1587 isinstance(node
.parent
[1], nodes
.subtitle
)):
1588 atts
['CLASS'] = 'with-subtitle'
1590 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1592 if node
.hasattr('refid'):
1593 atts
['class'] = 'toc-backref'
1594 atts
['href'] = '#' + node
['refid']
1596 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1597 close_tag
= '</a></h%s>\n' % (h_level
)
1599 close_tag
= '</h%s>\n' % (h_level
)
1600 self
.context
.append(close_tag
)
1602 def depart_title(self
, node
):
1603 self
.body
.append(self
.context
.pop())
1604 if self
.in_document_title
:
1605 self
.title
= self
.body
[self
.in_document_title
:-1]
1606 self
.in_document_title
= 0
1607 self
.body_pre_docinfo
.extend(self
.body
)
1608 self
.html_title
.extend(self
.body
)
1611 def visit_title_reference(self
, node
):
1612 self
.body
.append(self
.starttag(node
, 'cite', ''))
1614 def depart_title_reference(self
, node
):
1615 self
.body
.append('</cite>')
1617 def visit_topic(self
, node
):
1618 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1619 self
.topic_classes
= node
['classes']
1620 # TODO: replace with ::
1621 # self.in_contents = 'contents' in node['classes']
1623 def depart_topic(self
, node
):
1624 self
.body
.append('</div>\n')
1625 self
.topic_classes
= []
1626 # TODO self.in_contents = False
1628 def visit_transition(self
, node
):
1629 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1631 def depart_transition(self
, node
):
1634 def visit_version(self
, node
):
1635 self
.visit_docinfo_item(node
, 'version', meta
=False)
1637 def depart_version(self
, node
):
1638 self
.depart_docinfo_item()
1640 def unimplemented_visit(self
, node
):
1641 raise NotImplementedError('visiting unimplemented node type: %s'
1642 % node
.__class
__.__name
__)
1645 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1648 Raise `nodes.NodeFound` if non-simple list item is encountered.
1650 Here "simple" means a list item containing nothing other than a single
1651 paragraph, a simple list, or a paragraph followed by a simple list.
1653 This version also checks for simple field lists and docinfo.
1656 def default_visit(self
, node
):
1657 raise nodes
.NodeFound
1659 def visit_list_item(self
, node
):
1660 # print "visiting list item", node.__class__
1661 children
= [child
for child
in node
.children
1662 if not isinstance(child
, nodes
.Invisible
)]
1663 # print "has %s visible children" % len(children)
1664 if (children
and isinstance(children
[0], nodes
.paragraph
)
1665 and (isinstance(children
[-1], nodes
.bullet_list
) or
1666 isinstance(children
[-1], nodes
.enumerated_list
) or
1667 isinstance(children
[-1], nodes
.field_list
))):
1669 # print "%s children remain" % len(children)
1670 if len(children
) <= 1:
1673 # print "found", child.__class__, "in", node.__class__
1674 raise nodes
.NodeFound
1676 def pass_node(self
, node
):
1679 def ignore_node(self
, node
):
1680 # ignore nodes that are never complex (can contain only inline nodes)
1681 raise nodes
.SkipNode
1683 # Paragraphs and text
1684 visit_Text
= ignore_node
1685 visit_paragraph
= ignore_node
1688 visit_bullet_list
= pass_node
1689 visit_enumerated_list
= pass_node
1690 visit_docinfo
= pass_node
1693 visit_author
= ignore_node
1694 visit_authors
= visit_list_item
1695 visit_address
= visit_list_item
1696 visit_contact
= pass_node
1697 visit_copyright
= ignore_node
1698 visit_date
= ignore_node
1699 visit_organization
= ignore_node
1700 visit_status
= ignore_node
1701 visit_version
= visit_list_item
1704 visit_definition_list
= pass_node
1705 visit_definition_list_item
= pass_node
1706 visit_term
= ignore_node
1707 visit_classifier
= pass_node
1708 visit_definition
= visit_list_item
1711 visit_field_list
= pass_node
1712 visit_field
= pass_node
1713 # the field body corresponds to a list item
1714 visit_field_body
= visit_list_item
1715 visit_field_name
= ignore_node
1717 # Invisible nodes should be ignored.
1718 visit_comment
= ignore_node
1719 visit_substitution_definition
= ignore_node
1720 visit_target
= ignore_node
1721 visit_pending
= ignore_node