Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / common / extensions / docs / server2 / reference_resolver.py
blob7ba77941b6d45058898801928b83fada6b8d0bc7
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.
5 from copy import deepcopy
6 import logging
7 import re
9 from file_system import FileNotFoundError
12 def _ClassifySchemaNode(node_name, api):
13 """Attempt to classify |node_name| in an API, determining whether |node_name|
14 refers to a type, function, event, or property in |api|.
15 """
16 if '.' in node_name:
17 node_name, rest = node_name.split('.', 1)
18 else:
19 rest = None
20 for key, group in [('types', 'type'),
21 ('functions', 'method'),
22 ('events', 'event'),
23 ('properties', 'property')]:
24 for item in api.get(key, []):
25 if item['name'] == node_name:
26 if rest is not None:
27 ret = _ClassifySchemaNode(rest, item)
28 if ret is not None:
29 return ret
30 else:
31 return group, node_name
32 return None
35 def _MakeKey(namespace, ref):
36 key = '%s/%s' % (namespace, ref)
37 # AppEngine doesn't like keys > 500, but there will be some other stuff
38 # that goes into this key, so truncate it earlier. This shoudn't be
39 # happening anyway unless there's a bug, such as http://crbug.com/314102.
40 max_size = 256
41 if len(key) > max_size:
42 logging.error('Key was >%s characters: %s' % (max_size, key))
43 key = key[:max_size]
44 return key
47 class ReferenceResolver(object):
48 """Resolves references to $ref's by searching through the APIs to find the
49 correct node.
51 $ref's have two forms:
53 $ref:api.node - Replaces the $ref with a link to node on the API page. The
54 title is set to the name of the node.
56 $ref:[api.node The Title] - Same as the previous form but title is set to
57 "The Title".
58 """
60 # Matches after a $ref: that doesn't have []s.
61 _bare_ref = re.compile('\w+(\.\w+)*')
63 class Factory(object):
64 def __init__(self,
65 api_data_source_factory,
66 api_models,
67 object_store_creator):
68 self._api_data_source_factory = api_data_source_factory
69 self._api_models = api_models
70 self._object_store_creator = object_store_creator
72 def Create(self):
73 return ReferenceResolver(
74 self._api_data_source_factory.Create(None),
75 self._api_models,
76 self._object_store_creator.Create(ReferenceResolver))
78 def __init__(self, api_data_source, api_models, object_store):
79 self._api_data_source = api_data_source
80 self._api_models = api_models
81 self._object_store = object_store
83 def _GetRefLink(self, ref, api_list, namespace):
84 # Check nodes within each API the ref might refer to.
85 parts = ref.split('.')
86 for i, part in enumerate(parts):
87 api_name = '.'.join(parts[:i])
88 if api_name not in api_list:
89 continue
90 try:
91 api = self._api_data_source.get(api_name, disable_refs=True)
92 except FileNotFoundError:
93 continue
94 name = '.'.join(parts[i:])
95 # Attempt to find |name| in the API.
96 node_info = _ClassifySchemaNode(name, api)
97 if node_info is None:
98 # Check to see if this ref is a property. If it is, we want the ref to
99 # the underlying type the property is referencing.
100 for prop in api.get('properties', []):
101 # If the name of this property is in the ref text, replace the
102 # property with its type, and attempt to classify it.
103 if prop['name'] in name and 'link' in prop:
104 name_as_prop_type = name.replace(prop['name'], prop['link']['name'])
105 node_info = _ClassifySchemaNode(name_as_prop_type, api)
106 if node_info is not None:
107 name = name_as_prop_type
108 text = ref.replace(prop['name'], prop['link']['name'])
109 break
110 if node_info is None:
111 continue
112 else:
113 text = ref
114 category, node_name = node_info
115 if namespace is not None and text.startswith('%s.' % namespace):
116 text = text[len('%s.' % namespace):]
117 return {
118 'href': '%s.html#%s-%s' % (api_name, category, name.replace('.', '-')),
119 'text': text,
120 'name': node_name
123 # If it's not a reference to an API node it might just be a reference to an
124 # API. Check this last so that links within APIs take precedence over links
125 # to other APIs.
126 if ref in api_list:
127 return {
128 'href': '%s.html' % ref,
129 'text': ref,
130 'name': ref
133 return None
135 def GetLink(self, ref, namespace=None, title=None):
136 """Resolve $ref |ref| in namespace |namespace| if not None, returning None
137 if it cannot be resolved.
139 db_key = _MakeKey(namespace, ref)
140 link = self._object_store.Get(db_key).Get()
141 if link is None:
142 api_list = self._api_models.GetNames()
143 link = self._GetRefLink(ref, api_list, namespace)
144 if link is None and namespace is not None:
145 # Try to resolve the ref in the current namespace if there is one.
146 link = self._GetRefLink('%s.%s' % (namespace, ref), api_list, namespace)
147 if link is None:
148 return None
149 self._object_store.Set(db_key, link)
150 else:
151 link = deepcopy(link)
152 if title is not None:
153 link['text'] = title
154 return link
156 def SafeGetLink(self, ref, namespace=None, title=None):
157 """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
158 cannot be resolved, pretend like it is a link to a type.
160 ref_data = self.GetLink(ref, namespace=namespace, title=title)
161 if ref_data is not None:
162 return ref_data
163 logging.error('$ref %s could not be resolved in namespace %s.' %
164 (ref, namespace))
165 type_name = ref.rsplit('.', 1)[-1]
166 return {
167 'href': '#type-%s' % type_name,
168 'text': title or ref,
169 'name': ref
172 def ResolveAllLinks(self, text, relative_to='', namespace=None):
173 """This method will resolve all $ref links in |text| using namespace
174 |namespace| if not None. Any links that cannot be resolved will be replaced
175 using the default link format that |SafeGetLink| uses.
176 The links will be generated relative to |relative_to|.
178 if text is None or '$ref:' not in text:
179 return text
181 # requestPath should be of the form (apps|extensions)/...../page.html.
182 # link_prefix should that the target will point to
183 # (apps|extensions)/target.html. Note multiplying a string by a negative
184 # number gives the empty string.
185 link_prefix = '../' * (relative_to.count('/') - 1)
186 split_text = text.split('$ref:')
187 # |split_text| is an array of text chunks that all start with the
188 # argument to '$ref:'.
189 formatted_text = [split_text[0]]
190 for ref_and_rest in split_text[1:]:
191 title = None
192 if ref_and_rest.startswith('[') and ']' in ref_and_rest:
193 # Text was '$ref:[foo.bar maybe title] other stuff'.
194 ref_with_title, rest = ref_and_rest[1:].split(']', 1)
195 ref_with_title = ref_with_title.split(None, 1)
196 if len(ref_with_title) == 1:
197 # Text was '$ref:[foo.bar] other stuff'.
198 ref = ref_with_title[0]
199 else:
200 # Text was '$ref:[foo.bar title] other stuff'.
201 ref, title = ref_with_title
202 else:
203 # Text was '$ref:foo.bar other stuff'.
204 match = self._bare_ref.match(ref_and_rest)
205 if match is None:
206 ref = ''
207 rest = ref_and_rest
208 else:
209 ref = match.group()
210 rest = ref_and_rest[match.end():]
212 ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title)
213 formatted_text.append('<a href="%s%s">%s</a>%s' %
214 (link_prefix, ref_dict['href'], ref_dict['text'], rest))
215 return ''.join(formatted_text)