blob: 192534e0e294c9c31f4b97b24f98478ae35d82c4 [file] [log] [blame]
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This is a Sphinx extension.
#
from __future__ import print_function
import codecs
from collections import namedtuple, OrderedDict
import os
import string
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from sphinx.util.osutil import ensuredir
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.writers.html import HTMLWriter
from sphinx.writers.html import SmartyPantsHTMLTranslator as HTMLTranslator
from sphinx.util.console import bold
PEPPER_VERSION = "31"
# TODO(eliben): it may be interesting to use an actual Sphinx template here at
# some point.
PAGE_TEMPLATE = string.Template(r'''
${devsite_prefix}
<html devsite>
<head>
${nonprod_meta_head}
<title>${doc_title}</title>
<meta name="project_path" value="/native-client${folder}/_project.yaml" />
<meta name="book_path" value="/native-client${folder}/_book.yaml" />
<link href="/native-client/css/local_extensions.css" rel="stylesheet" type="text/css"/>
${nonprod_css}
<style type="text/css">
.nested-def {list-style-type: none; margin-bottom: 0.3em;}
.maxheight {height: 200px;}
</style>
</head>
<body>
${devsite_butterbar}
${doc_body}
</body>
</html>
'''.lstrip())
DEVSITE_PREFIX = r'''
{% setvar pepperversion %}pepper''' + PEPPER_VERSION + '''{% endsetvar %}
{% include "native-client/_local_variables.html" %}'''
DEVSITE_BUTTERBAR = '{{butterbar}}'
# We want the non-production-mode HTML to resemble the real one, so this points
# to a copied version of the devsite CSS that we'll keep locally. It's for
# testing purposes only.
NONPROD_CSS = '<link href="/_static/css/local_extensions.css"'\
'rel="stylesheet" type="text/css"/>'
NONPROD_META_HEAD = '<meta charset="utf-8" />'
# Path to the top-level YAML table-of-contents file for the devsite
BOOK_TOC_TEMPLATE = '_book_template.yaml'
class DevsiteHTMLTranslator(HTMLTranslator):
""" Custom HTML translator for devsite output.
Hooked into the HTML builder by setting the html_translator_class
option in conf.py
HTMLTranslator is provided by Sphinx. We're actually using
SmartyPantsHTMLTranslator to use its quote and dash-formatting
capabilities. It's a subclass of the HTMLTranslator provided by docutils,
with Sphinx-specific features added. Here we provide devsite-specific
behavior by overriding some of the visiting methods.
"""
def __init__(self, builder, *args, **kwds):
# HTMLTranslator is an old-style Python class, so 'super' doesn't work: use
# direct parent invocation.
HTMLTranslator.__init__(self, builder, *args, **kwds)
self.within_ignored_h1 = False
self.within_toc = False
def visit_bullet_list(self, node):
if self.within_toc:
# Within a TOC, devsite wants <ol>
self.body.append(self.starttag(node, 'ol'))
else:
# Use our own class attribute for <ul>. Don't care about compacted lists.
self.body.append(self.starttag(node, 'ul', **{'class': 'small-gap'}))
def depart_bullet_list(self, node):
if self.within_toc:
self.body.append('</ol>\n')
else:
# Override to not pop anything from context
self.body.append('</ul>\n')
def visit_literal(self, node):
# Don't insert "smart" quotes here
self.no_smarty += 1
# Sphinx emits <tt></tt> for literals (``like this``), with <span> per word
# to protect against wrapping, etc. We're required to emit plain <code>
# tags for them.
# Emit a simple <code> tag without enabling "protect_literal_text" mode,
# so Sphinx's visit_Text doesn't mess with the contents.
self.body.append(self.starttag(node, 'code', suffix=''))
def depart_literal(self, node):
self.no_smarty -= 1
self.body.append('</code>')
def visit_literal_block(self, node):
# Don't insert "smart" quotes here
self.no_smarty += 1
# We don't use Sphinx's buildin pygments integration for code highlighting,
# because the devsite requires special <pre> tags for that and handles the
# highlighting on its own.
attrs = {'class': 'prettyprint'} if node.get('prettyprint', 1) else {}
self.body.append(self.starttag(node, 'pre', **attrs))
def depart_literal_block(self, node):
self.no_smarty -= 1
self.body.append('\n</pre>\n')
def visit_paragraph(self, node):
# Don't generate <p>s within the table of contents
if not self.within_toc:
HTMLTranslator.visit_paragraph(self, node)
def depart_paragraph(self, node):
if not self.within_toc:
HTMLTranslator.depart_paragraph(self, node)
def visit_section(self, node):
# devsite needs <section> instead of <div class='section'>
self.section_level += 1
self.body.append(self.starttag(node, 'section'))
def depart_section(self, node):
self.section_level -= 1
self.body.append('</section>')
def visit_image(self, node):
# Paths to images in .rst sources should be absolute. This visitor does the
# required transformation for the path to be correct in the final HTML.
if self.builder.devsite_production_mode:
node['uri'] = self.builder.get_production_url(node['uri'])
HTMLTranslator.visit_image(self, node)
def visit_reference(self, node):
# In "kill_internal_links" mode, we don't emit the actual links for internal
# nodes.
if self.builder.kill_internal_links and node.get('internal'):
pass
else:
HTMLTranslator.visit_reference(self, node)
def depart_reference(self, node):
if self.builder.kill_internal_links and node.get('internal'):
pass
else:
HTMLTranslator.depart_reference(self, node)
def visit_title(self, node):
# Why this?
#
# Sphinx insists on inserting a <h1>Page Title</h1> into the page, but this
# is not how the devsite wants it. The devsite inserts the page title on
# its own, the the extra h1 is duplication.
#
# Killing the doctree title node in a custom transform doesn't work, because
# Sphinx explicitly looks for it when writing a document. So instead we rig
# the HTML produced.
#
# When a title node is visited, and this is the h1-to-be, we ignore it and
# also set a flag that tells visit_Text not to print the actual text of the
# header.
# The h1 node is in the section whose parent is the document itself. Other
# sections have this top-section as their parent.
if (node.parent and node.parent.parent and
isinstance(node.parent.parent, nodes.document)):
# Internal flag. Also, nothing is pushed to the context. Our depart_title
# doesn't pop anything when this flag is set.
self.within_ignored_h1 = True
else:
HTMLTranslator.visit_title(self, node)
def depart_title(self, node):
if not self.within_ignored_h1:
HTMLTranslator.depart_title(self, node)
self.within_ignored_h1 = False
def visit_Text(self, node):
if not self.within_ignored_h1:
HTMLTranslator.visit_Text(self, node)
def visit_topic(self, node):
if 'contents' in node['classes']:
# Detect a TOC: this requires special treatment for devsite.
self.within_toc = True
# Emit <nav> manually and not through starttage because we don't want
# additional class components to be added
self.body.append('\n<nav class="inline-toc">')
def depart_topic(self, node):
if self.within_toc:
self.within_toc = False
self.body.append('</nav>\n')
def write_colspecs(self):
# Override this method from docutils to do nothing. We don't need those
# pesky <col width=NN /> tags in our markup.
pass
def visit_admonition(self, node, name=''):
self.body.append(self.starttag(node, 'aside', CLASS=node.get('class', '')))
def depart_admonition(self, node=''):
self.body.append('\n</aside>\n')
def unknown_visit(self, node):
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
class DevsiteBuilder(StandaloneHTMLBuilder):
""" Builder for the NaCl devsite HTML output.
Loosely based on the code of Sphinx's standard SerializingHTMLBuilder.
"""
name = 'devsite'
out_suffix = '.html'
link_suffix = '.html'
# Disable the addition of "pi"-permalinks to each section header
add_permalinks = False
def init(self):
self.devsite_production_mode = int(self.config.devsite_production_mode) == 1
self.kill_internal_links = int(self.config.kill_internal_links) == 1
self.info("----> Devsite builder with production mode = %d" % (
self.devsite_production_mode,))
self.config_hash = ''
self.tags_hash = ''
self.theme = None # no theme necessary
self.templates = None # no template bridge necessary
self.init_translator_class()
self.init_highlighter()
def finish(self):
super(DevsiteBuilder, self).finish()
if self.devsite_production_mode:
# We decided to keep the manual _book.yaml for now;
# The code for auto-generating YAML TOCs from index.rst was removed in
# https://codereview.chromium.org/57923006/
self.info(bold('generating YAML table-of-contents... '))
subs = {
'version': PEPPER_VERSION,
'folder': self.config.devsite_foldername or ''}
with open(os.path.join(self.env.srcdir, '_book.yaml')) as in_f:
with open(os.path.join(self.outdir, '_book.yaml'), 'w') as out_f:
out_f.write(string.Template(in_f.read()).substitute(subs))
self.info()
def dump_inventory(self):
# We don't want an inventory file when building for devsite
if not self.devsite_production_mode:
super(DevsiteBuilder, self).dump_inventory()
def get_production_url(self, url):
if not self.devsite_production_mode:
return url
if self.config.devsite_foldername:
return '/native-client/%s/%s' % (self.config.devsite_foldername, url)
return '/native-client/%s' % url
def get_target_uri(self, docname, typ=None):
if self.devsite_production_mode:
return self.get_production_url(docname)
else:
return docname + self.link_suffix
def handle_page(self, pagename, ctx, templatename='page.html',
outfilename=None, event_arg=None):
ctx['current_page_name'] = pagename
if not outfilename:
outfilename = os.path.join(self.outdir,
pagename + self.out_suffix)
# Emit an event to Sphinx
self.app.emit('html-page-context', pagename, templatename,
ctx, event_arg)
ensuredir(os.path.dirname(outfilename))
self._dump_context(ctx, outfilename)
def _dump_context(self, context, filename):
""" Do the actual dumping of the page to the file. context is a dict. Some
important fields:
body - document contents
title
current_page_name
Some special pages (genindex, etc.) may not have some of the fields, so
fetch them conservatively.
"""
if not 'body' in context:
return
folder = ''
if self.devsite_production_mode and self.config.devsite_foldername:
folder = "/" + self.config.devsite_foldername
# codecs.open is the fast Python 2.x way of emulating the encoding= argument
# in Python 3's builtin open.
with codecs.open(filename, 'w', encoding='utf-8') as f:
f.write(PAGE_TEMPLATE.substitute(
doc_title=context.get('title', ''),
doc_body=context.get('body'),
folder=folder,
nonprod_css=self._conditional_nonprod(NONPROD_CSS),
nonprod_meta_head=self._conditional_nonprod(NONPROD_META_HEAD),
devsite_prefix=self._conditional_devsite(DEVSITE_PREFIX),
devsite_butterbar=self._conditional_devsite(DEVSITE_BUTTERBAR)))
def _conditional_devsite(self, s):
return s if self.devsite_production_mode else ''
def _conditional_nonprod(self, s):
return s if not self.devsite_production_mode else ''
class NaclCodeDirective(Directive):
""" Custom "naclcode" directive for code snippets. To keep it under our
control.
"""
has_content = True
required_arguments = 0
optional_arguments = 1
option_spec = {
'prettyprint': int,
}
def run(self):
code = u'\n'.join(self.content)
literal = nodes.literal_block(code, code)
literal['prettyprint'] = self.options.get('prettyprint', 1)
return [literal]
def setup(app):
""" Extension registration hook.
"""
# linkcheck issues HEAD requests to save time, but some Google properties
# reject them and we get spurious 405 responses. Monkey-patch sphinx to
# just use normal GET requests.
# See: https://bitbucket.org/birkenfeld/sphinx/issue/1292/
from sphinx.builders import linkcheck
import urllib2
linkcheck.HeadRequest = urllib2.Request
app.add_directive('naclcode', NaclCodeDirective)
app.add_builder(DevsiteBuilder)
# "Production mode" for local testing vs. on-server documentation.
app.add_config_value('devsite_production_mode', default='1', rebuild='html')
app.add_config_value('kill_internal_links', default='0', rebuild='html')
app.add_config_value('devsite_foldername', default=None, rebuild='html')