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