#!/usr/bin/env python # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Server for viewing the compiled C++ code from tools/json_schema_compiler. """ import cc_generator import code import cpp_type_generator import cpp_util import h_generator import idl_schema import json_schema import model import optparse import os import schema_loader import urlparse from highlighters import ( pygments_highlighter, none_highlighter, hilite_me_highlighter) from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer class CompilerHandler(BaseHTTPRequestHandler): """A HTTPRequestHandler that outputs the result of tools/json_schema_compiler. """ def do_GET(self): parsed_url = urlparse.urlparse(self.path) request_path = self._GetRequestPath(parsed_url) chromium_favicon = 'http://codereview.chromium.org/static/favicon.ico' head = code.Code() head.Append('<link rel="icon" href="%s">' % chromium_favicon) head.Append('<link rel="shortcut icon" href="%s">' % chromium_favicon) body = code.Code() try: if os.path.isdir(request_path): self._ShowPanels(parsed_url, head, body) else: self._ShowCompiledFile(parsed_url, head, body) finally: self.wfile.write('<html><head>') self.wfile.write(head.Render()) self.wfile.write('</head><body>') self.wfile.write(body.Render()) self.wfile.write('</body></html>') def _GetRequestPath(self, parsed_url, strip_nav=False): """Get the relative path from the current directory to the requested file. """ path = parsed_url.path if strip_nav: path = parsed_url.path.replace('/nav', '') return os.path.normpath(os.curdir + path) def _ShowPanels(self, parsed_url, head, body): """Show the previewer frame structure. Code panes are populated via XHR after links in the nav pane are clicked. """ (head.Append('<style>') .Append('body {') .Append(' margin: 0;') .Append('}') .Append('.pane {') .Append(' height: 100%;') .Append(' overflow-x: auto;') .Append(' overflow-y: scroll;') .Append(' display: inline-block;') .Append('}') .Append('#nav_pane {') .Append(' width: 20%;') .Append('}') .Append('#nav_pane ul {') .Append(' list-style-type: none;') .Append(' padding: 0 0 0 1em;') .Append('}') .Append('#cc_pane {') .Append(' width: 40%;') .Append('}') .Append('#h_pane {') .Append(' width: 40%;') .Append('}') .Append('</style>') ) body.Append( '<div class="pane" id="nav_pane">%s</div>' '<div class="pane" id="h_pane"></div>' '<div class="pane" id="cc_pane"></div>' % self._RenderNavPane(parsed_url.path[1:]) ) # The Javascript that interacts with the nav pane and panes to show the # compiled files as the URL or highlighting options change. body.Append('''<script type="text/javascript"> // Calls a function for each highlighter style <select> element. function forEachHighlighterStyle(callback) { var highlighterStyles = document.getElementsByClassName('highlighter_styles'); for (var i = 0; i < highlighterStyles.length; ++i) callback(highlighterStyles[i]); } // Called when anything changes, such as the highlighter or hashtag. function updateEverything() { var highlighters = document.getElementById('highlighters'); var highlighterName = highlighters.value; // Cache in localStorage for when the page loads next. localStorage.highlightersValue = highlighterName; // Show/hide the highlighter styles. var highlighterStyleName = ''; forEachHighlighterStyle(function(highlighterStyle) { if (highlighterStyle.id === highlighterName + '_styles') { highlighterStyle.removeAttribute('style') highlighterStyleName = highlighterStyle.value; } else { highlighterStyle.setAttribute('style', 'display:none') } // Cache in localStorage for when the page next loads. localStorage[highlighterStyle.id + 'Value'] = highlighterStyle.value; }); // Populate the code panes. function populateViaXHR(elementId, requestPath) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.status != 200) { alert('XHR error to ' + requestPath); return; } document.getElementById(elementId).innerHTML = xhr.responseText; }; xhr.open('GET', requestPath, true); xhr.send(); } var targetName = window.location.hash; targetName = targetName.substring('#'.length); targetName = targetName.split('.', 1)[0] if (targetName !== '') { var basePath = window.location.pathname; var query = 'highlighter=' + highlighterName + '&' + 'style=' + highlighterStyleName; populateViaXHR('h_pane', basePath + '/' + targetName + '.h?' + query); populateViaXHR('cc_pane', basePath + '/' + targetName + '.cc?' + query); } } // Initial load: set the values of highlighter and highlighterStyles from // localStorage. (function() { var cachedValue = localStorage.highlightersValue; if (cachedValue) document.getElementById('highlighters').value = cachedValue; forEachHighlighterStyle(function(highlighterStyle) { var cachedValue = localStorage[highlighterStyle.id + 'Value']; if (cachedValue) highlighterStyle.value = cachedValue; }); })(); window.addEventListener('hashchange', updateEverything, false); updateEverything(); </script>''') def _LoadModel(self, basedir, name): """Loads and returns the model for the |name| API from either its JSON or IDL file, e.g. name=contextMenus will be loaded from |basedir|/context_menus.json, name=alarms will be loaded from |basedir|/alarms.idl. """ loaders = { 'json': json_schema.Load, 'idl': idl_schema.Load } # APIs are referred to like "webRequest" but that's in a file # "web_request.json" so we need to unixify the name. unix_name = model.UnixName(name) for loader_ext, loader_fn in loaders.items(): file_path = '%s/%s.%s' % (basedir, unix_name, loader_ext) if os.path.exists(file_path): # For historical reasons these files contain a singleton list with the # model, so just return that single object. return (loader_fn(file_path)[0], file_path) raise ValueError('File for model "%s" not found' % name) def _ShowCompiledFile(self, parsed_url, head, body): """Show the compiled version of a json or idl file given the path to the compiled file. """ api_model = model.Model() request_path = self._GetRequestPath(parsed_url) (file_root, file_ext) = os.path.splitext(request_path) (filedir, filename) = os.path.split(file_root) try: # Get main file. (api_def, file_path) = self._LoadModel(filedir, filename) namespace = api_model.AddNamespace(api_def, file_path) type_generator = cpp_type_generator.CppTypeGenerator( api_model, schema_loader.SchemaLoader(filedir), namespace) # Get the model's dependencies. for dependency in api_def.get('dependencies', []): # Dependencies can contain : in which case they don't refer to APIs, # rather, permissions or manifest keys. if ':' in dependency: continue (api_def, file_path) = self._LoadModel(filedir, dependency) referenced_namespace = api_model.AddNamespace(api_def, file_path) if referenced_namespace: type_generator.AddNamespace(referenced_namespace, cpp_util.Classname(referenced_namespace.name).lower()) # Generate code cpp_namespace = 'generated_api_schemas' if file_ext == '.h': cpp_code = (h_generator.HGenerator(type_generator, cpp_namespace) .Generate(namespace).Render()) elif file_ext == '.cc': cpp_code = (cc_generator.CCGenerator(type_generator, cpp_namespace) .Generate(namespace).Render()) else: self.send_error(404, "File not found: %s" % request_path) return # Do highlighting on the generated code (highlighter_param, style_param) = self._GetHighlighterParams(parsed_url) head.Append('<style>' + self.server.highlighters[highlighter_param].GetCSS(style_param) + '</style>') body.Append(self.server.highlighters[highlighter_param] .GetCodeElement(cpp_code, style_param)) except IOError: self.send_error(404, "File not found: %s" % request_path) return except (TypeError, KeyError, AttributeError, AssertionError, NotImplementedError) as error: body.Append('<pre>') body.Append('compiler error: %s' % error) body.Append('Check server log for more details') body.Append('</pre>') raise def _GetHighlighterParams(self, parsed_url): """Get the highlighting parameters from a parsed url. """ query_dict = urlparse.parse_qs(parsed_url.query) return (query_dict.get('highlighter', ['pygments'])[0], query_dict.get('style', ['colorful'])[0]) def _RenderNavPane(self, path): """Renders an HTML nav pane. This consists of a select element to set highlight style, and a list of all files at |path| with the appropriate onclick handlers to open either subdirectories or JSON files. """ html = code.Code() # Highlighter chooser. html.Append('<select id="highlighters" onChange="updateEverything()">') for name, highlighter in self.server.highlighters.items(): html.Append('<option value="%s">%s</option>' % (name, highlighter.DisplayName())) html.Append('</select>') html.Append('<br/>') # Style for each highlighter. # The correct highlighting will be shown by Javascript. for name, highlighter in self.server.highlighters.items(): styles = sorted(highlighter.GetStyles()) if not styles: continue html.Append('<select class="highlighter_styles" id="%s_styles" ' 'onChange="updateEverything()">' % name) for style in styles: html.Append('<option>%s</option>' % style) html.Append('</select>') html.Append('<br/>') # The files, with appropriate handlers. html.Append('<ul>') # Make path point to a non-empty directory. This can happen if a URL like # http://localhost:8000 is navigated to. if path == '': path = os.curdir # Firstly, a .. link if this isn't the root. if not os.path.samefile(os.curdir, path): normpath = os.path.normpath(os.path.join(path, os.pardir)) html.Append('<li><a href="/%s">%s/</a>' % (normpath, os.pardir)) # Each file under path/ for filename in sorted(os.listdir(path)): full_path = os.path.join(path, filename) (file_root, file_ext) = os.path.splitext(full_path) if os.path.isdir(full_path) and not full_path.endswith('.xcodeproj'): html.Append('<li><a href="/%s/">%s/</a>' % (full_path, filename)) elif file_ext in ['.json', '.idl']: # cc/h panes will automatically update via the hash change event. html.Append('<li><a href="#%s">%s</a>' % (filename, filename)) html.Append('</ul>') return html.Render() class PreviewHTTPServer(HTTPServer, object): def __init__(self, server_address, handler, highlighters): super(PreviewHTTPServer, self).__init__(server_address, handler) self.highlighters = highlighters if __name__ == '__main__': parser = optparse.OptionParser( description='Runs a server to preview the json_schema_compiler output.', usage='usage: %prog [option]...') parser.add_option('-p', '--port', default='8000', help='port to run the server on') (opts, argv) = parser.parse_args() try: print('Starting previewserver on port %s' % opts.port) print('The extension documentation can be found at:') print('') print(' http://localhost:%s/chrome/common/extensions/api' % opts.port) print('') highlighters = { 'hilite': hilite_me_highlighter.HiliteMeHighlighter(), 'none': none_highlighter.NoneHighlighter() } try: highlighters['pygments'] = pygments_highlighter.PygmentsHighlighter() except ImportError as e: pass server = PreviewHTTPServer(('', int(opts.port)), CompilerHandler, highlighters) server.serve_forever() except KeyboardInterrupt: server.socket.close()