diff options
Diffstat (limited to 'doc/_extensions/odoo/translator.py')
| -rw-r--r-- | doc/_extensions/odoo/translator.py | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/doc/_extensions/odoo/translator.py b/doc/_extensions/odoo/translator.py new file mode 100644 index 00000000000..681d4456e2f --- /dev/null +++ b/doc/_extensions/odoo/translator.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- +import os.path +import posixpath +import re +import urllib + +from docutils import nodes +from sphinx import addnodes, util +from sphinx.locale import admonitionlabels + + +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' + + # <meta> tags + meta = [ + '<meta http-equiv="X-UA-Compatible" content="IE=edge">', + '<meta name="viewport" content="width=device-width, initial-scale=1">' + ] + + 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 unicode(text).translate({ + ord('&'): u'&', + ord('<'): u'<', + ord('"'): u'"', + ord('>'): u'>', + 0xa0: u' ' + }) + + def starttag(self, node, tagname, **attributes): + tagname = unicode(tagname).lower() + + # extract generic attributes + attrs = {name.lower(): value for name, value in attributes.iteritems()} + attrs.update( + (name, value) for name, value in node.attributes.iteritems() + 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'<i id="{}"></i>'.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.iteritems()), + 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' ', unicode(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'</section>') + 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'</section>') + + 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 + 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'</p>') + 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'</div>\n') + raise nodes.SkipNode + def depart_literal_block(self, node): + self.body.append(u'</pre>') + + def visit_bullet_list(self, node): + self.body.append(self.starttag(node, 'ul')) + def depart_bullet_list(self, node): + self.body.append(u'</ul>') + def visit_enumerated_list(self, node): + self.body.append(self.starttag(node, 'ol')) + def depart_enumerated_list(self, node): + self.body.append(u'</ol>') + def visit_list_item(self, node): + self.body.append(self.starttag(node, 'li')) + def depart_list_item(self, node): + self.body.append(u'</li>') + def visit_definition_list(self, node): + self.body.append(self.starttag(node, 'dl')) + def depart_definition_list(self, node): + self.body.append(u'</dl>') + 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'</dt>') + 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'</dd>') + + def visit_admonition(self, node, type=None): + clss = { + # ???: 'alert-success', + + 'note': 'alert-info', + 'hint': 'alert-info', + 'tip': 'alert-info', + 'seealso': 'alert-info', + + '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'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' + u'<span aria-hidden="true">×</span>' + u'</button>') + if type: + node.insert(0, nodes.title(type, admonitionlabels[type])) + def depart_admonition(self, node): + self.body.append(u'</div>') + 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'</div>') + + def visit_title(self, node): + parent = node.parent + closing = u'</p>' + if isinstance(parent, nodes.Admonition): + self.body.append(self.starttag(node, 'p', CLASS='alert-title')) + elif isinstance(node.parent, nodes.document): + self.body.append(self.starttag(node, 'h1')) + closing = u'</h1>' + 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'</h{}>'.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'</blockquote>') + def visit_attribution(self, node): + self.body.append(self.starttag(node, 'footer')) + def depart_attribution(self, node): + self.body.append(u'</footer>') + + def visit_container(self, node): + self.body.append(self.starttag(node, 'div')) + def depart_container(self, node): + self.body.append(u'</div>') + def visit_compound(self, node): + self.body.append(self.starttag(node, 'div')) + def depart_compound(self, node): + self.body.append(u'</div>') + + 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'] + # 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'</div>') + 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'</h4>') + def visit_legend(self, node): pass + def depart_legend(self, node): pass + + def visit_table(self, node): + self.body.append(self.starttag(node, 'table', CLASS='table')) + def depart_table(self, node): + self.body.append(u'</table>') + def visit_tgroup(self, node): pass + def depart_tgroup(self, node): pass + def visit_colspec(self, node): raise nodes.SkipNode + def visit_thead(self, node): + self.body.append(self.starttag(node, 'thead')) + def depart_thead(self, node): + self.body.append(u'</thead>') + def visit_tbody(self, node): + self.body.append(self.starttag(node, 'tbody')) + def depart_tbody(self, node): + self.body.append(u'</tbody>') + def visit_row(self, node): + self.body.append(self.starttag(node, 'tr')) + def depart_row(self, node): + self.body.append(u'</tr>') + def visit_entry(self, node): + if isinstance(node.parent.parent, nodes.thead): + tagname = 'th' + else: + tagname = 'td' + self.body.append(self.starttag(node, tagname)) + self.context.append(tagname) + def depart_entry(self, node): + self.body.append(u'</{}>'.format(self.context.pop())) + + def visit_Text(self, node): + self.body.append(self.encode(node.astext())) + def depart_Text(self, node): + pass + def visit_literal(self, node): + self.body.append(self.starttag(node, 'code')) + def depart_literal(self, node): + self.body.append(u'</code>') + visit_literal_emphasis = visit_literal + depart_literal_emphasis = depart_literal + def visit_emphasis(self, node): + self.body.append(self.starttag(node, 'em')) + def depart_emphasis(self, node): + self.body.append(u'</em>') + def visit_strong(self, node): + self.body.append(self.starttag(node, 'strong')) + def depart_strong(self, node): + self.body.append(u'</strong>') + visit_literal_strong = visit_strong + depart_literal_strong = depart_strong + def visit_inline(self, node): + self.body.append(self.starttag(node, 'span')) + def depart_inline(self, node): + self.body.append(u'</span>') + def visit_abbreviation(self, node): + attrs = {} + if 'explanation' in node: + attrs['title'] = node['explanation'] + self.body.append(self.starttag(node, 'abbr', **attrs)) + def depart_abbreviation(self, node): + self.body.append(u'</abbr>') + + def visit_reference(self, node): + attrs = { + 'class': 'reference', + 'href': node['refuri'] if 'refuri' in node else '#' + node['refid'] + } + attrs['class'] += ' internal' if (node.get('internal') or 'refuri' not in node) else ' external' + if any(isinstance(ancestor, nodes.Admonition) for ancestor in _parents(node)): + attrs['class'] += ' alert-link' + + if 'reftitle' in node: + attrs['title'] = node['reftitle'] + + self.body.append(self.starttag(node, 'a', **attrs)) + def depart_reference(self, node): + self.body.append(u'</a>') + def visit_target(self, node): pass + def depart_target(self, node): pass + def visit_footnote(self, node): + self.body.append(self.starttag(node, 'div', CLASS='footnote')) + self.footnote_backrefs(node) + def depart_footnote(self, node): + self.body.append(u'</div>') + def visit_footnote_reference(self, node): + self.body.append(self.starttag( + node, 'a', href='#' + node['refid'], CLASS="footnote-ref")) + def depart_footnote_reference(self, node): + self.body.append(u'</a>') + def visit_label(self, node): + self.body.append(self.starttag(node, 'span', CLASS='footnote-label')) + self.body.append(u'%s[' % self.context.pop()) + def depart_label(self, node): + # Context added in footnote_backrefs. + self.body.append(u']%s</span> %s' % (self.context.pop(), self.context.pop())) + def footnote_backrefs(self, node): + # should store following data on context stack (in that order since + # they'll be popped so LIFO) + # + # * outside (after) label + # * after label text + # * before label text + backrefs = node['backrefs'] + if not backrefs: + self.context.extend(['', '', '']) + elif len(backrefs) == 1: + self.context.extend([ + '', + '</a>', + '<a class="footnote-backref" href="#%s">' % backrefs[0] + ]) + else: + backlinks = ( + '<a class="footnote-backref" href="#%s">%s</a>' % (backref, i) + for i, backref in enumerate(backrefs, start=1) + ) + self.context.extend([ + '<em class="footnote-backrefs">(%s)</em> ' % ', '.join(backlinks), + '', + '' + ]) + + def visit_desc(self, node): + self.body.append(self.starttag(node, 'section', CLASS='code-' + node['objtype'])) + def depart_desc(self, node): + self.body.append(u'</section>') + def visit_desc_signature(self, node): + self.body.append(self.starttag(node, 'h6')) + self.body.append(u'<code>') + def depart_desc_signature(self, node): + self.body.append(u'</code>') + self.body.append(u'</h6>') + 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'<em>') + def depart_desc_parameter(self, node): + if 'noemph' not in node: self.body.append(u'</em>') + 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'</em>') + 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'</div>') + def visit_field(self, node): + self.body.append(self.starttag(node, 'div', CLASS='code-field')) + def depart_field(self, node): + self.body.append(u'</div>') + 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'</div>') + 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'</div>') + + 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'<h2 class="col-sm-12">') + self.body.append(title if title else util.nodes.clean_astext(env.titles[ref])) + self.body.append(u'</h2>') + + entries = [(title, ref)] if not toc else ((e[0], e[1]) for e in toc[0]['entries']) + for subtitle, subref in entries: + if subref in env.metadata: + cover = env.metadata[subref].get('banner', conf.odoo_cover_default) + elif subref in conf.odoo_cover_external: + cover = conf.odoo_cover_external[subref] + else: + cover = conf.odoo_cover_default_external + banner = '_static/' + cover + base, ext = os.path.splitext(banner) + small = "{}.small{}".format(base, ext) + if os.path.isfile(urllib.url2pathname(small)): + banner = small + baseuri = self.builder.get_target_uri(node['parent']) + self.body.append(u""" + <div class="col-sm-6 col-md-3"> + <figure class="card"> + <a href="{link}" class="card-img"> + <span style="background-image: url('{banner}')"></span> + <figcaption>{title}</figcaption> + </a> + </figure> + </div> + """.format( + link=subref if util.url_re.match(subref) else util.relative_uri( + baseuri, self.builder.get_target_uri(subref)), + banner=util.relative_uri(baseuri, banner) or '#', + title=subtitle if subtitle else util.nodes.clean_astext(env.titles[subref]), + )) + + self.body.append(u'</div>') + raise nodes.SkipNode + + def visit_index(self, node): raise nodes.SkipNode + + def visit_raw(self, node): + if 'html' in node.get('format', '').split(): + t = 'span' if isinstance(node.parent, nodes.TextElement) else 'div' + if node['classes']: + self.body.append(self.starttag(node, t)) + self.body.append(node.astext()) + if node['classes']: + self.body.append('</%s>' % t) + # Keep non-HTML raw text out of output: + raise nodes.SkipNode |
