#!/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()