11320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci# Copyright 2014 The Chromium Authors. All rights reserved.
21320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci# Use of this source code is governed by a BSD-style license that can be
31320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci# found in the LICENSE file.
41320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
51320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci#
61320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci# This is a Sphinx extension.
71320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci#
8
9from __future__ import print_function
10import codecs
11from collections import namedtuple, OrderedDict
12import os
13import string
14from docutils import nodes
15from docutils.parsers.rst import Directive, directives
16from sphinx.util.osutil import ensuredir
17from sphinx.builders.html import StandaloneHTMLBuilder
18from sphinx.writers.html import HTMLWriter
19from sphinx.writers.html import SmartyPantsHTMLTranslator as HTMLTranslator
20from sphinx.util.console import bold
21
22# PEPPER_VERSION = "31"
23
24# TODO(eliben): it may be interesting to use an actual Sphinx template here at
25# some point.
26PAGE_TEMPLATE = string.Template(r'''
27{{+bindTo:partials.${doc_template}}}
28
29${doc_body}
30
31{{/partials.${doc_template}}}
32'''.lstrip())
33
34
35# Path to the top-level YAML table-of-contents file for the chromesite
36BOOK_TOC_TEMPLATE = '_book_template.yaml'
37
38
39class ChromesiteHTMLTranslator(HTMLTranslator):
40  """ Custom HTML translator for chromesite output.
41
42      Hooked into the HTML builder by setting the html_translator_class
43      option in conf.py
44
45      HTMLTranslator is provided by Sphinx. We're actually using
46      SmartyPantsHTMLTranslator to use its quote and dash-formatting
47      capabilities. It's a subclass of the HTMLTranslator provided by docutils,
48      with Sphinx-specific features added. Here we provide chromesite-specific
49      behavior by overriding some of the visiting methods.
50  """
51  def __init__(self, builder, *args, **kwds):
52    # HTMLTranslator is an old-style Python class, so 'super' doesn't work: use
53    # direct parent invocation.
54    HTMLTranslator.__init__(self, builder, *args, **kwds)
55
56    self.within_toc = False
57
58  def visit_bullet_list(self, node):
59    # Use our own class attribute for <ul>. Don't care about compacted lists.
60    self.body.append(self.starttag(node, 'ul', **{'class': 'small-gap'}))
61
62  def depart_bullet_list(self, node):
63    # Override to not pop anything from context
64    self.body.append('</ul>\n')
65
66  def visit_literal(self, node):
67    # Don't insert "smart" quotes here
68    self.no_smarty += 1
69    # Sphinx emits <tt></tt> for literals (``like this``), with <span> per word
70    # to protect against wrapping, etc. We're required to emit plain <code>
71    # tags for them.
72    # Emit a simple <code> tag without enabling "protect_literal_text" mode,
73    # so Sphinx's visit_Text doesn't mess with the contents.
74    self.body.append(self.starttag(node, 'code', suffix=''))
75
76  def depart_literal(self, node):
77    self.no_smarty -= 1
78    self.body.append('</code>')
79
80  def visit_literal_block(self, node):
81    # Don't insert "smart" quotes here
82    self.no_smarty += 1
83    # We don't use Sphinx's buildin pygments integration for code highlighting,
84    # because the chromesite requires special <pre> tags for that and handles
85    # the highlighting on its own.
86    attrs = {'class': 'prettyprint'} if node.get('prettyprint', 1) else {}
87    self.body.append(self.starttag(node, 'pre', **attrs))
88
89  def depart_literal_block(self, node):
90    self.no_smarty -= 1
91    self.body.append('\n</pre>\n')
92
93  def visit_title(self, node):
94    if isinstance(node.parent, nodes.section):
95      # Steal the id from the parent. This is used in chromesite to handle the
96      # auto-generated navbar and permalinks.
97      if node.parent.hasattr('ids'):
98        node['ids'] = node.parent['ids'][:]
99
100    HTMLTranslator.visit_title(self, node)
101
102
103  def visit_section(self, node):
104    # chromesite needs <section> instead of <div class='section'>
105    self.section_level += 1
106    if self.section_level == 1:
107      self.body.append(self.starttag(node, 'section'))
108
109  def depart_section(self, node):
110    if self.section_level == 1:
111      self.body.append('</section>')
112    self.section_level -= 1
113
114  def visit_image(self, node):
115    # Paths to images in .rst sources should be absolute. This visitor does the
116    # required transformation for the path to be correct in the final HTML.
117    # if self.builder.chromesite_production_mode:
118    node['uri'] = self.builder.get_production_url(node['uri'])
119    HTMLTranslator.visit_image(self, node)
120
121  def visit_reference(self, node):
122    # In "kill_internal_links" mode, we don't emit the actual links for internal
123    # nodes.
124    if self.builder.chromesite_kill_internal_links and node.get('internal'):
125      pass
126    else:
127      HTMLTranslator.visit_reference(self, node)
128
129  def depart_reference(self, node):
130    if self.builder.chromesite_kill_internal_links and node.get('internal'):
131      pass
132    else:
133      HTMLTranslator.depart_reference(self, node)
134
135  def visit_topic(self, node):
136    if 'contents' in node['classes']:
137      # TODO(binji):
138      # Detect a TOC: we want to hide these from chromesite, but still keep
139      # them in devsite. An easy hack is to add display: none to the element
140      # here.
141      # When we remove devsite support, we can remove this hack.
142      self.within_toc = True
143      attrs = {'style': 'display: none'}
144      self.body.append(self.starttag(node, 'div', **attrs))
145    else:
146      HTMLTranslator.visit_topic(self, node)
147
148  def depart_topic(self, node):
149    if self.within_toc:
150      self.body.append('\n</div>')
151    else:
152      HTMLTranslator.visit_topic(self, node)
153
154  def write_colspecs(self):
155    # Override this method from docutils to do nothing. We don't need those
156    # pesky <col width=NN /> tags in our markup.
157    pass
158
159  def visit_admonition(self, node, name=''):
160    self.body.append(self.starttag(node, 'aside', CLASS=node.get('class', '')))
161
162  def depart_admonition(self, node=''):
163    self.body.append('\n</aside>\n')
164
165  def unknown_visit(self, node):
166    raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
167
168
169class ChromesiteBuilder(StandaloneHTMLBuilder):
170  """ Builder for the NaCl chromesite HTML output.
171
172      Loosely based on the code of Sphinx's standard SerializingHTMLBuilder.
173  """
174  name = 'chromesite'
175  out_suffix = '.html'
176  link_suffix = '.html'
177
178  # Disable the addition of "pi"-permalinks to each section header
179  add_permalinks = False
180
181  def init(self):
182    self.config.html_translator_class = \
183        'chromesite_builder.ChromesiteHTMLTranslator'
184    self.chromesite_kill_internal_links = \
185        int(self.config.chromesite_kill_internal_links) == 1
186    self.info("----> Chromesite builder")
187    self.config_hash = ''
188    self.tags_hash = ''
189    self.theme = None       # no theme necessary
190    self.templates = None   # no template bridge necessary
191    self.init_translator_class()
192    self.init_highlighter()
193
194  def finish(self):
195    super(ChromesiteBuilder, self).finish()
196    # if self.chromesite_production_mode:
197    #   # We decided to keep the manual _book.yaml for now;
198    #   # The code for auto-generating YAML TOCs from index.rst was removed in
199    #   # https://codereview.chromium.org/57923006/
200    #   self.info(bold('generating YAML table-of-contents... '))
201    #   subs = { 'version': PEPPER_VERSION }
202    #   with open(os.path.join(self.env.srcdir, '_book.yaml')) as in_f:
203    #     with open(os.path.join(self.outdir, '_book.yaml'), 'w') as out_f:
204    #       out_f.write(string.Template(in_f.read()).substitute(subs))
205    self.info()
206
207  def dump_inventory(self):
208    # We don't want an inventory file when building for chromesite
209    # if not self.chromesite_production_mode:
210    #   super(ChromesiteBuilder, self).dump_inventory()
211    pass
212
213  def get_production_url(self, url):
214    # if not self.chromesite_production_mode:
215    #   return url
216
217    return '/native-client/%s' % url
218
219  def get_target_uri(self, docname, typ=None):
220    # if self.chromesite_production_mode:
221      return self.get_production_url(docname) + self.link_suffix
222    # else:
223    #   return docname + self.link_suffix
224
225  def handle_page(self, pagename, ctx, templatename='page.html',
226                  outfilename=None, event_arg=None):
227    ctx['current_page_name'] = pagename
228
229    if not outfilename:
230      outfilename = os.path.join(self.outdir,
231                                 pagename + self.out_suffix)
232
233    # Emit an event to Sphinx
234    self.app.emit('html-page-context', pagename, templatename,
235                  ctx, event_arg)
236
237    ensuredir(os.path.dirname(outfilename))
238    self._dump_context(ctx, outfilename)
239
240  def _dump_context(self, context, filename):
241    """ Do the actual dumping of the page to the file. context is a dict. Some
242        important fields:
243          body - document contents
244          title
245          current_page_name
246        Some special pages (genindex, etc.) may not have some of the fields, so
247        fetch them conservatively.
248    """
249    if not 'body' in context:
250      return
251
252    template = context.get('meta', {}).get('template', 'standard_nacl_article')
253    title = context.get('title', '')
254    body = context.get('body', '')
255
256    # codecs.open is the fast Python 2.x way of emulating the encoding= argument
257    # in Python 3's builtin open.
258    with codecs.open(filename, 'w', encoding='utf-8') as f:
259      f.write(PAGE_TEMPLATE.substitute(
260        doc_template=template,
261        doc_title=title,
262        doc_body=body))
263
264  def _conditional_chromesite(self, s):
265    # return s if self.chromesite_production_mode else ''
266    return s
267
268  def _conditional_nonprod(self, s):
269    # return s if not self.chromesite_production_mode else ''
270    return ''
271
272
273class NaclCodeDirective(Directive):
274  """ Custom "naclcode" directive for code snippets. To keep it under our
275      control.
276  """
277  has_content = True
278  required_arguments = 0
279  optional_arguments = 1
280  option_spec = {
281      'prettyprint': int,
282  }
283
284  def run(self):
285    code = u'\n'.join(self.content)
286    literal = nodes.literal_block(code, code)
287    literal['prettyprint'] = self.options.get('prettyprint', 1)
288    return [literal]
289
290def setup(app):
291  """ Extension registration hook.
292  """
293  # linkcheck issues HEAD requests to save time, but some Google properties
294  # reject them and we get spurious 405 responses. Monkey-patch sphinx to
295  # just use normal GET requests.
296  # See: https://bitbucket.org/birkenfeld/sphinx/issue/1292/
297  from sphinx.builders import linkcheck
298  import urllib2
299  linkcheck.HeadRequest = urllib2.Request
300
301  app.add_directive('naclcode', NaclCodeDirective)
302  app.add_builder(ChromesiteBuilder)
303
304  # "Production mode" for local testing vs. on-server documentation.
305  app.add_config_value('chromesite_kill_internal_links', default='0',
306                       rebuild='html')
307