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