OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2013 The Native Client 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. |
| 4 |
| 5 # |
| 6 # This is a Sphinx extension. |
| 7 # |
| 8 |
| 9 import codecs |
| 10 import os |
| 11 import string |
| 12 from docutils import nodes |
| 13 from docutils.parsers.rst import Directive, directives |
| 14 from sphinx.util.osutil import ensuredir |
| 15 from sphinx.builders.html import StandaloneHTMLBuilder |
| 16 from sphinx.writers.html import HTMLWriter |
| 17 from sphinx.writers.html import SmartyPantsHTMLTranslator as HTMLTranslator |
| 18 |
| 19 |
| 20 # TODO(eliben): it may be interesting to use an actual Sphinx template here at |
| 21 # some point. |
| 22 PAGE_TEMPLATE = string.Template(r''' |
| 23 ${devsite_prefix} |
| 24 <html devsite> |
| 25 <head> |
| 26 <title>${doc_title}</title> |
| 27 <meta name="project_path" value="/native-client/_project.yaml" /> |
| 28 <meta name="book_path" value="/native-client/_book.yaml" /> |
| 29 <link href="/native-client/css/local_extensions.css" rel="stylesheet" type="
text/css"/> |
| 30 ${nonprod_css} |
| 31 <style type="text/css"> |
| 32 .nested-def {list-style-type: none; margin-bottom: 0.3em;} |
| 33 .maxheight {height: 200px;} |
| 34 </style> |
| 35 </head> |
| 36 <body> |
| 37 ${devsite_butterbar} |
| 38 |
| 39 ${doc_body} |
| 40 |
| 41 </body> |
| 42 </html> |
| 43 '''.lstrip()) |
| 44 |
| 45 DEVSITE_PREFIX = r''' |
| 46 {% setvar pepperversion %}pepper28{% endsetvar %} |
| 47 {% include "native-client/_local_variables.html" %} |
| 48 '''.lstrip() |
| 49 |
| 50 DEVSITE_BUTTERBAR = '{{butterbar}}' |
| 51 |
| 52 # We want the non-production-mode HTML to resemble the real one, so this points |
| 53 # to a copied version of the devsite CSS that we'll keep locally. It's for |
| 54 # testing purposes only. |
| 55 NONPROD_CSS = '<link href="/_static/css/local_extensions.css" rel="stylesheet" t
ype="text/css"/>' |
| 56 |
| 57 |
| 58 class DevsiteHTMLTranslator(HTMLTranslator): |
| 59 """ Custom HTML translator for devsite output. |
| 60 |
| 61 Hooked into the HTML builder by setting the html_translator_class |
| 62 option in conf.py |
| 63 """ |
| 64 def __init__(self, builder, *args, **kwds): |
| 65 # HTMLTranslator is an old-style Python class, so 'super' doesn't work: use |
| 66 # direct parent invocation. |
| 67 HTMLTranslator.__init__(self, builder, *args, **kwds) |
| 68 |
| 69 self.within_ignored_h1 = False |
| 70 self.within_toc = False |
| 71 |
| 72 def visit_bullet_list(self, node): |
| 73 if self.within_toc: |
| 74 # Within a TOC, devsite wants <ol> |
| 75 self.body.append(self.starttag(node, 'ol')) |
| 76 else: |
| 77 # Use our own class attribute for <ul>. Don't care about compacted lists. |
| 78 self.body.append(self.starttag(node, 'ul', **{'class': 'small-gap'})) |
| 79 |
| 80 def depart_bullet_list(self, node): |
| 81 if self.within_toc: |
| 82 self.body.append('</ol>\n') |
| 83 else: |
| 84 # Override to not pop anything from context |
| 85 self.body.append('</ul>\n') |
| 86 |
| 87 def visit_literal_block(self, node): |
| 88 # We don't use Sphinx's buildin pygments integration for code highlighting, |
| 89 # because the devsite requires special <pre> tags for that and handles the |
| 90 # highlighting on its own. |
| 91 attrs = {'class': 'prettyprint'} if node.get('prettyprint', 1) else {} |
| 92 self.body.append(self.starttag(node, 'pre', **attrs)) |
| 93 |
| 94 def depart_literal_block(self, node): |
| 95 self.body.append('\n</pre>\n') |
| 96 |
| 97 def visit_paragraph(self, node): |
| 98 # Don't generate <p>s within the table of contents |
| 99 if not self.within_toc: |
| 100 HTMLTranslator.visit_paragraph(self, node) |
| 101 |
| 102 def depart_paragraph(self, node): |
| 103 if not self.within_toc: |
| 104 HTMLTranslator.depart_paragraph(self, node) |
| 105 |
| 106 def visit_section(self, node): |
| 107 # devsite needs <section> instead of <div class='section'> |
| 108 self.section_level += 1 |
| 109 self.body.append(self.starttag(node, 'section')) |
| 110 |
| 111 def depart_section(self, node): |
| 112 self.section_level -= 1 |
| 113 self.body.append('</section>') |
| 114 |
| 115 def visit_image(self, node): |
| 116 # Paths to images in .rst sources should be absolute. This visitor does the |
| 117 # required transformation for the path to be correct in the final HTML. |
| 118 if self.builder.devsite_production_mode: |
| 119 node['uri'] = '/native-client/' + node['uri'] |
| 120 HTMLTranslator.visit_image(self, node) |
| 121 |
| 122 def visit_title(self, node): |
| 123 # Why this? |
| 124 # |
| 125 # Sphinx insists on inserting a <h1>Page Title</h1> into the page, but this |
| 126 # is not how the devsite wants it. The devsite inserts the page title on |
| 127 # its own, the the extra h1 is duplication. |
| 128 # |
| 129 # Killing the doctree title node in a custom transform doesn't work, because |
| 130 # Sphinx explicitly looks for it when writing a document. So instead we rig |
| 131 # the HTML produced. |
| 132 # |
| 133 # When a title node is visited, and this is the h1-to-be, we ignore it and |
| 134 # also set a flag that tells visit_Text not to print the actual text of the |
| 135 # header. |
| 136 |
| 137 # The h1 node is in the section whose parent is the document itself. Other |
| 138 # sections have this top-section as their parent. |
| 139 if (node.parent and node.parent.parent and |
| 140 isinstance(node.parent.parent, nodes.document)): |
| 141 # Internal flag. Also, nothing is pushed to the context. Our depart_title |
| 142 # doesn't pop anything when this flag is set. |
| 143 self.within_ignored_h1 = True |
| 144 else: |
| 145 HTMLTranslator.visit_title(self, node) |
| 146 |
| 147 def depart_title(self, node): |
| 148 if not self.within_ignored_h1: |
| 149 HTMLTranslator.depart_title(self, node) |
| 150 self.within_ignored_h1 = False |
| 151 |
| 152 def visit_Text(self, node): |
| 153 if not self.within_ignored_h1: |
| 154 HTMLTranslator.visit_Text(self, node) |
| 155 |
| 156 def visit_topic(self, node): |
| 157 if 'contents' in node['classes']: |
| 158 # Detect a TOC: this requires special treatment for devsite. |
| 159 self.within_toc = True |
| 160 # Emit <nav> manually and not through starttage because we don't want |
| 161 # additional class components to be added |
| 162 self.body.append('\n<nav class="inline-toc">') |
| 163 |
| 164 def depart_topic(self, node): |
| 165 if self.within_toc: |
| 166 self.within_toc = False |
| 167 self.body.append('</nav>\n') |
| 168 |
| 169 def write_colspecs(self): |
| 170 # Override this method from docutils to do nothing. We don't need those |
| 171 # pesky <col width=NN /> tags in our markup. |
| 172 pass |
| 173 |
| 174 def visit_admonition(self, node, name=''): |
| 175 self.body.append(self.starttag(node, 'aside', CLASS=node.get('class', ''))) |
| 176 |
| 177 def depart_admonition(self, node): |
| 178 self.body.append('\n</aside>\n') |
| 179 |
| 180 |
| 181 class DevsiteBuilder(StandaloneHTMLBuilder): |
| 182 """ Builder for the NaCl devsite HTML output. |
| 183 |
| 184 Loosely based on the code of Sphinx's standard SerializingHTMLBuilder. |
| 185 """ |
| 186 name = 'devsite' |
| 187 out_suffix = '.html' |
| 188 link_suffix = '.html' |
| 189 |
| 190 # Disable the addition of "pi"-permalinks to each section header |
| 191 add_permalinks = False |
| 192 |
| 193 def init(self): |
| 194 self.devsite_production_mode = int(self.config.devsite_production_mode) == 1 |
| 195 print "----> Devsite builder with production mode = %d" % ( |
| 196 self.devsite_production_mode,) |
| 197 self.config_hash = '' |
| 198 self.tags_hash = '' |
| 199 self.theme = None # no theme necessary |
| 200 self.templates = None # no template bridge necessary |
| 201 self.init_translator_class() |
| 202 self.init_highlighter() |
| 203 |
| 204 def get_target_uri(self, docname, typ=None): |
| 205 if self.devsite_production_mode: |
| 206 # TODO(eliben): testrst here will have to be replaced with |
| 207 # {{pepperversion}} |
| 208 return '/native-client/testrst/%s' % docname |
| 209 else: |
| 210 return docname + self.link_suffix |
| 211 |
| 212 def handle_page(self, pagename, ctx, templatename='page.html', |
| 213 outfilename=None, event_arg=None): |
| 214 ctx['current_page_name'] = pagename |
| 215 |
| 216 if not outfilename: |
| 217 outfilename = os.path.join(self.outdir, |
| 218 pagename + self.out_suffix) |
| 219 |
| 220 # Emit an event to Sphinx |
| 221 self.app.emit('html-page-context', pagename, templatename, |
| 222 ctx, event_arg) |
| 223 |
| 224 ensuredir(os.path.dirname(outfilename)) |
| 225 self._dump_context(ctx, outfilename) |
| 226 |
| 227 def _dump_context(self, context, filename): |
| 228 """ Do the actual dumping of the page to the file. context is a dict. Some |
| 229 important fields: |
| 230 body - document contents |
| 231 title |
| 232 current_page_name |
| 233 Some special pages (genindex, etc.) may not have some of the fields, so |
| 234 fetch them conservatively. |
| 235 """ |
| 236 if not 'body' in context: |
| 237 return |
| 238 # codecs.open is the fast Python 2.x way of emulating the encoding= argument |
| 239 # in Python 3's builtin open. |
| 240 with codecs.open(filename, 'w', encoding='utf-8') as f: |
| 241 f.write(PAGE_TEMPLATE.substitute( |
| 242 doc_title=context.get('title', ''), |
| 243 doc_body=context.get('body'), |
| 244 nonprod_css=self._conditional_nonprod(NONPROD_CSS), |
| 245 devsite_prefix=self._conditional_devsite(DEVSITE_PREFIX), |
| 246 devsite_butterbar=self._conditional_devsite(DEVSITE_BUTTERBAR))) |
| 247 |
| 248 def _indent_each_line(self, text, howmany=2): |
| 249 # Can't really use this because it messes up <pre> formatting |
| 250 indent = ' ' * howmany |
| 251 result = [] |
| 252 for line in text.splitlines(): |
| 253 result.append(indent + line) |
| 254 return '\n'.join(result) |
| 255 |
| 256 def _conditional_devsite(self, s): |
| 257 return s if self.devsite_production_mode else '' |
| 258 |
| 259 def _conditional_nonprod(self, s): |
| 260 return s if not self.devsite_production_mode else '' |
| 261 |
| 262 |
| 263 class NaclCodeDirective(Directive): |
| 264 """ Custom "naclcode" directive for code snippets. To keep it under our |
| 265 control. |
| 266 """ |
| 267 has_content = True |
| 268 required_arguments = 0 |
| 269 optional_arguments = 1 |
| 270 option_spec = { |
| 271 'prettyprint': int, |
| 272 } |
| 273 |
| 274 def run(self): |
| 275 code = u'\n'.join(self.content) |
| 276 literal = nodes.literal_block(code, code) |
| 277 literal['prettyprint'] = self.options.get('prettyprint', 1) |
| 278 return [literal] |
| 279 |
| 280 |
| 281 def setup(app): |
| 282 """ Extension registration hook. |
| 283 """ |
| 284 app.add_directive('naclcode', NaclCodeDirective) |
| 285 app.add_builder(DevsiteBuilder) |
| 286 |
| 287 # "Production mode" for local testing vs. on-server documentation. |
| 288 app.add_config_value('devsite_production_mode', default='1', rebuild='html') |
| 289 |
| 290 |
OLD | NEW |