reference_resolver.py revision b2df76ea8fec9e32f6f3718986dba0d95315b29c
1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from file_system import FileNotFoundError
6import logging
7import re
8import string
9
10def _ClassifySchemaNode(node_name, api):
11  """Attempt to classify |node_name| in an API, determining whether |node_name|
12  refers to a type, function, event, or property in |api|.
13  """
14  if '.' in node_name:
15    node_name, rest = node_name.split('.', 1)
16  else:
17    rest = None
18  for key, group in [('types', 'type'),
19                     ('functions', 'method'),
20                     ('events', 'event'),
21                     ('properties', 'property')]:
22    for item in api.get(key, []):
23      if item['name'] == node_name:
24        if rest is not None:
25          ret = _ClassifySchemaNode(rest, item)
26          if ret is not None:
27            return ret
28        else:
29          return group, node_name
30  return None
31
32def _MakeKey(namespace, ref, title):
33  return '%s.%s.%s' % (namespace, ref, title)
34
35class ReferenceResolver(object):
36  """Resolves references to $ref's by searching through the APIs to find the
37  correct node.
38
39  $ref's have two forms:
40
41    $ref:api.node - Replaces the $ref with a link to node on the API page. The
42                    title is set to the name of the node.
43
44    $ref:[api.node The Title] - Same as the previous form but title is set to
45                                "The Title".
46  """
47
48  # Matches after a $ref: that doesn't have []s.
49  _bare_ref = re.compile('\w+(\.\w+)*')
50
51  class Factory(object):
52    def __init__(self,
53                 api_data_source_factory,
54                 api_list_data_source_factory,
55                 object_store_creator):
56      self._api_data_source_factory = api_data_source_factory
57      self._api_list_data_source_factory = api_list_data_source_factory
58      self._object_store_creator = object_store_creator
59
60    def Create(self):
61      return ReferenceResolver(
62          self._api_data_source_factory.Create(None, disable_refs=True),
63          self._api_list_data_source_factory.Create(),
64          self._object_store_creator.Create(ReferenceResolver))
65
66  def __init__(self, api_data_source, api_list_data_source, object_store):
67    self._api_data_source = api_data_source
68    self._api_list_data_source = api_list_data_source
69    self._object_store = object_store
70
71  def _GetRefLink(self, ref, api_list, namespace, title):
72    # Check nodes within each API the ref might refer to.
73    parts = ref.split('.')
74    for i, part in enumerate(parts):
75      api_name = '.'.join(parts[:i])
76      if api_name not in api_list:
77        continue
78      try:
79        api = self._api_data_source.get(api_name)
80      except FileNotFoundError:
81        continue
82      name = '.'.join(parts[i:])
83      # Attempt to find |name| in the API.
84      node_info = _ClassifySchemaNode(name, api)
85      if node_info is None:
86        # Check to see if this ref is a property. If it is, we want the ref to
87        # the underlying type the property is referencing.
88        for prop in api.get('properties', []):
89          # If the name of this property is in the ref text, replace the
90          # property with its type, and attempt to classify it.
91          if prop['name'] in name and 'link' in prop:
92            name_as_prop_type = name.replace(prop['name'], prop['link']['name'])
93            node_info = _ClassifySchemaNode(name_as_prop_type, api)
94            if node_info is not None:
95              name = name_as_prop_type
96              text = ref.replace(prop['name'], prop['link']['name'])
97              break
98        if node_info is None:
99          continue
100      else:
101        text = ref
102      category, node_name = node_info
103      if namespace is not None and text.startswith('%s.' % namespace):
104        text = text[len('%s.' % namespace):]
105      return {
106        'href': '%s.html#%s-%s' % (api_name, category, name.replace('.', '-')),
107        'text': title if title else text,
108        'name': node_name
109      }
110
111    # If it's not a reference to an API node it might just be a reference to an
112    # API. Check this last so that links within APIs take precedence over links
113    # to other APIs.
114    if ref in api_list:
115      return {
116        'href': '%s.html' % ref,
117        'text': title if title else ref,
118        'name': ref
119      }
120
121    return None
122
123  def GetLink(self, ref, namespace=None, title=None):
124    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
125    if it cannot be resolved.
126    """
127    link = self._object_store.Get(_MakeKey(namespace, ref, title)).Get()
128    if link is not None:
129      return link
130
131    api_list = self._api_list_data_source.GetAllNames()
132    link = self._GetRefLink(ref, api_list, namespace, title)
133
134    if link is None and namespace is not None:
135      # Try to resolve the ref in the current namespace if there is one.
136      link = self._GetRefLink('%s.%s' % (namespace, ref),
137                              api_list,
138                              namespace,
139                              title)
140
141    if link is not None:
142      self._object_store.Set(_MakeKey(namespace, ref, title), link)
143    return link
144
145  def SafeGetLink(self, ref, namespace=None, title=None):
146    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
147    cannot be resolved, pretend like it is a link to a type.
148    """
149    ref_data = self.GetLink(ref, namespace=namespace, title=title)
150    if ref_data is not None:
151      return ref_data
152    logging.error('$ref %s could not be resolved in namespace %s.' %
153        (ref, namespace))
154    type_name = ref.rsplit('.', 1)[-1]
155    return {
156      'href': '#type-%s' % type_name,
157      'text': title if title else ref,
158      'name': ref
159    }
160
161  def ResolveAllLinks(self, text, namespace=None):
162    """This method will resolve all $ref links in |text| using namespace
163    |namespace| if not None. Any links that cannot be resolved will be replaced
164    using the default link format that |SafeGetLink| uses.
165    """
166    if text is None or '$ref:' not in text:
167      return text
168    split_text = text.split('$ref:')
169    # |split_text| is an array of text chunks that all start with the
170    # argument to '$ref:'.
171    formatted_text = [split_text[0]]
172    for ref_and_rest in split_text[1:]:
173      title = None
174      if ref_and_rest.startswith('[') and ']' in ref_and_rest:
175        # Text was '$ref:[foo.bar maybe title] other stuff'.
176        ref_with_title, rest = ref_and_rest[1:].split(']', 1)
177        ref_with_title = ref_with_title.split(None, 1)
178        if len(ref_with_title) == 1:
179          # Text was '$ref:[foo.bar] other stuff'.
180          ref = ref_with_title[0]
181        else:
182          # Text was '$ref:[foo.bar title] other stuff'.
183          ref, title = ref_with_title
184      else:
185        # Text was '$ref:foo.bar other stuff'.
186        match = self._bare_ref.match(ref_and_rest)
187        if match is None:
188          ref = ''
189          rest = ref_and_rest
190        else:
191          ref = match.group()
192          rest = ref_and_rest[match.end():]
193
194      ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title)
195      formatted_text.append('<a href="%(href)s">%(text)s</a>%(rest)s' %
196          { 'href': ref_dict['href'], 'text': ref_dict['text'], 'rest': rest })
197    return ''.join(formatted_text)
198