1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# :Author: David Goodger, Günter Milde 4# Based on the html4css1 writer by David Goodger. 5# :Maintainer: docutils-develop@lists.sourceforge.net 6# :Revision: $Revision: 8644 $ 7# :Date: $Date: 2005-06-28$ 8# :Copyright: © 2016 David Goodger, Günter Milde 9# :License: Released under the terms of the `2-Clause BSD license`_, in short: 10# 11# Copying and distribution of this file, with or without modification, 12# are permitted in any medium without royalty provided the copyright 13# notice and this notice are preserved. 14# This file is offered as-is, without any warranty. 15# 16# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause 17 18"""common definitions for Docutils HTML writers""" 19 20import base64 21import mimetypes 22import os, os.path 23import re 24import sys 25 26try: # check for the Python Imaging Library 27 import PIL.Image 28except ImportError: 29 try: # sometimes PIL modules are put in PYTHONPATH's root 30 import Image 31 class PIL(object): pass # dummy wrapper 32 PIL.Image = Image 33 except ImportError: 34 PIL = None 35 36import docutils 37from docutils import nodes, utils, writers, languages, io 38from docutils.utils.error_reporting import SafeString 39from docutils.transforms import writer_aux 40from docutils.utils.math import (unichar2tex, pick_math_environment, 41 math2html, latex2mathml, tex2mathml_extern) 42 43if sys.version_info >= (3, 0): 44 from urllib.request import url2pathname 45else: 46 from urllib import url2pathname 47 48if sys.version_info >= (3, 0): 49 unicode = str # noqa 50 51 52class Writer(writers.Writer): 53 54 supported = ('html', 'xhtml') # update in subclass 55 """Formats this writer supports.""" 56 57 # default_stylesheets = [] # set in subclass! 58 # default_stylesheet_dirs = ['.'] # set in subclass! 59 default_template = 'template.txt' 60 # default_template_path = ... # set in subclass! 61 # settings_spec = ... # set in subclass! 62 63 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'} 64 65 # config_section = ... # set in subclass! 66 config_section_dependencies = ('writers', 'html writers') 67 68 visitor_attributes = ( 69 'head_prefix', 'head', 'stylesheet', 'body_prefix', 70 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix', 71 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment', 72 'html_prolog', 'html_head', 'html_title', 'html_subtitle', 73 'html_body') 74 75 def get_transforms(self): 76 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions] 77 78 def translate(self): 79 self.visitor = visitor = self.translator_class(self.document) 80 self.document.walkabout(visitor) 81 for attr in self.visitor_attributes: 82 setattr(self, attr, getattr(visitor, attr)) 83 self.output = self.apply_template() 84 85 def apply_template(self): 86 template_file = open(self.document.settings.template, 'rb') 87 template = unicode(template_file.read(), 'utf-8') 88 template_file.close() 89 subs = self.interpolation_dict() 90 return template % subs 91 92 def interpolation_dict(self): 93 subs = {} 94 settings = self.document.settings 95 for attr in self.visitor_attributes: 96 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n') 97 subs['encoding'] = settings.output_encoding 98 subs['version'] = docutils.__version__ 99 return subs 100 101 def assemble_parts(self): 102 writers.Writer.assemble_parts(self) 103 for part in self.visitor_attributes: 104 self.parts[part] = ''.join(getattr(self, part)) 105 106 107class HTMLTranslator(nodes.NodeVisitor): 108 109 """ 110 Generic Docutils to HTML translator. 111 112 See the `html4css1` and `html5_polyglot` writers for full featured 113 HTML writers. 114 115 .. IMPORTANT:: 116 The `visit_*` and `depart_*` methods use a 117 heterogeneous stack, `self.context`. 118 When subclassing, make sure to be consistent in its use! 119 120 Examples for robust coding: 121 122 a) Override both `visit_*` and `depart_*` methods, don't call the 123 parent functions. 124 125 b) Extend both and unconditionally call the parent functions:: 126 127 def visit_example(self, node): 128 if foo: 129 self.body.append('<div class="foo">') 130 html4css1.HTMLTranslator.visit_example(self, node) 131 132 def depart_example(self, node): 133 html4css1.HTMLTranslator.depart_example(self, node) 134 if foo: 135 self.body.append('</div>') 136 137 c) Extend both, calling the parent functions under the same 138 conditions:: 139 140 def visit_example(self, node): 141 if foo: 142 self.body.append('<div class="foo">\n') 143 else: # call the parent method 144 _html_base.HTMLTranslator.visit_example(self, node) 145 146 def depart_example(self, node): 147 if foo: 148 self.body.append('</div>\n') 149 else: # call the parent method 150 _html_base.HTMLTranslator.depart_example(self, node) 151 152 d) Extend one method (call the parent), but don't otherwise use the 153 `self.context` stack:: 154 155 def depart_example(self, node): 156 _html_base.HTMLTranslator.depart_example(self, node) 157 if foo: 158 # implementation-specific code 159 # that does not use `self.context` 160 self.body.append('</div>\n') 161 162 This way, changes in stack use will not bite you. 163 """ 164 165 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n' 166 doctype = '<!DOCTYPE html>\n' 167 doctype_mathml = doctype 168 169 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"' 170 ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n') 171 content_type = '<meta charset="%s"/>\n' 172 generator = ('<meta name="generator" content="Docutils %s: ' 173 'http://docutils.sourceforge.net/" />\n') 174 175 # Template for the MathJax script in the header: 176 mathjax_script = '<script type="text/javascript" src="%s"></script>\n' 177 178 mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js' 179 """ 180 URL of the MathJax javascript library. 181 182 The MathJax library ought to be installed on the same 183 server as the rest of the deployed site files and specified 184 in the `math-output` setting appended to "mathjax". 185 See `Docutils Configuration`__. 186 187 __ http://docutils.sourceforge.net/docs/user/config.html#math-output 188 189 The fallback tries a local MathJax installation at 190 ``/usr/share/javascript/mathjax/MathJax.js``. 191 """ 192 193 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n' 194 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n' 195 words_and_spaces = re.compile(r'[^ \n]+| +|\n') 196 # wrap point inside word: 197 in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+', re.U) 198 lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1 199 200 special_characters = {ord('&'): u'&', 201 ord('<'): u'<', 202 ord('"'): u'"', 203 ord('>'): u'>', 204 ord('@'): u'@', # may thwart address harvesters 205 } 206 """Character references for characters with a special meaning in HTML.""" 207 208 209 def __init__(self, document): 210 nodes.NodeVisitor.__init__(self, document) 211 self.settings = settings = document.settings 212 lcode = settings.language_code 213 self.language = languages.get_language(lcode, document.reporter) 214 self.meta = [self.generator % docutils.__version__] 215 self.head_prefix = [] 216 self.html_prolog = [] 217 if settings.xml_declaration: 218 self.head_prefix.append(self.xml_declaration 219 % settings.output_encoding) 220 # self.content_type = "" 221 # encoding not interpolated: 222 self.html_prolog.append(self.xml_declaration) 223 self.head = self.meta[:] 224 self.stylesheet = [self.stylesheet_call(path) 225 for path in utils.get_stylesheet_list(settings)] 226 self.body_prefix = ['</head>\n<body>\n'] 227 # document title, subtitle display 228 self.body_pre_docinfo = [] 229 # author, date, etc. 230 self.docinfo = [] 231 self.body = [] 232 self.fragment = [] 233 self.body_suffix = ['</body>\n</html>\n'] 234 self.section_level = 0 235 self.initial_header_level = int(settings.initial_header_level) 236 237 self.math_output = settings.math_output.split() 238 self.math_output_options = self.math_output[1:] 239 self.math_output = self.math_output[0].lower() 240 241 self.context = [] 242 """Heterogeneous stack. 243 244 Used by visit_* and depart_* functions in conjunction with the tree 245 traversal. Make sure that the pops correspond to the pushes.""" 246 247 self.topic_classes = [] 248 self.colspecs = [] 249 self.compact_p = True 250 self.compact_simple = False 251 self.compact_field_list = False 252 self.in_docinfo = False 253 self.in_sidebar = False 254 self.in_footnote_list = False 255 self.title = [] 256 self.subtitle = [] 257 self.header = [] 258 self.footer = [] 259 self.html_head = [self.content_type] # charset not interpolated 260 self.html_title = [] 261 self.html_subtitle = [] 262 self.html_body = [] 263 self.in_document_title = 0 # len(self.body) or 0 264 self.in_mailto = False 265 self.author_in_authors = False # for html4css1 266 self.math_header = [] 267 268 def astext(self): 269 return ''.join(self.head_prefix + self.head 270 + self.stylesheet + self.body_prefix 271 + self.body_pre_docinfo + self.docinfo 272 + self.body + self.body_suffix) 273 274 def encode(self, text): 275 """Encode special characters in `text` & return.""" 276 # Use only named entities known in both XML and HTML 277 # other characters are automatically encoded "by number" if required. 278 # @@@ A codec to do these and all other HTML entities would be nice. 279 text = unicode(text) 280 return text.translate(self.special_characters) 281 282 def cloak_mailto(self, uri): 283 """Try to hide a mailto: URL from harvesters.""" 284 # Encode "@" using a URL octet reference (see RFC 1738). 285 # Further cloaking with HTML entities will be done in the 286 # `attval` function. 287 return uri.replace('@', '%40') 288 289 def cloak_email(self, addr): 290 """Try to hide the link text of a email link from harversters.""" 291 # Surround at-signs and periods with <span> tags. ("@" has 292 # already been encoded to "@" by the `encode` method.) 293 addr = addr.replace('@', '<span>@</span>') 294 addr = addr.replace('.', '<span>.</span>') 295 return addr 296 297 def attval(self, text, 298 whitespace=re.compile('[\n\r\t\v\f]')): 299 """Cleanse, HTML encode, and return attribute value text.""" 300 encoded = self.encode(whitespace.sub(' ', text)) 301 if self.in_mailto and self.settings.cloak_email_addresses: 302 # Cloak at-signs ("%40") and periods with HTML entities. 303 encoded = encoded.replace('%40', '%40') 304 encoded = encoded.replace('.', '.') 305 return encoded 306 307 def stylesheet_call(self, path): 308 """Return code to reference or embed stylesheet file `path`""" 309 if self.settings.embed_stylesheet: 310 try: 311 content = io.FileInput(source_path=path, 312 encoding='utf-8').read() 313 self.settings.record_dependencies.add(path) 314 except IOError as err: 315 msg = u"Cannot embed stylesheet '%r': %s." % ( 316 path, SafeString(err.strerror)) 317 self.document.reporter.error(msg) 318 return '<--- %s --->\n' % msg 319 return self.embedded_stylesheet % content 320 # else link to style file: 321 if self.settings.stylesheet_path: 322 # adapt path relative to output (cf. config.html#stylesheet-path) 323 path = utils.relative_path(self.settings._destination, path) 324 return self.stylesheet_link % self.encode(path) 325 326 def starttag(self, node, tagname, suffix='\n', empty=False, **attributes): 327 """ 328 Construct and return a start tag given a node (id & class attributes 329 are extracted), tag name, and optional attributes. 330 """ 331 tagname = tagname.lower() 332 prefix = [] 333 atts = {} 334 ids = [] 335 for (name, value) in attributes.items(): 336 atts[name.lower()] = value 337 classes = [] 338 languages = [] 339 # unify class arguments and move language specification 340 for cls in node.get('classes', []) + atts.pop('class', '').split(): 341 if cls.startswith('language-'): 342 languages.append(cls[9:]) 343 elif cls.strip() and cls not in classes: 344 classes.append(cls) 345 if languages: 346 # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1 347 atts[self.lang_attribute] = languages[0] 348 if classes: 349 atts['class'] = ' '.join(classes) 350 assert 'id' not in atts 351 ids.extend(node.get('ids', [])) 352 if 'ids' in atts: 353 ids.extend(atts['ids']) 354 del atts['ids'] 355 if ids: 356 atts['id'] = ids[0] 357 for id in ids[1:]: 358 # Add empty "span" elements for additional IDs. Note 359 # that we cannot use empty "a" elements because there 360 # may be targets inside of references, but nested "a" 361 # elements aren't allowed in XHTML (even if they do 362 # not all have a "href" attribute). 363 if empty or isinstance(node, 364 (nodes.bullet_list, nodes.docinfo, 365 nodes.definition_list, nodes.enumerated_list, 366 nodes.field_list, nodes.option_list, 367 nodes.table)): 368 # Insert target right in front of element. 369 prefix.append('<span id="%s"></span>' % id) 370 else: 371 # Non-empty tag. Place the auxiliary <span> tag 372 # *inside* the element, as the first child. 373 suffix += '<span id="%s"></span>' % id 374 attlist = sorted(atts.items()) 375 parts = [tagname] 376 for name, value in attlist: 377 # value=None was used for boolean attributes without 378 # value, but this isn't supported by XHTML. 379 assert value is not None 380 if isinstance(value, list): 381 values = [unicode(v) for v in value] 382 parts.append('%s="%s"' % (name.lower(), 383 self.attval(' '.join(values)))) 384 else: 385 parts.append('%s="%s"' % (name.lower(), 386 self.attval(unicode(value)))) 387 if empty: 388 infix = ' /' 389 else: 390 infix = '' 391 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix 392 393 def emptytag(self, node, tagname, suffix='\n', **attributes): 394 """Construct and return an XML-compatible empty tag.""" 395 return self.starttag(node, tagname, suffix, empty=True, **attributes) 396 397 def set_class_on_child(self, node, class_, index=0): 398 """ 399 Set class `class_` on the visible child no. index of `node`. 400 Do nothing if node has fewer children than `index`. 401 """ 402 children = [n for n in node if not isinstance(n, nodes.Invisible)] 403 try: 404 child = children[index] 405 except IndexError: 406 return 407 child['classes'].append(class_) 408 409 def visit_Text(self, node): 410 text = node.astext() 411 encoded = self.encode(text) 412 if self.in_mailto and self.settings.cloak_email_addresses: 413 encoded = self.cloak_email(encoded) 414 self.body.append(encoded) 415 416 def depart_Text(self, node): 417 pass 418 419 def visit_abbreviation(self, node): 420 # @@@ implementation incomplete ("title" attribute) 421 self.body.append(self.starttag(node, 'abbr', '')) 422 423 def depart_abbreviation(self, node): 424 self.body.append('</abbr>') 425 426 def visit_acronym(self, node): 427 # @@@ implementation incomplete ("title" attribute) 428 self.body.append(self.starttag(node, 'acronym', '')) 429 430 def depart_acronym(self, node): 431 self.body.append('</acronym>') 432 433 def visit_address(self, node): 434 self.visit_docinfo_item(node, 'address', meta=False) 435 self.body.append(self.starttag(node, 'pre', 436 suffix= '', CLASS='address')) 437 438 def depart_address(self, node): 439 self.body.append('\n</pre>\n') 440 self.depart_docinfo_item() 441 442 def visit_admonition(self, node): 443 node['classes'].insert(0, 'admonition') 444 self.body.append(self.starttag(node, 'div')) 445 446 def depart_admonition(self, node=None): 447 self.body.append('</div>\n') 448 449 attribution_formats = {'dash': (u'\u2014', ''), 450 'parentheses': ('(', ')'), 451 'parens': ('(', ')'), 452 'none': ('', '')} 453 454 def visit_attribution(self, node): 455 prefix, suffix = self.attribution_formats[self.settings.attribution] 456 self.context.append(suffix) 457 self.body.append( 458 self.starttag(node, 'p', prefix, CLASS='attribution')) 459 460 def depart_attribution(self, node): 461 self.body.append(self.context.pop() + '</p>\n') 462 463 def visit_author(self, node): 464 if not(isinstance(node.parent, nodes.authors)): 465 self.visit_docinfo_item(node, 'author') 466 self.body.append('<p>') 467 468 def depart_author(self, node): 469 self.body.append('</p>') 470 if isinstance(node.parent, nodes.authors): 471 self.body.append('\n') 472 else: 473 self.depart_docinfo_item() 474 475 def visit_authors(self, node): 476 self.visit_docinfo_item(node, 'authors') 477 478 def depart_authors(self, node): 479 self.depart_docinfo_item() 480 481 def visit_block_quote(self, node): 482 self.body.append(self.starttag(node, 'blockquote')) 483 484 def depart_block_quote(self, node): 485 self.body.append('</blockquote>\n') 486 487 def check_simple_list(self, node): 488 """Check for a simple list that can be rendered compactly.""" 489 visitor = SimpleListChecker(self.document) 490 try: 491 node.walk(visitor) 492 except nodes.NodeFound: 493 return False 494 else: 495 return True 496 497 # Compact lists 498 # ------------ 499 # Include definition lists and field lists (in addition to ordered 500 # and unordered lists) in the test if a list is "simple" (cf. the 501 # html4css1.HTMLTranslator docstring and the SimpleListChecker class at 502 # the end of this file). 503 504 def is_compactable(self, node): 505 # explicite class arguments have precedence 506 if 'compact' in node['classes']: 507 return True 508 if 'open' in node['classes']: 509 return False 510 # check config setting: 511 if (isinstance(node, (nodes.field_list, nodes.definition_list)) 512 and not self.settings.compact_field_lists): 513 return False 514 if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list)) 515 and not self.settings.compact_lists): 516 return False 517 # Table of Contents: 518 if (self.topic_classes == ['contents']): 519 # TODO: look in parent nodes, remove self.topic_classes? 520 return True 521 # check the list items: 522 return self.check_simple_list(node) 523 524 def visit_bullet_list(self, node): 525 atts = {} 526 old_compact_simple = self.compact_simple 527 self.context.append((self.compact_simple, self.compact_p)) 528 self.compact_p = None 529 self.compact_simple = self.is_compactable(node) 530 if self.compact_simple and not old_compact_simple: 531 atts['class'] = 'simple' 532 self.body.append(self.starttag(node, 'ul', **atts)) 533 534 def depart_bullet_list(self, node): 535 self.compact_simple, self.compact_p = self.context.pop() 536 self.body.append('</ul>\n') 537 538 def visit_caption(self, node): 539 self.body.append(self.starttag(node, 'p', '', CLASS='caption')) 540 541 def depart_caption(self, node): 542 self.body.append('</p>\n') 543 544 def visit_citation(self, node): 545 # Use definition list for bibliographic references. 546 # Join adjacent citation entries. 547 # TODO: use <aside>. 548 if not self.in_footnote_list: 549 listnode = node.copy() 550 listnode['ids'] = [] 551 self.body.append(self.starttag(listnode, 'dl', CLASS='citation')) 552 # self.body.append('<dl class="citation">\n') 553 self.in_footnote_list = True 554 555 def depart_citation(self, node): 556 self.body.append('</dd>\n') 557 if not isinstance(node.next_node(descend=False, siblings=True), 558 nodes.citation): 559 self.body.append('</dl>\n') 560 self.in_footnote_list = False 561 562 def visit_citation_reference(self, node): 563 href = '#' 564 if 'refid' in node: 565 href += node['refid'] 566 elif 'refname' in node: 567 href += self.document.nameids[node['refname']] 568 # else: # TODO system message (or already in the transform)? 569 # 'Citation reference missing.' 570 self.body.append(self.starttag( 571 node, 'a', '[', CLASS='citation-reference', href=href)) 572 573 def depart_citation_reference(self, node): 574 self.body.append(']</a>') 575 576 # classifier 577 # ---------- 578 # don't insert classifier-delimiter here (done by CSS) 579 580 def visit_classifier(self, node): 581 self.body.append(self.starttag(node, 'span', '', CLASS='classifier')) 582 583 def depart_classifier(self, node): 584 self.body.append('</span>') 585 586 def visit_colspec(self, node): 587 self.colspecs.append(node) 588 # "stubs" list is an attribute of the tgroup element: 589 node.parent.stubs.append(node.attributes.get('stub')) 590 591 def depart_colspec(self, node): 592 # write out <colgroup> when all colspecs are processed 593 if isinstance(node.next_node(descend=False, siblings=True), 594 nodes.colspec): 595 return 596 if 'colwidths-auto' in node.parent.parent['classes'] or ( 597 'colwidths-auto' in self.settings.table_style and 598 ('colwidths-given' not in node.parent.parent['classes'])): 599 return 600 total_width = sum(node['colwidth'] for node in self.colspecs) 601 self.body.append(self.starttag(node, 'colgroup')) 602 for node in self.colspecs: 603 colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5) 604 self.body.append(self.emptytag(node, 'col', 605 style='width: %i%%' % colwidth)) 606 self.body.append('</colgroup>\n') 607 608 def visit_comment(self, node, 609 sub=re.compile('-(?=-)').sub): 610 """Escape double-dashes in comment text.""" 611 self.body.append('<!-- %s -->\n' % sub('- ', node.astext())) 612 # Content already processed: 613 raise nodes.SkipNode 614 615 def visit_compound(self, node): 616 self.body.append(self.starttag(node, 'div', CLASS='compound')) 617 if len(node) > 1: 618 node[0]['classes'].append('compound-first') 619 node[-1]['classes'].append('compound-last') 620 for child in node[1:-1]: 621 child['classes'].append('compound-middle') 622 623 def depart_compound(self, node): 624 self.body.append('</div>\n') 625 626 def visit_container(self, node): 627 self.body.append(self.starttag(node, 'div', CLASS='docutils container')) 628 629 def depart_container(self, node): 630 self.body.append('</div>\n') 631 632 def visit_contact(self, node): 633 self.visit_docinfo_item(node, 'contact', meta=False) 634 635 def depart_contact(self, node): 636 self.depart_docinfo_item() 637 638 def visit_copyright(self, node): 639 self.visit_docinfo_item(node, 'copyright') 640 641 def depart_copyright(self, node): 642 self.depart_docinfo_item() 643 644 def visit_date(self, node): 645 self.visit_docinfo_item(node, 'date') 646 647 def depart_date(self, node): 648 self.depart_docinfo_item() 649 650 def visit_decoration(self, node): 651 pass 652 653 def depart_decoration(self, node): 654 pass 655 656 def visit_definition(self, node): 657 self.body.append('</dt>\n') 658 self.body.append(self.starttag(node, 'dd', '')) 659 660 def depart_definition(self, node): 661 self.body.append('</dd>\n') 662 663 def visit_definition_list(self, node): 664 classes = node.setdefault('classes', []) 665 if self.is_compactable(node): 666 classes.append('simple') 667 self.body.append(self.starttag(node, 'dl')) 668 669 def depart_definition_list(self, node): 670 self.body.append('</dl>\n') 671 672 def visit_definition_list_item(self, node): 673 # pass class arguments, ids and names to definition term: 674 node.children[0]['classes'] = ( 675 node.get('classes', []) + node.children[0].get('classes', [])) 676 node.children[0]['ids'] = ( 677 node.get('ids', []) + node.children[0].get('ids', [])) 678 node.children[0]['names'] = ( 679 node.get('names', []) + node.children[0].get('names', [])) 680 681 def depart_definition_list_item(self, node): 682 pass 683 684 def visit_description(self, node): 685 self.body.append(self.starttag(node, 'dd', '')) 686 687 def depart_description(self, node): 688 self.body.append('</dd>\n') 689 690 def visit_docinfo(self, node): 691 self.context.append(len(self.body)) 692 classes = 'docinfo' 693 if (self.is_compactable(node)): 694 classes += ' simple' 695 self.body.append(self.starttag(node, 'dl', CLASS=classes)) 696 697 def depart_docinfo(self, node): 698 self.body.append('</dl>\n') 699 start = self.context.pop() 700 self.docinfo = self.body[start:] 701 self.body = [] 702 703 def visit_docinfo_item(self, node, name, meta=True): 704 if meta: 705 meta_tag = '<meta name="%s" content="%s" />\n' \ 706 % (name, self.attval(node.astext())) 707 self.add_meta(meta_tag) 708 self.body.append('<dt class="%s">%s</dt>\n' 709 % (name, self.language.labels[name])) 710 self.body.append(self.starttag(node, 'dd', '', CLASS=name)) 711 712 def depart_docinfo_item(self): 713 self.body.append('</dd>\n') 714 715 def visit_doctest_block(self, node): 716 self.body.append(self.starttag(node, 'pre', suffix='', 717 CLASS='code python doctest')) 718 719 def depart_doctest_block(self, node): 720 self.body.append('\n</pre>\n') 721 722 def visit_document(self, node): 723 title = (node.get('title', '') or os.path.basename(node['source']) 724 or 'docutils document without title') 725 self.head.append('<title>%s</title>\n' % self.encode(title)) 726 727 def depart_document(self, node): 728 self.head_prefix.extend([self.doctype, 729 self.head_prefix_template % 730 {'lang': self.settings.language_code}]) 731 self.html_prolog.append(self.doctype) 732 self.meta.insert(0, self.content_type % self.settings.output_encoding) 733 self.head.insert(0, self.content_type % self.settings.output_encoding) 734 if 'name="dcterms.' in ''.join(self.meta): 735 self.head.append( 736 '<link rel="schema.dcterms" href="http://purl.org/dc/terms/"/>') 737 if self.math_header: 738 if self.math_output == 'mathjax': 739 self.head.extend(self.math_header) 740 else: 741 self.stylesheet.extend(self.math_header) 742 # skip content-type meta tag with interpolated charset value: 743 self.html_head.extend(self.head[1:]) 744 self.body_prefix.append(self.starttag(node, 'div', CLASS='document')) 745 self.body_suffix.insert(0, '</div>\n') 746 self.fragment.extend(self.body) # self.fragment is the "naked" body 747 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo 748 + self.docinfo + self.body 749 + self.body_suffix[:-1]) 750 assert not self.context, 'len(context) = %s' % len(self.context) 751 752 def visit_emphasis(self, node): 753 self.body.append(self.starttag(node, 'em', '')) 754 755 def depart_emphasis(self, node): 756 self.body.append('</em>') 757 758 def visit_entry(self, node): 759 atts = {'class': []} 760 if isinstance(node.parent.parent, nodes.thead): 761 atts['class'].append('head') 762 if node.parent.parent.parent.stubs[node.parent.column]: 763 # "stubs" list is an attribute of the tgroup element 764 atts['class'].append('stub') 765 if atts['class']: 766 tagname = 'th' 767 atts['class'] = ' '.join(atts['class']) 768 else: 769 tagname = 'td' 770 del atts['class'] 771 node.parent.column += 1 772 if 'morerows' in node: 773 atts['rowspan'] = node['morerows'] + 1 774 if 'morecols' in node: 775 atts['colspan'] = node['morecols'] + 1 776 node.parent.column += node['morecols'] 777 self.body.append(self.starttag(node, tagname, '', **atts)) 778 self.context.append('</%s>\n' % tagname.lower()) 779 # TODO: why does the html4css1 writer insert an NBSP into empty cells? 780 # if len(node) == 0: # empty cell 781 # self.body.append(' ') # no-break space 782 783 def depart_entry(self, node): 784 self.body.append(self.context.pop()) 785 786 def visit_enumerated_list(self, node): 787 atts = {} 788 if 'start' in node: 789 atts['start'] = node['start'] 790 if 'enumtype' in node: 791 atts['class'] = node['enumtype'] 792 if self.is_compactable(node): 793 atts['class'] = (atts.get('class', '') + ' simple').strip() 794 self.body.append(self.starttag(node, 'ol', **atts)) 795 796 def depart_enumerated_list(self, node): 797 self.body.append('</ol>\n') 798 799 def visit_field_list(self, node): 800 # Keep simple paragraphs in the field_body to enable CSS 801 # rule to start body on new line if the label is too long 802 classes = 'field-list' 803 if (self.is_compactable(node)): 804 classes += ' simple' 805 self.body.append(self.starttag(node, 'dl', CLASS=classes)) 806 807 def depart_field_list(self, node): 808 self.body.append('</dl>\n') 809 810 def visit_field(self, node): 811 pass 812 813 def depart_field(self, node): 814 pass 815 816 # as field is ignored, pass class arguments to field-name and field-body: 817 818 def visit_field_name(self, node): 819 self.body.append(self.starttag(node, 'dt', '', 820 CLASS=''.join(node.parent['classes']))) 821 822 def depart_field_name(self, node): 823 self.body.append('</dt>\n') 824 825 def visit_field_body(self, node): 826 self.body.append(self.starttag(node, 'dd', '', 827 CLASS=''.join(node.parent['classes']))) 828 # prevent misalignment of following content if the field is empty: 829 if not node.children: 830 self.body.append('<p></p>') 831 832 def depart_field_body(self, node): 833 self.body.append('</dd>\n') 834 835 def visit_figure(self, node): 836 atts = {'class': 'figure'} 837 if node.get('width'): 838 atts['style'] = 'width: %s' % node['width'] 839 if node.get('align'): 840 atts['class'] += " align-" + node['align'] 841 self.body.append(self.starttag(node, 'div', **atts)) 842 843 def depart_figure(self, node): 844 self.body.append('</div>\n') 845 846 # use HTML 5 <footer> element? 847 def visit_footer(self, node): 848 self.context.append(len(self.body)) 849 850 def depart_footer(self, node): 851 start = self.context.pop() 852 footer = [self.starttag(node, 'div', CLASS='footer'), 853 '<hr class="footer" />\n'] 854 footer.extend(self.body[start:]) 855 footer.append('\n</div>\n') 856 self.footer.extend(footer) 857 self.body_suffix[:0] = footer 858 del self.body[start:] 859 860 # TODO: use the new HTML5 element <aside> for footnote text 861 # (allows better styling with CSS, the current <dl> list styling 862 # with "float" interferes with sidebars). 863 def visit_footnote(self, node): 864 if not self.in_footnote_list: 865 listnode = node.copy() 866 listnode['ids'] = [] 867 classes = 'footnote ' + self.settings.footnote_references 868 self.body.append(self.starttag(listnode, 'dl', CLASS=classes)) 869 # self.body.append('<dl class="%s">\n'%classes) 870 self.in_footnote_list = True 871 872 def depart_footnote(self, node): 873 self.body.append('</dd>\n') 874 if not isinstance(node.next_node(descend=False, siblings=True), 875 nodes.footnote): 876 self.body.append('</dl>\n') 877 self.in_footnote_list = False 878 879 def visit_footnote_reference(self, node): 880 href = '#' + node['refid'] 881 classes = 'footnote-reference ' + self.settings.footnote_references 882 self.body.append(self.starttag(node, 'a', '', #suffix, 883 CLASS=classes, href=href)) 884 885 def depart_footnote_reference(self, node): 886 self.body.append('</a>') 887 888 # Docutils-generated text: put section numbers in a span for CSS styling: 889 def visit_generated(self, node): 890 if 'sectnum' in node['classes']: 891 # get section number (strip trailing no-break-spaces) 892 sectnum = node.astext().rstrip(u' ') 893 self.body.append('<span class="sectnum">%s</span> ' 894 % self.encode(sectnum)) 895 # Content already processed: 896 raise nodes.SkipNode 897 898 def depart_generated(self, node): 899 pass 900 901 def visit_header(self, node): 902 self.context.append(len(self.body)) 903 904 def depart_header(self, node): 905 start = self.context.pop() 906 header = [self.starttag(node, 'div', CLASS='header')] 907 header.extend(self.body[start:]) 908 header.append('\n<hr class="header"/>\n</div>\n') 909 self.body_prefix.extend(header) 910 self.header.extend(header) 911 del self.body[start:] 912 913 def visit_image(self, node): 914 atts = {} 915 uri = node['uri'] 916 mimetype = mimetypes.guess_type(uri)[0] 917 # image size 918 if 'width' in node: 919 atts['width'] = node['width'] 920 if 'height' in node: 921 atts['height'] = node['height'] 922 if 'scale' in node: 923 if (PIL and not ('width' in node and 'height' in node) 924 and self.settings.file_insertion_enabled): 925 imagepath = url2pathname(uri) 926 try: 927 img = PIL.Image.open( 928 imagepath.encode(sys.getfilesystemencoding())) 929 except (IOError, UnicodeEncodeError): 930 pass # TODO: warn? 931 else: 932 self.settings.record_dependencies.add( 933 imagepath.replace('\\', '/')) 934 if 'width' not in atts: 935 atts['width'] = '%dpx' % img.size[0] 936 if 'height' not in atts: 937 atts['height'] = '%dpx' % img.size[1] 938 del img 939 for att_name in 'width', 'height': 940 if att_name in atts: 941 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name]) 942 assert match 943 atts[att_name] = '%s%s' % ( 944 float(match.group(1)) * (float(node['scale']) / 100), 945 match.group(2)) 946 style = [] 947 for att_name in 'width', 'height': 948 if att_name in atts: 949 if re.match(r'^[0-9.]+$', atts[att_name]): 950 # Interpret unitless values as pixels. 951 atts[att_name] += 'px' 952 style.append('%s: %s;' % (att_name, atts[att_name])) 953 del atts[att_name] 954 if style: 955 atts['style'] = ' '.join(style) 956 if (isinstance(node.parent, nodes.TextElement) or 957 (isinstance(node.parent, nodes.reference) and 958 not isinstance(node.parent.parent, nodes.TextElement))): 959 # Inline context or surrounded by <a>...</a>. 960 suffix = '' 961 else: 962 suffix = '\n' 963 if 'align' in node: 964 atts['class'] = 'align-%s' % node['align'] 965 # Embed image file (embedded SVG or data URI): 966 if self.settings.embed_images or ('embed' in node): 967 err_msg = '' 968 if not mimetype: 969 err_msg = 'unknown MIME type' 970 if not self.settings.file_insertion_enabled: 971 err_msg = 'file insertion disabled.' 972 try: 973 with open(url2pathname(uri), 'rb') as imagefile: 974 imagedata = imagefile.read() 975 except IOError as err: 976 err_msg = err.strerror 977 if err_msg: 978 self.document.reporter.error('Cannot embed image %r: %s' 979 %(uri, err_msg)) 980 else: 981 self.settings.record_dependencies.add( 982 uri.replace('\\', '/')) 983 # TODO: insert SVG as-is? 984 # if mimetype == 'image/svg+xml': 985 # read/parse, apply arguments, 986 # insert as <svg ....> ... </svg> # (about 1/3 less data) 987 data64 = base64.b64encode(imagedata).decode() 988 uri = u'data:%s;base64,%s' % (mimetype, data64) 989 if mimetype == 'application/x-shockwave-flash': 990 atts['type'] = mimetype 991 # do NOT use an empty tag: incorrect rendering in browsers 992 tag = (self.starttag(node, 'object', '', data=uri, **atts) 993 + node.get('alt', uri) + '</object>' + suffix) 994 else: 995 atts['alt'] = node.get('alt', node['uri']) 996 tag = self.emptytag(node, 'img', suffix, src=uri, **atts) 997 self.body.append(tag) 998 999 def depart_image(self, node): 1000 pass 1001 1002 def visit_inline(self, node): 1003 self.body.append(self.starttag(node, 'span', '')) 1004 1005 def depart_inline(self, node): 1006 self.body.append('</span>') 1007 1008 # footnote and citation labels: 1009 def visit_label(self, node): 1010 if (isinstance(node.parent, nodes.footnote)): 1011 classes = self.settings.footnote_references 1012 else: 1013 classes = 'brackets' 1014 # pass parent node to get id into starttag: 1015 self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label')) 1016 self.body.append(self.starttag(node, 'span', '', CLASS=classes)) 1017 # footnote/citation backrefs: 1018 if self.settings.footnote_backlinks: 1019 backrefs = node.parent['backrefs'] 1020 if len(backrefs) == 1: 1021 self.body.append('<a class="fn-backref" href="#%s">' 1022 % backrefs[0]) 1023 1024 def depart_label(self, node): 1025 if self.settings.footnote_backlinks: 1026 backrefs = node.parent['backrefs'] 1027 if len(backrefs) == 1: 1028 self.body.append('</a>') 1029 self.body.append('</span>') 1030 if self.settings.footnote_backlinks and len(backrefs) > 1: 1031 backlinks = ['<a href="#%s">%s</a>' % (ref, i) 1032 for (i, ref) in enumerate(backrefs, 1)] 1033 self.body.append('<span class="fn-backref">(%s)</span>' 1034 % ','.join(backlinks)) 1035 self.body.append('</dt>\n<dd>') 1036 1037 def visit_legend(self, node): 1038 self.body.append(self.starttag(node, 'div', CLASS='legend')) 1039 1040 def depart_legend(self, node): 1041 self.body.append('</div>\n') 1042 1043 def visit_line(self, node): 1044 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line')) 1045 if not len(node): 1046 self.body.append('<br />') 1047 1048 def depart_line(self, node): 1049 self.body.append('</div>\n') 1050 1051 def visit_line_block(self, node): 1052 self.body.append(self.starttag(node, 'div', CLASS='line-block')) 1053 1054 def depart_line_block(self, node): 1055 self.body.append('</div>\n') 1056 1057 def visit_list_item(self, node): 1058 self.body.append(self.starttag(node, 'li', '')) 1059 1060 def depart_list_item(self, node): 1061 self.body.append('</li>\n') 1062 1063 # inline literal 1064 def visit_literal(self, node): 1065 # special case: "code" role 1066 classes = node.get('classes', []) 1067 if 'code' in classes: 1068 # filter 'code' from class arguments 1069 node['classes'] = [cls for cls in classes if cls != 'code'] 1070 self.body.append(self.starttag(node, 'code', '')) 1071 return 1072 self.body.append( 1073 self.starttag(node, 'span', '', CLASS='docutils literal')) 1074 text = node.astext() 1075 # remove hard line breaks (except if in a parsed-literal block) 1076 if not isinstance(node.parent, nodes.literal_block): 1077 text = text.replace('\n', ' ') 1078 # Protect text like ``--an-option`` and the regular expression 1079 # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping 1080 for token in self.words_and_spaces.findall(text): 1081 if token.strip() and self.in_word_wrap_point.search(token): 1082 self.body.append('<span class="pre">%s</span>' 1083 % self.encode(token)) 1084 else: 1085 self.body.append(self.encode(token)) 1086 self.body.append('</span>') 1087 # Content already processed: 1088 raise nodes.SkipNode 1089 1090 def depart_literal(self, node): 1091 # skipped unless literal element is from "code" role: 1092 self.body.append('</code>') 1093 1094 def visit_literal_block(self, node): 1095 self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block')) 1096 if 'code' in node.get('classes', []): 1097 self.body.append('<code>') 1098 1099 def depart_literal_block(self, node): 1100 if 'code' in node.get('classes', []): 1101 self.body.append('</code>') 1102 self.body.append('</pre>\n') 1103 1104 # Mathematics: 1105 # As there is no native HTML math support, we provide alternatives 1106 # for the math-output: LaTeX and MathJax simply wrap the content, 1107 # HTML and MathML also convert the math_code. 1108 # HTML container 1109 math_tags = {# math_output: (block, inline, class-arguments) 1110 'mathml': ('div', '', ''), 1111 'html': ('div', 'span', 'formula'), 1112 'mathjax': ('div', 'span', 'math'), 1113 'latex': ('pre', 'tt', 'math'), 1114 } 1115 1116 def visit_math(self, node, math_env=''): 1117 # If the method is called from visit_math_block(), math_env != ''. 1118 1119 if self.math_output not in self.math_tags: 1120 self.document.reporter.error( 1121 'math-output format "%s" not supported ' 1122 'falling back to "latex"'% self.math_output) 1123 self.math_output = 'latex' 1124 tag = self.math_tags[self.math_output][math_env == ''] 1125 clsarg = self.math_tags[self.math_output][2] 1126 # LaTeX container 1127 wrappers = {# math_mode: (inline, block) 1128 'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'), 1129 'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'), 1130 'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'), 1131 'latex': (None, None), 1132 } 1133 wrapper = wrappers[self.math_output][math_env != ''] 1134 if self.math_output == 'mathml' and (not self.math_output_options or 1135 self.math_output_options[0] == 'blahtexml'): 1136 wrapper = None 1137 # get and wrap content 1138 math_code = node.astext().translate(unichar2tex.uni2tex_table) 1139 if wrapper: 1140 try: # wrapper with three "%s" 1141 math_code = wrapper % (math_env, math_code, math_env) 1142 except TypeError: # wrapper with one "%s" 1143 math_code = wrapper % math_code 1144 # settings and conversion 1145 if self.math_output in ('latex', 'mathjax'): 1146 math_code = self.encode(math_code) 1147 if self.math_output == 'mathjax' and not self.math_header: 1148 try: 1149 self.mathjax_url = self.math_output_options[0] 1150 except IndexError: 1151 self.document.reporter.warning('No MathJax URL specified, ' 1152 'using local fallback (see config.html)') 1153 # append configuration, if not already present in the URL: 1154 # input LaTeX with AMS, output common HTML 1155 if '?' not in self.mathjax_url: 1156 self.mathjax_url += '?config=TeX-AMS_CHTML' 1157 self.math_header = [self.mathjax_script % self.mathjax_url] 1158 elif self.math_output == 'html': 1159 if self.math_output_options and not self.math_header: 1160 self.math_header = [self.stylesheet_call( 1161 utils.find_file_in_dirs(s, self.settings.stylesheet_dirs)) 1162 for s in self.math_output_options[0].split(',')] 1163 # TODO: fix display mode in matrices and fractions 1164 math2html.DocumentParameters.displaymode = (math_env != '') 1165 math_code = math2html.math2html(math_code) 1166 elif self.math_output == 'mathml': 1167 if 'XHTML 1' in self.doctype: 1168 self.doctype = self.doctype_mathml 1169 self.content_type = self.content_type_mathml 1170 converter = ' '.join(self.math_output_options).lower() 1171 try: 1172 if converter == 'latexml': 1173 math_code = tex2mathml_extern.latexml(math_code, 1174 self.document.reporter) 1175 elif converter == 'ttm': 1176 math_code = tex2mathml_extern.ttm(math_code, 1177 self.document.reporter) 1178 elif converter == 'blahtexml': 1179 math_code = tex2mathml_extern.blahtexml(math_code, 1180 inline=not(math_env), 1181 reporter=self.document.reporter) 1182 elif not converter: 1183 math_code = latex2mathml.tex2mathml(math_code, 1184 inline=not(math_env)) 1185 else: 1186 self.document.reporter.error('option "%s" not supported ' 1187 'with math-output "MathML"') 1188 except OSError: 1189 raise OSError('is "latexmlmath" in your PATH?') 1190 except SyntaxError as err: 1191 err_node = self.document.reporter.error(err, base_node=node) 1192 self.visit_system_message(err_node) 1193 self.body.append(self.starttag(node, 'p')) 1194 self.body.append(u','.join(err.args)) 1195 self.body.append('</p>\n') 1196 self.body.append(self.starttag(node, 'pre', 1197 CLASS='literal-block')) 1198 self.body.append(self.encode(math_code)) 1199 self.body.append('\n</pre>\n') 1200 self.depart_system_message(err_node) 1201 raise nodes.SkipNode 1202 # append to document body 1203 if tag: 1204 self.body.append(self.starttag(node, tag, 1205 suffix='\n'*bool(math_env), 1206 CLASS=clsarg)) 1207 self.body.append(math_code) 1208 if math_env: # block mode (equation, display) 1209 self.body.append('\n') 1210 if tag: 1211 self.body.append('</%s>' % tag) 1212 if math_env: 1213 self.body.append('\n') 1214 # Content already processed: 1215 raise nodes.SkipNode 1216 1217 def depart_math(self, node): 1218 pass # never reached 1219 1220 def visit_math_block(self, node): 1221 math_env = pick_math_environment(node.astext()) 1222 self.visit_math(node, math_env=math_env) 1223 1224 def depart_math_block(self, node): 1225 pass # never reached 1226 1227 # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1 1228 # HTML5/polyglot recommends using both 1229 def visit_meta(self, node): 1230 meta = self.emptytag(node, 'meta', **node.non_default_attributes()) 1231 self.add_meta(meta) 1232 1233 def depart_meta(self, node): 1234 pass 1235 1236 def add_meta(self, tag): 1237 self.meta.append(tag) 1238 self.head.append(tag) 1239 1240 def visit_option(self, node): 1241 self.body.append(self.starttag(node, 'span', '', CLASS='option')) 1242 1243 def depart_option(self, node): 1244 self.body.append('</span>') 1245 if isinstance(node.next_node(descend=False, siblings=True), 1246 nodes.option): 1247 self.body.append(', ') 1248 1249 def visit_option_argument(self, node): 1250 self.body.append(node.get('delimiter', ' ')) 1251 self.body.append(self.starttag(node, 'var', '')) 1252 1253 def depart_option_argument(self, node): 1254 self.body.append('</var>') 1255 1256 def visit_option_group(self, node): 1257 self.body.append(self.starttag(node, 'dt', '')) 1258 self.body.append('<kbd>') 1259 1260 def depart_option_group(self, node): 1261 self.body.append('</kbd></dt>\n') 1262 1263 def visit_option_list(self, node): 1264 self.body.append( 1265 self.starttag(node, 'dl', CLASS='option-list')) 1266 1267 def depart_option_list(self, node): 1268 self.body.append('</dl>\n') 1269 1270 def visit_option_list_item(self, node): 1271 pass 1272 1273 def depart_option_list_item(self, node): 1274 pass 1275 1276 def visit_option_string(self, node): 1277 pass 1278 1279 def depart_option_string(self, node): 1280 pass 1281 1282 def visit_organization(self, node): 1283 self.visit_docinfo_item(node, 'organization') 1284 1285 def depart_organization(self, node): 1286 self.depart_docinfo_item() 1287 1288 # Do not omit <p> tags 1289 # -------------------- 1290 # 1291 # The HTML4CSS1 writer does this to "produce 1292 # visually compact lists (less vertical whitespace)". This writer 1293 # relies on CSS rules for"visual compactness". 1294 # 1295 # * In XHTML 1.1, e.g. a <blockquote> element may not contain 1296 # character data, so you cannot drop the <p> tags. 1297 # * Keeping simple paragraphs in the field_body enables a CSS 1298 # rule to start the field-body on a new line if the label is too long 1299 # * it makes the code simpler. 1300 # 1301 # TODO: omit paragraph tags in simple table cells? 1302 1303 def visit_paragraph(self, node): 1304 self.body.append(self.starttag(node, 'p', '')) 1305 1306 def depart_paragraph(self, node): 1307 self.body.append('</p>') 1308 if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and 1309 (len(node.parent) == 1)): 1310 self.body.append('\n') 1311 1312 def visit_problematic(self, node): 1313 if node.hasattr('refid'): 1314 self.body.append('<a href="#%s">' % node['refid']) 1315 self.context.append('</a>') 1316 else: 1317 self.context.append('') 1318 self.body.append(self.starttag(node, 'span', '', CLASS='problematic')) 1319 1320 def depart_problematic(self, node): 1321 self.body.append('</span>') 1322 self.body.append(self.context.pop()) 1323 1324 def visit_raw(self, node): 1325 if 'html' in node.get('format', '').split(): 1326 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div' 1327 if node['classes']: 1328 self.body.append(self.starttag(node, t, suffix='')) 1329 self.body.append(node.astext()) 1330 if node['classes']: 1331 self.body.append('</%s>' % t) 1332 # Keep non-HTML raw text out of output: 1333 raise nodes.SkipNode 1334 1335 def visit_reference(self, node): 1336 atts = {'class': 'reference'} 1337 if 'refuri' in node: 1338 atts['href'] = node['refuri'] 1339 if ( self.settings.cloak_email_addresses 1340 and atts['href'].startswith('mailto:')): 1341 atts['href'] = self.cloak_mailto(atts['href']) 1342 self.in_mailto = True 1343 atts['class'] += ' external' 1344 else: 1345 assert 'refid' in node, \ 1346 'References must have "refuri" or "refid" attribute.' 1347 atts['href'] = '#' + node['refid'] 1348 atts['class'] += ' internal' 1349 if len(node) == 1 and isinstance(node[0], nodes.image): 1350 atts['class'] += ' image-reference' 1351 if not isinstance(node.parent, nodes.TextElement): 1352 assert len(node) == 1 and isinstance(node[0], nodes.image) 1353 atts['class'] += ' image-reference' 1354 self.body.append(self.starttag(node, 'a', '', **atts)) 1355 1356 def depart_reference(self, node): 1357 self.body.append('</a>') 1358 if not isinstance(node.parent, nodes.TextElement): 1359 self.body.append('\n') 1360 self.in_mailto = False 1361 1362 def visit_revision(self, node): 1363 self.visit_docinfo_item(node, 'revision', meta=False) 1364 1365 def depart_revision(self, node): 1366 self.depart_docinfo_item() 1367 1368 def visit_row(self, node): 1369 self.body.append(self.starttag(node, 'tr', '')) 1370 node.column = 0 1371 1372 def depart_row(self, node): 1373 self.body.append('</tr>\n') 1374 1375 def visit_rubric(self, node): 1376 self.body.append(self.starttag(node, 'p', '', CLASS='rubric')) 1377 1378 def depart_rubric(self, node): 1379 self.body.append('</p>\n') 1380 1381 def visit_section(self, node): 1382 self.section_level += 1 1383 self.body.append( 1384 self.starttag(node, 'div', CLASS='section')) 1385 1386 def depart_section(self, node): 1387 self.section_level -= 1 1388 self.body.append('</div>\n') 1389 1390 # TODO: use the new HTML5 element <aside> 1391 def visit_sidebar(self, node): 1392 self.body.append( 1393 self.starttag(node, 'div', CLASS='sidebar')) 1394 self.in_sidebar = True 1395 1396 def depart_sidebar(self, node): 1397 self.body.append('</div>\n') 1398 self.in_sidebar = False 1399 1400 def visit_status(self, node): 1401 self.visit_docinfo_item(node, 'status', meta=False) 1402 1403 def depart_status(self, node): 1404 self.depart_docinfo_item() 1405 1406 def visit_strong(self, node): 1407 self.body.append(self.starttag(node, 'strong', '')) 1408 1409 def depart_strong(self, node): 1410 self.body.append('</strong>') 1411 1412 def visit_subscript(self, node): 1413 self.body.append(self.starttag(node, 'sub', '')) 1414 1415 def depart_subscript(self, node): 1416 self.body.append('</sub>') 1417 1418 def visit_substitution_definition(self, node): 1419 """Internal only.""" 1420 raise nodes.SkipNode 1421 1422 def visit_substitution_reference(self, node): 1423 self.unimplemented_visit(node) 1424 1425 # h1–h6 elements must not be used to markup subheadings, subtitles, 1426 # alternative titles and taglines unless intended to be the heading for a 1427 # new section or subsection. 1428 # -- http://www.w3.org/TR/html/sections.html#headings-and-sections 1429 def visit_subtitle(self, node): 1430 if isinstance(node.parent, nodes.sidebar): 1431 classes = 'sidebar-subtitle' 1432 elif isinstance(node.parent, nodes.document): 1433 classes = 'subtitle' 1434 self.in_document_title = len(self.body)+1 1435 elif isinstance(node.parent, nodes.section): 1436 classes = 'section-subtitle' 1437 self.body.append(self.starttag(node, 'p', '', CLASS=classes)) 1438 1439 def depart_subtitle(self, node): 1440 self.body.append('</p>\n') 1441 if isinstance(node.parent, nodes.document): 1442 self.subtitle = self.body[self.in_document_title:-1] 1443 self.in_document_title = 0 1444 self.body_pre_docinfo.extend(self.body) 1445 self.html_subtitle.extend(self.body) 1446 del self.body[:] 1447 1448 def visit_superscript(self, node): 1449 self.body.append(self.starttag(node, 'sup', '')) 1450 1451 def depart_superscript(self, node): 1452 self.body.append('</sup>') 1453 1454 def visit_system_message(self, node): 1455 self.body.append(self.starttag(node, 'div', CLASS='system-message')) 1456 self.body.append('<p class="system-message-title">') 1457 backref_text = '' 1458 if len(node['backrefs']): 1459 backrefs = node['backrefs'] 1460 if len(backrefs) == 1: 1461 backref_text = ('; <em><a href="#%s">backlink</a></em>' 1462 % backrefs[0]) 1463 else: 1464 i = 1 1465 backlinks = [] 1466 for backref in backrefs: 1467 backlinks.append('<a href="#%s">%s</a>' % (backref, i)) 1468 i += 1 1469 backref_text = ('; <em>backlinks: %s</em>' 1470 % ', '.join(backlinks)) 1471 if node.hasattr('line'): 1472 line = ', line %s' % node['line'] 1473 else: 1474 line = '' 1475 self.body.append('System Message: %s/%s ' 1476 '(<span class="docutils literal">%s</span>%s)%s</p>\n' 1477 % (node['type'], node['level'], 1478 self.encode(node['source']), line, backref_text)) 1479 1480 def depart_system_message(self, node): 1481 self.body.append('</div>\n') 1482 1483 def visit_table(self, node): 1484 atts = {} 1485 classes = [cls.strip(u' \t\n') 1486 for cls in self.settings.table_style.split(',')] 1487 if 'align' in node: 1488 classes.append('align-%s' % node['align']) 1489 if 'width' in node: 1490 atts['style'] = 'width: %s' % node['width'] 1491 tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts) 1492 self.body.append(tag) 1493 1494 def depart_table(self, node): 1495 self.body.append('</table>\n') 1496 1497 def visit_target(self, node): 1498 if not ('refuri' in node or 'refid' in node 1499 or 'refname' in node): 1500 self.body.append(self.starttag(node, 'span', '', CLASS='target')) 1501 self.context.append('</span>') 1502 else: 1503 self.context.append('') 1504 1505 def depart_target(self, node): 1506 self.body.append(self.context.pop()) 1507 1508 # no hard-coded vertical alignment in table body 1509 def visit_tbody(self, node): 1510 self.body.append(self.starttag(node, 'tbody')) 1511 1512 def depart_tbody(self, node): 1513 self.body.append('</tbody>\n') 1514 1515 def visit_term(self, node): 1516 self.body.append(self.starttag(node, 'dt', '')) 1517 1518 def depart_term(self, node): 1519 """ 1520 Leave the end tag to `self.visit_definition()`, in case there's a 1521 classifier. 1522 """ 1523 pass 1524 1525 def visit_tgroup(self, node): 1526 self.colspecs = [] 1527 node.stubs = [] 1528 1529 def depart_tgroup(self, node): 1530 pass 1531 1532 def visit_thead(self, node): 1533 self.body.append(self.starttag(node, 'thead')) 1534 1535 def depart_thead(self, node): 1536 self.body.append('</thead>\n') 1537 1538 def visit_title(self, node): 1539 """Only 6 section levels are supported by HTML.""" 1540 close_tag = '</p>\n' 1541 if isinstance(node.parent, nodes.topic): 1542 self.body.append( 1543 self.starttag(node, 'p', '', CLASS='topic-title')) 1544 elif isinstance(node.parent, nodes.sidebar): 1545 self.body.append( 1546 self.starttag(node, 'p', '', CLASS='sidebar-title')) 1547 elif isinstance(node.parent, nodes.Admonition): 1548 self.body.append( 1549 self.starttag(node, 'p', '', CLASS='admonition-title')) 1550 elif isinstance(node.parent, nodes.table): 1551 self.body.append( 1552 self.starttag(node, 'caption', '')) 1553 close_tag = '</caption>\n' 1554 elif isinstance(node.parent, nodes.document): 1555 self.body.append(self.starttag(node, 'h1', '', CLASS='title')) 1556 close_tag = '</h1>\n' 1557 self.in_document_title = len(self.body) 1558 else: 1559 assert isinstance(node.parent, nodes.section) 1560 h_level = self.section_level + self.initial_header_level - 1 1561 atts = {} 1562 if (len(node.parent) >= 2 and 1563 isinstance(node.parent[1], nodes.subtitle)): 1564 atts['CLASS'] = 'with-subtitle' 1565 self.body.append( 1566 self.starttag(node, 'h%s' % h_level, '', **atts)) 1567 atts = {} 1568 if node.hasattr('refid'): 1569 atts['class'] = 'toc-backref' 1570 atts['href'] = '#' + node['refid'] 1571 if atts: 1572 self.body.append(self.starttag({}, 'a', '', **atts)) 1573 close_tag = '</a></h%s>\n' % (h_level) 1574 else: 1575 close_tag = '</h%s>\n' % (h_level) 1576 self.context.append(close_tag) 1577 1578 def depart_title(self, node): 1579 self.body.append(self.context.pop()) 1580 if self.in_document_title: 1581 self.title = self.body[self.in_document_title:-1] 1582 self.in_document_title = 0 1583 self.body_pre_docinfo.extend(self.body) 1584 self.html_title.extend(self.body) 1585 del self.body[:] 1586 1587 def visit_title_reference(self, node): 1588 self.body.append(self.starttag(node, 'cite', '')) 1589 1590 def depart_title_reference(self, node): 1591 self.body.append('</cite>') 1592 1593 def visit_topic(self, node): 1594 self.body.append(self.starttag(node, 'div', CLASS='topic')) 1595 self.topic_classes = node['classes'] 1596 1597 def depart_topic(self, node): 1598 self.body.append('</div>\n') 1599 self.topic_classes = [] 1600 1601 def visit_transition(self, node): 1602 self.body.append(self.emptytag(node, 'hr', CLASS='docutils')) 1603 1604 def depart_transition(self, node): 1605 pass 1606 1607 def visit_version(self, node): 1608 self.visit_docinfo_item(node, 'version', meta=False) 1609 1610 def depart_version(self, node): 1611 self.depart_docinfo_item() 1612 1613 def unimplemented_visit(self, node): 1614 raise NotImplementedError('visiting unimplemented node type: %s' 1615 % node.__class__.__name__) 1616 1617 1618class SimpleListChecker(nodes.GenericNodeVisitor): 1619 1620 """ 1621 Raise `nodes.NodeFound` if non-simple list item is encountered. 1622 1623 Here "simple" means a list item containing nothing other than a single 1624 paragraph, a simple list, or a paragraph followed by a simple list. 1625 1626 This version also checks for simple field lists and docinfo. 1627 """ 1628 1629 def default_visit(self, node): 1630 raise nodes.NodeFound 1631 1632 def visit_list_item(self, node): 1633 children = [child for child in node.children 1634 if not isinstance(child, nodes.Invisible)] 1635 if (children and isinstance(children[0], nodes.paragraph) 1636 and (isinstance(children[-1], nodes.bullet_list) or 1637 isinstance(children[-1], nodes.enumerated_list) or 1638 isinstance(children[-1], nodes.field_list))): 1639 children.pop() 1640 if len(children) <= 1: 1641 return 1642 else: 1643 raise nodes.NodeFound 1644 1645 def pass_node(self, node): 1646 pass 1647 1648 def ignore_node(self, node): 1649 # ignore nodes that are never complex (can contain only inline nodes) 1650 raise nodes.SkipNode 1651 1652 # Paragraphs and text 1653 visit_Text = ignore_node 1654 visit_paragraph = ignore_node 1655 1656 # Lists 1657 visit_bullet_list = pass_node 1658 visit_enumerated_list = pass_node 1659 visit_docinfo = pass_node 1660 1661 # Docinfo nodes: 1662 visit_author = ignore_node 1663 visit_authors = visit_list_item 1664 visit_address = visit_list_item 1665 visit_contact = pass_node 1666 visit_copyright = ignore_node 1667 visit_date = ignore_node 1668 visit_organization = ignore_node 1669 visit_status = ignore_node 1670 visit_version = visit_list_item 1671 1672 # Definition list: 1673 visit_definition_list = pass_node 1674 visit_definition_list_item = pass_node 1675 visit_term = ignore_node 1676 visit_classifier = pass_node 1677 visit_definition = visit_list_item 1678 1679 # Field list: 1680 visit_field_list = pass_node 1681 visit_field = pass_node 1682 # the field body corresponds to a list item 1683 visit_field_body = visit_list_item 1684 visit_field_name = ignore_node 1685 1686 # Invisible nodes should be ignored. 1687 visit_comment = ignore_node 1688 visit_substitution_definition = ignore_node 1689 visit_target = ignore_node 1690 visit_pending = ignore_node 1691