# -*- coding: utf-8 -*- from __future__ import print_function import os.path import posixpath import re try: from urllib.request import url2pathname # pylint: disable=deprecated-module except ImportError: from urllib import url2pathname # pylint: disable=deprecated-module from docutils import nodes from sphinx import addnodes, util from sphinx.locale import admonitionlabels from flectra.tools import pycompat def _parents(node): while node.parent: node = node.parent yield node class BootstrapTranslator(nodes.NodeVisitor, object): head_prefix = 'head_prefix' head = 'head' stylesheet = 'stylesheet' body_prefix = 'body_prefix' body_pre_docinfo = 'body_pre_docinfo' docinfo = 'docinfo' body_suffix = 'body_suffix' subtitle = 'subtitle' header = 'header' footer = 'footer' html_prolog = 'html_prolog' html_head = 'html_head' html_title = 'html_title' html_subtitle = 'html_subtitle' # tags meta = [ '', '' ] def __init__(self, builder, document): super(BootstrapTranslator, self).__init__(document) self.builder = builder self.body = [] self.fragment = self.body self.html_body = self.body # document title self.title = [] self.start_document_title = 0 self.first_title = False self.context = [] self.section_level = 0 self.highlightlang = self.highlightlang_base = self.builder.config.highlight_language self.highlightopts = getattr(builder.config, 'highlight_options', {}) self.first_param = 1 self.optional_param_level = 0 self.required_params_left = 0 self.param_separator = ',' def encode(self, text): return pycompat.text_type(text).translate({ ord('&'): u'&', ord('<'): u'<', ord('"'): u'"', ord('>'): u'>', 0xa0: u' ' }) def starttag(self, node, tagname, **attributes): tagname = pycompat.text_type(tagname).lower() # extract generic attributes attrs = {name.lower(): value for name, value in attributes.items()} attrs.update( (name, value) for name, value in node.attributes.items() if name.startswith('data-') ) prefix = [] postfix = [] # handle possibly multiple ids assert 'id' not in attrs, "starttag can't be passed a single id attribute, use a list of ids" ids = node.get('ids', []) + attrs.pop('ids', []) if ids: _ids = iter(ids) attrs['id'] = next(_ids) postfix.extend(u''.format(_id) for _id in _ids) # set CSS class classes = set(node.get('classes', []) + attrs.pop('class', '').split()) if classes: attrs['class'] = u' '.join(classes) return u'{prefix}<{tag} {attrs}>{postfix}'.format( prefix=u''.join(prefix), tag=tagname, attrs=u' '.join(u'{}="{}"'.format(name, self.attval(value)) for name, value in attrs.items()), postfix=u''.join(postfix), ) # only "space characters" SPACE, CHARACTER TABULATION, LINE FEED, # FORM FEED and CARRIAGE RETURN should be collapsed, not al White_Space def attval(self, value, whitespace=re.compile(u'[ \t\n\f\r]+')): return self.encode(whitespace.sub(u' ', pycompat.text_type(value))) def astext(self): return u''.join(self.body) def unknown_visit(self, node): print("unknown node", node.__class__.__name__) self.body.append(u'[UNKNOWN NODE {}]'.format(node.__class__.__name__)) raise nodes.SkipNode def visit_highlightlang(self, node): self.highlightlang = node['lang'] def depart_highlightlang(self, node): pass def visit_document(self, node): self.first_title = True def depart_document(self, node): pass def visit_section(self, node): # close "parent" or preceding section, unless this is the opening of # the first section if self.section_level: self.body.append(u'') self.section_level += 1 self.body.append(self.starttag(node, 'section')) def depart_section(self, node): self.section_level -= 1 # close last section of document if not self.section_level: self.body.append(u'') def visit_topic(self, node): self.body.append(self.starttag(node, 'nav')) def depart_topic(self, node): self.body.append(u'') def is_compact_paragraph(self, node): parent = node.parent if isinstance(parent, (nodes.document, nodes.compound, addnodes.desc_content, addnodes.versionmodified)): # Never compact paragraphs in document or compound. return False for key, value in node.attlist(): # we can ignore a few specific classes, all other non-default # attributes require that a
node remains if key != 'classes' or value not in ([], ['first'], ['last'], ['first', 'last']): return False first = isinstance(node.parent[0], nodes.label) for child in parent.children[first:]: # only first paragraph can be compact if isinstance(child, nodes.Invisible): continue if child is node: break return False parent_length = len([ 1 for n in parent if not isinstance(n, (nodes.Invisible, nodes.label)) ]) return parent_length == 1 def visit_paragraph(self, node): if self.is_compact_paragraph(node): self.context.append(u'') return self.body.append(self.starttag(node, 'p')) self.context.append(u'
') def depart_paragraph(self, node): self.body.append(self.context.pop()) def visit_compact_paragraph(self, node): pass def depart_compact_paragraph(self, node): pass def visit_literal_block(self, node): if node.rawsource != node.astext(): # most probably a parsed-literal block -- don't highlight self.body.append(self.starttag(node, 'pre')) return lang = self.highlightlang highlight_args = node.get('highlight_args', {}) if 'language' in node: # code-block directives lang = node['language'] highlight_args['force'] = True linenos = node.get('linenos', False) if lang is self.highlightlang_base: # only pass highlighter options for original language opts = self.highlightopts else: opts = {} def warner(msg): self.builder.warn(msg, (self.builder.current_docname, node.line)) highlighted = self.builder.highlighter.highlight_block( node.rawsource, lang, opts=opts, warn=warner, linenos=linenos, **highlight_args) self.body.append(self.starttag(node, 'div', CLASS='highlight-%s' % lang)) self.body.append(highlighted) self.body.append(u'\n') raise nodes.SkipNode def depart_literal_block(self, node): self.body.append(u'') def visit_bullet_list(self, node): self.body.append(self.starttag(node, 'ul')) def depart_bullet_list(self, node): self.body.append(u'') def visit_enumerated_list(self, node): self.body.append(self.starttag(node, 'ol')) def depart_enumerated_list(self, node): self.body.append(u'') def visit_list_item(self, node): self.body.append(self.starttag(node, 'li')) def depart_list_item(self, node): self.body.append(u'') def visit_definition_list(self, node): self.body.append(self.starttag(node, 'dl')) def depart_definition_list(self, node): self.body.append(u'') def visit_definition_list_item(self, node): pass def depart_definition_list_item(self, node): pass def visit_term(self, node): self.body.append(self.starttag(node, 'dt')) def depart_term(self, node): self.body.append(u'') def visit_termsep(self, node): self.body.append(self.starttag(node, 'br')) raise nodes.SkipNode def visit_definition(self, node): self.body.append(self.starttag(node, 'dd')) def depart_definition(self, node): self.body.append(u'') def visit_admonition(self, node, type=None): clss = { # ???: 'alert-success', 'note': 'alert-info', 'hint': 'alert-info', 'tip': 'alert-info', 'seealso': 'alert-go_to', 'warning': 'alert-warning', 'attention': 'alert-warning', 'caution': 'alert-warning', 'important': 'alert-warning', 'danger': 'alert-danger', 'error': 'alert-danger', 'exercise': 'alert-exercise', } self.body.append(self.starttag(node, 'div', role='alert', CLASS='alert {}'.format( clss.get(type, '') ))) if 'alert-dismissible' in node.get('classes', []): self.body.append( u'') if type: node.insert(0, nodes.title(type, admonitionlabels[type])) def depart_admonition(self, node): self.body.append(u'') visit_note = lambda self, node: self.visit_admonition(node, 'note') visit_warning = lambda self, node: self.visit_admonition(node, 'warning') visit_attention = lambda self, node: self.visit_admonition(node, 'attention') visit_caution = lambda self, node: self.visit_admonition(node, 'caution') visit_danger = lambda self, node: self.visit_admonition(node, 'danger') visit_error = lambda self, node: self.visit_admonition(node, 'error') visit_hint = lambda self, node: self.visit_admonition(node, 'hint') visit_important = lambda self, node: self.visit_admonition(node, 'important') visit_tip = lambda self, node: self.visit_admonition(node, 'tip') visit_exercise = lambda self, node: self.visit_admonition(node, 'exercise') visit_seealso = lambda self, node: self.visit_admonition(node, 'seealso') depart_note = depart_admonition depart_warning = depart_admonition depart_attention = depart_admonition depart_caution = depart_admonition depart_danger = depart_admonition depart_error = depart_admonition depart_hint = depart_admonition depart_important = depart_admonition depart_tip = depart_admonition depart_exercise = depart_admonition depart_seealso = depart_admonition def visit_versionmodified(self, node): self.body.append(self.starttag(node, 'div', CLASS=node['type'])) def depart_versionmodified(self, node): self.body.append(u'') def visit_title(self, node): parent = node.parent closing = u'' if isinstance(parent, nodes.Admonition): self.body.append(self.starttag(node, 'h3', CLASS='alert-title')) elif isinstance(node.parent, nodes.document): self.body.append(self.starttag(node, 'h1')) closing = u'' self.start_document_title = len(self.body) else: assert isinstance(parent, nodes.section), "expected a section node as parent to the title, found {}".format(parent) if self.first_title: self.first_title = False raise nodes.SkipNode() nodename = 'h{}'.format(self.section_level) self.body.append(self.starttag(node, nodename)) closing = u'{}>'.format(nodename) self.context.append(closing) def depart_title(self, node): self.body.append(self.context.pop()) if self.start_document_title: self.title = self.body[self.start_document_title:-1] self.start_document_title = 0 del self.body[:] # the rubric should be a smaller heading than the current section, up to # h6... maybe "h7" should be a ``p`` instead? def visit_rubric(self, node): self.body.append(self.starttag(node, 'h{}'.format(min(self.section_level + 1, 6)))) def depart_rubric(self, node): self.body.append(u''.format(min(self.section_level + 1, 6))) def visit_block_quote(self, node): self.body.append(self.starttag(node, 'blockquote')) def depart_block_quote(self, node): self.body.append(u'') def visit_attribution(self, node): self.body.append(self.starttag(node, 'footer')) def depart_attribution(self, node): self.body.append(u'') def visit_container(self, node): self.body.append(self.starttag(node, 'div')) def depart_container(self, node): self.body.append(u'') def visit_compound(self, node): self.body.append(self.starttag(node, 'div')) def depart_compound(self, node): self.body.append(u'') def visit_image(self, node): uri = node['uri'] if uri in self.builder.images: uri = posixpath.join(self.builder.imgpath, self.builder.images[uri]) attrs = {'src': uri, 'class': 'img-responsive'} if 'alt' in node: attrs['alt'] = node['alt'] if 'align' in node: if node['align'] == 'center': attrs['class'] += ' center-block' else: doc = None if node.source: doc = node.source if node.line: doc += ':%d' % node.line self.builder.app.warn( "Unsupported alignment value \"%s\"" % node['align'], location=doc ) elif 'align' in node.parent and node.parent['align'] == 'center': # figure > image attrs['class'] += ' center-block' # todo: explicit width/height/scale? self.body.append(self.starttag(node, 'img', **attrs)) def depart_image(self, node): pass def visit_figure(self, node): self.body.append(self.starttag(node, 'div')) def depart_figure(self, node): self.body.append(u'') def visit_caption(self, node): # first paragraph of figure content self.body.append(self.starttag(node, 'h4')) def depart_caption(self, node): self.body.append(u'') def visit_legend(self, node): pass def depart_legend(self, node): pass def visit_line(self, node): self.body.append(self.starttag(node, 'div', CLASS='line')) # ensure the line still takes the room it needs if not len(node): self.body.append(u'')
def depart_desc_signature(self, node):
self.body.append(u'
')
self.body.append(u'')
def visit_desc_addname(self, node): pass
def depart_desc_addname(self, node): pass
def visit_desc_type(self, node): pass
def depart_desc_type(self, node): pass
def visit_desc_returns(self, node):
self.body.append(u' → ')
def depart_desc_returns(self, node):
pass
def visit_desc_name(self, node): pass
def depart_desc_name(self, node): pass
def visit_desc_parameterlist(self, node):
self.body.append(u'(')
self.first_param = True
self.optional_param_level = 0
# How many required parameters are left.
self.required_params_left = sum(isinstance(c, addnodes.desc_parameter) for c in node.children)
self.param_separator = node.child_text_separator
def depart_desc_parameterlist(self, node):
self.body.append(u')')
# If required parameters are still to come, then put the comma after
# the parameter. Otherwise, put the comma before. This ensures that
# signatures like the following render correctly (see issue #1001):
#
# foo([a, ]b, c[, d])
#
def visit_desc_parameter(self, node):
if self.first_param:
self.first_param = 0
elif not self.required_params_left:
self.body.append(self.param_separator)
if self.optional_param_level == 0:
self.required_params_left -= 1
if 'noemph' not in node: self.body.append(u'')
def depart_desc_parameter(self, node):
if 'noemph' not in node: self.body.append(u'')
if self.required_params_left:
self.body.append(self.param_separator)
def visit_desc_optional(self, node):
self.optional_param_level += 1
self.body.append(u'[')
def depart_desc_optional(self, node):
self.optional_param_level -= 1
self.body.append(u']')
def visit_desc_annotation(self, node):
self.body.append(self.starttag(node, 'em'))
def depart_desc_annotation(self, node):
self.body.append(u'')
def visit_desc_content(self, node): pass
def depart_desc_content(self, node): pass
def visit_field_list(self, node):
self.body.append(self.starttag(node, 'div', CLASS='code-fields'))
def depart_field_list(self, node):
self.body.append(u'')
def visit_field(self, node):
self.body.append(self.starttag(node, 'div', CLASS='code-field'))
def depart_field(self, node):
self.body.append(u'')
def visit_field_name(self, node):
self.body.append(self.starttag(node, 'div', CLASS='code-field-name'))
def depart_field_name(self, node):
self.body.append(u'')
def visit_field_body(self, node):
self.body.append(self.starttag(node, 'div', CLASS='code-field-body'))
def depart_field_body(self, node):
self.body.append(u'')
def visit_glossary(self, node): pass
def depart_glossary(self, node): pass
def visit_comment(self, node): raise nodes.SkipNode
def visit_toctree(self, node):
# div class=row {{ section_type }}
# h2 class=col-sm-12
# {{ section title }}
# div class=col-sm-6 col-md-3
# figure class=card
# a href=current_link style=background-image: document-image-attribute class=card-img
# figcaption
# {{ card title }}
env = self.builder.env
conf = self.builder.app.config
for title, ref in ((e[0], e[1]) for e in node['entries']):
# external URL, no toc, can't recurse into
if ref not in env.tocs:
continue
toc = env.tocs[ref].traverse(addnodes.toctree)
classes = env.metadata[ref].get('types', 'tutorials')
classes += ' toc-single-entry' if not toc else ' toc-section'
self.body.append(self.starttag(node, 'div', CLASS="row " + classes))
self.body.append(u'