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 |