1#!/usr/bin/env python 2 3# Copyright (c) 2012 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6"""Server for viewing the compiled C++ code from tools/json_schema_compiler. 7""" 8 9from __future__ import print_function 10 11import cc_generator 12import code 13import cpp_type_generator 14import cpp_util 15import h_generator 16import idl_schema 17import json_schema 18import model 19import optparse 20import os 21import shlex 22import urlparse 23from highlighters import ( 24 pygments_highlighter, none_highlighter, hilite_me_highlighter) 25from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 26from cpp_namespace_environment import CppNamespaceEnvironment 27from namespace_resolver import NamespaceResolver 28 29 30class CompilerHandler(BaseHTTPRequestHandler): 31 """A HTTPRequestHandler that outputs the result of tools/json_schema_compiler. 32 """ 33 def do_GET(self): 34 parsed_url = urlparse.urlparse(self.path) 35 request_path = self._GetRequestPath(parsed_url) 36 37 chromium_favicon = 'http://codereview.chromium.org/static/favicon.ico' 38 39 head = code.Code() 40 head.Append('<link rel="icon" href="%s">' % chromium_favicon) 41 head.Append('<link rel="shortcut icon" href="%s">' % chromium_favicon) 42 43 body = code.Code() 44 45 try: 46 if os.path.isdir(request_path): 47 self._ShowPanels(parsed_url, head, body) 48 else: 49 self._ShowCompiledFile(parsed_url, head, body) 50 finally: 51 self.wfile.write('<html><head>') 52 self.wfile.write(head.Render()) 53 self.wfile.write('</head><body>') 54 self.wfile.write(body.Render()) 55 self.wfile.write('</body></html>') 56 57 def _GetRequestPath(self, parsed_url, strip_nav=False): 58 """Get the relative path from the current directory to the requested file. 59 """ 60 path = parsed_url.path 61 if strip_nav: 62 path = parsed_url.path.replace('/nav', '') 63 return os.path.normpath(os.curdir + path) 64 65 def _ShowPanels(self, parsed_url, head, body): 66 """Show the previewer frame structure. 67 68 Code panes are populated via XHR after links in the nav pane are clicked. 69 """ 70 (head.Append('<style>') 71 .Append('body {') 72 .Append(' margin: 0;') 73 .Append('}') 74 .Append('.pane {') 75 .Append(' height: 100%;') 76 .Append(' overflow-x: auto;') 77 .Append(' overflow-y: scroll;') 78 .Append(' display: inline-block;') 79 .Append('}') 80 .Append('#nav_pane {') 81 .Append(' width: 20%;') 82 .Append('}') 83 .Append('#nav_pane ul {') 84 .Append(' list-style-type: none;') 85 .Append(' padding: 0 0 0 1em;') 86 .Append('}') 87 .Append('#cc_pane {') 88 .Append(' width: 40%;') 89 .Append('}') 90 .Append('#h_pane {') 91 .Append(' width: 40%;') 92 .Append('}') 93 .Append('</style>') 94 ) 95 96 body.Append( 97 '<div class="pane" id="nav_pane">%s</div>' 98 '<div class="pane" id="h_pane"></div>' 99 '<div class="pane" id="cc_pane"></div>' % 100 self._RenderNavPane(parsed_url.path[1:]) 101 ) 102 103 # The Javascript that interacts with the nav pane and panes to show the 104 # compiled files as the URL or highlighting options change. 105 body.Append('''<script type="text/javascript"> 106// Calls a function for each highlighter style <select> element. 107function forEachHighlighterStyle(callback) { 108 var highlighterStyles = 109 document.getElementsByClassName('highlighter_styles'); 110 for (var i = 0; i < highlighterStyles.length; ++i) 111 callback(highlighterStyles[i]); 112} 113 114// Called when anything changes, such as the highlighter or hashtag. 115function updateEverything() { 116 var highlighters = document.getElementById('highlighters'); 117 var highlighterName = highlighters.value; 118 119 // Cache in localStorage for when the page loads next. 120 localStorage.highlightersValue = highlighterName; 121 122 // Show/hide the highlighter styles. 123 var highlighterStyleName = ''; 124 forEachHighlighterStyle(function(highlighterStyle) { 125 if (highlighterStyle.id === highlighterName + '_styles') { 126 highlighterStyle.removeAttribute('style') 127 highlighterStyleName = highlighterStyle.value; 128 } else { 129 highlighterStyle.setAttribute('style', 'display:none') 130 } 131 132 // Cache in localStorage for when the page next loads. 133 localStorage[highlighterStyle.id + 'Value'] = highlighterStyle.value; 134 }); 135 136 // Populate the code panes. 137 function populateViaXHR(elementId, requestPath) { 138 var xhr = new XMLHttpRequest(); 139 xhr.onreadystatechange = function() { 140 if (xhr.readyState != 4) 141 return; 142 if (xhr.status != 200) { 143 alert('XHR error to ' + requestPath); 144 return; 145 } 146 document.getElementById(elementId).innerHTML = xhr.responseText; 147 }; 148 xhr.open('GET', requestPath, true); 149 xhr.send(); 150 } 151 152 var targetName = window.location.hash; 153 targetName = targetName.substring('#'.length); 154 targetName = targetName.split('.', 1)[0] 155 156 if (targetName !== '') { 157 var basePath = window.location.pathname; 158 var query = 'highlighter=' + highlighterName + '&' + 159 'style=' + highlighterStyleName; 160 populateViaXHR('h_pane', basePath + '/' + targetName + '.h?' + query); 161 populateViaXHR('cc_pane', basePath + '/' + targetName + '.cc?' + query); 162 } 163} 164 165// Initial load: set the values of highlighter and highlighterStyles from 166// localStorage. 167(function() { 168var cachedValue = localStorage.highlightersValue; 169if (cachedValue) 170 document.getElementById('highlighters').value = cachedValue; 171 172forEachHighlighterStyle(function(highlighterStyle) { 173 var cachedValue = localStorage[highlighterStyle.id + 'Value']; 174 if (cachedValue) 175 highlighterStyle.value = cachedValue; 176}); 177})(); 178 179window.addEventListener('hashchange', updateEverything, false); 180updateEverything(); 181</script>''') 182 183 def _ShowCompiledFile(self, parsed_url, head, body): 184 """Show the compiled version of a json or idl file given the path to the 185 compiled file. 186 """ 187 api_model = model.Model() 188 189 request_path = self._GetRequestPath(parsed_url) 190 (file_root, file_ext) = os.path.splitext(request_path) 191 (filedir, filename) = os.path.split(file_root) 192 193 namespace_resolver = NamespaceResolver("./", 194 filedir, 195 self.server.include_rules, 196 self.server.cpp_namespace_pattern) 197 try: 198 # Get main file. 199 namespace = namespace_resolver.ResolveNamespace(filename) 200 type_generator = cpp_type_generator.CppTypeGenerator( 201 api_model, 202 namespace_resolver, 203 namespace) 204 205 # Generate code 206 if file_ext == '.h': 207 cpp_code = (h_generator.HGenerator(type_generator) 208 .Generate(namespace).Render()) 209 elif file_ext == '.cc': 210 cpp_code = (cc_generator.CCGenerator(type_generator) 211 .Generate(namespace).Render()) 212 else: 213 self.send_error(404, "File not found: %s" % request_path) 214 return 215 216 # Do highlighting on the generated code 217 (highlighter_param, style_param) = self._GetHighlighterParams(parsed_url) 218 head.Append('<style>' + 219 self.server.highlighters[highlighter_param].GetCSS(style_param) + 220 '</style>') 221 body.Append(self.server.highlighters[highlighter_param] 222 .GetCodeElement(cpp_code, style_param)) 223 except IOError: 224 self.send_error(404, "File not found: %s" % request_path) 225 return 226 except (TypeError, KeyError, AttributeError, 227 AssertionError, NotImplementedError) as error: 228 body.Append('<pre>') 229 body.Append('compiler error: %s' % error) 230 body.Append('Check server log for more details') 231 body.Append('</pre>') 232 raise 233 234 def _GetHighlighterParams(self, parsed_url): 235 """Get the highlighting parameters from a parsed url. 236 """ 237 query_dict = urlparse.parse_qs(parsed_url.query) 238 return (query_dict.get('highlighter', ['pygments'])[0], 239 query_dict.get('style', ['colorful'])[0]) 240 241 def _RenderNavPane(self, path): 242 """Renders an HTML nav pane. 243 244 This consists of a select element to set highlight style, and a list of all 245 files at |path| with the appropriate onclick handlers to open either 246 subdirectories or JSON files. 247 """ 248 html = code.Code() 249 250 # Highlighter chooser. 251 html.Append('<select id="highlighters" onChange="updateEverything()">') 252 for name, highlighter in self.server.highlighters.items(): 253 html.Append('<option value="%s">%s</option>' % 254 (name, highlighter.DisplayName())) 255 html.Append('</select>') 256 257 html.Append('<br/>') 258 259 # Style for each highlighter. 260 # The correct highlighting will be shown by Javascript. 261 for name, highlighter in self.server.highlighters.items(): 262 styles = sorted(highlighter.GetStyles()) 263 if not styles: 264 continue 265 266 html.Append('<select class="highlighter_styles" id="%s_styles" ' 267 'onChange="updateEverything()">' % name) 268 for style in styles: 269 html.Append('<option>%s</option>' % style) 270 html.Append('</select>') 271 272 html.Append('<br/>') 273 274 # The files, with appropriate handlers. 275 html.Append('<ul>') 276 277 # Make path point to a non-empty directory. This can happen if a URL like 278 # http://localhost:8000 is navigated to. 279 if path == '': 280 path = os.curdir 281 282 # Firstly, a .. link if this isn't the root. 283 if not os.path.samefile(os.curdir, path): 284 normpath = os.path.normpath(os.path.join(path, os.pardir)) 285 html.Append('<li><a href="/%s">%s/</a>' % (normpath, os.pardir)) 286 287 # Each file under path/ 288 for filename in sorted(os.listdir(path)): 289 full_path = os.path.join(path, filename) 290 _, file_ext = os.path.splitext(full_path) 291 if os.path.isdir(full_path) and not full_path.endswith('.xcodeproj'): 292 html.Append('<li><a href="/%s/">%s/</a>' % (full_path, filename)) 293 elif file_ext in ['.json', '.idl']: 294 # cc/h panes will automatically update via the hash change event. 295 html.Append('<li><a href="#%s">%s</a>' % 296 (filename, filename)) 297 298 html.Append('</ul>') 299 300 return html.Render() 301 302 303class PreviewHTTPServer(HTTPServer, object): 304 def __init__(self, 305 server_address, 306 handler, 307 highlighters, 308 include_rules, 309 cpp_namespace_pattern): 310 super(PreviewHTTPServer, self).__init__(server_address, handler) 311 self.highlighters = highlighters 312 self.include_rules = include_rules 313 self.cpp_namespace_pattern = cpp_namespace_pattern 314 315 316if __name__ == '__main__': 317 parser = optparse.OptionParser( 318 description='Runs a server to preview the json_schema_compiler output.', 319 usage='usage: %prog [option]...') 320 parser.add_option('-p', '--port', default='8000', 321 help='port to run the server on') 322 parser.add_option('-n', '--namespace', default='generated_api_schemas', 323 help='C++ namespace for generated files. e.g extensions::api.') 324 parser.add_option('-I', '--include-rules', 325 help='A list of paths to include when searching for referenced objects,' 326 ' with the namespace separated by a \':\'. Example: ' 327 '/foo/bar:Foo::Bar::%(namespace)s') 328 329 (opts, argv) = parser.parse_args() 330 331 def split_path_and_namespace(path_and_namespace): 332 if ':' not in path_and_namespace: 333 raise ValueError('Invalid include rule "%s". Rules must be of ' 334 'the form path:namespace' % path_and_namespace) 335 return path_and_namespace.split(':', 1) 336 337 include_rules = [] 338 if opts.include_rules: 339 include_rules = map(split_path_and_namespace, 340 shlex.split(opts.include_rules)) 341 342 try: 343 print('Starting previewserver on port %s' % opts.port) 344 print('The extension documentation can be found at:') 345 print('') 346 print(' http://localhost:%s/chrome/common/extensions/api' % opts.port) 347 print('') 348 349 highlighters = { 350 'hilite': hilite_me_highlighter.HiliteMeHighlighter(), 351 'none': none_highlighter.NoneHighlighter() 352 } 353 try: 354 highlighters['pygments'] = pygments_highlighter.PygmentsHighlighter() 355 except ImportError as e: 356 pass 357 358 server = PreviewHTTPServer(('', int(opts.port)), 359 CompilerHandler, 360 highlighters, 361 include_rules, 362 opts.namespace) 363 server.serve_forever() 364 except KeyboardInterrupt: 365 server.socket.close() 366