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