2 # Author: David Goodger
3 # Maintainer: docutils-develop@lists.sourceforge.net
4 # Copyright: This module has been placed in the public domain.
7 Simple HyperText Markup Language document tree Writer.
9 The output conforms to the XHTML version 1.0 Transitional DTD
10 (*almost* strict). The output contains a minimum of formatting
11 information. The cascading style sheet "html4css1.css" is required
12 for proper viewing with a modern graphical browser.
15 __docformat__
= 'reStructuredText'
24 try: # check for the Python Imaging Library
27 try: # sometimes PIL modules are put in PYTHONPATH's root
29 class PIL(object): pass # dummy wrapper
34 from docutils
import frontend
, nodes
, utils
, writers
, languages
, io
35 from docutils
.utils
.error_reporting
import SafeString
36 from docutils
.transforms
import writer_aux
37 from docutils
.utils
.math
import unichar2tex
, pick_math_environment
, math2html
38 from docutils
.utils
.math
.latex2mathml
import parse_latex_math
40 class Writer(writers
.Writer
):
42 supported
= ('html', 'html4css1', 'xhtml')
43 """Formats this writer supports."""
45 default_stylesheet
= 'html4css1.css'
46 default_stylesheet_dirs
= ['.', utils
.relative_path(
47 os
.path
.join(os
.getcwd(), 'dummy'), os
.path
.dirname(__file__
))]
49 default_template
= 'template.txt'
51 default_template_path
= utils
.relative_path(
52 os
.path
.join(os
.getcwd(), 'dummy'),
53 os
.path
.join(os
.path
.dirname(__file__
), default_template
))
56 'HTML-Specific Options',
58 (('Specify the template file (UTF-8 encoded). Default is "%s".'
59 % default_template_path
,
61 {'default': default_template_path
, 'metavar': '<file>'}),
62 ('Comma separated list of stylesheet URLs. '
63 'Overrides previous --stylesheet and --stylesheet-path settings.',
65 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
66 'validator': frontend
.validate_comma_separated_list
}),
67 ('Comma separated list of stylesheet paths. '
68 'Relative paths are expanded if a matching file is found in '
69 'the --stylesheet-dirs. With --link-stylesheet, '
70 'the path is rewritten relative to the output HTML file. '
71 'Default: "%s"' % default_stylesheet
,
72 ['--stylesheet-path'],
73 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
74 'validator': frontend
.validate_comma_separated_list
,
75 'default': [default_stylesheet
]}),
76 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
77 'files must be accessible during processing. This is the default.',
78 ['--embed-stylesheet'],
79 {'default': 1, 'action': 'store_true',
80 'validator': frontend
.validate_boolean
}),
81 ('Link to the stylesheet(s) in the output HTML file. '
82 'Default: embed stylesheets.',
83 ['--link-stylesheet'],
84 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
85 ('Comma-separated list of directories where stylesheets are found. '
86 'Used by --stylesheet-path when expanding relative path arguments. '
87 'Default: "%s"' % default_stylesheet_dirs
,
88 ['--stylesheet-dirs'],
89 {'metavar': '<dir[,dir,...]>',
90 'validator': frontend
.validate_comma_separated_list
,
91 'default': default_stylesheet_dirs
}),
92 ('Specify the initial header level. Default is 1 for "<h1>". '
93 'Does not affect document title & subtitle (see --no-doc-title).',
94 ['--initial-header-level'],
95 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
96 'metavar': '<level>'}),
97 ('Specify the maximum width (in characters) for one-column field '
98 'names. Longer field names will span an entire row of the table '
99 'used to render the field list. Default is 14 characters. '
100 'Use 0 for "no limit".',
101 ['--field-name-limit'],
102 {'default': 14, 'metavar': '<level>',
103 'validator': frontend
.validate_nonnegative_int
}),
104 ('Specify the maximum width (in characters) for options in option '
105 'lists. Longer options will span an entire row of the table used '
106 'to render the option list. Default is 14 characters. '
107 'Use 0 for "no limit".',
109 {'default': 14, 'metavar': '<level>',
110 'validator': frontend
.validate_nonnegative_int
}),
111 ('Format for footnote references: one of "superscript" or '
112 '"brackets". Default is "brackets".',
113 ['--footnote-references'],
114 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
115 'metavar': '<format>',
116 'overrides': 'trim_footnote_reference_space'}),
117 ('Format for block quote attributions: one of "dash" (em-dash '
118 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
120 {'choices': ['dash', 'parentheses', 'parens', 'none'],
121 'default': 'dash', 'metavar': '<format>'}),
122 ('Remove extra vertical whitespace between items of "simple" bullet '
123 'lists and enumerated lists. Default: enabled.',
125 {'default': 1, 'action': 'store_true',
126 'validator': frontend
.validate_boolean
}),
127 ('Disable compact simple bullet and enumerated lists.',
128 ['--no-compact-lists'],
129 {'dest': 'compact_lists', 'action': 'store_false'}),
130 ('Remove extra vertical whitespace between items of simple field '
131 'lists. Default: enabled.',
132 ['--compact-field-lists'],
133 {'default': 1, 'action': 'store_true',
134 'validator': frontend
.validate_boolean
}),
135 ('Disable compact simple field lists.',
136 ['--no-compact-field-lists'],
137 {'dest': 'compact_field_lists', 'action': 'store_false'}),
138 ('Added to standard table classes. '
139 'Defined styles: "borderless". Default: ""',
142 ('Math output format, one of "MathML", "HTML", "MathJax" '
143 'or "LaTeX". Default: "HTML math.css"',
145 {'default': 'HTML math.css'}),
146 ('Omit the XML declaration. Use with caution.',
147 ['--no-xml-declaration'],
148 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
149 'validator': frontend
.validate_boolean
}),
150 ('Obfuscate email addresses to confuse harvesters while still '
151 'keeping email links usable with standards-compliant browsers.',
152 ['--cloak-email-addresses'],
153 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
155 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
157 config_section
= 'html4css1 writer'
158 config_section_dependencies
= ('writers',)
160 visitor_attributes
= (
161 'head_prefix', 'head', 'stylesheet', 'body_prefix',
162 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
163 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
164 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
167 def get_transforms(self
):
168 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
171 writers
.Writer
.__init
__(self
)
172 self
.translator_class
= HTMLTranslator
175 self
.visitor
= visitor
= self
.translator_class(self
.document
)
176 self
.document
.walkabout(visitor
)
177 for attr
in self
.visitor_attributes
:
178 setattr(self
, attr
, getattr(visitor
, attr
))
179 self
.output
= self
.apply_template()
181 def apply_template(self
):
182 template_file
= open(self
.document
.settings
.template
, 'rb')
183 template
= unicode(template_file
.read(), 'utf-8')
184 template_file
.close()
185 subs
= self
.interpolation_dict()
186 return template
% subs
188 def interpolation_dict(self
):
190 settings
= self
.document
.settings
191 for attr
in self
.visitor_attributes
:
192 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
193 subs
['encoding'] = settings
.output_encoding
194 subs
['version'] = docutils
.__version
__
197 def assemble_parts(self
):
198 writers
.Writer
.assemble_parts(self
)
199 for part
in self
.visitor_attributes
:
200 self
.parts
[part
] = ''.join(getattr(self
, part
))
203 class HTMLTranslator(nodes
.NodeVisitor
):
206 This HTML writer has been optimized to produce visually compact
207 lists (less vertical whitespace). HTML's mixed content models
208 allow list items to contain "<li><p>body elements</p></li>" or
209 "<li>just text</li>" or even "<li>text<p>and body
210 elements</p>combined</li>", each with different effects. It would
211 be best to stick with strict body elements in list items, but they
212 affect vertical spacing in browsers (although they really
215 Here is an outline of the optimization:
217 - Check for and omit <p> tags in "simple" lists: list items
218 contain either a single paragraph, a nested simple list, or a
219 paragraph followed by a nested simple list. This means that
220 this list can be compact:
225 But this list cannot be compact:
229 This second paragraph forces space between list items.
233 - In non-list contexts, omit <p> tags on a paragraph if that
234 paragraph is the only child of its parent (footnotes & citations
235 are allowed a label first).
237 - Regardless of the above, in definitions, table cells, field bodies,
238 option descriptions, and list items, mark the first child with
239 'class="first"' and the last child with 'class="last"'. The stylesheet
240 sets the margins (top & bottom respectively) to 0 for these elements.
242 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
243 option) disables list whitespace optimization.
246 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
248 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
249 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
250 doctype_mathml
= doctype
252 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
253 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
254 content_type
= ('<meta http-equiv="Content-Type"'
255 ' content="text/html; charset=%s" />\n')
256 content_type_mathml
= ('<meta http-equiv="Content-Type"'
257 ' content="application/xhtml+xml; charset=%s" />\n')
259 generator
= ('<meta name="generator" content="Docutils %s: '
260 'http://docutils.sourceforge.net/" />\n')
262 # Template for the MathJax script in the header:
263 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
264 # The latest version of MathJax from the distributed server:
265 # avaliable to the public under the `MathJax CDN Terms of Service`__
266 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
267 mathjax_url
= ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
268 'config=TeX-AMS-MML_HTMLorMML')
269 # may be overwritten by custom URL appended to "mathjax"
271 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
272 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
273 words_and_spaces
= re
.compile(r
'\S+| +|\n')
274 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
275 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
277 def __init__(self
, document
):
278 nodes
.NodeVisitor
.__init
__(self
, document
)
279 self
.settings
= settings
= document
.settings
280 lcode
= settings
.language_code
281 self
.language
= languages
.get_language(lcode
, document
.reporter
)
282 self
.meta
= [self
.generator
% docutils
.__version
__]
283 self
.head_prefix
= []
284 self
.html_prolog
= []
285 if settings
.xml_declaration
:
286 self
.head_prefix
.append(self
.xml_declaration
287 % settings
.output_encoding
)
288 # encoding not interpolated:
289 self
.html_prolog
.append(self
.xml_declaration
)
290 self
.head
= self
.meta
[:]
291 self
.stylesheet
= [self
.stylesheet_call(path
)
292 for path
in utils
.get_stylesheet_list(settings
)]
293 self
.body_prefix
= ['</head>\n<body>\n']
294 # document title, subtitle display
295 self
.body_pre_docinfo
= []
300 self
.body_suffix
= ['</body>\n</html>\n']
301 self
.section_level
= 0
302 self
.initial_header_level
= int(settings
.initial_header_level
)
304 self
.math_output
= settings
.math_output
.split()
305 self
.math_output_options
= self
.math_output
[1:]
306 self
.math_output
= self
.math_output
[0].lower()
308 # A heterogenous stack used in conjunction with the tree traversal.
309 # Make sure that the pops correspond to the pushes:
311 self
.topic_classes
= []
313 self
.compact_p
= True
314 self
.compact_simple
= False
315 self
.compact_field_list
= False
316 self
.in_docinfo
= False
317 self
.in_sidebar
= False
322 self
.html_head
= [self
.content_type
] # charset not interpolated
324 self
.html_subtitle
= []
326 self
.in_document_title
= 0 # len(self.body) or 0
327 self
.in_mailto
= False
328 self
.author_in_authors
= False
329 self
.math_header
= []
332 return ''.join(self
.head_prefix
+ self
.head
333 + self
.stylesheet
+ self
.body_prefix
334 + self
.body_pre_docinfo
+ self
.docinfo
335 + self
.body
+ self
.body_suffix
)
337 def encode(self
, text
):
338 """Encode special characters in `text` & return."""
339 # @@@ A codec to do these and all other HTML entities would be nice.
341 return text
.translate({
346 ord('@'): u
'@', # may thwart some address harvesters
347 # TODO: convert non-breaking space only if needed?
348 0xa0: u
' '}) # non-breaking space
350 def cloak_mailto(self
, uri
):
351 """Try to hide a mailto: URL from harvesters."""
352 # Encode "@" using a URL octet reference (see RFC 1738).
353 # Further cloaking with HTML entities will be done in the
355 return uri
.replace('@', '%40')
357 def cloak_email(self
, addr
):
358 """Try to hide the link text of a email link from harversters."""
359 # Surround at-signs and periods with <span> tags. ("@" has
360 # already been encoded to "@" by the `encode` method.)
361 addr
= addr
.replace('@', '<span>@</span>')
362 addr
= addr
.replace('.', '<span>.</span>')
365 def attval(self
, text
,
366 whitespace
=re
.compile('[\n\r\t\v\f]')):
367 """Cleanse, HTML encode, and return attribute value text."""
368 encoded
= self
.encode(whitespace
.sub(' ', text
))
369 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
370 # Cloak at-signs ("%40") and periods with HTML entities.
371 encoded
= encoded
.replace('%40', '%40')
372 encoded
= encoded
.replace('.', '.')
375 def stylesheet_call(self
, path
):
376 """Return code to reference or embed stylesheet file `path`"""
377 if self
.settings
.embed_stylesheet
:
379 content
= io
.FileInput(source_path
=path
,
380 encoding
='utf-8').read()
381 self
.settings
.record_dependencies
.add(path
)
383 msg
= u
"Cannot embed stylesheet '%s': %s." % (
384 path
, SafeString(err
.strerror
))
385 self
.document
.reporter
.error(msg
)
386 return '<--- %s --->\n' % msg
387 return self
.embedded_stylesheet
% content
388 # else link to style file:
389 if self
.settings
.stylesheet_path
:
390 # adapt path relative to output (cf. config.html#stylesheet-path)
391 path
= utils
.relative_path(self
.settings
._destination
, path
)
392 return self
.stylesheet_link
% self
.encode(path
)
394 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
396 Construct and return a start tag given a node (id & class attributes
397 are extracted), tag name, and optional attributes.
399 tagname
= tagname
.lower()
403 for (name
, value
) in attributes
.items():
404 atts
[name
.lower()] = value
407 # unify class arguments and move language specification
408 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
409 if cls
.startswith('language-'):
410 languages
.append(cls
[9:])
411 elif cls
.strip() and cls
not in classes
:
414 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
415 atts
[self
.lang_attribute
] = languages
[0]
417 atts
['class'] = ' '.join(classes
)
418 assert 'id' not in atts
419 ids
.extend(node
.get('ids', []))
421 ids
.extend(atts
['ids'])
426 # Add empty "span" elements for additional IDs. Note
427 # that we cannot use empty "a" elements because there
428 # may be targets inside of references, but nested "a"
429 # elements aren't allowed in XHTML (even if they do
430 # not all have a "href" attribute).
432 # Empty tag. Insert target right in front of element.
433 prefix
.append('<span id="%s"></span>' % id)
435 # Non-empty tag. Place the auxiliary <span> tag
436 # *inside* the element, as the first child.
437 suffix
+= '<span id="%s"></span>' % id
438 attlist
= atts
.items()
441 for name
, value
in attlist
:
442 # value=None was used for boolean attributes without
443 # value, but this isn't supported by XHTML.
444 assert value
is not None
445 if isinstance(value
, list):
446 values
= [unicode(v
) for v
in value
]
447 parts
.append('%s="%s"' % (name
.lower(),
448 self
.attval(' '.join(values
))))
450 parts
.append('%s="%s"' % (name
.lower(),
451 self
.attval(unicode(value
))))
456 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
458 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
459 """Construct and return an XML-compatible empty tag."""
460 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
462 def set_class_on_child(self
, node
, class_
, index
=0):
464 Set class `class_` on the visible child no. index of `node`.
465 Do nothing if node has fewer children than `index`.
467 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
469 child
= children
[index
]
472 child
['classes'].append(class_
)
474 def set_first_last(self
, node
):
475 self
.set_class_on_child(node
, 'first', 0)
476 self
.set_class_on_child(node
, 'last', -1)
478 def visit_Text(self
, node
):
480 encoded
= self
.encode(text
)
481 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
482 encoded
= self
.cloak_email(encoded
)
483 self
.body
.append(encoded
)
485 def depart_Text(self
, node
):
488 def visit_abbreviation(self
, node
):
489 # @@@ implementation incomplete ("title" attribute)
490 self
.body
.append(self
.starttag(node
, 'abbr', ''))
492 def depart_abbreviation(self
, node
):
493 self
.body
.append('</abbr>')
495 def visit_acronym(self
, node
):
496 # @@@ implementation incomplete ("title" attribute)
497 self
.body
.append(self
.starttag(node
, 'acronym', ''))
499 def depart_acronym(self
, node
):
500 self
.body
.append('</acronym>')
502 def visit_address(self
, node
):
503 self
.visit_docinfo_item(node
, 'address', meta
=False)
504 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='address'))
506 def depart_address(self
, node
):
507 self
.body
.append('\n</pre>\n')
508 self
.depart_docinfo_item()
510 def visit_admonition(self
, node
):
511 self
.body
.append(self
.starttag(node
, 'div'))
512 self
.set_first_last(node
)
514 def depart_admonition(self
, node
=None):
515 self
.body
.append('</div>\n')
517 attribution_formats
= {'dash': ('—', ''),
518 'parentheses': ('(', ')'),
519 'parens': ('(', ')'),
522 def visit_attribution(self
, node
):
523 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
524 self
.context
.append(suffix
)
526 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
528 def depart_attribution(self
, node
):
529 self
.body
.append(self
.context
.pop() + '</p>\n')
531 def visit_author(self
, node
):
532 if isinstance(node
.parent
, nodes
.authors
):
533 if self
.author_in_authors
:
534 self
.body
.append('\n<br />')
536 self
.visit_docinfo_item(node
, 'author')
538 def depart_author(self
, node
):
539 if isinstance(node
.parent
, nodes
.authors
):
540 self
.author_in_authors
= True
542 self
.depart_docinfo_item()
544 def visit_authors(self
, node
):
545 self
.visit_docinfo_item(node
, 'authors')
546 self
.author_in_authors
= False # initialize
548 def depart_authors(self
, node
):
549 self
.depart_docinfo_item()
551 def visit_block_quote(self
, node
):
552 self
.body
.append(self
.starttag(node
, 'blockquote'))
554 def depart_block_quote(self
, node
):
555 self
.body
.append('</blockquote>\n')
557 def check_simple_list(self
, node
):
558 """Check for a simple list that can be rendered compactly."""
559 visitor
= SimpleListChecker(self
.document
)
562 except nodes
.NodeFound
:
567 def is_compactable(self
, node
):
568 return ('compact' in node
['classes']
569 or (self
.settings
.compact_lists
570 and 'open' not in node
['classes']
571 and (self
.compact_simple
572 or self
.topic_classes
== ['contents']
573 or self
.check_simple_list(node
))))
575 def visit_bullet_list(self
, node
):
577 old_compact_simple
= self
.compact_simple
578 self
.context
.append((self
.compact_simple
, self
.compact_p
))
579 self
.compact_p
= None
580 self
.compact_simple
= self
.is_compactable(node
)
581 if self
.compact_simple
and not old_compact_simple
:
582 atts
['class'] = 'simple'
583 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
585 def depart_bullet_list(self
, node
):
586 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
587 self
.body
.append('</ul>\n')
589 def visit_caption(self
, node
):
590 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
592 def depart_caption(self
, node
):
593 self
.body
.append('</p>\n')
595 def visit_citation(self
, node
):
596 self
.body
.append(self
.starttag(node
, 'table',
597 CLASS
='docutils citation',
598 frame
="void", rules
="none"))
599 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
600 '<tbody valign="top">\n'
602 self
.footnote_backrefs(node
)
604 def depart_citation(self
, node
):
605 self
.body
.append('</td></tr>\n'
606 '</tbody>\n</table>\n')
608 def visit_citation_reference(self
, node
):
611 href
+= node
['refid']
612 elif 'refname' in node
:
613 href
+= self
.document
.nameids
[node
['refname']]
614 # else: # TODO system message (or already in the transform)?
615 # 'Citation reference missing.'
616 self
.body
.append(self
.starttag(
617 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
619 def depart_citation_reference(self
, node
):
620 self
.body
.append(']</a>')
622 def visit_classifier(self
, node
):
623 self
.body
.append(' <span class="classifier-delimiter">:</span> ')
624 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
626 def depart_classifier(self
, node
):
627 self
.body
.append('</span>')
629 def visit_colspec(self
, node
):
630 self
.colspecs
.append(node
)
631 # "stubs" list is an attribute of the tgroup element:
632 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
634 def depart_colspec(self
, node
):
637 def write_colspecs(self
):
639 for node
in self
.colspecs
:
640 width
+= node
['colwidth']
641 for node
in self
.colspecs
:
642 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
643 self
.body
.append(self
.emptytag(node
, 'col',
644 width
='%i%%' % colwidth
))
647 def visit_comment(self
, node
,
648 sub
=re
.compile('-(?=-)').sub
):
649 """Escape double-dashes in comment text."""
650 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
651 # Content already processed:
654 def visit_compound(self
, node
):
655 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
657 node
[0]['classes'].append('compound-first')
658 node
[-1]['classes'].append('compound-last')
659 for child
in node
[1:-1]:
660 child
['classes'].append('compound-middle')
662 def depart_compound(self
, node
):
663 self
.body
.append('</div>\n')
665 def visit_container(self
, node
):
666 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
668 def depart_container(self
, node
):
669 self
.body
.append('</div>\n')
671 def visit_contact(self
, node
):
672 self
.visit_docinfo_item(node
, 'contact', meta
=False)
674 def depart_contact(self
, node
):
675 self
.depart_docinfo_item()
677 def visit_copyright(self
, node
):
678 self
.visit_docinfo_item(node
, 'copyright')
680 def depart_copyright(self
, node
):
681 self
.depart_docinfo_item()
683 def visit_date(self
, node
):
684 self
.visit_docinfo_item(node
, 'date')
686 def depart_date(self
, node
):
687 self
.depart_docinfo_item()
689 def visit_decoration(self
, node
):
692 def depart_decoration(self
, node
):
695 def visit_definition(self
, node
):
696 self
.body
.append('</dt>\n')
697 self
.body
.append(self
.starttag(node
, 'dd', ''))
698 self
.set_first_last(node
)
700 def depart_definition(self
, node
):
701 self
.body
.append('</dd>\n')
703 def visit_definition_list(self
, node
):
704 self
.body
.append(self
.starttag(node
, 'dl', CLASS
='docutils'))
706 def depart_definition_list(self
, node
):
707 self
.body
.append('</dl>\n')
709 def visit_definition_list_item(self
, node
):
712 def depart_definition_list_item(self
, node
):
715 def visit_description(self
, node
):
716 self
.body
.append(self
.starttag(node
, 'td', ''))
717 self
.set_first_last(node
)
719 def depart_description(self
, node
):
720 self
.body
.append('</td>')
722 def visit_docinfo(self
, node
):
723 self
.context
.append(len(self
.body
))
724 self
.body
.append(self
.starttag(node
, 'table',
726 frame
="void", rules
="none"))
727 self
.body
.append('<col class="docinfo-name" />\n'
728 '<col class="docinfo-content" />\n'
729 '<tbody valign="top">\n')
730 self
.in_docinfo
= True
732 def depart_docinfo(self
, node
):
733 self
.body
.append('</tbody>\n</table>\n')
734 self
.in_docinfo
= False
735 start
= self
.context
.pop()
736 self
.docinfo
= self
.body
[start
:]
739 def visit_docinfo_item(self
, node
, name
, meta
=True):
741 meta_tag
= '<meta name="%s" content="%s" />\n' \
742 % (name
, self
.attval(node
.astext()))
743 self
.add_meta(meta_tag
)
744 self
.body
.append(self
.starttag(node
, 'tr', ''))
745 self
.body
.append('<th class="docinfo-name">%s:</th>\n<td>'
746 % self
.language
.labels
[name
])
748 if isinstance(node
[0], nodes
.Element
):
749 node
[0]['classes'].append('first')
750 if isinstance(node
[-1], nodes
.Element
):
751 node
[-1]['classes'].append('last')
753 def depart_docinfo_item(self
):
754 self
.body
.append('</td></tr>\n')
756 def visit_doctest_block(self
, node
):
757 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='doctest-block'))
759 def depart_doctest_block(self
, node
):
760 self
.body
.append('\n</pre>\n')
762 def visit_document(self
, node
):
763 self
.head
.append('<title>%s</title>\n'
764 % self
.encode(node
.get('title', '')))
766 def depart_document(self
, node
):
767 self
.head_prefix
.extend([self
.doctype
,
768 self
.head_prefix_template
%
769 {'lang': self
.settings
.language_code
}])
770 self
.html_prolog
.append(self
.doctype
)
771 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
772 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
774 if self
.math_output
== 'mathjax':
775 self
.head
.extend(self
.math_header
)
777 self
.stylesheet
.extend(self
.math_header
)
778 # skip content-type meta tag with interpolated charset value:
779 self
.html_head
.extend(self
.head
[1:])
780 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
781 self
.body_suffix
.insert(0, '</div>\n')
782 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
783 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
784 + self
.docinfo
+ self
.body
785 + self
.body_suffix
[:-1])
786 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
788 def visit_emphasis(self
, node
):
789 self
.body
.append(self
.starttag(node
, 'em', ''))
791 def depart_emphasis(self
, node
):
792 self
.body
.append('</em>')
794 def visit_entry(self
, node
):
796 if isinstance(node
.parent
.parent
, nodes
.thead
):
797 atts
['class'].append('head')
798 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
799 # "stubs" list is an attribute of the tgroup element
800 atts
['class'].append('stub')
803 atts
['class'] = ' '.join(atts
['class'])
807 node
.parent
.column
+= 1
808 if 'morerows' in node
:
809 atts
['rowspan'] = node
['morerows'] + 1
810 if 'morecols' in node
:
811 atts
['colspan'] = node
['morecols'] + 1
812 node
.parent
.column
+= node
['morecols']
813 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
814 self
.context
.append('</%s>\n' % tagname
.lower())
815 if len(node
) == 0: # empty cell
816 self
.body
.append(' ')
817 self
.set_first_last(node
)
819 def depart_entry(self
, node
):
820 self
.body
.append(self
.context
.pop())
822 def visit_enumerated_list(self
, node
):
824 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
825 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
830 atts
['start'] = node
['start']
831 if 'enumtype' in node
:
832 atts
['class'] = node
['enumtype']
833 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
834 # single "format" attribute? Use CSS2?
835 old_compact_simple
= self
.compact_simple
836 self
.context
.append((self
.compact_simple
, self
.compact_p
))
837 self
.compact_p
= None
838 self
.compact_simple
= self
.is_compactable(node
)
839 if self
.compact_simple
and not old_compact_simple
:
840 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
841 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
843 def depart_enumerated_list(self
, node
):
844 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
845 self
.body
.append('</ol>\n')
847 def visit_field(self
, node
):
848 self
.body
.append(self
.starttag(node
, 'tr', '', CLASS
='field'))
850 def depart_field(self
, node
):
851 self
.body
.append('</tr>\n')
853 def visit_field_body(self
, node
):
854 self
.body
.append(self
.starttag(node
, 'td', '', CLASS
='field-body'))
855 self
.set_class_on_child(node
, 'first', 0)
857 if (self
.compact_field_list
or
858 isinstance(field
.parent
, nodes
.docinfo
) or
859 field
.parent
.index(field
) == len(field
.parent
) - 1):
860 # If we are in a compact list, the docinfo, or if this is
861 # the last field of the field list, do not add vertical
862 # space after last element.
863 self
.set_class_on_child(node
, 'last', -1)
865 def depart_field_body(self
, node
):
866 self
.body
.append('</td>\n')
868 def visit_field_list(self
, node
):
869 self
.context
.append((self
.compact_field_list
, self
.compact_p
))
870 self
.compact_p
= None
871 if 'compact' in node
['classes']:
872 self
.compact_field_list
= True
873 elif (self
.settings
.compact_field_lists
874 and 'open' not in node
['classes']):
875 self
.compact_field_list
= True
876 if self
.compact_field_list
:
878 field_body
= field
[-1]
879 assert isinstance(field_body
, nodes
.field_body
)
880 children
= [n
for n
in field_body
881 if not isinstance(n
, nodes
.Invisible
)]
882 if not (len(children
) == 0 or
883 len(children
) == 1 and
884 isinstance(children
[0],
885 (nodes
.paragraph
, nodes
.line_block
))):
886 self
.compact_field_list
= False
888 self
.body
.append(self
.starttag(node
, 'table', frame
='void',
890 CLASS
='docutils field-list'))
891 self
.body
.append('<col class="field-name" />\n'
892 '<col class="field-body" />\n'
893 '<tbody valign="top">\n')
895 def depart_field_list(self
, node
):
896 self
.body
.append('</tbody>\n</table>\n')
897 self
.compact_field_list
, self
.compact_p
= self
.context
.pop()
899 def visit_field_name(self
, node
):
902 atts
['class'] = 'docinfo-name'
904 atts
['class'] = 'field-name'
905 if ( self
.settings
.field_name_limit
906 and len(node
.astext()) > self
.settings
.field_name_limit
):
908 self
.context
.append('</tr>\n'
909 + self
.starttag(node
.parent
, 'tr', '',
913 self
.context
.append('')
914 self
.body
.append(self
.starttag(node
, 'th', '', **atts
))
916 def depart_field_name(self
, node
):
917 self
.body
.append(':</th>')
918 self
.body
.append(self
.context
.pop())
920 def visit_figure(self
, node
):
921 atts
= {'class': 'figure'}
922 if node
.get('width'):
923 atts
['style'] = 'width: %s' % node
['width']
924 if node
.get('align'):
925 atts
['class'] += " align-" + node
['align']
926 self
.body
.append(self
.starttag(node
, 'div', **atts
))
928 def depart_figure(self
, node
):
929 self
.body
.append('</div>\n')
931 def visit_footer(self
, node
):
932 self
.context
.append(len(self
.body
))
934 def depart_footer(self
, node
):
935 start
= self
.context
.pop()
936 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
937 '<hr class="footer" />\n']
938 footer
.extend(self
.body
[start
:])
939 footer
.append('\n</div>\n')
940 self
.footer
.extend(footer
)
941 self
.body_suffix
[:0] = footer
942 del self
.body
[start
:]
944 def visit_footnote(self
, node
):
945 self
.body
.append(self
.starttag(node
, 'table',
946 CLASS
='docutils footnote',
947 frame
="void", rules
="none"))
948 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
949 '<tbody valign="top">\n'
951 self
.footnote_backrefs(node
)
953 def footnote_backrefs(self
, node
):
955 backrefs
= node
['backrefs']
956 if self
.settings
.footnote_backlinks
and backrefs
:
957 if len(backrefs
) == 1:
958 self
.context
.append('')
959 self
.context
.append('</a>')
960 self
.context
.append('<a class="fn-backref" href="#%s">'
964 for backref
in backrefs
:
965 backlinks
.append('<a class="fn-backref" href="#%s">%s</a>'
968 self
.context
.append('<em>(%s)</em> ' % ', '.join(backlinks
))
969 self
.context
+= ['', '']
971 self
.context
.append('')
972 self
.context
+= ['', '']
973 # If the node does not only consist of a label.
975 # If there are preceding backlinks, we do not set class
976 # 'first', because we need to retain the top-margin.
978 node
[1]['classes'].append('first')
979 node
[-1]['classes'].append('last')
981 def depart_footnote(self
, node
):
982 self
.body
.append('</td></tr>\n'
983 '</tbody>\n</table>\n')
985 def visit_footnote_reference(self
, node
):
986 href
= '#' + node
['refid']
987 format
= self
.settings
.footnote_references
988 if format
== 'brackets':
990 self
.context
.append(']')
992 assert format
== 'superscript'
994 self
.context
.append('</sup>')
995 self
.body
.append(self
.starttag(node
, 'a', suffix
,
996 CLASS
='footnote-reference', href
=href
))
998 def depart_footnote_reference(self
, node
):
999 self
.body
.append(self
.context
.pop() + '</a>')
1001 def visit_generated(self
, node
):
1004 def depart_generated(self
, node
):
1007 def visit_header(self
, node
):
1008 self
.context
.append(len(self
.body
))
1010 def depart_header(self
, node
):
1011 start
= self
.context
.pop()
1012 header
= [self
.starttag(node
, 'div', CLASS
='header')]
1013 header
.extend(self
.body
[start
:])
1014 header
.append('\n<hr class="header"/>\n</div>\n')
1015 self
.body_prefix
.extend(header
)
1016 self
.header
.extend(header
)
1017 del self
.body
[start
:]
1019 def visit_image(self
, node
):
1022 # place SVG and SWF images in an <object> element
1023 types
= {'.svg': 'image/svg+xml',
1024 '.swf': 'application/x-shockwave-flash'}
1025 ext
= os
.path
.splitext(uri
)[1].lower()
1026 if ext
in ('.svg', '.swf'):
1028 atts
['type'] = types
[ext
]
1031 atts
['alt'] = node
.get('alt', uri
)
1034 atts
['width'] = node
['width']
1035 if 'height' in node
:
1036 atts
['height'] = node
['height']
1038 if (PIL
and not ('width' in node
and 'height' in node
)
1039 and self
.settings
.file_insertion_enabled
):
1040 imagepath
= urllib
.url2pathname(uri
)
1042 img
= PIL
.Image
.open(
1043 imagepath
.encode(sys
.getfilesystemencoding()))
1044 except (IOError, UnicodeEncodeError):
1047 self
.settings
.record_dependencies
.add(
1048 imagepath
.replace('\\', '/'))
1049 if 'width' not in atts
:
1050 atts
['width'] = '%dpx' % img
.size
[0]
1051 if 'height' not in atts
:
1052 atts
['height'] = '%dpx' % img
.size
[1]
1054 for att_name
in 'width', 'height':
1055 if att_name
in atts
:
1056 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1058 atts
[att_name
] = '%s%s' % (
1059 float(match
.group(1)) * (float(node
['scale']) / 100),
1062 for att_name
in 'width', 'height':
1063 if att_name
in atts
:
1064 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1065 # Interpret unitless values as pixels.
1066 atts
[att_name
] += 'px'
1067 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1070 atts
['style'] = ' '.join(style
)
1071 if (isinstance(node
.parent
, nodes
.TextElement
) or
1072 (isinstance(node
.parent
, nodes
.reference
) and
1073 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1074 # Inline context or surrounded by <a>...</a>.
1079 atts
['class'] = 'align-%s' % node
['align']
1080 self
.context
.append('')
1081 if ext
in ('.svg', '.swf'): # place in an object element,
1082 # do NOT use an empty tag: incorrect rendering in browsers
1083 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1084 node
.get('alt', uri
) + '</object>' + suffix
)
1086 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1088 def depart_image(self
, node
):
1089 self
.body
.append(self
.context
.pop())
1091 def visit_inline(self
, node
):
1092 self
.body
.append(self
.starttag(node
, 'span', ''))
1094 def depart_inline(self
, node
):
1095 self
.body
.append('</span>')
1097 def visit_label(self
, node
):
1098 # Context added in footnote_backrefs.
1099 self
.body
.append(self
.starttag(node
, 'td', '%s[' % self
.context
.pop(),
1102 def depart_label(self
, node
):
1103 # Context added in footnote_backrefs.
1104 self
.body
.append(']%s</td><td>%s' % (self
.context
.pop(), self
.context
.pop()))
1106 def visit_legend(self
, node
):
1107 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1109 def depart_legend(self
, node
):
1110 self
.body
.append('</div>\n')
1112 def visit_line(self
, node
):
1113 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1115 self
.body
.append('<br />')
1117 def depart_line(self
, node
):
1118 self
.body
.append('</div>\n')
1120 def visit_line_block(self
, node
):
1121 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1123 def depart_line_block(self
, node
):
1124 self
.body
.append('</div>\n')
1126 def visit_list_item(self
, node
):
1127 self
.body
.append(self
.starttag(node
, 'li', ''))
1129 node
[0]['classes'].append('first')
1131 def depart_list_item(self
, node
):
1132 self
.body
.append('</li>\n')
1134 def visit_literal(self
, node
):
1135 # special case: "code" role
1136 classes
= node
.get('classes', [])
1137 if 'code' in classes
:
1138 # filter 'code' from class arguments
1139 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1140 self
.body
.append(self
.starttag(node
, 'code', ''))
1143 self
.starttag(node
, 'tt', '', CLASS
='docutils literal'))
1144 text
= node
.astext()
1145 for token
in self
.words_and_spaces
.findall(text
):
1147 # Protect text like "--an-option" and the regular expression
1148 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1149 if self
.sollbruchstelle
.search(token
):
1150 self
.body
.append('<span class="pre">%s</span>'
1151 % self
.encode(token
))
1153 self
.body
.append(self
.encode(token
))
1154 elif token
in ('\n', ' '):
1155 # Allow breaks at whitespace:
1156 self
.body
.append(token
)
1158 # Protect runs of multiple spaces; the last space can wrap:
1159 self
.body
.append(' ' * (len(token
) - 1) + ' ')
1160 self
.body
.append('</tt>')
1161 # Content already processed:
1162 raise nodes
.SkipNode
1164 def depart_literal(self
, node
):
1165 # skipped unless literal element is from "code" role:
1166 self
.body
.append('</code>')
1168 def visit_literal_block(self
, node
):
1169 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='literal-block'))
1171 def depart_literal_block(self
, node
):
1172 self
.body
.append('\n</pre>\n')
1174 def visit_math(self
, node
, math_env
=''):
1175 # If the method is called from visit_math_block(), math_env != ''.
1177 # As there is no native HTML math support, we provide alternatives:
1178 # LaTeX and MathJax math_output modes simply wrap the content,
1179 # HTML and MathML math_output modes also convert the math_code.
1180 if self
.math_output
not in ('mathml', 'html', 'mathjax', 'latex'):
1181 self
.document
.reporter
.error(
1182 'math-output format "%s" not supported '
1183 'falling back to "latex"'% self
.math_output
)
1184 self
.math_output
= 'latex'
1187 tags
= {# math_output: (block, inline, class-arguments)
1188 'mathml': ('div', '', ''),
1189 'html': ('div', 'span', 'formula'),
1190 'mathjax': ('div', 'span', 'math'),
1191 'latex': ('pre', 'tt', 'math'),
1193 tag
= tags
[self
.math_output
][math_env
== '']
1194 clsarg
= tags
[self
.math_output
][2]
1196 wrappers
= {# math_mode: (inline, block)
1197 'mathml': (None, None),
1198 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1199 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1200 'latex': (None, None),
1202 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1203 # get and wrap content
1204 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1205 if wrapper
and math_env
:
1206 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1208 math_code
= wrapper
% math_code
1209 # settings and conversion
1210 if self
.math_output
in ('latex', 'mathjax'):
1211 math_code
= self
.encode(math_code
)
1212 if self
.math_output
== 'mathjax' and not self
.math_header
:
1213 if self
.math_output_options
:
1214 self
.mathjax_url
= self
.math_output_options
[0]
1215 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1216 elif self
.math_output
== 'html':
1217 if self
.math_output_options
and not self
.math_header
:
1218 self
.math_header
= [self
.stylesheet_call(
1219 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1220 for s
in self
.math_output_options
[0].split(',')]
1221 # TODO: fix display mode in matrices and fractions
1222 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1223 math_code
= math2html
.math2html(math_code
)
1224 elif self
.math_output
== 'mathml':
1225 self
.doctype
= self
.doctype_mathml
1226 self
.content_type
= self
.content_type_mathml
1228 mathml_tree
= parse_latex_math(math_code
, inline
=not(math_env
))
1229 math_code
= ''.join(mathml_tree
.xml())
1230 except SyntaxError, err
:
1231 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1232 self
.visit_system_message(err_node
)
1233 self
.body
.append(self
.starttag(node
, 'p'))
1234 self
.body
.append(u
','.join(err
.args
))
1235 self
.body
.append('</p>\n')
1236 self
.body
.append(self
.starttag(node
, 'pre',
1237 CLASS
='literal-block'))
1238 self
.body
.append(self
.encode(math_code
))
1239 self
.body
.append('\n</pre>\n')
1240 self
.depart_system_message(err_node
)
1241 raise nodes
.SkipNode
1242 # append to document body
1244 self
.body
.append(self
.starttag(node
, tag
,
1245 suffix
='\n'*bool(math_env
),
1247 self
.body
.append(math_code
)
1248 if math_env
: # block mode (equation, display)
1249 self
.body
.append('\n')
1251 self
.body
.append('</%s>' % tag
)
1253 self
.body
.append('\n')
1254 # Content already processed:
1255 raise nodes
.SkipNode
1257 def depart_math(self
, node
):
1258 pass # never reached
1260 def visit_math_block(self
, node
):
1261 # print node.astext().encode('utf8')
1262 math_env
= pick_math_environment(node
.astext())
1263 self
.visit_math(node
, math_env
=math_env
)
1265 def depart_math_block(self
, node
):
1266 pass # never reached
1268 def visit_meta(self
, node
):
1269 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1272 def depart_meta(self
, node
):
1275 def add_meta(self
, tag
):
1276 self
.meta
.append(tag
)
1277 self
.head
.append(tag
)
1279 def visit_option(self
, node
):
1280 if self
.context
[-1]:
1281 self
.body
.append(', ')
1282 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1284 def depart_option(self
, node
):
1285 self
.body
.append('</span>')
1286 self
.context
[-1] += 1
1288 def visit_option_argument(self
, node
):
1289 self
.body
.append(node
.get('delimiter', ' '))
1290 self
.body
.append(self
.starttag(node
, 'var', ''))
1292 def depart_option_argument(self
, node
):
1293 self
.body
.append('</var>')
1295 def visit_option_group(self
, node
):
1297 if ( self
.settings
.option_limit
1298 and len(node
.astext()) > self
.settings
.option_limit
):
1300 self
.context
.append('</tr>\n<tr><td> </td>')
1302 self
.context
.append('')
1304 self
.starttag(node
, 'td', CLASS
='option-group', **atts
))
1305 self
.body
.append('<kbd>')
1306 self
.context
.append(0) # count number of options
1308 def depart_option_group(self
, node
):
1310 self
.body
.append('</kbd></td>\n')
1311 self
.body
.append(self
.context
.pop())
1313 def visit_option_list(self
, node
):
1315 self
.starttag(node
, 'table', CLASS
='docutils option-list',
1316 frame
="void", rules
="none"))
1317 self
.body
.append('<col class="option" />\n'
1318 '<col class="description" />\n'
1319 '<tbody valign="top">\n')
1321 def depart_option_list(self
, node
):
1322 self
.body
.append('</tbody>\n</table>\n')
1324 def visit_option_list_item(self
, node
):
1325 self
.body
.append(self
.starttag(node
, 'tr', ''))
1327 def depart_option_list_item(self
, node
):
1328 self
.body
.append('</tr>\n')
1330 def visit_option_string(self
, node
):
1333 def depart_option_string(self
, node
):
1336 def visit_organization(self
, node
):
1337 self
.visit_docinfo_item(node
, 'organization')
1339 def depart_organization(self
, node
):
1340 self
.depart_docinfo_item()
1342 def should_be_compact_paragraph(self
, node
):
1344 Determine if the <p> tags around paragraph ``node`` can be omitted.
1346 if (isinstance(node
.parent
, nodes
.document
) or
1347 isinstance(node
.parent
, nodes
.compound
)):
1348 # Never compact paragraphs in document or compound.
1350 for key
, value
in node
.attlist():
1351 if (node
.is_not_default(key
) and
1352 not (key
== 'classes' and value
in
1353 ([], ['first'], ['last'], ['first', 'last']))):
1354 # Attribute which needs to survive.
1356 first
= isinstance(node
.parent
[0], nodes
.label
) # skip label
1357 for child
in node
.parent
.children
[first
:]:
1358 # only first paragraph can be compact
1359 if isinstance(child
, nodes
.Invisible
):
1364 parent_length
= len([n
for n
in node
.parent
if not isinstance(
1365 n
, (nodes
.Invisible
, nodes
.label
))])
1366 if ( self
.compact_simple
1367 or self
.compact_field_list
1368 or self
.compact_p
and parent_length
== 1):
1372 def visit_paragraph(self
, node
):
1373 if self
.should_be_compact_paragraph(node
):
1374 self
.context
.append('')
1376 self
.body
.append(self
.starttag(node
, 'p', ''))
1377 self
.context
.append('</p>\n')
1379 def depart_paragraph(self
, node
):
1380 self
.body
.append(self
.context
.pop())
1382 def visit_problematic(self
, node
):
1383 if node
.hasattr('refid'):
1384 self
.body
.append('<a href="#%s">' % node
['refid'])
1385 self
.context
.append('</a>')
1387 self
.context
.append('')
1388 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1390 def depart_problematic(self
, node
):
1391 self
.body
.append('</span>')
1392 self
.body
.append(self
.context
.pop())
1394 def visit_raw(self
, node
):
1395 if 'html' in node
.get('format', '').split():
1396 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1398 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1399 self
.body
.append(node
.astext())
1401 self
.body
.append('</%s>' % t
)
1402 # Keep non-HTML raw text out of output:
1403 raise nodes
.SkipNode
1405 def visit_reference(self
, node
):
1406 atts
= {'class': 'reference'}
1407 if 'refuri' in node
:
1408 atts
['href'] = node
['refuri']
1409 if ( self
.settings
.cloak_email_addresses
1410 and atts
['href'].startswith('mailto:')):
1411 atts
['href'] = self
.cloak_mailto(atts
['href'])
1412 self
.in_mailto
= True
1413 atts
['class'] += ' external'
1415 assert 'refid' in node
, \
1416 'References must have "refuri" or "refid" attribute.'
1417 atts
['href'] = '#' + node
['refid']
1418 atts
['class'] += ' internal'
1419 if not isinstance(node
.parent
, nodes
.TextElement
):
1420 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1421 atts
['class'] += ' image-reference'
1422 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1424 def depart_reference(self
, node
):
1425 self
.body
.append('</a>')
1426 if not isinstance(node
.parent
, nodes
.TextElement
):
1427 self
.body
.append('\n')
1428 self
.in_mailto
= False
1430 def visit_revision(self
, node
):
1431 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1433 def depart_revision(self
, node
):
1434 self
.depart_docinfo_item()
1436 def visit_row(self
, node
):
1437 self
.body
.append(self
.starttag(node
, 'tr', ''))
1440 def depart_row(self
, node
):
1441 self
.body
.append('</tr>\n')
1443 def visit_rubric(self
, node
):
1444 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1446 def depart_rubric(self
, node
):
1447 self
.body
.append('</p>\n')
1449 def visit_section(self
, node
):
1450 self
.section_level
+= 1
1452 self
.starttag(node
, 'div', CLASS
='section'))
1454 def depart_section(self
, node
):
1455 self
.section_level
-= 1
1456 self
.body
.append('</div>\n')
1458 def visit_sidebar(self
, node
):
1460 self
.starttag(node
, 'div', CLASS
='sidebar'))
1461 self
.set_first_last(node
)
1462 self
.in_sidebar
= True
1464 def depart_sidebar(self
, node
):
1465 self
.body
.append('</div>\n')
1466 self
.in_sidebar
= False
1468 def visit_status(self
, node
):
1469 self
.visit_docinfo_item(node
, 'status', meta
=False)
1471 def depart_status(self
, node
):
1472 self
.depart_docinfo_item()
1474 def visit_strong(self
, node
):
1475 self
.body
.append(self
.starttag(node
, 'strong', ''))
1477 def depart_strong(self
, node
):
1478 self
.body
.append('</strong>')
1480 def visit_subscript(self
, node
):
1481 self
.body
.append(self
.starttag(node
, 'sub', ''))
1483 def depart_subscript(self
, node
):
1484 self
.body
.append('</sub>')
1486 def visit_substitution_definition(self
, node
):
1487 """Internal only."""
1488 raise nodes
.SkipNode
1490 def visit_substitution_reference(self
, node
):
1491 self
.unimplemented_visit(node
)
1493 def visit_subtitle(self
, node
):
1494 if isinstance(node
.parent
, nodes
.sidebar
):
1495 self
.body
.append(self
.starttag(node
, 'p', '',
1496 CLASS
='sidebar-subtitle'))
1497 self
.context
.append('</p>\n')
1498 elif isinstance(node
.parent
, nodes
.document
):
1499 self
.body
.append(self
.starttag(node
, 'h2', '', CLASS
='subtitle'))
1500 self
.context
.append('</h2>\n')
1501 self
.in_document_title
= len(self
.body
)
1502 elif isinstance(node
.parent
, nodes
.section
):
1503 tag
= 'h%s' % (self
.section_level
+ self
.initial_header_level
- 1)
1505 self
.starttag(node
, tag
, '', CLASS
='section-subtitle') +
1506 self
.starttag({}, 'span', '', CLASS
='section-subtitle'))
1507 self
.context
.append('</span></%s>\n' % tag
)
1509 def depart_subtitle(self
, node
):
1510 self
.body
.append(self
.context
.pop())
1511 if self
.in_document_title
:
1512 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1513 self
.in_document_title
= 0
1514 self
.body_pre_docinfo
.extend(self
.body
)
1515 self
.html_subtitle
.extend(self
.body
)
1518 def visit_superscript(self
, node
):
1519 self
.body
.append(self
.starttag(node
, 'sup', ''))
1521 def depart_superscript(self
, node
):
1522 self
.body
.append('</sup>')
1524 def visit_system_message(self
, node
):
1525 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1526 self
.body
.append('<p class="system-message-title">')
1528 if len(node
['backrefs']):
1529 backrefs
= node
['backrefs']
1530 if len(backrefs
) == 1:
1531 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1536 for backref
in backrefs
:
1537 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1539 backref_text
= ('; <em>backlinks: %s</em>'
1540 % ', '.join(backlinks
))
1541 if node
.hasattr('line'):
1542 line
= ', line %s' % node
['line']
1545 self
.body
.append('System Message: %s/%s '
1546 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1547 % (node
['type'], node
['level'],
1548 self
.encode(node
['source']), line
, backref_text
))
1550 def depart_system_message(self
, node
):
1551 self
.body
.append('</div>\n')
1553 def visit_table(self
, node
):
1554 self
.context
.append(self
.compact_p
)
1555 self
.compact_p
= True
1556 classes
= ' '.join(['docutils', self
.settings
.table_style
]).strip()
1558 self
.starttag(node
, 'table', CLASS
=classes
, border
="1"))
1560 def depart_table(self
, node
):
1561 self
.compact_p
= self
.context
.pop()
1562 self
.body
.append('</table>\n')
1564 def visit_target(self
, node
):
1565 if not ('refuri' in node
or 'refid' in node
1566 or 'refname' in node
):
1567 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1568 self
.context
.append('</span>')
1570 self
.context
.append('')
1572 def depart_target(self
, node
):
1573 self
.body
.append(self
.context
.pop())
1575 def visit_tbody(self
, node
):
1576 self
.write_colspecs()
1577 self
.body
.append(self
.context
.pop()) # '</colgroup>\n' or ''
1578 self
.body
.append(self
.starttag(node
, 'tbody', valign
='top'))
1580 def depart_tbody(self
, node
):
1581 self
.body
.append('</tbody>\n')
1583 def visit_term(self
, node
):
1584 self
.body
.append(self
.starttag(node
, 'dt', ''))
1586 def depart_term(self
, node
):
1588 Leave the end tag to `self.visit_definition()`, in case there's a
1593 def visit_tgroup(self
, node
):
1594 # Mozilla needs <colgroup>:
1595 self
.body
.append(self
.starttag(node
, 'colgroup'))
1596 # Appended by thead or tbody:
1597 self
.context
.append('</colgroup>\n')
1600 def depart_tgroup(self
, node
):
1603 def visit_thead(self
, node
):
1604 self
.write_colspecs()
1605 self
.body
.append(self
.context
.pop()) # '</colgroup>\n'
1606 # There may or may not be a <thead>; this is for <tbody> to use:
1607 self
.context
.append('')
1608 self
.body
.append(self
.starttag(node
, 'thead', valign
='bottom'))
1610 def depart_thead(self
, node
):
1611 self
.body
.append('</thead>\n')
1613 def visit_title(self
, node
):
1614 """Only 6 section levels are supported by HTML."""
1615 check_id
= 0 # TODO: is this a bool (False) or a counter?
1616 close_tag
= '</p>\n'
1617 if isinstance(node
.parent
, nodes
.topic
):
1619 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1620 elif isinstance(node
.parent
, nodes
.sidebar
):
1622 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1623 elif isinstance(node
.parent
, nodes
.Admonition
):
1625 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1626 elif isinstance(node
.parent
, nodes
.table
):
1628 self
.starttag(node
, 'caption', ''))
1629 close_tag
= '</caption>\n'
1630 elif isinstance(node
.parent
, nodes
.document
):
1631 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1632 close_tag
= '</h1>\n'
1633 self
.in_document_title
= len(self
.body
)
1635 assert isinstance(node
.parent
, nodes
.section
)
1636 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1638 if (len(node
.parent
) >= 2 and
1639 isinstance(node
.parent
[1], nodes
.subtitle
)):
1640 atts
['CLASS'] = 'with-subtitle'
1642 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1644 if node
.hasattr('refid'):
1645 atts
['class'] = 'toc-backref'
1646 atts
['href'] = '#' + node
['refid']
1648 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1649 close_tag
= '</a></h%s>\n' % (h_level
)
1651 close_tag
= '</h%s>\n' % (h_level
)
1652 self
.context
.append(close_tag
)
1654 def depart_title(self
, node
):
1655 self
.body
.append(self
.context
.pop())
1656 if self
.in_document_title
:
1657 self
.title
= self
.body
[self
.in_document_title
:-1]
1658 self
.in_document_title
= 0
1659 self
.body_pre_docinfo
.extend(self
.body
)
1660 self
.html_title
.extend(self
.body
)
1663 def visit_title_reference(self
, node
):
1664 self
.body
.append(self
.starttag(node
, 'cite', ''))
1666 def depart_title_reference(self
, node
):
1667 self
.body
.append('</cite>')
1669 def visit_topic(self
, node
):
1670 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1671 self
.topic_classes
= node
['classes']
1673 def depart_topic(self
, node
):
1674 self
.body
.append('</div>\n')
1675 self
.topic_classes
= []
1677 def visit_transition(self
, node
):
1678 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1680 def depart_transition(self
, node
):
1683 def visit_version(self
, node
):
1684 self
.visit_docinfo_item(node
, 'version', meta
=False)
1686 def depart_version(self
, node
):
1687 self
.depart_docinfo_item()
1689 def unimplemented_visit(self
, node
):
1690 raise NotImplementedError('visiting unimplemented node type: %s'
1691 % node
.__class
__.__name
__)
1694 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1697 Raise `nodes.NodeFound` if non-simple list item is encountered.
1699 Here "simple" means a list item containing nothing other than a single
1700 paragraph, a simple list, or a paragraph followed by a simple list.
1703 def default_visit(self
, node
):
1704 raise nodes
.NodeFound
1706 def visit_bullet_list(self
, node
):
1709 def visit_enumerated_list(self
, node
):
1712 def visit_list_item(self
, node
):
1714 for child
in node
.children
:
1715 if not isinstance(child
, nodes
.Invisible
):
1716 children
.append(child
)
1717 if (children
and isinstance(children
[0], nodes
.paragraph
)
1718 and (isinstance(children
[-1], nodes
.bullet_list
)
1719 or isinstance(children
[-1], nodes
.enumerated_list
))):
1721 if len(children
) <= 1:
1724 raise nodes
.NodeFound
1726 def visit_paragraph(self
, node
):
1727 raise nodes
.SkipNode
1729 def invisible_visit(self
, node
):
1730 """Invisible nodes should be ignored."""
1731 raise nodes
.SkipNode
1733 visit_comment
= invisible_visit
1734 visit_substitution_definition
= invisible_visit
1735 visit_target
= invisible_visit
1736 visit_pending
= invisible_visit