15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Copyright (c) 2012 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)from file_system import FileNotFoundError
62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import logging
72a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import re
82a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import string
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _ClassifySchemaNode(node_name, api):
112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  """Attempt to classify |node_name| in an API, determining whether |node_name|
122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  refers to a type, function, event, or property in |api|.
132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  """
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if '.' in node_name:
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    node_name, rest = node_name.split('.', 1)
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  else:
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    rest = None
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for key, group in [('types', 'type'),
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                     ('functions', 'method'),
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                     ('events', 'event'),
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                     ('properties', 'property')]:
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for item in api.get(key, []):
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if item['name'] == node_name:
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if rest is not None:
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          ret = _ClassifySchemaNode(rest, item)
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if ret is not None:
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return ret
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        else:
292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          return group, node_name
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return None
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)def _MakeKey(namespace, ref, title):
332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  return '%s.%s.%s' % (namespace, ref, title)
342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class ReferenceResolver(object):
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Resolves references to $ref's by searching through the APIs to find the
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  correct node.
382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  $ref's have two forms:
402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    $ref:api.node - Replaces the $ref with a link to node on the API page. The
422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                    title is set to the name of the node.
432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    $ref:[api.node The Title] - Same as the previous form but title is set to
452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                                "The Title".
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """
472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  # Matches after a $ref: that doesn't have []s.
492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  _bare_ref = re.compile('\w+(\.\w+)*')
502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  class Factory(object):
522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def __init__(self,
532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                 api_data_source_factory,
542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                 api_list_data_source_factory,
55b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles)                 object_store_creator):
562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      self._api_data_source_factory = api_data_source_factory
572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      self._api_list_data_source_factory = api_list_data_source_factory
58b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles)      self._object_store_creator = object_store_creator
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    def Create(self):
612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      return ReferenceResolver(
622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          self._api_data_source_factory.Create(None, disable_refs=True),
632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          self._api_list_data_source_factory.Create(),
64b2df76ea8fec9e32f6f3718986dba0d95315b29cTorne (Richard Coles)          self._object_store_creator.Create(ReferenceResolver))
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def __init__(self, api_data_source, api_list_data_source, object_store):
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self._api_data_source = api_data_source
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self._api_list_data_source = api_list_data_source
692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    self._object_store = object_store
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def _GetRefLink(self, ref, api_list, namespace, title):
722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # Check nodes within each API the ref might refer to.
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    parts = ref.split('.')
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for i, part in enumerate(parts):
752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      api_name = '.'.join(parts[:i])
762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if api_name not in api_list:
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        continue
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      try:
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        api = self._api_data_source.get(api_name)
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      except FileNotFoundError:
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        continue
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      name = '.'.join(parts[i:])
832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      # Attempt to find |name| in the API.
842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      node_info = _ClassifySchemaNode(name, api)
852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if node_info is None:
862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Check to see if this ref is a property. If it is, we want the ref to
872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # the underlying type the property is referencing.
882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        for prop in api.get('properties', []):
892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          # If the name of this property is in the ref text, replace the
902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          # property with its type, and attempt to classify it.
912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          if prop['name'] in name and 'link' in prop:
922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            name_as_prop_type = name.replace(prop['name'], prop['link']['name'])
932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            node_info = _ClassifySchemaNode(name_as_prop_type, api)
942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            if node_info is not None:
952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)              name = name_as_prop_type
962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)              text = ref.replace(prop['name'], prop['link']['name'])
972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)              break
982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if node_info is None:
992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          continue
1002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      else:
1012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        text = ref
1022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      category, node_name = node_info
1032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if namespace is not None and text.startswith('%s.' % namespace):
1042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        text = text[len('%s.' % namespace):]
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return {
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        'href': '%s.html#%s-%s' % (api_name, category, name.replace('.', '-')),
1072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        'text': title if title else text,
1082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        'name': node_name
1092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      }
1102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # If it's not a reference to an API node it might just be a reference to an
1122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # API. Check this last so that links within APIs take precedence over links
1132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # to other APIs.
1142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if ref in api_list:
1152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      return {
1162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        'href': '%s.html' % ref,
1172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        'text': title if title else ref,
1182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        'name': ref
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
1202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return None
1222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def GetLink(self, ref, namespace=None, title=None):
1242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
1252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if it cannot be resolved.
1262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """
127c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)    link = self._object_store.Get(_MakeKey(namespace, ref, title)).Get()
1282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if link is not None:
1292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      return link
1302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    api_list = self._api_list_data_source.GetAllNames()
1322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    link = self._GetRefLink(ref, api_list, namespace, title)
1332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if link is None and namespace is not None:
1352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      # Try to resolve the ref in the current namespace if there is one.
1362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      link = self._GetRefLink('%s.%s' % (namespace, ref),
1372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                              api_list,
1382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                              namespace,
1392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                              title)
1402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if link is not None:
142c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)      self._object_store.Set(_MakeKey(namespace, ref, title), link)
1432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    return link
1442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def SafeGetLink(self, ref, namespace=None, title=None):
1462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
1472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    cannot be resolved, pretend like it is a link to a type.
1482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """
1492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    ref_data = self.GetLink(ref, namespace=namespace, title=title)
1502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if ref_data is not None:
1512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      return ref_data
1522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    logging.error('$ref %s could not be resolved in namespace %s.' %
153c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)        (ref, namespace))
1542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    type_name = ref.rsplit('.', 1)[-1]
1552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    return {
1562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      'href': '#type-%s' % type_name,
1572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      'text': title if title else ref,
1582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      'name': ref
1592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
1602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  def ResolveAllLinks(self, text, namespace=None):
1622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """This method will resolve all $ref links in |text| using namespace
1632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    |namespace| if not None. Any links that cannot be resolved will be replaced
1642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    using the default link format that |SafeGetLink| uses.
1652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """
1662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if text is None or '$ref:' not in text:
1672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      return text
1682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    split_text = text.split('$ref:')
1692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # |split_text| is an array of text chunks that all start with the
1702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    # argument to '$ref:'.
1712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    formatted_text = [split_text[0]]
1722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    for ref_and_rest in split_text[1:]:
1732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      title = None
1742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      if ref_and_rest.startswith('[') and ']' in ref_and_rest:
1752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Text was '$ref:[foo.bar maybe title] other stuff'.
1762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        ref_with_title, rest = ref_and_rest[1:].split(']', 1)
1772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        ref_with_title = ref_with_title.split(None, 1)
1782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if len(ref_with_title) == 1:
1792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          # Text was '$ref:[foo.bar] other stuff'.
1802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          ref = ref_with_title[0]
1812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
1822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          # Text was '$ref:[foo.bar title] other stuff'.
1832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          ref, title = ref_with_title
1842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      else:
1852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Text was '$ref:foo.bar other stuff'.
1862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        match = self._bare_ref.match(ref_and_rest)
1872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if match is None:
1882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          ref = ''
1892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          rest = ref_and_rest
1902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
1912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          ref = match.group()
1922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          rest = ref_and_rest[match.end():]
1932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title)
1952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      formatted_text.append('<a href="%(href)s">%(text)s</a>%(rest)s' %
1962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)          { 'href': ref_dict['href'], 'text': ref_dict['text'], 'rest': rest })
1972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    return ''.join(formatted_text)
198