Stop converting text to full capitals (bug #481).
[docutils.git] / docutils / docutils / writers / html4css1 / __init__.py
blobdb72fad94e8b85ac8e9cbcc058fb8b895022517c
1 # $Id$
2 # Author: David Goodger
3 # Maintainer: docutils-develop@lists.sourceforge.net
4 # Copyright: This module has been placed in the public domain.
6 """
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.
13 """
15 __docformat__ = 'reStructuredText'
17 import os.path
18 import re
20 from docutils import frontend, nodes, writers
21 from docutils.writers import _html_base
22 from docutils.writers._html_base import PIL
25 class Writer(writers._html_base.Writer):
27 supported = ('html', 'html4', 'html4css1', 'xhtml', 'xhtml10')
28 """Formats this writer supports."""
30 default_stylesheets = ['html4css1.css']
31 default_stylesheet_dirs = ['.',
32 os.path.abspath(os.path.dirname(__file__)),
33 os.path.abspath(os.path.join(
34 os.path.dirname(os.path.dirname(__file__)),
35 'html5_polyglot')) # for math.css
37 default_template = os.path.join(
38 os.path.dirname(os.path.abspath(__file__)), 'template.txt')
40 # use a copy of the parent spec with some modifications
41 settings_spec = frontend.filter_settings_spec(
42 writers._html_base.Writer.settings_spec,
43 template=(
44 'Template file. (UTF-8 encoded, default: "%s")' % default_template,
45 ['--template'],
46 {'default': default_template, 'metavar': '<file>'}),
47 stylesheet_path=(
48 'Comma separated list of stylesheet paths. '
49 'Relative paths are expanded if a matching file is found in '
50 'the --stylesheet-dirs. With --link-stylesheet, '
51 'the path is rewritten relative to the output HTML file. '
52 '(default: "%s")' % ','.join(default_stylesheets),
53 ['--stylesheet-path'],
54 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
55 'validator': frontend.validate_comma_separated_list,
56 'default': default_stylesheets}),
57 stylesheet_dirs=(
58 'Comma-separated list of directories where stylesheets are found. '
59 'Used by --stylesheet-path when expanding relative path '
60 'arguments. (default: "%s")' % ','.join(default_stylesheet_dirs),
61 ['--stylesheet-dirs'],
62 {'metavar': '<dir[,dir,...]>',
63 'validator': frontend.validate_comma_separated_list,
64 'default': default_stylesheet_dirs}),
65 initial_header_level=(
66 'Specify the initial header level. Does not affect document '
67 'title & subtitle (see --no-doc-title). (default: 1 for "<h1>")',
68 ['--initial-header-level'],
69 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
70 'metavar': '<level>'}),
71 xml_declaration=(
72 'Prepend an XML declaration (default). ',
73 ['--xml-declaration'],
74 {'default': True, 'action': 'store_true',
75 'validator': frontend.validate_boolean}),
77 settings_spec = settings_spec + (
78 'HTML4 Writer Options',
79 '',
80 (('Specify the maximum width (in characters) for one-column field '
81 'names. Longer field names will span an entire row of the table '
82 'used to render the field list. Default is 14 characters. '
83 'Use 0 for "no limit".',
84 ['--field-name-limit'],
85 {'default': 14, 'metavar': '<level>',
86 'validator': frontend.validate_nonnegative_int}),
87 ('Specify the maximum width (in characters) for options in option '
88 'lists. Longer options will span an entire row of the table used '
89 'to render the option list. Default is 14 characters. '
90 'Use 0 for "no limit".',
91 ['--option-limit'],
92 {'default': 14, 'metavar': '<level>',
93 'validator': frontend.validate_nonnegative_int}),
97 config_section = 'html4css1 writer'
99 def __init__(self):
100 self.parts = {}
101 self.translator_class = HTMLTranslator
104 class HTMLTranslator(writers._html_base.HTMLTranslator):
106 The html4css1 writer has been optimized to produce visually compact
107 lists (less vertical whitespace). HTML's mixed content models
108 allow list items to contain "<li><p>body elements</p></li>" or
109 "<li>just text</li>" or even "<li>text<p>and body
110 elements</p>combined</li>", each with different effects. It would
111 be best to stick with strict body elements in list items, but they
112 affect vertical spacing in older browsers (although they really
113 shouldn't).
114 The html5_polyglot writer solves this using CSS2.
116 Here is an outline of the optimization:
118 - Check for and omit <p> tags in "simple" lists: list items
119 contain either a single paragraph, a nested simple list, or a
120 paragraph followed by a nested simple list. This means that
121 this list can be compact:
123 - Item 1.
124 - Item 2.
126 But this list cannot be compact:
128 - Item 1.
130 This second paragraph forces space between list items.
132 - Item 2.
134 - In non-list contexts, omit <p> tags on a paragraph if that
135 paragraph is the only child of its parent (footnotes & citations
136 are allowed a label first).
138 - Regardless of the above, in definitions, table cells, field bodies,
139 option descriptions, and list items, mark the first child with
140 'class="first"' and the last child with 'class="last"'. The stylesheet
141 sets the margins (top & bottom respectively) to 0 for these elements.
143 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
144 option) disables list whitespace optimization.
147 # The following definitions are required for display in browsers limited
148 # to CSS1 or backwards compatible behaviour of the writer:
150 doctype = (
151 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
152 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
154 content_type = ('<meta http-equiv="Content-Type"'
155 ' content="text/html; charset=%s" />\n')
156 content_type_mathml = ('<meta http-equiv="Content-Type"'
157 ' content="application/xhtml+xml; charset=%s" />\n')
159 # encode also non-breaking space
160 special_characters = _html_base.HTMLTranslator.special_characters.copy()
161 special_characters[0xa0] = '&nbsp;'
163 # use character reference for dash (not valid in HTML5)
164 attribution_formats = {'dash': ('&mdash;', ''),
165 'parentheses': ('(', ')'),
166 'parens': ('(', ')'),
167 'none': ('', '')}
169 # ersatz for first/last pseudo-classes missing in CSS1
170 def set_first_last(self, node):
171 self.set_class_on_child(node, 'first', 0)
172 self.set_class_on_child(node, 'last', -1)
174 # add newline after opening tag
175 def visit_address(self, node):
176 self.visit_docinfo_item(node, 'address', meta=False)
177 self.body.append(self.starttag(node, 'pre', CLASS='address'))
179 def depart_address(self, node):
180 self.body.append('\n</pre>\n')
181 self.depart_docinfo_item()
183 # ersatz for first/last pseudo-classes
184 def visit_admonition(self, node):
185 node['classes'].insert(0, 'admonition')
186 self.body.append(self.starttag(node, 'div'))
187 self.set_first_last(node)
189 def depart_admonition(self, node=None):
190 self.body.append('</div>\n')
192 # author, authors: use <br> instead of paragraphs
193 def visit_author(self, node):
194 if isinstance(node.parent, nodes.authors):
195 if self.author_in_authors:
196 self.body.append('\n<br />')
197 else:
198 self.visit_docinfo_item(node, 'author')
200 def depart_author(self, node):
201 if isinstance(node.parent, nodes.authors):
202 self.author_in_authors = True
203 else:
204 self.depart_docinfo_item()
206 def visit_authors(self, node):
207 self.visit_docinfo_item(node, 'authors')
208 self.author_in_authors = False # initialize
210 def depart_authors(self, node):
211 self.depart_docinfo_item()
213 # use "width" argument instead of "style: 'width'":
214 def visit_colspec(self, node):
215 self.colspecs.append(node)
216 # "stubs" list is an attribute of the tgroup element:
217 node.parent.stubs.append(node.attributes.get('stub'))
219 def depart_colspec(self, node):
220 # write out <colgroup> when all colspecs are processed
221 if isinstance(node.next_node(descend=False, siblings=True),
222 nodes.colspec):
223 return
224 if ('colwidths-auto' in node.parent.parent['classes']
225 or ('colwidths-auto' in self.settings.table_style
226 and 'colwidths-given' not in node.parent.parent['classes'])):
227 return
228 total_width = sum(node['colwidth'] for node in self.colspecs)
229 self.body.append(self.starttag(node, 'colgroup'))
230 for node in self.colspecs:
231 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
232 self.body.append(self.emptytag(node, 'col',
233 width='%i%%' % colwidth))
234 self.body.append('</colgroup>\n')
236 # Compact lists:
237 # exclude definition lists and field lists (non-compact by default)
239 def is_compactable(self, node):
240 return ('compact' in node['classes']
241 or (self.settings.compact_lists
242 and 'open' not in node['classes']
243 and (self.compact_simple
244 or 'contents' in node.parent['classes']
245 # TODO: self.in_contents
246 or self.check_simple_list(node))))
248 # citations: Use table for bibliographic references.
249 def visit_citation(self, node):
250 self.body.append(self.starttag(node, 'table',
251 CLASS='docutils citation',
252 frame="void", rules="none"))
253 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
254 '<tbody valign="top">\n'
255 '<tr>')
256 self.footnote_backrefs(node)
258 def depart_citation(self, node):
259 self.body.append('</td></tr>\n'
260 '</tbody>\n</table>\n')
262 def visit_citation_reference(self, node):
263 href = '#'
264 if 'refid' in node:
265 href += node['refid']
266 elif 'refname' in node:
267 href += self.document.nameids[node['refname']]
268 self.body.append(self.starttag(node, 'a', suffix='[', href=href,
269 classes=['citation-reference']))
271 def depart_citation_reference(self, node):
272 self.body.append(']</a>')
274 # insert classifier-delimiter (not required with CSS2)
275 def visit_classifier(self, node):
276 self.body.append(' <span class="classifier-delimiter">:</span> ')
277 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
279 def depart_classifier(self, node):
280 self.body.append('</span>')
281 self.depart_term(node) # close the <dt> after last classifier
283 # ersatz for first/last pseudo-classes
284 def visit_compound(self, node):
285 self.body.append(self.starttag(node, 'div', CLASS='compound'))
286 if len(node) > 1:
287 node[0]['classes'].append('compound-first')
288 node[-1]['classes'].append('compound-last')
289 for child in node[1:-1]:
290 child['classes'].append('compound-middle')
292 def depart_compound(self, node):
293 self.body.append('</div>\n')
295 # ersatz for first/last pseudo-classes, no special handling of "details"
296 def visit_definition(self, node):
297 self.body.append(self.starttag(node, 'dd', ''))
298 self.set_first_last(node)
300 def depart_definition(self, node):
301 self.body.append('</dd>\n')
303 # don't add "simple" class value, no special handling of "details"
304 def visit_definition_list(self, node):
305 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
307 def depart_definition_list(self, node):
308 self.body.append('</dl>\n')
310 # no special handling of "details"
311 def visit_definition_list_item(self, node):
312 pass
314 def depart_definition_list_item(self, node):
315 pass
317 # use a table for description lists
318 def visit_description(self, node):
319 self.body.append(self.starttag(node, 'td', ''))
320 self.set_first_last(node)
322 def depart_description(self, node):
323 self.body.append('</td>')
325 # use table for docinfo
326 def visit_docinfo(self, node):
327 self.context.append(len(self.body))
328 self.body.append(self.starttag(node, 'table',
329 CLASS='docinfo',
330 frame="void", rules="none"))
331 self.body.append('<col class="docinfo-name" />\n'
332 '<col class="docinfo-content" />\n'
333 '<tbody valign="top">\n')
334 self.in_docinfo = True
336 def depart_docinfo(self, node):
337 self.body.append('</tbody>\n</table>\n')
338 self.in_docinfo = False
339 start = self.context.pop()
340 self.docinfo = self.body[start:]
341 self.body = []
343 def visit_docinfo_item(self, node, name, meta=True):
344 if meta:
345 meta_tag = '<meta name="%s" content="%s" />\n' \
346 % (name, self.attval(node.astext()))
347 self.meta.append(meta_tag)
348 self.body.append(self.starttag(node, 'tr', ''))
349 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
350 % self.language.labels[name])
351 if len(node):
352 if isinstance(node[0], nodes.Element):
353 node[0]['classes'].append('first')
354 if isinstance(node[-1], nodes.Element):
355 node[-1]['classes'].append('last')
357 def depart_docinfo_item(self):
358 self.body.append('</td></tr>\n')
360 # add newline after opening tag
361 def visit_doctest_block(self, node):
362 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
364 def depart_doctest_block(self, node):
365 self.body.append('\n</pre>\n')
367 # insert an NBSP into empty cells, ersatz for first/last
368 def visit_entry(self, node):
369 writers._html_base.HTMLTranslator.visit_entry(self, node)
370 if len(node) == 0: # empty cell
371 self.body.append('&nbsp;')
372 self.set_first_last(node)
374 def depart_entry(self, node):
375 self.body.append(self.context.pop())
377 # ersatz for first/last pseudo-classes
378 def visit_enumerated_list(self, node):
380 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
381 cannot be emulated in CSS1 (HTML 5 reincludes it).
383 atts = {}
384 if 'start' in node:
385 atts['start'] = node['start']
386 if 'enumtype' in node:
387 atts['class'] = node['enumtype']
388 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
389 # single "format" attribute? Use CSS2?
390 old_compact_simple = self.compact_simple
391 self.context.append((self.compact_simple, self.compact_p))
392 self.compact_p = None
393 self.compact_simple = self.is_compactable(node)
394 if self.compact_simple and not old_compact_simple:
395 atts['class'] = (atts.get('class', '') + ' simple').strip()
396 self.body.append(self.starttag(node, 'ol', **atts))
398 def depart_enumerated_list(self, node):
399 self.compact_simple, self.compact_p = self.context.pop()
400 self.body.append('</ol>\n')
402 # use table for field-list:
403 def visit_field(self, node):
404 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
406 def depart_field(self, node):
407 self.body.append('</tr>\n')
409 def visit_field_body(self, node):
410 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
411 self.set_class_on_child(node, 'first', 0)
412 field = node.parent
413 if (self.compact_field_list
414 or isinstance(field.parent, nodes.docinfo)
415 or field.parent.index(field) == len(field.parent) - 1):
416 # If we are in a compact list, the docinfo, or if this is
417 # the last field of the field list, do not add vertical
418 # space after last element.
419 self.set_class_on_child(node, 'last', -1)
421 def depart_field_body(self, node):
422 self.body.append('</td>\n')
424 def visit_field_list(self, node):
425 self.context.append((self.compact_field_list, self.compact_p))
426 self.compact_p = None
427 if 'compact' in node['classes']:
428 self.compact_field_list = True
429 elif (self.settings.compact_field_lists
430 and 'open' not in node['classes']):
431 self.compact_field_list = True
432 if self.compact_field_list:
433 for field in node:
434 field_body = field[-1]
435 assert isinstance(field_body, nodes.field_body)
436 children = [n for n in field_body
437 if not isinstance(n, nodes.Invisible)]
438 if not (len(children) == 0
439 or len(children) == 1
440 and isinstance(children[0],
441 (nodes.paragraph, nodes.line_block))):
442 self.compact_field_list = False
443 break
444 self.body.append(self.starttag(node, 'table', frame='void',
445 rules='none',
446 CLASS='docutils field-list'))
447 self.body.append('<col class="field-name" />\n'
448 '<col class="field-body" />\n'
449 '<tbody valign="top">\n')
451 def depart_field_list(self, node):
452 self.body.append('</tbody>\n</table>\n')
453 self.compact_field_list, self.compact_p = self.context.pop()
455 def visit_field_name(self, node):
456 atts = {}
457 if self.in_docinfo:
458 atts['class'] = 'docinfo-name'
459 else:
460 atts['class'] = 'field-name'
461 if (self.settings.field_name_limit
462 and len(node.astext()) > self.settings.field_name_limit):
463 atts['colspan'] = 2
464 self.context.append('</tr>\n'
465 + self.starttag(node.parent, 'tr', '',
466 CLASS='field')
467 + '<td>&nbsp;</td>')
468 else:
469 self.context.append('')
470 self.body.append(self.starttag(node, 'th', '', **atts))
472 def depart_field_name(self, node):
473 self.body.append(':</th>')
474 self.body.append(self.context.pop())
476 # use table for footnote text
477 def visit_footnote(self, node):
478 self.body.append(self.starttag(node, 'table',
479 CLASS='docutils footnote',
480 frame="void", rules="none"))
481 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
482 '<tbody valign="top">\n'
483 '<tr>')
484 self.footnote_backrefs(node)
486 def footnote_backrefs(self, node):
487 backlinks = []
488 backrefs = node['backrefs']
489 if self.settings.footnote_backlinks and backrefs:
490 if len(backrefs) == 1:
491 self.context.append('')
492 self.context.append('</a>')
493 self.context.append('<a class="fn-backref" href="#%s">'
494 % backrefs[0])
495 else:
496 for (i, backref) in enumerate(backrefs, 1):
497 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
498 % (backref, i))
499 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
500 self.context += ['', '']
501 else:
502 self.context.append('')
503 self.context += ['', '']
504 # If the node does not only consist of a label.
505 if len(node) > 1:
506 # If there are preceding backlinks, we do not set class
507 # 'first', because we need to retain the top-margin.
508 if not backlinks:
509 node[1]['classes'].append('first')
510 node[-1]['classes'].append('last')
512 def depart_footnote(self, node):
513 self.body.append('</td></tr>\n'
514 '</tbody>\n</table>\n')
516 # insert markers in text (pseudo-classes are not supported in CSS1):
517 def visit_footnote_reference(self, node):
518 href = '#' + node['refid']
519 format = self.settings.footnote_references
520 if format == 'brackets':
521 suffix = '['
522 self.context.append(']')
523 else:
524 assert format == 'superscript'
525 suffix = '<sup>'
526 self.context.append('</sup>')
527 self.body.append(self.starttag(node, 'a', suffix,
528 CLASS='footnote-reference', href=href))
530 def depart_footnote_reference(self, node):
531 self.body.append(self.context.pop() + '</a>')
533 # just pass on generated text
534 def visit_generated(self, node):
535 pass
537 # Backwards-compatibility implementation:
538 # * Do not use <video>,
539 # * don't embed images,
540 # * use <object> instead of <img> for SVG.
541 # (SVG not supported by IE up to version 8,
542 # html4css1 strives for IE6 compatibility.)
543 object_image_types = {'.svg': 'image/svg+xml',
544 '.swf': 'application/x-shockwave-flash',
545 '.mp4': 'video/mp4',
546 '.webm': 'video/webm',
547 '.ogg': 'video/ogg',
550 def visit_image(self, node):
551 atts = {}
552 uri = node['uri']
553 ext = os.path.splitext(uri)[1].lower()
554 if ext in self.object_image_types:
555 atts['data'] = uri
556 atts['type'] = self.object_image_types[ext]
557 else:
558 atts['src'] = uri
559 atts['alt'] = node.get('alt', uri)
560 # image size
561 if 'width' in node:
562 atts['width'] = node['width']
563 if 'height' in node:
564 atts['height'] = node['height']
565 if 'scale' in node:
566 if (PIL and ('width' not in node or 'height' not in node)
567 and self.settings.file_insertion_enabled):
568 imagepath = self.uri2imagepath(uri)
569 try:
570 with PIL.Image.open(imagepath) as img:
571 img_size = img.size
572 except (OSError, UnicodeEncodeError):
573 pass # TODO: warn/info?
574 else:
575 self.settings.record_dependencies.add(
576 imagepath.replace('\\', '/'))
577 if 'width' not in atts:
578 atts['width'] = '%dpx' % img_size[0]
579 if 'height' not in atts:
580 atts['height'] = '%dpx' % img_size[1]
581 for att_name in 'width', 'height':
582 if att_name in atts:
583 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
584 assert match
585 atts[att_name] = '%s%s' % (
586 float(match.group(1)) * (float(node['scale']) / 100),
587 match.group(2))
588 style = []
589 for att_name in 'width', 'height':
590 if att_name in atts:
591 if re.match(r'^[0-9.]+$', atts[att_name]):
592 # Interpret unitless values as pixels.
593 atts[att_name] += 'px'
594 style.append('%s: %s;' % (att_name, atts[att_name]))
595 del atts[att_name]
596 if style:
597 atts['style'] = ' '.join(style)
598 # No newlines around inline images.
599 if (not isinstance(node.parent, nodes.TextElement)
600 or isinstance(node.parent, nodes.reference)
601 and not isinstance(node.parent.parent, nodes.TextElement)):
602 suffix = '\n'
603 else:
604 suffix = ''
605 if 'align' in node:
606 atts['class'] = 'align-%s' % node['align']
607 if ext in self.object_image_types:
608 # do NOT use an empty tag: incorrect rendering in browsers
609 self.body.append(self.starttag(node, 'object', '', **atts)
610 + node.get('alt', uri) + '</object>' + suffix)
611 else:
612 self.body.append(self.emptytag(node, 'img', suffix, **atts))
614 def depart_image(self, node):
615 pass
617 # use table for footnote text,
618 # context added in footnote_backrefs.
619 def visit_label(self, node):
620 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
621 CLASS='label'))
623 def depart_label(self, node):
624 self.body.append(f']{self.context.pop()}</td><td>{self.context.pop()}')
626 # ersatz for first/last pseudo-classes
627 def visit_list_item(self, node):
628 self.body.append(self.starttag(node, 'li', ''))
629 if len(node):
630 node[0]['classes'].append('first')
632 def depart_list_item(self, node):
633 self.body.append('</li>\n')
635 # use <tt> (not supported by HTML5),
636 # cater for limited styling options in CSS1 using hard-coded NBSPs
637 def visit_literal(self, node):
638 # special case: "code" role
639 classes = node['classes']
640 if 'code' in classes:
641 # filter 'code' from class arguments
642 node['classes'] = [cls for cls in classes if cls != 'code']
643 self.body.append(self.starttag(node, 'code', ''))
644 return
645 self.body.append(
646 self.starttag(node, 'tt', '', CLASS='docutils literal'))
647 text = node.astext()
648 for token in self.words_and_spaces.findall(text):
649 if token.strip():
650 # Protect text like "--an-option" and the regular expression
651 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
652 if self.in_word_wrap_point.search(token):
653 self.body.append('<span class="pre">%s</span>'
654 % self.encode(token))
655 else:
656 self.body.append(self.encode(token))
657 elif token in ('\n', ' '):
658 # Allow breaks at whitespace:
659 self.body.append(token)
660 else:
661 # Protect runs of multiple spaces; the last space can wrap:
662 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
663 self.body.append('</tt>')
664 # Content already processed:
665 raise nodes.SkipNode
667 def depart_literal(self, node):
668 # skipped unless literal element is from "code" role:
669 self.body.append('</code>')
671 # add newline after wrapper tags, don't use <code> for code
672 def visit_literal_block(self, node):
673 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
675 def depart_literal_block(self, node):
676 self.body.append('\n</pre>\n')
678 # use table for option list
679 def visit_option_group(self, node):
680 atts = {}
681 if (self.settings.option_limit
682 and len(node.astext()) > self.settings.option_limit):
683 atts['colspan'] = 2
684 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
685 else:
686 self.context.append('')
687 self.body.append(
688 self.starttag(node, 'td', CLASS='option-group', **atts))
689 self.body.append('<kbd>')
690 self.context.append(0) # count number of options
692 def depart_option_group(self, node):
693 self.context.pop()
694 self.body.append('</kbd></td>\n')
695 self.body.append(self.context.pop())
697 def visit_option_list(self, node):
698 self.body.append(
699 self.starttag(node, 'table', CLASS='docutils option-list',
700 frame="void", rules="none"))
701 self.body.append('<col class="option" />\n'
702 '<col class="description" />\n'
703 '<tbody valign="top">\n')
705 def depart_option_list(self, node):
706 self.body.append('</tbody>\n</table>\n')
708 def visit_option_list_item(self, node):
709 self.body.append(self.starttag(node, 'tr', ''))
711 def depart_option_list_item(self, node):
712 self.body.append('</tr>\n')
714 # Omit <p> tags to produce visually compact lists (less vertical
715 # whitespace) as CSS styling requires CSS2.
716 def should_be_compact_paragraph(self, node):
718 Determine if the <p> tags around paragraph ``node`` can be omitted.
720 if (isinstance(node.parent, nodes.document)
721 or isinstance(node.parent, nodes.compound)):
722 # Never compact paragraphs in document or compound.
723 return False
724 for key, value in node.attlist():
725 if (node.is_not_default(key)
726 and not (key == 'classes'
727 and value in ([], ['first'],
728 ['last'], ['first', 'last']))):
729 # Attribute which needs to survive.
730 return False
731 first = isinstance(node.parent[0], nodes.label) # skip label
732 for child in node.parent.children[first:]:
733 # only first paragraph can be compact
734 if isinstance(child, nodes.Invisible):
735 continue
736 if child is node:
737 break
738 return False
739 parent_length = len([n for n in node.parent if not isinstance(
740 n, (nodes.Invisible, nodes.label))])
741 if (self.compact_simple
742 or self.compact_field_list
743 or self.compact_p and parent_length == 1):
744 return True
745 return False
747 def visit_paragraph(self, node):
748 if self.should_be_compact_paragraph(node):
749 self.context.append('')
750 else:
751 self.body.append(self.starttag(node, 'p', ''))
752 self.context.append('</p>\n')
754 def depart_paragraph(self, node):
755 self.body.append(self.context.pop())
756 self.report_messages(node)
758 # ersatz for first/last pseudo-classes
759 def visit_sidebar(self, node):
760 self.body.append(
761 self.starttag(node, 'div', CLASS='sidebar'))
762 self.set_first_last(node)
763 self.in_sidebar = True
765 def depart_sidebar(self, node):
766 self.body.append('</div>\n')
767 self.in_sidebar = False
769 # <sub> not allowed in <pre>
770 def visit_subscript(self, node):
771 if isinstance(node.parent, nodes.literal_block):
772 self.body.append(self.starttag(node, 'span', '',
773 CLASS='subscript'))
774 else:
775 self.body.append(self.starttag(node, 'sub', ''))
777 def depart_subscript(self, node):
778 if isinstance(node.parent, nodes.literal_block):
779 self.body.append('</span>')
780 else:
781 self.body.append('</sub>')
783 # Use <h*> for subtitles (deprecated in HTML 5)
784 def visit_subtitle(self, node):
785 if isinstance(node.parent, nodes.sidebar):
786 self.body.append(self.starttag(node, 'p', '',
787 CLASS='sidebar-subtitle'))
788 self.context.append('</p>\n')
789 elif isinstance(node.parent, nodes.document):
790 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
791 self.context.append('</h2>\n')
792 self.in_document_title = len(self.body)
793 elif isinstance(node.parent, nodes.section):
794 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
795 self.body.append(
796 self.starttag(node, tag, '', CLASS='section-subtitle')
797 + self.starttag({}, 'span', '', CLASS='section-subtitle'))
798 self.context.append('</span></%s>\n' % tag)
800 def depart_subtitle(self, node):
801 self.body.append(self.context.pop())
802 if self.in_document_title:
803 self.subtitle = self.body[self.in_document_title:-1]
804 self.in_document_title = 0
805 self.body_pre_docinfo.extend(self.body)
806 self.html_subtitle.extend(self.body)
807 del self.body[:]
809 # <sup> not allowed in <pre> in HTML 4
810 def visit_superscript(self, node):
811 if isinstance(node.parent, nodes.literal_block):
812 self.body.append(self.starttag(node, 'span', '',
813 CLASS='superscript'))
814 else:
815 self.body.append(self.starttag(node, 'sup', ''))
817 def depart_superscript(self, node):
818 if isinstance(node.parent, nodes.literal_block):
819 self.body.append('</span>')
820 else:
821 self.body.append('</sup>')
823 # <tt> element deprecated in HTML 5
824 def visit_system_message(self, node):
825 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
826 self.body.append('<p class="system-message-title">')
827 backref_text = ''
828 if len(node['backrefs']):
829 backrefs = node['backrefs']
830 if len(backrefs) == 1:
831 backref_text = ('; <em><a href="#%s">backlink</a></em>'
832 % backrefs[0])
833 else:
834 i = 1
835 backlinks = []
836 for backref in backrefs:
837 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
838 i += 1
839 backref_text = ('; <em>backlinks: %s</em>'
840 % ', '.join(backlinks))
841 if node.hasattr('line'):
842 line = ', line %s' % node['line']
843 else:
844 line = ''
845 self.body.append('System Message: %s/%s '
846 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
847 % (node['type'], node['level'],
848 self.encode(node['source']), line, backref_text))
850 def depart_system_message(self, node):
851 self.body.append('</div>\n')
853 # "hard coded" border setting
854 def visit_table(self, node):
855 self.context.append(self.compact_p)
856 self.compact_p = True
857 atts = {'border': 1}
858 classes = ['docutils', self.settings.table_style]
859 if 'align' in node:
860 classes.append('align-%s' % node['align'])
861 if 'width' in node:
862 atts['style'] = 'width: %s' % node['width']
863 self.body.append(
864 self.starttag(node, 'table', CLASS=' '.join(classes), **atts))
866 def depart_table(self, node):
867 self.compact_p = self.context.pop()
868 self.body.append('</table>\n')
870 # hard-coded vertical alignment
871 def visit_tbody(self, node):
872 self.body.append(self.starttag(node, 'tbody', valign='top'))
874 def depart_tbody(self, node):
875 self.body.append('</tbody>\n')
877 # no special handling of "details" in definition list
878 def visit_term(self, node):
879 self.body.append(self.starttag(node, 'dt', '',
880 classes=node.parent['classes'],
881 ids=node.parent['ids']))
883 def depart_term(self, node):
884 # Nest (optional) classifier(s) in the <dt> element
885 if node.next_node(nodes.classifier, descend=False, siblings=True):
886 return # skip (depart_classifier() calls this function again)
887 self.body.append('</dt>\n')
889 # hard-coded vertical alignment
890 def visit_thead(self, node):
891 self.body.append(self.starttag(node, 'thead', valign='bottom'))
893 def depart_thead(self, node):
894 self.body.append('</thead>\n')
896 # auxiliary method, called by visit_title()
897 # "with-subtitle" class, no ARIA roles
898 def section_title_tags(self, node):
899 classes = []
900 h_level = self.section_level + self.initial_header_level - 1
901 if (len(node.parent) >= 2
902 and isinstance(node.parent[1], nodes.subtitle)):
903 classes.append('with-subtitle')
904 if h_level > 6:
905 classes.append('h%i' % h_level)
906 tagname = 'h%i' % min(h_level, 6)
907 start_tag = self.starttag(node, tagname, '', classes=classes)
908 if node.hasattr('refid'):
909 atts = {}
910 atts['class'] = 'toc-backref'
911 atts['href'] = '#' + node['refid']
912 start_tag += self.starttag({}, 'a', '', **atts)
913 close_tag = '</a></%s>\n' % tagname
914 else:
915 close_tag = '</%s>\n' % tagname
916 return start_tag, close_tag
919 class SimpleListChecker(writers._html_base.SimpleListChecker):
922 Raise `nodes.NodeFound` if non-simple list item is encountered.
924 Here "simple" means a list item containing nothing other than a single
925 paragraph, a simple list, or a paragraph followed by a simple list.
928 def visit_list_item(self, node):
929 children = []
930 for child in node.children:
931 if not isinstance(child, nodes.Invisible):
932 children.append(child)
933 if (children and isinstance(children[0], nodes.paragraph)
934 and (isinstance(children[-1], nodes.bullet_list)
935 or isinstance(children[-1], nodes.enumerated_list))):
936 children.pop()
937 if len(children) <= 1:
938 return
939 else:
940 raise nodes.NodeFound
942 # def visit_bullet_list(self, node):
943 # pass
945 # def visit_enumerated_list(self, node):
946 # pass
948 def visit_paragraph(self, node):
949 raise nodes.SkipNode
951 def visit_definition_list(self, node):
952 raise nodes.NodeFound
954 def visit_docinfo(self, node):
955 raise nodes.NodeFound