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 shlex
     20 import urlparse
     21 from highlighters import (
     22     pygments_highlighter, none_highlighter, hilite_me_highlighter)
     23 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
     24 from cpp_namespace_environment import CppNamespaceEnvironment
     25 from schema_loader import SchemaLoader
     26 
     27 
     28 class 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.
    105 function 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.
    113 function 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() {
    166 var cachedValue = localStorage.highlightersValue;
    167 if (cachedValue)
    168   document.getElementById('highlighters').value = cachedValue;
    169 
    170 forEachHighlighterStyle(function(highlighterStyle) {
    171   var cachedValue = localStorage[highlighterStyle.id + 'Value'];
    172   if (cachedValue)
    173     highlighterStyle.value = cachedValue;
    174 });
    175 })();
    176 
    177 window.addEventListener('hashchange', updateEverything, false);
    178 updateEverything();
    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 
    302 class 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 
    315 if __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