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