HTML writers: Outsourcing of common code to _html_base.py.
[docutils.git] / docutils / docutils / writers / html4css1 / __init__.py
blob8e2f12d3b4c096c2c0cd2764b32947068cc78108
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 docutils
19 from docutils import frontend, nodes, writers, io
20 from docutils.transforms import writer_aux
21 from docutils.writers import _html_base
23 class Writer(writers._html_base.Writer):
25 supported = ('html', 'html4', 'html4css1', 'xhtml', 'xhtml10')
26 """Formats this writer supports."""
28 default_stylesheets = ['html4css1.css']
29 default_stylesheet_dirs = ['.',
30 os.path.abspath(os.path.dirname(__file__)),
31 # for math.css
32 os.path.abspath(os.path.join(
33 os.path.dirname(os.path.dirname(__file__)), 'html5_polyglot'))
36 default_template = 'template.txt'
37 default_template_path = os.path.join(
38 os.path.dirname(os.path.abspath(__file__)), default_template)
40 settings_spec = (
41 'HTML-Specific Options',
42 None,
43 (('Specify the template file (UTF-8 encoded). Default is "%s".'
44 % default_template_path,
45 ['--template'],
46 {'default': default_template_path, 'metavar': '<file>'}),
47 ('Comma separated list of stylesheet URLs. '
48 'Overrides previous --stylesheet and --stylesheet-path settings.',
49 ['--stylesheet'],
50 {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
51 'validator': frontend.validate_comma_separated_list}),
52 ('Comma separated list of stylesheet paths. '
53 'Relative paths are expanded if a matching file is found in '
54 'the --stylesheet-dirs. With --link-stylesheet, '
55 'the path is rewritten relative to the output HTML file. '
56 'Default: "%s"' % ','.join(default_stylesheets),
57 ['--stylesheet-path'],
58 {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
59 'validator': frontend.validate_comma_separated_list,
60 'default': default_stylesheets}),
61 ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
62 'files must be accessible during processing. This is the default.',
63 ['--embed-stylesheet'],
64 {'default': 1, 'action': 'store_true',
65 'validator': frontend.validate_boolean}),
66 ('Link to the stylesheet(s) in the output HTML file. '
67 'Default: embed stylesheets.',
68 ['--link-stylesheet'],
69 {'dest': 'embed_stylesheet', 'action': 'store_false'}),
70 ('Comma-separated list of directories where stylesheets are found. '
71 'Used by --stylesheet-path when expanding relative path arguments. '
72 'Default: "%s"' % default_stylesheet_dirs,
73 ['--stylesheet-dirs'],
74 {'metavar': '<dir[,dir,...]>',
75 'validator': frontend.validate_comma_separated_list,
76 'default': default_stylesheet_dirs}),
77 ('Specify the initial header level. Default is 1 for "<h1>". '
78 'Does not affect document title & subtitle (see --no-doc-title).',
79 ['--initial-header-level'],
80 {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
81 'metavar': '<level>'}),
82 ('Specify the maximum width (in characters) for one-column field '
83 'names. Longer field names will span an entire row of the table '
84 'used to render the field list. Default is 14 characters. '
85 'Use 0 for "no limit".',
86 ['--field-name-limit'],
87 {'default': 14, 'metavar': '<level>',
88 'validator': frontend.validate_nonnegative_int}),
89 ('Specify the maximum width (in characters) for options in option '
90 'lists. Longer options will span an entire row of the table used '
91 'to render the option list. Default is 14 characters. '
92 'Use 0 for "no limit".',
93 ['--option-limit'],
94 {'default': 14, 'metavar': '<level>',
95 'validator': frontend.validate_nonnegative_int}),
96 ('Format for footnote references: one of "superscript" or '
97 '"brackets". Default is "brackets".',
98 ['--footnote-references'],
99 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
100 'metavar': '<format>',
101 'overrides': 'trim_footnote_reference_space'}),
102 ('Format for block quote attributions: one of "dash" (em-dash '
103 'prefix), "parentheses"/"parens", or "none". Default is "dash".',
104 ['--attribution'],
105 {'choices': ['dash', 'parentheses', 'parens', 'none'],
106 'default': 'dash', 'metavar': '<format>'}),
107 ('Remove extra vertical whitespace between items of "simple" bullet '
108 'lists and enumerated lists. Default: enabled.',
109 ['--compact-lists'],
110 {'default': 1, 'action': 'store_true',
111 'validator': frontend.validate_boolean}),
112 ('Disable compact simple bullet and enumerated lists.',
113 ['--no-compact-lists'],
114 {'dest': 'compact_lists', 'action': 'store_false'}),
115 ('Remove extra vertical whitespace between items of simple field '
116 'lists. Default: enabled.',
117 ['--compact-field-lists'],
118 {'default': 1, 'action': 'store_true',
119 'validator': frontend.validate_boolean}),
120 ('Disable compact simple field lists.',
121 ['--no-compact-field-lists'],
122 {'dest': 'compact_field_lists', 'action': 'store_false'}),
123 ('Added to standard table classes. '
124 'Defined styles: "borderless". Default: ""',
125 ['--table-style'],
126 {'default': ''}),
127 ('Math output format, one of "MathML", "HTML", "MathJax" '
128 'or "LaTeX". Default: "HTML math.css"',
129 ['--math-output'],
130 {'default': 'HTML math.css'}),
131 ('Omit the XML declaration. Use with caution.',
132 ['--no-xml-declaration'],
133 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
134 'validator': frontend.validate_boolean}),
135 ('Obfuscate email addresses to confuse harvesters while still '
136 'keeping email links usable with standards-compliant browsers.',
137 ['--cloak-email-addresses'],
138 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
140 config_section = 'html4css1 writer'
142 def __init__(self):
143 self.parts = {}
144 self.translator_class = HTMLTranslator
147 class HTMLTranslator(writers._html_base.HTMLTranslator):
149 The html4css1 writer has been optimized to produce visually compact
150 lists (less vertical whitespace). HTML's mixed content models
151 allow list items to contain "<li><p>body elements</p></li>" or
152 "<li>just text</li>" or even "<li>text<p>and body
153 elements</p>combined</li>", each with different effects. It would
154 be best to stick with strict body elements in list items, but they
155 affect vertical spacing in older browsers (although they really
156 shouldn't).
157 The html5_polyglot writer solves this using CSS2.
159 Here is an outline of the optimization:
161 - Check for and omit <p> tags in "simple" lists: list items
162 contain either a single paragraph, a nested simple list, or a
163 paragraph followed by a nested simple list. This means that
164 this list can be compact:
166 - Item 1.
167 - Item 2.
169 But this list cannot be compact:
171 - Item 1.
173 This second paragraph forces space between list items.
175 - Item 2.
177 - In non-list contexts, omit <p> tags on a paragraph if that
178 paragraph is the only child of its parent (footnotes & citations
179 are allowed a label first).
181 - Regardless of the above, in definitions, table cells, field bodies,
182 option descriptions, and list items, mark the first child with
183 'class="first"' and the last child with 'class="last"'. The stylesheet
184 sets the margins (top & bottom respectively) to 0 for these elements.
186 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
187 option) disables list whitespace optimization.
190 # The following definitions are required for display in browsers limited
191 # to CSS1 or backwards compatible behaviour of the writer:
193 doctype = (
194 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
195 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
197 content_type = ('<meta http-equiv="Content-Type"'
198 ' content="text/html; charset=%s" />\n')
199 content_type_mathml = ('<meta http-equiv="Content-Type"'
200 ' content="application/xhtml+xml; charset=%s" />\n')
202 # encode also non-breaking space
203 special_characters = dict(_html_base.HTMLTranslator.special_characters)
204 special_characters[0xa0] = u'&nbsp;'
206 # use character reference for dash (not valid in HTML5)
207 attribution_formats = {'dash': ('&mdash;', ''),
208 'parentheses': ('(', ')'),
209 'parens': ('(', ')'),
210 'none': ('', '')}
212 # ersatz for first/last pseudo-classes missing in CSS1
213 def set_first_last(self, node):
214 self.set_class_on_child(node, 'first', 0)
215 self.set_class_on_child(node, 'last', -1)
217 # add newline after opening tag
218 def visit_address(self, node):
219 self.visit_docinfo_item(node, 'address', meta=False)
220 self.body.append(self.starttag(node, 'pre', CLASS='address'))
223 # ersatz for first/last pseudo-classes
224 def visit_admonition(self, node):
225 node['classes'].insert(0, 'admonition')
226 self.body.append(self.starttag(node, 'div'))
227 self.set_first_last(node)
229 # author, authors: use <br> instead of paragraphs
230 def visit_author(self, node):
231 if isinstance(node.parent, nodes.authors):
232 if self.author_in_authors:
233 self.body.append('\n<br />')
234 else:
235 self.visit_docinfo_item(node, 'author')
237 def depart_author(self, node):
238 if isinstance(node.parent, nodes.authors):
239 self.author_in_authors = True
240 else:
241 self.depart_docinfo_item()
243 def visit_authors(self, node):
244 self.visit_docinfo_item(node, 'authors')
245 self.author_in_authors = False # initialize
247 def depart_authors(self, node):
248 self.depart_docinfo_item()
250 # Compact lists:
251 # exclude definition lists and field lists (non-compact by default)
253 def is_compactable(self, node):
254 return ('compact' in node['classes']
255 or (self.settings.compact_lists
256 and 'open' not in node['classes']
257 and (self.compact_simple
258 or self.topic_classes == ['contents']
259 # TODO: self.in_contents
260 or self.check_simple_list(node))))
262 # citations: Use table for bibliographic references.
263 def visit_citation(self, node):
264 self.body.append(self.starttag(node, 'table',
265 CLASS='docutils citation',
266 frame="void", rules="none"))
267 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
268 '<tbody valign="top">\n'
269 '<tr>')
270 self.footnote_backrefs(node)
272 def depart_citation(self, node):
273 self.body.append('</td></tr>\n'
274 '</tbody>\n</table>\n')
276 # insert classifier-delimiter (not required with CSS2)
277 def visit_classifier(self, node):
278 self.body.append(' <span class="classifier-delimiter">:</span> ')
279 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
281 # rewritten in _html_base (support for "auto" width)
282 def depart_colspec(self, node):
283 pass
285 def write_colspecs(self):
286 width = 0
287 for node in self.colspecs:
288 width += node['colwidth']
289 for node in self.colspecs:
290 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
291 self.body.append(self.emptytag(node, 'col',
292 width='%i%%' % colwidth))
293 self.colspecs = []
295 # ersatz for first/last pseudo-classes
296 def visit_definition(self, node):
297 self.body.append('</dt>\n')
298 self.body.append(self.starttag(node, 'dd', ''))
299 self.set_first_last(node)
301 # don't add "simple" class value
302 def visit_definition_list(self, node):
303 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
305 # use a table for description lists
306 def visit_description(self, node):
307 self.body.append(self.starttag(node, 'td', ''))
308 self.set_first_last(node)
310 def depart_description(self, node):
311 self.body.append('</td>')
313 # use table for docinfo
314 def visit_docinfo(self, node):
315 self.context.append(len(self.body))
316 self.body.append(self.starttag(node, 'table',
317 CLASS='docinfo',
318 frame="void", rules="none"))
319 self.body.append('<col class="docinfo-name" />\n'
320 '<col class="docinfo-content" />\n'
321 '<tbody valign="top">\n')
322 self.in_docinfo = True
324 def depart_docinfo(self, node):
325 self.body.append('</tbody>\n</table>\n')
326 self.in_docinfo = False
327 start = self.context.pop()
328 self.docinfo = self.body[start:]
329 self.body = []
331 def visit_docinfo_item(self, node, name, meta=True):
332 if meta:
333 meta_tag = '<meta name="%s" content="%s" />\n' \
334 % (name, self.attval(node.astext()))
335 self.add_meta(meta_tag)
336 self.body.append(self.starttag(node, 'tr', ''))
337 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
338 % self.language.labels[name])
339 if len(node):
340 if isinstance(node[0], nodes.Element):
341 node[0]['classes'].append('first')
342 if isinstance(node[-1], nodes.Element):
343 node[-1]['classes'].append('last')
345 def depart_docinfo_item(self):
346 self.body.append('</td></tr>\n')
348 # add newline after opening tag
349 def visit_doctest_block(self, node):
350 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
352 # insert an NBSP into empty cells, ersatz for first/last
353 def visit_entry(self, node):
354 writers._html_base.HTMLTranslator.visit_entry(self, node)
355 if len(node) == 0: # empty cell
356 self.body.append('&nbsp;')
357 self.set_first_last(node)
359 # ersatz for first/last pseudo-classes
360 def visit_enumerated_list(self, node):
362 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
363 cannot be emulated in CSS1 (HTML 5 reincludes it).
365 atts = {}
366 if 'start' in node:
367 atts['start'] = node['start']
368 if 'enumtype' in node:
369 atts['class'] = node['enumtype']
370 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
371 # single "format" attribute? Use CSS2?
372 old_compact_simple = self.compact_simple
373 self.context.append((self.compact_simple, self.compact_p))
374 self.compact_p = None
375 self.compact_simple = self.is_compactable(node)
376 if self.compact_simple and not old_compact_simple:
377 atts['class'] = (atts.get('class', '') + ' simple').strip()
378 self.body.append(self.starttag(node, 'ol', **atts))
380 def depart_enumerated_list(self, node):
381 self.compact_simple, self.compact_p = self.context.pop()
382 self.body.append('</ol>\n')
384 # use table for field-list:
385 def visit_field(self, node):
386 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
388 def depart_field(self, node):
389 self.body.append('</tr>\n')
391 def visit_field_body(self, node):
392 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
393 self.set_class_on_child(node, 'first', 0)
394 field = node.parent
395 if (self.compact_field_list or
396 isinstance(field.parent, nodes.docinfo) or
397 field.parent.index(field) == len(field.parent) - 1):
398 # If we are in a compact list, the docinfo, or if this is
399 # the last field of the field list, do not add vertical
400 # space after last element.
401 self.set_class_on_child(node, 'last', -1)
403 def depart_field_body(self, node):
404 self.body.append('</td>\n')
406 def visit_field_list(self, node):
407 self.context.append((self.compact_field_list, self.compact_p))
408 self.compact_p = None
409 if 'compact' in node['classes']:
410 self.compact_field_list = True
411 elif (self.settings.compact_field_lists
412 and 'open' not in node['classes']):
413 self.compact_field_list = True
414 if self.compact_field_list:
415 for field in node:
416 field_body = field[-1]
417 assert isinstance(field_body, nodes.field_body)
418 children = [n for n in field_body
419 if not isinstance(n, nodes.Invisible)]
420 if not (len(children) == 0 or
421 len(children) == 1 and
422 isinstance(children[0],
423 (nodes.paragraph, nodes.line_block))):
424 self.compact_field_list = False
425 break
426 self.body.append(self.starttag(node, 'table', frame='void',
427 rules='none',
428 CLASS='docutils field-list'))
429 self.body.append('<col class="field-name" />\n'
430 '<col class="field-body" />\n'
431 '<tbody valign="top">\n')
433 def depart_field_list(self, node):
434 self.body.append('</tbody>\n</table>\n')
435 self.compact_field_list, self.compact_p = self.context.pop()
437 def visit_field_name(self, node):
438 atts = {}
439 if self.in_docinfo:
440 atts['class'] = 'docinfo-name'
441 else:
442 atts['class'] = 'field-name'
443 if ( self.settings.field_name_limit
444 and len(node.astext()) > self.settings.field_name_limit):
445 atts['colspan'] = 2
446 self.context.append('</tr>\n'
447 + self.starttag(node.parent, 'tr', '',
448 CLASS='field')
449 + '<td>&nbsp;</td>')
450 else:
451 self.context.append('')
452 self.body.append(self.starttag(node, 'th', '', **atts))
454 def depart_field_name(self, node):
455 self.body.append(':</th>')
456 self.body.append(self.context.pop())
458 # use table for footnote text
459 def visit_footnote(self, node):
460 self.body.append(self.starttag(node, 'table',
461 CLASS='docutils footnote',
462 frame="void", rules="none"))
463 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
464 '<tbody valign="top">\n'
465 '<tr>')
466 self.footnote_backrefs(node)
468 def footnote_backrefs(self, node):
469 backlinks = []
470 backrefs = node['backrefs']
471 if self.settings.footnote_backlinks and backrefs:
472 if len(backrefs) == 1:
473 self.context.append('')
474 self.context.append('</a>')
475 self.context.append('<a class="fn-backref" href="#%s">'
476 % backrefs[0])
477 else:
478 # Python 2.4 fails with enumerate(backrefs, 1)
479 for (i, backref) in enumerate(backrefs):
480 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
481 % (backref, i+1))
482 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
483 self.context += ['', '']
484 else:
485 self.context.append('')
486 self.context += ['', '']
487 # If the node does not only consist of a label.
488 if len(node) > 1:
489 # If there are preceding backlinks, we do not set class
490 # 'first', because we need to retain the top-margin.
491 if not backlinks:
492 node[1]['classes'].append('first')
493 node[-1]['classes'].append('last')
495 def depart_footnote(self, node):
496 self.body.append('</td></tr>\n'
497 '</tbody>\n</table>\n')
499 # insert markers in text as pseudo-classes are not supported in CSS1:
500 def visit_footnote_reference(self, node):
501 href = '#' + node['refid']
502 format = self.settings.footnote_references
503 if format == 'brackets':
504 suffix = '['
505 self.context.append(']')
506 else:
507 assert format == 'superscript'
508 suffix = '<sup>'
509 self.context.append('</sup>')
510 self.body.append(self.starttag(node, 'a', suffix,
511 CLASS='footnote-reference', href=href))
513 def depart_footnote_reference(self, node):
514 self.body.append(self.context.pop() + '</a>')
516 # just pass on generated text
517 def visit_generated(self, node):
518 pass
520 # Image types to place in an <object> element
521 # SVG not supported by IE up to version 8
522 # (html4css1 strives for IE6 compatibility)
523 object_image_types = {'.svg': 'image/svg+xml',
524 '.swf': 'application/x-shockwave-flash'}
526 # use table for footnote text,
527 # context added in footnote_backrefs.
528 def visit_label(self, node):
529 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
530 CLASS='label'))
532 def depart_label(self, node):
533 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
536 # ersatz for first/last pseudo-classes
537 def visit_list_item(self, node):
538 self.body.append(self.starttag(node, 'li', ''))
539 if len(node):
540 node[0]['classes'].append('first')
542 # use <tt> (not supported by HTML5),
543 # cater for limited styling options in CSS1 using hard-coded NBSPs
544 def visit_literal(self, node):
545 # special case: "code" role
546 classes = node.get('classes', [])
547 if 'code' in classes:
548 # filter 'code' from class arguments
549 node['classes'] = [cls for cls in classes if cls != 'code']
550 self.body.append(self.starttag(node, 'code', ''))
551 return
552 self.body.append(
553 self.starttag(node, 'tt', '', CLASS='docutils literal'))
554 text = node.astext()
555 for token in self.words_and_spaces.findall(text):
556 if token.strip():
557 # Protect text like "--an-option" and the regular expression
558 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
559 if self.sollbruchstelle.search(token):
560 self.body.append('<span class="pre">%s</span>'
561 % self.encode(token))
562 else:
563 self.body.append(self.encode(token))
564 elif token in ('\n', ' '):
565 # Allow breaks at whitespace:
566 self.body.append(token)
567 else:
568 # Protect runs of multiple spaces; the last space can wrap:
569 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
570 self.body.append('</tt>')
571 # Content already processed:
572 raise nodes.SkipNode
574 # add newline after opening tag, don't use <code> for code
575 def visit_literal_block(self, node):
576 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
578 # add newline
579 def depart_literal_block(self, node):
580 self.body.append('\n</pre>\n')
582 # use table for option list
583 def visit_option_group(self, node):
584 atts = {}
585 if ( self.settings.option_limit
586 and len(node.astext()) > self.settings.option_limit):
587 atts['colspan'] = 2
588 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
589 else:
590 self.context.append('')
591 self.body.append(
592 self.starttag(node, 'td', CLASS='option-group', **atts))
593 self.body.append('<kbd>')
594 self.context.append(0) # count number of options
596 def depart_option_group(self, node):
597 self.context.pop()
598 self.body.append('</kbd></td>\n')
599 self.body.append(self.context.pop())
601 def visit_option_list(self, node):
602 self.body.append(
603 self.starttag(node, 'table', CLASS='docutils option-list',
604 frame="void", rules="none"))
605 self.body.append('<col class="option" />\n'
606 '<col class="description" />\n'
607 '<tbody valign="top">\n')
609 def depart_option_list(self, node):
610 self.body.append('</tbody>\n</table>\n')
612 def visit_option_list_item(self, node):
613 self.body.append(self.starttag(node, 'tr', ''))
615 def depart_option_list_item(self, node):
616 self.body.append('</tr>\n')
618 # Omit <p> tags to produce visually compact lists (less vertical
619 # whitespace) as CSS styling requires CSS2.
620 def should_be_compact_paragraph(self, node):
622 Determine if the <p> tags around paragraph ``node`` can be omitted.
624 if (isinstance(node.parent, nodes.document) or
625 isinstance(node.parent, nodes.compound)):
626 # Never compact paragraphs in document or compound.
627 return False
628 for key, value in node.attlist():
629 if (node.is_not_default(key) and
630 not (key == 'classes' and value in
631 ([], ['first'], ['last'], ['first', 'last']))):
632 # Attribute which needs to survive.
633 return False
634 first = isinstance(node.parent[0], nodes.label) # skip label
635 for child in node.parent.children[first:]:
636 # only first paragraph can be compact
637 if isinstance(child, nodes.Invisible):
638 continue
639 if child is node:
640 break
641 return False
642 parent_length = len([n for n in node.parent if not isinstance(
643 n, (nodes.Invisible, nodes.label))])
644 if ( self.compact_simple
645 or self.compact_field_list
646 or self.compact_p and parent_length == 1):
647 return True
648 return False
650 def visit_paragraph(self, node):
651 if self.should_be_compact_paragraph(node):
652 self.context.append('')
653 else:
654 self.body.append(self.starttag(node, 'p', ''))
655 self.context.append('</p>\n')
657 def depart_paragraph(self, node):
658 self.body.append(self.context.pop())
660 # ersatz for first/last pseudo-classes
661 def visit_sidebar(self, node):
662 self.body.append(
663 self.starttag(node, 'div', CLASS='sidebar'))
664 self.set_first_last(node)
665 self.in_sidebar = True
667 # <sub> not allowed in <pre>
668 def visit_subscript(self, node):
669 if isinstance(node.parent, nodes.literal_block):
670 self.body.append(self.starttag(node, 'span', '',
671 CLASS='subscript'))
672 else:
673 self.body.append(self.starttag(node, 'sub', ''))
675 def depart_subscript(self, node):
676 if isinstance(node.parent, nodes.literal_block):
677 self.body.append('</span>')
678 else:
679 self.body.append('</sub>')
681 # Use <h*> for subtitles (deprecated in HTML 5)
682 def visit_subtitle(self, node):
683 if isinstance(node.parent, nodes.sidebar):
684 self.body.append(self.starttag(node, 'p', '',
685 CLASS='sidebar-subtitle'))
686 self.context.append('</p>\n')
687 elif isinstance(node.parent, nodes.document):
688 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
689 self.context.append('</h2>\n')
690 self.in_document_title = len(self.body)
691 elif isinstance(node.parent, nodes.section):
692 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
693 self.body.append(
694 self.starttag(node, tag, '', CLASS='section-subtitle') +
695 self.starttag({}, 'span', '', CLASS='section-subtitle'))
696 self.context.append('</span></%s>\n' % tag)
698 def depart_subtitle(self, node):
699 self.body.append(self.context.pop())
700 if self.in_document_title:
701 self.subtitle = self.body[self.in_document_title:-1]
702 self.in_document_title = 0
703 self.body_pre_docinfo.extend(self.body)
704 self.html_subtitle.extend(self.body)
705 del self.body[:]
707 # <sup> not allowed in <pre> in HTML 4
708 def visit_superscript(self, node):
709 if isinstance(node.parent, nodes.literal_block):
710 self.body.append(self.starttag(node, 'span', '',
711 CLASS='superscript'))
712 else:
713 self.body.append(self.starttag(node, 'sup', ''))
715 def depart_superscript(self, node):
716 if isinstance(node.parent, nodes.literal_block):
717 self.body.append('</span>')
718 else:
719 self.body.append('</sup>')
721 # <tt> element deprecated in HTML 5
722 def visit_system_message(self, node):
723 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
724 self.body.append('<p class="system-message-title">')
725 backref_text = ''
726 if len(node['backrefs']):
727 backrefs = node['backrefs']
728 if len(backrefs) == 1:
729 backref_text = ('; <em><a href="#%s">backlink</a></em>'
730 % backrefs[0])
731 else:
732 i = 1
733 backlinks = []
734 for backref in backrefs:
735 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
736 i += 1
737 backref_text = ('; <em>backlinks: %s</em>'
738 % ', '.join(backlinks))
739 if node.hasattr('line'):
740 line = ', line %s' % node['line']
741 else:
742 line = ''
743 self.body.append('System Message: %s/%s '
744 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
745 % (node['type'], node['level'],
746 self.encode(node['source']), line, backref_text))
748 # "hard coded" border setting
749 def visit_table(self, node):
750 self.context.append(self.compact_p)
751 self.compact_p = True
752 classes = ['docutils', self.settings.table_style]
753 if 'align' in node:
754 classes.append('align-%s' % node['align'])
755 self.body.append(
756 self.starttag(node, 'table', CLASS=' '.join(classes), border="1"))
758 def depart_table(self, node):
759 self.compact_p = self.context.pop()
760 self.body.append('</table>\n')
762 # hard-coded vertical alignment
763 def visit_tbody(self, node):
764 self.write_colspecs()
765 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
766 self.body.append(self.starttag(node, 'tbody', valign='top'))
768 # rewritten in _html_base
769 def visit_tgroup(self, node):
770 self.body.append(self.starttag(node, 'colgroup'))
771 # Appended by thead or tbody:
772 self.context.append('</colgroup>\n')
773 node.stubs = []
775 # rewritten in _html_base
776 def visit_thead(self, node):
777 self.write_colspecs()
778 self.body.append(self.context.pop()) # '</colgroup>\n'
779 # There may or may not be a <thead>; this is for <tbody> to use:
780 self.context.append('')
781 self.body.append(self.starttag(node, 'thead', valign='bottom'))
784 class SimpleListChecker(writers._html_base.SimpleListChecker):
787 Raise `nodes.NodeFound` if non-simple list item is encountered.
789 Here "simple" means a list item containing nothing other than a single
790 paragraph, a simple list, or a paragraph followed by a simple list.
793 def visit_list_item(self, node):
794 children = []
795 for child in node.children:
796 if not isinstance(child, nodes.Invisible):
797 children.append(child)
798 if (children and isinstance(children[0], nodes.paragraph)
799 and (isinstance(children[-1], nodes.bullet_list)
800 or isinstance(children[-1], nodes.enumerated_list))):
801 children.pop()
802 if len(children) <= 1:
803 return
804 else:
805 raise nodes.NodeFound
807 # def visit_bullet_list(self, node):
808 # pass
810 # def visit_enumerated_list(self, node):
811 # pass
813 # def visit_paragraph(self, node):
814 # raise nodes.SkipNode
816 def visit_definition_list(self, node):
817 raise nodes.NodeFound
819 def visit_docinfo(self, node):
820 raise nodes.NodeFound
822 def visit_definition_list(self, node):
823 raise nodes.NodeFound