1""" 2 sphinx.writers.html 3 ~~~~~~~~~~~~~~~~~~~ 4 5 docutils writers handling Sphinx' custom nodes. 6 7 :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. 8 :license: BSD, see LICENSE for details. 9""" 10 11import copy 12import os 13import posixpath 14import re 15import warnings 16from typing import Any, Iterable, Tuple, cast 17 18from docutils import nodes 19from docutils.nodes import Element, Node, Text 20from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator 21from docutils.writers.html4css1 import Writer 22 23from sphinx import addnodes 24from sphinx.builders import Builder 25from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning 26from sphinx.locale import _, __, admonitionlabels 27from sphinx.util import logging 28from sphinx.util.docutils import SphinxTranslator 29from sphinx.util.images import get_image_size 30 31if False: 32 # For type annotation 33 from sphinx.builders.html import StandaloneHTMLBuilder 34 35 36logger = logging.getLogger(__name__) 37 38# A good overview of the purpose behind these classes can be found here: 39# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html 40 41 42def multiply_length(length: str, scale: int) -> str: 43 """Multiply *length* (width or height) by *scale*.""" 44 matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) 45 if not matched: 46 return length 47 elif scale == 100: 48 return length 49 else: 50 amount, unit = matched.groups() 51 result = float(amount) * scale / 100 52 return "%s%s" % (int(result), unit) 53 54 55class HTMLWriter(Writer): 56 57 # override embed-stylesheet default value to 0. 58 settings_spec = copy.deepcopy(Writer.settings_spec) 59 for _setting in settings_spec[2]: 60 if '--embed-stylesheet' in _setting[1]: 61 _setting[2]['default'] = 0 62 63 def __init__(self, builder: "StandaloneHTMLBuilder") -> None: 64 super().__init__() 65 self.builder = builder 66 67 def translate(self) -> None: 68 # sadly, this is mostly copied from parent class 69 visitor = self.builder.create_translator(self.document, self.builder) 70 self.visitor = cast(HTMLTranslator, visitor) 71 self.document.walkabout(visitor) 72 self.output = self.visitor.astext() 73 for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix', 74 'body_pre_docinfo', 'docinfo', 'body', 'fragment', 75 'body_suffix', 'meta', 'title', 'subtitle', 'header', 76 'footer', 'html_prolog', 'html_head', 'html_title', 77 'html_subtitle', 'html_body', ): 78 setattr(self, attr, getattr(visitor, attr, None)) 79 self.clean_meta = ''.join(self.visitor.meta[2:]) 80 81 82class HTMLTranslator(SphinxTranslator, BaseTranslator): 83 """ 84 Our custom HTML translator. 85 """ 86 87 builder = None # type: StandaloneHTMLBuilder 88 89 def __init__(self, *args: Any) -> None: 90 if isinstance(args[0], nodes.document) and isinstance(args[1], Builder): 91 document, builder = args 92 else: 93 warnings.warn('The order of arguments for HTMLTranslator has been changed. ' 94 'Please give "document" as 1st and "builder" as 2nd.', 95 RemovedInSphinx40Warning, stacklevel=2) 96 builder, document = args 97 super().__init__(document, builder) 98 99 self.highlighter = self.builder.highlighter 100 self.docnames = [self.builder.current_docname] # for singlehtml builder 101 self.manpages_url = self.config.manpages_url 102 self.protect_literal_text = 0 103 self.secnumber_suffix = self.config.html_secnumber_suffix 104 self.param_separator = '' 105 self.optional_param_level = 0 106 self._table_row_index = 0 107 self._fieldlist_row_index = 0 108 self.required_params_left = 0 109 110 def visit_start_of_file(self, node: Element) -> None: 111 # only occurs in the single-file builder 112 self.docnames.append(node['docname']) 113 self.body.append('<span id="document-%s"></span>' % node['docname']) 114 115 def depart_start_of_file(self, node: Element) -> None: 116 self.docnames.pop() 117 118 def visit_desc(self, node: Element) -> None: 119 self.body.append(self.starttag(node, 'dl', CLASS=node['objtype'])) 120 121 def depart_desc(self, node: Element) -> None: 122 self.body.append('</dl>\n\n') 123 124 def visit_desc_signature(self, node: Element) -> None: 125 # the id is set automatically 126 self.body.append(self.starttag(node, 'dt')) 127 self.protect_literal_text += 1 128 129 def depart_desc_signature(self, node: Element) -> None: 130 self.protect_literal_text -= 1 131 if not node.get('is_multiline'): 132 self.add_permalink_ref(node, _('Permalink to this definition')) 133 self.body.append('</dt>\n') 134 135 def visit_desc_signature_line(self, node: Element) -> None: 136 pass 137 138 def depart_desc_signature_line(self, node: Element) -> None: 139 if node.get('add_permalink'): 140 # the permalink info is on the parent desc_signature node 141 self.add_permalink_ref(node.parent, _('Permalink to this definition')) 142 self.body.append('<br />') 143 144 def visit_desc_addname(self, node: Element) -> None: 145 self.body.append(self.starttag(node, 'code', '', CLASS='descclassname')) 146 147 def depart_desc_addname(self, node: Element) -> None: 148 self.body.append('</code>') 149 150 def visit_desc_type(self, node: Element) -> None: 151 pass 152 153 def depart_desc_type(self, node: Element) -> None: 154 pass 155 156 def visit_desc_returns(self, node: Element) -> None: 157 self.body.append(' → ') 158 159 def depart_desc_returns(self, node: Element) -> None: 160 pass 161 162 def visit_desc_name(self, node: Element) -> None: 163 self.body.append(self.starttag(node, 'code', '', CLASS='descname')) 164 165 def depart_desc_name(self, node: Element) -> None: 166 self.body.append('</code>') 167 168 def visit_desc_parameterlist(self, node: Element) -> None: 169 self.body.append('<span class="sig-paren">(</span>') 170 self.first_param = 1 171 self.optional_param_level = 0 172 # How many required parameters are left. 173 self.required_params_left = sum([isinstance(c, addnodes.desc_parameter) 174 for c in node.children]) 175 self.param_separator = node.child_text_separator 176 177 def depart_desc_parameterlist(self, node: Element) -> None: 178 self.body.append('<span class="sig-paren">)</span>') 179 180 # If required parameters are still to come, then put the comma after 181 # the parameter. Otherwise, put the comma before. This ensures that 182 # signatures like the following render correctly (see issue #1001): 183 # 184 # foo([a, ]b, c[, d]) 185 # 186 def visit_desc_parameter(self, node: Element) -> None: 187 if self.first_param: 188 self.first_param = 0 189 elif not self.required_params_left: 190 self.body.append(self.param_separator) 191 if self.optional_param_level == 0: 192 self.required_params_left -= 1 193 if not node.hasattr('noemph'): 194 self.body.append('<em>') 195 196 def depart_desc_parameter(self, node: Element) -> None: 197 if not node.hasattr('noemph'): 198 self.body.append('</em>') 199 if self.required_params_left: 200 self.body.append(self.param_separator) 201 202 def visit_desc_optional(self, node: Element) -> None: 203 self.optional_param_level += 1 204 self.body.append('<span class="optional">[</span>') 205 206 def depart_desc_optional(self, node: Element) -> None: 207 self.optional_param_level -= 1 208 self.body.append('<span class="optional">]</span>') 209 210 def visit_desc_annotation(self, node: Element) -> None: 211 self.body.append(self.starttag(node, 'em', '', CLASS='property')) 212 213 def depart_desc_annotation(self, node: Element) -> None: 214 self.body.append('</em>') 215 216 def visit_desc_content(self, node: Element) -> None: 217 self.body.append(self.starttag(node, 'dd', '')) 218 219 def depart_desc_content(self, node: Element) -> None: 220 self.body.append('</dd>') 221 222 def visit_versionmodified(self, node: Element) -> None: 223 self.body.append(self.starttag(node, 'div', CLASS=node['type'])) 224 225 def depart_versionmodified(self, node: Element) -> None: 226 self.body.append('</div>\n') 227 228 # overwritten 229 def visit_reference(self, node: Element) -> None: 230 atts = {'class': 'reference'} 231 if node.get('internal') or 'refuri' not in node: 232 atts['class'] += ' internal' 233 else: 234 atts['class'] += ' external' 235 if 'refuri' in node: 236 atts['href'] = node['refuri'] or '#' 237 if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'): 238 atts['href'] = self.cloak_mailto(atts['href']) 239 self.in_mailto = True 240 else: 241 assert 'refid' in node, \ 242 'References must have "refuri" or "refid" attribute.' 243 atts['href'] = '#' + node['refid'] 244 if not isinstance(node.parent, nodes.TextElement): 245 assert len(node) == 1 and isinstance(node[0], nodes.image) 246 atts['class'] += ' image-reference' 247 if 'reftitle' in node: 248 atts['title'] = node['reftitle'] 249 if 'target' in node: 250 atts['target'] = node['target'] 251 self.body.append(self.starttag(node, 'a', '', **atts)) 252 253 if node.get('secnumber'): 254 self.body.append(('%s' + self.secnumber_suffix) % 255 '.'.join(map(str, node['secnumber']))) 256 257 def visit_number_reference(self, node: Element) -> None: 258 self.visit_reference(node) 259 260 def depart_number_reference(self, node: Element) -> None: 261 self.depart_reference(node) 262 263 # overwritten -- we don't want source comments to show up in the HTML 264 def visit_comment(self, node: Element) -> None: # type: ignore 265 raise nodes.SkipNode 266 267 # overwritten 268 def visit_admonition(self, node: Element, name: str = '') -> None: 269 self.body.append(self.starttag( 270 node, 'div', CLASS=('admonition ' + name))) 271 if name: 272 node.insert(0, nodes.title(name, admonitionlabels[name])) 273 self.set_first_last(node) 274 275 def visit_seealso(self, node: Element) -> None: 276 self.visit_admonition(node, 'seealso') 277 278 def depart_seealso(self, node: Element) -> None: 279 self.depart_admonition(node) 280 281 def get_secnumber(self, node: Element) -> Tuple[int, ...]: 282 if node.get('secnumber'): 283 return node['secnumber'] 284 elif isinstance(node.parent, nodes.section): 285 if self.builder.name == 'singlehtml': 286 docname = self.docnames[-1] 287 anchorname = "%s/#%s" % (docname, node.parent['ids'][0]) 288 if anchorname not in self.builder.secnumbers: 289 anchorname = "%s/" % docname # try first heading which has no anchor 290 else: 291 anchorname = '#' + node.parent['ids'][0] 292 if anchorname not in self.builder.secnumbers: 293 anchorname = '' # try first heading which has no anchor 294 295 if self.builder.secnumbers.get(anchorname): 296 return self.builder.secnumbers[anchorname] 297 298 return None 299 300 def add_secnumber(self, node: Element) -> None: 301 secnumber = self.get_secnumber(node) 302 if secnumber: 303 self.body.append('<span class="section-number">%s</span>' % 304 ('.'.join(map(str, secnumber)) + self.secnumber_suffix)) 305 306 def add_fignumber(self, node: Element) -> None: 307 def append_fignumber(figtype: str, figure_id: str) -> None: 308 if self.builder.name == 'singlehtml': 309 key = "%s/%s" % (self.docnames[-1], figtype) 310 else: 311 key = figtype 312 313 if figure_id in self.builder.fignumbers.get(key, {}): 314 self.body.append('<span class="caption-number">') 315 prefix = self.config.numfig_format.get(figtype) 316 if prefix is None: 317 msg = __('numfig_format is not defined for %s') % figtype 318 logger.warning(msg) 319 else: 320 numbers = self.builder.fignumbers[key][figure_id] 321 self.body.append(prefix % '.'.join(map(str, numbers)) + ' ') 322 self.body.append('</span>') 323 324 figtype = self.builder.env.domains['std'].get_enumerable_node_type(node) 325 if figtype: 326 if len(node['ids']) == 0: 327 msg = __('Any IDs not assigned for %s node') % node.tagname 328 logger.warning(msg, location=node) 329 else: 330 append_fignumber(figtype, node['ids'][0]) 331 332 def add_permalink_ref(self, node: Element, title: str) -> None: 333 if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks: 334 format = '<a class="headerlink" href="#%s" title="%s">%s</a>' 335 self.body.append(format % (node['ids'][0], title, 336 self.config.html_permalinks_icon)) 337 338 def generate_targets_for_listing(self, node: Element) -> None: 339 """Generate hyperlink targets for listings. 340 341 Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list() 342 generates hyperlink targets inside listing tags (<ul>, <ol> and <dl>) if multiple 343 IDs are assigned to listings. That is invalid DOM structure. 344 (This is a bug of docutils <= 0.12) 345 346 This exports hyperlink targets before listings to make valid DOM structure. 347 """ 348 for id in node['ids'][1:]: 349 self.body.append('<span id="%s"></span>' % id) 350 node['ids'].remove(id) 351 352 # overwritten 353 def visit_bullet_list(self, node: Element) -> None: 354 if len(node) == 1 and isinstance(node[0], addnodes.toctree): 355 # avoid emitting empty <ul></ul> 356 raise nodes.SkipNode 357 self.generate_targets_for_listing(node) 358 super().visit_bullet_list(node) 359 360 # overwritten 361 def visit_enumerated_list(self, node: Element) -> None: 362 self.generate_targets_for_listing(node) 363 super().visit_enumerated_list(node) 364 365 # overwritten 366 def visit_definition(self, node: Element) -> None: 367 # don't insert </dt> here. 368 self.body.append(self.starttag(node, 'dd', '')) 369 370 # overwritten 371 def depart_definition(self, node: Element) -> None: 372 self.body.append('</dd>\n') 373 374 # overwritten 375 def visit_classifier(self, node: Element) -> None: 376 self.body.append(self.starttag(node, 'span', '', CLASS='classifier')) 377 378 # overwritten 379 def depart_classifier(self, node: Element) -> None: 380 self.body.append('</span>') 381 382 next_node = node.next_node(descend=False, siblings=True) # type: Node 383 if not isinstance(next_node, nodes.classifier): 384 # close `<dt>` tag at the tail of classifiers 385 self.body.append('</dt>') 386 387 # overwritten 388 def visit_term(self, node: Element) -> None: 389 self.body.append(self.starttag(node, 'dt', '')) 390 391 # overwritten 392 def depart_term(self, node: Element) -> None: 393 next_node = node.next_node(descend=False, siblings=True) # type: Node 394 if isinstance(next_node, nodes.classifier): 395 # Leave the end tag to `self.depart_classifier()`, in case 396 # there's a classifier. 397 pass 398 else: 399 if isinstance(node.parent.parent.parent, addnodes.glossary): 400 # add permalink if glossary terms 401 self.add_permalink_ref(node, _('Permalink to this term')) 402 403 self.body.append('</dt>') 404 405 # overwritten 406 def visit_title(self, node: Element) -> None: 407 super().visit_title(node) 408 self.add_secnumber(node) 409 self.add_fignumber(node.parent) 410 if isinstance(node.parent, nodes.table): 411 self.body.append('<span class="caption-text">') 412 413 def depart_title(self, node: Element) -> None: 414 close_tag = self.context[-1] 415 if (self.config.html_permalinks and self.builder.add_permalinks and 416 node.parent.hasattr('ids') and node.parent['ids']): 417 # add permalink anchor 418 if close_tag.startswith('</h'): 419 self.add_permalink_ref(node.parent, _('Permalink to this headline')) 420 elif close_tag.startswith('</a></h'): 421 self.body.append('</a><a class="headerlink" href="#%s" ' % 422 node.parent['ids'][0] + 423 'title="%s">%s' % ( 424 _('Permalink to this headline'), 425 self.config.html_permalinks_icon)) 426 elif isinstance(node.parent, nodes.table): 427 self.body.append('</span>') 428 self.add_permalink_ref(node.parent, _('Permalink to this table')) 429 elif isinstance(node.parent, nodes.table): 430 self.body.append('</span>') 431 432 super().depart_title(node) 433 434 # overwritten 435 def visit_literal_block(self, node: Element) -> None: 436 if node.rawsource != node.astext(): 437 # most probably a parsed-literal block -- don't highlight 438 return super().visit_literal_block(node) 439 440 lang = node.get('language', 'default') 441 linenos = node.get('linenos', False) 442 highlight_args = node.get('highlight_args', {}) 443 highlight_args['force'] = node.get('force', False) 444 opts = self.config.highlight_options.get(lang, {}) 445 446 if linenos and self.config.html_codeblock_linenos_style: 447 linenos = self.config.html_codeblock_linenos_style 448 449 highlighted = self.highlighter.highlight_block( 450 node.rawsource, lang, opts=opts, linenos=linenos, 451 location=node, **highlight_args 452 ) 453 starttag = self.starttag(node, 'div', suffix='', 454 CLASS='highlight-%s notranslate' % lang) 455 self.body.append(starttag + highlighted + '</div>\n') 456 raise nodes.SkipNode 457 458 def visit_caption(self, node: Element) -> None: 459 if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): 460 self.body.append('<div class="code-block-caption">') 461 else: 462 super().visit_caption(node) 463 self.add_fignumber(node.parent) 464 self.body.append(self.starttag(node, 'span', '', CLASS='caption-text')) 465 466 def depart_caption(self, node: Element) -> None: 467 self.body.append('</span>') 468 469 # append permalink if available 470 if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): 471 self.add_permalink_ref(node.parent, _('Permalink to this code')) 472 elif isinstance(node.parent, nodes.figure): 473 self.add_permalink_ref(node.parent, _('Permalink to this image')) 474 elif node.parent.get('toctree'): 475 self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree')) 476 477 if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): 478 self.body.append('</div>\n') 479 else: 480 super().depart_caption(node) 481 482 def visit_doctest_block(self, node: Element) -> None: 483 self.visit_literal_block(node) 484 485 # overwritten to add the <div> (for XHTML compliance) 486 def visit_block_quote(self, node: Element) -> None: 487 self.body.append(self.starttag(node, 'blockquote') + '<div>') 488 489 def depart_block_quote(self, node: Element) -> None: 490 self.body.append('</div></blockquote>\n') 491 492 # overwritten 493 def visit_literal(self, node: Element) -> None: 494 if 'kbd' in node['classes']: 495 self.body.append(self.starttag(node, 'kbd', '', 496 CLASS='docutils literal notranslate')) 497 else: 498 self.body.append(self.starttag(node, 'code', '', 499 CLASS='docutils literal notranslate')) 500 self.protect_literal_text += 1 501 502 def depart_literal(self, node: Element) -> None: 503 if 'kbd' in node['classes']: 504 self.body.append('</kbd>') 505 else: 506 self.protect_literal_text -= 1 507 self.body.append('</code>') 508 509 def visit_productionlist(self, node: Element) -> None: 510 self.body.append(self.starttag(node, 'pre')) 511 names = [] 512 productionlist = cast(Iterable[addnodes.production], node) 513 for production in productionlist: 514 names.append(production['tokenname']) 515 maxlen = max(len(name) for name in names) 516 lastname = None 517 for production in productionlist: 518 if production['tokenname']: 519 lastname = production['tokenname'].ljust(maxlen) 520 self.body.append(self.starttag(production, 'strong', '')) 521 self.body.append(lastname + '</strong> ::= ') 522 elif lastname is not None: 523 self.body.append('%s ' % (' ' * len(lastname))) 524 production.walkabout(self) 525 self.body.append('\n') 526 self.body.append('</pre>\n') 527 raise nodes.SkipNode 528 529 def depart_productionlist(self, node: Element) -> None: 530 pass 531 532 def visit_production(self, node: Element) -> None: 533 pass 534 535 def depart_production(self, node: Element) -> None: 536 pass 537 538 def visit_centered(self, node: Element) -> None: 539 self.body.append(self.starttag(node, 'p', CLASS="centered") + 540 '<strong>') 541 542 def depart_centered(self, node: Element) -> None: 543 self.body.append('</strong></p>') 544 545 # overwritten 546 def should_be_compact_paragraph(self, node: Node) -> bool: 547 """Determine if the <p> tags around paragraph can be omitted.""" 548 if isinstance(node.parent, addnodes.desc_content): 549 # Never compact desc_content items. 550 return False 551 if isinstance(node.parent, addnodes.versionmodified): 552 # Never compact versionmodified nodes. 553 return False 554 return super().should_be_compact_paragraph(node) 555 556 def visit_compact_paragraph(self, node: Element) -> None: 557 pass 558 559 def depart_compact_paragraph(self, node: Element) -> None: 560 pass 561 562 def visit_download_reference(self, node: Element) -> None: 563 atts = {'class': 'reference download', 564 'download': ''} 565 566 if not self.builder.download_support: 567 self.context.append('') 568 elif 'refuri' in node: 569 atts['class'] += ' external' 570 atts['href'] = node['refuri'] 571 self.body.append(self.starttag(node, 'a', '', **atts)) 572 self.context.append('</a>') 573 elif 'filename' in node: 574 atts['class'] += ' internal' 575 atts['href'] = posixpath.join(self.builder.dlpath, node['filename']) 576 self.body.append(self.starttag(node, 'a', '', **atts)) 577 self.context.append('</a>') 578 else: 579 self.context.append('') 580 581 def depart_download_reference(self, node: Element) -> None: 582 self.body.append(self.context.pop()) 583 584 # overwritten 585 def visit_image(self, node: Element) -> None: 586 olduri = node['uri'] 587 # rewrite the URI if the environment knows about it 588 if olduri in self.builder.images: 589 node['uri'] = posixpath.join(self.builder.imgpath, 590 self.builder.images[olduri]) 591 592 if 'scale' in node: 593 # Try to figure out image height and width. Docutils does that too, 594 # but it tries the final file name, which does not necessarily exist 595 # yet at the time the HTML file is written. 596 if not ('width' in node and 'height' in node): 597 size = get_image_size(os.path.join(self.builder.srcdir, olduri)) 598 if size is None: 599 logger.warning(__('Could not obtain image size. :scale: option is ignored.'), # NOQA 600 location=node) 601 else: 602 if 'width' not in node: 603 node['width'] = str(size[0]) 604 if 'height' not in node: 605 node['height'] = str(size[1]) 606 607 uri = node['uri'] 608 if uri.lower().endswith(('svg', 'svgz')): 609 atts = {'src': uri} 610 if 'width' in node: 611 atts['width'] = node['width'] 612 if 'height' in node: 613 atts['height'] = node['height'] 614 if 'scale' in node: 615 if 'width' in atts: 616 atts['width'] = multiply_length(atts['width'], node['scale']) 617 if 'height' in atts: 618 atts['height'] = multiply_length(atts['height'], node['scale']) 619 atts['alt'] = node.get('alt', uri) 620 if 'align' in node: 621 atts['class'] = 'align-%s' % node['align'] 622 self.body.append(self.emptytag(node, 'img', '', **atts)) 623 return 624 625 super().visit_image(node) 626 627 # overwritten 628 def depart_image(self, node: Element) -> None: 629 if node['uri'].lower().endswith(('svg', 'svgz')): 630 pass 631 else: 632 super().depart_image(node) 633 634 def visit_toctree(self, node: Element) -> None: 635 # this only happens when formatting a toc from env.tocs -- in this 636 # case we don't want to include the subtree 637 raise nodes.SkipNode 638 639 def visit_index(self, node: Element) -> None: 640 raise nodes.SkipNode 641 642 def visit_tabular_col_spec(self, node: Element) -> None: 643 raise nodes.SkipNode 644 645 def visit_glossary(self, node: Element) -> None: 646 pass 647 648 def depart_glossary(self, node: Element) -> None: 649 pass 650 651 def visit_acks(self, node: Element) -> None: 652 pass 653 654 def depart_acks(self, node: Element) -> None: 655 pass 656 657 def visit_hlist(self, node: Element) -> None: 658 self.body.append('<table class="hlist"><tr>') 659 660 def depart_hlist(self, node: Element) -> None: 661 self.body.append('</tr></table>\n') 662 663 def visit_hlistcol(self, node: Element) -> None: 664 self.body.append('<td>') 665 666 def depart_hlistcol(self, node: Element) -> None: 667 self.body.append('</td>') 668 669 def visit_option_group(self, node: Element) -> None: 670 super().visit_option_group(node) 671 self.context[-2] = self.context[-2].replace(' ', ' ') 672 673 # overwritten 674 def visit_Text(self, node: Text) -> None: 675 text = node.astext() 676 encoded = self.encode(text) 677 if self.protect_literal_text: 678 # moved here from base class's visit_literal to support 679 # more formatting in literal nodes 680 for token in self.words_and_spaces.findall(encoded): 681 if token.strip(): 682 # protect literal text from line wrapping 683 self.body.append('<span class="pre">%s</span>' % token) 684 elif token in ' \n': 685 # allow breaks at whitespace 686 self.body.append(token) 687 else: 688 # protect runs of multiple spaces; the last one can wrap 689 self.body.append(' ' * (len(token) - 1) + ' ') 690 else: 691 if self.in_mailto and self.settings.cloak_email_addresses: 692 encoded = self.cloak_email(encoded) 693 self.body.append(encoded) 694 695 def visit_note(self, node: Element) -> None: 696 self.visit_admonition(node, 'note') 697 698 def depart_note(self, node: Element) -> None: 699 self.depart_admonition(node) 700 701 def visit_warning(self, node: Element) -> None: 702 self.visit_admonition(node, 'warning') 703 704 def depart_warning(self, node: Element) -> None: 705 self.depart_admonition(node) 706 707 def visit_attention(self, node: Element) -> None: 708 self.visit_admonition(node, 'attention') 709 710 def depart_attention(self, node: Element) -> None: 711 self.depart_admonition(node) 712 713 def visit_caution(self, node: Element) -> None: 714 self.visit_admonition(node, 'caution') 715 716 def depart_caution(self, node: Element) -> None: 717 self.depart_admonition(node) 718 719 def visit_danger(self, node: Element) -> None: 720 self.visit_admonition(node, 'danger') 721 722 def depart_danger(self, node: Element) -> None: 723 self.depart_admonition(node) 724 725 def visit_error(self, node: Element) -> None: 726 self.visit_admonition(node, 'error') 727 728 def depart_error(self, node: Element) -> None: 729 self.depart_admonition(node) 730 731 def visit_hint(self, node: Element) -> None: 732 self.visit_admonition(node, 'hint') 733 734 def depart_hint(self, node: Element) -> None: 735 self.depart_admonition(node) 736 737 def visit_important(self, node: Element) -> None: 738 self.visit_admonition(node, 'important') 739 740 def depart_important(self, node: Element) -> None: 741 self.depart_admonition(node) 742 743 def visit_tip(self, node: Element) -> None: 744 self.visit_admonition(node, 'tip') 745 746 def depart_tip(self, node: Element) -> None: 747 self.depart_admonition(node) 748 749 def visit_literal_emphasis(self, node: Element) -> None: 750 return self.visit_emphasis(node) 751 752 def depart_literal_emphasis(self, node: Element) -> None: 753 return self.depart_emphasis(node) 754 755 def visit_literal_strong(self, node: Element) -> None: 756 return self.visit_strong(node) 757 758 def depart_literal_strong(self, node: Element) -> None: 759 return self.depart_strong(node) 760 761 def visit_abbreviation(self, node: Element) -> None: 762 attrs = {} 763 if node.hasattr('explanation'): 764 attrs['title'] = node['explanation'] 765 self.body.append(self.starttag(node, 'abbr', '', **attrs)) 766 767 def depart_abbreviation(self, node: Element) -> None: 768 self.body.append('</abbr>') 769 770 def visit_manpage(self, node: Element) -> None: 771 self.visit_literal_emphasis(node) 772 if self.manpages_url: 773 node['refuri'] = self.manpages_url.format(**node.attributes) 774 self.visit_reference(node) 775 776 def depart_manpage(self, node: Element) -> None: 777 if self.manpages_url: 778 self.depart_reference(node) 779 self.depart_literal_emphasis(node) 780 781 # overwritten to add even/odd classes 782 783 def visit_table(self, node: Element) -> None: 784 self._table_row_index = 0 785 return super().visit_table(node) 786 787 def visit_row(self, node: Element) -> None: 788 self._table_row_index += 1 789 if self._table_row_index % 2 == 0: 790 node['classes'].append('row-even') 791 else: 792 node['classes'].append('row-odd') 793 self.body.append(self.starttag(node, 'tr', '')) 794 node.column = 0 # type: ignore 795 796 def visit_entry(self, node: Element) -> None: 797 super().visit_entry(node) 798 if self.body[-1] == ' ': 799 self.body[-1] = ' ' 800 801 def visit_field_list(self, node: Element) -> None: 802 self._fieldlist_row_index = 0 803 return super().visit_field_list(node) 804 805 def visit_field(self, node: Element) -> None: 806 self._fieldlist_row_index += 1 807 if self._fieldlist_row_index % 2 == 0: 808 node['classes'].append('field-even') 809 else: 810 node['classes'].append('field-odd') 811 self.body.append(self.starttag(node, 'tr', '', CLASS='field')) 812 813 def visit_field_name(self, node: Element) -> None: 814 context_count = len(self.context) 815 super().visit_field_name(node) 816 if context_count != len(self.context): 817 self.context[-1] = self.context[-1].replace(' ', ' ') 818 819 def visit_math(self, node: Element, math_env: str = '') -> None: 820 name = self.builder.math_renderer_name 821 visit, _ = self.builder.app.registry.html_inline_math_renderers[name] 822 visit(self, node) 823 824 def depart_math(self, node: Element, math_env: str = '') -> None: 825 name = self.builder.math_renderer_name 826 _, depart = self.builder.app.registry.html_inline_math_renderers[name] 827 if depart: 828 depart(self, node) 829 830 def visit_math_block(self, node: Element, math_env: str = '') -> None: 831 name = self.builder.math_renderer_name 832 visit, _ = self.builder.app.registry.html_block_math_renderers[name] 833 visit(self, node) 834 835 def depart_math_block(self, node: Element, math_env: str = '') -> None: 836 name = self.builder.math_renderer_name 837 _, depart = self.builder.app.registry.html_block_math_renderers[name] 838 if depart: 839 depart(self, node) 840 841 def unknown_visit(self, node: Node) -> None: 842 raise NotImplementedError('Unknown node: ' + node.__class__.__name__) 843 844 @property 845 def permalink_text(self) -> str: 846 warnings.warn('HTMLTranslator.permalink_text is deprecated.', 847 RemovedInSphinx50Warning, stacklevel=2) 848 return self.config.html_permalinks_icon 849