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_stylesheets
= ['html4css1.css']
46 default_stylesheet_dirs
= ['.',
47 os
.path
.abspath(os
.path
.dirname(__file__
)),
49 os
.path
.abspath(os
.path
.join(
50 os
.path
.dirname(os
.path
.dirname(__file__
)), 'html_base'))
53 default_template
= 'template.txt'
54 default_template_path
= os
.path
.join(
55 os
.path
.dirname(os
.path
.abspath(__file__
)), default_template
)
58 'HTML-Specific Options',
60 (('Specify the template file (UTF-8 encoded). Default is "%s".'
61 % default_template_path
,
63 {'default': default_template_path
, 'metavar': '<file>'}),
64 ('Comma separated list of stylesheet URLs. '
65 'Overrides previous --stylesheet and --stylesheet-path settings.',
67 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
68 'validator': frontend
.validate_comma_separated_list
}),
69 ('Comma separated list of stylesheet paths. '
70 'Relative paths are expanded if a matching file is found in '
71 'the --stylesheet-dirs. With --link-stylesheet, '
72 'the path is rewritten relative to the output HTML file. '
73 'Default: "%s"' % ','.join(default_stylesheets
),
74 ['--stylesheet-path'],
75 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
76 'validator': frontend
.validate_comma_separated_list
,
77 'default': default_stylesheets
}),
78 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
79 'files must be accessible during processing. This is the default.',
80 ['--embed-stylesheet'],
81 {'default': 1, 'action': 'store_true',
82 'validator': frontend
.validate_boolean
}),
83 ('Link to the stylesheet(s) in the output HTML file. '
84 'Default: embed stylesheets.',
85 ['--link-stylesheet'],
86 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
87 ('Comma-separated list of directories where stylesheets are found. '
88 'Used by --stylesheet-path when expanding relative path arguments. '
89 'Default: "%s"' % default_stylesheet_dirs
,
90 ['--stylesheet-dirs'],
91 {'metavar': '<dir[,dir,...]>',
92 'validator': frontend
.validate_comma_separated_list
,
93 'default': default_stylesheet_dirs
}),
94 ('Specify the initial header level. Default is 1 for "<h1>". '
95 'Does not affect document title & subtitle (see --no-doc-title).',
96 ['--initial-header-level'],
97 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
98 'metavar': '<level>'}),
99 ('Specify the maximum width (in characters) for one-column field '
100 'names. Longer field names will span an entire row of the table '
101 'used to render the field list. Default is 14 characters. '
102 'Use 0 for "no limit".',
103 ['--field-name-limit'],
104 {'default': 14, 'metavar': '<level>',
105 'validator': frontend
.validate_nonnegative_int
}),
106 ('Specify the maximum width (in characters) for options in option '
107 'lists. Longer options will span an entire row of the table used '
108 'to render the option list. Default is 14 characters. '
109 'Use 0 for "no limit".',
111 {'default': 14, 'metavar': '<level>',
112 'validator': frontend
.validate_nonnegative_int
}),
113 ('Format for footnote references: one of "superscript" or '
114 '"brackets". Default is "brackets".',
115 ['--footnote-references'],
116 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
117 'metavar': '<format>',
118 'overrides': 'trim_footnote_reference_space'}),
119 ('Format for block quote attributions: one of "dash" (em-dash '
120 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
122 {'choices': ['dash', 'parentheses', 'parens', 'none'],
123 'default': 'dash', 'metavar': '<format>'}),
124 ('Remove extra vertical whitespace between items of "simple" bullet '
125 'lists and enumerated lists. Default: enabled.',
127 {'default': 1, 'action': 'store_true',
128 'validator': frontend
.validate_boolean
}),
129 ('Disable compact simple bullet and enumerated lists.',
130 ['--no-compact-lists'],
131 {'dest': 'compact_lists', 'action': 'store_false'}),
132 ('Remove extra vertical whitespace between items of simple field '
133 'lists. Default: enabled.',
134 ['--compact-field-lists'],
135 {'default': 1, 'action': 'store_true',
136 'validator': frontend
.validate_boolean
}),
137 ('Disable compact simple field lists.',
138 ['--no-compact-field-lists'],
139 {'dest': 'compact_field_lists', 'action': 'store_false'}),
140 ('Added to standard table classes. '
141 'Defined styles: "borderless". Default: ""',
144 ('Math output format, one of "MathML", "HTML", "MathJax" '
145 'or "LaTeX". Default: "HTML math.css"',
147 {'default': 'HTML math.css'}),
148 ('Omit the XML declaration. Use with caution.',
149 ['--no-xml-declaration'],
150 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
151 'validator': frontend
.validate_boolean
}),
152 ('Obfuscate email addresses to confuse harvesters while still '
153 'keeping email links usable with standards-compliant browsers.',
154 ['--cloak-email-addresses'],
155 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
157 settings_defaults
= {'output_encoding_error_handler': 'xmlcharrefreplace'}
159 config_section
= 'html4css1 writer'
160 config_section_dependencies
= ('writers',)
162 visitor_attributes
= (
163 'head_prefix', 'head', 'stylesheet', 'body_prefix',
164 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
165 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
166 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
169 def get_transforms(self
):
170 return writers
.Writer
.get_transforms(self
) + [writer_aux
.Admonitions
]
173 writers
.Writer
.__init
__(self
)
174 self
.translator_class
= HTMLTranslator
177 self
.visitor
= visitor
= self
.translator_class(self
.document
)
178 self
.document
.walkabout(visitor
)
179 for attr
in self
.visitor_attributes
:
180 setattr(self
, attr
, getattr(visitor
, attr
))
181 self
.output
= self
.apply_template()
183 def apply_template(self
):
184 template_file
= open(self
.document
.settings
.template
, 'rb')
185 template
= unicode(template_file
.read(), 'utf-8')
186 template_file
.close()
187 subs
= self
.interpolation_dict()
188 return template
% subs
190 def interpolation_dict(self
):
192 settings
= self
.document
.settings
193 for attr
in self
.visitor_attributes
:
194 subs
[attr
] = ''.join(getattr(self
, attr
)).rstrip('\n')
195 subs
['encoding'] = settings
.output_encoding
196 subs
['version'] = docutils
.__version
__
199 def assemble_parts(self
):
200 writers
.Writer
.assemble_parts(self
)
201 for part
in self
.visitor_attributes
:
202 self
.parts
[part
] = ''.join(getattr(self
, part
))
205 class HTMLTranslator(nodes
.NodeVisitor
):
208 This HTML writer has been optimized to produce visually compact
209 lists (less vertical whitespace). HTML's mixed content models
210 allow list items to contain "<li><p>body elements</p></li>" or
211 "<li>just text</li>" or even "<li>text<p>and body
212 elements</p>combined</li>", each with different effects. It would
213 be best to stick with strict body elements in list items, but they
214 affect vertical spacing in browsers (although they really
217 Here is an outline of the optimization:
219 - Check for and omit <p> tags in "simple" lists: list items
220 contain either a single paragraph, a nested simple list, or a
221 paragraph followed by a nested simple list. This means that
222 this list can be compact:
227 But this list cannot be compact:
231 This second paragraph forces space between list items.
235 - In non-list contexts, omit <p> tags on a paragraph if that
236 paragraph is the only child of its parent (footnotes & citations
237 are allowed a label first).
239 - Regardless of the above, in definitions, table cells, field bodies,
240 option descriptions, and list items, mark the first child with
241 'class="first"' and the last child with 'class="last"'. The stylesheet
242 sets the margins (top & bottom respectively) to 0 for these elements.
244 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
245 option) disables list whitespace optimization.
248 xml_declaration
= '<?xml version="1.0" encoding="%s" ?>\n'
250 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
251 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
252 doctype_mathml
= doctype
254 head_prefix_template
= ('<html xmlns="http://www.w3.org/1999/xhtml"'
255 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
256 content_type
= ('<meta http-equiv="Content-Type"'
257 ' content="text/html; charset=%s" />\n')
258 content_type_mathml
= ('<meta http-equiv="Content-Type"'
259 ' content="application/xhtml+xml; charset=%s" />\n')
261 generator
= ('<meta name="generator" content="Docutils %s: '
262 'http://docutils.sourceforge.net/" />\n')
264 # Template for the MathJax script in the header:
265 mathjax_script
= '<script type="text/javascript" src="%s"></script>\n'
266 # The latest version of MathJax from the distributed server:
267 # avaliable to the public under the `MathJax CDN Terms of Service`__
268 # __http://www.mathjax.org/download/mathjax-cdn-terms-of-service/
269 mathjax_url
= ('http://cdn.mathjax.org/mathjax/latest/MathJax.js?'
270 'config=TeX-AMS-MML_HTMLorMML')
271 # may be overwritten by custom URL appended to "mathjax"
273 stylesheet_link
= '<link rel="stylesheet" href="%s" type="text/css" />\n'
274 embedded_stylesheet
= '<style type="text/css">\n\n%s\n</style>\n'
275 words_and_spaces
= re
.compile(r
'\S+| +|\n')
276 sollbruchstelle
= re
.compile(r
'.+\W\W.+|[-?].+', re
.U
) # wrap point inside word
277 lang_attribute
= 'lang' # name changes to 'xml:lang' in XHTML 1.1
279 def __init__(self
, document
):
280 nodes
.NodeVisitor
.__init
__(self
, document
)
281 self
.settings
= settings
= document
.settings
282 lcode
= settings
.language_code
283 self
.language
= languages
.get_language(lcode
, document
.reporter
)
284 self
.meta
= [self
.generator
% docutils
.__version
__]
285 self
.head_prefix
= []
286 self
.html_prolog
= []
287 if settings
.xml_declaration
:
288 self
.head_prefix
.append(self
.xml_declaration
289 % settings
.output_encoding
)
290 # encoding not interpolated:
291 self
.html_prolog
.append(self
.xml_declaration
)
292 self
.head
= self
.meta
[:]
293 self
.stylesheet
= [self
.stylesheet_call(path
)
294 for path
in utils
.get_stylesheet_list(settings
)]
295 self
.body_prefix
= ['</head>\n<body>\n']
296 # document title, subtitle display
297 self
.body_pre_docinfo
= []
302 self
.body_suffix
= ['</body>\n</html>\n']
303 self
.section_level
= 0
304 self
.initial_header_level
= int(settings
.initial_header_level
)
306 self
.math_output
= settings
.math_output
.split()
307 self
.math_output_options
= self
.math_output
[1:]
308 self
.math_output
= self
.math_output
[0].lower()
310 # A heterogenous stack used in conjunction with the tree traversal.
311 # Make sure that the pops correspond to the pushes:
314 self
.topic_classes
= [] # TODO: replace with self_in_contents
316 self
.compact_p
= True
317 self
.compact_simple
= False
318 self
.compact_field_list
= False
319 self
.in_docinfo
= False
320 self
.in_sidebar
= False
325 self
.html_head
= [self
.content_type
] # charset not interpolated
327 self
.html_subtitle
= []
329 self
.in_document_title
= 0 # len(self.body) or 0
330 self
.in_mailto
= False
331 self
.author_in_authors
= False
332 self
.math_header
= []
335 return ''.join(self
.head_prefix
+ self
.head
336 + self
.stylesheet
+ self
.body_prefix
337 + self
.body_pre_docinfo
+ self
.docinfo
338 + self
.body
+ self
.body_suffix
)
340 def encode(self
, text
):
341 """Encode special characters in `text` & return."""
342 # @@@ A codec to do these and all other HTML entities would be nice.
344 return text
.translate({
349 ord('@'): u
'@', # may thwart some address harvesters
350 # TODO: convert non-breaking space only if needed?
351 0xa0: u
' '}) # non-breaking space
353 def cloak_mailto(self
, uri
):
354 """Try to hide a mailto: URL from harvesters."""
355 # Encode "@" using a URL octet reference (see RFC 1738).
356 # Further cloaking with HTML entities will be done in the
358 return uri
.replace('@', '%40')
360 def cloak_email(self
, addr
):
361 """Try to hide the link text of a email link from harversters."""
362 # Surround at-signs and periods with <span> tags. ("@" has
363 # already been encoded to "@" by the `encode` method.)
364 addr
= addr
.replace('@', '<span>@</span>')
365 addr
= addr
.replace('.', '<span>.</span>')
368 def attval(self
, text
,
369 whitespace
=re
.compile('[\n\r\t\v\f]')):
370 """Cleanse, HTML encode, and return attribute value text."""
371 encoded
= self
.encode(whitespace
.sub(' ', text
))
372 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
373 # Cloak at-signs ("%40") and periods with HTML entities.
374 encoded
= encoded
.replace('%40', '%40')
375 encoded
= encoded
.replace('.', '.')
378 def stylesheet_call(self
, path
):
379 """Return code to reference or embed stylesheet file `path`"""
380 if self
.settings
.embed_stylesheet
:
382 content
= io
.FileInput(source_path
=path
,
383 encoding
='utf-8').read()
384 self
.settings
.record_dependencies
.add(path
)
386 msg
= u
"Cannot embed stylesheet '%s': %s." % (
387 path
, SafeString(err
.strerror
))
388 self
.document
.reporter
.error(msg
)
389 return '<--- %s --->\n' % msg
390 return self
.embedded_stylesheet
% content
391 # else link to style file:
392 if self
.settings
.stylesheet_path
:
393 # adapt path relative to output (cf. config.html#stylesheet-path)
394 path
= utils
.relative_path(self
.settings
._destination
, path
)
395 return self
.stylesheet_link
% self
.encode(path
)
397 def starttag(self
, node
, tagname
, suffix
='\n', empty
=False, **attributes
):
399 Construct and return a start tag given a node (id & class attributes
400 are extracted), tag name, and optional attributes.
402 tagname
= tagname
.lower()
406 for (name
, value
) in attributes
.items():
407 atts
[name
.lower()] = value
410 # unify class arguments and move language specification
411 for cls
in node
.get('classes', []) + atts
.pop('class', '').split() :
412 if cls
.startswith('language-'):
413 languages
.append(cls
[9:])
414 elif cls
.strip() and cls
not in classes
:
417 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
418 atts
[self
.lang_attribute
] = languages
[0]
420 atts
['class'] = ' '.join(classes
)
421 assert 'id' not in atts
422 ids
.extend(node
.get('ids', []))
424 ids
.extend(atts
['ids'])
429 # Add empty "span" elements for additional IDs. Note
430 # that we cannot use empty "a" elements because there
431 # may be targets inside of references, but nested "a"
432 # elements aren't allowed in XHTML (even if they do
433 # not all have a "href" attribute).
435 # Empty tag. Insert target right in front of element.
436 prefix
.append('<span id="%s"></span>' % id)
438 # Non-empty tag. Place the auxiliary <span> tag
439 # *inside* the element, as the first child.
440 suffix
+= '<span id="%s"></span>' % id
441 attlist
= atts
.items()
444 for name
, value
in attlist
:
445 # value=None was used for boolean attributes without
446 # value, but this isn't supported by XHTML.
447 assert value
is not None
448 if isinstance(value
, list):
449 values
= [unicode(v
) for v
in value
]
450 parts
.append('%s="%s"' % (name
.lower(),
451 self
.attval(' '.join(values
))))
453 parts
.append('%s="%s"' % (name
.lower(),
454 self
.attval(unicode(value
))))
459 return ''.join(prefix
) + '<%s%s>' % (' '.join(parts
), infix
) + suffix
461 def emptytag(self
, node
, tagname
, suffix
='\n', **attributes
):
462 """Construct and return an XML-compatible empty tag."""
463 return self
.starttag(node
, tagname
, suffix
, empty
=True, **attributes
)
465 def set_class_on_child(self
, node
, class_
, index
=0):
467 Set class `class_` on the visible child no. index of `node`.
468 Do nothing if node has fewer children than `index`.
470 children
= [n
for n
in node
if not isinstance(n
, nodes
.Invisible
)]
472 child
= children
[index
]
475 child
['classes'].append(class_
)
477 def set_first_last(self
, node
):
478 self
.set_class_on_child(node
, 'first', 0)
479 self
.set_class_on_child(node
, 'last', -1)
481 def visit_Text(self
, node
):
483 encoded
= self
.encode(text
)
484 if self
.in_mailto
and self
.settings
.cloak_email_addresses
:
485 encoded
= self
.cloak_email(encoded
)
486 self
.body
.append(encoded
)
488 def depart_Text(self
, node
):
491 def visit_abbreviation(self
, node
):
492 # @@@ implementation incomplete ("title" attribute)
493 self
.body
.append(self
.starttag(node
, 'abbr', ''))
495 def depart_abbreviation(self
, node
):
496 self
.body
.append('</abbr>')
498 def visit_acronym(self
, node
):
499 # @@@ implementation incomplete ("title" attribute)
500 self
.body
.append(self
.starttag(node
, 'acronym', ''))
502 def depart_acronym(self
, node
):
503 self
.body
.append('</acronym>')
505 def visit_address(self
, node
):
506 self
.visit_docinfo_item(node
, 'address', meta
=False)
507 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='address'))
509 def depart_address(self
, node
):
510 self
.body
.append('\n</pre>\n')
511 self
.depart_docinfo_item()
513 def visit_admonition(self
, node
):
514 node
['classes'].insert(0, 'admonition')
515 self
.body
.append(self
.starttag(node
, 'div'))
516 self
.set_first_last(node
)
518 def depart_admonition(self
, node
=None):
519 self
.body
.append('</div>\n')
521 attribution_formats
= {'dash': ('—', ''),
522 'parentheses': ('(', ')'),
523 'parens': ('(', ')'),
526 def visit_attribution(self
, node
):
527 prefix
, suffix
= self
.attribution_formats
[self
.settings
.attribution
]
528 self
.context
.append(suffix
)
530 self
.starttag(node
, 'p', prefix
, CLASS
='attribution'))
532 def depart_attribution(self
, node
):
533 self
.body
.append(self
.context
.pop() + '</p>\n')
535 def visit_author(self
, node
):
536 if isinstance(node
.parent
, nodes
.authors
):
537 if self
.author_in_authors
:
538 self
.body
.append('\n<br />')
540 self
.visit_docinfo_item(node
, 'author')
542 def depart_author(self
, node
):
543 if isinstance(node
.parent
, nodes
.authors
):
544 self
.author_in_authors
= True
546 self
.depart_docinfo_item()
548 def visit_authors(self
, node
):
549 self
.visit_docinfo_item(node
, 'authors')
550 self
.author_in_authors
= False # initialize
552 def depart_authors(self
, node
):
553 self
.depart_docinfo_item()
555 def visit_block_quote(self
, node
):
556 self
.body
.append(self
.starttag(node
, 'blockquote'))
558 def depart_block_quote(self
, node
):
559 self
.body
.append('</blockquote>\n')
561 def check_simple_list(self
, node
):
562 """Check for a simple list that can be rendered compactly."""
563 visitor
= SimpleListChecker(self
.document
)
566 except nodes
.NodeFound
:
571 def is_compactable(self
, node
):
572 return ('compact' in node
['classes']
573 or (self
.settings
.compact_lists
574 and 'open' not in node
['classes']
575 and (self
.compact_simple
576 or self
.topic_classes
== ['contents']
577 # TODO: self.in_contents
578 or self
.check_simple_list(node
))))
580 def visit_bullet_list(self
, node
):
582 old_compact_simple
= self
.compact_simple
583 self
.context
.append((self
.compact_simple
, self
.compact_p
))
584 self
.compact_p
= None
585 self
.compact_simple
= self
.is_compactable(node
)
586 if self
.compact_simple
and not old_compact_simple
:
587 atts
['class'] = 'simple'
588 self
.body
.append(self
.starttag(node
, 'ul', **atts
))
590 def depart_bullet_list(self
, node
):
591 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
592 self
.body
.append('</ul>\n')
594 def visit_caption(self
, node
):
595 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='caption'))
597 def depart_caption(self
, node
):
598 self
.body
.append('</p>\n')
600 def visit_citation(self
, node
):
601 self
.body
.append(self
.starttag(node
, 'table',
602 CLASS
='docutils citation',
603 frame
="void", rules
="none"))
604 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
605 '<tbody valign="top">\n'
607 self
.footnote_backrefs(node
)
609 def depart_citation(self
, node
):
610 self
.body
.append('</td></tr>\n'
611 '</tbody>\n</table>\n')
613 def visit_citation_reference(self
, node
):
616 href
+= node
['refid']
617 elif 'refname' in node
:
618 href
+= self
.document
.nameids
[node
['refname']]
619 # else: # TODO system message (or already in the transform)?
620 # 'Citation reference missing.'
621 self
.body
.append(self
.starttag(
622 node
, 'a', '[', CLASS
='citation-reference', href
=href
))
624 def depart_citation_reference(self
, node
):
625 self
.body
.append(']</a>')
627 def visit_classifier(self
, node
):
628 self
.body
.append(' <span class="classifier-delimiter">:</span> ')
629 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
631 def depart_classifier(self
, node
):
632 self
.body
.append('</span>')
634 def visit_colspec(self
, node
):
635 self
.colspecs
.append(node
)
636 # "stubs" list is an attribute of the tgroup element:
637 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
639 def depart_colspec(self
, node
):
642 def write_colspecs(self
):
644 for node
in self
.colspecs
:
645 width
+= node
['colwidth']
646 for node
in self
.colspecs
:
647 colwidth
= int(node
['colwidth'] * 100.0 / width
+ 0.5)
648 self
.body
.append(self
.emptytag(node
, 'col',
649 width
='%i%%' % colwidth
))
652 def visit_comment(self
, node
,
653 sub
=re
.compile('-(?=-)').sub
):
654 """Escape double-dashes in comment text."""
655 self
.body
.append('<!-- %s -->\n' % sub('- ', node
.astext()))
656 # Content already processed:
659 def visit_compound(self
, node
):
660 self
.body
.append(self
.starttag(node
, 'div', CLASS
='compound'))
662 node
[0]['classes'].append('compound-first')
663 node
[-1]['classes'].append('compound-last')
664 for child
in node
[1:-1]:
665 child
['classes'].append('compound-middle')
667 def depart_compound(self
, node
):
668 self
.body
.append('</div>\n')
670 def visit_container(self
, node
):
671 self
.body
.append(self
.starttag(node
, 'div', CLASS
='docutils container'))
673 def depart_container(self
, node
):
674 self
.body
.append('</div>\n')
676 def visit_contact(self
, node
):
677 self
.visit_docinfo_item(node
, 'contact', meta
=False)
679 def depart_contact(self
, node
):
680 self
.depart_docinfo_item()
682 def visit_copyright(self
, node
):
683 self
.visit_docinfo_item(node
, 'copyright')
685 def depart_copyright(self
, node
):
686 self
.depart_docinfo_item()
688 def visit_date(self
, node
):
689 self
.visit_docinfo_item(node
, 'date')
691 def depart_date(self
, node
):
692 self
.depart_docinfo_item()
694 def visit_decoration(self
, node
):
697 def depart_decoration(self
, node
):
700 def visit_definition(self
, node
):
701 self
.body
.append('</dt>\n')
702 self
.body
.append(self
.starttag(node
, 'dd', ''))
703 self
.set_first_last(node
)
705 def depart_definition(self
, node
):
706 self
.body
.append('</dd>\n')
708 def visit_definition_list(self
, node
):
709 self
.body
.append(self
.starttag(node
, 'dl', CLASS
='docutils'))
711 def depart_definition_list(self
, node
):
712 self
.body
.append('</dl>\n')
714 def visit_definition_list_item(self
, node
):
715 # pass class arguments, ids and names to definition term:
716 node
.children
[0]['classes'] = (
717 node
.get('classes', []) + node
.children
[0].get('classes', []))
718 node
.children
[0]['ids'] = (
719 node
.get('ids', []) + node
.children
[0].get('ids', []))
720 node
.children
[0]['names'] = (
721 node
.get('names', []) + node
.children
[0].get('names', []))
723 def depart_definition_list_item(self
, node
):
726 def visit_description(self
, node
):
727 self
.body
.append(self
.starttag(node
, 'td', ''))
728 self
.set_first_last(node
)
730 def depart_description(self
, node
):
731 self
.body
.append('</td>')
733 def visit_docinfo(self
, node
):
734 self
.context
.append(len(self
.body
))
735 self
.body
.append(self
.starttag(node
, 'table',
737 frame
="void", rules
="none"))
738 self
.body
.append('<col class="docinfo-name" />\n'
739 '<col class="docinfo-content" />\n'
740 '<tbody valign="top">\n')
741 self
.in_docinfo
= True
743 def depart_docinfo(self
, node
):
744 self
.body
.append('</tbody>\n</table>\n')
745 self
.in_docinfo
= False
746 start
= self
.context
.pop()
747 self
.docinfo
= self
.body
[start
:]
750 def visit_docinfo_item(self
, node
, name
, meta
=True):
752 meta_tag
= '<meta name="%s" content="%s" />\n' \
753 % (name
, self
.attval(node
.astext()))
754 self
.add_meta(meta_tag
)
755 self
.body
.append(self
.starttag(node
, 'tr', ''))
756 self
.body
.append('<th class="docinfo-name">%s:</th>\n<td>'
757 % self
.language
.labels
[name
])
759 if isinstance(node
[0], nodes
.Element
):
760 node
[0]['classes'].append('first')
761 if isinstance(node
[-1], nodes
.Element
):
762 node
[-1]['classes'].append('last')
764 def depart_docinfo_item(self
):
765 self
.body
.append('</td></tr>\n')
767 def visit_doctest_block(self
, node
):
768 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='doctest-block'))
770 def depart_doctest_block(self
, node
):
771 self
.body
.append('\n</pre>\n')
773 def visit_document(self
, node
):
774 self
.head
.append('<title>%s</title>\n'
775 % self
.encode(node
.get('title', '')))
777 def depart_document(self
, node
):
778 self
.head_prefix
.extend([self
.doctype
,
779 self
.head_prefix_template
%
780 {'lang': self
.settings
.language_code
}])
781 self
.html_prolog
.append(self
.doctype
)
782 self
.meta
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
783 self
.head
.insert(0, self
.content_type
% self
.settings
.output_encoding
)
785 if self
.math_output
== 'mathjax':
786 self
.head
.extend(self
.math_header
)
788 self
.stylesheet
.extend(self
.math_header
)
789 # skip content-type meta tag with interpolated charset value:
790 self
.html_head
.extend(self
.head
[1:])
791 self
.body_prefix
.append(self
.starttag(node
, 'div', CLASS
='document'))
792 self
.body_suffix
.insert(0, '</div>\n')
793 self
.fragment
.extend(self
.body
) # self.fragment is the "naked" body
794 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
795 + self
.docinfo
+ self
.body
796 + self
.body_suffix
[:-1])
797 assert not self
.context
, 'len(context) = %s' % len(self
.context
)
799 def visit_emphasis(self
, node
):
800 self
.body
.append(self
.starttag(node
, 'em', ''))
802 def depart_emphasis(self
, node
):
803 self
.body
.append('</em>')
805 def visit_entry(self
, node
):
807 if isinstance(node
.parent
.parent
, nodes
.thead
):
808 atts
['class'].append('head')
809 if node
.parent
.parent
.parent
.stubs
[node
.parent
.column
]:
810 # "stubs" list is an attribute of the tgroup element
811 atts
['class'].append('stub')
814 atts
['class'] = ' '.join(atts
['class'])
818 node
.parent
.column
+= 1
819 if 'morerows' in node
:
820 atts
['rowspan'] = node
['morerows'] + 1
821 if 'morecols' in node
:
822 atts
['colspan'] = node
['morecols'] + 1
823 node
.parent
.column
+= node
['morecols']
824 self
.body
.append(self
.starttag(node
, tagname
, '', **atts
))
825 self
.context
.append('</%s>\n' % tagname
.lower())
826 if len(node
) == 0: # empty cell
827 self
.body
.append(' ')
828 self
.set_first_last(node
)
830 def depart_entry(self
, node
):
831 self
.body
.append(self
.context
.pop())
833 def visit_enumerated_list(self
, node
):
835 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
836 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
841 atts
['start'] = node
['start']
842 if 'enumtype' in node
:
843 atts
['class'] = node
['enumtype']
844 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
845 # single "format" attribute? Use CSS2?
846 old_compact_simple
= self
.compact_simple
847 self
.context
.append((self
.compact_simple
, self
.compact_p
))
848 self
.compact_p
= None
849 self
.compact_simple
= self
.is_compactable(node
)
850 if self
.compact_simple
and not old_compact_simple
:
851 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
852 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
854 def depart_enumerated_list(self
, node
):
855 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
856 self
.body
.append('</ol>\n')
858 def visit_field(self
, node
):
859 self
.body
.append(self
.starttag(node
, 'tr', '', CLASS
='field'))
861 def depart_field(self
, node
):
862 self
.body
.append('</tr>\n')
864 def visit_field_body(self
, node
):
865 self
.body
.append(self
.starttag(node
, 'td', '', CLASS
='field-body'))
866 self
.set_class_on_child(node
, 'first', 0)
868 if (self
.compact_field_list
or
869 isinstance(field
.parent
, nodes
.docinfo
) or
870 field
.parent
.index(field
) == len(field
.parent
) - 1):
871 # If we are in a compact list, the docinfo, or if this is
872 # the last field of the field list, do not add vertical
873 # space after last element.
874 self
.set_class_on_child(node
, 'last', -1)
876 def depart_field_body(self
, node
):
877 self
.body
.append('</td>\n')
879 def visit_field_list(self
, node
):
880 self
.context
.append((self
.compact_field_list
, self
.compact_p
))
881 self
.compact_p
= None
882 if 'compact' in node
['classes']:
883 self
.compact_field_list
= True
884 elif (self
.settings
.compact_field_lists
885 and 'open' not in node
['classes']):
886 self
.compact_field_list
= True
887 if self
.compact_field_list
:
889 field_body
= field
[-1]
890 assert isinstance(field_body
, nodes
.field_body
)
891 children
= [n
for n
in field_body
892 if not isinstance(n
, nodes
.Invisible
)]
893 if not (len(children
) == 0 or
894 len(children
) == 1 and
895 isinstance(children
[0],
896 (nodes
.paragraph
, nodes
.line_block
))):
897 self
.compact_field_list
= False
899 self
.body
.append(self
.starttag(node
, 'table', frame
='void',
901 CLASS
='docutils field-list'))
902 self
.body
.append('<col class="field-name" />\n'
903 '<col class="field-body" />\n'
904 '<tbody valign="top">\n')
906 def depart_field_list(self
, node
):
907 self
.body
.append('</tbody>\n</table>\n')
908 self
.compact_field_list
, self
.compact_p
= self
.context
.pop()
910 def visit_field_name(self
, node
):
913 atts
['class'] = 'docinfo-name'
915 atts
['class'] = 'field-name'
916 if ( self
.settings
.field_name_limit
917 and len(node
.astext()) > self
.settings
.field_name_limit
):
919 self
.context
.append('</tr>\n'
920 + self
.starttag(node
.parent
, 'tr', '',
924 self
.context
.append('')
925 self
.body
.append(self
.starttag(node
, 'th', '', **atts
))
927 def depart_field_name(self
, node
):
928 self
.body
.append(':</th>')
929 self
.body
.append(self
.context
.pop())
931 def visit_figure(self
, node
):
932 atts
= {'class': 'figure'}
933 if node
.get('width'):
934 atts
['style'] = 'width: %s' % node
['width']
935 if node
.get('align'):
936 atts
['class'] += " align-" + node
['align']
937 self
.body
.append(self
.starttag(node
, 'div', **atts
))
939 def depart_figure(self
, node
):
940 self
.body
.append('</div>\n')
942 def visit_footer(self
, node
):
943 self
.context
.append(len(self
.body
))
945 def depart_footer(self
, node
):
946 start
= self
.context
.pop()
947 footer
= [self
.starttag(node
, 'div', CLASS
='footer'),
948 '<hr class="footer" />\n']
949 footer
.extend(self
.body
[start
:])
950 footer
.append('\n</div>\n')
951 self
.footer
.extend(footer
)
952 self
.body_suffix
[:0] = footer
953 del self
.body
[start
:]
955 def visit_footnote(self
, node
):
956 self
.body
.append(self
.starttag(node
, 'table',
957 CLASS
='docutils footnote',
958 frame
="void", rules
="none"))
959 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
960 '<tbody valign="top">\n'
962 self
.footnote_backrefs(node
)
964 def footnote_backrefs(self
, node
):
966 backrefs
= node
['backrefs']
967 if self
.settings
.footnote_backlinks
and backrefs
:
968 if len(backrefs
) == 1:
969 self
.context
.append('')
970 self
.context
.append('</a>')
971 self
.context
.append('<a class="fn-backref" href="#%s">'
974 # Python 2.4 fails with enumerate(backrefs, 1)
975 for (i
, backref
) in enumerate(backrefs
):
976 backlinks
.append('<a class="fn-backref" href="#%s">%s</a>'
978 self
.context
.append('<em>(%s)</em> ' % ', '.join(backlinks
))
979 self
.context
+= ['', '']
981 self
.context
.append('')
982 self
.context
+= ['', '']
983 # If the node does not only consist of a label.
985 # If there are preceding backlinks, we do not set class
986 # 'first', because we need to retain the top-margin.
988 node
[1]['classes'].append('first')
989 node
[-1]['classes'].append('last')
991 def depart_footnote(self
, node
):
992 self
.body
.append('</td></tr>\n'
993 '</tbody>\n</table>\n')
995 def visit_footnote_reference(self
, node
):
996 href
= '#' + node
['refid']
997 format
= self
.settings
.footnote_references
998 if format
== 'brackets':
1000 self
.context
.append(']')
1002 assert format
== 'superscript'
1004 self
.context
.append('</sup>')
1005 self
.body
.append(self
.starttag(node
, 'a', suffix
,
1006 CLASS
='footnote-reference', href
=href
))
1008 def depart_footnote_reference(self
, node
):
1009 self
.body
.append(self
.context
.pop() + '</a>')
1011 def visit_generated(self
, node
):
1014 def depart_generated(self
, node
):
1017 def visit_header(self
, node
):
1018 self
.context
.append(len(self
.body
))
1020 def depart_header(self
, node
):
1021 start
= self
.context
.pop()
1022 header
= [self
.starttag(node
, 'div', CLASS
='header')]
1023 header
.extend(self
.body
[start
:])
1024 header
.append('\n<hr class="header"/>\n</div>\n')
1025 self
.body_prefix
.extend(header
)
1026 self
.header
.extend(header
)
1027 del self
.body
[start
:]
1029 # Image types to place in an <object> element
1030 # SVG not supported by IE up to version 8
1031 # (html4css1 strives for IE6 compatibility)
1032 object_image_types
= {'.svg': 'image/svg+xml',
1033 '.swf': 'application/x-shockwave-flash'}
1035 def visit_image(self
, node
):
1038 ext
= os
.path
.splitext(uri
)[1].lower()
1039 if ext
in self
.object_image_types
: # ('.svg', '.swf'):
1041 atts
['type'] = self
.object_image_types
[ext
]
1044 atts
['alt'] = node
.get('alt', uri
)
1047 atts
['width'] = node
['width']
1048 if 'height' in node
:
1049 atts
['height'] = node
['height']
1051 if (PIL
and not ('width' in node
and 'height' in node
)
1052 and self
.settings
.file_insertion_enabled
):
1053 imagepath
= urllib
.url2pathname(uri
)
1055 img
= PIL
.Image
.open(
1056 imagepath
.encode(sys
.getfilesystemencoding()))
1057 except (IOError, UnicodeEncodeError):
1060 self
.settings
.record_dependencies
.add(
1061 imagepath
.replace('\\', '/'))
1062 if 'width' not in atts
:
1063 atts
['width'] = '%dpx' % img
.size
[0]
1064 if 'height' not in atts
:
1065 atts
['height'] = '%dpx' % img
.size
[1]
1067 for att_name
in 'width', 'height':
1068 if att_name
in atts
:
1069 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
1071 atts
[att_name
] = '%s%s' % (
1072 float(match
.group(1)) * (float(node
['scale']) / 100),
1075 for att_name
in 'width', 'height':
1076 if att_name
in atts
:
1077 if re
.match(r
'^[0-9.]+$', atts
[att_name
]):
1078 # Interpret unitless values as pixels.
1079 atts
[att_name
] += 'px'
1080 style
.append('%s: %s;' % (att_name
, atts
[att_name
]))
1083 atts
['style'] = ' '.join(style
)
1084 if (isinstance(node
.parent
, nodes
.TextElement
) or
1085 (isinstance(node
.parent
, nodes
.reference
) and
1086 not isinstance(node
.parent
.parent
, nodes
.TextElement
))):
1087 # Inline context or surrounded by <a>...</a>.
1092 atts
['class'] = 'align-%s' % node
['align']
1093 if ext
in self
.object_image_types
: # ('.svg', '.swf')
1094 # do NOT use an empty tag: incorrect rendering in browsers
1095 self
.body
.append(self
.starttag(node
, 'object', suffix
, **atts
) +
1096 node
.get('alt', uri
) + '</object>' + suffix
)
1098 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
1100 def depart_image(self
, node
):
1101 # self.body.append(self.context.pop())
1104 def visit_inline(self
, node
):
1105 self
.body
.append(self
.starttag(node
, 'span', ''))
1107 def depart_inline(self
, node
):
1108 self
.body
.append('</span>')
1110 def visit_label(self
, node
):
1111 # Context added in footnote_backrefs.
1112 self
.body
.append(self
.starttag(node
, 'td', '%s[' % self
.context
.pop(),
1115 def depart_label(self
, node
):
1116 # Context added in footnote_backrefs.
1117 self
.body
.append(']%s</td><td>%s' % (self
.context
.pop(), self
.context
.pop()))
1119 def visit_legend(self
, node
):
1120 self
.body
.append(self
.starttag(node
, 'div', CLASS
='legend'))
1122 def depart_legend(self
, node
):
1123 self
.body
.append('</div>\n')
1125 def visit_line(self
, node
):
1126 self
.body
.append(self
.starttag(node
, 'div', suffix
='', CLASS
='line'))
1128 self
.body
.append('<br />')
1130 def depart_line(self
, node
):
1131 self
.body
.append('</div>\n')
1133 def visit_line_block(self
, node
):
1134 self
.body
.append(self
.starttag(node
, 'div', CLASS
='line-block'))
1136 def depart_line_block(self
, node
):
1137 self
.body
.append('</div>\n')
1139 def visit_list_item(self
, node
):
1140 self
.body
.append(self
.starttag(node
, 'li', ''))
1142 node
[0]['classes'].append('first')
1144 def depart_list_item(self
, node
):
1145 self
.body
.append('</li>\n')
1147 def visit_literal(self
, node
):
1148 # special case: "code" role
1149 classes
= node
.get('classes', [])
1150 if 'code' in classes
:
1151 # filter 'code' from class arguments
1152 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
1153 self
.body
.append(self
.starttag(node
, 'code', ''))
1156 self
.starttag(node
, 'tt', '', CLASS
='docutils literal'))
1157 text
= node
.astext()
1158 for token
in self
.words_and_spaces
.findall(text
):
1160 # Protect text like "--an-option" and the regular expression
1161 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1162 if self
.sollbruchstelle
.search(token
):
1163 self
.body
.append('<span class="pre">%s</span>'
1164 % self
.encode(token
))
1166 self
.body
.append(self
.encode(token
))
1167 elif token
in ('\n', ' '):
1168 # Allow breaks at whitespace:
1169 self
.body
.append(token
)
1171 # Protect runs of multiple spaces; the last space can wrap:
1172 self
.body
.append(' ' * (len(token
) - 1) + ' ')
1173 self
.body
.append('</tt>')
1174 # Content already processed:
1175 raise nodes
.SkipNode
1177 def depart_literal(self
, node
):
1178 # skipped unless literal element is from "code" role:
1179 self
.body
.append('</code>')
1181 def visit_literal_block(self
, node
):
1182 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='literal-block'))
1184 def depart_literal_block(self
, node
):
1185 self
.body
.append('\n</pre>\n')
1187 def visit_math(self
, node
, math_env
=''):
1188 # If the method is called from visit_math_block(), math_env != ''.
1190 # As there is no native HTML math support, we provide alternatives:
1191 # LaTeX and MathJax math_output modes simply wrap the content,
1192 # HTML and MathML math_output modes also convert the math_code.
1193 if self
.math_output
not in ('mathml', 'html', 'mathjax', 'latex'):
1194 self
.document
.reporter
.error(
1195 'math-output format "%s" not supported '
1196 'falling back to "latex"'% self
.math_output
)
1197 self
.math_output
= 'latex'
1200 tags
= {# math_output: (block, inline, class-arguments)
1201 'mathml': ('div', '', ''),
1202 'html': ('div', 'span', 'formula'),
1203 'mathjax': ('div', 'span', 'math'),
1204 'latex': ('pre', 'tt', 'math'),
1206 tag
= tags
[self
.math_output
][math_env
== '']
1207 clsarg
= tags
[self
.math_output
][2]
1209 wrappers
= {# math_mode: (inline, block)
1210 'mathml': (None, None),
1211 'html': ('$%s$', u
'\\begin{%s}\n%s\n\\end{%s}'),
1212 'mathjax': ('\(%s\)', u
'\\begin{%s}\n%s\n\\end{%s}'),
1213 'latex': (None, None),
1215 wrapper
= wrappers
[self
.math_output
][math_env
!= '']
1216 # get and wrap content
1217 math_code
= node
.astext().translate(unichar2tex
.uni2tex_table
)
1218 if wrapper
and math_env
:
1219 math_code
= wrapper
% (math_env
, math_code
, math_env
)
1221 math_code
= wrapper
% math_code
1222 # settings and conversion
1223 if self
.math_output
in ('latex', 'mathjax'):
1224 math_code
= self
.encode(math_code
)
1225 if self
.math_output
== 'mathjax' and not self
.math_header
:
1226 if self
.math_output_options
:
1227 self
.mathjax_url
= self
.math_output_options
[0]
1228 self
.math_header
= [self
.mathjax_script
% self
.mathjax_url
]
1229 elif self
.math_output
== 'html':
1230 if self
.math_output_options
and not self
.math_header
:
1231 self
.math_header
= [self
.stylesheet_call(
1232 utils
.find_file_in_dirs(s
, self
.settings
.stylesheet_dirs
))
1233 for s
in self
.math_output_options
[0].split(',')]
1234 # TODO: fix display mode in matrices and fractions
1235 math2html
.DocumentParameters
.displaymode
= (math_env
!= '')
1236 math_code
= math2html
.math2html(math_code
)
1237 elif self
.math_output
== 'mathml':
1238 self
.doctype
= self
.doctype_mathml
1239 self
.content_type
= self
.content_type_mathml
1241 mathml_tree
= parse_latex_math(math_code
, inline
=not(math_env
))
1242 math_code
= ''.join(mathml_tree
.xml())
1243 except SyntaxError, err
:
1244 err_node
= self
.document
.reporter
.error(err
, base_node
=node
)
1245 self
.visit_system_message(err_node
)
1246 self
.body
.append(self
.starttag(node
, 'p'))
1247 self
.body
.append(u
','.join(err
.args
))
1248 self
.body
.append('</p>\n')
1249 self
.body
.append(self
.starttag(node
, 'pre',
1250 CLASS
='literal-block'))
1251 self
.body
.append(self
.encode(math_code
))
1252 self
.body
.append('\n</pre>\n')
1253 self
.depart_system_message(err_node
)
1254 raise nodes
.SkipNode
1255 # append to document body
1257 self
.body
.append(self
.starttag(node
, tag
,
1258 suffix
='\n'*bool(math_env
),
1260 self
.body
.append(math_code
)
1261 if math_env
: # block mode (equation, display)
1262 self
.body
.append('\n')
1264 self
.body
.append('</%s>' % tag
)
1266 self
.body
.append('\n')
1267 # Content already processed:
1268 raise nodes
.SkipNode
1270 def depart_math(self
, node
):
1271 pass # never reached
1273 def visit_math_block(self
, node
):
1274 # print node.astext().encode('utf8')
1275 math_env
= pick_math_environment(node
.astext())
1276 self
.visit_math(node
, math_env
=math_env
)
1278 def depart_math_block(self
, node
):
1279 pass # never reached
1281 def visit_meta(self
, node
):
1282 meta
= self
.emptytag(node
, 'meta', **node
.non_default_attributes())
1285 def depart_meta(self
, node
):
1288 def add_meta(self
, tag
):
1289 self
.meta
.append(tag
)
1290 self
.head
.append(tag
)
1292 def visit_option(self
, node
):
1293 if self
.context
[-1]:
1294 self
.body
.append(', ')
1295 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='option'))
1297 def depart_option(self
, node
):
1298 self
.body
.append('</span>')
1299 self
.context
[-1] += 1
1301 def visit_option_argument(self
, node
):
1302 self
.body
.append(node
.get('delimiter', ' '))
1303 self
.body
.append(self
.starttag(node
, 'var', ''))
1305 def depart_option_argument(self
, node
):
1306 self
.body
.append('</var>')
1308 def visit_option_group(self
, node
):
1310 if ( self
.settings
.option_limit
1311 and len(node
.astext()) > self
.settings
.option_limit
):
1313 self
.context
.append('</tr>\n<tr><td> </td>')
1315 self
.context
.append('')
1317 self
.starttag(node
, 'td', CLASS
='option-group', **atts
))
1318 self
.body
.append('<kbd>')
1319 self
.context
.append(0) # count number of options
1321 def depart_option_group(self
, node
):
1323 self
.body
.append('</kbd></td>\n')
1324 self
.body
.append(self
.context
.pop())
1326 def visit_option_list(self
, node
):
1328 self
.starttag(node
, 'table', CLASS
='docutils option-list',
1329 frame
="void", rules
="none"))
1330 self
.body
.append('<col class="option" />\n'
1331 '<col class="description" />\n'
1332 '<tbody valign="top">\n')
1334 def depart_option_list(self
, node
):
1335 self
.body
.append('</tbody>\n</table>\n')
1337 def visit_option_list_item(self
, node
):
1338 self
.body
.append(self
.starttag(node
, 'tr', ''))
1340 def depart_option_list_item(self
, node
):
1341 self
.body
.append('</tr>\n')
1343 def visit_option_string(self
, node
):
1346 def depart_option_string(self
, node
):
1349 def visit_organization(self
, node
):
1350 self
.visit_docinfo_item(node
, 'organization')
1352 def depart_organization(self
, node
):
1353 self
.depart_docinfo_item()
1355 def should_be_compact_paragraph(self
, node
):
1357 Determine if the <p> tags around paragraph ``node`` can be omitted.
1359 if (isinstance(node
.parent
, nodes
.document
) or
1360 isinstance(node
.parent
, nodes
.compound
)):
1361 # Never compact paragraphs in document or compound.
1363 for key
, value
in node
.attlist():
1364 if (node
.is_not_default(key
) and
1365 not (key
== 'classes' and value
in
1366 ([], ['first'], ['last'], ['first', 'last']))):
1367 # Attribute which needs to survive.
1369 first
= isinstance(node
.parent
[0], nodes
.label
) # skip label
1370 for child
in node
.parent
.children
[first
:]:
1371 # only first paragraph can be compact
1372 if isinstance(child
, nodes
.Invisible
):
1377 parent_length
= len([n
for n
in node
.parent
if not isinstance(
1378 n
, (nodes
.Invisible
, nodes
.label
))])
1379 if ( self
.compact_simple
1380 or self
.compact_field_list
1381 or self
.compact_p
and parent_length
== 1):
1385 def visit_paragraph(self
, node
):
1386 if self
.should_be_compact_paragraph(node
):
1387 self
.context
.append('')
1389 self
.body
.append(self
.starttag(node
, 'p', ''))
1390 self
.context
.append('</p>\n')
1392 def depart_paragraph(self
, node
):
1393 self
.body
.append(self
.context
.pop())
1395 def visit_problematic(self
, node
):
1396 if node
.hasattr('refid'):
1397 self
.body
.append('<a href="#%s">' % node
['refid'])
1398 self
.context
.append('</a>')
1400 self
.context
.append('')
1401 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='problematic'))
1403 def depart_problematic(self
, node
):
1404 self
.body
.append('</span>')
1405 self
.body
.append(self
.context
.pop())
1407 def visit_raw(self
, node
):
1408 if 'html' in node
.get('format', '').split():
1409 t
= isinstance(node
.parent
, nodes
.TextElement
) and 'span' or 'div'
1411 self
.body
.append(self
.starttag(node
, t
, suffix
=''))
1412 self
.body
.append(node
.astext())
1414 self
.body
.append('</%s>' % t
)
1415 # Keep non-HTML raw text out of output:
1416 raise nodes
.SkipNode
1418 def visit_reference(self
, node
):
1419 atts
= {'class': 'reference'}
1420 if 'refuri' in node
:
1421 atts
['href'] = node
['refuri']
1422 if ( self
.settings
.cloak_email_addresses
1423 and atts
['href'].startswith('mailto:')):
1424 atts
['href'] = self
.cloak_mailto(atts
['href'])
1425 self
.in_mailto
= True
1426 atts
['class'] += ' external'
1428 assert 'refid' in node
, \
1429 'References must have "refuri" or "refid" attribute.'
1430 atts
['href'] = '#' + node
['refid']
1431 atts
['class'] += ' internal'
1432 if not isinstance(node
.parent
, nodes
.TextElement
):
1433 assert len(node
) == 1 and isinstance(node
[0], nodes
.image
)
1434 atts
['class'] += ' image-reference'
1435 self
.body
.append(self
.starttag(node
, 'a', '', **atts
))
1437 def depart_reference(self
, node
):
1438 self
.body
.append('</a>')
1439 if not isinstance(node
.parent
, nodes
.TextElement
):
1440 self
.body
.append('\n')
1441 self
.in_mailto
= False
1443 def visit_revision(self
, node
):
1444 self
.visit_docinfo_item(node
, 'revision', meta
=False)
1446 def depart_revision(self
, node
):
1447 self
.depart_docinfo_item()
1449 def visit_row(self
, node
):
1450 self
.body
.append(self
.starttag(node
, 'tr', ''))
1453 def depart_row(self
, node
):
1454 self
.body
.append('</tr>\n')
1456 def visit_rubric(self
, node
):
1457 self
.body
.append(self
.starttag(node
, 'p', '', CLASS
='rubric'))
1459 def depart_rubric(self
, node
):
1460 self
.body
.append('</p>\n')
1462 def visit_section(self
, node
):
1463 self
.section_level
+= 1
1465 self
.starttag(node
, 'div', CLASS
='section'))
1467 def depart_section(self
, node
):
1468 self
.section_level
-= 1
1469 self
.body
.append('</div>\n')
1471 def visit_sidebar(self
, node
):
1473 self
.starttag(node
, 'div', CLASS
='sidebar'))
1474 self
.set_first_last(node
)
1475 self
.in_sidebar
= True
1477 def depart_sidebar(self
, node
):
1478 self
.body
.append('</div>\n')
1479 self
.in_sidebar
= False
1481 def visit_status(self
, node
):
1482 self
.visit_docinfo_item(node
, 'status', meta
=False)
1484 def depart_status(self
, node
):
1485 self
.depart_docinfo_item()
1487 def visit_strong(self
, node
):
1488 self
.body
.append(self
.starttag(node
, 'strong', ''))
1490 def depart_strong(self
, node
):
1491 self
.body
.append('</strong>')
1493 def visit_subscript(self
, node
):
1494 self
.body
.append(self
.starttag(node
, 'sub', ''))
1496 def depart_subscript(self
, node
):
1497 self
.body
.append('</sub>')
1499 def visit_substitution_definition(self
, node
):
1500 """Internal only."""
1501 raise nodes
.SkipNode
1503 def visit_substitution_reference(self
, node
):
1504 self
.unimplemented_visit(node
)
1506 def visit_subtitle(self
, node
):
1507 if isinstance(node
.parent
, nodes
.sidebar
):
1508 self
.body
.append(self
.starttag(node
, 'p', '',
1509 CLASS
='sidebar-subtitle'))
1510 self
.context
.append('</p>\n')
1511 elif isinstance(node
.parent
, nodes
.document
):
1512 self
.body
.append(self
.starttag(node
, 'h2', '', CLASS
='subtitle'))
1513 self
.context
.append('</h2>\n')
1514 self
.in_document_title
= len(self
.body
)
1515 elif isinstance(node
.parent
, nodes
.section
):
1516 tag
= 'h%s' % (self
.section_level
+ self
.initial_header_level
- 1)
1518 self
.starttag(node
, tag
, '', CLASS
='section-subtitle') +
1519 self
.starttag({}, 'span', '', CLASS
='section-subtitle'))
1520 self
.context
.append('</span></%s>\n' % tag
)
1522 def depart_subtitle(self
, node
):
1523 self
.body
.append(self
.context
.pop())
1524 if self
.in_document_title
:
1525 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
1526 self
.in_document_title
= 0
1527 self
.body_pre_docinfo
.extend(self
.body
)
1528 self
.html_subtitle
.extend(self
.body
)
1531 def visit_superscript(self
, node
):
1532 self
.body
.append(self
.starttag(node
, 'sup', ''))
1534 def depart_superscript(self
, node
):
1535 self
.body
.append('</sup>')
1537 def visit_system_message(self
, node
):
1538 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
1539 self
.body
.append('<p class="system-message-title">')
1541 if len(node
['backrefs']):
1542 backrefs
= node
['backrefs']
1543 if len(backrefs
) == 1:
1544 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
1549 for backref
in backrefs
:
1550 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
1552 backref_text
= ('; <em>backlinks: %s</em>'
1553 % ', '.join(backlinks
))
1554 if node
.hasattr('line'):
1555 line
= ', line %s' % node
['line']
1558 self
.body
.append('System Message: %s/%s '
1559 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
1560 % (node
['type'], node
['level'],
1561 self
.encode(node
['source']), line
, backref_text
))
1563 def depart_system_message(self
, node
):
1564 self
.body
.append('</div>\n')
1566 def visit_table(self
, node
):
1567 self
.context
.append(self
.compact_p
)
1568 self
.compact_p
= True
1569 classes
= ' '.join(['docutils', self
.settings
.table_style
]).strip()
1571 self
.starttag(node
, 'table', CLASS
=classes
, border
="1"))
1573 def depart_table(self
, node
):
1574 self
.compact_p
= self
.context
.pop()
1575 self
.body
.append('</table>\n')
1577 def visit_target(self
, node
):
1578 if not ('refuri' in node
or 'refid' in node
1579 or 'refname' in node
):
1580 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='target'))
1581 self
.context
.append('</span>')
1583 self
.context
.append('')
1585 def depart_target(self
, node
):
1586 self
.body
.append(self
.context
.pop())
1588 def visit_tbody(self
, node
):
1589 self
.write_colspecs()
1590 self
.body
.append(self
.context
.pop()) # '</colgroup>\n' or ''
1591 self
.body
.append(self
.starttag(node
, 'tbody', valign
='top'))
1593 def depart_tbody(self
, node
):
1594 self
.body
.append('</tbody>\n')
1596 def visit_term(self
, node
):
1597 self
.body
.append(self
.starttag(node
, 'dt', ''))
1599 def depart_term(self
, node
):
1601 Leave the end tag to `self.visit_definition()`, in case there's a
1606 def visit_tgroup(self
, node
):
1607 # Mozilla needs <colgroup>:
1608 self
.body
.append(self
.starttag(node
, 'colgroup'))
1609 # Appended by thead or tbody:
1610 self
.context
.append('</colgroup>\n')
1613 def depart_tgroup(self
, node
):
1616 def visit_thead(self
, node
):
1617 self
.write_colspecs()
1618 self
.body
.append(self
.context
.pop()) # '</colgroup>\n'
1619 # There may or may not be a <thead>; this is for <tbody> to use:
1620 self
.context
.append('')
1621 self
.body
.append(self
.starttag(node
, 'thead', valign
='bottom'))
1623 def depart_thead(self
, node
):
1624 self
.body
.append('</thead>\n')
1626 def visit_title(self
, node
):
1627 """Only 6 section levels are supported by HTML."""
1628 check_id
= 0 # TODO: is this a bool (False) or a counter?
1629 close_tag
= '</p>\n'
1630 if isinstance(node
.parent
, nodes
.topic
):
1632 self
.starttag(node
, 'p', '', CLASS
='topic-title first'))
1633 elif isinstance(node
.parent
, nodes
.sidebar
):
1635 self
.starttag(node
, 'p', '', CLASS
='sidebar-title'))
1636 elif isinstance(node
.parent
, nodes
.Admonition
):
1638 self
.starttag(node
, 'p', '', CLASS
='admonition-title'))
1639 elif isinstance(node
.parent
, nodes
.table
):
1641 self
.starttag(node
, 'caption', ''))
1642 close_tag
= '</caption>\n'
1643 elif isinstance(node
.parent
, nodes
.document
):
1644 self
.body
.append(self
.starttag(node
, 'h1', '', CLASS
='title'))
1645 close_tag
= '</h1>\n'
1646 self
.in_document_title
= len(self
.body
)
1648 assert isinstance(node
.parent
, nodes
.section
)
1649 h_level
= self
.section_level
+ self
.initial_header_level
- 1
1651 if (len(node
.parent
) >= 2 and
1652 isinstance(node
.parent
[1], nodes
.subtitle
)):
1653 atts
['CLASS'] = 'with-subtitle'
1655 self
.starttag(node
, 'h%s' % h_level
, '', **atts
))
1657 if node
.hasattr('refid'):
1658 atts
['class'] = 'toc-backref'
1659 atts
['href'] = '#' + node
['refid']
1661 self
.body
.append(self
.starttag({}, 'a', '', **atts
))
1662 close_tag
= '</a></h%s>\n' % (h_level
)
1664 close_tag
= '</h%s>\n' % (h_level
)
1665 self
.context
.append(close_tag
)
1667 def depart_title(self
, node
):
1668 self
.body
.append(self
.context
.pop())
1669 if self
.in_document_title
:
1670 self
.title
= self
.body
[self
.in_document_title
:-1]
1671 self
.in_document_title
= 0
1672 self
.body_pre_docinfo
.extend(self
.body
)
1673 self
.html_title
.extend(self
.body
)
1676 def visit_title_reference(self
, node
):
1677 self
.body
.append(self
.starttag(node
, 'cite', ''))
1679 def depart_title_reference(self
, node
):
1680 self
.body
.append('</cite>')
1682 def visit_topic(self
, node
):
1683 self
.body
.append(self
.starttag(node
, 'div', CLASS
='topic'))
1684 self
.topic_classes
= node
['classes']
1685 # TODO: replace with ::
1686 # self.in_contents = 'contents' in node['classes']
1688 def depart_topic(self
, node
):
1689 self
.body
.append('</div>\n')
1690 self
.topic_classes
= []
1691 # TODO self.in_contents = False
1693 def visit_transition(self
, node
):
1694 self
.body
.append(self
.emptytag(node
, 'hr', CLASS
='docutils'))
1696 def depart_transition(self
, node
):
1699 def visit_version(self
, node
):
1700 self
.visit_docinfo_item(node
, 'version', meta
=False)
1702 def depart_version(self
, node
):
1703 self
.depart_docinfo_item()
1705 def unimplemented_visit(self
, node
):
1706 raise NotImplementedError('visiting unimplemented node type: %s'
1707 % node
.__class
__.__name
__)
1710 class SimpleListChecker(nodes
.GenericNodeVisitor
):
1713 Raise `nodes.NodeFound` if non-simple list item is encountered.
1715 Here "simple" means a list item containing nothing other than a single
1716 paragraph, a simple list, or a paragraph followed by a simple list.
1719 def default_visit(self
, node
):
1720 raise nodes
.NodeFound
1722 def visit_bullet_list(self
, node
):
1725 def visit_enumerated_list(self
, node
):
1728 def visit_list_item(self
, node
):
1730 for child
in node
.children
:
1731 if not isinstance(child
, nodes
.Invisible
):
1732 children
.append(child
)
1733 if (children
and isinstance(children
[0], nodes
.paragraph
)
1734 and (isinstance(children
[-1], nodes
.bullet_list
)
1735 or isinstance(children
[-1], nodes
.enumerated_list
))):
1737 if len(children
) <= 1:
1740 raise nodes
.NodeFound
1742 def visit_paragraph(self
, node
):
1743 raise nodes
.SkipNode
1745 def invisible_visit(self
, node
):
1746 """Invisible nodes should be ignored."""
1747 raise nodes
.SkipNode
1749 visit_comment
= invisible_visit
1750 visit_substitution_definition
= invisible_visit
1751 visit_target
= invisible_visit
1752 visit_pending
= invisible_visit