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