summaryrefslogtreecommitdiffstats
path: root/chrome/common/extensions/docs/server2/reference_resolver.py
blob: 57dcbf6ae4e9ba90d942a1607b1142ece4f011aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from file_system import FileNotFoundError
import logging
import re
import string

def _ClassifySchemaNode(node_name, api):
  """Attempt to classify |node_name| in an API, determining whether |node_name|
  refers to a type, function, event, or property in |api|.
  """
  if '.' in node_name:
    node_name, rest = node_name.split('.', 1)
  else:
    rest = None
  for key, group in [('types', 'type'),
                     ('functions', 'method'),
                     ('events', 'event'),
                     ('properties', 'property')]:
    for item in api.get(key, []):
      if item['name'] == node_name:
        if rest is not None:
          ret = _ClassifySchemaNode(rest, item)
          if ret is not None:
            return ret
        else:
          return group, node_name
  return None

def _MakeKey(namespace, ref, title):
  return '%s.%s.%s' % (namespace, ref, title)

class ReferenceResolver(object):
  """Resolves references to $ref's by searching through the APIs to find the
  correct node.

  $ref's have two forms:

    $ref:api.node - Replaces the $ref with a link to node on the API page. The
                    title is set to the name of the node.

    $ref:[api.node The Title] - Same as the previous form but title is set to
                                "The Title".
  """

  # Matches after a $ref: that doesn't have []s.
  _bare_ref = re.compile('\w+(\.\w+)*')

  class Factory(object):
    def __init__(self,
                 api_data_source_factory,
                 api_list_data_source_factory,
                 object_store_creator):
      self._api_data_source_factory = api_data_source_factory
      self._api_list_data_source_factory = api_list_data_source_factory
      self._object_store_creator = object_store_creator

    def Create(self):
      return ReferenceResolver(
          self._api_data_source_factory.Create(None, disable_refs=True),
          self._api_list_data_source_factory.Create(),
          self._object_store_creator.Create(ReferenceResolver))

  def __init__(self, api_data_source, api_list_data_source, object_store):
    self._api_data_source = api_data_source
    self._api_list_data_source = api_list_data_source
    self._object_store = object_store

  def _GetRefLink(self, ref, api_list, namespace, title):
    # Check nodes within each API the ref might refer to.
    parts = ref.split('.')
    for i, part in enumerate(parts):
      api_name = '.'.join(parts[:i])
      if api_name not in api_list:
        continue
      try:
        api = self._api_data_source.get(api_name)
      except FileNotFoundError:
        continue
      name = '.'.join(parts[i:])
      # Attempt to find |name| in the API.
      node_info = _ClassifySchemaNode(name, api)
      if node_info is None:
        # Check to see if this ref is a property. If it is, we want the ref to
        # the underlying type the property is referencing.
        for prop in api.get('properties', []):
          # If the name of this property is in the ref text, replace the
          # property with its type, and attempt to classify it.
          if prop['name'] in name and 'link' in prop:
            name_as_prop_type = name.replace(prop['name'], prop['link']['name'])
            node_info = _ClassifySchemaNode(name_as_prop_type, api)
            if node_info is not None:
              name = name_as_prop_type
              text = ref.replace(prop['name'], prop['link']['name'])
              break
        if node_info is None:
          continue
      else:
        text = ref
      category, node_name = node_info
      if namespace is not None and text.startswith('%s.' % namespace):
        text = text[len('%s.' % namespace):]
      return {
        'href': '%s.html#%s-%s' % (api_name, category, name.replace('.', '-')),
        'text': title if title else text,
        'name': node_name
      }

    # If it's not a reference to an API node it might just be a reference to an
    # API. Check this last so that links within APIs take precedence over links
    # to other APIs.
    if ref in api_list:
      return {
        'href': '%s.html' % ref,
        'text': title if title else ref,
        'name': ref
      }

    return None

  def GetLink(self, ref, namespace=None, title=None):
    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
    if it cannot be resolved.
    """
    link = self._object_store.Get(_MakeKey(namespace, ref, title)).Get()
    if link is not None:
      return link

    api_list = self._api_list_data_source.GetAllNames()
    link = self._GetRefLink(ref, api_list, namespace, title)

    if link is None and namespace is not None:
      # Try to resolve the ref in the current namespace if there is one.
      link = self._GetRefLink('%s.%s' % (namespace, ref),
                              api_list,
                              namespace,
                              title)

    if link is not None:
      self._object_store.Set(_MakeKey(namespace, ref, title), link)
    return link

  def SafeGetLink(self, ref, namespace=None, title=None):
    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
    cannot be resolved, pretend like it is a link to a type.
    """
    ref_data = self.GetLink(ref, namespace=namespace, title=title)
    if ref_data is not None:
      return ref_data
    logging.error('$ref %s could not be resolved in namespace %s.' %
        (ref, namespace))
    type_name = ref.rsplit('.', 1)[-1]
    return {
      'href': '#type-%s' % type_name,
      'text': title if title else ref,
      'name': ref
    }

  def ResolveAllLinks(self, text, namespace=None):
    """This method will resolve all $ref links in |text| using namespace
    |namespace| if not None. Any links that cannot be resolved will be replaced
    using the default link format that |SafeGetLink| uses.
    """
    if text is None or '$ref:' not in text:
      return text
    split_text = text.split('$ref:')
    # |split_text| is an array of text chunks that all start with the
    # argument to '$ref:'.
    formatted_text = [split_text[0]]
    for ref_and_rest in split_text[1:]:
      title = None
      if ref_and_rest.startswith('[') and ']' in ref_and_rest:
        # Text was '$ref:[foo.bar maybe title] other stuff'.
        ref_with_title, rest = ref_and_rest[1:].split(']', 1)
        ref_with_title = ref_with_title.split(None, 1)
        if len(ref_with_title) == 1:
          # Text was '$ref:[foo.bar] other stuff'.
          ref = ref_with_title[0]
        else:
          # Text was '$ref:[foo.bar title] other stuff'.
          ref, title = ref_with_title
      else:
        # Text was '$ref:foo.bar other stuff'.
        match = self._bare_ref.match(ref_and_rest)
        if match is None:
          ref = ''
          rest = ref_and_rest
        else:
          ref = match.group()
          rest = ref_and_rest[match.end():]

      ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title)
      formatted_text.append('<a href="%(href)s">%(text)s</a>%(rest)s' %
          { 'href': ref_dict['href'], 'text': ref_dict['text'], 'rest': rest })
    return ''.join(formatted_text)