Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / native_client_sdk / src / doc / _sphinxext / devsite_builder.py
blob192534e0e294c9c31f4b97b24f98478ae35d82c4
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
6 # This is a Sphinx extension.
9 from __future__ import print_function
10 import codecs
11 from collections import namedtuple, OrderedDict
12 import os
13 import string
14 from docutils import nodes
15 from docutils.parsers.rst import Directive, directives
16 from sphinx.util.osutil import ensuredir
17 from sphinx.builders.html import StandaloneHTMLBuilder
18 from sphinx.writers.html import HTMLWriter
19 from sphinx.writers.html import SmartyPantsHTMLTranslator as HTMLTranslator
20 from sphinx.util.console import bold
22 PEPPER_VERSION = "31"
24 # TODO(eliben): it may be interesting to use an actual Sphinx template here at
25 # some point.
26 PAGE_TEMPLATE = string.Template(r'''
27 ${devsite_prefix}
28 <html devsite>
29 <head>
30 ${nonprod_meta_head}
31 <title>${doc_title}</title>
32 <meta name="project_path" value="/native-client${folder}/_project.yaml" />
33 <meta name="book_path" value="/native-client${folder}/_book.yaml" />
34 <link href="/native-client/css/local_extensions.css" rel="stylesheet" type="text/css"/>
35 ${nonprod_css}
36 <style type="text/css">
37 .nested-def {list-style-type: none; margin-bottom: 0.3em;}
38 .maxheight {height: 200px;}
39 </style>
40 </head>
41 <body>
42 ${devsite_butterbar}
44 ${doc_body}
46 </body>
47 </html>
48 '''.lstrip())
50 DEVSITE_PREFIX = r'''
51 {% setvar pepperversion %}pepper''' + PEPPER_VERSION + '''{% endsetvar %}
52 {% include "native-client/_local_variables.html" %}'''
54 DEVSITE_BUTTERBAR = '{{butterbar}}'
56 # We want the non-production-mode HTML to resemble the real one, so this points
57 # to a copied version of the devsite CSS that we'll keep locally. It's for
58 # testing purposes only.
59 NONPROD_CSS = '<link href="/_static/css/local_extensions.css"'\
60 'rel="stylesheet" type="text/css"/>'
61 NONPROD_META_HEAD = '<meta charset="utf-8" />'
63 # Path to the top-level YAML table-of-contents file for the devsite
64 BOOK_TOC_TEMPLATE = '_book_template.yaml'
67 class DevsiteHTMLTranslator(HTMLTranslator):
68 """ Custom HTML translator for devsite output.
70 Hooked into the HTML builder by setting the html_translator_class
71 option in conf.py
73 HTMLTranslator is provided by Sphinx. We're actually using
74 SmartyPantsHTMLTranslator to use its quote and dash-formatting
75 capabilities. It's a subclass of the HTMLTranslator provided by docutils,
76 with Sphinx-specific features added. Here we provide devsite-specific
77 behavior by overriding some of the visiting methods.
78 """
79 def __init__(self, builder, *args, **kwds):
80 # HTMLTranslator is an old-style Python class, so 'super' doesn't work: use
81 # direct parent invocation.
82 HTMLTranslator.__init__(self, builder, *args, **kwds)
84 self.within_ignored_h1 = False
85 self.within_toc = False
87 def visit_bullet_list(self, node):
88 if self.within_toc:
89 # Within a TOC, devsite wants <ol>
90 self.body.append(self.starttag(node, 'ol'))
91 else:
92 # Use our own class attribute for <ul>. Don't care about compacted lists.
93 self.body.append(self.starttag(node, 'ul', **{'class': 'small-gap'}))
95 def depart_bullet_list(self, node):
96 if self.within_toc:
97 self.body.append('</ol>\n')
98 else:
99 # Override to not pop anything from context
100 self.body.append('</ul>\n')
102 def visit_literal(self, node):
103 # Don't insert "smart" quotes here
104 self.no_smarty += 1
105 # Sphinx emits <tt></tt> for literals (``like this``), with <span> per word
106 # to protect against wrapping, etc. We're required to emit plain <code>
107 # tags for them.
108 # Emit a simple <code> tag without enabling "protect_literal_text" mode,
109 # so Sphinx's visit_Text doesn't mess with the contents.
110 self.body.append(self.starttag(node, 'code', suffix=''))
112 def depart_literal(self, node):
113 self.no_smarty -= 1
114 self.body.append('</code>')
116 def visit_literal_block(self, node):
117 # Don't insert "smart" quotes here
118 self.no_smarty += 1
119 # We don't use Sphinx's buildin pygments integration for code highlighting,
120 # because the devsite requires special <pre> tags for that and handles the
121 # highlighting on its own.
122 attrs = {'class': 'prettyprint'} if node.get('prettyprint', 1) else {}
123 self.body.append(self.starttag(node, 'pre', **attrs))
125 def depart_literal_block(self, node):
126 self.no_smarty -= 1
127 self.body.append('\n</pre>\n')
129 def visit_paragraph(self, node):
130 # Don't generate <p>s within the table of contents
131 if not self.within_toc:
132 HTMLTranslator.visit_paragraph(self, node)
134 def depart_paragraph(self, node):
135 if not self.within_toc:
136 HTMLTranslator.depart_paragraph(self, node)
138 def visit_section(self, node):
139 # devsite needs <section> instead of <div class='section'>
140 self.section_level += 1
141 self.body.append(self.starttag(node, 'section'))
143 def depart_section(self, node):
144 self.section_level -= 1
145 self.body.append('</section>')
147 def visit_image(self, node):
148 # Paths to images in .rst sources should be absolute. This visitor does the
149 # required transformation for the path to be correct in the final HTML.
150 if self.builder.devsite_production_mode:
151 node['uri'] = self.builder.get_production_url(node['uri'])
152 HTMLTranslator.visit_image(self, node)
154 def visit_reference(self, node):
155 # In "kill_internal_links" mode, we don't emit the actual links for internal
156 # nodes.
157 if self.builder.kill_internal_links and node.get('internal'):
158 pass
159 else:
160 HTMLTranslator.visit_reference(self, node)
162 def depart_reference(self, node):
163 if self.builder.kill_internal_links and node.get('internal'):
164 pass
165 else:
166 HTMLTranslator.depart_reference(self, node)
168 def visit_title(self, node):
169 # Why this?
171 # Sphinx insists on inserting a <h1>Page Title</h1> into the page, but this
172 # is not how the devsite wants it. The devsite inserts the page title on
173 # its own, the the extra h1 is duplication.
175 # Killing the doctree title node in a custom transform doesn't work, because
176 # Sphinx explicitly looks for it when writing a document. So instead we rig
177 # the HTML produced.
179 # When a title node is visited, and this is the h1-to-be, we ignore it and
180 # also set a flag that tells visit_Text not to print the actual text of the
181 # header.
183 # The h1 node is in the section whose parent is the document itself. Other
184 # sections have this top-section as their parent.
185 if (node.parent and node.parent.parent and
186 isinstance(node.parent.parent, nodes.document)):
187 # Internal flag. Also, nothing is pushed to the context. Our depart_title
188 # doesn't pop anything when this flag is set.
189 self.within_ignored_h1 = True
190 else:
191 HTMLTranslator.visit_title(self, node)
193 def depart_title(self, node):
194 if not self.within_ignored_h1:
195 HTMLTranslator.depart_title(self, node)
196 self.within_ignored_h1 = False
198 def visit_Text(self, node):
199 if not self.within_ignored_h1:
200 HTMLTranslator.visit_Text(self, node)
202 def visit_topic(self, node):
203 if 'contents' in node['classes']:
204 # Detect a TOC: this requires special treatment for devsite.
205 self.within_toc = True
206 # Emit <nav> manually and not through starttage because we don't want
207 # additional class components to be added
208 self.body.append('\n<nav class="inline-toc">')
210 def depart_topic(self, node):
211 if self.within_toc:
212 self.within_toc = False
213 self.body.append('</nav>\n')
215 def write_colspecs(self):
216 # Override this method from docutils to do nothing. We don't need those
217 # pesky <col width=NN /> tags in our markup.
218 pass
220 def visit_admonition(self, node, name=''):
221 self.body.append(self.starttag(node, 'aside', CLASS=node.get('class', '')))
223 def depart_admonition(self, node=''):
224 self.body.append('\n</aside>\n')
226 def unknown_visit(self, node):
227 raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
230 class DevsiteBuilder(StandaloneHTMLBuilder):
231 """ Builder for the NaCl devsite HTML output.
233 Loosely based on the code of Sphinx's standard SerializingHTMLBuilder.
235 name = 'devsite'
236 out_suffix = '.html'
237 link_suffix = '.html'
239 # Disable the addition of "pi"-permalinks to each section header
240 add_permalinks = False
242 def init(self):
243 self.devsite_production_mode = int(self.config.devsite_production_mode) == 1
244 self.kill_internal_links = int(self.config.kill_internal_links) == 1
245 self.info("----> Devsite builder with production mode = %d" % (
246 self.devsite_production_mode,))
247 self.config_hash = ''
248 self.tags_hash = ''
249 self.theme = None # no theme necessary
250 self.templates = None # no template bridge necessary
251 self.init_translator_class()
252 self.init_highlighter()
254 def finish(self):
255 super(DevsiteBuilder, self).finish()
256 if self.devsite_production_mode:
257 # We decided to keep the manual _book.yaml for now;
258 # The code for auto-generating YAML TOCs from index.rst was removed in
259 # https://codereview.chromium.org/57923006/
260 self.info(bold('generating YAML table-of-contents... '))
261 subs = {
262 'version': PEPPER_VERSION,
263 'folder': self.config.devsite_foldername or ''}
264 with open(os.path.join(self.env.srcdir, '_book.yaml')) as in_f:
265 with open(os.path.join(self.outdir, '_book.yaml'), 'w') as out_f:
266 out_f.write(string.Template(in_f.read()).substitute(subs))
267 self.info()
269 def dump_inventory(self):
270 # We don't want an inventory file when building for devsite
271 if not self.devsite_production_mode:
272 super(DevsiteBuilder, self).dump_inventory()
274 def get_production_url(self, url):
275 if not self.devsite_production_mode:
276 return url
278 if self.config.devsite_foldername:
279 return '/native-client/%s/%s' % (self.config.devsite_foldername, url)
281 return '/native-client/%s' % url
283 def get_target_uri(self, docname, typ=None):
284 if self.devsite_production_mode:
285 return self.get_production_url(docname)
286 else:
287 return docname + self.link_suffix
289 def handle_page(self, pagename, ctx, templatename='page.html',
290 outfilename=None, event_arg=None):
291 ctx['current_page_name'] = pagename
293 if not outfilename:
294 outfilename = os.path.join(self.outdir,
295 pagename + self.out_suffix)
297 # Emit an event to Sphinx
298 self.app.emit('html-page-context', pagename, templatename,
299 ctx, event_arg)
301 ensuredir(os.path.dirname(outfilename))
302 self._dump_context(ctx, outfilename)
304 def _dump_context(self, context, filename):
305 """ Do the actual dumping of the page to the file. context is a dict. Some
306 important fields:
307 body - document contents
308 title
309 current_page_name
310 Some special pages (genindex, etc.) may not have some of the fields, so
311 fetch them conservatively.
313 if not 'body' in context:
314 return
316 folder = ''
317 if self.devsite_production_mode and self.config.devsite_foldername:
318 folder = "/" + self.config.devsite_foldername
320 # codecs.open is the fast Python 2.x way of emulating the encoding= argument
321 # in Python 3's builtin open.
322 with codecs.open(filename, 'w', encoding='utf-8') as f:
323 f.write(PAGE_TEMPLATE.substitute(
324 doc_title=context.get('title', ''),
325 doc_body=context.get('body'),
326 folder=folder,
327 nonprod_css=self._conditional_nonprod(NONPROD_CSS),
328 nonprod_meta_head=self._conditional_nonprod(NONPROD_META_HEAD),
329 devsite_prefix=self._conditional_devsite(DEVSITE_PREFIX),
330 devsite_butterbar=self._conditional_devsite(DEVSITE_BUTTERBAR)))
332 def _conditional_devsite(self, s):
333 return s if self.devsite_production_mode else ''
335 def _conditional_nonprod(self, s):
336 return s if not self.devsite_production_mode else ''
339 class NaclCodeDirective(Directive):
340 """ Custom "naclcode" directive for code snippets. To keep it under our
341 control.
343 has_content = True
344 required_arguments = 0
345 optional_arguments = 1
346 option_spec = {
347 'prettyprint': int,
350 def run(self):
351 code = u'\n'.join(self.content)
352 literal = nodes.literal_block(code, code)
353 literal['prettyprint'] = self.options.get('prettyprint', 1)
354 return [literal]
356 def setup(app):
357 """ Extension registration hook.
359 # linkcheck issues HEAD requests to save time, but some Google properties
360 # reject them and we get spurious 405 responses. Monkey-patch sphinx to
361 # just use normal GET requests.
362 # See: https://bitbucket.org/birkenfeld/sphinx/issue/1292/
363 from sphinx.builders import linkcheck
364 import urllib2
365 linkcheck.HeadRequest = urllib2.Request
367 app.add_directive('naclcode', NaclCodeDirective)
368 app.add_builder(DevsiteBuilder)
370 # "Production mode" for local testing vs. on-server documentation.
371 app.add_config_value('devsite_production_mode', default='1', rebuild='html')
372 app.add_config_value('kill_internal_links', default='0', rebuild='html')
373 app.add_config_value('devsite_foldername', default=None, rebuild='html')