# -*- coding: utf-8 -*- """ sphinx.writers.html ~~~~~~~~~~~~~~~~~~~ docutils writers handling Sphinx' custom nodes. :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import sys import posixpath import os from docutils import nodes from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator from sphinx.locale import admonitionlabels, versionlabels from sphinx.util.smartypants import sphinx_smarty_pants try: import Image # check for the Python Imaging Library except ImportError: Image = None class HTMLWriter(Writer): def __init__(self, builder): Writer.__init__(self) self.builder = builder def translate(self): # sadly, this is mostly copied from parent class self.visitor = visitor = self.builder.translator_class(self.builder, self.document) self.document.walkabout(visitor) self.output = visitor.astext() for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix', 'body_pre_docinfo', 'docinfo', 'body', 'fragment', 'body_suffix', 'meta', 'title', 'subtitle', 'header', 'footer', 'html_prolog', 'html_head', 'html_title', 'html_subtitle', 'html_body', ): setattr(self, attr, getattr(visitor, attr, None)) self.clean_meta = ''.join(visitor.meta[2:]) class HTMLTranslator(BaseTranslator): """ Our custom HTML translator. """ def __init__(self, builder, *args, **kwds): BaseTranslator.__init__(self, *args, **kwds) self.highlighter = builder.highlighter self.no_smarty = 0 self.builder = builder self.highlightlang = builder.config.highlight_language self.highlightlinenothreshold = sys.maxint self.protect_literal_text = 0 self.add_permalinks = builder.config.html_add_permalinks def visit_desc(self, node): self.body.append(self.starttag(node, 'dl', CLASS=node['desctype'])) def depart_desc(self, node): self.body.append('\n\n') def visit_desc_signature(self, node): # the id is set automatically self.body.append(self.starttag(node, 'dt')) # anchor for per-desc interactive data if node.parent['desctype'] != 'describe' \ and node['ids'] and node['first']: self.body.append('' % node['ids'][0]) if node.parent['desctype'] in ('class', 'exception'): self.body.append('%s ' % node.parent['desctype']) def depart_desc_signature(self, node): if node['ids'] and self.add_permalinks and self.builder.add_permalinks: self.body.append(u'\u00B6' % _('Permalink to this definition')) self.body.append('\n') def visit_desc_addname(self, node): self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname')) def depart_desc_addname(self, node): self.body.append('') def visit_desc_type(self, node): pass def depart_desc_type(self, node): pass def visit_desc_returns(self, node): self.body.append(' → ') def depart_desc_returns(self, node): pass def visit_desc_name(self, node): self.body.append(self.starttag(node, 'tt', '', CLASS='descname')) def depart_desc_name(self, node): self.body.append('') def visit_desc_parameterlist(self, node): self.body.append('(') self.first_param = 1 def depart_desc_parameterlist(self, node): self.body.append(')') def visit_desc_parameter(self, node): if not self.first_param: self.body.append(', ') else: self.first_param = 0 if not node.hasattr('noemph'): self.body.append('') def depart_desc_parameter(self, node): if not node.hasattr('noemph'): self.body.append('') def visit_desc_optional(self, node): self.body.append('[') def depart_desc_optional(self, node): self.body.append(']') def visit_desc_annotation(self, node): self.body.append(self.starttag(node, 'em', CLASS='property')) def depart_desc_annotation(self, node): self.body.append('') def visit_desc_content(self, node): self.body.append(self.starttag(node, 'dd', '')) def depart_desc_content(self, node): self.body.append('') def visit_refcount(self, node): self.body.append(self.starttag(node, 'em', '', CLASS='refcount')) def depart_refcount(self, node): self.body.append('') def visit_versionmodified(self, node): self.body.append(self.starttag(node, 'p')) text = versionlabels[node['type']] % node['version'] if len(node): text += ': ' else: text += '.' self.body.append('%s' % text) def depart_versionmodified(self, node): self.body.append('

\n') # overwritten def visit_reference(self, node): BaseTranslator.visit_reference(self, node) if node.hasattr('reftitle'): # ugly hack to add a title attribute starttag = self.body[-1] if not starttag.startswith(' tag self.section_level += 1 self.body.append(self.starttag(node, 'div', CLASS='section')) def visit_title(self, node): # don't move the id attribute inside the tag BaseTranslator.visit_title(self, node, move_ids=0) # overwritten def visit_literal_block(self, node): if node.rawsource != node.astext(): # most probably a parsed-literal block -- don't highlight return BaseTranslator.visit_literal_block(self, node) lang = self.highlightlang linenos = node.rawsource.count('\n') >= \ self.highlightlinenothreshold - 1 if node.has_key('language'): # code-block directives lang = node['language'] if node.has_key('linenos'): linenos = node['linenos'] highlighted = self.highlighter.highlight_block(node.rawsource, lang, linenos) starttag = self.starttag(node, 'div', suffix='', CLASS='highlight-%s' % lang) self.body.append(starttag + highlighted + '\n') raise nodes.SkipNode def visit_doctest_block(self, node): self.visit_literal_block(node) # overwritten def visit_literal(self, node): if len(node.children) == 1 and \ node.children[0] in ('None', 'True', 'False'): node['classes'].append('xref') self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal')) self.protect_literal_text += 1 def depart_literal(self, node): self.protect_literal_text -= 1 self.body.append('') def visit_productionlist(self, node): self.body.append(self.starttag(node, 'pre')) names = [] for production in node: names.append(production['tokenname']) maxlen = max(len(name) for name in names) for production in node: if production['tokenname']: lastname = production['tokenname'].ljust(maxlen) self.body.append(self.starttag(production, 'strong', '')) self.body.append(lastname + ' ::= ') else: self.body.append('%s ' % (' '*len(lastname))) production.walkabout(self) self.body.append('\n') self.body.append('\n') raise nodes.SkipNode def depart_productionlist(self, node): pass def visit_production(self, node): pass def depart_production(self, node): pass def visit_centered(self, node): self.body.append(self.starttag(node, 'p', CLASS="centered") + '') def depart_centered(self, node): self.body.append('

') def visit_compact_paragraph(self, node): pass def depart_compact_paragraph(self, node): pass def visit_highlightlang(self, node): self.highlightlang = node['lang'] self.highlightlinenothreshold = node['linenothreshold'] def depart_highlightlang(self, node): pass def visit_download_reference(self, node): if node.hasattr('filename'): self.body.append('
' % posixpath.join( self.builder.dlpath, node['filename'])) self.context.append('') else: self.context.append('') def depart_download_reference(self, node): self.body.append(self.context.pop()) # overwritten def visit_image(self, node): olduri = node['uri'] # rewrite the URI if the environment knows about it if olduri in self.builder.images: node['uri'] = posixpath.join(self.builder.imgpath, self.builder.images[olduri]) if node['uri'].lower().endswith('svg') or \ node['uri'].lower().endswith('svgz'): atts = {'data': node['uri'], 'type': 'image/svg+xml'} if 'width' in node: atts['width'] = node['width'] if 'height' in node: atts['height'] = node['height'] if 'align' in node: self.body.append('
' % (node['align'], node['align'])) self.context.append('
\n') else: self.context.append('') embatts = atts.copy() embatts['src'] = embatts.pop('data') self.body.append(self.starttag(node, 'object', '', **atts)) self.body.append(self.emptytag(node, 'embed', '', **embatts)) self.body.append('\n') return if node.has_key('scale'): if Image and not (node.has_key('width') and node.has_key('height')): try: im = Image.open(os.path.join(self.builder.srcdir, olduri)) except (IOError, # Source image can't be found or opened UnicodeError): # PIL doesn't like Unicode paths. pass else: if not node.has_key('width'): node['width'] = str(im.size[0]) if not node.has_key('height'): node['height'] = str(im.size[1]) del im BaseTranslator.visit_image(self, node) def visit_toctree(self, node): # this only happens when formatting a toc from env.tocs -- in this # case we don't want to include the subtree raise nodes.SkipNode def visit_index(self, node): raise nodes.SkipNode def visit_tabular_col_spec(self, node): raise nodes.SkipNode def visit_glossary(self, node): pass def depart_glossary(self, node): pass def visit_acks(self, node): pass def depart_acks(self, node): pass def visit_module(self, node): pass def depart_module(self, node): pass def visit_hlist(self, node): self.body.append('') def depart_hlist(self, node): self.body.append('
\n') def visit_hlistcol(self, node): self.body.append('') def depart_hlistcol(self, node): self.body.append('') def bulk_text_processor(self, text): return text # overwritten def visit_Text(self, node): text = node.astext() encoded = self.encode(text) if self.protect_literal_text: # moved here from base class's visit_literal to support # more formatting in literal nodes for token in self.words_and_spaces.findall(encoded): if token.strip(): # protect literal text from line wrapping self.body.append('%s' % token) elif token in ' \n': # allow breaks at whitespace self.body.append(token) else: # protect runs of multiple spaces; the last one can wrap self.body.append(' ' * (len(token)-1) + ' ') else: if self.in_mailto and self.settings.cloak_email_addresses: encoded = self.cloak_email(encoded) else: encoded = self.bulk_text_processor(encoded) self.body.append(encoded) # these are all for docutils 0.5 compatibility def visit_note(self, node): self.visit_admonition(node, 'note') def depart_note(self, node): self.depart_admonition(node) def visit_warning(self, node): self.visit_admonition(node, 'warning') def depart_warning(self, node): self.depart_admonition(node) def visit_attention(self, node): self.visit_admonition(node, 'attention') def depart_attention(self, node): self.depart_admonition() def visit_caution(self, node): self.visit_admonition(node, 'caution') def depart_caution(self, node): self.depart_admonition() def visit_danger(self, node): self.visit_admonition(node, 'danger') def depart_danger(self, node): self.depart_admonition() def visit_error(self, node): self.visit_admonition(node, 'error') def depart_error(self, node): self.depart_admonition() def visit_hint(self, node): self.visit_admonition(node, 'hint') def depart_hint(self, node): self.depart_admonition() def visit_important(self, node): self.visit_admonition(node, 'important') def depart_important(self, node): self.depart_admonition() def visit_tip(self, node): self.visit_admonition(node, 'tip') def depart_tip(self, node): self.depart_admonition() # these are only handled specially in the SmartyPantsHTMLTranslator def visit_literal_emphasis(self, node): return self.visit_emphasis(node) def depart_literal_emphasis(self, node): return self.depart_emphasis(node) def depart_title(self, node): close_tag = self.context[-1] if self.add_permalinks and self.builder.add_permalinks and \ (close_tag.startswith('\u00B6' % _('Permalink to this headline')) BaseTranslator.depart_title(self, node) def unknown_visit(self, node): raise NotImplementedError('Unknown node: ' + node.__class__.__name__) class SmartyPantsHTMLTranslator(HTMLTranslator): """ Handle ordinary text via smartypants, converting quotes and dashes to the correct entities. """ def __init__(self, *args, **kwds): self.no_smarty = 0 HTMLTranslator.__init__(self, *args, **kwds) def visit_literal(self, node): self.no_smarty += 1 try: # this raises SkipNode HTMLTranslator.visit_literal(self, node) finally: self.no_smarty -= 1 def visit_literal_emphasis(self, node): self.no_smarty += 1 self.visit_emphasis(node) def depart_literal_emphasis(self, node): self.depart_emphasis(node) self.no_smarty -= 1 def visit_desc_signature(self, node): self.no_smarty += 1 HTMLTranslator.visit_desc_signature(self, node) def depart_desc_signature(self, node): self.no_smarty -= 1 HTMLTranslator.depart_desc_signature(self, node) def visit_productionlist(self, node): self.no_smarty += 1 try: HTMLTranslator.visit_productionlist(self, node) finally: self.no_smarty -= 1 def visit_option(self, node): self.no_smarty += 1 HTMLTranslator.visit_option(self, node) def depart_option(self, node): self.no_smarty -= 1 HTMLTranslator.depart_option(self, node) def bulk_text_processor(self, text): if self.no_smarty <= 0: return sphinx_smarty_pants(text) return text