Use `nodes.parse_measure()` in rST directive option conversion.
[docutils.git] / docutils / docutils / writers / html4css1 / __init__.py
bloba77402464438f38b0cc95e6da30678e42765d073
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 math_output=(
72 'Math output format (one of "MathML", "HTML", "MathJax", or '
73 '"LaTeX") and option(s). (default: "HTML math.css")',
74 ['--math-output'],
75 {'default': 'HTML math.css',
76 'validator': frontend.validate_math_output}),
77 xml_declaration=(
78 'Prepend an XML declaration (default). ',
79 ['--xml-declaration'],
80 {'default': True, 'action': 'store_true',
81 'validator': frontend.validate_boolean}),
83 settings_spec = settings_spec + (
84 'HTML4 Writer Options',
85 '',
86 (('Specify the maximum width (in characters) for one-column field '
87 'names. Longer field names will span an entire row of the table '
88 'used to render the field list. Default is 14 characters. '
89 'Use 0 for "no limit".',
90 ['--field-name-limit'],
91 {'default': 14, 'metavar': '<level>',
92 'validator': frontend.validate_nonnegative_int}),
93 ('Specify the maximum width (in characters) for options in option '
94 'lists. Longer options will span an entire row of the table used '
95 'to render the option list. Default is 14 characters. '
96 'Use 0 for "no limit".',
97 ['--option-limit'],
98 {'default': 14, 'metavar': '<level>',
99 'validator': frontend.validate_nonnegative_int}),
103 config_section = 'html4css1 writer'
105 def __init__(self) -> None:
106 self.parts = {}
107 self.translator_class = HTMLTranslator
110 class HTMLTranslator(writers._html_base.HTMLTranslator):
112 The html4css1 writer has been optimized to produce visually compact
113 lists (less vertical whitespace). HTML's mixed content models
114 allow list items to contain "<li><p>body elements</p></li>" or
115 "<li>just text</li>" or even "<li>text<p>and body
116 elements</p>combined</li>", each with different effects. It would
117 be best to stick with strict body elements in list items, but they
118 affect vertical spacing in older browsers (although they really
119 shouldn't).
120 The html5_polyglot writer solves this using CSS2.
122 Here is an outline of the optimization:
124 - Check for and omit <p> tags in "simple" lists: list items
125 contain either a single paragraph, a nested simple list, or a
126 paragraph followed by a nested simple list. This means that
127 this list can be compact:
129 - Item 1.
130 - Item 2.
132 But this list cannot be compact:
134 - Item 1.
136 This second paragraph forces space between list items.
138 - Item 2.
140 - In non-list contexts, omit <p> tags on a paragraph if that
141 paragraph is the only child of its parent (footnotes & citations
142 are allowed a label first).
144 - Regardless of the above, in definitions, table cells, field bodies,
145 option descriptions, and list items, mark the first child with
146 'class="first"' and the last child with 'class="last"'. The stylesheet
147 sets the margins (top & bottom respectively) to 0 for these elements.
149 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
150 option) disables list whitespace optimization.
153 # The following definitions are required for display in browsers limited
154 # to CSS1 or backwards compatible behaviour of the writer:
156 doctype = (
157 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
158 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
160 content_type = ('<meta http-equiv="Content-Type"'
161 ' content="text/html; charset=%s" />\n')
162 content_type_mathml = ('<meta http-equiv="Content-Type"'
163 ' content="application/xhtml+xml; charset=%s" />\n')
165 # encode also non-breaking space
166 special_characters = _html_base.HTMLTranslator.special_characters.copy()
167 special_characters[0xa0] = '&nbsp;'
169 # use character reference for dash (not valid in HTML5)
170 attribution_formats = {'dash': ('&mdash;', ''),
171 'parentheses': ('(', ')'),
172 'parens': ('(', ')'),
173 'none': ('', '')}
175 # ersatz for first/last pseudo-classes missing in CSS1
176 def set_first_last(self, node) -> None:
177 self.set_class_on_child(node, 'first', 0)
178 self.set_class_on_child(node, 'last', -1)
180 # add newline after opening tag
181 def visit_address(self, node) -> None:
182 self.visit_docinfo_item(node, 'address', meta=False)
183 self.body.append(self.starttag(node, 'pre', CLASS='address'))
185 def depart_address(self, node) -> None:
186 self.body.append('\n</pre>\n')
187 self.depart_docinfo_item()
189 # ersatz for first/last pseudo-classes
190 def visit_admonition(self, node) -> None:
191 node['classes'].insert(0, 'admonition')
192 self.body.append(self.starttag(node, 'div'))
193 self.set_first_last(node)
195 def depart_admonition(self, node=None) -> None:
196 self.body.append('</div>\n')
198 # author, authors: use <br> instead of paragraphs
199 def visit_author(self, node) -> None:
200 if isinstance(node.parent, nodes.authors):
201 if self.author_in_authors:
202 self.body.append('\n<br />')
203 else:
204 self.visit_docinfo_item(node, 'author')
206 def depart_author(self, node) -> None:
207 if isinstance(node.parent, nodes.authors):
208 self.author_in_authors = True
209 else:
210 self.depart_docinfo_item()
212 def visit_authors(self, node) -> None:
213 self.visit_docinfo_item(node, 'authors')
214 self.author_in_authors = False # initialize
216 def depart_authors(self, node) -> None:
217 self.depart_docinfo_item()
219 # use "width" argument instead of "style: 'width'":
220 def visit_colspec(self, node) -> None:
221 self.colspecs.append(node)
222 # "stubs" list is an attribute of the tgroup element:
223 node.parent.stubs.append(node.attributes.get('stub'))
225 def depart_colspec(self, node) -> None:
226 # write out <colgroup> when all colspecs are processed
227 if isinstance(node.next_node(descend=False, siblings=True),
228 nodes.colspec):
229 return
230 if ('colwidths-auto' in node.parent.parent['classes']
231 or ('colwidths-auto' in self.settings.table_style
232 and 'colwidths-given' not in node.parent.parent['classes'])):
233 return
234 total_width = sum(node['colwidth'] for node in self.colspecs)
235 self.body.append(self.starttag(node, 'colgroup'))
236 for node in self.colspecs:
237 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
238 self.body.append(self.emptytag(node, 'col',
239 width='%i%%' % colwidth))
240 self.body.append('</colgroup>\n')
242 # Compact lists:
243 # exclude definition lists and field lists (non-compact by default)
245 def is_compactable(self, node):
246 return ('compact' in node['classes']
247 or (self.settings.compact_lists
248 and 'open' not in node['classes']
249 and (self.compact_simple
250 or 'contents' in node.parent['classes']
251 # TODO: self.in_contents
252 or self.check_simple_list(node))))
254 # citations: Use table for bibliographic references.
255 def visit_citation(self, node) -> None:
256 self.body.append(self.starttag(node, 'table',
257 CLASS='docutils citation',
258 frame="void", rules="none"))
259 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
260 '<tbody valign="top">\n'
261 '<tr>')
262 self.footnote_backrefs(node)
264 def depart_citation(self, node) -> None:
265 self.body.append('</td></tr>\n'
266 '</tbody>\n</table>\n')
268 def visit_citation_reference(self, node) -> None:
269 href = '#'
270 if 'refid' in node:
271 href += node['refid']
272 elif 'refname' in node:
273 href += self.document.nameids[node['refname']]
274 self.body.append(self.starttag(node, 'a', suffix='[', href=href,
275 classes=['citation-reference']))
277 def depart_citation_reference(self, node) -> None:
278 self.body.append(']</a>')
280 # insert classifier-delimiter (not required with CSS2)
281 def visit_classifier(self, node) -> None:
282 self.body.append(' <span class="classifier-delimiter">:</span> ')
283 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
285 def depart_classifier(self, node) -> None:
286 self.body.append('</span>')
287 self.depart_term(node) # close the <dt> after last classifier
289 # ersatz for first/last pseudo-classes
290 def visit_compound(self, node) -> None:
291 self.body.append(self.starttag(node, 'div', CLASS='compound'))
292 if len(node) > 1:
293 node[0]['classes'].append('compound-first')
294 node[-1]['classes'].append('compound-last')
295 for child in node[1:-1]:
296 child['classes'].append('compound-middle')
298 def depart_compound(self, node) -> None:
299 self.body.append('</div>\n')
301 # ersatz for first/last pseudo-classes, no special handling of "details"
302 def visit_definition(self, node) -> None:
303 self.body.append(self.starttag(node, 'dd', ''))
304 self.set_first_last(node)
306 def depart_definition(self, node) -> None:
307 self.body.append('</dd>\n')
309 # don't add "simple" class value, no special handling of "details"
310 def visit_definition_list(self, node) -> None:
311 self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
313 def depart_definition_list(self, node) -> None:
314 self.body.append('</dl>\n')
316 # no special handling of "details"
317 def visit_definition_list_item(self, node) -> None:
318 pass
320 def depart_definition_list_item(self, node) -> None:
321 pass
323 # use a table for description lists
324 def visit_description(self, node) -> None:
325 self.body.append(self.starttag(node, 'td', ''))
326 self.set_first_last(node)
328 def depart_description(self, node) -> None:
329 self.body.append('</td>')
331 # use table for docinfo
332 def visit_docinfo(self, node) -> None:
333 self.context.append(len(self.body))
334 self.body.append(self.starttag(node, 'table',
335 CLASS='docinfo',
336 frame="void", rules="none"))
337 self.body.append('<col class="docinfo-name" />\n'
338 '<col class="docinfo-content" />\n'
339 '<tbody valign="top">\n')
340 self.in_docinfo = True
342 def depart_docinfo(self, node) -> None:
343 self.body.append('</tbody>\n</table>\n')
344 self.in_docinfo = False
345 start = self.context.pop()
346 self.docinfo = self.body[start:]
347 self.body = []
349 def visit_docinfo_item(self, node, name, meta=True) -> None:
350 if meta:
351 meta_tag = '<meta name="%s" content="%s" />\n' \
352 % (name, self.attval(node.astext()))
353 self.meta.append(meta_tag)
354 self.body.append(self.starttag(node, 'tr', ''))
355 self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
356 % self.language.labels[name])
357 if len(node):
358 if isinstance(node[0], nodes.Element):
359 node[0]['classes'].append('first')
360 if isinstance(node[-1], nodes.Element):
361 node[-1]['classes'].append('last')
363 def depart_docinfo_item(self) -> None:
364 self.body.append('</td></tr>\n')
366 # add newline after opening tag
367 def visit_doctest_block(self, node) -> None:
368 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
370 def depart_doctest_block(self, node) -> None:
371 self.body.append('\n</pre>\n')
373 # insert an NBSP into empty cells, ersatz for first/last
374 def visit_entry(self, node) -> None:
375 writers._html_base.HTMLTranslator.visit_entry(self, node)
376 if len(node) == 0: # empty cell
377 self.body.append('&nbsp;')
378 self.set_first_last(node)
380 def depart_entry(self, node) -> None:
381 self.body.append(self.context.pop())
383 # ersatz for first/last pseudo-classes
384 def visit_enumerated_list(self, node) -> None:
386 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
387 cannot be emulated in CSS1 (HTML 5 reincludes it).
389 atts = {}
390 if 'start' in node:
391 atts['start'] = node['start']
392 if 'enumtype' in node:
393 atts['class'] = node['enumtype']
394 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
395 # single "format" attribute? Use CSS2?
396 old_compact_simple = self.compact_simple
397 self.context.append((self.compact_simple, self.compact_p))
398 self.compact_p = None
399 self.compact_simple = self.is_compactable(node)
400 if self.compact_simple and not old_compact_simple:
401 atts['class'] = (atts.get('class', '') + ' simple').strip()
402 self.body.append(self.starttag(node, 'ol', **atts))
404 def depart_enumerated_list(self, node) -> None:
405 self.compact_simple, self.compact_p = self.context.pop()
406 self.body.append('</ol>\n')
408 # use table for field-list:
409 def visit_field(self, node) -> None:
410 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
412 def depart_field(self, node) -> None:
413 self.body.append('</tr>\n')
415 def visit_field_body(self, node) -> None:
416 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
417 self.set_class_on_child(node, 'first', 0)
418 field = node.parent
419 if (self.compact_field_list
420 or isinstance(field.parent, nodes.docinfo)
421 or field.parent.index(field) == len(field.parent) - 1):
422 # If we are in a compact list, the docinfo, or if this is
423 # the last field of the field list, do not add vertical
424 # space after last element.
425 self.set_class_on_child(node, 'last', -1)
427 def depart_field_body(self, node) -> None:
428 self.body.append('</td>\n')
430 def visit_field_list(self, node) -> None:
431 self.context.append((self.compact_field_list, self.compact_p))
432 self.compact_p = None
433 if 'compact' in node['classes']:
434 self.compact_field_list = True
435 elif (self.settings.compact_field_lists
436 and 'open' not in node['classes']):
437 self.compact_field_list = True
438 if self.compact_field_list:
439 for field in node:
440 field_body = field[-1]
441 assert isinstance(field_body, nodes.field_body)
442 children = [n for n in field_body
443 if not isinstance(n, nodes.Invisible)]
444 if not (len(children) == 0
445 or len(children) == 1
446 and isinstance(children[0],
447 (nodes.paragraph, nodes.line_block))):
448 self.compact_field_list = False
449 break
450 self.body.append(self.starttag(node, 'table', frame='void',
451 rules='none',
452 CLASS='docutils field-list'))
453 self.body.append('<col class="field-name" />\n'
454 '<col class="field-body" />\n'
455 '<tbody valign="top">\n')
457 def depart_field_list(self, node) -> None:
458 self.body.append('</tbody>\n</table>\n')
459 self.compact_field_list, self.compact_p = self.context.pop()
461 def visit_field_name(self, node) -> None:
462 atts = {}
463 if self.in_docinfo:
464 atts['class'] = 'docinfo-name'
465 else:
466 atts['class'] = 'field-name'
467 if (self.settings.field_name_limit
468 and len(node.astext()) > self.settings.field_name_limit):
469 atts['colspan'] = 2
470 self.context.append('</tr>\n'
471 + self.starttag(node.parent, 'tr', '',
472 CLASS='field')
473 + '<td>&nbsp;</td>')
474 else:
475 self.context.append('')
476 self.body.append(self.starttag(node, 'th', '', **atts))
478 def depart_field_name(self, node) -> None:
479 self.body.append(':</th>')
480 self.body.append(self.context.pop())
482 # use table for footnote text
483 def visit_footnote(self, node) -> None:
484 self.body.append(self.starttag(node, 'table',
485 CLASS='docutils footnote',
486 frame="void", rules="none"))
487 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
488 '<tbody valign="top">\n'
489 '<tr>')
490 self.footnote_backrefs(node)
492 def footnote_backrefs(self, node) -> None:
493 backlinks = []
494 backrefs = node['backrefs']
495 if self.settings.footnote_backlinks and backrefs:
496 if len(backrefs) == 1:
497 self.context.append('')
498 self.context.append('</a>')
499 self.context.append('<a class="fn-backref" href="#%s">'
500 % backrefs[0])
501 else:
502 for (i, backref) in enumerate(backrefs, 1):
503 backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
504 % (backref, i))
505 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
506 self.context += ['', '']
507 else:
508 self.context.append('')
509 self.context += ['', '']
510 # If the node does not only consist of a label.
511 if len(node) > 1:
512 # If there are preceding backlinks, we do not set class
513 # 'first', because we need to retain the top-margin.
514 if not backlinks:
515 node[1]['classes'].append('first')
516 node[-1]['classes'].append('last')
518 def depart_footnote(self, node) -> None:
519 self.body.append('</td></tr>\n'
520 '</tbody>\n</table>\n')
522 # insert markers in text (pseudo-classes are not supported in CSS1):
523 def visit_footnote_reference(self, node) -> None:
524 href = '#' + node['refid']
525 format = self.settings.footnote_references
526 if format == 'brackets':
527 suffix = '['
528 self.context.append(']')
529 else:
530 assert format == 'superscript'
531 suffix = '<sup>'
532 self.context.append('</sup>')
533 self.body.append(self.starttag(node, 'a', suffix,
534 CLASS='footnote-reference', href=href))
536 def depart_footnote_reference(self, node) -> None:
537 self.body.append(self.context.pop() + '</a>')
539 # just pass on generated text
540 def visit_generated(self, node) -> None:
541 pass
543 # Backwards-compatibility implementation:
544 # * Do not use <video>,
545 # * don't embed images,
546 # * use <object> instead of <img> for SVG.
547 # (SVG not supported by IE up to version 8,
548 # html4css1 strives for IE6 compatibility.)
549 object_image_types = {'.svg': 'image/svg+xml',
550 '.swf': 'application/x-shockwave-flash',
551 '.mp4': 'video/mp4',
552 '.webm': 'video/webm',
553 '.ogg': 'video/ogg',
556 def visit_image(self, node) -> None:
557 atts = {}
558 uri = node['uri']
559 ext = os.path.splitext(uri)[1].lower()
560 if ext in self.object_image_types:
561 atts['data'] = uri
562 atts['type'] = self.object_image_types[ext]
563 else:
564 atts['src'] = uri
565 atts['alt'] = node.get('alt', uri)
566 # image size
567 if 'width' in node:
568 atts['width'] = node['width']
569 if 'height' in node:
570 atts['height'] = node['height']
571 if 'scale' in node:
572 if (PIL and ('width' not in node or 'height' not in node)
573 and self.settings.file_insertion_enabled):
574 try:
575 imagepath = self.uri2imagepath(uri)
576 with PIL.Image.open(imagepath) as img:
577 img_size = img.size
578 except (ValueError, OSError, UnicodeEncodeError) as e:
579 self.document.reporter.warning(
580 f'Problem reading image file: {e}')
581 else:
582 self.settings.record_dependencies.add(
583 imagepath.replace('\\', '/'))
584 if 'width' not in atts:
585 atts['width'] = '%dpx' % img_size[0]
586 if 'height' not in atts:
587 atts['height'] = '%dpx' % img_size[1]
588 for att_name in 'width', 'height':
589 if att_name in atts:
590 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
591 assert match
592 atts[att_name] = '%s%s' % (
593 float(match.group(1)) * (float(node['scale']) / 100),
594 match.group(2))
595 style = []
596 for att_name in 'width', 'height':
597 if att_name in atts:
598 if re.match(r'^[0-9.]+$', atts[att_name]):
599 # Interpret unitless values as pixels.
600 atts[att_name] += 'px'
601 style.append('%s: %s;' % (att_name, atts[att_name]))
602 del atts[att_name]
603 if style:
604 atts['style'] = ' '.join(style)
605 # No newlines around inline images.
606 if (not isinstance(node.parent, nodes.TextElement)
607 or isinstance(node.parent, nodes.reference)
608 and not isinstance(node.parent.parent, nodes.TextElement)):
609 suffix = '\n'
610 else:
611 suffix = ''
612 if 'align' in node:
613 atts['class'] = 'align-%s' % node['align']
614 if ext in self.object_image_types:
615 # do NOT use an empty tag: incorrect rendering in browsers
616 self.body.append(self.starttag(node, 'object', '', **atts)
617 + node.get('alt', uri) + '</object>' + suffix)
618 else:
619 self.body.append(self.emptytag(node, 'img', suffix, **atts))
621 def depart_image(self, node) -> None:
622 pass
624 # use table for footnote text,
625 # context added in footnote_backrefs.
626 def visit_label(self, node) -> None:
627 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
628 CLASS='label'))
630 def depart_label(self, node) -> None:
631 self.body.append(f']{self.context.pop()}</td><td>{self.context.pop()}')
633 # ersatz for first/last pseudo-classes
634 def visit_list_item(self, node) -> None:
635 self.body.append(self.starttag(node, 'li', ''))
636 if len(node):
637 node[0]['classes'].append('first')
639 def depart_list_item(self, node) -> None:
640 self.body.append('</li>\n')
642 # use <tt> (not supported by HTML5),
643 # cater for limited styling options in CSS1 using hard-coded NBSPs
644 def visit_literal(self, node):
645 # special case: "code" role
646 classes = node['classes']
647 if 'code' in classes:
648 # filter 'code' from class arguments
649 node['classes'] = [cls for cls in classes if cls != 'code']
650 self.body.append(self.starttag(node, 'code', ''))
651 return
652 self.body.append(
653 self.starttag(node, 'tt', '', CLASS='docutils literal'))
654 text = node.astext()
655 for token in self.words_and_spaces.findall(text):
656 if token.strip():
657 # Protect text like "--an-option" and the regular expression
658 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
659 if self.in_word_wrap_point.search(token):
660 self.body.append('<span class="pre">%s</span>'
661 % self.encode(token))
662 else:
663 self.body.append(self.encode(token))
664 elif token in ('\n', ' '):
665 # Allow breaks at whitespace:
666 self.body.append(token)
667 else:
668 # Protect runs of multiple spaces; the last space can wrap:
669 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
670 self.body.append('</tt>')
671 # Content already processed:
672 raise nodes.SkipNode
674 def depart_literal(self, node) -> None:
675 # skipped unless literal element is from "code" role:
676 self.body.append('</code>')
678 # add newline after wrapper tags, don't use <code> for code
679 def visit_literal_block(self, node) -> None:
680 self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
682 def depart_literal_block(self, node) -> None:
683 self.body.append('\n</pre>\n')
685 # use table for option list
686 def visit_option_group(self, node) -> None:
687 atts = {}
688 if (self.settings.option_limit
689 and len(node.astext()) > self.settings.option_limit):
690 atts['colspan'] = 2
691 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
692 else:
693 self.context.append('')
694 self.body.append(
695 self.starttag(node, 'td', CLASS='option-group', **atts))
696 self.body.append('<kbd>')
697 self.context.append(0) # count number of options
699 def depart_option_group(self, node) -> None:
700 self.context.pop()
701 self.body.append('</kbd></td>\n')
702 self.body.append(self.context.pop())
704 def visit_option_list(self, node) -> None:
705 self.body.append(
706 self.starttag(node, 'table', CLASS='docutils option-list',
707 frame="void", rules="none"))
708 self.body.append('<col class="option" />\n'
709 '<col class="description" />\n'
710 '<tbody valign="top">\n')
712 def depart_option_list(self, node) -> None:
713 self.body.append('</tbody>\n</table>\n')
715 def visit_option_list_item(self, node) -> None:
716 self.body.append(self.starttag(node, 'tr', ''))
718 def depart_option_list_item(self, node) -> None:
719 self.body.append('</tr>\n')
721 # Omit <p> tags to produce visually compact lists (less vertical
722 # whitespace) as CSS styling requires CSS2.
723 def should_be_compact_paragraph(self, node) -> bool:
725 Determine if the <p> tags around paragraph ``node`` can be omitted.
727 if (isinstance(node.parent, nodes.document)
728 or isinstance(node.parent, nodes.compound)):
729 # Never compact paragraphs in document or compound.
730 return False
731 for key, value in node.attlist():
732 if (node.is_not_default(key)
733 and not (key == 'classes'
734 and value in ([], ['first'],
735 ['last'], ['first', 'last']))):
736 # Attribute which needs to survive.
737 return False
738 first = isinstance(node.parent[0], nodes.label) # skip label
739 for child in node.parent.children[first:]:
740 # only first paragraph can be compact
741 if isinstance(child, nodes.Invisible):
742 continue
743 if child is node:
744 break
745 return False
746 parent_length = len([n for n in node.parent if not isinstance(
747 n, (nodes.Invisible, nodes.label))])
748 if (self.compact_simple
749 or self.compact_field_list
750 or self.compact_p and parent_length == 1):
751 return True
752 return False
754 def visit_paragraph(self, node) -> None:
755 if self.should_be_compact_paragraph(node):
756 self.context.append('')
757 else:
758 self.body.append(self.starttag(node, 'p', ''))
759 self.context.append('</p>\n')
761 def depart_paragraph(self, node) -> None:
762 self.body.append(self.context.pop())
763 self.report_messages(node)
765 # ersatz for first/last pseudo-classes
766 def visit_sidebar(self, node) -> None:
767 self.body.append(
768 self.starttag(node, 'div', CLASS='sidebar'))
769 self.set_first_last(node)
770 self.in_sidebar = True
772 def depart_sidebar(self, node) -> None:
773 self.body.append('</div>\n')
774 self.in_sidebar = False
776 # <sub> not allowed in <pre>
777 def visit_subscript(self, node) -> None:
778 if isinstance(node.parent, nodes.literal_block):
779 self.body.append(self.starttag(node, 'span', '',
780 CLASS='subscript'))
781 else:
782 self.body.append(self.starttag(node, 'sub', ''))
784 def depart_subscript(self, node) -> None:
785 if isinstance(node.parent, nodes.literal_block):
786 self.body.append('</span>')
787 else:
788 self.body.append('</sub>')
790 # Use <h*> for subtitles (deprecated in HTML 5)
791 def visit_subtitle(self, node) -> None:
792 if isinstance(node.parent, nodes.sidebar):
793 self.body.append(self.starttag(node, 'p', '',
794 CLASS='sidebar-subtitle'))
795 self.context.append('</p>\n')
796 elif isinstance(node.parent, nodes.document):
797 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
798 self.context.append('</h2>\n')
799 self.in_document_title = len(self.body)
800 elif isinstance(node.parent, nodes.section):
801 tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
802 self.body.append(
803 self.starttag(node, tag, '', CLASS='section-subtitle')
804 + self.starttag({}, 'span', '', CLASS='section-subtitle'))
805 self.context.append('</span></%s>\n' % tag)
807 def depart_subtitle(self, node) -> None:
808 self.body.append(self.context.pop())
809 if self.in_document_title:
810 self.subtitle = self.body[self.in_document_title:-1]
811 self.in_document_title = 0
812 self.body_pre_docinfo.extend(self.body)
813 self.html_subtitle.extend(self.body)
814 del self.body[:]
816 # <sup> not allowed in <pre> in HTML 4
817 def visit_superscript(self, node) -> None:
818 if isinstance(node.parent, nodes.literal_block):
819 self.body.append(self.starttag(node, 'span', '',
820 CLASS='superscript'))
821 else:
822 self.body.append(self.starttag(node, 'sup', ''))
824 def depart_superscript(self, node) -> None:
825 if isinstance(node.parent, nodes.literal_block):
826 self.body.append('</span>')
827 else:
828 self.body.append('</sup>')
830 # <tt> element deprecated in HTML 5
831 def visit_system_message(self, node) -> None:
832 self.body.append(self.starttag(node, 'div', CLASS='system-message'))
833 self.body.append('<p class="system-message-title">')
834 backref_text = ''
835 if len(node['backrefs']):
836 backrefs = node['backrefs']
837 if len(backrefs) == 1:
838 backref_text = ('; <em><a href="#%s">backlink</a></em>'
839 % backrefs[0])
840 else:
841 i = 1
842 backlinks = []
843 for backref in backrefs:
844 backlinks.append('<a href="#%s">%s</a>' % (backref, i))
845 i += 1
846 backref_text = ('; <em>backlinks: %s</em>'
847 % ', '.join(backlinks))
848 if node.hasattr('line'):
849 line = ', line %s' % node['line']
850 else:
851 line = ''
852 self.body.append('System Message: %s/%s '
853 '(<tt class="docutils">%s</tt>%s)%s</p>\n'
854 % (node['type'], node['level'],
855 self.encode(node['source']), line, backref_text))
857 def depart_system_message(self, node) -> None:
858 self.body.append('</div>\n')
860 # "hard coded" border setting
861 def visit_table(self, node) -> None:
862 self.context.append(self.compact_p)
863 self.compact_p = True
864 atts = {'border': 1}
865 classes = ['docutils', self.settings.table_style]
866 if 'align' in node:
867 classes.append('align-%s' % node['align'])
868 if 'width' in node:
869 width = node['width']
870 if width[-1:] in '0123456789.': # unitless value
871 width += 'px' # add default length unit
872 atts['style'] = f'width: {width}'
873 self.body.append(
874 self.starttag(node, 'table', CLASS=' '.join(classes), **atts))
876 def depart_table(self, node) -> None:
877 self.compact_p = self.context.pop()
878 self.body.append('</table>\n')
880 # hard-coded vertical alignment
881 def visit_tbody(self, node) -> None:
882 self.body.append(self.starttag(node, 'tbody', valign='top'))
884 def depart_tbody(self, node) -> None:
885 self.body.append('</tbody>\n')
887 # no special handling of "details" in definition list
888 def visit_term(self, node) -> None:
889 self.body.append(self.starttag(node, 'dt', '',
890 classes=node.parent['classes'],
891 ids=node.parent['ids']))
893 def depart_term(self, node) -> None:
894 # Nest (optional) classifier(s) in the <dt> element
895 if node.next_node(nodes.classifier, descend=False, siblings=True):
896 return # skip (depart_classifier() calls this function again)
897 self.body.append('</dt>\n')
899 # hard-coded vertical alignment
900 def visit_thead(self, node) -> None:
901 self.body.append(self.starttag(node, 'thead', valign='bottom'))
903 def depart_thead(self, node) -> None:
904 self.body.append('</thead>\n')
906 # auxiliary method, called by visit_title()
907 # "with-subtitle" class, no ARIA roles
908 def section_title_tags(self, node):
909 classes = []
910 h_level = self.section_level + self.initial_header_level - 1
911 if (len(node.parent) >= 2
912 and isinstance(node.parent[1], nodes.subtitle)):
913 classes.append('with-subtitle')
914 if h_level > 6:
915 classes.append('h%i' % h_level)
916 tagname = 'h%i' % min(h_level, 6)
917 start_tag = self.starttag(node, tagname, '', classes=classes)
918 if node.hasattr('refid'):
919 atts = {}
920 atts['class'] = 'toc-backref'
921 atts['href'] = '#' + node['refid']
922 start_tag += self.starttag({}, 'a', '', **atts)
923 close_tag = '</a></%s>\n' % tagname
924 else:
925 close_tag = '</%s>\n' % tagname
926 return start_tag, close_tag
929 class SimpleListChecker(writers._html_base.SimpleListChecker):
932 Raise `nodes.NodeFound` if non-simple list item is encountered.
934 Here "simple" means a list item containing nothing other than a single
935 paragraph, a simple list, or a paragraph followed by a simple list.
938 def visit_list_item(self, node):
939 children = [child for child in node.children
940 if not isinstance(child, nodes.Invisible)]
941 if (children and isinstance(children[0], nodes.paragraph)
942 and (isinstance(children[-1], nodes.bullet_list)
943 or isinstance(children[-1], nodes.enumerated_list))):
944 children.pop()
945 if len(children) <= 1:
946 return
947 else:
948 raise nodes.NodeFound
950 # def visit_bullet_list(self, node):
951 # pass
953 # def visit_enumerated_list(self, node):
954 # pass
956 def visit_paragraph(self, node):
957 raise nodes.SkipNode
959 def visit_definition_list(self, node):
960 raise nodes.NodeFound
962 def visit_docinfo(self, node):
963 raise nodes.NodeFound