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