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'
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__
)),
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
)
41 'HTML-Specific Options',
43 (('Specify the template file (UTF-8 encoded). Default is "%s".'
44 % default_template_path
,
46 {'default': default_template_path
, 'metavar': '<file>'}),
47 ('Comma separated list of stylesheet URLs. '
48 'Overrides previous --stylesheet and --stylesheet-path settings.',
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".',
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".',
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.',
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: ""',
127 ('Math output format, one of "MathML", "HTML", "MathJax" '
128 'or "LaTeX". Default: "HTML math.css"',
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'
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
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:
169 But this list cannot be compact:
173 This second paragraph forces space between list items.
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:
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
' '
206 # use character reference for dash (not valid in HTML5)
207 attribution_formats
= {'dash': ('—', ''),
208 'parentheses': ('(', ')'),
209 'parens': ('(', ')'),
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 />')
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
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 # use "width" argument insted of "style: 'width'":
251 def visit_colspec(self
, node
):
252 self
.colspecs
.append(node
)
253 # "stubs" list is an attribute of the tgroup element:
254 node
.parent
.stubs
.append(node
.attributes
.get('stub'))
256 def depart_colspec(self
, node
):
257 # write out <colgroup> when all colspecs are processed
258 if isinstance(node
.next_node(descend
=False, siblings
=True),
261 if 'colwidths-auto' in node
.parent
.parent
['classes'] or (
262 'colwidths-auto' in self
.settings
.table_style
and
263 ('colwidths-given' not in node
.parent
.parent
['classes'])):
265 total_width
= sum(node
['colwidth'] for node
in self
.colspecs
)
266 self
.body
.append(self
.starttag(node
, 'colgroup'))
267 for node
in self
.colspecs
:
268 colwidth
= int(node
['colwidth'] * 100.0 / total_width
+ 0.5)
269 self
.body
.append(self
.emptytag(node
, 'col',
270 width
='%i%%' % colwidth
))
271 self
.body
.append('</colgroup>\n')
274 # exclude definition lists and field lists (non-compact by default)
276 def is_compactable(self
, node
):
277 return ('compact' in node
['classes']
278 or (self
.settings
.compact_lists
279 and 'open' not in node
['classes']
280 and (self
.compact_simple
281 or self
.topic_classes
== ['contents']
282 # TODO: self.in_contents
283 or self
.check_simple_list(node
))))
285 # citations: Use table for bibliographic references.
286 def visit_citation(self
, node
):
287 self
.body
.append(self
.starttag(node
, 'table',
288 CLASS
='docutils citation',
289 frame
="void", rules
="none"))
290 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
291 '<tbody valign="top">\n'
293 self
.footnote_backrefs(node
)
295 def depart_citation(self
, node
):
296 self
.body
.append('</td></tr>\n'
297 '</tbody>\n</table>\n')
299 # insert classifier-delimiter (not required with CSS2)
300 def visit_classifier(self
, node
):
301 self
.body
.append(' <span class="classifier-delimiter">:</span> ')
302 self
.body
.append(self
.starttag(node
, 'span', '', CLASS
='classifier'))
304 # ersatz for first/last pseudo-classes
305 def visit_definition(self
, node
):
306 self
.body
.append('</dt>\n')
307 self
.body
.append(self
.starttag(node
, 'dd', ''))
308 self
.set_first_last(node
)
310 # don't add "simple" class value
311 def visit_definition_list(self
, node
):
312 self
.body
.append(self
.starttag(node
, 'dl', CLASS
='docutils'))
314 # use a table for description lists
315 def visit_description(self
, node
):
316 self
.body
.append(self
.starttag(node
, 'td', ''))
317 self
.set_first_last(node
)
319 def depart_description(self
, node
):
320 self
.body
.append('</td>')
322 # use table for docinfo
323 def visit_docinfo(self
, node
):
324 self
.context
.append(len(self
.body
))
325 self
.body
.append(self
.starttag(node
, 'table',
327 frame
="void", rules
="none"))
328 self
.body
.append('<col class="docinfo-name" />\n'
329 '<col class="docinfo-content" />\n'
330 '<tbody valign="top">\n')
331 self
.in_docinfo
= True
333 def depart_docinfo(self
, node
):
334 self
.body
.append('</tbody>\n</table>\n')
335 self
.in_docinfo
= False
336 start
= self
.context
.pop()
337 self
.docinfo
= self
.body
[start
:]
340 def visit_docinfo_item(self
, node
, name
, meta
=True):
342 meta_tag
= '<meta name="%s" content="%s" />\n' \
343 % (name
, self
.attval(node
.astext()))
344 self
.add_meta(meta_tag
)
345 self
.body
.append(self
.starttag(node
, 'tr', ''))
346 self
.body
.append('<th class="docinfo-name">%s:</th>\n<td>'
347 % self
.language
.labels
[name
])
349 if isinstance(node
[0], nodes
.Element
):
350 node
[0]['classes'].append('first')
351 if isinstance(node
[-1], nodes
.Element
):
352 node
[-1]['classes'].append('last')
354 def depart_docinfo_item(self
):
355 self
.body
.append('</td></tr>\n')
357 # add newline after opening tag
358 def visit_doctest_block(self
, node
):
359 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='doctest-block'))
361 # insert an NBSP into empty cells, ersatz for first/last
362 def visit_entry(self
, node
):
363 writers
._html
_base
.HTMLTranslator
.visit_entry(self
, node
)
364 if len(node
) == 0: # empty cell
365 self
.body
.append(' ')
366 self
.set_first_last(node
)
368 # ersatz for first/last pseudo-classes
369 def visit_enumerated_list(self
, node
):
371 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
372 cannot be emulated in CSS1 (HTML 5 reincludes it).
376 atts
['start'] = node
['start']
377 if 'enumtype' in node
:
378 atts
['class'] = node
['enumtype']
379 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
380 # single "format" attribute? Use CSS2?
381 old_compact_simple
= self
.compact_simple
382 self
.context
.append((self
.compact_simple
, self
.compact_p
))
383 self
.compact_p
= None
384 self
.compact_simple
= self
.is_compactable(node
)
385 if self
.compact_simple
and not old_compact_simple
:
386 atts
['class'] = (atts
.get('class', '') + ' simple').strip()
387 self
.body
.append(self
.starttag(node
, 'ol', **atts
))
389 def depart_enumerated_list(self
, node
):
390 self
.compact_simple
, self
.compact_p
= self
.context
.pop()
391 self
.body
.append('</ol>\n')
393 # use table for field-list:
394 def visit_field(self
, node
):
395 self
.body
.append(self
.starttag(node
, 'tr', '', CLASS
='field'))
397 def depart_field(self
, node
):
398 self
.body
.append('</tr>\n')
400 def visit_field_body(self
, node
):
401 self
.body
.append(self
.starttag(node
, 'td', '', CLASS
='field-body'))
402 self
.set_class_on_child(node
, 'first', 0)
404 if (self
.compact_field_list
or
405 isinstance(field
.parent
, nodes
.docinfo
) or
406 field
.parent
.index(field
) == len(field
.parent
) - 1):
407 # If we are in a compact list, the docinfo, or if this is
408 # the last field of the field list, do not add vertical
409 # space after last element.
410 self
.set_class_on_child(node
, 'last', -1)
412 def depart_field_body(self
, node
):
413 self
.body
.append('</td>\n')
415 def visit_field_list(self
, node
):
416 self
.context
.append((self
.compact_field_list
, self
.compact_p
))
417 self
.compact_p
= None
418 if 'compact' in node
['classes']:
419 self
.compact_field_list
= True
420 elif (self
.settings
.compact_field_lists
421 and 'open' not in node
['classes']):
422 self
.compact_field_list
= True
423 if self
.compact_field_list
:
425 field_body
= field
[-1]
426 assert isinstance(field_body
, nodes
.field_body
)
427 children
= [n
for n
in field_body
428 if not isinstance(n
, nodes
.Invisible
)]
429 if not (len(children
) == 0 or
430 len(children
) == 1 and
431 isinstance(children
[0],
432 (nodes
.paragraph
, nodes
.line_block
))):
433 self
.compact_field_list
= False
435 self
.body
.append(self
.starttag(node
, 'table', frame
='void',
437 CLASS
='docutils field-list'))
438 self
.body
.append('<col class="field-name" />\n'
439 '<col class="field-body" />\n'
440 '<tbody valign="top">\n')
442 def depart_field_list(self
, node
):
443 self
.body
.append('</tbody>\n</table>\n')
444 self
.compact_field_list
, self
.compact_p
= self
.context
.pop()
446 def visit_field_name(self
, node
):
449 atts
['class'] = 'docinfo-name'
451 atts
['class'] = 'field-name'
452 if ( self
.settings
.field_name_limit
453 and len(node
.astext()) > self
.settings
.field_name_limit
):
455 self
.context
.append('</tr>\n'
456 + self
.starttag(node
.parent
, 'tr', '',
460 self
.context
.append('')
461 self
.body
.append(self
.starttag(node
, 'th', '', **atts
))
463 def depart_field_name(self
, node
):
464 self
.body
.append(':</th>')
465 self
.body
.append(self
.context
.pop())
467 # use table for footnote text
468 def visit_footnote(self
, node
):
469 self
.body
.append(self
.starttag(node
, 'table',
470 CLASS
='docutils footnote',
471 frame
="void", rules
="none"))
472 self
.body
.append('<colgroup><col class="label" /><col /></colgroup>\n'
473 '<tbody valign="top">\n'
475 self
.footnote_backrefs(node
)
477 def footnote_backrefs(self
, node
):
479 backrefs
= node
['backrefs']
480 if self
.settings
.footnote_backlinks
and backrefs
:
481 if len(backrefs
) == 1:
482 self
.context
.append('')
483 self
.context
.append('</a>')
484 self
.context
.append('<a class="fn-backref" href="#%s">'
487 for (i
, backref
) in enumerate(backrefs
, 1):
488 backlinks
.append('<a class="fn-backref" href="#%s">%s</a>'
490 self
.context
.append('<em>(%s)</em> ' % ', '.join(backlinks
))
491 self
.context
+= ['', '']
493 self
.context
.append('')
494 self
.context
+= ['', '']
495 # If the node does not only consist of a label.
497 # If there are preceding backlinks, we do not set class
498 # 'first', because we need to retain the top-margin.
500 node
[1]['classes'].append('first')
501 node
[-1]['classes'].append('last')
503 def depart_footnote(self
, node
):
504 self
.body
.append('</td></tr>\n'
505 '</tbody>\n</table>\n')
507 # insert markers in text as pseudo-classes are not supported in CSS1:
508 def visit_footnote_reference(self
, node
):
509 href
= '#' + node
['refid']
510 format
= self
.settings
.footnote_references
511 if format
== 'brackets':
513 self
.context
.append(']')
515 assert format
== 'superscript'
517 self
.context
.append('</sup>')
518 self
.body
.append(self
.starttag(node
, 'a', suffix
,
519 CLASS
='footnote-reference', href
=href
))
521 def depart_footnote_reference(self
, node
):
522 self
.body
.append(self
.context
.pop() + '</a>')
524 # just pass on generated text
525 def visit_generated(self
, node
):
528 # Image types to place in an <object> element
529 # SVG not supported by IE up to version 8
530 # (html4css1 strives for IE6 compatibility)
531 object_image_types
= {'.svg': 'image/svg+xml',
532 '.swf': 'application/x-shockwave-flash'}
534 # use table for footnote text,
535 # context added in footnote_backrefs.
536 def visit_label(self
, node
):
537 self
.body
.append(self
.starttag(node
, 'td', '%s[' % self
.context
.pop(),
540 def depart_label(self
, node
):
541 self
.body
.append(']%s</td><td>%s' % (self
.context
.pop(), self
.context
.pop()))
544 # ersatz for first/last pseudo-classes
545 def visit_list_item(self
, node
):
546 self
.body
.append(self
.starttag(node
, 'li', ''))
548 node
[0]['classes'].append('first')
550 # use <tt> (not supported by HTML5),
551 # cater for limited styling options in CSS1 using hard-coded NBSPs
552 def visit_literal(self
, node
):
553 # special case: "code" role
554 classes
= node
.get('classes', [])
555 if 'code' in classes
:
556 # filter 'code' from class arguments
557 node
['classes'] = [cls
for cls
in classes
if cls
!= 'code']
558 self
.body
.append(self
.starttag(node
, 'code', ''))
561 self
.starttag(node
, 'tt', '', CLASS
='docutils literal'))
563 for token
in self
.words_and_spaces
.findall(text
):
565 # Protect text like "--an-option" and the regular expression
566 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
567 if self
.in_word_wrap_point
.search(token
):
568 self
.body
.append('<span class="pre">%s</span>'
569 % self
.encode(token
))
571 self
.body
.append(self
.encode(token
))
572 elif token
in ('\n', ' '):
573 # Allow breaks at whitespace:
574 self
.body
.append(token
)
576 # Protect runs of multiple spaces; the last space can wrap:
577 self
.body
.append(' ' * (len(token
) - 1) + ' ')
578 self
.body
.append('</tt>')
579 # Content already processed:
582 # add newline after opening tag, don't use <code> for code
583 def visit_literal_block(self
, node
):
584 self
.body
.append(self
.starttag(node
, 'pre', CLASS
='literal-block'))
587 def depart_literal_block(self
, node
):
588 self
.body
.append('\n</pre>\n')
590 # use table for option list
591 def visit_option_group(self
, node
):
593 if ( self
.settings
.option_limit
594 and len(node
.astext()) > self
.settings
.option_limit
):
596 self
.context
.append('</tr>\n<tr><td> </td>')
598 self
.context
.append('')
600 self
.starttag(node
, 'td', CLASS
='option-group', **atts
))
601 self
.body
.append('<kbd>')
602 self
.context
.append(0) # count number of options
604 def depart_option_group(self
, node
):
606 self
.body
.append('</kbd></td>\n')
607 self
.body
.append(self
.context
.pop())
609 def visit_option_list(self
, node
):
611 self
.starttag(node
, 'table', CLASS
='docutils option-list',
612 frame
="void", rules
="none"))
613 self
.body
.append('<col class="option" />\n'
614 '<col class="description" />\n'
615 '<tbody valign="top">\n')
617 def depart_option_list(self
, node
):
618 self
.body
.append('</tbody>\n</table>\n')
620 def visit_option_list_item(self
, node
):
621 self
.body
.append(self
.starttag(node
, 'tr', ''))
623 def depart_option_list_item(self
, node
):
624 self
.body
.append('</tr>\n')
626 # Omit <p> tags to produce visually compact lists (less vertical
627 # whitespace) as CSS styling requires CSS2.
628 def should_be_compact_paragraph(self
, node
):
630 Determine if the <p> tags around paragraph ``node`` can be omitted.
632 if (isinstance(node
.parent
, nodes
.document
) or
633 isinstance(node
.parent
, nodes
.compound
)):
634 # Never compact paragraphs in document or compound.
636 for key
, value
in node
.attlist():
637 if (node
.is_not_default(key
) and
638 not (key
== 'classes' and value
in
639 ([], ['first'], ['last'], ['first', 'last']))):
640 # Attribute which needs to survive.
642 first
= isinstance(node
.parent
[0], nodes
.label
) # skip label
643 for child
in node
.parent
.children
[first
:]:
644 # only first paragraph can be compact
645 if isinstance(child
, nodes
.Invisible
):
650 parent_length
= len([n
for n
in node
.parent
if not isinstance(
651 n
, (nodes
.Invisible
, nodes
.label
))])
652 if ( self
.compact_simple
653 or self
.compact_field_list
654 or self
.compact_p
and parent_length
== 1):
658 def visit_paragraph(self
, node
):
659 if self
.should_be_compact_paragraph(node
):
660 self
.context
.append('')
662 self
.body
.append(self
.starttag(node
, 'p', ''))
663 self
.context
.append('</p>\n')
665 def depart_paragraph(self
, node
):
666 self
.body
.append(self
.context
.pop())
668 # ersatz for first/last pseudo-classes
669 def visit_sidebar(self
, node
):
671 self
.starttag(node
, 'div', CLASS
='sidebar'))
672 self
.set_first_last(node
)
673 self
.in_sidebar
= True
675 # <sub> not allowed in <pre>
676 def visit_subscript(self
, node
):
677 if isinstance(node
.parent
, nodes
.literal_block
):
678 self
.body
.append(self
.starttag(node
, 'span', '',
681 self
.body
.append(self
.starttag(node
, 'sub', ''))
683 def depart_subscript(self
, node
):
684 if isinstance(node
.parent
, nodes
.literal_block
):
685 self
.body
.append('</span>')
687 self
.body
.append('</sub>')
689 # Use <h*> for subtitles (deprecated in HTML 5)
690 def visit_subtitle(self
, node
):
691 if isinstance(node
.parent
, nodes
.sidebar
):
692 self
.body
.append(self
.starttag(node
, 'p', '',
693 CLASS
='sidebar-subtitle'))
694 self
.context
.append('</p>\n')
695 elif isinstance(node
.parent
, nodes
.document
):
696 self
.body
.append(self
.starttag(node
, 'h2', '', CLASS
='subtitle'))
697 self
.context
.append('</h2>\n')
698 self
.in_document_title
= len(self
.body
)
699 elif isinstance(node
.parent
, nodes
.section
):
700 tag
= 'h%s' % (self
.section_level
+ self
.initial_header_level
- 1)
702 self
.starttag(node
, tag
, '', CLASS
='section-subtitle') +
703 self
.starttag({}, 'span', '', CLASS
='section-subtitle'))
704 self
.context
.append('</span></%s>\n' % tag
)
706 def depart_subtitle(self
, node
):
707 self
.body
.append(self
.context
.pop())
708 if self
.in_document_title
:
709 self
.subtitle
= self
.body
[self
.in_document_title
:-1]
710 self
.in_document_title
= 0
711 self
.body_pre_docinfo
.extend(self
.body
)
712 self
.html_subtitle
.extend(self
.body
)
715 # <sup> not allowed in <pre> in HTML 4
716 def visit_superscript(self
, node
):
717 if isinstance(node
.parent
, nodes
.literal_block
):
718 self
.body
.append(self
.starttag(node
, 'span', '',
719 CLASS
='superscript'))
721 self
.body
.append(self
.starttag(node
, 'sup', ''))
723 def depart_superscript(self
, node
):
724 if isinstance(node
.parent
, nodes
.literal_block
):
725 self
.body
.append('</span>')
727 self
.body
.append('</sup>')
729 # <tt> element deprecated in HTML 5
730 def visit_system_message(self
, node
):
731 self
.body
.append(self
.starttag(node
, 'div', CLASS
='system-message'))
732 self
.body
.append('<p class="system-message-title">')
734 if len(node
['backrefs']):
735 backrefs
= node
['backrefs']
736 if len(backrefs
) == 1:
737 backref_text
= ('; <em><a href="#%s">backlink</a></em>'
742 for backref
in backrefs
:
743 backlinks
.append('<a href="#%s">%s</a>' % (backref
, i
))
745 backref_text
= ('; <em>backlinks: %s</em>'
746 % ', '.join(backlinks
))
747 if node
.hasattr('line'):
748 line
= ', line %s' % node
['line']
751 self
.body
.append('System Message: %s/%s '
752 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
753 % (node
['type'], node
['level'],
754 self
.encode(node
['source']), line
, backref_text
))
756 # "hard coded" border setting
757 def visit_table(self
, node
):
758 self
.context
.append(self
.compact_p
)
759 self
.compact_p
= True
761 classes
= ['docutils', self
.settings
.table_style
]
763 classes
.append('align-%s' % node
['align'])
765 atts
['style'] = 'width: %s' % node
['width']
767 self
.starttag(node
, 'table', CLASS
=' '.join(classes
), **atts
))
769 def depart_table(self
, node
):
770 self
.compact_p
= self
.context
.pop()
771 self
.body
.append('</table>\n')
773 # hard-coded vertical alignment
774 def visit_tbody(self
, node
):
775 self
.body
.append(self
.starttag(node
, 'tbody', valign
='top'))
777 def depart_tbody(self
, node
):
778 self
.body
.append('</tbody>\n')
780 # hard-coded vertical alignment
781 def visit_thead(self
, node
):
782 self
.body
.append(self
.starttag(node
, 'thead', valign
='bottom'))
784 def depart_thead(self
, node
):
785 self
.body
.append('</thead>\n')
788 class SimpleListChecker(writers
._html
_base
.SimpleListChecker
):
791 Raise `nodes.NodeFound` if non-simple list item is encountered.
793 Here "simple" means a list item containing nothing other than a single
794 paragraph, a simple list, or a paragraph followed by a simple list.
797 def visit_list_item(self
, node
):
799 for child
in node
.children
:
800 if not isinstance(child
, nodes
.Invisible
):
801 children
.append(child
)
802 if (children
and isinstance(children
[0], nodes
.paragraph
)
803 and (isinstance(children
[-1], nodes
.bullet_list
)
804 or isinstance(children
[-1], nodes
.enumerated_list
))):
806 if len(children
) <= 1:
809 raise nodes
.NodeFound
811 # def visit_bullet_list(self, node):
814 # def visit_enumerated_list(self, node):
817 def visit_paragraph(self
, node
):
820 def visit_definition_list(self
, node
):
821 raise nodes
.NodeFound
823 def visit_docinfo(self
, node
):
824 raise nodes
.NodeFound