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 import Image
# check for the Python Imaging Library
27 from docutils
import frontend
, nodes
, utils
, writers
, languages
, io
28 from docutils
.transforms
import writer_aux
29 from docutils
.math
import unichar2tex
, pick_math_environment
30 from docutils
.math
.latex2mathml
import parse_latex_math
31 from docutils
.math
.math2html
import math2html
33 class Writer(writers
.Writer
):
35 supported
= ('html', 'html4css1', 'xhtml')
36 """Formats this writer supports."""
38 default_stylesheet
= 'html4css1.css'
40 default_stylesheet_path
= utils
.relative_path(
41 os
.path
.join(os
.getcwd(), 'dummy'),
42 os
.path
.join(os
.path
.dirname(__file__
), default_stylesheet
))
44 default_template
= 'template.txt'
46 default_template_path
= utils
.relative_path(
47 os
.path
.join(os
.getcwd(), 'dummy'),
48 os
.path
.join(os
.path
.dirname(__file__
), default_template
))
51 'HTML-Specific Options',
53 (('Specify the template file (UTF-8 encoded). Default is "%s".'
54 % default_template_path
,
56 {'default': default_template_path
, 'metavar': '<file>'}),
57 ('Specify comma separated list of stylesheet URLs. '
58 'Overrides previous --stylesheet and --stylesheet-path settings.',
60 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}),
61 ('Specify comma separated list of stylesheet paths. '
62 'With --link-stylesheet, '
63 'the path is rewritten relative to the output HTML file. '
64 'Default: "%s"' % default_stylesheet_path
,
65 ['--stylesheet-path'],
66 {'metavar': '<file>', 'overrides': 'stylesheet',
67 'default': default_stylesheet_path
}),
68 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
69 'files must be accessible during processing. This is the default.',
70 ['--embed-stylesheet'],
71 {'default': 1, 'action': 'store_true',
72 'validator': frontend
.validate_boolean
}),
73 ('Link to the stylesheet(s) in the output HTML file. '
74 'Default: embed stylesheets.',
75 ['--link-stylesheet'],
76 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
77 ('Specify the initial header level. Default is 1 for "<h1>". '
78 'Does not affect document title & subtitle (see --no-doc-title).',
79 ['--initial-header-level'],
80 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
81 'metavar': '<level>'}),
82 ('Specify the maximum width (in characters) for one-column field '
83 'names. Longer field names will span an entire row of the table '
84 'used to render the field list. Default is 14 characters. '
85 'Use 0 for "no limit".',
86 ['--field-name-limit'],
87 {'default': 14, 'metavar': '<level>',
88 'validator': frontend
.validate_nonnegative_int
}),
89 ('Specify the maximum width (in characters) for options in option '
90 'lists. Longer options will span an entire row of the table used '
91 'to render the option list. Default is 14 characters. '
92 'Use 0 for "no limit".',
94 {'default': 14, 'metavar': '<level>',
95 'validator': frontend
.validate_nonnegative_int
}),
96 ('Format for footnote references: one of "superscript" or '
97 '"brackets". Default is "brackets".',
98 ['--footnote-references'],
99 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
100 'metavar': '<format>',
101 'overrides': 'trim_footnote_reference_space'}),
102 ('Format for block quote attributions: one of "dash" (em-dash '
103 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
105 {'choices': ['dash', 'parentheses', 'parens', 'none'],
106 'default': 'dash', 'metavar': '<format>'}),
107 ('Remove extra vertical whitespace between items of "simple" bullet '
108 'lists and enumerated lists. Default: enabled.',
110 {'default': 1, 'action': 'store_true',
111 'validator': frontend
.validate_boolean
}),
112 ('Disable compact simple bullet and enumerated lists.',
113 ['--no-compact-lists'],
114 {'dest': 'compact_lists', 'action': 'store_false'}),
115 ('Remove extra vertical whitespace between items of simple field '
116 'lists. Default: enabled.',
117 ['--compact-field-lists'],
118 {'default': 1, 'action': 'store_true',
119 'validator': frontend
.validate_boolean
}),
120 ('Disable compact simple field lists.',
121 ['--no-compact-field-lists'],
122 {'dest': 'compact_field_lists', 'action': 'store_false'}),
123 ('Added to standard table classes. '
124 'Defined styles: "borderless". Default: ""',
127 ('Math output format, one of "MathML", "HTML", "MathJax" '
128 'or "LaTeX". Default: "MathJax"',
130 {'default': 'MathJax'}),
131 ('Omit the XML declaration. Use with caution.',
132 ['--no-xml-declaration'],
133 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
134 'validator': frontend
.validate_boolean
}),
135 ('Obfuscate email addresses to confuse harvesters while still '
136 'keeping email links usable with standards-compliant browsers.',
137 ['--cloak-email-addresses'],
138 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
140 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
142 relative_path_settings
= ('stylesheet_path',)
144 config_section
= 'html4css1 writer'
145 config_section_dependencies
= ('writers',)
147 visitor_attributes
= (
148 'head_prefix', 'head', 'stylesheet', 'body_prefix',
149 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
150 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
151 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
154 def get_transforms(self
):
155 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
158 writers
.Writer
.__init
__(self
)
159 self
.translator_class
= HTMLTranslator
162 self
.visitor
= visitor
= self
.translator_class(self
.document
)
163 self
.document
.walkabout(visitor
)
164 for attr
in self
.visitor_attributes
:
165 setattr(self
, attr
, getattr(visitor
, attr
))
166 self
.output
= self
.apply_template()
168 def apply_template(self
):
169 template_file
= open(self
.document
.settings
.template
, 'rb')
170 template
= unicode(template_file
.read(), 'utf-8')
171 template_file
.close()
172 subs
= self
.interpolation_dict()
173 return template
% subs
175 def interpolation_dict(self
):
177 settings
= self
.document
.settings
178 for attr
in self
.visitor_attributes
:
179 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
180 subs
['encoding'] = settings
.output_encoding
181 subs
['version'] = docutils
.__version
__
184 def assemble_parts(self
):
185 writers
.Writer
.assemble_parts(self
)
186 for part
in self
.visitor_attributes
:
187 self
.parts
[part
] = ''.join(getattr(self
, part
))
190 class HTMLTranslator(nodes
.NodeVisitor
):
193 This HTML writer has been optimized to produce visually compact
194 lists (less vertical whitespace). HTML's mixed content models
195 allow list items to contain "<li><p>body elements</p></li>" or
196 "<li>just text</li>" or even "<li>text<p>and body
197 elements</p>combined</li>", each with different effects. It would
198 be best to stick with strict body elements in list items, but they
199 affect vertical spacing in browsers (although they really
202 Here is an outline of the optimization:
204 - Check for and omit <p> tags in "simple" lists: list items
205 contain either a single paragraph, a nested simple list, or a
206 paragraph followed by a nested simple list. This means that
207 this list can be compact:
212 But this list cannot be compact:
216 This second paragraph forces space between list items.
220 - In non-list contexts, omit <p> tags on a paragraph if that
221 paragraph is the only child of its parent (footnotes & citations
222 are allowed a label first).
224 - Regardless of the above, in definitions, table cells, field bodies,
225 option descriptions, and list items, mark the first child with
226 'class="first"' and the last child with 'class="last"'. The stylesheet
227 sets the margins (top & bottom respectively) to 0 for these elements.
229 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
230 option) disables list whitespace optimization.
233 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
235 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
236 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
237 doctype_mathml
= doctype
239 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
240 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
241 content_type
= ('<meta http-equiv="Content-Type"'
242 ' content="text/html; charset=%s" />\n')
243 content_type_mathml
= ('<meta http-equiv="Content-Type"'
244 ' content="application/xhtml+xml; charset=%s" />\n')
246 generator
= ('<meta name="generator" content="Docutils %s: '
247 'http://docutils.sourceforge.net/" />\n')
249 # Template for the MathJax script in the header:
250 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
251 # The latest version of MathJax from the distributed server:
252 # avaliable to the public under the `MathJax CDN Terms of Service`__
253 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
254 mathjax_url
= ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
255 'config=TeX-AMS-MML_HTMLorMML')
256 # TODO: make this configurable:
258 # a) as extra option or
259 # b) appended to math-output="MathJax"?
261 # If b), which delimiter/delimter-set (':', ',', ' ')?
263 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
264 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
265 words_and_spaces
= re
.compile(r
'\S+| +|\n')
266 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
267 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
269 def __init__(self
, document
):
270 nodes
.NodeVisitor
.__init
__(self
, document
)
271 self
.settings
= settings
= document
.settings
272 lcode
= settings
.language_code
273 self
.language
= languages
.get_language(lcode
, document
.reporter
)
274 self
.meta
= [self
.generator
% docutils
.__version
__]
275 self
.head_prefix
= []
276 self
.html_prolog
= []
277 if settings
.xml_declaration
:
278 self
.head_prefix
.append(self
.xml_declaration
279 % settings
.output_encoding
)
280 # encoding not interpolated:
281 self
.html_prolog
.append(self
.xml_declaration
)
282 self
.head
= self
.meta
[:]
284 styles
= utils
.get_stylesheet_list(settings
)
285 if settings
.stylesheet_path
and not(settings
.embed_stylesheet
):
286 styles
= [utils
.relative_path(settings
._destination
, sheet
)
288 if settings
.embed_stylesheet
:
289 settings
.record_dependencies
.add(*styles
)
290 self
.stylesheet
= [self
.embedded_stylesheet
%
291 io
.FileInput(source_path
=sheet
, encoding
='utf-8').read()
293 else: # link to stylesheets
294 self
.stylesheet
= [self
.stylesheet_link
% self
.encode(stylesheet
)
295 for stylesheet
in styles
]
296 self
.body_prefix
= ['</head>\n<body>\n']
297 # document title, subtitle display
298 self
.body_pre_docinfo
= []
303 self
.body_suffix
= ['</body>\n</html>\n']
304 self
.section_level
= 0
305 self
.initial_header_level
= int(settings
.initial_header_level
)
306 self
.math_output
= settings
.math_output
.lower()
307 # A heterogenous stack used in conjunction with the tree traversal.
308 # Make sure that the pops correspond to the pushes:
310 self
.topic_classes
= []
313 self
.compact_simple
= None
314 self
.compact_field_list
= None
315 self
.in_docinfo
= None
316 self
.in_sidebar
= None
321 self
.html_head
= [self
.content_type
] # charset not interpolated
323 self
.html_subtitle
= []
325 self
.in_document_title
= 0
327 self
.author_in_authors
= None
328 self
.math_header
= ''
331 return ''.join(self
.head_prefix
+ self
.head
332 + self
.stylesheet
+ self
.body_prefix
333 + self
.body_pre_docinfo
+ self
.docinfo
334 + self
.body
+ self
.body_suffix
)
336 def encode(self
, text
):
337 """Encode special characters in `text` & return."""
338 # @@@ A codec to do these and all other HTML entities would be nice.
340 return text
.translate({
345 ord('@'): u
'@', # may thwart some address harvesters
346 # TODO: convert non-breaking space only if needed?
347 0xa0: u
' '}) # non-breaking space
349 def cloak_mailto(self
, uri
):
350 """Try to hide a mailto: URL from harvesters."""
351 # Encode "@" using a URL octet reference (see RFC 1738).
352 # Further cloaking with HTML entities will be done in the
354 return uri
.replace('@', '%40')
356 def cloak_email(self
, addr
):
357 """Try to hide the link text of a email link from harversters."""
358 # Surround at-signs and periods with <span> tags. ("@" has
359 # already been encoded to "@" by the `encode` method.)
360 addr
= addr
.replace('@', '<span>@</span>')
361 addr
= addr
.replace('.', '<span>.</span>')
364 def attval(self
, text
,
365 whitespace
=re
.compile('[\n\r\t\v\f]')):
366 """Cleanse, HTML encode, and return attribute value text."""
367 encoded
= self
.encode(whitespace
.sub(' ', text
))
368 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
369 # Cloak at-signs ("%40") and periods with HTML entities.
370 encoded
= encoded
.replace('%40', '%40')
371 encoded
= encoded
.replace('.', '.')
374 def starttag(self
, node
, tagname
, suffix
='\n', empty
=0, **attributes
):
376 Construct and return a start tag given a node (id & class attributes
377 are extracted), tag name, and optional attributes.
379 tagname
= tagname
.lower()
383 for (name
, value
) in attributes
.items():
384 atts
[name
.lower()] = value
385 classes
= node
.get('classes', [])
387 classes
.append(atts
.pop('class'))
388 # move language specification to 'lang' attribute
389 languages
= [cls
for cls
in classes
390 if cls
.startswith('language-')]
392 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
393 atts
[self
.lang_attribute
] = languages
[0][9:]
394 classes
.pop(classes
.index(languages
[0]))
395 classes
= ' '.join(classes
).strip()
397 atts
['class'] = classes
398 assert 'id' not in atts
399 ids
.extend(node
.get('ids', []))
401 ids
.extend(atts
['ids'])
406 # Add empty "span" elements for additional IDs. Note
407 # that we cannot use empty "a" elements because there
408 # may be targets inside of references, but nested "a"
409 # elements aren't allowed in XHTML (even if they do
410 # not all have a "href" attribute).
412 # Empty tag. Insert target right in front of element.
413 prefix
.append('<span id="%s"></span>' % id)
415 # Non-empty tag. Place the auxiliary <span> tag
416 # *inside* the element, as the first child.
417 suffix
+= '<span id="%s"></span>' % id
418 attlist
= atts
.items()
421 for name
, value
in attlist
:
422 # value=None was used for boolean attributes without
423 # value, but this isn't supported by XHTML.
424 assert value
is not None
425 if isinstance(value
, list):
426 values
= [unicode(v
) for v
in value
]
427 parts
.append('%s="%s"' % (name
.lower(),
428 self
.attval(' '.join(values
))))
430 parts
.append('%s="%s"' % (name
.lower(),
431 self
.attval(unicode(value
))))
436 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
438 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
439 """Construct and return an XML-compatible empty tag."""
440 return self
.starttag(node
, tagname
, suffix
, empty
=1, **attributes
)
442 def set_class_on_child(self
, node
, class_
, index
=0):
444 Set class `class_` on the visible child no. index of `node`.
445 Do nothing if node has fewer children than `index`.
447 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
449 child
= children
[index
]
452 child
['classes'].append(class_
)
454 def set_first_last(self
, node
):
455 self
.set_class_on_child(node
, 'first', 0)
456 self
.set_class_on_child(node
, 'last', -1)
458 def visit_Text(self
, node
):
460 encoded
= self
.encode(text
)
461 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
462 encoded
= self
.cloak_email(encoded
)
463 self
.body
.append(encoded
)
465 def depart_Text(self
, node
):
468 def visit_abbreviation(self
, node
):
469 # @@@ implementation incomplete ("title" attribute)
470 self
.body
.append(self
.starttag(node
, 'abbr', ''))
472 def depart_abbreviation(self
, node
):
473 self
.body
.append('</abbr>')
475 def visit_acronym(self
, node
):
476 # @@@ implementation incomplete ("title" attribute)
477 self
.body
.append(self
.starttag(node
, 'acronym', ''))
479 def depart_acronym(self
, node
):
480 self
.body
.append('</acronym>')
482 def visit_address(self
, node
):
483 self
.visit_docinfo_item(node
, 'address', meta
=None)
484 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='address'))
486 def depart_address(self
, node
):
487 self
.body
.append('\n</pre>\n')
488 self
.depart_docinfo_item()
490 def visit_admonition(self
, node
):
491 self
.body
.append(self
.starttag(node
, 'div'))
492 self
.set_first_last(node
)
494 def depart_admonition(self
, node
=None):
495 self
.body
.append('</div>\n')
497 attribution_formats
= {'dash': ('—', ''),
498 'parentheses': ('(', ')'),
499 'parens': ('(', ')'),
502 def visit_attribution(self
, node
):
503 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
504 self
.context
.append(suffix
)
506 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
508 def depart_attribution(self
, node
):
509 self
.body
.append(self
.context
.pop() + '</p>\n')
511 def visit_author(self
, node
):
512 if isinstance(node
.parent
, nodes
.authors
):
513 if self
.author_in_authors
:
514 self
.body
.append('\n<br />')
516 self
.visit_docinfo_item(node
, 'author')
518 def depart_author(self
, node
):
519 if isinstance(node
.parent
, nodes
.authors
):
520 self
.author_in_authors
+= 1
522 self
.depart_docinfo_item()
524 def visit_authors(self
, node
):
525 self
.visit_docinfo_item(node
, 'authors')
526 self
.author_in_authors
= 0 # initialize counter
528 def depart_authors(self
, node
):
529 self
.depart_docinfo_item()
530 self
.author_in_authors
= None
532 def visit_block_quote(self
, node
):
533 self
.body
.append(self
.starttag(node
, 'blockquote'))
535 def depart_block_quote(self
, node
):
536 self
.body
.append('</blockquote>\n')
538 def check_simple_list(self
, node
):
539 """Check for a simple list that can be rendered compactly."""
540 visitor
= SimpleListChecker(self
.document
)
543 except nodes
.NodeFound
:
548 def is_compactable(self
, node
):
549 return ('compact' in node
['classes']
550 or (self
.settings
.compact_lists
551 and 'open' not in node
['classes']
552 and (self
.compact_simple
553 or self
.topic_classes
== ['contents']
554 or self
.check_simple_list(node
))))
556 def visit_bullet_list(self
, node
):
558 old_compact_simple
= self
.compact_simple
559 self
.context
.append((self
.compact_simple
, self
.compact_p
))
560 self
.compact_p
= None
561 self
.compact_simple
= self
.is_compactable(node
)
562 if self
.compact_simple
and not old_compact_simple
:
563 atts
['class'] = 'simple'
564 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
566 def depart_bullet_list(self
, node
):
567 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
568 self
.body
.append('</ul>\n')
570 def visit_caption(self
, node
):
571 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
573 def depart_caption(self
, node
):
574 self
.body
.append('</p>\n')
576 def visit_citation(self
, node
):
577 self
.body
.append(self
.starttag(node
, 'table',
578 CLASS
='docutils citation',
579 frame
="void", rules
="none"))
580 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
581 '<tbody valign="top">\n'
583 self
.footnote_backrefs(node
)
585 def depart_citation(self
, node
):
586 self
.body
.append('</td></tr>\n'
587 '</tbody>\n</table>\n')
589 def visit_citation_reference(self
, node
):
590 href
= '#' + node
['refid']
591 self
.body
.append(self
.starttag(
592 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
594 def depart_citation_reference(self
, node
):
595 self
.body
.append(']</a>')
597 def visit_classifier(self
, node
):
598 self
.body
.append(' <span class="classifier-delimiter">:</span> ')
599 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
601 def depart_classifier(self
, node
):
602 self
.body
.append('</span>')
604 def visit_colspec(self
, node
):
605 self
.colspecs
.append(node
)
606 # "stubs" list is an attribute of the tgroup element:
607 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
609 def depart_colspec(self
, node
):
612 def write_colspecs(self
):
614 for node
in self
.colspecs
:
615 width
+= node
['colwidth']
616 for node
in self
.colspecs
:
617 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
618 self
.body
.append(self
.emptytag(node
, 'col',
619 width
='%i%%' % colwidth
))
622 def visit_comment(self
, node
,
623 sub
=re
.compile('-(?=-)').sub
):
624 """Escape double-dashes in comment text."""
625 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
626 # Content already processed:
629 def visit_compound(self
, node
):
630 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
632 node
[0]['classes'].append('compound-first')
633 node
[-1]['classes'].append('compound-last')
634 for child
in node
[1:-1]:
635 child
['classes'].append('compound-middle')
637 def depart_compound(self
, node
):
638 self
.body
.append('</div>\n')
640 def visit_container(self
, node
):
641 self
.body
.append(self
.starttag(node
, 'div', CLASS
='container'))
643 def depart_container(self
, node
):
644 self
.body
.append('</div>\n')
646 def visit_contact(self
, node
):
647 self
.visit_docinfo_item(node
, 'contact', meta
=None)
649 def depart_contact(self
, node
):
650 self
.depart_docinfo_item()
652 def visit_copyright(self
, node
):
653 self
.visit_docinfo_item(node
, 'copyright')
655 def depart_copyright(self
, node
):
656 self
.depart_docinfo_item()
658 def visit_date(self
, node
):
659 self
.visit_docinfo_item(node
, 'date')
661 def depart_date(self
, node
):
662 self
.depart_docinfo_item()
664 def visit_decoration(self
, node
):
667 def depart_decoration(self
, node
):
670 def visit_definition(self
, node
):
671 self
.body
.append('</dt>\n')
672 self
.body
.append(self
.starttag(node
, 'dd', ''))
673 self
.set_first_last(node
)
675 def depart_definition(self
, node
):
676 self
.body
.append('</dd>\n')
678 def visit_definition_list(self
, node
):
679 self
.body
.append(self
.starttag(node
, 'dl', CLASS
='docutils'))
681 def depart_definition_list(self
, node
):
682 self
.body
.append('</dl>\n')
684 def visit_definition_list_item(self
, node
):
687 def depart_definition_list_item(self
, node
):
690 def visit_description(self
, node
):
691 self
.body
.append(self
.starttag(node
, 'td', ''))
692 self
.set_first_last(node
)
694 def depart_description(self
, node
):
695 self
.body
.append('</td>')
697 def visit_docinfo(self
, node
):
698 self
.context
.append(len(self
.body
))
699 self
.body
.append(self
.starttag(node
, 'table',
701 frame
="void", rules
="none"))
702 self
.body
.append('<col class="docinfo-name" />\n'
703 '<col class="docinfo-content" />\n'
704 '<tbody valign="top">\n')
707 def depart_docinfo(self
, node
):
708 self
.body
.append('</tbody>\n</table>\n')
709 self
.in_docinfo
= None
710 start
= self
.context
.pop()
711 self
.docinfo
= self
.body
[start
:]
714 def visit_docinfo_item(self
, node
, name
, meta
=1):
716 meta_tag
= '<meta name="%s" content="%s" />\n' \
717 % (name
, self
.attval(node
.astext()))
718 self
.add_meta(meta_tag
)
719 self
.body
.append(self
.starttag(node
, 'tr', ''))
720 self
.body
.append('<th class="docinfo-name">%s:</th>\n<td>'
721 % self
.language
.labels
[name
])
723 if isinstance(node
[0], nodes
.Element
):
724 node
[0]['classes'].append('first')
725 if isinstance(node
[-1], nodes
.Element
):
726 node
[-1]['classes'].append('last')
728 def depart_docinfo_item(self
):
729 self
.body
.append('</td></tr>\n')
731 def visit_doctest_block(self
, node
):
732 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='doctest-block'))
734 def depart_doctest_block(self
, node
):
735 self
.body
.append('\n</pre>\n')
737 def visit_document(self
, node
):
738 self
.head
.append('<title>%s</title>\n'
739 % self
.encode(node
.get('title', '')))
741 def depart_document(self
, node
):
742 self
.head_prefix
.extend([self
.doctype
,
743 self
.head_prefix_template
%
744 {'lang': self
.settings
.language_code
}])
745 self
.html_prolog
.append(self
.doctype
)
746 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
747 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
749 self
.head
.append(self
.math_header
)
750 # skip content-type meta tag with interpolated charset value:
751 self
.html_head
.extend(self
.head
[1:])
752 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
753 self
.body_suffix
.insert(0, '</div>\n')
754 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
755 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
756 + self
.docinfo
+ self
.body
757 + self
.body_suffix
[:-1])
758 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
760 def visit_emphasis(self
, node
):
761 self
.body
.append(self
.starttag(node
, 'em', ''))
763 def depart_emphasis(self
, node
):
764 self
.body
.append('</em>')
766 def visit_entry(self
, node
):
768 if isinstance(node
.parent
.parent
, nodes
.thead
):
769 atts
['class'].append('head')
770 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
771 # "stubs" list is an attribute of the tgroup element
772 atts
['class'].append('stub')
775 atts
['class'] = ' '.join(atts
['class'])
779 node
.parent
.column
+= 1
780 if 'morerows' in node
:
781 atts
['rowspan'] = node
['morerows'] + 1
782 if 'morecols' in node
:
783 atts
['colspan'] = node
['morecols'] + 1
784 node
.parent
.column
+= node
['morecols']
785 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
786 self
.context
.append('</%s>\n' % tagname
.lower())
787 if len(node
) == 0: # empty cell
788 self
.body
.append(' ')
789 self
.set_first_last(node
)
791 def depart_entry(self
, node
):
792 self
.body
.append(self
.context
.pop())
794 def visit_enumerated_list(self
, node
):
796 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
797 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
802 atts
['start'] = node
['start']
803 if 'enumtype' in node
:
804 atts
['class'] = node
['enumtype']
805 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
806 # single "format" attribute? Use CSS2?
807 old_compact_simple
= self
.compact_simple
808 self
.context
.append((self
.compact_simple
, self
.compact_p
))
809 self
.compact_p
= None
810 self
.compact_simple
= self
.is_compactable(node
)
811 if self
.compact_simple
and not old_compact_simple
:
812 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
813 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
815 def depart_enumerated_list(self
, node
):
816 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
817 self
.body
.append('</ol>\n')
819 def visit_field(self
, node
):
820 self
.body
.append(self
.starttag(node
, 'tr', '', CLASS
='field'))
822 def depart_field(self
, node
):
823 self
.body
.append('</tr>\n')
825 def visit_field_body(self
, node
):
826 self
.body
.append(self
.starttag(node
, 'td', '', CLASS
='field-body'))
827 self
.set_class_on_child(node
, 'first', 0)
829 if (self
.compact_field_list
or
830 isinstance(field
.parent
, nodes
.docinfo
) or
831 field
.parent
.index(field
) == len(field
.parent
) - 1):
832 # If we are in a compact list, the docinfo, or if this is
833 # the last field of the field list, do not add vertical
834 # space after last element.
835 self
.set_class_on_child(node
, 'last', -1)
837 def depart_field_body(self
, node
):
838 self
.body
.append('</td>\n')
840 def visit_field_list(self
, node
):
841 self
.context
.append((self
.compact_field_list
, self
.compact_p
))
842 self
.compact_p
= None
843 if 'compact' in node
['classes']:
844 self
.compact_field_list
= 1
845 elif (self
.settings
.compact_field_lists
846 and 'open' not in node
['classes']):
847 self
.compact_field_list
= 1
848 if self
.compact_field_list
:
850 field_body
= field
[-1]
851 assert isinstance(field_body
, nodes
.field_body
)
852 children
= [n
for n
in field_body
853 if not isinstance(n
, nodes
.Invisible
)]
854 if not (len(children
) == 0 or
855 len(children
) == 1 and
856 isinstance(children
[0],
857 (nodes
.paragraph
, nodes
.line_block
))):
858 self
.compact_field_list
= 0
860 self
.body
.append(self
.starttag(node
, 'table', frame
='void',
862 CLASS
='docutils field-list'))
863 self
.body
.append('<col class="field-name" />\n'
864 '<col class="field-body" />\n'
865 '<tbody valign="top">\n')
867 def depart_field_list(self
, node
):
868 self
.body
.append('</tbody>\n</table>\n')
869 self
.compact_field_list
, self
.compact_p
= self
.context
.pop()
871 def visit_field_name(self
, node
):
874 atts
['class'] = 'docinfo-name'
876 atts
['class'] = 'field-name'
877 if ( self
.settings
.field_name_limit
878 and len(node
.astext()) > self
.settings
.field_name_limit
):
880 self
.context
.append('</tr>\n'
881 + self
.starttag(node
.parent
, 'tr', '')
884 self
.context
.append('')
885 self
.body
.append(self
.starttag(node
, 'th', '', **atts
))
887 def depart_field_name(self
, node
):
888 self
.body
.append(':</th>')
889 self
.body
.append(self
.context
.pop())
891 def visit_figure(self
, node
):
892 atts
= {'class': 'figure'}
893 if node
.get('width'):
894 atts
['style'] = 'width: %s' % node
['width']
895 if node
.get('align'):
896 atts
['class'] += " align-" + node
['align']
897 self
.body
.append(self
.starttag(node
, 'div', **atts
))
899 def depart_figure(self
, node
):
900 self
.body
.append('</div>\n')
902 def visit_footer(self
, node
):
903 self
.context
.append(len(self
.body
))
905 def depart_footer(self
, node
):
906 start
= self
.context
.pop()
907 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
908 '<hr class="footer" />\n']
909 footer
.extend(self
.body
[start
:])
910 footer
.append('\n</div>\n')
911 self
.footer
.extend(footer
)
912 self
.body_suffix
[:0] = footer
913 del self
.body
[start
:]
915 def visit_footnote(self
, node
):
916 self
.body
.append(self
.starttag(node
, 'table',
917 CLASS
='docutils footnote',
918 frame
="void", rules
="none"))
919 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
920 '<tbody valign="top">\n'
922 self
.footnote_backrefs(node
)
924 def footnote_backrefs(self
, node
):
926 backrefs
= node
['backrefs']
927 if self
.settings
.footnote_backlinks
and backrefs
:
928 if len(backrefs
) == 1:
929 self
.context
.append('')
930 self
.context
.append('</a>')
931 self
.context
.append('<a class="fn-backref" href="#%s">'
935 for backref
in backrefs
:
936 backlinks
.append('<a class="fn-backref" href="#%s">%s</a>'
939 self
.context
.append('<em>(%s)</em> ' % ', '.join(backlinks
))
940 self
.context
+= ['', '']
942 self
.context
.append('')
943 self
.context
+= ['', '']
944 # If the node does not only consist of a label.
946 # If there are preceding backlinks, we do not set class
947 # 'first', because we need to retain the top-margin.
949 node
[1]['classes'].append('first')
950 node
[-1]['classes'].append('last')
952 def depart_footnote(self
, node
):
953 self
.body
.append('</td></tr>\n'
954 '</tbody>\n</table>\n')
956 def visit_footnote_reference(self
, node
):
957 href
= '#' + node
['refid']
958 format
= self
.settings
.footnote_references
959 if format
== 'brackets':
961 self
.context
.append(']')
963 assert format
== 'superscript'
965 self
.context
.append('</sup>')
966 self
.body
.append(self
.starttag(node
, 'a', suffix
,
967 CLASS
='footnote-reference', href
=href
))
969 def depart_footnote_reference(self
, node
):
970 self
.body
.append(self
.context
.pop() + '</a>')
972 def visit_generated(self
, node
):
975 def depart_generated(self
, node
):
978 def visit_header(self
, node
):
979 self
.context
.append(len(self
.body
))
981 def depart_header(self
, node
):
982 start
= self
.context
.pop()
983 header
= [self
.starttag(node
, 'div', CLASS
='header')]
984 header
.extend(self
.body
[start
:])
985 header
.append('\n<hr class="header"/>\n</div>\n')
986 self
.body_prefix
.extend(header
)
987 self
.header
.extend(header
)
988 del self
.body
[start
:]
990 def visit_image(self
, node
):
993 # place SVG and SWF images in an <object> element
994 types
= {'.svg': 'image/svg+xml',
995 '.swf': 'application/x-shockwave-flash'}
996 ext
= os
.path
.splitext(uri
)[1].lower()
997 if ext
in ('.svg', '.swf'):
999 atts
['type'] = types
[ext
]
1002 atts
['alt'] = node
.get('alt', uri
)
1005 atts
['width'] = node
['width']
1006 if 'height' in node
:
1007 atts
['height'] = node
['height']
1009 if Image
and not ('width' in node
and 'height' in node
):
1011 im
= Image
.open(str(uri
))
1012 except (IOError, # Source image can't be found or opened
1013 UnicodeError): # PIL doesn't like Unicode paths.
1016 if 'width' not in atts
:
1017 atts
['width'] = str(im
.size
[0])
1018 if 'height' not in atts
:
1019 atts
['height'] = str(im
.size
[1])
1021 for att_name
in 'width', 'height':
1022 if att_name
in atts
:
1023 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1025 atts
[att_name
] = '%s%s' % (
1026 float(match
.group(1)) * (float(node
['scale']) / 100),
1029 for att_name
in 'width', 'height':
1030 if att_name
in atts
:
1031 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1032 # Interpret unitless values as pixels.
1033 atts
[att_name
] += 'px'
1034 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1037 atts
['style'] = ' '.join(style
)
1038 if (isinstance(node
.parent
, nodes
.TextElement
) or
1039 (isinstance(node
.parent
, nodes
.reference
) and
1040 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1041 # Inline context or surrounded by <a>...</a>.
1046 atts
['class'] = 'align-%s' % node
['align']
1047 self
.context
.append('')
1048 if ext
in ('.svg', '.swf'): # place in an object element,
1049 # do NOT use an empty tag: incorrect rendering in browsers
1050 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1051 node
.get('alt', uri
) + '</object>' + suffix
)
1053 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1055 def depart_image(self
, node
):
1056 self
.body
.append(self
.context
.pop())
1058 def visit_inline(self
, node
):
1059 self
.body
.append(self
.starttag(node
, 'span', ''))
1061 def depart_inline(self
, node
):
1062 self
.body
.append('</span>')
1064 def visit_label(self
, node
):
1065 # Context added in footnote_backrefs.
1066 self
.body
.append(self
.starttag(node
, 'td', '%s[' % self
.context
.pop(),
1069 def depart_label(self
, node
):
1070 # Context added in footnote_backrefs.
1071 self
.body
.append(']%s</td><td>%s' % (self
.context
.pop(), self
.context
.pop()))
1073 def visit_legend(self
, node
):
1074 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1076 def depart_legend(self
, node
):
1077 self
.body
.append('</div>\n')
1079 def visit_line(self
, node
):
1080 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1082 self
.body
.append('<br />')
1084 def depart_line(self
, node
):
1085 self
.body
.append('</div>\n')
1087 def visit_line_block(self
, node
):
1088 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1090 def depart_line_block(self
, node
):
1091 self
.body
.append('</div>\n')
1093 def visit_list_item(self
, node
):
1094 self
.body
.append(self
.starttag(node
, 'li', ''))
1096 node
[0]['classes'].append('first')
1098 def depart_list_item(self
, node
):
1099 self
.body
.append('</li>\n')
1101 def visit_literal(self
, node
):
1102 """Process text to prevent tokens from wrapping."""
1104 self
.starttag(node
, 'tt', '', CLASS
='docutils literal'))
1105 text
= node
.astext()
1106 for token
in self
.words_and_spaces
.findall(text
):
1108 # Protect text like "--an-option" and the regular expression
1109 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1110 if self
.sollbruchstelle
.search(token
):
1111 self
.body
.append('<span class="pre">%s</span>'
1112 % self
.encode(token
))
1114 self
.body
.append(self
.encode(token
))
1115 elif token
in ('\n', ' '):
1116 # Allow breaks at whitespace:
1117 self
.body
.append(token
)
1119 # Protect runs of multiple spaces; the last space can wrap:
1120 self
.body
.append(' ' * (len(token
) - 1) + ' ')
1121 self
.body
.append('</tt>')
1122 # Content already processed:
1123 raise nodes
.SkipNode
1125 def visit_literal_block(self
, node
):
1126 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='literal-block'))
1128 def depart_literal_block(self
, node
):
1129 self
.body
.append('\n</pre>\n')
1131 def visit_math(self
, node
, math_env
=''):
1132 # As there is no native HTML math support, we provide alternatives:
1133 # LaTeX and MathJax math_output modes simply wrap the content,
1134 # HTML and MathML math_output modes also convert the math_code.
1135 # If the method is called from visit_math_block(), math_env != ''.
1138 tags
= {# math_output: (block, inline, class-arguments)
1139 'mathml': ('div', '', ''),
1140 'html': ('div', 'span', 'formula'),
1141 'mathjax': ('div', 'span', 'math'),
1142 'latex': ('pre', 'tt', 'math'),
1144 tag
= tags
[self
.math_output
][math_env
== '']
1145 clsarg
= tags
[self
.math_output
][2]
1147 wrappers
= {# math_mode: (inline, block)
1148 'mathml': (None, None),
1149 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1150 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1151 'latex': (None, None),
1153 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1154 # get and wrap content
1155 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1156 if wrapper
and math_env
:
1157 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1159 math_code
= wrapper
% math_code
1160 # settings and conversion
1161 if self
.math_output
in ('latex', 'mathjax'):
1162 math_code
= self
.encode(math_code
)
1163 if self
.math_output
== 'mathjax':
1164 self
.math_header
= self
.mathjax_script
% self
.mathjax_url
1165 elif self
.math_output
== 'html':
1166 math_code
= math2html(math_code
)
1167 elif self
.math_output
== 'mathml':
1168 self
.doctype
= self
.doctype_mathml
1169 self
.content_type
= self
.content_type_mathml
1171 mathml_tree
= parse_latex_math(math_code
, inline
=not(math_env
))
1172 math_code
= ''.join(mathml_tree
.xml())
1173 except SyntaxError, err
:
1174 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1175 self
.visit_system_message(err_node
)
1176 self
.body
.append(self
.starttag(node
, 'p'))
1177 self
.body
.append(u
','.join(err
.args
))
1178 self
.body
.append('</p>\n')
1179 self
.body
.append(self
.starttag(node
, 'pre',
1180 CLASS
='literal-block'))
1181 self
.body
.append(self
.encode(math_code
))
1182 self
.body
.append('\n</pre>\n')
1183 self
.depart_system_message(err_node
)
1184 raise nodes
.SkipNode
1185 # append to document body
1187 self
.body
.append(self
.starttag(node
, tag
, CLASS
=clsarg
))
1188 self
.body
.append(math_code
)
1190 self
.body
.append('\n')
1192 self
.body
.append('</%s>\n' % tag
)
1193 # Content already processed:
1194 raise nodes
.SkipNode
1196 def depart_math(self
, node
):
1197 pass # never reached
1199 def visit_math_block(self
, node
):
1200 # print node.astext().encode('utf8')
1201 math_env
= pick_math_environment(node
.astext())
1202 self
.visit_math(node
, math_env
=math_env
)
1204 def depart_math_block(self
, node
):
1205 pass # never reached
1207 def visit_meta(self
, node
):
1208 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1211 def depart_meta(self
, node
):
1214 def add_meta(self
, tag
):
1215 self
.meta
.append(tag
)
1216 self
.head
.append(tag
)
1218 def visit_option(self
, node
):
1219 if self
.context
[-1]:
1220 self
.body
.append(', ')
1221 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1223 def depart_option(self
, node
):
1224 self
.body
.append('</span>')
1225 self
.context
[-1] += 1
1227 def visit_option_argument(self
, node
):
1228 self
.body
.append(node
.get('delimiter', ' '))
1229 self
.body
.append(self
.starttag(node
, 'var', ''))
1231 def depart_option_argument(self
, node
):
1232 self
.body
.append('</var>')
1234 def visit_option_group(self
, node
):
1236 if ( self
.settings
.option_limit
1237 and len(node
.astext()) > self
.settings
.option_limit
):
1239 self
.context
.append('</tr>\n<tr><td> </td>')
1241 self
.context
.append('')
1243 self
.starttag(node
, 'td', CLASS
='option-group', **atts
))
1244 self
.body
.append('<kbd>')
1245 self
.context
.append(0) # count number of options
1247 def depart_option_group(self
, node
):
1249 self
.body
.append('</kbd></td>\n')
1250 self
.body
.append(self
.context
.pop())
1252 def visit_option_list(self
, node
):
1254 self
.starttag(node
, 'table', CLASS
='docutils option-list',
1255 frame
="void", rules
="none"))
1256 self
.body
.append('<col class="option" />\n'
1257 '<col class="description" />\n'
1258 '<tbody valign="top">\n')
1260 def depart_option_list(self
, node
):
1261 self
.body
.append('</tbody>\n</table>\n')
1263 def visit_option_list_item(self
, node
):
1264 self
.body
.append(self
.starttag(node
, 'tr', ''))
1266 def depart_option_list_item(self
, node
):
1267 self
.body
.append('</tr>\n')
1269 def visit_option_string(self
, node
):
1272 def depart_option_string(self
, node
):
1275 def visit_organization(self
, node
):
1276 self
.visit_docinfo_item(node
, 'organization')
1278 def depart_organization(self
, node
):
1279 self
.depart_docinfo_item()
1281 def should_be_compact_paragraph(self
, node
):
1283 Determine if the <p> tags around paragraph ``node`` can be omitted.
1285 if (isinstance(node
.parent
, nodes
.document
) or
1286 isinstance(node
.parent
, nodes
.compound
)):
1287 # Never compact paragraphs in document or compound.
1289 for key
, value
in node
.attlist():
1290 if (node
.is_not_default(key
) and
1291 not (key
== 'classes' and value
in
1292 ([], ['first'], ['last'], ['first', 'last']))):
1293 # Attribute which needs to survive.
1295 first
= isinstance(node
.parent
[0], nodes
.label
) # skip label
1296 for child
in node
.parent
.children
[first
:]:
1297 # only first paragraph can be compact
1298 if isinstance(child
, nodes
.Invisible
):
1303 parent_length
= len([n
for n
in node
.parent
if not isinstance(
1304 n
, (nodes
.Invisible
, nodes
.label
))])
1305 if ( self
.compact_simple
1306 or self
.compact_field_list
1307 or self
.compact_p
and parent_length
== 1):
1311 def visit_paragraph(self
, node
):
1312 if self
.should_be_compact_paragraph(node
):
1313 self
.context
.append('')
1315 self
.body
.append(self
.starttag(node
, 'p', ''))
1316 self
.context
.append('</p>\n')
1318 def depart_paragraph(self
, node
):
1319 self
.body
.append(self
.context
.pop())
1321 def visit_problematic(self
, node
):
1322 if node
.hasattr('refid'):
1323 self
.body
.append('<a href="#%s">' % node
['refid'])
1324 self
.context
.append('</a>')
1326 self
.context
.append('')
1327 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1329 def depart_problematic(self
, node
):
1330 self
.body
.append('</span>')
1331 self
.body
.append(self
.context
.pop())
1333 def visit_raw(self
, node
):
1334 if 'html' in node
.get('format', '').split():
1335 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1337 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1338 self
.body
.append(node
.astext())
1340 self
.body
.append('</%s>' % t
)
1341 # Keep non-HTML raw text out of output:
1342 raise nodes
.SkipNode
1344 def visit_reference(self
, node
):
1345 atts
= {'class': 'reference'}
1346 if 'refuri' in node
:
1347 atts
['href'] = node
['refuri']
1348 if ( self
.settings
.cloak_email_addresses
1349 and atts
['href'].startswith('mailto:')):
1350 atts
['href'] = self
.cloak_mailto(atts
['href'])
1352 atts
['class'] += ' external'
1354 assert 'refid' in node
, \
1355 'References must have "refuri" or "refid" attribute.'
1356 atts
['href'] = '#' + node
['refid']
1357 atts
['class'] += ' internal'
1358 if not isinstance(node
.parent
, nodes
.TextElement
):
1359 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1360 atts
['class'] += ' image-reference'
1361 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1363 def depart_reference(self
, node
):
1364 self
.body
.append('</a>')
1365 if not isinstance(node
.parent
, nodes
.TextElement
):
1366 self
.body
.append('\n')
1369 def visit_revision(self
, node
):
1370 self
.visit_docinfo_item(node
, 'revision', meta
=None)
1372 def depart_revision(self
, node
):
1373 self
.depart_docinfo_item()
1375 def visit_row(self
, node
):
1376 self
.body
.append(self
.starttag(node
, 'tr', ''))
1379 def depart_row(self
, node
):
1380 self
.body
.append('</tr>\n')
1382 def visit_rubric(self
, node
):
1383 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1385 def depart_rubric(self
, node
):
1386 self
.body
.append('</p>\n')
1388 def visit_section(self
, node
):
1389 self
.section_level
+= 1
1391 self
.starttag(node
, 'div', CLASS
='section'))
1393 def depart_section(self
, node
):
1394 self
.section_level
-= 1
1395 self
.body
.append('</div>\n')
1397 def visit_sidebar(self
, node
):
1399 self
.starttag(node
, 'div', CLASS
='sidebar'))
1400 self
.set_first_last(node
)
1403 def depart_sidebar(self
, node
):
1404 self
.body
.append('</div>\n')
1405 self
.in_sidebar
= None
1407 def visit_status(self
, node
):
1408 self
.visit_docinfo_item(node
, 'status', meta
=None)
1410 def depart_status(self
, node
):
1411 self
.depart_docinfo_item()
1413 def visit_strong(self
, node
):
1414 self
.body
.append(self
.starttag(node
, 'strong', ''))
1416 def depart_strong(self
, node
):
1417 self
.body
.append('</strong>')
1419 def visit_subscript(self
, node
):
1420 self
.body
.append(self
.starttag(node
, 'sub', ''))
1422 def depart_subscript(self
, node
):
1423 self
.body
.append('</sub>')
1425 def visit_substitution_definition(self
, node
):
1426 """Internal only."""
1427 raise nodes
.SkipNode
1429 def visit_substitution_reference(self
, node
):
1430 self
.unimplemented_visit(node
)
1432 def visit_subtitle(self
, node
):
1433 if isinstance(node
.parent
, nodes
.sidebar
):
1434 self
.body
.append(self
.starttag(node
, 'p', '',
1435 CLASS
='sidebar-subtitle'))
1436 self
.context
.append('</p>\n')
1437 elif isinstance(node
.parent
, nodes
.document
):
1438 self
.body
.append(self
.starttag(node
, 'h2', '', CLASS
='subtitle'))
1439 self
.context
.append('</h2>\n')
1440 self
.in_document_title
= len(self
.body
)
1441 elif isinstance(node
.parent
, nodes
.section
):
1442 tag
= 'h%s' % (self
.section_level
+ self
.initial_header_level
- 1)
1444 self
.starttag(node
, tag
, '', CLASS
='section-subtitle') +
1445 self
.starttag({}, 'span', '', CLASS
='section-subtitle'))
1446 self
.context
.append('</span></%s>\n' % tag
)
1448 def depart_subtitle(self
, node
):
1449 self
.body
.append(self
.context
.pop())
1450 if self
.in_document_title
:
1451 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1452 self
.in_document_title
= 0
1453 self
.body_pre_docinfo
.extend(self
.body
)
1454 self
.html_subtitle
.extend(self
.body
)
1457 def visit_superscript(self
, node
):
1458 self
.body
.append(self
.starttag(node
, 'sup', ''))
1460 def depart_superscript(self
, node
):
1461 self
.body
.append('</sup>')
1463 def visit_system_message(self
, node
):
1464 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1465 self
.body
.append('<p class="system-message-title">')
1467 if len(node
['backrefs']):
1468 backrefs
= node
['backrefs']
1469 if len(backrefs
) == 1:
1470 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1475 for backref
in backrefs
:
1476 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1478 backref_text
= ('; <em>backlinks: %s</em>'
1479 % ', '.join(backlinks
))
1480 if node
.hasattr('line'):
1481 line
= ', line %s' % node
['line']
1484 self
.body
.append('System Message: %s/%s '
1485 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1486 % (node
['type'], node
['level'],
1487 self
.encode(node
['source']), line
, backref_text
))
1489 def depart_system_message(self
, node
):
1490 self
.body
.append('</div>\n')
1492 def visit_table(self
, node
):
1493 classes
= ' '.join(['docutils', self
.settings
.table_style
]).strip()
1495 self
.starttag(node
, 'table', CLASS
=classes
, border
="1"))
1497 def depart_table(self
, node
):
1498 self
.body
.append('</table>\n')
1500 def visit_target(self
, node
):
1501 if not ('refuri' in node
or 'refid' in node
1502 or 'refname' in node
):
1503 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1504 self
.context
.append('</span>')
1506 self
.context
.append('')
1508 def depart_target(self
, node
):
1509 self
.body
.append(self
.context
.pop())
1511 def visit_tbody(self
, node
):
1512 self
.write_colspecs()
1513 self
.body
.append(self
.context
.pop()) # '</colgroup>\n' or ''
1514 self
.body
.append(self
.starttag(node
, 'tbody', valign
='top'))
1516 def depart_tbody(self
, node
):
1517 self
.body
.append('</tbody>\n')
1519 def visit_term(self
, node
):
1520 self
.body
.append(self
.starttag(node
, 'dt', ''))
1522 def depart_term(self
, node
):
1524 Leave the end tag to `self.visit_definition()`, in case there's a
1529 def visit_tgroup(self
, node
):
1530 # Mozilla needs <colgroup>:
1531 self
.body
.append(self
.starttag(node
, 'colgroup'))
1532 # Appended by thead or tbody:
1533 self
.context
.append('</colgroup>\n')
1536 def depart_tgroup(self
, node
):
1539 def visit_thead(self
, node
):
1540 self
.write_colspecs()
1541 self
.body
.append(self
.context
.pop()) # '</colgroup>\n'
1542 # There may or may not be a <thead>; this is for <tbody> to use:
1543 self
.context
.append('')
1544 self
.body
.append(self
.starttag(node
, 'thead', valign
='bottom'))
1546 def depart_thead(self
, node
):
1547 self
.body
.append('</thead>\n')
1549 def visit_title(self
, node
):
1550 """Only 6 section levels are supported by HTML."""
1552 close_tag
= '</p>\n'
1553 if isinstance(node
.parent
, nodes
.topic
):
1555 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1556 elif isinstance(node
.parent
, nodes
.sidebar
):
1558 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1559 elif isinstance(node
.parent
, nodes
.Admonition
):
1561 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1562 elif isinstance(node
.parent
, nodes
.table
):
1564 self
.starttag(node
, 'caption', ''))
1565 close_tag
= '</caption>\n'
1566 elif isinstance(node
.parent
, nodes
.document
):
1567 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1568 close_tag
= '</h1>\n'
1569 self
.in_document_title
= len(self
.body
)
1571 assert isinstance(node
.parent
, nodes
.section
)
1572 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1574 if (len(node
.parent
) >= 2 and
1575 isinstance(node
.parent
[1], nodes
.subtitle
)):
1576 atts
['CLASS'] = 'with-subtitle'
1578 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1580 if node
.hasattr('refid'):
1581 atts
['class'] = 'toc-backref'
1582 atts
['href'] = '#' + node
['refid']
1584 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1585 close_tag
= '</a></h%s>\n' % (h_level
)
1587 close_tag
= '</h%s>\n' % (h_level
)
1588 self
.context
.append(close_tag
)
1590 def depart_title(self
, node
):
1591 self
.body
.append(self
.context
.pop())
1592 if self
.in_document_title
:
1593 self
.title
= self
.body
[self
.in_document_title
:-1]
1594 self
.in_document_title
= 0
1595 self
.body_pre_docinfo
.extend(self
.body
)
1596 self
.html_title
.extend(self
.body
)
1599 def visit_title_reference(self
, node
):
1600 self
.body
.append(self
.starttag(node
, 'cite', ''))
1602 def depart_title_reference(self
, node
):
1603 self
.body
.append('</cite>')
1605 def visit_topic(self
, node
):
1606 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1607 self
.topic_classes
= node
['classes']
1609 def depart_topic(self
, node
):
1610 self
.body
.append('</div>\n')
1611 self
.topic_classes
= []
1613 def visit_transition(self
, node
):
1614 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1616 def depart_transition(self
, node
):
1619 def visit_version(self
, node
):
1620 self
.visit_docinfo_item(node
, 'version', meta
=None)
1622 def depart_version(self
, node
):
1623 self
.depart_docinfo_item()
1625 def unimplemented_visit(self
, node
):
1626 raise NotImplementedError('visiting unimplemented node type: %s'
1627 % node
.__class
__.__name
__)
1630 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1633 Raise `nodes.NodeFound` if non-simple list item is encountered.
1635 Here "simple" means a list item containing nothing other than a single
1636 paragraph, a simple list, or a paragraph followed by a simple list.
1639 def default_visit(self
, node
):
1640 raise nodes
.NodeFound
1642 def visit_bullet_list(self
, node
):
1645 def visit_enumerated_list(self
, node
):
1648 def visit_list_item(self
, node
):
1650 for child
in node
.children
:
1651 if not isinstance(child
, nodes
.Invisible
):
1652 children
.append(child
)
1653 if (children
and isinstance(children
[0], nodes
.paragraph
)
1654 and (isinstance(children
[-1], nodes
.bullet_list
)
1655 or isinstance(children
[-1], nodes
.enumerated_list
))):
1657 if len(children
) <= 1:
1660 raise nodes
.NodeFound
1662 def visit_paragraph(self
, node
):
1663 raise nodes
.SkipNode
1665 def invisible_visit(self
, node
):
1666 """Invisible nodes should be ignored."""
1667 raise nodes
.SkipNode
1669 visit_comment
= invisible_visit
1670 visit_substitution_definition
= invisible_visit
1671 visit_target
= invisible_visit
1672 visit_pending
= invisible_visit