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'
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
,
44 'Template file. (UTF-8 encoded, default: "%s")' % default_template
,
46 {'default': default_template
, 'metavar': '<file>'}),
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
}),
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>'}),
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',
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".',
92 {'default': 14, 'metavar': '<level>',
93 'validator': frontend
.validate_nonnegative_int
}),
97 config_section
= 'html4css1 writer'
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
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:
126 But this list cannot be compact:
130 This second paragraph forces space between list items.
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:
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] = ' '
163 # use character reference for dash (not valid in HTML5)
164 attribution_formats
= {'dash': ('—', ''),
165 'parentheses': ('(', ')'),
166 'parens': ('(', ')'),
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 />')
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
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),
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'])):
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')
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'
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
):
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'))
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
):
314 def depart_definition_list_item(self
, node
):
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',
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
:]
343 def visit_docinfo_item(self
, node
, name
, meta
=True):
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
])
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(' ')
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).
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)
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
:
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
444 self
.body
.append(self
.starttag(node
, 'table', frame
='void',
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
):
458 atts
['class'] = 'docinfo-name'
460 atts
['class'] = 'field-name'
461 if (self
.settings
.field_name_limit
462 and len(node
.astext()) > self
.settings
.field_name_limit
):
464 self
.context
.append('</tr>\n'
465 + self
.starttag(node
.parent
, 'tr', '',
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'
484 self
.footnote_backrefs(node
)
486 def footnote_backrefs(self
, node
):
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">'
496 for (i
, backref
) in enumerate(backrefs
, 1):
497 backlinks
.append('<a class="fn-backref" href="#%s">%s</a>'
499 self
.context
.append('<em>(%s)</em> ' % ', '.join(backlinks
))
500 self
.context
+= ['', '']
502 self
.context
.append('')
503 self
.context
+= ['', '']
504 # If the node does not only consist of a label.
506 # If there are preceding backlinks, we do not set class
507 # 'first', because we need to retain the top-margin.
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':
522 self
.context
.append(']')
524 assert format
== 'superscript'
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
):
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',
546 '.webm': 'video/webm',
550 def visit_image(self
, node
):
553 ext
= os
.path
.splitext(uri
)[1].lower()
554 if ext
in self
.object_image_types
:
556 atts
['type'] = self
.object_image_types
[ext
]
559 atts
['alt'] = node
.get('alt', uri
)
562 atts
['width'] = node
['width']
564 atts
['height'] = node
['height']
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
)
570 with PIL
.Image
.open(imagepath
) as img
:
572 except (OSError, UnicodeEncodeError):
573 pass # TODO: warn/info?
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':
583 match
= re
.match(r
'([0-9.]+)(\S*)$', atts
[att_name
])
585 atts
[att_name
] = '%s%s' % (
586 float(match
.group(1)) * (float(node
['scale']) / 100),
589 for att_name
in 'width', 'height':
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
]))
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
)):
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
)
612 self
.body
.append(self
.emptytag(node
, 'img', suffix
, **atts
))
614 def depart_image(self
, node
):
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(),
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', ''))
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', ''))
646 self
.starttag(node
, 'tt', '', CLASS
='docutils literal'))
648 for token
in self
.words_and_spaces
.findall(text
):
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
))
656 self
.body
.append(self
.encode(token
))
657 elif token
in ('\n', ' '):
658 # Allow breaks at whitespace:
659 self
.body
.append(token
)
661 # Protect runs of multiple spaces; the last space can wrap:
662 self
.body
.append(' ' * (len(token
) - 1) + ' ')
663 self
.body
.append('</tt>')
664 # Content already processed:
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
):
681 if (self
.settings
.option_limit
682 and len(node
.astext()) > self
.settings
.option_limit
):
684 self
.context
.append('</tr>\n<tr><td> </td>')
686 self
.context
.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
):
694 self
.body
.append('</kbd></td>\n')
695 self
.body
.append(self
.context
.pop())
697 def visit_option_list(self
, node
):
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.
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.
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
):
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):
747 def visit_paragraph(self
, node
):
748 if self
.should_be_compact_paragraph(node
):
749 self
.context
.append('')
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
):
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', '',
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>')
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)
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
)
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'))
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>')
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">')
828 if len(node
['backrefs']):
829 backrefs
= node
['backrefs']
830 if len(backrefs
) == 1:
831 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
836 for backref
in backrefs
:
837 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
839 backref_text
= ('; <em>backlinks: %s</em>'
840 % ', '.join(backlinks
))
841 if node
.hasattr('line'):
842 line
= ', line %s' % node
['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
858 classes
= ['docutils', self
.settings
.table_style
]
860 classes
.append('align-%s' % node
['align'])
862 atts
['style'] = 'width: %s' % node
['width']
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
):
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')
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'):
910 atts
['class'] = 'toc-backref'
911 atts
['href'] = '#' + node
['refid']
912 start_tag
+= self
.starttag({}, 'a', '', **atts
)
913 close_tag
= '</a></%s>\n' % tagname
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
):
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
))):
937 if len(children
) <= 1:
940 raise nodes
.NodeFound
942 # def visit_bullet_list(self, node):
945 # def visit_enumerated_list(self, node):
948 def visit_paragraph(self
, node
):
951 def visit_definition_list(self
, node
):
952 raise nodes
.NodeFound
954 def visit_docinfo(self
, node
):
955 raise nodes
.NodeFound