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