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