1324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# Copyright (c) 2013 The Chromium Authors. All rights reserved. 2324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# Use of this source code is governed by a BSD-style license that can be 3324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# found in the LICENSE file. 4324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 5324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# 6324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# This is a Sphinx extension. 7324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# 8324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 9324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom __future__ import print_function 10324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverimport codecs 11324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom collections import namedtuple, OrderedDict 12324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverimport os 13324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverimport string 14324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom docutils import nodes 15324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom docutils.parsers.rst import Directive, directives 16324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom sphinx.util.osutil import ensuredir 17324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom sphinx.builders.html import StandaloneHTMLBuilder 18324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom sphinx.writers.html import HTMLWriter 19324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom sphinx.writers.html import SmartyPantsHTMLTranslator as HTMLTranslator 20324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverfrom sphinx.util.console import bold 21324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 22324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverPEPPER_VERSION = "31" 23324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 24324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# TODO(eliben): it may be interesting to use an actual Sphinx template here at 25324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# some point. 26324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverPAGE_TEMPLATE = string.Template(r''' 27324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver${devsite_prefix} 28324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver<html devsite> 29324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <head> 30324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver ${nonprod_meta_head} 31324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <title>${doc_title}</title> 32324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <meta name="project_path" value="/native-client${folder}/_project.yaml" /> 33324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <meta name="book_path" value="/native-client${folder}/_book.yaml" /> 34324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <link href="/native-client/css/local_extensions.css" rel="stylesheet" type="text/css"/> 35324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver ${nonprod_css} 36324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <style type="text/css"> 37324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver .nested-def {list-style-type: none; margin-bottom: 0.3em;} 38324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver .maxheight {height: 200px;} 39324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver </style> 40324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver </head> 41324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver <body> 42324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver ${devsite_butterbar} 43324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 44324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver${doc_body} 45324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 46324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver </body> 47324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver</html> 48324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver'''.lstrip()) 49324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 50324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverDEVSITE_PREFIX = r''' 51324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver{% setvar pepperversion %}pepper''' + PEPPER_VERSION + '''{% endsetvar %} 52324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver{% include "native-client/_local_variables.html" %}''' 53324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 54324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverDEVSITE_BUTTERBAR = '{{butterbar}}' 55324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 56324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# We want the non-production-mode HTML to resemble the real one, so this points 57324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# to a copied version of the devsite CSS that we'll keep locally. It's for 58324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# testing purposes only. 59324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverNONPROD_CSS = '<link href="/_static/css/local_extensions.css"'\ 60324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 'rel="stylesheet" type="text/css"/>' 61324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverNONPROD_META_HEAD = '<meta charset="utf-8" />' 62324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 63324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver# Path to the top-level YAML table-of-contents file for the devsite 64324c4644fee44b9898524c09511bd33c3f12e2dfBen GruverBOOK_TOC_TEMPLATE = '_book_template.yaml' 65324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 66324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 67324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruverclass DevsiteHTMLTranslator(HTMLTranslator): 68324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver """ Custom HTML translator for devsite output. 69324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 70324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver Hooked into the HTML builder by setting the html_translator_class 71324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver option in conf.py 72324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 73324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver HTMLTranslator is provided by Sphinx. We're actually using 74324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver SmartyPantsHTMLTranslator to use its quote and dash-formatting 75324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver capabilities. It's a subclass of the HTMLTranslator provided by docutils, 76324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver with Sphinx-specific features added. Here we provide devsite-specific 77324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver behavior by overriding some of the visiting methods. 78324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver """ 79324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver def __init__(self, builder, *args, **kwds): 80324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver # HTMLTranslator is an old-style Python class, so 'super' doesn't work: use 81324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver # direct parent invocation. 82324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver HTMLTranslator.__init__(self, builder, *args, **kwds) 83324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 84324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver self.within_ignored_h1 = False 85324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver self.within_toc = False 86324c4644fee44b9898524c09511bd33c3f12e2dfBen Gruver 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'})) 94 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') 101 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='')) 111 112 def depart_literal(self, node): 113 self.no_smarty -= 1 114 self.body.append('</code>') 115 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)) 124 125 def depart_literal_block(self, node): 126 self.no_smarty -= 1 127 self.body.append('\n</pre>\n') 128 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) 133 134 def depart_paragraph(self, node): 135 if not self.within_toc: 136 HTMLTranslator.depart_paragraph(self, node) 137 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')) 142 143 def depart_section(self, node): 144 self.section_level -= 1 145 self.body.append('</section>') 146 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) 153 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) 161 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) 167 168 def visit_title(self, node): 169 # Why this? 170 # 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. 174 # 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. 178 # 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. 182 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) 192 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 197 198 def visit_Text(self, node): 199 if not self.within_ignored_h1: 200 HTMLTranslator.visit_Text(self, node) 201 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">') 209 210 def depart_topic(self, node): 211 if self.within_toc: 212 self.within_toc = False 213 self.body.append('</nav>\n') 214 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 219 220 def visit_admonition(self, node, name=''): 221 self.body.append(self.starttag(node, 'aside', CLASS=node.get('class', ''))) 222 223 def depart_admonition(self, node=''): 224 self.body.append('\n</aside>\n') 225 226 def unknown_visit(self, node): 227 raise NotImplementedError('Unknown node: ' + node.__class__.__name__) 228 229 230class DevsiteBuilder(StandaloneHTMLBuilder): 231 """ Builder for the NaCl devsite HTML output. 232 233 Loosely based on the code of Sphinx's standard SerializingHTMLBuilder. 234 """ 235 name = 'devsite' 236 out_suffix = '.html' 237 link_suffix = '.html' 238 239 # Disable the addition of "pi"-permalinks to each section header 240 add_permalinks = False 241 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() 253 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() 268 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() 273 274 def get_production_url(self, url): 275 if not self.devsite_production_mode: 276 return url 277 278 if self.config.devsite_foldername: 279 return '/native-client/%s/%s' % (self.config.devsite_foldername, url) 280 281 return '/native-client/%s' % url 282 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 288 289 def handle_page(self, pagename, ctx, templatename='page.html', 290 outfilename=None, event_arg=None): 291 ctx['current_page_name'] = pagename 292 293 if not outfilename: 294 outfilename = os.path.join(self.outdir, 295 pagename + self.out_suffix) 296 297 # Emit an event to Sphinx 298 self.app.emit('html-page-context', pagename, templatename, 299 ctx, event_arg) 300 301 ensuredir(os.path.dirname(outfilename)) 302 self._dump_context(ctx, outfilename) 303 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. 312 """ 313 if not 'body' in context: 314 return 315 316 folder = '' 317 if self.devsite_production_mode and self.config.devsite_foldername: 318 folder = "/" + self.config.devsite_foldername 319 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))) 331 332 def _conditional_devsite(self, s): 333 return s if self.devsite_production_mode else '' 334 335 def _conditional_nonprod(self, s): 336 return s if not self.devsite_production_mode else '' 337 338 339class NaclCodeDirective(Directive): 340 """ Custom "naclcode" directive for code snippets. To keep it under our 341 control. 342 """ 343 has_content = True 344 required_arguments = 0 345 optional_arguments = 1 346 option_spec = { 347 'prettyprint': int, 348 } 349 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] 355 356def setup(app): 357 """ Extension registration hook. 358 """ 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 366 367 app.add_directive('naclcode', NaclCodeDirective) 368 app.add_builder(DevsiteBuilder) 369 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') 374