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