2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
6 Simple HyperText Markup Language document tree Writer.
8 The output conforms to the XHTML version 1.0 Transitional DTD
9 (*almost* strict). The output contains a minimum of formatting
10 information. The cascading style sheet "html4css1.css" is required
11 for proper viewing with a modern graphical browser.
14 __docformat__
= 'reStructuredText'
23 try: # check for the Python Imaging Library
26 try: # sometimes PIL modules are put in PYTHONPATH's root
28 class PIL(object): pass # dummy wrapper
33 from docutils
import frontend
, nodes
, utils
, writers
, languages
, io
34 from docutils
.transforms
import writer_aux
35 from docutils
.math
import unichar2tex
, pick_math_environment
36 from docutils
.math
.latex2mathml
import parse_latex_math
37 from docutils
.math
.math2html
import math2html
39 class Writer(writers
.Writer
):
41 supported
= ('html', 'html4css1', 'xhtml')
42 """Formats this writer supports."""
44 default_stylesheet
= 'html4css1.css'
46 default_stylesheet_path
= utils
.relative_path(
47 os
.path
.join(os
.getcwd(), 'dummy'),
48 os
.path
.join(os
.path
.dirname(__file__
), default_stylesheet
))
50 default_template
= 'template.txt'
52 default_template_path
= utils
.relative_path(
53 os
.path
.join(os
.getcwd(), 'dummy'),
54 os
.path
.join(os
.path
.dirname(__file__
), default_template
))
57 'HTML-Specific Options',
59 (('Specify the template file (UTF-8 encoded). Default is "%s".'
60 % default_template_path
,
62 {'default': default_template_path
, 'metavar': '<file>'}),
63 ('Specify comma separated list of stylesheet URLs. '
64 'Overrides previous --stylesheet and --stylesheet-path settings.',
66 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
67 ('Specify comma separated list of stylesheet paths. '
68 'With --link-stylesheet, '
69 'the path is rewritten relative to the output HTML file. '
70 'Default: "%s"' % default_stylesheet_path
,
71 ['--stylesheet-path'],
72 {'metavar': '<file>', 'overrides': 'stylesheet',
73 'default': default_stylesheet_path
}),
74 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
75 'files must be accessible during processing. This is the default.',
76 ['--embed-stylesheet'],
77 {'default': 1, 'action': 'store_true',
78 'validator': frontend
.validate_boolean
}),
79 ('Link to the stylesheet(s) in the output HTML file. '
80 'Default: embed stylesheets.',
81 ['--link-stylesheet'],
82 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
83 ('Specify the initial header level. Default is 1 for "<h1>". '
84 'Does not affect document title & subtitle (see --no-doc-title).',
85 ['--initial-header-level'],
86 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
87 'metavar': '<level>'}),
88 ('Specify the maximum width (in characters) for one-column field '
89 'names. Longer field names will span an entire row of the table '
90 'used to render the field list. Default is 14 characters. '
91 'Use 0 for "no limit".',
92 ['--field-name-limit'],
93 {'default': 14, 'metavar': '<level>',
94 'validator': frontend
.validate_nonnegative_int
}),
95 ('Specify the maximum width (in characters) for options in option '
96 'lists. Longer options will span an entire row of the table used '
97 'to render the option list. Default is 14 characters. '
98 'Use 0 for "no limit".',
100 {'default': 14, 'metavar': '<level>',
101 'validator': frontend
.validate_nonnegative_int
}),
102 ('Format for footnote references: one of "superscript" or '
103 '"brackets". Default is "brackets".',
104 ['--footnote-references'],
105 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
106 'metavar': '<format>',
107 'overrides': 'trim_footnote_reference_space'}),
108 ('Format for block quote attributions: one of "dash" (em-dash '
109 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
111 {'choices': ['dash', 'parentheses', 'parens', 'none'],
112 'default': 'dash', 'metavar': '<format>'}),
113 ('Remove extra vertical whitespace between items of "simple" bullet '
114 'lists and enumerated lists. Default: enabled.',
116 {'default': 1, 'action': 'store_true',
117 'validator': frontend
.validate_boolean
}),
118 ('Disable compact simple bullet and enumerated lists.',
119 ['--no-compact-lists'],
120 {'dest': 'compact_lists', 'action': 'store_false'}),
121 ('Remove extra vertical whitespace between items of simple field '
122 'lists. Default: enabled.',
123 ['--compact-field-lists'],
124 {'default': 1, 'action': 'store_true',
125 'validator': frontend
.validate_boolean
}),
126 ('Disable compact simple field lists.',
127 ['--no-compact-field-lists'],
128 {'dest': 'compact_field_lists', 'action': 'store_false'}),
129 ('Added to standard table classes. '
130 'Defined styles: "borderless". Default: ""',
133 ('Math output format, one of "MathML", "HTML", "MathJax" '
134 'or "LaTeX". Default: "MathJax"',
136 {'default': 'MathJax'}),
137 ('Omit the XML declaration. Use with caution.',
138 ['--no-xml-declaration'],
139 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
140 'validator': frontend
.validate_boolean
}),
141 ('Obfuscate email addresses to confuse harvesters while still '
142 'keeping email links usable with standards-compliant browsers.',
143 ['--cloak-email-addresses'],
144 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
146 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
148 relative_path_settings
= ('stylesheet_path',)
150 config_section
= 'html4css1 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 HTML writer has been optimized to produce visually compact
200 lists (less vertical whitespace). HTML's mixed content models
201 allow list items to contain "<li><p>body elements</p></li>" or
202 "<li>just text</li>" or even "<li>text<p>and body
203 elements</p>combined</li>", each with different effects. It would
204 be best to stick with strict body elements in list items, but they
205 affect vertical spacing in browsers (although they really
208 Here is an outline of the optimization:
210 - Check for and omit <p> tags in "simple" lists: list items
211 contain either a single paragraph, a nested simple list, or a
212 paragraph followed by a nested simple list. This means that
213 this list can be compact:
218 But this list cannot be compact:
222 This second paragraph forces space between list items.
226 - In non-list contexts, omit <p> tags on a paragraph if that
227 paragraph is the only child of its parent (footnotes & citations
228 are allowed a label first).
230 - Regardless of the above, in definitions, table cells, field bodies,
231 option descriptions, and list items, mark the first child with
232 'class="first"' and the last child with 'class="last"'. The stylesheet
233 sets the margins (top & bottom respectively) to 0 for these elements.
235 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
236 option) disables list whitespace optimization.
239 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
241 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
242 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
243 doctype_mathml
= doctype
245 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
246 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
247 content_type
= ('<meta http-equiv="Content-Type"'
248 ' content="text/html; charset=%s" />\n')
249 content_type_mathml
= ('<meta http-equiv="Content-Type"'
250 ' content="application/xhtml+xml; charset=%s" />\n')
252 generator
= ('<meta name="generator" content="Docutils %s: '
253 'http://docutils.sourceforge.net/" />\n')
255 # Template for the MathJax script in the header:
256 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
257 # The latest version of MathJax from the distributed server:
258 # avaliable to the public under the `MathJax CDN Terms of Service`__
259 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
260 mathjax_url
= ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
261 'config=TeX-AMS-MML_HTMLorMML')
262 # TODO: make this configurable:
264 # a) as extra option or
265 # b) appended to math-output="MathJax"?
267 # If b), which delimiter/delimter-set (':', ',', ' ')?
269 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
270 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
271 words_and_spaces
= re
.compile(r
'\S+| +|\n')
272 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
273 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
275 def __init__(self
, document
):
276 nodes
.NodeVisitor
.__init
__(self
, document
)
277 self
.settings
= settings
= document
.settings
278 lcode
= settings
.language_code
279 self
.language
= languages
.get_language(lcode
, document
.reporter
)
280 self
.meta
= [self
.generator
% docutils
.__version
__]
281 self
.head_prefix
= []
282 self
.html_prolog
= []
283 if settings
.xml_declaration
:
284 self
.head_prefix
.append(self
.xml_declaration
285 % settings
.output_encoding
)
286 # encoding not interpolated:
287 self
.html_prolog
.append(self
.xml_declaration
)
288 self
.head
= self
.meta
[:]
290 styles
= utils
.get_stylesheet_list(settings
)
291 if settings
.stylesheet_path
and not(settings
.embed_stylesheet
):
292 styles
= [utils
.relative_path(settings
._destination
, sheet
)
294 if settings
.embed_stylesheet
:
295 self
.stylesheet
= [self
.embedded_stylesheet
%
296 io
.FileInput(source_path
=sheet
, encoding
='utf-8').read()
298 settings
.record_dependencies
.add(*styles
)
299 else: # link to stylesheets
300 self
.stylesheet
= [self
.stylesheet_link
% self
.encode(stylesheet
)
301 for stylesheet
in styles
]
302 self
.body_prefix
= ['</head>\n<body>\n']
303 # document title, subtitle display
304 self
.body_pre_docinfo
= []
309 self
.body_suffix
= ['</body>\n</html>\n']
310 self
.section_level
= 0
311 self
.initial_header_level
= int(settings
.initial_header_level
)
312 self
.math_output
= settings
.math_output
.lower()
313 # A heterogenous stack used in conjunction with the tree traversal.
314 # Make sure that the pops correspond to the pushes:
316 self
.topic_classes
= []
319 self
.compact_simple
= None
320 self
.compact_field_list
= None
321 self
.in_docinfo
= None
322 self
.in_sidebar
= None
327 self
.html_head
= [self
.content_type
] # charset not interpolated
329 self
.html_subtitle
= []
331 self
.in_document_title
= 0
333 self
.author_in_authors
= None
334 self
.math_header
= ''
337 return ''.join(self
.head_prefix
+ self
.head
338 + self
.stylesheet
+ self
.body_prefix
339 + self
.body_pre_docinfo
+ self
.docinfo
340 + self
.body
+ self
.body_suffix
)
342 def encode(self
, text
):
343 """Encode special characters in `text` & return."""
344 # @@@ A codec to do these and all other HTML entities would be nice.
346 return text
.translate({
351 ord('@'): u
'@', # may thwart some address harvesters
352 # TODO: convert non-breaking space only if needed?
353 0xa0: u
' '}) # non-breaking space
355 def cloak_mailto(self
, uri
):
356 """Try to hide a mailto: URL from harvesters."""
357 # Encode "@" using a URL octet reference (see RFC 1738).
358 # Further cloaking with HTML entities will be done in the
360 return uri
.replace('@', '%40')
362 def cloak_email(self
, addr
):
363 """Try to hide the link text of a email link from harversters."""
364 # Surround at-signs and periods with <span> tags. ("@" has
365 # already been encoded to "@" by the `encode` method.)
366 addr
= addr
.replace('@', '<span>@</span>')
367 addr
= addr
.replace('.', '<span>.</span>')
370 def attval(self
, text
,
371 whitespace
=re
.compile('[\n\r\t\v\f]')):
372 """Cleanse, HTML encode, and return attribute value text."""
373 encoded
= self
.encode(whitespace
.sub(' ', text
))
374 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
375 # Cloak at-signs ("%40") and periods with HTML entities.
376 encoded
= encoded
.replace('%40', '%40')
377 encoded
= encoded
.replace('.', '.')
380 def starttag(self
, node
, tagname
, suffix
='\n', empty
=0, **attributes
):
382 Construct and return a start tag given a node (id & class attributes
383 are extracted), tag name, and optional attributes.
385 tagname
= tagname
.lower()
389 for (name
, value
) in attributes
.items():
390 atts
[name
.lower()] = value
391 classes
= node
.get('classes', [])
393 classes
.append(atts
.pop('class'))
394 # move language specification to 'lang' attribute
395 languages
= [cls
for cls
in classes
396 if cls
.startswith('language-')]
398 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
399 atts
[self
.lang_attribute
] = languages
[0][9:]
400 classes
.pop(classes
.index(languages
[0]))
401 classes
= ' '.join(classes
).strip()
403 atts
['class'] = classes
404 assert 'id' not in atts
405 ids
.extend(node
.get('ids', []))
407 ids
.extend(atts
['ids'])
412 # Add empty "span" elements for additional IDs. Note
413 # that we cannot use empty "a" elements because there
414 # may be targets inside of references, but nested "a"
415 # elements aren't allowed in XHTML (even if they do
416 # not all have a "href" attribute).
418 # Empty tag. Insert target right in front of element.
419 prefix
.append('<span id="%s"></span>' % id)
421 # Non-empty tag. Place the auxiliary <span> tag
422 # *inside* the element, as the first child.
423 suffix
+= '<span id="%s"></span>' % id
424 attlist
= atts
.items()
427 for name
, value
in attlist
:
428 # value=None was used for boolean attributes without
429 # value, but this isn't supported by XHTML.
430 assert value
is not None
431 if isinstance(value
, list):
432 values
= [unicode(v
) for v
in value
]
433 parts
.append('%s="%s"' % (name
.lower(),
434 self
.attval(' '.join(values
))))
436 parts
.append('%s="%s"' % (name
.lower(),
437 self
.attval(unicode(value
))))
442 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
444 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
445 """Construct and return an XML-compatible empty tag."""
446 return self
.starttag(node
, tagname
, suffix
, empty
=1, **attributes
)
448 def set_class_on_child(self
, node
, class_
, index
=0):
450 Set class `class_` on the visible child no. index of `node`.
451 Do nothing if node has fewer children than `index`.
453 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
455 child
= children
[index
]
458 child
['classes'].append(class_
)
460 def set_first_last(self
, node
):
461 self
.set_class_on_child(node
, 'first', 0)
462 self
.set_class_on_child(node
, 'last', -1)
464 def visit_Text(self
, node
):
466 encoded
= self
.encode(text
)
467 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
468 encoded
= self
.cloak_email(encoded
)
469 self
.body
.append(encoded
)
471 def depart_Text(self
, node
):
474 def visit_abbreviation(self
, node
):
475 # @@@ implementation incomplete ("title" attribute)
476 self
.body
.append(self
.starttag(node
, 'abbr', ''))
478 def depart_abbreviation(self
, node
):
479 self
.body
.append('</abbr>')
481 def visit_acronym(self
, node
):
482 # @@@ implementation incomplete ("title" attribute)
483 self
.body
.append(self
.starttag(node
, 'acronym', ''))
485 def depart_acronym(self
, node
):
486 self
.body
.append('</acronym>')
488 def visit_address(self
, node
):
489 self
.visit_docinfo_item(node
, 'address', meta
=None)
490 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='address'))
492 def depart_address(self
, node
):
493 self
.body
.append('\n</pre>\n')
494 self
.depart_docinfo_item()
496 def visit_admonition(self
, node
):
497 self
.body
.append(self
.starttag(node
, 'div'))
498 self
.set_first_last(node
)
500 def depart_admonition(self
, node
=None):
501 self
.body
.append('</div>\n')
503 attribution_formats
= {'dash': ('—', ''),
504 'parentheses': ('(', ')'),
505 'parens': ('(', ')'),
508 def visit_attribution(self
, node
):
509 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
510 self
.context
.append(suffix
)
512 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
514 def depart_attribution(self
, node
):
515 self
.body
.append(self
.context
.pop() + '</p>\n')
517 def visit_author(self
, node
):
518 if isinstance(node
.parent
, nodes
.authors
):
519 if self
.author_in_authors
:
520 self
.body
.append('\n<br />')
522 self
.visit_docinfo_item(node
, 'author')
524 def depart_author(self
, node
):
525 if isinstance(node
.parent
, nodes
.authors
):
526 self
.author_in_authors
+= 1
528 self
.depart_docinfo_item()
530 def visit_authors(self
, node
):
531 self
.visit_docinfo_item(node
, 'authors')
532 self
.author_in_authors
= 0 # initialize counter
534 def depart_authors(self
, node
):
535 self
.depart_docinfo_item()
536 self
.author_in_authors
= None
538 def visit_block_quote(self
, node
):
539 self
.body
.append(self
.starttag(node
, 'blockquote'))
541 def depart_block_quote(self
, node
):
542 self
.body
.append('</blockquote>\n')
544 def check_simple_list(self
, node
):
545 """Check for a simple list that can be rendered compactly."""
546 visitor
= SimpleListChecker(self
.document
)
549 except nodes
.NodeFound
:
554 def is_compactable(self
, node
):
555 return ('compact' in node
['classes']
556 or (self
.settings
.compact_lists
557 and 'open' not in node
['classes']
558 and (self
.compact_simple
559 or self
.topic_classes
== ['contents']
560 or self
.check_simple_list(node
))))
562 def visit_bullet_list(self
, node
):
564 old_compact_simple
= self
.compact_simple
565 self
.context
.append((self
.compact_simple
, self
.compact_p
))
566 self
.compact_p
= None
567 self
.compact_simple
= self
.is_compactable(node
)
568 if self
.compact_simple
and not old_compact_simple
:
569 atts
['class'] = 'simple'
570 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
572 def depart_bullet_list(self
, node
):
573 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
574 self
.body
.append('</ul>\n')
576 def visit_caption(self
, node
):
577 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
579 def depart_caption(self
, node
):
580 self
.body
.append('</p>\n')
582 def visit_citation(self
, node
):
583 self
.body
.append(self
.starttag(node
, 'table',
584 CLASS
='docutils citation',
585 frame
="void", rules
="none"))
586 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
587 '<tbody valign="top">\n'
589 self
.footnote_backrefs(node
)
591 def depart_citation(self
, node
):
592 self
.body
.append('</td></tr>\n'
593 '</tbody>\n</table>\n')
595 def visit_citation_reference(self
, node
):
596 href
= '#' + node
['refid']
597 self
.body
.append(self
.starttag(
598 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
600 def depart_citation_reference(self
, node
):
601 self
.body
.append(']</a>')
603 def visit_classifier(self
, node
):
604 self
.body
.append(' <span class="classifier-delimiter">:</span> ')
605 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
607 def depart_classifier(self
, node
):
608 self
.body
.append('</span>')
610 def visit_colspec(self
, node
):
611 self
.colspecs
.append(node
)
612 # "stubs" list is an attribute of the tgroup element:
613 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
615 def depart_colspec(self
, node
):
618 def write_colspecs(self
):
620 for node
in self
.colspecs
:
621 width
+= node
['colwidth']
622 for node
in self
.colspecs
:
623 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
624 self
.body
.append(self
.emptytag(node
, 'col',
625 width
='%i%%' % colwidth
))
628 def visit_comment(self
, node
,
629 sub
=re
.compile('-(?=-)').sub
):
630 """Escape double-dashes in comment text."""
631 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
632 # Content already processed:
635 def visit_compound(self
, node
):
636 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
638 node
[0]['classes'].append('compound-first')
639 node
[-1]['classes'].append('compound-last')
640 for child
in node
[1:-1]:
641 child
['classes'].append('compound-middle')
643 def depart_compound(self
, node
):
644 self
.body
.append('</div>\n')
646 def visit_container(self
, node
):
647 self
.body
.append(self
.starttag(node
, 'div', CLASS
='container'))
649 def depart_container(self
, node
):
650 self
.body
.append('</div>\n')
652 def visit_contact(self
, node
):
653 self
.visit_docinfo_item(node
, 'contact', meta
=None)
655 def depart_contact(self
, node
):
656 self
.depart_docinfo_item()
658 def visit_copyright(self
, node
):
659 self
.visit_docinfo_item(node
, 'copyright')
661 def depart_copyright(self
, node
):
662 self
.depart_docinfo_item()
664 def visit_date(self
, node
):
665 self
.visit_docinfo_item(node
, 'date')
667 def depart_date(self
, node
):
668 self
.depart_docinfo_item()
670 def visit_decoration(self
, node
):
673 def depart_decoration(self
, node
):
676 def visit_definition(self
, node
):
677 self
.body
.append('</dt>\n')
678 self
.body
.append(self
.starttag(node
, 'dd', ''))
679 self
.set_first_last(node
)
681 def depart_definition(self
, node
):
682 self
.body
.append('</dd>\n')
684 def visit_definition_list(self
, node
):
685 self
.body
.append(self
.starttag(node
, 'dl', CLASS
='docutils'))
687 def depart_definition_list(self
, node
):
688 self
.body
.append('</dl>\n')
690 def visit_definition_list_item(self
, node
):
693 def depart_definition_list_item(self
, node
):
696 def visit_description(self
, node
):
697 self
.body
.append(self
.starttag(node
, 'td', ''))
698 self
.set_first_last(node
)
700 def depart_description(self
, node
):
701 self
.body
.append('</td>')
703 def visit_docinfo(self
, node
):
704 self
.context
.append(len(self
.body
))
705 self
.body
.append(self
.starttag(node
, 'table',
707 frame
="void", rules
="none"))
708 self
.body
.append('<col class="docinfo-name" />\n'
709 '<col class="docinfo-content" />\n'
710 '<tbody valign="top">\n')
713 def depart_docinfo(self
, node
):
714 self
.body
.append('</tbody>\n</table>\n')
715 self
.in_docinfo
= None
716 start
= self
.context
.pop()
717 self
.docinfo
= self
.body
[start
:]
720 def visit_docinfo_item(self
, node
, name
, meta
=1):
722 meta_tag
= '<meta name="%s" content="%s" />\n' \
723 % (name
, self
.attval(node
.astext()))
724 self
.add_meta(meta_tag
)
725 self
.body
.append(self
.starttag(node
, 'tr', ''))
726 self
.body
.append('<th class="docinfo-name">%s:</th>\n<td>'
727 % self
.language
.labels
[name
])
729 if isinstance(node
[0], nodes
.Element
):
730 node
[0]['classes'].append('first')
731 if isinstance(node
[-1], nodes
.Element
):
732 node
[-1]['classes'].append('last')
734 def depart_docinfo_item(self
):
735 self
.body
.append('</td></tr>\n')
737 def visit_doctest_block(self
, node
):
738 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='doctest-block'))
740 def depart_doctest_block(self
, node
):
741 self
.body
.append('\n</pre>\n')
743 def visit_document(self
, node
):
744 self
.head
.append('<title>%s</title>\n'
745 % self
.encode(node
.get('title', '')))
747 def depart_document(self
, node
):
748 self
.head_prefix
.extend([self
.doctype
,
749 self
.head_prefix_template
%
750 {'lang': self
.settings
.language_code
}])
751 self
.html_prolog
.append(self
.doctype
)
752 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
753 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
755 self
.head
.append(self
.math_header
)
756 # skip content-type meta tag with interpolated charset value:
757 self
.html_head
.extend(self
.head
[1:])
758 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
759 self
.body_suffix
.insert(0, '</div>\n')
760 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
761 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
762 + self
.docinfo
+ self
.body
763 + self
.body_suffix
[:-1])
764 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
766 def visit_emphasis(self
, node
):
767 self
.body
.append(self
.starttag(node
, 'em', ''))
769 def depart_emphasis(self
, node
):
770 self
.body
.append('</em>')
772 def visit_entry(self
, node
):
774 if isinstance(node
.parent
.parent
, nodes
.thead
):
775 atts
['class'].append('head')
776 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
777 # "stubs" list is an attribute of the tgroup element
778 atts
['class'].append('stub')
781 atts
['class'] = ' '.join(atts
['class'])
785 node
.parent
.column
+= 1
786 if 'morerows' in node
:
787 atts
['rowspan'] = node
['morerows'] + 1
788 if 'morecols' in node
:
789 atts
['colspan'] = node
['morecols'] + 1
790 node
.parent
.column
+= node
['morecols']
791 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
792 self
.context
.append('</%s>\n' % tagname
.lower())
793 if len(node
) == 0: # empty cell
794 self
.body
.append(' ')
795 self
.set_first_last(node
)
797 def depart_entry(self
, node
):
798 self
.body
.append(self
.context
.pop())
800 def visit_enumerated_list(self
, node
):
802 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
803 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
808 atts
['start'] = node
['start']
809 if 'enumtype' in node
:
810 atts
['class'] = node
['enumtype']
811 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
812 # single "format" attribute? Use CSS2?
813 old_compact_simple
= self
.compact_simple
814 self
.context
.append((self
.compact_simple
, self
.compact_p
))
815 self
.compact_p
= None
816 self
.compact_simple
= self
.is_compactable(node
)
817 if self
.compact_simple
and not old_compact_simple
:
818 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
819 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
821 def depart_enumerated_list(self
, node
):
822 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
823 self
.body
.append('</ol>\n')
825 def visit_field(self
, node
):
826 self
.body
.append(self
.starttag(node
, 'tr', '', CLASS
='field'))
828 def depart_field(self
, node
):
829 self
.body
.append('</tr>\n')
831 def visit_field_body(self
, node
):
832 self
.body
.append(self
.starttag(node
, 'td', '', CLASS
='field-body'))
833 self
.set_class_on_child(node
, 'first', 0)
835 if (self
.compact_field_list
or
836 isinstance(field
.parent
, nodes
.docinfo
) or
837 field
.parent
.index(field
) == len(field
.parent
) - 1):
838 # If we are in a compact list, the docinfo, or if this is
839 # the last field of the field list, do not add vertical
840 # space after last element.
841 self
.set_class_on_child(node
, 'last', -1)
843 def depart_field_body(self
, node
):
844 self
.body
.append('</td>\n')
846 def visit_field_list(self
, node
):
847 self
.context
.append((self
.compact_field_list
, self
.compact_p
))
848 self
.compact_p
= None
849 if 'compact' in node
['classes']:
850 self
.compact_field_list
= 1
851 elif (self
.settings
.compact_field_lists
852 and 'open' not in node
['classes']):
853 self
.compact_field_list
= 1
854 if self
.compact_field_list
:
856 field_body
= field
[-1]
857 assert isinstance(field_body
, nodes
.field_body
)
858 children
= [n
for n
in field_body
859 if not isinstance(n
, nodes
.Invisible
)]
860 if not (len(children
) == 0 or
861 len(children
) == 1 and
862 isinstance(children
[0],
863 (nodes
.paragraph
, nodes
.line_block
))):
864 self
.compact_field_list
= 0
866 self
.body
.append(self
.starttag(node
, 'table', frame
='void',
868 CLASS
='docutils field-list'))
869 self
.body
.append('<col class="field-name" />\n'
870 '<col class="field-body" />\n'
871 '<tbody valign="top">\n')
873 def depart_field_list(self
, node
):
874 self
.body
.append('</tbody>\n</table>\n')
875 self
.compact_field_list
, self
.compact_p
= self
.context
.pop()
877 def visit_field_name(self
, node
):
880 atts
['class'] = 'docinfo-name'
882 atts
['class'] = 'field-name'
883 if ( self
.settings
.field_name_limit
884 and len(node
.astext()) > self
.settings
.field_name_limit
):
886 self
.context
.append('</tr>\n'
887 + self
.starttag(node
.parent
, 'tr', '')
890 self
.context
.append('')
891 self
.body
.append(self
.starttag(node
, 'th', '', **atts
))
893 def depart_field_name(self
, node
):
894 self
.body
.append(':</th>')
895 self
.body
.append(self
.context
.pop())
897 def visit_figure(self
, node
):
898 atts
= {'class': 'figure'}
899 if node
.get('width'):
900 atts
['style'] = 'width: %s' % node
['width']
901 if node
.get('align'):
902 atts
['class'] += " align-" + node
['align']
903 self
.body
.append(self
.starttag(node
, 'div', **atts
))
905 def depart_figure(self
, node
):
906 self
.body
.append('</div>\n')
908 def visit_footer(self
, node
):
909 self
.context
.append(len(self
.body
))
911 def depart_footer(self
, node
):
912 start
= self
.context
.pop()
913 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
914 '<hr class="footer" />\n']
915 footer
.extend(self
.body
[start
:])
916 footer
.append('\n</div>\n')
917 self
.footer
.extend(footer
)
918 self
.body_suffix
[:0] = footer
919 del self
.body
[start
:]
921 def visit_footnote(self
, node
):
922 self
.body
.append(self
.starttag(node
, 'table',
923 CLASS
='docutils footnote',
924 frame
="void", rules
="none"))
925 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
926 '<tbody valign="top">\n'
928 self
.footnote_backrefs(node
)
930 def footnote_backrefs(self
, node
):
932 backrefs
= node
['backrefs']
933 if self
.settings
.footnote_backlinks
and backrefs
:
934 if len(backrefs
) == 1:
935 self
.context
.append('')
936 self
.context
.append('</a>')
937 self
.context
.append('<a class="fn-backref" href="#%s">'
941 for backref
in backrefs
:
942 backlinks
.append('<a class="fn-backref" href="#%s">%s</a>'
945 self
.context
.append('<em>(%s)</em> ' % ', '.join(backlinks
))
946 self
.context
+= ['', '']
948 self
.context
.append('')
949 self
.context
+= ['', '']
950 # If the node does not only consist of a label.
952 # If there are preceding backlinks, we do not set class
953 # 'first', because we need to retain the top-margin.
955 node
[1]['classes'].append('first')
956 node
[-1]['classes'].append('last')
958 def depart_footnote(self
, node
):
959 self
.body
.append('</td></tr>\n'
960 '</tbody>\n</table>\n')
962 def visit_footnote_reference(self
, node
):
963 href
= '#' + node
['refid']
964 format
= self
.settings
.footnote_references
965 if format
== 'brackets':
967 self
.context
.append(']')
969 assert format
== 'superscript'
971 self
.context
.append('</sup>')
972 self
.body
.append(self
.starttag(node
, 'a', suffix
,
973 CLASS
='footnote-reference', href
=href
))
975 def depart_footnote_reference(self
, node
):
976 self
.body
.append(self
.context
.pop() + '</a>')
978 def visit_generated(self
, node
):
981 def depart_generated(self
, node
):
984 def visit_header(self
, node
):
985 self
.context
.append(len(self
.body
))
987 def depart_header(self
, node
):
988 start
= self
.context
.pop()
989 header
= [self
.starttag(node
, 'div', CLASS
='header')]
990 header
.extend(self
.body
[start
:])
991 header
.append('\n<hr class="header"/>\n</div>\n')
992 self
.body_prefix
.extend(header
)
993 self
.header
.extend(header
)
994 del self
.body
[start
:]
996 def visit_image(self
, node
):
999 # place SVG and SWF images in an <object> element
1000 types
= {'.svg': 'image/svg+xml',
1001 '.swf': 'application/x-shockwave-flash'}
1002 ext
= os
.path
.splitext(uri
)[1].lower()
1003 if ext
in ('.svg', '.swf'):
1005 atts
['type'] = types
[ext
]
1008 atts
['alt'] = node
.get('alt', uri
)
1011 atts
['width'] = node
['width']
1012 if 'height' in node
:
1013 atts
['height'] = node
['height']
1015 if (PIL
and not ('width' in node
and 'height' in node
)
1016 and self
.settings
.file_insertion_enabled
):
1017 imagepath
= urllib
.url2pathname(uri
)
1019 img
= PIL
.Image
.open(
1020 imagepath
.encode(sys
.getfilesystemencoding()))
1021 except (IOError, UnicodeEncodeError):
1024 self
.settings
.record_dependencies
.add(
1025 imagepath
.replace('\\', '/'))
1026 if 'width' not in atts
:
1027 atts
['width'] = str(img
.size
[0])
1028 if 'height' not in atts
:
1029 atts
['height'] = str(img
.size
[1])
1031 for att_name
in 'width', 'height':
1032 if att_name
in atts
:
1033 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1035 atts
[att_name
] = '%s%s' % (
1036 float(match
.group(1)) * (float(node
['scale']) / 100),
1039 for att_name
in 'width', 'height':
1040 if att_name
in atts
:
1041 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1042 # Interpret unitless values as pixels.
1043 atts
[att_name
] += 'px'
1044 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1047 atts
['style'] = ' '.join(style
)
1048 if (isinstance(node
.parent
, nodes
.TextElement
) or
1049 (isinstance(node
.parent
, nodes
.reference
) and
1050 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1051 # Inline context or surrounded by <a>...</a>.
1056 atts
['class'] = 'align-%s' % node
['align']
1057 self
.context
.append('')
1058 if ext
in ('.svg', '.swf'): # place in an object element,
1059 # do NOT use an empty tag: incorrect rendering in browsers
1060 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1061 node
.get('alt', uri
) + '</object>' + suffix
)
1063 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1065 def depart_image(self
, node
):
1066 self
.body
.append(self
.context
.pop())
1068 def visit_inline(self
, node
):
1069 self
.body
.append(self
.starttag(node
, 'span', ''))
1071 def depart_inline(self
, node
):
1072 self
.body
.append('</span>')
1074 def visit_label(self
, node
):
1075 # Context added in footnote_backrefs.
1076 self
.body
.append(self
.starttag(node
, 'td', '%s[' % self
.context
.pop(),
1079 def depart_label(self
, node
):
1080 # Context added in footnote_backrefs.
1081 self
.body
.append(']%s</td><td>%s' % (self
.context
.pop(), self
.context
.pop()))
1083 def visit_legend(self
, node
):
1084 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1086 def depart_legend(self
, node
):
1087 self
.body
.append('</div>\n')
1089 def visit_line(self
, node
):
1090 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1092 self
.body
.append('<br />')
1094 def depart_line(self
, node
):
1095 self
.body
.append('</div>\n')
1097 def visit_line_block(self
, node
):
1098 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1100 def depart_line_block(self
, node
):
1101 self
.body
.append('</div>\n')
1103 def visit_list_item(self
, node
):
1104 self
.body
.append(self
.starttag(node
, 'li', ''))
1106 node
[0]['classes'].append('first')
1108 def depart_list_item(self
, node
):
1109 self
.body
.append('</li>\n')
1111 def visit_literal(self
, node
):
1112 """Process text to prevent tokens from wrapping."""
1114 self
.starttag(node
, 'tt', '', CLASS
='docutils literal'))
1115 text
= node
.astext()
1116 for token
in self
.words_and_spaces
.findall(text
):
1118 # Protect text like "--an-option" and the regular expression
1119 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1120 if self
.sollbruchstelle
.search(token
):
1121 self
.body
.append('<span class="pre">%s</span>'
1122 % self
.encode(token
))
1124 self
.body
.append(self
.encode(token
))
1125 elif token
in ('\n', ' '):
1126 # Allow breaks at whitespace:
1127 self
.body
.append(token
)
1129 # Protect runs of multiple spaces; the last space can wrap:
1130 self
.body
.append(' ' * (len(token
) - 1) + ' ')
1131 self
.body
.append('</tt>')
1132 # Content already processed:
1133 raise nodes
.SkipNode
1135 def visit_literal_block(self
, node
):
1136 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='literal-block'))
1138 def depart_literal_block(self
, node
):
1139 self
.body
.append('\n</pre>\n')
1141 def visit_math(self
, node
, math_env
=''):
1142 # As there is no native HTML math support, we provide alternatives:
1143 # LaTeX and MathJax math_output modes simply wrap the content,
1144 # HTML and MathML math_output modes also convert the math_code.
1145 # If the method is called from visit_math_block(), math_env != ''.
1148 tags
= {# math_output: (block, inline, class-arguments)
1149 'mathml': ('div', '', ''),
1150 'html': ('div', 'span', 'formula'),
1151 'mathjax': ('div', 'span', 'math'),
1152 'latex': ('pre', 'tt', 'math'),
1154 tag
= tags
[self
.math_output
][math_env
== '']
1155 clsarg
= tags
[self
.math_output
][2]
1157 wrappers
= {# math_mode: (inline, block)
1158 'mathml': (None, None),
1159 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1160 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1161 'latex': (None, None),
1163 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1164 # get and wrap content
1165 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1166 if wrapper
and math_env
:
1167 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1169 math_code
= wrapper
% math_code
1170 # settings and conversion
1171 if self
.math_output
in ('latex', 'mathjax'):
1172 math_code
= self
.encode(math_code
)
1173 if self
.math_output
== 'mathjax':
1174 self
.math_header
= self
.mathjax_script
% self
.mathjax_url
1175 elif self
.math_output
== 'html':
1176 math_code
= math2html(math_code
)
1177 elif self
.math_output
== 'mathml':
1178 self
.doctype
= self
.doctype_mathml
1179 self
.content_type
= self
.content_type_mathml
1181 mathml_tree
= parse_latex_math(math_code
, inline
=not(math_env
))
1182 math_code
= ''.join(mathml_tree
.xml())
1183 except SyntaxError, err
:
1184 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1185 self
.visit_system_message(err_node
)
1186 self
.body
.append(self
.starttag(node
, 'p'))
1187 self
.body
.append(u
','.join(err
.args
))
1188 self
.body
.append('</p>\n')
1189 self
.body
.append(self
.starttag(node
, 'pre',
1190 CLASS
='literal-block'))
1191 self
.body
.append(self
.encode(math_code
))
1192 self
.body
.append('\n</pre>\n')
1193 self
.depart_system_message(err_node
)
1194 raise nodes
.SkipNode
1195 # append to document body
1197 self
.body
.append(self
.starttag(node
, tag
, CLASS
=clsarg
))
1198 self
.body
.append(math_code
)
1200 self
.body
.append('\n')
1202 self
.body
.append('</%s>\n' % tag
)
1203 # Content already processed:
1204 raise nodes
.SkipNode
1206 def depart_math(self
, node
):
1207 pass # never reached
1209 def visit_math_block(self
, node
):
1210 # print node.astext().encode('utf8')
1211 math_env
= pick_math_environment(node
.astext())
1212 self
.visit_math(node
, math_env
=math_env
)
1214 def depart_math_block(self
, node
):
1215 pass # never reached
1217 def visit_meta(self
, node
):
1218 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1221 def depart_meta(self
, node
):
1224 def add_meta(self
, tag
):
1225 self
.meta
.append(tag
)
1226 self
.head
.append(tag
)
1228 def visit_option(self
, node
):
1229 if self
.context
[-1]:
1230 self
.body
.append(', ')
1231 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1233 def depart_option(self
, node
):
1234 self
.body
.append('</span>')
1235 self
.context
[-1] += 1
1237 def visit_option_argument(self
, node
):
1238 self
.body
.append(node
.get('delimiter', ' '))
1239 self
.body
.append(self
.starttag(node
, 'var', ''))
1241 def depart_option_argument(self
, node
):
1242 self
.body
.append('</var>')
1244 def visit_option_group(self
, node
):
1246 if ( self
.settings
.option_limit
1247 and len(node
.astext()) > self
.settings
.option_limit
):
1249 self
.context
.append('</tr>\n<tr><td> </td>')
1251 self
.context
.append('')
1253 self
.starttag(node
, 'td', CLASS
='option-group', **atts
))
1254 self
.body
.append('<kbd>')
1255 self
.context
.append(0) # count number of options
1257 def depart_option_group(self
, node
):
1259 self
.body
.append('</kbd></td>\n')
1260 self
.body
.append(self
.context
.pop())
1262 def visit_option_list(self
, node
):
1264 self
.starttag(node
, 'table', CLASS
='docutils option-list',
1265 frame
="void", rules
="none"))
1266 self
.body
.append('<col class="option" />\n'
1267 '<col class="description" />\n'
1268 '<tbody valign="top">\n')
1270 def depart_option_list(self
, node
):
1271 self
.body
.append('</tbody>\n</table>\n')
1273 def visit_option_list_item(self
, node
):
1274 self
.body
.append(self
.starttag(node
, 'tr', ''))
1276 def depart_option_list_item(self
, node
):
1277 self
.body
.append('</tr>\n')
1279 def visit_option_string(self
, node
):
1282 def depart_option_string(self
, node
):
1285 def visit_organization(self
, node
):
1286 self
.visit_docinfo_item(node
, 'organization')
1288 def depart_organization(self
, node
):
1289 self
.depart_docinfo_item()
1291 def should_be_compact_paragraph(self
, node
):
1293 Determine if the <p> tags around paragraph ``node`` can be omitted.
1295 if (isinstance(node
.parent
, nodes
.document
) or
1296 isinstance(node
.parent
, nodes
.compound
)):
1297 # Never compact paragraphs in document or compound.
1299 for key
, value
in node
.attlist():
1300 if (node
.is_not_default(key
) and
1301 not (key
== 'classes' and value
in
1302 ([], ['first'], ['last'], ['first', 'last']))):
1303 # Attribute which needs to survive.
1305 first
= isinstance(node
.parent
[0], nodes
.label
) # skip label
1306 for child
in node
.parent
.children
[first
:]:
1307 # only first paragraph can be compact
1308 if isinstance(child
, nodes
.Invisible
):
1313 parent_length
= len([n
for n
in node
.parent
if not isinstance(
1314 n
, (nodes
.Invisible
, nodes
.label
))])
1315 if ( self
.compact_simple
1316 or self
.compact_field_list
1317 or self
.compact_p
and parent_length
== 1):
1321 def visit_paragraph(self
, node
):
1322 if self
.should_be_compact_paragraph(node
):
1323 self
.context
.append('')
1325 self
.body
.append(self
.starttag(node
, 'p', ''))
1326 self
.context
.append('</p>\n')
1328 def depart_paragraph(self
, node
):
1329 self
.body
.append(self
.context
.pop())
1331 def visit_problematic(self
, node
):
1332 if node
.hasattr('refid'):
1333 self
.body
.append('<a href="#%s">' % node
['refid'])
1334 self
.context
.append('</a>')
1336 self
.context
.append('')
1337 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1339 def depart_problematic(self
, node
):
1340 self
.body
.append('</span>')
1341 self
.body
.append(self
.context
.pop())
1343 def visit_raw(self
, node
):
1344 if 'html' in node
.get('format', '').split():
1345 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1347 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1348 self
.body
.append(node
.astext())
1350 self
.body
.append('</%s>' % t
)
1351 # Keep non-HTML raw text out of output:
1352 raise nodes
.SkipNode
1354 def visit_reference(self
, node
):
1355 atts
= {'class': 'reference'}
1356 if 'refuri' in node
:
1357 atts
['href'] = node
['refuri']
1358 if ( self
.settings
.cloak_email_addresses
1359 and atts
['href'].startswith('mailto:')):
1360 atts
['href'] = self
.cloak_mailto(atts
['href'])
1362 atts
['class'] += ' external'
1364 assert 'refid' in node
, \
1365 'References must have "refuri" or "refid" attribute.'
1366 atts
['href'] = '#' + node
['refid']
1367 atts
['class'] += ' internal'
1368 if not isinstance(node
.parent
, nodes
.TextElement
):
1369 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1370 atts
['class'] += ' image-reference'
1371 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1373 def depart_reference(self
, node
):
1374 self
.body
.append('</a>')
1375 if not isinstance(node
.parent
, nodes
.TextElement
):
1376 self
.body
.append('\n')
1379 def visit_revision(self
, node
):
1380 self
.visit_docinfo_item(node
, 'revision', meta
=None)
1382 def depart_revision(self
, node
):
1383 self
.depart_docinfo_item()
1385 def visit_row(self
, node
):
1386 self
.body
.append(self
.starttag(node
, 'tr', ''))
1389 def depart_row(self
, node
):
1390 self
.body
.append('</tr>\n')
1392 def visit_rubric(self
, node
):
1393 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1395 def depart_rubric(self
, node
):
1396 self
.body
.append('</p>\n')
1398 def visit_section(self
, node
):
1399 self
.section_level
+= 1
1401 self
.starttag(node
, 'div', CLASS
='section'))
1403 def depart_section(self
, node
):
1404 self
.section_level
-= 1
1405 self
.body
.append('</div>\n')
1407 def visit_sidebar(self
, node
):
1409 self
.starttag(node
, 'div', CLASS
='sidebar'))
1410 self
.set_first_last(node
)
1413 def depart_sidebar(self
, node
):
1414 self
.body
.append('</div>\n')
1415 self
.in_sidebar
= None
1417 def visit_status(self
, node
):
1418 self
.visit_docinfo_item(node
, 'status', meta
=None)
1420 def depart_status(self
, node
):
1421 self
.depart_docinfo_item()
1423 def visit_strong(self
, node
):
1424 self
.body
.append(self
.starttag(node
, 'strong', ''))
1426 def depart_strong(self
, node
):
1427 self
.body
.append('</strong>')
1429 def visit_subscript(self
, node
):
1430 self
.body
.append(self
.starttag(node
, 'sub', ''))
1432 def depart_subscript(self
, node
):
1433 self
.body
.append('</sub>')
1435 def visit_substitution_definition(self
, node
):
1436 """Internal only."""
1437 raise nodes
.SkipNode
1439 def visit_substitution_reference(self
, node
):
1440 self
.unimplemented_visit(node
)
1442 def visit_subtitle(self
, node
):
1443 if isinstance(node
.parent
, nodes
.sidebar
):
1444 self
.body
.append(self
.starttag(node
, 'p', '',
1445 CLASS
='sidebar-subtitle'))
1446 self
.context
.append('</p>\n')
1447 elif isinstance(node
.parent
, nodes
.document
):
1448 self
.body
.append(self
.starttag(node
, 'h2', '', CLASS
='subtitle'))
1449 self
.context
.append('</h2>\n')
1450 self
.in_document_title
= len(self
.body
)
1451 elif isinstance(node
.parent
, nodes
.section
):
1452 tag
= 'h%s' % (self
.section_level
+ self
.initial_header_level
- 1)
1454 self
.starttag(node
, tag
, '', CLASS
='section-subtitle') +
1455 self
.starttag({}, 'span', '', CLASS
='section-subtitle'))
1456 self
.context
.append('</span></%s>\n' % tag
)
1458 def depart_subtitle(self
, node
):
1459 self
.body
.append(self
.context
.pop())
1460 if self
.in_document_title
:
1461 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1462 self
.in_document_title
= 0
1463 self
.body_pre_docinfo
.extend(self
.body
)
1464 self
.html_subtitle
.extend(self
.body
)
1467 def visit_superscript(self
, node
):
1468 self
.body
.append(self
.starttag(node
, 'sup', ''))
1470 def depart_superscript(self
, node
):
1471 self
.body
.append('</sup>')
1473 def visit_system_message(self
, node
):
1474 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1475 self
.body
.append('<p class="system-message-title">')
1477 if len(node
['backrefs']):
1478 backrefs
= node
['backrefs']
1479 if len(backrefs
) == 1:
1480 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1485 for backref
in backrefs
:
1486 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1488 backref_text
= ('; <em>backlinks: %s</em>'
1489 % ', '.join(backlinks
))
1490 if node
.hasattr('line'):
1491 line
= ', line %s' % node
['line']
1494 self
.body
.append('System Message: %s/%s '
1495 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1496 % (node
['type'], node
['level'],
1497 self
.encode(node
['source']), line
, backref_text
))
1499 def depart_system_message(self
, node
):
1500 self
.body
.append('</div>\n')
1502 def visit_table(self
, node
):
1503 classes
= ' '.join(['docutils', self
.settings
.table_style
]).strip()
1505 self
.starttag(node
, 'table', CLASS
=classes
, border
="1"))
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 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', valign
='top'))
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', valign
='bottom'))
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."""
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 def visit_topic(self
, node
):
1616 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1617 self
.topic_classes
= node
['classes']
1619 def depart_topic(self
, node
):
1620 self
.body
.append('</div>\n')
1621 self
.topic_classes
= []
1623 def visit_transition(self
, node
):
1624 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1626 def depart_transition(self
, node
):
1629 def visit_version(self
, node
):
1630 self
.visit_docinfo_item(node
, 'version', meta
=None)
1632 def depart_version(self
, node
):
1633 self
.depart_docinfo_item()
1635 def unimplemented_visit(self
, node
):
1636 raise NotImplementedError('visiting unimplemented node type: %s'
1637 % node
.__class
__.__name
__)
1640 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1643 Raise `nodes.NodeFound` if non-simple list item is encountered.
1645 Here "simple" means a list item containing nothing other than a single
1646 paragraph, a simple list, or a paragraph followed by a simple list.
1649 def default_visit(self
, node
):
1650 raise nodes
.NodeFound
1652 def visit_bullet_list(self
, node
):
1655 def visit_enumerated_list(self
, node
):
1658 def visit_list_item(self
, node
):
1660 for child
in node
.children
:
1661 if not isinstance(child
, nodes
.Invisible
):
1662 children
.append(child
)
1663 if (children
and isinstance(children
[0], nodes
.paragraph
)
1664 and (isinstance(children
[-1], nodes
.bullet_list
)
1665 or isinstance(children
[-1], nodes
.enumerated_list
))):
1667 if len(children
) <= 1:
1670 raise nodes
.NodeFound
1672 def visit_paragraph(self
, node
):
1673 raise nodes
.SkipNode
1675 def invisible_visit(self
, node
):
1676 """Invisible nodes should be ignored."""
1677 raise nodes
.SkipNode
1679 visit_comment
= invisible_visit
1680 visit_substitution_definition
= invisible_visit
1681 visit_target
= invisible_visit
1682 visit_pending
= invisible_visit