Home | History | Annotate | Download | only in json_schema_compiler
      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 
      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
     24 
     25 
     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)
     32 
     33     chromium_favicon = 'http://codereview.chromium.org/static/favicon.ico'
     34 
     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)
     38 
     39     body = code.Code()
     40 
     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>')
     52 
     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)
     60 
     61   def _ShowPanels(self, parsed_url, head, body):
     62     """Show the previewer frame structure.
     63 
     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>')
     90     )
     91 
     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:])
     97     )
     98 
     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]);
    108 }
    109 
    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;
    114 
    115   // Cache in localStorage for when the page loads next.
    116   localStorage.highlightersValue = highlighterName;
    117 
    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')
    126     }
    127 
    128     // Cache in localStorage for when the page next loads.
    129     localStorage[highlighterStyle.id + 'Value'] = highlighterStyle.value;
    130   });
    131 
    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;
    141       }
    142       document.getElementById(elementId).innerHTML = xhr.responseText;
    143     };
    144     xhr.open('GET', requestPath, true);
    145     xhr.send();
    146   }
    147 
    148   var targetName = window.location.hash;
    149   targetName = targetName.substring('#'.length);
    150   targetName = targetName.split('.', 1)[0]
    151 
    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);
    158   }
    159 }
    160 
    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;
    167 
    168 forEachHighlighterStyle(function(highlighterStyle) {
    169   var cachedValue = localStorage[highlighterStyle.id + 'Value'];
    170   if (cachedValue)
    171     highlighterStyle.value = cachedValue;
    172 });
    173 })();
    174 
    175 window.addEventListener('hashchange', updateEverything, false);
    176 updateEverything();
    177 </script>''')
    178 
    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.
    184     """
    185     loaders = {
    186       'json': json_schema.Load,
    187       'idl': idl_schema.Load
    188     }
    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)
    199 
    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.
    203     """
    204     api_model = model.Model()
    205 
    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)
    209 
    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)
    218 
    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())
    230 
    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
    242 
    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
    260 
    261   def _GetHighlighterParams(self, parsed_url):
    262     """Get the highlighting parameters from a parsed url.
    263     """
    264     query_dict = urlparse.parse_qs(parsed_url.query)
    265     return (query_dict.get('highlighter', ['pygments'])[0],
    266         query_dict.get('style', ['colorful'])[0])
    267 
    268   def _RenderNavPane(self, path):
    269     """Renders an HTML nav pane.
    270 
    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.
    274     """
    275     html = code.Code()
    276 
    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>')
    283 
    284     html.Append('<br/>')
    285 
    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
    292 
    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>')
    298 
    299     html.Append('<br/>')
    300 
    301     # The files, with appropriate handlers.
    302     html.Append('<ul>')
    303 
    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
    308 
    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))
    313 
    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))
    324 
    325     html.Append('</ul>')
    326 
    327     return html.Render()
    328 
    329 
    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
    334 
    335 
    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')
    342 
    343   (opts, argv) = parser.parse_args()
    344 
    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('')
    351 
    352     highlighters = {
    353       'hilite': hilite_me_highlighter.HiliteMeHighlighter(),
    354       'none': none_highlighter.NoneHighlighter()
    355     }
    356     try:
    357       highlighters['pygments'] = pygments_highlighter.PygmentsHighlighter()
    358     except ImportError as e:
    359       pass
    360 
    361     server = PreviewHTTPServer(('', int(opts.port)),
    362                                CompilerHandler,
    363                                highlighters)
    364     server.serve_forever()
    365   except KeyboardInterrupt:
    366     server.socket.close()
    367